@aicopycoders/exodus 2026.9.2000-beta.10 → 2026.9.2000-beta.11
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)
|
|
@@ -602,6 +602,9 @@ scenes[] one per scene, in order
|
|
|
602
602
|
on record when the voice pass replaced the
|
|
603
603
|
clip with a new file; null when the clip IS
|
|
604
604
|
that file, or there is no clip
|
|
605
|
+
voiceMode "voice-first" when the clip was redone voice
|
|
606
|
+
first (its line recorded, then the clip made
|
|
607
|
+
to match it); null otherwise
|
|
605
608
|
provenance which saved rulebook this run followed:
|
|
606
609
|
{format: {via, nodeId, rigId, rigName,
|
|
607
610
|
rulesFrom, specVersion}}. Absent when the
|
|
@@ -746,6 +749,7 @@ is no second run, no new storyboard and no re-render of anything else.
|
|
|
746
749
|
exodus video retry-clip <runId> --scene 3
|
|
747
750
|
exodus video retry-clip <runId> --scene 3 --note "keep both hands in frame on the handshake"
|
|
748
751
|
exodus video retry-clip <runId> --scene 3 --node video-2
|
|
752
|
+
exodus video retry-clip <runId> --scene 3 --voice-first
|
|
749
753
|
```
|
|
750
754
|
|
|
751
755
|
`--node` is only needed when a run has clips for that scene on two video steps;
|
|
@@ -768,7 +772,17 @@ revoiced and trimmed the same way, with fresh findings and fresh word timings.
|
|
|
768
772
|
scene's motion prompt only (`withRedoDirection`,
|
|
769
773
|
`scout/src/trigger/workflow-scene-retry.ts`); the spoken line still comes from
|
|
770
774
|
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.
|
|
775
|
+
them. If the script's words are wrong, that is a new run.
|
|
776
|
+
|
|
777
|
+
**`--voice-first` fixes a clip that said its line wrong** (#2299). It is for a
|
|
778
|
+
clip whose findings include `speech-mismatch`, `speech-repeat`,
|
|
779
|
+
`speech-after-line` or `speech-long-pause`. The scene's line is recorded in each
|
|
780
|
+
speaker's pinned ElevenLabs voice, and MiniMax (`minimax-h3/reference-to-video`)
|
|
781
|
+
makes the clip to match the recording. Only that clip changes; the word check
|
|
782
|
+
and the other checks still run on it. It needs an ElevenLabs key and a voice for
|
|
783
|
+
every speaker in the scene, and it is refused for a scene with no spoken line.
|
|
784
|
+
The clip then reads `clip: redone voice first` in `status`, carries
|
|
785
|
+
`voiceMode: "voice-first"` in the manifest, and is listed in the `pull` summary.
|
|
772
786
|
|
|
773
787
|
**A redo whose take fails the checks keeps the original clip.** On a finished
|
|
774
788
|
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;
|
|
@@ -273,6 +274,7 @@ export interface ManifestScene {
|
|
|
273
274
|
revoiced: boolean | null;
|
|
274
275
|
speechTrimmed: boolean | null;
|
|
275
276
|
rawStorageId: string | null;
|
|
277
|
+
voiceMode: "voice-first" | null;
|
|
276
278
|
clipStatus: string;
|
|
277
279
|
error: string | null;
|
|
278
280
|
flagged: boolean;
|
|
@@ -411,7 +413,11 @@ export interface ClipRedoTarget {
|
|
|
411
413
|
nodeId?: string;
|
|
412
414
|
}
|
|
413
415
|
export declare function planClipRedo(run: VideoRun, items: NodeItem[], target: ClipRedoTarget): ClipRedoPlan;
|
|
414
|
-
export
|
|
416
|
+
export interface ClipRedoOptions {
|
|
417
|
+
note?: string;
|
|
418
|
+
voiceFirst?: boolean;
|
|
419
|
+
}
|
|
420
|
+
export declare function retryClipFlow(runId: string, target: ClipRedoTarget, { note, voiceFirst }: ClipRedoOptions, json: boolean, deps: VideoDeps): Promise<FlowResult>;
|
|
415
421
|
export declare const VOICE_PATHS_NEVER_HEARD: Set<string>;
|
|
416
422
|
export declare function planClipRevoice(run: VideoRun, items: NodeItem[], target: ClipRedoTarget): ClipRedoPlan;
|
|
417
423
|
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";
|
|
@@ -540,6 +549,7 @@ function clipFromArtifact(sceneIndex, artifact) {
|
|
|
540
549
|
revoiced: artifact.revoiced === true,
|
|
541
550
|
speechTrimmed: artifact.speechTrimmed === true,
|
|
542
551
|
rawStorageId: artifact.rawStorageId ?? null,
|
|
552
|
+
voiceMode: artifact.voiceMode ?? null,
|
|
543
553
|
};
|
|
544
554
|
}
|
|
545
555
|
function keyframeDownload(sceneIndex, imageUrl) {
|
|
@@ -849,6 +859,7 @@ export function planPull(run, items, opts) {
|
|
|
849
859
|
revoiced: clip ? clip.revoiced : null,
|
|
850
860
|
speechTrimmed: clip ? clip.speechTrimmed : null,
|
|
851
861
|
rawStorageId: clip ? clip.rawStorageId : null,
|
|
862
|
+
voiceMode: clip ? clip.voiceMode : null,
|
|
852
863
|
clipStatus: item?.status ?? "missing",
|
|
853
864
|
error: item?.error ?? null,
|
|
854
865
|
flagged: item?.flagged === true,
|
|
@@ -922,6 +933,7 @@ export function markPullFailure(manifest, failure) {
|
|
|
922
933
|
scene.revoiced = null;
|
|
923
934
|
scene.speechTrimmed = null;
|
|
924
935
|
scene.rawStorageId = null;
|
|
936
|
+
scene.voiceMode = null;
|
|
925
937
|
}
|
|
926
938
|
if (scene.voice === failure.file)
|
|
927
939
|
scene.voice = null;
|
|
@@ -1419,8 +1431,14 @@ export async function statusFlow(runId, json, deps) {
|
|
|
1419
1431
|
};
|
|
1420
1432
|
}
|
|
1421
1433
|
const revoicedByScene = new Set();
|
|
1434
|
+
const voiceFirstByScene = new Set();
|
|
1422
1435
|
const timedOnOriginalVoice = new Set();
|
|
1423
1436
|
for (const artifact of outputsOfNodeKind(run, "video")) {
|
|
1437
|
+
if (artifact.type === "video" &&
|
|
1438
|
+
artifact.voiceMode === "voice-first" &&
|
|
1439
|
+
typeof artifact.sceneIndex === "number") {
|
|
1440
|
+
voiceFirstByScene.add(artifact.sceneIndex);
|
|
1441
|
+
}
|
|
1424
1442
|
if (artifact.type === "video" && artifact.revoiced === true && typeof artifact.sceneIndex === "number") {
|
|
1425
1443
|
revoicedByScene.add(artifact.sceneIndex);
|
|
1426
1444
|
if ((artifact.words?.length ?? 0) > 0)
|
|
@@ -1489,6 +1507,8 @@ export async function statusFlow(runId, json, deps) {
|
|
|
1489
1507
|
lines.push(` ${row.clip.error}`);
|
|
1490
1508
|
if (revoicedByScene.has(sceneIndex))
|
|
1491
1509
|
lines.push(" voice: cast voice applied");
|
|
1510
|
+
if (voiceFirstByScene.has(sceneIndex))
|
|
1511
|
+
lines.push(" clip: redone voice first");
|
|
1492
1512
|
if (timedOnOriginalVoice.has(sceneIndex)) {
|
|
1493
1513
|
lines.push(" word timings: measured on the original voice (close, not frame-exact)");
|
|
1494
1514
|
}
|
|
@@ -1789,7 +1809,7 @@ function findClipRow(run, items, target) {
|
|
|
1789
1809
|
}
|
|
1790
1810
|
return { row, uploadedCut };
|
|
1791
1811
|
}
|
|
1792
|
-
export async function retryClipFlow(runId, target, note, json, deps) {
|
|
1812
|
+
export async function retryClipFlow(runId, target, { note, voiceFirst = false }, json, deps) {
|
|
1793
1813
|
const runRes = await deps.get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`);
|
|
1794
1814
|
if (!runRes.ok)
|
|
1795
1815
|
return errorResult(runRes, json);
|
|
@@ -1805,12 +1825,20 @@ export async function retryClipFlow(runId, target, note, json, deps) {
|
|
|
1805
1825
|
lines: json ? [JSON.stringify({ ok: false, error: plan.reason })] : [plan.reason],
|
|
1806
1826
|
};
|
|
1807
1827
|
}
|
|
1808
|
-
const res = await deps.post(SCENE_RETRY_PATH, {
|
|
1828
|
+
const res = await deps.post(voiceFirst ? SCENE_VOICE_FIRST_PATH : SCENE_RETRY_PATH, {
|
|
1809
1829
|
runId,
|
|
1810
1830
|
nodeId: plan.nodeId,
|
|
1811
1831
|
sceneIndex: plan.sceneIndex,
|
|
1812
1832
|
...(note ? { note } : {}),
|
|
1813
1833
|
});
|
|
1834
|
+
if (voiceFirst && missingRouteLine(res, "exodus video retry-clip --voice-first")) {
|
|
1835
|
+
return {
|
|
1836
|
+
code: 1,
|
|
1837
|
+
lines: json
|
|
1838
|
+
? [JSON.stringify({ ok: false, status: 404, error: VOICE_FIRST_NOT_ON_THIS_SERVER })]
|
|
1839
|
+
: [VOICE_FIRST_NOT_ON_THIS_SERVER],
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1814
1842
|
if (!res.ok)
|
|
1815
1843
|
return errorResult(res, json);
|
|
1816
1844
|
const triggerRunId = res.data.triggerRunId;
|
|
@@ -1829,6 +1857,7 @@ export async function retryClipFlow(runId, target, note, json, deps) {
|
|
|
1829
1857
|
reviewUrl: review,
|
|
1830
1858
|
warnings: plan.warnings,
|
|
1831
1859
|
...(note ? { note } : {}),
|
|
1860
|
+
...(voiceFirst ? { voiceMode: "voice-first" } : {}),
|
|
1832
1861
|
}),
|
|
1833
1862
|
],
|
|
1834
1863
|
};
|
|
@@ -1836,7 +1865,10 @@ export async function retryClipFlow(runId, target, note, json, deps) {
|
|
|
1836
1865
|
return {
|
|
1837
1866
|
code: 0,
|
|
1838
1867
|
lines: [
|
|
1839
|
-
|
|
1868
|
+
voiceFirst
|
|
1869
|
+
? `Redoing scene ${plan.sceneIndex}'s clip on ${plan.nodeId}, voice first: the line is ` +
|
|
1870
|
+
"recorded in the character's own voice, then the clip is made to match it."
|
|
1871
|
+
: `Redoing scene ${plan.sceneIndex}'s clip on ${plan.nodeId}.`,
|
|
1840
1872
|
`triggerRunId: ${triggerRunId ?? "-"}`,
|
|
1841
1873
|
"Every other scene stays as it is, and so do the pictures, the voices and the script.",
|
|
1842
1874
|
...(note
|
|
@@ -2207,6 +2239,11 @@ export async function pullFlow(runId, dir, json, deps) {
|
|
|
2207
2239
|
`Every piece is indexed in ${path.join(dir, "manifest.json")} — scene numbers there are the run's own.`,
|
|
2208
2240
|
...musicLines(plan.manifest),
|
|
2209
2241
|
];
|
|
2242
|
+
const voiceFirst = plan.manifest.scenes.filter((s) => s.voiceMode === "voice-first");
|
|
2243
|
+
if (voiceFirst.length > 0) {
|
|
2244
|
+
lines.push("", `Redone voice first (the line recorded, then the clip made to match it): ` +
|
|
2245
|
+
`scene${voiceFirst.length === 1 ? "" : "s"} ${voiceFirst.map((s) => s.sceneIndex).join(", ")}`);
|
|
2246
|
+
}
|
|
2210
2247
|
const flagged = plan.manifest.scenes.filter((s) => s.flagged);
|
|
2211
2248
|
if (flagged.length > 0) {
|
|
2212
2249
|
lines.push("", `${flagged.length} clip${flagged.length === 1 ? "" : "s"} came back flagged — usable, but look before you cut:`);
|
|
@@ -2583,7 +2620,7 @@ export async function run(flags, occurrences) {
|
|
|
2583
2620
|
if (!Number.isInteger(sceneIndex)) {
|
|
2584
2621
|
usage(`video retry-clip --scene must be a whole scene number, not "${sceneRaw}".`);
|
|
2585
2622
|
}
|
|
2586
|
-
return printResult(await retryClipFlow(runId, { sceneIndex, nodeId: flagString(flags, "node") }, flagString(flags, "note"), json, defaultDeps));
|
|
2623
|
+
return printResult(await retryClipFlow(runId, { sceneIndex, nodeId: flagString(flags, "node") }, { note: flagString(flags, "note"), voiceFirst: flags["voice-first"] === true }, json, defaultDeps));
|
|
2587
2624
|
}
|
|
2588
2625
|
if (sub === "revoice") {
|
|
2589
2626
|
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;
|