@lalalic/markcut 2.9.0 → 3.1.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/package.json +1 -1
- package/skills/markcut/SKILL.md +7 -0
- package/skills/markcut/docs/components.md +45 -2
- package/skills/markcut/docs/map-dynamic-camera.md +244 -0
- package/skills/markcut/docs/markdown-descriptive.md +7 -2
- package/src/components/Markdown.tsx +138 -24
- package/src/components/Mermaid.tsx +223 -22
- package/src/context/EventContext.tsx +3 -0
- package/src/descriptive/compiler.ts +105 -29
- package/src/descriptive/dsl.ts +42 -5
- package/src/descriptive/markdown.ts +23 -0
- package/src/descriptive/resolve.test.ts +5 -5
- package/src/descriptive/resolve.ts +51 -12
- package/src/player/bundle/player.js +751 -143
- package/src/player/pipeline.mjs +130 -32
- package/src/player/pipeline.ts +5 -4
- package/src/player/server.mjs +22 -42
- package/src/render/cli.mjs +54 -3
- package/src/render/validate-assets.mjs +140 -0
- package/src/schema/index.ts +58 -2
- package/src/spots/cli.mjs +266 -0
- package/src/types/Component.tsx +27 -1
- package/src/types/Effect.tsx +13 -6
- package/src/types/Folder.tsx +1 -1
- package/src/types/Map.tsx +501 -127
- package/src/utils/index.ts +14 -2
- package/src/utils/tween.ts +49 -1
- package/tests/dsl.test.ts +43 -0
- package/tests/fixtures/map-dynamic.json +52 -0
- package/tests/fixtures/md/animate-diagrams.md +42 -0
- package/tests/fixtures/md/electricity-grow.md +130 -0
- package/tests/fixtures/md/map-all-views.md +28 -0
- package/tests/md-descriptive.test.ts +58 -0
- package/tests/render.test.ts +1 -0
- package/tests/schema.test.ts +58 -1
- package/tests/validate-assets.test.ts +106 -0
package/package.json
CHANGED
package/skills/markcut/SKILL.md
CHANGED
|
@@ -36,6 +36,11 @@ Implies `--edit` so you can chat to reshape the story and see live reloads.
|
|
|
36
36
|
### Video Variants
|
|
37
37
|
- define variants for different video configurations, such as different languages, aspects, platforms
|
|
38
38
|
|
|
39
|
+
### Route / Vlog videos (map clips)
|
|
40
|
+
- For **route / travel-vlog clips** (drone flyover, route map with photo stops, Street View walks) use the `map` stream type with `view: overview | route | cinematic | streetview` and `tween(from,to,easing)` camera moves.
|
|
41
|
+
- Discover interesting stops along a route with `markcut spots --waypoints "lat,lng;lat,lng" --photos --markdown`, then compose a `map` storyboard from the returned `waypoints:[...]` line.
|
|
42
|
+
- See [docs/map-dynamic-camera.md](docs/map-dynamic-camera.md) — the agent guide with effects, spots workflow, and copy-paste markdown examples.
|
|
43
|
+
|
|
39
44
|
### Viral Story requires
|
|
40
45
|
- **Hooks** : why should the viewer watch this?
|
|
41
46
|
- **Conflict** : what challenges or obstacles do the characters face?
|
|
@@ -60,6 +65,7 @@ npx @lalalic/markcut preview <file> --storyboard # fast structure preview: repla
|
|
|
60
65
|
npx @lalalic/markcut render <file> # render the video to mp4
|
|
61
66
|
npx @lalalic/markcut vision <folder> # vision understanding medias in folder
|
|
62
67
|
npx @lalalic/markcut vision <folder> --label # an extra step to provide UI to label the medias with text, time ranges
|
|
68
|
+
npx @lalalic/markcut spots --waypoints "lat,lng;lat,lng" # discover POIs along a route (for route/vlog map clips)
|
|
63
69
|
|
|
64
70
|
```
|
|
65
71
|
|
|
@@ -80,6 +86,7 @@ some common issues (photo or video can't be displayed, audio missing), take belo
|
|
|
80
86
|
| Topic | File |
|
|
81
87
|
|-------|------|
|
|
82
88
|
| Markdown descriptive format (primary authoring format) | [docs/markdown-descriptive.md](docs/markdown-descriptive.md) |
|
|
89
|
+
| Route / vlog map clips (effects, spots, examples) | [docs/map-dynamic-camera.md](docs/map-dynamic-camera.md) |
|
|
83
90
|
| Built-in components & common npm packages | [docs/components.md](docs/components.md) |
|
|
84
91
|
| Sound effects | [docs/sound-effects.md](docs/sound-effects.md) |
|
|
85
92
|
|
|
@@ -9,6 +9,13 @@ These components are available directly in `jsx:".."` fields or importable from
|
|
|
9
9
|
- Uses `react-markdown` + `remark-gfm` (tables, strikethrough, task lists)
|
|
10
10
|
- Supports `plugins` and `components` props: define custom renderers in imports block and pass them in
|
|
11
11
|
- ` ```mermaid ` code fences inside markdown are automatically rendered as Mermaid diagrams
|
|
12
|
+
- **`highlight`** — array of 0-based list item indices to highlight (e.g., `highlight={[0,2]}`).
|
|
13
|
+
Matching `<li>` elements receive CSS class `highlight-list-item` with a default golden
|
|
14
|
+
left-border and background tint. Works for both ordered and unordered lists.
|
|
15
|
+
```md
|
|
16
|
+
<Markdown highlight={[0,2]}
|
|
17
|
+
source={"1. First item\n2. Second item\n3. Third item\n4. Fourth item"} />
|
|
18
|
+
```
|
|
12
19
|
- Importable: `import {Markdown} from "@lalalic/markcut/components"`
|
|
13
20
|
|
|
14
21
|
### `<Mermaid />` — render Mermaid diagrams as SVG
|
|
@@ -16,7 +23,42 @@ These components are available directly in `jsx:".."` fields or importable from
|
|
|
16
23
|
- component jsx:"<Mermaid source='graph TD; A-->B; A-->C; B-->D;' />"
|
|
17
24
|
```
|
|
18
25
|
- Theme prop: `theme="default" | "dark" | "forest" | "neutral"` (default: `dark`)
|
|
19
|
-
-
|
|
26
|
+
- **`highlight`** — node name(s) to highlight. String or array of strings.
|
|
27
|
+
Toggles CSS class `highlight` on matching SVG elements. The diagram source
|
|
28
|
+
must define the class via `classDef`:
|
|
29
|
+
```md
|
|
30
|
+
classDef highlight fill:#ffd700,stroke:#ff6600,stroke-width:3px,color:#000
|
|
31
|
+
```
|
|
32
|
+
- **`animateEdges`** — edge(s) to animate with a flowing dash effect.
|
|
33
|
+
`true` = animate all edges. `string[]` = specific edges by source→target alias:
|
|
34
|
+
```md
|
|
35
|
+
animateEdges={["A->B","D->F","F->G"]}
|
|
36
|
+
```
|
|
37
|
+
Animates matching edge `<path>` elements via CSS `stroke-dasharray` + `stroke-dashoffset` keyframes.
|
|
38
|
+
- Source from code fences: use `~~~mermaid` to define the diagram source in a
|
|
39
|
+
separate block, then reference it as `source={mermaid}` in the JSX:
|
|
40
|
+
```md
|
|
41
|
+
- component id:flowChart duration:12
|
|
42
|
+
~~~jsx
|
|
43
|
+
<Mermaid highlight={highlight} animateEdges={animateEdges}
|
|
44
|
+
theme='dark' source={mermaid}/>
|
|
45
|
+
~~~
|
|
46
|
+
~~~mermaid
|
|
47
|
+
graph TD
|
|
48
|
+
A["Receive Request"] --> B["Validate Input"]
|
|
49
|
+
B --> C{"Valid?"}
|
|
50
|
+
classDef highlight fill:#ffd700,stroke:#ff6600,stroke-width:3px
|
|
51
|
+
~~~
|
|
52
|
+
highlight:"A"
|
|
53
|
+
animateEdges:true
|
|
54
|
+
```
|
|
55
|
+
- Dynamic highlight/edges via events: define `on` specs that mutate the
|
|
56
|
+
component's registered id:
|
|
57
|
+
```md
|
|
58
|
+
- event duration:3 start:3 on:(start, flowChart.highlight="B")
|
|
59
|
+
- event duration:3 start:6 on:(start, flowChart.highlight="C")
|
|
60
|
+
- event duration:3 start:9 on:(start, flowChart.highlight=["D","G"])
|
|
61
|
+
```
|
|
20
62
|
- Errors shown inline in the output
|
|
21
63
|
- Importable: `import {Mermaid} from "@lalalic/markcut/components"`
|
|
22
64
|
|
|
@@ -43,4 +85,5 @@ export function SuperMarkdown({ source }) {
|
|
|
43
85
|
- `@remotion/shapes` — render shapes like arrows, circles, rectangles, etc
|
|
44
86
|
- `@remotion/starburst` — render starburst animations
|
|
45
87
|
- `react-webcam-pro` — render webcam video
|
|
46
|
-
- `react-chartjs-2` — render charts with Chart.js at https://react-chartjs-2.js.org/components
|
|
88
|
+
- `react-chartjs-2` — render charts with Chart.js at `https://react-chartjs-2.js.org/components`
|
|
89
|
+
- `@xyflow/react` - render diagrams with `https://reactflow.dev/api-reference`
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Route & Vlog Map Videos — Agent Guide
|
|
2
|
+
|
|
3
|
+
> Make route / travel-vlog video clips with the `map` stream type: establishing
|
|
4
|
+
> dolly shots, route flyovers with photo stops, cinematic chase cameras, and
|
|
5
|
+
> immersive Street View moves — all narrated by TTS. This is the **agent-facing**
|
|
6
|
+
> reference: what effects you can get, how to discover spots, and copy-paste
|
|
7
|
+
> markdown. No implementation details.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. When to reach for this
|
|
12
|
+
|
|
13
|
+
Use `map` clips when the video is about **a place and getting around**:
|
|
14
|
+
|
|
15
|
+
- travel vlog: "we drove from the Golden Gate to SFO, stopping for photos"
|
|
16
|
+
- city tour: establishing shot → flyover → street level
|
|
17
|
+
- route explainers: commute, road trip, walking tour, "how to get there"
|
|
18
|
+
|
|
19
|
+
Every map clip can carry a `script` narration next to it (see §6 for the
|
|
20
|
+
scene pattern that plays map + narration together).
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 2. The 4 views = 4 effects
|
|
25
|
+
|
|
26
|
+
`view` is the one field that decides the whole feel. Default is `route`.
|
|
27
|
+
|
|
28
|
+
| view | effect | best for |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `overview` | **Dolly** — camera starts far (satellite) and flies down to the city/street | establishing shot, "this is where we are" |
|
|
31
|
+
| `route` | **Route map** — path + animated marker + optional photo thumbnails at each stop | itinerary, travel log |
|
|
32
|
+
| `cinematic` | **Chase flyover** — camera follows the route like a drone, road always "forward-up" | exciting transitions between stops |
|
|
33
|
+
| `streetview` | **Street View** — immersive pan / tilt / walk at ground level | being on location, "here we are" |
|
|
34
|
+
|
|
35
|
+
Example — one line each:
|
|
36
|
+
|
|
37
|
+
```md
|
|
38
|
+
- map view:overview duration:4 camera:{zoom:tween(6, 12, easeInOut)}
|
|
39
|
+
- map view:route duration:6 waypoints:[...]
|
|
40
|
+
- map view:cinematic duration:8 waypoints:[...] cinematic:{tilt:tween(0, 45)}
|
|
41
|
+
- map view:streetview duration:6 streetView:{location:{lat:37.77,lng:-122.41}, pov:{heading:tween(200, 320)}}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 3. Animating the camera — `tween()`
|
|
47
|
+
|
|
48
|
+
Any camera value can be **static** (a number) or **animated** with a tween
|
|
49
|
+
expression written directly in markdown:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
tween(from, to, easing?)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| easing | feel |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `easeInOut` (default) | smooth ease at both ends — default for camera moves |
|
|
58
|
+
| `easeIn` | starts slow, finishes fast |
|
|
59
|
+
| `easeOut` | starts fast, settles |
|
|
60
|
+
| `linear` | constant speed — use for constant pan |
|
|
61
|
+
|
|
62
|
+
Fields that accept `tween(...)`:
|
|
63
|
+
|
|
64
|
+
- `camera.zoom` — `tween(6, 12)` = dolly in (higher zoom = closer)
|
|
65
|
+
- `camera.heading` — `tween(0, 90)` = rotate the map
|
|
66
|
+
- `camera.tilt` — `tween(0, 45)` = tilt reveal (top-down → perspective)
|
|
67
|
+
- `camera.center.{lat,lng}` — fly across the map
|
|
68
|
+
- `cinematic.tilt` / `cinematic.range` / `cinematic.roll` — 3D camera moves
|
|
69
|
+
- `streetView.pov.heading` — pan: `tween(200, 320)` sweeps the view;
|
|
70
|
+
`tween(200, 560)` = whip pan
|
|
71
|
+
- `streetView.pov.pitch` — `tween(0, -10)` tilts from sky to street
|
|
72
|
+
- `streetView.zoom` — Street View field-of-view: `tween(0, 1)` = dolly-zoom feel
|
|
73
|
+
|
|
74
|
+
Rules of thumb:
|
|
75
|
+
|
|
76
|
+
- **Zoom is not linear in perceived distance** — use `easeInOut` to hide the
|
|
77
|
+
"pop". For a strong dolly: `zoom:tween(5, 13)` (city region → streets).
|
|
78
|
+
- Combine tweens for a richer move, e.g. dolly + tilt reveal:
|
|
79
|
+
`camera:{zoom:tween(12, 14, easeInOut)} cinematic:{tilt:tween(0, 45, easeInOut)}`.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 4. Photo stops (the vlog touch)
|
|
84
|
+
|
|
85
|
+
`waypoints` take an optional **4th field = media** (image path or URL). It
|
|
86
|
+
renders as a small thumbnail marker at that stop — perfect for "here's the
|
|
87
|
+
place" moments in a vlog:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
[lat,lng,"Label"] # plain stop
|
|
91
|
+
[lat,lng,"Label","assets/photo.jpg"] # stop with photo thumbnail
|
|
92
|
+
[lat,lng,"","assets/photo.jpg"] # empty label = thumbnail only
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Full example:
|
|
96
|
+
|
|
97
|
+
```md
|
|
98
|
+
- map view:route duration:6 travelMode:DRIVING mapType:roadmap routeColor:"#4285F4" routeWeight:5 routeMarker:"🚗"
|
|
99
|
+
waypoints:[37.8199,-122.4783,"Golden Gate","https://picsum.photos/seed/gg/96/96"; 37.7749,-122.4194,"Civic Center","https://picsum.photos/seed/civic/96/96"; 37.6213,-122.3790,"SFO","https://picsum.photos/seed/sfo/96/96"]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 5. Discovering spots along a route — `markcut spots`
|
|
105
|
+
|
|
106
|
+
Don't hand-pick coordinates for a route video — **discover** interesting stops
|
|
107
|
+
with the spots CLI. It samples points along a route (Directions API) and ranks
|
|
108
|
+
nearby places by `rating × log(ratings)`, then hands you ready-to-paste
|
|
109
|
+
markdown.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npx @lalalic/markcut spots --waypoints "37.8199,-122.4783;37.6213,-122.3790" \
|
|
113
|
+
--travelMode DRIVING --limit 8 --photos --markdown
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
| flag | meaning |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `--waypoints "lat,lng;lat,lng"` | route endpoints (semicolon-separated) — required |
|
|
119
|
+
| `--travelMode DRIVING\|WALKING\|BICYCLING` | how you travel the route (default DRIVING) |
|
|
120
|
+
| `--limit <n>` | max spots after ranking (default 8) |
|
|
121
|
+
| `--photos` | attach a photo URL per spot (becomes `waypoint.media`) |
|
|
122
|
+
| `--markdown` | print a copy-paste `waypoints:[...]` line to stderr |
|
|
123
|
+
| `--output spots.json` | also save the full JSON |
|
|
124
|
+
|
|
125
|
+
**Agent workflow — the tool only discovers, you compose:**
|
|
126
|
+
|
|
127
|
+
1. Get route endpoints from the user's media/GPS or pick two landmarks.
|
|
128
|
+
2. `markcut spots --waypoints "..." --photos --markdown`
|
|
129
|
+
3. Pick the 2–5 spots that fit the narrative arc (opening, highlights, ending).
|
|
130
|
+
4. Build a `map` storyboard with those `waypoints` + `script` narration.
|
|
131
|
+
5. `markcut preview` to check, `markcut render` to export.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 6. Markdown examples (copy-paste)
|
|
136
|
+
|
|
137
|
+
### 6.1 Simple route clip with narration
|
|
138
|
+
|
|
139
|
+
Map + narration play together inside a `layout:parallel` scene; scenes are
|
|
140
|
+
sequenced by the root `layout:series`:
|
|
141
|
+
|
|
142
|
+
```md
|
|
143
|
+
# video
|
|
144
|
+
width:1080 height:1920 layout:series transition:fade transitionTime:0.5
|
|
145
|
+
|
|
146
|
+
## The-Drive
|
|
147
|
+
layout:parallel
|
|
148
|
+
- script "We leave the Golden Gate and head down to the airport."
|
|
149
|
+
- map view:route duration:6 travelMode:DRIVING mapType:roadmap routeColor:"#4285F4" routeWeight:5 routeMarker:"🚗"
|
|
150
|
+
waypoints:[37.8199,-122.4783,"Golden Gate"; 37.7749,-122.4194,"Civic Center"; 37.6213,-122.3790,"SFO"]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 6.2 Establishing dolly → cinematic chase (two scenes)
|
|
154
|
+
|
|
155
|
+
```md
|
|
156
|
+
# video
|
|
157
|
+
width:1080 height:1920 layout:series transition:fade
|
|
158
|
+
|
|
159
|
+
## Opening-Dive-In
|
|
160
|
+
layout:parallel
|
|
161
|
+
- script "High above San Francisco, then we dive into the streets."
|
|
162
|
+
- map view:overview mapType:satellite duration:4 camera:{zoom:tween(6, 12, easeInOut)}
|
|
163
|
+
|
|
164
|
+
## Chase-The-Road
|
|
165
|
+
layout:parallel
|
|
166
|
+
- script "The camera chases the route like a drone."
|
|
167
|
+
- map view:cinematic duration:8 travelMode:DRIVING mapType:satellite routeMarker:"🚗"
|
|
168
|
+
cinematic:{mode:flyAlong, headingFollow:true, tilt:tween(0, 45, easeInOut)}
|
|
169
|
+
camera:{zoom:tween(12, 14, easeInOut)}
|
|
170
|
+
waypoints:[37.8199,-122.4783,"Golden Gate"; 37.7749,-122.4194,"Civic Center"; 37.6213,-122.3790,"SFO"]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 6.3 Street-level: pan, then walk
|
|
174
|
+
|
|
175
|
+
```md
|
|
176
|
+
# video
|
|
177
|
+
width:1080 height:1920 layout:series transition:fade
|
|
178
|
+
|
|
179
|
+
## On-The-Street-Pan
|
|
180
|
+
layout:parallel
|
|
181
|
+
- script "And here we are, right on the sidewalk."
|
|
182
|
+
- map view:streetview duration:8 streetView:{location:{lat:37.7793,lng:-122.4193}, radius:50, pov:{heading:tween(200, 420, easeInOut), pitch:tween(0, -8)}, zoom:tween(0, 0.6, easeInOut)}
|
|
183
|
+
|
|
184
|
+
## A-Quick-Walk
|
|
185
|
+
layout:parallel
|
|
186
|
+
- script "A short stroll down the block."
|
|
187
|
+
- map view:streetview duration:6 streetView:{route:[{lat:37.7793,lng:-122.4193},{lat:37.7785,lng:-122.4185},{lat:37.7777,lng:-122.4178}], radius:50, pov:{heading:tween(0, 40, easeInOut), pitch:-5}}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 6.4 Full route-vlog recipe (5 scenes)
|
|
191
|
+
|
|
192
|
+
Combine everything for a complete vlog arc: **establish → route with photos →
|
|
193
|
+
cinematic → street view pan → walk**.
|
|
194
|
+
|
|
195
|
+
```md
|
|
196
|
+
# video
|
|
197
|
+
width:1080 height:1920 layout:series transition:fade transitionTime:0.5
|
|
198
|
+
|
|
199
|
+
## Satellite-Dolly
|
|
200
|
+
layout:parallel
|
|
201
|
+
- script "We begin high above San Francisco, then dive into the city."
|
|
202
|
+
- map view:overview mapType:satellite duration:4 center:{lat:37.7749,lng:-122.4194} camera:{zoom:tween(6, 12, easeInOut)}
|
|
203
|
+
|
|
204
|
+
## Route
|
|
205
|
+
layout:parallel
|
|
206
|
+
- script "The route winds from the Golden Gate to the airport, with photos at each stop."
|
|
207
|
+
- map view:route duration:6 travelMode:DRIVING mapType:roadmap routeColor:"#4285F4" routeWeight:5 routeMarker:"🚗" waypoints:[37.8199,-122.4783,"Golden Gate","https://picsum.photos/seed/gg-bridge/96/96"; 37.7749,-122.4194,"Civic Center","https://picsum.photos/seed/civic-center/96/96"; 37.6213,-122.3790,"SFO","https://picsum.photos/seed/sfo-airport/96/96"]
|
|
208
|
+
|
|
209
|
+
## Cinematic
|
|
210
|
+
layout:parallel
|
|
211
|
+
- script "The camera tilts and chases the road like a drone."
|
|
212
|
+
- map view:cinematic duration:8 travelMode:DRIVING mapType:satellite routeMarker:"🚗" cinematic:{mode:flyAlong, headingFollow:true, tilt:tween(0, 45, easeInOut)} camera:{zoom:tween(12, 14, easeInOut)} waypoints:[37.8199,-122.4783,"Golden Gate"; 37.7749,-122.4194,"Civic Center"; 37.6213,-122.3790,"SFO"]
|
|
213
|
+
|
|
214
|
+
## Street-View
|
|
215
|
+
layout:parallel
|
|
216
|
+
- script "And finally, we land on the street itself."
|
|
217
|
+
- map view:streetview duration:8 streetView:{location:{lat:37.7793,lng:-122.4193}, radius:50, pov:{heading:tween(200, 420, easeInOut), pitch:tween(0, -8)}, zoom:tween(0, 0.6, easeInOut)}
|
|
218
|
+
|
|
219
|
+
## Street-View-Walk
|
|
220
|
+
layout:parallel
|
|
221
|
+
- script "A quick walk down the block."
|
|
222
|
+
- map view:streetview duration:6 streetView:{route:[{lat:37.7793,lng:-122.4193},{lat:37.7785,lng:-122.4185},{lat:37.7777,lng:-122.4178},{lat:37.7769,lng:-122.4170}], radius:50, pov:{heading:tween(0, 40, easeInOut), pitch:-5}}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 7. Recipe — build a route/vlog clip
|
|
228
|
+
|
|
229
|
+
1. **Get the route** — endpoints from the user's media/GPS, or two landmarks.
|
|
230
|
+
2. **Discover spots** — `markcut spots --waypoints "lat,lng;lat,lng" --photos --markdown`; pick 2–5 that tell the story.
|
|
231
|
+
3. **Pick views per scene** — overview (establish) → route with photo stops (log) → cinematic (transition) → streetview (arrive).
|
|
232
|
+
4. **Write narration** — one `script` per scene; the resolver sizes the clip from the TTS audio automatically (don't hand-set durations that depend on script length).
|
|
233
|
+
5. **Verify** — `markcut preview book.md` (screenshot key frames, check map/pano loaded and captions), then `markcut render book.md --output out/vlog.mp4`.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 8. Tips & pitfalls
|
|
238
|
+
|
|
239
|
+
- **Map + narration**: put `- map ...` and `- script "..."` as sibling bullets in a `layout:parallel` scene so they play together. If the scene is `series`, narration plays alone first and the map shows nothing while speaking.
|
|
240
|
+
- **Street View coverage is not universal** — a spot may have no panorama. If a `streetview` scene looks dark/empty, pick a nearby well-covered location or use `view:cinematic` instead.
|
|
241
|
+
- **Route maps need ≥2 waypoints**; a single point falls back to a plain marker.
|
|
242
|
+
- **Media assets** — photo thumbnails (`waypoint.media`), images, bgm etc. should live in the md file's folder (e.g. `assets/...`). See the asset-path rules in `markdown-descriptive.md` §14 (verify enforces md-folder-relative paths).
|
|
243
|
+
- **Don't set duration from script length** — the resolver computes it from TTS audio.
|
|
244
|
+
- **3D (Map3D)** is experimental and opt-in (`cinematic.fallback:"none"`); the default `fallback:"2d"` gives the safe drone-chase look.
|
|
@@ -69,6 +69,7 @@ Where:
|
|
|
69
69
|
| `audio` | `audio` | Voiceover, BGM, SFX | `src` + (`duration` or `endAt`) |
|
|
70
70
|
| `script "..."` | `audio` | Narration/TTS (shorthand for audio with script) | `script` text (the `"..."` is the primary content) |
|
|
71
71
|
| `component` | `component` | JSX React component | `jsx` (inline or code fence) + `duration` (unless `isBackground`) |
|
|
72
|
+
| `event` | `component` (stub) | **Event-only** — renders nothing, fires JS on registered components at a specific frame | `on:(when, state)` + `start` + `duration` |
|
|
72
73
|
| `rhythm` | `rhythm` | Beat-synced audio with timed children | `src`, `spots`, `children` |
|
|
73
74
|
| `map` | `map` | Animated route visualization | `duration`, `waypoints` |
|
|
74
75
|
| `include` | `include` | Embed external `.md` sub-video | `src` (path to another `.md` file) |
|
|
@@ -112,10 +113,13 @@ Fire JS expression at a specific frame to mutate component state.
|
|
|
112
113
|
```
|
|
113
114
|
- script "Narration" on:(start, slide.current=0)
|
|
114
115
|
- script "Beat" on:(50%, slide.current++)
|
|
116
|
+
- event duration:3 start:6 on:(start, slide.current=1)
|
|
115
117
|
```
|
|
116
118
|
|
|
117
119
|
`when`: `start`/`end`/`50%` (percent) / `2.5s` (seconds value). `state`: any JS expression.
|
|
118
120
|
|
|
121
|
+
Use the `event` type token to create an **event-only stub** — a node that renders nothing and exists solely to fire an event at a specific time. This avoids boilerplate like `- component duration:3 jsx:"<></>" start:6 on:(...)`.
|
|
122
|
+
|
|
119
123
|
#### Lists — `spots:[n,n,n]`, `start:n`
|
|
120
124
|
|
|
121
125
|
```
|
|
@@ -160,8 +164,7 @@ When a property value is too long for a single line (JSX, prompts, scripts, mark
|
|
|
160
164
|
| `~~~jsx jsx` or `~~~jsx` | `jsx` | Component JSX expression |
|
|
161
165
|
| `~~~prompt prompt` | `prompt` | TTI/TTV generation prompt |
|
|
162
166
|
| `~~~script script` or `~~~script` | `script` | Narration text on audio nodes |
|
|
163
|
-
| `~~~css stylesheet` | `stylesheet` | Global CSS (only valid at root level) |
|
|
164
|
-
| `~~~md <key>` | arbitrary | Markdown content for a specific key (e.g., `source` for `react-markdown`) |
|
|
167
|
+
| `~~~css stylesheet` | `stylesheet` | Global CSS (only valid at root level) || `~~~mermaid` | `mermaid` | Mermaid diagram source (referenced as `{mermaid}` in JSX) || `~~~md <key>` | arbitrary | Markdown content for a specific key (e.g., `source` for `react-markdown`) |
|
|
165
168
|
|
|
166
169
|
### 7. Scene Metadata Block
|
|
167
170
|
|
|
@@ -265,6 +268,8 @@ all src paths are resolved relative to the markdown file's location. For example
|
|
|
265
268
|
### 14. verify
|
|
266
269
|
- all assets path are resolved relative to the markdown file's location.
|
|
267
270
|
- `npx @lalalic/markcut verify book.md` to verify the markdown document is valid and can be rendered. It will check for missing required fields, invalid values, and other common issues.
|
|
271
|
+
- **Asset-relative check**: `verify` also walks the tree and requires every local asset (image/video/audio/subtitle `.vtt`, map `waypoints[].media`, root `subtitle.src`) to be a path **relative to the markdown file's folder** — no absolute paths (`/...`), no `..` escapes. Remote URLs (`https:`, `data:`, etc.) and inline subtitle text are fine. Violations are printed as actionable errors (exit 1) so an agent can fix the source and re-run.
|
|
272
|
+
- `markcut render` runs the same check on the compiled tree right before rendering and aborts with errors instead of failing mid-render with 404s.
|
|
268
273
|
|
|
269
274
|
|
|
270
275
|
## Template Variables
|
|
@@ -11,39 +11,153 @@ import { Mermaid } from "./Mermaid";
|
|
|
11
11
|
*
|
|
12
12
|
* The `source` prop is the markdown string to render.
|
|
13
13
|
*
|
|
14
|
+
* `highlight` is an array of 0-based list item indices to highlight.
|
|
15
|
+
* Works for both ordered (`ol`) and unordered (`ul`) lists. Matching
|
|
16
|
+
* `<li>` elements get CSS class `highlight-list-item` (intended for use
|
|
17
|
+
* with a `classDef` or a custom stylesheet).
|
|
18
|
+
*
|
|
14
19
|
* Built-in — no imports or frontmatter registration needed.
|
|
15
20
|
*/
|
|
16
21
|
export interface MarkdownProps {
|
|
17
|
-
|
|
22
|
+
children?: string;
|
|
18
23
|
source?: string;
|
|
19
24
|
className?: string;
|
|
20
25
|
plugins?: any[];
|
|
21
26
|
components?: any;
|
|
27
|
+
/** 0-based indices of list items to highlight. */
|
|
28
|
+
highlight?: number[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Context to pass highlightList down to list renderers
|
|
32
|
+
const ListCtx = React.createContext<number[]>([]);
|
|
33
|
+
|
|
34
|
+
function OrderedList({
|
|
35
|
+
children,
|
|
36
|
+
...props
|
|
37
|
+
}: React.ComponentPropsWithoutRef<"ol">) {
|
|
38
|
+
const hl = React.useContext(ListCtx);
|
|
39
|
+
const items = React.Children.toArray(children).filter(
|
|
40
|
+
(c): c is React.ReactElement => React.isValidElement(c) && c.type === "li",
|
|
41
|
+
);
|
|
42
|
+
return (
|
|
43
|
+
<ol {...props}>
|
|
44
|
+
{items.map((child, i) => {
|
|
45
|
+
if (hl.includes(i)) {
|
|
46
|
+
return React.cloneElement(child, { className: "highlight-list-item", key: (child as any).key } as any);
|
|
47
|
+
}
|
|
48
|
+
return child;
|
|
49
|
+
})}
|
|
50
|
+
</ol>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function UnorderedList({
|
|
55
|
+
children,
|
|
56
|
+
...props
|
|
57
|
+
}: React.ComponentPropsWithoutRef<"ul">) {
|
|
58
|
+
const hl = React.useContext(ListCtx);
|
|
59
|
+
const items = React.Children.toArray(children).filter(
|
|
60
|
+
(c): c is React.ReactElement => React.isValidElement(c) && c.type === "li",
|
|
61
|
+
);
|
|
62
|
+
return (
|
|
63
|
+
<ul {...props}>
|
|
64
|
+
{items.map((child, i) => {
|
|
65
|
+
if (hl.includes(i)) {
|
|
66
|
+
return React.cloneElement(child, { className: "highlight-list-item", key: (child as any).key } as any);
|
|
67
|
+
}
|
|
68
|
+
return child;
|
|
69
|
+
})}
|
|
70
|
+
</ul>
|
|
71
|
+
);
|
|
22
72
|
}
|
|
23
73
|
|
|
24
|
-
export function Markdown({
|
|
74
|
+
export function Markdown({
|
|
75
|
+
children,
|
|
76
|
+
source = children,
|
|
77
|
+
className,
|
|
78
|
+
plugins,
|
|
79
|
+
components: propComponents,
|
|
80
|
+
highlight,
|
|
81
|
+
}: MarkdownProps) {
|
|
82
|
+
const hl = React.useMemo(() => highlight ?? [], [highlight]);
|
|
83
|
+
|
|
84
|
+
// Inject default styles once
|
|
85
|
+
React.useEffect(() => {
|
|
86
|
+
const id = "markcut-markdown-defaults";
|
|
87
|
+
if (document.getElementById(id)) return;
|
|
88
|
+
const el = document.createElement("style");
|
|
89
|
+
el.id = id;
|
|
90
|
+
el.textContent = `
|
|
91
|
+
.highlight-list-item {
|
|
92
|
+
background: rgba(255, 215, 0, 0.15);
|
|
93
|
+
border-left: 3px solid #ffd700;
|
|
94
|
+
padding-left: 8px;
|
|
95
|
+
border-radius: 0 4px 4px 0;
|
|
96
|
+
}
|
|
97
|
+
.slide {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
align-items: center;
|
|
101
|
+
justify-content: center;
|
|
102
|
+
padding: 24px;
|
|
103
|
+
text-align: center;
|
|
104
|
+
}
|
|
105
|
+
.slide h1 { font-size: 2em; margin: 0.4em 0; font-weight: 700; }
|
|
106
|
+
.slide h2 { font-size: 1.6em; margin: 0.35em 0; font-weight: 600; }
|
|
107
|
+
.slide h3 { font-size: 1.3em; margin: 0.3em 0; font-weight: 600; }
|
|
108
|
+
.slide p { margin: 0.6em 0; line-height: 1.6; }
|
|
109
|
+
.slide ul, .slide ol { margin: 0.5em 0; padding-left: 1.5em; text-align: left; }
|
|
110
|
+
.slide li { margin: 0.3em 0; }
|
|
111
|
+
.slide blockquote {
|
|
112
|
+
margin: 0.6em 0;
|
|
113
|
+
padding: 0.4em 1em;
|
|
114
|
+
border-left: 3px solid rgba(255,255,255,.3);
|
|
115
|
+
font-style: italic;
|
|
116
|
+
opacity: .85;
|
|
117
|
+
}
|
|
118
|
+
.slide code {
|
|
119
|
+
background: rgba(255,255,255,.08);
|
|
120
|
+
padding: 0.15em 0.4em;
|
|
121
|
+
border-radius: 4px;
|
|
122
|
+
font-size: 0.9em;
|
|
123
|
+
}
|
|
124
|
+
.slide pre { margin: 0.6em 0; text-align: left; width: 100%; }
|
|
125
|
+
.slide a { color: #4a9eff; text-decoration: none; }
|
|
126
|
+
.slide a:hover { text-decoration: underline; }
|
|
127
|
+
`;
|
|
128
|
+
document.head.appendChild(el);
|
|
129
|
+
return () => { document.getElementById(id)?.remove(); };
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
const mergedComponents = React.useMemo(
|
|
133
|
+
() => ({
|
|
134
|
+
...propComponents,
|
|
135
|
+
ol: OrderedList,
|
|
136
|
+
ul: UnorderedList,
|
|
137
|
+
pre: ({ children: preChildren }: { children: React.ReactNode }) => {
|
|
138
|
+
const code = React.Children.toArray(preChildren)[0] as React.ReactElement<any>;
|
|
139
|
+
if (code?.props?.className === "language-mermaid") {
|
|
140
|
+
return <Mermaid source={String(code.props.children)} />;
|
|
141
|
+
}
|
|
142
|
+
if (propComponents?.pre) {
|
|
143
|
+
return (propComponents.pre as any)({ children: preChildren });
|
|
144
|
+
}
|
|
145
|
+
return <pre>{preChildren}</pre>;
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
[propComponents],
|
|
149
|
+
);
|
|
150
|
+
|
|
25
151
|
return (
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
} else if (components.pre) {
|
|
37
|
-
return components.pre({ children });
|
|
38
|
-
}
|
|
39
|
-
return <pre>{children}</pre>;
|
|
40
|
-
},
|
|
41
|
-
}),
|
|
42
|
-
[components]
|
|
43
|
-
)}
|
|
44
|
-
>
|
|
45
|
-
{source}
|
|
46
|
-
</ReactMarkdown>
|
|
47
|
-
</div>
|
|
152
|
+
<ListCtx.Provider value={hl}>
|
|
153
|
+
<div className={className}>
|
|
154
|
+
<ReactMarkdown
|
|
155
|
+
remarkPlugins={React.useMemo(() => [remarkGfm, ...(plugins || [])], [plugins])}
|
|
156
|
+
components={mergedComponents}
|
|
157
|
+
>
|
|
158
|
+
{source}
|
|
159
|
+
</ReactMarkdown>
|
|
160
|
+
</div>
|
|
161
|
+
</ListCtx.Provider>
|
|
48
162
|
);
|
|
49
163
|
}
|