@aicopycoders/exodus 2026.10.1200-beta.1 → 2026.10.1200
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.
|
@@ -38,6 +38,37 @@ storyboard itself when its checks pass (see "What costs money"). The full line i
|
|
|
38
38
|
note cannot contain a colon. "The script file" shows the right form and the
|
|
39
39
|
wrong one.
|
|
40
40
|
|
|
41
|
+
**The kinds of ad.** `--conceit` takes one of the kinds the server offers. The
|
|
42
|
+
server's list is the truth, and nothing prints it on request. Pass a key it
|
|
43
|
+
doesn't offer and the refusal names every key it does: `video start --conceit
|
|
44
|
+
must be one of …`. Nothing is spent on a refused start. Today the server offers
|
|
45
|
+
15 formats and then Custom:
|
|
46
|
+
|
|
47
|
+
| Key | Kind |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `podcast` | Podcast |
|
|
50
|
+
| `ugc` | UGC talking head |
|
|
51
|
+
| `stage` | Speaking on stage |
|
|
52
|
+
| `street` | Street interview |
|
|
53
|
+
| `personification` | Personification |
|
|
54
|
+
| `news-broadcast` | News Broadcast |
|
|
55
|
+
| `court-tv` | Court TV |
|
|
56
|
+
| `soap-opera` | Soap Opera |
|
|
57
|
+
| `storytime` | Storytime While Doing a Task |
|
|
58
|
+
| `customer-service-call` | Customer Service Call |
|
|
59
|
+
| `debate-circle` | Debate Circle |
|
|
60
|
+
| `talk-show-panel` | Talk Show Panel |
|
|
61
|
+
| `infomercial` | Infomercial |
|
|
62
|
+
| `dating-show` | Dating Show |
|
|
63
|
+
| `game-show` | Game Show |
|
|
64
|
+
| `custom` | Custom |
|
|
65
|
+
|
|
66
|
+
Custom builds the ad from the script itself. A script with no `CAST:` runs as a
|
|
67
|
+
voice-over, and a script with people runs as a scene. Voice-over and Scene are
|
|
68
|
+
not on the menu by name; Custom is the way to them. A narrator-only script fits
|
|
69
|
+
Custom alone. If the refusal lists different keys from this table, the server
|
|
70
|
+
is right and this table is stale.
|
|
71
|
+
|
|
41
72
|
The Rig box on that workflow is where the run's Conceit and Style come from.
|
|
42
73
|
Conceit is the family plus Wrapper, Narrator Person, and Delivery. Style is the
|
|
43
74
|
look. Narrated Story is the voice-over family and Acted Story is the dialogue
|
|
@@ -368,6 +399,78 @@ Do not use a long dash in words a character says, or in words shown on screen.
|
|
|
368
399
|
Synthesised speech reads a long dash oddly, and so does burned-in text. A spoken
|
|
369
400
|
line stays `NAME: what they say`.
|
|
370
401
|
|
|
402
|
+
### Directing a line
|
|
403
|
+
|
|
404
|
+
A spoken line can carry two kinds of note. Neither is required.
|
|
405
|
+
|
|
406
|
+
- **How the line is said goes in parentheses.** The voice acts it out.
|
|
407
|
+
- **What happens on camera goes in square brackets.** The storyboard plans the
|
|
408
|
+
shot around it.
|
|
409
|
+
|
|
410
|
+
```
|
|
411
|
+
MIA: (whispering, excited) I can't believe this actually worked.
|
|
412
|
+
MIA: [holds the jar up to the camera] This is the one.
|
|
413
|
+
MIA: [leans in] (quietly) Don't tell my sister.
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
A note may sit anywhere on the line. It comes out of the spoken words, so the
|
|
417
|
+
voice never says it. Several notes of one kind on a line are joined. Notes work
|
|
418
|
+
on `CTA:` lines and on narrator lines too. They do not work on `CAST:` lines,
|
|
419
|
+
where anything in brackets is just part of the description.
|
|
420
|
+
|
|
421
|
+
How the voice uses a delivery note. By default a run records each line first,
|
|
422
|
+
on the Eleven v4 voices, which take the note as a short cue in front of the
|
|
423
|
+
line. The cue is the note's first clause, at most six words. A full stop,
|
|
424
|
+
colon, semicolon, question mark, exclamation mark, or a dash with a space on
|
|
425
|
+
each side ends the clause. A comma does not, so `whispering, excited` reaches
|
|
426
|
+
the voice whole. Keep the note short and put the part that matters first. Put
|
|
427
|
+
one delivery note on a line: two are joined with a semicolon, so the voice
|
|
428
|
+
hears only the first.
|
|
429
|
+
|
|
430
|
+
Right.
|
|
431
|
+
|
|
432
|
+
```
|
|
433
|
+
MIA: (whispering, excited) I can't believe this actually worked.
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Wrong.
|
|
437
|
+
|
|
438
|
+
```
|
|
439
|
+
MIA: (She is whispering. She can barely contain how excited she is) I can't believe this actually worked.
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
The wrong note reaches the voice as `[she is whispering]`. The excitement is
|
|
443
|
+
lost.
|
|
444
|
+
|
|
445
|
+
In a narrated scene the whole narration is read as one part, so it takes one
|
|
446
|
+
cue: the note on the first narrator line in that scene.
|
|
447
|
+
|
|
448
|
+
What the reader refuses, with the code `malformed-direction`:
|
|
449
|
+
|
|
450
|
+
- an empty note, `()` or `[]`
|
|
451
|
+
- a square bracket that is never closed, or a stray `]`
|
|
452
|
+
- a line with no spoken words left once the notes come out
|
|
453
|
+
|
|
454
|
+
An unclosed `(` is not refused. It is read as ordinary words, and the voice
|
|
455
|
+
says it. Check every `(` has its `)`.
|
|
456
|
+
|
|
457
|
+
Both ways in read the file the same way: `exodus video start --script` and
|
|
458
|
+
`exodus workflow run --input <field>=@script.txt`. The second holds only when
|
|
459
|
+
the script goes straight from a Brief into the Storyboard. In a workflow where a
|
|
460
|
+
bot writes or rewrites the script first, the notes are not read this way, and
|
|
461
|
+
the bot may drop them or leave them in the spoken words.
|
|
462
|
+
|
|
463
|
+
With `--voice native` (admins only) the video model speaks the lines itself and
|
|
464
|
+
nothing is recorded first, so the six-word cue above does not apply.
|
|
465
|
+
|
|
466
|
+
**A line with no note still gets a natural read.** A note is the surest way to
|
|
467
|
+
get a particular delivery. The storyboard planner may add a note of its own to
|
|
468
|
+
a line you left plain. A note you wrote always wins over the planner's.
|
|
469
|
+
|
|
470
|
+
When you help the user write or tidy a script, suggest a delivery note where
|
|
471
|
+
the tone carries the ad: the hook, the turn, the CTA. Do not put a note on every
|
|
472
|
+
line. A note on every line reads as a script nobody trusted.
|
|
473
|
+
|
|
371
474
|
**How many speakers you may have depends on the rig.** Three by default. A rig
|
|
372
475
|
that carries saved format rules sets its own number: an ensemble rig seats more,
|
|
373
476
|
a talking-head rig seats one. Over the number, the launch refuses before
|
package/dist/commands/video.d.ts
CHANGED
|
@@ -79,9 +79,11 @@ export type ArtifactSubset = {
|
|
|
79
79
|
type: "image";
|
|
80
80
|
imageUrl?: string;
|
|
81
81
|
storageId?: string;
|
|
82
|
-
anchor?: "set-plate" | "product-sheet" | "filled-plate";
|
|
82
|
+
anchor?: "set-plate" | "product-sheet" | "filled-plate" | "product-plate" | "layout-plate";
|
|
83
83
|
holds?: string[];
|
|
84
84
|
lid?: "closed";
|
|
85
|
+
camera?: string;
|
|
86
|
+
view?: "side" | "back" | "three-quarter";
|
|
85
87
|
} | {
|
|
86
88
|
type: "video";
|
|
87
89
|
sceneIndex?: number;
|
|
@@ -158,6 +160,7 @@ export interface PullCastLockMember {
|
|
|
158
160
|
identityRefs?: Array<{
|
|
159
161
|
storageId?: string;
|
|
160
162
|
imageUrl?: string;
|
|
163
|
+
view?: string;
|
|
161
164
|
}>;
|
|
162
165
|
}
|
|
163
166
|
export interface PullCastLock {
|
|
@@ -363,6 +366,11 @@ export interface ManifestCastRef {
|
|
|
363
366
|
voiceId: string | null;
|
|
364
367
|
voiceLabel: string | null;
|
|
365
368
|
voiceDescription: string | null;
|
|
369
|
+
views?: ManifestCastView[];
|
|
370
|
+
}
|
|
371
|
+
export interface ManifestCastView {
|
|
372
|
+
view: string;
|
|
373
|
+
file: string | null;
|
|
366
374
|
}
|
|
367
375
|
export type MusicBedState = "on" | "off" | "unknown";
|
|
368
376
|
export interface RunVideoChoice {
|
|
@@ -409,6 +417,7 @@ export interface RunShots {
|
|
|
409
417
|
pacing?: string;
|
|
410
418
|
}
|
|
411
419
|
export declare const CAST_LEDGER_BASE: number;
|
|
420
|
+
export declare const CAST_VIEW_LEDGER_BASE: number;
|
|
412
421
|
export declare function musicBedState(run: VideoRun): MusicBedState;
|
|
413
422
|
export interface PullPlan {
|
|
414
423
|
downloads: PullDownload[];
|
package/dist/commands/video.js
CHANGED
|
@@ -639,6 +639,7 @@ export function planFailureLines(node, runId) {
|
|
|
639
639
|
}
|
|
640
640
|
export const MUSIC_HEARD_CODE = "music-heard";
|
|
641
641
|
export const CAST_LEDGER_BASE = HELPER_LEDGER_BASE + 10000;
|
|
642
|
+
export const CAST_VIEW_LEDGER_BASE = CAST_LEDGER_BASE + 1000;
|
|
642
643
|
export function musicBedState(run) {
|
|
643
644
|
if (run.musicBed === undefined)
|
|
644
645
|
return "unknown";
|
|
@@ -737,9 +738,25 @@ function castVoicePins(artifact) {
|
|
|
737
738
|
}
|
|
738
739
|
return pins;
|
|
739
740
|
}
|
|
741
|
+
function lockViews(member, viewRows) {
|
|
742
|
+
return (member.identityRefs ?? []).flatMap((ref) => {
|
|
743
|
+
if (!ref.view)
|
|
744
|
+
return [];
|
|
745
|
+
const row = viewRows.find(({ artifact: art }) => art?.type !== "image"
|
|
746
|
+
? false
|
|
747
|
+
: art.storageId
|
|
748
|
+
? art.storageId === ref.storageId
|
|
749
|
+
: !!art.imageUrl && art.imageUrl === ref.imageUrl);
|
|
750
|
+
const url = (row?.artifact?.type === "image" ? row.artifact.imageUrl : undefined) ?? ref.imageUrl;
|
|
751
|
+
return url ? [{ view: ref.view, url }] : [];
|
|
752
|
+
});
|
|
753
|
+
}
|
|
740
754
|
function planCastRefs(run, items, pins) {
|
|
741
755
|
const ledger = reservedCastFrames(items);
|
|
742
|
-
const
|
|
756
|
+
const isViewRow = ([sceneIndex, row]) => sceneIndex >= CAST_VIEW_LEDGER_BASE || (row.artifact?.type === "image" && row.artifact.view !== undefined);
|
|
757
|
+
const sorted = [...ledger.entries()].sort(([a], [b]) => a - b);
|
|
758
|
+
const ledgerRows = sorted.filter((r) => !isViewRow(r));
|
|
759
|
+
const viewRows = sorted.filter(isViewRow).map(([, row]) => row);
|
|
743
760
|
const locked = run.castLock?.cast;
|
|
744
761
|
const rows = [];
|
|
745
762
|
if (locked !== undefined && locked.length > 0) {
|
|
@@ -765,6 +782,7 @@ function planCastRefs(run, items, pins) {
|
|
|
765
782
|
status: item?.status ?? (url ? "done" : "missing"),
|
|
766
783
|
error: item?.error ?? null,
|
|
767
784
|
stem: `cast-${member.characterId}`,
|
|
785
|
+
views: lockViews(member, viewRows),
|
|
768
786
|
});
|
|
769
787
|
}
|
|
770
788
|
for (const [sceneIndex, item] of ledgerRows) {
|
|
@@ -780,6 +798,7 @@ function planCastRefs(run, items, pins) {
|
|
|
780
798
|
status: item.status,
|
|
781
799
|
error: item.error ?? null,
|
|
782
800
|
stem: `cast-ref-${String(sceneIndex - CAST_LEDGER_BASE).padStart(2, "0")}`,
|
|
801
|
+
views: [],
|
|
783
802
|
});
|
|
784
803
|
}
|
|
785
804
|
}
|
|
@@ -794,6 +813,7 @@ function planCastRefs(run, items, pins) {
|
|
|
794
813
|
status: item.status,
|
|
795
814
|
error: item.error ?? null,
|
|
796
815
|
stem: `cast-ref-${String(sceneIndex - CAST_LEDGER_BASE).padStart(2, "0")}`,
|
|
816
|
+
views: [],
|
|
797
817
|
});
|
|
798
818
|
}
|
|
799
819
|
}
|
|
@@ -802,6 +822,7 @@ function planCastRefs(run, items, pins) {
|
|
|
802
822
|
for (const row of rows) {
|
|
803
823
|
const file = row.url ? `${row.stem}.${extFor(row.url, "image")}` : null;
|
|
804
824
|
const pin = row.characterId ? pins.get(row.characterId) : undefined;
|
|
825
|
+
const views = row.views.map((v) => ({ view: v.view, file: `${row.stem}-${v.view}.${extFor(v.url, "image")}`, url: v.url }));
|
|
805
826
|
cast.push({
|
|
806
827
|
characterId: row.characterId,
|
|
807
828
|
name: row.name,
|
|
@@ -811,9 +832,12 @@ function planCastRefs(run, items, pins) {
|
|
|
811
832
|
voiceId: pin?.voiceId ?? null,
|
|
812
833
|
voiceLabel: pin?.voiceLabel ?? null,
|
|
813
834
|
voiceDescription: pin?.voiceDescription ?? null,
|
|
835
|
+
...(views.length ? { views: views.map(({ view, file: viewFile }) => ({ view, file: viewFile })) } : {}),
|
|
814
836
|
});
|
|
815
837
|
if (file && row.url)
|
|
816
838
|
downloads.push({ file, url: row.url });
|
|
839
|
+
for (const v of views)
|
|
840
|
+
downloads.push({ file: v.file, url: v.url });
|
|
817
841
|
}
|
|
818
842
|
return { cast, downloads };
|
|
819
843
|
}
|
|
@@ -1105,6 +1129,10 @@ export function markPullFailure(manifest, failure) {
|
|
|
1105
1129
|
for (const ref of manifest.cast) {
|
|
1106
1130
|
if (ref.file === failure.file)
|
|
1107
1131
|
ref.file = null;
|
|
1132
|
+
for (const view of ref.views ?? []) {
|
|
1133
|
+
if (view.file === failure.file)
|
|
1134
|
+
view.file = null;
|
|
1135
|
+
}
|
|
1108
1136
|
}
|
|
1109
1137
|
if (manifest.narration?.file === failure.file)
|
|
1110
1138
|
manifest.narration = null;
|