@xiaohhhh1/canvas-agent 0.4.53 → 0.4.55

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.
@@ -1,5 +1,5 @@
1
1
  type JsonSchema = Record<string, unknown>;
2
- export declare const FLOW_C_CREATIVE_CANDIDATE_CONTRACT_VERSION = "flow-c-creative-candidates-v1";
2
+ export declare const FLOW_C_CREATIVE_CANDIDATE_CONTRACT_VERSION = "flow-c-creative-candidates-v2";
3
3
  export declare function flowCCreativeCandidateOutputSchema(count: number): JsonSchema;
4
4
  export declare function parseFlowCCreativeCandidateOutput(value: string, expectedOrdinals: number[]): Record<string, unknown>[];
5
5
  export {};
@@ -1,5 +1,5 @@
1
1
  import { assertStrictResponseSchema } from './script-output.js';
2
- export const FLOW_C_CREATIVE_CANDIDATE_CONTRACT_VERSION = 'flow-c-creative-candidates-v1';
2
+ export const FLOW_C_CREATIVE_CANDIDATE_CONTRACT_VERSION = 'flow-c-creative-candidates-v2';
3
3
  const text = { type: 'string', minLength: 1 };
4
4
  function object(properties) {
5
5
  return { type: 'object', properties, required: Object.keys(properties), additionalProperties: false };
@@ -17,6 +17,13 @@ function candidateSchema() {
17
17
  truthBoundary: text,
18
18
  riskFlags: { type: 'array', minItems: 0, maxItems: 8, items: text },
19
19
  firstFrame: text,
20
+ firstFrameFocus: text,
21
+ visualContrast: text,
22
+ sensoryTexture: text,
23
+ motionPeak: text,
24
+ compositionLighting: text,
25
+ localNativeDetail: text,
26
+ antiFlatness: text,
20
27
  creativeFingerprint: object({
21
28
  characterRelation: text,
22
29
  scene: text,
@@ -20,20 +20,19 @@ type ScriptRecord = {
20
20
  updatedAt: string;
21
21
  };
22
22
  type ReferenceStyleCard = {
23
+ id?: unknown;
23
24
  visualPremisePattern?: unknown;
24
25
  mutationAxes?: unknown;
25
26
  culturePattern?: unknown;
26
27
  spectaclePattern?: unknown;
27
28
  productIntegrationPattern?: unknown;
28
- openingComposition?: unknown;
29
- visualDensity?: unknown;
30
- conflictContrast?: unknown;
31
- characterProductEntrance?: unknown;
32
- rhythm?: unknown;
33
- turn?: unknown;
34
- proofMethod?: unknown;
29
+ openingPattern?: unknown;
30
+ proofPattern?: unknown;
31
+ rhythmPattern?: unknown;
32
+ capturePattern?: unknown;
35
33
  voiceTone?: unknown;
36
- categoryFit?: unknown;
34
+ sourceEvidenceTier?: unknown;
35
+ truthBoundaries?: unknown;
37
36
  applicability?: {
38
37
  markets?: unknown;
39
38
  categories?: unknown;
@@ -60,6 +59,13 @@ type SelectedCandidate = {
60
59
  truthBoundary: string;
61
60
  riskFlags: string[];
62
61
  firstFrame: string;
62
+ firstFrameFocus: string;
63
+ visualContrast: string;
64
+ sensoryTexture: string;
65
+ motionPeak: string;
66
+ compositionLighting: string;
67
+ localNativeDetail: string;
68
+ antiFlatness: string;
63
69
  creativeFingerprint: Record<string, string>;
64
70
  fingerprintKey?: string;
65
71
  selectionScore?: number;
@@ -264,7 +270,6 @@ export declare class WorkflowManager {
264
270
  private pumpScriptQueue;
265
271
  private finishDownloadDirectorySelection;
266
272
  private runScript;
267
- private ensureCreativeCandidateSelections;
268
273
  private runCandidateChunk;
269
274
  /** 一个 chunk 使用独立线程;解析与中心持久化失败不会影响并行 lane。 */
270
275
  private runScriptChunk;
@@ -290,5 +295,17 @@ export declare function terminalScriptChunkError(results: Array<{
290
295
  }>): string;
291
296
  export declare function scriptChunkPrompt(id: string, task: ScriptTask, ordinals: number[]): string;
292
297
  export declare function creativeCandidatePrompt(id: string, task: ScriptTask, ordinals: number[]): string;
298
+ /**
299
+ * 已选创意永远优先进入脚本扩写;只有当前没有可写脚本时才生成下一小波候选。
300
+ * 每次最多占用受控 worker 数量的子批,避免把整批候选提前塞满 worker 队列。
301
+ */
302
+ export declare function nextScriptPipelineWave(task: Pick<ScriptTask, "requested_count" | "selected_candidates">, receivedOrdinals: number[], durationSeconds: 10 | 20 | 30, scriptChunkSize: number, candidateChunkSize?: number, concurrency?: number): {
303
+ stage: "script";
304
+ chunks: number[][];
305
+ } | {
306
+ stage: "candidate";
307
+ chunks: number[][];
308
+ };
309
+ export declare function immediateScriptOrdinals(task: Pick<ScriptTask, "selected_candidates">, receivedOrdinals: number[], candidateOrdinals: number[]): number[];
293
310
  export declare function missingOrdinals(total: number, received: number[]): number[];
294
311
  export {};
@@ -244,7 +244,6 @@ export class WorkflowManager {
244
244
  if (Date.parse(task.expires_at) <= Date.now())
245
245
  throw new ExpiredCapabilityError("脚本交接已过期,请在网页重新点击交给本机 Codex");
246
246
  const workspace = ensureSiteWorkspace(this.config);
247
- task = await this.ensureCreativeCandidateSelections(id, task, workspace.workspacePath);
248
247
  const durationSeconds = Number(task.duration_seconds || 10);
249
248
  const chunkSizes = flowCScriptChunkSizes(durationSeconds);
250
249
  // Fail locally before starting any worker if a future schema edit
@@ -253,17 +252,55 @@ export class WorkflowManager {
253
252
  flowCScriptOutputSchema(durationSeconds, chunkSize);
254
253
  record.activeChunks = 0;
255
254
  let chunkSizeIndex = 0;
255
+ let candidateChunkSize = 2;
256
256
  while (record.receivedOrdinals.length < task.requested_count) {
257
+ task = await this.scriptTask(id);
258
+ const selectedBefore = selectedCandidateOrdinals(task);
259
+ const wave = nextScriptPipelineWave(task, record.receivedOrdinals, durationSeconds, chunkSizes[chunkSizeIndex], candidateChunkSize);
260
+ if (wave.stage === "candidate") {
261
+ const receivedBefore = record.receivedOrdinals.length;
262
+ record.message = `本机 Codex 正在选择下一小批创意(${selectedBefore.length}/${task.requested_count});选好后立即写对应脚本`;
263
+ record.activeChunks = 0;
264
+ record.updatedAt = now();
265
+ this.save();
266
+ const pipelineResults = await Promise.all(wave.chunks.map(async (ordinals) => {
267
+ const candidateResult = await this.runCandidateChunk(id, task, ordinals, workspace.workspacePath);
268
+ if (candidateResult.error)
269
+ return [candidateResult];
270
+ const selectedTask = await this.scriptTask(id);
271
+ const scriptOrdinals = immediateScriptOrdinals(selectedTask, this.scriptRecord(id).receivedOrdinals, ordinals);
272
+ if (!scriptOrdinals.length)
273
+ return [candidateResult];
274
+ this.scriptRecord(id).attempts += 1;
275
+ const scriptResult = await this.runScriptChunk(id, selectedTask, scriptOrdinals, workspace.workspacePath);
276
+ return [candidateResult, scriptResult];
277
+ }));
278
+ const results = pipelineResults.flat();
279
+ task = await this.scriptTask(id);
280
+ const terminalError = terminalScriptChunkError(results);
281
+ if (terminalError)
282
+ throw new Error(`创意或脚本结构化契约被 Codex 拒绝,已停止自动重试(${terminalError})`);
283
+ if (record.receivedOrdinals.length > receivedBefore)
284
+ chunkSizeIndex = 0;
285
+ if (selectedCandidateOrdinals(task).length > selectedBefore.length) {
286
+ candidateChunkSize = 2;
287
+ continue;
288
+ }
289
+ if (candidateChunkSize > 1) {
290
+ candidateChunkSize = 1;
291
+ continue;
292
+ }
293
+ const lastError = results.map((result) => result.error).filter(Boolean).at(-1) || "候选生成或中心选题未返回结果";
294
+ throw new Error(`创意候选阶段已隔离到单条仍失败(${lastError}),请点击重试`);
295
+ }
257
296
  const before = record.receivedOrdinals.length;
258
- const missing = missingOrdinals(task.requested_count, record.receivedOrdinals);
259
297
  const chunkSize = chunkSizes[chunkSizeIndex];
260
- const chunks = flowCScriptChunks(durationSeconds, missing, chunkSize);
261
298
  record.chunkSize = chunkSize;
262
- record.attempts += chunks.length;
263
- record.message = `本机 Codex 正在用 ${FLOW_C_CODEX_WORKER_CONCURRENCY} 个受控 worker 写 ${chunks.length} 个独立子批(${before}/${task.requested_count})`;
299
+ record.attempts += wave.chunks.length;
300
+ record.message = `创意已选 ${selectedBefore.length}/${task.requested_count};正在立即写 ${wave.chunks.length} 个对应脚本子批(已回传 ${before}/${task.requested_count})`;
264
301
  record.updatedAt = now();
265
302
  this.save();
266
- const results = await Promise.all(chunks.map((ordinals) => this.runScriptChunk(id, task, ordinals, workspace.workspacePath)));
303
+ const results = await Promise.all(wave.chunks.map((ordinals) => this.runScriptChunk(id, task, ordinals, workspace.workspacePath)));
267
304
  task = await this.scriptTask(id);
268
305
  const terminalError = terminalScriptChunkError(results);
269
306
  if (terminalError) {
@@ -304,36 +341,6 @@ export class WorkflowManager {
304
341
  this.runningScripts.delete(id);
305
342
  }
306
343
  }
307
- async ensureCreativeCandidateSelections(id, initialTask, cwd) {
308
- let task = initialTask;
309
- let chunkSize = 2;
310
- while (selectedCandidateOrdinals(task).length < task.requested_count) {
311
- const before = selectedCandidateOrdinals(task).length;
312
- const missing = missingOrdinals(task.requested_count, selectedCandidateOrdinals(task));
313
- const chunks = chunkNumbers(missing, chunkSize);
314
- const record = this.scriptRecord(id);
315
- record.message = `本机 Codex 正在生成可审核创意候选,中心将自动选题(${before}/${task.requested_count})`;
316
- record.activeChunks = 0;
317
- record.updatedAt = now();
318
- this.save();
319
- const results = await Promise.all(chunks.map((ordinals) => this.runCandidateChunk(id, task, ordinals, cwd)));
320
- task = await this.scriptTask(id);
321
- const terminalError = terminalScriptChunkError(results);
322
- if (terminalError)
323
- throw new Error(`创意候选结构化契约被 Codex 拒绝,已停止自动重试(${terminalError})`);
324
- if (selectedCandidateOrdinals(task).length > before) {
325
- chunkSize = 2;
326
- continue;
327
- }
328
- if (chunkSize > 1) {
329
- chunkSize = 1;
330
- continue;
331
- }
332
- const lastError = results.map((result) => result.error).filter(Boolean).at(-1) || "候选生成或中心选题未返回结果";
333
- throw new Error(`创意候选阶段已隔离到单条仍失败(${lastError}),请点击重试`);
334
- }
335
- return task;
336
- }
337
344
  async runCandidateChunk(id, task, ordinals, cwd) {
338
345
  const result = await runCodexWorkflowTurn(creativeCandidatePrompt(id, task, ordinals), this.emit, {
339
346
  cwd,
@@ -583,7 +590,7 @@ export function scriptChunkPrompt(id, task, ordinals) {
583
590
  ${compactJson([...selected.values()], 16_000)}
584
591
  ${durationRules}
585
592
  写作要求:
586
- 1. visualHook、conflict、escalation/turn、productIntervention、visibleProof、callbackMotivation 与所有 shots 必须执行选中卡的 visualPremise、firstFrame、spectacleEscalation、productProofAction 和 truthBoundary;不得扩大功效或换成普通模板。
593
+ 1. visualHook、conflict、escalation/turn、productIntervention、visibleProof、callbackMotivation 与所有 shots 必须执行选中卡的 visualPremise、firstFrame、spectacleEscalation、productProofAction、truthBoundary 以及七项视觉执行字段;不得扩大功效、稀释构图或换成普通模板。
587
594
  2. 每条生成一份稳定 voiceProfile;每段只写简短 voiceCue。口播必须是目标市场 ${task.market} 的自然原生语言、偏快但清晰;导演说明统一用简洁制作英文。
588
595
  3. 每段永远是独立 0–10 秒,含 1–8 个按剧情需要决定的 shots、准确 voiceover、onScreenText、evidence、soundBgm、emotionalNote 与 endingState;模型不要输出 continuity 或 continuityMode。
589
596
  4. shot.visual 要直接写出主体位置、景别、光线、材质、动作峰值和当地自然环境,并保留真实皮肤、细发、布料/商品微纹理和实景混合光;禁止塑料蜡感、静态举产品、品牌广告片、电视购物或伪造工厂/销量/来源。
@@ -601,13 +608,14 @@ Optional learned style abstractions: ${compactJson(styleCards, 5_000)}
601
608
  Already accepted fingerprint ledger: ${compactJson(ledger, 8_000)}
602
609
  For each ordinal return exactly 6 short, auditable candidate cards. These are visible structured alternatives, not hidden reasoning. Vary the overall creative skeleton through open dimensions such as relationship/identity, scene mismatch, scale/quantity, group reaction, viewpoint, sensory texture and proof action; do not rotate a fixed template list.
603
610
  Every card must be surprising but physically filmable and product-relevant. productProofAction and truthBoundary must follow supplied facts. For food, baby, medical/efficacy and personal-hygiene products, reject unhygienic display, fear marketing, dangerous use and unsupported promises.
611
+ Every card must also contain a concrete visual execution card, not mood adjectives: firstFrameFocus names one dominant subject and its approximate 35%-85% frame share; visualContrast defines one readable scale/color/light/relationship contrast; sensoryTexture names truthful category-specific material evidence; motionPeak names the single peak physical instant; compositionLighting specifies foreground/midground/background, practical light direction and one plausible exposure/optical imperfection; localNativeDetail names observable local behavior/environment/aesthetic density with the same evidence discipline as culturalAnchors; antiFlatness names the exact generic catalogue treatment this concept must avoid. The firstFrame must pass a screenshot test: even without dialogue, a still image must show the unusual relationship, product relevance and depth hierarchy.
604
612
  Cultural anchors are optional (0–3). source must be learned-style-card, product-market-evidence, or conservative-everyday-detail. A learned-style-card anchor must cite the supplied card id in evidence. A product-market-evidence anchor must cite a concrete supplied product/market fact; if none exists, use conservative-everyday-detail and explicitly say it is ordinary context, not a custom claim. Never use flags, national costumes, royalty/public figures, religion, stereotypes or cultural mockery as shortcut hooks. Locality belongs in consumption context, natural interaction, language rhythm and aesthetic density.
605
613
  creativeFingerprint must use five concise semantic labels: characterRelation, scene, firstFrameComposition, spectacleMechanism and proofMethod. Do not hide duplicate ideas behind paraphrases. Do not copy people, clothing, sets, dialogue or shot sequences from references. Return only the candidate schema; do not write scripts or call tools.`;
606
614
  }
607
615
  function compactReferenceStyleCard(value) {
608
616
  if (!value || typeof value !== "object")
609
617
  return "";
610
- const fields = ["visualPremisePattern", "mutationAxes", "culturePattern", "spectaclePattern", "productIntegrationPattern", "openingComposition", "visualDensity", "conflictContrast", "characterProductEntrance", "rhythm", "turn", "proofMethod", "voiceTone", "categoryFit"];
618
+ const fields = ["id", "visualPremisePattern", "mutationAxes", "culturePattern", "spectaclePattern", "productIntegrationPattern", "openingPattern", "proofPattern", "rhythmPattern", "capturePattern", "voiceTone", "sourceEvidenceTier"];
611
619
  const compact = Object.fromEntries(fields.flatMap((field) => {
612
620
  const text = String(value[field] || "").trim().replace(/\s+/g, " ").slice(0, 400);
613
621
  return text ? [[field, text]] : [];
@@ -616,7 +624,7 @@ function compactReferenceStyleCard(value) {
616
624
  const markets = Array.isArray(value.applicability?.markets) ? value.applicability.markets.map((item) => String(item || "").trim()).filter(Boolean).slice(0, 8) : [];
617
625
  if (categories.length || markets.length)
618
626
  Object.assign(compact, { applicability: { categories, markets } });
619
- return Object.keys(compact).length ? JSON.stringify(compact) : "";
627
+ return Object.keys(compact).length ? compact : null;
620
628
  }
621
629
  function relevantStyleCards(cards, products) {
622
630
  const categories = new Set(products.map((product) => String(product.category || "").trim().toLowerCase()).filter(Boolean));
@@ -654,11 +662,19 @@ function selectedCandidatePlan(value) {
654
662
  truthBoundary: value.truthBoundary,
655
663
  riskFlags: value.riskFlags,
656
664
  firstFrame: value.firstFrame,
665
+ firstFrameFocus: value.firstFrameFocus,
666
+ visualContrast: value.visualContrast,
667
+ sensoryTexture: value.sensoryTexture,
668
+ motionPeak: value.motionPeak,
669
+ compositionLighting: value.compositionLighting,
670
+ localNativeDetail: value.localNativeDetail,
671
+ antiFlatness: value.antiFlatness,
657
672
  creativeFingerprint: value.creativeFingerprint,
658
673
  fingerprintKey: value.fingerprintKey,
659
674
  selectionScore: value.selectionScore,
660
675
  selectionBreakdown: value.selectionBreakdown,
661
676
  selectionMode: value.selectionMode,
677
+ visualExecutionVersion: "flow-c-visual-execution-v1",
662
678
  };
663
679
  }
664
680
  function chunkNumbers(values, size) {
@@ -667,6 +683,25 @@ function chunkNumbers(values, size) {
667
683
  chunks.push(values.slice(index, index + size));
668
684
  return chunks;
669
685
  }
686
+ /**
687
+ * 已选创意永远优先进入脚本扩写;只有当前没有可写脚本时才生成下一小波候选。
688
+ * 每次最多占用受控 worker 数量的子批,避免把整批候选提前塞满 worker 队列。
689
+ */
690
+ export function nextScriptPipelineWave(task, receivedOrdinals, durationSeconds, scriptChunkSize, candidateChunkSize = 2, concurrency = FLOW_C_CODEX_WORKER_CONCURRENCY) {
691
+ const selected = selectedCandidateOrdinals(task);
692
+ const selectedSet = new Set(selected);
693
+ const scriptReady = missingOrdinals(task.requested_count, receivedOrdinals).filter((ordinal) => selectedSet.has(ordinal));
694
+ if (scriptReady.length) {
695
+ return { stage: "script", chunks: flowCScriptChunks(durationSeconds, scriptReady, scriptChunkSize).slice(0, concurrency) };
696
+ }
697
+ const candidateMissing = missingOrdinals(task.requested_count, selected);
698
+ return { stage: "candidate", chunks: chunkNumbers(candidateMissing, candidateChunkSize).slice(0, concurrency) };
699
+ }
700
+ export function immediateScriptOrdinals(task, receivedOrdinals, candidateOrdinals) {
701
+ const selected = new Set(selectedCandidateOrdinals(task));
702
+ const received = new Set(receivedOrdinals);
703
+ return candidateOrdinals.filter((ordinal) => selected.has(ordinal) && !received.has(ordinal));
704
+ }
670
705
  function compactJson(value, limit) {
671
706
  return JSON.stringify(value).slice(0, limit);
672
707
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaohhhh1/canvas-agent",
3
- "version": "0.4.53",
3
+ "version": "0.4.55",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",