@xiaohhhh1/canvas-agent 0.4.49 → 0.4.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent/codex.d.ts
CHANGED
|
@@ -17,7 +17,18 @@ export type CodexRunResult = {
|
|
|
17
17
|
ok: false;
|
|
18
18
|
error: string;
|
|
19
19
|
};
|
|
20
|
-
export type CodexWorkflowRunResult =
|
|
20
|
+
export type CodexWorkflowRunResult = {
|
|
21
|
+
ok: true;
|
|
22
|
+
text: string;
|
|
23
|
+
timings: {
|
|
24
|
+
queueWaitMs: number;
|
|
25
|
+
threadStartMs: number;
|
|
26
|
+
modelMs: number;
|
|
27
|
+
};
|
|
28
|
+
} | {
|
|
29
|
+
ok: false;
|
|
30
|
+
error: string;
|
|
31
|
+
retryable: boolean;
|
|
21
32
|
timings: {
|
|
22
33
|
queueWaitMs: number;
|
|
23
34
|
threadStartMs: number;
|
|
@@ -45,6 +56,8 @@ export declare function runCodexWorkflowTurn(prompt: string, emit: AgentEmit, op
|
|
|
45
56
|
onWorkerStart?: () => void;
|
|
46
57
|
onWorkerFinish?: () => void;
|
|
47
58
|
}): Promise<CodexWorkflowRunResult>;
|
|
59
|
+
/** Contract/preflight 4xx errors are deterministic and must never burn every fallback chunk size. */
|
|
60
|
+
export declare function isDeterministicWorkflowContractError(error: unknown): boolean;
|
|
48
61
|
/** 强制回收失去响应的 app-server;只用于有界超时恢复。 */
|
|
49
62
|
export declare function restartCodexApp(message?: string): Promise<void>;
|
|
50
63
|
/** 回复当前 app-server 的待处理权限请求。 */
|
package/dist/agent/codex.js
CHANGED
|
@@ -37,7 +37,7 @@ export async function interruptCodexTurn(threadId) {
|
|
|
37
37
|
*/
|
|
38
38
|
export async function runCodexWorkflowTurn(prompt, emit, options) {
|
|
39
39
|
if (!prompt.trim())
|
|
40
|
-
return { ok: false, error: "Codex prompt is empty", timings: { queueWaitMs: 0, threadStartMs: 0, modelMs: 0 } };
|
|
40
|
+
return { ok: false, error: "Codex prompt is empty", retryable: false, timings: { queueWaitMs: 0, threadStartMs: 0, modelMs: 0 } };
|
|
41
41
|
return await workflowCodexPool.run(async (workerIndex, queueWaitMs) => {
|
|
42
42
|
options.onWorkerStart?.();
|
|
43
43
|
const modelSettings = { model: FLOW_C_CODEX_MODEL, reasoningEffort: FLOW_C_CODEX_REASONING_EFFORT };
|
|
@@ -61,7 +61,7 @@ export async function runCodexWorkflowTurn(prompt, emit, options) {
|
|
|
61
61
|
workflowCodexApps.delete(workerIndex);
|
|
62
62
|
await app.terminate("Flow C 脚本回合超时,已仅回收当前 worker");
|
|
63
63
|
await turn.catch(() => undefined);
|
|
64
|
-
return { ok: false, error: "Flow C 脚本回合超过 8 分钟,已自动终止当前 worker", timings: { queueWaitMs, threadStartMs, modelMs: Date.now() - modelStartedAt } };
|
|
64
|
+
return { ok: false, error: "Flow C 脚本回合超过 8 分钟,已自动终止当前 worker", retryable: true, timings: { queueWaitMs, threadStartMs, modelMs: Date.now() - modelStartedAt } };
|
|
65
65
|
}
|
|
66
66
|
return { ok: true, text: result, timings: { queueWaitMs, threadStartMs, modelMs: Date.now() - modelStartedAt } };
|
|
67
67
|
}
|
|
@@ -69,7 +69,7 @@ export async function runCodexWorkflowTurn(prompt, emit, options) {
|
|
|
69
69
|
logger.error("Flow C Codex worker failed", { workerIndex, error });
|
|
70
70
|
const message = errorMessage(error);
|
|
71
71
|
emit("agent_error", { message });
|
|
72
|
-
return { ok: false, error: message, timings: { queueWaitMs, threadStartMs: threadStartMs || Date.now() - threadStartedAt, modelMs: modelStartedAt ? Date.now() - modelStartedAt : 0 } };
|
|
72
|
+
return { ok: false, error: message, retryable: !isDeterministicWorkflowContractError(message), timings: { queueWaitMs, threadStartMs: threadStartMs || Date.now() - threadStartedAt, modelMs: modelStartedAt ? Date.now() - modelStartedAt : 0 } };
|
|
73
73
|
}
|
|
74
74
|
finally {
|
|
75
75
|
if (timer)
|
|
@@ -78,6 +78,11 @@ export async function runCodexWorkflowTurn(prompt, emit, options) {
|
|
|
78
78
|
}
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
|
+
/** Contract/preflight 4xx errors are deterministic and must never burn every fallback chunk size. */
|
|
82
|
+
export function isDeterministicWorkflowContractError(error) {
|
|
83
|
+
const message = errorMessage(error);
|
|
84
|
+
return /invalid_json_schema|invalid schema for response_format|text\.format\.schema|response[_ ]format[^\n]*(?:invalid|schema)/i.test(message);
|
|
85
|
+
}
|
|
81
86
|
async function startWorkflowCodexApp(workerIndex, emit) {
|
|
82
87
|
let started;
|
|
83
88
|
started = await CodexAppClient.start(emit, () => {
|
|
@@ -30,13 +30,14 @@ type ReferenceStyleCard = {
|
|
|
30
30
|
voiceTone?: unknown;
|
|
31
31
|
categoryFit?: unknown;
|
|
32
32
|
};
|
|
33
|
+
type StoryboardLayoutVersion = "director-table-scripted-v1" | "hybrid-three-anchor-v1" | "legacy-five-row-v1";
|
|
33
34
|
type ScriptTask = {
|
|
34
35
|
id: string;
|
|
35
36
|
workflow: "flow-c";
|
|
36
37
|
market: string;
|
|
37
38
|
duration_seconds?: 10 | 20 | 30;
|
|
38
|
-
storyboard_layout_version?:
|
|
39
|
-
storyboardLayoutVersion?:
|
|
39
|
+
storyboard_layout_version?: StoryboardLayoutVersion;
|
|
40
|
+
storyboardLayoutVersion?: StoryboardLayoutVersion;
|
|
40
41
|
reference_style_card?: ReferenceStyleCard;
|
|
41
42
|
referenceStyleCard?: ReferenceStyleCard;
|
|
42
43
|
requested_count: number;
|
|
@@ -229,6 +230,11 @@ export declare class WorkflowManager {
|
|
|
229
230
|
private save;
|
|
230
231
|
}
|
|
231
232
|
export declare function compareScriptQueueRecords(left: Pick<ScriptRecord, "priorityAt" | "updatedAt">, right: Pick<ScriptRecord, "priorityAt" | "updatedAt">): number;
|
|
233
|
+
/** A deterministic response-format failure stops fallback isolation immediately. */
|
|
234
|
+
export declare function terminalScriptChunkError(results: Array<{
|
|
235
|
+
error?: string;
|
|
236
|
+
terminal?: boolean;
|
|
237
|
+
}>): string;
|
|
232
238
|
export declare function scriptChunkPrompt(id: string, task: ScriptTask, ordinals: number[]): string;
|
|
233
239
|
export declare function missingOrdinals(total: number, received: number[]): number[];
|
|
234
240
|
export {};
|
package/dist/workflow/manager.js
CHANGED
|
@@ -239,6 +239,10 @@ export class WorkflowManager {
|
|
|
239
239
|
const workspace = ensureSiteWorkspace(this.config);
|
|
240
240
|
const durationSeconds = Number(task.duration_seconds || 10);
|
|
241
241
|
const chunkSizes = flowCScriptChunkSizes(durationSeconds);
|
|
242
|
+
// Fail locally before starting any worker if a future schema edit
|
|
243
|
+
// violates strict response-format invariants.
|
|
244
|
+
for (const chunkSize of new Set(chunkSizes))
|
|
245
|
+
flowCScriptOutputSchema(durationSeconds, chunkSize);
|
|
242
246
|
record.activeChunks = 0;
|
|
243
247
|
let chunkSizeIndex = 0;
|
|
244
248
|
while (record.receivedOrdinals.length < task.requested_count) {
|
|
@@ -253,6 +257,10 @@ export class WorkflowManager {
|
|
|
253
257
|
this.save();
|
|
254
258
|
const results = await Promise.all(chunks.map((ordinals) => this.runScriptChunk(id, task, ordinals, workspace.workspacePath)));
|
|
255
259
|
await this.scriptTask(id);
|
|
260
|
+
const terminalError = terminalScriptChunkError(results);
|
|
261
|
+
if (terminalError) {
|
|
262
|
+
throw new Error(`本机脚本结构化契约被 Codex 拒绝,已停止自动重试且未提交缺失脚本。请先升级或修复 Canvas Agent,再手动重试(${terminalError})`);
|
|
263
|
+
}
|
|
256
264
|
const progressed = record.receivedOrdinals.length > before;
|
|
257
265
|
if (progressed) {
|
|
258
266
|
chunkSizeIndex = 0;
|
|
@@ -304,7 +312,7 @@ export class WorkflowManager {
|
|
|
304
312
|
this.emitScriptStage(id, ordinals, "thread_start", result.timings.threadStartMs);
|
|
305
313
|
this.emitScriptStage(id, ordinals, "model", result.timings.modelMs);
|
|
306
314
|
if (!result.ok || !result.text)
|
|
307
|
-
return { error: result.ok ? "Codex 未返回可用的结构化脚本" : result.error };
|
|
315
|
+
return { error: result.ok ? "Codex 未返回可用的结构化脚本" : result.error, terminal: !result.ok && !result.retryable };
|
|
308
316
|
try {
|
|
309
317
|
const parseStartedAt = Date.now();
|
|
310
318
|
const jobs = parseFlowCScriptOutput(result.text, ordinals);
|
|
@@ -312,10 +320,10 @@ export class WorkflowManager {
|
|
|
312
320
|
const persistStartedAt = Date.now();
|
|
313
321
|
await this.submitGeneratedScriptJobs(id, jobs);
|
|
314
322
|
this.emitScriptStage(id, ordinals, "persist", Date.now() - persistStartedAt);
|
|
315
|
-
return {};
|
|
323
|
+
return { terminal: false };
|
|
316
324
|
}
|
|
317
325
|
catch (error) {
|
|
318
|
-
return { error: error instanceof Error ? `结构化脚本未通过本机校验:${error.message}` : "结构化脚本未通过本机校验" };
|
|
326
|
+
return { error: error instanceof Error ? `结构化脚本未通过本机校验:${error.message}` : "结构化脚本未通过本机校验", terminal: false };
|
|
319
327
|
}
|
|
320
328
|
}
|
|
321
329
|
/** 只记录阶段、ordinal 和毫秒数;禁止记录 prompt、令牌或中心能力。 */
|
|
@@ -487,11 +495,15 @@ export function compareScriptQueueRecords(left, right) {
|
|
|
487
495
|
return String(right.priorityAt || "").localeCompare(String(left.priorityAt || ""));
|
|
488
496
|
return left.updatedAt.localeCompare(right.updatedAt);
|
|
489
497
|
}
|
|
498
|
+
/** A deterministic response-format failure stops fallback isolation immediately. */
|
|
499
|
+
export function terminalScriptChunkError(results) {
|
|
500
|
+
return results.find((result) => result.terminal)?.error || "";
|
|
501
|
+
}
|
|
490
502
|
class ExpiredCapabilityError extends Error {
|
|
491
503
|
}
|
|
492
504
|
export function scriptChunkPrompt(id, task, ordinals) {
|
|
493
505
|
const duration = Number(task.duration_seconds || 10);
|
|
494
|
-
const layoutVersion = task.storyboard_layout_version || task.storyboardLayoutVersion || "
|
|
506
|
+
const layoutVersion = task.storyboard_layout_version || task.storyboardLayoutVersion || "director-table-scripted-v1";
|
|
495
507
|
const styleCard = compactReferenceStyleCard(task.reference_style_card || task.referenceStyleCard);
|
|
496
508
|
const durationRules = duration === 10 ? `
|
|
497
509
|
本任务每条成片为 10 秒,不生成 masterScript。每条只输出一份 openingState 和一个完整 0–10 秒 segment;Agent 会从 structured shots 确定性渲染兼容顶层 script,并按 shots 顺序派生 segmentVoiceovers,模型不要生成这两个派生字段。` : `
|
|
@@ -500,10 +512,11 @@ export function scriptChunkPrompt(id, task, ordinals) {
|
|
|
500
512
|
return `你正在后台处理“抖音小辉跨境工具”Flow C 脚本交接 ${id}。
|
|
501
513
|
完整中心任务已经附在本提示词末尾。只创作 ordinal ${ordinals[0]} 到 ${ordinals.at(-1)}(精确列表:${ordinals.join(", ")})。
|
|
502
514
|
本段 productIndex 必须严格按此映射填写:${scriptProductAssignments(task.product_quantities, ordinals)}。不得凭产品名称猜测或把相邻产品编号混用。
|
|
503
|
-
脚本质量绝不能因批量而降低:每条都必须独立构思、完整、真实合规,严格遵守任务 instructions、产品图片顺序和 productIndex;使用目标市场 ${task.market}
|
|
515
|
+
脚本质量绝不能因批量而降低:每条都必须独立构思、完整、真实合规,严格遵守任务 instructions、产品图片顺序和 productIndex;使用目标市场 ${task.market} 的自然本地口播与偏快但清晰的短视频节奏。${durationRules}
|
|
504
516
|
每条先按“画面先行”写 creativePlan,再写本段/总脚本。creativePlan 必须给出 visualHook、conflict、productIntervention、visibleProof、callbackMotivation、truthBoundary、differentiationKey,并在 escalation 或 turn 中至少给出一项真实成立的升级/转折;format 必须依据当前产品、市场、事实证据和真实场景适配选择。中国短剧式冲突/喜剧只能是适合时的实验路线,绝不是默认;工厂、仓库、超市等条件场景也只有在商品动作自然且不伪造来源、库存或销量证据时才可选,不能固定轮换。
|
|
505
517
|
styleCardSummary 只使用已缓存的压缩风格卡,没有就明确写“无”;不要分析、读取或转录原始参考视频。每条只生成一份 voiceProfile(性别、年龄感、音高、音质、语速、口音、停顿习惯、情绪基调),所有局部段共同引用它;每段仅用简短 voiceCue 表达情绪变化,不复写整套音色。
|
|
506
|
-
每个 segment 必须有完整 0–10 秒 shots、voiceCue 和 endingState;不要让模型生成 continuityMode 或 continuity
|
|
518
|
+
每个 segment 必须有完整 0–10 秒 shots、voiceCue 和 endingState;不要让模型生成 continuityMode 或 continuity。镜头数由本条剧情实际需要决定(契约允许 1–8 个),不得为了故事板版式强行固定为三个或五个。每个 shot 都包含画面、准确本地口播、可控屏幕字、商品证据、soundBgm 与 emotionalNote。qualityGate 必须逐项用具体证据核对强钩子、冲突、升级/转折、商品介入、可见证明、开头回扣购买动机、事实边界与批内差异,禁止用“已满足”空话。
|
|
519
|
+
【回传语言契约】为了让导演故事板稳定可读,creativePlan、masterScript 中的导演/剧情说明、voiceProfile、qualityGate、openingState、voiceCue、endingState,以及每个 shot 的 visual、evidence、soundBgm、emotionalNote 必须用清晰简洁的制作英文。每个 shot.voiceover 必须逐字使用目标市场 ${task.market} 的自然当地语言,不得翻译成英文;onScreenText 若画面确实需要展示文字,则写目标市场当地语言的准确短文案,否则写英文 NONE。不得在英文导演字段里混入另一份口播或全局时轴。
|
|
507
520
|
每条 differentiationKey 必须在带货形式、第一眼画面、剧情骨架、痛点/反差、商品证明和口播气质上与本批其他条目显著不同,不能只换措辞。
|
|
508
521
|
本脚本回合固定使用 GPT-5.6 Terra 高推理;不要建议换低档模型或降低推理。故事版布局由中心顶层 storyboardLayoutVersion=${layoutVersion} 管理,不要让模型选择,也不要把它重复进每条 job。
|
|
509
522
|
【已缓存压缩的参考 style card】${styleCard || "无参考风格卡"}【style card 结束】
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
type JsonSchema = Record<string, unknown>;
|
|
2
2
|
/** Codex app-server 的结构化脚本返回契约;回传由 Agent 完成,不依赖模型调用 MCP。 */
|
|
3
3
|
export declare function flowCScriptOutputSchema(durationSeconds: 10 | 20 | 30, count: number): JsonSchema;
|
|
4
|
+
/**
|
|
5
|
+
* OpenAI strict structured output requires every declared object property to
|
|
6
|
+
* appear in `required`. Optional semantics must therefore be represented by a
|
|
7
|
+
* required nullable field, never by omitting that key from `required`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function assertStrictResponseSchema(schemaValue: unknown, path?: string): asserts schemaValue is JsonSchema;
|
|
4
10
|
export declare function parseFlowCScriptOutput(value: string, expectedOrdinals: number[]): unknown[];
|
|
5
11
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const text = { type: "string", minLength: 1 };
|
|
2
|
+
const nullableText = { anyOf: [text, { type: "null" }] };
|
|
2
3
|
const continuityFields = ["character", "wardrobe", "location", "lighting", "productState", "unfinishedAction", "nextGoal"];
|
|
3
4
|
function object(properties, required = Object.keys(properties)) {
|
|
4
5
|
return { type: "object", properties, required, additionalProperties: false };
|
|
@@ -26,16 +27,16 @@ function creativePlanSchema() {
|
|
|
26
27
|
format: text,
|
|
27
28
|
visualHook: text,
|
|
28
29
|
conflict: text,
|
|
29
|
-
escalation:
|
|
30
|
-
turn:
|
|
30
|
+
escalation: nullableText,
|
|
31
|
+
turn: nullableText,
|
|
31
32
|
productIntervention: text,
|
|
32
33
|
visibleProof: text,
|
|
33
34
|
callbackMotivation: text,
|
|
34
35
|
truthBoundary: text,
|
|
35
36
|
differentiationKey: text,
|
|
36
|
-
styleCardSummary:
|
|
37
|
+
styleCardSummary: nullableText,
|
|
37
38
|
};
|
|
38
|
-
return object(properties
|
|
39
|
+
return object(properties);
|
|
39
40
|
}
|
|
40
41
|
function qualityGateSchema() {
|
|
41
42
|
return object({
|
|
@@ -87,7 +88,40 @@ export function flowCScriptOutputSchema(durationSeconds, count) {
|
|
|
87
88
|
properties.masterScript = { type: "string", minLength: 40 };
|
|
88
89
|
properties.segments = { type: "array", minItems: durationSeconds / 10, maxItems: durationSeconds / 10, items: segmentSchema() };
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
+
const schema = object({ jobs: { type: "array", minItems: count, maxItems: count, items: object(properties) } });
|
|
92
|
+
assertStrictResponseSchema(schema);
|
|
93
|
+
return schema;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* OpenAI strict structured output requires every declared object property to
|
|
97
|
+
* appear in `required`. Optional semantics must therefore be represented by a
|
|
98
|
+
* required nullable field, never by omitting that key from `required`.
|
|
99
|
+
*/
|
|
100
|
+
export function assertStrictResponseSchema(schemaValue, path = "$") {
|
|
101
|
+
const schema = recordOf(schemaValue);
|
|
102
|
+
if (!schema)
|
|
103
|
+
throw new Error(`Strict response schema at ${path} must be an object`);
|
|
104
|
+
if (schema.type === "object") {
|
|
105
|
+
const properties = recordOf(schema.properties);
|
|
106
|
+
if (!properties)
|
|
107
|
+
throw new Error(`Strict response schema object at ${path} needs properties`);
|
|
108
|
+
const keys = Object.keys(properties).sort();
|
|
109
|
+
const required = Array.isArray(schema.required) ? schema.required.map(String).sort() : [];
|
|
110
|
+
if (keys.length !== required.length || keys.some((key, index) => key !== required[index])) {
|
|
111
|
+
throw new Error(`Strict response schema object at ${path} must require every property`);
|
|
112
|
+
}
|
|
113
|
+
if (schema.additionalProperties !== false)
|
|
114
|
+
throw new Error(`Strict response schema object at ${path} must disable additional properties`);
|
|
115
|
+
for (const [key, child] of Object.entries(properties))
|
|
116
|
+
assertStrictResponseSchema(child, `${path}.properties.${key}`);
|
|
117
|
+
}
|
|
118
|
+
if (schema.items)
|
|
119
|
+
assertStrictResponseSchema(schema.items, `${path}.items`);
|
|
120
|
+
for (const branchKey of ["anyOf", "oneOf", "allOf"]) {
|
|
121
|
+
const branches = schema[branchKey];
|
|
122
|
+
if (Array.isArray(branches))
|
|
123
|
+
branches.forEach((branch, index) => assertStrictResponseSchema(branch, `${path}.${branchKey}[${index}]`));
|
|
124
|
+
}
|
|
91
125
|
}
|
|
92
126
|
export function parseFlowCScriptOutput(value, expectedOrdinals) {
|
|
93
127
|
const source = String(value || "").trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
|
|
@@ -147,8 +181,8 @@ function segmentVoiceoverLines(segmentValue) {
|
|
|
147
181
|
return shots.map((shot) => String(shot.voiceover || "").trim()).filter((line) => line && !/^(none|无|sin voz|sin diálogo)$/i.test(line));
|
|
148
182
|
}
|
|
149
183
|
function continuityFromOpeningState(openingState) {
|
|
150
|
-
const continuity = Object.fromEntries(continuityFields.map((field) => [field, openingState?.[field] || "
|
|
151
|
-
continuity.previousEndingFrame = openingState?.openingFrame || "
|
|
184
|
+
const continuity = Object.fromEntries(continuityFields.map((field) => [field, openingState?.[field] || "Established by the opening shot"]));
|
|
185
|
+
continuity.previousEndingFrame = openingState?.openingFrame || "Opening shot of this segment";
|
|
152
186
|
return continuity;
|
|
153
187
|
}
|
|
154
188
|
/**
|
|
@@ -163,11 +197,11 @@ function withLegacySegmentScript(segmentValue, segmentIndex, segmentCount, voice
|
|
|
163
197
|
const shots = Array.isArray(segment?.shots) ? segment.shots.map(recordOf).filter((shot) => Boolean(shot)) : [];
|
|
164
198
|
if (!segment || !continuity || !endingState || !shots.length)
|
|
165
199
|
return segmentValue;
|
|
166
|
-
const context =
|
|
167
|
-
const ending =
|
|
168
|
-
const voice = voiceProfile ?
|
|
169
|
-
const timeline = shots.map((shot, index) =>
|
|
170
|
-
return { ...segment, script: `
|
|
200
|
+
const context = `CHARACTER: ${continuity.character}; WARDROBE: ${continuity.wardrobe}; LOCATION: ${continuity.location}; LIGHTING: ${continuity.lighting}; PRODUCT STATE: ${continuity.productState}; INHERITED ACTION: ${continuity.unfinishedAction}; SEGMENT GOAL: ${continuity.nextGoal}; PREVIOUS ENDING FRAME: ${continuity.previousEndingFrame}`;
|
|
201
|
+
const ending = `CHARACTER: ${endingState.character}; WARDROBE: ${endingState.wardrobe}; LOCATION: ${endingState.location}; LIGHTING: ${endingState.lighting}; PRODUCT STATE: ${endingState.productState}; UNFINISHED ACTION: ${endingState.unfinishedAction}; NEXT GOAL: ${endingState.nextGoal}; ENDING FRAME: ${endingState.endingFrame}`;
|
|
202
|
+
const voice = voiceProfile ? `GENDER: ${voiceProfile.gender}; AGE IMPRESSION: ${voiceProfile.ageImpression}; PITCH: ${voiceProfile.pitch}; TIMBRE: ${voiceProfile.timbre}; SPEAKING RATE: ${voiceProfile.speakingRate}; ACCENT: ${voiceProfile.accent}; PAUSE HABIT: ${voiceProfile.pauseHabit}; EMOTIONAL BASELINE: ${voiceProfile.emotionalBaseline}` : "Use the shared voice profile for this video";
|
|
203
|
+
const timeline = shots.map((shot, index) => `SHOT ${index + 1} | ${shot.startSeconds}-${shot.endSeconds}s | VISUAL: ${shot.visual} | VO: ${shot.voiceover} | ON-SCREEN TEXT: ${shot.onScreenText} | EVIDENCE: ${shot.evidence} | SOUND/BGM: ${shot.soundBgm} | EMOTION: ${shot.emotionalNote}`).join("\n");
|
|
204
|
+
return { ...segment, script: `FLOW C INDEPENDENT SEGMENT ${segmentIndex + 1}/${segmentCount}\nLOCAL TIMELINE: 0-10 seconds only. Never reference or draw a full-video timeline.\nVOICE PROFILE: ${voice}\nSEGMENT VOICE CUE: ${segment.voiceCue}\nOPENING CONTINUITY: ${context}\n${timeline}\nENDING STATE: ${ending}` };
|
|
171
205
|
}
|
|
172
206
|
function recordOf(value) {
|
|
173
207
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|