@xiaohhhh1/canvas-agent 0.4.52 → 0.4.53
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.
|
@@ -137,7 +137,9 @@ export function localVideoAnalysisPrompt(source, durationMs, transcript, attachm
|
|
|
137
137
|
`3. segments 每段必须写 visual;若该段有人声,spokenText 必须概述口播;可见字幕写 onScreenText。画面、口播或字幕缺失就写 null。没有听觉音轨输入,audio 必须写 null,不得猜音乐、语气或音效。\n` +
|
|
138
138
|
`4. 必须先判断整条视频的主带货形式 primary,只能选 factory-demo/comedy/mini-drama/ugc-testimonial/review-demo/tutorial/problem-solution/price-shock/unboxing/expert-explainer/lifestyle/live-cut/comparison/other。secondary 可选其他辅助形式。不能把 Hook 或实验假设冒充带货形式。\n` +
|
|
139
139
|
`5. 0-3 秒至少按 500ms 证据判断;后续按镜头变化。所有机制、带货形式和“为什么可能爆”的假设都必须引用真实 startMs/endMs。榜单指标只是相关性,不是因果。\n` +
|
|
140
|
-
`6. 目标是学习、融合、进化:只提炼可迁移机制,不复制原文案、人物、视觉资产或连续镜头顺序。\n
|
|
140
|
+
`6. 目标是学习、融合、进化:只提炼可迁移机制,不复制原文案、人物、视觉资产或连续镜头顺序。\n` +
|
|
141
|
+
`7. 必须单独输出 creativeMutationProfile,识别这条视频怎样把普通商品变成反常但现实可拍的完整画面事件。mutationAxes 是开放维度,不是固定模板轮换;从实际证据判断人物身份/关系、场景错位、规模/数量、群体反应、角色反转、构图视角、感官材质、当地行为/审美和真实商品证明,也允许发现新的维度。\n` +
|
|
142
|
+
`8. culturalSignals 必须引用画面时间证据,说明当地行为、环境、幽默、地位符号、色彩/画面密度或消费场景怎样参与叙事,并写清迁移条件和避免刻板印象的方法。只出现当地字幕、旗帜或民族服装不等于理解了文化。\n\n` +
|
|
141
143
|
`样本:${JSON.stringify({
|
|
142
144
|
sourceUrl: source.sourceUrl || source.source_url,
|
|
143
145
|
platformVideoId: source.platformVideoId || source.platform_video_id,
|
|
@@ -151,11 +153,12 @@ export function localVideoAnalysisPrompt(source, durationMs, transcript, attachm
|
|
|
151
153
|
durationMs,
|
|
152
154
|
})}\n\n` +
|
|
153
155
|
`带时间码的口播字幕:\n${transcript}\n\n` +
|
|
154
|
-
`只返回一个 JSON 对象,不要 Markdown。字段必须是:schemaVersion="commerce-video-intelligence-
|
|
156
|
+
`只返回一个 JSON 对象,不要 Markdown。字段必须是:schemaVersion="commerce-video-intelligence-v3";durationMs;language;summary;` +
|
|
155
157
|
`sellingFormat{primary,label,secondary,rationale,evidence[{startMs,endMs,observation}],confidence};` +
|
|
156
158
|
`hook{startMs,endMs,visual,spokenText,onScreenText,patternInterrupt,openLoop};productFirstSeenMs;` +
|
|
157
159
|
`segments[{startMs,endMs,role,visual,spokenText,onScreenText,audio,editing,confidence}](至少2段);` +
|
|
158
|
-
`
|
|
160
|
+
`creativeMutationProfile{coreVisualPremise,mutationAxes[至少2项],culturalSignals[{market,signal,narrativeRole,aestheticRole,transferConditions,avoidStereotype,evidence[{startMs,endMs,observation}]}],firstFrameComposition,spectacleMechanism,productIntegration,transferableRule,culturalLimitations,doNotCopy,evidence[{startMs,endMs,observation}],confidence};` +
|
|
161
|
+
`mechanisms[{type,label,mechanism,whyItMayWork,evidence[{startMs,endMs,observation}],confidence,replicationRisk}](type 仅 hook/conflict/proof/pacing/trust/product-reveal/offer-framing/cta/audio/visual-grammar/environment/visual-mutation/culture-code/spectacle);` +
|
|
159
162
|
`viralHypotheses[{hypothesis,supportingMetrics,supportingEvidence[{startMs,endMs,observation}],confounders,confidence}];` +
|
|
160
163
|
`nonReplicableFactors;complianceRisks;originalityGuidance{preserveMechanisms,mustRewrite,forbiddenCopying}。`;
|
|
161
164
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
type JsonSchema = Record<string, unknown>;
|
|
2
|
+
export declare const FLOW_C_CREATIVE_CANDIDATE_CONTRACT_VERSION = "flow-c-creative-candidates-v1";
|
|
3
|
+
export declare function flowCCreativeCandidateOutputSchema(count: number): JsonSchema;
|
|
4
|
+
export declare function parseFlowCCreativeCandidateOutput(value: string, expectedOrdinals: number[]): Record<string, unknown>[];
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { assertStrictResponseSchema } from './script-output.js';
|
|
2
|
+
export const FLOW_C_CREATIVE_CANDIDATE_CONTRACT_VERSION = 'flow-c-creative-candidates-v1';
|
|
3
|
+
const text = { type: 'string', minLength: 1 };
|
|
4
|
+
function object(properties) {
|
|
5
|
+
return { type: 'object', properties, required: Object.keys(properties), additionalProperties: false };
|
|
6
|
+
}
|
|
7
|
+
function candidateSchema() {
|
|
8
|
+
return object({
|
|
9
|
+
visualPremise: text,
|
|
10
|
+
mutationAxes: { type: 'array', minItems: 2, maxItems: 4, items: text },
|
|
11
|
+
culturalAnchors: {
|
|
12
|
+
type: 'array', minItems: 0, maxItems: 3,
|
|
13
|
+
items: object({ signal: text, narrativeRole: text, aestheticRole: text, source: { type: 'string', enum: ['learned-style-card', 'product-market-evidence', 'conservative-everyday-detail'] }, evidence: text, avoidStereotype: text }),
|
|
14
|
+
},
|
|
15
|
+
spectacleEscalation: text,
|
|
16
|
+
productProofAction: text,
|
|
17
|
+
truthBoundary: text,
|
|
18
|
+
riskFlags: { type: 'array', minItems: 0, maxItems: 8, items: text },
|
|
19
|
+
firstFrame: text,
|
|
20
|
+
creativeFingerprint: object({
|
|
21
|
+
characterRelation: text,
|
|
22
|
+
scene: text,
|
|
23
|
+
firstFrameComposition: text,
|
|
24
|
+
spectacleMechanism: text,
|
|
25
|
+
proofMethod: text,
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function flowCCreativeCandidateOutputSchema(count) {
|
|
30
|
+
const schema = object({
|
|
31
|
+
contractVersion: { type: 'string', enum: [FLOW_C_CREATIVE_CANDIDATE_CONTRACT_VERSION] },
|
|
32
|
+
groups: {
|
|
33
|
+
type: 'array', minItems: count, maxItems: count,
|
|
34
|
+
items: object({
|
|
35
|
+
ordinal: { type: 'integer', minimum: 1 },
|
|
36
|
+
productIndex: { type: 'integer', minimum: 0 },
|
|
37
|
+
candidates: { type: 'array', minItems: 6, maxItems: 10, items: candidateSchema() },
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
assertStrictResponseSchema(schema);
|
|
42
|
+
return schema;
|
|
43
|
+
}
|
|
44
|
+
export function parseFlowCCreativeCandidateOutput(value, expectedOrdinals) {
|
|
45
|
+
const source = String(value || '').trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
|
|
46
|
+
const parsed = JSON.parse(source);
|
|
47
|
+
if (parsed.contractVersion !== FLOW_C_CREATIVE_CANDIDATE_CONTRACT_VERSION)
|
|
48
|
+
throw new Error('Creative candidate contract version does not match the current Agent');
|
|
49
|
+
if (!Array.isArray(parsed.groups))
|
|
50
|
+
throw new Error('Codex did not return creative candidate groups');
|
|
51
|
+
const expected = new Set(expectedOrdinals);
|
|
52
|
+
const accepted = new Map();
|
|
53
|
+
for (const value of parsed.groups) {
|
|
54
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
55
|
+
continue;
|
|
56
|
+
const group = value;
|
|
57
|
+
const ordinal = Number(group.ordinal);
|
|
58
|
+
if (!expected.has(ordinal) || accepted.has(ordinal) || !Array.isArray(group.candidates) || group.candidates.length < 6 || group.candidates.length > 10)
|
|
59
|
+
continue;
|
|
60
|
+
accepted.set(ordinal, group);
|
|
61
|
+
}
|
|
62
|
+
if (accepted.size !== expectedOrdinals.length)
|
|
63
|
+
throw new Error('Creative candidate ordinals or counts do not match the current chunk');
|
|
64
|
+
return expectedOrdinals.map((ordinal) => accepted.get(ordinal));
|
|
65
|
+
}
|
|
@@ -20,6 +20,11 @@ type ScriptRecord = {
|
|
|
20
20
|
updatedAt: string;
|
|
21
21
|
};
|
|
22
22
|
type ReferenceStyleCard = {
|
|
23
|
+
visualPremisePattern?: unknown;
|
|
24
|
+
mutationAxes?: unknown;
|
|
25
|
+
culturePattern?: unknown;
|
|
26
|
+
spectaclePattern?: unknown;
|
|
27
|
+
productIntegrationPattern?: unknown;
|
|
23
28
|
openingComposition?: unknown;
|
|
24
29
|
visualDensity?: unknown;
|
|
25
30
|
conflictContrast?: unknown;
|
|
@@ -29,8 +34,38 @@ type ReferenceStyleCard = {
|
|
|
29
34
|
proofMethod?: unknown;
|
|
30
35
|
voiceTone?: unknown;
|
|
31
36
|
categoryFit?: unknown;
|
|
37
|
+
applicability?: {
|
|
38
|
+
markets?: unknown;
|
|
39
|
+
categories?: unknown;
|
|
40
|
+
};
|
|
32
41
|
};
|
|
33
42
|
type StoryboardLayoutVersion = "director-table-scripted-v1" | "hybrid-three-anchor-v1" | "legacy-five-row-v1";
|
|
43
|
+
type ProductInput = {
|
|
44
|
+
productIndex: number;
|
|
45
|
+
title: string;
|
|
46
|
+
category?: string;
|
|
47
|
+
quantity: number;
|
|
48
|
+
sellingForm?: string;
|
|
49
|
+
creativeBrief?: string;
|
|
50
|
+
productImageUrlsInExactOrder?: string[];
|
|
51
|
+
};
|
|
52
|
+
type SelectedCandidate = {
|
|
53
|
+
ordinal: number;
|
|
54
|
+
productIndex: number;
|
|
55
|
+
visualPremise: string;
|
|
56
|
+
mutationAxes: string[];
|
|
57
|
+
culturalAnchors: Array<Record<string, unknown>>;
|
|
58
|
+
spectacleEscalation: string;
|
|
59
|
+
productProofAction: string;
|
|
60
|
+
truthBoundary: string;
|
|
61
|
+
riskFlags: string[];
|
|
62
|
+
firstFrame: string;
|
|
63
|
+
creativeFingerprint: Record<string, string>;
|
|
64
|
+
fingerprintKey?: string;
|
|
65
|
+
selectionScore?: number;
|
|
66
|
+
selectionBreakdown?: Record<string, number>;
|
|
67
|
+
selectionMode?: string;
|
|
68
|
+
};
|
|
34
69
|
type ScriptTask = {
|
|
35
70
|
id: string;
|
|
36
71
|
workflow: "flow-c";
|
|
@@ -40,6 +75,14 @@ type ScriptTask = {
|
|
|
40
75
|
storyboardLayoutVersion?: StoryboardLayoutVersion;
|
|
41
76
|
reference_style_card?: ReferenceStyleCard;
|
|
42
77
|
referenceStyleCard?: ReferenceStyleCard;
|
|
78
|
+
product_inputs?: ProductInput[];
|
|
79
|
+
reference_style_cards?: ReferenceStyleCard[];
|
|
80
|
+
selected_candidates?: SelectedCandidate[];
|
|
81
|
+
creative_fingerprint_ledger?: Array<Record<string, unknown>>;
|
|
82
|
+
candidate_stage?: {
|
|
83
|
+
selected?: number;
|
|
84
|
+
requested?: number;
|
|
85
|
+
};
|
|
43
86
|
requested_count: number;
|
|
44
87
|
product_quantities: number[];
|
|
45
88
|
instructions: string;
|
|
@@ -112,6 +155,14 @@ export declare class WorkflowManager {
|
|
|
112
155
|
} & {
|
|
113
156
|
error?: string;
|
|
114
157
|
}>;
|
|
158
|
+
submitCandidateChunk(idValue: unknown, groupsValue: unknown): Promise<{
|
|
159
|
+
accepted: number;
|
|
160
|
+
selected: number;
|
|
161
|
+
requestedCount: number;
|
|
162
|
+
selectedCandidates: SelectedCandidate[];
|
|
163
|
+
} & {
|
|
164
|
+
error?: string;
|
|
165
|
+
}>;
|
|
115
166
|
downloadState(): {
|
|
116
167
|
configured: boolean;
|
|
117
168
|
directoryName: string | undefined;
|
|
@@ -213,6 +264,8 @@ export declare class WorkflowManager {
|
|
|
213
264
|
private pumpScriptQueue;
|
|
214
265
|
private finishDownloadDirectorySelection;
|
|
215
266
|
private runScript;
|
|
267
|
+
private ensureCreativeCandidateSelections;
|
|
268
|
+
private runCandidateChunk;
|
|
216
269
|
/** 一个 chunk 使用独立线程;解析与中心持久化失败不会影响并行 lane。 */
|
|
217
270
|
private runScriptChunk;
|
|
218
271
|
/** 只记录阶段、ordinal 和毫秒数;禁止记录 prompt、令牌或中心能力。 */
|
|
@@ -236,5 +289,6 @@ export declare function terminalScriptChunkError(results: Array<{
|
|
|
236
289
|
terminal?: boolean;
|
|
237
290
|
}>): string;
|
|
238
291
|
export declare function scriptChunkPrompt(id: string, task: ScriptTask, ordinals: number[]): string;
|
|
292
|
+
export declare function creativeCandidatePrompt(id: string, task: ScriptTask, ordinals: number[]): string;
|
|
239
293
|
export declare function missingOrdinals(total: number, received: number[]): number[];
|
|
240
294
|
export {};
|
package/dist/workflow/manager.js
CHANGED
|
@@ -10,6 +10,7 @@ import { CONFIG_DIR, ensureSiteWorkspace } from "../config.js";
|
|
|
10
10
|
import { logger } from "../utils/logger.js";
|
|
11
11
|
import { windowsPowerShellExecutable } from "../utils/windows.js";
|
|
12
12
|
import { FLOW_C_SCRIPT_CHUNK_MAX, flowCScriptChunks, flowCScriptChunkSizes } from "./constants.js";
|
|
13
|
+
import { FLOW_C_CREATIVE_CANDIDATE_CONTRACT_VERSION, flowCCreativeCandidateOutputSchema, parseFlowCCreativeCandidateOutput } from "./creative-candidates.js";
|
|
13
14
|
import { flowCScriptOutputSchema, parseFlowCScriptOutput } from "./script-output.js";
|
|
14
15
|
const STATE_FILE = path.join(CONFIG_DIR, "workflow-state.json");
|
|
15
16
|
export const FLOW_C_CODEX_TURN_TIMEOUT_MS = 8 * 60 * 1000;
|
|
@@ -112,6 +113,12 @@ export class WorkflowManager {
|
|
|
112
113
|
this.save();
|
|
113
114
|
return data;
|
|
114
115
|
}
|
|
116
|
+
async submitCandidateChunk(idValue, groupsValue) {
|
|
117
|
+
const record = this.scriptRecord(workflowId(idValue, "脚本交接 ID"));
|
|
118
|
+
if (!Array.isArray(groupsValue) || !groupsValue.length || groupsValue.length > 10)
|
|
119
|
+
throw new Error("每个候选子批必须包含 1–10 个 ordinal 组");
|
|
120
|
+
return commerceJson(`${record.apiBase}/workflow-script-handoffs/${encodeURIComponent(record.id)}/candidate-chunks`, record.accessToken, "x-workflow-handoff-token", { method: "POST", body: JSON.stringify({ contractVersion: FLOW_C_CREATIVE_CANDIDATE_CONTRACT_VERSION, groups: groupsValue }) });
|
|
121
|
+
}
|
|
115
122
|
downloadState() {
|
|
116
123
|
return {
|
|
117
124
|
configured: Boolean(this.state.downloadDirectory),
|
|
@@ -233,10 +240,11 @@ export class WorkflowManager {
|
|
|
233
240
|
record.message = "正在读取完整产品清单";
|
|
234
241
|
record.updatedAt = now();
|
|
235
242
|
this.save();
|
|
236
|
-
|
|
243
|
+
let task = await this.scriptTask(id);
|
|
237
244
|
if (Date.parse(task.expires_at) <= Date.now())
|
|
238
245
|
throw new ExpiredCapabilityError("脚本交接已过期,请在网页重新点击交给本机 Codex");
|
|
239
246
|
const workspace = ensureSiteWorkspace(this.config);
|
|
247
|
+
task = await this.ensureCreativeCandidateSelections(id, task, workspace.workspacePath);
|
|
240
248
|
const durationSeconds = Number(task.duration_seconds || 10);
|
|
241
249
|
const chunkSizes = flowCScriptChunkSizes(durationSeconds);
|
|
242
250
|
// Fail locally before starting any worker if a future schema edit
|
|
@@ -256,7 +264,7 @@ export class WorkflowManager {
|
|
|
256
264
|
record.updatedAt = now();
|
|
257
265
|
this.save();
|
|
258
266
|
const results = await Promise.all(chunks.map((ordinals) => this.runScriptChunk(id, task, ordinals, workspace.workspacePath)));
|
|
259
|
-
await this.scriptTask(id);
|
|
267
|
+
task = await this.scriptTask(id);
|
|
260
268
|
const terminalError = terminalScriptChunkError(results);
|
|
261
269
|
if (terminalError) {
|
|
262
270
|
throw new Error(`本机脚本结构化契约被 Codex 拒绝,已停止自动重试且未提交缺失脚本。请先升级或修复 Canvas Agent,再手动重试(${terminalError})`);
|
|
@@ -296,6 +304,60 @@ export class WorkflowManager {
|
|
|
296
304
|
this.runningScripts.delete(id);
|
|
297
305
|
}
|
|
298
306
|
}
|
|
307
|
+
async ensureCreativeCandidateSelections(id, initialTask, cwd) {
|
|
308
|
+
let task = initialTask;
|
|
309
|
+
let chunkSize = 2;
|
|
310
|
+
while (selectedCandidateOrdinals(task).length < task.requested_count) {
|
|
311
|
+
const before = selectedCandidateOrdinals(task).length;
|
|
312
|
+
const missing = missingOrdinals(task.requested_count, selectedCandidateOrdinals(task));
|
|
313
|
+
const chunks = chunkNumbers(missing, chunkSize);
|
|
314
|
+
const record = this.scriptRecord(id);
|
|
315
|
+
record.message = `本机 Codex 正在生成可审核创意候选,中心将自动选题(${before}/${task.requested_count})`;
|
|
316
|
+
record.activeChunks = 0;
|
|
317
|
+
record.updatedAt = now();
|
|
318
|
+
this.save();
|
|
319
|
+
const results = await Promise.all(chunks.map((ordinals) => this.runCandidateChunk(id, task, ordinals, cwd)));
|
|
320
|
+
task = await this.scriptTask(id);
|
|
321
|
+
const terminalError = terminalScriptChunkError(results);
|
|
322
|
+
if (terminalError)
|
|
323
|
+
throw new Error(`创意候选结构化契约被 Codex 拒绝,已停止自动重试(${terminalError})`);
|
|
324
|
+
if (selectedCandidateOrdinals(task).length > before) {
|
|
325
|
+
chunkSize = 2;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (chunkSize > 1) {
|
|
329
|
+
chunkSize = 1;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const lastError = results.map((result) => result.error).filter(Boolean).at(-1) || "候选生成或中心选题未返回结果";
|
|
333
|
+
throw new Error(`创意候选阶段已隔离到单条仍失败(${lastError}),请点击重试`);
|
|
334
|
+
}
|
|
335
|
+
return task;
|
|
336
|
+
}
|
|
337
|
+
async runCandidateChunk(id, task, ordinals, cwd) {
|
|
338
|
+
const result = await runCodexWorkflowTurn(creativeCandidatePrompt(id, task, ordinals), this.emit, {
|
|
339
|
+
cwd,
|
|
340
|
+
permissionMode: "full",
|
|
341
|
+
timeoutMs: FLOW_C_CODEX_TURN_TIMEOUT_MS,
|
|
342
|
+
outputSchema: flowCCreativeCandidateOutputSchema(ordinals.length),
|
|
343
|
+
onThread: (threadId) => { this.scriptRecord(id).threadId = threadId; this.save(); },
|
|
344
|
+
onWorkerStart: () => { const record = this.scriptRecord(id); record.activeChunks = Number(record.activeChunks || 0) + 1; record.updatedAt = now(); this.save(); },
|
|
345
|
+
onWorkerFinish: () => { const record = this.scriptRecord(id); record.activeChunks = Math.max(0, Number(record.activeChunks || 0) - 1); record.updatedAt = now(); this.save(); },
|
|
346
|
+
});
|
|
347
|
+
this.emitScriptStage(id, ordinals, "candidate_model", result.timings.queueWaitMs + result.timings.threadStartMs + result.timings.modelMs);
|
|
348
|
+
if (!result.ok || !result.text)
|
|
349
|
+
return { error: result.ok ? "Codex 未返回创意候选" : result.error, terminal: !result.ok && !result.retryable };
|
|
350
|
+
try {
|
|
351
|
+
const groups = parseFlowCCreativeCandidateOutput(result.text, ordinals);
|
|
352
|
+
const startedAt = Date.now();
|
|
353
|
+
await this.submitCandidateChunk(id, groups);
|
|
354
|
+
this.emitScriptStage(id, ordinals, "candidate_select", Date.now() - startedAt);
|
|
355
|
+
return { terminal: false };
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
return { error: error instanceof Error ? `候选未通过中心结构/安全/去重校验:${error.message}` : "候选未通过中心校验", terminal: false };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
299
361
|
/** 一个 chunk 使用独立线程;解析与中心持久化失败不会影响并行 lane。 */
|
|
300
362
|
async runScriptChunk(id, task, ordinals, cwd) {
|
|
301
363
|
const durationSeconds = Number(task.duration_seconds || 10);
|
|
@@ -315,7 +377,11 @@ export class WorkflowManager {
|
|
|
315
377
|
return { error: result.ok ? "Codex 未返回可用的结构化脚本" : result.error, terminal: !result.ok && !result.retryable };
|
|
316
378
|
try {
|
|
317
379
|
const parseStartedAt = Date.now();
|
|
318
|
-
const
|
|
380
|
+
const selected = selectedCandidatesForOrdinals(task, ordinals);
|
|
381
|
+
const jobs = parseFlowCScriptOutput(result.text, ordinals).map((job) => ({
|
|
382
|
+
...job,
|
|
383
|
+
creativePlan: { ...(job.creativePlan || {}), ...selectedCandidatePlan(selected.get(job.ordinal)) },
|
|
384
|
+
}));
|
|
319
385
|
this.emitScriptStage(id, ordinals, "parse", Date.now() - parseStartedAt);
|
|
320
386
|
const persistStartedAt = Date.now();
|
|
321
387
|
await this.submitGeneratedScriptJobs(id, jobs);
|
|
@@ -504,40 +570,106 @@ class ExpiredCapabilityError extends Error {
|
|
|
504
570
|
export function scriptChunkPrompt(id, task, ordinals) {
|
|
505
571
|
const duration = Number(task.duration_seconds || 10);
|
|
506
572
|
const layoutVersion = task.storyboard_layout_version || task.storyboardLayoutVersion || "director-table-scripted-v1";
|
|
507
|
-
const
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
573
|
+
const products = relevantProductInputs(task, ordinals);
|
|
574
|
+
const selected = selectedCandidatesForOrdinals(task, ordinals);
|
|
575
|
+
if (selected.size !== ordinals.length)
|
|
576
|
+
throw new Error("中心尚未为当前 ordinal 完成创意选题");
|
|
577
|
+
const durationRules = duration === 10
|
|
578
|
+
? "每条只输出 openingState 和一个完整 0–10 秒 segment,不生成 masterScript。"
|
|
579
|
+
: `每条先写完整 ${duration} 秒 masterScript,再严格展开成 ${duration / 10} 个各自 0–10 秒的 segments;不得写 10–20 或 20–30 全局时轴。`;
|
|
580
|
+
return `你正在后台完成 Flow C 脚本交接 ${id},只写 ordinal:${ordinals.join(", ")};映射:${scriptProductAssignments(task.product_quantities, ordinals)}。
|
|
581
|
+
目标市场:${task.market}。商品事实:${compactJson(products, 12_000)}
|
|
582
|
+
中心已完成结构校验、安全检查、评分与批内去重;下面每个 ordinal 的 selected candidate 是唯一创意权威,必须忠实展开,不要再生成、替换或重新评分创意:
|
|
583
|
+
${compactJson([...selected.values()], 16_000)}
|
|
584
|
+
${durationRules}
|
|
585
|
+
写作要求:
|
|
586
|
+
1. visualHook、conflict、escalation/turn、productIntervention、visibleProof、callbackMotivation 与所有 shots 必须执行选中卡的 visualPremise、firstFrame、spectacleEscalation、productProofAction 和 truthBoundary;不得扩大功效或换成普通模板。
|
|
587
|
+
2. 每条生成一份稳定 voiceProfile;每段只写简短 voiceCue。口播必须是目标市场 ${task.market} 的自然原生语言、偏快但清晰;导演说明统一用简洁制作英文。
|
|
588
|
+
3. 每段永远是独立 0–10 秒,含 1–8 个按剧情需要决定的 shots、准确 voiceover、onScreenText、evidence、soundBgm、emotionalNote 与 endingState;模型不要输出 continuity 或 continuityMode。
|
|
589
|
+
4. shot.visual 要直接写出主体位置、景别、光线、材质、动作峰值和当地自然环境,并保留真实皮肤、细发、布料/商品微纹理和实景混合光;禁止塑料蜡感、静态举产品、品牌广告片、电视购物或伪造工厂/销量/来源。
|
|
590
|
+
5. 20/30 秒的后一段剧情承接前段 endingState,但每段仍只使用自己的局部 0–10 秒执行内容。
|
|
591
|
+
6. creativePlan 只补充脚本执行字段;Agent 会把选中卡的结构化裂变、文化、安全和 fingerprint 字段确定性并回,模型不要重复这些字段。
|
|
592
|
+
固定使用 GPT-5.6 Terra 高推理。storyboardLayoutVersion=${layoutVersion} 仅由中心管理。只返回当前结构化 jobs,不调用工具、不创建任何图片或视频任务。`;
|
|
593
|
+
}
|
|
594
|
+
export function creativeCandidatePrompt(id, task, ordinals) {
|
|
595
|
+
const products = relevantProductInputs(task, ordinals);
|
|
596
|
+
const styleCards = relevantStyleCards(task.reference_style_cards || [], products).map(compactReferenceStyleCard).filter(Boolean).slice(0, 6);
|
|
597
|
+
const ledger = (task.creative_fingerprint_ledger || []).slice(-60);
|
|
598
|
+
return `Flow C creative-candidate stage for handoff ${id}. Set contractVersion exactly to ${FLOW_C_CREATIVE_CANDIDATE_CONTRACT_VERSION}. Return exactly one group for each ordinal: ${ordinals.join(", ")}; mapping: ${scriptProductAssignments(task.product_quantities, ordinals)}.
|
|
599
|
+
Market: ${task.market}. Product facts: ${compactJson(products, 12_000)}
|
|
600
|
+
Optional learned style abstractions: ${compactJson(styleCards, 5_000)}
|
|
601
|
+
Already accepted fingerprint ledger: ${compactJson(ledger, 8_000)}
|
|
602
|
+
For each ordinal return exactly 6 short, auditable candidate cards. These are visible structured alternatives, not hidden reasoning. Vary the overall creative skeleton through open dimensions such as relationship/identity, scene mismatch, scale/quantity, group reaction, viewpoint, sensory texture and proof action; do not rotate a fixed template list.
|
|
603
|
+
Every card must be surprising but physically filmable and product-relevant. productProofAction and truthBoundary must follow supplied facts. For food, baby, medical/efficacy and personal-hygiene products, reject unhygienic display, fear marketing, dangerous use and unsupported promises.
|
|
604
|
+
Cultural anchors are optional (0–3). source must be learned-style-card, product-market-evidence, or conservative-everyday-detail. A learned-style-card anchor must cite the supplied card id in evidence. A product-market-evidence anchor must cite a concrete supplied product/market fact; if none exists, use conservative-everyday-detail and explicitly say it is ordinary context, not a custom claim. Never use flags, national costumes, royalty/public figures, religion, stereotypes or cultural mockery as shortcut hooks. Locality belongs in consumption context, natural interaction, language rhythm and aesthetic density.
|
|
605
|
+
creativeFingerprint must use five concise semantic labels: characterRelation, scene, firstFrameComposition, spectacleMechanism and proofMethod. Do not hide duplicate ideas behind paraphrases. Do not copy people, clothing, sets, dialogue or shot sequences from references. Return only the candidate schema; do not write scripts or call tools.`;
|
|
530
606
|
}
|
|
531
607
|
function compactReferenceStyleCard(value) {
|
|
532
608
|
if (!value || typeof value !== "object")
|
|
533
609
|
return "";
|
|
534
|
-
const fields = ["openingComposition", "visualDensity", "conflictContrast", "characterProductEntrance", "rhythm", "turn", "proofMethod", "voiceTone", "categoryFit"];
|
|
610
|
+
const fields = ["visualPremisePattern", "mutationAxes", "culturePattern", "spectaclePattern", "productIntegrationPattern", "openingComposition", "visualDensity", "conflictContrast", "characterProductEntrance", "rhythm", "turn", "proofMethod", "voiceTone", "categoryFit"];
|
|
535
611
|
const compact = Object.fromEntries(fields.flatMap((field) => {
|
|
536
612
|
const text = String(value[field] || "").trim().replace(/\s+/g, " ").slice(0, 400);
|
|
537
613
|
return text ? [[field, text]] : [];
|
|
538
614
|
}));
|
|
615
|
+
const categories = Array.isArray(value.applicability?.categories) ? value.applicability.categories.map((item) => String(item || "").trim()).filter(Boolean).slice(0, 8) : [];
|
|
616
|
+
const markets = Array.isArray(value.applicability?.markets) ? value.applicability.markets.map((item) => String(item || "").trim()).filter(Boolean).slice(0, 8) : [];
|
|
617
|
+
if (categories.length || markets.length)
|
|
618
|
+
Object.assign(compact, { applicability: { categories, markets } });
|
|
539
619
|
return Object.keys(compact).length ? JSON.stringify(compact) : "";
|
|
540
620
|
}
|
|
621
|
+
function relevantStyleCards(cards, products) {
|
|
622
|
+
const categories = new Set(products.map((product) => String(product.category || "").trim().toLowerCase()).filter(Boolean));
|
|
623
|
+
if (!categories.size)
|
|
624
|
+
return cards.slice(0, 3);
|
|
625
|
+
const matched = cards.filter((card) => {
|
|
626
|
+
const allowed = Array.isArray(card.applicability?.categories) ? card.applicability.categories.map((item) => String(item || "").trim().toLowerCase()).filter(Boolean) : [];
|
|
627
|
+
return !allowed.length || allowed.includes("all") || allowed.includes("global") || allowed.some((item) => categories.has(item));
|
|
628
|
+
});
|
|
629
|
+
return matched.length ? matched : [];
|
|
630
|
+
}
|
|
631
|
+
function relevantProductInputs(task, ordinals) {
|
|
632
|
+
const indexes = new Set(ordinals.map((ordinal) => productIndexForOrdinal(task.product_quantities, ordinal)));
|
|
633
|
+
const products = (task.product_inputs || []).filter((product) => indexes.has(Number(product.productIndex)));
|
|
634
|
+
if (products.length)
|
|
635
|
+
return products;
|
|
636
|
+
return [{ productIndex: -1, title: "Server compatibility fallback", quantity: ordinals.length, creativeBrief: String(task.instructions || "").slice(0, 4_000) }];
|
|
637
|
+
}
|
|
638
|
+
function selectedCandidateOrdinals(task) {
|
|
639
|
+
return [...new Set((task.selected_candidates || []).map((candidate) => Number(candidate.ordinal)).filter(Number.isInteger))].sort((left, right) => left - right);
|
|
640
|
+
}
|
|
641
|
+
function selectedCandidatesForOrdinals(task, ordinals) {
|
|
642
|
+
const expected = new Set(ordinals);
|
|
643
|
+
return new Map((task.selected_candidates || []).filter((candidate) => expected.has(Number(candidate.ordinal))).map((candidate) => [Number(candidate.ordinal), candidate]));
|
|
644
|
+
}
|
|
645
|
+
function selectedCandidatePlan(value) {
|
|
646
|
+
if (!value)
|
|
647
|
+
throw new Error("中心缺少选中的创意候选");
|
|
648
|
+
return {
|
|
649
|
+
visualPremise: value.visualPremise,
|
|
650
|
+
mutationAxes: value.mutationAxes,
|
|
651
|
+
culturalAnchors: value.culturalAnchors,
|
|
652
|
+
spectacleEscalation: value.spectacleEscalation,
|
|
653
|
+
productProofAction: value.productProofAction,
|
|
654
|
+
truthBoundary: value.truthBoundary,
|
|
655
|
+
riskFlags: value.riskFlags,
|
|
656
|
+
firstFrame: value.firstFrame,
|
|
657
|
+
creativeFingerprint: value.creativeFingerprint,
|
|
658
|
+
fingerprintKey: value.fingerprintKey,
|
|
659
|
+
selectionScore: value.selectionScore,
|
|
660
|
+
selectionBreakdown: value.selectionBreakdown,
|
|
661
|
+
selectionMode: value.selectionMode,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
function chunkNumbers(values, size) {
|
|
665
|
+
const chunks = [];
|
|
666
|
+
for (let index = 0; index < values.length; index += size)
|
|
667
|
+
chunks.push(values.slice(index, index + size));
|
|
668
|
+
return chunks;
|
|
669
|
+
}
|
|
670
|
+
function compactJson(value, limit) {
|
|
671
|
+
return JSON.stringify(value).slice(0, limit);
|
|
672
|
+
}
|
|
541
673
|
function scriptProductAssignments(productQuantities, ordinals) {
|
|
542
674
|
const assignments = [];
|
|
543
675
|
let first = ordinals[0];
|