测试视频复刻

This commit is contained in:
Azmat@qq.com
2026-08-27 18:27:08 +08:00
parent b3bf159e08
commit 52c5cdae1d
7 changed files with 299 additions and 106 deletions
+39 -26
View File
@@ -48,6 +48,8 @@ MAX_FRAMES = 36
FRAME_WIDTH = 768
FRAME_QUALITY = 3
DIGEST_MAX_TOKENS = 12288
# 视频复刻内联拆解的同模型重试次数(Gemini 偶尔吐废稿;复刻失败要用户从头重选素材,值得多试一次)
DIGEST_MAX_ATTEMPTS = 2
_SHOT_MARK = re.compile(r"【(?:镜头\s*\d+|第\s*\d+\s*镜)】")
_FFMPEG_TIMEOUT = 60
@@ -728,12 +730,12 @@ def _store_raw_source(*, team, path: str, suffix: str) -> tuple[str, str]:
return stored.object_key, storage.public_url(object_key=stored.object_key)
def _download_source_to_temp(object_key: str, suffix: str) -> str:
def _download_source_to_temp(object_key: str, suffix: str, *, bucket: str = "") -> str:
from apps.assets.storage import TosStorage
ext = suffix if str(suffix).startswith(".") else f".{suffix or 'mp4'}"
storage = TosStorage()
body = storage.client.get_object(Bucket=storage.bucket, Key=object_key)["Body"].read()
body = storage.client.get_object(Bucket=bucket or storage.bucket, Key=object_key)["Body"].read()
tmp = tempfile.NamedTemporaryFile(suffix=ext.lower(), delete=False)
try:
tmp.write(body)
@@ -1357,7 +1359,7 @@ def digest_asset_video(*, asset, task, model_config_id=None) -> tuple[str, dict]
复刻本来就是一次收费,拆解是它的内部工序。模型调用的审计仍记在传入的复刻
task 上(AIModelAttempt),出问题查得到。
"""
from apps.ai.services import execute_routed_text_request
from apps.ai.services import _collect_extract_text, get_text_provider
primary = asset.files.filter(is_primary=True).first() or asset.files.first()
if primary is None or not primary.object_key:
@@ -1372,7 +1374,7 @@ def digest_asset_video(*, asset, task, model_config_id=None) -> tuple[str, dict]
local_path = ""
try:
local_path = _download_source_to_temp(primary.object_key, suffix)
local_path = _download_source_to_temp(primary.object_key, suffix, bucket=primary.bucket or "")
base_name = (asset.name or "参考视频").rsplit(".", 1)[0]
upload = _FileFromPath(local_path, f"{base_name}{suffix}")
video, frames, duration, extras = digest_input_from_upload(upload)
@@ -1385,6 +1387,11 @@ def digest_asset_video(*, asset, task, model_config_id=None) -> tuple[str, dict]
duration,
len(frames),
)
# ⚠️ 这里必须和「提炼提示词」页(_digest_video)构造出完全相同的 messages:
# 同一份 SKILL.md 作 system、同一段 user 引导语、同样的 temperature / max_tokens。
# 绝对不要在这里追加商品信息(product_hint 保持默认空)——两处提炼稿一旦不同,
# 用户在提炼页看到的分镜和复刻实际用的分镜就对不上,排查会乱。
# test_video_replace.test_replace_digest_prompt_matches_standalone 会守住这一点。
messages = build_digest_messages(
frames,
duration,
@@ -1392,28 +1399,34 @@ def digest_asset_video(*, asset, task, model_config_id=None) -> tuple[str, dict]
aspect_ratio=ratio_label(int(extras.get("width") or 0), int(extras.get("height") or 0)),
file_title=title_from_filename(str(extras.get("file_name") or "")),
)
routed = execute_routed_text_request(
task=task,
primary_model=model_config,
messages=messages,
streaming=True,
structured_output=False,
business_operation="video_digest",
temperature=0.4,
validate_text=lambda text: validate_digest_text(
text, duration=duration, frame_count=len(frames) or (24 if video else 0)
),
extra_body={"max_tokens": DIGEST_MAX_TOKENS},
request_summary={
"duration_seconds": round(duration, 2),
"frame_count": len(frames),
"input": "native_video" if video is not None else "frames",
"for": "video_replace",
},
allow_retry=False,
allow_fallback=False,
)
_text, _response, digest = routed.value
# 这里不能走 execute_routed_text_request:它硬性要求 task.model_config 就是本次主模型,
# 而复刻任务的 model_config 是 Seedance(视频模型),传进去必抛
# 「AITask.model_config 必须保持为用户选择或系统默认的主模型」。
# 改为直连脚本/提炼同一条流式通道,并自己做「同模型重试」——绝不 fallback 到别的
# 文本模型,它们看不了视频,换过去必然废稿。
provider = get_text_provider(model_config)
expected_frames = len(frames) or (24 if video else 0)
digest = ""
last_error: Exception | None = None
for attempt in range(1, DIGEST_MAX_ATTEMPTS + 1):
try:
text, _payload = _collect_extract_text(
provider,
model_config,
messages,
temperature=0.4,
extra_body={"max_tokens": DIGEST_MAX_TOKENS},
)
digest = validate_digest_text(text, duration=duration, frame_count=expected_frames)
break
except Exception as exc: # noqa: BLE001 — 空文/废稿/网络抖动都值得再来一次
last_error = exc
logger.warning(
"replace digest attempt %s/%s failed for task %s: %s",
attempt, DIGEST_MAX_ATTEMPTS, task.id, exc,
)
if not digest:
raise VideoDigestError(f"参考视频拆解失败:{last_error}")
meta = {
"digest_model": model_config.name,
"digest_input": "native_video" if video is not None else "frames",