auto-save 2026-05-27 23:01 (~5)

This commit is contained in:
2026-05-27 23:02:52 +08:00
parent b6a7e7b4b8
commit d7f72f6b42
5 changed files with 141 additions and 36 deletions

View File

@@ -259,6 +259,11 @@ VIDEO_SIZE_CHOICES = [
"description": "适合更接近图文卡片的竖版素材",
},
]
VIDEO_RESOLUTION_CHOICES = [
{"id": "480p", "label": "480p", "value": "480p", "description": "低清预览,生成更快"},
{"id": "720p", "label": "720p", "value": "720p", "description": "日常短视频默认清晰度"},
{"id": "1080p", "label": "1080p 高清", "value": "1080p", "description": "Seedance 2.0 标准版高清输出"},
]
SubjectModelBundle = Literal["gpt", "gemini"]
SubjectAgentMode = Literal["realistic", "cartoon", "elements", "custom"]
SUBJECT_AGENT_GPT_MODEL = gpt_model_env("SUBJECT_AGENT_GPT_MODEL", VISION_MODEL)
@@ -324,6 +329,7 @@ def env_video_model(name: str, default: str) -> str:
VIDEO_MODEL_ALIASES = {
"seedance": env_video_model("VIDEO_MODEL_SEEDANCE", "seedance-2-fast"),
"seedance_hd": env_video_model("VIDEO_MODEL_SEEDANCE_HD", "doubao-seedance-2-0-260128"),
"kling": env_video_model("VIDEO_MODEL_KLING", "kling-omni"),
"veo3": env_video_model("VIDEO_MODEL_VEO3", "veo-3.1-fast"),
"veo": env_video_model("VIDEO_MODEL_VEO3", "veo-3.1-fast"),
@@ -554,6 +560,7 @@ class GeneratedVideo(BaseModel):
url: str = ""
poster_url: str = ""
duration: float = 4.0
resolution: str = "720p"
progress: int = 0
error: str = ""
created_at: float = 0.0
@@ -4875,6 +4882,52 @@ def video_size_options() -> list[dict]:
return VIDEO_SIZE_CHOICES
def _video_resolution_choice(value: str) -> dict:
return next(
(item for item in VIDEO_RESOLUTION_CHOICES if item["value"] == value),
{"id": value, "label": value, "value": value, "description": ""},
)
def _video_resolution_values_for_model(model: str | None) -> list[str]:
concrete = (model or "").strip().lower()
if video_uses_ark():
if "seedance-2-0-fast" in concrete:
return ["480p", "720p"]
if "seedance-2-0" in concrete or "seedance-1-5-pro" in concrete or "seedance-1-0-pro" in concrete:
return ["480p", "720p", "1080p"]
return ["720p"]
def video_resolution_options(model: str | None = None) -> list[dict]:
return [_video_resolution_choice(value) for value in _video_resolution_values_for_model(model or resolve_video_model(VIDEO_MODEL))]
def default_video_resolution(model: str | None = None) -> str:
values = _video_resolution_values_for_model(model or resolve_video_model(VIDEO_MODEL))
return "1080p" if "1080p" in values and "fast" not in (model or "").lower() else (values[-1] if values else "720p")
def _normalize_video_resolution(raw: str | None, model: str | None = None) -> str:
value = (raw or default_video_resolution(model)).strip().lower().replace(" ", "")
aliases = {
"sd": "480p",
"ld": "480p",
"low": "480p",
"standard": "720p",
"std": "720p",
"hd": "1080p",
"high": "1080p",
"高清": "1080p",
}
value = aliases.get(value, value)
allowed = set(_video_resolution_values_for_model(model or resolve_video_model(VIDEO_MODEL)))
if value not in allowed:
label = model or VIDEO_MODEL
raise HTTPException(400, f"unsupported video resolution for {label}: {raw}")
return value
def _normalize_video_size(raw: str | None) -> str:
value = (raw or "720x1280").strip().lower().replace(" ", "")
aliases = {
@@ -4901,6 +4954,7 @@ def _normalize_video_size(raw: str | None) -> str:
def video_model_options() -> list[dict]:
label_map = {
"seedance": "Seedance 2.0 Fast",
"seedance_hd": "Seedance 2.0 高清",
"kling": "Kling",
"veo3": "Veo 3",
"veo": "Veo",
@@ -4908,10 +4962,11 @@ def video_model_options() -> list[dict]:
}
concrete_label_map = {
"doubao-seedance-2-0-fast-260128": "Seedance 2.0 Fast",
"doubao-seedance-2-0-260128": "Seedance 2.0 高清",
}
seen_models: set[str] = set()
options: list[dict] = []
for key in ["seedance", "kling", "veo3", "veo"]:
for key in ["seedance", "seedance_hd", "kling", "veo3", "veo"]:
if key not in VIDEO_MODEL_ALIASES:
continue
model = VIDEO_MODEL_ALIASES[key]
@@ -4925,6 +4980,8 @@ def video_model_options() -> list[dict]:
"description": f"当前视频网关可选模型;单次时长最高 {max(video_duration_options())}",
"duration_options": video_duration_options(),
"size_options": video_size_options(),
"resolution_options": video_resolution_options(model),
"default_resolution": default_video_resolution(model),
"max_duration_seconds": max(video_duration_options()),
"available": bool(video_api_key()),
})
@@ -4937,6 +4994,8 @@ def video_model_options() -> list[dict]:
"description": "默认视频模型",
"duration_options": video_duration_options(),
"size_options": video_size_options(),
"resolution_options": video_resolution_options(default_model),
"default_resolution": default_video_resolution(default_model),
"max_duration_seconds": max(video_duration_options()),
"available": bool(video_api_key()),
})
@@ -6420,6 +6479,7 @@ def health() -> dict:
"video_duration_options": video_duration_options(),
"video_max_duration_seconds": max(video_duration_options()),
"video_size_options": video_size_options(),
"video_resolution_options": video_resolution_options(),
"video_provider": video_provider_name(),
"video_base_url": video_api_base(),
"video_configured": bool(video_api_key()),
@@ -8473,6 +8533,7 @@ class GenerateStoryboardVideoReq(BaseModel):
source_ref: VideoSourceRef | None = None
model: str = ""
size: str = "720x1280"
resolution: str = ""
class QuickStoryboardPlanReq(BaseModel):
@@ -8499,6 +8560,7 @@ class BatchGenerateStoryboardReq(BaseModel):
concurrency: int = 1
model: str = ""
size: str = "720x1280"
resolution: str = ""
def _quick_field_en(en: str, zh: str) -> str:
@@ -8906,7 +8968,7 @@ def submit_video_create(
"ratio": size_to_video_ratio(str(payload.get("size", ""))),
"duration": int(float(str(payload.get(VIDEO_DURATION_FIELD, 5)))),
"watermark": False,
"resolution": "720p",
"resolution": _normalize_video_resolution(str(payload.get("resolution") or ""), str(payload.get("model") or "")),
}
return client.post(url, headers={**headers, "Content-Type": "application/json"}, json=data)
@@ -8934,6 +8996,7 @@ def render_storyboard_video(
model: str,
seconds: str,
size: str,
resolution: str = "",
source_ref: VideoSourceRef | None = None,
last_ref_path: Path | None = None,
product_ref_paths: list[Path] | None = None,
@@ -8962,7 +9025,7 @@ def render_storyboard_video(
prepared_product_imgs.append(product_img)
update_generated_video(job_id, local_id, status="in_progress", progress=5, queue_message="准备素材…")
with httpx.Client(timeout=120) as client:
payload = {"model": model, "prompt": prompt, "size": size}
payload = {"model": model, "prompt": prompt, "size": size, "resolution": resolution}
payload[VIDEO_DURATION_FIELD] = seconds
create = None
create_errors: list[str] = []
@@ -9150,6 +9213,7 @@ def _enqueue_storyboard_videos(job: Job, frame: KeyFrame, req: GenerateStoryboar
model = resolve_video_model(req.model)
seconds = video_seconds(float(req.duration or 4))
video_size = _normalize_video_size(req.size)
video_resolution = _normalize_video_resolution(req.resolution, model)
source_ref = req.source_ref
if source_ref and source_ref.kind == "source_video" and not source_ref.url:
source_ref = None
@@ -9174,11 +9238,12 @@ def _enqueue_storyboard_videos(job: Job, frame: KeyFrame, req: GenerateStoryboar
url="",
poster_url=poster,
duration=float(seconds),
resolution=video_resolution,
progress=0,
created_at=time.time(),
queue_message="排队中…",
))
task_args = (job.id, local_id, "", ref_path, variant_prompt, model, seconds, video_size, source_ref, last_ref_path, reference_ref_paths, primary_role)
task_args = (job.id, local_id, "", ref_path, variant_prompt, model, seconds, video_size, video_resolution, source_ref, last_ref_path, reference_ref_paths, primary_role)
queued_tasks.append((local_id, task_args))
update(job, generated_videos=items + job.generated_videos, message=f"视频候选已提交 · 分镜 {frame.index + 1} · {count}")
for local_id, task_args in queued_tasks:
@@ -9239,6 +9304,7 @@ def _batch_generate_worker(job_id: str, req: BatchGenerateStoryboardReq) -> None
action_image=scene.action_image,
model=req.model,
size=req.size,
resolution=req.resolution,
)
_enqueue_storyboard_videos(job, frame, video_req, None)
except Exception as e: