@lalalic/markcut 2.4.3 → 2.4.6
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
CHANGED
|
@@ -242,9 +242,9 @@ npx vitest run tests/render.test.ts -t "Test Name" # single test
|
|
|
242
242
|
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.
|
|
243
243
|
2. **Video endAt formula**: Must be `startFrom_frames + ((endAt_source - startFrom_source) * fps / playbackRate)` — changing to `endAt_source * fps` breaks slow-motion playback.
|
|
244
244
|
3. **`signalstats` ffmpeg filter**: Doesn't output YAVG on all systems; use file size as proxy for visual content.
|
|
245
|
-
4. **Asset paths**:
|
|
245
|
+
4. **Asset paths**: All `src` paths in markdown are resolved relative to the `.md` file's directory. Preview mode serves files via a multi-directory HTTP server; render mode uses absolute paths directly (bypassing `staticFile()`). Never use a `public/` prefix in markdown `src`.
|
|
246
246
|
5. **Map rendering**: Depends on external tile availability; may produce blank frames offline.
|
|
247
|
-
6. **CLI md render needs
|
|
247
|
+
6. **CLI md render needs two 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). Asset staging (`stageLocalAssets`) was removed — render now keeps absolute paths from `resolve.ts`, bypassing `staticFile()` entirely. All 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`).
|
|
248
248
|
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.
|
|
249
249
|
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`).
|
|
250
250
|
|
package/README.md
CHANGED
|
@@ -91,10 +91,7 @@ Inline components can be defined entirely in the imports block using `export fun
|
|
|
91
91
|
|
|
92
92
|
| Document | What it covers |
|
|
93
93
|
|---|---|
|
|
94
|
-
| [docs/json-descriptive.md](docs/json-descriptive.md) | Full JSON descriptive schema reference |
|
|
95
94
|
| [docs/markdown-strict-descriptive.md](docs/markdown-strict-descriptive.md) | Markdown descriptive syntax reference |
|
|
96
|
-
| [docs/label-mode.md](docs/label-mode.md) | Label mode player and workflow |
|
|
97
|
-
| [docs/edit-mode.md](docs/edit-mode.md) | Live edit mode with SSE reload |
|
|
98
95
|
|
|
99
96
|
|
|
100
97
|
## Variants
|
package/package.json
CHANGED
|
@@ -43,7 +43,7 @@ This section defines the markcut Markdown Descriptive grammar. An agent should r
|
|
|
43
43
|
|---|---|---|---|
|
|
44
44
|
| H1 | `# video` | **Root** — video canvas definition | Exactly one `# video` required. Everything else is nested inside this section. |
|
|
45
45
|
| H1 | `# zh`, `# youtube`, `# portrait` etc. | **Variant** — override section for alternate versions | Any `# <name>` other than `video`. Only contains override configs + variant-prefixed overrides on base nodes. |
|
|
46
|
-
| H2 | `## <name>` | **Scene** — top-level scene container | `name` = single
|
|
46
|
+
| H2 | `## <name>` | **Scene** — top-level scene container | `name` = single word (no spaces).|
|
|
47
47
|
| H3 | `### <name>` | **Nested scene** — sub-scene inside parent | Same rules as H2. Creates nesting: parent scene's children array includes this as a sub-container. |
|
|
48
48
|
| H4+ | `#### <name>` | **Deeply nested** — further nesting | Works identically; depth reflects nesting level. |
|
|
49
49
|
|
|
@@ -258,8 +258,14 @@ width:1080 height:1920 fps:30 layout:series
|
|
|
258
258
|
- script "Hello world"
|
|
259
259
|
```
|
|
260
260
|
|
|
261
|
-
### 13.
|
|
262
|
-
|
|
261
|
+
### 13. `src` Path Resolution — Context Root
|
|
262
|
+
all src paths are resolved relative to the markdown file's location. For example, if your markdown file is at `./videos/course.md`, then `src:./assets/bg.jpg` resolves to `./videos/assets/bg.jpg`. Absolute paths (starting with `/`) are resolved relative to the project root.
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
### 14. verify
|
|
266
|
+
- all assets path are resolved relative to the markdown file's location.
|
|
267
|
+
- `npx @lalalic/markcut verify book.md` to verify the markdown document is valid and can be rendered. It will check for missing required fields, invalid values, and other common issues.
|
|
268
|
+
|
|
263
269
|
|
|
264
270
|
## Template Variables
|
|
265
271
|
|
|
@@ -285,22 +291,12 @@ Subtitles are configured at the root level as a VTT overlay. Set via `subtitle:`
|
|
|
285
291
|
|
|
286
292
|
| Field | Required | Type | Notes |
|
|
287
293
|
|---|---|---|---|
|
|
288
|
-
| `src` | yes | string | VTT path/URL, inline VTT body, or plain text |
|
|
289
294
|
| `type` | opt | string | caption animation: `Bounce`, `Fade`, `Typewriter`, `Colorful`, `Glowing`, `Neon`, `Zoom`. Default: plain static caption |
|
|
290
295
|
| `fontSize` | opt | number \| string | default 56 (accepts CSS value like `"2em"` or number) |
|
|
291
296
|
| `fontFamily` | opt | string | font family |
|
|
292
297
|
| `fontStyle` | opt | string | `normal`, `italic`, `bold`, `bold italic` |
|
|
293
298
|
| `style` | opt | string | inline CSS for the overlay container |
|
|
294
299
|
|
|
295
|
-
> **HTML in cue text**: Cue text supports HTML tags with inline CSS, so you can style individual words:
|
|
296
|
-
> ```vtt
|
|
297
|
-
> 00:00:01.000 --> 00:00:03.000
|
|
298
|
-
> Hello <span style="color:#ff6b6b">world</span>, welcome to <b>our show</b>!
|
|
299
|
-
> ```
|
|
300
|
-
> The engine renders cue text via `dangerouslySetInnerHTML`, making `<span>`, `<b>`, `<i>`, `<br>`, and inline `style` attributes all work.
|
|
301
|
-
|
|
302
|
-
If `src` is plain text (no `-->` markers), it renders as a single caption for the full video duration. The `type` field maps to a `remotion-subtitle` animation component — omit for a plain static caption.
|
|
303
|
-
|
|
304
300
|
## Key Reference (use these names)
|
|
305
301
|
|
|
306
302
|
| Key | Means | Applies to | Note
|
|
@@ -9,11 +9,15 @@ Full path: ${filePath}
|
|
|
9
9
|
@{skills/markcut/docs/markdown-descriptive.md}
|
|
10
10
|
|
|
11
11
|
TASKS:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
- locate the section of the .md file according to current timestamp and active scene, and user instructions
|
|
13
|
+
- edit the .md file to implement user instructions, while preserving existing content and formatting
|
|
14
|
+
- Read the .md file directly using your file tools to see its current content
|
|
15
|
+
- Edit the .md file directly — change content, add/remove sections, update frontmatter
|
|
16
|
+
- **Caption**: directly `edit` vtt file when stylishing text in captions
|
|
17
|
+
**HTML in cue text**: Cue text supports HTML tags with inline CSS, so you can style individual words:
|
|
18
|
+
~~~vtt
|
|
19
|
+
00:00:01.000 --> 00:00:03.000
|
|
20
|
+
Hello <span style="color:#ff6b6b">world</span>, welcome to <b>our show</b>!
|
|
21
|
+
~~~
|
|
22
|
+
- Save the changes to the .md file using your `edit` tool
|
|
23
|
+
|
package/src/render/cli.mjs
CHANGED
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
* (TTS/STT/durations)
|
|
8
8
|
*/
|
|
9
9
|
import { execSync, spawn } from "node:child_process";
|
|
10
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync
|
|
11
|
-
import {
|
|
12
|
-
import { resolve, dirname, join, extname } from "node:path";
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
11
|
+
import { resolve, dirname, join } from "node:path";
|
|
13
12
|
import { fileURLToPath } from "node:url";
|
|
14
13
|
import {
|
|
15
14
|
DEFAULT_TTS_CLI, DEFAULT_STT_CLI, DEFAULT_TTI_CLI, DEFAULT_TTV_CLI,
|
|
@@ -96,62 +95,10 @@ Commands:
|
|
|
96
95
|
* Use --verbose to see every frame line (original behavior).
|
|
97
96
|
*/
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
* Resolve a potentially relative source path against baseDir, skipping
|
|
101
|
-
* absolute paths, URLs, and data URIs.
|
|
102
|
-
*/
|
|
103
|
-
function resolveAssetPath(src, baseDir) {
|
|
104
|
-
if (!src || typeof src !== "string") return null;
|
|
105
|
-
// Already absolute on disk
|
|
106
|
-
if (src.startsWith("/")) return src;
|
|
107
|
-
// URL or data URI — can't stage locally
|
|
108
|
-
if (/^(https?:|data:|blob:)/.test(src)) return null;
|
|
109
|
-
// Relative path: resolve against the input file's directory
|
|
110
|
-
const abs = resolve(baseDir, src);
|
|
111
|
-
if (existsSync(abs)) return abs;
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Stage all local src files (absolute and relative) into
|
|
117
|
-
* ROOT/public/.render-assets/ so `npx remotion render` (publicDir = ROOT/public)
|
|
118
|
-
* can serve them. Relative paths are resolved against `baseDir` (the input
|
|
119
|
-
* file's directory). The preview server handles these paths via multi-root
|
|
120
|
-
* serving; the render CLI needs them staged into the one public dir Remotion
|
|
121
|
-
* knows about.
|
|
122
|
-
*/
|
|
123
|
-
function stageLocalAssets(tree, baseDir) {
|
|
124
|
-
const assetsDir = join(ROOT, "public", ".render-assets");
|
|
125
|
-
const seen = new Map();
|
|
126
|
-
const walk = (node) => {
|
|
127
|
-
if (!node || typeof node !== "object") return;
|
|
128
|
-
if (typeof node.src === "string") {
|
|
129
|
-
const absPath = resolveAssetPath(node.src, baseDir);
|
|
130
|
-
if (absPath) {
|
|
131
|
-
let staged = seen.get(absPath);
|
|
132
|
-
if (!staged) {
|
|
133
|
-
const name = createHash("md5").update(absPath).digest("hex").slice(0, 12) + extname(absPath);
|
|
134
|
-
mkdirSync(assetsDir, { recursive: true });
|
|
135
|
-
copyFileSync(absPath, join(assetsDir, name));
|
|
136
|
-
staged = `.render-assets/${name}`;
|
|
137
|
-
seen.set(absPath, staged);
|
|
138
|
-
}
|
|
139
|
-
node.src = staged;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
for (const value of Object.values(node)) {
|
|
143
|
-
if (value && typeof value === "object") walk(value);
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
walk(tree);
|
|
147
|
-
return tree;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function renderOne(streamTree, outputPath, verbose, baseDir) {
|
|
151
|
-
const adapted = stageLocalAssets(JSON.parse(JSON.stringify(streamTree)), baseDir);
|
|
98
|
+
function renderOne(streamTree, outputPath, verbose) {
|
|
152
99
|
const tmpProps = join(ROOT, ".tmp", "render-stream.json");
|
|
153
100
|
mkdirSync(dirname(tmpProps), { recursive: true });
|
|
154
|
-
writeFileSync(tmpProps, JSON.stringify({ root:
|
|
101
|
+
writeFileSync(tmpProps, JSON.stringify({ root: streamTree }));
|
|
155
102
|
|
|
156
103
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
157
104
|
|
|
@@ -409,19 +356,18 @@ edit=${DEFAULT_EDIT_CLI}`);
|
|
|
409
356
|
const { bundleFromEntries } = await import("../player/bundler.mjs");
|
|
410
357
|
const entries = parseImportsBlock(rawSource);
|
|
411
358
|
const extraSpecs = extractDependencySpecs(rawSource);
|
|
412
|
-
const bundleDir = join(ROOT, "
|
|
359
|
+
const bundleDir = join(ROOT, ".tmp", "component-bundle");
|
|
413
360
|
mkdirSync(bundleDir, { recursive: true });
|
|
414
361
|
const bundle = await bundleFromEntries(entries, extraSpecs, rawSource, bundleDir);
|
|
415
362
|
if (bundle.url) {
|
|
416
|
-
streamTree.imports =
|
|
363
|
+
streamTree.imports = join(bundleDir, bundle.url.split("/").pop());
|
|
417
364
|
console.log(` \u2705 components \u2192 ${bundle.exports.join(", ")}`);
|
|
418
365
|
}
|
|
419
366
|
}
|
|
420
367
|
}
|
|
421
368
|
|
|
422
369
|
const output = args.output ? resolve(args.output) : join(ROOT, "out", "video.mp4");
|
|
423
|
-
|
|
424
|
-
await renderOne(streamTree, output, args.verbose, fileDir);
|
|
370
|
+
await renderOne(streamTree, output, args.verbose);
|
|
425
371
|
|
|
426
372
|
console.log("\n✅ Render complete.");
|
|
427
373
|
process.exit(0);
|