@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.
@@ -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
- if (!characterId || !voiceId)
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, { voiceId, voiceLabel: label ? label : null });
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 { ok: false, reason: `Scene ${target.sceneIndex} has no clip to redo.` };
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 { ok: false, reason: `Scene ${target.sceneIndex} is already being redone.` };
1232
+ return { reason: `Scene ${target.sceneIndex} is already being redone.` };
1182
1233
  }
1183
- const redoable = row.status === "failed" ||
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
- export function parseVoiceFlags(argv, readFile) {
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 = (i, flag) => {
1271
- const arg = argv[i];
1272
- if (arg.startsWith(`${flag}=`))
1273
- return [arg.slice(flag.length + 1), i];
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 (let i = 0; i < argv.length; i++) {
1280
- const arg = argv[i];
1281
- if (arg === "--set" || arg.startsWith("--set=")) {
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 (arg === "--clear" || arg.startsWith("--clear=")) {
1293
- const [raw, next] = valueFor(i, "--clear");
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 (arg === "--from" || arg.startsWith("--from=")) {
1301
- const [file, next] = valueFor(i, "--from");
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(voice) {
1524
+ function voiceWords(row) {
1525
+ const voice = row.voice;
1353
1526
  if (!voice)
1354
1527
  return "—";
1355
- return voice.label ? `${voice.label} (${voice.voiceId})` : voice.voiceId;
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.voice)} ${availabilityWords(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.voice);
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
- lines.push(`Cost: billed to your own ElevenLabs key. ${sheet.treatment.costNote} ` +
1390
- `About ${sheet.treatment.usage.seconds} seconds of speech across ` +
1391
- `${sheet.treatment.usage.clips} clips will be converted.`);
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(process.argv.slice(3), defaultDeps.readFile);
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(args: string[]): Record<string, string>;
420
- export declare function parseInputFlags(args: string[], readFile?: (path: string) => string): Record<string, string>;
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(args: string[]): void;
467
+ export declare function rejectTerminalFlag(occurrences: FlagOccurrence[]): void;
430
468
  export declare function parseAutoApproveFlag(args: string[]): boolean;
431
- export declare function parseRigOverridesFlag(args: string[], readFile?: (path: string) => string): Record<string, unknown> | undefined;
432
- export declare function parseVoicesFlag(args: string[], readFile?: (path: string) => string): VoiceMap | undefined;
433
- export declare function parseFillFlag(args: string[]): string | undefined;
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(args: string[]): Record<string, string>;
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>): Promise<void>;
637
+ export declare function run(flags: Record<string, string | boolean>, occurrences: FlagOccurrence[]): Promise<void>;
594
638
  export {};