@lalalic/markcut 1.1.1 → 2.0.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/AGENTS.md +39 -15
- package/README.md +7 -3
- package/SKILL.md +26 -172
- package/docs/edit-mode.md +1 -1
- package/docs/label-mode.md +6 -4
- package/docs/markdown-descriptive.md +46 -1
- package/docs/system-prompt-edit.md +16 -0
- package/package.json +1 -1
- package/src/config.mjs +8 -3
- package/src/descriptive/compiler.test.ts +42 -42
- package/src/descriptive/compiler.ts +41 -52
- package/src/descriptive/markdown.ts +8 -4
- package/src/descriptive/resolve.test.ts +14 -7
- package/src/descriptive/resolve.ts +148 -20
- package/src/entry.tsx +8 -2
- package/src/player/browser.tsx +178 -54
- package/src/player/bundle/player.js +1168 -566
- package/src/player/components/EditControls.tsx +92 -0
- package/src/player/components/HeaderBar.tsx +60 -0
- package/src/player/components/LabelControls.tsx +367 -0
- package/src/player/components/SceneThumbnails.tsx +87 -0
- package/src/player/components/VariantBar.tsx +39 -0
- package/src/player/components/index.ts +5 -0
- package/src/player/pipeline.mjs +130 -66
- package/src/player/pipeline.ts +3 -1
- package/src/player/server-shared.mjs +5 -7
- package/src/player/server.mjs +194 -187
- package/src/render/cli.mjs +66 -13
- package/src/schema/index.ts +20 -23
- package/src/types/Audio.tsx +25 -33
- package/src/types/Component.tsx +18 -24
- package/src/types/Effect.tsx +31 -39
- package/src/types/Image.tsx +23 -30
- package/src/types/Include.tsx +70 -76
- package/src/types/Map.tsx +48 -44
- package/src/types/Rhythm.tsx +19 -27
- package/src/types/Video.tsx +40 -47
- package/src/utils/component-import-map.ts +93 -0
- package/src/utils/index.ts +23 -10
- package/src/vision/cli.mjs +6 -6
- package/templates/courseware/TEMPLATE.md +317 -0
- package/templates/courseware/agents/reviewer.md +84 -0
- package/templates/courseware/prompts/fix.md +30 -0
- package/templates/courseware/prompts/outline.md +53 -0
- package/templates/courseware/prompts/scene.md +64 -0
- package/tests/fixtures/audio.json +4 -2
- package/tests/fixtures/basic.json +2 -1
- package/tests/fixtures/component-all.json +4 -2
- package/tests/fixtures/components.json +4 -2
- package/tests/fixtures/effects.json +12 -6
- package/tests/fixtures/full.json +8 -4
- package/tests/fixtures/map.json +2 -1
- package/tests/fixtures/md/dialogue.md +12 -0
- package/tests/fixtures/md/edge-cases.md +9 -0
- package/tests/fixtures/scenes.json +4 -2
- package/tests/fixtures/subtitle.json +6 -3
- package/tests/fixtures/subvideo.json +11 -6
- package/tests/fixtures/templates/courseware.md +61 -4
- package/tests/fixtures/tween-visual.json +2 -6
- package/tests/fixtures/video-series.json +6 -14
- package/tests/md-descriptive.test.ts +170 -0
- package/tests/render.test.ts +32 -16
- package/tests/schema.test.ts +6 -3
- package/tests/server.test.ts +9 -6
- package/tests/utils.ts +4 -4
- package/docs/dynamic-components.md +0 -191
- package/docs/templates.md +0 -52
- package/scripts/artlist-dl.mjs +0 -190
- package/src/player/label-server.mjs +0 -599
package/src/types/Include.tsx
CHANGED
|
@@ -104,17 +104,20 @@ function useSubVideoRegistry(importsUrl: string | undefined):
|
|
|
104
104
|
* registered by the sub-video are available during its rendering.
|
|
105
105
|
*
|
|
106
106
|
* Usage:
|
|
107
|
-
* { type: "include", src: "./path/to/video.json",
|
|
107
|
+
* { type: "include", src: "./path/to/video.json", start: 0, end: 5 }
|
|
108
108
|
*/
|
|
109
109
|
export function IncludeLeaf({ stream }: { stream: IncludeStream }) {
|
|
110
110
|
const { fps: parentFps, width: parentWidth, height: parentHeight } = useVideoConfig();
|
|
111
111
|
const parentCompose = React.useContext(ComposeContext);
|
|
112
112
|
const { Container } = parentCompose;
|
|
113
113
|
const parentAudio = React.useContext(AudioContext);
|
|
114
|
-
const
|
|
114
|
+
const start = stream.start ?? 0;
|
|
115
|
+
const end = stream.end ?? (stream.durationInSeconds ?? (start + (stream.duration ?? 1)));
|
|
116
|
+
const totalDur = stream.durationInSeconds ?? end;
|
|
115
117
|
useFrameEvents(stream.on, Math.max(1, Math.floor(totalDur * parentFps)));
|
|
116
118
|
|
|
117
|
-
|
|
119
|
+
const hasContent = !!stream.src || (stream.children?.length ?? 0) > 0;
|
|
120
|
+
if (!hasContent) return null;
|
|
118
121
|
|
|
119
122
|
// ── External JSON loading ─────────────────────────────────────────────
|
|
120
123
|
const [externalData, setExternalData] = React.useState<unknown | null>(null);
|
|
@@ -296,87 +299,78 @@ export function IncludeLeaf({ stream }: { stream: IncludeStream }) {
|
|
|
296
299
|
);
|
|
297
300
|
}, [externalData, loadError, parentFps, parentWidth, parentHeight]);
|
|
298
301
|
|
|
299
|
-
// ──
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
type="include"
|
|
319
|
-
style={{
|
|
320
|
-
...cssJS(stream.style) as React.CSSProperties,
|
|
321
|
-
width: parentWidth,
|
|
322
|
-
height: parentHeight,
|
|
323
|
-
overflow: "hidden",
|
|
324
|
-
position: "relative",
|
|
325
|
-
}}
|
|
326
|
-
className={`include ${toClassName(stream.id ?? "")}`}
|
|
327
|
-
>
|
|
328
|
-
<FolderLeaf stream={stream as any} />
|
|
329
|
-
</Container>
|
|
330
|
-
</Sequence>
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// ── External src rendering ─────────────────────────────────────
|
|
335
|
-
return (
|
|
336
|
-
<Sequence
|
|
337
|
-
key={a.id}
|
|
338
|
-
durationInFrames={durFrames}
|
|
339
|
-
from={Math.floor(parentFps * start)}
|
|
340
|
-
layout="none"
|
|
341
|
-
showInTimeline={false}
|
|
302
|
+
// ── Single-span rendering ────────────────────────────────────────
|
|
303
|
+
const dur = Math.max(0.1, end - start);
|
|
304
|
+
const durFrames = Math.max(1, Math.floor(parentFps * dur));
|
|
305
|
+
|
|
306
|
+
// ── Inline children fallback (legacy) ─────────────────────────
|
|
307
|
+
let innerNode: React.ReactNode;
|
|
308
|
+
if (!stream.src && stream.children?.length) {
|
|
309
|
+
innerNode = (
|
|
310
|
+
<Container
|
|
311
|
+
id={stream.id}
|
|
312
|
+
type="include"
|
|
313
|
+
style={{
|
|
314
|
+
...cssJS(stream.style) as React.CSSProperties,
|
|
315
|
+
width: parentWidth,
|
|
316
|
+
height: parentHeight,
|
|
317
|
+
overflow: "hidden",
|
|
318
|
+
position: "relative",
|
|
319
|
+
}}
|
|
320
|
+
className={`include ${toClassName(stream.id ?? "")}`}
|
|
342
321
|
>
|
|
343
|
-
{
|
|
344
|
-
|
|
345
|
-
style={{
|
|
346
|
-
width: parentWidth,
|
|
347
|
-
height: parentHeight,
|
|
348
|
-
overflow: "hidden",
|
|
349
|
-
position: "relative",
|
|
350
|
-
}}
|
|
351
|
-
>
|
|
352
|
-
{renderExternalContent()}
|
|
353
|
-
</div>
|
|
354
|
-
) : (
|
|
355
|
-
<div
|
|
356
|
-
style={{
|
|
357
|
-
width: parentWidth,
|
|
358
|
-
height: parentHeight,
|
|
359
|
-
display: "flex",
|
|
360
|
-
alignItems: "center",
|
|
361
|
-
justifyContent: "center",
|
|
362
|
-
color: "#666",
|
|
363
|
-
fontSize: 20,
|
|
364
|
-
fontFamily: "monospace",
|
|
365
|
-
}}
|
|
366
|
-
>
|
|
367
|
-
Loading… {stream.src}
|
|
368
|
-
</div>
|
|
369
|
-
)}
|
|
370
|
-
</Sequence>
|
|
322
|
+
<FolderLeaf stream={stream as any} />
|
|
323
|
+
</Container>
|
|
371
324
|
);
|
|
372
|
-
}
|
|
325
|
+
} else if (externalData || loadError) {
|
|
326
|
+
innerNode = (
|
|
327
|
+
<div
|
|
328
|
+
style={{
|
|
329
|
+
width: parentWidth,
|
|
330
|
+
height: parentHeight,
|
|
331
|
+
overflow: "hidden",
|
|
332
|
+
position: "relative",
|
|
333
|
+
}}
|
|
334
|
+
>
|
|
335
|
+
{renderExternalContent()}
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
} else {
|
|
339
|
+
innerNode = (
|
|
340
|
+
<div
|
|
341
|
+
style={{
|
|
342
|
+
width: parentWidth,
|
|
343
|
+
height: parentHeight,
|
|
344
|
+
display: "flex",
|
|
345
|
+
alignItems: "center",
|
|
346
|
+
justifyContent: "center",
|
|
347
|
+
color: "#666",
|
|
348
|
+
fontSize: 20,
|
|
349
|
+
fontFamily: "monospace",
|
|
350
|
+
}}
|
|
351
|
+
>
|
|
352
|
+
Loading… {stream.src}
|
|
353
|
+
</div>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const sequence = (
|
|
358
|
+
<Sequence
|
|
359
|
+
durationInFrames={durFrames}
|
|
360
|
+
from={Math.floor(parentFps * start)}
|
|
361
|
+
layout="none"
|
|
362
|
+
showInTimeline={false}
|
|
363
|
+
>
|
|
364
|
+
{innerNode}
|
|
365
|
+
</Sequence>
|
|
366
|
+
);
|
|
373
367
|
|
|
374
368
|
// ── Determine if we need a nested ComposeContext ───────────────────
|
|
375
369
|
const needsNestedContext = subRegistry !== null && subRegistry !== parentCompose.components;
|
|
376
370
|
|
|
377
371
|
const inner = (
|
|
378
372
|
<AudioContext.Provider value={audioCtx as any}>
|
|
379
|
-
{
|
|
373
|
+
{sequence}
|
|
380
374
|
</AudioContext.Provider>
|
|
381
375
|
);
|
|
382
376
|
|
package/src/types/Map.tsx
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* travelMode: "DRIVING", // DRIVING | WALKING | BICYCLING
|
|
16
16
|
* mapType: "roadmap", // roadmap | satellite | hybrid | terrain
|
|
17
17
|
* routeMarker: "🚗", // emoji/char for animated pin
|
|
18
|
-
*
|
|
18
|
+
* start: 0, end: 5
|
|
19
19
|
* }
|
|
20
20
|
*/
|
|
21
21
|
import React from "react";
|
|
@@ -27,7 +27,17 @@ import {
|
|
|
27
27
|
} from "@vis.gl/react-google-maps";
|
|
28
28
|
import type { MapStream } from "../schema/index";
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// API key is injected by the compiler onto the stream node (see compileLeaf in compiler.ts).
|
|
31
|
+
// This fallback handles the case where Map.tsx is used directly without the compiler.
|
|
32
|
+
function resolveApiKey(stream: MapStream): string {
|
|
33
|
+
if (stream.googleMapsApiKey) return stream.googleMapsApiKey;
|
|
34
|
+
// Safe fallback for Node.js contexts (e.g. remotion render without compiler).
|
|
35
|
+
// In the browser player the key is always pre-stamped by the server-side compiler.
|
|
36
|
+
if (typeof process !== "undefined" && typeof process.env !== "undefined" && process.env.GOOGLE_MAPS_API_KEY) {
|
|
37
|
+
return process.env.GOOGLE_MAPS_API_KEY;
|
|
38
|
+
}
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
31
41
|
|
|
32
42
|
// ============================================================
|
|
33
43
|
// MapLeaf — entry point, renders each action as a Sequence
|
|
@@ -35,52 +45,46 @@ const GM_API_KEY = process.env.GOOGLE_MAPS_API_KEY || "";
|
|
|
35
45
|
export function MapLeaf({ stream }: { stream: MapStream }) {
|
|
36
46
|
const { fps } = useVideoConfig();
|
|
37
47
|
const waypoints = stream.waypoints ?? [];
|
|
38
|
-
const
|
|
48
|
+
const start = stream.start ?? 0;
|
|
49
|
+
const end = stream.end ?? start + (stream.duration ?? 1);
|
|
50
|
+
const totalDur = stream.durationInSeconds ?? end;
|
|
51
|
+
const apiKey = resolveApiKey(stream);
|
|
39
52
|
useFrameEvents(stream.on, Math.max(1, Math.floor(totalDur * fps)));
|
|
40
53
|
if (waypoints.length === 0) return null;
|
|
41
54
|
|
|
55
|
+
const durFrames = Math.max(1, Math.floor(fps * (end - start)));
|
|
56
|
+
const center = stream.center ?? { lat: waypoints[0].lat, lng: waypoints[0].lng };
|
|
57
|
+
const zoom = stream.zoom ?? 10;
|
|
58
|
+
const mapType = stream.mapType ?? "roadmap";
|
|
59
|
+
const travelMode = stream.travelMode ?? "DRIVING";
|
|
60
|
+
const markerEmoji = stream.routeMarker ?? "🚗";
|
|
42
61
|
return (
|
|
43
|
-
|
|
44
|
-
{
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}}
|
|
70
|
-
style={{ width: "100%", height: "100%", position: "absolute" }}
|
|
71
|
-
>
|
|
72
|
-
<RouteWithMarker
|
|
73
|
-
waypoints={waypoints}
|
|
74
|
-
travelMode={travelMode}
|
|
75
|
-
markerEmoji={markerEmoji}
|
|
76
|
-
actionDuration={end - start}
|
|
77
|
-
/>
|
|
78
|
-
</GoogleMap>
|
|
79
|
-
</APIProvider>
|
|
80
|
-
</Sequence>
|
|
81
|
-
);
|
|
82
|
-
})}
|
|
83
|
-
</>
|
|
62
|
+
<Sequence
|
|
63
|
+
durationInFrames={durFrames}
|
|
64
|
+
from={Math.floor(fps * start)}
|
|
65
|
+
layout="none"
|
|
66
|
+
>
|
|
67
|
+
<APIProvider apiKey={apiKey}>
|
|
68
|
+
<GoogleMap
|
|
69
|
+
mapId={String(stream.id ?? "map")}
|
|
70
|
+
defaultCenter={center}
|
|
71
|
+
defaultZoom={zoom}
|
|
72
|
+
defaultOptions={{
|
|
73
|
+
mapTypeId: mapType,
|
|
74
|
+
disableDefaultUI: true,
|
|
75
|
+
zoomControl: false,
|
|
76
|
+
}}
|
|
77
|
+
style={{ width: "100%", height: "100%", position: "absolute" }}
|
|
78
|
+
>
|
|
79
|
+
<RouteWithMarker
|
|
80
|
+
waypoints={waypoints}
|
|
81
|
+
travelMode={travelMode}
|
|
82
|
+
markerEmoji={markerEmoji}
|
|
83
|
+
actionDuration={end - start}
|
|
84
|
+
/>
|
|
85
|
+
</GoogleMap>
|
|
86
|
+
</APIProvider>
|
|
87
|
+
</Sequence>
|
|
84
88
|
);
|
|
85
89
|
}
|
|
86
90
|
|
package/src/types/Rhythm.tsx
CHANGED
|
@@ -21,38 +21,30 @@ function resolveAudioSrc(src: string): string {
|
|
|
21
21
|
export function RhythmLeaf({ stream }: { stream: Rhythm }) {
|
|
22
22
|
const { fps } = useVideoConfig();
|
|
23
23
|
const environment = useRemotionEnvironment();
|
|
24
|
-
const
|
|
24
|
+
const start = stream.start ?? 0;
|
|
25
|
+
const end = stream.end ?? start + (stream.duration ?? 1);
|
|
26
|
+
const totalDur = stream.durationInSeconds ?? end;
|
|
25
27
|
useFrameEvents(stream.on, Math.max(1, Math.floor(totalDur * fps)));
|
|
26
28
|
|
|
27
29
|
if (!stream.src || environment.isStudio) return null;
|
|
28
30
|
|
|
29
31
|
const resolvedSrc = resolveAudioSrc(stream.src);
|
|
30
|
-
|
|
32
|
+
const volume = stream.volume ?? 1;
|
|
31
33
|
return (
|
|
32
|
-
|
|
33
|
-
{stream.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
src={resolvedSrc}
|
|
48
|
-
muted={volume === 0}
|
|
49
|
-
volume={volume}
|
|
50
|
-
loop
|
|
51
|
-
showInTimeline={false}
|
|
52
|
-
/>
|
|
53
|
-
</Sequence>
|
|
54
|
-
);
|
|
55
|
-
})}
|
|
56
|
-
</>
|
|
34
|
+
<Sequence
|
|
35
|
+
name={stream.src ?? "rhythm"}
|
|
36
|
+
durationInFrames={Math.max(1, Math.floor(fps * (end - start)))}
|
|
37
|
+
from={Math.floor(fps * start)}
|
|
38
|
+
layout="none"
|
|
39
|
+
showInTimeline={false}
|
|
40
|
+
>
|
|
41
|
+
<RemotionAudio
|
|
42
|
+
src={resolvedSrc}
|
|
43
|
+
muted={volume === 0}
|
|
44
|
+
volume={volume}
|
|
45
|
+
loop
|
|
46
|
+
showInTimeline={false}
|
|
47
|
+
/>
|
|
48
|
+
</Sequence>
|
|
57
49
|
);
|
|
58
50
|
}
|
package/src/types/Video.tsx
CHANGED
|
@@ -13,58 +13,51 @@ import { FrameSyncStyle } from "./FrameSyncStyle";
|
|
|
13
13
|
export function VideoLeaf({ stream }: { stream: Video }) {
|
|
14
14
|
const { fps } = useVideoConfig();
|
|
15
15
|
const audio = React.useContext(AudioContext);
|
|
16
|
-
const
|
|
16
|
+
const start = stream.start ?? 0;
|
|
17
|
+
const end = stream.end ?? start + (stream.duration ?? 1);
|
|
18
|
+
const totalDur = stream.durationInSeconds ?? end;
|
|
17
19
|
useFrameEvents(stream.on, Math.max(1, Math.floor(totalDur * fps)));
|
|
18
20
|
if (!stream.src) return null;
|
|
19
21
|
const resolvedSrc = resolveVideoSrc(stream.src);
|
|
20
22
|
|
|
23
|
+
const startFrom = stream.startFrom ?? 0;
|
|
24
|
+
const endAt = stream.endAt ?? totalDur;
|
|
25
|
+
const volume = stream.volume ?? 1;
|
|
26
|
+
const playbackRate = stream.loop ? 1 : Math.min(1, toPlaybackRate((endAt - startFrom) / (end - start)));
|
|
27
|
+
const streamStyle = cssJS(stream.style);
|
|
28
|
+
const hasAnimation = "animation" in streamStyle;
|
|
21
29
|
return (
|
|
22
|
-
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
layout="none"
|
|
30
|
+
<Sequence
|
|
31
|
+
durationInFrames={Math.max(1, Math.floor(fps * (end - start)))}
|
|
32
|
+
from={Math.floor(fps * start)}
|
|
33
|
+
layout="none"
|
|
34
|
+
showInTimeline={false}
|
|
35
|
+
>
|
|
36
|
+
{hasAnimation ? (
|
|
37
|
+
<FrameSyncStyle style={streamStyle}>
|
|
38
|
+
<OffthreadVideo
|
|
39
|
+
src={resolvedSrc}
|
|
40
|
+
startFrom={Math.floor(startFrom * fps)}
|
|
41
|
+
endAt={Math.floor(startFrom * fps) + Math.floor(((endAt - startFrom) * fps) / playbackRate)}
|
|
42
|
+
muted={volume === 0 || !!audio?.foreground}
|
|
43
|
+
volume={volume}
|
|
44
|
+
playbackRate={playbackRate}
|
|
38
45
|
showInTimeline={false}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
src={resolvedSrc}
|
|
56
|
-
startFrom={Math.floor(startFrom * fps)}
|
|
57
|
-
endAt={Math.floor(startFrom * fps) + Math.floor(((endAt - startFrom) * fps) / playbackRate)}
|
|
58
|
-
muted={volume === 0 || !!audio?.foreground}
|
|
59
|
-
volume={volume}
|
|
60
|
-
playbackRate={playbackRate}
|
|
61
|
-
showInTimeline={false}
|
|
62
|
-
style={{ width: "100%", height: "100%", ...actionStyle }}
|
|
63
|
-
/>
|
|
64
|
-
)}
|
|
65
|
-
</Sequence>
|
|
66
|
-
);
|
|
67
|
-
})}
|
|
68
|
-
</>
|
|
46
|
+
style={{ width: "100%", height: "100%" }}
|
|
47
|
+
/>
|
|
48
|
+
</FrameSyncStyle>
|
|
49
|
+
) : (
|
|
50
|
+
<OffthreadVideo
|
|
51
|
+
src={resolvedSrc}
|
|
52
|
+
startFrom={Math.floor(startFrom * fps)}
|
|
53
|
+
endAt={Math.floor(startFrom * fps) + Math.floor(((endAt - startFrom) * fps) / playbackRate)}
|
|
54
|
+
muted={volume === 0 || !!audio?.foreground}
|
|
55
|
+
volume={volume}
|
|
56
|
+
playbackRate={playbackRate}
|
|
57
|
+
showInTimeline={false}
|
|
58
|
+
style={{ width: "100%", height: "100%", ...streamStyle }}
|
|
59
|
+
/>
|
|
60
|
+
)}
|
|
61
|
+
</Sequence>
|
|
69
62
|
);
|
|
70
63
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared-module import map for dynamically loaded component bundles.
|
|
3
|
+
*
|
|
4
|
+
* Component bundles are built with react/react-dom/remotion marked as
|
|
5
|
+
* --external (see player/bundler.mjs getSharedExternals), so their output
|
|
6
|
+
* contains bare imports like `import { useState } from "react"`. The page
|
|
7
|
+
* that dynamically imports such a bundle must provide an import map that
|
|
8
|
+
* resolves those bare specifiers back to the already-loaded module
|
|
9
|
+
* instances — otherwise the import fails ("Failed to resolve module
|
|
10
|
+
* specifier") or, worse, a duplicate React breaks hooks and context.
|
|
11
|
+
*
|
|
12
|
+
* The preview player injects this map itself (player/browser.tsx). This
|
|
13
|
+
* helper provides the same shim for the Remotion render page (headless
|
|
14
|
+
* Chrome during `markcut render`). Both use the `#rmtr-import-map` guard,
|
|
15
|
+
* so whichever runs first wins and the other becomes a no-op.
|
|
16
|
+
*/
|
|
17
|
+
import * as React from "react";
|
|
18
|
+
import * as ReactDOM from "react-dom";
|
|
19
|
+
import * as Remotion from "remotion";
|
|
20
|
+
|
|
21
|
+
export function ensureSharedImportMap(): void {
|
|
22
|
+
if (typeof document === "undefined") return;
|
|
23
|
+
if (document.querySelector("#rmtr-import-map")) return;
|
|
24
|
+
|
|
25
|
+
const g = globalThis as any;
|
|
26
|
+
g.__remotionShared ??= {};
|
|
27
|
+
g.__remotionShared["react"] ??= React;
|
|
28
|
+
g.__remotionShared["react-dom"] ??= ReactDOM;
|
|
29
|
+
g.__remotionShared["remotion"] ??= Remotion;
|
|
30
|
+
|
|
31
|
+
// Stub for node:* imports that some component deps reference at module
|
|
32
|
+
// level for build-time helpers never called during rendering.
|
|
33
|
+
if (!g.__nodeModuleStub) {
|
|
34
|
+
const nodeModuleStubCode = `export function createRequire() { return () => ({ resolve: () => { throw new Error("node:module.createRequire is not available in the browser"); } }); }`;
|
|
35
|
+
g.__nodeModuleStub = URL.createObjectURL(
|
|
36
|
+
new Blob([nodeModuleStubCode], { type: "application/javascript" }),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const imports: Record<string, string> = {};
|
|
42
|
+
for (const spec of ["node:module", "node:fs", "node:path", "node:os"]) {
|
|
43
|
+
imports[spec] = g.__nodeModuleStub;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const [specifier, mod] of Object.entries(g.__remotionShared) as [string, any][]) {
|
|
47
|
+
const lines: string[] = [
|
|
48
|
+
`const _mod = globalThis.__remotionShared[${JSON.stringify(specifier)}];`,
|
|
49
|
+
];
|
|
50
|
+
for (const name of Object.keys(mod)) {
|
|
51
|
+
if (name === "default") continue;
|
|
52
|
+
lines.push(
|
|
53
|
+
`const __${name} = _mod[${JSON.stringify(name)}];`,
|
|
54
|
+
`export { __${name} as ${name} };`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if ("default" in mod) lines.push("export default _mod.default;");
|
|
58
|
+
imports[specifier] = URL.createObjectURL(
|
|
59
|
+
new Blob([lines.join("\n")], { type: "application/javascript" }),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// react/jsx-runtime — built manually because it's a sub-path of react.
|
|
64
|
+
const blobJsx = new Blob(
|
|
65
|
+
[
|
|
66
|
+
`
|
|
67
|
+
const R = globalThis.__remotionShared["react"];
|
|
68
|
+
const { createElement, Fragment } = R;
|
|
69
|
+
export { Fragment };
|
|
70
|
+
export function jsx(type, props, key) {
|
|
71
|
+
return createElement(type, key != null ? { ...props, key } : props);
|
|
72
|
+
}
|
|
73
|
+
export function jsxs(type, props, key) {
|
|
74
|
+
return createElement(type, key != null ? { ...props, key } : props);
|
|
75
|
+
}
|
|
76
|
+
export function jsxDEV(type, props, key, isStaticChildren, source, self) {
|
|
77
|
+
return createElement(type, key != null ? { ...props, key } : props);
|
|
78
|
+
}
|
|
79
|
+
`,
|
|
80
|
+
],
|
|
81
|
+
{ type: "application/javascript" },
|
|
82
|
+
);
|
|
83
|
+
imports["react/jsx-runtime"] = URL.createObjectURL(blobJsx);
|
|
84
|
+
|
|
85
|
+
const script = document.createElement("script");
|
|
86
|
+
script.type = "importmap";
|
|
87
|
+
script.id = "rmtr-import-map";
|
|
88
|
+
script.textContent = JSON.stringify({ imports });
|
|
89
|
+
document.head.appendChild(script);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.warn("Failed to create shared module import map:", e);
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -57,11 +57,12 @@ export function walkDown<T extends StreamNode>(
|
|
|
57
57
|
* Compute duration of a stream subtree, in seconds.
|
|
58
58
|
*
|
|
59
59
|
* - rhythm streams use their pre-set durationInSeconds (set by host)
|
|
60
|
-
* - leaf
|
|
60
|
+
* - leaf duration uses base `end` (or `start + duration`) via leafEnd()
|
|
61
61
|
* - series sums children, subtracting transition overlaps
|
|
62
62
|
* - sequence (parallel) takes max child duration
|
|
63
63
|
* - background children do not contribute
|
|
64
|
-
* -
|
|
64
|
+
* - leaf duration uses base `end` (or `start + duration`) via leafEnd()
|
|
65
|
+
* - streamRef/template instancing is unsupported in lite
|
|
65
66
|
*/
|
|
66
67
|
export interface DurationStream extends StreamNode {
|
|
67
68
|
type?: string;
|
|
@@ -70,10 +71,24 @@ export interface DurationStream extends StreamNode {
|
|
|
70
71
|
transition?: string;
|
|
71
72
|
transitionTime?: number;
|
|
72
73
|
durationInSeconds?: number;
|
|
73
|
-
|
|
74
|
+
start?: number;
|
|
75
|
+
end?: number;
|
|
76
|
+
duration?: number;
|
|
74
77
|
children?: DurationStream[];
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a leaf node's effective end time from its base timing fields.
|
|
82
|
+
* `end` is the source of truth; `duration` is a convenience that normalizes
|
|
83
|
+
* to `start + duration` when `end` is absent. Returns 0 when neither is set.
|
|
84
|
+
*/
|
|
85
|
+
export function leafEnd(stream: { start?: number; end?: number; duration?: number }): number {
|
|
86
|
+
if (typeof stream.end === "number") return stream.end;
|
|
87
|
+
const s = stream.start ?? 0;
|
|
88
|
+
if (typeof stream.duration === "number") return s + stream.duration;
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
export function getDurationInSeconds(stream: DurationStream, update = true): number {
|
|
78
93
|
if (!stream) return 0;
|
|
79
94
|
|
|
@@ -81,13 +96,12 @@ export function getDurationInSeconds(stream: DurationStream, update = true): num
|
|
|
81
96
|
return stream.durationInSeconds ?? 0;
|
|
82
97
|
}
|
|
83
98
|
|
|
84
|
-
// include: if src is set, treat as leaf (duration from
|
|
99
|
+
// include: if src is set, treat as leaf (duration from base end).
|
|
85
100
|
// Otherwise fall back to inline children (legacy).
|
|
86
101
|
if (stream.type === "include") {
|
|
87
102
|
if (stream.src) {
|
|
88
|
-
// External reference — use
|
|
89
|
-
const
|
|
90
|
-
const d = last?.end ?? 0;
|
|
103
|
+
// External reference — use base end (like other leaves)
|
|
104
|
+
const d = leafEnd(stream);
|
|
91
105
|
if (update) stream.durationInSeconds = d;
|
|
92
106
|
return d;
|
|
93
107
|
}
|
|
@@ -109,10 +123,9 @@ export function getDurationInSeconds(stream: DurationStream, update = true): num
|
|
|
109
123
|
}
|
|
110
124
|
|
|
111
125
|
// scene is a container (like folder) — falls through to general children logic
|
|
112
|
-
// leaf with
|
|
126
|
+
// leaf with base timing fields (start/end/duration)
|
|
113
127
|
if (!stream.children?.length) {
|
|
114
|
-
const
|
|
115
|
-
const d = last?.end ?? 0;
|
|
128
|
+
const d = leafEnd(stream);
|
|
116
129
|
if (update) stream.durationInSeconds = d;
|
|
117
130
|
return d;
|
|
118
131
|
}
|
package/src/vision/cli.mjs
CHANGED
|
@@ -662,7 +662,7 @@ function buildPreviewTree(folder, metadata) {
|
|
|
662
662
|
id: `${e.name}-media`,
|
|
663
663
|
type: e.isVideo ? "video" : "image",
|
|
664
664
|
src: e.relSrc, fit: "cover",
|
|
665
|
-
|
|
665
|
+
start: 0, end: e.dur,
|
|
666
666
|
// Preserve existing labels from metadata so re-labeling shows them
|
|
667
667
|
userHints: e.userHints || undefined,
|
|
668
668
|
}],
|
|
@@ -943,9 +943,9 @@ async function runFullPipeline(folder, prompts, ittCli, vttCli, sttCli, context,
|
|
|
943
943
|
const previewJsonPath = join(folder, ".preview.json");
|
|
944
944
|
writeFileSync(previewJsonPath, JSON.stringify(previewTree, null, 2), "utf-8");
|
|
945
945
|
|
|
946
|
-
const
|
|
947
|
-
if (!existsSync(
|
|
948
|
-
emitError(`
|
|
946
|
+
const playerServer = join(__dirname, "..", "player", "server.mjs");
|
|
947
|
+
if (!existsSync(playerServer)) {
|
|
948
|
+
emitError(`Player server not found at ${playerServer}`);
|
|
949
949
|
process.exit(1);
|
|
950
950
|
}
|
|
951
951
|
|
|
@@ -954,7 +954,7 @@ async function runFullPipeline(folder, prompts, ittCli, vttCli, sttCli, context,
|
|
|
954
954
|
emitInfo(` Labels will be merged into metadata.json as user hints.\n`);
|
|
955
955
|
|
|
956
956
|
const port = 3031;
|
|
957
|
-
const child = spawn("node", [
|
|
957
|
+
const child = spawn("node", [playerServer, previewJsonPath, "--label", `--port=${port}`], {
|
|
958
958
|
cwd: resolve(__dirname, "..", ".."),
|
|
959
959
|
stdio: ["ignore", "pipe", "inherit"],
|
|
960
960
|
});
|
|
@@ -963,7 +963,7 @@ async function runFullPipeline(folder, prompts, ittCli, vttCli, sttCli, context,
|
|
|
963
963
|
if (child.stdout) {
|
|
964
964
|
child.stdout.on("data", (chunk) => {
|
|
965
965
|
process.stdout.write(chunk);
|
|
966
|
-
if (!serverReady && chunk.toString().includes("
|
|
966
|
+
if (!serverReady && chunk.toString().includes("Player ready")) {
|
|
967
967
|
serverReady = true;
|
|
968
968
|
try { execSync(`open http://localhost:${port}`, { stdio: "ignore" }); } catch {}
|
|
969
969
|
}
|