fix(pipeline): 视频为末步时收口项目完成态 + 列表 5→4 步同步
V1 雪藏「拼接导出」第 5 阶段后,完成态(COMPLETED)只在 export.py 拼接成片时 才置,导致视频全部出片的项目永远卡在 videoing,列表末格一直闪烁。 - 后端:新增 settle_video_completion(幂等)+ VIDEO_IS_FINAL_STAGE 开关。 全部片段出片→项目 completed、video 阶段标成功;某段被重跑→退回 videoing。 挂 retrieve(自愈历史卡住项目)/poll-video-segment/submit-video-segment/ adopt-video-version 四入口;收口直接查库,不吃 prefetch 旧缓存致误判。 V2 恢复导出把 VIDEO_IS_FINAL_STAGE 置 False,完成判定交回 export.py。 - 前端:项目列表/网格进度从 5 步同步成 4 步(PROJ_STAGE_TOTAL=4), export 阶段并入第 4;已完成项目末格走 done(绿)不再用闪烁的 cur。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -120,6 +120,49 @@ def promote_base_asset_stage_if_ready(project: Project) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
# V1 雪藏「拼接导出」第 5 阶段(前端 STAGE_STEPS 同步隐藏)→ 视频是当前最后一步。
|
||||
# 因此「全部片段出片」即视为项目完成,不再依赖导出任务(原来只有 export.py 在拼接成片后
|
||||
# 才置 COMPLETED,导出一藏起来就没有任何环节会把 videoing 翻成 completed)。
|
||||
# V2 恢复导出时把此常量置 False:完成重新交回 export.py。
|
||||
VIDEO_IS_FINAL_STAGE = True
|
||||
|
||||
|
||||
def settle_video_completion(project: Project) -> bool:
|
||||
"""视频阶段收口(幂等):
|
||||
· 全部片段已出片(SUCCEEDED + 有采用版)→ 项目 COMPLETED、VIDEO 阶段标成功;
|
||||
· 否则若项目却仍标着 COMPLETED(用户重跑了某一镜)→ 退回 VIDEOING。
|
||||
仅当视频为末阶段时生效;非视频阶段不动。返回是否发生状态变更。"""
|
||||
if not VIDEO_IS_FINAL_STAGE:
|
||||
return False
|
||||
if project.current_stage != ProjectStage.Stage.VIDEO:
|
||||
return False
|
||||
# 直接查库,不用 project.video_segments 的 prefetch 缓存:调用方常在刚 save 完某段后调用,
|
||||
# 缓存里仍是旧状态会误判(漏判完成 / 误判未完成)。
|
||||
segments = list(
|
||||
VideoSegment.objects.filter(project=project).values_list("status", "adopted_version_id")
|
||||
)
|
||||
if not segments:
|
||||
return False
|
||||
all_done = all(
|
||||
status_ == VideoSegment.Status.SUCCEEDED and adopted_id is not None
|
||||
for status_, adopted_id in segments
|
||||
)
|
||||
if all_done and project.status != Project.Status.COMPLETED:
|
||||
stage, _ = ProjectStage.objects.get_or_create(project=project, stage=ProjectStage.Stage.VIDEO)
|
||||
if stage.status != ProjectStage.Status.SUCCEEDED:
|
||||
stage.status = ProjectStage.Status.SUCCEEDED
|
||||
stage.save(update_fields=["status", "updated_at"])
|
||||
project.status = Project.Status.COMPLETED
|
||||
project.save(update_fields=["status", "updated_at"])
|
||||
return True
|
||||
if not all_done and project.status == Project.Status.COMPLETED:
|
||||
# 重跑某段后又回到在制 → 从「已完成」退回「视频生成中」(用户重跑第 3 镜的情形)
|
||||
project.status = Project.Status.VIDEOING
|
||||
project.save(update_fields=["status", "updated_at"])
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
queryset = Project.objects.select_related("product", "timeline").prefetch_related(
|
||||
"stages",
|
||||
@@ -159,6 +202,9 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
adopted_script = proj.script_versions.filter(is_adopted=True).order_by("-created_at").first()
|
||||
if adopted_script is not None:
|
||||
self._sync_video_segments_to_script(proj, adopted_script)
|
||||
# 自愈完成态:历史项目「全部片段已出片」却卡在 videoing(导出藏起来后没有环节翻状态),
|
||||
# 进详情时收口成 completed;若有段被重跑则退回 videoing。
|
||||
settle_video_completion(proj)
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -651,6 +697,8 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
timeline.clips.filter(asset_id__in=segment_asset_ids).exclude(asset_id=version.asset_id).update(
|
||||
asset_id=version.asset_id, trim_start_ms=0, trim_end_ms=None
|
||||
)
|
||||
# 采用历史版后该段为已出片 → 若全部段就绪,收口项目为 completed
|
||||
settle_video_completion(project)
|
||||
return Response(ProjectSerializer(project).data)
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="generate-storyboard")
|
||||
@@ -760,6 +808,8 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
poll_video_segment_task.apply_async(args=[str(segment.id)], countdown=30)
|
||||
except Exception: # noqa: BLE001
|
||||
logger.error("poll_video_segment_task enqueue failed; relying on client polling", exc_info=True)
|
||||
# 重跑某段 → 该段回到在制;若项目此前已 completed,退回 videoing
|
||||
settle_video_completion(project)
|
||||
return Response(ProjectSerializer(project).data, status=status.HTTP_202_ACCEPTED)
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="poll-video-segment")
|
||||
@@ -769,6 +819,8 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
version = poll_video_segment(video_segment=segment, user=request.user)
|
||||
if version is None:
|
||||
return Response({"status": segment.status}, status=status.HTTP_202_ACCEPTED)
|
||||
# 这一段刚出片 → 若是最后一段,收口整个项目为 completed(视频是末阶段)
|
||||
settle_video_completion(project)
|
||||
return Response(VideoSegmentVersionSerializer(version).data)
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="submit-export")
|
||||
|
||||
Reference in New Issue
Block a user