@xiaohhhh1/canvas-agent 0.4.80 → 0.4.82
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/relay-bridge.js +8 -5
- package/dist/relay-request.d.ts +6 -0
- package/dist/relay-request.js +33 -0
- package/dist/server/http.js +11 -2
- package/dist/video-intelligence/jobs.d.ts +32 -0
- package/dist/video-intelligence/jobs.js +89 -0
- package/dist/video-intelligence/local-analysis.d.ts +9 -1
- package/dist/video-intelligence/local-analysis.js +36 -6
- 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/agent/codex.d.ts
CHANGED
|
@@ -48,6 +48,7 @@ export declare function flowCCodexWorkerStatus(): {
|
|
|
48
48
|
active: number;
|
|
49
49
|
limit: number;
|
|
50
50
|
};
|
|
51
|
+
export declare function boundedWorkflowProcessAttempts(value?: number): number;
|
|
51
52
|
/**
|
|
52
53
|
* 记录尚未确认 OS exit 的 worker 进程。worker 可以释放给队列,但在对应
|
|
53
54
|
* barrier 完成前只能等待/失败,绝不能启动替代 app-server。
|
|
@@ -70,6 +71,7 @@ export declare function interruptCodexTurn(threadId?: string): Promise<boolean>;
|
|
|
70
71
|
export declare function runCodexWorkflowTurn(prompt: string, emit: AgentEmit, options: CodexRunOptions & {
|
|
71
72
|
timeoutMs: number;
|
|
72
73
|
attachments?: AgentAttachment[];
|
|
74
|
+
maxProcessAttempts?: number;
|
|
73
75
|
onWorkerStart?: () => void;
|
|
74
76
|
onWorkerFinish?: () => void;
|
|
75
77
|
}): Promise<CodexWorkflowRunResult>;
|
package/dist/agent/codex.js
CHANGED
|
@@ -12,6 +12,11 @@ export const FLOW_C_CODEX_WORKER_CONCURRENCY = boundedWorkerConcurrency(process.
|
|
|
12
12
|
export const FLOW_C_CODEX_PROCESS_MAX_ATTEMPTS = 3;
|
|
13
13
|
const FLOW_C_CODEX_MIN_START_BUDGET_MS = 5_000;
|
|
14
14
|
export function flowCCodexWorkerStatus() { return { active: workflowCodexPool.activeCount, limit: FLOW_C_CODEX_WORKER_CONCURRENCY }; }
|
|
15
|
+
export function boundedWorkflowProcessAttempts(value) {
|
|
16
|
+
if (value === undefined)
|
|
17
|
+
return FLOW_C_CODEX_PROCESS_MAX_ATTEMPTS;
|
|
18
|
+
return Math.max(1, Math.min(FLOW_C_CODEX_PROCESS_MAX_ATTEMPTS, Math.floor(Number(value) || 1)));
|
|
19
|
+
}
|
|
15
20
|
let codexQueue = Promise.resolve();
|
|
16
21
|
let codexApp = null;
|
|
17
22
|
let codexAppStart = null;
|
|
@@ -88,6 +93,7 @@ export async function runCodexWorkflowTurn(prompt, emit, options) {
|
|
|
88
93
|
let modelMs = 0;
|
|
89
94
|
let app;
|
|
90
95
|
let processAttempts = 0;
|
|
96
|
+
const processMaxAttempts = boundedWorkflowProcessAttempts(options.maxProcessAttempts);
|
|
91
97
|
let processRecoveryObserved = false;
|
|
92
98
|
let cleanupConfirmed = true;
|
|
93
99
|
let files = [];
|
|
@@ -136,17 +142,17 @@ export async function runCodexWorkflowTurn(prompt, emit, options) {
|
|
|
136
142
|
threadStartMs += Date.now() - attemptStartedAt;
|
|
137
143
|
}
|
|
138
144
|
}, {
|
|
139
|
-
maxAttempts:
|
|
145
|
+
maxAttempts: processMaxAttempts,
|
|
140
146
|
backoffMs: (failedAttempt) => Math.max(0, Math.min(failedAttempt === 1 ? 250 : 750, deadlineAt - Date.now())),
|
|
141
147
|
onRetry: async (error, failedAttempt) => {
|
|
142
148
|
processRecoveryObserved = true;
|
|
143
|
-
cleanupConfirmed = await discardWorkflowCodexApp(workerIndex, app, `Flow C 本机脚本引擎第 ${failedAttempt}/${
|
|
149
|
+
cleanupConfirmed = await discardWorkflowCodexApp(workerIndex, app, `Flow C 本机脚本引擎第 ${failedAttempt}/${processMaxAttempts} 次进程异常,正在换新进程重试`);
|
|
144
150
|
app = undefined;
|
|
145
151
|
if (!cleanupConfirmed)
|
|
146
152
|
throw error;
|
|
147
153
|
if (deadlineAt - Date.now() < FLOW_C_CODEX_MIN_START_BUDGET_MS)
|
|
148
154
|
throw new CodexWorkflowTimeoutError();
|
|
149
|
-
emit("agent_log", { text: `Flow C 本机脚本引擎异常,已回收当前 worker,将进行第 ${failedAttempt + 1}/${
|
|
155
|
+
emit("agent_log", { text: `Flow C 本机脚本引擎异常,已回收当前 worker,将进行第 ${failedAttempt + 1}/${processMaxAttempts} 次尝试(${error.message})` });
|
|
150
156
|
},
|
|
151
157
|
});
|
|
152
158
|
return { ok: true, text, timings: { queueWaitMs, threadStartMs, modelMs } };
|
|
@@ -167,7 +173,7 @@ export async function runCodexWorkflowTurn(prompt, emit, options) {
|
|
|
167
173
|
let message = deadlineAfterProcessFailure
|
|
168
174
|
? `本机 Codex 脚本引擎进程异常后未能在原 8 分钟截止时间内完成恢复,已停止当前任务:${rawMessage}`
|
|
169
175
|
: transportFailure
|
|
170
|
-
? `本机 Codex 脚本引擎已自动尝试 ${Math.max(1, processAttempts)}/${
|
|
176
|
+
? `本机 Codex 脚本引擎已自动尝试 ${Math.max(1, processAttempts)}/${processMaxAttempts} 次仍失败:${rawMessage}`
|
|
171
177
|
: timeoutFailure ? "Flow C 脚本执行链路超过 8 分钟,已自动终止当前 worker" : rawMessage;
|
|
172
178
|
if ((processFailure || timeoutFailure) && app) {
|
|
173
179
|
cleanupConfirmed = await discardWorkflowCodexApp(workerIndex, app, "Flow C 脚本执行结束,正在确认当前 worker 已退出", timeoutFailure);
|
package/dist/relay-bridge.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
|
+
import { relayLocalRequest } from "./relay-request.js";
|
|
2
3
|
const DEFAULT_RELAY_URL = "wss://canvas.xiaohhhh1.com/api/agent-relay";
|
|
3
4
|
const RECONNECT_DELAY_MS = 3_000;
|
|
4
5
|
const HEARTBEAT_INTERVAL_MS = 15_000;
|
|
@@ -78,18 +79,20 @@ export function startRelayBridge(config, options = {}) {
|
|
|
78
79
|
const headers = new Headers(message.headers || {});
|
|
79
80
|
headers.set("x-canvas-agent-token", config.token);
|
|
80
81
|
const body = message.bodyBase64 ? Buffer.from(message.bodyBase64, "base64") : undefined;
|
|
81
|
-
const response = await
|
|
82
|
-
const bytes = Buffer.from(await response.arrayBuffer());
|
|
82
|
+
const response = await relayLocalRequest(target, message.method || "GET", headers, body);
|
|
83
83
|
send({
|
|
84
84
|
type: "response",
|
|
85
85
|
id: message.id,
|
|
86
86
|
status: response.status,
|
|
87
|
-
contentType: response.
|
|
88
|
-
bodyBase64: bytes.toString("base64"),
|
|
87
|
+
contentType: response.contentType,
|
|
88
|
+
bodyBase64: response.bytes.toString("base64"),
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
91
|
catch (error) {
|
|
92
|
-
|
|
92
|
+
const detail = error instanceof Error && error.message.startsWith("本机处理等待超时")
|
|
93
|
+
? error.message
|
|
94
|
+
: "本机 Agent 请求连接中断;原视频仍保留,请恢复连接后重试。";
|
|
95
|
+
send({ type: "response", id: message.id, status: 502, contentType: "application/json", bodyBase64: Buffer.from(JSON.stringify({ ok: false, error: detail })).toString("base64") });
|
|
93
96
|
}
|
|
94
97
|
};
|
|
95
98
|
const onMessage = (raw) => {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function relayRequestTimeoutMs(pathname: string): number;
|
|
2
|
+
export declare function relayLocalRequest(target: URL, method: string, headers: Headers, body?: Buffer, timeoutMs?: number): Promise<{
|
|
3
|
+
status: number;
|
|
4
|
+
contentType: string;
|
|
5
|
+
bytes: Buffer;
|
|
6
|
+
}>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import https from "node:https";
|
|
3
|
+
export function relayRequestTimeoutMs(pathname) {
|
|
4
|
+
if (["/agent/integrations/fastmoss/capture", "/agent/integrations/fastmoss/video-learning/capture"].includes(pathname))
|
|
5
|
+
return 25 * 60_000;
|
|
6
|
+
if (["/agent/video-intelligence/analyze", "/agent/video-intelligence/archive"].includes(pathname))
|
|
7
|
+
return 12 * 60_000;
|
|
8
|
+
return 20_000;
|
|
9
|
+
}
|
|
10
|
+
// Native fetch has a separate five-minute response-header deadline. A longer
|
|
11
|
+
// AbortSignal does not extend it: use Node's HTTP client with one explicit deadline.
|
|
12
|
+
export async function relayLocalRequest(target, method, headers, body, timeoutMs = relayRequestTimeoutMs(target.pathname)) {
|
|
13
|
+
return await new Promise((resolve, reject) => {
|
|
14
|
+
const request = (target.protocol === "https:" ? https : http).request(target, {
|
|
15
|
+
method, headers: Object.fromEntries(headers.entries()),
|
|
16
|
+
}, (response) => {
|
|
17
|
+
const chunks = [];
|
|
18
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
19
|
+
response.on("error", reject);
|
|
20
|
+
response.on("end", () => resolve({
|
|
21
|
+
status: response.statusCode || 502,
|
|
22
|
+
contentType: String(response.headers["content-type"] || "application/octet-stream"),
|
|
23
|
+
bytes: Buffer.concat(chunks),
|
|
24
|
+
}));
|
|
25
|
+
response.on("close", () => clearTimeout(timer));
|
|
26
|
+
});
|
|
27
|
+
const timer = setTimeout(() => request.destroy(new Error("本机处理等待超时;任务可能仍在运行,请先查看状态,勿重复提交。")), timeoutMs);
|
|
28
|
+
timer.unref();
|
|
29
|
+
request.on("error", reject);
|
|
30
|
+
request.on("close", () => clearTimeout(timer));
|
|
31
|
+
request.end(body);
|
|
32
|
+
});
|
|
33
|
+
}
|
package/dist/server/http.js
CHANGED
|
@@ -5,9 +5,10 @@ import express from "express";
|
|
|
5
5
|
import { runClaudeTurn } from "../agent/claude.js";
|
|
6
6
|
import { archiveCodexThread, interruptCodexTurn, isRecoverableThreadError, listCodexThreads, readCodexThread, resolveCodexApproval, resumeCodexThread, runCodexTurn, startCodexThread, summarizeCodexThread, verifyCodexThreadWorkspace } from "../agent/codex.js";
|
|
7
7
|
import { CanvasSession } from "../canvas/session.js";
|
|
8
|
-
import { DEFAULT_PORT, ensureSiteWorkspace, loadConfig, saveConfig, updateSiteWorkspace, VERSION } from "../config.js";
|
|
8
|
+
import { CONFIG_DIR, DEFAULT_PORT, ensureSiteWorkspace, loadConfig, saveConfig, updateSiteWorkspace, VERSION } from "../config.js";
|
|
9
9
|
import { FastMossIntegration } from "../integrations/fastmoss.js";
|
|
10
10
|
import { LocalVideoIntelligence } from "../video-intelligence/local-analysis.js";
|
|
11
|
+
import { VideoAnalysisJobs } from "../video-intelligence/jobs.js";
|
|
11
12
|
import { startRelayBridge } from "../relay-bridge.js";
|
|
12
13
|
import { logger } from "../utils/logger.js";
|
|
13
14
|
import { windowsRootExecutable, windowsSystemExecutable } from "../utils/windows.js";
|
|
@@ -35,6 +36,7 @@ export function startHttpServer() {
|
|
|
35
36
|
const workflows = new WorkflowManager(config, emit);
|
|
36
37
|
const fastmoss = new FastMossIntegration();
|
|
37
38
|
const videoIntelligence = new LocalVideoIntelligence(fastmoss);
|
|
39
|
+
const videoAnalysisJobs = new VideoAnalysisJobs(path.join(CONFIG_DIR, "video-analysis-jobs"), (source, onStage) => videoIntelligence.analyze(source, { onStage, timeoutMs: 30 * 60_000 }));
|
|
38
40
|
let relayStatus = { ready: false };
|
|
39
41
|
const app = express();
|
|
40
42
|
app.disable("x-powered-by");
|
|
@@ -59,7 +61,7 @@ export function startHttpServer() {
|
|
|
59
61
|
return void res.json({});
|
|
60
62
|
next();
|
|
61
63
|
});
|
|
62
|
-
app.get("/health", (_req, res) => res.json({ ...session.health(), version: VERSION, workflow: workflows.health(), relayReady: relayStatus.ready, relayLastReadyAt: relayStatus.lastReadyAt, relayLastDisconnectAt: relayStatus.lastDisconnectAt }));
|
|
64
|
+
app.get("/health", (_req, res) => res.json({ ...session.health(), version: VERSION, workflow: workflows.health(), videoAnalysisJobs: { version: 1, active: videoAnalysisJobs.activeCount }, relayReady: relayStatus.ready, relayLastReadyAt: relayStatus.lastReadyAt, relayLastDisconnectAt: relayStatus.lastDisconnectAt }));
|
|
63
65
|
app.get("/config", (_req, res) => res.json({ ok: true, url: config.url, hasToken: true }));
|
|
64
66
|
app.use((req, res, next) => {
|
|
65
67
|
if (validToken(req, requestUrl(req, config), config.token))
|
|
@@ -145,6 +147,13 @@ export function startHttpServer() {
|
|
|
145
147
|
app.post("/agent/integrations/fastmoss/video-learning/capture", route(async (req, res) => res.json({ ok: true, ...await fastmoss.captureLearningVideos(req.body || {}) })));
|
|
146
148
|
app.post("/agent/video-intelligence/archive", route(async (req, res) => res.json({ ok: true, ...await videoIntelligence.archive(req.body?.source || {}, req.body?.upload || {}) })));
|
|
147
149
|
app.post("/agent/video-intelligence/analyze", route(async (req, res) => res.json({ ok: true, ...await videoIntelligence.analyze(req.body?.source || req.body || {}) })));
|
|
150
|
+
app.post("/agent/video-intelligence/jobs", route(async (req, res) => res.json({ ok: true, job: await videoAnalysisJobs.start(req.body?.source || {}, String(req.body?.requestId || "")) })));
|
|
151
|
+
app.get("/agent/video-intelligence/jobs/:id", route(async (req, res) => {
|
|
152
|
+
const job = await videoAnalysisJobs.get(routeParam(req.params.id));
|
|
153
|
+
if (!job)
|
|
154
|
+
return res.status(404).json({ ok: false, error: "本机视频分析任务不存在" });
|
|
155
|
+
return res.json({ ok: true, job });
|
|
156
|
+
}));
|
|
148
157
|
app.post("/agent/integrations/fastmoss/close", route(async (_req, res) => res.json({ ok: true, ...await fastmoss.close() })));
|
|
149
158
|
app.get("/agent/codex/workspace", (_req, res) => {
|
|
150
159
|
const workspace = ensureSiteWorkspace(config);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type LocalVideoLearningSource } from "./local-analysis.js";
|
|
2
|
+
type Result = {
|
|
3
|
+
analysis: Record<string, unknown>;
|
|
4
|
+
evidence: Record<string, unknown>;
|
|
5
|
+
};
|
|
6
|
+
type Job = {
|
|
7
|
+
id: string;
|
|
8
|
+
attemptId: string;
|
|
9
|
+
requestId: string;
|
|
10
|
+
status: "running" | "completed" | "failed";
|
|
11
|
+
stage: string;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
result?: Result;
|
|
14
|
+
error?: string;
|
|
15
|
+
};
|
|
16
|
+
type Analyze = (source: LocalVideoLearningSource, stage: (value: string) => Promise<void>) => Promise<Result>;
|
|
17
|
+
export declare class VideoAnalysisJobs {
|
|
18
|
+
private directory;
|
|
19
|
+
private analyze;
|
|
20
|
+
private active;
|
|
21
|
+
private locks;
|
|
22
|
+
constructor(directory: string, analyze: Analyze);
|
|
23
|
+
get activeCount(): number;
|
|
24
|
+
start(source: LocalVideoLearningSource & {
|
|
25
|
+
archived_sha256?: string;
|
|
26
|
+
archivedSha256?: string;
|
|
27
|
+
}, requestId: string): Promise<Job>;
|
|
28
|
+
get(id: string): Promise<Job | null>;
|
|
29
|
+
private run;
|
|
30
|
+
private save;
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { localVideoAnalysisPrompt } from "./local-analysis.js";
|
|
5
|
+
export class VideoAnalysisJobs {
|
|
6
|
+
directory;
|
|
7
|
+
analyze;
|
|
8
|
+
active = new Map();
|
|
9
|
+
locks = new Map();
|
|
10
|
+
constructor(directory, analyze) {
|
|
11
|
+
this.directory = directory;
|
|
12
|
+
this.analyze = analyze;
|
|
13
|
+
}
|
|
14
|
+
get activeCount() { return this.active.size; }
|
|
15
|
+
async start(source, requestId) {
|
|
16
|
+
if (!requestId || requestId.length > 100)
|
|
17
|
+
throw new Error("视频分析请求标识无效");
|
|
18
|
+
const identity = source.archived_sha256 || source.archivedSha256 || source.sourceUrl || source.source_url;
|
|
19
|
+
if (!source.id || !identity)
|
|
20
|
+
throw new Error("视频分析缺少稳定原片身份,请刷新后重试");
|
|
21
|
+
const id = createHash("sha256").update(JSON.stringify([source.id, identity, localVideoAnalysisPrompt(source, 0, "", [])])).digest("hex");
|
|
22
|
+
// Serialize admission for the same source, including network replay.
|
|
23
|
+
const previous = this.locks.get(id) || Promise.resolve();
|
|
24
|
+
const admission = previous.catch(() => undefined).then(async () => {
|
|
25
|
+
const existing = await this.get(id);
|
|
26
|
+
if (existing && (existing.status === "completed" || this.active.has(id) || existing.requestId === requestId))
|
|
27
|
+
return existing;
|
|
28
|
+
const job = { id, attemptId: randomUUID(), requestId, status: "running", stage: "读取原片与抽帧", updatedAt: new Date().toISOString() };
|
|
29
|
+
await this.save(job);
|
|
30
|
+
const task = this.run(job, source).catch(() => undefined).finally(() => this.active.delete(id));
|
|
31
|
+
this.active.set(id, task);
|
|
32
|
+
return { ...job };
|
|
33
|
+
});
|
|
34
|
+
this.locks.set(id, admission);
|
|
35
|
+
try {
|
|
36
|
+
return await admission;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
if (this.locks.get(id) === admission)
|
|
40
|
+
this.locks.delete(id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async get(id) {
|
|
44
|
+
if (!/^[a-f0-9]{64}$/.test(id))
|
|
45
|
+
throw new Error("视频分析任务标识无效");
|
|
46
|
+
let job;
|
|
47
|
+
try {
|
|
48
|
+
job = JSON.parse(await readFile(path.join(this.directory, id + ".json"), "utf8"));
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code === "ENOENT")
|
|
52
|
+
return null;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
if (job.status === "running" && !this.active.has(id)) {
|
|
56
|
+
job = { ...job, status: "failed", error: "本机程序曾中断,原视频保留;点击重试可重新分析。", stage: "等待手动重试" };
|
|
57
|
+
await this.save(job);
|
|
58
|
+
}
|
|
59
|
+
return job;
|
|
60
|
+
}
|
|
61
|
+
async run(job, source) {
|
|
62
|
+
try {
|
|
63
|
+
const result = await this.analyze(source, async (stage) => {
|
|
64
|
+
job.stage = stage;
|
|
65
|
+
await this.save(job);
|
|
66
|
+
});
|
|
67
|
+
// Persist before the browser can import the result. A lost response
|
|
68
|
+
// or refreshed page reads the same output without another model call.
|
|
69
|
+
job.result = result;
|
|
70
|
+
job.status = "completed";
|
|
71
|
+
job.stage = "分析已保存,等待网页回传";
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
job.status = "failed";
|
|
75
|
+
const detail = error instanceof Error ? error.message : "本机视频分析失败";
|
|
76
|
+
job.error = detail === "fetch failed" ? `${job.stage}时网络连接失败;原片保留,请恢复网络后重试。` : detail;
|
|
77
|
+
job.stage = "分析失败,可手动重试";
|
|
78
|
+
}
|
|
79
|
+
await this.save(job);
|
|
80
|
+
}
|
|
81
|
+
async save(job) {
|
|
82
|
+
job.updatedAt = new Date().toISOString();
|
|
83
|
+
await mkdir(this.directory, { recursive: true });
|
|
84
|
+
const file = path.join(this.directory, job.id + ".json");
|
|
85
|
+
const temporary = file + "." + randomUUID() + ".tmp";
|
|
86
|
+
await writeFile(temporary, JSON.stringify(job), { mode: 0o600 });
|
|
87
|
+
await rename(temporary, file);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -53,7 +53,10 @@ export declare class LocalVideoIntelligence {
|
|
|
53
53
|
durationMs: number;
|
|
54
54
|
};
|
|
55
55
|
}>;
|
|
56
|
-
analyze(source: LocalVideoLearningSource
|
|
56
|
+
analyze(source: LocalVideoLearningSource, options?: {
|
|
57
|
+
onStage?: (stage: string) => Promise<void>;
|
|
58
|
+
timeoutMs?: number;
|
|
59
|
+
}): Promise<{
|
|
57
60
|
analysis: Record<string, unknown>;
|
|
58
61
|
evidence: {
|
|
59
62
|
durationMs: number;
|
|
@@ -100,4 +103,9 @@ export declare function assertVideoMediaProbe(parsed: VideoProbeResult): {
|
|
|
100
103
|
};
|
|
101
104
|
export declare function transcribeLocalMedia(videoFile: string | undefined, workDir: string): Promise<string>;
|
|
102
105
|
export declare function resolveLocalCodexEntrypoint(): string;
|
|
106
|
+
/** CLI stderr can contain the prompt and unrelated MCP logs; never return it to the website. */
|
|
107
|
+
export declare function localCodexAnalysisError(result: {
|
|
108
|
+
stdout: string;
|
|
109
|
+
error: string;
|
|
110
|
+
}): "本机 Codex 视频理解失败:Agent 自带的 Codex 版本过旧,当前模型需要更新运行程序。原视频已保存,更新后可直接重试反推,无需重新上传。" | "本机 Codex 视频理解失败:当前账号的模型额度或请求频率受限,请待额度恢复后重试。原视频已保存,无需重新上传。" | "本机 Codex 视频理解失败:本机 Codex 登录已失效,请恢复登录后重试。原视频已保存,无需重新上传。" | "本机 Codex 视频理解超时,原视频已保存,可直接重试反推,无需重新上传。" | "本机 Codex 未完成视频理解,原视频已保存,可直接重试反推。若持续失败,请检查本机 Codex 运行状态。";
|
|
103
111
|
export {};
|
|
@@ -60,7 +60,7 @@ export class LocalVideoIntelligence {
|
|
|
60
60
|
await rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
-
async analyze(source) {
|
|
63
|
+
async analyze(source, options) {
|
|
64
64
|
const archivedVideoUrl = String(source.archivedVideoUrl || source.archived_video_url || "").trim();
|
|
65
65
|
const archivedTranscript = String(source.archivedTranscript || source.archived_transcript || "").trim();
|
|
66
66
|
const hasArchivedMedia = Boolean(archivedVideoUrl);
|
|
@@ -74,6 +74,7 @@ export class LocalVideoIntelligence {
|
|
|
74
74
|
let transcript = archivedTranscript;
|
|
75
75
|
let transcriptSource = archivedTranscript ? "website-archive-caption" : "";
|
|
76
76
|
if (!transcript && hasArchivedMedia) {
|
|
77
|
+
await options?.onStage?.("识别原片口播");
|
|
77
78
|
transcript = await transcribeLocalMedia("videoFile" in visualEvidence && typeof visualEvidence.videoFile === "string" ? visualEvidence.videoFile : undefined, workDir);
|
|
78
79
|
transcriptSource = transcript ? `local-asr:${LOCAL_ASR_MODEL}` : "local-asr:no-speech-detected";
|
|
79
80
|
}
|
|
@@ -85,7 +86,8 @@ export class LocalVideoIntelligence {
|
|
|
85
86
|
throw new Error("没有取得任何真实视频画面,已停止分析");
|
|
86
87
|
const attachments = await materializeFrames(visualEvidence.frames, workDir);
|
|
87
88
|
const prompt = localVideoAnalysisPrompt(source, visualEvidence.durationMs, transcript, attachments);
|
|
88
|
-
|
|
89
|
+
await options?.onStage?.("Codex 正在对齐画面与口播,生成详细蓝图");
|
|
90
|
+
const analysis = await runLocalCodexAnalysis(prompt, attachments, workDir, options?.timeoutMs);
|
|
89
91
|
assertJointVisualAndSpokenEvidence(analysis);
|
|
90
92
|
return {
|
|
91
93
|
analysis,
|
|
@@ -485,19 +487,19 @@ function transcriptPreference(file) {
|
|
|
485
487
|
return 3;
|
|
486
488
|
return 1;
|
|
487
489
|
}
|
|
488
|
-
async function runLocalCodexAnalysis(prompt, attachments, workDir) {
|
|
490
|
+
async function runLocalCodexAnalysis(prompt, attachments, workDir, timeoutMs = ANALYSIS_TIMEOUT_MS) {
|
|
489
491
|
const outputFile = path.join(workDir, "analysis.json");
|
|
490
492
|
// npm hoists dependencies next to the installed package in production, while
|
|
491
493
|
// local development may keep them at a different ancestor. Resolve from this
|
|
492
494
|
// module instead of assuming a nested node_modules directory.
|
|
493
495
|
const codexEntrypoint = resolveLocalCodexEntrypoint();
|
|
494
|
-
const args = [codexEntrypoint, "exec", "--ephemeral", "--skip-git-repo-check", "--sandbox", "read-only", "--color", "never", "--output-last-message", outputFile];
|
|
496
|
+
const args = [codexEntrypoint, "exec", "--json", "--ephemeral", "--skip-git-repo-check", "--sandbox", "read-only", "--color", "never", "--output-last-message", outputFile];
|
|
495
497
|
for (const attachment of attachments)
|
|
496
498
|
args.push("--image", path.join(workDir, String(attachment.name)));
|
|
497
499
|
args.push("-");
|
|
498
|
-
const result = await runProcess(process.execPath, args, { cwd: workDir, timeoutMs
|
|
500
|
+
const result = await runProcess(process.execPath, args, { cwd: workDir, timeoutMs, stdin: prompt });
|
|
499
501
|
if (!result.ok)
|
|
500
|
-
throw new Error(
|
|
502
|
+
throw new Error(localCodexAnalysisError(result));
|
|
501
503
|
const raw = (await readFile(outputFile, "utf8")).trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "");
|
|
502
504
|
try {
|
|
503
505
|
return JSON.parse(raw);
|
|
@@ -509,6 +511,34 @@ async function runLocalCodexAnalysis(prompt, attachments, workDir) {
|
|
|
509
511
|
export function resolveLocalCodexEntrypoint() {
|
|
510
512
|
return createRequire(import.meta.url).resolve("@openai/codex/bin/codex.js");
|
|
511
513
|
}
|
|
514
|
+
/** CLI stderr can contain the prompt and unrelated MCP logs; never return it to the website. */
|
|
515
|
+
export function localCodexAnalysisError(result) {
|
|
516
|
+
const events = result.stdout.split(/\r?\n/).flatMap((line) => {
|
|
517
|
+
try {
|
|
518
|
+
const event = JSON.parse(line);
|
|
519
|
+
if (["error", "turn.failed"].includes(String(event.type)))
|
|
520
|
+
return [String(event.error?.message || event.message || "")];
|
|
521
|
+
if (event.item?.type === "error")
|
|
522
|
+
return [String(event.item.message || "")];
|
|
523
|
+
}
|
|
524
|
+
catch { /* Non-event output is not a user-facing diagnostic. */ }
|
|
525
|
+
return [];
|
|
526
|
+
});
|
|
527
|
+
const terminal = events.at(-1) || result.error;
|
|
528
|
+
if (/requires a newer version of Codex|please upgrade to the latest app or CLI/i.test(terminal)) {
|
|
529
|
+
return "本机 Codex 视频理解失败:Agent 自带的 Codex 版本过旧,当前模型需要更新运行程序。原视频已保存,更新后可直接重试反推,无需重新上传。";
|
|
530
|
+
}
|
|
531
|
+
if (/usage limit|rate.?limit|quota exceeded|too many requests/i.test(terminal)) {
|
|
532
|
+
return "本机 Codex 视频理解失败:当前账号的模型额度或请求频率受限,请待额度恢复后重试。原视频已保存,无需重新上传。";
|
|
533
|
+
}
|
|
534
|
+
if (/unauthori[sz]ed|authentication|not logged in|please (?:log|sign) in/i.test(terminal)) {
|
|
535
|
+
return "本机 Codex 视频理解失败:本机 Codex 登录已失效,请恢复登录后重试。原视频已保存,无需重新上传。";
|
|
536
|
+
}
|
|
537
|
+
if (/处理超过 \d+ 秒|timed? ?out|timeout/i.test(terminal)) {
|
|
538
|
+
return "本机 Codex 视频理解超时,原视频已保存,可直接重试反推,无需重新上传。";
|
|
539
|
+
}
|
|
540
|
+
return "本机 Codex 未完成视频理解,原视频已保存,可直接重试反推。若持续失败,请检查本机 Codex 运行状态。";
|
|
541
|
+
}
|
|
512
542
|
async function runProcess(command, args, options) {
|
|
513
543
|
return await new Promise((resolve) => {
|
|
514
544
|
let stdout = "";
|
|
@@ -2,6 +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 FLOW_C_VOICE_PACING_REPAIR_MIN_WORDS_PER_SECOND = 3;
|
|
6
|
+
export declare const FLOW_C_VOICE_PACING_REPAIR_MIN_EXCESS_WORDS = 3;
|
|
5
7
|
export type FlowCContentStrategy = {
|
|
6
8
|
contractVersion: typeof FLOW_C_CONTENT_STRATEGY_VERSION;
|
|
7
9
|
mode: "smart-diverse" | "best-match";
|
|
@@ -38,6 +40,9 @@ export type FlowCContentAdvisory = {
|
|
|
38
40
|
durationSeconds?: number;
|
|
39
41
|
matchedOrdinal?: number;
|
|
40
42
|
};
|
|
43
|
+
export type FlowCVoicePacingRepairIssue = Required<Pick<FlowCContentAdvisory, "ordinal" | "segment" | "shot" | "wordCount" | "suggestedMaxWords" | "durationSeconds">> & {
|
|
44
|
+
code: "voice_pacing";
|
|
45
|
+
};
|
|
41
46
|
/** Explicit styles are capability-versioned; an unknown pair must never silently fall back. */
|
|
42
47
|
export declare function flowCGeneratedMontage(value: unknown): boolean;
|
|
43
48
|
/** A short same-turn writing/review sequence. It never creates another model stage or output field. */
|
|
@@ -58,6 +63,59 @@ export declare function flowCContentAdvisories(jobs: unknown, strategy: FlowCCon
|
|
|
58
63
|
targetLanguage?: unknown;
|
|
59
64
|
recentScripts?: unknown;
|
|
60
65
|
}): FlowCContentAdvisory[];
|
|
66
|
+
/**
|
|
67
|
+
* A deliberately narrower pre-delivery repair trigger than the public pacing
|
|
68
|
+
* advisory. It only covers explicit English/Spanish text that is both very fast
|
|
69
|
+
* and materially over the normal two-words-per-second writing budget.
|
|
70
|
+
*/
|
|
71
|
+
export declare function flowCVoicePacingRepairIssues(jobs: unknown, options?: {
|
|
72
|
+
targetLanguage?: unknown;
|
|
73
|
+
}): FlowCVoicePacingRepairIssue[];
|
|
74
|
+
/** Compact, text-bearing repair input derived only from an already validated draft. */
|
|
75
|
+
export declare function flowCVoicePacingRepairScaffold(value: unknown): {
|
|
76
|
+
segments: {
|
|
77
|
+
voiceCue: unknown;
|
|
78
|
+
endingState: {
|
|
79
|
+
[k: string]: unknown;
|
|
80
|
+
};
|
|
81
|
+
shots: {
|
|
82
|
+
[k: string]: unknown;
|
|
83
|
+
}[];
|
|
84
|
+
}[];
|
|
85
|
+
ordinal: unknown;
|
|
86
|
+
productIndex: unknown;
|
|
87
|
+
sellingFormId: unknown;
|
|
88
|
+
voiceProfile: {
|
|
89
|
+
[k: string]: unknown;
|
|
90
|
+
};
|
|
91
|
+
openingState: {
|
|
92
|
+
openingFrame: unknown;
|
|
93
|
+
};
|
|
94
|
+
} | {
|
|
95
|
+
segment: {
|
|
96
|
+
voiceCue: unknown;
|
|
97
|
+
endingState: {
|
|
98
|
+
[k: string]: unknown;
|
|
99
|
+
};
|
|
100
|
+
shots: {
|
|
101
|
+
[k: string]: unknown;
|
|
102
|
+
}[];
|
|
103
|
+
};
|
|
104
|
+
ordinal: unknown;
|
|
105
|
+
productIndex: unknown;
|
|
106
|
+
sellingFormId: unknown;
|
|
107
|
+
voiceProfile: {
|
|
108
|
+
[k: string]: unknown;
|
|
109
|
+
};
|
|
110
|
+
openingState: {
|
|
111
|
+
openingFrame: unknown;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
/** One bounded correction turn; the caller still enforces VO-only projection. */
|
|
115
|
+
export declare function flowCVoicePacingRepairPrompt(jobs: unknown, options?: {
|
|
116
|
+
targetLanguage?: unknown;
|
|
117
|
+
frameworkOrdinals?: readonly number[];
|
|
118
|
+
}): string;
|
|
61
119
|
export declare function flowCContentMethodPrompt(strategy: FlowCContentStrategy | null, options: {
|
|
62
120
|
recentScripts?: unknown;
|
|
63
121
|
productIndexes: number[];
|
|
@@ -2,6 +2,8 @@ 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
|
+
export const FLOW_C_VOICE_PACING_REPAIR_MIN_WORDS_PER_SECOND = 3;
|
|
6
|
+
export const FLOW_C_VOICE_PACING_REPAIR_MIN_EXCESS_WORDS = 3;
|
|
5
7
|
function object(value) {
|
|
6
8
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7
9
|
}
|
|
@@ -11,6 +13,22 @@ function text(value, limit) {
|
|
|
11
13
|
function summaryText(value, limit) {
|
|
12
14
|
return text(typeof value === "string" ? value.replace(/https?:\/\/\S+|data:\S+/gi, "[link]") : "", limit);
|
|
13
15
|
}
|
|
16
|
+
function spokenText(value) {
|
|
17
|
+
const line = text(typeof value === "string" ? value : "", 20_000);
|
|
18
|
+
return /^(?:none|无|sin voz|sin diálogo)$/i.test(line) ? "" : line;
|
|
19
|
+
}
|
|
20
|
+
function usesWhitespaceWordBudget(value) {
|
|
21
|
+
const language = text(value, 160).toLowerCase();
|
|
22
|
+
return /^(?:en|es)(?:[-_]|$)|\b(?:english|spanish|español|espanol|inglés|ingles)\b|英语|英語|美语|美語|西班牙语|西班牙語|西语|西語/u.test(language);
|
|
23
|
+
}
|
|
24
|
+
function voicePacingMeasurement(value, durationSeconds, targetLanguage) {
|
|
25
|
+
const voice = spokenText(value);
|
|
26
|
+
const seconds = Number(durationSeconds);
|
|
27
|
+
if (!voice || !usesWhitespaceWordBudget(targetLanguage) || /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u.test(voice) || !Number.isFinite(seconds) || seconds <= 0)
|
|
28
|
+
return null;
|
|
29
|
+
const wordCount = (voice.match(/[\p{L}\p{N}]+(?:['’][\p{L}\p{N}]+)*/gu) || []).length;
|
|
30
|
+
return { wordCount, suggestedMaxWords: Math.floor(seconds * 2), durationSeconds: seconds };
|
|
31
|
+
}
|
|
14
32
|
/** Explicit styles are capability-versioned; an unknown pair must never silently fall back. */
|
|
15
33
|
export function flowCGeneratedMontage(value) {
|
|
16
34
|
const input = object(value);
|
|
@@ -109,12 +127,6 @@ export function mergeFlowCContentSummaries(values, jobs, acceptedOrdinals) {
|
|
|
109
127
|
export function flowCContentAdvisories(jobs, strategy, options = {}) {
|
|
110
128
|
if (!strategy || !Array.isArray(jobs))
|
|
111
129
|
return [];
|
|
112
|
-
const language = text(options.targetLanguage, 160).toLowerCase();
|
|
113
|
-
const wordBudgetApplies = /^(?:en|es)(?:[-_]|$)|\b(?:english|spanish|español|espanol|inglés|ingles)\b|英语|英語|美语|美語|西班牙语|西班牙語|西语|西語/u.test(language);
|
|
114
|
-
const spoken = (value) => {
|
|
115
|
-
const line = text(typeof value === "string" ? value : "", 20_000);
|
|
116
|
-
return /^(?:none|无|sin voz|sin diálogo)$/i.test(line) ? "" : line;
|
|
117
|
-
};
|
|
118
130
|
const seen = (Array.isArray(options.recentScripts) ? options.recentScripts : []).map(summary).filter((item) => Boolean(item));
|
|
119
131
|
const advisories = [];
|
|
120
132
|
for (const value of jobs) {
|
|
@@ -128,7 +140,7 @@ export function flowCContentAdvisories(jobs, strategy, options = {}) {
|
|
|
128
140
|
const segment = object(segmentValue);
|
|
129
141
|
const shots = Array.isArray(segment.shots) ? segment.shots.map(object) : [];
|
|
130
142
|
for (const [shotIndex, shot] of shots.entries()) {
|
|
131
|
-
const voice =
|
|
143
|
+
const voice = spokenText(shot.voiceover);
|
|
132
144
|
if (!opening && voice)
|
|
133
145
|
opening = voice;
|
|
134
146
|
const evidence = text(shot.evidence, 20_000).replace(/[.!。!]+$/u, "").toLowerCase();
|
|
@@ -137,12 +149,9 @@ export function flowCContentAdvisories(jobs, strategy, options = {}) {
|
|
|
137
149
|
advisories.push({ ordinal, code: "voice_without_visible_evidence", segment: segmentIndex + 1, shot: shotIndex + 1 });
|
|
138
150
|
const seconds = Number(shot.endSeconds) - Number(shot.startSeconds);
|
|
139
151
|
// Do not apply an English/Spanish word estimate to other scripts.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const suggestedMaxWords = Math.floor(seconds * 2);
|
|
144
|
-
if (words > suggestedMaxWords)
|
|
145
|
-
advisories.push({ ordinal, code: "voice_pacing", segment: segmentIndex + 1, shot: shotIndex + 1, wordCount: words, suggestedMaxWords, durationSeconds: seconds });
|
|
152
|
+
const pacing = voicePacingMeasurement(voice, seconds, options.targetLanguage);
|
|
153
|
+
if (pacing && pacing.wordCount > pacing.suggestedMaxWords)
|
|
154
|
+
advisories.push({ ordinal, code: "voice_pacing", segment: segmentIndex + 1, shot: shotIndex + 1, ...pacing });
|
|
146
155
|
}
|
|
147
156
|
const frame = text(object(segment.endingState).endingFrame, 20_000);
|
|
148
157
|
if (shots.length && frame && !text(shots.at(-1)?.visual, 20_000).endsWith(frame)) {
|
|
@@ -150,7 +159,7 @@ export function flowCContentAdvisories(jobs, strategy, options = {}) {
|
|
|
150
159
|
}
|
|
151
160
|
}
|
|
152
161
|
if (strategy.mode === "smart-diverse" && opening) {
|
|
153
|
-
const match = seen.find((item) => item.ordinal !== ordinal && item.productIndex === job.productIndex &&
|
|
162
|
+
const match = seen.find((item) => item.ordinal !== ordinal && item.productIndex === job.productIndex && spokenText(item.voiceover).toLowerCase() === opening.toLowerCase());
|
|
154
163
|
if (match)
|
|
155
164
|
advisories.push({ ordinal, code: "repeated_opening", matchedOrdinal: match.ordinal });
|
|
156
165
|
seen.push({ ordinal, productIndex: Number(job.productIndex), opening: "", proof: "", voiceover: opening });
|
|
@@ -158,6 +167,83 @@ export function flowCContentAdvisories(jobs, strategy, options = {}) {
|
|
|
158
167
|
}
|
|
159
168
|
return advisories;
|
|
160
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* A deliberately narrower pre-delivery repair trigger than the public pacing
|
|
172
|
+
* advisory. It only covers explicit English/Spanish text that is both very fast
|
|
173
|
+
* and materially over the normal two-words-per-second writing budget.
|
|
174
|
+
*/
|
|
175
|
+
export function flowCVoicePacingRepairIssues(jobs, options = {}) {
|
|
176
|
+
if (!Array.isArray(jobs))
|
|
177
|
+
return [];
|
|
178
|
+
const issues = [];
|
|
179
|
+
for (const value of jobs) {
|
|
180
|
+
const job = object(value);
|
|
181
|
+
if (!Number.isInteger(job.ordinal) || Number(job.ordinal) < 1)
|
|
182
|
+
continue;
|
|
183
|
+
const segments = Array.isArray(job.segments) ? job.segments : job.segment ? [job.segment] : [];
|
|
184
|
+
for (const [segmentIndex, segmentValue] of segments.entries()) {
|
|
185
|
+
const shots = Array.isArray(object(segmentValue).shots) ? object(segmentValue).shots.map(object) : [];
|
|
186
|
+
for (const [shotIndex, shot] of shots.entries()) {
|
|
187
|
+
const pacing = voicePacingMeasurement(shot.voiceover, Number(shot.endSeconds) - Number(shot.startSeconds), options.targetLanguage);
|
|
188
|
+
if (!pacing || pacing.wordCount / pacing.durationSeconds < FLOW_C_VOICE_PACING_REPAIR_MIN_WORDS_PER_SECOND || pacing.wordCount - pacing.suggestedMaxWords < FLOW_C_VOICE_PACING_REPAIR_MIN_EXCESS_WORDS)
|
|
189
|
+
continue;
|
|
190
|
+
issues.push({ ordinal: Number(job.ordinal), code: "voice_pacing", segment: segmentIndex + 1, shot: shotIndex + 1, ...pacing });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return issues;
|
|
195
|
+
}
|
|
196
|
+
const repairContinuityFields = ["character", "wardrobe", "location", "lighting", "productState", "unfinishedAction", "nextGoal"];
|
|
197
|
+
const repairVoiceProfileFields = ["gender", "ageImpression", "pitch", "timbre", "speakingRate", "accent", "pauseHabit", "emotionalBaseline"];
|
|
198
|
+
const repairShotFields = ["startSeconds", "endSeconds", "visual", "voiceover", "onScreenText", "evidence", "soundBgm", "emotionalNote"];
|
|
199
|
+
function repairPromptSegment(value) {
|
|
200
|
+
const segment = object(value);
|
|
201
|
+
const endingState = object(segment.endingState);
|
|
202
|
+
const shots = Array.isArray(segment.shots) ? segment.shots.map((shotValue) => {
|
|
203
|
+
const shot = object(shotValue);
|
|
204
|
+
return Object.fromEntries(repairShotFields.map((field) => [field, shot[field]]));
|
|
205
|
+
}) : [];
|
|
206
|
+
return {
|
|
207
|
+
voiceCue: segment.voiceCue,
|
|
208
|
+
endingState: Object.fromEntries([...repairContinuityFields, "endingFrame"].map((field) => [field, endingState[field]])),
|
|
209
|
+
shots,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/** Compact, text-bearing repair input derived only from an already validated draft. */
|
|
213
|
+
export function flowCVoicePacingRepairScaffold(value) {
|
|
214
|
+
const job = object(value);
|
|
215
|
+
const segments = Array.isArray(job.segments) ? job.segments : job.segment ? [job.segment] : [];
|
|
216
|
+
const firstContinuity = object(object(segments[0]).continuity);
|
|
217
|
+
const voiceProfile = object(job.voiceProfile);
|
|
218
|
+
const base = {
|
|
219
|
+
ordinal: job.ordinal,
|
|
220
|
+
productIndex: job.productIndex,
|
|
221
|
+
sellingFormId: job.sellingFormId,
|
|
222
|
+
voiceProfile: Object.fromEntries(repairVoiceProfileFields.map((field) => [field, voiceProfile[field]])),
|
|
223
|
+
openingState: {
|
|
224
|
+
...Object.fromEntries(repairContinuityFields.map((field) => [field, firstContinuity[field]])),
|
|
225
|
+
openingFrame: firstContinuity.previousEndingFrame,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
return Array.isArray(job.segments)
|
|
229
|
+
? { ...base, segments: segments.map(repairPromptSegment) }
|
|
230
|
+
: { ...base, segment: repairPromptSegment(segments[0]) };
|
|
231
|
+
}
|
|
232
|
+
/** One bounded correction turn; the caller still enforces VO-only projection. */
|
|
233
|
+
export function flowCVoicePacingRepairPrompt(jobs, options = {}) {
|
|
234
|
+
const values = Array.isArray(jobs) ? jobs : [];
|
|
235
|
+
const issues = flowCVoicePacingRepairIssues(values, { targetLanguage: options.targetLanguage });
|
|
236
|
+
const issueOrdinals = [...new Set(issues.map((issue) => issue.ordinal))];
|
|
237
|
+
const frameworkOrdinals = [...new Set((Array.isArray(options.frameworkOrdinals) ? options.frameworkOrdinals : []).map(Number).filter((ordinal) => issueOrdinals.includes(ordinal)))];
|
|
238
|
+
const payload = values.filter((value) => issueOrdinals.includes(Number(object(value).ordinal))).map(flowCVoicePacingRepairScaffold);
|
|
239
|
+
return `这是 Flow C 首次回传前唯一一次、仅针对 ordinal ${JSON.stringify(issueOrdinals)} 的短镜口播定向修复。目标口播语言保持为 ${text(options.targetLanguage, 160) || "原稿的显式目标语言"}。只返回下方完整 strict jobs,不调用工具、不创建媒体、不输出分析或新增字段。
|
|
240
|
+
- 只允许修改每个 shot.voiceover;逐字复制其它所有字段,包括 ordinal/productIndex/sellingFormId、voiceProfile、openingState、segment/segments 数量、voiceCue、shots 数量和顺序、startSeconds/endSeconds、visual、onScreenText、evidence、soundBgm、emotionalNote 与 endingState。不得改画面、时轴、商品、事实、用户框架、所选结构、分段承接或末帧。
|
|
241
|
+
- 保留原口播的核心购买理由、画面对应事实、语气、CTA 意图和目标语言。先把模型自行增加的赘词压成能自然说完的短句,再按完整词组或自然分句重分配到展示相关动作的镜头;不得截断单词、留下未完句、提高语速或把超载片段简单改成 none。若一个原本有口播的局部 10 秒段修后完全静默,视为失败。
|
|
242
|
+
- 英语/西语逐镜以约 2 词/秒为写作目标并留呼吸;当前硬修复命中位置:${JSON.stringify(issues.map(({ ordinal, segment, shot, wordCount, suggestedMaxWords, durationSeconds }) => ({ ordinal, segment, shot, wordCount, suggestedMaxWords, durationSeconds })))}。不能用同段其它静默镜头抵消当前短镜超载。
|
|
243
|
+
- 用户逐字框架 ordinal ${JSON.stringify(frameworkOrdinals)}:所有原口播文字与顺序必须保持,不能删、换词或压缩,只能在同一局部 10 秒段内按完整自然短语重新分配到语义相关、时长足够的镜头;无法容纳就原样返回,让 Agent 明确交给人工处理。
|
|
244
|
+
- 其它 ordinal 可以压缩模型自增措辞,但不能通过删除整段口播来消除告警。每个 10 秒段仍须在自然句法边界收尾,segmentVoiceovers 与渲染 script 将由 Agent 从最终逐镜 voiceover 确定性重建。
|
|
245
|
+
待修复的已校验原稿:${JSON.stringify({ jobs: payload })}`;
|
|
246
|
+
}
|
|
161
247
|
export function flowCContentMethodPrompt(strategy, options) {
|
|
162
248
|
if (!strategy)
|
|
163
249
|
return "";
|
|
@@ -7,6 +7,8 @@ export declare const FLOW_C_CODEX_TURN_TIMEOUT_MS: number;
|
|
|
7
7
|
export declare const FLOW_C_SELECTED_BLUEPRINT_PROMPT_MAX_CHARS = 45000;
|
|
8
8
|
export declare const FLOW_C_SCRIPT_REWRITE_MAX_ATTEMPTS = 2;
|
|
9
9
|
export declare const FLOW_C_CANDIDATE_REVISION_MAX_ATTEMPTS = 2;
|
|
10
|
+
export declare const FLOW_C_VOICE_PACING_REPAIR_MAX_ATTEMPTS = 1;
|
|
11
|
+
export declare const FLOW_C_VOICE_PACING_REVIEW_REQUIRED = "FLOW_C_VOICE_PACING_REVIEW_REQUIRED";
|
|
10
12
|
export declare const FLOW_C_LEGACY_DIRECT_CONTRACT_VERSION = "flow-c-template-direct-v2";
|
|
11
13
|
export declare const FLOW_C_PRODUCT_PROFILE_DIRECT_CONTRACT_VERSION = "flow-c-product-profile-direct-v1";
|
|
12
14
|
export declare const FLOW_C_CREATIVE_SOURCE_DIRECT_CONTRACT_VERSION = "flow-c-creative-source-direct-v1";
|
|
@@ -27,6 +29,13 @@ type ScriptRecord = {
|
|
|
27
29
|
activeChunks?: number;
|
|
28
30
|
productProfiles?: FlowCProductExecutionProfile[];
|
|
29
31
|
pendingScriptJobs?: DraftJob[];
|
|
32
|
+
/** Locally recoverable originals; unlike pendingScriptJobs these must never be replayed to the center. */
|
|
33
|
+
voicePacingReviewJobs?: DraftJob[];
|
|
34
|
+
voicePacingRepairAttempts?: Array<{
|
|
35
|
+
ordinal: number;
|
|
36
|
+
candidateRevision: string;
|
|
37
|
+
attempts: number;
|
|
38
|
+
}>;
|
|
30
39
|
contentStrategy?: FlowCContentStrategy;
|
|
31
40
|
contentSummaries?: FlowCContentSummary[];
|
|
32
41
|
contentAdvisories?: FlowCContentAdvisory[];
|
|
@@ -187,7 +196,8 @@ type DraftJob = {
|
|
|
187
196
|
type ScriptChunkResult = {
|
|
188
197
|
error?: string;
|
|
189
198
|
terminal: boolean;
|
|
190
|
-
terminalKind?: "contract" | "transport" | "timeout" | "turn" | "delivery";
|
|
199
|
+
terminalKind?: "contract" | "transport" | "timeout" | "turn" | "delivery" | "review";
|
|
200
|
+
affectedOrdinals?: number[];
|
|
191
201
|
replanOrdinals?: number[];
|
|
192
202
|
};
|
|
193
203
|
/** 本机持久化的 Flow C 控制器:脚本交给本机 Codex,视频直接落盘。 */
|
|
@@ -392,6 +402,13 @@ export declare class WorkflowManager {
|
|
|
392
402
|
private runCandidateChunk;
|
|
393
403
|
/** 一个 chunk 使用独立线程;解析与中心持久化失败不会影响并行 lane。 */
|
|
394
404
|
private runScriptChunk;
|
|
405
|
+
private submitScriptJobsWithVoicePacingRepair;
|
|
406
|
+
/**
|
|
407
|
+
* One correction turn only. Originals live outside pendingScriptJobs, so a
|
|
408
|
+
* restart or response loss can never replay an overcrowded draft as accepted.
|
|
409
|
+
*/
|
|
410
|
+
private repairVoicePacingJobs;
|
|
411
|
+
private runVoicePacingRepairTurn;
|
|
395
412
|
/** Nonblocking, text-free diagnostics for newly generated policy scripts only. */
|
|
396
413
|
private noteScriptContentAdvisories;
|
|
397
414
|
/** 只记录阶段、ordinal 和毫秒数;禁止记录 prompt、令牌或中心能力。 */
|
|
@@ -409,6 +426,16 @@ export declare class WorkflowManager {
|
|
|
409
426
|
private save;
|
|
410
427
|
}
|
|
411
428
|
export declare function compareScriptQueueRecords(left: Pick<ScriptRecord, "priorityAt" | "updatedAt">, right: Pick<ScriptRecord, "priorityAt" | "updatedAt">): number;
|
|
429
|
+
export declare function flowCVoicePacingRepairTurnOptions(): {
|
|
430
|
+
readonly maxProcessAttempts: 1;
|
|
431
|
+
};
|
|
432
|
+
export declare function voicePacingRepairAttemptCount(record: Pick<ScriptRecord, "voicePacingRepairAttempts">, job: Pick<DraftJob, "ordinal" | "expectedCandidateRevision">): number;
|
|
433
|
+
/**
|
|
434
|
+
* Accept only repaired per-shot speech. Every visual/timeline/state/selection
|
|
435
|
+
* field comes from the locally validated original and rendered projections are
|
|
436
|
+
* rebuilt from that one final voice source.
|
|
437
|
+
*/
|
|
438
|
+
export declare function applyFlowCVoicePacingRepair(originalValue: unknown, candidateValue: unknown, preserveExactTranscript?: boolean): DraftJob;
|
|
412
439
|
/** A deterministic response-format failure stops fallback isolation immediately. */
|
|
413
440
|
export declare function terminalScriptChunkError(results: Array<{
|
|
414
441
|
error?: string;
|
|
@@ -418,11 +445,13 @@ export declare function terminalScriptChunkFailure<T extends {
|
|
|
418
445
|
error?: string;
|
|
419
446
|
terminal?: boolean;
|
|
420
447
|
terminalKind?: ScriptChunkResult["terminalKind"];
|
|
421
|
-
|
|
448
|
+
affectedOrdinals?: number[];
|
|
449
|
+
}>(results: T[], receivedOrdinals?: number[]): T | undefined;
|
|
422
450
|
export declare function scriptCreativeReplanOrdinals(results: unknown[]): number[];
|
|
423
451
|
/** Keep a mixed reset signal after an exact-duplicate rewrite finishes. */
|
|
424
452
|
export declare function preserveScriptRecoveryReplans(result: ScriptChunkResult, error: unknown): ScriptChunkResult;
|
|
425
453
|
export declare function creativeReplanOrdinals(error: unknown): number[];
|
|
454
|
+
export declare function scriptDeliveryOrdinals(error: unknown): number[];
|
|
426
455
|
export declare function candidateRevisionChangedOrdinals(error: unknown): number[];
|
|
427
456
|
export declare function scriptRewriteOrdinals(error: unknown): number[];
|
|
428
457
|
export declare function terminalScriptValidationError(error: unknown): boolean;
|
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.82",
|
|
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",
|