@lalalic/markcut 3.0.0 → 3.1.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/package.json +1 -1
- package/skills/markcut/SKILL.md +7 -0
- package/skills/markcut/docs/map-dynamic-camera.md +328 -0
- package/skills/markcut/docs/markdown-descriptive.md +2 -0
- package/src/descriptive/compiler.ts +101 -1
- package/src/descriptive/dsl.ts +64 -7
- package/src/descriptive/markdown.ts +7 -1
- package/src/descriptive/resolve.test.ts +103 -5
- package/src/descriptive/resolve.ts +207 -24
- package/src/player/bundle/player.js +223296 -222155
- package/src/player/pipeline.mjs +314 -25
- 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 +60 -1
- package/src/spots/cli.mjs +266 -0
- package/src/types/Effect.tsx +12 -1
- package/src/types/Map.tsx +1078 -130
- package/src/utils/directions.ts +101 -0
- package/src/utils/index.ts +11 -0
- package/src/utils/route-legs.ts +199 -0
- package/src/utils/tween.ts +49 -1
- package/tests/dsl.test.ts +78 -0
- package/tests/fixtures/map-dynamic.json +52 -0
- package/tests/fixtures/map-overlay.json +56 -0
- package/tests/fixtures/md/animate-diagrams.md +9 -7
- package/tests/fixtures/md/map-all-views.md +35 -0
- 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 +133 -0
- package/tests/render.test.ts +93 -0
- package/tests/route-legs.test.ts +178 -0
- package/tests/schema.test.ts +76 -1
- package/tests/validate-assets.test.ts +106 -0
- package/B] +0 -2
- package/tests/tmp/vision-1785081637127-video/videos/.normalized/segments/test-clip_0to3_seg_1100to3000.mp4 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/.normalized/test-clip_0to3.mp4 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/.normalized/test-clip_audio.mp3 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/metadata.json +0 -9
- package/tests/tmp/vision-1785081637127-video/videos/test-clip.mp4 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/test-clip.vtt +0 -5
package/src/player/pipeline.mjs
CHANGED
|
@@ -243,6 +243,7 @@ function wrapWithEffects(node2, result, parentKind) {
|
|
|
243
243
|
start: isOutermost && isBgNoEnd ? innerStream.start : effStart,
|
|
244
244
|
end: isOutermost && isBgNoEnd ? void 0 : effEnd,
|
|
245
245
|
visible: innerStream.visible ?? true,
|
|
246
|
+
at: node2.at,
|
|
246
247
|
...pickOn(node2)
|
|
247
248
|
};
|
|
248
249
|
}
|
|
@@ -261,6 +262,7 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
261
262
|
style: node2.style,
|
|
262
263
|
visible: node2.visible ?? true,
|
|
263
264
|
isBackground: node2.isBackground,
|
|
265
|
+
at: node2.at,
|
|
264
266
|
start: isBgNoOwnTiming ? typeof node2.start === "number" ? node2.start : void 0 : start,
|
|
265
267
|
end,
|
|
266
268
|
startFrom: isBgNoOwnTiming ? void 0 : node2.type === "video" || node2.type === "audio" ? node2.startFrom : void 0,
|
|
@@ -343,6 +345,7 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
343
345
|
const stream = {
|
|
344
346
|
...base,
|
|
345
347
|
type: "map",
|
|
348
|
+
view: node2.view ?? "route",
|
|
346
349
|
waypoints: node2.waypoints,
|
|
347
350
|
routeColor: node2.routeColor ?? "#4285F4",
|
|
348
351
|
routeWeight: node2.routeWeight ?? 4,
|
|
@@ -353,6 +356,9 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
353
356
|
region: node2.region,
|
|
354
357
|
travelMode: node2.travelMode ?? "DRIVING",
|
|
355
358
|
routeMarker: node2.routeMarker ?? "\u{1F697}",
|
|
359
|
+
camera: node2.camera,
|
|
360
|
+
cinematic: node2.cinematic,
|
|
361
|
+
streetView: node2.streetView,
|
|
356
362
|
googleMapsApiKey: ctx.googleMapsApiKey
|
|
357
363
|
};
|
|
358
364
|
return { stream, duration: end ?? 0 };
|
|
@@ -371,7 +377,7 @@ function compileChildren(children, ctx, parentKind) {
|
|
|
371
377
|
} else if (isRhythm(child)) {
|
|
372
378
|
result = compileRhythm(child, ctx, parentKind);
|
|
373
379
|
} else if (isMap(child)) {
|
|
374
|
-
result =
|
|
380
|
+
result = compileMap(child, ctx, parentKind);
|
|
375
381
|
} else {
|
|
376
382
|
result = compileLeaf(child, ctx, parentKind);
|
|
377
383
|
}
|
|
@@ -522,6 +528,44 @@ function compileRhythm(node2, ctx, parentKind) {
|
|
|
522
528
|
};
|
|
523
529
|
return { stream, duration: end };
|
|
524
530
|
}
|
|
531
|
+
function compileMap(node2, ctx, parentKind) {
|
|
532
|
+
const id = node2.id ?? uid();
|
|
533
|
+
const start = parentKind === "parallel" ? Math.max(0, node2.start ?? 0) : 0;
|
|
534
|
+
const ownDuration = deriveLeafDuration(node2, ctx);
|
|
535
|
+
const end = ownDuration != null ? start + ownDuration : void 0;
|
|
536
|
+
const children = node2.children ?? [];
|
|
537
|
+
const compiledChildren = children.length ? compileChildren(children, ctx, "parallel") : [];
|
|
538
|
+
const maxChildEnd = compiledChildren.reduce((max, c) => Math.max(max, c.duration), 0);
|
|
539
|
+
const mapDuration = Math.max(end ?? 0, maxChildEnd, ownDuration ?? 0);
|
|
540
|
+
const stream = {
|
|
541
|
+
id,
|
|
542
|
+
type: "map",
|
|
543
|
+
style: node2.style,
|
|
544
|
+
visible: node2.visible ?? true,
|
|
545
|
+
isBackground: node2.isBackground,
|
|
546
|
+
start,
|
|
547
|
+
end: mapDuration,
|
|
548
|
+
durationInSeconds: mapDuration,
|
|
549
|
+
view: node2.view ?? "route",
|
|
550
|
+
waypoints: node2.waypoints,
|
|
551
|
+
routeColor: node2.routeColor ?? "#4285F4",
|
|
552
|
+
routeWeight: node2.routeWeight ?? 4,
|
|
553
|
+
zoom: node2.zoom ?? 10,
|
|
554
|
+
center: node2.center,
|
|
555
|
+
mapType: node2.mapType ?? "roadmap",
|
|
556
|
+
language: node2.language,
|
|
557
|
+
region: node2.region,
|
|
558
|
+
travelMode: node2.travelMode ?? "DRIVING",
|
|
559
|
+
routeMarker: node2.routeMarker ?? "\u{1F697}",
|
|
560
|
+
camera: node2.camera,
|
|
561
|
+
cinematic: node2.cinematic,
|
|
562
|
+
streetView: node2.streetView,
|
|
563
|
+
googleMapsApiKey: ctx.googleMapsApiKey,
|
|
564
|
+
children: compiledChildren.map((c) => c.stream),
|
|
565
|
+
...pickOn(node2)
|
|
566
|
+
};
|
|
567
|
+
return { stream, duration: mapDuration };
|
|
568
|
+
}
|
|
525
569
|
function compileContainer(node2, ctx, parentKind) {
|
|
526
570
|
const id = node2.id ?? uid();
|
|
527
571
|
ensureUniqueIds(node2.children, id);
|
|
@@ -1034,6 +1078,114 @@ var require_format = __commonJS({
|
|
|
1034
1078
|
}
|
|
1035
1079
|
});
|
|
1036
1080
|
|
|
1081
|
+
// src/utils/route-legs.ts
|
|
1082
|
+
function haversineKm(a, b) {
|
|
1083
|
+
const dLat = toRad(b.lat - a.lat);
|
|
1084
|
+
const dLng = toRad(b.lng - a.lng);
|
|
1085
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(a.lat)) * Math.cos(toRad(b.lat)) * Math.sin(dLng / 2) ** 2;
|
|
1086
|
+
return 2 * EARTH_RADIUS_KM * Math.asin(Math.sqrt(h));
|
|
1087
|
+
}
|
|
1088
|
+
function greatCirclePath(a, b, points = 64) {
|
|
1089
|
+
const \u03C61 = toRad(a.lat);
|
|
1090
|
+
const \u03BB1 = toRad(a.lng);
|
|
1091
|
+
const \u03C62 = toRad(b.lat);
|
|
1092
|
+
const \u03BB2 = toRad(b.lng);
|
|
1093
|
+
const d = haversineKm(a, b) / EARTH_RADIUS_KM;
|
|
1094
|
+
const out = [];
|
|
1095
|
+
for (let i = 0; i <= points; i++) {
|
|
1096
|
+
if (d === 0) {
|
|
1097
|
+
out.push({ lat: a.lat, lng: a.lng });
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
const f = i / points;
|
|
1101
|
+
const A = Math.sin((1 - f) * d) / Math.sin(d);
|
|
1102
|
+
const B = Math.sin(f * d) / Math.sin(d);
|
|
1103
|
+
const x = A * Math.cos(\u03C61) * Math.cos(\u03BB1) + B * Math.cos(\u03C62) * Math.cos(\u03BB2);
|
|
1104
|
+
const y = A * Math.cos(\u03C61) * Math.sin(\u03BB1) + B * Math.cos(\u03C62) * Math.sin(\u03BB2);
|
|
1105
|
+
const z = A * Math.sin(\u03C61) + B * Math.sin(\u03C62);
|
|
1106
|
+
out.push({ lat: toDeg(Math.atan2(z, Math.sqrt(x * x + y * y))), lng: toDeg(Math.atan2(y, x)) });
|
|
1107
|
+
}
|
|
1108
|
+
return out;
|
|
1109
|
+
}
|
|
1110
|
+
function makeSyntheticLeg(from, to, mode) {
|
|
1111
|
+
const speedKmh = (mode || "").toUpperCase() === "BOAT" ? BOAT_SPEED_KMH : FLIGHT_SPEED_KMH;
|
|
1112
|
+
const durationSec = haversineKm(from, to) / speedKmh * 3600;
|
|
1113
|
+
return {
|
|
1114
|
+
mode: mode.toUpperCase(),
|
|
1115
|
+
from,
|
|
1116
|
+
to,
|
|
1117
|
+
durationSec,
|
|
1118
|
+
steps: [{ path: greatCirclePath(from, to), durationSec }]
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
var FLIGHT_SPEED_KMH, BOAT_SPEED_KMH, EARTH_RADIUS_KM, toRad, toDeg;
|
|
1122
|
+
var init_route_legs = __esm({
|
|
1123
|
+
"src/utils/route-legs.ts"() {
|
|
1124
|
+
"use strict";
|
|
1125
|
+
FLIGHT_SPEED_KMH = 850;
|
|
1126
|
+
BOAT_SPEED_KMH = 40;
|
|
1127
|
+
EARTH_RADIUS_KM = 6371;
|
|
1128
|
+
toRad = (d) => d * Math.PI / 180;
|
|
1129
|
+
toDeg = (d) => d * 180 / Math.PI;
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
// src/utils/directions.ts
|
|
1134
|
+
var directions_exports = {};
|
|
1135
|
+
__export(directions_exports, {
|
|
1136
|
+
legDurationSec: () => legDurationSec,
|
|
1137
|
+
routeLegTimings: () => routeLegTimings
|
|
1138
|
+
});
|
|
1139
|
+
function apiKey() {
|
|
1140
|
+
return typeof process !== "undefined" && process.env.GOOGLE_MAPS_API_KEY || "";
|
|
1141
|
+
}
|
|
1142
|
+
async function legDurationSec(from, to, mode, key = apiKey()) {
|
|
1143
|
+
const m = (mode || "").toUpperCase();
|
|
1144
|
+
if (!ROAD_MODES.has(m)) {
|
|
1145
|
+
return makeSyntheticLeg(from, to, m).durationSec;
|
|
1146
|
+
}
|
|
1147
|
+
if (!key) {
|
|
1148
|
+
return straightLineSec(from, to);
|
|
1149
|
+
}
|
|
1150
|
+
const url = new URL("https://maps.googleapis.com/maps/api/directions/json");
|
|
1151
|
+
url.searchParams.set("origin", `${from.lat},${from.lng}`);
|
|
1152
|
+
url.searchParams.set("destination", `${to.lat},${to.lng}`);
|
|
1153
|
+
url.searchParams.set("mode", m.toLowerCase());
|
|
1154
|
+
url.searchParams.set("key", key);
|
|
1155
|
+
try {
|
|
1156
|
+
const res = await fetch(url);
|
|
1157
|
+
const data = await res.json();
|
|
1158
|
+
const dur = data?.routes?.[0]?.legs?.[0]?.duration?.value;
|
|
1159
|
+
if (typeof dur === "number" && dur > 0) return dur;
|
|
1160
|
+
return straightLineSec(from, to);
|
|
1161
|
+
} catch {
|
|
1162
|
+
return straightLineSec(from, to);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
function straightLineSec(from, to) {
|
|
1166
|
+
return haversineKm(from, to) / 50 * 3600;
|
|
1167
|
+
}
|
|
1168
|
+
async function routeLegTimings(waypoints, defaultMode = "DRIVING", key = apiKey()) {
|
|
1169
|
+
if (waypoints.length < 2) return [];
|
|
1170
|
+
const out = [];
|
|
1171
|
+
for (let i = 0; i < waypoints.length - 1; i++) {
|
|
1172
|
+
const from = waypoints[i];
|
|
1173
|
+
const to = waypoints[i + 1];
|
|
1174
|
+
const mode = (from.mode ?? defaultMode).toUpperCase();
|
|
1175
|
+
const durationSec = await legDurationSec(from, to, mode, key) ?? 0;
|
|
1176
|
+
out.push({ mode, from, to, durationSec });
|
|
1177
|
+
}
|
|
1178
|
+
return out;
|
|
1179
|
+
}
|
|
1180
|
+
var ROAD_MODES;
|
|
1181
|
+
var init_directions = __esm({
|
|
1182
|
+
"src/utils/directions.ts"() {
|
|
1183
|
+
"use strict";
|
|
1184
|
+
init_route_legs();
|
|
1185
|
+
ROAD_MODES = /* @__PURE__ */ new Set(["DRIVING", "WALKING", "BICYCLING", "TRANSIT"]);
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1037
1189
|
// src/player/pipeline.ts
|
|
1038
1190
|
init_compiler();
|
|
1039
1191
|
|
|
@@ -9952,19 +10104,50 @@ function parseWaypoints(raw) {
|
|
|
9952
10104
|
const bits = splitTokens(part.replace(/,/g, " "));
|
|
9953
10105
|
const lat = Number(bits[0] ?? 0);
|
|
9954
10106
|
const lng = Number(bits[1] ?? 0);
|
|
9955
|
-
|
|
9956
|
-
const
|
|
9957
|
-
|
|
10107
|
+
let mode;
|
|
10108
|
+
const values = [];
|
|
10109
|
+
for (const tok of bits.slice(2)) {
|
|
10110
|
+
const value2 = unquote(tok);
|
|
10111
|
+
if (!isQuoted(tok) && value2 && KNOWN_TRAVEL_MODES.has(value2.toUpperCase())) {
|
|
10112
|
+
mode = value2.toUpperCase();
|
|
10113
|
+
continue;
|
|
10114
|
+
}
|
|
10115
|
+
values.push(value2);
|
|
10116
|
+
}
|
|
10117
|
+
const label = values[0] || void 0;
|
|
10118
|
+
const media = values[1] || void 0;
|
|
10119
|
+
return { lat, lng, label, media, mode };
|
|
9958
10120
|
});
|
|
9959
10121
|
}
|
|
10122
|
+
var KNOWN_TRAVEL_MODES = /* @__PURE__ */ new Set(["DRIVING", "WALKING", "BICYCLING", "TRANSIT", "FLIGHT", "BOAT"]);
|
|
10123
|
+
function rewriteTweenExprs(s) {
|
|
10124
|
+
return s.replace(
|
|
10125
|
+
/tween\(\s*([^,()]+?)\s*,\s*([^,()]+?)\s*(?:,\s*([^()]+?))?\s*\)/g,
|
|
10126
|
+
(_match, fromRaw, toRaw, easingRaw) => {
|
|
10127
|
+
const scalar = (v) => {
|
|
10128
|
+
const t = v.trim();
|
|
10129
|
+
if (/^[+-]?(\d+(\.\d+)?|\.\d+)$/.test(t)) return t;
|
|
10130
|
+
if (/^"(?:[^"\\]|\\.)*"$/.test(t)) return t;
|
|
10131
|
+
if (t === "true" || t === "false" || t === "null") return t;
|
|
10132
|
+
return JSON.stringify(t);
|
|
10133
|
+
};
|
|
10134
|
+
const items = [scalar(fromRaw), scalar(toRaw)];
|
|
10135
|
+
if (easingRaw !== void 0 && easingRaw.trim()) {
|
|
10136
|
+
items.push(scalar(easingRaw));
|
|
10137
|
+
}
|
|
10138
|
+
return `{"__tween":[${items.join(",")}]}`;
|
|
10139
|
+
}
|
|
10140
|
+
);
|
|
10141
|
+
}
|
|
9960
10142
|
function parseProps(raw) {
|
|
9961
10143
|
const s = raw.trim();
|
|
9962
10144
|
if (!s.startsWith("{") && !s.startsWith("[")) return {};
|
|
9963
10145
|
if (!s.endsWith("}") && !s.endsWith("]")) return {};
|
|
10146
|
+
const withTweens = rewriteTweenExprs(s);
|
|
9964
10147
|
try {
|
|
9965
|
-
return JSON.parse(
|
|
10148
|
+
return JSON.parse(withTweens);
|
|
9966
10149
|
} catch {
|
|
9967
|
-
let normalized =
|
|
10150
|
+
let normalized = withTweens.replace(
|
|
9968
10151
|
/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:(?=\s*["{[]?)/g,
|
|
9969
10152
|
'$1"$2":'
|
|
9970
10153
|
);
|
|
@@ -9980,7 +10163,7 @@ function parseProps(raw) {
|
|
|
9980
10163
|
return JSON.parse(normalized);
|
|
9981
10164
|
} catch {
|
|
9982
10165
|
try {
|
|
9983
|
-
const result = (0, eval)("(" +
|
|
10166
|
+
const result = (0, eval)("(" + withTweens + ")");
|
|
9984
10167
|
return typeof result === "object" && result !== null ? result : {};
|
|
9985
10168
|
} catch {
|
|
9986
10169
|
return {};
|
|
@@ -10233,6 +10416,10 @@ function preserveVariantAttrs(node2, attrs) {
|
|
|
10233
10416
|
"mapType",
|
|
10234
10417
|
"data",
|
|
10235
10418
|
"prompt",
|
|
10419
|
+
"view",
|
|
10420
|
+
"camera",
|
|
10421
|
+
"cinematic",
|
|
10422
|
+
"streetView",
|
|
10236
10423
|
"name",
|
|
10237
10424
|
"title",
|
|
10238
10425
|
"transition",
|
|
@@ -10455,6 +10642,7 @@ function parseNodeLine(content3, lineNum) {
|
|
|
10455
10642
|
waypoints: attrs.waypoints ?? [],
|
|
10456
10643
|
duration: attrs.duration,
|
|
10457
10644
|
start: attrs.start,
|
|
10645
|
+
view: attrs.view,
|
|
10458
10646
|
routeMarker: attrs.routeMarker,
|
|
10459
10647
|
travelMode: attrs.travelMode,
|
|
10460
10648
|
routeColor: attrs.routeColor,
|
|
@@ -10462,6 +10650,9 @@ function parseNodeLine(content3, lineNum) {
|
|
|
10462
10650
|
zoom: attrs.zoom,
|
|
10463
10651
|
center: attrs.center,
|
|
10464
10652
|
mapType: attrs.mapType,
|
|
10653
|
+
camera: attrs.camera,
|
|
10654
|
+
cinematic: attrs.cinematic,
|
|
10655
|
+
streetView: attrs.streetView,
|
|
10465
10656
|
language: attrs.language ?? attrs.lang,
|
|
10466
10657
|
region: attrs.region,
|
|
10467
10658
|
instruction: attrs.instruction,
|
|
@@ -10630,7 +10821,7 @@ function processMDASTListItem(item, parent, lines) {
|
|
|
10630
10821
|
Object.assign(node2, attrs);
|
|
10631
10822
|
}
|
|
10632
10823
|
} else if (child.type === "list") {
|
|
10633
|
-
if (node2.type === "series" || node2.type === "parallel" || node2.type === "transitionSeries" || node2.type === "effect" || node2.type === "include" || node2.type === "rhythm") {
|
|
10824
|
+
if (node2.type === "series" || node2.type === "parallel" || node2.type === "transitionSeries" || node2.type === "effect" || node2.type === "include" || node2.type === "rhythm" || node2.type === "map") {
|
|
10634
10825
|
for (const subItem of child.children) {
|
|
10635
10826
|
processMDASTListItem(subItem, node2, lines);
|
|
10636
10827
|
}
|
|
@@ -10764,7 +10955,22 @@ function probeDuration(src, baseDir) {
|
|
|
10764
10955
|
}
|
|
10765
10956
|
function resolveSrc(src, baseDir) {
|
|
10766
10957
|
if (/^(https?:|file:|\/)/.test(src)) return src;
|
|
10767
|
-
return resolvePath(baseDir
|
|
10958
|
+
return resolvePath(baseDir, src);
|
|
10959
|
+
}
|
|
10960
|
+
function relativizeAssetsUnder(node2, baseDir) {
|
|
10961
|
+
if (!node2 || typeof node2 !== "object") return node2;
|
|
10962
|
+
if (Array.isArray(node2)) return node2.map((v) => relativizeAssetsUnder(v, baseDir));
|
|
10963
|
+
const out = {};
|
|
10964
|
+
for (const [k, v] of Object.entries(node2)) {
|
|
10965
|
+
if (typeof v === "string" && v.startsWith(baseDir + "/")) {
|
|
10966
|
+
out[k] = v.slice(baseDir.length + 1);
|
|
10967
|
+
} else if (v && typeof v === "object") {
|
|
10968
|
+
out[k] = relativizeAssetsUnder(v, baseDir);
|
|
10969
|
+
} else {
|
|
10970
|
+
out[k] = v;
|
|
10971
|
+
}
|
|
10972
|
+
}
|
|
10973
|
+
return out;
|
|
10768
10974
|
}
|
|
10769
10975
|
var COMMON_RESOLUTIONS = [
|
|
10770
10976
|
{ width: 1920, height: 1080 },
|
|
@@ -10809,7 +11015,7 @@ function resolveMediaSrc(src, targetWidth, targetHeight, baseDir) {
|
|
|
10809
11015
|
console.warn(` \u26A0 No matching media file for "${src}" at ${targetWidth}x${targetHeight}`);
|
|
10810
11016
|
return exactAbs;
|
|
10811
11017
|
}
|
|
10812
|
-
async function resolveMediaSrcs(root, options
|
|
11018
|
+
async function resolveMediaSrcs(root, options) {
|
|
10813
11019
|
const clone = JSON.parse(JSON.stringify(root));
|
|
10814
11020
|
const baseDir = options.baseDir;
|
|
10815
11021
|
const targetWidth = clone.width ?? 1080;
|
|
@@ -10830,7 +11036,7 @@ async function resolveMediaSrcs(root, options = {}) {
|
|
|
10830
11036
|
});
|
|
10831
11037
|
return clone;
|
|
10832
11038
|
}
|
|
10833
|
-
async function resolveMediaDurations(root, options
|
|
11039
|
+
async function resolveMediaDurations(root, options) {
|
|
10834
11040
|
const clone = JSON.parse(JSON.stringify(root));
|
|
10835
11041
|
const baseDir = options.baseDir;
|
|
10836
11042
|
walkDown(clone, (node2) => {
|
|
@@ -10910,13 +11116,15 @@ async function resolveScripts(root, options) {
|
|
|
10910
11116
|
const clone = JSON.parse(JSON.stringify(root));
|
|
10911
11117
|
mkdirSync2(options.outputDir, { recursive: true });
|
|
10912
11118
|
const allScriptNodes = [];
|
|
10913
|
-
|
|
10914
|
-
|
|
10915
|
-
if (
|
|
10916
|
-
|
|
10917
|
-
|
|
10918
|
-
|
|
10919
|
-
|
|
11119
|
+
const collect = (node2, inherited) => {
|
|
11120
|
+
const ttsOverride = typeof node2.tts === "string" && node2.tts.length > 0 ? node2.tts : inherited;
|
|
11121
|
+
if (node2.type === "audio" && node2.script && typeof node2.script === "string" && !node2.src) {
|
|
11122
|
+
const id = node2.id ?? `audio-${allScriptNodes.length}`;
|
|
11123
|
+
allScriptNodes.push({ node: node2, id, ttsOverride });
|
|
11124
|
+
}
|
|
11125
|
+
for (const c of node2.children ?? []) collect(c, ttsOverride);
|
|
11126
|
+
};
|
|
11127
|
+
collect(clone);
|
|
10920
11128
|
const totalScripts = allScriptNodes.length;
|
|
10921
11129
|
let scriptsDone = 0;
|
|
10922
11130
|
let cacheHits = 0;
|
|
@@ -10924,9 +11132,9 @@ async function resolveScripts(root, options) {
|
|
|
10924
11132
|
if (totalScripts > 0) {
|
|
10925
11133
|
console.log(` \u{1F50A} TTS: generating ${totalScripts} script${totalScripts > 1 ? "s" : ""}...`);
|
|
10926
11134
|
}
|
|
10927
|
-
for (const { node: node2, id } of allScriptNodes) {
|
|
11135
|
+
for (const { node: node2, id, ttsOverride } of allScriptNodes) {
|
|
10928
11136
|
scriptsDone++;
|
|
10929
|
-
let ttsCli =
|
|
11137
|
+
let ttsCli = ttsOverride ?? options.ttsCli ?? DEFAULT_TTS_CLI;
|
|
10930
11138
|
if (node2.speaker && clone.voices) {
|
|
10931
11139
|
const speakerVoice = clone.voices[node2.speaker];
|
|
10932
11140
|
if (speakerVoice) {
|
|
@@ -11242,9 +11450,9 @@ function applyStoryboardOverrides(root, options) {
|
|
|
11242
11450
|
}
|
|
11243
11451
|
return clone;
|
|
11244
11452
|
}
|
|
11245
|
-
async function resolveIncludes(root, options
|
|
11453
|
+
async function resolveIncludes(root, options) {
|
|
11246
11454
|
const clone = JSON.parse(JSON.stringify(root));
|
|
11247
|
-
const baseDir = options.baseDir
|
|
11455
|
+
const baseDir = options.baseDir;
|
|
11248
11456
|
const outputDir = options.includeOutputDir ?? join(baseDir, ".markcut", "generated", "includes");
|
|
11249
11457
|
mkdirSync2(outputDir, { recursive: true });
|
|
11250
11458
|
function extractImportEntriesFromRaw(raw) {
|
|
@@ -11323,7 +11531,7 @@ async function resolveIncludes(root, options = {}) {
|
|
|
11323
11531
|
}
|
|
11324
11532
|
return clone;
|
|
11325
11533
|
}
|
|
11326
|
-
async function resolveAll2(root, options
|
|
11534
|
+
async function resolveAll2(root, options) {
|
|
11327
11535
|
let result = root;
|
|
11328
11536
|
if (result.seed == null && options.seed == null && options.sourcePath) {
|
|
11329
11537
|
const autoSeed = parseInt(computeCacheKey(result).slice(0, 8), 16);
|
|
@@ -11391,8 +11599,89 @@ async function resolveAll2(root, options = {}) {
|
|
|
11391
11599
|
mergedOutputDir: options.subtitleOutputDir
|
|
11392
11600
|
});
|
|
11393
11601
|
}
|
|
11602
|
+
result = await resolveRouteStops(result);
|
|
11603
|
+
result = relativizeAssetsUnder(result, options.baseDir);
|
|
11394
11604
|
return result;
|
|
11395
11605
|
}
|
|
11606
|
+
var DEFAULT_CHILD_SECONDS = 3;
|
|
11607
|
+
var DEFAULT_MAP_DRIVE_SECONDS = 10;
|
|
11608
|
+
async function resolveRouteStops(root) {
|
|
11609
|
+
const { routeLegTimings: routeLegTimings2 } = await Promise.resolve().then(() => (init_directions(), directions_exports));
|
|
11610
|
+
async function resolveMap(node2) {
|
|
11611
|
+
if (!node2 || typeof node2 !== "object") return;
|
|
11612
|
+
if (Array.isArray(node2)) {
|
|
11613
|
+
for (const n of node2) await resolveMap(n);
|
|
11614
|
+
return;
|
|
11615
|
+
}
|
|
11616
|
+
if (node2.type === "map" && Array.isArray(node2.children) && node2.children.length) {
|
|
11617
|
+
const children = node2.children;
|
|
11618
|
+
const anchored = children.filter((c) => c && c.at);
|
|
11619
|
+
if (anchored.length) {
|
|
11620
|
+
await timeMapChildren(node2, anchored, routeLegTimings2);
|
|
11621
|
+
}
|
|
11622
|
+
}
|
|
11623
|
+
for (const child of node2.children ?? []) await resolveMap(child);
|
|
11624
|
+
}
|
|
11625
|
+
await resolveMap(root.children);
|
|
11626
|
+
return root;
|
|
11627
|
+
}
|
|
11628
|
+
async function timeMapChildren(map, anchored, routeLegTimings2) {
|
|
11629
|
+
const waypoints = map.waypoints ?? [];
|
|
11630
|
+
if (waypoints.length < 2) return;
|
|
11631
|
+
const legs = await routeLegTimings2(waypoints, map.travelMode ?? "DRIVING");
|
|
11632
|
+
const totalDrive = legs.reduce((s, l) => s + (l.durationSec || 0), 0);
|
|
11633
|
+
if (totalDrive <= 0) return;
|
|
11634
|
+
const childDuration = (c) => typeof c.duration === "number" && c.duration > 0 ? c.duration : DEFAULT_CHILD_SECONDS;
|
|
11635
|
+
const dwellAt = /* @__PURE__ */ new Map();
|
|
11636
|
+
for (const c of anchored) {
|
|
11637
|
+
const d = childDuration(c);
|
|
11638
|
+
dwellAt.set(c.at, Math.max(dwellAt.get(c.at) ?? 0, d));
|
|
11639
|
+
}
|
|
11640
|
+
const totalDwell = [...dwellAt.values()].reduce((s, d) => s + d, 0);
|
|
11641
|
+
const hasOwnDur = typeof map.duration === "number" || typeof map.endAt === "number";
|
|
11642
|
+
const totalDwellBudget = Math.max(0, map.duration ?? map.endAt ?? DEFAULT_MAP_DRIVE_SECONDS);
|
|
11643
|
+
const mapDur = hasOwnDur ? map.duration ?? map.endAt ?? totalDwellBudget + totalDwell : totalDwellBudget + totalDwell;
|
|
11644
|
+
const driveBudget = Math.max(0.1, mapDur - totalDwell);
|
|
11645
|
+
const scale = driveBudget / totalDrive;
|
|
11646
|
+
const arrivalAt = /* @__PURE__ */ new Map();
|
|
11647
|
+
const wp0 = waypoints[0];
|
|
11648
|
+
arrivalAt.set(wp0.label ?? String(wp0.lat) + "," + String(wp0.lng), 0);
|
|
11649
|
+
let cumDrive = 0;
|
|
11650
|
+
let cumDwells = 0;
|
|
11651
|
+
for (let i = 0; i < legs.length; i++) {
|
|
11652
|
+
cumDrive += legs[i].durationSec || 0;
|
|
11653
|
+
const wp = waypoints[i + 1];
|
|
11654
|
+
const arrival = cumDrive * scale + cumDwells;
|
|
11655
|
+
arrivalAt.set(wp.label ?? String(wp.lat) + "," + String(wp.lng), arrival);
|
|
11656
|
+
if (dwellAt.has(wp.label)) cumDwells += dwellAt.get(wp.label);
|
|
11657
|
+
}
|
|
11658
|
+
const schedule = [];
|
|
11659
|
+
for (const c of anchored) {
|
|
11660
|
+
const arrival = arrivalAt.get(c.at);
|
|
11661
|
+
if (arrival == null) {
|
|
11662
|
+
console.warn(` \u26A0 map child at:"${c.at}" \u2014 no waypoint with that label; renders full-screen`);
|
|
11663
|
+
continue;
|
|
11664
|
+
}
|
|
11665
|
+
if (typeof c.start === "number") {
|
|
11666
|
+
schedule.push(` \u{1F6D1} ${c.at}: manual ${c.start.toFixed(1)}s (unchanged)`);
|
|
11667
|
+
continue;
|
|
11668
|
+
}
|
|
11669
|
+
const dur = childDuration(c);
|
|
11670
|
+
c.start = arrival;
|
|
11671
|
+
c.end = arrival + dur;
|
|
11672
|
+
schedule.push(` \u{1F6D1} ${c.at}: ${arrival.toFixed(1)}s \u2192 ${(arrival + dur).toFixed(1)}s (${dur.toFixed(1)}s)`);
|
|
11673
|
+
}
|
|
11674
|
+
if (schedule.length) {
|
|
11675
|
+
console.log(` \u{1F5FA} route stops (${mapDur.toFixed(1)}s total):`);
|
|
11676
|
+
for (const line of schedule) console.log(line);
|
|
11677
|
+
}
|
|
11678
|
+
if (!hasOwnDur) {
|
|
11679
|
+
map.duration = mapDur;
|
|
11680
|
+
} else if ((map.duration ?? 0) < mapDur) {
|
|
11681
|
+
map.duration = mapDur;
|
|
11682
|
+
console.log(` \u{1F5FA} extended map duration \u2192 ${mapDur.toFixed(1)}s (drives + dwells)`);
|
|
11683
|
+
}
|
|
11684
|
+
}
|
|
11396
11685
|
|
|
11397
11686
|
// src/player/pipeline.ts
|
|
11398
11687
|
function isDescriptiveRoot(data) {
|
|
@@ -11408,7 +11697,7 @@ function isDescriptiveRoot(data) {
|
|
|
11408
11697
|
)) return true;
|
|
11409
11698
|
return false;
|
|
11410
11699
|
}
|
|
11411
|
-
async function resolveAndCompile(data, options
|
|
11700
|
+
async function resolveAndCompile(data, options) {
|
|
11412
11701
|
const resolved = await resolveAll2(data, {
|
|
11413
11702
|
sourcePath: options.sourcePath,
|
|
11414
11703
|
baseDir: options.baseDir,
|
|
@@ -11427,7 +11716,7 @@ async function resolveAndCompile(data, options = {}) {
|
|
|
11427
11716
|
});
|
|
11428
11717
|
return compiled;
|
|
11429
11718
|
}
|
|
11430
|
-
async function resolveAndCompileMarkdown(markdown, options
|
|
11719
|
+
async function resolveAndCompileMarkdown(markdown, options) {
|
|
11431
11720
|
const descriptive = parseMarkdownDescriptive(markdown);
|
|
11432
11721
|
return resolveAndCompile(descriptive, options);
|
|
11433
11722
|
}
|
package/src/player/pipeline.ts
CHANGED
|
@@ -21,8 +21,9 @@ import type { DescriptiveRoot } from "../descriptive/compiler";
|
|
|
21
21
|
import type { Root } from "../schema/index";
|
|
22
22
|
|
|
23
23
|
export interface ResolveAndCompileOptions {
|
|
24
|
-
/** Base directory for resolving
|
|
25
|
-
|
|
24
|
+
/** Base directory (the source file's folder) for resolving/emitting asset paths.
|
|
25
|
+
* Always set by the CLI/server; no fallback. */
|
|
26
|
+
baseDir: string;
|
|
26
27
|
/** Output directory for generated TTS audio / STT VTT files */
|
|
27
28
|
scriptOutputDir?: string;
|
|
28
29
|
/** Output directory for generated TTI/TTV media files */
|
|
@@ -88,7 +89,7 @@ export function isDescriptiveRoot(data: any): boolean {
|
|
|
88
89
|
*/
|
|
89
90
|
export async function resolveAndCompile(
|
|
90
91
|
data: DescriptiveRoot,
|
|
91
|
-
options: ResolveAndCompileOptions
|
|
92
|
+
options: ResolveAndCompileOptions,
|
|
92
93
|
): Promise<Root> {
|
|
93
94
|
// 1. Async resolve: durations, TTS, STT, includes
|
|
94
95
|
const resolved = await resolveAll(data, {
|
|
@@ -121,7 +122,7 @@ export async function resolveAndCompile(
|
|
|
121
122
|
*/
|
|
122
123
|
export async function resolveAndCompileMarkdown(
|
|
123
124
|
markdown: string,
|
|
124
|
-
options: ResolveAndCompileOptions
|
|
125
|
+
options: ResolveAndCompileOptions,
|
|
125
126
|
): Promise<Root> {
|
|
126
127
|
const descriptive = parseMarkdownDescriptive(markdown);
|
|
127
128
|
return resolveAndCompile(descriptive, options);
|
package/src/player/server.mjs
CHANGED
|
@@ -326,59 +326,39 @@ function makePathsRelative(obj) {
|
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
/**
|
|
329
|
-
* Convert
|
|
330
|
-
*
|
|
331
|
-
*
|
|
329
|
+
* Convert asset paths in the compiled root to server-relative URLs for the
|
|
330
|
+
* browser. The compiled JSON now carries md-folder-relative paths (e.g.
|
|
331
|
+
* ".markcut/generated/tts/x.mp3" or "assets/photo.jpg") because the resolve
|
|
332
|
+
* step emits them relative to the source .md folder — the server document
|
|
333
|
+
* root. This prepends "/" so the browser can fetch them (resolveAsset() then
|
|
334
|
+
* maps the URL back onto the md folder).
|
|
332
335
|
*
|
|
333
|
-
*
|
|
334
|
-
* - compiled.imports (component bundle URL)
|
|
335
|
-
* - compiled.subtitle.src (VTT file)
|
|
336
|
-
* - Any node.src (media/tts/images) that is a relative path under MARKCUT_BASE
|
|
336
|
+
* Also handles legacy absolute paths under MARKCUT_BASE (older cached trees).
|
|
337
337
|
*/
|
|
338
338
|
function resolveAssetPaths(root) {
|
|
339
339
|
const out = JSON.parse(JSON.stringify(root));
|
|
340
340
|
|
|
341
|
+
function toServerUrl(p) {
|
|
342
|
+
if (typeof p !== "string") return p;
|
|
343
|
+
if (/^(https?:|data:|blob:)/.test(p)) return p;
|
|
344
|
+
if (p.startsWith("/")) return p; // already a server URL
|
|
345
|
+
// Legacy: absolute path under the .markcut base (older cached trees)
|
|
346
|
+
if (p.startsWith(MARKCUT_BASE)) return p.slice(MARKCUT_BASE.length);
|
|
347
|
+
// md-folder-relative path (new scheme): ".markcut/...", "assets/..."
|
|
348
|
+
const rel = p.startsWith("./") ? p.slice(2) : p;
|
|
349
|
+
return "/" + rel;
|
|
350
|
+
}
|
|
351
|
+
|
|
341
352
|
function walkNode(node) {
|
|
342
353
|
if (!node || typeof node !== "object") return;
|
|
343
354
|
if (Array.isArray(node)) { node.forEach(walkNode); return; }
|
|
344
355
|
|
|
345
|
-
|
|
346
|
-
if (typeof node.src === "string" && !node.src.startsWith("http://") && !node.src.startsWith("https://") && !node.src.startsWith("data:")) {
|
|
347
|
-
// Already a relative path under MARKCUT_BASE (shouldn't be absolute)
|
|
348
|
-
if (!node.src.startsWith("/") && !node.src.startsWith(".")) {
|
|
349
|
-
// Plain relative path — prefix with / (server serves from project root)
|
|
350
|
-
node.src = "/" + node.src;
|
|
351
|
-
} else if (node.src.startsWith("./")) {
|
|
352
|
-
// Dot-prefixed relative path — strip ./ and prefix with /
|
|
353
|
-
node.src = "/" + node.src.slice(2);
|
|
354
|
-
}
|
|
355
|
-
// Convert absolute paths under MARKCUT_BASE
|
|
356
|
-
if (typeof node.src === "string" && node.src.startsWith(MARKCUT_BASE)) {
|
|
357
|
-
node.src = "/" + node.src.replace(MARKCUT_BASE + "/", "");
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Convert subtitle src
|
|
356
|
+
if (typeof node.src === "string") node.src = toServerUrl(node.src);
|
|
362
357
|
if (node.subtitle && typeof node.subtitle.src === "string") {
|
|
363
|
-
|
|
364
|
-
node.subtitle.src = "/" + node.subtitle.src.replace(MARKCUT_BASE + "/", "");
|
|
365
|
-
} else if (!node.subtitle.src.startsWith("/") && !node.subtitle.src.startsWith("http")) {
|
|
366
|
-
node.subtitle.src = "/" + node.subtitle.src;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Convert imports (component bundle URL)
|
|
371
|
-
if (typeof node.imports === "string") {
|
|
372
|
-
if (node.imports.startsWith(MARKCUT_BASE)) {
|
|
373
|
-
node.imports = "/" + node.imports.replace(MARKCUT_BASE + "/", "");
|
|
374
|
-
} else if (!node.imports.startsWith("/") && !node.imports.startsWith("http")) {
|
|
375
|
-
node.imports = "/" + node.imports;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (Array.isArray(node.children)) {
|
|
380
|
-
node.children.forEach(walkNode);
|
|
358
|
+
node.subtitle.src = toServerUrl(node.subtitle.src);
|
|
381
359
|
}
|
|
360
|
+
if (typeof node.imports === "string") node.imports = toServerUrl(node.imports);
|
|
361
|
+
if (Array.isArray(node.children)) node.children.forEach(walkNode);
|
|
382
362
|
}
|
|
383
363
|
|
|
384
364
|
walkNode(out);
|