@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
|
@@ -4,6 +4,8 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { chromium } from "playwright-core";
|
|
6
6
|
import { CONFIG_DIR } from "../config.js";
|
|
7
|
+
import { FastMossSelectionSessions } from "./fastmoss-selection-session.js";
|
|
8
|
+
import { VisibleFastMossCollector, readVisibleSection } from "./fastmoss-visible-evidence.js";
|
|
7
9
|
const FASTMOSS_HOME = "https://www.fastmoss.com/";
|
|
8
10
|
const MEMBERSHIP_RECHECK_MS = 5 * 60_000;
|
|
9
11
|
const MANUAL_VERIFICATION_TIMEOUT_MS = 10 * 60_000;
|
|
@@ -37,6 +39,11 @@ export class FastMossIntegration {
|
|
|
37
39
|
profileDir = path.join(CONFIG_DIR, "fastmoss-session");
|
|
38
40
|
authenticatedMarker = path.join(this.profileDir, "authenticated.json");
|
|
39
41
|
dataDir = path.join(CONFIG_DIR, "fastmoss-selection");
|
|
42
|
+
selectionPage = null;
|
|
43
|
+
selection = new FastMossSelectionSessions(path.join(this.dataDir, "sessions-v2"), {
|
|
44
|
+
discover: async (ranking, config, save) => new VisibleFastMossCollector(await this.selectionBrowser()).discover(ranking, config, save),
|
|
45
|
+
detail: async (product, config, save) => new VisibleFastMossCollector(await this.selectionBrowser()).detail(product, config, save),
|
|
46
|
+
});
|
|
40
47
|
savedAuthenticated = existsSync(this.authenticatedMarker);
|
|
41
48
|
observedVideoRecords = [];
|
|
42
49
|
videoResponseTasks = new Set();
|
|
@@ -48,6 +55,40 @@ export class FastMossIntegration {
|
|
|
48
55
|
this.authenticated = true;
|
|
49
56
|
this.message = "FastMoss 登录已保存在本机;窗口可以关闭,抓取时会自动恢复";
|
|
50
57
|
}
|
|
58
|
+
async startSelection(id, config, resume = false) {
|
|
59
|
+
return this.selection.start(id, config, resume);
|
|
60
|
+
}
|
|
61
|
+
async selectionBrowser() {
|
|
62
|
+
if (!this.context)
|
|
63
|
+
await this.start();
|
|
64
|
+
if (!this.context)
|
|
65
|
+
throw new Error("FastMoss 本机浏览器未启动");
|
|
66
|
+
if (!this.selectionPage || this.selectionPage.isClosed())
|
|
67
|
+
this.selectionPage = await this.context.newPage();
|
|
68
|
+
const page = this.selectionPage;
|
|
69
|
+
return {
|
|
70
|
+
goto: async (url) => {
|
|
71
|
+
const parsed = new URL(url);
|
|
72
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "www.fastmoss.com")
|
|
73
|
+
throw new Error("商品证据地址不是 FastMoss 页面");
|
|
74
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45_000 });
|
|
75
|
+
},
|
|
76
|
+
snapshot: selector => page.evaluate(readVisibleSection, selector),
|
|
77
|
+
clickText: async (selector, text, occurrence = 0) => {
|
|
78
|
+
const scope = page.locator(selector);
|
|
79
|
+
const matches = selector.endsWith(" label") ? scope.filter({ hasText: new RegExp(`^${text}$`) }) : scope.getByText(text, { exact: true });
|
|
80
|
+
await matches.nth(occurrence).click({ timeout: 15_000 });
|
|
81
|
+
},
|
|
82
|
+
next: async (selector) => {
|
|
83
|
+
const next = page.locator(`${selector} .ant-pagination-next`).first();
|
|
84
|
+
if (!(await next.count()) || await next.getAttribute("aria-disabled") === "true" || (await next.getAttribute("class"))?.includes("ant-pagination-disabled"))
|
|
85
|
+
return false;
|
|
86
|
+
await next.click({ timeout: 10_000 });
|
|
87
|
+
return true;
|
|
88
|
+
},
|
|
89
|
+
pause: () => page.waitForTimeout(600),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
51
92
|
status() {
|
|
52
93
|
return {
|
|
53
94
|
phase: this.phase,
|
|
@@ -172,6 +213,8 @@ export class FastMossIntegration {
|
|
|
172
213
|
return this.status();
|
|
173
214
|
}
|
|
174
215
|
async capture(input = {}) {
|
|
216
|
+
if (this.selection.isRunning())
|
|
217
|
+
throw new Error("新版选品正在采集,请等待完成");
|
|
175
218
|
if (!this.context || !this.page || this.page.isClosed())
|
|
176
219
|
await this.start();
|
|
177
220
|
await this.inspect();
|
|
@@ -280,6 +323,8 @@ export class FastMossIntegration {
|
|
|
280
323
|
* private FastMoss API requests and pauses for the operator when a CAPTCHA appears.
|
|
281
324
|
*/
|
|
282
325
|
async captureLearningVideos(input = {}) {
|
|
326
|
+
if (this.selection.isRunning())
|
|
327
|
+
throw new Error("选品正在使用 FastMoss,请等待完成后再采集素材");
|
|
283
328
|
if (!this.context || !this.page || this.page.isClosed())
|
|
284
329
|
await this.start();
|
|
285
330
|
await this.inspect();
|
|
@@ -385,6 +430,8 @@ export class FastMossIntegration {
|
|
|
385
430
|
return { ...this.status(), sources, rejected, requestedTarget, complete: sources.length >= requestedTarget };
|
|
386
431
|
}
|
|
387
432
|
async close() {
|
|
433
|
+
if (this.selection.isRunning())
|
|
434
|
+
throw new Error("选品正在保存数据,请等待完成后关闭浏览器");
|
|
388
435
|
const context = this.context;
|
|
389
436
|
this.context = null;
|
|
390
437
|
this.page = null;
|
|
@@ -500,6 +547,8 @@ export class FastMossIntegration {
|
|
|
500
547
|
}
|
|
501
548
|
}
|
|
502
549
|
async switchAccount() {
|
|
550
|
+
if (this.selection.isRunning())
|
|
551
|
+
throw new Error("选品尚未完成,请等待完成后更换账号");
|
|
503
552
|
if (!this.context || !this.page || this.page.isClosed())
|
|
504
553
|
await this.start();
|
|
505
554
|
this.useLatestPage();
|
package/dist/server/http.js
CHANGED
|
@@ -133,7 +133,10 @@ export function startHttpServer() {
|
|
|
133
133
|
await openExternalUrl(url.toString());
|
|
134
134
|
res.json({ ok: true });
|
|
135
135
|
}));
|
|
136
|
-
app.get("/agent/integrations/fastmoss/status", route(async (_req, res) => res.json({ ok: true, ...await fastmoss.inspect() })));
|
|
136
|
+
app.get("/agent/integrations/fastmoss/status", route(async (_req, res) => res.json({ ok: true, ...(fastmoss.selection.isRunning() ? fastmoss.status() : await fastmoss.inspect()) })));
|
|
137
|
+
app.get("/agent/integrations/fastmoss/selection", route(async (_req, res) => res.json({ ok: true, session: await fastmoss.selection.get() })));
|
|
138
|
+
app.get("/agent/integrations/fastmoss/selection/:sessionId", route(async (req, res) => res.json({ ok: true, session: await fastmoss.selection.get(routeParam(req.params.sessionId)) })));
|
|
139
|
+
app.post("/agent/integrations/fastmoss/selection", route(async (req, res) => res.json({ ok: true, session: await fastmoss.startSelection(String(req.body?.sessionId || ""), req.body?.config || {}, req.body?.resume === true) })));
|
|
137
140
|
app.get("/agent/integrations/fastmoss/observations", route(async (_req, res) => res.json({ ok: true, ...await fastmoss.observations() })));
|
|
138
141
|
app.post("/agent/integrations/fastmoss/start", route(async (_req, res) => res.json({ ok: true, ...await fastmoss.start() })));
|
|
139
142
|
app.post("/agent/integrations/fastmoss/refresh", route(async (_req, res) => res.json({ ok: true, ...await fastmoss.inspect() })));
|
|
@@ -100,4 +100,9 @@ export declare function assertVideoMediaProbe(parsed: VideoProbeResult): {
|
|
|
100
100
|
};
|
|
101
101
|
export declare function transcribeLocalMedia(videoFile: string | undefined, workDir: string): Promise<string>;
|
|
102
102
|
export declare function resolveLocalCodexEntrypoint(): string;
|
|
103
|
+
/** CLI stderr can contain the prompt and unrelated MCP logs; never return it to the website. */
|
|
104
|
+
export declare function localCodexAnalysisError(result: {
|
|
105
|
+
stdout: string;
|
|
106
|
+
error: string;
|
|
107
|
+
}): "本机 Codex 视频理解失败:Agent 自带的 Codex 版本过旧,当前模型需要更新运行程序。原视频已保存,更新后可直接重试反推,无需重新上传。" | "本机 Codex 视频理解失败:当前账号的模型额度或请求频率受限,请待额度恢复后重试。原视频已保存,无需重新上传。" | "本机 Codex 视频理解失败:本机 Codex 登录已失效,请恢复登录后重试。原视频已保存,无需重新上传。" | "本机 Codex 视频理解超时,原视频已保存,可直接重试反推,无需重新上传。" | "本机 Codex 未完成视频理解,原视频已保存,可直接重试反推。若持续失败,请检查本机 Codex 运行状态。";
|
|
103
108
|
export {};
|
|
@@ -491,13 +491,13 @@ async function runLocalCodexAnalysis(prompt, attachments, workDir) {
|
|
|
491
491
|
// local development may keep them at a different ancestor. Resolve from this
|
|
492
492
|
// module instead of assuming a nested node_modules directory.
|
|
493
493
|
const codexEntrypoint = resolveLocalCodexEntrypoint();
|
|
494
|
-
const args = [codexEntrypoint, "exec", "--ephemeral", "--skip-git-repo-check", "--sandbox", "read-only", "--color", "never", "--output-last-message", outputFile];
|
|
494
|
+
const args = [codexEntrypoint, "exec", "--json", "--ephemeral", "--skip-git-repo-check", "--sandbox", "read-only", "--color", "never", "--output-last-message", outputFile];
|
|
495
495
|
for (const attachment of attachments)
|
|
496
496
|
args.push("--image", path.join(workDir, String(attachment.name)));
|
|
497
497
|
args.push("-");
|
|
498
498
|
const result = await runProcess(process.execPath, args, { cwd: workDir, timeoutMs: ANALYSIS_TIMEOUT_MS, stdin: prompt });
|
|
499
499
|
if (!result.ok)
|
|
500
|
-
throw new Error(
|
|
500
|
+
throw new Error(localCodexAnalysisError(result));
|
|
501
501
|
const raw = (await readFile(outputFile, "utf8")).trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "");
|
|
502
502
|
try {
|
|
503
503
|
return JSON.parse(raw);
|
|
@@ -509,6 +509,34 @@ async function runLocalCodexAnalysis(prompt, attachments, workDir) {
|
|
|
509
509
|
export function resolveLocalCodexEntrypoint() {
|
|
510
510
|
return createRequire(import.meta.url).resolve("@openai/codex/bin/codex.js");
|
|
511
511
|
}
|
|
512
|
+
/** CLI stderr can contain the prompt and unrelated MCP logs; never return it to the website. */
|
|
513
|
+
export function localCodexAnalysisError(result) {
|
|
514
|
+
const events = result.stdout.split(/\r?\n/).flatMap((line) => {
|
|
515
|
+
try {
|
|
516
|
+
const event = JSON.parse(line);
|
|
517
|
+
if (["error", "turn.failed"].includes(String(event.type)))
|
|
518
|
+
return [String(event.error?.message || event.message || "")];
|
|
519
|
+
if (event.item?.type === "error")
|
|
520
|
+
return [String(event.item.message || "")];
|
|
521
|
+
}
|
|
522
|
+
catch { /* Non-event output is not a user-facing diagnostic. */ }
|
|
523
|
+
return [];
|
|
524
|
+
});
|
|
525
|
+
const terminal = events.at(-1) || result.error;
|
|
526
|
+
if (/requires a newer version of Codex|please upgrade to the latest app or CLI/i.test(terminal)) {
|
|
527
|
+
return "本机 Codex 视频理解失败:Agent 自带的 Codex 版本过旧,当前模型需要更新运行程序。原视频已保存,更新后可直接重试反推,无需重新上传。";
|
|
528
|
+
}
|
|
529
|
+
if (/usage limit|rate.?limit|quota exceeded|too many requests/i.test(terminal)) {
|
|
530
|
+
return "本机 Codex 视频理解失败:当前账号的模型额度或请求频率受限,请待额度恢复后重试。原视频已保存,无需重新上传。";
|
|
531
|
+
}
|
|
532
|
+
if (/unauthori[sz]ed|authentication|not logged in|please (?:log|sign) in/i.test(terminal)) {
|
|
533
|
+
return "本机 Codex 视频理解失败:本机 Codex 登录已失效,请恢复登录后重试。原视频已保存,无需重新上传。";
|
|
534
|
+
}
|
|
535
|
+
if (/处理超过 \d+ 秒|timed? ?out|timeout/i.test(terminal)) {
|
|
536
|
+
return "本机 Codex 视频理解超时,原视频已保存,可直接重试反推,无需重新上传。";
|
|
537
|
+
}
|
|
538
|
+
return "本机 Codex 未完成视频理解,原视频已保存,可直接重试反推。若持续失败,请检查本机 Codex 运行状态。";
|
|
539
|
+
}
|
|
512
540
|
async function runProcess(command, args, options) {
|
|
513
541
|
return await new Promise((resolve) => {
|
|
514
542
|
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;
|