完善视频提炼和视频复刻完善提交测试

This commit is contained in:
Azmat@qq.com
2026-08-28 10:15:15 +08:00
parent 52c5cdae1d
commit 849b14a0f6
33 changed files with 1475 additions and 208 deletions
+44 -7
View File
@@ -728,7 +728,7 @@ class VideoDigestView(APIView):
if upload is None and not reuse_task_id:
return Response({"detail": "请先上传参考视频"}, status=status.HTTP_400_BAD_REQUEST)
require_worker_task("apps.ai.tasks.run_video_digest_task")
from .video_digest import VideoDigestError, submit_team_digest
from .video_digest import VideoDigestError, VideoDigestInProgress, submit_team_digest
team = get_current_team(request.user)
try:
@@ -739,6 +739,9 @@ class VideoDigestView(APIView):
reuse_task_id=reuse_task_id,
model_config_id=request.data.get("model_config_id") or None,
)
except VideoDigestInProgress as exc:
# 409 + 在跑的那条:前端据此直接切到进行中状态,而不是弹个错就完了
return Response({"detail": str(exc), "inflight": exc.job}, status=status.HTTP_409_CONFLICT)
except VideoDigestError as exc:
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
except Exception as exc: # noqa: BLE001 — 模型/网络失败:走统一安全文案
@@ -866,11 +869,21 @@ class VideoReplaceView(APIView):
# 商品复刻的拆解在 worker 里跑;worker 还是旧镜像的话消息会被静默丢弃 → 永久「提炼中」。
if str((request.data or {}).get("replace_mode") or "") == "product":
require_worker_task("apps.ai.tasks.run_video_replace_digest_task")
from .video_replace import serialize_video_replace_task, submit_video_replace
from .video_replace import (
VideoReplaceInProgress,
serialize_video_replace_task,
submit_video_replace,
)
team = get_current_team(request.user)
try:
task = submit_video_replace(team=team, user=request.user, params=request.data or {})
except VideoReplaceInProgress as exc:
# 409 + 在跑的那条:前端据此直接切到进行中状态,而不是弹个错就完了
return Response(
{"detail": str(exc), "inflight": serialize_video_replace_task(exc.task)},
status=status.HTTP_409_CONFLICT,
)
except ValueError as exc:
message = str(exc)
internal_kind = (
@@ -906,11 +919,16 @@ class VideoReplaceView(APIView):
qs = _free_video_list_queryset(team, include_replace=True).order_by("-created_at")
total = qs.count()
tasks = list(qs[offset : offset + page_size])
from .video_replace import get_inflight_video_replace
running = get_inflight_video_replace(team) if offset == 0 else None
return Response(
{
"results": [serialize_video_replace_task(t) for t in tasks],
"total": total,
"has_more": offset + page_size < total,
# 首页才带:前端刷新后据此恢复「进行中」,不必自己在列表里翻状态
"inflight": serialize_video_replace_task(running) if running is not None else None,
}
)
@@ -1059,6 +1077,13 @@ _FREE_REF_VIDEO_TYPES = {"video/mp4", "video/quicktime"}
_FREE_REF_AUDIO_TYPES = {"audio/mpeg", "audio/wav", "audio/x-wav", "audio/wave"}
_FREE_REF_IMAGE_MAX = 30 * 1024 * 1024
_FREE_REF_VIDEO_MAX = 50 * 1024 * 1024
# 视频复刻·商品:参考视频只喂给提炼模型(不发火山),没有 15 秒的硬限制,
# 按「提炼提示词」页那套放宽到 60 秒 / 200MB。自由创作和角色复刻仍走上面的 15 秒。
# 拿这个 purpose 传上来的长视频,若被拿去自由创作/角色复刻,下游 build_content_items
# 与 submit_video_replace 各自还有 15 秒校验兜底,越不过去。
_REPLACE_SOURCE_PURPOSE = "video_replace_product"
_REPLACE_SOURCE_VIDEO_MAX = 200 * 1024 * 1024
_REPLACE_SOURCE_DURATION_MAX = 60.5
_FREE_REF_AUDIO_MAX = 15 * 1024 * 1024
@@ -1088,6 +1113,7 @@ class FreeVideoUploadView(APIView):
team = get_current_team(request.user)
content_type = (upload.content_type or "").lower()
size = upload.size or 0
long_source = str(request.data.get("purpose") or "").strip() == _REPLACE_SOURCE_PURPOSE
if content_type in _FREE_REF_IMAGE_TYPES:
kind, asset_type, suffix = "image", Asset.Type.IMAGE, {"image/jpeg": ".jpg", "image/png": ".png", "image/webp": ".webp"}[content_type]
@@ -1095,8 +1121,12 @@ class FreeVideoUploadView(APIView):
return Response({"detail": "图片大小不能超过 30MB"}, status=status.HTTP_400_BAD_REQUEST)
elif content_type in _FREE_REF_VIDEO_TYPES:
kind, asset_type, suffix = "video", Asset.Type.VIDEO, ".mp4" if content_type == "video/mp4" else ".mov"
if size > _FREE_REF_VIDEO_MAX:
return Response({"detail": "视频大小不能超过 50MB"}, status=status.HTTP_400_BAD_REQUEST)
video_max = _REPLACE_SOURCE_VIDEO_MAX if long_source else _FREE_REF_VIDEO_MAX
if size > video_max:
return Response(
{"detail": f"视频大小不能超过 {video_max // 1024 // 1024}MB"},
status=status.HTTP_400_BAD_REQUEST,
)
elif content_type in _FREE_REF_AUDIO_TYPES:
kind, asset_type, suffix = "audio", Asset.Type.AUDIO, ".mp3" if content_type == "audio/mpeg" else ".wav"
if size > _FREE_REF_AUDIO_MAX:
@@ -1132,9 +1162,16 @@ class FreeVideoUploadView(APIView):
duration = probe_duration(str(tmp_path))
if duration is None:
return Response({"detail": "媒体文件解析失败,请更换文件"}, status=status.HTTP_400_BAD_REQUEST)
if not duration_in_ref_range(duration):
label = "视频" if kind == "video" else "音频"
return Response({"detail": f"{label}时长需在 2-15 秒之间"}, status=status.HTTP_400_BAD_REQUEST)
from .media_probe import REF_DURATION_MIN
if long_source and kind == "video":
in_range = REF_DURATION_MIN <= duration <= _REPLACE_SOURCE_DURATION_MAX
range_hint = "视频时长需在 2-60 秒之间"
else:
in_range = duration_in_ref_range(duration)
range_hint = f"{'视频' if kind == 'video' else '音频'}时长需在 2-15 秒之间"
if not in_range:
return Response({"detail": range_hint}, status=status.HTTP_400_BAD_REQUEST)
if kind == "video":
poster_bytes = extract_video_poster(str(tmp_path))