@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.
Files changed (69) hide show
  1. package/AGENTS.md +39 -15
  2. package/README.md +7 -3
  3. package/SKILL.md +26 -172
  4. package/docs/edit-mode.md +1 -1
  5. package/docs/label-mode.md +6 -4
  6. package/docs/markdown-descriptive.md +46 -1
  7. package/docs/system-prompt-edit.md +16 -0
  8. package/package.json +1 -1
  9. package/src/config.mjs +8 -3
  10. package/src/descriptive/compiler.test.ts +42 -42
  11. package/src/descriptive/compiler.ts +41 -52
  12. package/src/descriptive/markdown.ts +8 -4
  13. package/src/descriptive/resolve.test.ts +14 -7
  14. package/src/descriptive/resolve.ts +148 -20
  15. package/src/entry.tsx +8 -2
  16. package/src/player/browser.tsx +178 -54
  17. package/src/player/bundle/player.js +1168 -566
  18. package/src/player/components/EditControls.tsx +92 -0
  19. package/src/player/components/HeaderBar.tsx +60 -0
  20. package/src/player/components/LabelControls.tsx +367 -0
  21. package/src/player/components/SceneThumbnails.tsx +87 -0
  22. package/src/player/components/VariantBar.tsx +39 -0
  23. package/src/player/components/index.ts +5 -0
  24. package/src/player/pipeline.mjs +130 -66
  25. package/src/player/pipeline.ts +3 -1
  26. package/src/player/server-shared.mjs +5 -7
  27. package/src/player/server.mjs +194 -187
  28. package/src/render/cli.mjs +66 -13
  29. package/src/schema/index.ts +20 -23
  30. package/src/types/Audio.tsx +25 -33
  31. package/src/types/Component.tsx +18 -24
  32. package/src/types/Effect.tsx +31 -39
  33. package/src/types/Image.tsx +23 -30
  34. package/src/types/Include.tsx +70 -76
  35. package/src/types/Map.tsx +48 -44
  36. package/src/types/Rhythm.tsx +19 -27
  37. package/src/types/Video.tsx +40 -47
  38. package/src/utils/component-import-map.ts +93 -0
  39. package/src/utils/index.ts +23 -10
  40. package/src/vision/cli.mjs +6 -6
  41. package/templates/courseware/TEMPLATE.md +317 -0
  42. package/templates/courseware/agents/reviewer.md +84 -0
  43. package/templates/courseware/prompts/fix.md +30 -0
  44. package/templates/courseware/prompts/outline.md +53 -0
  45. package/templates/courseware/prompts/scene.md +64 -0
  46. package/tests/fixtures/audio.json +4 -2
  47. package/tests/fixtures/basic.json +2 -1
  48. package/tests/fixtures/component-all.json +4 -2
  49. package/tests/fixtures/components.json +4 -2
  50. package/tests/fixtures/effects.json +12 -6
  51. package/tests/fixtures/full.json +8 -4
  52. package/tests/fixtures/map.json +2 -1
  53. package/tests/fixtures/md/dialogue.md +12 -0
  54. package/tests/fixtures/md/edge-cases.md +9 -0
  55. package/tests/fixtures/scenes.json +4 -2
  56. package/tests/fixtures/subtitle.json +6 -3
  57. package/tests/fixtures/subvideo.json +11 -6
  58. package/tests/fixtures/templates/courseware.md +61 -4
  59. package/tests/fixtures/tween-visual.json +2 -6
  60. package/tests/fixtures/video-series.json +6 -14
  61. package/tests/md-descriptive.test.ts +170 -0
  62. package/tests/render.test.ts +32 -16
  63. package/tests/schema.test.ts +6 -3
  64. package/tests/server.test.ts +9 -6
  65. package/tests/utils.ts +4 -4
  66. package/docs/dynamic-components.md +0 -191
  67. package/docs/templates.md +0 -52
  68. package/scripts/artlist-dl.mjs +0 -190
  69. package/src/player/label-server.mjs +0 -599
@@ -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
- actions: [{ start: 0, end: 3 }],
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("shows variant switcher bar with all variant links", async () => {
278
- const fixture = makeFixture("html-bar");
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
- expect(res.text).toContain('href="/"');
285
- expect(res.text).toContain('href="/zh-tiktok"');
286
- expect(res.text).toContain('href="/youtube"');
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 action duration
380
- if (fixtureData.actions?.length) {
381
- return Math.max(...fixtureData.actions.map((a: any) => a.end ?? 1));
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
  }
@@ -1,191 +0,0 @@
1
- # Dynamic Components
2
-
3
- Three ways to add custom visual content without rebuilding the engine.
4
-
5
- ## 1. Effects on Any Node (CSS Keyframes)
6
-
7
- Add the `effects` key to any node to apply CSS keyframe animations. No wrapper node needed.
8
-
9
- ```json
10
- {
11
- "id": "animated-scene",
12
- "type": "image",
13
- "src": "photo.jpg",
14
- "duration": 3,
15
- "effects": ["bounceIn(1, ease-out, 2)"]
16
- }
17
- ```
18
-
19
- ### 25+ Built-in Animation Names
20
-
21
- | Fades | Slides | Zooms | Attention | Bounce | Rotations |
22
- |-------|--------|-------|-----------|--------|-----------|
23
- | `fadeIn` | `slideInDown` | `zoomIn` | `pulse` | `bounceIn` | `rotateIn` |
24
- | `fadeOut` | `slideInUp` | `zoomOut` | `flash` | | `rotateOut` |
25
- | `fadeInDown` | `slideInLeft` | | `bounce` | | |
26
- | `fadeInUp` | `slideInRight` | | `heartBeat` | | |
27
- | `fadeInLeft` | | | `rubberBand` | | |
28
- | `fadeInRight` | | | `shakeX` | | |
29
- | (8 total) | | | | | |
30
-
31
- ### Custom Keyframes
32
-
33
- ```json
34
- {
35
- "type": "effect",
36
- "animation": "custom",
37
- "customKeyframes": {
38
- "0": { "opacity": "0", "transform": "scale3d(0,0,0) rotate(0deg)" },
39
- "50": { "opacity": "0.5", "transform": "scale3d(1.2,1.2,1.2) rotate(180deg)" },
40
- "100": { "opacity": "1", "transform": "scale3d(1,1,1) rotate(360deg)" }
41
- },
42
- "children": [...],
43
- "actions": [{ "start": 0, "end": 2 }]
44
- }
45
- ```
46
-
47
- Percentages `"0"`–`"100"` map to action duration. Any numeric CSS property works.
48
-
49
- ## 2. Remote Components (bundler-based)
50
-
51
- Register remote React components via the `~~~js imports` code fence (markdown) or `root.imports` array (JSON descriptive). The player server bundles all registered components into a single ESM module at startup.
52
-
53
- ```markdown
54
- ~~~js imports
55
- export { BarChart } from "recharts"
56
- export { Hello } from "git:user/repo/path/to/Hello.tsx"
57
- export { Badge } from "https://cdn.example.com/components/badge.js"
58
-
59
- export function Slide(props) {
60
- return <div style={{color: '#fff'}}>{props.children}</div>;
61
- }
62
- ~~~
63
- ```
64
-
65
- Then use them in component JSX:
66
-
67
- ```json
68
- {
69
- "type": "component",
70
- "jsx": "<Badge text='LIVE' color='#ff0000' />",
71
- "actions": [{ "start": 0, "end": 3 }]
72
- }
73
- ```
74
-
75
- ### How it works
76
-
77
- 1. The server extracts the imports block from the source file
78
- 2. `parseImportsBlock` resolves `export { X } from "spec"` and `export function X() {}` patterns
79
- 3. Inline functions' source code is scanned for `import X from "pkg"` statements — those packages are added to dependencies
80
- 4. `bundleFromEntries` creates a temp npm project, installs all packages, and runs `esbuild` to produce a single ESM file
81
- 5. The bundled file URL is set on `root.imports` and served from `/.component-cache/<hash>.js`
82
- 6. At runtime, `MarkCut.useComponentRegistry` does a dynamic `import(url)` to load all named exports
83
- 7. react-jsx-parser resolves component tags from the loaded registry
84
-
85
- ### Import spec forms
86
-
87
- | Pattern | Resolves to | Example |
88
- |---|---|---|
89
- | `pkg` or `npm:pkg` | npm package (installed + bundled) | `export { BarChart } from "recharts"` |
90
- | `git:user/repo` | GitHub repo source | `export { Comp } from "git:user/repo/src/Comp.tsx"` |
91
- | `https://...` | Raw URL (used as-is) | `export { X } from "https://cdn.example.com/x.js"` |
92
- | local path | Filesystem path | `export { X } from "./local/Component.tsx"` |
93
-
94
- ### Inline functions
95
-
96
- Define components directly in the imports block. Dependencies used inside the function body are automatically detected and installed:
97
-
98
- ```markdown
99
- ~~~js imports
100
- import ReactMarkdown from "react-markdown"
101
-
102
- export function Slide({ children }) {
103
- return <div className="slide"><ReactMarkdown>{children}</ReactMarkdown></div>;
104
- }
105
- ~~~
106
- ```
107
-
108
- The `import` statements at the top serve dual purpose: they bring packages into scope inside inline functions AND tell the bundler what to install. The packages are added to `package.json` during bundling.
109
-
110
- ### Component Contract
111
-
112
- Props injected automatically:
113
- - Nothing special required — just standard React props. The engine passes any `data` bindings from the component node as JSX variables.
114
-
115
- For frame-accurate animation, use standard Remotion hooks inside inline functions:
116
-
117
- ```markdown
118
- ~~~js imports
119
- import { useCurrentFrame } from "remotion"
120
-
121
- export function FadeIn({ children }) {
122
- const frame = useCurrentFrame();
123
- const opacity = Math.min(1, frame / 15);
124
- return <div style={{ opacity }}>{children}</div>;
125
- }
126
- ~~~
127
-
128
- Note: `remotion` and `react` are automatically available in the bundle's external scope — they do NOT need to be imported. However, adding `import { useCurrentFrame } from "remotion"` in the imports block is harmless and makes the dependency explicit.
129
-
130
- ## 3. Custom Components
131
-
132
- Create a React component file in `src/components/`, register in `builtinComponents`.
133
-
134
- ### Component Contract
135
-
136
- Props the engine injects automatically:
137
- - `action: { start: number; end: number }` — action timing from the stream node
138
-
139
- Use `useCurrentFrame()` for frame-accurate animation, `useVideoConfig()` for canvas size.
140
-
141
- ### Example
142
-
143
- ```tsx
144
- // src/components/my/Badge.tsx
145
- import React from "react";
146
- import { useCurrentFrame, interpolate } from "remotion";
147
-
148
- export const Badge: React.FC<{text: string; color?: string; action: any}> =
149
- ({ text, color, action }) => {
150
- const frame = useCurrentFrame();
151
- const local = frame - action.start * 30;
152
- const opacity = interpolate(local, [0, 15], [0, 1]);
153
- return (
154
- <div style={{ opacity, padding: 12, background: color || theme.colors.primary,
155
- color: "white", borderRadius: 8, fontSize: 48 }}>
156
- {text}
157
- </div>
158
- );
159
- };
160
- ```
161
-
162
- ### Register
163
-
164
- ```tsx
165
- // src/components/index.ts
166
- import { Badge } from "./my/Badge";
167
- export const builtinComponents = { ..., Badge };
168
- ```
169
-
170
- ### Reference in stream tree
171
-
172
- ```json
173
- {
174
- "type": "component",
175
- "jsx": "<Badge text='NEW' color='#ff6b35' />",
176
- "actions": [{ "start": 1, "end": 4 }]
177
- }
178
- ```
179
-
180
-
181
- # common used components
182
- - `react-markdown` — render markdown content, use plugins to extend functionality
183
- - `remark-gfm` — support GitHub Flavored Markdown (tables, strikethrough, task lists)
184
- - `remark-toc` — generate table of contents
185
- - `remark-math` — support math formulas
186
- - `rehype-katex` — render math formulas with KaTeX
187
- - `remark-mermaidjs` — render mermaid diagrams in
188
- - `react-markdown-mermaid` — render mermaid diagrams in standalone mode (no need to install mermaid separately)
189
-
190
- - `@remotion/shapes` — render shapes like arrows, circles, rectangles, etc
191
- - `@remotion/starburst` — render starburst animations
package/docs/templates.md DELETED
@@ -1,52 +0,0 @@
1
- # Video Templates
2
-
3
- Ready-to-use markdown templates for common video scenarios. Each template:
4
-
5
- - Contains **YAML frontmatter** with usage guidance (when, how, key decisions)
6
- - Lists **required external packages** (npm imports)
7
- - Uses **src:auto prompt:"..."** placeholders for AI-generated images/videos
8
- - Includes **TTS narration** with configured voice and pacing
9
- - Provides **scene structure** with named scenes, layouts, and durations
10
-
11
- ## Quick Start
12
-
13
- ```bash
14
- # 1. Pick a template
15
- cp docs/templates/courseware.md my-video.md
16
-
17
- # 2. Fill in [bracketed] placeholders with actual content
18
- # 3. Render
19
- npx markcut render my-video.md --aspect 16x9
20
- ```
21
-
22
- ## Template Catalog
23
-
24
- | Template | Scenario | Duration | Key Components |
25
- |---|---|---|---|
26
- | [courseware.md](templates/courseware.md) | 课件 / Lessons | 1-5 min | MarpSlide (markdown→slides), SlideText, MathDisplay |
27
- | [product-ad.md](templates/product-ad.md) | äŗ§å“å¹æå‘Š / Ads | 15-45s | TextReveal, DeviceMockup, QRCode, FeatureCard |
28
- | [movie-review.md](templates/movie-review.md) | 影视讲解 / Reviews | 1-3 min | CinematicText, StarRating, TextAnnotation, SplitScreen |
29
- | [audiobook.md](templates/audiobook.md) | ęœ‰å£°å›¾ä¹¦ / Audiobooks | 3-30 min | BookCover, AmbientBackground, ProgressBar |
30
- | [story-video.md](templates/story-video.md) | 故事视频 / Stories | 1-5 min | StoryTitle, SpeechBubble, ParticleEffect |
31
- | [travel-log.md](templates/travel-log.md) | ę—…č”Œę—„åæ— / Travel | 1-3 min | LocationCard, WeatherIcon, TravelStats, Map node |}
32
-
33
- ## Frontmatter Fields Used by Templates
34
-
35
- | Field | Purpose | Example |
36
- |---|---|---|
37
- | `tts` | TTS CLI command with `{input}` `{output}` | `edge-tts --voice "zh-CN-XiaoxiaoNeural" --text "{input}" --write-media "{output}"` |
38
- | `stt` | STT CLI command with `{input}` `{output}` | `whisper "{input}" --model tiny --language zh --output_format vtt --output_dir "{output}"` |
39
- | `tti` | TTI CLI command with `{input}` `{output}` | `pi --model agnes-2.0-flash --print "generate image: {input}" --output "{output}"` |
40
- | `ttv` | TTV CLI command with `{input}` `{output}` | `pi --model agnes-2.0-flash --print "generate video: {input}" --output "{output}"` |
41
-
42
- ## src:auto — AI-Generated Media
43
-
44
- Templates use a special `src:auto` value on `image` and `video` nodes to indicate AI generation:
45
-
46
- ```md
47
- - image src:auto prompt:"sunset over Tokyo skyline, cinematic" duration:5
48
- - video src:auto prompt:"person walking through cherry blossom park" duration:3
49
- ```
50
-
51
- The `prompt:` key provides the generation prompt to the configured TTI/TTV CLI.
52
- The pipeline resolves `src:auto` before compilation: runs the CLI command, gets the output path, and replaces `src:auto` with the actual generated file path.
@@ -1,190 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * artlist-dl — Download BGM/SFX from artlist.io CDN URLs or track pages.
4
- *
5
- * Usage:
6
- * node scripts/artlist-dl.mjs bgm <cdnUrl> <name> → public/assets/bgm/<name>.mp3
7
- * node scripts/artlist-dl.mjs sfx <cdnUrl> <name> [s] → public/assets/sfx/<name>.mp3 (trimmed to s seconds)
8
- * node scripts/artlist-dl.mjs grab <trackPageUrl> <name> [bgm|sfx] [s] → auto-extract CDN URL from track page
9
- * node scripts/artlist-dl.mjs list → show current assets
10
- *
11
- * CDN URLs are captured from artlist.io when playing a track (see SKILL.md).
12
- * Pattern: https://cms-public-artifacts.artlist.io/content/...
13
- *
14
- * The `grab` command scrapes the track page HTML for base64-encoded CDN paths,
15
- * no browser or login needed — works with just curl.
16
- */
17
- import { execSync } from "node:child_process";
18
- import { existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
19
- import { resolve, dirname } from "node:path";
20
- import { fileURLToPath } from "node:url";
21
-
22
- const __dirname = dirname(fileURLToPath(import.meta.url));
23
- const ROOT = resolve(__dirname, "..");
24
-
25
- const BGM_DIR = resolve(ROOT, "public/assets/bgm");
26
- const SFX_DIR = resolve(ROOT, "public/assets/sfx");
27
-
28
- function ensureDir(dir) {
29
- mkdirSync(dir, { recursive: true });
30
- }
31
-
32
- function download(url, outPath, trimSeconds) {
33
- const tmpPath = resolve(ROOT, ".tmp", `dl-${Date.now()}.aac`);
34
- ensureDir(dirname(tmpPath));
35
- ensureDir(dirname(outPath));
36
-
37
- console.log(`⬇ Downloading: ${url.substring(0, 80)}...`);
38
- execSync(`curl -sL "${url}" -o "${tmpPath}"`, { stdio: "inherit" });
39
-
40
- const trimFlag = trimSeconds ? `-t ${trimSeconds}` : "";
41
- console.log(`šŸ”„ Converting to MP3${trimSeconds ? ` (${trimSeconds}s)` : ""}...`);
42
- execSync(`ffmpeg -y -i "${tmpPath}" ${trimFlag} "${outPath}" 2>/dev/null`, { stdio: "inherit" });
43
-
44
- execSync(`rm "${tmpPath}"`);
45
-
46
- const size = statSync(outPath).size;
47
- console.log(`āœ… ${outPath} (${(size / 1024).toFixed(1)} KB)`);
48
- }
49
-
50
- function list() {
51
- console.log("\nšŸ“ BGM (public/assets/bgm/):");
52
- if (existsSync(BGM_DIR)) {
53
- for (const f of readdirSync(BGM_DIR)) {
54
- const size = statSync(resolve(BGM_DIR, f)).size;
55
- console.log(` šŸŽµ ${f} (${(size / 1024).toFixed(1)} KB)`);
56
- }
57
- }
58
- console.log("\nšŸ“ SFX (public/assets/sfx/):");
59
- if (existsSync(SFX_DIR)) {
60
- for (const f of readdirSync(SFX_DIR)) {
61
- const size = statSync(resolve(SFX_DIR, f)).size;
62
- console.log(` šŸ”Š ${f} (${(size / 1024).toFixed(1)} KB)`);
63
- }
64
- }
65
- }
66
-
67
- function search(term, type = "sfx", count = 10) {
68
- const GRAPHQL = "https://search-api.artlist.io/v1/graphql";
69
- let query, variables;
70
-
71
- if (type === "bgm" || type === "music") {
72
- query = `query SongList($page: Int!, $songSortType: Int!, $take: Int!, $vocalMenuId: Int!, $searchTerm: String) {
73
- songList(page: $page, songSortType: $songSortType, take: $take, vocalMenuId: $vocalMenuId, searchTerm: $searchTerm) {
74
- songs { songId songName artistName duration sitePlayableFilePath nameForURL }
75
- }
76
- }`;
77
- variables = { page: 1, songSortType: 0, take: count, vocalMenuId: 0, searchTerm: term };
78
- } else {
79
- query = `query SfxList($categoryIds: String!, $page: Float!, $tags: String!, $term: String!, $sortBy: SfxListRequestSortByOptions!) {
80
- sfxList(categoryIds: $categoryIds, page: $page, tags: $tags, term: $term, sortBy: $sortBy) {
81
- songs { songId songName artistName durationTime sitePlayableFilePath nameForURL }
82
- }
83
- }`;
84
- variables = { categoryIds: "", page: 1, tags: "", term, sortBy: "STAFF_PICKS" };
85
- }
86
-
87
- const body = JSON.stringify({ query, variables });
88
- const result = execSync(
89
- `curl -s '${GRAPHQL}' -H 'Content-Type: application/json' -d '${body.replace(/'/g, "'\\''")}'`,
90
- { encoding: "utf-8" }
91
- );
92
- const data = JSON.parse(result);
93
- const songs = type === "bgm" || type === "music"
94
- ? data.data?.songList?.songs || []
95
- : data.data?.sfxList?.songs || [];
96
-
97
- if (!songs.length) {
98
- console.log(`No results for "${term}"`);
99
- return;
100
- }
101
-
102
- const typeLabel = type === "bgm" || type === "music" ? "Music" : "SFX";
103
- console.log(`\nšŸ” ${typeLabel} results for "${term}" (${songs.length} tracks):\n`);
104
- for (const s of songs) {
105
- const dur = s.duration || `${s.durationTime}s`;
106
- const url = type === "bgm" || type === "music"
107
- ? `https://artlist.io/royalty-free-music/song/${s.nameForURL}/${s.songId}`
108
- : `https://artlist.io/sfx/track/${s.nameForURL}/${s.songId}`;
109
- console.log(` ${s.songId.padEnd(8)} ${s.songName} — ${s.artistName} (${dur})`);
110
- console.log(` ${url}`);
111
- }
112
- console.log(`\nTo download: node scripts/artlist-dl.mjs grab <url> <name> [bgm|sfx]`);
113
- }
114
-
115
- // CLI
116
- const [,, cmd, ...args] = process.argv;
117
-
118
- if (cmd === "list") {
119
- list();
120
- } else if (cmd === "search" && args.length >= 1) {
121
- const [term, type = "sfx", count] = args;
122
- search(term, type, count ? Number(count) : 10);
123
- } else if (cmd === "grab" && args.length >= 2) {
124
- const [trackUrl, name, type = "sfx", trim] = args;
125
- // Parse track ID and type from URL
126
- // SFX: artlist.io/sfx/track/.../81139 | Music: artlist.io/royalty-free-music/song/.../6000953
127
- const sfxMatch = trackUrl.match(/\/sfx\/track\/[^/]+\/(\d+)/);
128
- const musicMatch = trackUrl.match(/\/(?:royalty-free-music|song)\/[^/]+\/[^/]+\/(\d+)/);
129
- const trackId = sfxMatch?.[1] || musicMatch?.[1];
130
- const isSfx = !!sfxMatch || type === "sfx";
131
- const isMusic = !!musicMatch || type === "bgm";
132
-
133
- if (!trackId) {
134
- console.error("āŒ Could not parse track ID from URL. Expected format:");
135
- console.error(" SFX: artlist.io/sfx/track/<name>/<id>");
136
- console.error(" Music: artlist.io/royalty-free-music/song/<name>/<id>");
137
- process.exit(1);
138
- }
139
-
140
- console.log(`šŸ” Fetching ${isMusic ? "music" : "sfx"} track #${trackId} via GraphQL API...`);
141
- try {
142
- const query = isMusic
143
- ? `query Songs($ids: [String!]!) { songs(ids: $ids) { songId songName sitePlayableFilePath duration artistName } }`
144
- : `query Sfxs($ids: [Int!]!) { sfxs(ids: $ids) { songId songName sitePlayableFilePath duration artistName } }`;
145
- const variables = isMusic ? { ids: [trackId] } : { ids: [Number(trackId)] };
146
- const body = JSON.stringify({ query, variables });
147
-
148
- const result = execSync(
149
- `curl -s 'https://search-api.artlist.io/v1/graphql' -H 'Content-Type: application/json' -d '${body.replace(/'/g, "'\\''")}'`,
150
- { encoding: "utf-8" }
151
- );
152
- const data = JSON.parse(result);
153
- const track = isMusic ? data.data?.songs?.[0] : data.data?.sfxs?.[0];
154
-
155
- if (!track?.sitePlayableFilePath) {
156
- console.error("āŒ Track not found or no audio URL available.");
157
- process.exit(1);
158
- }
159
-
160
- console.log(`āœ… "${track.songName}" by ${track.artistName} (${track.duration})`);
161
- const cdnUrl = track.sitePlayableFilePath;
162
- const outDir = isMusic ? BGM_DIR : SFX_DIR;
163
- const trimSec = trim ? Number(trim) : (isSfx && !isMusic ? 3 : undefined);
164
- download(cdnUrl, resolve(outDir, `${name}.mp3`), trimSec);
165
- } catch (e) {
166
- console.error(`āŒ Failed: ${e.message}`);
167
- process.exit(1);
168
- }
169
- } else if (cmd === "bgm" && args.length >= 2) {
170
- const [url, name] = args;
171
- download(url, resolve(BGM_DIR, `${name}.mp3`));
172
- } else if (cmd === "sfx" && args.length >= 2) {
173
- const [url, name, trim] = args;
174
- download(url, resolve(SFX_DIR, `${name}.mp3`), trim ? Number(trim) : 3);
175
- } else {
176
- console.log(`
177
- Usage:
178
- node scripts/artlist-dl.mjs search <term> [sfx|bgm] [count] Search artlist.io
179
- node scripts/artlist-dl.mjs grab <trackPageUrl> <name> [bgm|sfx] Download from track page URL
180
- node scripts/artlist-dl.mjs bgm <cdnUrl> <name> Download BGM from CDN URL
181
- node scripts/artlist-dl.mjs sfx <cdnUrl> <name> [sec] Download SFX from CDN URL (trim to sec)
182
- node scripts/artlist-dl.mjs list List current audio assets
183
-
184
- Examples:
185
- node scripts/artlist-dl.mjs search "whoosh" sfx
186
- node scripts/artlist-dl.mjs search "cinematic epic" bgm
187
- node scripts/artlist-dl.mjs grab https://artlist.io/sfx/track/sci-fi-transitions---powerful-whoosh/81139 whoosh sfx
188
- node scripts/artlist-dl.mjs grab https://artlist.io/royalty-free-music/song/dominion/6000953 dominion bgm
189
- `);
190
- }