@xiaohhhh1/canvas-agent 0.4.83 → 0.4.85
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.
|
@@ -2,7 +2,8 @@ export declare const FLOW_C_CONTENT_STRATEGY_VERSION = "flow-c-content-strategy-
|
|
|
2
2
|
export declare const FLOW_C_GENERATED_MONTAGE_VERSION = "flow-c-generated-montage-v1";
|
|
3
3
|
export declare const FLOW_C_GENERATED_MONTAGE_STYLE = "generated-montage";
|
|
4
4
|
export declare const FLOW_C_GENERATED_MONTAGE_VERSION_UNSUPPORTED = "FLOW_C_GENERATED_MONTAGE_VERSION_UNSUPPORTED";
|
|
5
|
-
export declare const
|
|
5
|
+
export declare const FLOW_C_VOICE_PACING_QUICK_WORDS_PER_SECOND = 3;
|
|
6
|
+
export declare const FLOW_C_VOICE_PACING_REPAIR_MIN_WORDS_PER_SECOND = 4;
|
|
6
7
|
export declare const FLOW_C_VOICE_PACING_REPAIR_MIN_EXCESS_WORDS = 3;
|
|
7
8
|
export type FlowCContentStrategy = {
|
|
8
9
|
contractVersion: typeof FLOW_C_CONTENT_STRATEGY_VERSION;
|
|
@@ -42,6 +43,7 @@ export type FlowCContentAdvisory = {
|
|
|
42
43
|
};
|
|
43
44
|
export type FlowCVoicePacingRepairIssue = Required<Pick<FlowCContentAdvisory, "ordinal" | "segment" | "shot" | "wordCount" | "suggestedMaxWords" | "durationSeconds">> & {
|
|
44
45
|
code: "voice_pacing";
|
|
46
|
+
endShot?: number;
|
|
45
47
|
};
|
|
46
48
|
/** Explicit styles are capability-versioned; an unknown pair must never silently fall back. */
|
|
47
49
|
export declare function flowCGeneratedMontage(value: unknown): boolean;
|
|
@@ -66,7 +68,8 @@ export declare function flowCContentAdvisories(jobs: unknown, strategy: FlowCCon
|
|
|
66
68
|
/**
|
|
67
69
|
* A deliberately narrower pre-delivery repair trigger than the public pacing
|
|
68
70
|
* advisory. It only covers explicit English/Spanish text that is both very fast
|
|
69
|
-
* and materially over
|
|
71
|
+
* and materially over a quick three-words-per-second writing budget. Normal
|
|
72
|
+
* quick speech remains advisory, and visual cuts never become speech deadlines.
|
|
70
73
|
*/
|
|
71
74
|
export declare function flowCVoicePacingRepairIssues(jobs: unknown, options?: {
|
|
72
75
|
targetLanguage?: unknown;
|
|
@@ -2,7 +2,9 @@ export const FLOW_C_CONTENT_STRATEGY_VERSION = "flow-c-content-strategy-v1";
|
|
|
2
2
|
export const FLOW_C_GENERATED_MONTAGE_VERSION = "flow-c-generated-montage-v1";
|
|
3
3
|
export const FLOW_C_GENERATED_MONTAGE_STYLE = "generated-montage";
|
|
4
4
|
export const FLOW_C_GENERATED_MONTAGE_VERSION_UNSUPPORTED = "FLOW_C_GENERATED_MONTAGE_VERSION_UNSUPPORTED";
|
|
5
|
-
|
|
5
|
+
// Editorial estimates, not a certification of spoken-audio intelligibility.
|
|
6
|
+
export const FLOW_C_VOICE_PACING_QUICK_WORDS_PER_SECOND = 3;
|
|
7
|
+
export const FLOW_C_VOICE_PACING_REPAIR_MIN_WORDS_PER_SECOND = 4;
|
|
6
8
|
export const FLOW_C_VOICE_PACING_REPAIR_MIN_EXCESS_WORDS = 3;
|
|
7
9
|
function object(value) {
|
|
8
10
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
@@ -29,6 +31,31 @@ function voicePacingMeasurement(value, durationSeconds, targetLanguage) {
|
|
|
29
31
|
const wordCount = (voice.match(/[\p{L}\p{N}]+(?:['’][\p{L}\p{N}]+)*/gu) || []).length;
|
|
30
32
|
return { wordCount, suggestedMaxWords: Math.floor(seconds * 2), durationSeconds: seconds };
|
|
31
33
|
}
|
|
34
|
+
/** A camera cut does not end narration; only pool adjacent explicitly voiced shots. */
|
|
35
|
+
function continuousVoicePacingRuns(shots, targetLanguage) {
|
|
36
|
+
const runs = [];
|
|
37
|
+
let previousEnd = Number.NaN;
|
|
38
|
+
for (const [index, shot] of shots.entries()) {
|
|
39
|
+
const start = Number(shot.startSeconds);
|
|
40
|
+
const end = Number(shot.endSeconds);
|
|
41
|
+
const pacing = voicePacingMeasurement(shot.voiceover, end - start, targetLanguage);
|
|
42
|
+
if (!pacing) {
|
|
43
|
+
previousEnd = Number.NaN;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const current = runs.at(-1);
|
|
47
|
+
if (current && Number.isFinite(start) && Math.abs(start - previousEnd) < 0.001) {
|
|
48
|
+
current.endShot = index + 1;
|
|
49
|
+
current.wordCount += pacing.wordCount;
|
|
50
|
+
current.durationSeconds += pacing.durationSeconds;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
runs.push({ shot: index + 1, endShot: index + 1, wordCount: pacing.wordCount, durationSeconds: pacing.durationSeconds });
|
|
54
|
+
}
|
|
55
|
+
previousEnd = end;
|
|
56
|
+
}
|
|
57
|
+
return runs;
|
|
58
|
+
}
|
|
32
59
|
/** Explicit styles are capability-versioned; an unknown pair must never silently fall back. */
|
|
33
60
|
export function flowCGeneratedMontage(value) {
|
|
34
61
|
const input = object(value);
|
|
@@ -51,7 +78,7 @@ export function flowCContentWritingReviewPrompt(strategy, options) {
|
|
|
51
78
|
return `同回合按“框架 → 可见因果 → 当地口播 → 逐句对照”完成创作与自审,不增加模型调用或输出字段:
|
|
52
79
|
- 框架:creativeBrief 与用户框架 ordinal ${JSON.stringify(frameworkOrdinals)} 优先;保留其角色、开头、事件顺序、核心情节、锁定对白和结尾,只补留白,不为追求差异改掉已经合适的创意。
|
|
53
80
|
- 可见因果:先把一个有商品事实或参考图依据的动作、镜头内可见结果和核心购买理由连起来;evidence 只描述同镜真正看见的依据,不把生成表演冒充实测,也不从“有动作+有结果”自动推断未经提供的因果。
|
|
54
|
-
- 当地口播:只按显式 targetLanguage/targetLocale、creatorVoiceStyle 与 ctaStyle
|
|
81
|
+
- 当地口播:只按显式 targetLanguage/targetLocale、creatorVoiceStyle 与 ctaStyle 写自然口语语序和常用短句,不从国家推断语言、族群或口音,不逐字翻译、不生造俚语。用户锁定对白即使偏密或证据不足也不得静默删除、换义或改写;先给承载锁定对白的镜头足够秒数,必要时合并相邻同动作镜头、压缩无声过渡,再安排其它非锁定台词。允许偶尔偏密和适度加快语速,只要吐字清楚、符合画面、不夸张赶话;一句话可在同段相邻有声镜头间连续说完。锁定对白已经占用可说完的时长时,其余可选口播默认 none;未解决处保留给非阻断提示。
|
|
55
82
|
- 逐句对照:逐镜核对每个非 none 的 voiceover 片段与该镜 visual/evidence 及已知商品事实;商品事实句必须有同镜可见依据,处境、情绪或 CTA 不必伪装成产品证明但必须符合正在发生的画面。最后核对末镜实际动作、visual 末尾收尾短句与 endingState.endingFrame 精确同锚点;这里只做结构性自审,不声称靠词面规则完成语义验收。`;
|
|
56
83
|
}
|
|
57
84
|
/** Rules for the separately selected generated-montage content style. */
|
|
@@ -64,7 +91,7 @@ export function flowCGeneratedMontagePrompt(ordinals, segmentSeconds = 10) {
|
|
|
64
91
|
- 围绕一个由当前商品事实支持的核心购买理由,选择抓眼但真实可执行的使用、细节、多个适用画面或可见结果镜头;每次切镜带来新的有用观察,不做无关美图轮播,也不强制编痛点剧情、完整人物故事或为了差异放弃好创意。
|
|
65
92
|
- 跨镜、跨全片的人物、服装和场景一致性不是目标或验收门槛;用户框架明确指定角色/场景时仍严格尊重。每个单镜动作须自然,若一个动作明确跨相邻镜继续则保持该动作的手部、商品与物理状态连续;人物或地点变化时在下一 shot.visual 明写 HARD CUT,切后可直接进入新示例,但所有镜头的 SKU、颜色、结构、材质、数量、包装和表面文字图案始终不变。
|
|
66
93
|
- 仍是每个局部 0–${segmentSeconds} 秒、1–8 个 shots。${longDurationLabel}后一段 openingState 精确继承上一段 endingState 只是技术开场交接;紧接着可以明写 HARD CUT 进入新人物或新场景,不要求让上一段人物/场景贯穿下一段,也不得为连续剧情浪费本段秒数。
|
|
67
|
-
- 一句自然口播可以跨镜延续,但每镜 voiceover
|
|
94
|
+
- 一句自然口播可以跨镜延续,但每镜 voiceover 只保存该镜实际说出的完整词组/分句;镜头切换不强制口播停顿,相邻有声镜头按连续口播整体留呼吸,允许偶尔偏密和适度加快语速。明确标为 none 的镜头保留静默,不把整句塞进一秒镜头再假定后续静默仍在说话。每个 ${segmentSeconds} 秒段都以自然完整的句法边界收尾,不把半个词、未完短语或待补 CTA 留给另一次模型调用。
|
|
68
95
|
- 这些规则只改变当前已选脚本的内容表达;不新增候选或模型阶段,不改严格输出 schema、首帧/分镜媒体依赖、收费、队列、重试或归档。`;
|
|
69
96
|
}
|
|
70
97
|
/** Missing/unknown versions keep historical tasks on their exact original prompt. */
|
|
@@ -171,7 +198,8 @@ export function flowCContentAdvisories(jobs, strategy, options = {}) {
|
|
|
171
198
|
/**
|
|
172
199
|
* A deliberately narrower pre-delivery repair trigger than the public pacing
|
|
173
200
|
* advisory. It only covers explicit English/Spanish text that is both very fast
|
|
174
|
-
* and materially over
|
|
201
|
+
* and materially over a quick three-words-per-second writing budget. Normal
|
|
202
|
+
* quick speech remains advisory, and visual cuts never become speech deadlines.
|
|
175
203
|
*/
|
|
176
204
|
export function flowCVoicePacingRepairIssues(jobs, options = {}) {
|
|
177
205
|
if (!Array.isArray(jobs))
|
|
@@ -184,11 +212,11 @@ export function flowCVoicePacingRepairIssues(jobs, options = {}) {
|
|
|
184
212
|
const segments = Array.isArray(job.segments) ? job.segments : job.segment ? [job.segment] : [];
|
|
185
213
|
for (const [segmentIndex, segmentValue] of segments.entries()) {
|
|
186
214
|
const shots = Array.isArray(object(segmentValue).shots) ? object(segmentValue).shots.map(object) : [];
|
|
187
|
-
for (const
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
215
|
+
for (const run of continuousVoicePacingRuns(shots, options.targetLanguage)) {
|
|
216
|
+
const suggestedMaxWords = Math.floor(run.durationSeconds * FLOW_C_VOICE_PACING_QUICK_WORDS_PER_SECOND);
|
|
217
|
+
if (run.wordCount / run.durationSeconds <= FLOW_C_VOICE_PACING_REPAIR_MIN_WORDS_PER_SECOND || run.wordCount - suggestedMaxWords < FLOW_C_VOICE_PACING_REPAIR_MIN_EXCESS_WORDS)
|
|
190
218
|
continue;
|
|
191
|
-
issues.push({ ordinal: Number(job.ordinal), code: "voice_pacing", segment: segmentIndex + 1,
|
|
219
|
+
issues.push({ ordinal: Number(job.ordinal), code: "voice_pacing", segment: segmentIndex + 1, ...run, suggestedMaxWords });
|
|
192
220
|
}
|
|
193
221
|
}
|
|
194
222
|
}
|
|
@@ -238,10 +266,10 @@ export function flowCVoicePacingRepairPrompt(jobs, options = {}) {
|
|
|
238
266
|
const frameworkOrdinals = [...new Set((Array.isArray(options.frameworkOrdinals) ? options.frameworkOrdinals : []).map(Number).filter((ordinal) => issueOrdinals.includes(ordinal)))];
|
|
239
267
|
const segmentSeconds = options.segmentSeconds === 15 ? 15 : 10;
|
|
240
268
|
const payload = values.filter((value) => issueOrdinals.includes(Number(object(value).ordinal))).map(flowCVoicePacingRepairScaffold);
|
|
241
|
-
return `这是 Flow C 首次回传前唯一一次、仅针对 ordinal ${JSON.stringify(issueOrdinals)}
|
|
269
|
+
return `这是 Flow C 首次回传前唯一一次、仅针对 ordinal ${JSON.stringify(issueOrdinals)} 的极端过密口播定向修复。普通偏密与适度快语速只作提示,无需修复。目标口播语言保持为 ${text(options.targetLanguage, 160) || "原稿的显式目标语言"}。只返回下方完整 strict jobs,不调用工具、不创建媒体、不输出分析或新增字段。
|
|
242
270
|
- 只允许修改每个 shot.voiceover;逐字复制其它所有字段,包括 ordinal/productIndex/sellingFormId、voiceProfile、openingState、segment/segments 数量、voiceCue、shots 数量和顺序、startSeconds/endSeconds、visual、onScreenText、evidence、soundBgm、emotionalNote 与 endingState。不得改画面、时轴、商品、事实、用户框架、所选结构、分段承接或末帧。
|
|
243
|
-
- 保留原口播的核心购买理由、画面对应事实、语气、CTA
|
|
244
|
-
-
|
|
271
|
+
- 保留原口播的核心购买理由、画面对应事实、语气、CTA 意图和目标语言。先把模型自行增加的赘词压成能自然说完的短句,再按完整词组或自然分句重分配到展示相关动作的镜头;允许适度快语速但必须清晰,不能夸张赶话、截断单词、留下未完句或把超载片段简单改成 none。若一个原本有口播的局部 ${segmentSeconds} 秒段修后完全静默,视为失败。
|
|
272
|
+
- 英语/西语以约 2–3 词/秒作为自然到轻快表达的参考,不是固定配额;连续有声镜头整体超过 4 词/秒且明显超出轻快预算才进入此次修复。当前命中范围(shot 至 endShot):${JSON.stringify(issues.map(({ ordinal, segment, shot, endShot, wordCount, suggestedMaxWords, durationSeconds }) => ({ ordinal, segment, shot, endShot, wordCount, suggestedMaxWords, durationSeconds })))}。镜头切换不强制口播停顿;允许同段相邻有声镜头之间自然延续,不能把明确静默镜头或下一独立段的时长计入口播预算。
|
|
245
273
|
- 用户逐字框架 ordinal ${JSON.stringify(frameworkOrdinals)}:所有原口播文字与顺序必须保持,不能删、换词或压缩,只能在同一局部 ${segmentSeconds} 秒段内按完整自然短语重新分配到语义相关、时长足够的镜头;无法容纳就原样返回,让 Agent 明确交给人工处理。
|
|
246
274
|
- 其它 ordinal 可以压缩模型自增措辞,但不能通过删除整段口播来消除告警。每个 ${segmentSeconds} 秒段仍须在自然句法边界收尾,segmentVoiceovers 与渲染 script 将由 Agent 从最终逐镜 voiceover 确定性重建。
|
|
247
275
|
待修复的已校验原稿:${JSON.stringify({ jobs: payload })}`;
|
|
@@ -260,9 +288,7 @@ export function flowCContentMethodPrompt(strategy, options) {
|
|
|
260
288
|
const ordinaryOrdinals = options.ordinals.filter((ordinal) => !montageOrdinals.includes(ordinal));
|
|
261
289
|
const segmentSeconds = options.segmentSeconds === 15 ? 15 : 10;
|
|
262
290
|
const longDurationLabel = segmentSeconds === 15 ? "30秒双段" : "20/30秒";
|
|
263
|
-
const segmentPacingRule = segmentSeconds
|
|
264
|
-
? "整段15秒按自然语速留足动作与呼吸,可以更少,不是最低字数要求"
|
|
265
|
-
: "整段10秒约12–20词只是起点,可以更少,不是最低字数要求";
|
|
291
|
+
const segmentPacingRule = `整段${segmentSeconds}秒按自然或轻快语速留足动作与呼吸,可以更少,不是最低字数要求`;
|
|
266
292
|
const scenarioRule = montageOrdinals.length
|
|
267
293
|
? `1. 本条“先明确谁在生活节点遇到麻烦/需求”的剧情组织只适用于其余普通脚本 ordinal ${JSON.stringify(ordinaryOrdinals)};原创混剪 ordinal ${JSON.stringify(montageOrdinals)} 不强制痛点、麻烦、待解决问题或人物反转,可以从商品事实支持的好结果、真实使用动作或可见细节直接开场。普通脚本再选择地点里的动作坐标与必要可见物件;两类都用现有 shot.visual 写出具体微场景,不用“某人在家里”或泛气氛替代。人物是普通创作者/情景角色;真实身份、经历、用户证言、专家或权威背书只能来自明确提供的事实。缺非关键创意细节时用保守日常设定软适配,不追问、不拒绝、不编新事实。`
|
|
268
294
|
: "1. 先明确谁在什么生活节点遇到什么具体麻烦/需求,再选择地点里的动作坐标与必要可见物件;用现有 shot.visual 写出微场景,不用“某人在家里”或泛气氛替代。人物是普通创作者/情景角色;真实身份、经历、用户证言、专家或权威背书只能来自明确提供的事实。缺非关键创意细节时用保守日常设定软适配,不追问、不拒绝、不编新事实。";
|
|
@@ -275,7 +301,7 @@ ${writingReview}
|
|
|
275
301
|
${scenarioRule}
|
|
276
302
|
${openingRule}
|
|
277
303
|
3. 分开检查叙事段落逻辑、每镜目的与口播呼吸节奏:段落负责铺垫/证明/收束,镜头负责展示动作与证据,一句口播不必切一镜、一镜也不必念完一段。保留选中蓝图的因果顺序、节拍与镜头能量;仍为每段局部0–${segmentSeconds}秒、1–8镜,${longDurationLabel}连续关系和三种媒体共同内容契约不变。
|
|
278
|
-
4. 自检上方已定稿的目标语言的自然短句,不再按镜头补新台词。逐镜按实际时长 endSeconds − startSeconds
|
|
304
|
+
4. 自检上方已定稿的目标语言的自然短句,不再按镜头补新台词。逐镜按实际时长 endSeconds − startSeconds 检查,先为关键动作/可见结果留静默与呼吸;镜头切换不强制口播停顿,相邻有声镜头可连续说完一句。仅英语、西语等通常按空格分词的语言,约2–3词/秒是自然到轻快表达的写作参考,允许偶尔偏密和适度加快语速,只要清楚、符合画面、没有夸张赶话。${segmentPacingRule}。不要只因为某个短镜的词数较高而重写或打断整批;只对连续有声范围仍超过4词/秒且明显超量的极端情况收窄模型自增赘句。明确标为 none 的镜头和下一独立段不能计入口播时长。若用户锁定对白需要更多时间,先延长承载它的镜头并压缩无声过渡或合并相邻同动作镜头;不得改写锁定词。锁定对白分配妥当后才可加入其它口播,剩余时长不足就让其它镜头保持 none。不能套用到中文、日语等其它语言,不套统一英语词数或固定八秒模板。这是同轮软预算,不是语言验收拒绝条件或新重写环节;口播与画面脚本在同一次返回中完成。voiceProfile.speakingRate/pauseHabit 与 voiceCue 要如实写明 natural 或 brisk-but-clear 等实际语速,不得标成 unhurried/慢速却塞满台词;emotionalNote 只写镜头目的/情绪转折。不输出计算、推理或预算报告。
|
|
279
305
|
5. contentDirection 只细化既有蓝图可替换的人物、微场景、措辞和执行槽位,不改变商品身份、纹理文字、能力证据、0–${segmentSeconds}秒分段或媒体制作方式。creativeBrief 始终最高优先;用户框架 ordinal ${JSON.stringify(options.frameworkOrdinals)} 的开头、事件顺序、核心剧情和结尾不可被方向卡/避重覆盖,只能在其留白处具体化。
|
|
280
306
|
返回前按以上内容要求自检,修正能在本次写作中修正的空泛描述。自检不是新验收闸门;只输出原严格 schema 的既有字段,不输出推理、自检报告、contentDirection 或其它新增字段。\n`;
|
|
281
307
|
}
|
|
@@ -12,7 +12,7 @@ export declare const FLOW_C_VOICE_PACING_REVIEW_REQUIRED = "FLOW_C_VOICE_PACING_
|
|
|
12
12
|
export declare const FLOW_C_LEGACY_DIRECT_CONTRACT_VERSION = "flow-c-template-direct-v2";
|
|
13
13
|
export declare const FLOW_C_PRODUCT_PROFILE_DIRECT_CONTRACT_VERSION = "flow-c-product-profile-direct-v1";
|
|
14
14
|
export declare const FLOW_C_CREATIVE_SOURCE_DIRECT_CONTRACT_VERSION = "flow-c-creative-source-direct-v1";
|
|
15
|
-
type ScriptStatus = "queued" | "running" | "complete" | "error" | "expired";
|
|
15
|
+
type ScriptStatus = "queued" | "running" | "complete" | "review" | "error" | "expired";
|
|
16
16
|
type DownloadStatus = "waiting" | "running" | "complete" | "error" | "expired";
|
|
17
17
|
type ScriptRecord = {
|
|
18
18
|
id: string;
|
|
@@ -36,6 +36,12 @@ type ScriptRecord = {
|
|
|
36
36
|
candidateRevision: string;
|
|
37
37
|
attempts: number;
|
|
38
38
|
}>;
|
|
39
|
+
/** Only these failed revisions are excluded from automatic waves; clean siblings keep running. */
|
|
40
|
+
voicePacingReviewHolds?: Array<{
|
|
41
|
+
ordinal: number;
|
|
42
|
+
candidateRevision: string;
|
|
43
|
+
reason: string;
|
|
44
|
+
}>;
|
|
39
45
|
contentStrategy?: FlowCContentStrategy;
|
|
40
46
|
contentSummaries?: FlowCContentSummary[];
|
|
41
47
|
contentAdvisories?: FlowCContentAdvisory[];
|
|
@@ -197,7 +203,7 @@ type DraftJob = {
|
|
|
197
203
|
type ScriptChunkResult = {
|
|
198
204
|
error?: string;
|
|
199
205
|
terminal: boolean;
|
|
200
|
-
terminalKind?: "contract" | "transport" | "timeout" | "turn" | "delivery" | "review";
|
|
206
|
+
terminalKind?: "contract" | "transport" | "timeout" | "turn" | "delivery" | "review" | "isolation";
|
|
201
207
|
affectedOrdinals?: number[];
|
|
202
208
|
replanOrdinals?: number[];
|
|
203
209
|
};
|
|
@@ -228,6 +234,8 @@ export declare class WorkflowManager {
|
|
|
228
234
|
status: ScriptStatus;
|
|
229
235
|
requestedCount: number;
|
|
230
236
|
received: number;
|
|
237
|
+
reviewOrdinals: number[];
|
|
238
|
+
reviewCount: number;
|
|
231
239
|
threadId: string | undefined;
|
|
232
240
|
chunkSize: number | null;
|
|
233
241
|
activeChunks: number;
|
|
@@ -245,6 +253,8 @@ export declare class WorkflowManager {
|
|
|
245
253
|
status: ScriptStatus;
|
|
246
254
|
requestedCount: number;
|
|
247
255
|
received: number;
|
|
256
|
+
reviewOrdinals: number[];
|
|
257
|
+
reviewCount: number;
|
|
248
258
|
threadId: string | undefined;
|
|
249
259
|
chunkSize: number | null;
|
|
250
260
|
activeChunks: number;
|
|
@@ -262,6 +272,8 @@ export declare class WorkflowManager {
|
|
|
262
272
|
status: ScriptStatus;
|
|
263
273
|
requestedCount: number;
|
|
264
274
|
received: number;
|
|
275
|
+
reviewOrdinals: number[];
|
|
276
|
+
reviewCount: number;
|
|
265
277
|
threadId: string | undefined;
|
|
266
278
|
chunkSize: number | null;
|
|
267
279
|
activeChunks: number;
|
|
@@ -397,6 +409,8 @@ export declare class WorkflowManager {
|
|
|
397
409
|
private pumpScriptQueue;
|
|
398
410
|
private finishDownloadDirectorySelection;
|
|
399
411
|
private runScript;
|
|
412
|
+
/** A finished lane refills immediately; a fatal result closes admission but drains every admitted lane. */
|
|
413
|
+
private runScriptChunksRolling;
|
|
400
414
|
/** Analyze each product once; image 1 owns identity and later images may only corroborate visible evidence. */
|
|
401
415
|
private ensureProductExecutionProfiles;
|
|
402
416
|
private runProductExecutionProfile;
|
|
@@ -409,6 +423,7 @@ export declare class WorkflowManager {
|
|
|
409
423
|
* restart or response loss can never replay an overcrowded draft as accepted.
|
|
410
424
|
*/
|
|
411
425
|
private repairVoicePacingJobs;
|
|
426
|
+
private holdVoicePacingReview;
|
|
412
427
|
private runVoicePacingRepairTurn;
|
|
413
428
|
/** Nonblocking, text-free diagnostics for newly generated policy scripts only. */
|
|
414
429
|
private noteScriptContentAdvisories;
|
|
@@ -447,7 +462,7 @@ export declare function terminalScriptChunkFailure<T extends {
|
|
|
447
462
|
terminal?: boolean;
|
|
448
463
|
terminalKind?: ScriptChunkResult["terminalKind"];
|
|
449
464
|
affectedOrdinals?: number[];
|
|
450
|
-
}>(results: T[], receivedOrdinals?: number[]): T
|
|
465
|
+
}>(results: T[], receivedOrdinals?: number[]): T;
|
|
451
466
|
export declare function scriptCreativeReplanOrdinals(results: unknown[]): number[];
|
|
452
467
|
/** Keep a mixed reset signal after an exact-duplicate rewrite finishes. */
|
|
453
468
|
export declare function preserveScriptRecoveryReplans(result: ScriptChunkResult, error: unknown): ScriptChunkResult;
|
|
@@ -461,6 +476,8 @@ export declare function recordCreativeReplanAttempts(attempts: Map<number, numbe
|
|
|
461
476
|
export declare function isFlowCPromptPayloadTooLarge(error: unknown): error is Error & {
|
|
462
477
|
code: "FLOW_C_PROMPT_PAYLOAD_TOO_LARGE";
|
|
463
478
|
};
|
|
479
|
+
/** Preflight the real immutable prompt; only a size error permits subdividing it. */
|
|
480
|
+
export declare function promptSafeScriptChunks(id: string, task: ScriptTask, ordinals: number[]): number[][];
|
|
464
481
|
export declare function productExecutionProfilePrompt(product: ProductInput, contractVersion?: string): string;
|
|
465
482
|
export declare function scriptChunkPrompt(id: string, task: ScriptTask, ordinals: number[], rewriteAttempt?: number): string;
|
|
466
483
|
export declare function creativeCandidatePrompt(id: string, task: ScriptTask, ordinals: number[]): string;
|
|
@@ -482,6 +499,31 @@ export declare function selectedBlueprintPromptPayload(values: SelectedCandidate
|
|
|
482
499
|
variationSeed: string;
|
|
483
500
|
}[];
|
|
484
501
|
};
|
|
502
|
+
/** Avoid encoding a complete JSON blueprint as an escaped JSON string again. */
|
|
503
|
+
export declare function unescapeBlueprintPromptPayload(payload: ReturnType<typeof selectedBlueprintPromptPayload>): {
|
|
504
|
+
executionBlueprints: ({
|
|
505
|
+
blueprintRef: string;
|
|
506
|
+
executionBlueprint: string;
|
|
507
|
+
sourceDurationSeconds: number | null;
|
|
508
|
+
targetDurationSeconds: number | null;
|
|
509
|
+
retimingMode: "compress" | "expand" | "same" | null;
|
|
510
|
+
} | {
|
|
511
|
+
executionBlueprint: object;
|
|
512
|
+
blueprintRef: string;
|
|
513
|
+
sourceDurationSeconds: number | null;
|
|
514
|
+
targetDurationSeconds: number | null;
|
|
515
|
+
retimingMode: "compress" | "expand" | "same" | null;
|
|
516
|
+
})[];
|
|
517
|
+
productAdaptations: Record<string, unknown>[];
|
|
518
|
+
ordinalBindings: {
|
|
519
|
+
contentDirection?: import("./content-method.js").FlowCContentDirection | undefined;
|
|
520
|
+
ordinal: number;
|
|
521
|
+
productIndex: number;
|
|
522
|
+
blueprintRef: string | null;
|
|
523
|
+
adaptationRef: string;
|
|
524
|
+
variationSeed: string;
|
|
525
|
+
}[];
|
|
526
|
+
};
|
|
485
527
|
export declare function selectedCandidatePlan(value: SelectedCandidate | undefined, contentStrategy?: FlowCContentStrategy | null): {
|
|
486
528
|
contentDirection?: import("./content-method.js").FlowCContentDirection | undefined;
|
|
487
529
|
sellingFormCardId: string | null;
|
package/dist/workflow/manager.js
CHANGED
|
@@ -81,6 +81,7 @@ export class WorkflowManager {
|
|
|
81
81
|
pendingScriptJobs: previous?.pendingScriptJobs || [],
|
|
82
82
|
...(previous?.voicePacingReviewJobs ? { voicePacingReviewJobs: previous.voicePacingReviewJobs } : {}),
|
|
83
83
|
...(previous?.voicePacingRepairAttempts ? { voicePacingRepairAttempts: previous.voicePacingRepairAttempts } : {}),
|
|
84
|
+
...(previous?.voicePacingReviewHolds ? { voicePacingReviewHolds: previous.voicePacingReviewHolds } : {}),
|
|
84
85
|
...(previous?.contentAdvisories ? { contentAdvisories: previous.contentAdvisories } : {}),
|
|
85
86
|
lastFailure: previous?.lastFailure,
|
|
86
87
|
priorityAt: now(),
|
|
@@ -142,6 +143,7 @@ export class WorkflowManager {
|
|
|
142
143
|
if (!record.voicePacingReviewJobs.length)
|
|
143
144
|
delete record.voicePacingReviewJobs;
|
|
144
145
|
pruneVoicePacingRepairAttempts(record);
|
|
146
|
+
reconcileVoicePacingReviewHolds(record, task);
|
|
145
147
|
record.expiresAt = task.expires_at;
|
|
146
148
|
record.updatedAt = now();
|
|
147
149
|
this.save();
|
|
@@ -177,6 +179,7 @@ export class WorkflowManager {
|
|
|
177
179
|
if (!record.voicePacingReviewJobs.length)
|
|
178
180
|
delete record.voicePacingReviewJobs;
|
|
179
181
|
pruneVoicePacingRepairAttempts(record);
|
|
182
|
+
reconcileVoicePacingReviewHolds(record);
|
|
180
183
|
record.status = data.status === "ready" ? "complete" : "running";
|
|
181
184
|
record.message = data.status === "ready"
|
|
182
185
|
? flowCContentStrategy(record.contentStrategy) ? `全部 ${record.requestedCount} 条脚本已回传(内容语义仍需审阅)` : `全部 ${record.requestedCount} 条高质量脚本已回传`
|
|
@@ -222,7 +225,7 @@ export class WorkflowManager {
|
|
|
222
225
|
activeScripts: workers.active,
|
|
223
226
|
scriptConcurrencyLimit: workers.limit,
|
|
224
227
|
queuedScripts: records.filter((record) => record.status === "queued" || record.status === "running").length,
|
|
225
|
-
blockedScripts: records.filter((record) => record.status === "error").length,
|
|
228
|
+
blockedScripts: records.filter((record) => record.status === "error" || record.status === "review").length,
|
|
226
229
|
};
|
|
227
230
|
}
|
|
228
231
|
startDownloadDirectorySelection() {
|
|
@@ -353,33 +356,43 @@ export class WorkflowManager {
|
|
|
353
356
|
const creativeReplanAttempts = new Map();
|
|
354
357
|
while (record.receivedOrdinals.length < task.requested_count) {
|
|
355
358
|
task = await this.scriptTask(id);
|
|
359
|
+
const settledOrdinals = [...record.receivedOrdinals, ...voicePacingHeldOrdinals(record)];
|
|
360
|
+
if (!missingOrdinals(task.requested_count, settledOrdinals).length)
|
|
361
|
+
break;
|
|
356
362
|
const selectedBefore = selectedCandidateOrdinals(task);
|
|
357
|
-
const wave = nextScriptPipelineWave(task,
|
|
363
|
+
const wave = nextScriptPipelineWave(task, settledOrdinals, durationSeconds, chunkSizes[chunkSizeIndex], candidateChunkSize);
|
|
358
364
|
if (wave.stage === "candidate") {
|
|
359
365
|
const receivedBefore = record.receivedOrdinals.length;
|
|
366
|
+
const heldBefore = voicePacingHeldOrdinals(record).length;
|
|
360
367
|
record.message = `本机 Codex 正在选择下一小批创意(${selectedBefore.length}/${task.requested_count});选好后立即写对应脚本`;
|
|
361
368
|
record.activeChunks = 0;
|
|
362
369
|
record.updatedAt = now();
|
|
363
370
|
this.save();
|
|
371
|
+
const admission = { stopped: false };
|
|
364
372
|
const pipelineResults = await Promise.all(wave.chunks.map(async (ordinals) => {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
373
|
+
try {
|
|
374
|
+
const candidateResult = await this.runCandidateChunk(id, task, ordinals, workspace.workspacePath);
|
|
375
|
+
if (candidateResult.terminal)
|
|
376
|
+
admission.stopped = true;
|
|
377
|
+
if (candidateResult.error || admission.stopped)
|
|
378
|
+
return [candidateResult];
|
|
379
|
+
const selectedTask = await this.scriptTask(id);
|
|
380
|
+
const scriptOrdinals = immediateScriptOrdinals(selectedTask, this.scriptRecord(id).receivedOrdinals, ordinals);
|
|
381
|
+
if (!scriptOrdinals.length || admission.stopped)
|
|
382
|
+
return [candidateResult];
|
|
383
|
+
const scriptResults = await this.runScriptChunksRolling(id, selectedTask, scriptOrdinals, workspace.workspacePath, 1, admission);
|
|
384
|
+
return [candidateResult, ...scriptResults];
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
admission.stopped = true;
|
|
388
|
+
return [{ terminal: true, terminalKind: "isolation", affectedOrdinals: ordinals, error: error instanceof Error ? error.message : String(error) }];
|
|
389
|
+
}
|
|
375
390
|
}));
|
|
376
391
|
const results = pipelineResults.flat();
|
|
377
392
|
task = await this.scriptTask(id);
|
|
378
|
-
const terminalFailure = terminalScriptChunkFailure(results, this.scriptRecord(id).receivedOrdinals);
|
|
393
|
+
const terminalFailure = terminalScriptChunkFailure(results.filter((result) => result.terminalKind !== "review"), this.scriptRecord(id).receivedOrdinals);
|
|
379
394
|
if (terminalFailure) {
|
|
380
|
-
if (terminalFailure.terminalKind === "
|
|
381
|
-
throw new Error(terminalFailure.error);
|
|
382
|
-
if (terminalFailure.terminalKind === "delivery")
|
|
395
|
+
if (terminalFailure.terminalKind === "delivery" || terminalFailure.terminalKind === "isolation")
|
|
383
396
|
throw new Error(terminalFailure.error);
|
|
384
397
|
if (terminalFailure.terminalKind === "transport")
|
|
385
398
|
throw new Error(`创意或脚本阶段的本机 Codex 进程连续异常且自动恢复未成功,已停止当前任务且未提交缺失脚本(${terminalFailure.error})`);
|
|
@@ -408,7 +421,7 @@ export class WorkflowManager {
|
|
|
408
421
|
}
|
|
409
422
|
if (record.receivedOrdinals.length > receivedBefore)
|
|
410
423
|
chunkSizeIndex = 0;
|
|
411
|
-
if (selectedCandidateOrdinals(task).length > selectedBefore.length) {
|
|
424
|
+
if (selectedCandidateOrdinals(task).length > selectedBefore.length || voicePacingHeldOrdinals(record).length > heldBefore) {
|
|
412
425
|
candidateChunkSize = 2;
|
|
413
426
|
continue;
|
|
414
427
|
}
|
|
@@ -420,19 +433,18 @@ export class WorkflowManager {
|
|
|
420
433
|
throw new Error(`创意候选阶段已隔离到单条仍失败(${lastError}),请点击重试`);
|
|
421
434
|
}
|
|
422
435
|
const before = record.receivedOrdinals.length;
|
|
436
|
+
const heldBefore = voicePacingHeldOrdinals(record).length;
|
|
423
437
|
const chunkSize = chunkSizes[chunkSizeIndex];
|
|
424
438
|
record.chunkSize = chunkSize;
|
|
425
|
-
record.
|
|
426
|
-
record.message = `创意已选 ${selectedBefore.length}/${task.requested_count};正在立即写 ${wave.chunks.length} 个对应脚本子批(已回传 ${before}/${task.requested_count})`;
|
|
439
|
+
record.message = `创意已选 ${selectedBefore.length}/${task.requested_count};正在按完整蓝图大小分组并滚动写作(已回传 ${before}/${task.requested_count})`;
|
|
427
440
|
record.updatedAt = now();
|
|
428
441
|
this.save();
|
|
429
|
-
const
|
|
442
|
+
const readyOrdinals = immediateScriptOrdinals(task, settledOrdinals, missingOrdinals(task.requested_count, settledOrdinals));
|
|
443
|
+
const results = await this.runScriptChunksRolling(id, task, readyOrdinals, workspace.workspacePath);
|
|
430
444
|
task = await this.scriptTask(id);
|
|
431
|
-
const terminalFailure = terminalScriptChunkFailure(results, this.scriptRecord(id).receivedOrdinals);
|
|
445
|
+
const terminalFailure = terminalScriptChunkFailure(results.filter((result) => result.terminalKind !== "review"), this.scriptRecord(id).receivedOrdinals);
|
|
432
446
|
if (terminalFailure) {
|
|
433
|
-
if (terminalFailure.terminalKind === "
|
|
434
|
-
throw new Error(terminalFailure.error);
|
|
435
|
-
if (terminalFailure.terminalKind === "delivery")
|
|
447
|
+
if (terminalFailure.terminalKind === "delivery" || terminalFailure.terminalKind === "isolation")
|
|
436
448
|
throw new Error(terminalFailure.error);
|
|
437
449
|
if (terminalFailure.terminalKind === "transport")
|
|
438
450
|
throw new Error(`本机 Codex 脚本引擎连续异常且自动恢复未成功,已停止当前任务且未提交缺失脚本。诊断已保存在本机 Agent 日志中(${terminalFailure.error})`);
|
|
@@ -459,7 +471,7 @@ export class WorkflowManager {
|
|
|
459
471
|
chunkSizeIndex = 0;
|
|
460
472
|
continue;
|
|
461
473
|
}
|
|
462
|
-
const progressed = record.receivedOrdinals.length > before;
|
|
474
|
+
const progressed = record.receivedOrdinals.length > before || voicePacingHeldOrdinals(record).length > heldBefore;
|
|
463
475
|
if (progressed) {
|
|
464
476
|
chunkSizeIndex = 0;
|
|
465
477
|
continue;
|
|
@@ -479,6 +491,10 @@ export class WorkflowManager {
|
|
|
479
491
|
record.status = "complete";
|
|
480
492
|
record.message = flowCContentStrategy(task.creative_strategy) ? `全部 ${task.requested_count} 条脚本已回传(内容语义仍需审阅)` : `全部 ${task.requested_count} 条高质量脚本已回传`;
|
|
481
493
|
}
|
|
494
|
+
else if (voicePacingHeldOrdinals(record).length && !missingOrdinals(task.requested_count, [...record.receivedOrdinals, ...voicePacingHeldOrdinals(record)]).length) {
|
|
495
|
+
record.status = "review";
|
|
496
|
+
record.message = `已回传 ${record.receivedOrdinals.length}/${task.requested_count} 条;第 ${voicePacingHeldOrdinals(record).join("、")} 条口播仍明显超出可用时长,已单独保留待调整,其余脚本已处理完成。手动重试只处理这些未回传稿,已回传稿保持不变`;
|
|
497
|
+
}
|
|
482
498
|
else
|
|
483
499
|
throw new Error(`中心校验尚未通过(${record.receivedOrdinals.length}/${task.requested_count} 条),请点击重试`);
|
|
484
500
|
}
|
|
@@ -506,6 +522,82 @@ export class WorkflowManager {
|
|
|
506
522
|
this.scheduleScript(id);
|
|
507
523
|
}
|
|
508
524
|
}
|
|
525
|
+
/** A finished lane refills immediately; a fatal result closes admission but drains every admitted lane. */
|
|
526
|
+
async runScriptChunksRolling(id, initialTask, ordinals, cwd, concurrency = FLOW_C_CODEX_WORKER_CONCURRENCY, admission = { stopped: false }) {
|
|
527
|
+
const durationSeconds = Number(initialTask.duration_seconds || 10);
|
|
528
|
+
const chunkSizes = flowCScriptChunkSizes(durationSeconds);
|
|
529
|
+
const queue = flowCScriptChunks(durationSeconds, [...new Set(ordinals)], chunkSizes[0]);
|
|
530
|
+
const results = [];
|
|
531
|
+
let task = initialTask;
|
|
532
|
+
const settled = () => new Set([...this.scriptRecord(id).receivedOrdinals, ...voicePacingHeldOrdinals(this.scriptRecord(id))]);
|
|
533
|
+
const stopWithError = (error, affectedOrdinals) => {
|
|
534
|
+
admission.stopped = true;
|
|
535
|
+
results.push({ terminal: true, terminalKind: "isolation", affectedOrdinals, error: error instanceof Error ? error.message : String(error) });
|
|
536
|
+
};
|
|
537
|
+
const worker = async () => {
|
|
538
|
+
while (!admission.stopped && queue.length) {
|
|
539
|
+
// Ownership is acquired synchronously, before the first await. A
|
|
540
|
+
// queue entry is never visible to another lane while in flight.
|
|
541
|
+
let current = queue.shift().filter((ordinal) => !settled().has(ordinal));
|
|
542
|
+
if (!current.length)
|
|
543
|
+
continue;
|
|
544
|
+
try {
|
|
545
|
+
const record = this.scriptRecord(id);
|
|
546
|
+
const selected = selectedCandidatesForOrdinals(task, current);
|
|
547
|
+
const recoverable = new Set([
|
|
548
|
+
...(record.pendingScriptJobs || []).map((job) => job.ordinal),
|
|
549
|
+
...(record.voicePacingReviewJobs || []).filter((job) => String(job.expectedCandidateRevision || "") === String(selected.get(job.ordinal)?.candidateRevision || "")).map((job) => job.ordinal),
|
|
550
|
+
]);
|
|
551
|
+
// Exact saved delivery/repair must not be gated by a new
|
|
552
|
+
// writing prompt. Isolate it so an overlarge sibling cannot
|
|
553
|
+
// prevent its recovery or force its regeneration.
|
|
554
|
+
const retained = current.filter((ordinal) => recoverable.has(ordinal));
|
|
555
|
+
if (retained.length && current.length > 1) {
|
|
556
|
+
const fresh = current.filter((ordinal) => !recoverable.has(ordinal));
|
|
557
|
+
queue.unshift(...retained.map((ordinal) => [ordinal]), ...(fresh.length ? [fresh] : []));
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (!retained.length) {
|
|
561
|
+
const promptTask = scriptPromptTask(task, record);
|
|
562
|
+
const planned = promptSafeScriptChunks(id, promptTask, current);
|
|
563
|
+
current = planned[0];
|
|
564
|
+
queue.unshift(...planned.slice(1));
|
|
565
|
+
}
|
|
566
|
+
if (admission.stopped)
|
|
567
|
+
break;
|
|
568
|
+
record.chunkSize = current.length;
|
|
569
|
+
record.attempts += 1;
|
|
570
|
+
record.message = `正在滚动写作,本次 ${current.length} 条;完成即补下一组(已回传 ${record.receivedOrdinals.length}/${task.requested_count})`;
|
|
571
|
+
record.updatedAt = now();
|
|
572
|
+
this.save();
|
|
573
|
+
const result = await this.runScriptChunk(id, task, current, cwd);
|
|
574
|
+
results.push(result);
|
|
575
|
+
// Stop before the ACK refresh can yield. Other in-flight
|
|
576
|
+
// writes still finish and keep their exact pending/ACK state.
|
|
577
|
+
if (result.terminal && result.terminalKind !== "review" || scriptCreativeReplanOrdinals([result]).length)
|
|
578
|
+
admission.stopped = true;
|
|
579
|
+
task = await this.scriptTask(id);
|
|
580
|
+
const remaining = current.filter((ordinal) => !settled().has(ordinal));
|
|
581
|
+
if (admission.stopped || !remaining.length)
|
|
582
|
+
continue;
|
|
583
|
+
const nextSize = chunkSizes.find((size) => size < current.length);
|
|
584
|
+
if (!nextSize) {
|
|
585
|
+
stopWithError(new Error(`本机 Codex 已隔离到 ordinal ${remaining.join(", ")} 单条仍未回传(${result.error || "未返回可用的结构化脚本"}),请点击重试`), remaining);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
// Only this failed entry descends. Successful siblings never
|
|
589
|
+
// reset it to a larger size or retry its in-flight ordinals.
|
|
590
|
+
queue.unshift(...flowCScriptChunks(durationSeconds, remaining, nextSize));
|
|
591
|
+
}
|
|
592
|
+
catch (error) {
|
|
593
|
+
stopWithError(error, current);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
const count = Math.max(1, Math.min(FLOW_C_CODEX_WORKER_CONCURRENCY, Math.floor(concurrency) || 1));
|
|
598
|
+
await Promise.all(Array.from({ length: count }, () => worker()));
|
|
599
|
+
return results;
|
|
600
|
+
}
|
|
509
601
|
/** Analyze each product once; image 1 owns identity and later images may only corroborate visible evidence. */
|
|
510
602
|
async ensureProductExecutionProfiles(id, task, cwd) {
|
|
511
603
|
const record = this.scriptRecord(id);
|
|
@@ -663,6 +755,16 @@ export class WorkflowManager {
|
|
|
663
755
|
const candidate = selectedForRecovery.get(job.ordinal);
|
|
664
756
|
return String(job.expectedCandidateRevision || "") === String(candidate?.candidateRevision || "");
|
|
665
757
|
});
|
|
758
|
+
// A newer pacing policy may accept an untouched old draft. Deliver
|
|
759
|
+
// that exact draft rather than regenerating its script or speech.
|
|
760
|
+
const acceptedStoredReview = storedReview.filter((job) => !flowCVoicePacingRepairIssues([job], { targetLanguage }).length);
|
|
761
|
+
if (acceptedStoredReview.length) {
|
|
762
|
+
this.noteScriptContentAdvisories(id, task, acceptedStoredReview);
|
|
763
|
+
await this.submitGeneratedScriptJobs(id, acceptedStoredReview);
|
|
764
|
+
ordinals = ordinals.filter((ordinal) => !this.scriptRecord(id).receivedOrdinals.includes(ordinal));
|
|
765
|
+
if (!ordinals.length)
|
|
766
|
+
return { terminal: false };
|
|
767
|
+
}
|
|
666
768
|
const repairableStoredReview = storedReview.filter((job) => flowCVoicePacingRepairIssues([job], { targetLanguage }).length > 0);
|
|
667
769
|
if (repairableStoredReview.length) {
|
|
668
770
|
const reviewResult = await this.repairVoicePacingJobs(id, task, repairableStoredReview, cwd);
|
|
@@ -682,9 +784,7 @@ export class WorkflowManager {
|
|
|
682
784
|
const segmentSeconds = flowCTaskSegmentSeconds(task);
|
|
683
785
|
let prompt;
|
|
684
786
|
try {
|
|
685
|
-
const promptTask =
|
|
686
|
-
? { ...task, received_ordinals: activeRecord.receivedOrdinals, content_recent_scripts: mergeFlowCContentSummaries([...(Array.isArray(task.content_recent_scripts) ? task.content_recent_scripts : []), ...(activeRecord.contentSummaries || [])], [], activeRecord.receivedOrdinals) }
|
|
687
|
-
: task;
|
|
787
|
+
const promptTask = scriptPromptTask(task, activeRecord);
|
|
688
788
|
prompt = scriptChunkPrompt(id, promptTask, ordinals, rewriteAttempt);
|
|
689
789
|
}
|
|
690
790
|
catch (error) {
|
|
@@ -842,7 +942,7 @@ export class WorkflowManager {
|
|
|
842
942
|
const eligible = current.filter((job) => voicePacingRepairAttemptCount(record, job) < FLOW_C_VOICE_PACING_REPAIR_MAX_ATTEMPTS);
|
|
843
943
|
const exhausted = current.filter((job) => !eligible.includes(job));
|
|
844
944
|
if (!eligible.length)
|
|
845
|
-
return
|
|
945
|
+
return this.holdVoicePacingReview(id, exhausted.map((job) => job.ordinal), "本机已记录本次自动修复机会,后台恢复不会再次调用模型");
|
|
846
946
|
const localization = compactTaskLocalization(task);
|
|
847
947
|
const targetLanguage = localization.targetLanguage || task.target_language || localization.targetLocale;
|
|
848
948
|
const selected = selectedCandidatesForOrdinals(task, eligible.map((job) => job.ordinal));
|
|
@@ -863,23 +963,23 @@ export class WorkflowManager {
|
|
|
863
963
|
catch (error) {
|
|
864
964
|
const received = this.scriptRecord(id).receivedOrdinals;
|
|
865
965
|
const missing = [...exhausted.map((job) => job.ordinal), ...ordinals].filter((ordinal) => !received.includes(ordinal));
|
|
866
|
-
return missing.length ?
|
|
966
|
+
return missing.length ? this.holdVoicePacingReview(id, missing, error instanceof Error ? error.message : "口播修复回合异常") : { terminal: false };
|
|
867
967
|
}
|
|
868
968
|
this.emitScriptStage(id, ordinals, "voice_repair", result.timings.queueWaitMs + result.timings.threadStartMs + result.timings.modelMs);
|
|
869
969
|
const missingAfterTurn = [...exhausted.map((job) => job.ordinal), ...ordinals].filter((ordinal) => !this.scriptRecord(id).receivedOrdinals.includes(ordinal));
|
|
870
970
|
if (!missingAfterTurn.length)
|
|
871
971
|
return { terminal: false };
|
|
872
972
|
if (!result.ok)
|
|
873
|
-
return
|
|
973
|
+
return this.holdVoicePacingReview(id, missingAfterTurn, result.error || "Codex 未返回口播修复稿");
|
|
874
974
|
if (!result.text)
|
|
875
|
-
return
|
|
975
|
+
return this.holdVoicePacingReview(id, missingAfterTurn, "Codex 未返回口播修复稿");
|
|
876
976
|
let candidates;
|
|
877
977
|
try {
|
|
878
978
|
candidates = parseFlowCScriptOutput(result.text, ordinals, segmentSeconds);
|
|
879
979
|
}
|
|
880
980
|
catch (error) {
|
|
881
981
|
const missing = missingAfterTurn.filter((ordinal) => !this.scriptRecord(id).receivedOrdinals.includes(ordinal));
|
|
882
|
-
return missing.length ?
|
|
982
|
+
return missing.length ? this.holdVoicePacingReview(id, missing, error instanceof Error ? error.message : "口播修复稿未通过结构校验") : { terminal: false };
|
|
883
983
|
}
|
|
884
984
|
const byOrdinal = new Map(candidates.map((job) => [job.ordinal, job]));
|
|
885
985
|
const accepted = [];
|
|
@@ -894,7 +994,7 @@ export class WorkflowManager {
|
|
|
894
994
|
const repaired = applyFlowCVoicePacingRepair(original, candidate, frameworkOrdinals.includes(original.ordinal), segmentSeconds);
|
|
895
995
|
const remaining = flowCVoicePacingRepairIssues([repaired], { targetLanguage });
|
|
896
996
|
if (remaining.length) {
|
|
897
|
-
failed.set(original.ordinal, `仍有 ${remaining.length}
|
|
997
|
+
failed.set(original.ordinal, `仍有 ${remaining.length} 处极端过密口播`);
|
|
898
998
|
continue;
|
|
899
999
|
}
|
|
900
1000
|
accepted.push(repaired);
|
|
@@ -903,6 +1003,10 @@ export class WorkflowManager {
|
|
|
903
1003
|
failed.set(original.ordinal, error instanceof Error ? error.message : "修复稿改变了受保护字段");
|
|
904
1004
|
}
|
|
905
1005
|
}
|
|
1006
|
+
// Persist failed siblings before any repaired sibling's POST. A lost
|
|
1007
|
+
// response must retain both the exact deliverable and isolated reviews.
|
|
1008
|
+
if (failed.size)
|
|
1009
|
+
this.holdVoicePacingReview(id, [...failed.keys()], [...failed.entries()].map(([ordinal, reason]) => `ordinal ${ordinal}: ${reason}`).join(";"));
|
|
906
1010
|
const stillMissing = accepted.filter((job) => !this.scriptRecord(id).receivedOrdinals.includes(job.ordinal));
|
|
907
1011
|
if (stillMissing.length) {
|
|
908
1012
|
this.noteScriptContentAdvisories(id, task, stillMissing);
|
|
@@ -911,9 +1015,25 @@ export class WorkflowManager {
|
|
|
911
1015
|
for (const ordinal of this.scriptRecord(id).receivedOrdinals)
|
|
912
1016
|
failed.delete(ordinal);
|
|
913
1017
|
if (failed.size)
|
|
914
|
-
return
|
|
1018
|
+
return this.holdVoicePacingReview(id, [...failed.keys()], [...failed.entries()].map(([ordinal, reason]) => `ordinal ${ordinal}: ${reason}`).join(";"));
|
|
915
1019
|
return { terminal: false };
|
|
916
1020
|
}
|
|
1021
|
+
holdVoicePacingReview(id, ordinals, reason) {
|
|
1022
|
+
const record = this.scriptRecord(id);
|
|
1023
|
+
const missing = ordinals.filter((ordinal) => !record.receivedOrdinals.includes(ordinal));
|
|
1024
|
+
if (!missing.length)
|
|
1025
|
+
return { terminal: false };
|
|
1026
|
+
const holds = new Map((record.voicePacingReviewHolds || []).map((hold) => [hold.ordinal, hold]));
|
|
1027
|
+
for (const job of record.voicePacingReviewJobs || []) {
|
|
1028
|
+
if (missing.includes(job.ordinal))
|
|
1029
|
+
holds.set(job.ordinal, { ordinal: job.ordinal, candidateRevision: voicePacingCandidateRevision(job), reason });
|
|
1030
|
+
}
|
|
1031
|
+
record.voicePacingReviewHolds = [...holds.values()].sort((left, right) => left.ordinal - right.ordinal);
|
|
1032
|
+
record.message = `第 ${missing.join("、")} 条口播需单独调整,已保留原稿;继续处理其余脚本(已回传 ${record.receivedOrdinals.length}/${record.requestedCount} 条)`;
|
|
1033
|
+
record.updatedAt = now();
|
|
1034
|
+
this.save();
|
|
1035
|
+
return voicePacingReviewFailure(missing, reason);
|
|
1036
|
+
}
|
|
917
1037
|
runVoicePacingRepairTurn(id, prompt, cwd, durationSeconds, count, segmentSeconds) {
|
|
918
1038
|
return runCodexWorkflowTurn(prompt, this.emit, {
|
|
919
1039
|
cwd,
|
|
@@ -1141,6 +1261,30 @@ function pruneVoicePacingRepairAttempts(record) {
|
|
|
1141
1261
|
}
|
|
1142
1262
|
function resetVoicePacingRepairAttempts(record) {
|
|
1143
1263
|
delete record.voicePacingRepairAttempts;
|
|
1264
|
+
delete record.voicePacingReviewHolds;
|
|
1265
|
+
}
|
|
1266
|
+
function voicePacingHeldOrdinals(record) {
|
|
1267
|
+
return [...new Set((record.voicePacingReviewHolds || []).map((hold) => hold.ordinal).filter((ordinal) => !record.receivedOrdinals.includes(ordinal)))].sort((left, right) => left - right);
|
|
1268
|
+
}
|
|
1269
|
+
/** ACK and exact pending delivery always win; holds belong to one candidate revision. */
|
|
1270
|
+
function reconcileVoicePacingReviewHolds(record, task) {
|
|
1271
|
+
if (!record.voicePacingReviewHolds)
|
|
1272
|
+
return;
|
|
1273
|
+
const selected = task ? selectedCandidatesForOrdinals(task, record.voicePacingReviewHolds.map((hold) => hold.ordinal)) : undefined;
|
|
1274
|
+
const localization = task ? compactTaskLocalization(task) : undefined;
|
|
1275
|
+
const targetLanguage = localization?.targetLanguage || task?.target_language || localization?.targetLocale;
|
|
1276
|
+
record.voicePacingReviewHolds = record.voicePacingReviewHolds.filter((hold) => {
|
|
1277
|
+
if (record.receivedOrdinals.includes(hold.ordinal) || record.pendingScriptJobs?.some((job) => job.ordinal === hold.ordinal))
|
|
1278
|
+
return false;
|
|
1279
|
+
const original = record.voicePacingReviewJobs?.find((job) => job.ordinal === hold.ordinal && voicePacingCandidateRevision(job) === hold.candidateRevision);
|
|
1280
|
+
if (!original)
|
|
1281
|
+
return false;
|
|
1282
|
+
if (selected && String(selected.get(hold.ordinal)?.candidateRevision || "") !== hold.candidateRevision)
|
|
1283
|
+
return false;
|
|
1284
|
+
return !task || flowCVoicePacingRepairIssues([original], { targetLanguage }).length > 0;
|
|
1285
|
+
});
|
|
1286
|
+
if (!record.voicePacingReviewHolds.length)
|
|
1287
|
+
delete record.voicePacingReviewHolds;
|
|
1144
1288
|
}
|
|
1145
1289
|
function pacingSegments(value) {
|
|
1146
1290
|
const job = pacingObject(value);
|
|
@@ -1224,7 +1368,7 @@ export function applyFlowCVoicePacingRepair(originalValue, candidateValue, prese
|
|
|
1224
1368
|
function voicePacingReviewFailure(ordinals, reason) {
|
|
1225
1369
|
const scoped = [...new Set(ordinals.map(Number).filter((ordinal) => Number.isInteger(ordinal) && ordinal > 0))].sort((left, right) => left - right);
|
|
1226
1370
|
return {
|
|
1227
|
-
error: `${FLOW_C_VOICE_PACING_REVIEW_REQUIRED}: ordinal ${scoped.join(", ")}
|
|
1371
|
+
error: `${FLOW_C_VOICE_PACING_REVIEW_REQUIRED}: ordinal ${scoped.join(", ")} 的口播在唯一一次定向调整后仍需单独检查;原稿保存在本机待审区,其余脚本继续处理。${reason}。可调整后手动重试,已回传稿保持不变`,
|
|
1228
1372
|
terminal: true,
|
|
1229
1373
|
terminalKind: "review",
|
|
1230
1374
|
affectedOrdinals: scoped,
|
|
@@ -1236,12 +1380,15 @@ export function terminalScriptChunkError(results) {
|
|
|
1236
1380
|
}
|
|
1237
1381
|
export function terminalScriptChunkFailure(results, receivedOrdinals = []) {
|
|
1238
1382
|
const received = new Set(receivedOrdinals.map(Number).filter(Number.isInteger));
|
|
1239
|
-
|
|
1383
|
+
const failures = results.filter((result) => {
|
|
1240
1384
|
if (!result.terminal)
|
|
1241
1385
|
return false;
|
|
1242
1386
|
const affected = Array.isArray(result.affectedOrdinals) ? result.affectedOrdinals.map(Number).filter((ordinal) => Number.isInteger(ordinal) && ordinal > 0) : [];
|
|
1243
1387
|
return !affected.length || affected.some((ordinal) => !received.has(ordinal));
|
|
1244
1388
|
});
|
|
1389
|
+
// An isolated review must not hide a sibling's genuine delivery/transport
|
|
1390
|
+
// failure when nested revision or rewrite results are aggregated.
|
|
1391
|
+
return failures.find((result) => result.terminalKind !== "review") || failures[0];
|
|
1245
1392
|
}
|
|
1246
1393
|
export function scriptCreativeReplanOrdinals(results) {
|
|
1247
1394
|
return [...new Set(results.flatMap((result) => (result && typeof result === "object" ? result.replanOrdinals || [] : [])).filter((ordinal) => Number.isInteger(ordinal) && ordinal > 0))].sort((left, right) => left - right);
|
|
@@ -1329,6 +1476,29 @@ class FlowCPromptPayloadTooLargeError extends Error {
|
|
|
1329
1476
|
export function isFlowCPromptPayloadTooLarge(error) {
|
|
1330
1477
|
return Boolean(error && typeof error === "object" && error.code === "FLOW_C_PROMPT_PAYLOAD_TOO_LARGE");
|
|
1331
1478
|
}
|
|
1479
|
+
function scriptPromptTask(task, record) {
|
|
1480
|
+
return flowCContentStrategy(task.creative_strategy)
|
|
1481
|
+
? { ...task, received_ordinals: record.receivedOrdinals, content_recent_scripts: mergeFlowCContentSummaries([...(Array.isArray(task.content_recent_scripts) ? task.content_recent_scripts : []), ...(record.contentSummaries || [])], [], record.receivedOrdinals) }
|
|
1482
|
+
: task;
|
|
1483
|
+
}
|
|
1484
|
+
/** Preflight the real immutable prompt; only a size error permits subdividing it. */
|
|
1485
|
+
export function promptSafeScriptChunks(id, task, ordinals) {
|
|
1486
|
+
if (!ordinals.length)
|
|
1487
|
+
return [];
|
|
1488
|
+
try {
|
|
1489
|
+
scriptChunkPrompt(id, task, ordinals);
|
|
1490
|
+
return [[...ordinals]];
|
|
1491
|
+
}
|
|
1492
|
+
catch (error) {
|
|
1493
|
+
if (!isFlowCPromptPayloadTooLarge(error))
|
|
1494
|
+
throw error;
|
|
1495
|
+
if (ordinals.length === 1)
|
|
1496
|
+
throw new Error(`ordinal ${ordinals[0]} 的完整蓝图单条仍超限,已停止写作并保留任务与已完成脚本。${error.message}`);
|
|
1497
|
+
const durationSeconds = Number(task.duration_seconds || 10);
|
|
1498
|
+
const smallerSize = flowCScriptChunkSizes(durationSeconds).find((size) => size < ordinals.length) || 1;
|
|
1499
|
+
return flowCScriptChunks(durationSeconds, ordinals, smallerSize).flatMap((chunk) => promptSafeScriptChunks(id, task, chunk));
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1332
1502
|
export function productExecutionProfilePrompt(product, contractVersion = FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION) {
|
|
1333
1503
|
const referenceCount = contractVersion === FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION
|
|
1334
1504
|
? Math.max(1, Math.min(5, product.productImageUrlsInExactOrder?.length || 0))
|
|
@@ -1423,22 +1593,28 @@ export function scriptChunkPrompt(id, task, ordinals, rewriteAttempt = 0) {
|
|
|
1423
1593
|
segmentSeconds,
|
|
1424
1594
|
});
|
|
1425
1595
|
const generatedMontageMethod = flowCGeneratedMontagePrompt(generatedMontageOrdinals, segmentSeconds);
|
|
1596
|
+
const conciseExecution = Boolean(contentStrategy || generatedMontageOrdinals.length);
|
|
1597
|
+
const selectedPayload = selectedBlueprintPromptPayload([...selected.values()], duration, contentStrategy);
|
|
1598
|
+
const blueprintPayload = conciseExecution ? unescapeBlueprintPromptPayload(selectedPayload) : selectedPayload;
|
|
1599
|
+
const executionWriting = conciseExecution
|
|
1600
|
+
? '\n执行稿只写实际拍摄/生成需要的信息,不写创作理由、评分或模板讲解。visual 用简洁制作英文保留必要的主体位置、景别、运镜、光线、材质、动作与可见结果;已在共享设定确定且本镜未变化的内容不反复铺陈。evidence 简短说明本镜具体可见的证明或结果,不复述整段 visual;emotionalNote、voiceCue 用准确短语,不写情绪分析段落。镜头数量由所选蓝图的因果、节奏和目标时长决定,不为凑字段加镜,不为缩短文字删减有效镜头、动作、商品依据或用户锁定的原文对白。\n'
|
|
1601
|
+
: '';
|
|
1426
1602
|
return `你正在后台完成 Flow C 脚本交接 ${id},只写 ordinal:${ordinals.join(", ")};映射:${scriptProductAssignments(task.product_quantities, ordinals)}。
|
|
1427
1603
|
目标市场:${task.market}。目标口播语言:${targetVoiceLanguage}。结构化当地化事实(国家、语言/locale、出镜者、受众、生活场景、创作者口吻和CTA彼此独立;不得从国家推断族群):${compactJson(localization, 3_000, "当前脚本子批的当地化事实")}。商品事实:${compactJson(productFacts, 12_000, "当前脚本子批的商品事实")}
|
|
1428
1604
|
中心已完成当前 ordinal 的创意选择。executionBlueprints 只保存一次共享的模型无关创意蓝图(可能来自已学习爆款,也可能来自已筛选带货形式卡);productAdaptations 只保存一次每个不同的当前商品执行卡;ordinalBindings ${contentStrategy ? "用 blueprintRef/adaptationRef、variationSeed 和逐条 contentDirection" : "只用 blueprintRef/adaptationRef 和 variationSeed"} 映射到具体脚本。存在 blueprintRef 时,对应 executionBlueprint 是唯一结构权威;没有 blueprintRef 时,对应 productAdaptation 是用户框架的唯一执行权威。相同蓝图可供同一商品整批复用,直接替换成当前商品执行,不要生成候选、重新选题或重新评分:
|
|
1429
|
-
${compactJson(
|
|
1605
|
+
${compactJson(blueprintPayload, FLOW_C_SELECTED_BLUEPRINT_PROMPT_MAX_CHARS, "当前脚本子批的共享蓝图与商品适配")}
|
|
1430
1606
|
${rewriteInstruction}
|
|
1431
1607
|
${durationRules}
|
|
1432
1608
|
写作要求:
|
|
1433
|
-
1. visualHook、conflict、escalation/turn、productIntervention、visibleProof、purchaseReason、callbackMotivation 与所有 shots 必须执行选中卡的 visualPremise、firstFrame、spectacleEscalation、productProofAction、purchaseReason、truthBoundary 以及七项视觉执行字段;保留所选蓝图的因果时间线、节拍比例、镜头/剪辑节奏、动作压力、证明方式和购买逻辑,只替换当前商品、人物、场景与当地口播槽位,不得稀释构图或换成普通模板。爆款来源的旧商品身份/动作必须替换;形式卡只提供抽象创意结构,绝不能反向改变现有首帧、分段、${videoModelName}或参考图制作方式。形式库中的工厂、仓库、街访、使用者体验、主理人、探店、补货、对比、促销和耐用形式都可以用 AI 人物与场景正常演绎;缺少现实素材或事实证据不是脚本失败条件,必须按 executionBlueprint.executionAdaptation 保留所选形式,同时不把演绎的角色、地点、订单、库存、销量、价格、身份或经历写成已核验的现实事实。若 productAdaptation.templateMatch.actionCompatibility 的 openingAction、proofAction 或 spectacleAction 为 false,对应 productAdaptation 的 firstFrame、productProofAction 或 spectacleEscalation 对商品动作内容具有绝对优先级,必须删除其旧商品动作,不得折中混用。productProofAction 必须同时写清一个商品图或执行档案能支持的可见动作及其镜头内可见结果;purchaseReason 只解释该结果为何值得目标用户购买,不能变成泛泛 CTA。
|
|
1609
|
+
1. ${conciseExecution ? "所有 structured shots" : "visualHook、conflict、escalation/turn、productIntervention、visibleProof、purchaseReason、callbackMotivation 与所有 shots"} 必须执行选中卡的 visualPremise、firstFrame、spectacleEscalation、productProofAction、purchaseReason、truthBoundary 以及七项视觉执行字段;保留所选蓝图的因果时间线、节拍比例、镜头/剪辑节奏、动作压力、证明方式和购买逻辑,只替换当前商品、人物、场景与当地口播槽位,不得稀释构图或换成普通模板。爆款来源的旧商品身份/动作必须替换;形式卡只提供抽象创意结构,绝不能反向改变现有首帧、分段、${videoModelName}或参考图制作方式。形式库中的工厂、仓库、街访、使用者体验、主理人、探店、补货、对比、促销和耐用形式都可以用 AI 人物与场景正常演绎;缺少现实素材或事实证据不是脚本失败条件,必须按 executionBlueprint.executionAdaptation 保留所选形式,同时不把演绎的角色、地点、订单、库存、销量、价格、身份或经历写成已核验的现实事实。若 productAdaptation.templateMatch.actionCompatibility 的 openingAction、proofAction 或 spectacleAction 为 false,对应 productAdaptation 的 firstFrame、productProofAction 或 spectacleEscalation 对商品动作内容具有绝对优先级,必须删除其旧商品动作,不得折中混用。productProofAction 必须同时写清一个商品图或执行档案能支持的可见动作及其镜头内可见结果;purchaseReason 只解释该结果为何值得目标用户购买,不能变成泛泛 CTA。
|
|
1434
1610
|
2. 严格执行 executionBlueprints 中的 sourceDurationSeconds → targetDurationSeconds 和 retimingMode:expand 只增加有用的证明停留、使用语境与购买理由,compress 只压缩重复、冗余铺垫和转场,same 保持原节拍比例;三种模式都必须保留 Hook、证明和购买因果,禁止整体拉伸、截断或机械加速。
|
|
1435
|
-
3. 每条生成一份稳定 voiceProfile;每段只写简短 voiceCue。口播必须使用${targetVoiceLanguage},并执行 creatorVoiceStyle/ctaStyle;未提供 creatorVoiceStyle 时使用当地 TikTok 带货创作者真实会说的口吻。${contentStrategy || generatedMontageOrdinals.length > 0 ? `不是逐字翻译。Hook → Body/visible proof → Close 是画面叙事结构,不等于口播结构;镜头数量不等于台词数量。先基于已经确定的画面,为每个局部${segmentSeconds}秒拟一份按实际镜头时长能自然说完的很短当地口播:用户锁定对白先以原词计入并优先给承载镜头足够秒数,不得删改;除此之外整段只留1–2个必要短句。再按完整词组或自然分句分配到真正展示相关动作/判断的镜头,允许一句在连续相关 cuts 间自然延续,但每镜 voiceover 只存该镜实际说出的片段,不逐镜复述 visual
|
|
1611
|
+
3. 每条生成一份稳定 voiceProfile;每段只写简短 voiceCue。口播必须使用${targetVoiceLanguage},并执行 creatorVoiceStyle/ctaStyle;未提供 creatorVoiceStyle 时使用当地 TikTok 带货创作者真实会说的口吻。${contentStrategy || generatedMontageOrdinals.length > 0 ? `不是逐字翻译。Hook → Body/visible proof → Close 是画面叙事结构,不等于口播结构;镜头数量不等于台词数量。先基于已经确定的画面,为每个局部${segmentSeconds}秒拟一份按实际镜头时长能自然说完的很短当地口播:用户锁定对白先以原词计入并优先给承载镜头足够秒数,不得删改;除此之外整段只留1–2个必要短句。再按完整词组或自然分句分配到真正展示相关动作/判断的镜头,允许一句在连续相关 cuts 间自然延续,但每镜 voiceover 只存该镜实际说出的片段,不逐镜复述 visual。允许符合当地带货口吻的自然偏快语速,偶尔密一点无需重写;不要夸张急促、吞字或失去可懂度。连续相关镜头可承接同一句口播;只有明显超出实际可用时长时才删除模型自行增加的赘句,不为填满有声镜硬塞台词;其它镜头默认 voiceover="none",只在 soundBgm 保留动作现场声。结尾的一个简短自然 CTA 并入上述已定短句;若锁定对白占满可用口播时长就不再追加 CTA。用户锁定的对白、原框架和目标语言优先;上述拟稿与分配在同一次写作内完成,不输出中间声音轨。` : '不是逐字翻译,整条内容按 Hook → Body/visible proof → Close 组织,短句口语化且与当前镜头可见动作同步,结尾只给一个简短自然 CTA。'}只使用少量可信的 localSceneProfile/audienceContext 日常细节;presenterContext 没填写时使用普通创作者,不得把国家代码、语言或地区自动等同于人物族群。商品事实没有提供时不得编造价格、折扣、销量、库存或稀缺性。商品标题只用于理解商品类型与事实,所有输出不得主动写出或念出品牌名、品牌广告语、包装文案,也不得要求模型把品牌字样变得更清晰;商品外观与已有标识的位置、比例、颜色和版式只服从第1张产品参考图。TikTok/TikTok Shop 只用于定义这条写作规则,不得把平台名写入 voiceover、onScreenText、visual、soundBgm、emotionalNote、openingState、endingState 或任何会进入故事板/视频的导演字段。导演说明统一用简洁制作英文;这里只做写作指令,不做语言检测字段。
|
|
1436
1612
|
4. 每段永远是独立 0–${segmentSeconds} 秒,含 1–8 个按剧情需要决定的 ${contentStrategy ? `shots。每镜 voiceover 字段必须存在,但不等于必须有台词:只填上一步分配给该镜的原句,其余填非空静默标记 "none",绝不让配音念出 none;每镜同时给出准确 onScreenText、evidence、soundBgm、emotionalNote,并提供每段 endingState。先写末镜实际动作,再在末镜 visual 最后用一个短句明确第${segmentSeconds}秒的最终画面;endingState.endingFrame 逐字复用这个收尾短句,其余 endingState 字段也只描述同一瞬间,不另编姿势或动作。已走出画面的人不能仍在画内回头,已经落地的脚不能又悬在半空,已经完成的动作不能仍写为待完成;确已完成且无续接动作时 unfinishedAction 写 none。下一段首镜从这个真实终点继续,不重置人物、物体或动作;最终一段也要检查,不能只保证跨段字段相等` : 'shots、准确 voiceover、onScreenText、evidence、soundBgm、emotionalNote 与 endingState'};模型不要输出 continuity 或 continuityMode。
|
|
1437
1613
|
5. shot.visual 要直接写出主体位置、景别、光线、材质、动作峰值和当地自然环境,并保留真实皮肤、细发、布料/商品微纹理和实景混合光;禁止塑料蜡感、静态举产品、品牌广告片或电视购物。允许执行所选形式的 AI 工厂/仓库/零售/人物场景,但不得声称它们是该商家真实工厂、产地、生产档案、订单、库存、销量、实名客户证言或线下价格。
|
|
1438
1614
|
6. 多段视频的后一段剧情承接前段 endingState,但每段仍只使用自己的局部 0–${segmentSeconds} 秒执行内容。
|
|
1439
1615
|
7. 不要输出 creativePlan、executionBindings 或 masterScript;这些字段由 Agent 从选中蓝图确定性回填/合成,避免重复 token 和脆弱的文字绑定校验。若 selectionMode=user-framework,用户原框架的开头、事件顺序、核心剧情和结尾仍是最高权威。
|
|
1440
1616
|
8. 商品身份以按顺序提供的原商品图为最高权威:第1张是主SKU身份图,当前商品标题和大概类目只补充商品用途与事实,后续图片只是同一SKU补充视角。任何爆款源商品、首帧、故事板或上一段尾帧都不得改变商品颜色、款式、结构、比例、材质、包装、标识位置、部件、配件和实际套装数量;修正身份时不得降低动作、运镜、节奏、景深、真实感或画质。商品本体或包装表面的印花、图案、微纹理、已印文字/字符、图标、色块、行距、相对位置和朝向全部视为不可编辑的原图纹理;不得在 shot.visual、onScreenText、voiceover、evidence、openingState、endingState 或任何导演字段中要求重写、翻译、替换、删改、重排、镜像这些表面内容,也不得要求生成“另一段清晰可读文案”。新增屏幕字只能是与商品像素分离的场景叠加字幕,绝不能印到商品或包装上。旋转、翻转、弯折或开合商品时,完整表面纹理必须随实体整体运动,不能漂移、翻面后复写或重新生成。若 executionBlueprint 或 productAdaptation 与本规则冲突,只保留其钩子、动作因果和镜头节奏,改写构图、机位或表演,绝不改商品表面。
|
|
1441
|
-
${contentMethod}${generatedMontageMethod}固定使用 GPT-5.6 Terra 中等推理。媒体制作方式与分镜布局由中心在脚本完成后管理,不得影响这里的创意、镜头、口播或结构化脚本。只返回当前结构化 jobs,不调用工具、不创建任何图片或视频任务。`;
|
|
1617
|
+
${contentMethod}${generatedMontageMethod}${executionWriting}固定使用 GPT-5.6 Terra 中等推理。媒体制作方式与分镜布局由中心在脚本完成后管理,不得影响这里的创意、镜头、口播或结构化脚本。只返回当前结构化 jobs,不调用工具、不创建任何图片或视频任务。`;
|
|
1442
1618
|
}
|
|
1443
1619
|
export function creativeCandidatePrompt(id, task, ordinals) {
|
|
1444
1620
|
const products = relevantProductInputs(task, ordinals);
|
|
@@ -1636,6 +1812,21 @@ export function selectedBlueprintPromptPayload(values, fallbackTargetDurationSec
|
|
|
1636
1812
|
ordinalBindings,
|
|
1637
1813
|
};
|
|
1638
1814
|
}
|
|
1815
|
+
/** Avoid encoding a complete JSON blueprint as an escaped JSON string again. */
|
|
1816
|
+
export function unescapeBlueprintPromptPayload(payload) {
|
|
1817
|
+
return {
|
|
1818
|
+
...payload,
|
|
1819
|
+
executionBlueprints: payload.executionBlueprints.map((entry) => {
|
|
1820
|
+
try {
|
|
1821
|
+
const blueprint = JSON.parse(entry.executionBlueprint);
|
|
1822
|
+
if (blueprint && typeof blueprint === "object" && !Array.isArray(blueprint))
|
|
1823
|
+
return { ...entry, executionBlueprint: blueprint };
|
|
1824
|
+
}
|
|
1825
|
+
catch { /* Plain-text and legacy blueprints keep their exact content. */ }
|
|
1826
|
+
return entry;
|
|
1827
|
+
}),
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1639
1830
|
function positiveDuration(value) {
|
|
1640
1831
|
const duration = Number(value);
|
|
1641
1832
|
return Number.isFinite(duration) && duration > 0 ? Math.round(duration * 1000) / 1000 : null;
|
|
@@ -1710,7 +1901,7 @@ export function nextScriptPipelineWave(task, receivedOrdinals, durationSeconds,
|
|
|
1710
1901
|
if (scriptReady.length) {
|
|
1711
1902
|
return { stage: "script", chunks: flowCScriptChunks(durationSeconds, scriptReady, scriptChunkSize).slice(0, concurrency) };
|
|
1712
1903
|
}
|
|
1713
|
-
const candidateMissing = missingOrdinals(task.requested_count, selected);
|
|
1904
|
+
const candidateMissing = missingOrdinals(task.requested_count, [...selected, ...receivedOrdinals]);
|
|
1714
1905
|
return { stage: "candidate", chunks: chunkNumbers(candidateMissing, candidateChunkSize).slice(0, concurrency) };
|
|
1715
1906
|
}
|
|
1716
1907
|
export function immediateScriptOrdinals(task, receivedOrdinals, candidateOrdinals) {
|
|
@@ -1762,7 +1953,8 @@ function productIndexForOrdinal(productQuantities, ordinal) {
|
|
|
1762
1953
|
return -1;
|
|
1763
1954
|
}
|
|
1764
1955
|
function publicScript(record) {
|
|
1765
|
-
|
|
1956
|
+
const reviewOrdinals = voicePacingHeldOrdinals(record);
|
|
1957
|
+
return { id: record.id, status: record.status, requestedCount: record.requestedCount, received: record.receivedOrdinals.length, reviewOrdinals, reviewCount: reviewOrdinals.length, threadId: record.threadId, chunkSize: record.chunkSize || null, activeChunks: Number(record.activeChunks || 0), message: record.message, expiresAt: record.expiresAt, updatedAt: record.updatedAt, ...(record.contentAdvisories ? { contentReview: { blocking: false, semanticAcceptance: "not-evaluated", advisories: record.contentAdvisories } } : {}) };
|
|
1766
1958
|
}
|
|
1767
1959
|
function publicDownload(record) {
|
|
1768
1960
|
return { batchId: record.batchId, status: record.status, downloaded: record.downloadedOrdinals.length, market: record.market, message: record.message, expiresAt: record.expiresAt, updatedAt: record.updatedAt };
|