@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.
- package/assets/docs/CLAUDE.md +1 -1
- package/assets/docs/PIPELINES.md +7 -10
- package/assets/skills/exodus-meme/SKILL.md +4 -3
- package/assets/skills/exodus-template/SKILL.md +2 -1
- package/assets/skills/exodus-video/SKILL.md +215 -163
- package/assets/skills/exodus-video/scripts/first-cut.mjs +17 -30
- package/assets/skills/exodus-workflow/SKILL.md +1 -1
- package/dist/commands/genesis.js +12 -133
- package/dist/commands/image.js +1 -1
- package/dist/commands/status.js +8 -18
- package/dist/commands/template.js +11 -4
- package/dist/commands/video.d.ts +119 -4
- package/dist/commands/video.js +639 -77
- package/dist/commands/workflow.d.ts +51 -3
- package/dist/commands/workflow.js +62 -9
- package/dist/lib/helperLedgerBase.d.ts +3 -0
- package/dist/lib/helperLedgerBase.js +3 -0
- package/package.json +1 -1
|
@@ -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
|
|
13
|
-
//
|
|
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
|
|
95
|
-
* a beat of air
|
|
96
|
-
* is
|
|
97
|
-
*
|
|
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,
|
|
99
|
+
if (clipSeconds === null) return { seconds: target, holdSec: 0, trimmedBy: 0 };
|
|
102
100
|
if (target <= clipSeconds) {
|
|
103
|
-
return { seconds: target,
|
|
101
|
+
return { seconds: target, holdSec: 0, trimmedBy: clipSeconds - target };
|
|
104
102
|
}
|
|
105
|
-
|
|
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,
|
|
193
|
-
holdSec: fit.holdSec, words:
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
package/dist/commands/genesis.js
CHANGED
|
@@ -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(
|
|
282
|
-
|
|
283
|
-
|
|
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(
|
|
311
|
-
|
|
312
|
-
|
|
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(
|
|
617
|
-
|
|
618
|
-
|
|
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(
|
|
649
|
-
|
|
650
|
-
|
|
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
|
}
|
package/dist/commands/image.js
CHANGED
|
@@ -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 } : {}),
|
package/dist/commands/status.js
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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|
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
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
|
}
|
package/dist/commands/video.d.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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,
|
|
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 {};
|