@aicopycoders/exodus 2026.9.2000-beta.0 → 2026.9.2000-beta.10

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.
@@ -9,8 +9,8 @@
9
9
  // The audio track comes first. A narrated scene (the storyboard gives it
10
10
  // narration and no on-camera line) plays its voice track and NOTHING the clip
11
11
  // recorded: the voice replaces the clip's audio, and the picture is fitted to
12
- // the voice, trimmed when the clip runs longer, sped up to at most 1.26x and
13
- // then held on its last frame when the voice runs longer. A dialogue scene
12
+ // the voice, trimmed when the clip runs longer and held on its last frame when
13
+ // the voice runs longer. The voice itself is never altered. A dialogue scene
14
14
  // keeps the audio it performed. A scene with no clip but a keyframe becomes a
15
15
  // still for the length of its voice track, else the storyboard's planned
16
16
  // duration. Every segment is loudness-normalized to -16 LUFS before the join.
@@ -35,8 +35,6 @@ const stereo = "aresample=48000,aformat=channel_layouts=stereo";
35
35
  const LOUDNORM = "loudnorm=I=-16:TP=-1.5:LRA=11,aresample=48000";
36
36
  /** Air after the last spoken word of a narrated scene, so the line lands. */
37
37
  export const TAIL_AIR_SEC = 0.8;
38
- /** The most a voice track may be sped up to fit its clip. */
39
- export const ATEMPO_CAP = 1.26;
40
38
  /** A continuous low bed under the whole ad, so the joins between clips do not read as dead air. */
41
39
  const ROOM_TONE =
42
40
  "anoisesrc=color=brown:amplitude=0.0035:r=48000,highpass=f=60,lowpass=f=900,aformat=channel_layouts=stereo";
@@ -91,27 +89,18 @@ export function scenePlans(storyboard) {
91
89
  }
92
90
 
93
91
  /**
94
- * Fit a picture to its voice track. The segment runs as long as the voice plus
95
- * a beat of air. A clip that runs longer is trimmed; a voice that runs longer
96
- * is sped up to at most ATEMPO_CAP and the clip's last frame is held for
97
- * whatever is still left. A still (no clip) simply runs the voice's length.
92
+ * Fit a picture to its voice track. The segment always runs as long as the
93
+ * voice plus a beat of air, because the voice is never altered. A clip that
94
+ * runs longer is trimmed; a clip that runs shorter holds its last frame for the
95
+ * difference. A still (no clip) simply runs the voice's length.
98
96
  */
99
97
  export function fitNarration({ clipSeconds, voiceSeconds }) {
100
98
  const target = voiceSeconds + TAIL_AIR_SEC;
101
- if (clipSeconds === null) return { seconds: target, atempo: 1, holdSec: 0, trimmedBy: 0 };
99
+ if (clipSeconds === null) return { seconds: target, holdSec: 0, trimmedBy: 0 };
102
100
  if (target <= clipSeconds) {
103
- return { seconds: target, atempo: 1, holdSec: 0, trimmedBy: clipSeconds - target };
101
+ return { seconds: target, holdSec: 0, trimmedBy: clipSeconds - target };
104
102
  }
105
- const room = Math.max(clipSeconds - TAIL_AIR_SEC, 0.1);
106
- const atempo = Math.min(ATEMPO_CAP, voiceSeconds / room);
107
- const fitted = voiceSeconds / atempo + TAIL_AIR_SEC;
108
- if (fitted <= clipSeconds) return { seconds: fitted, atempo, holdSec: 0, trimmedBy: 0 };
109
- return { seconds: fitted, atempo, holdSec: fitted - clipSeconds, trimmedBy: 0 };
110
- }
111
-
112
- function scaleWords(words, atempo) {
113
- if (!words || atempo === 1) return words;
114
- return words.map((w) => ({ ...w, s: w.s / atempo, e: w.e / atempo }));
103
+ return { seconds: target, holdSec: target - clipSeconds, trimmedBy: 0 };
115
104
  }
116
105
 
117
106
  export function cueOffsetOnSpine(spine, counts, cueLineId) {
@@ -186,27 +175,26 @@ export function buildTimeline({ manifest, storyboard, skip, probe, exists, readW
186
175
  const fit = fitNarration({ clipSeconds: clip.seconds, voiceSeconds: probe(voice).seconds });
187
176
  const how = [];
188
177
  if (fit.trimmedBy > 0.05) how.push(`clip trimmed ${clip.seconds.toFixed(1)}s -> ${fit.seconds.toFixed(1)}s`);
189
- if (fit.atempo > 1.001) how.push(`voice sped up ${fit.atempo.toFixed(2)}x`);
190
178
  if (fit.holdSec > 0.05) how.push(`last frame held ${fit.holdSec.toFixed(1)}s`);
191
179
  spine.push({
192
- ...base, audio: "narration", voice, seconds: fit.seconds, atempo: fit.atempo,
193
- holdSec: fit.holdSec, words: scaleWords(readWords(scene.words), fit.atempo),
180
+ ...base, audio: "narration", voice, seconds: fit.seconds,
181
+ holdSec: fit.holdSec, words: readWords(scene.words),
194
182
  note: `${flag}, ${voice} replaces the clip's audio${how.length ? ` (${how.join(", ")})` : ""}`,
195
183
  });
196
184
  } else if (narrated) {
197
185
  spine.push({
198
- ...base, audio: "silence", voice: null, seconds: clip.seconds, atempo: 1, holdSec: 0,
186
+ ...base, audio: "silence", voice: null, seconds: clip.seconds, holdSec: 0,
199
187
  words: null,
200
188
  note: `${flag}, narrated scene without its voice track: the clip's own audio is muted`,
201
189
  });
202
190
  } else if (clip.hasAudio) {
203
191
  spine.push({
204
- ...base, audio: "clip", voice: null, seconds: clip.seconds, atempo: 1, holdSec: 0,
192
+ ...base, audio: "clip", voice: null, seconds: clip.seconds, holdSec: 0,
205
193
  words: readWords(scene.words), note: `${flag}, its own dialogue`,
206
194
  });
207
195
  } else {
208
196
  spine.push({
209
- ...base, audio: "silence", voice: null, seconds: clip.seconds, atempo: 1, holdSec: 0,
197
+ ...base, audio: "silence", voice: null, seconds: clip.seconds, holdSec: 0,
210
198
  words: null, note: `${flag}, silent clip`,
211
199
  });
212
200
  }
@@ -216,10 +204,10 @@ export function buildTimeline({ manifest, storyboard, skip, probe, exists, readW
216
204
  if (exists(scene.keyframe)) {
217
205
  const fit = voice
218
206
  ? fitNarration({ clipSeconds: null, voiceSeconds: probe(voice).seconds })
219
- : { seconds: plan.durationSec ?? 4, atempo: 1, holdSec: 0 };
207
+ : { seconds: plan.durationSec ?? 4, holdSec: 0 };
220
208
  spine.push({
221
209
  sceneIndex: n, source: "still", file: scene.keyframe, clipSeconds: null,
222
- seconds: fit.seconds, audio: voice ? "narration" : "silence", voice, atempo: 1, holdSec: 0,
210
+ seconds: fit.seconds, audio: voice ? "narration" : "silence", voice, holdSec: 0,
223
211
  startSec, lineIds: plan.lineIds,
224
212
  // #1689: on a narrated scene the words are the voice track's, and the
225
213
  // voice starts where the segment does, so they need no offset here.
@@ -288,8 +276,7 @@ export function buildFfmpegArgs(timeline, { resolve, music, out }) {
288
276
  filters.push(`${norm(`${i}:v`)}${fit.length ? `,${fit.join(",")}` : ""}[v${i}]`);
289
277
  if (seg.audio === "narration") {
290
278
  const v = addInput("-i", resolve(seg.voice));
291
- const tempo = seg.atempo > 1.001 ? `atempo=${seg.atempo.toFixed(4)},` : "";
292
- filters.push(`[${v}:a]${stereo},${tempo}${LOUDNORM},apad,atrim=0:${sec(seg.seconds)}[a${i}]`);
279
+ filters.push(`[${v}:a]${stereo},${LOUDNORM},apad,atrim=0:${sec(seg.seconds)}[a${i}]`);
293
280
  } else if (seg.audio === "clip") {
294
281
  filters.push(`[${i}:a]${stereo},${LOUDNORM},apad,atrim=0:${sec(seg.seconds)}[a${i}]`);
295
282
  } else {
@@ -161,7 +161,7 @@ Accepts a workflow id **or** a name. It reports three things:
161
161
 
162
162
  Fill every required input. For a free-text brief you can compose a **bespoke brief from in-session research** — the winning angle you just worked out, the swipe you analyzed — and pass it inline (`--input brief="..."`) or, if it's long, write it to a project file and pass it with `@file` (see Chain, below). For URL/swipe sources, pass the value the source expects.
163
163
 
164
- **A brief wired straight into a Storyboard is a script, not a prompt.** On a video workflow whose Storyboard takes its `script` port from a Brief node, the text you pass for that brief boards VERBATIM, the same way a Show ad's pasted script does: one speaker per scene, every line rendered once in order, long lines split across clips, narrated lines rendered silent under the voice track. So it must be in the script form the `exodus-video` skill documents (an optional `CAST:` block of `NAME — description` lines, then `NAME:` turns, then a `CTA:` block; `NARRATOR:` lines only for Narrated Story and Explainer). A brief that is not a script, or one that does not fit the wired rig's family, is refused at launch with the parser's own message and nothing is spent. Write the script to a file and pass it with `--input script=@script.txt`. A Storyboard fed by a bot is unaffected.
164
+ **A brief wired straight into a Storyboard is a script, not a prompt.** On a video workflow whose Storyboard takes its `script` port from a Brief node, the text you pass for that brief boards VERBATIM, the same way a Show ad's pasted script does: one speaker per scene, every line rendered once in order, long lines split across clips, narrated lines rendered silent under the voice track. So it must be in the script form the `exodus-video` skill documents (an optional `DIRECTION:` block with no colon in its prose, then an optional `CAST:` block of `NAME — description` lines, then `NAME:` turns, then a `CTA:` block; `NARRATOR:` lines only for Narrated Story and Explainer). A brief that is not a script, or one that does not fit the wired rig's family, is refused at launch with the parser's own message and nothing is spent. Write the script to a file and pass it with `--input script=@script.txt`. A Storyboard fed by a bot is unaffected.
165
165
 
166
166
  **A saved rig can bring a rulebook with it.** Some saved rigs carry saved format rules: how many speakers the script may have, which checks run on the clips, how the cast is built. Point a Rig box at one of those rigs and the run follows its rules — an ensemble rig seats more speakers than the usual three, a talking-head rig seats one. The start receipt says which rulebook ran:
167
167
 
@@ -1,6 +1,5 @@
1
1
  import { readFileSync, existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { createInterface } from "node:readline/promises";
4
3
  import { apiGet, apiPost, getDashboardUrl } from "../lib/client.js";
5
4
  import { pollUntilDone } from "../lib/poll.js";
6
5
  import { displayRunStatus, formatGenesisRun, formatError, tickerRunStatus } from "../lib/format.js";
@@ -278,52 +277,13 @@ export async function run(flags) {
278
277
  return submitGenesis(action.body, { noWait, cc });
279
278
  }
280
279
  }
281
- async function listBank(limit) {
282
- const res = await apiGet(`/api/v2/scout/bank?limit=${limit}`);
283
- if (!res.ok) {
284
- console.log(formatError(res));
285
- process.exit(1);
286
- }
287
- const ideas = Array.isArray(res.data.ideas) ? res.data.ideas : [];
288
- if (ideas.length === 0) {
289
- console.log("Bank is empty.");
290
- console.log('Fill it with: exodus genesis scrape (or paste a reel: exodus genesis --reel "<url>")');
291
- return;
292
- }
293
- console.log(`## Idea Bank (${ideas.length} reel${ideas.length === 1 ? "" : "s"})`);
294
- console.log("");
295
- for (const i of ideas) {
296
- const hook = i.hook ?? "(no hook)";
297
- const user = typeof i.sourceUsername === "string" && i.sourceUsername.length > 0
298
- ? ` @${i.sourceUsername.replace(/^@+/, "")}`
299
- : "";
300
- const score = typeof i.relevanceScore === "number" ? ` score=${i.relevanceScore.toFixed(2)}` : "";
301
- const used = typeof i.useCount === "number" && i.useCount > 0 ? " · used" : "";
302
- console.log(` • ${hook}${user}${score}`);
303
- console.log(` id: ${i._id ?? "?"} [${i.status ?? "new"}${used}]`);
304
- if (i.sourceUrl)
305
- console.log(` ${i.sourceUrl}`);
306
- }
307
- console.log("");
308
- console.log("Write from one: exodus genesis --from-bank --idea <id>");
280
+ async function listBank(_limit) {
281
+ console.error(`This command has been retired. View the idea bank in the dashboard: ${getDashboardUrl()}/idea-bank`);
282
+ process.exit(1);
309
283
  }
310
- async function runFromBank(ideaId, opts, rt) {
311
- const res = await apiPost("/api/v2/scout/bank/ideas", { ideaIds: [ideaId] }, { ccCommand: rt.cc });
312
- if (!res.ok) {
313
- console.log(formatError(res));
314
- process.exit(1);
315
- }
316
- const idea = (res.data.ideas ?? [])[0];
317
- if (!idea) {
318
- console.error(`No banked idea found for id "${ideaId}". List the bank: exodus genesis --from-bank`);
319
- process.exit(1);
320
- }
321
- if (!idea.sourceUrl) {
322
- console.error(`Banked idea "${ideaId}" has no source reel URL to write from.`);
323
- process.exit(1);
324
- }
325
- console.log(`Writing from banked reel: ${idea.sourceUrl}`);
326
- return runReel([idea.sourceUrl], opts, rt);
284
+ async function runFromBank(_ideaId, _opts, _rt) {
285
+ console.error(`This command has been retired. Write from banked ideas in the dashboard: ${getDashboardUrl()}/idea-bank`);
286
+ process.exit(1);
327
287
  }
328
288
  async function runReel(urls, opts, rt) {
329
289
  console.log(`Transcribing ${urls.length === 1 ? "the reel" : `${urls.length} reels`} → idea → Genesis…`);
@@ -613,92 +573,11 @@ function requireActiveBrand() {
613
573
  }
614
574
  return slug;
615
575
  }
616
- async function runConnectInstagram(flags, cc) {
617
- const client = requireActiveBrand();
618
- const start = await apiPost("/api/v2/genesis/instagram/connect/start", { client }, { ccCommand: cc });
619
- if (!start.ok || !start.data.liveViewUrl) {
620
- console.log(formatError(start));
621
- process.exit(1);
622
- }
623
- console.log(`\nConnecting an Instagram account to brand "${client}".`);
624
- console.log("\n1. Open this link in your browser:\n");
625
- console.log(` ${start.data.liveViewUrl}\n`);
626
- console.log('2. Go to instagram.com and log into the account for THIS brand.');
627
- console.log("3. Scroll your feed for a bit so the algorithm learns the niche.");
628
- console.log("4. Come back here and press Enter.\n");
629
- const rl = createInterface({ input: process.stdin, output: process.stdout });
630
- try {
631
- await rl.question("Press Enter once you're logged in and done grooming... ");
632
- let username = typeof flags["username"] === "string" ? flags["username"].trim() : "";
633
- while (!username) {
634
- username = (await rl.question("Instagram handle you just logged in as (e.g. mybrand): ")).trim();
635
- }
636
- const finish = await apiPost("/api/v2/genesis/instagram/connect/finish", { client, igUsername: username }, { ccCommand: cc });
637
- if (!finish.ok) {
638
- console.log(formatError(finish));
639
- process.exit(1);
640
- }
641
- console.log(`\n✓ Instagram @${username.replace(/^@+/, "")} connected to "${client}".`);
642
- console.log("Fill the idea bank now: exodus genesis scrape --organic");
643
- }
644
- finally {
645
- rl.close();
646
- }
576
+ async function runConnectInstagram(_flags, _cc) {
577
+ console.error(`This command has been retired. Connect Instagram accounts in the dashboard: ${getDashboardUrl()}/settings`);
578
+ process.exit(1);
647
579
  }
648
- async function runScrape(flags, cc) {
649
- const client = requireActiveBrand();
650
- const noWait = flags["wait"] === false || flags["no-wait"] === true;
651
- if (flags["organic"] === true) {
652
- const smoke = flags["smoke"] === true;
653
- const res = await apiPost("/api/v2/scout/organic", { client, ...(smoke ? { smoke: true } : {}) }, { ccCommand: cc });
654
- if (!res.ok || !res.data.runId) {
655
- console.log(formatError(res));
656
- process.exit(1);
657
- }
658
- console.log(`Organic scrape started for "${client}": ${res.data.runId}${smoke ? " (smoke)" : ""}`);
659
- return pollScrape(res.data.runId, noWait, 1_800_000);
660
- }
661
- const term = typeof flags["term"] === "string" ? flags["term"] : undefined;
662
- const body = { sourceMode: "fresh", client };
663
- if (term)
664
- body.term = term;
665
- const res = await apiPost("/api/v2/scout", body, { ccCommand: cc });
666
- if (!res.ok || !res.data.runId) {
667
- console.log(formatError(res));
668
- process.exit(1);
669
- }
670
- console.log(`Discovery scrape started for "${client}": ${res.data.runId}`);
671
- return pollScrape(res.data.runId, noWait, 1_200_000);
672
- }
673
- async function pollScrape(runId, noWait, timeoutMs) {
674
- if (noWait) {
675
- console.log(`Run ID: ${runId}`);
676
- console.log(`Check status: exodus status --type scout --id ${runId}`);
677
- return;
678
- }
679
- console.log("Polling for completion...");
680
- const result = await pollUntilDone({
681
- path: `/api/v2/scout?runId=${runId}`,
682
- intervalMs: 15_000,
683
- timeoutMs,
684
- onProgress: (data) => {
685
- const status = data["status"];
686
- const captured = data["organicCaptured"];
687
- const qualified = data["organicQualified"];
688
- const counts = captured !== undefined || qualified !== undefined
689
- ? ` captured=${captured ?? 0} qualified=${qualified ?? 0}`
690
- : "";
691
- if (status)
692
- process.stdout.write(`\r status: ${tickerRunStatus(status)}${counts} `);
693
- },
694
- });
695
- console.log();
696
- if (result.timedOut) {
697
- console.log(`Timed out waiting. Check later: exodus status --type scout --id ${runId}`);
698
- }
699
- else {
700
- console.log(`Done. Banked reels are ready to write: exodus genesis --from-bank`);
701
- }
702
- if (!result.ok && !result.timedOut)
703
- process.exit(1);
580
+ async function runScrape(_flags, _cc) {
581
+ console.error(`This command has been retired. Fill the idea bank from the Mining page in the dashboard: ${getDashboardUrl()}/mining`);
582
+ process.exit(1);
704
583
  }
@@ -184,7 +184,7 @@ async function runTemplate(text, flags, who) {
184
184
  renderMode: "images",
185
185
  inputAds: text,
186
186
  aspectRatio: aspect,
187
- model: "gpt-image-2",
187
+ model: "gpt-image-2.5",
188
188
  realismMode: realisticFlag ? "realistic" : "off",
189
189
  ...(steering ? { steering } : {}),
190
190
  ...(requestedImageCount !== undefined ? { requestedImageCount } : {}),
@@ -1,6 +1,5 @@
1
- import { apiGet } from "../lib/client.js";
1
+ import { apiGet, getDashboardUrl } from "../lib/client.js";
2
2
  import { formatGeneration, formatGenesisRun, formatError, displayRunStatus } from "../lib/format.js";
3
- import { hydrateScoutIdeasCount } from "../lib/scout-hydrate.js";
4
3
  import { formatIntelResult as formatIntelResultRich } from "../lib/intel-format.js";
5
4
  export const helpText = `
6
5
  exodus status — Check the status of a pipeline run
@@ -12,19 +11,13 @@ Required:
12
11
  --id <runId> The generation or run ID returned by a prior exodus command
13
12
 
14
13
  Options:
15
- --type <pipeline> Pipeline type: generation (default) | genesis | intel | pulse | scout
14
+ --type <pipeline> Pipeline type: generation (default) | genesis | intel | pulse
16
15
  | creative | template
17
16
  Spark, Viral, Mirror, Remix runs use --type generation (default).
18
- Intel, Pulse, Scout require --type intel|pulse|scout.
17
+ Intel, Pulse require --type intel|pulse.
19
18
  Creative-suite (native/copy-derived/ref-match/meme)
20
19
  use --type creative; Template uses --type template.
21
20
  `.trim();
22
- async function fetchScoutIdeasCount(runId) {
23
- const res = await apiGet(`/api/v2/scout/ideas?runId=${runId}&limit=1`);
24
- if (res.ok && typeof res.data.count === "number")
25
- return res.data.count;
26
- return null;
27
- }
28
21
  export function formatIntelResult(data) {
29
22
  return formatIntelResultRich(data).replace("## Intel Analysis Result", "## Intel Status");
30
23
  }
@@ -157,18 +150,21 @@ export async function run(flags) {
157
150
  process.exit(1);
158
151
  }
159
152
  const type = flags["type"] ?? "generation";
153
+ if (type === "scout") {
154
+ console.error(`--type scout has been retired. Check run status in the dashboard: ${getDashboardUrl()}/runs`);
155
+ process.exit(1);
156
+ }
160
157
  const pathMap = {
161
158
  generation: `/api/v2/generations?id=${id}`,
162
159
  genesis: `/api/v2/genesis?id=${id}`,
163
160
  intel: `/api/v2/intel?id=${id}`,
164
161
  pulse: `/api/v2/pulse?id=${id}`,
165
- scout: `/api/v2/scout?runId=${id}`,
166
162
  creative: `/api/v2/creative?runId=${id}`,
167
163
  template: `/api/v2/template?runId=${id}`,
168
164
  };
169
165
  const path = pathMap[type];
170
166
  if (!path) {
171
- console.error(`Unknown type: "${type}". Valid types: generation, genesis, intel, pulse, scout, creative, template`);
167
+ console.error(`Unknown type: "${type}". Valid types: generation, genesis, intel, pulse, creative, template`);
172
168
  process.exit(1);
173
169
  }
174
170
  const res = await apiGet(path);
@@ -176,9 +172,6 @@ export async function run(flags) {
176
172
  console.log(formatError(res));
177
173
  process.exit(1);
178
174
  }
179
- if (type === "scout") {
180
- await hydrateScoutIdeasCount(id, res.data, fetchScoutIdeasCount);
181
- }
182
175
  switch (type) {
183
176
  case "generation":
184
177
  console.log(formatGeneration(res.data));
@@ -192,9 +185,6 @@ export async function run(flags) {
192
185
  case "pulse":
193
186
  console.log(formatPulseResult(res.data));
194
187
  break;
195
- case "scout":
196
- console.log(formatScoutResult(res.data));
197
- break;
198
188
  case "creative":
199
189
  console.log(formatCreativeSuiteStatus("Creative Suite Status", res.data));
200
190
  break;
@@ -17,7 +17,8 @@ Run options:
17
17
  --mode auto|manual Generation mode (default: auto)
18
18
  --render-mode images|prompts Skip render and return prompts only (default: images)
19
19
  --aspect 1:1|9:16 Image aspect ratio (default: 1:1)
20
- --model gpt-image-2|nano-banana-pro Kie.ai model (default: gpt-image-2)
20
+ --model gpt-image-2.5|gpt-image-2.5-sunburst|gpt-image-2|nano-banana-pro
21
+ Kie.ai model (default: gpt-image-2.5)
21
22
  --realism off|realistic Realism enforcement (default: off)
22
23
  --quantities <slug:N,slug:N> Manual-mode per-type counts (e.g. "testimonial:3,hero:2")
23
24
  --requested-count N Auto-mode total render target (optional)
@@ -103,9 +104,15 @@ async function runTemplate(flags) {
103
104
  process.exit(1);
104
105
  return;
105
106
  }
106
- const model = typeof flags["model"] === "string" ? flags["model"] : "gpt-image-2";
107
- if (model !== "gpt-image-2" && model !== "nano-banana-pro") {
108
- console.error(`Error: --model must be gpt-image-2 | nano-banana-pro (got "${model}").`);
107
+ const TEMPLATE_MODELS = [
108
+ "gpt-image-2.5",
109
+ "gpt-image-2.5-sunburst",
110
+ "gpt-image-2",
111
+ "nano-banana-pro",
112
+ ];
113
+ const model = typeof flags["model"] === "string" ? flags["model"] : "gpt-image-2.5";
114
+ if (!TEMPLATE_MODELS.includes(model)) {
115
+ console.error(`Error: --model must be one of ${TEMPLATE_MODELS.join(" | ")} (got "${model}").`);
109
116
  process.exit(1);
110
117
  return;
111
118
  }
@@ -1,6 +1,9 @@
1
1
  import { type ApiResponse } from "../lib/client.js";
2
2
  import type { FlagOccurrence } from "../lib/args.js";
3
3
  import { type RunProvenance, type WorkflowRun } from "./workflow.js";
4
+ declare const CONCEIT_KEYS: readonly ["podcast", "ugc", "stage", "street", "personification"];
5
+ declare const VOICE_MODES: readonly ["native", "voice-first"];
6
+ type VoiceMode = (typeof VOICE_MODES)[number];
4
7
  export declare const helpText: string;
5
8
  export declare const VOICES_PATH = "/api/v2/video/voices";
6
9
  export interface ClipWord {
@@ -8,10 +11,31 @@ export interface ClipWord {
8
11
  s: number;
9
12
  e: number;
10
13
  }
14
+ export type ClipQcTakeNeighbours = {
15
+ state: "not-applicable";
16
+ } | {
17
+ state: "not-attached";
18
+ } | {
19
+ state: "none-accepted";
20
+ } | {
21
+ state: "attached";
22
+ scenes: number[];
23
+ };
24
+ export interface ClipQcTake {
25
+ kind: "take" | "soft-retry";
26
+ failCodes: string[];
27
+ warnCodes: string[];
28
+ neighbours: ClipQcTakeNeighbours;
29
+ judgeWording?: {
30
+ code: string;
31
+ wording: string;
32
+ }[];
33
+ }
11
34
  export interface ClipQc {
12
35
  verdict: "pass" | "fail";
13
36
  attempts: number;
14
37
  neighbours?: number[];
38
+ takes?: ClipQcTake[];
15
39
  }
16
40
  export interface ClipFinding {
17
41
  check: string;
@@ -21,6 +45,9 @@ export interface ClipFinding {
21
45
  judgeDetail?: string;
22
46
  judgeSeverity?: "fail" | "warn";
23
47
  }
48
+ export declare function qcWithoutJudgeWording(qc: ClipQc): ClipQc;
49
+ export declare const NO_TAKE_HISTORY_LINE = "no history recorded for this clip";
50
+ export declare function renderQcTakeHistory(takes: ClipQcTake[] | undefined): string[];
24
51
  export type ArtifactSubset = {
25
52
  type: "storyboard";
26
53
  storyboard?: unknown;
@@ -69,12 +96,23 @@ export type ArtifactSubset = {
69
96
  } | {
70
97
  type: "text" | "primer" | "session" | "document";
71
98
  };
99
+ export interface PlanFailureRecord {
100
+ kind: string;
101
+ partsTotal?: number;
102
+ acceptedParts?: number;
103
+ acceptedScenes?: number;
104
+ failedPart?: number;
105
+ reasons?: string[];
106
+ lines: string[];
107
+ }
72
108
  export interface VideoRunNode {
73
109
  nodeId: string;
74
110
  kind: string;
75
- status: "idle" | "running" | "done" | "failed" | "skipped";
111
+ status: "idle" | "running" | "done" | "failed" | "skipped" | "out-of-scope";
76
112
  error?: string;
77
113
  warning?: string;
114
+ planFailure?: PlanFailureRecord;
115
+ hasRejectedDraft?: boolean;
78
116
  outputs?: ArtifactSubset[];
79
117
  }
80
118
  export type BuilderPauseReason = "taste" | "repair" | "slots" | "call" | "checkpoint";
@@ -103,6 +141,8 @@ export interface VideoRun {
103
141
  pauseAhead?: WorkflowRun["pauseAhead"];
104
142
  castLock?: PullCastLock | null;
105
143
  provenance?: RunProvenance;
144
+ musicBed?: boolean;
145
+ videoChoice?: RunVideoChoice;
106
146
  workflowId?: string;
107
147
  moduleOwned?: boolean;
108
148
  }
@@ -117,6 +157,15 @@ export interface NodeItem {
117
157
  flagged?: boolean;
118
158
  findings?: ClipFinding[];
119
159
  staleClaim?: boolean;
160
+ lastRedo?: {
161
+ take: number;
162
+ outcome: string;
163
+ label: string;
164
+ displacedHistory?: {
165
+ title: string;
166
+ lines: string[];
167
+ };
168
+ };
120
169
  artifact?: ArtifactSubset;
121
170
  }
122
171
  export interface ShowRow {
@@ -168,6 +217,7 @@ export type RunStop = {
168
217
  at: "storyboard-gate";
169
218
  nodeId?: string;
170
219
  framesNodeId?: string;
220
+ showAd?: true;
171
221
  } | {
172
222
  at: "final-watch";
173
223
  } | {
@@ -184,11 +234,24 @@ export type RunStop = {
184
234
  at: "finished";
185
235
  status: string;
186
236
  };
237
+ export type ResolvedStop = Exclude<RunStop, {
238
+ at: "final-watch";
239
+ }> | {
240
+ at: "final-watch";
241
+ cutAttached: boolean | null;
242
+ };
187
243
  export declare function classifyRun(run: VideoRun): RunStop;
244
+ export declare function hasAttachedCut(items: NodeItem[]): boolean;
245
+ export declare function resolveStop(stop: RunStop, cutAttached: boolean | null): ResolvedStop;
246
+ export declare function resolveStopAtPark(stop: RunStop, runId: string, deps: Pick<VideoDeps, "get">): Promise<ResolvedStop>;
188
247
  export declare function stageWord(stage: string): string;
189
248
  export declare function stepName(kind: string | undefined): string;
249
+ export declare function isShowAd(run: Pick<VideoRun, "moduleOwned">): boolean;
190
250
  export declare function reviewUrl(dashboardUrl: string, run: Pick<VideoRun, "_id" | "workflowId" | "moduleOwned">): string;
191
- export declare function stopLines(stop: RunStop, runId: string, runUrl: string): string[];
251
+ export declare function stopLines(stop: ResolvedStop, runId: string, runUrl: string): string[];
252
+ export declare function failedStoryboardNode(run: VideoRun): VideoRunNode | undefined;
253
+ export declare function errorEchoesReasons(error: string | undefined, record: PlanFailureRecord | undefined): boolean;
254
+ export declare function planFailureLines(node: VideoRunNode | undefined, runId: string): string[];
192
255
  export interface PullDownload {
193
256
  file: string;
194
257
  url: string;
@@ -231,6 +294,14 @@ export interface ManifestCastRef {
231
294
  voiceLabel: string | null;
232
295
  voiceDescription: string | null;
233
296
  }
297
+ export type MusicBedState = "on" | "off" | "unknown";
298
+ export interface RunVideoChoice {
299
+ videoModel: string;
300
+ videoModelLabel: string;
301
+ voiceMode: string;
302
+ voiceModeLabel: string;
303
+ }
304
+ export declare const MUSIC_HEARD_CODE = "music-heard";
234
305
  export interface VideoManifest {
235
306
  runId: string;
236
307
  pulledAt: string;
@@ -238,6 +309,8 @@ export interface VideoManifest {
238
309
  storyboard: string | null;
239
310
  reference: string | null;
240
311
  music: string | null;
312
+ musicBed: MusicBedState;
313
+ musicHeardScenes: number[];
241
314
  cast: ManifestCastRef[];
242
315
  narration: {
243
316
  file: string;
@@ -246,8 +319,13 @@ export interface VideoManifest {
246
319
  scenes: ManifestScene[];
247
320
  failed: PullFailure[];
248
321
  provenance?: RunProvenance;
322
+ videoChoice?: {
323
+ videoModel: string;
324
+ voiceMode: string;
325
+ };
249
326
  }
250
- export declare const CAST_LEDGER_BASE = 910000;
327
+ export declare const CAST_LEDGER_BASE: number;
328
+ export declare function musicBedState(run: VideoRun): MusicBedState;
251
329
  export interface PullPlan {
252
330
  downloads: PullDownload[];
253
331
  texts: PullTextFile[];
@@ -270,7 +348,39 @@ export interface StartOptions {
270
348
  wait: boolean;
271
349
  json: boolean;
272
350
  }
351
+ export interface ScriptStartOptions {
352
+ scriptFile: string;
353
+ conceit: (typeof CONCEIT_KEYS)[number];
354
+ style: string;
355
+ direction?: string;
356
+ voicePath?: string;
357
+ videoModel?: string;
358
+ voiceMode?: VoiceMode;
359
+ music?: boolean;
360
+ wait: boolean;
361
+ json: boolean;
362
+ }
363
+ export type VideoStartPlan = {
364
+ kind: "usage";
365
+ line: string;
366
+ } | {
367
+ kind: "show";
368
+ opts: StartOptions;
369
+ } | {
370
+ kind: "script";
371
+ opts: ScriptStartOptions;
372
+ };
273
373
  export declare function startFlow(opts: StartOptions, deps: VideoDeps): Promise<FlowResult>;
374
+ export type MusicChoice = {
375
+ ok: true;
376
+ music: boolean | undefined;
377
+ } | {
378
+ ok: false;
379
+ line: string;
380
+ };
381
+ export declare function planMusicChoice(occurrences: FlagOccurrence[]): MusicChoice;
382
+ export declare function planVideoStart(flags: Record<string, string | boolean>, occurrences: FlagOccurrence[]): VideoStartPlan;
383
+ export declare function startScriptFlow(opts: ScriptStartOptions, deps: VideoDeps): Promise<FlowResult>;
274
384
  export declare function waitFlow(runId: string, opts: {
275
385
  json: boolean;
276
386
  url?: string;
@@ -278,8 +388,12 @@ export declare function waitFlow(runId: string, opts: {
278
388
  maxPolls?: number;
279
389
  }, deps: VideoDeps): Promise<FlowResult>;
280
390
  export declare function statusFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
391
+ export declare function rejectedDraftFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
281
392
  export declare function storyboardFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
282
- export declare function approveFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
393
+ export declare function approveFlow(runId: string, opts: {
394
+ json: boolean;
395
+ approveStaleCut: boolean;
396
+ }, deps: VideoDeps): Promise<FlowResult>;
283
397
  export declare function flagFlow(runId: string, note: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
284
398
  export declare function retryFrameFlow(runId: string, nodeId: string, sceneIndex: number, note: string | undefined, json: boolean, deps: VideoDeps): Promise<FlowResult>;
285
399
  export type ClipRedoPlan = {
@@ -365,3 +479,4 @@ export declare function describeFetchFailure(err: unknown): string;
365
479
  export declare function uploadFlow(runId: string, filePath: string, durationFlag: string | undefined, json: boolean, deps: VideoDeps): Promise<FlowResult>;
366
480
  export declare function parsePositional(args?: string[]): string[];
367
481
  export declare function run(flags: Record<string, string | boolean>, occurrences: FlagOccurrence[]): Promise<void>;
482
+ export {};