← Files PixVerseARCHIVED FILE
pvx/routing.py
20.8 KB · Oct 3, 2026 · 06:30 UTC
from __future__ import annotations
from typing import Any
from .capabilities import create_model_parameter, create_model_supported, load_create_capabilities
from .compatibility import PIXVERSE_CLI_BASELINE_VERSION
from .model_defaults import (
IMAGE_MODEL, IMAGE_QUALITY, IMAGE_DETAIL, VIDEO_MODEL, VIDEO_QUALITY,
FALLBACK_VIDEO_QUALITY, PROMPT_ENHANCE_SKILL,
)
SEEDANCE_ASPECTS = {"16:9", "4:3", "1:1", "3:4", "9:16", "21:9"}
SEEDANCE_25_ASPECTS = SEEDANCE_ASPECTS | {"auto"}
V6_ASPECTS = SEEDANCE_ASPECTS | {"3:2", "2:3"}
IMAGE_FAMILIES = ("gpt-image", "qwen-image", "nano-banana-2", "nano-banana-2-lite", "nano-banana-pro")
def _image_family_config(family: str, *, final: bool) -> dict[str, str]:
selected = "gpt-image" if family == "auto" else family
configs = {
"gpt-image": {
"model": IMAGE_MODEL,
"quality": IMAGE_QUALITY,
"detail_level": IMAGE_DETAIL,
"label": "GPT Image 2.5 Sunburst",
},
"qwen-image": {
"model": "qwen-image",
"quality": "1080p",
"detail_level": "",
"label": "Qwen Image",
},
"nano-banana-2": {
"model": "gemini-3.1-flash",
"quality": "1440p" if final else "1080p",
"detail_level": "",
"label": "Nano Banana 2",
},
"nano-banana-2-lite": {
"model": "gemini-3.1-flash-lite",
"quality": "1080p",
"detail_level": "",
"label": "Nano Banana 2 Lite",
},
"nano-banana-pro": {
"model": "gemini-3.0",
"quality": "1440p" if final else "1080p",
"detail_level": "",
"label": "Nano Banana Pro",
},
}
if selected not in configs:
selected = "gpt-image"
return {"family": selected, **configs[selected]}
def recommend_route(
*,
kind: str,
intent: str = "final",
mode: str = "auto",
references: int = 0,
image_family: str = "auto",
aspect_ratio: str = "16:9",
duration: float = 8,
quality: str = "",
membership_tier: str = "premium",
accept_basic_fallback: bool = False,
) -> dict[str, Any]:
membership_tier = membership_tier if membership_tier in {"basic", "premium", "unknown"} else "unknown"
if accept_basic_fallback:
image_family = "nano-banana-2-lite"
if kind == "image":
route = _recommend_image(
intent=intent,
image_family=image_family,
aspect_ratio=aspect_ratio,
quality=quality,
references=max(0, references),
membership_tier=membership_tier,
)
else:
route = _recommend_video(
intent=intent, mode=mode, references=max(0, references),
aspect_ratio=aspect_ratio, duration=duration, quality=quality,
image_family=image_family, membership_tier=membership_tier,
accept_basic_fallback=accept_basic_fallback,
)
route["basic_fallback_accepted"] = accept_basic_fallback
route["membership_choice_required"] = membership_tier == "basic" and not accept_basic_fallback
if route["membership_choice_required"]:
route["issues"].append(
"Free/Basic account: stop before generation, show the PixVerse subscription link, "
"and ask whether to upgrade or explicitly accept the lower-quality fallback."
)
if accept_basic_fallback:
route["reasons"].append(
"The user explicitly accepted the fallback. Detail, motion fidelity and consistency "
"may be more limited than the premium route; preserve the creative brief and references."
)
if route.get("model") == VIDEO_MODEL:
route["prompt_enhance_skill"] = PROMPT_ENHANCE_SKILL
return route
def render_route_markdown(route: dict[str, Any]) -> str:
lines = [
"# PixVerse Route Recommendation",
"",
f"- Valid: `{route.get('valid')}`",
f"- Intent: `{route.get('intent')}`",
f"- Membership route: `{route.get('membership_tier') or 'unknown'}`",
f"- Control layer: `{route.get('control_layer')}`",
f"- Primary model: `{route.get('model')}`",
f"- Quality: `{route.get('quality')}`",
]
if route.get("detail_level"):
lines.append(f"- GPT Image detail level: `{route.get('detail_level')}`")
lines.extend(["", "## Why", ""])
lines.extend(f"- {reason}" for reason in route.get("reasons", []))
if route.get("issues"):
lines.extend(["", "## Capability notes", ""])
lines.extend(f"- {issue}" for issue in route["issues"])
chain = route.get("chain") if isinstance(route.get("chain"), list) else []
if chain:
lines.extend(["", "## Recommended chain", ""])
for index, step in enumerate(chain, start=1):
lines.append(
f"{index}. **{step.get('role')}** — `{step.get('model')}` via `{step.get('mode')}` ({step.get('quality')})"
)
if step.get("command_template"):
lines.append(f" `{step['command_template']}`")
alternatives = route.get("alternatives") if isinstance(route.get("alternatives"), list) else []
if alternatives:
lines.extend(["", "## Useful alternative", ""])
lines.extend(f"- `{item.get('model')}` — {item.get('when')}" for item in alternatives)
if route.get("command_template"):
lines.extend(["", "## Queue command template", "", "```bash", str(route["command_template"]), "```"])
return "\n".join(lines)
def _recommend_image(
*,
intent: str,
image_family: str,
aspect_ratio: str,
quality: str,
references: int,
membership_tier: str,
) -> dict[str, Any]:
final = intent == "final"
config = _image_family_config(image_family, final=final)
family = config["family"]
model = config["model"]
selected_quality = quality or config["quality"]
detail_level = config["detail_level"]
allowed_qualities = {
"gpt-image": {"1080p", "1440p", "2160p"},
"qwen-image": {"720p", "1080p"},
"nano-banana-2": {"512p", "1080p", "1440p", "2160p"},
"nano-banana-2-lite": {"1080p"},
"nano-banana-pro": {"1080p", "1440p", "2160p"},
}
issues: list[str] = []
if load_create_capabilities().get("modes") and not create_model_supported("image", model):
issues.append(f"The installed CLI does not expose {model}; refresh setup/capabilities before spending.")
limits = create_model_parameter("image", model, "images")
if isinstance(limits.get("max_count"), int) and references > limits["max_count"]:
issues.append(f"{model} accepts at most {limits['max_count']} image references.")
if selected_quality not in allowed_qualities[family]:
supported = ", ".join(sorted(allowed_qualities[family]))
issues.append(f"{model} does not support quality {selected_quality}; choose one of {supported}.")
if family != "gpt-image":
reasons = [
f"The selected image route is {config['label']} ({model}).",
"Keep supplied references attached; fallback generation requires the user's explicit choice.",
]
alternatives = [
{
"model": IMAGE_MODEL,
"when": "Use for the default locked board/plate route with explicit high detail control.",
}
]
detail_flag = ""
else:
model = IMAGE_MODEL
selected_quality = quality or IMAGE_QUALITY
detail_level = IMAGE_DETAIL
reasons = [
"GPT Image 2.5 Sunburst at 2K/high is the default for final stills and every control plate for boards, identity, products, UI/text, and storyboard frames.",
f"{intent.capitalize()} intent maps to --detail-level {detail_level}; pixel quality and detail level are separate controls.",
]
alternatives = [
{
"model": "gemini-3.1-flash",
"when": "Use Nano Banana 2 for a current high-resolution alternate image route.",
},
{
"model": "gemini-3.0",
"when": "Use Nano Banana Pro when explicitly preferred for a premium comparison.",
},
]
detail_flag = f" --detail-level {detail_level}"
reference_flag = ""
if references == 1:
reference_flag = " --image <reference-image>"
elif references > 1:
reference_flag = " --images <reference-images...>"
if references:
reasons.append(
f"Keep the {references} supplied image reference(s) in the generation command so subject, product, character, or style control is not discarded."
)
return {
"valid": not issues,
"kind": "image",
"intent": intent,
"control_layer": "image-reference" if references else "image",
"model": model,
"quality": selected_quality,
"detail_level": detail_level,
"aspect_ratio": aspect_ratio,
"reference_count": references,
"membership_tier": membership_tier,
"reasons": reasons,
"issues": issues,
"chain": [
{
"role": "locked visual board or final still",
"mode": "create image",
"model": model,
"quality": selected_quality,
"command_template": (
f"pixverse create image --model {model} --quality {selected_quality}"
f" --aspect-ratio {aspect_ratio}{detail_flag}{reference_flag} --prompt <prompt>"
),
}
],
"alternatives": alternatives,
"command_template": (
f"pixverse create image --model {model} --quality {selected_quality}"
f" --aspect-ratio {aspect_ratio}{detail_flag}{reference_flag} --prompt <prompt>"
),
"capability_source": "PixVerse CLI image help and supported-model table",
}
def _recommend_video(
*,
intent: str,
mode: str,
references: int,
aspect_ratio: str,
duration: float,
quality: str,
image_family: str,
membership_tier: str,
accept_basic_fallback: bool,
) -> dict[str, Any]:
final = intent == "final"
requested_board = mode == "board-to-video"
selected_mode = (
"reference"
if requested_board or (mode == "auto" and references)
else "video"
if mode == "auto"
else mode
)
source_references = references
effective_references = references + 1 if requested_board else references
issues: list[str] = []
blocking_issues: list[str] = []
reasons: list[str] = []
basic_membership = accept_basic_fallback
if basic_membership and selected_mode in {"modify", "motion-control"}:
model = "v6"
blocking_issues.append(
f"Basic/Free membership is routed through v6, but v6 does not support {selected_mode}. "
"Use a v6-compatible video/reference/transition/extend route, or upgrade before using this mode."
)
reasons.append("The user explicitly selected the v6 fallback; this operation exceeds its capabilities.")
elif basic_membership and selected_mode == "transition" and effective_references >= 3:
model = "v6"
blocking_issues.append(
"Basic/Free membership is routed through v6, but a 3+ frame transition requires a different model. "
"Reduce this to two keyframes or upgrade before using multi-frame transition."
)
reasons.append("The user explicitly selected the v6 fallback; this operation exceeds its capabilities.")
elif basic_membership:
model = "v6"
reasons.append(
"Explicitly accepted fallback uses PixVerse v6 at 540p."
)
elif selected_mode in {"extend", "modify", "motion-control"}:
model = VIDEO_MODEL
reasons.append("Use Seedance 2.5 reference input for this edit, extension or motion-reference brief.")
if load_create_capabilities().get("modes") and not create_model_supported("reference", model):
blocking_issues.append("The installed CLI does not expose Seedance 2.5 reference mode; refresh setup/capabilities before spending.")
elif selected_mode == "transition" and references >= 3:
model = "v5"
reasons.append("Transitions with three or more keyframes require v5.")
else:
model = VIDEO_MODEL
reasons.append("Seedance 2.5 at 1080p is the default for drafts and final video alike.")
if load_create_capabilities().get("modes") and not create_model_supported(selected_mode, model):
blocking_issues.append(
"The installed CLI does not expose Seedance 2.5 for this mode. Refresh capabilities/setup; "
"do not silently substitute an older model."
)
supported_seedance_aspects = SEEDANCE_25_ASPECTS if model == "seedance-2.5" else SEEDANCE_ASPECTS
if aspect_ratio not in supported_seedance_aspects and model.startswith("seedance"):
if aspect_ratio in V6_ASPECTS:
model = "v6"
issues.append(
f"Seedance does not support {aspect_ratio}; route to v6 to preserve the requested aspect."
)
else:
blocking_issues.append(
f"Aspect ratio {aspect_ratio} is not in the reviewed Seedance or v6 capability set; verify live capabilities before spending."
)
if duration < 4 and model.startswith("seedance"):
model = "v6"
issues.append("Seedance starts at 4 seconds; route to v6 to preserve a shorter requested duration.")
if duration > 30 or (duration > 15 and model != "seedance-2.5"):
blocking_issues.append(
"The selected model cannot produce this duration in one generation; Seedance 2.5 supports up to 30 seconds when its reviewed capability domain is available."
)
selected_quality = quality or (
FALLBACK_VIDEO_QUALITY if basic_membership else VIDEO_QUALITY
)
known_qualities = {
"seedance-2.5": {"480p", "720p", "1080p"},
"v6": {"360p", "540p", "720p", "1080p"},
}
if model in known_qualities and selected_quality not in known_qualities[model]:
blocking_issues.append(f"{model} does not support quality {selected_quality}; choose a supported quality.")
if selected_mode == "reference" and effective_references < 1:
blocking_issues.append("Reference mode needs at least one image, video, or audio reference.")
if selected_mode == "reference":
if model == "seedance-2.5":
reference_capability = create_model_parameter("reference", model, "images")
max_references = int(reference_capability.get("max_count") or 30)
else:
max_references = 9 if model.startswith("seedance") else 7
if effective_references > max_references:
blocking_issues.append(f"{model} reference mode accepts at most {max_references} image references.")
if selected_mode == "video" and effective_references > 1:
blocking_issues.append("Image-to-video accepts one opening-frame image; use reference mode for multiple images.")
if selected_mode == "transition" and effective_references < 2:
blocking_issues.append("Transition mode needs at least two keyframe images.")
if model == VIDEO_MODEL and selected_mode in {"modify", "extend", "motion-control"}:
reference_capability = create_model_parameter("reference", model, "images")
max_references = int(reference_capability.get("max_count") or 30)
if effective_references > max_references:
blocking_issues.append(f"Seedance 2.5 accepts at most {max_references} image references for this task.")
chain: list[dict[str, str]] = []
if requested_board:
# A control plate is paid work and must never be silently inserted into
# a user's request for a video. It is available only through the
# explicit board-to-video route.
board_config = _image_family_config(image_family, final=False)
board_model = board_config["model"]
board_quality = board_config["quality"]
detail_flag = f" --detail-level {board_config['detail_level']}" if board_config["detail_level"] else ""
if source_references == 1:
reference_flag = " --image <reference-image>"
elif source_references > 1:
reference_flag = " --images <reference-images...>"
else:
reference_flag = ""
chain.append(
{
"role": "explicit user-approved visual control plate",
"mode": (
f"create image --detail-level {board_config['detail_level']}"
if board_config["detail_level"]
else "create image"
),
"model": board_model,
"quality": board_quality,
"command_template": (
f"pixverse create image --model {board_model} --quality {board_quality}"
f" --aspect-ratio {aspect_ratio}{detail_flag}{reference_flag} --prompt <board-prompt>"
),
}
)
reasons.append(
f"The user explicitly selected board-to-video, so create a {board_config['label']} control plate before motion."
)
elif selected_mode == "video" and effective_references == 0:
reasons.append(
"No image or storyboard step is added: a plain video request stays a single paid video task."
)
command = _video_command_template(
mode=selected_mode, model=model, quality=selected_quality,
duration=duration, aspect_ratio=aspect_ratio, references=effective_references,
)
if model == VIDEO_MODEL and selected_mode in {"modify", "extend", "motion-control"}:
task_type = {"modify": "edit", "extend": "extend", "motion-control": "reference"}[selected_mode]
command = f"pixverse create reference --model {model} --quality {selected_quality} --task-type {task_type}"
command += f" --duration {'auto' if task_type == 'edit' else f'{duration:g}'}"
command += f" --aspect-ratio {'auto' if task_type in {'edit', 'extend'} else aspect_ratio} --videos <source-video>"
if effective_references:
command += " --images <reference-image>" if effective_references == 1 else " --images <reference-images...>"
command += " --prompt <prompt>"
selected_mode = "reference"
chain.append(
{
"role": "final motion render" if final else "motion draft",
"mode": f"create {selected_mode}",
"model": model,
"quality": selected_quality,
"command_template": command,
}
)
issues.extend(blocking_issues)
return {
"valid": not blocking_issues,
"kind": "video",
"intent": intent,
"control_layer": selected_mode,
"model": model,
"quality": selected_quality,
"detail_level": "",
"aspect_ratio": aspect_ratio,
"duration": duration,
"reference_count": effective_references,
# CLI parameter availability does not describe the provider's native
# output. Seedance 2.5 omits the toggle and uses provider defaults.
"generated_audio_supported": None if model == "seedance-2.5" else True,
"audio_flag_supported": model != "seedance-2.5",
"audio_control": "provider_default" if model == "seedance-2.5" else "toggle",
"membership_tier": membership_tier,
"reasons": reasons,
"issues": issues,
"chain": chain,
"alternatives": [],
"command_template": command,
"capability_source": (
"PixVerse CLI normalized create capability domain"
if model == "seedance-2.5"
else "PixVerse CLI video/reference help and supported-model table"
),
}
def _video_command_template(
*,
mode: str,
model: str,
quality: str,
duration: float,
aspect_ratio: str,
references: int,
) -> str:
seconds = f"{duration:g}"
base = f"pixverse create {mode} --model {model} --quality {quality}"
if mode not in {"modify", "motion-control"}:
base += f" --duration {seconds}"
if mode in {"video", "reference"}:
base += f" --aspect-ratio {aspect_ratio}"
if mode == "reference":
base += " --images <reference-image>" if references <= 1 else " --images <reference-images...>"
elif mode == "video" and references:
base += " --image <locked-opening-frame>"
elif mode == "transition":
base += " --images <keyframe-images...>" if references >= 3 else " --images <first-frame> <last-frame>"
elif mode in {"extend", "modify"}:
base += " --video <source-video>"
if mode == "modify" and references:
base += " --images <reference-image>" if references == 1 else " --images <reference-images...>"
elif mode == "motion-control":
base += " --image <subject-image> --video <motion-reference>"
if mode != "motion-control":
base += " --prompt <prompt>"
return base
SHA-256: ea11bb4b2614678e54617bda747b48539f91ddc5583a3f1a126bd6788150851d