@aicopycoders/exodus 2026.9.2000 → 2026.10.1200-beta.1
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/skills/exodus-video/SKILL.md +40 -14
- package/assets/skills/exodus-video/scripts/first-cut.d.mts +18 -1
- package/assets/skills/exodus-video/scripts/first-cut.mjs +434 -93
- package/dist/commands/video-customer.d.ts +3 -0
- package/dist/commands/video-customer.js +12 -1
- package/dist/commands/video.d.ts +35 -2
- package/dist/commands/video.js +126 -25
- package/package.json +3 -3
|
@@ -10,6 +10,8 @@ export type CustomerStopReason = {
|
|
|
10
10
|
kind: "out-of-credits";
|
|
11
11
|
} | {
|
|
12
12
|
kind: "openrouter-out-of-credits";
|
|
13
|
+
} | {
|
|
14
|
+
kind: "plan-too-big";
|
|
13
15
|
} | {
|
|
14
16
|
kind: "keys-missing";
|
|
15
17
|
keys: string[];
|
|
@@ -25,6 +27,7 @@ export interface CustomerRun {
|
|
|
25
27
|
} | null;
|
|
26
28
|
mode: "one-shot" | "guided";
|
|
27
29
|
stage: CustomerRunStage;
|
|
30
|
+
awaitingSignOff?: boolean;
|
|
28
31
|
waiting: {
|
|
29
32
|
ahead: number;
|
|
30
33
|
stepsDone: number;
|
|
@@ -39,6 +39,12 @@ function stoppedLines(run, page) {
|
|
|
39
39
|
"Add credits to your OpenRouter account at openrouter.ai, then try again from the video's page. The OpenRouter key this video uses is in Settings → API Keys.",
|
|
40
40
|
];
|
|
41
41
|
}
|
|
42
|
+
if (reason?.kind === "plan-too-big") {
|
|
43
|
+
return [
|
|
44
|
+
"This video's plan came out too big to make in one go.",
|
|
45
|
+
`Try again from the video's page and it will be planned fresh: ${page} If it stops here again, try a shorter script.`,
|
|
46
|
+
];
|
|
47
|
+
}
|
|
42
48
|
if (reason?.kind === "keys-missing") {
|
|
43
49
|
return [
|
|
44
50
|
`This video stopped because ${reason.keys.length === 1 ? "a key is" : "some keys are"} missing (${reason.keys.join(", ")}).`,
|
|
@@ -67,7 +73,12 @@ export function customerStageLines(run, page) {
|
|
|
67
73
|
];
|
|
68
74
|
case "ready":
|
|
69
75
|
return [
|
|
70
|
-
|
|
76
|
+
...(run.awaitingSignOff
|
|
77
|
+
? [
|
|
78
|
+
`Your video is ready to watch: ${run.video?.url ?? page}`,
|
|
79
|
+
`When you're happy with it, finish it: exodus video approve ${id}`,
|
|
80
|
+
]
|
|
81
|
+
: [`Your video is ready: ${run.video?.url ?? page}`]),
|
|
71
82
|
...(run.video?.wordsOff
|
|
72
83
|
? ["Some words may not match your script exactly. Watch it through before you post it."]
|
|
73
84
|
: []),
|
package/dist/commands/video.d.ts
CHANGED
|
@@ -2,9 +2,19 @@ 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
4
|
import { type CustomerItem, type CustomerRun } from "./video-customer.js";
|
|
5
|
-
declare const CONCEIT_KEYS: readonly ["podcast", "ugc", "stage", "street", "personification"];
|
|
6
5
|
declare const VOICE_MODES: readonly ["native", "voice-first"];
|
|
7
6
|
type VoiceMode = (typeof VOICE_MODES)[number];
|
|
7
|
+
export type ScriptRunOptions = {
|
|
8
|
+
conceits: {
|
|
9
|
+
key: string;
|
|
10
|
+
label: string;
|
|
11
|
+
}[];
|
|
12
|
+
styles: {
|
|
13
|
+
slug: string;
|
|
14
|
+
label: string;
|
|
15
|
+
}[];
|
|
16
|
+
};
|
|
17
|
+
export declare function scriptRunPickProblem(options: ScriptRunOptions, conceit: string, style: string): string | null;
|
|
8
18
|
export declare const helpText: string;
|
|
9
19
|
export declare const RUN_PATH = "/api/v2/workflow";
|
|
10
20
|
export declare const ITEMS_PATH = "/api/v2/workflow/items";
|
|
@@ -168,6 +178,7 @@ export interface VideoRun {
|
|
|
168
178
|
provenance?: RunProvenance;
|
|
169
179
|
musicBed?: boolean;
|
|
170
180
|
videoChoice?: RunVideoChoice;
|
|
181
|
+
shots?: RunShots;
|
|
171
182
|
storyboardAutoApproval?: {
|
|
172
183
|
outcome: "approved" | "held";
|
|
173
184
|
at: number;
|
|
@@ -231,6 +242,7 @@ export interface ShowRow {
|
|
|
231
242
|
export interface FlowResult {
|
|
232
243
|
code: number;
|
|
233
244
|
lines: string[];
|
|
245
|
+
usage?: true;
|
|
234
246
|
}
|
|
235
247
|
export interface VideoDeps {
|
|
236
248
|
get: (path: string) => Promise<ApiResponse<unknown>>;
|
|
@@ -390,6 +402,11 @@ export interface VideoManifest {
|
|
|
390
402
|
videoModel: string;
|
|
391
403
|
voiceMode: string;
|
|
392
404
|
};
|
|
405
|
+
shots?: RunShots;
|
|
406
|
+
}
|
|
407
|
+
export interface RunShots {
|
|
408
|
+
frame?: string;
|
|
409
|
+
pacing?: string;
|
|
393
410
|
}
|
|
394
411
|
export declare const CAST_LEDGER_BASE: number;
|
|
395
412
|
export declare function musicBedState(run: VideoRun): MusicBedState;
|
|
@@ -417,7 +434,7 @@ export interface StartOptions {
|
|
|
417
434
|
}
|
|
418
435
|
export interface ScriptStartOptions {
|
|
419
436
|
scriptFile: string;
|
|
420
|
-
conceit:
|
|
437
|
+
conceit: string;
|
|
421
438
|
style: string;
|
|
422
439
|
direction?: string;
|
|
423
440
|
voicePath?: string;
|
|
@@ -426,6 +443,7 @@ export interface ScriptStartOptions {
|
|
|
426
443
|
music?: boolean;
|
|
427
444
|
reviewStoryboard?: true;
|
|
428
445
|
director?: true;
|
|
446
|
+
shots?: RunShots;
|
|
429
447
|
wait: boolean;
|
|
430
448
|
json: boolean;
|
|
431
449
|
}
|
|
@@ -574,6 +592,21 @@ export declare function pullFlow(runId: string, dir: string, json: boolean, deps
|
|
|
574
592
|
export declare function parseMvhdDurationSec(bytes: Uint8Array): number | null;
|
|
575
593
|
export declare const NO_DURATION_MESSAGE: string;
|
|
576
594
|
export declare function describeFetchFailure(err: unknown): string;
|
|
595
|
+
export interface CutScene {
|
|
596
|
+
scene: number;
|
|
597
|
+
startSec: number;
|
|
598
|
+
seconds: number;
|
|
599
|
+
}
|
|
600
|
+
export interface CutWord {
|
|
601
|
+
text: string;
|
|
602
|
+
start: number;
|
|
603
|
+
end: number;
|
|
604
|
+
scene: number;
|
|
605
|
+
}
|
|
606
|
+
export declare function cutSidecars(filePath: string, deps: VideoDeps): {
|
|
607
|
+
cutScenes?: CutScene[];
|
|
608
|
+
cutWords?: CutWord[];
|
|
609
|
+
};
|
|
577
610
|
export declare function uploadFlow(runId: string, filePath: string, durationFlag: string | undefined, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
578
611
|
export declare function parsePositional(args?: string[]): string[];
|
|
579
612
|
export declare function run(flags: Record<string, string | boolean>, occurrences: FlagOccurrence[]): Promise<void>;
|
package/dist/commands/video.js
CHANGED
|
@@ -8,20 +8,27 @@ import { hasBinary } from "../lib/preflight.js";
|
|
|
8
8
|
import { ASSET_UPLOAD_POLICY, pauseAheadLine, } from "./workflow.js";
|
|
9
9
|
import { HELPER_LEDGER_BASE, SET_OPTION_LEDGER_BASE } from "../lib/helperLedgerBase.js";
|
|
10
10
|
import { customerAnswer, customerRunPage, customerStageIsWorking, customerStageLines, customerStatusLines, customerStoryboardLines, customerVoicesLines, } from "./video-customer.js";
|
|
11
|
-
const CONCEIT_KEYS = ["podcast", "ugc", "stage", "street", "personification"];
|
|
12
11
|
const VOICE_MODES = ["native", "voice-first"];
|
|
13
|
-
const STYLE_SLUGS = [
|
|
14
|
-
"pixar-3d",
|
|
15
|
-
"claymation-stop-motion",
|
|
16
|
-
"lego-toy-brick",
|
|
17
|
-
"balloon",
|
|
18
|
-
"video-game-8bit",
|
|
19
|
-
];
|
|
20
12
|
function joinedOr(items) {
|
|
21
13
|
if (items.length <= 1)
|
|
22
14
|
return items[0] ?? "";
|
|
23
15
|
return `${items.slice(0, -1).join(", ")} or ${items[items.length - 1]}`;
|
|
24
16
|
}
|
|
17
|
+
export function scriptRunPickProblem(options, conceit, style) {
|
|
18
|
+
const keys = options.conceits.map((row) => row.key);
|
|
19
|
+
if (!keys.includes(conceit)) {
|
|
20
|
+
return keys.length === 0
|
|
21
|
+
? "This Exodus server offers no kinds of ad yet."
|
|
22
|
+
: `video start --conceit must be one of ${joinedOr(keys)}.`;
|
|
23
|
+
}
|
|
24
|
+
const slugs = options.styles.map((row) => row.slug);
|
|
25
|
+
if (!slugs.includes(style)) {
|
|
26
|
+
return slugs.length === 0
|
|
27
|
+
? "This Exodus server offers no looks yet."
|
|
28
|
+
: `video start --style must be one of ${joinedOr(slugs)}.`;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
25
32
|
function cantSayNames(names) {
|
|
26
33
|
return `the recorded voice can't say ${joinedOr(names.map((n) => `"${n}"`))}`;
|
|
27
34
|
}
|
|
@@ -105,7 +112,7 @@ Usage:
|
|
|
105
112
|
exodus video voices <runId> [--set <character>=<voiceId>] [--clear <character>] [--from <file.json>] [--json]
|
|
106
113
|
exodus video pull <runId> --out <dir> [--json]
|
|
107
114
|
exodus video upload <runId> --file <cut.mp4> [--duration <sec>] [--json]
|
|
108
|
-
exodus video start --script <file> --conceit
|
|
115
|
+
exodus video start --script <file> --conceit <kind> --style <look> [--direction "<note>"] [--frame pov] [--pacing <jump-cuts|long-take|montage>] [--voice-path <path>] [--video-model <id>] [--voice <native|voice-first>] [--music] [--review-storyboard] [--director] [--wait] [--json]
|
|
109
116
|
exodus video director <runId> <on|off|follow> [--json]
|
|
110
117
|
|
|
111
118
|
Options:
|
|
@@ -137,9 +144,16 @@ Options:
|
|
|
137
144
|
--rejected-draft Print the storyboard draft the system turned down
|
|
138
145
|
(status). It was thrown away — a record only
|
|
139
146
|
--script <file> Text file of what the ad says (start)
|
|
140
|
-
--conceit <
|
|
141
|
-
--style <
|
|
147
|
+
--conceit <kind> Kind of ad (start)
|
|
148
|
+
--style <look> Look (start)
|
|
149
|
+
The server lists the kinds and looks it offers. An unknown word prints them.
|
|
142
150
|
--direction "<note>" Note for the whole ad (start)
|
|
151
|
+
--frame pov Camera-as-character POV: the camera is a person in the
|
|
152
|
+
scene (start, with --conceit). Leave it off for a
|
|
153
|
+
single frame. Conversation ads only
|
|
154
|
+
--pacing <pace> jump-cuts, long-take or montage (start, with
|
|
155
|
+
--conceit). Leave it off for natural pacing. Presenter
|
|
156
|
+
ads only
|
|
143
157
|
--voice-path <path> How the voices are made (start)
|
|
144
158
|
--video-model <id> Which video model films this ad, instead of the kind of
|
|
145
159
|
ad's own default (start, with --conceit)
|
|
@@ -163,8 +177,8 @@ Options:
|
|
|
163
177
|
|
|
164
178
|
Who can use what. Admins get every command here. Beta testers with video
|
|
165
179
|
switched on get these, on their own brands: start (--script, --conceit,
|
|
166
|
-
--style, --direction, --music, --review-storyboard),
|
|
167
|
-
approve (the storyboard), voices, retry-frame --scene <n> (redo one scene's
|
|
180
|
+
--style, --direction, --frame, --pacing, --music, --review-storyboard),
|
|
181
|
+
status, storyboard, approve (the storyboard), voices, retry-frame --scene <n> (redo one scene's
|
|
168
182
|
picture while the storyboard waits), pull (the finished video, each scene's
|
|
169
183
|
clip and voice, and the music) and upload. These stay admin-only:
|
|
170
184
|
start --video-model / --voice / --voice-path / --director, director,
|
|
@@ -1068,6 +1082,7 @@ export function planPull(run, items, opts) {
|
|
|
1068
1082
|
},
|
|
1069
1083
|
}
|
|
1070
1084
|
: {}),
|
|
1085
|
+
...(run.shots?.frame || run.shots?.pacing ? { shots: run.shots } : {}),
|
|
1071
1086
|
},
|
|
1072
1087
|
};
|
|
1073
1088
|
}
|
|
@@ -1247,9 +1262,6 @@ function nonBlank(value) {
|
|
|
1247
1262
|
function isVoiceMode(value) {
|
|
1248
1263
|
return VOICE_MODES.some((mode) => mode === value);
|
|
1249
1264
|
}
|
|
1250
|
-
function isConceit(value) {
|
|
1251
|
-
return CONCEIT_KEYS.some((key) => key === value);
|
|
1252
|
-
}
|
|
1253
1265
|
export function planMusicChoice(occurrences) {
|
|
1254
1266
|
let music;
|
|
1255
1267
|
for (const { flag, value } of occurrences) {
|
|
@@ -1314,6 +1326,12 @@ export function planVideoStart(flags, occurrences) {
|
|
|
1314
1326
|
const voiceFlag = planValueFlag(occurrences, "voice");
|
|
1315
1327
|
if (!voiceFlag.ok)
|
|
1316
1328
|
return { kind: "usage", line: voiceFlag.line };
|
|
1329
|
+
const frameFlag = planValueFlag(occurrences, "frame");
|
|
1330
|
+
if (!frameFlag.ok)
|
|
1331
|
+
return { kind: "usage", line: frameFlag.line };
|
|
1332
|
+
const pacingFlag = planValueFlag(occurrences, "pacing");
|
|
1333
|
+
if (!pacingFlag.ok)
|
|
1334
|
+
return { kind: "usage", line: pacingFlag.line };
|
|
1317
1335
|
const videoModel = videoModelFlag.value;
|
|
1318
1336
|
const voiceMode = voiceFlag.value;
|
|
1319
1337
|
const reviewStoryboard = occurrences.some((o) => o.flag === "review-storyboard");
|
|
@@ -1337,6 +1355,12 @@ export function planVideoStart(flags, occurrences) {
|
|
|
1337
1355
|
line: "--director works with --conceit, not --show.",
|
|
1338
1356
|
};
|
|
1339
1357
|
}
|
|
1358
|
+
if (showId && (frameFlag.value || pacingFlag.value)) {
|
|
1359
|
+
return {
|
|
1360
|
+
kind: "usage",
|
|
1361
|
+
line: "--frame and --pacing work with --conceit, not --show.",
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1340
1364
|
if (showId && (videoModel || voiceMode)) {
|
|
1341
1365
|
return {
|
|
1342
1366
|
kind: "usage",
|
|
@@ -1365,19 +1389,13 @@ export function planVideoStart(flags, occurrences) {
|
|
|
1365
1389
|
if (!conceit) {
|
|
1366
1390
|
return {
|
|
1367
1391
|
kind: "usage",
|
|
1368
|
-
line:
|
|
1369
|
-
};
|
|
1370
|
-
}
|
|
1371
|
-
if (!isConceit(conceit)) {
|
|
1372
|
-
return {
|
|
1373
|
-
kind: "usage",
|
|
1374
|
-
line: `video start --conceit must be one of ${joinedOr(CONCEIT_KEYS)}.`,
|
|
1392
|
+
line: "video start needs --conceit <kind>.",
|
|
1375
1393
|
};
|
|
1376
1394
|
}
|
|
1377
1395
|
if (!style) {
|
|
1378
1396
|
return {
|
|
1379
1397
|
kind: "usage",
|
|
1380
|
-
line:
|
|
1398
|
+
line: "video start needs --style <look>.",
|
|
1381
1399
|
};
|
|
1382
1400
|
}
|
|
1383
1401
|
const direction = nonBlank(flagString(flags, "direction"));
|
|
@@ -1403,11 +1421,35 @@ export function planVideoStart(flags, occurrences) {
|
|
|
1403
1421
|
opts.reviewStoryboard = true;
|
|
1404
1422
|
if (director)
|
|
1405
1423
|
opts.director = true;
|
|
1424
|
+
if (frameFlag.value || pacingFlag.value) {
|
|
1425
|
+
opts.shots = {
|
|
1426
|
+
...(frameFlag.value ? { frame: frameFlag.value } : {}),
|
|
1427
|
+
...(pacingFlag.value ? { pacing: pacingFlag.value } : {}),
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1406
1430
|
return { kind: "script", opts };
|
|
1407
1431
|
}
|
|
1408
1432
|
function isRecord(value) {
|
|
1409
1433
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1410
1434
|
}
|
|
1435
|
+
function scriptRunOptionsOf(data) {
|
|
1436
|
+
if (!isRecord(data) || !Array.isArray(data.conceits) || !Array.isArray(data.styles)) {
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
const conceits = [];
|
|
1440
|
+
for (const row of data.conceits) {
|
|
1441
|
+
if (!isRecord(row) || typeof row.key !== "string" || typeof row.label !== "string")
|
|
1442
|
+
return null;
|
|
1443
|
+
conceits.push({ key: row.key, label: row.label });
|
|
1444
|
+
}
|
|
1445
|
+
const styles = [];
|
|
1446
|
+
for (const row of data.styles) {
|
|
1447
|
+
if (!isRecord(row) || typeof row.slug !== "string" || typeof row.label !== "string")
|
|
1448
|
+
return null;
|
|
1449
|
+
styles.push({ slug: row.slug, label: row.label });
|
|
1450
|
+
}
|
|
1451
|
+
return { conceits, styles };
|
|
1452
|
+
}
|
|
1411
1453
|
function isResolvedCard(value) {
|
|
1412
1454
|
if (!isRecord(value))
|
|
1413
1455
|
return false;
|
|
@@ -1482,6 +1524,17 @@ export async function startScriptFlow(opts, deps) {
|
|
|
1482
1524
|
if (script.trim().length === 0) {
|
|
1483
1525
|
return { code: 1, lines: [`The script file "${opts.scriptFile}" is empty.`] };
|
|
1484
1526
|
}
|
|
1527
|
+
const listed = await deps.get(SCRIPT_RUNS_PATH).catch(() => null);
|
|
1528
|
+
if (listed?.ok) {
|
|
1529
|
+
const options = scriptRunOptionsOf(listed.data);
|
|
1530
|
+
if (!options) {
|
|
1531
|
+
return adminOnlyResult("The server's list of kinds and looks could not be read.", opts.json);
|
|
1532
|
+
}
|
|
1533
|
+
const problem = scriptRunPickProblem(options, opts.conceit, opts.style);
|
|
1534
|
+
if (problem) {
|
|
1535
|
+
return opts.json ? adminOnlyResult(problem, true) : { code: 1, lines: [problem], usage: true };
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1485
1538
|
const res = await deps.post(SCRIPT_RUNS_PATH, {
|
|
1486
1539
|
script,
|
|
1487
1540
|
conceit: opts.conceit,
|
|
@@ -1493,6 +1546,7 @@ export async function startScriptFlow(opts, deps) {
|
|
|
1493
1546
|
...(opts.music === true ? { music: true } : {}),
|
|
1494
1547
|
...(opts.reviewStoryboard ? { reviewStoryboard: true } : {}),
|
|
1495
1548
|
...(opts.director ? { director: true } : {}),
|
|
1549
|
+
...(opts.shots ? { shots: opts.shots } : {}),
|
|
1496
1550
|
});
|
|
1497
1551
|
if (!res.ok)
|
|
1498
1552
|
return errorResult(res, opts.json);
|
|
@@ -2898,6 +2952,46 @@ function uploadRetryResult(failed, json) {
|
|
|
2898
2952
|
],
|
|
2899
2953
|
};
|
|
2900
2954
|
}
|
|
2955
|
+
const CUT_SCENES_MAX = 200;
|
|
2956
|
+
const CUT_WORDS_MAX = 2000;
|
|
2957
|
+
const isTime = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
2958
|
+
function cutScene(row) {
|
|
2959
|
+
const r = row;
|
|
2960
|
+
return r && Number.isInteger(r.scene) && isTime(r.startSec) && isTime(r.seconds) && r.seconds > 0
|
|
2961
|
+
? { scene: r.scene, startSec: r.startSec, seconds: r.seconds }
|
|
2962
|
+
: null;
|
|
2963
|
+
}
|
|
2964
|
+
function cutWord(row) {
|
|
2965
|
+
const r = row;
|
|
2966
|
+
return r &&
|
|
2967
|
+
typeof r.text === "string" &&
|
|
2968
|
+
r.text.trim() !== "" &&
|
|
2969
|
+
isTime(r.start) &&
|
|
2970
|
+
isTime(r.end) &&
|
|
2971
|
+
r.end >= r.start &&
|
|
2972
|
+
Number.isInteger(r.scene)
|
|
2973
|
+
? { text: r.text, start: r.start, end: r.end, scene: r.scene }
|
|
2974
|
+
: null;
|
|
2975
|
+
}
|
|
2976
|
+
function readSidecar(file, parse, max, deps) {
|
|
2977
|
+
let rows;
|
|
2978
|
+
try {
|
|
2979
|
+
rows = JSON.parse(deps.readFile(file));
|
|
2980
|
+
}
|
|
2981
|
+
catch {
|
|
2982
|
+
return null;
|
|
2983
|
+
}
|
|
2984
|
+
if (!Array.isArray(rows) || rows.length === 0 || rows.length > max)
|
|
2985
|
+
return null;
|
|
2986
|
+
const parsed = rows.map(parse);
|
|
2987
|
+
return parsed.every((r) => r !== null) ? parsed : null;
|
|
2988
|
+
}
|
|
2989
|
+
export function cutSidecars(filePath, deps) {
|
|
2990
|
+
const base = path.join(path.dirname(filePath), path.basename(filePath, path.extname(filePath)));
|
|
2991
|
+
const cutScenes = readSidecar(`${base}.scenes.json`, cutScene, CUT_SCENES_MAX, deps);
|
|
2992
|
+
const cutWords = readSidecar(`${base}.words.json`, cutWord, CUT_WORDS_MAX, deps);
|
|
2993
|
+
return { ...(cutScenes ? { cutScenes } : {}), ...(cutWords ? { cutWords } : {}) };
|
|
2994
|
+
}
|
|
2901
2995
|
export async function uploadFlow(runId, filePath, durationFlag, json, deps) {
|
|
2902
2996
|
const stat = deps.statFile(filePath);
|
|
2903
2997
|
if (!stat)
|
|
@@ -2940,6 +3034,7 @@ export async function uploadFlow(runId, filePath, durationFlag, json, deps) {
|
|
|
2940
3034
|
const durationSec = asked ?? deps.probeDurationSec(filePath) ?? parseMvhdDurationSec(bytes);
|
|
2941
3035
|
if (durationSec === null)
|
|
2942
3036
|
return { code: 1, lines: [NO_DURATION_MESSAGE] };
|
|
3037
|
+
const timing = cutSidecars(filePath, deps);
|
|
2943
3038
|
const minting = await withUploadRetry("mint", deps, () => deps.post(ASSET_UPLOAD_URL_PATH, {}));
|
|
2944
3039
|
if (!minting.ok)
|
|
2945
3040
|
return uploadRetryResult(minting, json);
|
|
@@ -2980,6 +3075,7 @@ export async function uploadFlow(runId, filePath, durationFlag, json, deps) {
|
|
|
2980
3075
|
runId,
|
|
2981
3076
|
assetId,
|
|
2982
3077
|
durationSec,
|
|
3078
|
+
...timing,
|
|
2983
3079
|
}));
|
|
2984
3080
|
if (!attaching.ok)
|
|
2985
3081
|
return uploadRetryResult(attaching, json);
|
|
@@ -3033,6 +3129,8 @@ const VALUE_FLAGS = new Set([
|
|
|
3033
3129
|
"voice-path",
|
|
3034
3130
|
"video-model",
|
|
3035
3131
|
"voice",
|
|
3132
|
+
"frame",
|
|
3133
|
+
"pacing",
|
|
3036
3134
|
"note",
|
|
3037
3135
|
"out",
|
|
3038
3136
|
"file",
|
|
@@ -3090,7 +3188,10 @@ export async function run(flags, occurrences) {
|
|
|
3090
3188
|
usage(plan.line);
|
|
3091
3189
|
if (plan.kind === "show")
|
|
3092
3190
|
return printResult(await startFlow(plan.opts, defaultDeps));
|
|
3093
|
-
|
|
3191
|
+
const started = await startScriptFlow(plan.opts, defaultDeps);
|
|
3192
|
+
if (started.usage)
|
|
3193
|
+
usage(started.lines[0] ?? "");
|
|
3194
|
+
return printResult(started);
|
|
3094
3195
|
}
|
|
3095
3196
|
const runId = rest[0];
|
|
3096
3197
|
const needsRunId = [
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aicopycoders/exodus",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.10.1200-beta.1",
|
|
4
4
|
"description": "CLI + Claude Code skills for the Copycoders Exodus ad pipeline",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
|
-
"url": "git+https://github.com/aicopycoders/exodus.git"
|
|
7
|
+
"url": "git+https://github.com/aicopycoders/exodus-cli.git"
|
|
8
8
|
},
|
|
9
|
-
"homepage": "https://github.com/aicopycoders/exodus#readme",
|
|
9
|
+
"homepage": "https://github.com/aicopycoders/exodus-cli#readme",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"bin": {
|
|
12
12
|
"exodus": "dist/bin/exodus.js"
|