From 5f855075d9e01802b7c1a36001175be068cf24b5 Mon Sep 17 00:00:00 2001 From: "Azmat@qq.com" Date: Mon, 31 Aug 2026 12:18:03 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=A7=92=E8=89=B2=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/backend/apps/ai/free_video.py | 7 +++-- core/backend/apps/ai/model_routing.py | 10 ++++--- core/backend/apps/ai/test_video_replace.py | 4 +++ core/backend/apps/ai/video_replace.py | 32 +++++++++++++++++----- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/core/backend/apps/ai/free_video.py b/core/backend/apps/ai/free_video.py index eb393f4..dc1ae72 100644 --- a/core/backend/apps/ai/free_video.py +++ b/core/backend/apps/ai/free_video.py @@ -635,11 +635,14 @@ def start_pending_free_video(task: AITask) -> AITask: return task return _fail_pending_free_video(task, message) + # 角色视频编辑向火山传 duration=-1 / ratio=adaptive;账务仍按参考视频的真实时长与比例预估。 + billing_duration = int(payload.get("billing_duration") or duration) + billing_ratio = str(payload.get("billing_aspect_ratio") or aspect_ratio) tokens, quote = quote_video_estimate( task.model_config, - aspect_ratio=aspect_ratio, + aspect_ratio=billing_ratio, resolution=resolution, - duration=duration, + duration=billing_duration, references=built["snapshots"], team=task.team, ) diff --git a/core/backend/apps/ai/model_routing.py b/core/backend/apps/ai/model_routing.py index 40f2159..2c49c8c 100644 --- a/core/backend/apps/ai/model_routing.py +++ b/core/backend/apps/ai/model_routing.py @@ -48,8 +48,9 @@ class ModelRequirements: raise ValueError("reference_mode=single 时 reference_images 必须等于 1") if self.reference_mode == "multiple" and self.reference_images < 2: raise ValueError("reference_mode=multiple 时 reference_images 必须至少为 2") - if self.duration is not None and self.duration <= 0: - raise ValueError("duration 必须大于 0") + # Seedance 的视频编辑模式用 -1 表示“跟随参考视频时长”。 + if self.duration is not None and self.duration <= 0 and self.duration != -1: + raise ValueError("duration 必须大于 0,或为 -1(跟随参考视频)") if self.char_count is not None and self.char_count < 0: raise ValueError("char_count 不能为负数") if self.speed_ratio is not None and self.speed_ratio <= 0: @@ -221,10 +222,11 @@ def match_model_requirements(model: ModelConfig, requirements: ModelRequirements ("languages", requirements.language, "语言"), ("output_formats", requirements.output_format, "输出格式"), ): - if required and required not in _set(capabilities.get(field_name)): + # 视频编辑的 adaptive 比例由火山按输入视频决定,不是模型静态能力表中的比例档。 + if required and required != "adaptive" and required not in _set(capabilities.get(field_name)): reasons.append(f"不支持{label} {required}") - if requirements.duration is not None: + if requirements.duration is not None and requirements.duration != -1: durations = capabilities.get("durations") supported = set(durations) if isinstance(durations, (list, tuple, set, frozenset)) else set() if requirements.duration not in supported: diff --git a/core/backend/apps/ai/test_video_replace.py b/core/backend/apps/ai/test_video_replace.py index 28dfef8..0965a06 100644 --- a/core/backend/apps/ai/test_video_replace.py +++ b/core/backend/apps/ai/test_video_replace.py @@ -902,8 +902,12 @@ class VideoReplaceReviewGateTests(TestCase): task = advance_video_replace(task) task.refresh_from_db() self.assertEqual(task.status, AITask.Status.SUBMITTED) + self.assertEqual(task.request_payload["aspect_ratio"], "adaptive") + self.assertEqual(task.request_payload["duration"], -1) self.assertTrue(CreditReservation.objects.filter(task=task).exists()) self.assertTrue(self.provider.create_video_task.called) + self.assertEqual(self.provider.create_video_task.call_args.kwargs.get("ratio"), "adaptive") + self.assertEqual(self.provider.create_video_task.call_args.kwargs.get("duration"), -1) content = self.provider.create_video_task.call_args.kwargs.get("content_items") or [] types = [item.get("type") for item in content] self.assertIn("video_url", types) diff --git a/core/backend/apps/ai/video_replace.py b/core/backend/apps/ai/video_replace.py index f09ef51..af050f7 100644 --- a/core/backend/apps/ai/video_replace.py +++ b/core/backend/apps/ai/video_replace.py @@ -546,6 +546,10 @@ def serialize_video_replace_task(task, *, include_deleted_assets: bool = False) "shot_index": shot_index, "shot_total": shot_total, }) + # 火山编辑接口内部用 adaptive/-1;页面仍展示用户实际上传视频的比例和时长。 + if replace_mode == "character": + data["duration"] = int(payload.get("billing_duration") or 0) + data["aspect_ratio"] = str(payload.get("billing_aspect_ratio") or data.get("aspect_ratio") or "") return data @@ -604,7 +608,11 @@ def submit_video_replace(*, team, user, params: dict): "说明": "临时上传图片只提供外观,未提供品类、用途或真实卖点。", } - duration = _output_duration(params.get("duration"), video_seconds) + # 角色替换是 Seedance 视频编辑:火山要求比例/时长跟随输入视频,不能由前端指定。 + duration = -1 if replace_mode == "character" else _output_duration(params.get("duration"), video_seconds) + billing_ratio = str(params.get("aspect_ratio") or "9:16") + if billing_ratio not in RATIOS: + billing_ratio = "9:16" extra = { "replace_mode": replace_mode, "subject_name": subject_name, @@ -612,12 +620,15 @@ def submit_video_replace(*, team, user, params: dict): "product_id": str(product_id) if product_id else "", "model_id": str(model_id) if model_id else "", "product_facts": product_facts, + # adaptive / -1 只用于火山请求;预估积分仍按参考视频实际时长和上传时检测出的比例计算。 + "billing_duration": max(4, int(round(video_seconds or 4))), + "billing_aspect_ratio": billing_ratio, } base_params = { "mode": "universal", # 固定 Seedance 2.5,不接受前端指定别的档 "model": REPLACE_MODEL, - "aspect_ratio": str(params.get("aspect_ratio") or "9:16"), + "aspect_ratio": "adaptive" if replace_mode == "character" else billing_ratio, "resolution": str(params.get("resolution") or "720p"), "duration": duration, "seed": params.get("seed", -1), @@ -1344,6 +1355,8 @@ def _create_reviewing_task(*, team, user, params: dict, digest_pending: bool = F """ model_name = str(params.get("model") or REPLACE_MODEL) aspect_ratio = str(params.get("aspect_ratio") or "9:16") + extra = params.get("extra_payload") if isinstance(params.get("extra_payload"), dict) else {} + is_character_edit = extra.get("replace_mode") == "character" resolution = str(params.get("resolution") or "720p") try: duration = int(params.get("duration") or 5) @@ -1351,12 +1364,14 @@ def _create_reviewing_task(*, team, user, params: dict, digest_pending: bool = F raise ValueError("时长参数无效") if model_name not in FREE_VIDEO_MODELS: raise ValueError("模型无效") - if aspect_ratio not in RATIOS: + if aspect_ratio not in RATIOS and not (is_character_edit and aspect_ratio == "adaptive"): raise ValueError("画面比例无效") if resolution not in RESOLUTIONS: raise ValueError("分辨率无效") low, high = replace_duration_range() - if not low <= duration <= high: + if is_character_edit and duration != -1: + raise ValueError("角色替换时长必须跟随参考视频") + if not is_character_edit and not low <= duration <= high: raise ValueError(f"视频时长需在 {low}-{high} 秒之间") model_config = ( @@ -1376,11 +1391,13 @@ def _create_reviewing_task(*, team, user, params: dict, digest_pending: bool = F raise ValueError(f"当前有 {in_flight} 个视频任务进行中(上限 {max_concurrent}),请等待完成后再提交") references = params.get("references") or [] + billing_duration = int(extra.get("billing_duration") or duration) + billing_ratio = str(extra.get("billing_aspect_ratio") or aspect_ratio) tokens, quote = quote_video_estimate( model_config, - aspect_ratio=aspect_ratio, + aspect_ratio=billing_ratio, resolution=resolution, - duration=duration, + duration=billing_duration, references=references, team=team, ) @@ -1394,7 +1411,6 @@ def _create_reviewing_task(*, team, user, params: dict, digest_pending: bool = F seed = int(params.get("seed") if params.get("seed") is not None else -1) except (TypeError, ValueError): seed = -1 - extra = params.get("extra_payload") if isinstance(params.get("extra_payload"), dict) else {} request_payload = { "feature": FEATURE, "mode": "universal", @@ -1405,6 +1421,8 @@ def _create_reviewing_task(*, team, user, params: dict, digest_pending: bool = F "aspect_ratio": aspect_ratio, "resolution": resolution, "duration": duration, + "billing_duration": billing_duration, + "billing_aspect_ratio": billing_ratio, "seed": seed, "generate_audio": True, "search_mode": "off",