@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 +15 -16
- package/README.md +89 -0
- package/package.json +1 -1
- package/skills/markcut/SKILL.md +1 -1
- package/skills/markcut/docs/markdown-descriptive.md +2 -2
- package/src/components/StoryboardCaption.tsx +74 -0
- package/src/components/StoryboardInfo.tsx +195 -0
- package/src/components/StoryboardSlot.tsx +96 -0
- package/src/config.mjs +2 -1
- package/src/descriptive/compiler.ts +52 -12
- package/src/descriptive/markdown.ts +5 -1
- package/src/descriptive/resolve.ts +160 -5
- package/src/entry.tsx +28 -5
- package/src/player/bundle/player.js +395 -65
- package/src/player/pipeline.mjs +149 -16
- package/src/player/pipeline.ts +4 -0
- package/src/player/server-shared.mjs +25 -9
- package/src/player/server.mjs +11 -3
- package/src/render/cli.mjs +12 -4
- package/src/schema/index.ts +1 -1
- package/src/types/Folder.tsx +1 -1
- package/src/types/Subtitle.tsx +4 -0
- package/src/vision/cli.mjs +43 -8
- package/tests/tmp/vision-1784830584961/images/.normalized/test-photo_384.jpg +0 -0
- package/tests/tmp/vision-1784830584961/images/metadata.json +8 -0
- package/tests/tmp/vision-1784830584961/images/test-photo.png +0 -0
- package/tests/vision.test.ts +207 -0
- package/tmp/frontmatter-test.ts +21 -0
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.
|
|
218
|
-
4.
|
|
219
|
-
5.
|
|
220
|
-
6.
|
|
221
|
-
7.
|
|
222
|
-
8.
|
|
223
|
-
9.
|
|
224
|
-
10.
|
|
225
|
-
11.
|
|
226
|
-
12.
|
|
227
|
-
13.
|
|
228
|
-
14.
|
|
229
|
-
15.
|
|
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.
|
|
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).
|
|
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
package/skills/markcut/SKILL.md
CHANGED
|
@@ -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 #
|
|
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:{
|
|
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:{
|
|
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);
|