@aicopycoders/exodus 2026.9.2000-beta.10 → 2026.9.2000-beta.12
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.
|
@@ -101,7 +101,7 @@ exodus video approve <runId> keep going
|
|
|
101
101
|
exodus video retry-frame <runId> --node <nodeId> --scene <n> redo one still at the pixel gate
|
|
102
102
|
exodus video status <runId> where it is, per scene
|
|
103
103
|
exodus video pull <runId> --out ./ad-<runId> every piece + manifest.json
|
|
104
|
-
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "…"]
|
|
104
|
+
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "…"] [--voice-first]
|
|
105
105
|
redo one clip at the final watch
|
|
106
106
|
exodus video revoice <runId> --all | --scene <n> redo only the voice; no new video
|
|
107
107
|
… make the cut (your choice of tool)
|
|
@@ -138,7 +138,11 @@ under it:
|
|
|
138
138
|
lines under `guidance`). It is also
|
|
139
139
|
the one park where a finished clip can be redone, with `retry-clip`. A run
|
|
140
140
|
only reaches this park if its video node sets `finalWatch: true` — otherwise
|
|
141
|
-
it skips straight to finished.
|
|
141
|
+
it skips straight to finished. If a scene has no clip, the stop block names
|
|
142
|
+
it instead (`stop.missingScenes` in `--json`) and approval is refused until
|
|
143
|
+
it has one. A scene whose picture the video service refused needs
|
|
144
|
+
`retry-frame --note` before `retry-clip`; the clip redo alone resends the
|
|
145
|
+
refused picture.
|
|
142
146
|
- paused: a builder checkpoint neither video verb resolves —
|
|
143
147
|
`exodus workflow checkpoint <runId>` is the verb for that one.
|
|
144
148
|
- failed: `status` shows how far it got, and a `--wait` that was running exits
|
|
@@ -602,6 +606,9 @@ scenes[] one per scene, in order
|
|
|
602
606
|
on record when the voice pass replaced the
|
|
603
607
|
clip with a new file; null when the clip IS
|
|
604
608
|
that file, or there is no clip
|
|
609
|
+
voiceMode "voice-first" when the clip was redone voice
|
|
610
|
+
first (its line recorded, then the clip made
|
|
611
|
+
to match it); null otherwise
|
|
605
612
|
provenance which saved rulebook this run followed:
|
|
606
613
|
{format: {via, nodeId, rigId, rigName,
|
|
607
614
|
rulesFrom, specVersion}}. Absent when the
|
|
@@ -746,6 +753,7 @@ is no second run, no new storyboard and no re-render of anything else.
|
|
|
746
753
|
exodus video retry-clip <runId> --scene 3
|
|
747
754
|
exodus video retry-clip <runId> --scene 3 --note "keep both hands in frame on the handshake"
|
|
748
755
|
exodus video retry-clip <runId> --scene 3 --node video-2
|
|
756
|
+
exodus video retry-clip <runId> --scene 3 --voice-first
|
|
749
757
|
```
|
|
750
758
|
|
|
751
759
|
`--node` is only needed when a run has clips for that scene on two video steps;
|
|
@@ -768,7 +776,17 @@ revoiced and trimmed the same way, with fresh findings and fresh word timings.
|
|
|
768
776
|
scene's motion prompt only (`withRedoDirection`,
|
|
769
777
|
`scout/src/trigger/workflow-scene-retry.ts`); the spoken line still comes from
|
|
770
778
|
the approved storyboard. Asking for different words in a note will not change
|
|
771
|
-
them. If the words are wrong, that is a new run.
|
|
779
|
+
them. If the script's words are wrong, that is a new run.
|
|
780
|
+
|
|
781
|
+
**`--voice-first` fixes a clip that said its line wrong** (#2299). It is for a
|
|
782
|
+
clip whose findings include `speech-mismatch`, `speech-repeat`,
|
|
783
|
+
`speech-after-line` or `speech-long-pause`. The scene's line is recorded in each
|
|
784
|
+
speaker's pinned ElevenLabs voice, and MiniMax (`minimax-h3/reference-to-video`)
|
|
785
|
+
makes the clip to match the recording. Only that clip changes; the word check
|
|
786
|
+
and the other checks still run on it. It needs an ElevenLabs key and a voice for
|
|
787
|
+
every speaker in the scene, and it is refused for a scene with no spoken line.
|
|
788
|
+
The clip then reads `clip: redone voice first` in `status`, carries
|
|
789
|
+
`voiceMode: "voice-first"` in the manifest, and is listed in the `pull` summary.
|
|
772
790
|
|
|
773
791
|
**A redo whose take fails the checks keeps the original clip.** On a finished
|
|
774
792
|
clip the row stays `done` with the ORIGINAL take and only its findings are
|
package/dist/commands/video.d.ts
CHANGED
|
@@ -74,6 +74,7 @@ export type ArtifactSubset = {
|
|
|
74
74
|
speechTrimmed?: boolean;
|
|
75
75
|
tailTrimmed?: boolean;
|
|
76
76
|
rawStorageId?: string;
|
|
77
|
+
voiceMode?: "voice-first";
|
|
77
78
|
} | {
|
|
78
79
|
type: "audio";
|
|
79
80
|
sceneIndex?: number;
|
|
@@ -114,6 +115,11 @@ export interface VideoRunNode {
|
|
|
114
115
|
planFailure?: PlanFailureRecord;
|
|
115
116
|
hasRejectedDraft?: boolean;
|
|
116
117
|
outputs?: ArtifactSubset[];
|
|
118
|
+
missingScenes?: MissingScene[];
|
|
119
|
+
}
|
|
120
|
+
export interface MissingScene {
|
|
121
|
+
sceneIndex: number;
|
|
122
|
+
pictureRefused: boolean;
|
|
117
123
|
}
|
|
118
124
|
export type BuilderPauseReason = "taste" | "repair" | "slots" | "call" | "checkpoint";
|
|
119
125
|
export interface PullCastLockMember {
|
|
@@ -161,6 +167,7 @@ export interface NodeItem {
|
|
|
161
167
|
take: number;
|
|
162
168
|
outcome: string;
|
|
163
169
|
label: string;
|
|
170
|
+
reason?: string;
|
|
164
171
|
displacedHistory?: {
|
|
165
172
|
title: string;
|
|
166
173
|
lines: string[];
|
|
@@ -220,6 +227,8 @@ export type RunStop = {
|
|
|
220
227
|
showAd?: true;
|
|
221
228
|
} | {
|
|
222
229
|
at: "final-watch";
|
|
230
|
+
missingScenes?: MissingScene[];
|
|
231
|
+
framesNodeId?: string;
|
|
223
232
|
} | {
|
|
224
233
|
at: "paused";
|
|
225
234
|
nodeId?: string;
|
|
@@ -236,10 +245,11 @@ export type RunStop = {
|
|
|
236
245
|
};
|
|
237
246
|
export type ResolvedStop = Exclude<RunStop, {
|
|
238
247
|
at: "final-watch";
|
|
239
|
-
}> | {
|
|
248
|
+
}> | (Extract<RunStop, {
|
|
240
249
|
at: "final-watch";
|
|
250
|
+
}> & {
|
|
241
251
|
cutAttached: boolean | null;
|
|
242
|
-
};
|
|
252
|
+
});
|
|
243
253
|
export declare function classifyRun(run: VideoRun): RunStop;
|
|
244
254
|
export declare function hasAttachedCut(items: NodeItem[]): boolean;
|
|
245
255
|
export declare function resolveStop(stop: RunStop, cutAttached: boolean | null): ResolvedStop;
|
|
@@ -273,6 +283,7 @@ export interface ManifestScene {
|
|
|
273
283
|
revoiced: boolean | null;
|
|
274
284
|
speechTrimmed: boolean | null;
|
|
275
285
|
rawStorageId: string | null;
|
|
286
|
+
voiceMode: "voice-first" | null;
|
|
276
287
|
clipStatus: string;
|
|
277
288
|
error: string | null;
|
|
278
289
|
flagged: boolean;
|
|
@@ -411,7 +422,11 @@ export interface ClipRedoTarget {
|
|
|
411
422
|
nodeId?: string;
|
|
412
423
|
}
|
|
413
424
|
export declare function planClipRedo(run: VideoRun, items: NodeItem[], target: ClipRedoTarget): ClipRedoPlan;
|
|
414
|
-
export
|
|
425
|
+
export interface ClipRedoOptions {
|
|
426
|
+
note?: string;
|
|
427
|
+
voiceFirst?: boolean;
|
|
428
|
+
}
|
|
429
|
+
export declare function retryClipFlow(runId: string, target: ClipRedoTarget, { note, voiceFirst }: ClipRedoOptions, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
415
430
|
export declare const VOICE_PATHS_NEVER_HEARD: Set<string>;
|
|
416
431
|
export declare function planClipRevoice(run: VideoRun, items: NodeItem[], target: ClipRedoTarget): ClipRedoPlan;
|
|
417
432
|
export type RevoiceTarget = ClipRedoTarget | {
|
package/dist/commands/video.js
CHANGED
|
@@ -70,7 +70,9 @@ The whole loop, in order:
|
|
|
70
70
|
|
|
71
71
|
exodus video retry-clip <runId> --scene <n>
|
|
72
72
|
Redo ONE finished clip while the run waits for the cut. Everything
|
|
73
|
-
else stays. A --note steers the motion, never the words.
|
|
73
|
+
else stays. A --note steers the motion, never the words. Add
|
|
74
|
+
--voice-first when the words came out wrong: the line is recorded in
|
|
75
|
+
the character's own voice first, then the clip is made to match it.
|
|
74
76
|
|
|
75
77
|
exodus video revoice <runId> --all
|
|
76
78
|
Redo only the VOICE on clips that kept the video model's own voice
|
|
@@ -93,7 +95,7 @@ Usage:
|
|
|
93
95
|
exodus video storyboard <runId> [--json]
|
|
94
96
|
exodus video approve <runId> [--approve-stale-cut] [--json]
|
|
95
97
|
exodus video retry-frame <runId> --node <nodeId> --scene <n> [--note "..."] [--json]
|
|
96
|
-
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "..."] [--json]
|
|
98
|
+
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "..."] [--voice-first] [--json]
|
|
97
99
|
exodus video revoice <runId> (--scene <n> | --all) [--node <nodeId>] [--json]
|
|
98
100
|
exodus video voices <runId> [--set <character>=<voiceId>] [--clear <character>] [--from <file.json>] [--json]
|
|
99
101
|
exodus video pull <runId> --out <dir> [--json]
|
|
@@ -113,6 +115,9 @@ Options:
|
|
|
113
115
|
(retry-frame, retry-clip, revoice)
|
|
114
116
|
--all Every finished clip that kept the video model's voice
|
|
115
117
|
(revoice)
|
|
118
|
+
--voice-first Record the scene's line in the character's own voice,
|
|
119
|
+
then make the clip to match it (retry-clip). For a clip
|
|
120
|
+
whose words came out wrong
|
|
116
121
|
--approve-stale-cut Deliver the cut you uploaded even though a clip changed
|
|
117
122
|
after you uploaded it (approve). Without it, approving
|
|
118
123
|
stops and names the scenes that changed
|
|
@@ -151,6 +156,7 @@ Examples:
|
|
|
151
156
|
exodus video approve run_123
|
|
152
157
|
exodus video retry-frame run_123 --node frames-1 --scene 2
|
|
153
158
|
exodus video retry-clip run_123 --scene 3 --note "keep the handshake in frame"
|
|
159
|
+
exodus video retry-clip run_123 --scene 3 --voice-first
|
|
154
160
|
exodus video revoice run_123 --all
|
|
155
161
|
exodus video voices run_123
|
|
156
162
|
exodus video voices run_123 --set C1=abc123voiceid --set "HOST 2=def456voiceid"
|
|
@@ -171,6 +177,9 @@ const FLAG_PATH = "/api/v2/video/storyboard/flag";
|
|
|
171
177
|
const APPROVE_PATH = "/api/v2/workflow/approve";
|
|
172
178
|
const SCENE_RETRY_PATH = "/api/v2/workflow/scene/retry";
|
|
173
179
|
const SCENE_REVOICE_PATH = "/api/v2/workflow/scene/revoice";
|
|
180
|
+
const SCENE_VOICE_FIRST_PATH = "/api/v2/workflow/scene/voice-first";
|
|
181
|
+
const VOICE_FIRST_NOT_ON_THIS_SERVER = "This Exodus server cannot redo a clip voice first yet, so nothing was started and nothing " +
|
|
182
|
+
"was spent. It arrives with the next server update.";
|
|
174
183
|
const REVOICE_NOT_ON_THIS_SERVER = "This Exodus server does not have the voice redo yet, so nothing was started and nothing " +
|
|
175
184
|
"was spent. It arrives with the next server update.";
|
|
176
185
|
const FINAL_PATH = "/api/v2/video/final";
|
|
@@ -214,6 +223,9 @@ export function renderQcTakeHistory(takes) {
|
|
|
214
223
|
});
|
|
215
224
|
return lines;
|
|
216
225
|
}
|
|
226
|
+
function redoLine(lastRedo) {
|
|
227
|
+
return lastRedo.reason ? `${lastRedo.label}: ${lastRedo.reason}` : lastRedo.label;
|
|
228
|
+
}
|
|
217
229
|
function printDisplacedHistory(lines, lastRedo) {
|
|
218
230
|
const history = lastRedo.displacedHistory;
|
|
219
231
|
if (!history)
|
|
@@ -337,8 +349,17 @@ export function classifyRun(run) {
|
|
|
337
349
|
...(isShowAd(run) ? { showAd: true } : {}),
|
|
338
350
|
};
|
|
339
351
|
}
|
|
340
|
-
if (parkedAtFinalWatch(run))
|
|
341
|
-
|
|
352
|
+
if (parkedAtFinalWatch(run)) {
|
|
353
|
+
const missingScenes = run.nodes
|
|
354
|
+
.filter((n) => n.kind === "video")
|
|
355
|
+
.flatMap((n) => n.missingScenes ?? []);
|
|
356
|
+
const framesNodeId = run.nodes.find((n) => n.kind === "scene-frames")?.nodeId;
|
|
357
|
+
return {
|
|
358
|
+
at: "final-watch",
|
|
359
|
+
...(missingScenes.length > 0 ? { missingScenes } : {}),
|
|
360
|
+
...(missingScenes.length > 0 && framesNodeId ? { framesNodeId } : {}),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
342
363
|
return { at: "paused", nodeId: run.pausedNodeId, reason: run.pauseReason };
|
|
343
364
|
}
|
|
344
365
|
const active = run.nodes.find((n) => n.status === "running");
|
|
@@ -350,7 +371,7 @@ export function hasAttachedCut(items) {
|
|
|
350
371
|
export function resolveStop(stop, cutAttached) {
|
|
351
372
|
if (stop.at !== "final-watch")
|
|
352
373
|
return stop;
|
|
353
|
-
return {
|
|
374
|
+
return { ...stop, cutAttached };
|
|
354
375
|
}
|
|
355
376
|
export async function resolveStopAtPark(stop, runId, deps) {
|
|
356
377
|
if (stop.at !== "final-watch")
|
|
@@ -395,6 +416,30 @@ export function reviewUrl(dashboardUrl, run) {
|
|
|
395
416
|
return `${dashboardUrl}/workflows/${run.workflowId}/runs/${run._id}`;
|
|
396
417
|
return `${dashboardUrl}/runs/${run._id}`;
|
|
397
418
|
}
|
|
419
|
+
function scenesPhrase(scenes) {
|
|
420
|
+
if (scenes.length === 1)
|
|
421
|
+
return `scene ${scenes[0]}`;
|
|
422
|
+
return `scenes ${scenes.slice(0, -1).join(", ")} and ${scenes[scenes.length - 1]}`;
|
|
423
|
+
}
|
|
424
|
+
function missingSceneLines(missing, framesNodeId, runId, runUrl) {
|
|
425
|
+
const scenes = missing.map((m) => m.sceneIndex);
|
|
426
|
+
const lines = [
|
|
427
|
+
`Parked: ${scenesPhrase(scenes)} ${scenes.length === 1 ? "has" : "have"} no clip yet, so the ad isn't complete.`,
|
|
428
|
+
];
|
|
429
|
+
for (const { sceneIndex, pictureRefused } of missing) {
|
|
430
|
+
if (!pictureRefused) {
|
|
431
|
+
lines.push(`Redo clip ${sceneIndex}: exodus video retry-clip ${runId} --scene ${sceneIndex}`);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
lines.push(`Scene ${sceneIndex}'s picture was refused by the video service as too close to an existing character, so redraw it before redoing the clip.`);
|
|
435
|
+
lines.push(framesNodeId
|
|
436
|
+
? `Redraw frame ${sceneIndex}: exodus video retry-frame ${runId} --node ${framesNodeId} --scene ${sceneIndex} --note "<what to change>"`
|
|
437
|
+
: `Redraw frame ${sceneIndex}: on the run page, ${runUrl}`);
|
|
438
|
+
lines.push(`Then redo clip ${sceneIndex}: exodus video retry-clip ${runId} --scene ${sceneIndex}`);
|
|
439
|
+
}
|
|
440
|
+
lines.push("The ad can't be approved until every scene has a clip.", `Watch it here: ${runUrl}`);
|
|
441
|
+
return lines;
|
|
442
|
+
}
|
|
398
443
|
export function stopLines(stop, runId, runUrl) {
|
|
399
444
|
if (stop.at === "storyboard-gate") {
|
|
400
445
|
const lines = [
|
|
@@ -411,6 +456,9 @@ export function stopLines(stop, runId, runUrl) {
|
|
|
411
456
|
return lines;
|
|
412
457
|
}
|
|
413
458
|
if (stop.at === "final-watch") {
|
|
459
|
+
if (stop.missingScenes?.length) {
|
|
460
|
+
return missingSceneLines(stop.missingScenes, stop.framesNodeId, runId, runUrl);
|
|
461
|
+
}
|
|
414
462
|
if (stop.cutAttached === null) {
|
|
415
463
|
return [
|
|
416
464
|
"Parked: every piece is made.",
|
|
@@ -540,6 +588,7 @@ function clipFromArtifact(sceneIndex, artifact) {
|
|
|
540
588
|
revoiced: artifact.revoiced === true,
|
|
541
589
|
speechTrimmed: artifact.speechTrimmed === true,
|
|
542
590
|
rawStorageId: artifact.rawStorageId ?? null,
|
|
591
|
+
voiceMode: artifact.voiceMode ?? null,
|
|
543
592
|
};
|
|
544
593
|
}
|
|
545
594
|
function keyframeDownload(sceneIndex, imageUrl) {
|
|
@@ -849,6 +898,7 @@ export function planPull(run, items, opts) {
|
|
|
849
898
|
revoiced: clip ? clip.revoiced : null,
|
|
850
899
|
speechTrimmed: clip ? clip.speechTrimmed : null,
|
|
851
900
|
rawStorageId: clip ? clip.rawStorageId : null,
|
|
901
|
+
voiceMode: clip ? clip.voiceMode : null,
|
|
852
902
|
clipStatus: item?.status ?? "missing",
|
|
853
903
|
error: item?.error ?? null,
|
|
854
904
|
flagged: item?.flagged === true,
|
|
@@ -922,6 +972,7 @@ export function markPullFailure(manifest, failure) {
|
|
|
922
972
|
scene.revoiced = null;
|
|
923
973
|
scene.speechTrimmed = null;
|
|
924
974
|
scene.rawStorageId = null;
|
|
975
|
+
scene.voiceMode = null;
|
|
925
976
|
}
|
|
926
977
|
if (scene.voice === failure.file)
|
|
927
978
|
scene.voice = null;
|
|
@@ -1419,8 +1470,14 @@ export async function statusFlow(runId, json, deps) {
|
|
|
1419
1470
|
};
|
|
1420
1471
|
}
|
|
1421
1472
|
const revoicedByScene = new Set();
|
|
1473
|
+
const voiceFirstByScene = new Set();
|
|
1422
1474
|
const timedOnOriginalVoice = new Set();
|
|
1423
1475
|
for (const artifact of outputsOfNodeKind(run, "video")) {
|
|
1476
|
+
if (artifact.type === "video" &&
|
|
1477
|
+
artifact.voiceMode === "voice-first" &&
|
|
1478
|
+
typeof artifact.sceneIndex === "number") {
|
|
1479
|
+
voiceFirstByScene.add(artifact.sceneIndex);
|
|
1480
|
+
}
|
|
1424
1481
|
if (artifact.type === "video" && artifact.revoiced === true && typeof artifact.sceneIndex === "number") {
|
|
1425
1482
|
revoicedByScene.add(artifact.sceneIndex);
|
|
1426
1483
|
if ((artifact.words?.length ?? 0) > 0)
|
|
@@ -1461,7 +1518,7 @@ export async function statusFlow(runId, json, deps) {
|
|
|
1461
1518
|
const row = byScene.get(sceneIndex);
|
|
1462
1519
|
lines.push(`${String(sceneIndex).padEnd(5)} ${itemWord(row.clip).padEnd(8)} ${itemWord(row.voiceover).padEnd(8)} ${itemWord(row.frame)}`);
|
|
1463
1520
|
if (row.clip?.lastRedo) {
|
|
1464
|
-
lines.push(` clip: ${row.clip.lastRedo
|
|
1521
|
+
lines.push(` clip: ${redoLine(row.clip.lastRedo)}`);
|
|
1465
1522
|
printDisplacedHistory(lines, row.clip.lastRedo);
|
|
1466
1523
|
}
|
|
1467
1524
|
if (row.clip?.lastRedo || row.clip?.flagged) {
|
|
@@ -1470,7 +1527,7 @@ export async function statusFlow(runId, json, deps) {
|
|
|
1470
1527
|
}
|
|
1471
1528
|
}
|
|
1472
1529
|
if (row.voiceover?.lastRedo) {
|
|
1473
|
-
lines.push(` voice: ${row.voiceover.lastRedo
|
|
1530
|
+
lines.push(` voice: ${redoLine(row.voiceover.lastRedo)}`);
|
|
1474
1531
|
printDisplacedHistory(lines, row.voiceover.lastRedo);
|
|
1475
1532
|
}
|
|
1476
1533
|
if (row.frame?.lastRedo)
|
|
@@ -1489,6 +1546,8 @@ export async function statusFlow(runId, json, deps) {
|
|
|
1489
1546
|
lines.push(` ${row.clip.error}`);
|
|
1490
1547
|
if (revoicedByScene.has(sceneIndex))
|
|
1491
1548
|
lines.push(" voice: cast voice applied");
|
|
1549
|
+
if (voiceFirstByScene.has(sceneIndex))
|
|
1550
|
+
lines.push(" clip: redone voice first");
|
|
1492
1551
|
if (timedOnOriginalVoice.has(sceneIndex)) {
|
|
1493
1552
|
lines.push(" word timings: measured on the original voice (close, not frame-exact)");
|
|
1494
1553
|
}
|
|
@@ -1789,7 +1848,7 @@ function findClipRow(run, items, target) {
|
|
|
1789
1848
|
}
|
|
1790
1849
|
return { row, uploadedCut };
|
|
1791
1850
|
}
|
|
1792
|
-
export async function retryClipFlow(runId, target, note, json, deps) {
|
|
1851
|
+
export async function retryClipFlow(runId, target, { note, voiceFirst = false }, json, deps) {
|
|
1793
1852
|
const runRes = await deps.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
1794
1853
|
if (!runRes.ok)
|
|
1795
1854
|
return errorResult(runRes, json);
|
|
@@ -1805,12 +1864,20 @@ export async function retryClipFlow(runId, target, note, json, deps) {
|
|
|
1805
1864
|
lines: json ? [JSON.stringify({ ok: false, error: plan.reason })] : [plan.reason],
|
|
1806
1865
|
};
|
|
1807
1866
|
}
|
|
1808
|
-
const res = await deps.post(SCENE_RETRY_PATH, {
|
|
1867
|
+
const res = await deps.post(voiceFirst ? SCENE_VOICE_FIRST_PATH : SCENE_RETRY_PATH, {
|
|
1809
1868
|
runId,
|
|
1810
1869
|
nodeId: plan.nodeId,
|
|
1811
1870
|
sceneIndex: plan.sceneIndex,
|
|
1812
1871
|
...(note ? { note } : {}),
|
|
1813
1872
|
});
|
|
1873
|
+
if (voiceFirst && missingRouteLine(res, "exodus video retry-clip --voice-first")) {
|
|
1874
|
+
return {
|
|
1875
|
+
code: 1,
|
|
1876
|
+
lines: json
|
|
1877
|
+
? [JSON.stringify({ ok: false, status: 404, error: VOICE_FIRST_NOT_ON_THIS_SERVER })]
|
|
1878
|
+
: [VOICE_FIRST_NOT_ON_THIS_SERVER],
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1814
1881
|
if (!res.ok)
|
|
1815
1882
|
return errorResult(res, json);
|
|
1816
1883
|
const triggerRunId = res.data.triggerRunId;
|
|
@@ -1829,6 +1896,7 @@ export async function retryClipFlow(runId, target, note, json, deps) {
|
|
|
1829
1896
|
reviewUrl: review,
|
|
1830
1897
|
warnings: plan.warnings,
|
|
1831
1898
|
...(note ? { note } : {}),
|
|
1899
|
+
...(voiceFirst ? { voiceMode: "voice-first" } : {}),
|
|
1832
1900
|
}),
|
|
1833
1901
|
],
|
|
1834
1902
|
};
|
|
@@ -1836,7 +1904,10 @@ export async function retryClipFlow(runId, target, note, json, deps) {
|
|
|
1836
1904
|
return {
|
|
1837
1905
|
code: 0,
|
|
1838
1906
|
lines: [
|
|
1839
|
-
|
|
1907
|
+
voiceFirst
|
|
1908
|
+
? `Redoing scene ${plan.sceneIndex}'s clip on ${plan.nodeId}, voice first: the line is ` +
|
|
1909
|
+
"recorded in the character's own voice, then the clip is made to match it."
|
|
1910
|
+
: `Redoing scene ${plan.sceneIndex}'s clip on ${plan.nodeId}.`,
|
|
1840
1911
|
`triggerRunId: ${triggerRunId ?? "-"}`,
|
|
1841
1912
|
"Every other scene stays as it is, and so do the pictures, the voices and the script.",
|
|
1842
1913
|
...(note
|
|
@@ -2207,6 +2278,11 @@ export async function pullFlow(runId, dir, json, deps) {
|
|
|
2207
2278
|
`Every piece is indexed in ${path.join(dir, "manifest.json")} — scene numbers there are the run's own.`,
|
|
2208
2279
|
...musicLines(plan.manifest),
|
|
2209
2280
|
];
|
|
2281
|
+
const voiceFirst = plan.manifest.scenes.filter((s) => s.voiceMode === "voice-first");
|
|
2282
|
+
if (voiceFirst.length > 0) {
|
|
2283
|
+
lines.push("", `Redone voice first (the line recorded, then the clip made to match it): ` +
|
|
2284
|
+
`scene${voiceFirst.length === 1 ? "" : "s"} ${voiceFirst.map((s) => s.sceneIndex).join(", ")}`);
|
|
2285
|
+
}
|
|
2210
2286
|
const flagged = plan.manifest.scenes.filter((s) => s.flagged);
|
|
2211
2287
|
if (flagged.length > 0) {
|
|
2212
2288
|
lines.push("", `${flagged.length} clip${flagged.length === 1 ? "" : "s"} came back flagged — usable, but look before you cut:`);
|
|
@@ -2583,7 +2659,7 @@ export async function run(flags, occurrences) {
|
|
|
2583
2659
|
if (!Number.isInteger(sceneIndex)) {
|
|
2584
2660
|
usage(`video retry-clip --scene must be a whole scene number, not "${sceneRaw}".`);
|
|
2585
2661
|
}
|
|
2586
|
-
return printResult(await retryClipFlow(runId, { sceneIndex, nodeId: flagString(flags, "node") }, flagString(flags, "note"), json, defaultDeps));
|
|
2662
|
+
return printResult(await retryClipFlow(runId, { sceneIndex, nodeId: flagString(flags, "node") }, { note: flagString(flags, "note"), voiceFirst: flags["voice-first"] === true }, json, defaultDeps));
|
|
2587
2663
|
}
|
|
2588
2664
|
if (sub === "revoice") {
|
|
2589
2665
|
const sceneRaw = flagString(flags, "scene");
|
|
@@ -409,7 +409,7 @@ export interface RunFormatProvenance {
|
|
|
409
409
|
};
|
|
410
410
|
specVersion: string | null;
|
|
411
411
|
}
|
|
412
|
-
export type VoicePath = "sts-revoice" | "lipsync-retarget" | "native-prompt" | "omni-audio-ids" | "gemini-direct";
|
|
412
|
+
export type VoicePath = "sts-revoice" | "lipsync-retarget" | "native-prompt" | "omni-audio-ids" | "gemini-direct" | "voice-first";
|
|
413
413
|
export type RunVoiceProvenance = {
|
|
414
414
|
source: "show";
|
|
415
415
|
path: VoicePath;
|