优化脚本
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user