@lalalic/markcut 1.2.0 → 2.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/AGENTS.md +40 -17
- package/README.md +7 -3
- package/SKILL.md +0 -2
- package/docs/edit-mode.md +1 -1
- package/docs/label-mode.md +6 -4
- package/docs/markdown-descriptive.md +34 -1
- package/docs/system-prompt-edit.md +19 -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/player/browser.tsx +178 -54
- package/src/player/bundle/player.js +1149 -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 +60 -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 +336 -239
- package/src/render/cli.mjs +4 -6
- 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/index.ts +23 -10
- package/src/vision/cli.mjs +6 -6
- package/templates/courseware/TEMPLATE.md +16 -10
- 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/scripts/artlist-dl.mjs +0 -190
- package/src/player/label-server.mjs +0 -599
|
@@ -31,3 +31,12 @@ layout:parallel
|
|
|
31
31
|
- image src:a.jpg duration:3 start:1
|
|
32
32
|
- image src:b.jpg duration:2 start:0
|
|
33
33
|
- image src:c.jpg duration:4 start:2
|
|
34
|
+
## ScriptCodeFence
|
|
35
|
+
layout:series
|
|
36
|
+
- script
|
|
37
|
+
~~~script
|
|
38
|
+
This is a longer narration
|
|
39
|
+
that goes across multiple lines
|
|
40
|
+
and uses a code fence instead of inline text.
|
|
41
|
+
~~~
|
|
42
|
+
- script "Inline text also works"
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"id": "scene-1-headline",
|
|
19
19
|
"type": "component",
|
|
20
20
|
"jsx": "<div style={{background:'linear-gradient(135deg,#667eea,#764ba2)',padding:'30px',borderRadius:'12px',textAlign:'center'}}><h2 style={{color:'white',fontSize:'28px',margin:0}}>Scene Rendering Works</h2><p style={{color:'rgba(255,255,255,0.7)',fontSize:'14px',marginTop:'8px'}}>Testing the scene type in stream tree</p></div>",
|
|
21
|
-
"
|
|
21
|
+
"start": 0,
|
|
22
|
+
"end": 3
|
|
22
23
|
}
|
|
23
24
|
]
|
|
24
25
|
},
|
|
@@ -32,7 +33,8 @@
|
|
|
32
33
|
"id": "scene-2-headline",
|
|
33
34
|
"type": "component",
|
|
34
35
|
"jsx": "<div style={{background:'linear-gradient(135deg,#764ba2,#667eea)',padding:'30px',borderRadius:'12px',textAlign:'center'}}><h2 style={{color:'white',fontSize:'28px',margin:0}}>Test Complete</h2><p style={{color:'rgba(255,255,255,0.7)',fontSize:'14px',marginTop:'8px'}}>All scene types render correctly</p></div>",
|
|
35
|
-
"
|
|
36
|
+
"start": 0,
|
|
37
|
+
"end": 2
|
|
36
38
|
}
|
|
37
39
|
]
|
|
38
40
|
}
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"type": "image",
|
|
24
24
|
"src": "https://picsum.photos/seed/sub1/640/480",
|
|
25
25
|
"fit": "cover",
|
|
26
|
-
"
|
|
26
|
+
"start": 0,
|
|
27
|
+
"end": 4
|
|
27
28
|
}
|
|
28
29
|
]
|
|
29
30
|
},
|
|
@@ -37,7 +38,8 @@
|
|
|
37
38
|
"type": "image",
|
|
38
39
|
"src": "https://picsum.photos/seed/sub2/640/480",
|
|
39
40
|
"fit": "cover",
|
|
40
|
-
"
|
|
41
|
+
"start": 0,
|
|
42
|
+
"end": 4
|
|
41
43
|
}
|
|
42
44
|
]
|
|
43
45
|
},
|
|
@@ -51,7 +53,8 @@
|
|
|
51
53
|
"type": "image",
|
|
52
54
|
"src": "https://picsum.photos/seed/sub3/640/480",
|
|
53
55
|
"fit": "cover",
|
|
54
|
-
"
|
|
56
|
+
"start": 0,
|
|
57
|
+
"end": 5
|
|
55
58
|
}
|
|
56
59
|
]
|
|
57
60
|
}
|
|
@@ -18,13 +18,15 @@
|
|
|
18
18
|
"type": "image",
|
|
19
19
|
"src": "https://picsum.photos/seed/subv-intro/640/480",
|
|
20
20
|
"fit": "cover",
|
|
21
|
-
"
|
|
21
|
+
"start": 0,
|
|
22
|
+
"end": 2
|
|
22
23
|
},
|
|
23
24
|
{
|
|
24
25
|
"id": "title-intro",
|
|
25
26
|
"type": "component",
|
|
26
27
|
"jsx": "<div style={{background:'#667eea',color:'white',padding:'30px',borderRadius:'12px',textAlign:'center'}}><h2 style={{margin:0,fontSize:'28px'}}>Main Video</h2></div>",
|
|
27
|
-
"
|
|
28
|
+
"start": 0,
|
|
29
|
+
"end": 2
|
|
28
30
|
}
|
|
29
31
|
]
|
|
30
32
|
},
|
|
@@ -32,8 +34,9 @@
|
|
|
32
34
|
"id": "subvideo-chapter",
|
|
33
35
|
"type": "include",
|
|
34
36
|
"name": "Embedded-Reel",
|
|
35
|
-
"src": "data:application/json;base64,
|
|
36
|
-
"
|
|
37
|
+
"src": "data:application/json;base64,eyJpZCI6InJvb3QiLCJ0eXBlIjoicm9vdCIsIndpZHRoIjo2NDAsImhlaWdodCI6NDgwLCJmcHMiOjMwLCJpc1NlcmllcyI6dHJ1ZSwidHJhbnNpdGlvbiI6ImZhZGUiLCJ0cmFuc2l0aW9uVGltZSI6MC4zLCJjaGlsZHJlbiI6W3siaWQiOiJzdWItc2NlbmUtMSIsInR5cGUiOiJmb2xkZXIiLCJpc1NlcmllcyI6ZmFsc2UsImNoaWxkcmVuIjpbeyJpZCI6InN1Yi1iZy0xIiwidHlwZSI6ImltYWdlIiwic3JjIjoiaHR0cHM6Ly9waWNzdW0ucGhvdG9zL3NlZWQvc3Vidi1pbm5lcjEvNjQwLzQ4MCIsImZpdCI6ImNvdmVyIiwic3RhcnQiOjAsImVuZCI6Mn1dfSx7ImlkIjoic3ViLXNjZW5lLTIiLCJ0eXBlIjoiZm9sZGVyIiwiaXNTZXJpZXMiOmZhbHNlLCJjaGlsZHJlbiI6W3siaWQiOiJzdWItYmctMiIsInR5cGUiOiJpbWFnZSIsInNyYyI6Imh0dHBzOi8vcGljc3VtLnBob3Rvcy9zZWVkL3N1YnYtaW5uZXIyLzY0MC80ODAiLCJmaXQiOiJjb3ZlciIsInN0YXJ0IjowLCJlbmQiOjJ9XX1dfQ==",
|
|
38
|
+
"start": 0,
|
|
39
|
+
"end": 4
|
|
37
40
|
},
|
|
38
41
|
{
|
|
39
42
|
"id": "outro",
|
|
@@ -45,13 +48,15 @@
|
|
|
45
48
|
"type": "image",
|
|
46
49
|
"src": "https://picsum.photos/seed/subv-outro/640/480",
|
|
47
50
|
"fit": "cover",
|
|
48
|
-
"
|
|
51
|
+
"start": 0,
|
|
52
|
+
"end": 2
|
|
49
53
|
},
|
|
50
54
|
{
|
|
51
55
|
"id": "title-outro",
|
|
52
56
|
"type": "component",
|
|
53
57
|
"jsx": "<div style={{background:'#764ba2',color:'white',padding:'30px',borderRadius:'12px',textAlign:'center'}}><h2 style={{margin:0,fontSize:'28px'}}>Back to Main</h2></div>",
|
|
54
|
-
"
|
|
58
|
+
"start": 0,
|
|
59
|
+
"end": 2
|
|
55
60
|
}
|
|
56
61
|
]
|
|
57
62
|
}
|
|
@@ -8,17 +8,67 @@ layout:series subtitle:{fontSize:"20px"} width:1920 height:1080 fps:30 tts:"edge
|
|
|
8
8
|
~~~js imports
|
|
9
9
|
import ReactMarkdown from 'react-markdown';
|
|
10
10
|
import remarkGfm from 'remark-gfm'
|
|
11
|
+
import mermaid from 'mermaid'
|
|
12
|
+
import { delayRender, continueRender } from 'remotion'
|
|
13
|
+
import React from "react"
|
|
14
|
+
|
|
15
|
+
// Initialize mermaid once. Theme matches the slide deck's dark scheme.
|
|
16
|
+
mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose' })
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Renders a mermaid diagram as inline SVG.
|
|
20
|
+
* Uses Remotion's delayRender/continueRender so the diagram is guaranteed
|
|
21
|
+
* to be ready before the frame is captured.
|
|
22
|
+
*/
|
|
23
|
+
export function Mermaid({ source }) {
|
|
24
|
+
const ref = React.useRef(null)
|
|
25
|
+
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
if (!source || !ref.current) return
|
|
28
|
+
const handle = delayRender('Mermaid rendering')
|
|
29
|
+
mermaid.render('mmd-' + Math.random().toString(36).slice(2), source)
|
|
30
|
+
.then((result) => {
|
|
31
|
+
if (ref.current) ref.current.innerHTML = result.svg
|
|
32
|
+
continueRender(handle)
|
|
33
|
+
})
|
|
34
|
+
.catch((err) => {
|
|
35
|
+
console.error('Mermaid error:', err)
|
|
36
|
+
continueRender(handle)
|
|
37
|
+
})
|
|
38
|
+
}, [source])
|
|
11
39
|
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
ref={ref}
|
|
43
|
+
style={{ width: '100%', maxWidth: 960, margin: '20px auto' }}
|
|
44
|
+
/>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Courseware slide component.
|
|
50
|
+
* - Renders markdown via react-markdown
|
|
51
|
+
* - Highlights the Nth bullet when `current=N`
|
|
52
|
+
* - Renders mermaid code blocks ( ```mermaid ...``` ) as inline diagrams
|
|
53
|
+
*/
|
|
12
54
|
export function Slide({ current = 0, children }) {
|
|
13
|
-
let idx = 1
|
|
55
|
+
let idx = 1
|
|
14
56
|
return (
|
|
15
57
|
<div className="slide">
|
|
16
58
|
<ReactMarkdown remarkPlugins={[remarkGfm]}
|
|
17
59
|
components={{
|
|
18
60
|
li: ({ children }) => {
|
|
19
|
-
const highlight = idx === current; idx
|
|
20
|
-
return <li className={highlight ? 'highlight' : ''}>{children}</li
|
|
21
|
-
}
|
|
61
|
+
const highlight = idx === current; idx++
|
|
62
|
+
return <li className={highlight ? 'highlight' : ''}>{children}</li>
|
|
63
|
+
},
|
|
64
|
+
// Fenced code blocks — detect mermaid, render as diagram
|
|
65
|
+
pre: ({ children }) => {
|
|
66
|
+
const code = React.Children.toArray(children)[0]
|
|
67
|
+
if (code?.props?.className === 'language-mermaid') {
|
|
68
|
+
return <Mermaid source={String(code.props.children)} />
|
|
69
|
+
}
|
|
70
|
+
return <pre>{children}</pre>
|
|
71
|
+
},
|
|
22
72
|
}}>{children}</ReactMarkdown>
|
|
23
73
|
</div>
|
|
24
74
|
)
|
|
@@ -128,6 +178,13 @@ layout:parallel
|
|
|
128
178
|
Introduction to Machine Learning
|
|
129
179
|
|
|
130
180
|
**Dr. AI** | AI Introductory Course
|
|
181
|
+
```mermaid
|
|
182
|
+
graph TD;
|
|
183
|
+
A-->B;
|
|
184
|
+
A-->C;
|
|
185
|
+
B-->D;
|
|
186
|
+
C-->D;
|
|
187
|
+
```
|
|
131
188
|
~~~
|
|
132
189
|
~~~md zh-source
|
|
133
190
|
# 机器学习导论
|
|
@@ -15,12 +15,8 @@
|
|
|
15
15
|
"id": "demo-tween",
|
|
16
16
|
"type": "component",
|
|
17
17
|
"jsx": "<div style={{width:'100%',height:'100%',display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',background:'#1a1a2e',overflow:'hidden'}}><div style={{padding:'40px 60px',borderRadius:'20px',background:'linear-gradient(135deg,#667eea,#764ba2)',boxShadow:'0 8px 32px rgba(102,126,234,0.4)',opacity:tween(0,1),transform:'scale('+tween(0.3,1.2)+') rotate('+tween(-10,5)+'deg)'}}><span style={{color:'white',fontSize:tween(24,72)+'px',fontWeight:'bold',fontFamily:'sans-serif'}}>Hello Tween!</span></div><div style={{marginTop:'30px',opacity:tween(0,1),color:'rgba(255,255,255,0.6)',fontSize:'16px',fontFamily:'monospace'}}>frame: {useCurrentFrame()} / {useVideoConfig().durationInFrames}</div></div>",
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
"start": 0,
|
|
21
|
-
"end": 4
|
|
22
|
-
}
|
|
23
|
-
]
|
|
18
|
+
"start": 0,
|
|
19
|
+
"end": 4
|
|
24
20
|
}
|
|
25
21
|
]
|
|
26
22
|
}
|
|
@@ -20,14 +20,10 @@
|
|
|
20
20
|
"volume": 0,
|
|
21
21
|
"width": 640,
|
|
22
22
|
"height": 480,
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"startFrom": 2,
|
|
28
|
-
"endAt": 8
|
|
29
|
-
}
|
|
30
|
-
]
|
|
23
|
+
"start": 0,
|
|
24
|
+
"end": 6,
|
|
25
|
+
"startFrom": 2,
|
|
26
|
+
"endAt": 8
|
|
31
27
|
}
|
|
32
28
|
]
|
|
33
29
|
},
|
|
@@ -41,12 +37,8 @@
|
|
|
41
37
|
"type": "image",
|
|
42
38
|
"src": "https://picsum.photos/seed/video-series-photo/640/480",
|
|
43
39
|
"fit": "cover",
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
"start": 0,
|
|
47
|
-
"end": 3
|
|
48
|
-
}
|
|
49
|
-
]
|
|
40
|
+
"start": 0,
|
|
41
|
+
"end": 3
|
|
50
42
|
}
|
|
51
43
|
]
|
|
52
44
|
}
|
|
@@ -635,6 +635,24 @@ describe("edge-cases fixture", () => {
|
|
|
635
635
|
);
|
|
636
636
|
expect(compiled.type).toBe("root");
|
|
637
637
|
});
|
|
638
|
+
|
|
639
|
+
it("parses script code fence for longer text", () => {
|
|
640
|
+
const parsed = parseMarkdownDescriptive(fx.source);
|
|
641
|
+
const fenceScene = parsed.children.find((s: any) => s.name === "ScriptCodeFence");
|
|
642
|
+
expect(fenceScene).toBeDefined();
|
|
643
|
+
// First child: script with code fence content
|
|
644
|
+
expect(fenceScene!.children[0].type).toBe("audio");
|
|
645
|
+
expect(fenceScene!.children[0].script).toContain("longer narration");
|
|
646
|
+
expect(fenceScene!.children[0].script).toContain("multiple lines");
|
|
647
|
+
expect(fenceScene!.children[0].script).toContain("code fence");
|
|
648
|
+
// Second child: regular inline script
|
|
649
|
+
expect(fenceScene!.children[1].type).toBe("audio");
|
|
650
|
+
expect(fenceScene!.children[1].script).toBe("Inline text also works");
|
|
651
|
+
|
|
652
|
+
// Compile should succeed
|
|
653
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
654
|
+
expect(compiled.type).toBe("root");
|
|
655
|
+
});
|
|
638
656
|
});
|
|
639
657
|
|
|
640
658
|
// ── Full Feature ──────────────────────────────────────────────────────────
|
|
@@ -727,6 +745,158 @@ describe("component-imports fixture", () => {
|
|
|
727
745
|
});
|
|
728
746
|
});
|
|
729
747
|
|
|
748
|
+
// ── Dialogue fixture ──────────────────────────────────────────────────────
|
|
749
|
+
// Tests multi-turn dialogue expansion in script fields
|
|
750
|
+
|
|
751
|
+
describe("dialogue fixture", () => {
|
|
752
|
+
const fx = loadFixture("dialogue");
|
|
753
|
+
|
|
754
|
+
it("parses dialogue scenes with script field", () => {
|
|
755
|
+
const parsed = parseMarkdownDescriptive(fx.source);
|
|
756
|
+
const convScene = parsed.children.find((s: any) => s.name === "Conversation");
|
|
757
|
+
expect(convScene).toBeDefined();
|
|
758
|
+
expect(convScene.layout).toBe("series");
|
|
759
|
+
// The dialogue script is a single script node with multi-line content
|
|
760
|
+
const scriptNode = convScene.children.find((c: any) => c.type === "audio" && c.script);
|
|
761
|
+
expect(scriptNode).toBeDefined();
|
|
762
|
+
expect(scriptNode.script).toContain("Ray:");
|
|
763
|
+
expect(scriptNode.script).toContain("Alice:");
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it("parses single-line script (not dialogue)", () => {
|
|
767
|
+
const parsed = parseMarkdownDescriptive(fx.source);
|
|
768
|
+
const singleScene = parsed.children.find((s: any) => s.name === "SingleLine");
|
|
769
|
+
expect(singleScene).toBeDefined();
|
|
770
|
+
const scriptNode = singleScene.children.find((c: any) => c.type === "audio" && c.script);
|
|
771
|
+
expect(scriptNode).toBeDefined();
|
|
772
|
+
expect(scriptNode.script).toBe("This is just a single line of narration, not dialogue");
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// ── resolveDialogue unit tests ─────────────────────────────────────────────
|
|
777
|
+
|
|
778
|
+
import { resolveDialogue, parseDialogueLines } from "../src/descriptive/resolve";
|
|
779
|
+
|
|
780
|
+
describe("parseDialogueLines", () => {
|
|
781
|
+
it("detects multi-turn dialogue", () => {
|
|
782
|
+
const lines = parseDialogueLines("Ray: Hello\nAlice: Good day\nRay: Welcome");
|
|
783
|
+
expect(lines).toHaveLength(3);
|
|
784
|
+
expect(lines[0]).toEqual({ speaker: "Ray", text: "Hello" });
|
|
785
|
+
expect(lines[1]).toEqual({ speaker: "Alice", text: "Good day" });
|
|
786
|
+
expect(lines[2]).toEqual({ speaker: "Ray", text: "Welcome" });
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it("returns empty array for single line", () => {
|
|
790
|
+
expect(parseDialogueLines("Just a single line")).toEqual([]);
|
|
791
|
+
expect(parseDialogueLines("Ray: Hello")).toEqual([]);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it("returns empty array for non-matching format", () => {
|
|
795
|
+
expect(parseDialogueLines("not a dialogue line")).toEqual([]);
|
|
796
|
+
expect(parseDialogueLines("No colon here")).toEqual([]);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it("handles multi-word speaker names", () => {
|
|
800
|
+
const lines = parseDialogueLines("Dr Smith: Hello\nMs Johnson: Hi there");
|
|
801
|
+
expect(lines).toHaveLength(2);
|
|
802
|
+
expect(lines[0]).toEqual({ speaker: "Dr Smith", text: "Hello" });
|
|
803
|
+
expect(lines[1]).toEqual({ speaker: "Ms Johnson", text: "Hi there" });
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it("handles Chinese speaker names", () => {
|
|
807
|
+
const lines = parseDialogueLines("小王: 你好\n小李: 早上好");
|
|
808
|
+
expect(lines).toHaveLength(2);
|
|
809
|
+
expect(lines[0]).toEqual({ speaker: "小王", text: "你好" });
|
|
810
|
+
expect(lines[1]).toEqual({ speaker: "小李", text: "早上好" });
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it("trims whitespace", () => {
|
|
814
|
+
const lines = parseDialogueLines(" Ray: Hello \n Alice: Hi ");
|
|
815
|
+
expect(lines).toHaveLength(2);
|
|
816
|
+
expect(lines[0]).toEqual({ speaker: "Ray", text: "Hello" });
|
|
817
|
+
expect(lines[1]).toEqual({ speaker: "Alice", text: "Hi" });
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it("requires at least 2 matching lines", () => {
|
|
821
|
+
expect(parseDialogueLines("Ray: One\nNot a dialogue line")).toEqual([]);
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
describe("resolveDialogue", () => {
|
|
826
|
+
it("expands dialogue script into sequential audio nodes wrapped in series", () => {
|
|
827
|
+
const root = {
|
|
828
|
+
children: [
|
|
829
|
+
{
|
|
830
|
+
type: "scene",
|
|
831
|
+
name: "Test",
|
|
832
|
+
children: [
|
|
833
|
+
{
|
|
834
|
+
type: "audio",
|
|
835
|
+
script: "Ray: Hello\nAlice: Good day",
|
|
836
|
+
volume: 1,
|
|
837
|
+
},
|
|
838
|
+
],
|
|
839
|
+
},
|
|
840
|
+
],
|
|
841
|
+
} as any;
|
|
842
|
+
|
|
843
|
+
const result = resolveDialogue(root);
|
|
844
|
+
|
|
845
|
+
const scene = result.children[0];
|
|
846
|
+
// Original audio node should be replaced with a series container
|
|
847
|
+
expect(scene.children[0].type).toBe("series");
|
|
848
|
+
expect(scene.children[0].children).toHaveLength(2);
|
|
849
|
+
expect(scene.children[0].children[0].script).toBe("Hello");
|
|
850
|
+
expect(scene.children[0].children[0].speaker).toBe("Ray");
|
|
851
|
+
expect(scene.children[0].children[1].script).toBe("Good day");
|
|
852
|
+
expect(scene.children[0].children[1].speaker).toBe("Alice");
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it("does not modify single-line scripts", () => {
|
|
856
|
+
const root = {
|
|
857
|
+
children: [
|
|
858
|
+
{
|
|
859
|
+
type: "scene",
|
|
860
|
+
children: [
|
|
861
|
+
{ type: "audio", script: "Just a single line", volume: 1 },
|
|
862
|
+
],
|
|
863
|
+
},
|
|
864
|
+
],
|
|
865
|
+
} as any;
|
|
866
|
+
|
|
867
|
+
const result = resolveDialogue(root);
|
|
868
|
+
expect(result.children[0].children).toHaveLength(1);
|
|
869
|
+
expect(result.children[0].children[0].type).toBe("audio");
|
|
870
|
+
expect(result.children[0].children[0].script).toBe("Just a single line");
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
it("wraps dialogue lines in series without tts/voices", () => {
|
|
874
|
+
const root = {
|
|
875
|
+
tts: "edge-tts --voice default --text {input} --write-media {output}",
|
|
876
|
+
voices: { Ray: "--voice en-US-GuyNeural", Alice: "--voice en-US-JennyNeural" },
|
|
877
|
+
children: [
|
|
878
|
+
{
|
|
879
|
+
type: "scene",
|
|
880
|
+
children: [
|
|
881
|
+
{
|
|
882
|
+
type: "audio",
|
|
883
|
+
script: "Ray: Hello\nAlice: Good day",
|
|
884
|
+
volume: 1,
|
|
885
|
+
},
|
|
886
|
+
],
|
|
887
|
+
},
|
|
888
|
+
],
|
|
889
|
+
} as any;
|
|
890
|
+
|
|
891
|
+
const result = resolveDialogue(root);
|
|
892
|
+
const series = result.children[0].children[0];
|
|
893
|
+
expect(series.tts).toBeUndefined();
|
|
894
|
+
expect(series.voices).toBeUndefined();
|
|
895
|
+
expect(series.type).toBe("series");
|
|
896
|
+
expect(series.children).toHaveLength(2);
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
|
|
730
900
|
// ── Helper ────────────────────────────────────────────────────────────────
|
|
731
901
|
|
|
732
902
|
function findComponents(node: any): any[] {
|
package/tests/render.test.ts
CHANGED
|
@@ -492,14 +492,16 @@ describe("Multiple Aspect Ratios", () => {
|
|
|
492
492
|
type: "image",
|
|
493
493
|
src: "https://picsum.photos/seed/aspect-test/1920/1080",
|
|
494
494
|
fit: "cover",
|
|
495
|
-
|
|
495
|
+
start: 0,
|
|
496
|
+
end: 2,
|
|
496
497
|
},
|
|
497
498
|
{
|
|
498
499
|
id: "title",
|
|
499
500
|
type: "image",
|
|
500
501
|
src: "https://picsum.photos/seed/aspect-title/1920/1080",
|
|
501
502
|
fit: "cover",
|
|
502
|
-
|
|
503
|
+
start: 0.3,
|
|
504
|
+
end: 1.7,
|
|
503
505
|
},
|
|
504
506
|
],
|
|
505
507
|
},
|
|
@@ -547,21 +549,24 @@ describe("Cross-Stream Type Compatibility", () => {
|
|
|
547
549
|
src: "assets/bgm/ambient-loop.mp3",
|
|
548
550
|
volume: 0.2,
|
|
549
551
|
isBackground: true,
|
|
550
|
-
|
|
552
|
+
start: 0,
|
|
553
|
+
end: 3,
|
|
551
554
|
},
|
|
552
555
|
{
|
|
553
556
|
id: "img-test",
|
|
554
557
|
type: "image",
|
|
555
558
|
src: "https://picsum.photos/seed/cross1/640/480",
|
|
556
559
|
fit: "cover",
|
|
557
|
-
|
|
560
|
+
start: 0,
|
|
561
|
+
end: 3,
|
|
558
562
|
},
|
|
559
563
|
{
|
|
560
564
|
id: "sub-test",
|
|
561
565
|
type: "image",
|
|
562
566
|
src: "https://picsum.photos/seed/cross-sub/640/480",
|
|
563
567
|
fit: "cover",
|
|
564
|
-
|
|
568
|
+
start: 0.5,
|
|
569
|
+
end: 2.5,
|
|
565
570
|
},
|
|
566
571
|
],
|
|
567
572
|
},
|
|
@@ -609,10 +614,12 @@ describe("Cross-Stream Type Compatibility", () => {
|
|
|
609
614
|
type: "image",
|
|
610
615
|
src: "https://picsum.photos/seed/eff-child/640/480",
|
|
611
616
|
fit: "cover",
|
|
612
|
-
|
|
617
|
+
start: 0,
|
|
618
|
+
end: 2,
|
|
613
619
|
},
|
|
614
620
|
],
|
|
615
|
-
|
|
621
|
+
start: 0,
|
|
622
|
+
end: 2,
|
|
616
623
|
},
|
|
617
624
|
],
|
|
618
625
|
},
|
|
@@ -688,7 +695,8 @@ describe("Instruction Field", () => {
|
|
|
688
695
|
type: "image",
|
|
689
696
|
src: "https://picsum.photos/seed/desc/640/480",
|
|
690
697
|
instruction: "A beautiful sunset",
|
|
691
|
-
|
|
698
|
+
start: 0,
|
|
699
|
+
end: 3,
|
|
692
700
|
});
|
|
693
701
|
expect(withInst.instruction).toBe("A beautiful sunset");
|
|
694
702
|
|
|
@@ -696,7 +704,8 @@ describe("Instruction Field", () => {
|
|
|
696
704
|
id: "test-img-2",
|
|
697
705
|
type: "image",
|
|
698
706
|
src: "https://picsum.photos/seed/no-desc/640/480",
|
|
699
|
-
|
|
707
|
+
start: 0,
|
|
708
|
+
end: 3,
|
|
700
709
|
});
|
|
701
710
|
expect(withoutInst.instruction).toBeUndefined();
|
|
702
711
|
});
|
|
@@ -709,7 +718,8 @@ describe("Instruction Field", () => {
|
|
|
709
718
|
type: "video",
|
|
710
719
|
src: "test.mp4",
|
|
711
720
|
instruction: "label for video clip",
|
|
712
|
-
|
|
721
|
+
start: 0,
|
|
722
|
+
end: 5,
|
|
713
723
|
});
|
|
714
724
|
expect(v.instruction).toBe("label for video clip");
|
|
715
725
|
|
|
@@ -718,7 +728,8 @@ describe("Instruction Field", () => {
|
|
|
718
728
|
type: "component",
|
|
719
729
|
jsx: "<StatCounter value={100} />",
|
|
720
730
|
instruction: "stats component label",
|
|
721
|
-
|
|
731
|
+
start: 0,
|
|
732
|
+
end: 3,
|
|
722
733
|
});
|
|
723
734
|
expect(c.instruction).toBe("stats component label");
|
|
724
735
|
});
|
|
@@ -740,7 +751,8 @@ describe("Instruction Field", () => {
|
|
|
740
751
|
src: "https://picsum.photos/seed/desc-render/640/480",
|
|
741
752
|
instruction: "Preserved instruction",
|
|
742
753
|
fit: "cover",
|
|
743
|
-
|
|
754
|
+
start: 0,
|
|
755
|
+
end: 2,
|
|
744
756
|
},
|
|
745
757
|
],
|
|
746
758
|
},
|
|
@@ -793,7 +805,8 @@ describe("Scene as Folder Alias", () => {
|
|
|
793
805
|
type: "image",
|
|
794
806
|
src: "https://picsum.photos/seed/scene-alias-1/640/480",
|
|
795
807
|
fit: "cover",
|
|
796
|
-
|
|
808
|
+
start: 0,
|
|
809
|
+
end: 2,
|
|
797
810
|
},
|
|
798
811
|
],
|
|
799
812
|
},
|
|
@@ -808,7 +821,8 @@ describe("Scene as Folder Alias", () => {
|
|
|
808
821
|
type: "image",
|
|
809
822
|
src: "https://picsum.photos/seed/scene-alias-2/640/480",
|
|
810
823
|
fit: "cover",
|
|
811
|
-
|
|
824
|
+
start: 0,
|
|
825
|
+
end: 2,
|
|
812
826
|
},
|
|
813
827
|
],
|
|
814
828
|
},
|
|
@@ -865,7 +879,8 @@ describe("Scene as Folder Alias", () => {
|
|
|
865
879
|
type: "image",
|
|
866
880
|
src: "https://picsum.photos/seed/mix-scene/640/480",
|
|
867
881
|
fit: "cover",
|
|
868
|
-
|
|
882
|
+
start: 0,
|
|
883
|
+
end: 2,
|
|
869
884
|
},
|
|
870
885
|
],
|
|
871
886
|
},
|
|
@@ -879,7 +894,8 @@ describe("Scene as Folder Alias", () => {
|
|
|
879
894
|
type: "image",
|
|
880
895
|
src: "https://picsum.photos/seed/mix-folder/640/480",
|
|
881
896
|
fit: "cover",
|
|
882
|
-
|
|
897
|
+
start: 0,
|
|
898
|
+
end: 2,
|
|
883
899
|
},
|
|
884
900
|
],
|
|
885
901
|
},
|
package/tests/schema.test.ts
CHANGED
|
@@ -41,7 +41,8 @@ describe("component node", () => {
|
|
|
41
41
|
const c = component.parse({
|
|
42
42
|
type: "component",
|
|
43
43
|
jsx: "<BarChart data={[{value: 1}]} />",
|
|
44
|
-
|
|
44
|
+
start: 0,
|
|
45
|
+
end: 2,
|
|
45
46
|
});
|
|
46
47
|
expect(c.type).toBe("component");
|
|
47
48
|
expect(c.jsx).toContain("BarChart");
|
|
@@ -52,7 +53,8 @@ describe("component node", () => {
|
|
|
52
53
|
type: "component",
|
|
53
54
|
jsx: "<Hello name={who} />",
|
|
54
55
|
data: { who: "World" },
|
|
55
|
-
|
|
56
|
+
start: 0,
|
|
57
|
+
end: 1,
|
|
56
58
|
});
|
|
57
59
|
expect(c.data).toEqual({ who: "World" });
|
|
58
60
|
});
|
|
@@ -61,7 +63,8 @@ describe("component node", () => {
|
|
|
61
63
|
expect(() =>
|
|
62
64
|
component.parse({
|
|
63
65
|
type: "component",
|
|
64
|
-
|
|
66
|
+
start: 0,
|
|
67
|
+
end: 1,
|
|
65
68
|
}),
|
|
66
69
|
).toThrow();
|
|
67
70
|
});
|
package/tests/server.test.ts
CHANGED
|
@@ -48,7 +48,8 @@ function makeFixture(name: string): string {
|
|
|
48
48
|
type: "component",
|
|
49
49
|
id: `comp-${name}`,
|
|
50
50
|
jsx: `<div>${name}</div>`,
|
|
51
|
-
|
|
51
|
+
start: 0,
|
|
52
|
+
end: 3,
|
|
52
53
|
},
|
|
53
54
|
],
|
|
54
55
|
},
|
|
@@ -274,16 +275,18 @@ describe("HTML page variant injection", () => {
|
|
|
274
275
|
}
|
|
275
276
|
});
|
|
276
277
|
|
|
277
|
-
it("
|
|
278
|
-
const fixture = makeFixture("html-
|
|
278
|
+
it("sets mode and variant in globals for client-side rendering", async () => {
|
|
279
|
+
const fixture = makeFixture("html-mode");
|
|
279
280
|
const port = nextPort();
|
|
280
281
|
const stop = await startServer(fixture, ["default", "zh-tiktok", "youtube"], port);
|
|
281
282
|
try {
|
|
282
283
|
const res = await fetchUrl(`http://localhost:${port}/`);
|
|
283
284
|
expect(res.status).toBe(200);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
expect(res.text).toContain('
|
|
285
|
+
// Variant bar is now rendered client-side by React (VariantBar.tsx).
|
|
286
|
+
// Server only provides the config globals and loads player.js.
|
|
287
|
+
expect(res.text).toContain('window.VARIANT = "default"');
|
|
288
|
+
expect(res.text).toContain('window.MODE = "preview"');
|
|
289
|
+
expect(res.text).toContain('src="/player.js"');
|
|
287
290
|
} finally {
|
|
288
291
|
stop();
|
|
289
292
|
}
|
package/tests/utils.ts
CHANGED
|
@@ -376,10 +376,10 @@ export function getExpectedDuration(fixtureData: any): number {
|
|
|
376
376
|
}
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
-
// Leaf
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
379
|
+
// Leaf duration from base timing fields (end is source of truth)
|
|
380
|
+
const leafStart = fixtureData.start ?? 0;
|
|
381
|
+
if (typeof fixtureData.end === "number") return fixtureData.end;
|
|
382
|
+
if (typeof fixtureData.duration === "number") return leafStart + fixtureData.duration;
|
|
383
383
|
|
|
384
384
|
return 1;
|
|
385
385
|
}
|