@xiaohhhh1/canvas-agent 0.4.79 → 0.4.81

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.
@@ -14,12 +14,14 @@ import { FLOW_C_CREATIVE_CANDIDATE_CONTRACT_VERSION, flowCCreativeCandidateOutpu
14
14
  import { FLOW_C_LEGACY_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION, FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION, flowCProductExecutionProfileOutputSchema, parseFlowCProductExecutionProfileOutput } from "./product-profile.js";
15
15
  import { flowCScriptOutputSchema, parseFlowCScriptOutput } from "./script-output.js";
16
16
  import { commerceJson, CommerceRequestError } from "./commerce-http.js";
17
- import { FLOW_C_CONTENT_STRATEGY_VERSION, FLOW_C_GENERATED_MONTAGE_STYLE, FLOW_C_GENERATED_MONTAGE_VERSION, FLOW_C_GENERATED_MONTAGE_VERSION_UNSUPPORTED, flowCContentAdvisories, flowCContentDirection, flowCContentMethodPrompt, flowCContentStrategy, flowCGeneratedMontage, flowCGeneratedMontagePrompt, mergeFlowCContentSummaries } from "./content-method.js";
17
+ import { FLOW_C_CONTENT_STRATEGY_VERSION, FLOW_C_GENERATED_MONTAGE_STYLE, FLOW_C_GENERATED_MONTAGE_VERSION, FLOW_C_GENERATED_MONTAGE_VERSION_UNSUPPORTED, flowCContentAdvisories, flowCContentDirection, flowCContentMethodPrompt, flowCContentStrategy, flowCGeneratedMontage, flowCGeneratedMontagePrompt, flowCVoicePacingRepairIssues, flowCVoicePacingRepairPrompt, flowCVoicePacingRepairScaffold, mergeFlowCContentSummaries } from "./content-method.js";
18
18
  const STATE_FILE = path.join(CONFIG_DIR, "workflow-state.json");
19
19
  export const FLOW_C_CODEX_TURN_TIMEOUT_MS = 8 * 60 * 1000;
20
20
  export const FLOW_C_SELECTED_BLUEPRINT_PROMPT_MAX_CHARS = 45_000;
21
21
  export const FLOW_C_SCRIPT_REWRITE_MAX_ATTEMPTS = 2;
22
22
  export const FLOW_C_CANDIDATE_REVISION_MAX_ATTEMPTS = 2;
23
+ export const FLOW_C_VOICE_PACING_REPAIR_MAX_ATTEMPTS = 1;
24
+ export const FLOW_C_VOICE_PACING_REVIEW_REQUIRED = "FLOW_C_VOICE_PACING_REVIEW_REQUIRED";
23
25
  export const FLOW_C_LEGACY_DIRECT_CONTRACT_VERSION = "flow-c-template-direct-v2";
24
26
  export const FLOW_C_PRODUCT_PROFILE_DIRECT_CONTRACT_VERSION = "flow-c-product-profile-direct-v1";
25
27
  export const FLOW_C_CREATIVE_SOURCE_DIRECT_CONTRACT_VERSION = "flow-c-creative-source-direct-v1";
@@ -70,6 +72,8 @@ export class WorkflowManager {
70
72
  activeChunks: 0,
71
73
  productProfiles: previous?.productProfiles || [],
72
74
  pendingScriptJobs: previous?.pendingScriptJobs || [],
75
+ ...(previous?.voicePacingReviewJobs ? { voicePacingReviewJobs: previous.voicePacingReviewJobs } : {}),
76
+ ...(previous?.voicePacingRepairAttempts ? { voicePacingRepairAttempts: previous.voicePacingRepairAttempts } : {}),
73
77
  ...(previous?.contentAdvisories ? { contentAdvisories: previous.contentAdvisories } : {}),
74
78
  lastFailure: previous?.lastFailure,
75
79
  priorityAt: now(),
@@ -92,6 +96,7 @@ export class WorkflowManager {
92
96
  return this.scriptStatus(id);
93
97
  }
94
98
  delete record.retryRequested;
99
+ resetVoicePacingRepairAttempts(record);
95
100
  record.status = "queued";
96
101
  record.activeChunks = 0;
97
102
  // A failed manual attempt may leave a very large or interrupted thread.
@@ -126,6 +131,10 @@ export class WorkflowManager {
126
131
  task.content_recent_scripts = record.contentSummaries;
127
132
  }
128
133
  record.pendingScriptJobs = (record.pendingScriptJobs || []).filter((job) => !record.receivedOrdinals.includes(job.ordinal));
134
+ record.voicePacingReviewJobs = (record.voicePacingReviewJobs || []).filter((job) => !record.receivedOrdinals.includes(job.ordinal));
135
+ if (!record.voicePacingReviewJobs.length)
136
+ delete record.voicePacingReviewJobs;
137
+ pruneVoicePacingRepairAttempts(record);
129
138
  record.expiresAt = task.expires_at;
130
139
  record.updatedAt = now();
131
140
  this.save();
@@ -157,6 +166,10 @@ export class WorkflowManager {
157
166
  if (flowCContentStrategy(record.contentStrategy))
158
167
  record.contentSummaries = mergeFlowCContentSummaries(record.contentSummaries, jobs, record.receivedOrdinals);
159
168
  record.pendingScriptJobs = (record.pendingScriptJobs || []).filter((job) => !record.receivedOrdinals.includes(job.ordinal));
169
+ record.voicePacingReviewJobs = (record.voicePacingReviewJobs || []).filter((job) => !record.receivedOrdinals.includes(job.ordinal));
170
+ if (!record.voicePacingReviewJobs.length)
171
+ delete record.voicePacingReviewJobs;
172
+ pruneVoicePacingRepairAttempts(record);
160
173
  record.status = data.status === "ready" ? "complete" : "running";
161
174
  record.message = data.status === "ready"
162
175
  ? flowCContentStrategy(record.contentStrategy) ? `全部 ${record.requestedCount} 条脚本已回传(内容语义仍需审阅)` : `全部 ${record.requestedCount} 条高质量脚本已回传`
@@ -354,8 +367,10 @@ export class WorkflowManager {
354
367
  }));
355
368
  const results = pipelineResults.flat();
356
369
  task = await this.scriptTask(id);
357
- const terminalFailure = terminalScriptChunkFailure(results);
370
+ const terminalFailure = terminalScriptChunkFailure(results, this.scriptRecord(id).receivedOrdinals);
358
371
  if (terminalFailure) {
372
+ if (terminalFailure.terminalKind === "review")
373
+ throw new Error(terminalFailure.error);
359
374
  if (terminalFailure.terminalKind === "delivery")
360
375
  throw new Error(terminalFailure.error);
361
376
  if (terminalFailure.terminalKind === "transport")
@@ -405,8 +420,10 @@ export class WorkflowManager {
405
420
  this.save();
406
421
  const results = await Promise.all(wave.chunks.map((ordinals) => this.runScriptChunk(id, task, ordinals, workspace.workspacePath)));
407
422
  task = await this.scriptTask(id);
408
- const terminalFailure = terminalScriptChunkFailure(results);
423
+ const terminalFailure = terminalScriptChunkFailure(results, this.scriptRecord(id).receivedOrdinals);
409
424
  if (terminalFailure) {
425
+ if (terminalFailure.terminalKind === "review")
426
+ throw new Error(terminalFailure.error);
410
427
  if (terminalFailure.terminalKind === "delivery")
411
428
  throw new Error(terminalFailure.error);
412
429
  if (terminalFailure.terminalKind === "transport")
@@ -468,6 +485,7 @@ export class WorkflowManager {
468
485
  const retry = record.retryRequested && record.status !== "complete";
469
486
  delete record.retryRequested;
470
487
  if (retry) {
488
+ resetVoicePacingRepairAttempts(record);
471
489
  record.status = "queued";
472
490
  delete record.threadId;
473
491
  record.message = "已保存本机结果,正在优先重试";
@@ -628,11 +646,35 @@ export class WorkflowManager {
628
646
  if (!ordinals.length)
629
647
  return { terminal: false };
630
648
  }
649
+ const selectedForRecovery = selectedCandidatesForOrdinals(task, ordinals);
650
+ const localization = compactTaskLocalization(task);
651
+ const targetLanguage = localization.targetLanguage || task.target_language || localization.targetLocale;
652
+ const storedReview = (record.voicePacingReviewJobs || []).filter((job) => {
653
+ if (!ordinals.includes(job.ordinal))
654
+ return false;
655
+ const candidate = selectedForRecovery.get(job.ordinal);
656
+ return String(job.expectedCandidateRevision || "") === String(candidate?.candidateRevision || "");
657
+ });
658
+ const repairableStoredReview = storedReview.filter((job) => flowCVoicePacingRepairIssues([job], { targetLanguage }).length > 0);
659
+ if (repairableStoredReview.length) {
660
+ const reviewResult = await this.repairVoicePacingJobs(id, task, repairableStoredReview, cwd);
661
+ if (reviewResult.error || reviewResult.terminal)
662
+ return reviewResult;
663
+ ordinals = ordinals.filter((ordinal) => !this.scriptRecord(id).receivedOrdinals.includes(ordinal));
664
+ if (!ordinals.length)
665
+ return { terminal: false };
666
+ }
667
+ const retainedOrdinals = new Set(repairableStoredReview.map((job) => job.ordinal));
668
+ const activeRecord = this.scriptRecord(id);
669
+ activeRecord.voicePacingReviewJobs = (activeRecord.voicePacingReviewJobs || []).filter((job) => !ordinals.includes(job.ordinal) || retainedOrdinals.has(job.ordinal));
670
+ if (!activeRecord.voicePacingReviewJobs.length)
671
+ delete activeRecord.voicePacingReviewJobs;
672
+ pruneVoicePacingRepairAttempts(activeRecord);
631
673
  const durationSeconds = Number(task.duration_seconds || 10);
632
674
  let prompt;
633
675
  try {
634
676
  const promptTask = flowCContentStrategy(task.creative_strategy)
635
- ? { ...task, received_ordinals: record.receivedOrdinals, content_recent_scripts: mergeFlowCContentSummaries([...(Array.isArray(task.content_recent_scripts) ? task.content_recent_scripts : []), ...(record.contentSummaries || [])], [], record.receivedOrdinals) }
677
+ ? { ...task, received_ordinals: activeRecord.receivedOrdinals, content_recent_scripts: mergeFlowCContentSummaries([...(Array.isArray(task.content_recent_scripts) ? task.content_recent_scripts : []), ...(activeRecord.contentSummaries || [])], [], activeRecord.receivedOrdinals) }
636
678
  : task;
637
679
  prompt = scriptChunkPrompt(id, promptTask, ordinals, rewriteAttempt);
638
680
  }
@@ -666,11 +708,10 @@ export class WorkflowManager {
666
708
  creativePlan: { ...(job.creativePlan || {}), ...selectedCandidatePlan(selected.get(job.ordinal), flowCContentStrategy(task.creative_strategy)) },
667
709
  }));
668
710
  this.emitScriptStage(id, ordinals, "parse", Date.now() - parseStartedAt);
669
- this.noteScriptContentAdvisories(id, task, jobs);
670
711
  const persistStartedAt = Date.now();
671
- await this.submitGeneratedScriptJobs(id, jobs);
712
+ const deliveryResult = await this.submitScriptJobsWithVoicePacingRepair(id, task, jobs, cwd);
672
713
  this.emitScriptStage(id, ordinals, "persist", Date.now() - persistStartedAt);
673
- return { terminal: false };
714
+ return deliveryResult;
674
715
  }
675
716
  catch (error) {
676
717
  // Only explicit semantic rejection may discard the affected saved output.
@@ -740,7 +781,7 @@ export class WorkflowManager {
740
781
  return preserveScriptRecoveryReplans(rewriteResult, error);
741
782
  }
742
783
  if (error instanceof CommerceRequestError && !creativeReplanOrdinals(error).length)
743
- return { error: error.message, terminal: true, terminalKind: "delivery" };
784
+ return { error: error.message, terminal: true, terminalKind: "delivery", affectedOrdinals: scriptDeliveryOrdinals(error).length ? scriptDeliveryOrdinals(error) : [...ordinals] };
744
785
  return {
745
786
  error: error instanceof Error ? `结构化脚本未通过本机校验:${error.message}` : "结构化脚本未通过本机校验",
746
787
  terminal: terminalScriptValidationError(error),
@@ -748,6 +789,133 @@ export class WorkflowManager {
748
789
  };
749
790
  }
750
791
  }
792
+ async submitScriptJobsWithVoicePacingRepair(id, task, jobs, cwd) {
793
+ const record = this.scriptRecord(id);
794
+ const current = jobs.filter((job) => !record.receivedOrdinals.includes(job.ordinal));
795
+ if (!current.length)
796
+ return { terminal: false };
797
+ const selected = selectedCandidatesForOrdinals(task, current.map((job) => job.ordinal));
798
+ const repairEnabled = Boolean(flowCContentStrategy(task.creative_strategy)) || [...selected.values()].some((candidate) => flowCGeneratedMontage(candidate));
799
+ const localization = compactTaskLocalization(task);
800
+ const targetLanguage = localization.targetLanguage || task.target_language || localization.targetLocale;
801
+ const repairIssues = repairEnabled ? flowCVoicePacingRepairIssues(current, { targetLanguage }) : [];
802
+ const repairOrdinals = new Set(repairIssues.map((issue) => issue.ordinal));
803
+ this.noteScriptContentAdvisories(id, task, current);
804
+ if (!repairOrdinals.size) {
805
+ await this.submitGeneratedScriptJobs(id, current);
806
+ return { terminal: false };
807
+ }
808
+ const review = new Map((record.voicePacingReviewJobs || []).map((job) => [job.ordinal, job]));
809
+ for (const job of current)
810
+ if (repairOrdinals.has(job.ordinal))
811
+ review.set(job.ordinal, structuredClone(job));
812
+ record.voicePacingReviewJobs = [...review.values()].sort((left, right) => left.ordinal - right.ordinal);
813
+ record.message = `检测到 ordinal ${[...repairOrdinals].join(", ")} 的短镜口播明显过密;合格稿先回传,受影响稿只做 1 次定向压缩/重分配`;
814
+ record.updatedAt = now();
815
+ this.save();
816
+ const clean = current.filter((job) => !repairOrdinals.has(job.ordinal));
817
+ if (clean.length)
818
+ await this.submitGeneratedScriptJobs(id, clean);
819
+ const affected = current.filter((job) => repairOrdinals.has(job.ordinal) && !this.scriptRecord(id).receivedOrdinals.includes(job.ordinal));
820
+ if (!affected.length)
821
+ return { terminal: false };
822
+ return this.repairVoicePacingJobs(id, task, affected, cwd);
823
+ }
824
+ /**
825
+ * One correction turn only. Originals live outside pendingScriptJobs, so a
826
+ * restart or response loss can never replay an overcrowded draft as accepted.
827
+ */
828
+ async repairVoicePacingJobs(id, task, originals, cwd) {
829
+ const record = this.scriptRecord(id);
830
+ const current = originals.filter((job) => !record.receivedOrdinals.includes(job.ordinal));
831
+ if (!current.length)
832
+ return { terminal: false };
833
+ const eligible = current.filter((job) => voicePacingRepairAttemptCount(record, job) < FLOW_C_VOICE_PACING_REPAIR_MAX_ATTEMPTS);
834
+ const exhausted = current.filter((job) => !eligible.includes(job));
835
+ if (!eligible.length)
836
+ return voicePacingReviewFailure(exhausted.map((job) => job.ordinal), "本机已记录本次自动修复机会,后台恢复不会再次调用模型");
837
+ const localization = compactTaskLocalization(task);
838
+ const targetLanguage = localization.targetLanguage || task.target_language || localization.targetLocale;
839
+ const selected = selectedCandidatesForOrdinals(task, eligible.map((job) => job.ordinal));
840
+ const frameworkOrdinals = [...selected.values()].filter(isFlowCUserFrameworkCandidate).map((candidate) => candidate.ordinal);
841
+ const ordinals = eligible.map((job) => job.ordinal);
842
+ const durationSeconds = Number(task.duration_seconds || 10);
843
+ const prompt = flowCVoicePacingRepairPrompt(eligible, { targetLanguage, frameworkOrdinals });
844
+ for (const job of eligible)
845
+ recordVoicePacingRepairAttempt(record, job);
846
+ record.message = `正在为 ordinal ${ordinals.join(", ")} 做第 1/${FLOW_C_VOICE_PACING_REPAIR_MAX_ATTEMPTS} 次短镜口播定向修复;画面、时轴与已选框架保持不变`;
847
+ record.updatedAt = now();
848
+ this.save();
849
+ let result;
850
+ try {
851
+ result = await this.runVoicePacingRepairTurn(id, prompt, cwd, durationSeconds, ordinals.length);
852
+ }
853
+ catch (error) {
854
+ const received = this.scriptRecord(id).receivedOrdinals;
855
+ const missing = [...exhausted.map((job) => job.ordinal), ...ordinals].filter((ordinal) => !received.includes(ordinal));
856
+ return missing.length ? voicePacingReviewFailure(missing, error instanceof Error ? error.message : "口播修复回合异常") : { terminal: false };
857
+ }
858
+ this.emitScriptStage(id, ordinals, "voice_repair", result.timings.queueWaitMs + result.timings.threadStartMs + result.timings.modelMs);
859
+ const missingAfterTurn = [...exhausted.map((job) => job.ordinal), ...ordinals].filter((ordinal) => !this.scriptRecord(id).receivedOrdinals.includes(ordinal));
860
+ if (!missingAfterTurn.length)
861
+ return { terminal: false };
862
+ if (!result.ok)
863
+ return voicePacingReviewFailure(missingAfterTurn, result.error || "Codex 未返回口播修复稿");
864
+ if (!result.text)
865
+ return voicePacingReviewFailure(missingAfterTurn, "Codex 未返回口播修复稿");
866
+ let candidates;
867
+ try {
868
+ candidates = parseFlowCScriptOutput(result.text, ordinals);
869
+ }
870
+ catch (error) {
871
+ const missing = missingAfterTurn.filter((ordinal) => !this.scriptRecord(id).receivedOrdinals.includes(ordinal));
872
+ return missing.length ? voicePacingReviewFailure(missing, error instanceof Error ? error.message : "口播修复稿未通过结构校验") : { terminal: false };
873
+ }
874
+ const byOrdinal = new Map(candidates.map((job) => [job.ordinal, job]));
875
+ const accepted = [];
876
+ const failed = new Map(exhausted.map((job) => [job.ordinal, "本次自动修复机会已经使用"]));
877
+ for (const original of eligible.filter((job) => !this.scriptRecord(id).receivedOrdinals.includes(job.ordinal))) {
878
+ const candidate = byOrdinal.get(original.ordinal);
879
+ if (!candidate) {
880
+ failed.set(original.ordinal, "修复回合未返回该 ordinal");
881
+ continue;
882
+ }
883
+ try {
884
+ const repaired = applyFlowCVoicePacingRepair(original, candidate, frameworkOrdinals.includes(original.ordinal));
885
+ const remaining = flowCVoicePacingRepairIssues([repaired], { targetLanguage });
886
+ if (remaining.length) {
887
+ failed.set(original.ordinal, `仍有 ${remaining.length} 个明显过密短镜`);
888
+ continue;
889
+ }
890
+ accepted.push(repaired);
891
+ }
892
+ catch (error) {
893
+ failed.set(original.ordinal, error instanceof Error ? error.message : "修复稿改变了受保护字段");
894
+ }
895
+ }
896
+ const stillMissing = accepted.filter((job) => !this.scriptRecord(id).receivedOrdinals.includes(job.ordinal));
897
+ if (stillMissing.length) {
898
+ this.noteScriptContentAdvisories(id, task, stillMissing);
899
+ await this.submitGeneratedScriptJobs(id, stillMissing);
900
+ }
901
+ for (const ordinal of this.scriptRecord(id).receivedOrdinals)
902
+ failed.delete(ordinal);
903
+ if (failed.size)
904
+ return voicePacingReviewFailure([...failed.keys()], [...failed.entries()].map(([ordinal, reason]) => `ordinal ${ordinal}: ${reason}`).join(";"));
905
+ return { terminal: false };
906
+ }
907
+ runVoicePacingRepairTurn(id, prompt, cwd, durationSeconds, count) {
908
+ return runCodexWorkflowTurn(prompt, this.emit, {
909
+ cwd,
910
+ permissionMode: "full",
911
+ timeoutMs: FLOW_C_CODEX_TURN_TIMEOUT_MS,
912
+ ...flowCVoicePacingRepairTurnOptions(),
913
+ outputSchema: flowCScriptOutputSchema(durationSeconds, count),
914
+ onThread: (threadId) => { this.scriptRecord(id).threadId = threadId; this.save(); },
915
+ onWorkerStart: () => { const next = this.scriptRecord(id); next.activeChunks = Number(next.activeChunks || 0) + 1; next.updatedAt = now(); this.save(); },
916
+ onWorkerFinish: () => { const next = this.scriptRecord(id); next.activeChunks = Math.max(0, Number(next.activeChunks || 0) - 1); next.updatedAt = now(); this.save(); },
917
+ });
918
+ }
751
919
  /** Nonblocking, text-free diagnostics for newly generated policy scripts only. */
752
920
  noteScriptContentAdvisories(id, task, jobs) {
753
921
  const strategy = flowCContentStrategy(task.creative_strategy);
@@ -778,7 +946,7 @@ export class WorkflowManager {
778
946
  }
779
947
  catch (error) {
780
948
  if (jobs.length === 1 || error instanceof CommerceRequestError && ![400, 409, 422].includes(error.status || 0))
781
- throw error;
949
+ throw markScriptDeliveryOrdinals(error, jobs);
782
950
  let accepted = 0;
783
951
  const failures = [];
784
952
  for (const job of jobs) {
@@ -788,7 +956,7 @@ export class WorkflowManager {
788
956
  }
789
957
  catch (jobError) {
790
958
  if (jobError instanceof CommerceRequestError && ![400, 409, 422].includes(jobError.status || 0))
791
- throw jobError;
959
+ throw markScriptDeliveryOrdinals(jobError, [job]);
792
960
  failures.push(jobError);
793
961
  }
794
962
  }
@@ -936,12 +1104,134 @@ export function compareScriptQueueRecords(left, right) {
936
1104
  return String(right.priorityAt || "").localeCompare(String(left.priorityAt || ""));
937
1105
  return left.updatedAt.localeCompare(right.updatedAt);
938
1106
  }
1107
+ function pacingObject(value) {
1108
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1109
+ }
1110
+ export function flowCVoicePacingRepairTurnOptions() {
1111
+ return { maxProcessAttempts: FLOW_C_VOICE_PACING_REPAIR_MAX_ATTEMPTS };
1112
+ }
1113
+ function voicePacingCandidateRevision(job) {
1114
+ return String(job.expectedCandidateRevision || "");
1115
+ }
1116
+ export function voicePacingRepairAttemptCount(record, job) {
1117
+ const revision = voicePacingCandidateRevision(job);
1118
+ return Math.max(0, ...((record.voicePacingRepairAttempts || []).filter((entry) => Number(entry.ordinal) === Number(job.ordinal) && String(entry.candidateRevision || "") === revision).map((entry) => Math.floor(Number(entry.attempts) || 0))));
1119
+ }
1120
+ function recordVoicePacingRepairAttempt(record, job) {
1121
+ const revision = voicePacingCandidateRevision(job);
1122
+ const attempts = (record.voicePacingRepairAttempts || []).filter((entry) => Number(entry.ordinal) !== job.ordinal || String(entry.candidateRevision || "") !== revision);
1123
+ attempts.push({ ordinal: job.ordinal, candidateRevision: revision, attempts: voicePacingRepairAttemptCount(record, job) + 1 });
1124
+ record.voicePacingRepairAttempts = attempts.sort((left, right) => left.ordinal - right.ordinal || left.candidateRevision.localeCompare(right.candidateRevision));
1125
+ }
1126
+ function pruneVoicePacingRepairAttempts(record) {
1127
+ const active = new Set((record.voicePacingReviewJobs || []).map((job) => `${job.ordinal}\u0000${voicePacingCandidateRevision(job)}`));
1128
+ record.voicePacingRepairAttempts = (record.voicePacingRepairAttempts || []).filter((entry) => active.has(`${Number(entry.ordinal)}\u0000${String(entry.candidateRevision || "")}`));
1129
+ if (!record.voicePacingRepairAttempts.length)
1130
+ delete record.voicePacingRepairAttempts;
1131
+ }
1132
+ function resetVoicePacingRepairAttempts(record) {
1133
+ delete record.voicePacingRepairAttempts;
1134
+ }
1135
+ function pacingSegments(value) {
1136
+ const job = pacingObject(value);
1137
+ return Array.isArray(job.segments) ? job.segments.map(pacingObject) : job.segment ? [pacingObject(job.segment)] : [];
1138
+ }
1139
+ function isPacingSilence(value) {
1140
+ return /^(?:none|无|sin voz|sin diálogo)$/i.test(String(value || "").trim());
1141
+ }
1142
+ function exactSegmentTranscript(shots) {
1143
+ return shots.map((shot) => String(shot.voiceover || "").trim()).filter((line) => line && !isPacingSilence(line)).join(" ").replace(/\s+/gu, " ").trim();
1144
+ }
1145
+ function stablePacingValue(value) {
1146
+ if (Array.isArray(value))
1147
+ return value.map(stablePacingValue);
1148
+ if (!value || typeof value !== "object")
1149
+ return value;
1150
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => [key, stablePacingValue(child)]));
1151
+ }
1152
+ function protectedPacingProjection(value) {
1153
+ const job = structuredClone(pacingObject(value));
1154
+ delete job.script;
1155
+ delete job.masterScript;
1156
+ delete job.segmentVoiceovers;
1157
+ for (const segment of pacingSegments(job)) {
1158
+ delete segment.script;
1159
+ const shots = Array.isArray(segment.shots) ? segment.shots.map(pacingObject) : [];
1160
+ for (const shot of shots)
1161
+ shot.voiceover = "[voiceover-only-repair]";
1162
+ }
1163
+ return stablePacingValue(job);
1164
+ }
1165
+ /**
1166
+ * Accept only repaired per-shot speech. Every visual/timeline/state/selection
1167
+ * field comes from the locally validated original and rendered projections are
1168
+ * rebuilt from that one final voice source.
1169
+ */
1170
+ export function applyFlowCVoicePacingRepair(originalValue, candidateValue, preserveExactTranscript = false) {
1171
+ const original = pacingObject(originalValue);
1172
+ const candidate = pacingObject(candidateValue);
1173
+ const ordinal = Number(original.ordinal);
1174
+ if (!Number.isInteger(ordinal) || Number(candidate.ordinal) !== ordinal || Number(candidate.productIndex) !== Number(original.productIndex))
1175
+ throw new Error("修复稿 ordinal/productIndex 与原稿不一致");
1176
+ if (JSON.stringify(protectedPacingProjection(flowCVoicePacingRepairScaffold(candidate))) !== JSON.stringify(protectedPacingProjection(flowCVoicePacingRepairScaffold(original)))) {
1177
+ throw new Error("修复稿改变了 VO 之外的画面、事实、声音方向或连续性字段");
1178
+ }
1179
+ const originalSegments = pacingSegments(original);
1180
+ const candidateSegments = pacingSegments(candidate);
1181
+ if (!originalSegments.length || candidateSegments.length !== originalSegments.length)
1182
+ throw new Error("修复稿改变了分段数量");
1183
+ const patchedSegments = originalSegments.map((originalSegment, segmentIndex) => {
1184
+ const originalShots = Array.isArray(originalSegment.shots) ? originalSegment.shots.map(pacingObject) : [];
1185
+ const candidateShots = Array.isArray(candidateSegments[segmentIndex]?.shots) ? candidateSegments[segmentIndex].shots.map(pacingObject) : [];
1186
+ if (!originalShots.length || candidateShots.length !== originalShots.length)
1187
+ throw new Error(`segment ${segmentIndex + 1} 改变了镜头数量`);
1188
+ for (const [shotIndex, originalShot] of originalShots.entries()) {
1189
+ const candidateShot = candidateShots[shotIndex];
1190
+ if (Number(candidateShot.startSeconds) !== Number(originalShot.startSeconds) || Number(candidateShot.endSeconds) !== Number(originalShot.endSeconds))
1191
+ throw new Error(`segment ${segmentIndex + 1} shot ${shotIndex + 1} 改变了镜头时轴`);
1192
+ if (!String(candidateShot.voiceover || "").trim())
1193
+ throw new Error(`segment ${segmentIndex + 1} shot ${shotIndex + 1} 缺少 voiceover`);
1194
+ }
1195
+ const originalTranscript = exactSegmentTranscript(originalShots);
1196
+ const repairedTranscript = exactSegmentTranscript(candidateShots);
1197
+ if (originalTranscript && !repairedTranscript)
1198
+ throw new Error(`segment ${segmentIndex + 1} 删除了整段口播`);
1199
+ if (preserveExactTranscript && repairedTranscript !== originalTranscript)
1200
+ throw new Error(`segment ${segmentIndex + 1} 改写了用户逐字框架口播`);
1201
+ return {
1202
+ ...originalSegment,
1203
+ shots: originalShots.map((shot, shotIndex) => ({ ...shot, voiceover: String(candidateShots[shotIndex].voiceover).trim() })),
1204
+ };
1205
+ });
1206
+ const patched = Array.isArray(original.segments) ? { ...original, segments: patchedSegments } : { ...original, segment: patchedSegments[0] };
1207
+ const scaffold = flowCVoicePacingRepairScaffold(patched);
1208
+ const rendered = parseFlowCScriptOutput(JSON.stringify({ jobs: [scaffold] }), [ordinal])[0];
1209
+ const result = { ...original, ...rendered };
1210
+ if (JSON.stringify(protectedPacingProjection(result)) !== JSON.stringify(protectedPacingProjection(original)))
1211
+ throw new Error("修复稿改变了 VO 之外的受保护字段");
1212
+ return result;
1213
+ }
1214
+ function voicePacingReviewFailure(ordinals, reason) {
1215
+ const scoped = [...new Set(ordinals.map(Number).filter((ordinal) => Number.isInteger(ordinal) && ordinal > 0))].sort((left, right) => left - right);
1216
+ return {
1217
+ error: `${FLOW_C_VOICE_PACING_REVIEW_REQUIRED}: ordinal ${scoped.join(", ")} 的短镜口播在唯一一次定向修复后仍未安全落入镜头时长;原稿仅保存在本机待审区,未作为可重传稿提交。${reason}。请人工审阅,或确认逐字框架可调整后手动重试`,
1218
+ terminal: true,
1219
+ terminalKind: "review",
1220
+ affectedOrdinals: scoped,
1221
+ };
1222
+ }
939
1223
  /** A deterministic response-format failure stops fallback isolation immediately. */
940
1224
  export function terminalScriptChunkError(results) {
941
1225
  return terminalScriptChunkFailure(results)?.error || "";
942
1226
  }
943
- export function terminalScriptChunkFailure(results) {
944
- return results.find((result) => result.terminal);
1227
+ export function terminalScriptChunkFailure(results, receivedOrdinals = []) {
1228
+ const received = new Set(receivedOrdinals.map(Number).filter(Number.isInteger));
1229
+ return results.find((result) => {
1230
+ if (!result.terminal)
1231
+ return false;
1232
+ const affected = Array.isArray(result.affectedOrdinals) ? result.affectedOrdinals.map(Number).filter((ordinal) => Number.isInteger(ordinal) && ordinal > 0) : [];
1233
+ return !affected.length || affected.some((ordinal) => !received.has(ordinal));
1234
+ });
945
1235
  }
946
1236
  export function scriptCreativeReplanOrdinals(results) {
947
1237
  return [...new Set(results.flatMap((result) => (result && typeof result === "object" ? result.replanOrdinals || [] : [])).filter((ordinal) => Number.isInteger(ordinal) && ordinal > 0))].sort((left, right) => left - right);
@@ -960,6 +1250,17 @@ export function creativeReplanOrdinals(error) {
960
1250
  const values = error.resetOrdinals;
961
1251
  return Array.isArray(values) ? [...new Set(values.map(Number).filter((ordinal) => Number.isInteger(ordinal) && ordinal > 0))].sort((left, right) => left - right) : [];
962
1252
  }
1253
+ function markScriptDeliveryOrdinals(error, jobs) {
1254
+ if (error && typeof error === "object") {
1255
+ const current = scriptDeliveryOrdinals(error);
1256
+ error.deliveryOrdinals = [...new Set([...current, ...jobs.map((job) => Number(job.ordinal))].filter((ordinal) => Number.isInteger(ordinal) && ordinal > 0))].sort((left, right) => left - right);
1257
+ }
1258
+ return error;
1259
+ }
1260
+ export function scriptDeliveryOrdinals(error) {
1261
+ const values = error && typeof error === "object" ? error.deliveryOrdinals : [];
1262
+ return Array.isArray(values) ? [...new Set(values.map(Number).filter((ordinal) => Number.isInteger(ordinal) && ordinal > 0))].sort((left, right) => left - right) : [];
1263
+ }
963
1264
  export function candidateRevisionChangedOrdinals(error) {
964
1265
  if (!error || typeof error !== "object")
965
1266
  return [];
@@ -1105,7 +1406,7 @@ export function scriptChunkPrompt(id, task, ordinals, rewriteAttempt = 0) {
1105
1406
  recentScripts: mergeFlowCContentSummaries(task.content_recent_scripts, [], task.received_ordinals),
1106
1407
  productIndexes: products.map((product) => product.productIndex),
1107
1408
  ordinals,
1108
- frameworkOrdinals: [...selected.values()].filter((candidate) => candidate.selectionMode === "user-framework").map((candidate) => candidate.ordinal),
1409
+ frameworkOrdinals: [...selected.values()].filter(isFlowCUserFrameworkCandidate).map((candidate) => candidate.ordinal),
1109
1410
  montageOrdinals: generatedMontageOrdinals,
1110
1411
  });
1111
1412
  const generatedMontageMethod = flowCGeneratedMontagePrompt(generatedMontageOrdinals);
@@ -1326,6 +1627,9 @@ function positiveDuration(value) {
1326
1627
  const duration = Number(value);
1327
1628
  return Number.isFinite(duration) && duration > 0 ? Math.round(duration * 1000) / 1000 : null;
1328
1629
  }
1630
+ function isFlowCUserFrameworkCandidate(value) {
1631
+ return value.selectionMode === "user-framework" || value.scriptSource === "user-framework" || value.creativeSource === "user-framework";
1632
+ }
1329
1633
  export function selectedCandidatePlan(value, contentStrategy = null) {
1330
1634
  if (!value)
1331
1635
  throw new Error("中心缺少选中的创意候选");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaohhhh1/canvas-agent",
3
- "version": "0.4.79",
3
+ "version": "0.4.81",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -23,7 +23,7 @@
23
23
  "dependencies": {
24
24
  "@modelcontextprotocol/sdk": "^1.12.1",
25
25
  "@huggingface/transformers": "4.2.0",
26
- "@openai/codex": "0.145.0",
26
+ "@openai/codex": "0.153.4",
27
27
  "express": "^5.1.0",
28
28
  "playwright-core": "^1.62.1",
29
29
  "ws": "^8.18.3",