优化脚本

This commit is contained in:
Azmat@qq.com
2026-08-28 14:45:38 +08:00
parent 849b14a0f6
commit c0a0b9603b
37 changed files with 541 additions and 1893 deletions
+19 -12
View File
@@ -5,14 +5,23 @@ from django.utils import timezone
from apps.projects.models import Project, ProjectStage, ScriptVersion, VideoSegment
# 故事板已从流程中去掉:脚本 → 基础资产 → 视频 → 导出。
# ProjectStage.Stage.STORYBOARD 枚举保留(老项目的历史行还引用它),但不再出现在流水线里。
STAGE_ORDER = [
ProjectStage.Stage.SCRIPT,
ProjectStage.Stage.BASE_ASSETS,
ProjectStage.Stage.STORYBOARD,
ProjectStage.Stage.VIDEO,
ProjectStage.Stage.EXPORT,
]
# 老项目可能正停在已下线的 storyboard 阶段;按「基础资产之后」对待,让它们能正常进视频。
LEGACY_STAGE_FALLBACK = {ProjectStage.Stage.STORYBOARD: ProjectStage.Stage.BASE_ASSETS}
def normalize_stage(stage: str) -> str:
"""把已下线的阶段名归一到现役阶段,供索引/比较使用。"""
return LEGACY_STAGE_FALLBACK.get(stage, stage)
@dataclass(frozen=True)
class StageTransition:
@@ -22,24 +31,22 @@ class StageTransition:
reason: str = ""
def can_enter_stage(current_stage: str, target_stage: str, allow_skip_storyboard: bool = True) -> StageTransition:
def can_enter_stage(current_stage: str, target_stage: str) -> StageTransition:
if target_stage not in STAGE_ORDER:
return StageTransition(current_stage, target_stage, False, "unknown target stage")
current_index = STAGE_ORDER.index(current_stage) if current_stage in STAGE_ORDER else -1
current = normalize_stage(current_stage)
current_index = STAGE_ORDER.index(current) if current in STAGE_ORDER else -1
target_index = STAGE_ORDER.index(target_stage)
if target_index <= current_index + 1:
return StageTransition(current_stage, target_stage, True)
if allow_skip_storyboard and current_stage == ProjectStage.Stage.BASE_ASSETS and target_stage == ProjectStage.Stage.VIDEO:
return StageTransition(current_stage, target_stage, True)
return StageTransition(current_stage, target_stage, False, "stage prerequisite is not satisfied")
def initialize_project_pipeline(project: Project, *, placeholder_segments: int = 4) -> None:
"""建立专业创作和极速成片共用的阶段、视频占位数据。幂等,可安全重试。"""
"""建立专业创作和一键成片共用的阶段、视频占位数据。幂等,可安全重试。"""
for stage_name in STAGE_ORDER:
ProjectStage.objects.get_or_create(project=project, stage=stage_name)
for index in range(placeholder_segments):
@@ -94,7 +101,7 @@ def sync_video_segments_to_script(project: Project, script: ScriptVersion) -> No
def adopt_script_version(project: Project, script: ScriptVersion) -> None:
"""采用脚本并推进到资产阶段,供专业创作按钮和极速编排共同调用。"""
"""采用脚本并推进到资产阶段,供专业创作按钮和一键成片编排共同调用。"""
ScriptVersion.objects.filter(project=project).exclude(id=script.id).update(is_adopted=False)
if not script.is_adopted:
script.is_adopted = True
@@ -111,9 +118,9 @@ def adopt_script_version(project: Project, script: ScriptVersion) -> None:
project.save(update_fields=["current_stage", "status", "failure_reason", "updated_at"])
def finish_storyboard_stage(project: Project) -> None:
"""故事板全部成功后推进到视频阶段,并再次校准片段数量。"""
stage, _ = ProjectStage.objects.get_or_create(project=project, stage=ProjectStage.Stage.STORYBOARD)
def finish_base_assets_stage(project: Project) -> None:
"""基础资产齐备后直接推进到视频阶段(故事板已下线),并再次校准片段数量。"""
stage, _ = ProjectStage.objects.get_or_create(project=project, stage=ProjectStage.Stage.BASE_ASSETS)
stage.status = ProjectStage.Status.SUCCEEDED
stage.completed_at = timezone.now()
stage.error_message = ""
@@ -127,7 +134,7 @@ def finish_storyboard_stage(project: Project) -> None:
def finish_video_stage(project: Project) -> bool:
"""全部片段成功即完成专业创作的视频阶段。"""
"""全部片段成功即完成视频阶段。"""
segments = list(project.video_segments.values_list("status", "adopted_version_id"))
if not segments or not all(status == VideoSegment.Status.SUCCEEDED and adopted for status, adopted in segments):
return False
@@ -1,7 +1,10 @@
"""“极速成片”自动编排。
"""“一键成片”自动编排。
这不是另一套生成实现:脚本、基础资产、三视图、故事板、视频和合成全部调用专业创作现有服务。
这不是另一套生成实现:脚本、基础资产、三视图、视频和合成全部调用专业创作现有服务。
本模块只负责按状态推进,并把子任务 ID 持久化到 QuickCreateJob,方便刷新后恢复进度。
★ 故事板已从流程中去掉:资产齐备后直接提交视频,导演信息全部由脚本的秒级分镜
经 build_video_segment_prompt 直达出片模型。
"""
from __future__ import annotations
@@ -27,8 +30,6 @@ from apps.ai.services import (
create_export_job,
generate_base_asset,
generate_person_triview,
poll_storyboard,
submit_storyboard,
submit_video_segment,
video_segment_has_inflight_task,
)
@@ -48,7 +49,7 @@ from apps.projects.models import (
from apps.projects.services.pipeline import (
STAGE_ORDER,
adopt_script_version,
finish_storyboard_stage,
finish_base_assets_stage,
finish_video_stage,
sync_video_segments_to_script,
)
@@ -305,9 +306,7 @@ def _restore_project_after_orchestrator_timeout(job: QuickCreateJob) -> None:
project = job.project
if project.status != Project.Status.FAILED or _videos_have_started(job):
return
shots = list(project.storyboard_shots.all())
storyboard_ready = bool(shots) and all(shot.status == "succeeded" and shot.adopted_version_id for shot in shots)
if project.current_stage != ProjectStage.Stage.VIDEO and not storyboard_ready:
if project.current_stage != ProjectStage.Stage.VIDEO:
return
project.status = Project.Status.VIDEOING
project.failure_reason = ""
@@ -804,7 +803,7 @@ def _all_portraits_have_triview(project: Project) -> bool:
return all(_portrait_has_triview(project, group.adopted_asset_id) for group in portraits)
def _finish_assets_and_enter_storyboard(job: QuickCreateJob) -> int:
def _finish_assets_and_enter_video(job: QuickCreateJob) -> int:
for group in job.project.base_asset_groups.filter(adopted_asset__isnull=False):
if group.kind != BaseAssetGroup.Kind.PRODUCT:
group_meta = dict(group.metadata or {})
@@ -816,14 +815,14 @@ def _finish_assets_and_enter_storyboard(job: QuickCreateJob) -> int:
stage.completed_at = timezone.now()
stage.error_message = ""
stage.save(update_fields=["status", "completed_at", "error_message", "updated_at"])
job.project.current_stage = ProjectStage.Stage.STORYBOARD
job.project.status = Project.Status.STORYBOARDING
job.project.current_stage = ProjectStage.Stage.VIDEO
job.project.status = Project.Status.VIDEOING
job.project.save(update_fields=["current_stage", "status", "updated_at"])
_save_job(
job,
phase=QuickCreateJob.Phase.PRODUCTION,
progress=72,
message="正在生成故事板与镜头画面",
message="素材已就绪,正在准备生成视频",
)
return 1
@@ -874,7 +873,7 @@ def _advance_assets(job: QuickCreateJob) -> int | None:
metadata.pop("triview_task_ids", None)
metadata.pop("triview_ready", None)
_save_job(job, metadata=metadata)
return _finish_assets_and_enter_storyboard(job)
return _finish_assets_and_enter_video(job)
def _reviews_ready(job: QuickCreateJob) -> bool | None:
@@ -896,7 +895,7 @@ def _reviews_ready(job: QuickCreateJob) -> bool | None:
metadata["reviews_skipped"] = True
_save_job(job, metadata=metadata, message="质量检查暂时不可用,继续生成视频")
return True
_save_job(job, metadata=metadata, progress=82, message="故事板已完成,正在进行视频素材质量检查")
_save_job(job, metadata=metadata, progress=82, message="素材已就绪,正在进行视频素材质量检查")
return False
blockers = collect_video_review_blockers(job.project)
if not blockers:
@@ -925,7 +924,7 @@ def _reviews_ready(job: QuickCreateJob) -> bool | None:
asset = Asset.objects.filter(team=job.team, id=item["asset_id"]).first()
if asset is not None:
submit_asset_for_review(asset)
_save_job(job, progress=82, message="故事板已完成,正在进行视频素材质量检查")
_save_job(job, progress=82, message="素材已就绪,正在进行视频素材质量检查")
return False
@@ -963,7 +962,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"],
@@ -1075,64 +1074,10 @@ def _complete(job: QuickCreateJob) -> None:
def _advance_production(job: QuickCreateJob) -> int | None:
metadata = dict(job.metadata or {})
if (
metadata.get("storyboard_started")
and job.project.current_stage != ProjectStage.Stage.VIDEO
and not job.project.storyboard_shots.exists()
):
metadata["storyboard_started"] = False
_save_job(job, metadata=metadata, message="正在生成故事板与镜头画面")
if not (job.metadata or {}).get("storyboard_started"):
with transaction.atomic():
job = QuickCreateJob.objects.select_for_update().select_related("project").get(id=job.id)
metadata = dict(job.metadata or {})
if not metadata.get("storyboard_started"):
metadata["storyboard_started"] = True
_save_job(job, metadata=metadata, progress=74, message="正在生成故事板与镜头画面")
should_submit = True
else:
should_submit = False
if should_submit:
try:
submit_storyboard(project=job.project, user=job.created_by or job.project.created_by, prompt="")
stage, _ = ProjectStage.objects.get_or_create(project=job.project, stage=ProjectStage.Stage.STORYBOARD)
stage.status = ProjectStage.Status.RUNNING
stage.save(update_fields=["status", "updated_at"])
except Exception as exc:
metadata = dict(job.metadata or {})
metadata.pop("storyboard_started", None)
metadata["internal_error"] = str(exc)[:2000]
hard = any(
token in str(exc).lower()
for token in ("insufficient credit", "no active", "not configured")
)
retries = int(metadata.get("storyboard_submit_retries") or 0)
if not hard and retries < 3:
metadata["storyboard_submit_retries"] = retries + 1
_save_job(job, metadata=metadata, message="正在继续生成故事板…")
return POLL_DELAY_SECONDS
_save_job(job, metadata=metadata)
raise
"""出片阶段:过审闸 → 提交各镜视频 → 合成导出。
故事板已下线,资产齐备后就直接提交视频,不再有中间出图环节。"""
if job.project.current_stage != ProjectStage.Stage.VIDEO:
result = poll_storyboard(project=job.project, user=job.created_by or job.project.created_by)
if result.get("status") == "failed":
retries = int((job.metadata or {}).get("storyboard_fail_retries") or 0)
if retries >= 2:
fail_quick_create(job, _public_error(str(result.get("error") or "故事板生成失败")))
return None
metadata = dict(job.metadata or {})
metadata["storyboard_fail_retries"] = retries + 1
metadata["storyboard_started"] = False
_save_job(job, metadata=metadata, message="故事板未成功,正在重试…")
return 1
if result.get("status") != "succeeded":
total = max(1, int(result.get("total") or 1))
done = int(result.get("done") or 0)
_save_job(job, progress=min(80, 74 + round(done / total * 6)), message=f"正在生成故事板({done}/{total})")
return POLL_DELAY_SECONDS
finish_storyboard_stage(job.project)
finish_base_assets_stage(job.project)
review_state = _reviews_ready(job)
if review_state is None:
@@ -1142,7 +1087,23 @@ def _advance_production(job: QuickCreateJob) -> int | None:
job.refresh_from_db(fields=["metadata"])
if not (job.metadata or {}).get("video_started"):
_start_videos(job)
try:
_start_videos(job)
except Exception as exc: # noqa: BLE001
# 提交视频失败:硬错(余额不足 / 没有可用模型 / 未配置)直接抛给上层落可恢复终态;
# 其余(网络抖动、素材还没写完)给 3 次机会,别让一次失败把整单打死。
metadata = dict(job.metadata or {})
metadata["internal_error"] = str(exc)[:2000]
hard = any(
token in str(exc).lower()
for token in ("insufficient credit", "no active", "not configured")
)
retries = int(metadata.get("video_submit_retries") or 0)
if hard or retries >= 3:
_save_job(job, metadata=metadata)
raise
metadata["video_submit_retries"] = retries + 1
_save_job(job, metadata=metadata, message="正在继续申请生成视频…")
return POLL_DELAY_SECONDS
segments = list(job.project.video_segments.order_by("sort_order"))
@@ -1480,7 +1441,7 @@ def _advance_without_quick_queue(job_id: str) -> None:
def resume_quick_create(job: QuickCreateJob) -> QuickCreateJob:
"""从失败处接着跑:已完成的脚本/资产/故事板保留,只补没做完的步骤。"""
"""从失败处接着跑:已完成的脚本 / 资产 / 视频保留,只补没做完的步骤。"""
job.refresh_from_db()
if job.status == QuickCreateJob.Status.SUCCEEDED:
return job
@@ -1498,12 +1459,11 @@ def resume_quick_create(job: QuickCreateJob) -> QuickCreateJob:
metadata.pop("review_poll_retries", None)
metadata.pop("next_advance_at", None)
metadata.pop("video_fail_retries", None)
metadata.pop("video_submit_retries", None)
metadata.pop("asset_fail_retries", None)
metadata.pop("asset_fail_retries_by_key", None)
metadata.pop("storyboard_fail_retries", None)
metadata.pop("triview_fail_retries", None)
metadata.pop("triview_fail_retries_by_portrait", None)
metadata.pop("storyboard_submit_retries", None)
if metadata.pop("triview_skipped", None):
metadata.pop("triview_task_ids", None)
metadata.pop("triview_ready", None)
@@ -1633,7 +1593,7 @@ def recover_quick_create(job: QuickCreateJob) -> None:
def advance_quick_create(job_id: str) -> int | None:
"""推进一个状态并返回下次轮询秒数;返回 None 表示终态。
一次只做一段:识别商品 → 脚本(独立任务) → 资产 → 故事板/视频/合成。
一次只做一段:识别商品 → 脚本(独立任务) → 资产 → 视频/合成。
脚本不再在编排任务里同步吃完整条 SSE,避免页面一直停在「推荐脚本方向」。
"""
job = QuickCreateJob.objects.select_related("project__product", "created_by", "team").get(id=job_id)