@aicopycoders/exodus 2026.9.1900-beta.0 → 2026.9.1900-beta.2
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 +12 -0
- package/assets/skills/exodus-video/SKILL.md +189 -13
- package/assets/skills/exodus-workflow/SKILL.md +33 -1
- package/dist/bin/exodus.js +3 -36
- package/dist/commands/video.d.ts +20 -2
- package/dist/commands/video.js +263 -62
- package/dist/commands/workflow.d.ts +52 -8
- package/dist/commands/workflow.js +188 -96
- package/dist/lib/args.d.ts +10 -0
- package/dist/lib/args.js +39 -0
- package/package.json +1 -1
package/dist/commands/video.js
CHANGED
|
@@ -44,6 +44,12 @@ The whole loop, in order:
|
|
|
44
44
|
Redo ONE finished clip while the run waits for the cut. Everything
|
|
45
45
|
else stays. A --note steers the motion, never the words.
|
|
46
46
|
|
|
47
|
+
exodus video revoice <runId> --all
|
|
48
|
+
Redo only the VOICE on clips that kept the video model's own voice
|
|
49
|
+
(for example, the run had no ElevenLabs key when they were made). The
|
|
50
|
+
picture stays, no new video is made, and a clip whose voice pass
|
|
51
|
+
cannot finish is kept as it was. Use --scene <n> for one clip.
|
|
52
|
+
|
|
47
53
|
8. Make your cut from those files.
|
|
48
54
|
|
|
49
55
|
9. exodus video upload <runId> --file cut.mp4
|
|
@@ -61,6 +67,7 @@ Usage:
|
|
|
61
67
|
exodus video flag <runId> --note "<what is wrong>" [--json]
|
|
62
68
|
exodus video retry-frame <runId> --node <nodeId> --scene <n> [--note "..."] [--json]
|
|
63
69
|
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "..."] [--json]
|
|
70
|
+
exodus video revoice <runId> (--scene <n> | --all) [--node <nodeId>] [--json]
|
|
64
71
|
exodus video voices <runId> [--set <character>=<voiceId>] [--clear <character>] [--from <file.json>] [--json]
|
|
65
72
|
exodus video pull <runId> --out <dir> [--json]
|
|
66
73
|
exodus video upload <runId> --file <cut.mp4> [--duration <sec>] [--json]
|
|
@@ -80,9 +87,11 @@ Options:
|
|
|
80
87
|
steer one redo (retry-frame, retry-clip)
|
|
81
88
|
--node <nodeId> Which scene-frames node holds the still (retry-frame).
|
|
82
89
|
Which video step holds the clip, when a scene has one on
|
|
83
|
-
more than one step (retry-clip)
|
|
90
|
+
more than one step (retry-clip, revoice)
|
|
84
91
|
--scene <n> Which scene to redo. One scene only, a whole number
|
|
85
|
-
(retry-frame, retry-clip)
|
|
92
|
+
(retry-frame, retry-clip, revoice)
|
|
93
|
+
--all Every finished clip that kept the video model's voice
|
|
94
|
+
(revoice)
|
|
86
95
|
--set <who>=<id> Give a character an ElevenLabs voice (voices). Name the
|
|
87
96
|
character by its ID or by the name your script uses.
|
|
88
97
|
Repeat it once per character
|
|
@@ -102,6 +111,7 @@ Examples:
|
|
|
102
111
|
exodus video approve run_123
|
|
103
112
|
exodus video retry-frame run_123 --node frames-1 --scene 2
|
|
104
113
|
exodus video retry-clip run_123 --scene 3 --note "keep the handshake in frame"
|
|
114
|
+
exodus video revoice run_123 --all
|
|
105
115
|
exodus video voices run_123
|
|
106
116
|
exodus video voices run_123 --set C1=abc123voiceid --set "HOST 2=def456voiceid"
|
|
107
117
|
exodus video voices run_123 --from voices.json
|
|
@@ -116,6 +126,9 @@ const STORYBOARD_PATH = "/api/v2/video/storyboard";
|
|
|
116
126
|
const FLAG_PATH = "/api/v2/video/storyboard/flag";
|
|
117
127
|
const APPROVE_PATH = "/api/v2/workflow/approve";
|
|
118
128
|
const SCENE_RETRY_PATH = "/api/v2/workflow/scene/retry";
|
|
129
|
+
const SCENE_REVOICE_PATH = "/api/v2/workflow/scene/revoice";
|
|
130
|
+
const REVOICE_NOT_ON_THIS_SERVER = "This Exodus server does not have the voice redo yet, so nothing was started and nothing " +
|
|
131
|
+
"was spent. It arrives with the next server update.";
|
|
119
132
|
const FINAL_PATH = "/api/v2/video/final";
|
|
120
133
|
export const VOICES_PATH = "/api/v2/video/voices";
|
|
121
134
|
const ASSET_UPLOAD_URL_PATH = "/api/v2/workflows/asset-upload-url";
|
|
@@ -359,6 +372,7 @@ function clipFromArtifact(sceneIndex, artifact) {
|
|
|
359
372
|
words: artifact.words ?? null,
|
|
360
373
|
revoiced: artifact.revoiced === true,
|
|
361
374
|
speechTrimmed: artifact.speechTrimmed === true,
|
|
375
|
+
rawStorageId: artifact.rawStorageId ?? null,
|
|
362
376
|
};
|
|
363
377
|
}
|
|
364
378
|
function keyframeDownload(sceneIndex, imageUrl) {
|
|
@@ -393,10 +407,15 @@ function castVoicePins(artifact) {
|
|
|
393
407
|
for (const member of cast) {
|
|
394
408
|
const characterId = typeof member.characterId === "string" ? member.characterId.trim() : "";
|
|
395
409
|
const voiceId = typeof member.voiceId === "string" ? member.voiceId.trim() : "";
|
|
396
|
-
|
|
410
|
+
const description = typeof member.voiceDescription === "string" ? member.voiceDescription.trim() : "";
|
|
411
|
+
if (!characterId || (!voiceId && !description))
|
|
397
412
|
continue;
|
|
398
413
|
const label = typeof member.voiceLabel === "string" ? member.voiceLabel.trim() : "";
|
|
399
|
-
pins.set(characterId, {
|
|
414
|
+
pins.set(characterId, {
|
|
415
|
+
voiceId: voiceId ? voiceId : null,
|
|
416
|
+
voiceLabel: voiceId && label ? label : null,
|
|
417
|
+
voiceDescription: description ? description : null,
|
|
418
|
+
});
|
|
400
419
|
}
|
|
401
420
|
return pins;
|
|
402
421
|
}
|
|
@@ -473,6 +492,7 @@ function planCastRefs(run, items, pins) {
|
|
|
473
492
|
error: row.error,
|
|
474
493
|
voiceId: pin?.voiceId ?? null,
|
|
475
494
|
voiceLabel: pin?.voiceLabel ?? null,
|
|
495
|
+
voiceDescription: pin?.voiceDescription ?? null,
|
|
476
496
|
});
|
|
477
497
|
if (file && row.url)
|
|
478
498
|
downloads.push({ file, url: row.url });
|
|
@@ -653,6 +673,7 @@ export function planPull(run, items, opts) {
|
|
|
653
673
|
qc: clip?.qc ?? null,
|
|
654
674
|
revoiced: clip ? clip.revoiced : null,
|
|
655
675
|
speechTrimmed: clip ? clip.speechTrimmed : null,
|
|
676
|
+
rawStorageId: clip ? clip.rawStorageId : null,
|
|
656
677
|
clipStatus: item?.status ?? "missing",
|
|
657
678
|
error: item?.error ?? null,
|
|
658
679
|
flagged: item?.flagged === true,
|
|
@@ -686,6 +707,9 @@ export function planPull(run, items, opts) {
|
|
|
686
707
|
: null,
|
|
687
708
|
scenes,
|
|
688
709
|
failed: [],
|
|
710
|
+
...(run.provenance?.format || run.provenance?.voice
|
|
711
|
+
? { provenance: run.provenance }
|
|
712
|
+
: {}),
|
|
689
713
|
},
|
|
690
714
|
};
|
|
691
715
|
}
|
|
@@ -710,6 +734,7 @@ export function markPullFailure(manifest, failure) {
|
|
|
710
734
|
scene.clip = null;
|
|
711
735
|
scene.revoiced = null;
|
|
712
736
|
scene.speechTrimmed = null;
|
|
737
|
+
scene.rawStorageId = null;
|
|
713
738
|
}
|
|
714
739
|
if (scene.voice === failure.file)
|
|
715
740
|
scene.voice = null;
|
|
@@ -1146,17 +1171,45 @@ const UPLOADED_CUT_WARNING = "The cut you already uploaded will not include the
|
|
|
1146
1171
|
"re-cut, and upload again.";
|
|
1147
1172
|
export function planClipRedo(run, items, target) {
|
|
1148
1173
|
const stop = classifyRun(run);
|
|
1174
|
+
const found = findClipRow(run, items, target);
|
|
1175
|
+
if ("reason" in found)
|
|
1176
|
+
return { ok: false, reason: found.reason };
|
|
1177
|
+
const { row, uploadedCut } = found;
|
|
1178
|
+
const redoable = row.status === "failed" ||
|
|
1179
|
+
(row.status === "done" && (row.flagged === true || stop.at === "final-watch"));
|
|
1180
|
+
if (!redoable) {
|
|
1181
|
+
if (row.status === "done") {
|
|
1182
|
+
return {
|
|
1183
|
+
ok: false,
|
|
1184
|
+
reason: `Scene ${target.sceneIndex}'s clip is finished and nothing flagged it. A finished clip ` +
|
|
1185
|
+
"can only be redone while the run is waiting for the final cut.",
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
const word = ITEM_STATUS_WORD[row.status] ?? row.status;
|
|
1189
|
+
return {
|
|
1190
|
+
ok: false,
|
|
1191
|
+
reason: `Scene ${target.sceneIndex}'s clip is ${word}, so there is nothing to redo yet.`,
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
return {
|
|
1195
|
+
ok: true,
|
|
1196
|
+
nodeId: row.nodeId,
|
|
1197
|
+
sceneIndex: target.sceneIndex,
|
|
1198
|
+
attempt: row.attempt ?? 0,
|
|
1199
|
+
warnings: uploadedCut ? [UPLOADED_CUT_WARNING] : [],
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
function findClipRow(run, items, target) {
|
|
1149
1203
|
const uploadedCut = items.some((i) => i.itemKind === "final" && i.status === "done");
|
|
1150
1204
|
const rows = items.filter((i) => i.itemKind === "clip" &&
|
|
1151
1205
|
i.sceneIndex === target.sceneIndex &&
|
|
1152
1206
|
(target.nodeId === undefined || i.nodeId === target.nodeId));
|
|
1153
1207
|
if (rows.length === 0) {
|
|
1154
|
-
return {
|
|
1208
|
+
return { reason: `Scene ${target.sceneIndex} has no clip to redo.` };
|
|
1155
1209
|
}
|
|
1156
1210
|
const nodeIds = [...new Set(rows.map((r) => r.nodeId))];
|
|
1157
1211
|
if (nodeIds.length > 1) {
|
|
1158
1212
|
return {
|
|
1159
|
-
ok: false,
|
|
1160
1213
|
reason: `Scene ${target.sceneIndex} has a clip on more than one step (${nodeIds.join(", ")}), ` +
|
|
1161
1214
|
"so say which one: --node <nodeId>.",
|
|
1162
1215
|
};
|
|
@@ -1165,44 +1218,20 @@ export function planClipRedo(run, items, target) {
|
|
|
1165
1218
|
const node = run.nodes.find((n) => n.nodeId === row.nodeId);
|
|
1166
1219
|
if (node?.status === "running") {
|
|
1167
1220
|
return {
|
|
1168
|
-
ok: false,
|
|
1169
1221
|
reason: `The "${row.nodeId}" step is still making other scenes. Try again once it has finished ` +
|
|
1170
1222
|
`(exodus video status ${run._id}).`,
|
|
1171
1223
|
};
|
|
1172
1224
|
}
|
|
1173
1225
|
if (node && node.status !== "done") {
|
|
1174
1226
|
return {
|
|
1175
|
-
ok: false,
|
|
1176
1227
|
reason: `The "${row.nodeId}" step did not finish (it is "${node.status}"), ` +
|
|
1177
1228
|
"and one clip can only be redone on a step that finished.",
|
|
1178
1229
|
};
|
|
1179
1230
|
}
|
|
1180
1231
|
if (row.status === "running") {
|
|
1181
|
-
return {
|
|
1232
|
+
return { reason: `Scene ${target.sceneIndex} is already being redone.` };
|
|
1182
1233
|
}
|
|
1183
|
-
|
|
1184
|
-
(row.status === "done" && (row.flagged === true || stop.at === "final-watch"));
|
|
1185
|
-
if (!redoable) {
|
|
1186
|
-
if (row.status === "done") {
|
|
1187
|
-
return {
|
|
1188
|
-
ok: false,
|
|
1189
|
-
reason: `Scene ${target.sceneIndex}'s clip is finished and nothing flagged it. A finished clip ` +
|
|
1190
|
-
"can only be redone while the run is waiting for the final cut.",
|
|
1191
|
-
};
|
|
1192
|
-
}
|
|
1193
|
-
const word = ITEM_STATUS_WORD[row.status] ?? row.status;
|
|
1194
|
-
return {
|
|
1195
|
-
ok: false,
|
|
1196
|
-
reason: `Scene ${target.sceneIndex}'s clip is ${word}, so there is nothing to redo yet.`,
|
|
1197
|
-
};
|
|
1198
|
-
}
|
|
1199
|
-
return {
|
|
1200
|
-
ok: true,
|
|
1201
|
-
nodeId: row.nodeId,
|
|
1202
|
-
sceneIndex: target.sceneIndex,
|
|
1203
|
-
attempt: row.attempt ?? 0,
|
|
1204
|
-
warnings: uploadedCut ? [UPLOADED_CUT_WARNING] : [],
|
|
1205
|
-
};
|
|
1234
|
+
return { row, uploadedCut };
|
|
1206
1235
|
}
|
|
1207
1236
|
export async function retryClipFlow(runId, target, note, json, deps) {
|
|
1208
1237
|
const runRes = await deps.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
@@ -1263,24 +1292,169 @@ export async function retryClipFlow(runId, target, note, json, deps) {
|
|
|
1263
1292
|
],
|
|
1264
1293
|
};
|
|
1265
1294
|
}
|
|
1266
|
-
|
|
1295
|
+
function clipIsStillRaw(item) {
|
|
1296
|
+
const a = item.artifact;
|
|
1297
|
+
return (item.status === "done" &&
|
|
1298
|
+
a?.type === "video" &&
|
|
1299
|
+
a.revoiced !== true &&
|
|
1300
|
+
a.speechTrimmed !== true &&
|
|
1301
|
+
a.tailTrimmed !== true);
|
|
1302
|
+
}
|
|
1303
|
+
const VOICE_NOT_CHANGED_CODES = new Set(["voice-unpinned", "voice-not-applied"]);
|
|
1304
|
+
export function planClipRevoice(run, items, target) {
|
|
1305
|
+
const found = findClipRow(run, items, target);
|
|
1306
|
+
if ("reason" in found)
|
|
1307
|
+
return { ok: false, reason: found.reason };
|
|
1308
|
+
const { row, uploadedCut } = found;
|
|
1309
|
+
const remake = `To make the clip again from scratch: exodus video retry-clip ${run._id} --scene ${target.sceneIndex}`;
|
|
1310
|
+
if (row.status !== "done" || row.artifact?.type !== "video") {
|
|
1311
|
+
return {
|
|
1312
|
+
ok: false,
|
|
1313
|
+
reason: `Scene ${target.sceneIndex} has no finished clip, so there is no voice to redo. ${remake}`,
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
if (!clipIsStillRaw(row)) {
|
|
1317
|
+
return {
|
|
1318
|
+
ok: false,
|
|
1319
|
+
reason: `Scene ${target.sceneIndex}'s clip has already been through the voice pass, ` +
|
|
1320
|
+
`so it cannot go through it again. ${remake}`,
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
return {
|
|
1324
|
+
ok: true,
|
|
1325
|
+
nodeId: row.nodeId,
|
|
1326
|
+
sceneIndex: target.sceneIndex,
|
|
1327
|
+
attempt: row.attempt ?? 0,
|
|
1328
|
+
warnings: uploadedCut ? [UPLOADED_CUT_WARNING] : [],
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
function sceneListWords(sceneIndexes) {
|
|
1332
|
+
if (sceneIndexes.length === 1)
|
|
1333
|
+
return `Scene ${sceneIndexes[0]}`;
|
|
1334
|
+
return `Scenes ${sceneIndexes.slice(0, -1).join(", ")} and ${sceneIndexes[sceneIndexes.length - 1]}`;
|
|
1335
|
+
}
|
|
1336
|
+
export async function revoiceFlow(runId, target, json, deps) {
|
|
1337
|
+
const runRes = await deps.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
1338
|
+
if (!runRes.ok)
|
|
1339
|
+
return errorResult(runRes, json);
|
|
1340
|
+
const run = asVideoRun(runRes.data);
|
|
1341
|
+
const itemsRes = await deps.get(`${ITEMS_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
1342
|
+
if (!itemsRes.ok)
|
|
1343
|
+
return errorResult(itemsRes, json);
|
|
1344
|
+
const items = itemsRes.data.items ?? [];
|
|
1345
|
+
const targets = "all" in target
|
|
1346
|
+
? items
|
|
1347
|
+
.filter((i) => i.itemKind === "clip" &&
|
|
1348
|
+
clipIsStillRaw(i) &&
|
|
1349
|
+
(i.findings ?? []).some((f) => VOICE_NOT_CHANGED_CODES.has(f.code)))
|
|
1350
|
+
.sort((a, b) => a.sceneIndex - b.sceneIndex)
|
|
1351
|
+
.map((i) => ({ sceneIndex: i.sceneIndex, nodeId: i.nodeId }))
|
|
1352
|
+
: [target];
|
|
1353
|
+
if (targets.length === 0) {
|
|
1354
|
+
const nothing = "No clip on this run is waiting for a voice pass, so nothing was started.";
|
|
1355
|
+
return {
|
|
1356
|
+
code: 0,
|
|
1357
|
+
lines: json ? [JSON.stringify({ ok: true, runId, scenes: [], note: nothing })] : [nothing],
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
const scenes = [];
|
|
1361
|
+
const warnings = new Set();
|
|
1362
|
+
for (const one of targets) {
|
|
1363
|
+
const plan = planClipRevoice(run, items, one);
|
|
1364
|
+
if (!plan.ok) {
|
|
1365
|
+
scenes.push({ sceneIndex: one.sceneIndex, nodeId: one.nodeId, ok: false, error: plan.reason });
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
const res = await deps.post(SCENE_REVOICE_PATH, {
|
|
1369
|
+
runId,
|
|
1370
|
+
nodeId: plan.nodeId,
|
|
1371
|
+
sceneIndex: plan.sceneIndex,
|
|
1372
|
+
});
|
|
1373
|
+
if (missingRouteLine(res, "exodus video revoice")) {
|
|
1374
|
+
return {
|
|
1375
|
+
code: 1,
|
|
1376
|
+
lines: json
|
|
1377
|
+
? [JSON.stringify({ ok: false, status: 404, error: REVOICE_NOT_ON_THIS_SERVER })]
|
|
1378
|
+
: [REVOICE_NOT_ON_THIS_SERVER],
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
if (res.ok) {
|
|
1382
|
+
plan.warnings.forEach((w) => warnings.add(w));
|
|
1383
|
+
scenes.push({
|
|
1384
|
+
sceneIndex: plan.sceneIndex,
|
|
1385
|
+
nodeId: plan.nodeId,
|
|
1386
|
+
ok: true,
|
|
1387
|
+
triggerRunId: res.data.triggerRunId,
|
|
1388
|
+
});
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
scenes.push({
|
|
1392
|
+
sceneIndex: plan.sceneIndex,
|
|
1393
|
+
nodeId: plan.nodeId,
|
|
1394
|
+
ok: false,
|
|
1395
|
+
error: videoApiError(res),
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
const queued = scenes.filter((r) => r.ok).length;
|
|
1399
|
+
const code = scenes.every((r) => r.ok) ? 0 : 1;
|
|
1400
|
+
const review = reviewUrl(deps.dashboardUrl, run);
|
|
1401
|
+
if (json) {
|
|
1402
|
+
return {
|
|
1403
|
+
code,
|
|
1404
|
+
lines: [
|
|
1405
|
+
JSON.stringify({
|
|
1406
|
+
ok: code === 0,
|
|
1407
|
+
runId,
|
|
1408
|
+
scenes,
|
|
1409
|
+
reviewUrl: review,
|
|
1410
|
+
warnings: [...warnings],
|
|
1411
|
+
}),
|
|
1412
|
+
],
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
const refusedWith = new Map();
|
|
1416
|
+
for (const r of scenes) {
|
|
1417
|
+
if (!r.ok)
|
|
1418
|
+
refusedWith.set(r.error, [...(refusedWith.get(r.error) ?? []), r.sceneIndex]);
|
|
1419
|
+
}
|
|
1420
|
+
const sceneLines = scenes.flatMap((r) => {
|
|
1421
|
+
if (r.ok) {
|
|
1422
|
+
return [
|
|
1423
|
+
`Scene ${r.sceneIndex}: redoing the voice on ${r.nodeId}. triggerRunId: ${r.triggerRunId ?? "-"}`,
|
|
1424
|
+
];
|
|
1425
|
+
}
|
|
1426
|
+
const sharing = refusedWith.get(r.error) ?? [];
|
|
1427
|
+
if (sharing[0] !== r.sceneIndex)
|
|
1428
|
+
return [];
|
|
1429
|
+
return [`${sceneListWords(sharing)}: not started. ${r.error}`];
|
|
1430
|
+
});
|
|
1431
|
+
if (queued === 0)
|
|
1432
|
+
return { code, lines: sceneLines };
|
|
1433
|
+
return {
|
|
1434
|
+
code,
|
|
1435
|
+
lines: [
|
|
1436
|
+
...sceneLines,
|
|
1437
|
+
"The picture in each clip stays exactly as it is. No new video is made.",
|
|
1438
|
+
"The words are checked against the script, the clip is trimmed to the speech, and the chosen voice is applied. ElevenLabs bills that to your own key.",
|
|
1439
|
+
"If the voice pass cannot finish on a clip, that clip is kept as it was.",
|
|
1440
|
+
`Watch them land: exodus video status ${runId}`,
|
|
1441
|
+
`Review the run: ${review}`,
|
|
1442
|
+
...warnings,
|
|
1443
|
+
],
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
export function parseVoiceFlags(occurrences, readFile) {
|
|
1267
1447
|
const fromFile = {};
|
|
1268
1448
|
const typed = {};
|
|
1269
1449
|
let asked = false;
|
|
1270
|
-
const valueFor = (
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
const next = argv[i + 1];
|
|
1275
|
-
if (next === undefined)
|
|
1276
|
-
throw new Error(`${flag} needs a value.`);
|
|
1277
|
-
return [next, i + 1];
|
|
1450
|
+
const valueFor = (occurrence) => {
|
|
1451
|
+
if (occurrence.value === undefined)
|
|
1452
|
+
throw new Error(`--${occurrence.flag} needs a value.`);
|
|
1453
|
+
return occurrence.value;
|
|
1278
1454
|
};
|
|
1279
|
-
for (
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
const [raw, next] = valueFor(i, "--set");
|
|
1283
|
-
i = next;
|
|
1455
|
+
for (const occurrence of occurrences) {
|
|
1456
|
+
if (occurrence.flag === "set") {
|
|
1457
|
+
const raw = valueFor(occurrence);
|
|
1284
1458
|
const eq = raw.lastIndexOf("=");
|
|
1285
1459
|
if (eq <= 0 || eq === raw.length - 1) {
|
|
1286
1460
|
throw new Error("--set must look like <character>=<voice id>, for example " +
|
|
@@ -1289,17 +1463,15 @@ export function parseVoiceFlags(argv, readFile) {
|
|
|
1289
1463
|
typed[raw.slice(0, eq).trim()] = raw.slice(eq + 1).trim();
|
|
1290
1464
|
asked = true;
|
|
1291
1465
|
}
|
|
1292
|
-
else if (
|
|
1293
|
-
const
|
|
1294
|
-
i = next;
|
|
1466
|
+
else if (occurrence.flag === "clear") {
|
|
1467
|
+
const raw = valueFor(occurrence);
|
|
1295
1468
|
if (!raw.trim())
|
|
1296
1469
|
throw new Error("--clear needs a character, for example --clear C1.");
|
|
1297
1470
|
typed[raw.trim()] = null;
|
|
1298
1471
|
asked = true;
|
|
1299
1472
|
}
|
|
1300
|
-
else if (
|
|
1301
|
-
const
|
|
1302
|
-
i = next;
|
|
1473
|
+
else if (occurrence.flag === "from") {
|
|
1474
|
+
const file = valueFor(occurrence);
|
|
1303
1475
|
let parsed;
|
|
1304
1476
|
try {
|
|
1305
1477
|
parsed = JSON.parse(readFile(file));
|
|
@@ -1337,7 +1509,7 @@ export async function voicesFlow(runId, voices, json, deps) {
|
|
|
1337
1509
|
}
|
|
1338
1510
|
function availabilityWords(row) {
|
|
1339
1511
|
if (!row.voice)
|
|
1340
|
-
return "no voice yet";
|
|
1512
|
+
return row.description ? "voice written below" : "no voice yet";
|
|
1341
1513
|
switch (row.availability?.state) {
|
|
1342
1514
|
case "available":
|
|
1343
1515
|
return "ElevenLabs has it";
|
|
@@ -1349,10 +1521,12 @@ function availabilityWords(row) {
|
|
|
1349
1521
|
return "not checked";
|
|
1350
1522
|
}
|
|
1351
1523
|
}
|
|
1352
|
-
function voiceWords(
|
|
1524
|
+
function voiceWords(row) {
|
|
1525
|
+
const voice = row.voice;
|
|
1353
1526
|
if (!voice)
|
|
1354
1527
|
return "—";
|
|
1355
|
-
|
|
1528
|
+
const words = voice.label ? `${voice.label} (${voice.voiceId})` : voice.voiceId;
|
|
1529
|
+
return row.voiceFrom === "run-default" ? `${words}, the run's default voice` : words;
|
|
1356
1530
|
}
|
|
1357
1531
|
function voiceSummaryLines(cast, notices) {
|
|
1358
1532
|
const rows = cast.filter((row) => row.spokenScenes > 0 || row.voice !== null);
|
|
@@ -1360,7 +1534,7 @@ function voiceSummaryLines(cast, notices) {
|
|
|
1360
1534
|
const lines = rows.length > 0 ? ["Voices"] : [];
|
|
1361
1535
|
for (const row of rows) {
|
|
1362
1536
|
lines.push(row.voice
|
|
1363
|
-
? ` ${row.name.padEnd(width)} → ${voiceWords(row
|
|
1537
|
+
? ` ${row.name.padEnd(width)} → ${voiceWords(row)} ${availabilityWords(row)}`
|
|
1364
1538
|
: ` ${row.name.padEnd(width)} → no voice chosen (keeps the generated voice)`);
|
|
1365
1539
|
}
|
|
1366
1540
|
for (const notice of notices)
|
|
@@ -1369,12 +1543,17 @@ function voiceSummaryLines(cast, notices) {
|
|
|
1369
1543
|
}
|
|
1370
1544
|
export function voiceSheetLines(sheet) {
|
|
1371
1545
|
const lines = [`Voices for run ${sheet.runId}`];
|
|
1546
|
+
if (typeof sheet.treatment.path === "string" && sheet.treatment.path !== "") {
|
|
1547
|
+
lines.push(` Treatment: ${sheet.treatment.path}`);
|
|
1548
|
+
}
|
|
1372
1549
|
if (sheet.cast.length === 0)
|
|
1373
1550
|
lines.push(" Nobody is in this ad yet.");
|
|
1374
1551
|
for (const row of sheet.cast) {
|
|
1375
|
-
const voice = voiceWords(row
|
|
1552
|
+
const voice = voiceWords(row);
|
|
1376
1553
|
lines.push(` ${row.characterId} ${row.name} ${voice} ${availabilityWords(row)} ` +
|
|
1377
1554
|
`speaks in ${row.spokenScenes} scenes, about ${row.spokenSeconds}s`);
|
|
1555
|
+
if (row.description)
|
|
1556
|
+
lines.push(` written voice: ${row.description}`);
|
|
1378
1557
|
}
|
|
1379
1558
|
if (sheet.narrator) {
|
|
1380
1559
|
const label = sheet.narrator.label ? `${sheet.narrator.label} ` : "";
|
|
@@ -1386,9 +1565,16 @@ export function voiceSheetLines(sheet) {
|
|
|
1386
1565
|
: "Nothing changed — those voices were already set. Pictures and script were not touched.");
|
|
1387
1566
|
}
|
|
1388
1567
|
lines.push(`How voices are applied: ${sheet.treatment.summary}${sheet.treatment.speedChange ? "" : " No speed change."}`);
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1568
|
+
const rendersOwnVoice = sheet.treatment.kind === "render-owns-voice";
|
|
1569
|
+
if (!rendersOwnVoice || sheet.treatment.usesElevenLabs === undefined) {
|
|
1570
|
+
lines.push(`Cost: billed to your own ElevenLabs key. ${sheet.treatment.costNote} ` +
|
|
1571
|
+
`About ${sheet.treatment.usage.seconds} seconds of speech across ` +
|
|
1572
|
+
`${sheet.treatment.usage.clips} clips will be converted.`);
|
|
1573
|
+
}
|
|
1574
|
+
else if (sheet.treatment.usesElevenLabs) {
|
|
1575
|
+
lines.push("Cost: the narration on this run is voiced by ElevenLabs and billed to your own key. " +
|
|
1576
|
+
`${sheet.treatment.costNote} The clips themselves are not converted.`);
|
|
1577
|
+
}
|
|
1392
1578
|
for (const notice of sheet.notices)
|
|
1393
1579
|
lines.push(notice);
|
|
1394
1580
|
lines.push(sheet.canChange
|
|
@@ -1738,7 +1924,7 @@ function usage(line) {
|
|
|
1738
1924
|
console.error(`Error: ${line}`);
|
|
1739
1925
|
process.exit(1);
|
|
1740
1926
|
}
|
|
1741
|
-
export async function run(flags) {
|
|
1927
|
+
export async function run(flags, occurrences) {
|
|
1742
1928
|
const [sub, ...rest] = parsePositional();
|
|
1743
1929
|
const json = flags["json"] === true;
|
|
1744
1930
|
if (!sub) {
|
|
@@ -1771,6 +1957,7 @@ export async function run(flags) {
|
|
|
1771
1957
|
"flag",
|
|
1772
1958
|
"retry-frame",
|
|
1773
1959
|
"retry-clip",
|
|
1960
|
+
"revoice",
|
|
1774
1961
|
"voices",
|
|
1775
1962
|
"pull",
|
|
1776
1963
|
"upload",
|
|
@@ -1815,10 +2002,24 @@ export async function run(flags) {
|
|
|
1815
2002
|
}
|
|
1816
2003
|
return printResult(await retryClipFlow(runId, { sceneIndex, nodeId: flagString(flags, "node") }, flagString(flags, "note"), json, defaultDeps));
|
|
1817
2004
|
}
|
|
2005
|
+
if (sub === "revoice") {
|
|
2006
|
+
const sceneRaw = flagString(flags, "scene");
|
|
2007
|
+
const all = flags["all"] === true;
|
|
2008
|
+
if (all === (sceneRaw !== undefined)) {
|
|
2009
|
+
usage("video revoice needs one of --scene <n> (one clip) or --all (every clip that kept the video model's voice).");
|
|
2010
|
+
}
|
|
2011
|
+
if (all)
|
|
2012
|
+
return printResult(await revoiceFlow(runId, { all: true }, json, defaultDeps));
|
|
2013
|
+
const sceneIndex = Number(sceneRaw);
|
|
2014
|
+
if (!Number.isInteger(sceneIndex)) {
|
|
2015
|
+
usage(`video revoice --scene must be a whole scene number, not "${sceneRaw}".`);
|
|
2016
|
+
}
|
|
2017
|
+
return printResult(await revoiceFlow(runId, { sceneIndex, nodeId: flagString(flags, "node") }, json, defaultDeps));
|
|
2018
|
+
}
|
|
1818
2019
|
if (sub === "voices") {
|
|
1819
2020
|
let voices;
|
|
1820
2021
|
try {
|
|
1821
|
-
voices = parseVoiceFlags(
|
|
2022
|
+
voices = parseVoiceFlags(occurrences, defaultDeps.readFile);
|
|
1822
2023
|
}
|
|
1823
2024
|
catch (err) {
|
|
1824
2025
|
usage(err.message);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ApiResponse } from "../lib/client.js";
|
|
2
|
+
import type { FlagOccurrence } from "../lib/args.js";
|
|
2
3
|
import { type PollOptions, type PollResult } from "../lib/poll.js";
|
|
3
4
|
import { LEGACY_WORKFLOW_RUN_STATUS_VALUES, type RunStatus } from "../lib/runStatus.js";
|
|
4
5
|
import { type RunDeliverySummary } from "../lib/runVerdict.js";
|
|
@@ -289,6 +290,11 @@ export interface WorkflowRunOutput {
|
|
|
289
290
|
durationSec?: number;
|
|
290
291
|
sceneIndex?: number;
|
|
291
292
|
final?: boolean;
|
|
293
|
+
words?: Array<{
|
|
294
|
+
w: string;
|
|
295
|
+
s: number;
|
|
296
|
+
e: number;
|
|
297
|
+
}>;
|
|
292
298
|
frames?: Array<{
|
|
293
299
|
sceneIndex: number;
|
|
294
300
|
imageUrl?: string;
|
|
@@ -346,6 +352,36 @@ export interface WorkflowPendingSlot {
|
|
|
346
352
|
value?: string;
|
|
347
353
|
hint?: string;
|
|
348
354
|
}
|
|
355
|
+
export interface RunFormatProvenance {
|
|
356
|
+
via: "show" | "rig-node";
|
|
357
|
+
nodeId?: string;
|
|
358
|
+
rigId: string;
|
|
359
|
+
rigName: string;
|
|
360
|
+
rulesFrom?: {
|
|
361
|
+
rigId: string;
|
|
362
|
+
rigName: string;
|
|
363
|
+
};
|
|
364
|
+
specVersion: string | null;
|
|
365
|
+
}
|
|
366
|
+
export type VoicePath = "sts-revoice" | "lipsync-retarget" | "native-prompt" | "omni-audio-ids" | "gemini-direct";
|
|
367
|
+
export type RunVoiceProvenance = {
|
|
368
|
+
source: "show";
|
|
369
|
+
path: VoicePath;
|
|
370
|
+
} | {
|
|
371
|
+
source: "launch";
|
|
372
|
+
path: VoicePath;
|
|
373
|
+
descriptions: {
|
|
374
|
+
speaker: string;
|
|
375
|
+
description: string;
|
|
376
|
+
}[];
|
|
377
|
+
} | {
|
|
378
|
+
source: "unreadable";
|
|
379
|
+
};
|
|
380
|
+
export interface RunProvenance {
|
|
381
|
+
format: RunFormatProvenance | null;
|
|
382
|
+
voice: RunVoiceProvenance | null;
|
|
383
|
+
}
|
|
384
|
+
export declare function formatRulesLine(provenance: unknown): string | undefined;
|
|
349
385
|
export interface WorkflowRun {
|
|
350
386
|
_id: string;
|
|
351
387
|
workflowId: string;
|
|
@@ -371,6 +407,7 @@ export interface WorkflowRun {
|
|
|
371
407
|
nodeId: string;
|
|
372
408
|
approvedAt: number;
|
|
373
409
|
}[];
|
|
410
|
+
provenance?: RunProvenance;
|
|
374
411
|
}
|
|
375
412
|
export type WorkflowRunProjection = Omit<WorkflowRun, "nodes"> & {
|
|
376
413
|
nodes?: never;
|
|
@@ -409,6 +446,7 @@ interface RunFlowOptions {
|
|
|
409
446
|
autoApprove?: boolean;
|
|
410
447
|
imageRigOverrides?: Record<string, unknown>;
|
|
411
448
|
voices?: VoiceMap;
|
|
449
|
+
voiceTreatment?: VoiceTreatmentBody;
|
|
412
450
|
wait: boolean;
|
|
413
451
|
json: boolean;
|
|
414
452
|
out?: string;
|
|
@@ -416,8 +454,8 @@ interface RunFlowOptions {
|
|
|
416
454
|
onWarningLine?: (line: string) => void;
|
|
417
455
|
}
|
|
418
456
|
export declare function formatPauseNotice(pauseReason: WorkflowPauseReason | undefined, runId: string, dashboardUrl: string): string[];
|
|
419
|
-
export declare function parseRawInputFlags(
|
|
420
|
-
export declare function parseInputFlags(
|
|
457
|
+
export declare function parseRawInputFlags(occurrences: FlagOccurrence[]): Record<string, string>;
|
|
458
|
+
export declare function parseInputFlags(occurrences: FlagOccurrence[], readFile?: (path: string) => string): Record<string, string>;
|
|
421
459
|
export declare const ASSET_UPLOAD_POLICY: Record<WorkflowMediaType, {
|
|
422
460
|
mimeByExtension: Record<string, string>;
|
|
423
461
|
maxBytes: number;
|
|
@@ -426,11 +464,16 @@ export declare const ASSET_UPLOAD_POLICY: Record<WorkflowMediaType, {
|
|
|
426
464
|
export declare const NO_DELIVERIES_WARNING: string;
|
|
427
465
|
export declare function noDeliveriesWarning(described: unknown): string | undefined;
|
|
428
466
|
export declare function serverWarningsToPrint(serverWarnings: unknown, alreadyPrinted: string[]): string[];
|
|
429
|
-
export declare function rejectTerminalFlag(
|
|
467
|
+
export declare function rejectTerminalFlag(occurrences: FlagOccurrence[]): void;
|
|
430
468
|
export declare function parseAutoApproveFlag(args: string[]): boolean;
|
|
431
|
-
export declare function parseRigOverridesFlag(
|
|
432
|
-
export declare function parseVoicesFlag(
|
|
433
|
-
export
|
|
469
|
+
export declare function parseRigOverridesFlag(occurrences: FlagOccurrence[], readFile?: (path: string) => string): Record<string, unknown> | undefined;
|
|
470
|
+
export declare function parseVoicesFlag(occurrences: FlagOccurrence[], readFile?: (path: string) => string): VoiceMap | undefined;
|
|
471
|
+
export interface VoiceTreatmentBody {
|
|
472
|
+
path: string;
|
|
473
|
+
describe?: Record<string, string>;
|
|
474
|
+
}
|
|
475
|
+
export declare function parseVoiceTreatmentFlag(occurrences: FlagOccurrence[], readFile?: (path: string) => string): VoiceTreatmentBody | undefined;
|
|
476
|
+
export declare function parseFillFlag(occurrences: FlagOccurrence[]): string | undefined;
|
|
434
477
|
export declare function formatWorkflowList(workflows: WorkflowListItem[]): string;
|
|
435
478
|
export declare function formatRecentRuns(runs: WorkflowRunProjection[]): string;
|
|
436
479
|
export declare function formatWorkflowVersions(versions: WorkflowVersion[]): string;
|
|
@@ -463,6 +506,7 @@ export declare function botsFlow(opts: {
|
|
|
463
506
|
slug?: string;
|
|
464
507
|
json: boolean;
|
|
465
508
|
}, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
509
|
+
export declare function voiceTreatmentLine(provenance: unknown): string | undefined;
|
|
466
510
|
export declare function runFlow(workflowRef: string, opts: RunFlowOptions, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
467
511
|
export declare function statusFlow(opts: {
|
|
468
512
|
id?: string;
|
|
@@ -586,9 +630,9 @@ export declare function repairFlow(runId: string, action: "retry" | "skip" | "ki
|
|
|
586
630
|
json: boolean;
|
|
587
631
|
onProgressLine?: (line: string) => void;
|
|
588
632
|
}, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
589
|
-
export declare function parseSlotFlags(
|
|
633
|
+
export declare function parseSlotFlags(occurrences: FlagOccurrence[]): Record<string, string>;
|
|
590
634
|
export declare function answerFlow(runId: string, values: Record<string, string>, opts: {
|
|
591
635
|
json: boolean;
|
|
592
636
|
}, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
593
|
-
export declare function run(flags: Record<string, string | boolean
|
|
637
|
+
export declare function run(flags: Record<string, string | boolean>, occurrences: FlagOccurrence[]): Promise<void>;
|
|
594
638
|
export {};
|