@lalalic/markcut 2.6.0 → 2.7.1
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 +20 -20
- package/README.md +4 -0
- package/package.json +1 -1
- package/skills/markcut/SKILL.md +17 -3
- 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 +157 -2
- 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 +11 -1
- package/src/schema/index.ts +1 -1
- package/src/types/Folder.tsx +1 -1
- package/src/types/Subtitle.tsx +4 -0
- 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/tmp/frontmatter-test.ts +21 -0
package/AGENTS.md
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm run render storyboard.md
|
|
9
|
-
npm run preview storyboard.md --edit
|
|
10
|
-
npm
|
|
11
|
-
npm
|
|
8
|
+
npm run render storyboard.md # render markdown → MP4 (9x16 default)
|
|
9
|
+
npm run preview storyboard.md --edit # live preview with auto-reload
|
|
10
|
+
npm run preview storyboard.md --storyboard # fast structure preview (skips TTI/TTV, shows prompts as placeholders)
|
|
11
|
+
npm test # unit + integration tests
|
|
12
|
+
npm run typecheck # TypeScript type check
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
## Input formats
|
|
@@ -214,20 +215,19 @@ node src/render/cli.mjs templates
|
|
|
214
215
|
|
|
215
216
|
1. Basic Rendering — empty composition, dimension verification
|
|
216
217
|
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
|
|
218
|
+
3. Effects — fadeIn, bounceIn, custom keyframes
|
|
219
|
+
4. Map Rendering — canvas route visualization
|
|
220
|
+
5. Audio Rendering — audio track, non-silent verification
|
|
221
|
+
6. Include — nested composition via include
|
|
222
|
+
7. Full Feature Combination — multiple features
|
|
223
|
+
8. Scene Node — scene-based rendering
|
|
224
|
+
9. Audio STT — speech-to-text verification via whisper
|
|
225
|
+
10. Multiple Aspect Ratios — 16x9, 9x16, 1x1
|
|
226
|
+
11. Cross-Stream Types — parallel rendering, effect-wrapped children
|
|
227
|
+
12. Frame-Accurate — timeline consistency, frame count
|
|
228
|
+
13. Description Field — schema validation, render preservation
|
|
229
|
+
14. Scene as Folder Alias — scene/folder interchangeability
|
|
230
|
+
15. **Video startFrom/endAt Trimming** — video trimming + photo in series
|
|
231
231
|
|
|
232
232
|
### Running Tests
|
|
233
233
|
|
|
@@ -242,9 +242,9 @@ npx vitest run tests/render.test.ts -t "Test Name" # single test
|
|
|
242
242
|
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
243
|
2. **Video endAt formula**: Must be `startFrom_frames + ((endAt_source - startFrom_source) * fps / playbackRate)` — changing to `endAt_source * fps` breaks slow-motion playback.
|
|
244
244
|
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.
|
|
245
|
+
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
246
|
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).
|
|
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). 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
248
|
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
249
|
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
250
|
|
package/README.md
CHANGED
|
@@ -9,6 +9,9 @@ npx markcut render storyboard.md
|
|
|
9
9
|
# Preview with live edit (edit .md file, player auto-reloads)
|
|
10
10
|
npx markcut preview storyboard.md --edit
|
|
11
11
|
|
|
12
|
+
# Fast structure preview: show TTI/TTV prompts as placeholders, skip slow generation
|
|
13
|
+
npx markcut preview storyboard.md --storyboard
|
|
14
|
+
|
|
12
15
|
# Label clips in a stream tree
|
|
13
16
|
npx markcut preview labels.json --label --port=3031
|
|
14
17
|
```
|
|
@@ -43,6 +46,7 @@ storyboard.md ──[parse]──▶ DescriptiveRoot ──[compile]──▶
|
|
|
43
46
|
| **Tween animation** | `tween(from, to)` function calls in JSX for frame-accurate numeric animation |
|
|
44
47
|
| **Styling** | Inline `style` strings on any node for CSS. JSX components use inline React styles |
|
|
45
48
|
| **Live edit** | `--edit` watches the input file, re-runs pipeline, auto-reloads player |
|
|
49
|
+
| **Storyboard** | `--storyboard` fast structure preview: replaces TTI/TTV prompts with placeholder components, skips slow generation. Implies `--edit` |
|
|
46
50
|
| **Label mode** | `--label` interactive player with per-scene label input, saves to labels.json |
|
|
47
51
|
| **CLI** | `render`, `preview` commands for MP4 export and Remotion Studio |
|
|
48
52
|
| **Programmatic** | `MarkCut` / `DescriptiveComposition` React components for embedding |
|
package/package.json
CHANGED
package/skills/markcut/SKILL.md
CHANGED
|
@@ -10,8 +10,9 @@ Everything video is a **stream tree** described with markdown. see [docs/markdow
|
|
|
10
10
|
|
|
11
11
|
## Video Design Utilities
|
|
12
12
|
|
|
13
|
-
###
|
|
14
|
-
- `npx @lalalic/markcut
|
|
13
|
+
### Vision — understand video and images
|
|
14
|
+
- `npx @lalalic/markcut vision <folder>` provides tool to understand video and images in a folder, and generate a `metadata.json` file with with text description, duration, and other metadata for each media file.
|
|
15
|
+
- `npx @lalalic/markcut vision <folder> --label`, interactive mode, provides tool to label video and images with text, and above metadata.
|
|
15
16
|
|
|
16
17
|
### Storyboard — plan video structure with scene nodes
|
|
17
18
|
|
|
@@ -20,6 +21,18 @@ Everything video is a **stream tree** described with markdown. see [docs/markdow
|
|
|
20
21
|
|
|
21
22
|
see [docs/markdown-descriptive.md](docs/markdown-descriptive.md) for full details.
|
|
22
23
|
|
|
24
|
+
### `--storyboard` — fast structure preview
|
|
25
|
+
```bash
|
|
26
|
+
npx @lalalic/markcut preview video.md --storyboard
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Skips slow TTI/TTV/STT generation and renders a fast preview where:
|
|
30
|
+
- `image`/`video` nodes with `prompt` but no `src` → `<StoryboardSlot>` placeholder showing the prompt text
|
|
31
|
+
- `audio`/`script` nodes → `<StoryboardCaption>` overlays showing narration text
|
|
32
|
+
- A `<StoryboardInfo>` card with root metadata (title, dimensions, fps) appears at the first frame
|
|
33
|
+
|
|
34
|
+
Implies `--edit` so you can chat to reshape the story and see live reloads.
|
|
35
|
+
|
|
23
36
|
### Video Variants
|
|
24
37
|
- define variants for different video configurations, such as different languages, aspects, platforms
|
|
25
38
|
|
|
@@ -43,10 +56,11 @@ npx @lalalic/markcut <command> [options]
|
|
|
43
56
|
npx @lalalic/markcut --help # get overall information
|
|
44
57
|
npx @lalalic/markcut --show-clis # get command specific information
|
|
45
58
|
npx @lalalic/markcut preview <file> # assemble and preview the video with a local server, and chat to edit the video and auto refresh
|
|
59
|
+
npx @lalalic/markcut preview <file> --storyboard # fast structure preview: replaces TTI/TTV prompts with placeholder components, skips slow generation
|
|
46
60
|
npx @lalalic/markcut render <file> # render the video to mp4
|
|
47
61
|
npx @lalalic/markcut vision <folder> # vision understanding medias in folder
|
|
48
62
|
npx @lalalic/markcut vision <folder> --label # an extra step to provide UI to label the medias with text, time ranges
|
|
49
|
-
|
|
63
|
+
|
|
50
64
|
```
|
|
51
65
|
|
|
52
66
|
---
|
|
@@ -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);
|
|
@@ -490,19 +490,27 @@ function wrapWithEffects(
|
|
|
490
490
|
|
|
491
491
|
function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | DescriptiveScene | DescriptiveInclude>, ctx: CompileContext, parentKind: "series" | "parallel" | "transitionSeries"): CompileResult {
|
|
492
492
|
const id = node.id ?? uid();
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const
|
|
493
|
+
|
|
494
|
+
// Background nodes without explicit duration/endAt: let parent fill timing later
|
|
495
|
+
const hasOwnDuration = typeof node.duration === "number" || typeof node.endAt === "number";
|
|
496
|
+
const isBgNoOwnTiming = node.isBackground && !hasOwnDuration;
|
|
497
|
+
|
|
498
|
+
// For background without own timing, skip deriving duration — will be filled by parent
|
|
499
|
+
const start = isBgNoOwnTiming
|
|
500
|
+
? (typeof node.start === "number" ? node.start : undefined)
|
|
501
|
+
: (parentKind === "parallel" ? Math.max(0, node.start ?? 0) : 0);
|
|
502
|
+
const duration = isBgNoOwnTiming ? undefined : deriveLeafDuration(node, ctx);
|
|
503
|
+
const end = duration != null ? start! + duration : undefined;
|
|
496
504
|
|
|
497
505
|
const base = {
|
|
498
506
|
id,
|
|
499
507
|
style: node.style,
|
|
500
508
|
visible: node.visible ?? true,
|
|
501
509
|
isBackground: node.isBackground,
|
|
502
|
-
start,
|
|
510
|
+
start: isBgNoOwnTiming ? (typeof node.start === "number" ? node.start : undefined) : start,
|
|
503
511
|
end,
|
|
504
|
-
startFrom: node.type === "video" || node.type === "audio" ? node.startFrom : undefined,
|
|
505
|
-
endAt: node.type === "video" || node.type === "audio" ? node.endAt : undefined,
|
|
512
|
+
startFrom: isBgNoOwnTiming ? undefined : (node.type === "video" || node.type === "audio" ? node.startFrom : undefined),
|
|
513
|
+
endAt: isBgNoOwnTiming ? undefined : (node.type === "video" || node.type === "audio" ? node.endAt : undefined),
|
|
506
514
|
durationInSeconds: end,
|
|
507
515
|
...pickOn(node),
|
|
508
516
|
};
|
|
@@ -518,7 +526,7 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
|
|
|
518
526
|
width: node.width ?? 1080,
|
|
519
527
|
height: node.height ?? 1920,
|
|
520
528
|
};
|
|
521
|
-
return { stream, duration: end };
|
|
529
|
+
return { stream, duration: end ?? 0 };
|
|
522
530
|
}
|
|
523
531
|
case "audio": {
|
|
524
532
|
const stream: Audio = {
|
|
@@ -529,7 +537,7 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
|
|
|
529
537
|
foreground: node.foreground,
|
|
530
538
|
speaker: node.speaker,
|
|
531
539
|
};
|
|
532
|
-
return { stream, duration: end };
|
|
540
|
+
return { stream, duration: end ?? 0 };
|
|
533
541
|
}
|
|
534
542
|
case "image": {
|
|
535
543
|
const stream: Image = {
|
|
@@ -538,7 +546,7 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
|
|
|
538
546
|
src: node.src,
|
|
539
547
|
fit: node.fit ?? "contain",
|
|
540
548
|
};
|
|
541
|
-
return { stream, duration: end };
|
|
549
|
+
return { stream, duration: end ?? 0 };
|
|
542
550
|
}
|
|
543
551
|
case "component": {
|
|
544
552
|
// Collect extra properties from descriptive node (e.g. `source` from ~~~md source fences)
|
|
@@ -560,7 +568,7 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
|
|
|
560
568
|
jsx: node.jsx,
|
|
561
569
|
data: Object.keys(bindings).length ? bindings : undefined,
|
|
562
570
|
};
|
|
563
|
-
return { stream, duration: end };
|
|
571
|
+
return { stream, duration: end ?? 0 };
|
|
564
572
|
}
|
|
565
573
|
case "rhythm": {
|
|
566
574
|
// rhythm without children compiles as a plain audio leaf
|
|
@@ -572,7 +580,7 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
|
|
|
572
580
|
spots: node.spots,
|
|
573
581
|
children: [],
|
|
574
582
|
};
|
|
575
|
-
return { stream, duration: end };
|
|
583
|
+
return { stream, duration: end ?? 0 };
|
|
576
584
|
}
|
|
577
585
|
case "map": {
|
|
578
586
|
const stream: MapStream = {
|
|
@@ -588,7 +596,7 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
|
|
|
588
596
|
routeMarker: node.routeMarker ?? "🚗",
|
|
589
597
|
googleMapsApiKey: ctx.googleMapsApiKey,
|
|
590
598
|
};
|
|
591
|
-
return { stream, duration: end };
|
|
599
|
+
return { stream, duration: end ?? 0 };
|
|
592
600
|
}
|
|
593
601
|
}
|
|
594
602
|
}
|
|
@@ -676,6 +684,16 @@ function compileScene(
|
|
|
676
684
|
? aggregateDuration(compiledChildren, "parallel")
|
|
677
685
|
: aggregateDuration(compiledChildren, sceneKind, resolved.time);
|
|
678
686
|
const localDuration = Math.max(node.duration ?? 0, sceneContentDuration);
|
|
687
|
+
|
|
688
|
+
// Back-propagate parent duration to background children without own timing
|
|
689
|
+
for (const c of compiledChildren) {
|
|
690
|
+
if (c.stream.isBackground && c.stream.end == null) {
|
|
691
|
+
c.stream.end = localDuration;
|
|
692
|
+
c.stream.durationInSeconds = localDuration;
|
|
693
|
+
if (c.stream.start == null) c.stream.start = 0;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
679
697
|
const start = parentKind === "parallel" ? Math.max(0, node.start ?? 0) : 0;
|
|
680
698
|
const end = start + localDuration;
|
|
681
699
|
|
|
@@ -814,6 +832,15 @@ function compileContainer(node: DescriptiveContainer, ctx: CompileContext, paren
|
|
|
814
832
|
const children = compileChildren(node.children, ctx, node.type);
|
|
815
833
|
const duration = aggregateDuration(children, node.type, resolved.time);
|
|
816
834
|
|
|
835
|
+
// Back-propagate parent duration to background children without own timing
|
|
836
|
+
for (const c of children) {
|
|
837
|
+
if (c.stream.isBackground && c.stream.end == null) {
|
|
838
|
+
c.stream.end = duration;
|
|
839
|
+
c.stream.durationInSeconds = duration;
|
|
840
|
+
if (c.stream.start == null) c.stream.start = 0;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
817
844
|
const stream: Folder = {
|
|
818
845
|
id,
|
|
819
846
|
type: "folder",
|
|
@@ -1200,6 +1227,10 @@ export function compileDescriptiveRoot(input: DescriptiveRoot, options: CompileO
|
|
|
1200
1227
|
|
|
1201
1228
|
// Resolve frontmatter imports / inline component defs onto each component node.
|
|
1202
1229
|
const registry = resolveComponentSources(input);
|
|
1230
|
+
// Add built-in components so warnUnregisteredComponents doesn't flag them.
|
|
1231
|
+
registry.add("StoryboardSlot");
|
|
1232
|
+
registry.add("StoryboardCaption");
|
|
1233
|
+
registry.add("StoryboardInfo");
|
|
1203
1234
|
warnUnregisteredComponents(input, registry);
|
|
1204
1235
|
|
|
1205
1236
|
ensureUniqueIds(input.children, "root");
|
|
@@ -1207,6 +1238,15 @@ export function compileDescriptiveRoot(input: DescriptiveRoot, options: CompileO
|
|
|
1207
1238
|
const children = compileChildren(input.children, ctx, rootKind);
|
|
1208
1239
|
const duration = aggregateDuration(children, rootKind, resolved.time);
|
|
1209
1240
|
|
|
1241
|
+
// Back-propagate root duration to background children without own timing
|
|
1242
|
+
for (const c of children) {
|
|
1243
|
+
if (c.stream.isBackground && c.stream.end == null) {
|
|
1244
|
+
c.stream.end = duration;
|
|
1245
|
+
c.stream.durationInSeconds = duration;
|
|
1246
|
+
if (c.stream.start == null) c.stream.start = 0;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1210
1250
|
const compiled: Root = {
|
|
1211
1251
|
id: "root",
|
|
1212
1252
|
type: "root",
|