@lalalic/markcut 2.9.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/markcut/SKILL.md +7 -0
- package/skills/markcut/docs/components.md +45 -2
- package/skills/markcut/docs/map-dynamic-camera.md +244 -0
- package/skills/markcut/docs/markdown-descriptive.md +7 -2
- package/src/components/Markdown.tsx +138 -24
- package/src/components/Mermaid.tsx +223 -22
- package/src/context/EventContext.tsx +3 -0
- package/src/descriptive/compiler.ts +105 -29
- package/src/descriptive/dsl.ts +42 -5
- package/src/descriptive/markdown.ts +23 -0
- package/src/descriptive/resolve.test.ts +5 -5
- package/src/descriptive/resolve.ts +51 -12
- package/src/player/bundle/player.js +751 -143
- package/src/player/pipeline.mjs +130 -32
- package/src/player/pipeline.ts +5 -4
- package/src/player/server.mjs +22 -42
- package/src/render/cli.mjs +54 -3
- package/src/render/validate-assets.mjs +140 -0
- package/src/schema/index.ts +58 -2
- package/src/spots/cli.mjs +266 -0
- package/src/types/Component.tsx +27 -1
- package/src/types/Effect.tsx +13 -6
- package/src/types/Folder.tsx +1 -1
- package/src/types/Map.tsx +501 -127
- package/src/utils/index.ts +14 -2
- package/src/utils/tween.ts +49 -1
- package/tests/dsl.test.ts +43 -0
- package/tests/fixtures/map-dynamic.json +52 -0
- package/tests/fixtures/md/animate-diagrams.md +42 -0
- package/tests/fixtures/md/electricity-grow.md +130 -0
- package/tests/fixtures/md/map-all-views.md +28 -0
- package/tests/md-descriptive.test.ts +58 -0
- package/tests/render.test.ts +1 -0
- package/tests/schema.test.ts +58 -1
- package/tests/validate-assets.test.ts +106 -0
package/src/player/pipeline.mjs
CHANGED
|
@@ -33,7 +33,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
33
33
|
|
|
34
34
|
// src/utils/index.ts
|
|
35
35
|
function uid() {
|
|
36
|
-
|
|
36
|
+
const raw = Math.random().toString(36).slice(2, 10);
|
|
37
|
+
const first = raw[0];
|
|
38
|
+
if (/^[0-9]/.test(first)) {
|
|
39
|
+
const letter = String.fromCharCode(97 + Math.floor(Math.random() * 26));
|
|
40
|
+
return letter + raw;
|
|
41
|
+
}
|
|
42
|
+
return raw;
|
|
37
43
|
}
|
|
38
44
|
function walkDown(node2, visit, parent = null, depth = 0) {
|
|
39
45
|
const keep = visit(node2, parent, depth);
|
|
@@ -207,7 +213,8 @@ function wrapWithEffects(node2, result, parentKind) {
|
|
|
207
213
|
const absStart = innerStream.start ?? 0;
|
|
208
214
|
const absEnd = innerStream.end ?? result.duration;
|
|
209
215
|
const duration = absEnd - absStart;
|
|
210
|
-
const
|
|
216
|
+
const isBgNoEnd = innerStream.isBackground && innerStream.end == null;
|
|
217
|
+
const resetStream = isBgNoEnd ? { ...innerStream } : {
|
|
211
218
|
...innerStream,
|
|
212
219
|
start: 0,
|
|
213
220
|
end: duration,
|
|
@@ -224,28 +231,33 @@ function wrapWithEffects(node2, result, parentKind) {
|
|
|
224
231
|
id: uid(),
|
|
225
232
|
type: "effect",
|
|
226
233
|
animation: spec.animation,
|
|
234
|
+
animationDurationSeconds: spec.duration,
|
|
227
235
|
durationInSeconds: spec.duration,
|
|
228
236
|
animationTimingFunction: spec.animationTimingFunction,
|
|
229
237
|
animationIterationCount: spec.animationIterationCount ?? 1,
|
|
230
238
|
customKeyframes: spec.customKeyframes,
|
|
231
239
|
children: [currentStream],
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
240
|
+
// For background inner nodes: propagate start/end as-is so parent
|
|
241
|
+
// back-propagation fills the correct scene duration. The effect's
|
|
242
|
+
// durationInSeconds (animation spec) controls animation timing.
|
|
243
|
+
start: isOutermost && isBgNoEnd ? innerStream.start : effStart,
|
|
244
|
+
end: isOutermost && isBgNoEnd ? void 0 : effEnd,
|
|
245
|
+
visible: innerStream.visible ?? true,
|
|
235
246
|
...pickOn(node2)
|
|
236
247
|
};
|
|
237
248
|
}
|
|
238
249
|
return { stream: currentStream, duration: result.duration };
|
|
239
250
|
}
|
|
240
251
|
function compileLeaf(node2, ctx, parentKind) {
|
|
241
|
-
const id = node2.id
|
|
252
|
+
const id = node2.id;
|
|
253
|
+
const hasExplicitId = node2.id != null;
|
|
242
254
|
const hasOwnDuration = typeof node2.duration === "number" || typeof node2.endAt === "number";
|
|
243
255
|
const isBgNoOwnTiming = node2.isBackground && !hasOwnDuration;
|
|
244
256
|
const start = isBgNoOwnTiming ? typeof node2.start === "number" ? node2.start : void 0 : parentKind === "parallel" ? Math.max(0, node2.start ?? 0) : 0;
|
|
245
257
|
const duration = isBgNoOwnTiming ? void 0 : deriveLeafDuration(node2, ctx);
|
|
246
258
|
const end = duration != null ? start + duration : void 0;
|
|
247
259
|
const base = {
|
|
248
|
-
id,
|
|
260
|
+
...id ? { id } : {},
|
|
249
261
|
style: node2.style,
|
|
250
262
|
visible: node2.visible ?? true,
|
|
251
263
|
isBackground: node2.isBackground,
|
|
@@ -305,8 +317,7 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
305
317
|
const bindings = {};
|
|
306
318
|
for (const key of Object.keys(node2)) {
|
|
307
319
|
if (!KNOWN_COMPONENT_KEYS.has(key)) {
|
|
308
|
-
|
|
309
|
-
if (typeof val === "string") bindings[key] = val;
|
|
320
|
+
bindings[key] = node2[key];
|
|
310
321
|
}
|
|
311
322
|
}
|
|
312
323
|
const stream = {
|
|
@@ -332,6 +343,7 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
332
343
|
const stream = {
|
|
333
344
|
...base,
|
|
334
345
|
type: "map",
|
|
346
|
+
view: node2.view ?? "route",
|
|
335
347
|
waypoints: node2.waypoints,
|
|
336
348
|
routeColor: node2.routeColor ?? "#4285F4",
|
|
337
349
|
routeWeight: node2.routeWeight ?? 4,
|
|
@@ -342,6 +354,9 @@ function compileLeaf(node2, ctx, parentKind) {
|
|
|
342
354
|
region: node2.region,
|
|
343
355
|
travelMode: node2.travelMode ?? "DRIVING",
|
|
344
356
|
routeMarker: node2.routeMarker ?? "\u{1F697}",
|
|
357
|
+
camera: node2.camera,
|
|
358
|
+
cinematic: node2.cinematic,
|
|
359
|
+
streetView: node2.streetView,
|
|
345
360
|
googleMapsApiKey: ctx.googleMapsApiKey
|
|
346
361
|
};
|
|
347
362
|
return { stream, duration: end ?? 0 };
|
|
@@ -407,13 +422,23 @@ function compileScene(node2, ctx, parentKind) {
|
|
|
407
422
|
];
|
|
408
423
|
const sceneContentDuration = sceneKind === "parallel" ? aggregateDuration(compiledChildren, "parallel") : aggregateDuration(compiledChildren, sceneKind, resolved.time);
|
|
409
424
|
const localDuration = Math.max(node2.duration ?? 0, sceneContentDuration);
|
|
410
|
-
|
|
411
|
-
if (
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
425
|
+
function backpropagate(stream2, dur) {
|
|
426
|
+
if (stream2.end == null) {
|
|
427
|
+
stream2.end = dur;
|
|
428
|
+
if (stream2.durationInSeconds == null) {
|
|
429
|
+
stream2.durationInSeconds = dur;
|
|
430
|
+
}
|
|
431
|
+
if (stream2.start == null) stream2.start = 0;
|
|
432
|
+
}
|
|
433
|
+
if (stream2.type === "effect" && Array.isArray(stream2.children)) {
|
|
434
|
+
for (const child of stream2.children) {
|
|
435
|
+
backpropagate(child, dur);
|
|
436
|
+
}
|
|
415
437
|
}
|
|
416
438
|
}
|
|
439
|
+
for (const c of compiledChildren) {
|
|
440
|
+
backpropagate(c.stream, localDuration);
|
|
441
|
+
}
|
|
417
442
|
const start = parentKind === "parallel" ? Math.max(0, node2.start ?? 0) : 0;
|
|
418
443
|
const end = start + localDuration;
|
|
419
444
|
const stream = {
|
|
@@ -507,12 +532,22 @@ function compileContainer(node2, ctx, parentKind) {
|
|
|
507
532
|
const resolved = node2.type === "transitionSeries" ? resolveTransition(node2.transition, node2.transitionTime) : { name: "fade", time: 0.5 };
|
|
508
533
|
const children = compileChildren(node2.children, ctx, node2.type);
|
|
509
534
|
const duration = aggregateDuration(children, node2.type, resolved.time);
|
|
510
|
-
|
|
511
|
-
if (
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
535
|
+
function backpropagate(stream2, dur) {
|
|
536
|
+
if (stream2.end == null) {
|
|
537
|
+
stream2.end = dur;
|
|
538
|
+
if (stream2.durationInSeconds == null) {
|
|
539
|
+
stream2.durationInSeconds = dur;
|
|
540
|
+
}
|
|
541
|
+
if (stream2.start == null) stream2.start = 0;
|
|
515
542
|
}
|
|
543
|
+
if (stream2.type === "effect" && Array.isArray(stream2.children)) {
|
|
544
|
+
for (const child of stream2.children) {
|
|
545
|
+
backpropagate(child, dur);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
for (const c of children) {
|
|
550
|
+
backpropagate(c.stream, duration);
|
|
516
551
|
}
|
|
517
552
|
const stream = {
|
|
518
553
|
id,
|
|
@@ -9922,18 +9957,41 @@ function parseWaypoints(raw) {
|
|
|
9922
9957
|
const lat = Number(bits[0] ?? 0);
|
|
9923
9958
|
const lng = Number(bits[1] ?? 0);
|
|
9924
9959
|
const labelRaw = bits[2];
|
|
9925
|
-
const
|
|
9926
|
-
|
|
9960
|
+
const labelRawUq = labelRaw ? unquote(labelRaw) : void 0;
|
|
9961
|
+
const label = labelRawUq ? labelRawUq : void 0;
|
|
9962
|
+
const mediaRaw = bits[3];
|
|
9963
|
+
const media = mediaRaw ? unquote(mediaRaw) : void 0;
|
|
9964
|
+
return { lat, lng, label, media };
|
|
9927
9965
|
});
|
|
9928
9966
|
}
|
|
9967
|
+
function rewriteTweenExprs(s) {
|
|
9968
|
+
return s.replace(
|
|
9969
|
+
/tween\(\s*([^,()]+?)\s*,\s*([^,()]+?)\s*(?:,\s*([^()]+?))?\s*\)/g,
|
|
9970
|
+
(_match, fromRaw, toRaw, easingRaw) => {
|
|
9971
|
+
const scalar = (v) => {
|
|
9972
|
+
const t = v.trim();
|
|
9973
|
+
if (/^[+-]?(\d+(\.\d+)?|\.\d+)$/.test(t)) return t;
|
|
9974
|
+
if (/^"(?:[^"\\]|\\.)*"$/.test(t)) return t;
|
|
9975
|
+
if (t === "true" || t === "false" || t === "null") return t;
|
|
9976
|
+
return JSON.stringify(t);
|
|
9977
|
+
};
|
|
9978
|
+
const items = [scalar(fromRaw), scalar(toRaw)];
|
|
9979
|
+
if (easingRaw !== void 0 && easingRaw.trim()) {
|
|
9980
|
+
items.push(scalar(easingRaw));
|
|
9981
|
+
}
|
|
9982
|
+
return `{"__tween":[${items.join(",")}]}`;
|
|
9983
|
+
}
|
|
9984
|
+
);
|
|
9985
|
+
}
|
|
9929
9986
|
function parseProps(raw) {
|
|
9930
9987
|
const s = raw.trim();
|
|
9931
9988
|
if (!s.startsWith("{") && !s.startsWith("[")) return {};
|
|
9932
9989
|
if (!s.endsWith("}") && !s.endsWith("]")) return {};
|
|
9990
|
+
const withTweens = rewriteTweenExprs(s);
|
|
9933
9991
|
try {
|
|
9934
|
-
return JSON.parse(
|
|
9992
|
+
return JSON.parse(withTweens);
|
|
9935
9993
|
} catch {
|
|
9936
|
-
let normalized =
|
|
9994
|
+
let normalized = withTweens.replace(
|
|
9937
9995
|
/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:(?=\s*["{[]?)/g,
|
|
9938
9996
|
'$1"$2":'
|
|
9939
9997
|
);
|
|
@@ -9949,7 +10007,7 @@ function parseProps(raw) {
|
|
|
9949
10007
|
return JSON.parse(normalized);
|
|
9950
10008
|
} catch {
|
|
9951
10009
|
try {
|
|
9952
|
-
const result = (0, eval)("(" +
|
|
10010
|
+
const result = (0, eval)("(" + withTweens + ")");
|
|
9953
10011
|
return typeof result === "object" && result !== null ? result : {};
|
|
9954
10012
|
} catch {
|
|
9955
10013
|
return {};
|
|
@@ -10122,6 +10180,7 @@ var TYPE_TOKENS = {
|
|
|
10122
10180
|
video: "video",
|
|
10123
10181
|
audio: "audio",
|
|
10124
10182
|
component: "component",
|
|
10183
|
+
event: "event",
|
|
10125
10184
|
rhythm: "rhythm",
|
|
10126
10185
|
include: "include",
|
|
10127
10186
|
map: "map",
|
|
@@ -10201,6 +10260,10 @@ function preserveVariantAttrs(node2, attrs) {
|
|
|
10201
10260
|
"mapType",
|
|
10202
10261
|
"data",
|
|
10203
10262
|
"prompt",
|
|
10263
|
+
"view",
|
|
10264
|
+
"camera",
|
|
10265
|
+
"cinematic",
|
|
10266
|
+
"streetView",
|
|
10204
10267
|
"name",
|
|
10205
10268
|
"title",
|
|
10206
10269
|
"transition",
|
|
@@ -10341,6 +10404,21 @@ function parseNodeLine(content3, lineNum) {
|
|
|
10341
10404
|
preserveVariantAttrs(node2, attrs);
|
|
10342
10405
|
return node2;
|
|
10343
10406
|
}
|
|
10407
|
+
case "event": {
|
|
10408
|
+
const node2 = {
|
|
10409
|
+
type: "component",
|
|
10410
|
+
id: attrs.id,
|
|
10411
|
+
jsx: "",
|
|
10412
|
+
duration: attrs.duration,
|
|
10413
|
+
start: attrs.start,
|
|
10414
|
+
instruction: attrs.instruction,
|
|
10415
|
+
style: attrs.style,
|
|
10416
|
+
effects: attrs.effects,
|
|
10417
|
+
on: attrs.on
|
|
10418
|
+
};
|
|
10419
|
+
preserveVariantAttrs(node2, attrs);
|
|
10420
|
+
return node2;
|
|
10421
|
+
}
|
|
10344
10422
|
case "rhythm": {
|
|
10345
10423
|
const src = firstPositional ?? attrs.src;
|
|
10346
10424
|
if (!src) throw new DslError("rhythm requires src", ctx);
|
|
@@ -10408,6 +10486,7 @@ function parseNodeLine(content3, lineNum) {
|
|
|
10408
10486
|
waypoints: attrs.waypoints ?? [],
|
|
10409
10487
|
duration: attrs.duration,
|
|
10410
10488
|
start: attrs.start,
|
|
10489
|
+
view: attrs.view,
|
|
10411
10490
|
routeMarker: attrs.routeMarker,
|
|
10412
10491
|
travelMode: attrs.travelMode,
|
|
10413
10492
|
routeColor: attrs.routeColor,
|
|
@@ -10415,6 +10494,9 @@ function parseNodeLine(content3, lineNum) {
|
|
|
10415
10494
|
zoom: attrs.zoom,
|
|
10416
10495
|
center: attrs.center,
|
|
10417
10496
|
mapType: attrs.mapType,
|
|
10497
|
+
camera: attrs.camera,
|
|
10498
|
+
cinematic: attrs.cinematic,
|
|
10499
|
+
streetView: attrs.streetView,
|
|
10418
10500
|
language: attrs.language ?? attrs.lang,
|
|
10419
10501
|
region: attrs.region,
|
|
10420
10502
|
instruction: attrs.instruction,
|
|
@@ -10717,7 +10799,22 @@ function probeDuration(src, baseDir) {
|
|
|
10717
10799
|
}
|
|
10718
10800
|
function resolveSrc(src, baseDir) {
|
|
10719
10801
|
if (/^(https?:|file:|\/)/.test(src)) return src;
|
|
10720
|
-
return resolvePath(baseDir
|
|
10802
|
+
return resolvePath(baseDir, src);
|
|
10803
|
+
}
|
|
10804
|
+
function relativizeAssetsUnder(node2, baseDir) {
|
|
10805
|
+
if (!node2 || typeof node2 !== "object") return node2;
|
|
10806
|
+
if (Array.isArray(node2)) return node2.map((v) => relativizeAssetsUnder(v, baseDir));
|
|
10807
|
+
const out = {};
|
|
10808
|
+
for (const [k, v] of Object.entries(node2)) {
|
|
10809
|
+
if (typeof v === "string" && v.startsWith(baseDir + "/")) {
|
|
10810
|
+
out[k] = v.slice(baseDir.length + 1);
|
|
10811
|
+
} else if (v && typeof v === "object") {
|
|
10812
|
+
out[k] = relativizeAssetsUnder(v, baseDir);
|
|
10813
|
+
} else {
|
|
10814
|
+
out[k] = v;
|
|
10815
|
+
}
|
|
10816
|
+
}
|
|
10817
|
+
return out;
|
|
10721
10818
|
}
|
|
10722
10819
|
var COMMON_RESOLUTIONS = [
|
|
10723
10820
|
{ width: 1920, height: 1080 },
|
|
@@ -10762,7 +10859,7 @@ function resolveMediaSrc(src, targetWidth, targetHeight, baseDir) {
|
|
|
10762
10859
|
console.warn(` \u26A0 No matching media file for "${src}" at ${targetWidth}x${targetHeight}`);
|
|
10763
10860
|
return exactAbs;
|
|
10764
10861
|
}
|
|
10765
|
-
async function resolveMediaSrcs(root, options
|
|
10862
|
+
async function resolveMediaSrcs(root, options) {
|
|
10766
10863
|
const clone = JSON.parse(JSON.stringify(root));
|
|
10767
10864
|
const baseDir = options.baseDir;
|
|
10768
10865
|
const targetWidth = clone.width ?? 1080;
|
|
@@ -10783,7 +10880,7 @@ async function resolveMediaSrcs(root, options = {}) {
|
|
|
10783
10880
|
});
|
|
10784
10881
|
return clone;
|
|
10785
10882
|
}
|
|
10786
|
-
async function resolveMediaDurations(root, options
|
|
10883
|
+
async function resolveMediaDurations(root, options) {
|
|
10787
10884
|
const clone = JSON.parse(JSON.stringify(root));
|
|
10788
10885
|
const baseDir = options.baseDir;
|
|
10789
10886
|
walkDown(clone, (node2) => {
|
|
@@ -11195,9 +11292,9 @@ function applyStoryboardOverrides(root, options) {
|
|
|
11195
11292
|
}
|
|
11196
11293
|
return clone;
|
|
11197
11294
|
}
|
|
11198
|
-
async function resolveIncludes(root, options
|
|
11295
|
+
async function resolveIncludes(root, options) {
|
|
11199
11296
|
const clone = JSON.parse(JSON.stringify(root));
|
|
11200
|
-
const baseDir = options.baseDir
|
|
11297
|
+
const baseDir = options.baseDir;
|
|
11201
11298
|
const outputDir = options.includeOutputDir ?? join(baseDir, ".markcut", "generated", "includes");
|
|
11202
11299
|
mkdirSync2(outputDir, { recursive: true });
|
|
11203
11300
|
function extractImportEntriesFromRaw(raw) {
|
|
@@ -11276,7 +11373,7 @@ async function resolveIncludes(root, options = {}) {
|
|
|
11276
11373
|
}
|
|
11277
11374
|
return clone;
|
|
11278
11375
|
}
|
|
11279
|
-
async function resolveAll2(root, options
|
|
11376
|
+
async function resolveAll2(root, options) {
|
|
11280
11377
|
let result = root;
|
|
11281
11378
|
if (result.seed == null && options.seed == null && options.sourcePath) {
|
|
11282
11379
|
const autoSeed = parseInt(computeCacheKey(result).slice(0, 8), 16);
|
|
@@ -11344,6 +11441,7 @@ async function resolveAll2(root, options = {}) {
|
|
|
11344
11441
|
mergedOutputDir: options.subtitleOutputDir
|
|
11345
11442
|
});
|
|
11346
11443
|
}
|
|
11444
|
+
result = relativizeAssetsUnder(result, options.baseDir);
|
|
11347
11445
|
return result;
|
|
11348
11446
|
}
|
|
11349
11447
|
|
|
@@ -11361,7 +11459,7 @@ function isDescriptiveRoot(data) {
|
|
|
11361
11459
|
)) return true;
|
|
11362
11460
|
return false;
|
|
11363
11461
|
}
|
|
11364
|
-
async function resolveAndCompile(data, options
|
|
11462
|
+
async function resolveAndCompile(data, options) {
|
|
11365
11463
|
const resolved = await resolveAll2(data, {
|
|
11366
11464
|
sourcePath: options.sourcePath,
|
|
11367
11465
|
baseDir: options.baseDir,
|
|
@@ -11380,7 +11478,7 @@ async function resolveAndCompile(data, options = {}) {
|
|
|
11380
11478
|
});
|
|
11381
11479
|
return compiled;
|
|
11382
11480
|
}
|
|
11383
|
-
async function resolveAndCompileMarkdown(markdown, options
|
|
11481
|
+
async function resolveAndCompileMarkdown(markdown, options) {
|
|
11384
11482
|
const descriptive = parseMarkdownDescriptive(markdown);
|
|
11385
11483
|
return resolveAndCompile(descriptive, options);
|
|
11386
11484
|
}
|
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);
|
package/src/render/cli.mjs
CHANGED
|
@@ -80,6 +80,12 @@ Commands:
|
|
|
80
80
|
vision <folder> Full pipeline: extract → normalize → percept → segments
|
|
81
81
|
--label Add interactive labeling step before AI pipeline
|
|
82
82
|
--instruct "text" Background context about people/places (injected into prompts)
|
|
83
|
+
|
|
84
|
+
spots --waypoints "lat,lng;..." Discover POIs along a route (Directions + Places API)
|
|
85
|
+
--travelMode DRIVING DRIVING | WALKING | BICYCLING (default DRIVING)
|
|
86
|
+
--limit 8 Max spots after ranking
|
|
87
|
+
--photos Attach a static-map thumbnail per spot
|
|
88
|
+
--markdown Print waypoints:[...] markdown to stderr
|
|
83
89
|
--prompts-file <path> Path to prompts markdown file (default: vision_prompts.md)
|
|
84
90
|
--vtt-sample-interval <n> Sample one video frame every N seconds (default: 5)
|
|
85
91
|
--skip-stt Skip speech-to-text for videos
|
|
@@ -90,6 +96,7 @@ Commands:
|
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
|
|
99
|
+
/**
|
|
93
100
|
/**
|
|
94
101
|
* Render a stream tree to an MP4 video with compact progress output.
|
|
95
102
|
*
|
|
@@ -100,7 +107,7 @@ Commands:
|
|
|
100
107
|
* Use --verbose to see every frame line (original behavior).
|
|
101
108
|
*/
|
|
102
109
|
|
|
103
|
-
function renderOne(streamTree, outputPath, verbose) {
|
|
110
|
+
function renderOne(streamTree, outputPath, verbose, publicDir) {
|
|
104
111
|
const tmpProps = join(ROOT, ".tmp", "render-stream.json");
|
|
105
112
|
mkdirSync(dirname(tmpProps), { recursive: true });
|
|
106
113
|
writeFileSync(tmpProps, JSON.stringify({ root: streamTree }));
|
|
@@ -116,7 +123,10 @@ console.log(`\n▶ Rendering → ${outputPath}`);
|
|
|
116
123
|
if (args.dev) {
|
|
117
124
|
spawnOpts.env = { ...process.env, NODE_ENV: "development" };
|
|
118
125
|
}
|
|
119
|
-
|
|
126
|
+
// Serve local media (TTS, subtitles, images) from the .markcut base dir.
|
|
127
|
+
const renderArgs = ["remotion", "render", "Root", outputPath, "--props", tmpProps, "--config", "remotion.config.ts"];
|
|
128
|
+
if (publicDir) renderArgs.push("--public-dir", publicDir);
|
|
129
|
+
const proc = spawn("npx", renderArgs, spawnOpts);
|
|
120
130
|
|
|
121
131
|
let lastLoggedFrame = 0;
|
|
122
132
|
let totalFrames = 0;
|
|
@@ -213,6 +223,11 @@ edit=${DEFAULT_EDIT_CLI}`);
|
|
|
213
223
|
process.exit(0);
|
|
214
224
|
}
|
|
215
225
|
|
|
226
|
+
if (args.command === "spots") {
|
|
227
|
+
await import("../spots/cli.mjs"); // self-executing top-level script
|
|
228
|
+
process.exit(0);
|
|
229
|
+
}
|
|
230
|
+
|
|
216
231
|
if (args.command === "preview") {
|
|
217
232
|
// --storyboard implies --edit: show story structure fast (prompts as
|
|
218
233
|
// placeholder components) and let user chat to reshape before generation.
|
|
@@ -276,6 +291,7 @@ edit=${DEFAULT_EDIT_CLI}`);
|
|
|
276
291
|
if (args.command === "render") {
|
|
277
292
|
let streamTree;
|
|
278
293
|
let rawInput = "";
|
|
294
|
+
let renderPublicDir; // .markcut base dir → Remotion publicDir for local media
|
|
279
295
|
|
|
280
296
|
if (args.file) {
|
|
281
297
|
const filePath = resolve(args.file);
|
|
@@ -334,6 +350,10 @@ edit=${DEFAULT_EDIT_CLI}`);
|
|
|
334
350
|
includeOutputDir: generatedDir(filePath, "includes"),
|
|
335
351
|
subtitleOutputDir: variantDir(filePath),
|
|
336
352
|
});
|
|
353
|
+
// Serve the source .md folder as Remotion's public dir (the "default
|
|
354
|
+
// root"): every asset in the JSON is md-folder-relative, so paths like
|
|
355
|
+
// .markcut/generated/... and assets/... resolve via staticFile.
|
|
356
|
+
renderPublicDir = fileDir;
|
|
337
357
|
} else {
|
|
338
358
|
const parsed = JSON.parse(raw);
|
|
339
359
|
const root = parsed.root ?? parsed;
|
|
@@ -348,6 +368,7 @@ edit=${DEFAULT_EDIT_CLI}`);
|
|
|
348
368
|
includeOutputDir: generatedDir(filePath, "includes"),
|
|
349
369
|
subtitleOutputDir: variantDir(filePath),
|
|
350
370
|
});
|
|
371
|
+
renderPublicDir = fileDir;
|
|
351
372
|
} else {
|
|
352
373
|
streamTree = root;
|
|
353
374
|
}
|
|
@@ -378,8 +399,24 @@ edit=${DEFAULT_EDIT_CLI}`);
|
|
|
378
399
|
}
|
|
379
400
|
}
|
|
380
401
|
|
|
402
|
+
// Guard: every local asset must be relative to the source folder (the
|
|
403
|
+
// render publicDir). Absolute paths or ".." escapes would 404 in Remotion
|
|
404
|
+
// (staticFile serves --public-dir), so fail fast with actionable errors.
|
|
405
|
+
if (renderPublicDir) {
|
|
406
|
+
const { validateAssetsRelative } = await import("./validate-assets.mjs");
|
|
407
|
+
const assetErrors = validateAssetsRelative(streamTree, renderPublicDir);
|
|
408
|
+
if (assetErrors.length > 0) {
|
|
409
|
+
for (const e of assetErrors) emitError(e);
|
|
410
|
+
emitError(
|
|
411
|
+
`Aborting render: ${assetErrors.length} asset(s) not relative to baseDir ` +
|
|
412
|
+
`(${renderPublicDir}). Fix the source or re-run resolve, then retry.`
|
|
413
|
+
);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
381
418
|
const output = args.output ? resolve(args.output) : join(ROOT, "out", "video.mp4");
|
|
382
|
-
await renderOne(streamTree, output, args.verbose);
|
|
419
|
+
await renderOne(streamTree, output, args.verbose, renderPublicDir);
|
|
383
420
|
|
|
384
421
|
console.log("\n✅ Render complete.");
|
|
385
422
|
process.exit(0);
|
|
@@ -487,6 +524,20 @@ function hasScript(root) {
|
|
|
487
524
|
}
|
|
488
525
|
walkMedia(descriptive.children);
|
|
489
526
|
|
|
527
|
+
// ── Check: all assets relative to baseDir ───────────────────────────
|
|
528
|
+
// Every local asset must be a path relative to the source file's folder
|
|
529
|
+
// (the render publicDir). Absolute paths, "/..." and ".." escapes would
|
|
530
|
+
// 404 in render / break media in preview.
|
|
531
|
+
{
|
|
532
|
+
const { validateAssetsRelative } = await import("./validate-assets.mjs");
|
|
533
|
+
const baseDir = dirname(filePath);
|
|
534
|
+
// Descriptive trees: include.src may point outside baseDir legitimately
|
|
535
|
+
// (it is compiled against its own baseDir, then relativized).
|
|
536
|
+
for (const e of validateAssetsRelative(descriptive, baseDir, { skipIncludeSrc: true })) {
|
|
537
|
+
errors.push(e);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
490
541
|
// ── Results ─────────────────────────────────────────────────────────
|
|
491
542
|
for (const w of warnings) emitWarn(w);
|
|
492
543
|
if (errors.length > 0) {
|