feat(storyboard): 故事板改分镜制(单场独立重跑/历史)+ 视频过审闸 + 审核前端如实化

三块改动在 services/views/pipeline/App/api 等文件按行交织、且过审闸后端依赖故事板新
模型,无法拆成各自独立可编译的提交,故合并为一次:

· 故事板分镜制:新增 StoryboardShot/StoryboardShotVersion(对标 VideoSegment/Version)
  + 数据迁移(采用版每帧转一 shot+已采用版本)。每镜一个 shot,各自历史版本,可单场
  重跑、逐场切换/采用,新增「开始生成故事板/单场重跑/采用某版」端点。重跑非阻塞(后台
  静默轮询出图,不锁其它场按钮);历史「查看」与「采用」分离(切版本不丢在制重跑)。
  视频取分镜图/删分镜清理/过审闸全部改读 storyboard_shots。
· 视频生成前过审闸:点生成校验含真人脸的人物立绘/故事板分镜是否过审,未过审弹窗列出
  哪一镜/哪个,可一键送审,挡住不生成(collect_video_review_blockers + precheck 端点)。
· 审核前端如实化:不再 `|| "processing"` 伪造审核中;送审失败经 onNotify 如实提示;
  审核轮询扩到视频阶段,送审后能看着盾变绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-24 17:11:55 +08:00
co-authored by Claude Opus 4.8
parent d6cb280018
commit 658aaa222f
12 changed files with 936 additions and 243 deletions
@@ -0,0 +1,134 @@
# Generated by Django 5.1.15 on 2026-06-24 08:01
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("ai", "0016_person_triview_16_9"),
("assets", "0006_alter_asset_category_model"),
("projects", "0004_project_projects_pr_team_id_e667f8_idx"),
]
operations = [
migrations.CreateModel(
name="StoryboardShot",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("sort_order", models.PositiveIntegerField(default=0)),
(
"status",
models.CharField(
choices=[
("not_started", "Not Started"),
("queued", "Queued"),
("running", "Running"),
("succeeded", "Succeeded"),
("failed", "Failed"),
],
default="not_started",
max_length=32,
),
),
("prompt", models.TextField(blank=True)),
("error_message", models.TextField(blank=True)),
(
"project",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="storyboard_shots",
to="projects.project",
),
),
(
"script_segment",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="storyboard_shots",
to="projects.scriptsegment",
),
),
],
options={
"ordering": ["sort_order", "created_at"],
},
),
migrations.CreateModel(
name="StoryboardShotVersion",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("prompt", models.TextField(blank=True)),
("is_adopted", models.BooleanField(default=False)),
("metadata", models.JSONField(blank=True, default=dict)),
(
"asset",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="storyboard_shot_versions",
to="assets.asset",
),
),
(
"shot",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="versions",
to="projects.storyboardshot",
),
),
(
"task",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="storyboard_shot_versions",
to="ai.aitask",
),
),
],
options={
"ordering": ["-created_at"],
},
),
migrations.AddField(
model_name="storyboardshot",
name="adopted_version",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="adopted_by_shots",
to="projects.storyboardshotversion",
),
),
migrations.AlterUniqueTogether(
name="storyboardshot",
unique_together={("project", "sort_order")},
),
]
@@ -0,0 +1,50 @@
from django.db import migrations
def forwards(apps, schema_editor):
"""把旧「整版制」故事板迁到新「分镜制」:每个项目只取采用版(无采用版则取最新版)的每一帧,
建一个 StoryboardShot + 一条已采用 StoryboardShotVersion(沿用同一张图)。旧的其它整版历史丢弃。"""
Project = apps.get_model("projects", "Project")
StoryboardVersion = apps.get_model("projects", "StoryboardVersion")
StoryboardShot = apps.get_model("projects", "StoryboardShot")
StoryboardShotVersion = apps.get_model("projects", "StoryboardShotVersion")
for project in Project.objects.all().iterator():
adopted = (
StoryboardVersion.objects.filter(project=project, is_adopted=True).order_by("-created_at").first()
or StoryboardVersion.objects.filter(project=project).order_by("-created_at").first()
)
if adopted is None:
continue
seen_orders = set()
for frame in adopted.frames.order_by("sort_order", "created_at"):
if frame.sort_order in seen_orders: # 同序帧只保第一张,守 (project, sort_order) 唯一约束
continue
seen_orders.add(frame.sort_order)
shot = StoryboardShot.objects.create(
project=project,
script_segment=frame.script_segment,
sort_order=frame.sort_order,
status="succeeded",
prompt=frame.prompt or "",
)
version = StoryboardShotVersion.objects.create(
shot=shot, asset=frame.asset, prompt=frame.prompt or "", is_adopted=True,
)
shot.adopted_version = version
shot.save(update_fields=["adopted_version"])
def backwards(apps, schema_editor):
StoryboardShot = apps.get_model("projects", "StoryboardShot")
StoryboardShot.objects.all().delete() # version 随 shot 级联删
class Migration(migrations.Migration):
dependencies = [
("projects", "0005_storyboardshot_storyboardshotversion_and_more"),
]
operations = [
migrations.RunPython(forwards, backwards),
]