@lalalic/markcut 3.1.0 → 3.2.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 +5 -1
- package/skills/markcut/SKILL.md +14 -17
- package/skills/markcut/docs/map-dynamic-camera.md +92 -8
- package/skills/markcut/docs/markdown-descriptive.md +1 -1
- package/skills/markcut/review.md +480 -0
- package/src/descriptive/compiler.ts +60 -1
- package/src/descriptive/dsl.ts +27 -7
- package/src/descriptive/markdown.ts +2 -1
- package/src/descriptive/resolve.test.ts +98 -0
- package/src/descriptive/resolve.ts +156 -12
- package/src/player/bundle/player.js +222961 -222169
- package/src/player/pipeline.mjs +255 -17
- package/src/schema/index.ts +4 -0
- package/src/types/Effect.tsx +12 -1
- package/src/types/Map.tsx +649 -75
- package/src/utils/directions.ts +101 -0
- package/src/utils/index.ts +11 -0
- package/src/utils/route-legs.ts +199 -0
- package/tests/dsl.test.ts +35 -0
- package/tests/evals/README.md +41 -0
- package/tests/evals/dataset.json +170 -0
- package/tests/evals/gen_dataset.py +63 -0
- package/tests/evals/metrics.py +70 -0
- package/tests/evals/openrouter_model.py +143 -0
- package/tests/evals/storyboard_app.py +55 -0
- package/tests/evals/test_storyboard.py +32 -0
- package/tests/fixtures/map-overlay.json +56 -0
- package/tests/fixtures/md/map-all-views.md +8 -1
- package/tests/fixtures/md/map-children.md +11 -0
- package/tests/fixtures/md/map-multimode.md +9 -0
- package/tests/fixtures/streetview-walk.json +36 -0
- package/tests/md-descriptive.test.ts +75 -0
- package/tests/render.test.ts +92 -0
- package/tests/route-legs.test.ts +178 -0
- package/tests/schema.test.ts +18 -0
- package/.vscode/settings.json +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lalalic/markcut",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Markdown-to-video engine. Describe scenes in markdown, get a rendered video.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"markcut": "bin/markcut"
|
|
@@ -65,5 +65,9 @@
|
|
|
65
65
|
"@types/react-dom": "^19.0.0",
|
|
66
66
|
"typescript": "^5.6.0",
|
|
67
67
|
"vitest": "^2.1.0"
|
|
68
|
+
},
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "git+https://github.com/lalalic/markcut.git"
|
|
68
72
|
}
|
|
69
73
|
}
|
package/skills/markcut/SKILL.md
CHANGED
|
@@ -18,6 +18,13 @@ Everything video is a **stream tree** described with markdown. see [docs/markdow
|
|
|
18
18
|
|
|
19
19
|
- Use `scene` nodes to organize your video. Scenes can nest inside other scenes.
|
|
20
20
|
- Use `description`, `scene.instruction`, `script`, `image|video.prompt` to structure your video content.
|
|
21
|
+
- **If a scene has a `script` (or `audio`) plus one primary visual, the visual MUST be `isBackground:true`.** Without it the visual plays only its own duration (3s default for images) while narration continues — the rest of the scene is a black screen. Scene duration follows the audio, not the image.
|
|
22
|
+
|
|
23
|
+
```md
|
|
24
|
+
## scene-1
|
|
25
|
+
- image prompt:"..." isBackground:true # fills the whole scene, narration decides duration
|
|
26
|
+
- script "..."
|
|
27
|
+
```
|
|
21
28
|
|
|
22
29
|
see [docs/markdown-descriptive.md](docs/markdown-descriptive.md) for full details.
|
|
23
30
|
|
|
@@ -71,15 +78,12 @@ npx @lalalic/markcut spots --waypoints "lat,lng;lat,lng" # discover POIs along a
|
|
|
71
78
|
|
|
72
79
|
---
|
|
73
80
|
|
|
74
|
-
##
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
- screenshot some key frames, and understand image to verify intent
|
|
81
|
-
- stt the final video audio, and verify if vtt result is correct
|
|
82
|
-
|
|
81
|
+
## review
|
|
82
|
+
use the review contract defined in [./review.md](./review.md) to guide the review process.
|
|
83
|
+
review as early as possible in the video production process to catch issues before they propagate.
|
|
84
|
+
* review md file
|
|
85
|
+
* review compiled.json
|
|
86
|
+
* review the rendered video
|
|
83
87
|
|
|
84
88
|
## Reference
|
|
85
89
|
|
|
@@ -89,8 +93,7 @@ some common issues (photo or video can't be displayed, audio missing), take belo
|
|
|
89
93
|
| Route / vlog map clips (effects, spots, examples) | [docs/map-dynamic-camera.md](docs/map-dynamic-camera.md) |
|
|
90
94
|
| Built-in components & common npm packages | [docs/components.md](docs/components.md) |
|
|
91
95
|
| Sound effects | [docs/sound-effects.md](docs/sound-effects.md) |
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
| Review contract | [./review.md](./review.md) |
|
|
94
97
|
|
|
95
98
|
## Built-in Components
|
|
96
99
|
|
|
@@ -98,12 +101,6 @@ Built-in components available via `@lalalic/markcut/components`. See [docs/compo
|
|
|
98
101
|
|
|
99
102
|
|
|
100
103
|
## Golden rule
|
|
101
|
-
- always check stream start and duration to avoid
|
|
102
|
-
- audio cut off
|
|
103
|
-
- video cut off
|
|
104
|
-
- subtitle mismatch
|
|
105
|
-
- sync issues between audio, video, and subtitles
|
|
106
|
-
according to the content change. rm `.markcut` will cause all content to be regenerated, which is time consuming and wasteful.
|
|
107
104
|
- put all manual assets in `assets` folder, such as bgm, logo, watermark, etc. don't put them in `.markcut` folder, which is auto generated and will be deleted when `markcut clean` command is run.
|
|
108
105
|
|
|
109
106
|
### Don'ts
|
|
@@ -16,7 +16,7 @@ Use `map` clips when the video is about **a place and getting around**:
|
|
|
16
16
|
- city tour: establishing shot → flyover → street level
|
|
17
17
|
- route explainers: commute, road trip, walking tour, "how to get there"
|
|
18
18
|
|
|
19
|
-
Every map clip can carry a `script` narration next to it (see §
|
|
19
|
+
Every map clip can carry a `script` narration next to it (see §7 for the
|
|
20
20
|
scene pattern that plays map + narration together).
|
|
21
21
|
|
|
22
22
|
---
|
|
@@ -101,7 +101,85 @@ Full example:
|
|
|
101
101
|
|
|
102
102
|
---
|
|
103
103
|
|
|
104
|
-
## 5.
|
|
104
|
+
## 5. Per-leg travel modes (flight ✈️ / boat 🚢 / walk 🚶 / drive 🚗)
|
|
105
|
+
|
|
106
|
+
A route is a sequence of **legs** — one per consecutive waypoint pair. Each
|
|
107
|
+
waypoint can tag the leg that **leaves it** with its own travel mode. The
|
|
108
|
+
parser is smart: a **bare (unquoted) mode word anywhere after lat/lng** is the
|
|
109
|
+
outgoing-leg mode — no empty slots needed. Untagged waypoints fall back to
|
|
110
|
+
the map's `travelMode` (default DRIVING).
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
[lat,lng,"Label","media",MODE] # full
|
|
114
|
+
[lat,lng,"Label",MODE] # mode without media (no empty "") — smart
|
|
115
|
+
[lat,lng,MODE] # just a mode
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
| mode | how it renders |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `DRIVING` (default) | Google Directions road route — solid line, 🚗 marker |
|
|
121
|
+
| `WALKING` | Google Directions pedestrian route — 🚶 marker |
|
|
122
|
+
| `BICYCLING` | Google Directions bike route — 🚲 marker |
|
|
123
|
+
| `TRANSIT` | Google Directions transit route — 🚌 marker |
|
|
124
|
+
| `FLIGHT` | **synthetic great-circle arc** (no road) — dashed yellow line, ✈️ marker |
|
|
125
|
+
| `BOAT` | **synthetic arc across water** — dashed teal line, 🚢 marker |
|
|
126
|
+
|
|
127
|
+
FLIGHT/BOAT have no Directions route, so they're drawn as curved "as the crow
|
|
128
|
+
flies" arcs and timed by distance at a cruise speed (✈️ ~850 km/h, 🚢 ~40 km/h).
|
|
129
|
+
The traveling marker switches glyph to the current leg's mode as it moves, and
|
|
130
|
+
time is split across legs **proportionally to each leg's duration**.
|
|
131
|
+
|
|
132
|
+
Rules:
|
|
133
|
+
- A mode word must be **unquoted** — quote it (`"FLIGHT"`) and it's a literal
|
|
134
|
+
label/media instead. That's the escape hatch if a media file is literally
|
|
135
|
+
named `FLIGHT`.
|
|
136
|
+
- Quoted-empty `""` slots are still accepted (backward compat).
|
|
137
|
+
- Remaining values map positionally: 1st = label, 2nd = media.
|
|
138
|
+
|
|
139
|
+
Example — fly in, take a boat, walk, then drive:
|
|
140
|
+
|
|
141
|
+
```md
|
|
142
|
+
- map view:route duration:12 travelMode:DRIVING
|
|
143
|
+
waypoints:[37.8199,-122.4783,"SFO",FLIGHT; 33.94,-118.41,"LAX",BOAT; 33.75,-118.28,"Long Beach",WALKING; 33.77,-118.19,"Promenade",DRIVING; 33.94,-118.41,"LAX"]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Legs above: SFO→LAX by ✈️, LAX→Long Beach by 🚢, Long Beach→Promenade by 🚶,
|
|
147
|
+
Promenade→LAX by 🚗 (the last waypoint has no outgoing leg, so its mode is
|
|
148
|
+
ignored).
|
|
149
|
+
|
|
150
|
+
## 6. Stops & anchored overlays (the vlog pattern)
|
|
151
|
+
|
|
152
|
+
The typical vlog: the pin moves leg-to-leg, then **stops at a hotspot** while a
|
|
153
|
+
photo/video/narration plays, then continues. Make other streams **children of
|
|
154
|
+
the map** and anchor them with `at:"WaypointLabel"` — the map renders them at
|
|
155
|
+
that waypoint's screen position (projected from the live map), so a photo
|
|
156
|
+
grows out of the pin's location.
|
|
157
|
+
|
|
158
|
+
```md
|
|
159
|
+
- map view:route duration:16 travelMode:DRIVING
|
|
160
|
+
waypoints:[37.81,-122.48,"Golden Gate"; 37.77,-122.42,"Civic Center"; 37.62,-122.38,"SFO"]
|
|
161
|
+
- image src:gg.jpg at:"Golden Gate" duration:3 effects:[zoomIn]
|
|
162
|
+
- image src:civic.jpg at:"Civic Center" duration:3 effects:[zoomIn]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- `at:"Label"` anchors the child at the waypoint with that label. The renderer
|
|
166
|
+
projects the waypoint's lat/lng to its screen pixel each frame and centers
|
|
167
|
+
the child there — combine with `effects:[zoomIn]` for a "grows out of the
|
|
168
|
+
pin" reveal.
|
|
169
|
+
- Children **without** `at` render full-screen over the map.
|
|
170
|
+
- **Auto-timing (default)** — give the child a `duration` and *no* `start`; at
|
|
171
|
+
resolve time the map computes each leg's travel time (Directions REST for
|
|
172
|
+
road modes, cruise speed for `FLIGHT`/`BOAT`) and schedules the child to
|
|
173
|
+
start **at the pin's arrival** at that waypoint. The pin pauses (dwells)
|
|
174
|
+
while the child plays, then resumes. Real drive times are used only for
|
|
175
|
+
*ratios* — the video length stays the map's `duration`, or a default 10s
|
|
176
|
+
drive budget + dwells when the map has none, so a 40-minute drive still
|
|
177
|
+
plays as a short clip with the pin timing proportional to each leg.
|
|
178
|
+
- **Manual timing (opt-out)** — set `start` explicitly and the resolver leaves
|
|
179
|
+
it untouched (it still contributes to the pin's pause window).
|
|
180
|
+
- The map's own `duration`/`end` defines its span; children run inside it.
|
|
181
|
+
|
|
182
|
+
## 7. Discovering spots along a route — `markcut spots`
|
|
105
183
|
|
|
106
184
|
Don't hand-pick coordinates for a route video — **discover** interesting stops
|
|
107
185
|
with the spots CLI. It samples points along a route (Directions API) and ranks
|
|
@@ -132,7 +210,7 @@ npx @lalalic/markcut spots --waypoints "37.8199,-122.4783;37.6213,-122.3790" \
|
|
|
132
210
|
|
|
133
211
|
---
|
|
134
212
|
|
|
135
|
-
##
|
|
213
|
+
## 8. Markdown examples (copy-paste)
|
|
136
214
|
|
|
137
215
|
### 6.1 Simple route clip with narration
|
|
138
216
|
|
|
@@ -184,9 +262,15 @@ layout:parallel
|
|
|
184
262
|
## A-Quick-Walk
|
|
185
263
|
layout:parallel
|
|
186
264
|
- script "A short stroll down the block."
|
|
187
|
-
- map view:streetview duration:6 streetView:{route:[{lat:37.
|
|
265
|
+
- map view:streetview duration:6 streetView:{route:[{lat:37.7785,lng:-122.4185},{lat:37.7777,lng:-122.4178}], radius:50, pov:{heading:tween(0, 40, easeInOut), pitch:-5}}
|
|
188
266
|
```
|
|
189
267
|
|
|
268
|
+
> **Walk = snap between covered waypoints.** A `route` walk holds at each
|
|
269
|
+
> waypoint's panorama and jumps between them (not continuous per-frame
|
|
270
|
+
> movement — that would fire hundreds of Street View requests and get
|
|
271
|
+
> rate-limited into black frames). Every waypoint needs imagery; gaps show the
|
|
272
|
+
> last loaded panorama instead of black.
|
|
273
|
+
|
|
190
274
|
### 6.4 Full route-vlog recipe (5 scenes)
|
|
191
275
|
|
|
192
276
|
Combine everything for a complete vlog arc: **establish → route with photos →
|
|
@@ -219,12 +303,12 @@ layout:parallel
|
|
|
219
303
|
## Street-View-Walk
|
|
220
304
|
layout:parallel
|
|
221
305
|
- script "A quick walk down the block."
|
|
222
|
-
- map view:streetview duration:6 streetView:{route:[{lat:37.
|
|
306
|
+
- map view:streetview duration:6 streetView:{route:[{lat:37.7785,lng:-122.4185},{lat:37.7777,lng:-122.4178}], radius:50, pov:{heading:tween(0, 40, easeInOut), pitch:-5}}
|
|
223
307
|
```
|
|
224
308
|
|
|
225
309
|
---
|
|
226
310
|
|
|
227
|
-
##
|
|
311
|
+
## 9. Recipe — build a route/vlog clip
|
|
228
312
|
|
|
229
313
|
1. **Get the route** — endpoints from the user's media/GPS, or two landmarks.
|
|
230
314
|
2. **Discover spots** — `markcut spots --waypoints "lat,lng;lat,lng" --photos --markdown`; pick 2–5 that tell the story.
|
|
@@ -234,10 +318,10 @@ layout:parallel
|
|
|
234
318
|
|
|
235
319
|
---
|
|
236
320
|
|
|
237
|
-
##
|
|
321
|
+
## 10. Tips & pitfalls
|
|
238
322
|
|
|
239
323
|
- **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.
|
|
324
|
+
- **Street View coverage is not universal** — a spot may have no panorama (Google returns "no imagery here"); interpolating between sparse points also crosses uncovered blocks. Pick waypoints you've verified have imagery (open the coords in Google Maps, drag the pegman), keep a `route` walk's waypoints on the same covered street, and set `radius` (meters) so the panorama search is constrained nearby. If a `streetview` scene still looks dark, use `view:cinematic` instead.
|
|
241
325
|
- **Route maps need ≥2 waypoints**; a single point falls back to a plain marker.
|
|
242
326
|
- **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
327
|
- **Don't set duration from script length** — the resolver computes it from TTS audio.
|
|
@@ -705,7 +705,7 @@ tween(#000, #FFF) — color interpolation
|
|
|
705
705
|
# Best practices
|
|
706
706
|
- Markcut engine will automatically determine the duration of background streams, video, script audio. Don't set duration for them if no special requirements (e.g., speed up/down on purpose).
|
|
707
707
|
- global streams (BGM, Logo, Lip-sync video,...) should be set on root level, and set `isBackground:true` to let it loop to fill the whole video duration.
|
|
708
|
-
- Set `isBackground:true` for static vision content
|
|
708
|
+
- **Set `isBackground:true` for static vision content (image) whenever the scene has audio/script.** Scene duration is driven by the audio (which can be much longer than the image's default 3s); without `isBackground` the image ends early and the scene tail plays over a black screen while narration continues.
|
|
709
709
|
- adjust audio start time with `start` property to avoid audio cut when transition effect is applied on vision scene
|
|
710
710
|
|
|
711
711
|
|