@lalalic/markcut 2.5.1 → 2.7.0

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/AGENTS.md CHANGED
@@ -214,20 +214,19 @@ node src/render/cli.mjs templates
214
214
 
215
215
  1. Basic Rendering — empty composition, dimension verification
216
216
  2. Image + Subtitle — image rendering, inline/VTT/karaoke subtitles
217
- 3. Built-in Components — AnimatedHeadline, StatCounter, GradientBackground
218
- 4. Effects — fadeIn, bounceIn, custom keyframes
219
- 5. Map Rendering — canvas route visualization
220
- 6. Audio Rendering — audio track, non-silent verification
221
- 7. Include — nested composition via include
222
- 8. Full Feature Combination — multiple features
223
- 9. Scene Node — scene-based rendering
224
- 10. Audio STT — speech-to-text verification via whisper
225
- 11. Multiple Aspect Ratios — 16x9, 9x16, 1x1
226
- 12. Cross-Stream Types — parallel rendering, effect-wrapped children
227
- 13. Frame-Accurate — timeline consistency, frame count
228
- 14. Description Field — schema validation, render preservation
229
- 15. Scene as Folder Alias — scene/folder interchangeability
230
- 16. **Video startFrom/endAt Trimming** — video trimming + photo in series
217
+ 3. Effects — fadeIn, bounceIn, custom keyframes
218
+ 4. Map Rendering — canvas route visualization
219
+ 5. Audio Rendering — audio track, non-silent verification
220
+ 6. Include — nested composition via include
221
+ 7. Full Feature Combination — multiple features
222
+ 8. Scene Node — scene-based rendering
223
+ 9. Audio STT — speech-to-text verification via whisper
224
+ 10. Multiple Aspect Ratios — 16x9, 9x16, 1x1
225
+ 11. Cross-Stream Types — parallel rendering, effect-wrapped children
226
+ 12. Frame-Accurate — timeline consistency, frame count
227
+ 13. Description Field — schema validation, render preservation
228
+ 14. Scene as Folder Alias — scene/folder interchangeability
229
+ 15. **Video startFrom/endAt Trimming** — video trimming + photo in series
231
230
 
232
231
  ### Running Tests
233
232
 
@@ -242,9 +241,9 @@ npx vitest run tests/render.test.ts -t "Test Name" # single test
242
241
  1. **TransitionSeries + isBackground**: `TransitionSeries` from `@remotion/transitions` rejects non-Sequence/Transition children. Don't mix `isBackground:true` children with `isSeries:true, transition:"..."` at the same level.
243
242
  2. **Video endAt formula**: Must be `startFrom_frames + ((endAt_source - startFrom_source) * fps / playbackRate)` — changing to `endAt_source * fps` breaks slow-motion playback.
244
243
  3. **`signalstats` ffmpeg filter**: Doesn't output YAVG on all systems; use file size as proxy for visual content.
245
- 4. **Asset paths**: All `src` paths in markdown are resolved relative to the `.md` file's directory. Preview mode serves files via a multi-directory HTTP server; render mode uses absolute paths directly (bypassing `staticFile()`). Never use a `public/` prefix in markdown `src`.
244
+ 4. **Asset paths (unified preview + render)**: All `src` paths in markdown are resolved relative to the `.md` file's directory. Both surfaces store every project-local `src` **relative to the `.md` folder** (e.g. `.markcut/generated/tts/x.mp3`, `assets/pic.png`). Preview: the server's `makePathsRelative` strips the project dir and `resolveAssetPaths`/`toServerUrl` turn them into `/`-rooted URLs served from the project folder. Render: `cli.mjs relativizePaths(streamTree, mdDir)` does the same, then Remotion is invoked with `--public-dir=<mdDir>` so `staticFile()` resolves them (including `fetch()`-based assets like subtitles and includes). Never use a `public/` prefix in markdown `src`.
246
245
  5. **Map rendering**: Depends on external tile availability; may produce blank frames offline.
247
- 6. **CLI md render needs two things preview gets for free**: (a) `parseMarkdownVariants(raw)` (returns `{base, variants}`) — `parseMarkdownDescriptive` returns the root directly; (b) bundling the ` ```js imports ``` ` block via `bundleFromEntries` and setting `root.imports` (otherwise jsx components are unregistered → slides render as unstyled black frames). Asset staging (`stageLocalAssets`) was removed — render now keeps absolute paths from `resolve.ts`, bypassing `staticFile()` entirely. All live in `src/render/cli.mjs`; the import-map shim for shared react/remotion instances is `src/utils/component-import-map.ts` (must stay in sync with `player/browser.tsx` + `bundler.mjs getSharedExternals`).
246
+ 6. **CLI md render needs two things preview gets for free**: (a) `parseMarkdownVariants(raw)` (returns `{base, variants}`) — `parseMarkdownDescriptive` returns the root directly; (b) bundling the ` ```js imports ``` ` block via `bundleFromEntries` and setting `root.imports` (otherwise jsx components are unregistered → slides render as unstyled black frames). Render and preview now share one folder layout **and** one path scheme: generated artifacts live in `.markcut/generated/{tts,media,includes,components}` (content-addressed, shared caches) and per-variant subtitles/compiled.json in `.markcut/<basename>/<label>/` (label defaults to `default`). Render makes paths relative to the `.md` folder and serves it via `--public-dir` (see gotcha 4) — do NOT revert to absolute paths, which break `fetch()`-based assets (subtitles, includes) that hit the render server origin and 404. All live in `src/render/cli.mjs`; the import-map shim for shared react/remotion instances is `src/utils/component-import-map.ts` (must stay in sync with `player/browser.tsx` + `bundler.mjs getSharedExternals`).
248
247
  7. **Publishing**: verify `npx @lalalic/markcut render <md>` from a clean temp dir before/after publish — v1.1.1 shipped with cli.mjs/pipeline.mjs out of sync and md rendering was completely broken. Also beware stale npx caches (`~/.npm/_npx`) silently running old code.
249
248
  8. **Clean break from `actions[]`**: leaf timing is now flat base fields (`start`/`end`/`duration`/`startFrom`/`endAt`). Old compiled JSON carrying `actions:[{...}]` no longer parses into a duration — re-compile from markdown/descriptive JSON, or migrate `actions[0]` → flat fields (drop `id`/`volume`/`style`/`effectId`; keep `start`/`end`/`startFrom`/`endAt`/`loop`).
250
249
 
package/README.md CHANGED
@@ -215,6 +215,95 @@ src/
215
215
  ```bash
216
216
  npm run render # render a JSON stream tree to MP4
217
217
  npm run preview # open Remotion Studio
218
+ npm run vision # analyze images/videos in a folder (metadata + AI perception)
218
219
  npm run typecheck # TypeScript type check
219
220
  npm test # run unit + integration tests
220
221
  ```
222
+
223
+ ## Vision Pipeline
224
+
225
+ ```bash
226
+ # Full pipeline: extract metadata → normalize → AI perception (images & videos)
227
+ npx markcut vision <folder>
228
+
229
+ # With interactive labeling step before AI
230
+ npx markcut vision <folder> --label
231
+
232
+ # Provide context about people/places
233
+ npx markcut vision <folder> --instruct "the girl is Maggie, the man in red is me"
234
+ ```
235
+
236
+ ```mermaid
237
+ flowchart LR
238
+ subgraph Input[Input]
239
+ A[Media Folder<br/>images + videos]
240
+ end
241
+
242
+ subgraph Metadata["1. Metadata Extraction"]
243
+ B[exiftool / ffprobe]
244
+ C[metadata.json<br/>width, height, created,<br/>GPS location, duration]
245
+ end
246
+
247
+ subgraph Normalize["2. Normalization"]
248
+ D[ffmpeg resize<br/>images → 384px<br/>videos → 360p]
249
+ end
250
+
251
+ subgraph Label["Optional: Labeling"]
252
+ E[Build preview tree<br/>with seed]
253
+ F[Label preview server<br/>user annotates scenes]
254
+ G[Merge labels → userHints<br/>into metadata.json]
255
+ end
256
+
257
+ subgraph Image["3. Image Perception"]
258
+ H[ITT: image-to-text<br/>mlx-vlm model]
259
+ I[Perception saved:<br/>description]
260
+ end
261
+
262
+ subgraph Video["4. Video Understanding"]
263
+ J[VTT: video-to-text<br/>mlx-vlm model]
264
+ K[STT: speech-to-text<br/>whisper]
265
+ L[VTT→ overall desc]
266
+ M[STT → VTT subtitles]
267
+ N[Build merged segment<br/>boundaries]
268
+ O[LLM merges boundaries<br/>into final segments]
269
+ P[Per-segment vision<br/>clip extraction + ITT]
270
+ end
271
+
272
+ subgraph Output[Output]
273
+ Q[metadata.json<br/>with perception desc,<br/>subtitles & segments]
274
+ end
275
+
276
+ A --> B --> C --> D
277
+ D --> Image
278
+ D --> Video
279
+ C -.->|--label| E --> F --> G --> D
280
+ Image --> H --> I --> Q
281
+ Video --> J --> L
282
+ Video --> K --> M
283
+ L --> N
284
+ M --> N
285
+ N --> O --> P --> Q
286
+
287
+ style Input fill:#1a1a2e,stroke:#16213e,color:#fff
288
+ style Metadata fill:#0f3460,stroke:#16213e,color:#fff
289
+ style Normalize fill:#533483,stroke:#16213e,color:#fff
290
+ style Label fill:#e94560,stroke:#16213e,color:#fff
291
+ style Image fill:#0f3460,stroke:#16213e,color:#fff
292
+ style Video fill:#0f3460,stroke:#16213e,color:#fff
293
+ style Output fill:#1a1a2e,stroke:#16213e,color:#fff
294
+ ```
295
+
296
+ ### Vision CLI Options
297
+
298
+ | Option | Description |
299
+ |---|---|
300
+ | `--label` | Add interactive labeling step before AI pipeline (opens browser preview) |
301
+ | `--instruct "text"` | Background context about people/places (injected into AI prompts) |
302
+ | `--prompts-file <path>` | Custom prompts markdown file (default: `src/vision/vision_prompts.md`) |
303
+ | `--vtt-sample-interval <n>` | Sample one video frame every N seconds for VTT (default: 5) |
304
+ | `--pick <files>` | Comma-separated filenames to process (skip others) |
305
+ | `--skip-stt` | Skip speech-to-text for videos |
306
+ | `--dry-run` | Show what would be processed without running AI |
307
+ | `--show-prompts` | Print the prompts template file and exit |
308
+
309
+ ## Architecture
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lalalic/markcut",
3
- "version": "2.5.1",
3
+ "version": "2.7.0",
4
4
  "description": "Markdown-to-video engine. Describe scenes in markdown, get a rendered video.",
5
5
  "bin": {
6
6
  "markcut": "bin/markcut"
@@ -45,7 +45,7 @@ npx @lalalic/markcut --show-clis # get command specific information
45
45
  npx @lalalic/markcut preview <file> # assemble and preview the video with a local server, and chat to edit the video and auto refresh
46
46
  npx @lalalic/markcut render <file> # render the video to mp4
47
47
  npx @lalalic/markcut vision <folder> # vision understanding medias in folder
48
- npx @lalalic/markcut vision <folder> --label # full pipeline vision understanding medias in folder, and provide ui to label the medias with text, time ranges
48
+ npx @lalalic/markcut vision <folder> --label # an extra step to provide UI to label the medias with text, time ranges
49
49
 
50
50
  ```
51
51
 
@@ -16,7 +16,7 @@ This section defines the markcut Markdown Descriptive grammar. An agent should r
16
16
  │ ───...───
17
17
  ├─ # video ← Root node (REQUIRED, heading level 1)
18
18
  │ width:1080 height:1920 fps:30 layout:series
19
- │ subtitle:{src:"captions.vtt",type:"Typewriter"}
19
+ │ subtitle:{type:"Typewriter"}
20
20
  │ ~~~css stylesheet ← root.stylesheet
21
21
  │ .bg { background: #000; }
22
22
  │ ~~~
@@ -223,7 +223,7 @@ The line after `# video` holds canvas-level config as space-separated `key:value
223
223
  ```markdown
224
224
  # video
225
225
  width:1920 height:1080 fps:30 layout:series transition:fade transitionTime:1.2
226
- subtitle:{src:"captions.vtt",type:"Typewriter",fontSize:48}
226
+ subtitle:{type:"Typewriter",fontSize:48}
227
227
  ```
228
228
 
229
229
  Supported root keys: `width`, `height`, `fps`, `layout`, `tts`, `stt`, `tti`, `ttv`, `transition`, `transitionTime`, `instruction`, `metadata`, `stylesheet`, `subtitle`, `voices`.
@@ -0,0 +1,74 @@
1
+ import * as React from "react";
2
+
3
+ /**
4
+ * Built-in component rendered by `--storyboard` preview mode instead of a
5
+ * TTS-generated audio clip. Shows the script text as a caption-style bar at
6
+ * the bottom of the frame.
7
+ *
8
+ * Usage in compiled JSON:
9
+ * <StoryboardCaption script={script} speaker={speaker} />
10
+ *
11
+ * No imports or frontmatter registration needed — it's a built-in.
12
+ */
13
+ export interface StoryboardCaptionProps {
14
+ /** The narration/dialogue text */
15
+ script: string;
16
+ /** Optional speaker name (for multi-turn dialogue) */
17
+ speaker?: string;
18
+ }
19
+
20
+ export function StoryboardCaption({ script, speaker }: StoryboardCaptionProps) {
21
+ return (
22
+ <div
23
+ style={{
24
+ position: "absolute",
25
+ bottom: "10%",
26
+ left: "5%",
27
+ right: "5%",
28
+ display: "flex",
29
+ flexDirection: "column",
30
+ alignItems: "center",
31
+ pointerEvents: "none",
32
+ }}
33
+ >
34
+ <div
35
+ style={{
36
+ maxWidth: "90%",
37
+ background: "rgba(0,0,0,0.7)",
38
+ backdropFilter: "blur(4px)",
39
+ borderRadius: 16,
40
+ padding: "2.5vh 4vh",
41
+ textAlign: "center",
42
+ color: "#fff",
43
+ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
44
+ }}
45
+ >
46
+ {speaker && (
47
+ <div
48
+ style={{
49
+ fontSize: "2vh",
50
+ fontWeight: 700,
51
+ color: "#93c5fd",
52
+ marginBottom: "0.6vh",
53
+ letterSpacing: "0.04em",
54
+ }}
55
+ >
56
+ {speaker}
57
+ </div>
58
+ )}
59
+ <div
60
+ style={{
61
+ fontSize: "2.6vh",
62
+ lineHeight: 1.5,
63
+ fontWeight: 500,
64
+ textShadow: "0 1px 4px rgba(0,0,0,0.4)",
65
+ whiteSpace: "pre-wrap",
66
+ wordBreak: "break-word",
67
+ }}
68
+ >
69
+ {script}
70
+ </div>
71
+ </div>
72
+ </div>
73
+ );
74
+ }
@@ -0,0 +1,195 @@
1
+ import * as React from "react";
2
+
3
+ /**
4
+ * Built-in component rendered by `--storyboard` preview mode as the first
5
+ * frame of the video. Shows all project frontmatter config and variant info
6
+ * so the user has full context before reviewing the storyboard scenes.
7
+ *
8
+ * All props are strings (the compiler only collects string bindings).
9
+ *
10
+ * Usage in compiled JSON:
11
+ * <StoryboardInfo config="width: 1080 · height: 1920 · fps: 30 · layout: series"
12
+ * variants="default, zh-tiktok" />
13
+ *
14
+ * No imports or frontmatter registration needed — it's a built-in.
15
+ */
16
+ export interface StoryboardInfoProps {
17
+ /** Formatted string of all root config fields (separated by " · ") */
18
+ config?: string;
19
+ /** Comma-separated list of variant labels */
20
+ variants?: string;
21
+ /** Raw YAML frontmatter from the markdown source (if any) */
22
+ frontmatter?: string;
23
+ }
24
+
25
+ export function StoryboardInfo({ config, variants, frontmatter }: StoryboardInfoProps) {
26
+ const parts = (config ?? "").split(" · ").filter(Boolean);
27
+ const variantList = (variants ?? "").split(/,\s*/).filter(Boolean);
28
+
29
+ return (
30
+ <div
31
+ style={{
32
+ position: "absolute",
33
+ inset: 0,
34
+ display: "flex",
35
+ flexDirection: "column",
36
+ alignItems: "center",
37
+ justifyContent: "center",
38
+ background: "linear-gradient(135deg,#0f172a,#1e293b,#334155)",
39
+ color: "#fff",
40
+ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
41
+ padding: "8%",
42
+ boxSizing: "border-box",
43
+ }}
44
+ >
45
+ {/* decorative rings */}
46
+ <div
47
+ style={{
48
+ position: "absolute",
49
+ inset: "5%",
50
+ border: "2px solid rgba(255,255,255,0.08)",
51
+ borderRadius: "50%",
52
+ }}
53
+ />
54
+ <div
55
+ style={{
56
+ position: "absolute",
57
+ inset: "12%",
58
+ border: "1px solid rgba(255,255,255,0.05)",
59
+ borderRadius: "50%",
60
+ }}
61
+ />
62
+
63
+ {/* header icon */}
64
+ <div style={{ fontSize: "6vh", marginBottom: "2.5vh", opacity: 0.7 }}>🎬</div>
65
+
66
+ {/* title */}
67
+ <div
68
+ style={{
69
+ fontSize: "4.2vh",
70
+ fontWeight: 800,
71
+ letterSpacing: "0.02em",
72
+ marginBottom: "3vh",
73
+ textAlign: "center",
74
+ textShadow: "0 2px 12px rgba(0,0,0,0.5)",
75
+ }}
76
+ >
77
+ Storyboard Preview
78
+ </div>
79
+
80
+ {/* config fields as a table */}
81
+ {parts.length > 0 && (
82
+ <div
83
+ style={{
84
+ display: "flex",
85
+ flexWrap: "wrap",
86
+ justifyContent: "center",
87
+ gap: "1.2vh",
88
+ maxWidth: "85%",
89
+ marginBottom: variantList.length > 0 ? "2.5vh" : 0,
90
+ }}
91
+ >
92
+ {parts.map((entry) => {
93
+ const colonIdx = entry.indexOf(":");
94
+ const label = colonIdx > 0 ? entry.slice(0, colonIdx).trim() : "";
95
+ const value = colonIdx > 0 ? entry.slice(colonIdx + 1).trim() : entry;
96
+ return (
97
+ <div
98
+ key={label || entry}
99
+ style={{
100
+ background: "rgba(255,255,255,0.07)",
101
+ border: "1px solid rgba(255,255,255,0.12)",
102
+ borderRadius: 12,
103
+ padding: "1.2vh 2.2vh",
104
+ textAlign: "center",
105
+ minWidth: "16vh",
106
+ }}
107
+ >
108
+ {label && (
109
+ <div
110
+ style={{
111
+ fontSize: "1.6vh",
112
+ fontWeight: 600,
113
+ color: "#93c5fd",
114
+ letterSpacing: "0.08em",
115
+ textTransform: "uppercase" as const,
116
+ marginBottom: "0.4vh",
117
+ }}
118
+ >
119
+ {label}
120
+ </div>
121
+ )}
122
+ <div
123
+ style={{
124
+ fontSize: "2.2vh",
125
+ fontWeight: 700,
126
+ whiteSpace: "pre-wrap",
127
+ wordBreak: "break-word",
128
+ opacity: value.startsWith("http") ? 0.8 : 1,
129
+ }}
130
+ >
131
+ {value}
132
+ </div>
133
+ </div>
134
+ );
135
+ })}
136
+ </div>
137
+ )}
138
+
139
+ {/* frontmatter block */}
140
+ {frontmatter && (
141
+ <div
142
+ style={{
143
+ maxWidth: "85%",
144
+ background: "rgba(255,255,255,0.05)",
145
+ border: "1px solid rgba(255,255,255,0.1)",
146
+ borderRadius: 12,
147
+ padding: "1.5vh 2.5vh",
148
+ marginBottom: "2.5vh",
149
+ fontFamily: "monospace",
150
+ fontSize: "1.8vh",
151
+ lineHeight: 1.6,
152
+ color: "rgba(255,255,255,0.8)",
153
+ whiteSpace: "pre-wrap",
154
+ wordBreak: "break-word",
155
+ textAlign: "left",
156
+ maxHeight: "28vh",
157
+ overflow: "auto",
158
+ }}
159
+ >
160
+ {frontmatter}
161
+ </div>
162
+ )}
163
+
164
+ {/* variants */}
165
+ {variantList.length > 0 && (
166
+ <div
167
+ style={{
168
+ display: "flex",
169
+ flexWrap: "wrap",
170
+ justifyContent: "center",
171
+ gap: "1vh",
172
+ }}
173
+ >
174
+ {variantList.map((v) => (
175
+ <div
176
+ key={v}
177
+ style={{
178
+ background: "rgba(251,191,36,0.15)",
179
+ border: "1px solid rgba(251,191,36,0.35)",
180
+ borderRadius: 999,
181
+ padding: "0.8vh 2vh",
182
+ fontSize: "1.8vh",
183
+ fontWeight: 600,
184
+ color: "#fbbf24",
185
+ letterSpacing: "0.04em",
186
+ }}
187
+ >
188
+ {v}
189
+ </div>
190
+ ))}
191
+ </div>
192
+ )}
193
+ </div>
194
+ );
195
+ }
@@ -0,0 +1,96 @@
1
+ import * as React from "react";
2
+
3
+ /**
4
+ * Built-in component rendered by `--storyboard` preview mode instead of a
5
+ * generated image (TTI) or video (TTV). Shows the generation prompt with a
6
+ * clear type indicator so the user can review the story structure fast and
7
+ * chat to reshape it before committing to slow media generation.
8
+ *
9
+ * Usage in markdown / compiled JSON:
10
+ * <StoryboardSlot kind="image" prompt={prompt} />
11
+ * <StoryboardSlot kind="video" prompt={prompt} />
12
+ *
13
+ * The `prompt` variable comes from the component node's data bindings.
14
+ * No imports or frontmatter registration needed — it's a built-in.
15
+ */
16
+ export interface StoryboardSlotProps {
17
+ kind: "image" | "video";
18
+ prompt?: string;
19
+ }
20
+
21
+ export function StoryboardSlot({ kind, prompt }: StoryboardSlotProps) {
22
+ const isVideo = kind === "video";
23
+
24
+ const bgGrad = isVideo
25
+ ? "linear-gradient(135deg,#7c2d12,#b91c1c)"
26
+ : "linear-gradient(135deg,#1e3a8a,#6d28d9)";
27
+ const accentColor = isVideo ? "#fbbf24" : "#93c5fd";
28
+ const label = isVideo ? "Video" : "Image";
29
+ const emoji = isVideo ? "🎬" : "🖼️";
30
+
31
+ return (
32
+ <div
33
+ style={{
34
+ position: "absolute",
35
+ inset: 0,
36
+ overflow: "hidden",
37
+ display: "flex",
38
+ flexDirection: "column",
39
+ alignItems: "center",
40
+ justifyContent: "center",
41
+ background: bgGrad,
42
+ color: "#fff",
43
+ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
44
+ padding: "6%",
45
+ boxSizing: "border-box",
46
+ }}
47
+ >
48
+ {/* dashed border frame */}
49
+ <div
50
+ style={{
51
+ position: "absolute",
52
+ inset: "4%",
53
+ border: "3px dashed rgba(255,255,255,0.35)",
54
+ borderRadius: 24,
55
+ pointerEvents: "none",
56
+ }}
57
+ />
58
+
59
+ {/* type badge */}
60
+ <div
61
+ style={{
62
+ display: "flex",
63
+ alignItems: "center",
64
+ gap: "0.5em",
65
+ background: "rgba(0,0,0,0.35)",
66
+ border: `2px solid ${accentColor}`,
67
+ borderRadius: 999,
68
+ padding: "0.5em 1.1em",
69
+ fontSize: "2.2vh",
70
+ fontWeight: 800,
71
+ letterSpacing: "0.12em",
72
+ marginBottom: "3vh",
73
+ }}
74
+ >
75
+ <span style={{ fontSize: "3.4vh", lineHeight: 1 }}>{emoji}</span>
76
+ </div>
77
+
78
+ {/* prompt text */}
79
+ <div
80
+ style={{
81
+ position: "relative",
82
+ maxWidth: "88%",
83
+ textAlign: "center",
84
+ fontSize: "3.4vh",
85
+ lineHeight: 1.4,
86
+ fontWeight: 600,
87
+ textShadow: "0 2px 8px rgba(0,0,0,0.5)",
88
+ whiteSpace: "pre-wrap",
89
+ wordBreak: "break-word",
90
+ }}
91
+ >
92
+ {prompt ?? (isVideo ? "(no video prompt)" : "(no image prompt)")}
93
+ </div>
94
+ </div>
95
+ );
96
+ }
package/src/config.mjs CHANGED
@@ -20,7 +20,7 @@ export const args=(function parseArgs(argv) {
20
20
  "--tti": "tti",
21
21
  "--ttv": "ttv"
22
22
  };
23
- const args = { command: "", file: "", output: "", forceNew: false, verbose: false, dev: false, label: false, edit: false, noBrowser: false, chat: false, port: 3001, compile: false, cli: false, showClis: false, scriptOutputDir: "", mediaOutputDir: "", variant: [], cliOverrides: {} };
23
+ const args = { command: "", file: "", output: "", forceNew: false, verbose: false, dev: false, label: false, edit: false, storyboard: false, noBrowser: false, chat: false, port: 3001, compile: false, cli: false, showClis: false, scriptOutputDir: "", mediaOutputDir: "", variant: [], cliOverrides: {} };
24
24
  let i = 2;
25
25
  if (argv[i]) args.command = argv[i++];
26
26
  if (argv[i] && !argv[i].startsWith("--")) args.file = argv[i++];
@@ -37,6 +37,7 @@ export const args=(function parseArgs(argv) {
37
37
  else if (flag === "--dev") args.dev = true;
38
38
  else if (flag === "--label") args.label = true;
39
39
  else if (flag === "--edit") args.edit = true;
40
+ else if (flag === "--storyboard") args.storyboard = true;
40
41
  else if (flag === "--no-browser") args.noBrowser = true;
41
42
  else if (flag === "--port" && argv[i]) args.port = parseInt(argv[i], 10);
42
43
  else if (flag.startsWith("--port=")) args.port = parseInt(flag.split("=")[1], 10);