完成视频复刻和优化

This commit is contained in:
Azmat@qq.com
2026-08-27 11:54:47 +08:00
parent c25060c6c6
commit 51620bf25b
46 changed files with 2575 additions and 663 deletions
@@ -170,14 +170,14 @@ def _public_error(raw: str) -> str:
text = (raw or "").strip()
lower = text.lower()
if QUICK_SCRIPT_MODEL_NAME in lower:
return "极速成片需要的豆包 Seed 2.1 Pro 模型未启用,请联系管理员配置"
return "一键成片需要的豆包 Seed 2.1 Pro 模型未启用,请联系管理员配置"
if "insufficient credit" in lower or "额度不足" in text:
return "可用积分不足,极速成片已暂停"
return "可用积分不足,一键成片已暂停"
if "no active" in lower or "not configured" in lower or "没有可用" in text:
return "当前缺少可用的生成模型,请联系管理员配置"
if _looks_like_review_error(text):
return REVIEW_FAIL_MESSAGE
return "极速成片暂未完成,请稍后重试或进入专业模式查看"
return "一键成片暂未完成,请稍后重试或进入专业模式查看"
def _safe_error(exc: Exception) -> str:
@@ -301,7 +301,7 @@ def fail_quick_create(job: QuickCreateJob, message: str, *, internal_error: str
job.refresh_from_db(fields=["status", "metadata", "phase"])
if job.status in {QuickCreateJob.Status.SUCCEEDED, QuickCreateJob.Status.CANCELLED}:
return
public_message = (message or "极速成片暂未完成,请稍后重试").strip()[:500]
public_message = (message or "一键成片暂未完成,请稍后重试").strip()[:500]
metadata = dict(job.metadata or {})
if internal_error:
metadata["internal_error"] = internal_error[:2000]
@@ -365,7 +365,7 @@ def _consume_script_agent(job: QuickCreateJob) -> None:
settings = _quick_settings(project)
user = job.created_by or project.created_by
if user is None:
raise ValueError("极速成片任务缺少创建人")
raise ValueError("一键成片任务缺少创建人")
model_config = get_quick_script_model()
if model_config is None:
raise ValueError(f"{QUICK_SCRIPT_MODEL_NAME} is not configured")
@@ -376,7 +376,8 @@ def _consume_script_agent(job: QuickCreateJob) -> None:
getattr(product, "category", "") or "",
getattr(product, "title", "") or "",
)
fmt = wizard.get("presentation_format") or rec["format"]
# 极速成片与专业创作一致,脚本表现形式统一为口播。
fmt = "oral"
structure = wizard.get("video_structure") or rec["structure"]
persona = wizard.get("persona") or rec["persona"]
format_label = PRESENTATION_FORMATS.get(fmt, fmt)
@@ -486,7 +487,7 @@ def _advance_product(job: QuickCreateJob) -> int:
ProductSellingPoint.objects.create(
product=product,
title=product.title,
detail="极速成片根据商品名称自动填写",
detail="一键成片根据商品名称自动填写",
sort_order=0,
)
_save_job(
@@ -695,7 +696,7 @@ def _start_base_assets(job: QuickCreateJob) -> None:
project = job.project
user = job.created_by or project.created_by
if user is None:
raise ValueError("极速成片任务缺少创建人")
raise ValueError("一键成片任务缺少创建人")
with transaction.atomic():
job = QuickCreateJob.objects.select_for_update().select_related("project").get(id=job.id)
metadata = dict(job.metadata or {})
@@ -939,7 +940,7 @@ def _start_videos(job: QuickCreateJob) -> None:
submit_video_segment(
video_segment=segment,
user=job.created_by or job.project.created_by,
prompt="极速成片自动生成,严格遵循本镜故事板与脚本。",
prompt="一键成片自动生成,严格遵循本镜故事板与脚本。",
model_config_id=settings["video_model_config_id"] or None,
aspect_ratio=settings["aspect_ratio"],
resolution=settings["resolution"],
+26 -26
View File
@@ -307,14 +307,14 @@ class QuickCreateApiTests(TestCase):
def test_retry_api_resumes_failed_job(self):
product = Product.objects.create(team=self.team, created_by=self.user, title="续跑商品")
project = Project.objects.create(team=self.team, created_by=self.user, product=product, name="续跑商品 · 极速成片")
project = Project.objects.create(team=self.team, created_by=self.user, product=product, name="续跑商品 · 一键成片")
job = QuickCreateJob.objects.create(
team=self.team,
created_by=self.user,
project=project,
status=QuickCreateJob.Status.FAILED,
phase=QuickCreateJob.Phase.PRODUCTION,
message="极速成片暂未完成,请稍后重试或进入专业模式查看",
message="一键成片暂未完成,请稍后重试或进入专业模式查看",
)
with patch("apps.projects.tasks.advance_quick_create_task.apply_async") as enqueue:
response = self.client.post(f"/api/projects/quick-create-retry/{job.id}/")
@@ -325,7 +325,7 @@ class QuickCreateApiTests(TestCase):
def test_history_lists_completed_jobs_only(self):
product = Product.objects.create(team=self.team, created_by=self.user, title="历史商品")
project = Project.objects.create(team=self.team, created_by=self.user, product=product, name="历史商品 · 极速成片")
project = Project.objects.create(team=self.team, created_by=self.user, product=product, name="历史商品 · 一键成片")
QuickCreateJob.objects.create(
team=self.team,
created_by=self.user,
@@ -334,17 +334,17 @@ class QuickCreateApiTests(TestCase):
phase=QuickCreateJob.Phase.COMPLETE,
)
failed_product = Product.objects.create(team=self.team, created_by=self.user, title="失败商品")
failed_project = Project.objects.create(team=self.team, created_by=self.user, product=failed_product, name="失败商品 · 极速成片")
failed_project = Project.objects.create(team=self.team, created_by=self.user, product=failed_product, name="失败商品 · 一键成片")
QuickCreateJob.objects.create(
team=self.team,
created_by=self.user,
project=failed_project,
status=QuickCreateJob.Status.FAILED,
phase=QuickCreateJob.Phase.PRODUCTION,
message="极速成片暂未完成,请稍后重试或进入专业模式查看",
message="一键成片暂未完成,请稍后重试或进入专业模式查看",
)
running_product = Product.objects.create(team=self.team, created_by=self.user, title="进行中商品")
running_project = Project.objects.create(team=self.team, created_by=self.user, product=running_product, name="进行中商品 · 极速成片")
running_project = Project.objects.create(team=self.team, created_by=self.user, product=running_product, name="进行中商品 · 一键成片")
QuickCreateJob.objects.create(
team=self.team,
created_by=self.user,
@@ -369,13 +369,13 @@ class QuickCreateApiTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["count"], 1)
titles = [item["title"] for item in response.data["results"]]
self.assertIn("历史商品 · 极速成片", titles)
self.assertNotIn("失败商品 · 极速成片", titles)
self.assertNotIn("进行中商品 · 极速成片", titles)
self.assertIn("历史商品 · 一键成片", titles)
self.assertNotIn("失败商品 · 一键成片", titles)
self.assertNotIn("进行中商品 · 一键成片", titles)
def test_history_hides_deleted_projects(self):
product = Product.objects.create(team=self.team, created_by=self.user, title="还在的商品")
alive = Project.objects.create(team=self.team, created_by=self.user, product=product, name="还在的商品 · 极速成片")
alive = Project.objects.create(team=self.team, created_by=self.user, product=product, name="还在的商品 · 一键成片")
QuickCreateJob.objects.create(
team=self.team,
created_by=self.user,
@@ -388,7 +388,7 @@ class QuickCreateApiTests(TestCase):
team=self.team,
created_by=self.user,
product=deleted_product,
name="已删商品 · 极速成片",
name="已删商品 · 一键成片",
is_deleted=True,
)
QuickCreateJob.objects.create(
@@ -403,8 +403,8 @@ class QuickCreateApiTests(TestCase):
self.assertEqual(response.status_code, 200)
titles = [item["title"] for item in response.data["results"]]
self.assertEqual(response.data["count"], 1)
self.assertIn("还在的商品 · 极速成片", titles)
self.assertNotIn("已删商品 · 极速成片", titles)
self.assertIn("还在的商品 · 一键成片", titles)
self.assertNotIn("已删商品 · 一键成片", titles)
def test_list_serializer_flags_quick_create_projects(self):
product = Product.objects.create(team=self.team, created_by=self.user, title="列表商品")
@@ -412,7 +412,7 @@ class QuickCreateApiTests(TestCase):
team=self.team,
created_by=self.user,
product=product,
name="列表商品 · 极速成片",
name="列表商品 · 一键成片",
metadata={"quick_create": True},
)
normal = Project.objects.create(
@@ -436,7 +436,7 @@ class QuickCreateApiTests(TestCase):
team=self.team,
created_by=self.user,
product=product,
name="锁单商品 · 极速成片",
name="锁单商品 · 一键成片",
metadata={"quick_create": True},
)
job = QuickCreateJob.objects.create(
@@ -448,7 +448,7 @@ class QuickCreateApiTests(TestCase):
blocked = self.client.patch(f"/api/projects/{project.id}/", {"name": "不该改"}, format="json")
self.assertEqual(blocked.status_code, 409)
self.assertIn("极速成片", str(blocked.data))
self.assertIn("一键成片", str(blocked.data))
allowed = self.client.get(f"/api/projects/{project.id}/")
self.assertEqual(allowed.status_code, 200)
@@ -473,7 +473,7 @@ class QuickCreateCoordinatorTests(TestCase):
team=self.team,
created_by=self.user,
product=self.product,
name="测试精华 · 极速成片",
name="测试精华 · 一键成片",
status=Project.Status.SCRIPTING,
current_stage=ProjectStage.Stage.SCRIPT,
)
@@ -554,7 +554,7 @@ class QuickCreateCoordinatorTests(TestCase):
QuickCreateJob.objects.filter(id=self.job.id).update(
status=QuickCreateJob.Status.QUEUED,
phase=QuickCreateJob.Phase.PRODUCT,
message="等待开始极速成片",
message="等待开始一键成片",
updated_at=timezone.now() - timedelta(seconds=30),
)
self.job.refresh_from_db()
@@ -748,7 +748,7 @@ class QuickCreateCoordinatorTests(TestCase):
self.job.metadata = {"storyboard_started": True}
self.job.save(update_fields=["status", "phase", "metadata", "updated_at"])
fail_quick_create(self.job, "极速成片暂未完成,请稍后重试或进入专业模式查看", internal_error="Timeout reading from socket")
fail_quick_create(self.job, "一键成片暂未完成,请稍后重试或进入专业模式查看", internal_error="Timeout reading from socket")
self.job.refresh_from_db()
self.project.refresh_from_db()
self.assertEqual(self.job.status, QuickCreateJob.Status.FAILED)
@@ -772,7 +772,7 @@ class QuickCreateCoordinatorTests(TestCase):
def test_recover_clears_false_failed_project_before_video_starts(self):
self.project.status = Project.Status.FAILED
self.project.current_stage = ProjectStage.Stage.VIDEO
self.project.failure_reason = "极速成片暂未完成,请稍后重试或进入专业模式查看"
self.project.failure_reason = "一键成片暂未完成,请稍后重试或进入专业模式查看"
self.project.save(update_fields=["status", "current_stage", "failure_reason", "updated_at"])
self.job.status = QuickCreateJob.Status.FAILED
self.job.phase = QuickCreateJob.Phase.PRODUCTION
@@ -805,7 +805,7 @@ class QuickCreateCoordinatorTests(TestCase):
def test_resume_failed_production_job_keeps_progress(self, enqueue):
self.job.status = QuickCreateJob.Status.FAILED
self.job.phase = QuickCreateJob.Phase.PRODUCTION
self.job.error_message = "极速成片暂未完成,请稍后重试或进入专业模式查看"
self.job.error_message = "一键成片暂未完成,请稍后重试或进入专业模式查看"
self.job.metadata = {"storyboard_started": True, "transient_retries": 8, "video_fail_retries": 2}
self.job.save(update_fields=["status", "phase", "error_message", "metadata", "updated_at"])
@@ -859,7 +859,7 @@ class QuickCreateCoordinatorTests(TestCase):
def test_failed_job_serializer_rewrites_hidden_moderation_error(self):
self.job.status = QuickCreateJob.Status.FAILED
self.job.phase = QuickCreateJob.Phase.PRODUCTION
self.job.error_message = "极速成片暂未完成,请稍后重试或进入专业模式查看"
self.job.error_message = "一键成片暂未完成,请稍后重试或进入专业模式查看"
self.job.metadata = {"internal_error": "400 moderation_blocked safety_violations=[sexual]"}
self.job.save(update_fields=["status", "phase", "error_message", "metadata", "updated_at"])
data = QuickCreateJobSerializer(self.job).data
@@ -882,7 +882,7 @@ class QuickCreateCoordinatorTests(TestCase):
segment.save(update_fields=["adopted_version", "status", "updated_at"])
self.job.status = QuickCreateJob.Status.FAILED
self.job.phase = QuickCreateJob.Phase.PRODUCTION
self.job.error_message = "极速成片暂未完成,请稍后重试或进入专业模式查看"
self.job.error_message = "一键成片暂未完成,请稍后重试或进入专业模式查看"
self.job.save(update_fields=["status", "phase", "error_message", "updated_at"])
recover_quick_create(self.job)
@@ -894,7 +894,7 @@ class QuickCreateCoordinatorTests(TestCase):
"""专业模式完成后,极速任务的旧失败状态必须自动被回收。"""
self.job.status = QuickCreateJob.Status.FAILED
self.job.phase = QuickCreateJob.Phase.PRODUCTION
self.job.error_message = "极速成片暂未完成,请稍后重试或进入专业模式查看"
self.job.error_message = "一键成片暂未完成,请稍后重试或进入专业模式查看"
self.job.save(update_fields=["status", "phase", "error_message", "updated_at"])
self.project.status = Project.Status.COMPLETED
self.project.current_stage = ProjectStage.Stage.VIDEO
@@ -1143,7 +1143,7 @@ class QuickCreateCoordinatorTests(TestCase):
base_ids = self._ready_asset_tasks()
self.job.status = QuickCreateJob.Status.FAILED
self.job.phase = QuickCreateJob.Phase.ASSETS
self.job.error_message = "极速成片暂未完成,请稍后重试或进入专业模式查看"
self.job.error_message = "一键成片暂未完成,请稍后重试或进入专业模式查看"
self.job.metadata = {
"base_asset_task_ids": base_ids,
"assets_started": True,
@@ -1194,7 +1194,7 @@ class QuickCreateCoordinatorTests(TestCase):
asset = Asset.objects.create(
team=self.team,
created_by=self.user,
name="极速成片",
name="一键成片",
asset_type=Asset.Type.VIDEO,
source=Asset.Source.AI_GENERATED,
category=Asset.Category.VIDEO_CLIP,
+14 -15
View File
@@ -98,7 +98,7 @@ logger = logging.getLogger(__name__)
class QuickCreateInProgress(APIException):
status_code = status.HTTP_409_CONFLICT
default_detail = "该项目正在极速成片中,请到极速成片页查看进度"
default_detail = "该项目正在一键成片中,请到一键成片页查看进度"
default_code = "quick_create_running"
@@ -553,7 +553,7 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
]
if text_model is None:
return Response(
{"detail": "极速成片需要的豆包 Seed 2.1 Pro 模型未启用,请联系管理员配置"},
{"detail": "一键成片需要的豆包 Seed 2.1 Pro 模型未启用,请联系管理员配置"},
status=status.HTTP_503_SERVICE_UNAVAILABLE,
)
if missing:
@@ -653,7 +653,7 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
team=team,
created_by=request.user,
product=product,
name=f"{name} · 极速成片",
name=f"{name} · 一键成片",
status=Project.Status.SCRIPTING,
current_stage=ProjectStage.Stage.SCRIPT,
metadata={
@@ -665,7 +665,7 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
"video_model_config_id": str(video_model.id),
"video_model_name": video_model.name,
"video_model_label": video_model.display_name,
"presentation_format": rec["format"],
"presentation_format": "oral",
"video_structure": rec["structure"],
"persona": rec["persona"],
},
@@ -676,7 +676,7 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
ProductSellingPoint.objects.create(
product=product,
title=name,
detail="极速成片根据商品名称自动填写",
detail="一键成片根据商品名称自动填写",
sort_order=0,
)
job = QuickCreateJob.objects.create(
@@ -686,7 +686,7 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
status=QuickCreateJob.Status.QUEUED,
phase=QuickCreateJob.Phase.PRODUCT,
progress=0,
message="等待开始极速成片",
message="等待开始一键成片",
)
try:
@@ -722,7 +722,7 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
def quick_create_status(self, request, job_id=None):
job = self._quick_job_queryset().filter(id=job_id).first()
if job is None:
return Response({"detail": "极速成片任务不存在"}, status=status.HTTP_404_NOT_FOUND)
return Response({"detail": "一键成片任务不存在"}, status=status.HTTP_404_NOT_FOUND)
from .services.quick_create import recover_quick_create
try:
@@ -736,7 +736,7 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
def quick_create_cancel(self, request, job_id=None):
job = self._quick_job_queryset().filter(id=job_id).first()
if job is None:
return Response({"detail": "极速成片任务不存在"}, status=status.HTTP_404_NOT_FOUND)
return Response({"detail": "一键成片任务不存在"}, status=status.HTTP_404_NOT_FOUND)
if job.status == QuickCreateJob.Status.SUCCEEDED:
return Response({"detail": "已完成的任务不能取消"}, status=status.HTTP_400_BAD_REQUEST)
from .services.quick_create import cancel_quick_create
@@ -749,7 +749,7 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
def quick_create_retry(self, request, job_id=None):
job = self._quick_job_queryset().filter(id=job_id).first()
if job is None:
return Response({"detail": "极速成片任务不存在"}, status=status.HTTP_404_NOT_FOUND)
return Response({"detail": "一键成片任务不存在"}, status=status.HTTP_404_NOT_FOUND)
if job.status == QuickCreateJob.Status.SUCCEEDED:
return Response({"detail": "任务已完成"}, status=status.HTTP_400_BAD_REQUEST)
if job.status == QuickCreateJob.Status.CANCELLED:
@@ -808,14 +808,13 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
"template_outline": render_outline_text({"outline": template.outline, "cta": template.cta}),
}
)
# 套路参数只在模板真有值时覆盖,空值不要把设定卡的智能推荐顶掉
# 旧模板可能存了中文标签(「短剧」),这里归一成 key 再写入 wizard。
# 表现形式已统一为口播;旧模板里的短剧/Vlog 只兼容读取结构,不能覆盖当前规则
raw_format = getattr(template, "presentation_format", "") or ""
raw_structure = getattr(template, "video_structure", "") or ""
if raw_format or raw_structure:
fmt, structure = coerce_template_combo(raw_format, raw_structure)
wizard["presentation_format"] = fmt
_fmt, structure = coerce_template_combo(raw_format, raw_structure)
wizard["video_structure"] = structure
wizard["presentation_format"] = "oral"
persona = coerce_persona(getattr(template, "persona", "") or "")
if persona:
wizard["persona"] = persona
@@ -860,7 +859,7 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
"""对话式脚本 agent · 流式(SSE)。出稿 + 改稿一体,多模型可选。
请求体:mode(auto|theme|revise)promptmodel_config_idselling_point_idspersona
base_version_id(改稿)aspect_ratiototal_duration(15/30/45/60)
presentation_format(oral|drama|vlog)video_structure(pain|contrast|review|scene)
video_structure(pain|contrast|review|scene)表现形式固定为口播
响应:text/event-stream,逐帧吐 tool/delta/draft/saved/done/error"""
project = self.get_object()
mode = str(request.data.get("mode") or "auto")
@@ -870,7 +869,7 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
# 非法值一律由 agent 侧 coerce 兜底(夹区间/回落默认),这里不做 400,避免生成被参数噪声打断
total_duration = coerce_total_duration(request.data.get("total_duration"))
presentation_format, video_structure = combo_keys(
request.data.get("presentation_format"),
"oral",
request.data.get("video_structure"),
)
wizard = ((project.metadata or {}).get("wizard") if isinstance(project.metadata, dict) else None) or {}