@lalalic/markcut 1.2.0 → 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 -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 +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/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 +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
package/AGENTS.md
CHANGED
|
@@ -63,8 +63,12 @@ src/
|
|
|
63
63
|
├── render/cli.mjs → CLI entry
|
|
64
64
|
├── render/tts.ts → TTS via CLI template + variable substitution
|
|
65
65
|
├── player/pipeline.mjs → Bundled pipeline (server imports this)
|
|
66
|
-
├── player/server.mjs → --edit
|
|
67
|
-
├── player/
|
|
66
|
+
├── player/server.mjs → Unified server (--edit, --label, preview)
|
|
67
|
+
├── player/components/ → React control components (edit, label, variant bar)
|
|
68
|
+
│ ├── index.ts
|
|
69
|
+
│ ├── EditControls.tsx
|
|
70
|
+
│ ├── LabelControls.tsx
|
|
71
|
+
│ └── VariantBar.tsx
|
|
68
72
|
└── tests/ → Vitest integration tests
|
|
69
73
|
```
|
|
70
74
|
|
|
@@ -107,22 +111,22 @@ The tree is validated by Zod and rendered by Remotion via recursive React compon
|
|
|
107
111
|
| **Series** (`isSeries:true`) | Children play sequentially. Optional `transition` (fade/slide/wipe/flip/clockWipe) + `transitionTime` (default 0.5s). |
|
|
108
112
|
| **Parallel** (`isSeries:false`) | Children play simultaneously. Duration = max child duration. |
|
|
109
113
|
| **Background** (`isBackground:true`) | Loops for parent duration. Excluded from duration calculation. |
|
|
110
|
-
| **
|
|
114
|
+
| **Timing** | Every leaf carries base fields `start`/`end` (seconds, relative to parent) + optional `startFrom`/`endAt` (source trim) + `duration` (convenience). `end` is the source of truth; `duration` normalizes to `end = start + duration` when `end` is absent. Inside series parents `start` is 0 (positioning is implicit). |
|
|
111
115
|
| **Scene** (`type:"scene"`) | UI storyboard card; engine treats as folder. Has `name` + `description` + `script`. |
|
|
112
116
|
|
|
113
117
|
### Duration Calculation
|
|
114
118
|
|
|
115
119
|
Computed by `getDurationInSeconds()` in `src/utils/index.ts`:
|
|
116
|
-
- **Leaf nodes**: duration =
|
|
120
|
+
- **Leaf nodes**: duration = `leafEnd(stream)` = `end ?? (start + duration)` (single deterministic read; `start`/`end`/`duration` live on the base node)
|
|
117
121
|
- **Series**: sum of children durations minus transition overlaps
|
|
118
122
|
- **Parallel**: max of children durations
|
|
119
123
|
- **Background children**: excluded from calculation
|
|
120
|
-
- **Include with `src`**: treated as leaf (duration from
|
|
124
|
+
- **Include with `src`**: treated as leaf (duration from base `end`)
|
|
121
125
|
- Sets `durationInSeconds` on every node as a side effect
|
|
122
126
|
|
|
123
127
|
### 12 Stream Types
|
|
124
128
|
|
|
125
|
-
All types share base fields from `BaseShape`: `id`, `name`, `title`, `description`, `script`, `src`, `style`, `visible`, `isBackground`, `durationInSeconds`.
|
|
129
|
+
All types share base fields from `BaseShape`: `id`, `name`, `title`, `description`, `script`, `src`, `style`, `visible`, `isBackground`, `start`, `end`, `duration`, `startFrom`, `endAt`, `durationInSeconds`.
|
|
126
130
|
|
|
127
131
|
| Type | Purpose | Key Fields |
|
|
128
132
|
|------|---------|------------|
|
|
@@ -130,8 +134,8 @@ All types share base fields from `BaseShape`: `id`, `name`, `title`, `descriptio
|
|
|
130
134
|
| `folder` | Group children | `isSeries`, `transition`, `children[]` |
|
|
131
135
|
| `scene` | Storyboard node (alias for folder) | `name`, `description`, `script`, `children[]` |
|
|
132
136
|
| `image` | Still photo | `src`, `fit` (contain/cover/fill) |
|
|
133
|
-
| `video` | Video clip | `src`, `volume`, `playbackRate` |
|
|
134
|
-
| `audio` | Soundtrack/SFX | `src`, `volume`, `foreground` (ducks parent) |
|
|
137
|
+
| `video` | Video clip | `src`, `volume`, `playbackRate`, `loop` |
|
|
138
|
+
| `audio` | Soundtrack/SFX | `src`, `volume`, `foreground` (ducks parent), `loop` |
|
|
135
139
|
| `subtitle` | Text overlay | `src` (text/VTT) or `cues[]`, `fontSize`, `style` |
|
|
136
140
|
| `component` | React component | `componentName`, `props`, `src` (remote ESM) |
|
|
137
141
|
| `effect` | CSS animation wrapper | `animation`, `customKeyframes`, `children[]` |
|
|
@@ -155,7 +159,7 @@ All types share base fields from `BaseShape`: `id`, `name`, `title`, `descriptio
|
|
|
155
159
|
### Video.tsx — startFrom/endAt Trimming
|
|
156
160
|
|
|
157
161
|
- Uses Remotion `<OffthreadVideo>` for CPU-efficient video decoding
|
|
158
|
-
- `startFrom` / `endAt` on
|
|
162
|
+
- `startFrom` / `endAt` on the base node trim the source video (in seconds)
|
|
159
163
|
- `playbackRate` is capped at 1 (`Math.min(1, ...)`) so source longer than timeline plays at normal speed and is truncated by the Sequence
|
|
160
164
|
- **Critical formula**: `OffthreadVideo endAt = startFrom_frames + ((endAt_source - startFrom_source) * fps / playbackRate)`
|
|
161
165
|
This sets `endAt` beyond the actual source end when `playbackRate < 1`, which prevents Remotion from rendering blank frames before the Sequence ends.
|
|
@@ -183,7 +187,7 @@ node src/render/cli.mjs templates
|
|
|
183
187
|
- `--edit` flag starts a live-reload loop: edit JSON → player auto-refreshes
|
|
184
188
|
- `--label` flag starts a labeling UI for selecting and annotating media
|
|
185
189
|
- `--port` flag sets the server port (default 3001)
|
|
186
|
-
-
|
|
190
|
+
- Unified server: `server.mjs` serves all modes (preview, edit, label). React control components in `components/`.
|
|
187
191
|
|
|
188
192
|
## Testing
|
|
189
193
|
|
|
@@ -235,13 +239,13 @@ npx vitest run tests/render.test.ts -t "Test Name" # single test
|
|
|
235
239
|
## Known Issues & Gotchas
|
|
236
240
|
|
|
237
241
|
1. **TransitionSeries + isBackground**: `TransitionSeries` from `@remotion/transitions` rejects non-Sequence/Transition children. Don't mix `isBackground:true` children with `isSeries:true, transition:"..."` at the same level.
|
|
238
|
-
2. **
|
|
239
|
-
3.
|
|
240
|
-
4.
|
|
241
|
-
5. **
|
|
242
|
-
6. **
|
|
243
|
-
7. **
|
|
244
|
-
8. **
|
|
242
|
+
2. **Video endAt formula**: Must be `startFrom_frames + ((endAt_source - startFrom_source) * fps / playbackRate)` — changing to `endAt_source * fps` breaks slow-motion playback.
|
|
243
|
+
3. **`signalstats` ffmpeg filter**: Doesn't output YAVG on all systems; use file size as proxy for visual content.
|
|
244
|
+
4. **Asset paths**: Local files are resolved via `staticFile()` — files go in `public/` and are referenced without the `public/` prefix.
|
|
245
|
+
5. **Map rendering**: Depends on external tile availability; may produce blank frames offline.
|
|
246
|
+
6. **CLI md render needs three things preview gets for free**: (a) `parseMarkdownVariants(raw)` (returns `{base, variants}`) — `parseMarkdownDescriptive` returns the root directly; (b) bundling the ` ```js imports ``` ` block via `bundleFromEntries` and setting `root.imports` (otherwise jsx components are unregistered → slides render as unstyled black frames); (c) staging `.markcut/` assets into `public/.render-assets/` and rewriting srcs to relative paths (remotion render only serves its publicDir). All three live in `src/render/cli.mjs`; the import-map shim for shared react/remotion instances is `src/utils/component-import-map.ts` (must stay in sync with `player/browser.tsx` + `bundler.mjs getSharedExternals`).
|
|
247
|
+
7. **Publishing**: verify `npx @lalalic/markcut render <md>` from a clean temp dir before/after publish — v1.1.1 shipped with cli.mjs/pipeline.mjs out of sync and md rendering was completely broken. Also beware stale npx caches (`~/.npm/_npx`) silently running old code.
|
|
248
|
+
8. **Clean break from `actions[]`**: leaf timing is now flat base fields (`start`/`end`/`duration`/`startFrom`/`endAt`). Old compiled JSON carrying `actions:[{...}]` no longer parses into a duration — re-compile from markdown/descriptive JSON, or migrate `actions[0]` → flat fields (drop `id`/`volume`/`style`/`effectId`; keep `start`/`end`/`startFrom`/`endAt`/`loop`).
|
|
245
249
|
|
|
246
250
|
## 3-Level Authoring Workflow (see SKILL.md)
|
|
247
251
|
|
|
@@ -261,6 +265,24 @@ Every node carries an optional `script` field for narration/dialogue text. The p
|
|
|
261
265
|
|
|
262
266
|
This separates concerns: `script` = authoring text (human/agent-friendly), `subtitle` = rendered output (timed VTT cues).
|
|
263
267
|
|
|
268
|
+
### Multi-turn Dialogue
|
|
269
|
+
|
|
270
|
+
When `script` text contains multiple `SpeakerName: text` lines (2+), the pipeline auto-expands it into a multi-turn dialogue. Each line becomes a separate audio node with `speaker` field, wrapped in a `series` container for sequential playback.
|
|
271
|
+
|
|
272
|
+
```md
|
|
273
|
+
- script "Ray: Hello everyone
|
|
274
|
+
Alice: Good day to you
|
|
275
|
+
Ray: Let's begin"
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Per-speaker voices are configured via `voices` on root. Each value is extra CLI flags appended to the TTS template:
|
|
279
|
+
|
|
280
|
+
```md
|
|
281
|
+
voices:{"Ray":"--voice en-US-GuyNeural","Alice":"--voice en-US-JennyNeural"}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The speaker name is matched against `voices` map and the extra flags are appended to the TTS CLI template, naturally supporting voice cloning or edge-tts features (rate, pitch, etc.). Subtitles include the speaker prefix (`Ray: Hello everyone`).
|
|
285
|
+
|
|
264
286
|
## Dependencies
|
|
265
287
|
|
|
266
288
|
- **Remotion 4.0.469** — core rendering engine
|
package/README.md
CHANGED
|
@@ -201,10 +201,14 @@ src/
|
|
|
201
201
|
│ ├── cli.mjs ← CLI entry point
|
|
202
202
|
│ └── tts.ts ← TTS via CLI template + variable substitution
|
|
203
203
|
├── player/
|
|
204
|
-
│ ├── server.mjs ← --edit
|
|
205
|
-
│ ├──
|
|
204
|
+
│ ├── server.mjs ← Unified server (--edit, --label, preview)
|
|
205
|
+
│ ├── components/ ← React control components for all modes
|
|
206
|
+
│ │ ├── index.ts
|
|
207
|
+
│ │ ├── EditControls.tsx
|
|
208
|
+
│ │ ├── LabelControls.tsx
|
|
209
|
+
│ │ └── VariantBar.tsx
|
|
206
210
|
│ ├── pipeline.ts ← Pipeline entry (bundled to pipeline.mjs)
|
|
207
|
-
│ └── browser.tsx ← Browser player (bundled
|
|
211
|
+
│ └── browser.tsx ← Browser player (React, bundled to player.js)
|
|
208
212
|
├── utils/ ← Duration calc, VTT parser, helpers
|
|
209
213
|
└── tests/ ← Vitest integration tests
|
|
210
214
|
```
|
package/SKILL.md
CHANGED
|
@@ -58,8 +58,6 @@ some common issues (photo or video can't be displayed, audio missing), take belo
|
|
|
58
58
|
- `remark-toc` — generate table of contents
|
|
59
59
|
- `remark-math` — support math formulas
|
|
60
60
|
- `rehype-katex` — render math formulas with KaTeX
|
|
61
|
-
- `remark-mermaidjs` — render mermaid diagrams in
|
|
62
|
-
- `react-markdown-mermaid` — render mermaid diagrams in standalone mode (no need to install mermaid separately)
|
|
63
61
|
|
|
64
62
|
- `@remotion/shapes` — render shapes like arrows, circles, rectangles, etc
|
|
65
63
|
- `@remotion/starburst` — render starburst animations
|
package/docs/edit-mode.md
CHANGED
|
@@ -177,7 +177,7 @@ The tree description walks the JSON recursively, showing:
|
|
|
177
177
|
- **Nesting depth** via indentation
|
|
178
178
|
- **Type** and **id/name** of each node
|
|
179
179
|
- **Key fields** (dimensions, fps, theme, series/parallel, transitions)
|
|
180
|
-
- **Timing** from
|
|
180
|
+
- **Timing** from leaf span (`[start→end]`)
|
|
181
181
|
- **isBackground** flag (`[bg]`)
|
|
182
182
|
- **Component props** and node-specific fields (fontSize, fit, style, volume, etc.)
|
|
183
183
|
|
package/docs/label-mode.md
CHANGED
|
@@ -36,7 +36,7 @@ Labels are saved to the source file.
|
|
|
36
36
|
"id": "scene-1-media",
|
|
37
37
|
"type": "image",
|
|
38
38
|
"src": "/media/photo.jpg",
|
|
39
|
-
"
|
|
39
|
+
"start": 0, "end": 5,
|
|
40
40
|
"description": "Best group shot from the campfire"
|
|
41
41
|
}
|
|
42
42
|
]
|
|
@@ -49,7 +49,7 @@ Labels are saved to the source file.
|
|
|
49
49
|
"id": "scene-2-media",
|
|
50
50
|
"type": "video",
|
|
51
51
|
"src": "/media/clip.mp4",
|
|
52
|
-
"
|
|
52
|
+
"start": 0, "end": 5,
|
|
53
53
|
"description": "Sunset timelapse — use for intro"
|
|
54
54
|
}
|
|
55
55
|
]
|
|
@@ -94,12 +94,14 @@ Stream tree JSON (labels.json)
|
|
|
94
94
|
|
|
95
95
|
## Implementation
|
|
96
96
|
|
|
97
|
-
Source: `src/player/
|
|
97
|
+
Source: `src/player/server.mjs` (unified server, use `--label` flag)
|
|
98
|
+
UI controls: `src/player/components/LabelControls.tsx` (React component)
|
|
98
99
|
|
|
99
100
|
- Built on Node.js `http` module (no Express dependency)
|
|
100
101
|
- Serves bundled `player.js` (esbuild output from `src/player/browser.tsx`)
|
|
101
102
|
- Labels saved atomically to disk on each POST
|
|
102
|
-
-
|
|
103
|
+
- Supports stream tree JSON, descriptive JSON, and markdown input
|
|
104
|
+
- React components in `src/player/components/` replace the old HTML-based `ui/` directory
|
|
103
105
|
|
|
104
106
|
## Reference
|
|
105
107
|
|
|
@@ -174,7 +174,9 @@ For compatibility, `import { Name } from "spec"` also works and produces the sam
|
|
|
174
174
|
| `title` | display title | scene |
|
|
175
175
|
| `instruction` | visual intent / style / any prompt; NOT rendered | any |
|
|
176
176
|
| `script` | narration/dialogue text; TTS source; NOT rendered directly | audio | Only on audio nodes — see Narration section below |
|
|
177
|
-
| `
|
|
177
|
+
| `speaker` | speaker name for multi-turn dialogue; set automatically by dialogue expansion | audio | Used for per-speaker voice lookup and subtitle prefix |
|
|
178
|
+
| `voices` | JSON object mapping speaker names to extra TTS CLI flags e.g. `{"Ray":"--voice en-US-GuyNeural"}` | root | Flags are appended to the resolved TTS CLI template |
|
|
179
|
+
| `tts` | CLI template string e.g. `"edge-tts --voice en-US-GuyNeural --text {input} --write-media {output}"` | root | Set once at the root level; all TTS uses this template |
|
|
178
180
|
| `metadata` | arbitrary metadata string | root |
|
|
179
181
|
| `stylesheet` | global CSS string; selectors use `.className` on elements | root |
|
|
180
182
|
| `style` | inline CSS applied to the node's container div e.g. `"border-radius:12px"` | any |
|
|
@@ -254,6 +256,37 @@ For longer text, use an indented code fence:
|
|
|
254
256
|
|
|
255
257
|
All three patterns produce an `audio` node with a `script` field. The pipeline's TTS resolver (`resolveScripts`) picks up these nodes, generates speech audio files, and sets the `src` field to the output path. The `script` field is consumed by the resolver and is not present in the compiled stream tree.
|
|
256
258
|
|
|
259
|
+
#### Multi-turn Dialogue
|
|
260
|
+
|
|
261
|
+
When a `script` contains multiple lines matching `SpeakerName: text` format, the pipeline automatically expands it into a multi-turn dialogue where each line becomes a separate audio node with its own TTS generation. This allows different speakers to have different voices.
|
|
262
|
+
|
|
263
|
+
```md
|
|
264
|
+
- script "Ray: Hello everyone and welcome
|
|
265
|
+
Alice: Good day to you all
|
|
266
|
+
Ray: Let's get started"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The dialogue lines play sequentially. Each line is transcribed separately, and subtitles include the speaker prefix (e.g., `Ray: Hello everyone and welcome`).
|
|
270
|
+
|
|
271
|
+
##### Per-Speaker Voices
|
|
272
|
+
|
|
273
|
+
Configure different TTS voices for each speaker via the root `voices` config. Each value is **extra CLI flags** appended to the TTS template:
|
|
274
|
+
|
|
275
|
+
```md
|
|
276
|
+
# video
|
|
277
|
+
voices:{"Ray":"--voice en-US-GuyNeural","Alice":"--voice en-US-JennyNeural"}
|
|
278
|
+
- script "Ray: Tell us about your project
|
|
279
|
+
Alice: I'm working on something exciting"
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Since values are raw CLI flags, they naturally support voice cloning, rate, pitch, and any edge-tts feature:
|
|
283
|
+
|
|
284
|
+
```md
|
|
285
|
+
voices:{"Ray":"--voice en-US-GuyNeural --rate +20%","Clone":"--voice clone-xxx --pitch +5Hz"}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The final TTS command for a speaker: `<root tts cli> <speaker voice flags>`.
|
|
289
|
+
|
|
257
290
|
### `subtitle` (root-level overlay)
|
|
258
291
|
|
|
259
292
|
Subtitles are configured at the root level as a VTT overlay, not as tree nodes. Set via YAML frontmatter `subtitle:` or `root.subtitle` in JSON.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
You are a video director editing a markdown-descriptive (.md) markcut video project.
|
|
2
|
+
|
|
3
|
+
The markcut engine compiles markdown descriptions into rendered videos with TTS narration, transitions, and media generation.
|
|
4
|
+
|
|
5
|
+
The file you are editing is: ${fileName}
|
|
6
|
+
Full path: ${filePath}
|
|
7
|
+
|
|
8
|
+
--- Markdown-Descriptive Format Reference ---
|
|
9
|
+
@{docs/markdown-descriptive.md}
|
|
10
|
+
|
|
11
|
+
TASKS:
|
|
12
|
+
1. Read the .md file directly using your file tools to see its current content
|
|
13
|
+
2. Edit the .md file directly — change content, add/remove sections, update frontmatter
|
|
14
|
+
3. Save the changes to the .md file using your write/edit tools
|
|
15
|
+
4. Output ONLY a one-line summary of what specific change you made
|
|
16
|
+
5. Do NOT output explanations, reasoning, or extra text
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -37,6 +37,8 @@ export const DEFAULT_ITT_CLI =
|
|
|
37
37
|
/**
|
|
38
38
|
* Seconds between sampled video frames when using the default ITT-based VTT.
|
|
39
39
|
*/
|
|
40
|
+
export const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY || "";
|
|
41
|
+
|
|
40
42
|
export const DEFAULT_VTT_SAMPLE_INTERVAL = Number(process.env.MARKCUT_VTT_SAMPLE_INTERVAL) || 5;
|
|
41
43
|
|
|
42
44
|
/**
|
|
@@ -73,11 +75,14 @@ export const DEFAULT_TTS_CLI =
|
|
|
73
75
|
|
|
74
76
|
/**
|
|
75
77
|
* Default agent CLI — general-purpose text LLM.
|
|
76
|
-
* Used for text-only tasks like detect-scenes.
|
|
77
|
-
* Placeholders:
|
|
78
|
+
* Used for text-only tasks like detect-scenes and editing.
|
|
79
|
+
* Placeholders:
|
|
80
|
+
* {systemprompt} = system prompt (role, instructions, knowledge)
|
|
81
|
+
* {prompt} = user prompt (current context + edit request)
|
|
82
|
+
* {sessionid} = unique session ID for conversation continuity
|
|
78
83
|
* Override with MARKCUT_AGENT_CLI env var.
|
|
79
84
|
*/
|
|
80
|
-
export const DEFAULT_AGENT_CLI = process.env.MARKCUT_AGENT_CLI || 'npx pi -p {prompt}';
|
|
85
|
+
export const DEFAULT_AGENT_CLI = process.env.MARKCUT_AGENT_CLI || 'npx pi --session-id {sessionid} --system-prompt {systemprompt} -p {prompt}';
|
|
81
86
|
|
|
82
87
|
// ── Render-only pipeline CLI templates ─────────────────────────────────────
|
|
83
88
|
// These are specific to the render pipeline (no vision pipeline equivalent).
|
|
@@ -132,7 +132,7 @@ export { BarChart } from "npm:recharts"`,
|
|
|
132
132
|
});
|
|
133
133
|
|
|
134
134
|
describe("compileDescriptiveRoot", () => {
|
|
135
|
-
it("compiles simple series into
|
|
135
|
+
it("compiles simple series into flat start/end timing", () => {
|
|
136
136
|
const compiled = compileDescriptiveRoot({
|
|
137
137
|
width: 640,
|
|
138
138
|
height: 480,
|
|
@@ -149,13 +149,13 @@ describe("compileDescriptiveRoot", () => {
|
|
|
149
149
|
expect(compiled.children).toHaveLength(2);
|
|
150
150
|
|
|
151
151
|
const first = compiled.children[0] as any;
|
|
152
|
-
expect(first.
|
|
153
|
-
expect(first.
|
|
152
|
+
expect(first.start).toBe(0);
|
|
153
|
+
expect(first.end).toBe(2);
|
|
154
154
|
|
|
155
155
|
const second = compiled.children[1] as any;
|
|
156
|
-
expect(second.
|
|
157
|
-
expect(second.
|
|
158
|
-
expect(second.
|
|
156
|
+
expect(second.startFrom).toBe(1);
|
|
157
|
+
expect(second.endAt).toBe(4);
|
|
158
|
+
expect(second.end).toBe(3);
|
|
159
159
|
});
|
|
160
160
|
|
|
161
161
|
it("allows explicit start in parallel root", () => {
|
|
@@ -170,8 +170,8 @@ describe("compileDescriptiveRoot", () => {
|
|
|
170
170
|
expect(compiled.isSeries).toBe(false);
|
|
171
171
|
expect(compiled.durationInSeconds).toBe(3);
|
|
172
172
|
const first = compiled.children[0] as any;
|
|
173
|
-
expect(first.
|
|
174
|
-
expect(first.
|
|
173
|
+
expect(first.start).toBe(1);
|
|
174
|
+
expect(first.end).toBe(3);
|
|
175
175
|
});
|
|
176
176
|
|
|
177
177
|
it("warns on duplicate ids", () => {
|
|
@@ -253,8 +253,8 @@ describe("compileDescriptiveRoot", () => {
|
|
|
253
253
|
|
|
254
254
|
const includeWithSrc = withSrc.children[0] as any;
|
|
255
255
|
expect(includeWithSrc.type).toBe("include");
|
|
256
|
-
expect(includeWithSrc.
|
|
257
|
-
expect(includeWithSrc.
|
|
256
|
+
expect(includeWithSrc.start).toBe(0);
|
|
257
|
+
expect(includeWithSrc.end).toBe(4);
|
|
258
258
|
|
|
259
259
|
const withChildren = compileDescriptiveRoot({
|
|
260
260
|
layout: "series",
|
|
@@ -273,7 +273,7 @@ describe("compileDescriptiveRoot", () => {
|
|
|
273
273
|
const includeInline = withChildren.children[0] as any;
|
|
274
274
|
expect(includeInline.type).toBe("include");
|
|
275
275
|
expect(includeInline.children).toHaveLength(2);
|
|
276
|
-
expect(includeInline.
|
|
276
|
+
expect(includeInline.end).toBe(3);
|
|
277
277
|
});
|
|
278
278
|
|
|
279
279
|
it("covers remaining types: effect (via effects:[]) and map", () => {
|
|
@@ -304,13 +304,13 @@ describe("compileDescriptiveRoot", () => {
|
|
|
304
304
|
// effects:[] on image compiles into Effect wrapper stream
|
|
305
305
|
const effect = compiled.children[0] as any;
|
|
306
306
|
expect(effect.type).toBe("effect");
|
|
307
|
-
expect(effect.
|
|
308
|
-
expect(effect.
|
|
307
|
+
expect(effect.start).toBe(0);
|
|
308
|
+
expect(effect.end).toBe(2);
|
|
309
309
|
expect(effect.children).toHaveLength(1);
|
|
310
310
|
|
|
311
311
|
const map = compiled.children.find((c: any) => c.id === "map-1") as any;
|
|
312
312
|
expect(map.type).toBe("map");
|
|
313
|
-
expect(map.
|
|
313
|
+
expect(map.end).toBe(4);
|
|
314
314
|
expect(map.waypoints).toHaveLength(2);
|
|
315
315
|
expect(map.routeMarker).toBe("🚲");
|
|
316
316
|
expect(map.travelMode).toBe("BICYCLING");
|
|
@@ -339,11 +339,11 @@ describe("compileDescriptiveRoot", () => {
|
|
|
339
339
|
expect(fx.children[0].type).toBe("image");
|
|
340
340
|
expect(fx.children[0].src).toBe("hero.jpg");
|
|
341
341
|
// Effect action spans leaf duration
|
|
342
|
-
expect(fx.
|
|
343
|
-
expect(fx.
|
|
342
|
+
expect(fx.start).toBe(0);
|
|
343
|
+
expect(fx.end).toBe(3);
|
|
344
344
|
// Inner leaf action is relative (start=0)
|
|
345
|
-
expect(fx.children[0].
|
|
346
|
-
expect(fx.children[0].
|
|
345
|
+
expect(fx.children[0].start).toBe(0);
|
|
346
|
+
expect(fx.children[0].end).toBe(3);
|
|
347
347
|
});
|
|
348
348
|
|
|
349
349
|
it("compiles leaf node with effects as object specs", () => {
|
|
@@ -371,11 +371,11 @@ describe("compileDescriptiveRoot", () => {
|
|
|
371
371
|
expect(fx.animationTimingFunction).toBe("ease-out");
|
|
372
372
|
expect(fx.animationIterationCount).toBe(2);
|
|
373
373
|
// Outermost effect carries the absolute timing from parallel layout
|
|
374
|
-
expect(fx.
|
|
375
|
-
expect(fx.
|
|
374
|
+
expect(fx.start).toBe(1);
|
|
375
|
+
expect(fx.end).toBe(3);
|
|
376
376
|
// Inner leaf timing is relative
|
|
377
|
-
expect(fx.children[0].
|
|
378
|
-
expect(fx.children[0].
|
|
377
|
+
expect(fx.children[0].start).toBe(0);
|
|
378
|
+
expect(fx.children[0].end).toBe(2);
|
|
379
379
|
});
|
|
380
380
|
|
|
381
381
|
it("compiles effects on container (parallel) nodes", () => {
|
|
@@ -471,9 +471,9 @@ describe("compileDescriptiveRoot", () => {
|
|
|
471
471
|
expect(fx.animationTimingFunction).toBe("ease-out");
|
|
472
472
|
expect(fx.animationIterationCount).toBe(3);
|
|
473
473
|
// Effect duration from param overrides to 2s
|
|
474
|
-
expect(fx.
|
|
474
|
+
expect(fx.end - fx.start).toBe(2);
|
|
475
475
|
// Inner leaf still plays full 5s
|
|
476
|
-
expect(fx.children[0].
|
|
476
|
+
expect(fx.children[0].end - fx.children[0].start).toBe(5);
|
|
477
477
|
});
|
|
478
478
|
|
|
479
479
|
it("compiles effect with comma-separated positional params (duration, timingFunction, iterationCount)", () => {
|
|
@@ -495,7 +495,7 @@ describe("compileDescriptiveRoot", () => {
|
|
|
495
495
|
expect(fx.animation).toBe("fadeIn");
|
|
496
496
|
expect(fx.animationTimingFunction).toBe("ease-in");
|
|
497
497
|
expect(fx.animationIterationCount).toBe(3);
|
|
498
|
-
expect(fx.
|
|
498
|
+
expect(fx.end - fx.start).toBe(2);
|
|
499
499
|
});
|
|
500
500
|
|
|
501
501
|
it("compiles effect with positional params — duration only", () => {
|
|
@@ -515,7 +515,7 @@ describe("compileDescriptiveRoot", () => {
|
|
|
515
515
|
const fx = compiled.children[0] as any;
|
|
516
516
|
expect(fx.type).toBe("effect");
|
|
517
517
|
expect(fx.animation).toBe("fadeIn");
|
|
518
|
-
expect(fx.
|
|
518
|
+
expect(fx.end - fx.start).toBe(1.5);
|
|
519
519
|
});
|
|
520
520
|
|
|
521
521
|
it("compiles effect with partial positional params — duration + timingFunction", () => {
|
|
@@ -536,7 +536,7 @@ describe("compileDescriptiveRoot", () => {
|
|
|
536
536
|
expect(fx.animation).toBe("fadeIn");
|
|
537
537
|
expect(fx.animationTimingFunction).toBe("ease-out");
|
|
538
538
|
expect(fx.animationIterationCount).toBe(1);
|
|
539
|
-
expect(fx.
|
|
539
|
+
expect(fx.end - fx.start).toBe(2.5);
|
|
540
540
|
});
|
|
541
541
|
|
|
542
542
|
it("supports deep nested layout containers across all modes", () => {
|
|
@@ -726,15 +726,15 @@ describe("compileDescriptiveRoot", () => {
|
|
|
726
726
|
expect(rhythm.children).toHaveLength(4);
|
|
727
727
|
|
|
728
728
|
// child[0] starts at beat 0.5, ends at beat 1.5 => duration 1
|
|
729
|
-
expect(rhythm.children[0].
|
|
730
|
-
expect(rhythm.children[0].
|
|
729
|
+
expect(rhythm.children[0].start).toBe(0.5);
|
|
730
|
+
expect(rhythm.children[0].end).toBe(1.5);
|
|
731
731
|
|
|
732
732
|
// child[1] starts at beat 1.5, ends at beat 2.5 => duration 1
|
|
733
|
-
expect(rhythm.children[1].
|
|
734
|
-
expect(rhythm.children[1].
|
|
733
|
+
expect(rhythm.children[1].start).toBe(1.5);
|
|
734
|
+
expect(rhythm.children[1].end).toBe(2.5);
|
|
735
735
|
|
|
736
736
|
// last child starts at last beat (3.5), extends by avg gap (1.0) => ends at 4.5
|
|
737
|
-
expect(rhythm.children[3].
|
|
737
|
+
expect(rhythm.children[3].end).toBe(4.5);
|
|
738
738
|
// rhythm duration = last spot + avg gap
|
|
739
739
|
expect(rhythm.durationInSeconds).toBe(4.5);
|
|
740
740
|
});
|
|
@@ -778,11 +778,11 @@ describe("compileDescriptiveRoot", () => {
|
|
|
778
778
|
expect(bgm.type).toBe("audio");
|
|
779
779
|
expect(bgm.volume).toBe(0.3);
|
|
780
780
|
expect(bgm.foreground).toBe(false);
|
|
781
|
-
expect(bgm.
|
|
781
|
+
expect(bgm.end).toBe(5);
|
|
782
782
|
|
|
783
783
|
const sfx = compiled.children.find((c: any) => c.id === "sfx") as any;
|
|
784
|
-
expect(sfx.
|
|
785
|
-
expect(sfx.
|
|
784
|
+
expect(sfx.start).toBe(2);
|
|
785
|
+
expect(sfx.end).toBe(3);
|
|
786
786
|
});
|
|
787
787
|
|
|
788
788
|
it("compiles component node with props", () => {
|
|
@@ -801,7 +801,7 @@ describe("compileDescriptiveRoot", () => {
|
|
|
801
801
|
const c = compiled.children[0] as any;
|
|
802
802
|
expect(c.type).toBe("component");
|
|
803
803
|
expect(c.jsx).toContain("AnimatedHeadline");
|
|
804
|
-
expect(c.
|
|
804
|
+
expect(c.end).toBe(3);
|
|
805
805
|
});
|
|
806
806
|
|
|
807
807
|
it("compiles rhythm without children as audio leaf", () => {
|
|
@@ -817,7 +817,7 @@ describe("compileDescriptiveRoot", () => {
|
|
|
817
817
|
expect(r.src).toBe("beat.mp3");
|
|
818
818
|
expect(r.children).toEqual([]);
|
|
819
819
|
// rhythm duration = last spot + avg gap = 2.5 + 2.0 = 4.5
|
|
820
|
-
expect(r.
|
|
820
|
+
expect(r.end).toBeCloseTo(4.5);
|
|
821
821
|
});
|
|
822
822
|
|
|
823
823
|
it("compiles include with src as leaf", () => {
|
|
@@ -831,7 +831,7 @@ describe("compileDescriptiveRoot", () => {
|
|
|
831
831
|
const inc = compiled.children[0] as any;
|
|
832
832
|
expect(inc.type).toBe("include");
|
|
833
833
|
expect(inc.src).toBe("./child.json");
|
|
834
|
-
expect(inc.
|
|
834
|
+
expect(inc.end).toBe(3);
|
|
835
835
|
});
|
|
836
836
|
|
|
837
837
|
it("compiles effects:[] with named params (duration, timingFunction, iterationCount)", () => {
|
|
@@ -865,10 +865,10 @@ describe("compileDescriptiveRoot", () => {
|
|
|
865
865
|
});
|
|
866
866
|
|
|
867
867
|
const img = compiled.children[0] as any;
|
|
868
|
-
expect(img.
|
|
868
|
+
expect(img.end).toBe(3); // image default is 3s
|
|
869
869
|
|
|
870
870
|
const vid = compiled.children[1] as any;
|
|
871
|
-
expect(vid.
|
|
871
|
+
expect(vid.end).toBe(3); // video default is 3s
|
|
872
872
|
});
|
|
873
873
|
|
|
874
874
|
it("uses default duration when duration is missing", () => {
|
|
@@ -879,7 +879,7 @@ describe("compileDescriptiveRoot", () => {
|
|
|
879
879
|
],
|
|
880
880
|
});
|
|
881
881
|
const img = compiled.children[0] as any;
|
|
882
|
-
expect(img.
|
|
882
|
+
expect(img.end).toBe(3); // image default is 3s
|
|
883
883
|
});
|
|
884
884
|
|
|
885
885
|
it("preserves root-level instruction and stylesheet", () => {
|
|
@@ -916,7 +916,7 @@ describe("compileDescriptiveRoot", () => {
|
|
|
916
916
|
expect(m.type).toBe("map");
|
|
917
917
|
expect(m.waypoints).toHaveLength(2);
|
|
918
918
|
expect(m.travelMode).toBe("DRIVING");
|
|
919
|
-
expect(m.
|
|
919
|
+
expect(m.end).toBe(5);
|
|
920
920
|
});
|
|
921
921
|
|
|
922
922
|
it("filters invisible children", () => {
|