From d7f72f6b42a7775afdddc8f5b6f2dc9e5f52d9fe Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 27 May 2026 23:02:52 +0800 Subject: [PATCH] auto-save 2026-05-27 23:01 (~5) --- .memory/worklog.json | 55 ++++++++--------- api/main.py | 74 +++++++++++++++++++++-- web/canvas-app/src/config/models.js | 19 +++++- web/canvas-app/src/stores/models.js | 5 +- web/canvas-app/src/stores/pinia/models.js | 24 +++++++- 5 files changed, 141 insertions(+), 36 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 0bc8f42..a6362b8 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,32 +1,5 @@ { "entries": [ - { - "files_changed": 6, - "hash": "92a7f2f", - "message": "auto-save 2026-05-20 20:00 (+1, ~2)", - "ts": "2026-05-20T20:00:28+08:00", - "type": "commit" - }, - { - "files_changed": 2, - "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 2 项未提交变更 · 最近提交:auto-save 2026-05-20 20:00 (+1, ~2)", - "ts": "2026-05-20T12:05:32Z", - "type": "session-heartbeat" - }, - { - "files_changed": 2, - "hash": "2544e09", - "message": "auto-save 2026-05-20 20:05 (~2)", - "ts": "2026-05-20T20:05:54+08:00", - "type": "commit" - }, - { - "files_changed": 2, - "hash": "f0f567b", - "message": "fix: center scaled workbench vertically", - "ts": "2026-05-20T20:09:39+08:00", - "type": "commit" - }, { "files_changed": 1, "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:fix: center scaled workbench vertically", @@ -3198,6 +3171,34 @@ "message": "auto-save 2026-05-27 18:08 (~2)", "hash": "13d9057", "files_changed": 2 + }, + { + "ts": "2026-05-27T18:13:45+08:00", + "type": "commit", + "message": "auto-save 2026-05-27 18:13 (~3)", + "hash": "0c30fb9", + "files_changed": 3 + }, + { + "ts": "2026-05-27T22:14:19+08:00", + "type": "commit", + "message": "chore: exclude local artifacts from production deploy", + "hash": "c6258e4", + "files_changed": 1 + }, + { + "ts": "2026-05-27T22:17:29+08:00", + "type": "commit", + "message": "chore: exclude local artifacts from production deploy", + "hash": "bf85f00", + "files_changed": 1 + }, + { + "ts": "2026-05-27T22:20:48+08:00", + "type": "commit", + "message": "chore: harden production deploy scripts", + "hash": "b6a7e7b", + "files_changed": 2 } ] } diff --git a/api/main.py b/api/main.py index 0d84dfe..6980ff3 100644 --- a/api/main.py +++ b/api/main.py @@ -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: diff --git a/web/canvas-app/src/config/models.js b/web/canvas-app/src/config/models.js index e2fb4be..512dfe9 100644 --- a/web/canvas-app/src/config/models.js +++ b/web/canvas-app/src/config/models.js @@ -123,10 +123,27 @@ export const VIDEO_MODELS = [ { label: '12 秒', key: 12 }, { label: '15 秒', key: 15 } ], - resolutions: ['720p'], + resolutions: ['480p', '720p'], defaultResolution: '720p', defaultParams: { ratio: '720x1280', duration: 10, resolution: '720p' } }, + { + label: 'Seedance 2.0 高清', + key: 'seedance_hd', + provider: ['chatfire'], + type: 't2v+i2v', + ratios: ['720x1280', '1280x720', '1024x1024', '960x1280'], + durs: [ + { label: '5 秒', key: 5 }, + { label: '8 秒', key: 8 }, + { label: '10 秒', key: 10 }, + { label: '12 秒', key: 12 }, + { label: '15 秒', key: 15 } + ], + resolutions: ['480p', '720p', '1080p'], + defaultResolution: '1080p', + defaultParams: { ratio: '720x1280', duration: 10, resolution: '1080p' } + }, ] // Chat/LLM models | 对话模型 diff --git a/web/canvas-app/src/stores/models.js b/web/canvas-app/src/stores/models.js index 4f48db0..888ed61 100644 --- a/web/canvas-app/src/stores/models.js +++ b/web/canvas-app/src/stores/models.js @@ -149,7 +149,10 @@ export const getModelDurationOptions = (modelKey) => { * Returns options based on model's resolutions array */ export const getModelResolutionOptions = (modelKey) => { - const model = VIDEO_MODELS.find(m => m.key === modelKey) + const model = getModelConfig(modelKey) || VIDEO_MODELS.find(m => m.key === modelKey) + if (model?.resolutionOptions) { + return model.resolutionOptions + } if (!model?.resolutions) return SEEDANCE_RESOLUTION_OPTIONS return model.resolutions.map(res => { diff --git a/web/canvas-app/src/stores/pinia/models.js b/web/canvas-app/src/stores/pinia/models.js index ec57aa9..34fb7c6 100644 --- a/web/canvas-app/src/stores/pinia/models.js +++ b/web/canvas-app/src/stores/pinia/models.js @@ -153,11 +153,28 @@ const normalizeRuntimeDurationOptions = (items = []) => { .filter(Boolean) } +const normalizeRuntimeResolutionOptions = (items = []) => { + if (!Array.isArray(items)) return [] + return items + .map(item => { + const key = typeof item === 'object' ? item?.value || item?.key || item?.id : item + if (!key) return null + return { + label: typeof item === 'object' ? item.label || key : key, + key + } + }) + .filter(Boolean) +} + const normalizeRuntimeVideoModel = (item) => { const key = item?.id || item?.model if (!key) return null const sizeOptions = normalizeRuntimeSizeOptions(item.size_options) const durationOptions = normalizeRuntimeDurationOptions(item.duration_options) + const resolutionOptions = normalizeRuntimeResolutionOptions(item.resolution_options) + const resolutions = resolutionOptions.map(option => option.key) + const defaultResolution = item.default_resolution || resolutions[0] || '720p' return { label: item.label || item.model || key, key, @@ -166,12 +183,13 @@ const normalizeRuntimeVideoModel = (item) => { model: item.model, ratios: sizeOptions.map(option => option.key), durs: durationOptions, - resolutions: ['720p'], - defaultResolution: '720p', + resolutions, + resolutionOptions, + defaultResolution, defaultParams: { ratio: sizeOptions[0]?.key || '720x1280', duration: durationOptions[0]?.key || 5, - resolution: '720p' + resolution: defaultResolution }, available: item.available !== false, isRuntime: true