@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.
- package/dist/agent/codex.d.ts +2 -0
- package/dist/agent/codex.js +10 -4
- package/dist/integrations/fastmoss-opportunity.d.ts +160 -0
- package/dist/integrations/fastmoss-opportunity.js +183 -0
- package/dist/integrations/fastmoss-selection-session.d.ts +112 -0
- package/dist/integrations/fastmoss-selection-session.js +174 -0
- package/dist/integrations/fastmoss-visible-evidence.d.ts +52 -0
- package/dist/integrations/fastmoss-visible-evidence.js +303 -0
- package/dist/integrations/fastmoss.d.ts +45 -0
- package/dist/integrations/fastmoss.js +49 -0
- package/dist/server/http.js +4 -1
- package/dist/video-intelligence/local-analysis.d.ts +5 -0
- package/dist/video-intelligence/local-analysis.js +30 -2
- package/dist/workflow/content-method.d.ts +58 -0
- package/dist/workflow/content-method.js +100 -14
- package/dist/workflow/manager.d.ts +31 -2
- package/dist/workflow/manager.js +317 -13
- package/package.json +2 -2
package/dist/workflow/manager.js
CHANGED
|
@@ -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:
|
|
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.
|
|
712
|
+
const deliveryResult = await this.submitScriptJobsWithVoicePacingRepair(id, task, jobs, cwd);
|
|
672
713
|
this.emitScriptStage(id, ordinals, "persist", Date.now() - persistStartedAt);
|
|
673
|
-
return
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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",
|