@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
@@ -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[];
@@ -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 ledgerRows = [...ledger.entries()].sort(([a], [b]) => a - b);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicopycoders/exodus",
3
- "version": "2026.10.1200-beta.1",
3
+ "version": "2026.10.1200",
4
4
  "description": "CLI + Claude Code skills for the Copycoders Exodus ad pipeline",
5
5
  "repository": {
6
6
  "type": "git",