fix(canvas): 任务结果恢复 - 规范化分镜行文本字段
This commit is contained in:
@@ -150,6 +150,42 @@ function parseStoryboardRowsPayload(raw: string): { title?: string; rows?: Array
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeStoryboardText(value: unknown): string {
|
||||||
|
if (typeof value === "string") return value;
|
||||||
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||||||
|
if (Array.isArray(value)) return value.map(normalizeStoryboardText).filter(Boolean).join("、");
|
||||||
|
if (!value || typeof value !== "object") return "";
|
||||||
|
const record = value as Record<string, unknown>;
|
||||||
|
for (const key of ["text", "content", "value", "description", "name"]) {
|
||||||
|
if (!(key in record)) continue;
|
||||||
|
const nested = normalizeStoryboardText(record[key]);
|
||||||
|
if (nested) return nested;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStoryboardRow(row: Partial<StoryboardRow>): Partial<StoryboardRow> {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
plotDescription: normalizeStoryboardText(row.plotDescription),
|
||||||
|
dialogue: normalizeStoryboardText(row.dialogue),
|
||||||
|
narrativeIntent: normalizeStoryboardText(row.narrativeIntent),
|
||||||
|
viewerPOV: normalizeStoryboardText(row.viewerPOV),
|
||||||
|
performanceBlocking: normalizeStoryboardText(row.performanceBlocking),
|
||||||
|
shotSize: normalizeStoryboardText(row.shotSize),
|
||||||
|
emotion: normalizeStoryboardText(row.emotion),
|
||||||
|
lightingAndAtmosphere: normalizeStoryboardText(row.lightingAndAtmosphere),
|
||||||
|
audioEffects: normalizeStoryboardText(row.audioEffects),
|
||||||
|
camera: normalizeStoryboardText(row.camera),
|
||||||
|
motion: normalizeStoryboardText(row.motion),
|
||||||
|
timeBeats: normalizeStoryboardText(row.timeBeats),
|
||||||
|
imageGenerationPrompt: normalizeStoryboardText(row.imageGenerationPrompt),
|
||||||
|
videoMotionPrompt: normalizeStoryboardText(row.videoMotionPrompt),
|
||||||
|
continuityOut: normalizeStoryboardText(row.continuityOut),
|
||||||
|
negativePrompt: normalizeStoryboardText(row.negativePrompt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function storyboardRowsFromTask(task: GenerationTask) {
|
export function storyboardRowsFromTask(task: GenerationTask) {
|
||||||
const result = parseStoryboardRowsPayload(task.resultJson || "{}");
|
const result = parseStoryboardRowsPayload(task.resultJson || "{}");
|
||||||
if (!Array.isArray(result.rows) || !result.rows.length) throw new Error("分镜任务没有返回镜头行");
|
if (!Array.isArray(result.rows) || !result.rows.length) throw new Error("分镜任务没有返回镜头行");
|
||||||
@@ -158,16 +194,17 @@ export function storyboardRowsFromTask(task: GenerationTask) {
|
|||||||
// 否则可选链只能防 undefined,仍会在对象上调用 trim 使整页恢复失败。
|
// 否则可选链只能防 undefined,仍会在对象上调用 trim 使整页恢复失败。
|
||||||
title: typeof result.title === "string" ? result.title.trim() : undefined,
|
title: typeof result.title === "string" ? result.title.trim() : undefined,
|
||||||
rows: result.rows.map((row, index) => {
|
rows: result.rows.map((row, index) => {
|
||||||
|
const source = row && typeof row === "object" ? row : {};
|
||||||
const next = createStoryboardRow(index + 1, {
|
const next = createStoryboardRow(index + 1, {
|
||||||
...row,
|
...normalizeStoryboardRow(source),
|
||||||
id: `shot-${Date.now()}-${index + 1}-${Math.random().toString(36).slice(2, 6)}`,
|
id: `shot-${Date.now()}-${index + 1}-${Math.random().toString(36).slice(2, 6)}`,
|
||||||
shotNumber: index + 1,
|
shotNumber: index + 1,
|
||||||
status: "idle",
|
status: "idle",
|
||||||
assetBindings: normalizeStoryboardAssetBindings(row.assetBindings),
|
assetBindings: normalizeStoryboardAssetBindings(Array.isArray(source.assetBindings) ? source.assetBindings : undefined),
|
||||||
});
|
});
|
||||||
next.characters = Array.isArray(row.characters) ? row.characters : [];
|
next.characters = Array.isArray(source.characters) ? source.characters : [];
|
||||||
next.mustHave = Array.isArray(row.mustHave) ? row.mustHave : [];
|
next.mustHave = Array.isArray(source.mustHave) ? source.mustHave : [];
|
||||||
next.optionalDetails = Array.isArray(row.optionalDetails) ? row.optionalDetails : [];
|
next.optionalDetails = Array.isArray(source.optionalDetails) ? source.optionalDetails : [];
|
||||||
return next;
|
return next;
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
import { storyboardRowsFromTask } from "../src/lib/canvas/canvas-project-domain";
|
import { storyboardRowsFromTask } from "../src/lib/canvas/canvas-project-domain";
|
||||||
|
import { storyboardRowsToProjectShots } from "../src/pages/projects/detail/chapter-storyboard-production";
|
||||||
import type { GenerationTask } from "../src/services/api/task-center";
|
import type { GenerationTask } from "../src/services/api/task-center";
|
||||||
|
|
||||||
function storyboardTask(resultJson: string): GenerationTask {
|
function storyboardTask(resultJson: string): GenerationTask {
|
||||||
@@ -55,6 +56,23 @@ describe("storyboardRowsFromTask", () => {
|
|||||||
expect(storyboardRowsFromTask(task).rows).toHaveLength(1);
|
expect(storyboardRowsFromTask(task).rows).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("镜头文本字段不是字符串时仍可继续落库", () => {
|
||||||
|
const task = storyboardTask(JSON.stringify({ rows: [{
|
||||||
|
plotDescription: { text: "对象形式的剧情" },
|
||||||
|
dialogue: ["台词一", "台词二"],
|
||||||
|
camera: { value: "中景" },
|
||||||
|
motion: { description: "缓慢推进" },
|
||||||
|
timeBeats: 3,
|
||||||
|
}] }));
|
||||||
|
const result = storyboardRowsFromTask(task);
|
||||||
|
const shots = storyboardRowsToProjectShots(result.rows);
|
||||||
|
expect(shots[0]?.description).toBe("对象形式的剧情");
|
||||||
|
expect(shots[0]?.revision.dialogue).toBe("台词一、台词二");
|
||||||
|
expect(shots[0]?.revision.cameraAngle).toBe("中景");
|
||||||
|
expect(shots[0]?.revision.cameraMovement).toBe("缓慢推进");
|
||||||
|
expect(shots[0]?.revision.actionBeats).toEqual([{ description: "3" }]);
|
||||||
|
});
|
||||||
|
|
||||||
test("text 内层不是 JSON 时抛出缺行错误", () => {
|
test("text 内层不是 JSON 时抛出缺行错误", () => {
|
||||||
// 无契约时期模型返回 Markdown 表格的真实场景。
|
// 无契约时期模型返回 Markdown 表格的真实场景。
|
||||||
const task = storyboardTask(JSON.stringify({ mode: "text", text: "| 7-41 | 全景/拉远 | 桃园结义 |" }));
|
const task = storyboardTask(JSON.stringify({ mode: "text", text: "| 7-41 | 全景/拉远 | 桃园结义 |" }));
|
||||||
|
|||||||
Reference in New Issue
Block a user