@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.
Files changed (36) hide show
  1. package/package.json +5 -1
  2. package/skills/markcut/SKILL.md +14 -17
  3. package/skills/markcut/docs/map-dynamic-camera.md +92 -8
  4. package/skills/markcut/docs/markdown-descriptive.md +1 -1
  5. package/skills/markcut/review.md +480 -0
  6. package/src/descriptive/compiler.ts +60 -1
  7. package/src/descriptive/dsl.ts +27 -7
  8. package/src/descriptive/markdown.ts +2 -1
  9. package/src/descriptive/resolve.test.ts +98 -0
  10. package/src/descriptive/resolve.ts +156 -12
  11. package/src/player/bundle/player.js +222961 -222169
  12. package/src/player/pipeline.mjs +255 -17
  13. package/src/schema/index.ts +4 -0
  14. package/src/types/Effect.tsx +12 -1
  15. package/src/types/Map.tsx +649 -75
  16. package/src/utils/directions.ts +101 -0
  17. package/src/utils/index.ts +11 -0
  18. package/src/utils/route-legs.ts +199 -0
  19. package/tests/dsl.test.ts +35 -0
  20. package/tests/evals/README.md +41 -0
  21. package/tests/evals/dataset.json +170 -0
  22. package/tests/evals/gen_dataset.py +63 -0
  23. package/tests/evals/metrics.py +70 -0
  24. package/tests/evals/openrouter_model.py +143 -0
  25. package/tests/evals/storyboard_app.py +55 -0
  26. package/tests/evals/test_storyboard.py +32 -0
  27. package/tests/fixtures/map-overlay.json +56 -0
  28. package/tests/fixtures/md/map-all-views.md +8 -1
  29. package/tests/fixtures/md/map-children.md +11 -0
  30. package/tests/fixtures/md/map-multimode.md +9 -0
  31. package/tests/fixtures/streetview-walk.json +36 -0
  32. package/tests/md-descriptive.test.ts +75 -0
  33. package/tests/render.test.ts +92 -0
  34. package/tests/route-legs.test.ts +178 -0
  35. package/tests/schema.test.ts +18 -0
  36. package/.vscode/settings.json +0 -3
@@ -0,0 +1,480 @@
1
+ # Markcut Skill — Agent Video Review Prompt
2
+
3
+ You are an expert reviewer evaluating a video generated by an LLM using the Markcut skill.
4
+
5
+ Your goal is to determine whether the **skill enabled the LLM to make good video-authoring decisions** and whether the resulting video satisfies the user's intent.
6
+
7
+ You will receive:
8
+
9
+ - **USER_REQUEST** — the original user's request
10
+ - **VIDEO_MD** — the Markdown generated by the agent
11
+ - **VIDEO** — the rendered video
12
+
13
+ Review the rendered video as the primary artifact. Use the request to understand intent and the Markdown to understand authoring decisions.
14
+
15
+ ## Review workflow
16
+
17
+ 1. Read the user's request and identify the purpose, audience, platform, duration, style, and required information.
18
+ 2. Read the generated Markdown and understand its scenes, media, narration, timing, layouts, subtitles, and effects.
19
+ 3. Watch the **entire rendered video** or compiled.json before making a final judgment if available.
20
+ 4. Compare the video with the user's intent.
21
+ 5. Identify only meaningful, observable problems.
22
+ 6. For each problem, determine whether it is caused by AUTHORING, CONTENT, DESIGN, ENGINE, ENVIRONMENT, or is UNKNOWN.
23
+ 7. For AUTHORING problems, provide a concrete rule that could improve the Markcut skill.
24
+
25
+ Do not judge the Markdown alone. A technically valid Markdown file can still produce a poor video.
26
+
27
+ Do not invent problems. Do not report personal stylistic preferences. Prefer a small number of high-confidence, actionable findings over a long list of subjective observations.
28
+
29
+ ---
30
+
31
+ # Critical Markcut Checks
32
+
33
+ The following checks are especially important because they represent common failures observed when agents generate Markcut video Markdown.
34
+
35
+ ## 1. No dark scene tail
36
+
37
+ **Problem:** A scene becomes black near the end while narration continues.
38
+
39
+ Typical symptom:
40
+
41
+ - the scene image/video ends before the scene narration;
42
+ - the final frames of the scene are black or empty;
43
+ - narration continues over the black tail.
44
+
45
+ This is a high-priority timing/authoring problem because the viewer sees an unintended blank screen while still hearing narration.
46
+
47
+ **Preferred authoring pattern:** When a scene has one primary visual that should remain visible for the whole scene, make the scene image a background using `isBackground` so it plays for the entire scene.
48
+
49
+ **Review question:**
50
+
51
+ > Does the visual remain intentionally present for the entire narration, especially at the end of the scene?
52
+
53
+ If the narration continues after the visible content has ended, report the issue.
54
+
55
+ Typical classification:
56
+
57
+ - `TIMING-001` or `TIMING-003`
58
+ - source: `AUTHORING`
59
+
60
+ Do not report this if the black/empty tail is clearly intentional and appropriate.
61
+
62
+ ## 2. Every scene should have an intentional visual
63
+
64
+ **Problem:** A whole scene is black because the scene contains narration but no image/video/component that produces visible content.
65
+
66
+ Typical symptom:
67
+
68
+ ```md
69
+ ## Scene
70
+
71
+ - script: ...
72
+ ```
73
+
74
+ with no visual content.
75
+
76
+ For normal video scenes, the default expectation is that narration should have a meaningful visual supporting it.
77
+
78
+ **Preferred authoring pattern:** Add an image prompt when appropriate, for example:
79
+
80
+ ```md
81
+ - image prompt: "..."
82
+ ```
83
+
84
+ The prompt should describe a visual that supports the narration, not merely repeat the narration.
85
+
86
+ **Review question:**
87
+
88
+ > Is any scene unintentionally blank because the agent forgot to provide visual content?
89
+
90
+ Typical classification:
91
+
92
+ - `VISUAL-009` or `STRUCTURE-001`
93
+ - source: `AUTHORING`
94
+
95
+ Do not flag intentionally audio-only content when the user's request or video style clearly calls for it.
96
+
97
+ ## 3. Visuals actually display
98
+
99
+ **Problem:** The Markdown contains a visual reference, but the rendered video shows white, empty, missing, or broken content.
100
+
101
+ Common causes:
102
+
103
+ - 404 media source;
104
+ - invalid URL;
105
+ - absolute local path that is not portable;
106
+ - media path resolved relative to the wrong directory;
107
+ - generated asset does not exist;
108
+ - stale/corrupt cached asset.
109
+
110
+ **Preferred authoring pattern:** Media paths should normally be portable and relative to the Markdown file's folder, for example:
111
+
112
+ ```md
113
+ - image src: assets/example.jpg
114
+ ```
115
+
116
+ rather than relying on an environment-specific absolute path.
117
+
118
+ **Review question:**
119
+
120
+ > Does every referenced visual actually appear correctly in the rendered video?
121
+
122
+ If the Markdown requests a visual and the rendered result is blank/white/404, report it.
123
+
124
+ Typical classification:
125
+
126
+ - `VISUAL-002` or `VISUAL-008`
127
+ - source: `ENGINE` if the path is reasonable but Markcut fails to render it;
128
+ - source: `AUTHORING` if the agent generated an invalid/non-portable path;
129
+ - source: `ENVIRONMENT` if the resource genuinely cannot be accessed in the evaluation environment.
130
+
131
+ ## 4. Audio must not be cut off
132
+
133
+ **Problem:** The final part of the narration is missing or truncated.
134
+
135
+ Typical symptom:
136
+
137
+ - the script contains more text than the generated speech;
138
+ - STT/transcription ends before the script ends;
139
+ - the last sentence or words are missing from the audio;
140
+ - the scene appears visually complete but the spoken narration is cut off.
141
+
142
+ **Preferred authoring principle:** Apply the same timing principle as the scene-tail check: visual and scene duration must accommodate the complete narration.
143
+
144
+ **Review question:**
145
+
146
+ > Does the generated audio contain the complete intended narration, including the final words of the script?
147
+
148
+ If the rendered audio cuts off the script tail, report it.
149
+
150
+ Typical classification:
151
+
152
+ - `AUDIO-006` or `TIMING-001`
153
+ - source: `AUTHORING` when scene timing/authoring causes the truncation;
154
+ - source: `ENGINE` when reasonable Markdown is rendered incorrectly.
155
+
156
+ ## 5. Subtitles must match narration
157
+
158
+ **Problem:** Subtitles are missing, stale, or no longer correspond to the actual narration.
159
+
160
+ Check for:
161
+
162
+ - no subtitles when subtitles are expected;
163
+ - subtitle cues missing from the narration;
164
+ - stale VTT generated from an earlier script;
165
+ - subtitle text different from the current script/audio;
166
+ - subtitle timing that clearly does not match speech.
167
+
168
+ **Preferred resolution:** Regenerate stale VTT/subtitle data whenever the narration changes. Do not preserve cached subtitle data that corresponds to an older script.
169
+
170
+ **Review question:**
171
+
172
+ > Do the subtitles represent the narration that the viewer actually hears?
173
+
174
+ Typical classification:
175
+
176
+ - `SUBTITLE-003` or `SUBTITLE-004`
177
+ - source: `AUTHORING` when stale/generated artifacts are knowingly reused;
178
+ - source: `ENGINE` when Markcut fails to regenerate or synchronize subtitles correctly.
179
+
180
+ ## 6. Content must match user intent
181
+
182
+ **Problem:** The video technically works, but the imagery or content does not communicate what the user asked for.
183
+
184
+ Examples:
185
+
186
+ - narration says "a child learning to ride a bicycle" but the image is unrelated;
187
+ - a financial explanation uses generic unrelated office imagery;
188
+ - the visual suggests a different meaning from the narration;
189
+ - an important concept is never visually represented;
190
+ - the generated story drifts away from the requested topic.
191
+
192
+ This is generally **advisory** rather than a rendering failure.
193
+
194
+ **Preferred resolution:** Improve the image/media prompt, scene decomposition, or narration. If the incorrect media is caused by a stale generated asset, invalidate the cache and regenerate it.
195
+
196
+ **Review question:**
197
+
198
+ > If a viewer watches the visuals while listening to the narration, do the two communicate the same intended idea?
199
+
200
+ Typical classification:
201
+
202
+ - `VISUAL-001`, `VISUAL-008`, or `VISUAL-009`
203
+ - source: `AUTHORING` or `CONTENT`
204
+
205
+ Do not fail a case merely because the image is metaphorical rather than literal, provided it supports the intended message.
206
+
207
+ ---
208
+
209
+ # General Review Dimensions
210
+
211
+ Score each applicable dimension from 1 to 5.
212
+
213
+ ## Storytelling / Communication
214
+
215
+ Check:
216
+
217
+ - Is there a clear purpose?
218
+ - Is the opening effective?
219
+ - Does each scene move the message forward?
220
+ - Is the information ordered logically?
221
+ - Is the main message understandable?
222
+ - Is there unnecessary repetition?
223
+ - Does the ending provide an appropriate conclusion or payoff?
224
+
225
+ ## Narration / Visual Alignment
226
+
227
+ Check whether visuals support the narration.
228
+
229
+ A visual does not need to literally illustrate every word. Conceptual visuals, examples, diagrams, and metaphors are acceptable when they improve understanding.
230
+
231
+ ## Visual Quality
232
+
233
+ Check:
234
+
235
+ - readability;
236
+ - cropping;
237
+ - overlap;
238
+ - empty space;
239
+ - visual clutter;
240
+ - media relevance;
241
+ - repetition;
242
+ - excessive effects;
243
+ - overall coherence.
244
+
245
+ ## Timing / Pacing
246
+
247
+ Check:
248
+
249
+ - narration vs visual duration;
250
+ - static visuals that last too long;
251
+ - visuals that disappear too early;
252
+ - important content shown too briefly;
253
+ - awkward cuts;
254
+ - excessive transitions;
255
+ - unnatural pacing.
256
+
257
+ ## Audio
258
+
259
+ Check:
260
+
261
+ - narration speed;
262
+ - pronunciation;
263
+ - unnatural pauses;
264
+ - missing/truncated narration;
265
+ - volume consistency;
266
+ - background music competing with speech.
267
+
268
+ ## Subtitles
269
+
270
+ Check:
271
+
272
+ - presence when expected;
273
+ - synchronization;
274
+ - stale content;
275
+ - readability;
276
+ - line breaks;
277
+ - density;
278
+ - visual overlap.
279
+
280
+ ## Platform Suitability
281
+
282
+ If the user specifies a platform, judge against it.
283
+
284
+ For short-form video, pay particular attention to the hook, pacing, mobile readability, and visual changes.
285
+
286
+ For tutorials, pay particular attention to step ordering, demonstrations, and sufficient time to understand each step.
287
+
288
+ Do not impose platform-specific requirements when the user did not specify a platform.
289
+
290
+ ## Markcut Authoring Quality
291
+
292
+ Evaluate whether the agent uses Markcut appropriately.
293
+
294
+ Look for:
295
+
296
+ - poor scene decomposition;
297
+ - poor duration choices;
298
+ - unnecessary complexity;
299
+ - unnecessary components/effects;
300
+ - poor media instructions;
301
+ - narration packed into one visual beat;
302
+ - missing visuals;
303
+ - non-portable media references.
304
+
305
+ Prefer the simplest Markcut structure that produces a strong result.
306
+
307
+ ---
308
+
309
+ # Issue Taxonomy
310
+
311
+ Use these IDs when applicable.
312
+
313
+ ### STORY
314
+
315
+ - `STORY-001` — weak-hook
316
+ - `STORY-002` — no-progression
317
+ - `STORY-003` — repetitive-content
318
+ - `STORY-004` — unclear-message
319
+ - `STORY-005` — weak-ending
320
+ - `STORY-006` — unnecessary-introduction
321
+ - `STORY-007` — poor-information-order
322
+
323
+ ### VISUAL
324
+
325
+ - `VISUAL-001` — narration-visual-mismatch
326
+ - `VISUAL-002` — missing-or-broken-visual
327
+ - `VISUAL-003` — repetitive-visual
328
+ - `VISUAL-004` — text-too-small
329
+ - `VISUAL-005` — text-overflow
330
+ - `VISUAL-006` — excessive-empty-space
331
+ - `VISUAL-007` — visual-clutter
332
+ - `VISUAL-008` — poor-media-selection
333
+ - `VISUAL-009` — insufficient-visual-explanation
334
+
335
+ ### TIMING
336
+
337
+ - `TIMING-001` — narration-too-long-for-scene
338
+ - `TIMING-002` — narration-too-short-for-scene
339
+ - `TIMING-003` — static-visual-too-long
340
+ - `TIMING-004` — visual-too-short
341
+ - `TIMING-005` — awkward-cut
342
+ - `TIMING-006` — transition-too-long
343
+ - `TIMING-007` — excessive-transitions
344
+ - `TIMING-008` — poor-pacing
345
+
346
+ ### AUDIO
347
+
348
+ - `AUDIO-001` — narration-too-fast
349
+ - `AUDIO-002` — narration-too-slow
350
+ - `AUDIO-003` — unnatural-narration
351
+ - `AUDIO-004` — awkward-pause
352
+ - `AUDIO-005` — music-too-loud
353
+ - `AUDIO-006` — narration-cut-off
354
+
355
+ ### SUBTITLE
356
+
357
+ - `SUBTITLE-001` — too-dense
358
+ - `SUBTITLE-002` — bad-line-break
359
+ - `SUBTITLE-003` — timing-mismatch
360
+ - `SUBTITLE-004` — missing-or-stale-subtitles
361
+ - `SUBTITLE-005` — visual-overlap
362
+
363
+ ### STRUCTURE
364
+
365
+ - `STRUCTURE-001` — missing-content
366
+ - `STRUCTURE-002` — unnecessary-scene
367
+ - `STRUCTURE-003` — wrong-order
368
+ - `STRUCTURE-004` — inappropriate-layout
369
+ - `STRUCTURE-005` — poor-scene-decomposition
370
+
371
+ ### AUTHORING
372
+
373
+ - `AUTHORING-001` — unnecessary-complexity
374
+ - `AUTHORING-002` — poor-duration-selection
375
+ - `AUTHORING-003` — poor-media-instruction
376
+ - `AUTHORING-004` — unnecessary-component
377
+ - `AUTHORING-005` — unnecessary-effect
378
+ - `AUTHORING-006` — non-portable-media-path
379
+ - `AUTHORING-007` — stale-generated-artifact
380
+
381
+ ### ENGINE
382
+
383
+ Use only when the Markdown contains a reasonable instruction and Markcut appears not to implement it correctly.
384
+
385
+ ---
386
+
387
+ # Severity
388
+
389
+ - `critical` — unusable or fails the core purpose
390
+ - `high` — substantially damages viewer experience
391
+ - `medium` — noticeable and worth fixing
392
+ - `low` — minor improvement or polish
393
+
394
+ ---
395
+
396
+ # Evidence Rules
397
+
398
+ Every issue must contain concrete evidence.
399
+
400
+ Reference, when possible:
401
+
402
+ - scene name;
403
+ - approximate timestamp;
404
+ - narration;
405
+ - visible content;
406
+ - duration;
407
+ - subtitle behavior;
408
+ - Markdown instruction.
409
+
410
+ Do not invent exact timestamps. Use approximate timestamps when necessary.
411
+
412
+ Do not report the same underlying problem multiple times. Report the root problem and mention related symptoms in the evidence.
413
+
414
+ ---
415
+
416
+ # Output Format
417
+
418
+ Return the following structure.
419
+
420
+ ## Overall
421
+
422
+ ```yaml
423
+ score: <1-5>
424
+ summary: <short assessment>
425
+ ```
426
+
427
+ ## Scores
428
+
429
+ ```yaml
430
+ storytelling: <1-5>
431
+ visual_quality: <1-5>
432
+ narration_visual_alignment: <1-5>
433
+ timing_pacing: <1-5>
434
+ audio: <1-5>
435
+ subtitles: <1-5>
436
+ platform_suitability: <1-5>
437
+ markcut_authoring: <1-5>
438
+ ```
439
+
440
+ Use the average of applicable dimensions for the overall score.
441
+
442
+ ## Issues
443
+
444
+ For each meaningful issue:
445
+
446
+ ```yaml
447
+ - id: VISUAL-001
448
+ severity: medium
449
+ source: AUTHORING
450
+ scene: "Solution"
451
+ timestamp: "approximately 00:18-00:25"
452
+ evidence: "..."
453
+ impact: "..."
454
+ recommendation: "..."
455
+ confidence: high
456
+ ```
457
+
458
+ If there are no meaningful issues:
459
+
460
+ ```yaml
461
+ issues: []
462
+ ```
463
+
464
+ ## Strengths
465
+
466
+ List the most important things the video does well.
467
+
468
+ ## Priority Improvements
469
+
470
+ List the three most valuable improvements, in priority order.
471
+
472
+ ---
473
+
474
+ # Final Principle
475
+
476
+ Answer this question above all others:
477
+
478
+ > **If a user installed the Markcut skill and asked an LLM to create this video, did the skill enable the LLM to make good decisions?**
479
+
480
+ Evaluate the actual viewer experience, use the Markdown to understand authoring intent, distinguish authoring mistakes from engine failures, and provide actionable evidence-based feedback.
@@ -137,6 +137,8 @@ export interface DescriptiveMapWaypoint {
137
137
  lng: number;
138
138
  label?: string;
139
139
  media?: string;
140
+ /** Travel mode for the leg leaving this waypoint (DRIVING|WALKING|BICYCLING|TRANSIT|FLIGHT|BOAT). */
141
+ mode?: string;
140
142
  }
141
143
 
142
144
  /** A tween expression `tween(from, to, easing?)` parsed into a tagged spec. */
@@ -188,6 +190,9 @@ export interface DescriptiveMap extends DescriptiveBaseNode {
188
190
  pitch?: DescriptiveTweenable;
189
191
  };
190
192
  };
193
+ /** Overlay children rendered on top of the map.
194
+ * A child with `at:"Label"` is positioned at that waypoint's screen pixel. */
195
+ children?: DescriptiveNode[];
191
196
  }
192
197
 
193
198
  export interface DescriptiveContainer extends DescriptiveBaseNode {
@@ -531,6 +536,7 @@ function wrapWithEffects(
531
536
  start: isOutermost && isBgNoEnd ? innerStream.start : effStart,
532
537
  end: isOutermost && isBgNoEnd ? undefined : effEnd,
533
538
  visible: innerStream.visible ?? true,
539
+ at: (node as any).at,
534
540
  ...pickOn(node),
535
541
  } as Effect;
536
542
  }
@@ -560,6 +566,7 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
560
566
  style: node.style,
561
567
  visible: node.visible ?? true,
562
568
  isBackground: node.isBackground,
569
+ at: (node as any).at,
563
570
  start: isBgNoOwnTiming ? (typeof node.start === "number" ? node.start : undefined) : start,
564
571
  end,
565
572
  startFrom: isBgNoOwnTiming ? undefined : (node.type === "video" || node.type === "audio" ? node.startFrom : undefined),
@@ -677,7 +684,7 @@ function compileChildren(
677
684
  } else if (isRhythm(child)) {
678
685
  result = compileRhythm(child, ctx, parentKind);
679
686
  } else if (isMap(child)) {
680
- result = compileLeaf(child, ctx, parentKind);
687
+ result = compileMap(child, ctx, parentKind);
681
688
  } else {
682
689
  result = compileLeaf(child, ctx, parentKind);
683
690
  }
@@ -893,6 +900,58 @@ function compileRhythm(
893
900
  return { stream, duration: end };
894
901
  }
895
902
 
903
+ function compileMap(
904
+ node: DescriptiveMap,
905
+ ctx: CompileContext,
906
+ parentKind: "series" | "parallel" | "transitionSeries",
907
+ ): CompileResult {
908
+ const id = node.id ?? uid();
909
+ const start = parentKind === "parallel" ? Math.max(0, node.start ?? 0) : 0;
910
+ const ownDuration = deriveLeafDuration(node as any, ctx);
911
+ const end = ownDuration != null ? start + ownDuration : undefined;
912
+
913
+ // Compile overlay children (they play in parallel on top of the map).
914
+ // Children with `at:"Label"` carry it through compilation on their base.
915
+ const children = node.children ?? [];
916
+ const compiledChildren = children.length
917
+ ? compileChildren(children, ctx, "parallel")
918
+ : [];
919
+
920
+ // Map duration covers its own span + any children that extend past it.
921
+ const maxChildEnd = compiledChildren.reduce((max, c) => Math.max(max, c.duration), 0);
922
+ const mapDuration = Math.max(end ?? 0, maxChildEnd, ownDuration ?? 0);
923
+
924
+ const stream: MapStream = {
925
+ id,
926
+ type: "map",
927
+ style: node.style,
928
+ visible: node.visible ?? true,
929
+ isBackground: node.isBackground,
930
+ start,
931
+ end: mapDuration,
932
+ durationInSeconds: mapDuration,
933
+ view: node.view ?? "route",
934
+ waypoints: node.waypoints,
935
+ routeColor: node.routeColor ?? "#4285F4",
936
+ routeWeight: node.routeWeight ?? 4,
937
+ zoom: node.zoom ?? 10,
938
+ center: node.center,
939
+ mapType: node.mapType ?? "roadmap",
940
+ language: node.language,
941
+ region: node.region,
942
+ travelMode: node.travelMode ?? "DRIVING",
943
+ routeMarker: node.routeMarker ?? "🚗",
944
+ camera: node.camera,
945
+ cinematic: node.cinematic,
946
+ streetView: node.streetView,
947
+ googleMapsApiKey: ctx.googleMapsApiKey,
948
+ children: compiledChildren.map((c) => c.stream),
949
+ ...pickOn(node),
950
+ };
951
+
952
+ return { stream, duration: mapDuration };
953
+ }
954
+
896
955
  function compileContainer(node: DescriptiveContainer, ctx: CompileContext, parentKind: "series" | "parallel" | "transitionSeries"): CompileResult {
897
956
  const id = node.id ?? uid();
898
957
  ensureUniqueIds(node.children, id);
@@ -162,7 +162,17 @@ export function parseNumberMaybe(v: string): number | string | boolean {
162
162
  * Parse a map `waypoints:[...]` value into an array of waypoints.
163
163
  *
164
164
  * Format: `[lat,lng,"Label"; lat,lng,"Label"]` — semicolon-separated entries,
165
- * comma-separated fields. Returns `[]` on any structural mismatch.
165
+ * comma-separated fields. After lat/lng, the optional fields are parsed
166
+ * smartly:
167
+ * - a BARE (unquoted) token that names a travel mode (DRIVING, WALKING,
168
+ * BICYCLING, TRANSIT, FLIGHT, BOAT) is the outgoing-leg mode, wherever it
169
+ * appears — so `[lat,lng,"Label",FLIGHT]` or `[lat,lng,FLIGHT]` work with
170
+ * no empty media slot;
171
+ * - quoted-empty `""` slots are skipped (backward compat for
172
+ * `[lat,lng,"Label","",MODE]`);
173
+ * - remaining values map positionally: 1st = label, 2nd = media.
174
+ * Quoted tokens are always literal (a quoted "FLIGHT" is a label/media, not a
175
+ * mode). Returns `[]` on any structural mismatch.
166
176
  */
167
177
  export function parseWaypoints(raw: string): DescriptiveMapWaypoint[] {
168
178
  const s = raw.trim();
@@ -173,15 +183,25 @@ export function parseWaypoints(raw: string): DescriptiveMapWaypoint[] {
173
183
  const bits = splitTokens(part.replace(/,/g, " "));
174
184
  const lat = Number(bits[0] ?? 0);
175
185
  const lng = Number(bits[1] ?? 0);
176
- const labelRaw = bits[2];
177
- const labelRawUq = labelRaw ? unquote(labelRaw) : undefined;
178
- const label = labelRawUq ? labelRawUq : undefined;
179
- const mediaRaw = bits[3];
180
- const media = mediaRaw ? unquote(mediaRaw) : undefined;
181
- return { lat, lng, label, media };
186
+ let mode: string | undefined;
187
+ const values: string[] = [];
188
+ for (const tok of bits.slice(2)) {
189
+ const value = unquote(tok);
190
+ if (!isQuoted(tok) && value && KNOWN_TRAVEL_MODES.has(value.toUpperCase())) {
191
+ mode = value.toUpperCase();
192
+ continue;
193
+ }
194
+ values.push(value); // keep position — a quoted-empty "" means "no value here"
195
+ }
196
+ const label = values[0] || undefined;
197
+ const media = values[1] || undefined;
198
+ return { lat, lng, label, media, mode };
182
199
  });
183
200
  }
184
201
 
202
+ /** Bare (unquoted) waypoint tokens recognized as a travel mode. */
203
+ const KNOWN_TRAVEL_MODES = new Set(["DRIVING", "WALKING", "BICYCLING", "TRANSIT", "FLIGHT", "BOAT"]);
204
+
185
205
  /**
186
206
  * Rewrite `tween(from, to, easing?)` expressions inside a JSON-ish string into
187
207
  * a tagged literal `{"__tween":[from,to,"easing"]}` so the regular JSON parser
@@ -588,7 +588,8 @@ function processMDASTListItem(item: any, parent: ParentNode, lines: string[]): v
588
588
  node.type === "transitionSeries" ||
589
589
  node.type === "effect" ||
590
590
  node.type === "include" ||
591
- node.type === "rhythm"
591
+ node.type === "rhythm" ||
592
+ node.type === "map"
592
593
  ) {
593
594
  for (const subItem of child.children) {
594
595
  processMDASTListItem(subItem, node as ParentNode, lines);