优化角色替换
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user