@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/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,
|
|
@@ -375,7 +377,7 @@ function compileChildren(children, ctx, parentKind) {
|
|
|
375
377
|
} else if (isRhythm(child)) {
|
|
376
378
|
result = compileRhythm(child, ctx, parentKind);
|
|
377
379
|
} else if (isMap(child)) {
|
|
378
|
-
result =
|
|
380
|
+
result = compileMap(child, ctx, parentKind);
|
|
379
381
|
} else {
|
|
380
382
|
result = compileLeaf(child, ctx, parentKind);
|
|
381
383
|
}
|
|
@@ -526,6 +528,44 @@ function compileRhythm(node2, ctx, parentKind) {
|
|
|
526
528
|
};
|
|
527
529
|
return { stream, duration: end };
|
|
528
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
|
+
}
|
|
529
569
|
function compileContainer(node2, ctx, parentKind) {
|
|
530
570
|
const id = node2.id ?? uid();
|
|
531
571
|
ensureUniqueIds(node2.children, id);
|
|
@@ -1038,6 +1078,114 @@ var require_format = __commonJS({
|
|
|
1038
1078
|
}
|
|
1039
1079
|
});
|
|
1040
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
|
+
|
|
1041
1189
|
// src/player/pipeline.ts
|
|
1042
1190
|
init_compiler();
|
|
1043
1191
|
|
|
@@ -9956,14 +10104,22 @@ function parseWaypoints(raw) {
|
|
|
9956
10104
|
const bits = splitTokens(part.replace(/,/g, " "));
|
|
9957
10105
|
const lat = Number(bits[0] ?? 0);
|
|
9958
10106
|
const lng = Number(bits[1] ?? 0);
|
|
9959
|
-
|
|
9960
|
-
const
|
|
9961
|
-
const
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
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 };
|
|
9965
10120
|
});
|
|
9966
10121
|
}
|
|
10122
|
+
var KNOWN_TRAVEL_MODES = /* @__PURE__ */ new Set(["DRIVING", "WALKING", "BICYCLING", "TRANSIT", "FLIGHT", "BOAT"]);
|
|
9967
10123
|
function rewriteTweenExprs(s) {
|
|
9968
10124
|
return s.replace(
|
|
9969
10125
|
/tween\(\s*([^,()]+?)\s*,\s*([^,()]+?)\s*(?:,\s*([^()]+?))?\s*\)/g,
|
|
@@ -10665,7 +10821,7 @@ function processMDASTListItem(item, parent, lines) {
|
|
|
10665
10821
|
Object.assign(node2, attrs);
|
|
10666
10822
|
}
|
|
10667
10823
|
} else if (child.type === "list") {
|
|
10668
|
-
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") {
|
|
10669
10825
|
for (const subItem of child.children) {
|
|
10670
10826
|
processMDASTListItem(subItem, node2, lines);
|
|
10671
10827
|
}
|
|
@@ -10960,13 +11116,15 @@ async function resolveScripts(root, options) {
|
|
|
10960
11116
|
const clone = JSON.parse(JSON.stringify(root));
|
|
10961
11117
|
mkdirSync2(options.outputDir, { recursive: true });
|
|
10962
11118
|
const allScriptNodes = [];
|
|
10963
|
-
|
|
10964
|
-
|
|
10965
|
-
if (
|
|
10966
|
-
|
|
10967
|
-
|
|
10968
|
-
|
|
10969
|
-
|
|
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);
|
|
10970
11128
|
const totalScripts = allScriptNodes.length;
|
|
10971
11129
|
let scriptsDone = 0;
|
|
10972
11130
|
let cacheHits = 0;
|
|
@@ -10974,9 +11132,9 @@ async function resolveScripts(root, options) {
|
|
|
10974
11132
|
if (totalScripts > 0) {
|
|
10975
11133
|
console.log(` \u{1F50A} TTS: generating ${totalScripts} script${totalScripts > 1 ? "s" : ""}...`);
|
|
10976
11134
|
}
|
|
10977
|
-
for (const { node: node2, id } of allScriptNodes) {
|
|
11135
|
+
for (const { node: node2, id, ttsOverride } of allScriptNodes) {
|
|
10978
11136
|
scriptsDone++;
|
|
10979
|
-
let ttsCli =
|
|
11137
|
+
let ttsCli = ttsOverride ?? options.ttsCli ?? DEFAULT_TTS_CLI;
|
|
10980
11138
|
if (node2.speaker && clone.voices) {
|
|
10981
11139
|
const speakerVoice = clone.voices[node2.speaker];
|
|
10982
11140
|
if (speakerVoice) {
|
|
@@ -11441,9 +11599,89 @@ async function resolveAll2(root, options) {
|
|
|
11441
11599
|
mergedOutputDir: options.subtitleOutputDir
|
|
11442
11600
|
});
|
|
11443
11601
|
}
|
|
11602
|
+
result = await resolveRouteStops(result);
|
|
11444
11603
|
result = relativizeAssetsUnder(result, options.baseDir);
|
|
11445
11604
|
return result;
|
|
11446
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
|
+
}
|
|
11447
11685
|
|
|
11448
11686
|
// src/player/pipeline.ts
|
|
11449
11687
|
function isDescriptiveRoot(data) {
|
package/src/schema/index.ts
CHANGED
|
@@ -42,6 +42,7 @@ const BaseShape = {
|
|
|
42
42
|
endAt: z.number().min(0).optional().describe("trim seconds at source end (video/audio)"),
|
|
43
43
|
durationInSeconds: z.number().optional().describe("set by engine; do not edit by hand"),
|
|
44
44
|
on: eventSpec.optional().describe("event that fires at a specific frame, mutating registered component state"),
|
|
45
|
+
at: z.string().optional().describe("when child of a map: waypoint label to anchor at (renderer positions at that waypoint's screen pixel)"),
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
export const base = z.object(BaseShape);
|
|
@@ -219,6 +220,8 @@ export const mapWaypoint = z.object({
|
|
|
219
220
|
lng: z.number(),
|
|
220
221
|
label: z.string().optional(),
|
|
221
222
|
media: z.string().optional().describe("image/video src for waypoint marker"),
|
|
223
|
+
mode: z.enum(["DRIVING", "WALKING", "BICYCLING", "TRANSIT", "FLIGHT", "BOAT"]).optional()
|
|
224
|
+
.describe("travel mode for the leg FROM this waypoint to the next; defaults to map travelMode"),
|
|
222
225
|
});
|
|
223
226
|
|
|
224
227
|
/**
|
|
@@ -289,6 +292,7 @@ export const mapStream = base.extend({
|
|
|
289
292
|
cinematic: mapCinematic.optional().describe("cinematic camera behavior"),
|
|
290
293
|
streetView: mapStreetView.optional().describe("immersive street view config"),
|
|
291
294
|
googleMapsApiKey: z.string().optional().describe("injected by compiler from GOOGLE_MAPS_API_KEY env var"),
|
|
295
|
+
children: z.array(z.lazy((): z.ZodTypeAny => stream)).default(() => []).describe("overlay children rendered on top of the map; use at:\"Label\" to anchor at a waypoint"),
|
|
292
296
|
});
|
|
293
297
|
export type MapStream = z.infer<typeof mapStream>;
|
|
294
298
|
|
package/src/types/Effect.tsx
CHANGED
|
@@ -17,9 +17,12 @@ import type { Effect as EffectStream } from "../schema/index";
|
|
|
17
17
|
export function EffectWrapper({
|
|
18
18
|
stream,
|
|
19
19
|
children,
|
|
20
|
+
contained,
|
|
20
21
|
}: {
|
|
21
22
|
stream: EffectStream;
|
|
22
23
|
children: React.ReactNode;
|
|
24
|
+
/** Fill the parent box instead of the whole canvas (map overlay children). */
|
|
25
|
+
contained?: boolean;
|
|
23
26
|
}) {
|
|
24
27
|
const frame = useCurrentFrame();
|
|
25
28
|
const { fps, width, height } = useVideoConfig();
|
|
@@ -78,7 +81,15 @@ export function EffectWrapper({
|
|
|
78
81
|
|
|
79
82
|
return (
|
|
80
83
|
<div
|
|
81
|
-
style={Object.assign(
|
|
84
|
+
style={Object.assign(
|
|
85
|
+
{
|
|
86
|
+
width: contained ? "100%" : width,
|
|
87
|
+
height: contained ? "100%" : height,
|
|
88
|
+
position: "absolute" as const,
|
|
89
|
+
inset: 0,
|
|
90
|
+
},
|
|
91
|
+
...styles,
|
|
92
|
+
)}
|
|
82
93
|
className="effect"
|
|
83
94
|
>
|
|
84
95
|
{children}
|