优化角色替换

This commit is contained in:
Azmat@qq.com
2026-08-31 12:18:03 +08:00
parent cbd4f362d9
commit 5f855075d9
4 changed files with 40 additions and 13 deletions
+5 -2
View File
@@ -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,
)
+6 -4
View File
@@ -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:
@@ -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)
+25 -7
View File
@@ -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",