fix(core): 测试清单 行24/28-39/46 — 流水线脚本&资产/消息页 + 单镜重跑后端接口
前端流水线(pipeline.tsx):脚本助手三选项指引(AI全生/一句话/自带脚本)+来源风格推荐;
风格/人物设定卡并持久化到 project.metadata;Enter发送/Shift+Enter换行;长文折叠10行;
生成进度流提示;分镜人物/场景标签可编辑并持久化+点击添加分镜;单镜重跑/删除即时反馈;
脚本助手记录按项目 localStorage 持久化;Stage2 基础资产补替换/重跑/可编辑提示词;
三视图采用弹窗+缺三视图气泡。
消息页(messages):复用全站 .search-inline 搜索框、去掉小标题背景方块。
视频项目(projects):列表按创建时间倒序,新建置顶。
性能:action 改后台 hydrate,消除创建/确认后白等(行29/37)。
后端(projects/ai):新增单镜 AI 重跑接口 POST /projects/{id}/rerun-script-segment/
(regenerate_script_segment 服务,复用 AITask+计费 reserve/charge/release;+2 单测)。
前端 build 通过;后端 check 0/无新迁移/apps.projects 14 测试通过。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ from apps.products.models import Product
|
||||
from apps.projects.models import (
|
||||
Project,
|
||||
ProjectStage,
|
||||
ScriptSegment,
|
||||
ScriptVersion,
|
||||
SubtitleTrack,
|
||||
Timeline,
|
||||
@@ -110,6 +111,47 @@ class ProjectApiTests(TestCase):
|
||||
self.assertEqual(script.segments.count(), 4)
|
||||
self.assertEqual(CreditLedger.objects.filter(team=self.team, ledger_type=CreditLedger.Type.CHARGE).count(), 1)
|
||||
|
||||
@patch("apps.ai.services.VolcanoArkProvider")
|
||||
def test_rerun_script_segment_updates_one_segment_and_charges_once(self, provider_cls):
|
||||
provider = provider_cls.return_value
|
||||
provider.chat_completion.return_value = {"choices": [{"message": {"content": "x"}}]}
|
||||
provider.extract_text.return_value = "旁白:全新口播文案\n画面:全新画面描述"
|
||||
project = Project.objects.create(team=self.team, created_by=self.user, product=self.product, name="P")
|
||||
script = ScriptVersion.objects.create(project=project, title="脚本", content="...", is_adopted=True)
|
||||
seg0 = ScriptSegment.objects.create(script_version=script, sort_order=0, narration="旧0", visual_prompt="画0")
|
||||
seg1 = ScriptSegment.objects.create(script_version=script, sort_order=1, narration="旧1", visual_prompt="画1")
|
||||
|
||||
response = self.client.post(
|
||||
f"/api/projects/{project.id}/rerun-script-segment/",
|
||||
{"segment_id": str(seg1.id), "instruction": "更俏皮"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
seg0.refresh_from_db()
|
||||
seg1.refresh_from_db()
|
||||
self.assertEqual(seg1.narration, "全新口播文案")
|
||||
self.assertEqual(seg1.visual_prompt, "全新画面描述")
|
||||
# 其它分镜保持不动
|
||||
self.assertEqual(seg0.narration, "旧0")
|
||||
self.assertEqual(seg0.visual_prompt, "画0")
|
||||
# 返回的是该镜所属 ScriptVersion
|
||||
self.assertEqual(str(response.data["id"]), str(script.id))
|
||||
self.assertEqual(CreditLedger.objects.filter(team=self.team, ledger_type=CreditLedger.Type.CHARGE).count(), 1)
|
||||
|
||||
def test_rerun_script_segment_rejects_foreign_segment(self):
|
||||
project = Project.objects.create(team=self.team, created_by=self.user, product=self.product, name="P")
|
||||
other = Project.objects.create(team=self.team, created_by=self.user, product=self.product, name="O")
|
||||
other_script = ScriptVersion.objects.create(project=other, title="脚本", content="...")
|
||||
other_seg = ScriptSegment.objects.create(script_version=other_script, sort_order=0, narration="x")
|
||||
|
||||
response = self.client.post(
|
||||
f"/api/projects/{project.id}/rerun-script-segment/",
|
||||
{"segment_id": str(other_seg.id)},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_adopt_video_version_remaps_timeline_draft(self):
|
||||
"""切换采用版本后,时间线草稿里引用本场旧版本资产的片段必须跟随换成新资产(剪辑台/导出都读草稿)。"""
|
||||
project = Project.objects.create(team=self.team, created_by=self.user, product=self.product, name="P")
|
||||
|
||||
@@ -18,6 +18,7 @@ from apps.ai.services import (
|
||||
generate_project_script,
|
||||
generate_storyboard_frame,
|
||||
poll_video_segment,
|
||||
regenerate_script_segment,
|
||||
submit_storyboard,
|
||||
submit_video_segment,
|
||||
synthesize_project_voiceover,
|
||||
@@ -216,6 +217,28 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
segment.save(update_fields=[*changed, "updated_at"])
|
||||
return Response(ScriptVersionSerializer(segment.script_version).data)
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="rerun-script-segment")
|
||||
def rerun_script_segment(self, request, pk=None):
|
||||
"""单条分镜 AI 重跑:只重新生成该 ScriptSegment 的旁白+画面提示(其它镜不动),按现有计费扣费。"""
|
||||
project = self.get_object()
|
||||
segment_id = request.data.get("segment_id")
|
||||
if not segment_id:
|
||||
return Response({"detail": "segment_id is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
try:
|
||||
segment = ScriptSegment.objects.get(id=segment_id, script_version__project=project)
|
||||
except ScriptSegment.DoesNotExist:
|
||||
return Response({"detail": "script segment not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
try:
|
||||
script = regenerate_script_segment(
|
||||
project=project,
|
||||
user=request.user,
|
||||
segment=segment,
|
||||
instruction=str(request.data.get("instruction") or "").strip(),
|
||||
)
|
||||
except ValueError as exc:
|
||||
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(ScriptVersionSerializer(script).data)
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="add-script-segment")
|
||||
@transaction.atomic
|
||||
def add_script_segment(self, request, pk=None):
|
||||
|
||||
Reference in New Issue
Block a user