@aicopycoders/exodus 2026.9.2000-beta.1 → 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.
- package/assets/docs/PIPELINES.md +2 -1
- package/assets/skills/exodus-meme/SKILL.md +4 -3
- package/assets/skills/exodus-template/SKILL.md +2 -1
- package/assets/skills/exodus-video/SKILL.md +142 -21
- package/assets/skills/exodus-workflow/SKILL.md +1 -1
- package/dist/commands/genesis.js +12 -133
- package/dist/commands/image.js +1 -1
- package/dist/commands/status.js +8 -18
- package/dist/commands/template.js +11 -4
- package/dist/commands/video.d.ts +126 -5
- package/dist/commands/video.js +656 -54
- package/dist/commands/workflow.d.ts +52 -4
- package/dist/commands/workflow.js +62 -9
- package/dist/lib/helperLedgerBase.d.ts +3 -0
- package/dist/lib/helperLedgerBase.js +3 -0
- package/package.json +1 -1
package/assets/docs/PIPELINES.md
CHANGED
|
@@ -152,7 +152,8 @@ run options:
|
|
|
152
152
|
--mode auto|manual|hybrid Generation mode (default: auto)
|
|
153
153
|
--render-mode images|prompts prompts = skip render, return LLM prompts only (default: images)
|
|
154
154
|
--aspect 1:1|9:16 Image aspect ratio (default: 1:1)
|
|
155
|
-
--model gpt-image-2|
|
|
155
|
+
--model gpt-image-2.5|gpt-image-2.5-sunburst|gpt-image-2|nano-banana-pro
|
|
156
|
+
Kie.ai model (default: gpt-image-2.5)
|
|
156
157
|
--realism off|realistic Realism enforcement (default: off)
|
|
157
158
|
--quantities "slug:N,slug:N" manual-mode per-type counts (e.g. "testimonial:3,hero:2")
|
|
158
159
|
--pairings '<JSON>' hybrid-mode host/modifier pairs: '[{"host":"testimonial","modifier":"hero"}]'
|
|
@@ -244,6 +244,7 @@ internals into chat.
|
|
|
244
244
|
- The rig's child image runs are still `creativeSuiteRuns` with engine `meme`,
|
|
245
245
|
so finished memes appear in the creative-suite library alongside other engines
|
|
246
246
|
and `exodus browse --agent meme` still covers them.
|
|
247
|
-
- `run` accepts only the
|
|
248
|
-
`nano-banana-pro`,
|
|
249
|
-
|
|
247
|
+
- `run` accepts only the models the rig renders (`gpt-image-2.5`, the house
|
|
248
|
+
default, plus `gpt-image-2.5-sunburst`, `gpt-image-2` and `nano-banana-pro`,
|
|
249
|
+
or their Kie slugs); anything else is a 400 listing the accepted values. It
|
|
250
|
+
used to accept any value and silently ignore it.
|
|
@@ -17,7 +17,8 @@ run options:
|
|
|
17
17
|
--mode auto|manual|hybrid Generation mode (default: auto)
|
|
18
18
|
--render-mode images|prompts prompts = skip render, return LLM prompts only (default: images)
|
|
19
19
|
--aspect 1:1|9:16 Image aspect ratio (default: 1:1)
|
|
20
|
-
--model gpt-image-2|
|
|
20
|
+
--model gpt-image-2.5|gpt-image-2.5-sunburst|gpt-image-2|nano-banana-pro
|
|
21
|
+
Kie.ai model (default: gpt-image-2.5)
|
|
21
22
|
--realism off|realistic Realism enforcement (default: off)
|
|
22
23
|
--quantities "slug:N,slug:N" manual-mode per-type counts (e.g. "testimonial:3,hero:2")
|
|
23
24
|
--pairings '<JSON>' hybrid-mode host/modifier pairs: '[{"host":"testimonial","modifier":"hero"}]'
|
|
@@ -21,12 +21,27 @@ Your job in this skill is to drive that loop, hand over the pieces with the
|
|
|
21
21
|
facts about them, and make whatever cut the user asked for. **How to edit is the
|
|
22
22
|
user's call, not this skill's** — see "Cutting the ad".
|
|
23
23
|
|
|
24
|
+
This file ships on the member's machine. It holds commands, the manifest's
|
|
25
|
+
shape, and recipes for the cut. It does not hold prompt text, house primers,
|
|
26
|
+
judge wording, or model instructions.
|
|
27
|
+
|
|
24
28
|
## How a run starts
|
|
25
29
|
|
|
26
30
|
The ad is a saved workflow in the brand, started script-first with `exodus
|
|
27
31
|
workflow run`. From there the same nodes render, `video status` reads the run,
|
|
28
32
|
`video pull` writes the folder, and `video upload` and `approve` finish it.
|
|
29
33
|
|
|
34
|
+
A Show is no longer required. `exodus video start` starts from a script, a
|
|
35
|
+
conceit, and a style, and the same parks follow. The full line is under
|
|
36
|
+
"Starting a run". An optional `--direction` note is whole-ad rules, and that
|
|
37
|
+
note cannot contain a colon. "The script file" shows the right form and the
|
|
38
|
+
wrong one.
|
|
39
|
+
|
|
40
|
+
The Rig box on that workflow is where the run's Conceit and Style come from.
|
|
41
|
+
Conceit is the family plus Wrapper, Narrator Person, and Delivery. Style is the
|
|
42
|
+
look. Narrated Story is the voice-over family and Acted Story is the dialogue
|
|
43
|
+
family (`familyTree.ts`). Those family names stay the code's names.
|
|
44
|
+
|
|
30
45
|
## Before anything
|
|
31
46
|
|
|
32
47
|
- `npx @aicopycoders/exodus video --help` and `workflow --help` are the
|
|
@@ -69,6 +84,11 @@ exodus workflow run <workflowId|name> --input <field>=@script.txt --voices @voic
|
|
|
69
84
|
exodus workflow run <workflowId|name> --input <field>=@script.txt --voice-treatment @voice-treatment.json --wait
|
|
70
85
|
same, with the video model speaking voices you WROTE
|
|
71
86
|
exodus video status <runId> where it is, per scene
|
|
87
|
+
exodus video start --script <file> --conceit <podcast|ugc|stage|street|personification> --style <slug> [--direction "…"] [--voice-path <path>] [--video-model <id>] [--voice native] [--music] [--wait]
|
|
88
|
+
start from a script, a conceit and a look.
|
|
89
|
+
No music bed unless you pass --music.
|
|
90
|
+
Each conceit has its own video model and voice;
|
|
91
|
+
--video-model / --voice override them for one run
|
|
72
92
|
```
|
|
73
93
|
|
|
74
94
|
**From the storyboard on.**
|
|
@@ -81,7 +101,7 @@ exodus video approve <runId> keep going
|
|
|
81
101
|
exodus video retry-frame <runId> --node <nodeId> --scene <n> redo one still at the pixel gate
|
|
82
102
|
exodus video status <runId> where it is, per scene
|
|
83
103
|
exodus video pull <runId> --out ./ad-<runId> every piece + manifest.json
|
|
84
|
-
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "…"]
|
|
104
|
+
exodus video retry-clip <runId> --scene <n> [--node <nodeId>] [--note "…"] [--voice-first]
|
|
85
105
|
redo one clip at the final watch
|
|
86
106
|
exodus video revoice <runId> --all | --scene <n> redo only the voice; no new video
|
|
87
107
|
… make the cut (your choice of tool)
|
|
@@ -98,13 +118,24 @@ already printed the same stop block, and `workflow inbox` lists a parked video
|
|
|
98
118
|
run, but neither shows the per-scene detail, and the checkpoint verbs cannot
|
|
99
119
|
resolve a video park — see "What the CLI can and cannot do".
|
|
100
120
|
|
|
101
|
-
**Where a run can stop.**
|
|
121
|
+
**Where a run can stop.** The two video parks come in order when the run has
|
|
122
|
+
both. The storyboard gate is first. You read the cards there and you set the
|
|
123
|
+
voices there. Approving that gate releases the voices and the clips. That is the
|
|
124
|
+
expensive part. The final watch is next, after the pieces exist, and only when
|
|
125
|
+
the video node sets `finalWatch: true`. That park is for the cut. You pull, you
|
|
126
|
+
cut, and you upload. Approving the upload makes it the ad. A node with
|
|
127
|
+
`finalWatch` off skips the second park and the run finishes.
|
|
128
|
+
|
|
129
|
+
Five stops, each with the exact next command printed
|
|
102
130
|
under it:
|
|
103
131
|
|
|
104
132
|
- storyboard gate: needs `approve`. When the stop block prints a `Redo one
|
|
105
133
|
frame:` line with a node id, one still can be redone first with `retry-frame`;
|
|
106
134
|
a gate parked before the stills are drawn has nothing to redraw.
|
|
107
|
-
- final watch: every piece is made; needs `pull`, a cut, and `upload`.
|
|
135
|
+
- final watch: every piece is made; needs `pull`, a cut, and `upload`. Once a
|
|
136
|
+
cut is uploaded the stop block leads with `approve` instead, and `status
|
|
137
|
+
--json` says which state it is in (`stop.cutAttached`, plus the same next-step
|
|
138
|
+
lines under `guidance`). It is also
|
|
108
139
|
the one park where a finished clip can be redone, with `retry-clip`. A run
|
|
109
140
|
only reaches this park if its video node sets `finalWatch: true` — otherwise
|
|
110
141
|
it skips straight to finished.
|
|
@@ -112,6 +143,11 @@ under it:
|
|
|
112
143
|
`exodus workflow checkpoint <runId>` is the verb for that one.
|
|
113
144
|
- failed: `status` shows how far it got, and a `--wait` that was running exits
|
|
114
145
|
1 with the error. Tell the user; do not start another run on your own.
|
|
146
|
+
When the storyboard is the step that failed, `status` adds a "What happened
|
|
147
|
+
to the storyboard" block: which parts of the plan were accepted, which part
|
|
148
|
+
was turned down, and why (#1902). Relay it as written. On an admin key it may
|
|
149
|
+
also offer `status <runId> --rejected-draft`. That prints the draft the system
|
|
150
|
+
threw away. It is a record only: never build on it, edit it, or re-run it.
|
|
115
151
|
- finished: pull what it made.
|
|
116
152
|
|
|
117
153
|
**`--wait` stops at both video parks** (#1818). `workflow run --wait` ends at
|
|
@@ -203,11 +239,42 @@ anything:
|
|
|
203
239
|
|
|
204
240
|
## The script file
|
|
205
241
|
|
|
206
|
-
Plain text.
|
|
207
|
-
`
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
242
|
+
Plain text. An optional `DIRECTION:` block may open the file, before `CAST:` or
|
|
243
|
+
the first spoken line. Then a `CAST:` block naming the speakers, one line per
|
|
244
|
+
turn, then a `CTA:` block.
|
|
245
|
+
|
|
246
|
+
`DIRECTION:` carries whole-ad rules and no spoken words. The header is its own
|
|
247
|
+
line. The lines under it are the rules. The block ends at a blank line, at
|
|
248
|
+
`CAST:`, at `CTA:`, or at the first line that names a speaker. A second
|
|
249
|
+
`DIRECTION:` block is refused. So is a `DIRECTION:` after the first spoken line.
|
|
250
|
+
An empty block is refused too. The error code is `malformed-direction`.
|
|
251
|
+
|
|
252
|
+
Do not put a colon inside direction prose. The reader splits a line at its first
|
|
253
|
+
colon to find a speaker, so a colon in that prose is read as a name.
|
|
254
|
+
|
|
255
|
+
Right.
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
DIRECTION:
|
|
259
|
+
Nobody looks at the camera. The product stays on the table until the last line.
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Wrong.
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
DIRECTION:
|
|
266
|
+
Rule: nobody looks at the camera.
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The wrong line is not direction. `Rule` is read as the speaker, and the
|
|
270
|
+
block above it is empty, so the file is refused.
|
|
271
|
+
|
|
272
|
+
The planner sizes every line at 2.5 words per second (`WORD_LADDER_WPS`), so a
|
|
273
|
+
turn near 14 words plans a clip near 6 seconds. If a line does get truncated in
|
|
274
|
+
its clip, the manifest says so (`speech-cutoff`). That finding means the clip
|
|
275
|
+
cut the words off. Redo the clip. Do not speed the speech or cut it short to
|
|
276
|
+
hide the finding. The product rule is under "What the product actually
|
|
277
|
+
requires".
|
|
211
278
|
|
|
212
279
|
```
|
|
213
280
|
CAST:
|
|
@@ -221,6 +288,15 @@ CTA:
|
|
|
221
288
|
THE SOFTGEL: Tap below and give your eyes ClearBlink.
|
|
222
289
|
```
|
|
223
290
|
|
|
291
|
+
The dash in a `CAST:` line is the parser's own separator. The example above
|
|
292
|
+
already shows that dash. A cast line may separate the name from the description
|
|
293
|
+
with an em dash, an en dash, or a hyphen with a space on each side. Keep that
|
|
294
|
+
dash on cast lines. A reader who strips it makes the line fail.
|
|
295
|
+
|
|
296
|
+
Do not use a long dash in words a character says, or in words shown on screen.
|
|
297
|
+
Synthesised speech reads a long dash oddly, and so does burned-in text. A spoken
|
|
298
|
+
line stays `NAME: what they say`.
|
|
299
|
+
|
|
224
300
|
**How many speakers you may have depends on the rig.** Three by default. A rig
|
|
225
301
|
that carries saved format rules sets its own number: an ensemble rig seats more,
|
|
226
302
|
a talking-head rig seats one. Over the number, the launch refuses before
|
|
@@ -253,9 +329,13 @@ unattended.
|
|
|
253
329
|
By default a workflow run's characters have no voice of their own. A character
|
|
254
330
|
without one gets the run's default voice (the workflow's narrator voice) when the
|
|
255
331
|
run has one; with no default either, the clip keeps the voice the video model
|
|
256
|
-
invents. `exodus video voices` shows which of the three applies and is where
|
|
257
|
-
|
|
258
|
-
|
|
332
|
+
invents. `exodus video voices` shows which of the three applies, and it is where you
|
|
333
|
+
fix the mapping. Fix it before you approve the storyboard gate. Approving that
|
|
334
|
+
gate locks the mapping, and `exodus video voices` refuses afterwards. On a
|
|
335
|
+
finished clip whose voice was never applied, the later command is `exodus video
|
|
336
|
+
revoice`, and that pass bills again. A clip that already went through the voice
|
|
337
|
+
pass is refused there. Use `retry-clip` for that one. See "Redoing only the
|
|
338
|
+
voice on finished clips".
|
|
259
339
|
|
|
260
340
|
```
|
|
261
341
|
exodus video voices <runId>
|
|
@@ -329,7 +409,7 @@ nobody knows the speaker names yet. When the storyboard arrives, the voices are
|
|
|
329
409
|
already set and `exodus video voices <runId>` shows them.
|
|
330
410
|
|
|
331
411
|
**Choosing how the voices are made.** `--voice-treatment` on `workflow run` says
|
|
332
|
-
HOW a run makes its voices, and it is the only
|
|
412
|
+
HOW a run makes its voices, and it is the only way. Two
|
|
333
413
|
kinds of answer.
|
|
334
414
|
|
|
335
415
|
A name on its own picks a way of working that plays a voice you already have,
|
|
@@ -402,7 +482,8 @@ person made at a keyboard for one run, and nobody made it for the sub-workflow.
|
|
|
402
482
|
**Written voices cannot be changed at the review.** They are chosen when the run
|
|
403
483
|
starts. `exodus video voices <runId>` on a described run names the treatment and
|
|
404
484
|
prints each written voice, and says plainly that `--voice-treatment` at launch is
|
|
405
|
-
what changes them. Do not set a voice path in node config
|
|
485
|
+
what changes them. Do not set a voice path in node config. Node config does
|
|
486
|
+
not set the treatment.
|
|
406
487
|
|
|
407
488
|
**Where it shows up afterwards.** The launch receipt and `exodus workflow status`
|
|
408
489
|
both print one line naming the treatment and the speakers who got a written
|
|
@@ -480,6 +561,15 @@ manifest.json the index below
|
|
|
480
561
|
runId, pulledAt, dashboardUrl the run and the page that opens it:
|
|
481
562
|
/workflows/<workflowId>/runs/<runId>
|
|
482
563
|
storyboard, reference, music filenames, or null when not delivered
|
|
564
|
+
musicBed on | off | unknown — whether the RUN has a
|
|
565
|
+
music bed, which is not the same question
|
|
566
|
+
as the `music` filename above. "unknown" is
|
|
567
|
+
a run made before the run carried the
|
|
568
|
+
answer; it is not "off"
|
|
569
|
+
musicHeardScenes[] scene numbers whose clip came back with
|
|
570
|
+
music the video model baked in. A run with
|
|
571
|
+
the bed off can still list scenes here, and
|
|
572
|
+
those are the ones to listen to
|
|
483
573
|
narration { file, timing }, or null on a per-scene run
|
|
484
574
|
cast[] one per identity still
|
|
485
575
|
characterId, name from the run's cast lock; null for an
|
|
@@ -512,6 +602,9 @@ scenes[] one per scene, in order
|
|
|
512
602
|
on record when the voice pass replaced the
|
|
513
603
|
clip with a new file; null when the clip IS
|
|
514
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
|
|
515
608
|
provenance which saved rulebook this run followed:
|
|
516
609
|
{format: {via, nodeId, rigId, rigName,
|
|
517
610
|
rulesFrom, specVersion}}. Absent when the
|
|
@@ -541,8 +634,9 @@ envelope also carries `overlays[]` (each with a `cueLineId` and a `placement`)
|
|
|
541
634
|
and `audio` (`roomTone`, `music`, `sfxCues[]`) — the planner's notes on what
|
|
542
635
|
should sit on top. The image and motion prompts (`framePrompt`, `videoPrompt`,
|
|
543
636
|
`referencePrompt`) reach an admin key in full. A caller who cannot use video on
|
|
544
|
-
this brand gets them stripped before the file arrives
|
|
545
|
-
(`redactVideoIp` in `convex/workflows.ts`).
|
|
637
|
+
this brand gets them stripped before the file arrives. They are private Style
|
|
638
|
+
fragment IP (`redactVideoIp` in `convex/workflows.ts`). Style pack is the
|
|
639
|
+
retired name.
|
|
546
640
|
|
|
547
641
|
**The narration track** (`narration.mp3` + `narration.json`). Present when the
|
|
548
642
|
run recorded continuously: every narrated scene's `voText` joined into one
|
|
@@ -569,9 +663,9 @@ speech inside the clip — under the default voice treatment the video model's
|
|
|
569
663
|
invented voice is then replaced, in place, with the cast member's pinned
|
|
570
664
|
ElevenLabs voice (that is `revoiced: true`). Other treatments leave the render
|
|
571
665
|
owning its own audio, e.g. lip-sync retargeting a VO track onto the video. The
|
|
572
|
-
treatment is frozen on the run when it starts, and there is exactly one
|
|
666
|
+
treatment is frozen on the run when it starts, and there is exactly one flag
|
|
573
667
|
that sets it: `--voice-treatment` on `workflow run` (see **Choosing how the
|
|
574
|
-
voices are made**). Node config
|
|
668
|
+
voices are made**). Node config never sets it. Nothing on the canvas sets a
|
|
575
669
|
voice treatment, and a run that was given none renders the default. Read
|
|
576
670
|
`wordsFrom` and `revoiced` per scene rather than assuming.
|
|
577
671
|
|
|
@@ -633,6 +727,11 @@ blocking the run.
|
|
|
633
727
|
A `speech-cutoff` on the CTA scene is the one to worry about; the ad's last
|
|
634
728
|
words are missing. `status` shows the checker's own wording behind each
|
|
635
729
|
finding; the manifest carries only the verdict.
|
|
730
|
+
- A `pass` verdict is not a review. The checks are often optimistic. Watch the
|
|
731
|
+
frames and listen to the audio before you call the run good. Two checks fail
|
|
732
|
+
the wrong way. A cast-count check can reject a silent
|
|
733
|
+
listener who belongs in the shot. A clip check can flag a clip that is fine.
|
|
734
|
+
Read the finding, then watch the scene.
|
|
636
735
|
- `clipStatus: "failed"` or `"missing"` with a `keyframe`: `durationSec` is null
|
|
637
736
|
(it comes from the clip), but the scene can still hold its place as a still —
|
|
638
737
|
for the length of its voice track, or the storyboard's planned duration when
|
|
@@ -650,6 +749,7 @@ is no second run, no new storyboard and no re-render of anything else.
|
|
|
650
749
|
exodus video retry-clip <runId> --scene 3
|
|
651
750
|
exodus video retry-clip <runId> --scene 3 --note "keep both hands in frame on the handshake"
|
|
652
751
|
exodus video retry-clip <runId> --scene 3 --node video-2
|
|
752
|
+
exodus video retry-clip <runId> --scene 3 --voice-first
|
|
653
753
|
```
|
|
654
754
|
|
|
655
755
|
`--node` is only needed when a run has clips for that scene on two video steps;
|
|
@@ -672,13 +772,26 @@ revoiced and trimmed the same way, with fresh findings and fresh word timings.
|
|
|
672
772
|
scene's motion prompt only (`withRedoDirection`,
|
|
673
773
|
`scout/src/trigger/workflow-scene-retry.ts`); the spoken line still comes from
|
|
674
774
|
the approved storyboard. Asking for different words in a note will not change
|
|
675
|
-
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.
|
|
676
786
|
|
|
677
787
|
**A redo whose take fails the checks keeps the original clip.** On a finished
|
|
678
788
|
clip the row stays `done` with the ORIGINAL take and only its findings are
|
|
679
789
|
refreshed, because a worse take must never replace a delivered one. So never
|
|
680
|
-
report that the clip changed
|
|
681
|
-
|
|
790
|
+
report that the clip changed without reading `status` again: under the scene it
|
|
791
|
+
prints what the redo did, `new take accepted (take N)` or `new take rejected,
|
|
792
|
+
original kept (take N)`, and `status --json` carries the same sentence on the
|
|
793
|
+
row as `lastRedo.label`. A row with no `lastRedo` was redone before outcomes
|
|
794
|
+
were recorded; for those, pull and compare the file before and after.
|
|
682
795
|
|
|
683
796
|
**Re-cut afterwards.** A cut you already uploaded still holds the old clip, and
|
|
684
797
|
the CLI says so when that is the case. Pull again, re-cut and upload again before
|
|
@@ -756,7 +869,10 @@ they want; if they have not said and are not around, say which you used and why.
|
|
|
756
869
|
- **HyperFrames** — HTML/CSS compositions rendered to video. Suits an ad that
|
|
757
870
|
needs captions, title cards, overlays or motion graphics laid over the clips.
|
|
758
871
|
It is a separate skill pack; check that a `hyperframes` skill is installed
|
|
759
|
-
before offering it.
|
|
872
|
+
before offering it. The cut is made outside Exodus. `exodus video pull` is the
|
|
873
|
+
handover. The folder it writes is what the next tool reads. When a HyperFrames
|
|
874
|
+
skill pack is installed, that pack is the route for an HTML and CSS
|
|
875
|
+
composition. Do not look for a stitch inside Exodus.
|
|
760
876
|
- **An NLE over MCP** — DaVinci Resolve publishes an MCP server, so a timeline
|
|
761
877
|
can be built in a real editor and the user can carry on by hand afterwards.
|
|
762
878
|
Suits a user who wants to keep editing after you stop. Check what MCP servers
|
|
@@ -853,6 +969,11 @@ lines, re-recording narration, or cutting words out of the CTA will ship,
|
|
|
853
969
|
silently. Cut the pictures to the words, not the words to the pictures. If the
|
|
854
970
|
words are wrong, that is a new run, not an edit.
|
|
855
971
|
|
|
972
|
+
Never stretch speech, speed it up, slow it down, or cut it short so it fits a
|
|
973
|
+
clip. That is a product rule, and it holds for every tool that makes the cut,
|
|
974
|
+
including the bundled script. If the audio does not fit, redo the clip with
|
|
975
|
+
`exodus video retry-clip`. Do not repair the fit in the editor.
|
|
976
|
+
|
|
856
977
|
## Reporting to the user
|
|
857
978
|
|
|
858
979
|
Lead with where the run is and what it needs from them. Then, when you pulled
|
|
@@ -161,7 +161,7 @@ Accepts a workflow id **or** a name. It reports three things:
|
|
|
161
161
|
|
|
162
162
|
Fill every required input. For a free-text brief you can compose a **bespoke brief from in-session research** — the winning angle you just worked out, the swipe you analyzed — and pass it inline (`--input brief="..."`) or, if it's long, write it to a project file and pass it with `@file` (see Chain, below). For URL/swipe sources, pass the value the source expects.
|
|
163
163
|
|
|
164
|
-
**A brief wired straight into a Storyboard is a script, not a prompt.** On a video workflow whose Storyboard takes its `script` port from a Brief node, the text you pass for that brief boards VERBATIM, the same way a Show ad's pasted script does: one speaker per scene, every line rendered once in order, long lines split across clips, narrated lines rendered silent under the voice track. So it must be in the script form the `exodus-video` skill documents (an optional `CAST:` block of `NAME — description` lines, then `NAME:` turns, then a `CTA:` block; `NARRATOR:` lines only for Narrated Story and Explainer). A brief that is not a script, or one that does not fit the wired rig's family, is refused at launch with the parser's own message and nothing is spent. Write the script to a file and pass it with `--input script=@script.txt`. A Storyboard fed by a bot is unaffected.
|
|
164
|
+
**A brief wired straight into a Storyboard is a script, not a prompt.** On a video workflow whose Storyboard takes its `script` port from a Brief node, the text you pass for that brief boards VERBATIM, the same way a Show ad's pasted script does: one speaker per scene, every line rendered once in order, long lines split across clips, narrated lines rendered silent under the voice track. So it must be in the script form the `exodus-video` skill documents (an optional `DIRECTION:` block with no colon in its prose, then an optional `CAST:` block of `NAME — description` lines, then `NAME:` turns, then a `CTA:` block; `NARRATOR:` lines only for Narrated Story and Explainer). A brief that is not a script, or one that does not fit the wired rig's family, is refused at launch with the parser's own message and nothing is spent. Write the script to a file and pass it with `--input script=@script.txt`. A Storyboard fed by a bot is unaffected.
|
|
165
165
|
|
|
166
166
|
**A saved rig can bring a rulebook with it.** Some saved rigs carry saved format rules: how many speakers the script may have, which checks run on the clips, how the cast is built. Point a Rig box at one of those rigs and the run follows its rules — an ensemble rig seats more speakers than the usual three, a talking-head rig seats one. The start receipt says which rulebook ran:
|
|
167
167
|
|
package/dist/commands/genesis.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { createInterface } from "node:readline/promises";
|
|
4
3
|
import { apiGet, apiPost, getDashboardUrl } from "../lib/client.js";
|
|
5
4
|
import { pollUntilDone } from "../lib/poll.js";
|
|
6
5
|
import { displayRunStatus, formatGenesisRun, formatError, tickerRunStatus } from "../lib/format.js";
|
|
@@ -278,52 +277,13 @@ export async function run(flags) {
|
|
|
278
277
|
return submitGenesis(action.body, { noWait, cc });
|
|
279
278
|
}
|
|
280
279
|
}
|
|
281
|
-
async function listBank(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
console.log(formatError(res));
|
|
285
|
-
process.exit(1);
|
|
286
|
-
}
|
|
287
|
-
const ideas = Array.isArray(res.data.ideas) ? res.data.ideas : [];
|
|
288
|
-
if (ideas.length === 0) {
|
|
289
|
-
console.log("Bank is empty.");
|
|
290
|
-
console.log('Fill it with: exodus genesis scrape (or paste a reel: exodus genesis --reel "<url>")');
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
console.log(`## Idea Bank (${ideas.length} reel${ideas.length === 1 ? "" : "s"})`);
|
|
294
|
-
console.log("");
|
|
295
|
-
for (const i of ideas) {
|
|
296
|
-
const hook = i.hook ?? "(no hook)";
|
|
297
|
-
const user = typeof i.sourceUsername === "string" && i.sourceUsername.length > 0
|
|
298
|
-
? ` @${i.sourceUsername.replace(/^@+/, "")}`
|
|
299
|
-
: "";
|
|
300
|
-
const score = typeof i.relevanceScore === "number" ? ` score=${i.relevanceScore.toFixed(2)}` : "";
|
|
301
|
-
const used = typeof i.useCount === "number" && i.useCount > 0 ? " · used" : "";
|
|
302
|
-
console.log(` • ${hook}${user}${score}`);
|
|
303
|
-
console.log(` id: ${i._id ?? "?"} [${i.status ?? "new"}${used}]`);
|
|
304
|
-
if (i.sourceUrl)
|
|
305
|
-
console.log(` ${i.sourceUrl}`);
|
|
306
|
-
}
|
|
307
|
-
console.log("");
|
|
308
|
-
console.log("Write from one: exodus genesis --from-bank --idea <id>");
|
|
280
|
+
async function listBank(_limit) {
|
|
281
|
+
console.error(`This command has been retired. View the idea bank in the dashboard: ${getDashboardUrl()}/idea-bank`);
|
|
282
|
+
process.exit(1);
|
|
309
283
|
}
|
|
310
|
-
async function runFromBank(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
console.log(formatError(res));
|
|
314
|
-
process.exit(1);
|
|
315
|
-
}
|
|
316
|
-
const idea = (res.data.ideas ?? [])[0];
|
|
317
|
-
if (!idea) {
|
|
318
|
-
console.error(`No banked idea found for id "${ideaId}". List the bank: exodus genesis --from-bank`);
|
|
319
|
-
process.exit(1);
|
|
320
|
-
}
|
|
321
|
-
if (!idea.sourceUrl) {
|
|
322
|
-
console.error(`Banked idea "${ideaId}" has no source reel URL to write from.`);
|
|
323
|
-
process.exit(1);
|
|
324
|
-
}
|
|
325
|
-
console.log(`Writing from banked reel: ${idea.sourceUrl}`);
|
|
326
|
-
return runReel([idea.sourceUrl], opts, rt);
|
|
284
|
+
async function runFromBank(_ideaId, _opts, _rt) {
|
|
285
|
+
console.error(`This command has been retired. Write from banked ideas in the dashboard: ${getDashboardUrl()}/idea-bank`);
|
|
286
|
+
process.exit(1);
|
|
327
287
|
}
|
|
328
288
|
async function runReel(urls, opts, rt) {
|
|
329
289
|
console.log(`Transcribing ${urls.length === 1 ? "the reel" : `${urls.length} reels`} → idea → Genesis…`);
|
|
@@ -613,92 +573,11 @@ function requireActiveBrand() {
|
|
|
613
573
|
}
|
|
614
574
|
return slug;
|
|
615
575
|
}
|
|
616
|
-
async function runConnectInstagram(
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
if (!start.ok || !start.data.liveViewUrl) {
|
|
620
|
-
console.log(formatError(start));
|
|
621
|
-
process.exit(1);
|
|
622
|
-
}
|
|
623
|
-
console.log(`\nConnecting an Instagram account to brand "${client}".`);
|
|
624
|
-
console.log("\n1. Open this link in your browser:\n");
|
|
625
|
-
console.log(` ${start.data.liveViewUrl}\n`);
|
|
626
|
-
console.log('2. Go to instagram.com and log into the account for THIS brand.');
|
|
627
|
-
console.log("3. Scroll your feed for a bit so the algorithm learns the niche.");
|
|
628
|
-
console.log("4. Come back here and press Enter.\n");
|
|
629
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
630
|
-
try {
|
|
631
|
-
await rl.question("Press Enter once you're logged in and done grooming... ");
|
|
632
|
-
let username = typeof flags["username"] === "string" ? flags["username"].trim() : "";
|
|
633
|
-
while (!username) {
|
|
634
|
-
username = (await rl.question("Instagram handle you just logged in as (e.g. mybrand): ")).trim();
|
|
635
|
-
}
|
|
636
|
-
const finish = await apiPost("/api/v2/genesis/instagram/connect/finish", { client, igUsername: username }, { ccCommand: cc });
|
|
637
|
-
if (!finish.ok) {
|
|
638
|
-
console.log(formatError(finish));
|
|
639
|
-
process.exit(1);
|
|
640
|
-
}
|
|
641
|
-
console.log(`\n✓ Instagram @${username.replace(/^@+/, "")} connected to "${client}".`);
|
|
642
|
-
console.log("Fill the idea bank now: exodus genesis scrape --organic");
|
|
643
|
-
}
|
|
644
|
-
finally {
|
|
645
|
-
rl.close();
|
|
646
|
-
}
|
|
576
|
+
async function runConnectInstagram(_flags, _cc) {
|
|
577
|
+
console.error(`This command has been retired. Connect Instagram accounts in the dashboard: ${getDashboardUrl()}/settings`);
|
|
578
|
+
process.exit(1);
|
|
647
579
|
}
|
|
648
|
-
async function runScrape(
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
if (flags["organic"] === true) {
|
|
652
|
-
const smoke = flags["smoke"] === true;
|
|
653
|
-
const res = await apiPost("/api/v2/scout/organic", { client, ...(smoke ? { smoke: true } : {}) }, { ccCommand: cc });
|
|
654
|
-
if (!res.ok || !res.data.runId) {
|
|
655
|
-
console.log(formatError(res));
|
|
656
|
-
process.exit(1);
|
|
657
|
-
}
|
|
658
|
-
console.log(`Organic scrape started for "${client}": ${res.data.runId}${smoke ? " (smoke)" : ""}`);
|
|
659
|
-
return pollScrape(res.data.runId, noWait, 1_800_000);
|
|
660
|
-
}
|
|
661
|
-
const term = typeof flags["term"] === "string" ? flags["term"] : undefined;
|
|
662
|
-
const body = { sourceMode: "fresh", client };
|
|
663
|
-
if (term)
|
|
664
|
-
body.term = term;
|
|
665
|
-
const res = await apiPost("/api/v2/scout", body, { ccCommand: cc });
|
|
666
|
-
if (!res.ok || !res.data.runId) {
|
|
667
|
-
console.log(formatError(res));
|
|
668
|
-
process.exit(1);
|
|
669
|
-
}
|
|
670
|
-
console.log(`Discovery scrape started for "${client}": ${res.data.runId}`);
|
|
671
|
-
return pollScrape(res.data.runId, noWait, 1_200_000);
|
|
672
|
-
}
|
|
673
|
-
async function pollScrape(runId, noWait, timeoutMs) {
|
|
674
|
-
if (noWait) {
|
|
675
|
-
console.log(`Run ID: ${runId}`);
|
|
676
|
-
console.log(`Check status: exodus status --type scout --id ${runId}`);
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
console.log("Polling for completion...");
|
|
680
|
-
const result = await pollUntilDone({
|
|
681
|
-
path: `/api/v2/scout?runId=${runId}`,
|
|
682
|
-
intervalMs: 15_000,
|
|
683
|
-
timeoutMs,
|
|
684
|
-
onProgress: (data) => {
|
|
685
|
-
const status = data["status"];
|
|
686
|
-
const captured = data["organicCaptured"];
|
|
687
|
-
const qualified = data["organicQualified"];
|
|
688
|
-
const counts = captured !== undefined || qualified !== undefined
|
|
689
|
-
? ` captured=${captured ?? 0} qualified=${qualified ?? 0}`
|
|
690
|
-
: "";
|
|
691
|
-
if (status)
|
|
692
|
-
process.stdout.write(`\r status: ${tickerRunStatus(status)}${counts} `);
|
|
693
|
-
},
|
|
694
|
-
});
|
|
695
|
-
console.log();
|
|
696
|
-
if (result.timedOut) {
|
|
697
|
-
console.log(`Timed out waiting. Check later: exodus status --type scout --id ${runId}`);
|
|
698
|
-
}
|
|
699
|
-
else {
|
|
700
|
-
console.log(`Done. Banked reels are ready to write: exodus genesis --from-bank`);
|
|
701
|
-
}
|
|
702
|
-
if (!result.ok && !result.timedOut)
|
|
703
|
-
process.exit(1);
|
|
580
|
+
async function runScrape(_flags, _cc) {
|
|
581
|
+
console.error(`This command has been retired. Fill the idea bank from the Mining page in the dashboard: ${getDashboardUrl()}/mining`);
|
|
582
|
+
process.exit(1);
|
|
704
583
|
}
|
package/dist/commands/image.js
CHANGED
|
@@ -184,7 +184,7 @@ async function runTemplate(text, flags, who) {
|
|
|
184
184
|
renderMode: "images",
|
|
185
185
|
inputAds: text,
|
|
186
186
|
aspectRatio: aspect,
|
|
187
|
-
model: "gpt-image-2",
|
|
187
|
+
model: "gpt-image-2.5",
|
|
188
188
|
realismMode: realisticFlag ? "realistic" : "off",
|
|
189
189
|
...(steering ? { steering } : {}),
|
|
190
190
|
...(requestedImageCount !== undefined ? { requestedImageCount } : {}),
|
package/dist/commands/status.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { apiGet } from "../lib/client.js";
|
|
1
|
+
import { apiGet, getDashboardUrl } from "../lib/client.js";
|
|
2
2
|
import { formatGeneration, formatGenesisRun, formatError, displayRunStatus } from "../lib/format.js";
|
|
3
|
-
import { hydrateScoutIdeasCount } from "../lib/scout-hydrate.js";
|
|
4
3
|
import { formatIntelResult as formatIntelResultRich } from "../lib/intel-format.js";
|
|
5
4
|
export const helpText = `
|
|
6
5
|
exodus status — Check the status of a pipeline run
|
|
@@ -12,19 +11,13 @@ Required:
|
|
|
12
11
|
--id <runId> The generation or run ID returned by a prior exodus command
|
|
13
12
|
|
|
14
13
|
Options:
|
|
15
|
-
--type <pipeline> Pipeline type: generation (default) | genesis | intel | pulse
|
|
14
|
+
--type <pipeline> Pipeline type: generation (default) | genesis | intel | pulse
|
|
16
15
|
| creative | template
|
|
17
16
|
Spark, Viral, Mirror, Remix runs use --type generation (default).
|
|
18
|
-
Intel, Pulse
|
|
17
|
+
Intel, Pulse require --type intel|pulse.
|
|
19
18
|
Creative-suite (native/copy-derived/ref-match/meme)
|
|
20
19
|
use --type creative; Template uses --type template.
|
|
21
20
|
`.trim();
|
|
22
|
-
async function fetchScoutIdeasCount(runId) {
|
|
23
|
-
const res = await apiGet(`/api/v2/scout/ideas?runId=${runId}&limit=1`);
|
|
24
|
-
if (res.ok && typeof res.data.count === "number")
|
|
25
|
-
return res.data.count;
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
21
|
export function formatIntelResult(data) {
|
|
29
22
|
return formatIntelResultRich(data).replace("## Intel Analysis Result", "## Intel Status");
|
|
30
23
|
}
|
|
@@ -157,18 +150,21 @@ export async function run(flags) {
|
|
|
157
150
|
process.exit(1);
|
|
158
151
|
}
|
|
159
152
|
const type = flags["type"] ?? "generation";
|
|
153
|
+
if (type === "scout") {
|
|
154
|
+
console.error(`--type scout has been retired. Check run status in the dashboard: ${getDashboardUrl()}/runs`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
160
157
|
const pathMap = {
|
|
161
158
|
generation: `/api/v2/generations?id=${id}`,
|
|
162
159
|
genesis: `/api/v2/genesis?id=${id}`,
|
|
163
160
|
intel: `/api/v2/intel?id=${id}`,
|
|
164
161
|
pulse: `/api/v2/pulse?id=${id}`,
|
|
165
|
-
scout: `/api/v2/scout?runId=${id}`,
|
|
166
162
|
creative: `/api/v2/creative?runId=${id}`,
|
|
167
163
|
template: `/api/v2/template?runId=${id}`,
|
|
168
164
|
};
|
|
169
165
|
const path = pathMap[type];
|
|
170
166
|
if (!path) {
|
|
171
|
-
console.error(`Unknown type: "${type}". Valid types: generation, genesis, intel, pulse,
|
|
167
|
+
console.error(`Unknown type: "${type}". Valid types: generation, genesis, intel, pulse, creative, template`);
|
|
172
168
|
process.exit(1);
|
|
173
169
|
}
|
|
174
170
|
const res = await apiGet(path);
|
|
@@ -176,9 +172,6 @@ export async function run(flags) {
|
|
|
176
172
|
console.log(formatError(res));
|
|
177
173
|
process.exit(1);
|
|
178
174
|
}
|
|
179
|
-
if (type === "scout") {
|
|
180
|
-
await hydrateScoutIdeasCount(id, res.data, fetchScoutIdeasCount);
|
|
181
|
-
}
|
|
182
175
|
switch (type) {
|
|
183
176
|
case "generation":
|
|
184
177
|
console.log(formatGeneration(res.data));
|
|
@@ -192,9 +185,6 @@ export async function run(flags) {
|
|
|
192
185
|
case "pulse":
|
|
193
186
|
console.log(formatPulseResult(res.data));
|
|
194
187
|
break;
|
|
195
|
-
case "scout":
|
|
196
|
-
console.log(formatScoutResult(res.data));
|
|
197
|
-
break;
|
|
198
188
|
case "creative":
|
|
199
189
|
console.log(formatCreativeSuiteStatus("Creative Suite Status", res.data));
|
|
200
190
|
break;
|
|
@@ -17,7 +17,8 @@ Run options:
|
|
|
17
17
|
--mode auto|manual Generation mode (default: auto)
|
|
18
18
|
--render-mode images|prompts Skip render and return prompts only (default: images)
|
|
19
19
|
--aspect 1:1|9:16 Image aspect ratio (default: 1:1)
|
|
20
|
-
--model gpt-image-2|
|
|
20
|
+
--model gpt-image-2.5|gpt-image-2.5-sunburst|gpt-image-2|nano-banana-pro
|
|
21
|
+
Kie.ai model (default: gpt-image-2.5)
|
|
21
22
|
--realism off|realistic Realism enforcement (default: off)
|
|
22
23
|
--quantities <slug:N,slug:N> Manual-mode per-type counts (e.g. "testimonial:3,hero:2")
|
|
23
24
|
--requested-count N Auto-mode total render target (optional)
|
|
@@ -103,9 +104,15 @@ async function runTemplate(flags) {
|
|
|
103
104
|
process.exit(1);
|
|
104
105
|
return;
|
|
105
106
|
}
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
const TEMPLATE_MODELS = [
|
|
108
|
+
"gpt-image-2.5",
|
|
109
|
+
"gpt-image-2.5-sunburst",
|
|
110
|
+
"gpt-image-2",
|
|
111
|
+
"nano-banana-pro",
|
|
112
|
+
];
|
|
113
|
+
const model = typeof flags["model"] === "string" ? flags["model"] : "gpt-image-2.5";
|
|
114
|
+
if (!TEMPLATE_MODELS.includes(model)) {
|
|
115
|
+
console.error(`Error: --model must be one of ${TEMPLATE_MODELS.join(" | ")} (got "${model}").`);
|
|
109
116
|
process.exit(1);
|
|
110
117
|
return;
|
|
111
118
|
}
|