测试视频复刻
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user