makaron-cli 0.13.3 → 0.13.5
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/.claude-plugin/plugin.json +4 -4
- package/.codex-plugin/plugin.json +21 -12
- package/README.md +46 -7
- package/bin/makaron.mjs +298 -50
- package/package.json +1 -1
- package/skills/makaron/SKILL.md +26 -7
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "makaron-cli",
|
|
3
|
-
"version": "0.13.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.13.5",
|
|
4
|
+
"description": "Give Claude Code a creative agent. Pass complete creative requests and source media to Makaron Chat.",
|
|
5
5
|
"author": {
|
|
6
|
-
"name": "
|
|
6
|
+
"name": "Versa AI",
|
|
7
|
+
"email": "tianyi@versa-ai.com",
|
|
7
8
|
"url": "https://www.makaron.app"
|
|
8
9
|
},
|
|
9
10
|
"homepage": "https://www.makaron.app/agent",
|
|
10
|
-
"repository": "https://github.com/vegekyd/ai-image-editor",
|
|
11
11
|
"skills": "./skills/"
|
|
12
12
|
}
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "makaron-cli",
|
|
3
|
-
"version": "0.13.
|
|
4
|
-
"description": "
|
|
5
|
-
"displayName": "Makaron",
|
|
6
|
-
"shortDescription": "AI image/video/music creation from the terminal",
|
|
7
|
-
"longDescription": "makaron.app is for humans. makaron-cli is for AI agents. Talk to Makaron Agent from the terminal — list, search, show, and install marketplace skills; create projects; edit images; generate videos; and compose music. Zero dependencies, single file, works with npx. Agents can self-register to get an API key without human intervention.",
|
|
3
|
+
"version": "0.13.5",
|
|
4
|
+
"description": "Give Codex a creative agent. Pass complete creative requests and source media to Makaron Chat.",
|
|
8
5
|
"author": {
|
|
9
|
-
"name": "
|
|
6
|
+
"name": "Versa AI",
|
|
7
|
+
"email": "tianyi@versa-ai.com",
|
|
10
8
|
"url": "https://www.makaron.app"
|
|
11
9
|
},
|
|
12
10
|
"homepage": "https://www.makaron.app/agent",
|
|
13
|
-
"repository": "https://github.com/vegekyd/ai-image-editor",
|
|
14
11
|
"license": "MIT",
|
|
15
|
-
"keywords": ["
|
|
16
|
-
"category": "Creative Tools",
|
|
17
|
-
"capabilities": ["image-editing", "video-generation", "music-creation", "text-to-image", "marketplace-skills"],
|
|
12
|
+
"keywords": ["creative-production", "image-editing", "video-generation", "music", "agent", "cli"],
|
|
18
13
|
"skills": "./skills/",
|
|
19
|
-
"
|
|
20
|
-
|
|
14
|
+
"interface": {
|
|
15
|
+
"displayName": "Makaron Creative Studio",
|
|
16
|
+
"shortDescription": "Give Codex a creative agent",
|
|
17
|
+
"longDescription": "Pass any complete creative request and source media to Makaron Chat. Makaron plans and produces images, video, music, and motion design in a persistent project that a human can continue editing in makaron.app.",
|
|
18
|
+
"developerName": "Versa AI",
|
|
19
|
+
"category": "Creative Tools",
|
|
20
|
+
"capabilities": ["Read", "Write"],
|
|
21
|
+
"websiteURL": "https://www.makaron.app/agent",
|
|
22
|
+
"privacyPolicyURL": "https://www.makaron.app/privacy",
|
|
23
|
+
"defaultPrompt": [
|
|
24
|
+
"Pass this complete creative brief and its source files to Makaron Chat.",
|
|
25
|
+
"Ask Makaron Chat to turn this image into the finished media described in my request.",
|
|
26
|
+
"Continue this Makaron project with my complete follow-up request."
|
|
27
|
+
],
|
|
28
|
+
"brandColor": "#d946ef"
|
|
29
|
+
}
|
|
21
30
|
}
|
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@ Returns immediately:
|
|
|
115
115
|
| Fix one moment in a video from a screenshot | `npx makaron-cli chat --project <id> --image screenshot.png "@4 this frame should be Paris; only fix this moment"` |
|
|
116
116
|
| Cut or assemble video | `npx makaron-cli chat --project <id> --video clip.mp4 "cut out the dead air and keep the best 20 seconds"` |
|
|
117
117
|
| Add music | `npx makaron-cli chat --project <id> "add calm piano background music"` |
|
|
118
|
-
| Beat-sync video from audio | `npx makaron-cli chat --project auto --audio beat.mp3
|
|
118
|
+
| Beat-sync video from audio | `npx makaron-cli chat --project auto --audio beat.mp3 "use Seedance Mini at 480p to make a beat-synced video"` |
|
|
119
119
|
| Create motion design | `npx makaron-cli chat --project <id> "make an animated Instagram story with this image"` |
|
|
120
120
|
|
|
121
121
|
### Marketplace skills
|
|
@@ -151,6 +151,41 @@ npx makaron-cli project media <projectId> --json
|
|
|
151
151
|
|
|
152
152
|
This is project-scoped. `responses get <runId> --pick output` only returns artifacts from one run; `project media` returns the whole project timeline: original uploads, references, generated images, video snapshots, and editable compositions.
|
|
153
153
|
|
|
154
|
+
External source ranges can be added without uploading either the original video or a derivative clip:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npx makaron-cli project media add <projectId> \
|
|
158
|
+
--source-url "https://cdn.example.com/source.mp4" \
|
|
159
|
+
--start-sec 12.5 --end-sec 19 \
|
|
160
|
+
--source-uri "dam://project/asset" \
|
|
161
|
+
--description "Racket frame molding"
|
|
162
|
+
|
|
163
|
+
# Batch form: a JSON array or {"source_ranges": [...]}
|
|
164
|
+
npx makaron-cli project media add <projectId> --input ranges.json --json
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The returned `<<<media_N>>>` is immediately usable by a later Agent run. Its range remains non-destructive metadata; Remotion must trim the original URL to these exact source bounds.
|
|
168
|
+
Use `description` as the provider-neutral media-understanding field. Put any
|
|
169
|
+
already-known summary, editorial purpose, concrete scene evidence, confidence,
|
|
170
|
+
and limitations there. Makaron exposes the full description in Media List
|
|
171
|
+
context so the Agent can edit from it without repeating image/video analysis
|
|
172
|
+
unless a required detail is missing or uncertain.
|
|
173
|
+
|
|
174
|
+
For an agent-to-agent handoff, create the project, import the external ranges,
|
|
175
|
+
and start the Agent in one command:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
npx makaron-cli chat --project auto \
|
|
179
|
+
--media-manifest set-01.json \
|
|
180
|
+
--json -b \
|
|
181
|
+
"Make a 30-second 9:16 TikTok with English VO and burned-in captions"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The manifest is a JSON array or `{ "title": "...", "source_ranges": [...] }`.
|
|
185
|
+
It is validated before project creation and supports up to 20 ranges for one
|
|
186
|
+
Makaron task. Batch planning remains the upstream orchestrator's responsibility:
|
|
187
|
+
convert each plan into one manifest and start one independent Makaron task.
|
|
188
|
+
|
|
154
189
|
### Export editable Remotion compositions
|
|
155
190
|
|
|
156
191
|
Animated Remotion compositions are saved as editable timeline/code artifacts first. To materialize one into an MP4 that CLI, V, or another service can read, call the backend export worker:
|
|
@@ -216,12 +251,13 @@ Attach a short song, beat, or voice recording when the video should follow audio
|
|
|
216
251
|
```bash
|
|
217
252
|
npx makaron-cli chat --project auto \
|
|
218
253
|
--audio beat.mp3 \
|
|
219
|
-
|
|
220
|
-
-b "make a 15s beat-synced video"
|
|
254
|
+
-b "use Seedance Mini at 480p to make a 15s beat-synced video"
|
|
221
255
|
```
|
|
222
256
|
|
|
223
257
|
`--audio` accepts repeatable local files or public URLs. Local MP3/WAV files must be 2-15s and <=15MB; reference audio currently works with Seedance video generation.
|
|
224
258
|
|
|
259
|
+
`chat` intentionally has no video model or resolution flags. State both in the chat message so the Agent selects a compatible provider and resolution together. Use `video create` only when you explicitly need direct provider controls.
|
|
260
|
+
|
|
225
261
|
### Fix one video moment from a screenshot
|
|
226
262
|
|
|
227
263
|
When a video is mostly good but one moment needs a local fix, attach a screenshot of the problem frame and describe the correction in normal language:
|
|
@@ -303,8 +339,9 @@ npx makaron-cli video create --script "Shot 1 (5s): <<<image_1>>> ..." --image h
|
|
|
303
339
|
npx makaron-cli video create --script "Shot 1 (5s): <<<image_1>>> slow cinematic push-in with native ambience" --image https://...jpg --duration 5 --video-model grok
|
|
304
340
|
npx makaron-cli video create --script "Shot 1 (15s): <<<image_1>>> and <<<image_2>>> build a neon one-person studio" --image https://...jpg --image https://...webp --duration 15 --video-model seedance-mini --video-resolution 480p --aspect 9:16
|
|
305
341
|
|
|
306
|
-
# 3b. Native SeeDance text-to-video (no image required)
|
|
342
|
+
# 3b. Native SeeDance or MiniMax H3 text-to-video (no image required)
|
|
307
343
|
npx makaron-cli video create --script "Shot 1 (5s): A neon one-person studio wakes at dawn" --duration 5 --video-model seedance-fast --aspect 16:9
|
|
344
|
+
npx makaron-cli video create --script "Shot 1 (15s): A premium creative editor comes alive" --duration 15 --video-model minimax-h3 --video-resolution 2k --aspect 16:9
|
|
308
345
|
|
|
309
346
|
# 3c. Edit a video from a local file or public URL
|
|
310
347
|
npx makaron-cli video create --script "make it funny" --video input.mp4 --duration 5 --video-model seedance-fast
|
|
@@ -320,9 +357,11 @@ For project/timeline video editing, use:
|
|
|
320
357
|
npx makaron-cli chat --project <id|auto> --video input.mp4 -b "make it funny"
|
|
321
358
|
```
|
|
322
359
|
|
|
323
|
-
Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to
|
|
360
|
+
Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to the selected model limit), `--video <file|url>` and `--audio <file|url>` (repeatable where supported), `--duration <seconds>`, `--aspect 9:16|16:9|1:1`, `--video-model seedance-fast|seedance-mini|seedance|seedance-2.5|kling|grok|google-omni|minimax-h3`, `--video-resolution auto|480p|720p|768p|1080p|2k|4k`. Default model is `seedance-fast`. SeeDance accepts native text-to-video with no image and integer output duration 4-15s (default 5s); `seedance-mini` supports 480p/720p and is best for cheaper drafts/multi-size tests; MiniMax H3 accepts native text-to-video, 4-15s output, and up to 9 image, up to 3 video, and up to 3 audio references through Makaron Agent/chat. H3 defaults to public 2k output; 768p requires provider preview access plus `MINIMAX_H3_ENABLE_768P=true` on the Makaron server. Kling supports 5-15s; Grok 1.5 supports 1-15s single-image-to-video only; Gemini Omni supports 3-10s fast 720p image/video generation and editing with native generated audio, including up to 6 image references when no video reference is provided. For `--video-model grok`, forced `--aspect` is ignored to avoid xAI stretching the source image; pad/create the image at the target shape first or use another model.
|
|
361
|
+
|
|
362
|
+
Seedance 2.5: use `--video-model seedance-2.5` for 4-30 second output at 480p/720p. `--image` accepts local files or URLs (up to 30), while repeatable `--video` and `--audio` accept up to 10 each. Use `--video-operation generate|edit|extend`, `--extend-direction forward|backward`, `--output-format mp4|mov`, `--web-search`, `--generated-audio` / `--no-generated-audio`, and `--relaxed-content-filter`. Edit/extend require a video reference. The Evolink route does not currently expose 4K output.
|
|
324
363
|
|
|
325
|
-
Video edit model behavior: `--video-model kling --video` uses Kling base/direct edit internally; `--video-model seedance-fast --video`, `--video-model seedance-mini --video`, or `--video-model seedance --video` uses the SeeDance video-reference path and requires target <=15s, <=50MB, width/height 300-6000px, aspect ratio 0.4-2.5, and frame pixels 409,600-2,086,876. `--video-model google-omni --video` uses Gemini Omni direct video editing and accepts one reference video in Makaron. Output duration is clamped to 3-10s. Grok does not support video references.
|
|
364
|
+
Video edit model behavior: `--video-model kling --video` uses Kling base/direct edit internally; `--video-model seedance-fast --video`, `--video-model seedance-mini --video`, or `--video-model seedance --video` uses the SeeDance video-reference path and requires target <=15s, <=50MB, width/height 300-6000px, aspect ratio 0.4-2.5, and frame pixels 409,600-2,086,876. `--video-model minimax-h3 --video` uses H3 feature/reference mode: up to 3 video references totaling <=15s, each <=50MB with width/height 256-5760px and aspect ratio 0.4-2.5. `--video-model google-omni --video` uses Gemini Omni direct video editing and accepts one reference video in Makaron. Output duration is clamped to 3-10s. Grok does not support video references.
|
|
326
365
|
|
|
327
366
|
### `music` — Music generation
|
|
328
367
|
|
|
@@ -435,7 +474,7 @@ send_message "All done!"
|
|
|
435
474
|
## Important Notes
|
|
436
475
|
|
|
437
476
|
- One project = one conversation thread. All history is preserved.
|
|
438
|
-
- One
|
|
477
|
+
- One active Agent Run at a time per project. A new message received while it is active is appended to that same Agent Run and processed at a durable work-unit boundary; it does not interrupt the execution or create a second owner for an in-progress Studio workflow.
|
|
439
478
|
- Multi-image: `create --image a.jpg --image b.jpg` or `chat --image ref.jpg`.
|
|
440
479
|
- Provider-generated videos can take 3-5 minutes; Grok is usually around 30-40 seconds; Gemini Omni is usually around 30-70 seconds plus Storage handoff. Remotion compositions should be converted with `materialize` / `responses get --materialize`, and timing should be read from `duration_seconds`, `render_seconds`, and `realtime_ratio`.
|
|
441
480
|
- Music takes ~60 seconds. Appears in output when done.
|
package/bin/makaron.mjs
CHANGED
|
@@ -45,11 +45,15 @@ const MAX_AUDIO_REFERENCE_DURATION_TOLERANCE = 0.5;
|
|
|
45
45
|
const MAX_AUDIO_REFERENCE_FILE_SIZE_MB = 15;
|
|
46
46
|
const MAX_AUDIO_REFERENCE_FILE_SIZE = MAX_AUDIO_REFERENCE_FILE_SIZE_MB * 1024 * 1024;
|
|
47
47
|
const MAX_VIDEO_FRAME_PIXELS = 2_086_876;
|
|
48
|
+
const SEEDANCE25_MAX_VIDEO_REFERENCE_DURATION = 30;
|
|
49
|
+
const SEEDANCE25_MAX_VIDEO_FRAME_PIXELS = 8_295_044;
|
|
48
50
|
const SEEDANCE_MIN_VIDEO_FRAME_PIXELS = 409_600;
|
|
49
51
|
const SEEDANCE_MIN_VIDEO_SIDE = 300;
|
|
50
52
|
const SEEDANCE_MAX_VIDEO_SIDE = 6000;
|
|
51
53
|
const SEEDANCE_MIN_VIDEO_ASPECT = 0.4;
|
|
52
54
|
const SEEDANCE_MAX_VIDEO_ASPECT = 2.5;
|
|
55
|
+
const MINIMAX_H3_MIN_VIDEO_SIDE = 256;
|
|
56
|
+
const MINIMAX_H3_MAX_VIDEO_SIDE = 5760;
|
|
53
57
|
|
|
54
58
|
function warnLegacyModelFlag(replacement) {
|
|
55
59
|
process.stderr.write(`⚠️ --model is deprecated here; use ${replacement}.\n`);
|
|
@@ -152,6 +156,71 @@ function readJsonInput(filePath) {
|
|
|
152
156
|
return JSON.parse(raw);
|
|
153
157
|
}
|
|
154
158
|
|
|
159
|
+
const MAX_MEDIA_MANIFEST_RANGES = 20;
|
|
160
|
+
|
|
161
|
+
function normalizeMediaManifest(input) {
|
|
162
|
+
const manifest = Array.isArray(input) ? { source_ranges: input } : input;
|
|
163
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
164
|
+
throw new Error('Media manifest must be a JSON object or an array of source ranges.');
|
|
165
|
+
}
|
|
166
|
+
const rawRanges = Array.isArray(manifest.source_ranges)
|
|
167
|
+
? manifest.source_ranges
|
|
168
|
+
: Array.isArray(manifest.sourceRanges)
|
|
169
|
+
? manifest.sourceRanges
|
|
170
|
+
: null;
|
|
171
|
+
if (!rawRanges?.length) {
|
|
172
|
+
throw new Error('Media manifest must contain a non-empty source_ranges array.');
|
|
173
|
+
}
|
|
174
|
+
if (rawRanges.length > MAX_MEDIA_MANIFEST_RANGES) {
|
|
175
|
+
throw new Error(`Media manifest supports at most ${MAX_MEDIA_MANIFEST_RANGES} source ranges per Makaron task.`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const sourceRanges = rawRanges.map((raw, index) => {
|
|
179
|
+
if (!raw || typeof raw !== 'object') throw new Error(`source_ranges[${index}] must be an object.`);
|
|
180
|
+
const sourceUrl = typeof raw.source_url === 'string' ? raw.source_url.trim() : '';
|
|
181
|
+
let parsed;
|
|
182
|
+
try {
|
|
183
|
+
parsed = new URL(sourceUrl);
|
|
184
|
+
} catch {
|
|
185
|
+
throw new Error(`source_ranges[${index}].source_url must be a valid HTTP(S) URL.`);
|
|
186
|
+
}
|
|
187
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
188
|
+
throw new Error(`source_ranges[${index}].source_url must use HTTP or HTTPS.`);
|
|
189
|
+
}
|
|
190
|
+
const startSec = Number(raw.start_sec);
|
|
191
|
+
const endSec = Number(raw.end_sec);
|
|
192
|
+
if (!Number.isFinite(startSec) || startSec < 0) {
|
|
193
|
+
throw new Error(`source_ranges[${index}].start_sec must be a finite number >= 0.`);
|
|
194
|
+
}
|
|
195
|
+
if (!Number.isFinite(endSec) || endSec <= startSec) {
|
|
196
|
+
throw new Error(`source_ranges[${index}].end_sec must be greater than start_sec.`);
|
|
197
|
+
}
|
|
198
|
+
const range = { source_url: sourceUrl, start_sec: startSec, end_sec: endSec };
|
|
199
|
+
for (const key of ['source_uri', 'project_id', 'asset_id', 'file_name', 'description']) {
|
|
200
|
+
if (typeof raw[key] === 'string' && raw[key].trim()) range[key] = raw[key].trim();
|
|
201
|
+
}
|
|
202
|
+
for (const key of ['width', 'height']) {
|
|
203
|
+
const value = Number(raw[key]);
|
|
204
|
+
if (Number.isFinite(value) && value > 0) range[key] = value;
|
|
205
|
+
}
|
|
206
|
+
return range;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
sourceRanges,
|
|
211
|
+
title: typeof manifest.title === 'string' && manifest.title.trim() ? manifest.title.trim() : undefined,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function readMediaManifest(filePath) {
|
|
216
|
+
try {
|
|
217
|
+
return normalizeMediaManifest(readJsonInput(filePath));
|
|
218
|
+
} catch (error) {
|
|
219
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
220
|
+
throw new Error(`Invalid media manifest: ${message}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
155
224
|
// ─── Auth ────────────────────────────────────────────────────────────────────
|
|
156
225
|
|
|
157
226
|
function loadAuth() {
|
|
@@ -281,8 +350,8 @@ Options:
|
|
|
281
350
|
--image <file|url> Attach a reference image or screenshot. Repeatable.
|
|
282
351
|
--video <file|url> Attach a video to the project timeline. Repeatable.
|
|
283
352
|
--audio <file|url> Attach a song, beat, or voice reference. MP3/WAV, repeatable.
|
|
353
|
+
--media-manifest <file|-> Import source_url + start_sec + end_sec ranges before this run.
|
|
284
354
|
--skill <id|label|name> Use an installed skill or auto-install a matched marketplace skill.
|
|
285
|
-
--video-resolution <res> Video resolution: auto, 480p, 720p, 1080p, or 4k.
|
|
286
355
|
--background, -b Submit and print a runId.
|
|
287
356
|
--json Output structured JSON.
|
|
288
357
|
--stream Legacy live SSE stream.
|
|
@@ -310,11 +379,14 @@ What you can ask:
|
|
|
310
379
|
Video cuts and assembly
|
|
311
380
|
makaron chat --project <id> --video clip.mp4 "cut out the dead air and keep the best 20 seconds"
|
|
312
381
|
|
|
382
|
+
Agent-to-agent source-range handoff
|
|
383
|
+
makaron chat --project auto --media-manifest set-01.json -b --json "make a 30s vertical video"
|
|
384
|
+
|
|
313
385
|
Music
|
|
314
386
|
makaron chat --project <id> "add calm piano background music"
|
|
315
387
|
|
|
316
388
|
Reference audio / beat sync
|
|
317
|
-
makaron chat --project auto --audio beat.mp3
|
|
389
|
+
makaron chat --project auto --audio beat.mp3 "用 Seedance Mini 480p 做卡点视频"
|
|
318
390
|
makaron chat --project <id> --audio https://example.com/beat.mp3 "add this as the soundtrack"
|
|
319
391
|
|
|
320
392
|
Motion design
|
|
@@ -351,7 +423,6 @@ async function streamAgent(baseUrl, headers, projectId, prompt, opts = {}) {
|
|
|
351
423
|
projectId,
|
|
352
424
|
prompt,
|
|
353
425
|
headless: true,
|
|
354
|
-
...(opts.videoResolution ? { videoResolution: opts.videoResolution } : {}),
|
|
355
426
|
...(opts.uploadedVideoCount ? { uploadedVideoCount: opts.uploadedVideoCount } : {}),
|
|
356
427
|
...(opts.turnMediaCount ? { turnMediaCount: opts.turnMediaCount } : {}),
|
|
357
428
|
}),
|
|
@@ -460,7 +531,6 @@ async function streamAgent(baseUrl, headers, projectId, prompt, opts = {}) {
|
|
|
460
531
|
|
|
461
532
|
async function submitRun(baseUrl, headers, projectId, prompt, opts = {}) {
|
|
462
533
|
const body = { projectId, prompt };
|
|
463
|
-
if (opts.videoResolution) body.videoResolution = opts.videoResolution;
|
|
464
534
|
if (opts.currentSnapshotIndex != null) body.currentSnapshotIndex = opts.currentSnapshotIndex;
|
|
465
535
|
if (opts.isNsfw) body.isNsfw = opts.isNsfw;
|
|
466
536
|
if (opts.audioAttachments?.length) body.audioAttachments = opts.audioAttachments;
|
|
@@ -825,9 +895,33 @@ async function listProjectMedia(baseUrl, headers, projectId, opts = {}) {
|
|
|
825
895
|
const status = item.status && item.status !== 'completed' ? ` ${item.status}` : '';
|
|
826
896
|
const duration = typeof item.duration === 'number' ? ` ${item.duration}s` : '';
|
|
827
897
|
const dimensions = item.width && item.height ? ` ${item.width}x${item.height}` : '';
|
|
828
|
-
const description = item.description ? ` — ${item.description}` : '';
|
|
898
|
+
const description = item.description ? ` — ${String(item.description).replace(/\s*\n\s*/g, ' | ')}` : '';
|
|
899
|
+
const sourceRange = item.source_range || item.sourceRange;
|
|
900
|
+
const range = sourceRange
|
|
901
|
+
? ` source ${formatSeconds(sourceRange.start_sec)}-${formatSeconds(sourceRange.end_sec)}s`
|
|
902
|
+
: '';
|
|
829
903
|
const url = item.url ? `\n ${item.url}` : '';
|
|
830
|
-
console.log(` ${String(item.index).padStart(2)}. ${ref} [${item.type}${status}${duration}${dimensions}]${description}${url}`);
|
|
904
|
+
console.log(` ${String(item.index).padStart(2)}. ${ref} [${item.type}${status}${duration}${dimensions}${range}]${description}${url}`);
|
|
905
|
+
}
|
|
906
|
+
return data;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
async function addProjectMediaSourceRanges(baseUrl, headers, projectId, ranges, opts = {}) {
|
|
910
|
+
const res = await fetch(`${baseUrl}/api/projects/${projectId}/media`, {
|
|
911
|
+
method: 'POST',
|
|
912
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
913
|
+
body: JSON.stringify({ source_ranges: ranges }),
|
|
914
|
+
});
|
|
915
|
+
if (!res.ok) { console.error('Project media add failed:', await res.text()); process.exit(1); }
|
|
916
|
+
const data = await res.json();
|
|
917
|
+
if (opts.json && !opts.silent) {
|
|
918
|
+
console.log(JSON.stringify(data, null, 2));
|
|
919
|
+
return data;
|
|
920
|
+
}
|
|
921
|
+
if (!opts.silent) {
|
|
922
|
+
for (const item of data.media || []) {
|
|
923
|
+
console.log(`${item.ref} ${item.source_url} ${formatSeconds(item.start_sec)}-${formatSeconds(item.end_sec)}s${item.created ? '' : ' (existing)'}`);
|
|
924
|
+
}
|
|
831
925
|
}
|
|
832
926
|
return data;
|
|
833
927
|
}
|
|
@@ -1368,7 +1462,8 @@ function getAudioMimeFromExt(ext) {
|
|
|
1368
1462
|
return null;
|
|
1369
1463
|
}
|
|
1370
1464
|
|
|
1371
|
-
function validateAudioReferenceFile(audioPath) {
|
|
1465
|
+
function validateAudioReferenceFile(audioPath, options = {}) {
|
|
1466
|
+
const maxDuration = options.maxDuration ?? MAX_AUDIO_REFERENCE_DURATION;
|
|
1372
1467
|
if (!fs.existsSync(audioPath)) {
|
|
1373
1468
|
return { ok: false, error: `Audio file not found: ${audioPath}` };
|
|
1374
1469
|
}
|
|
@@ -1391,8 +1486,8 @@ function validateAudioReferenceFile(audioPath) {
|
|
|
1391
1486
|
if (duration < MIN_AUDIO_REFERENCE_DURATION) {
|
|
1392
1487
|
return { ok: false, error: `Audio too short: ${formatSeconds(duration)}s (min ${MIN_AUDIO_REFERENCE_DURATION}s).` };
|
|
1393
1488
|
}
|
|
1394
|
-
if (duration >
|
|
1395
|
-
return { ok: false, error: `Audio too long: ${formatSeconds(duration)}s (max ${
|
|
1489
|
+
if (duration > maxDuration + MAX_AUDIO_REFERENCE_DURATION_TOLERANCE) {
|
|
1490
|
+
return { ok: false, error: `Audio too long: ${formatSeconds(duration)}s (max ${maxDuration}s, with ${MAX_AUDIO_REFERENCE_DURATION_TOLERANCE}s metadata tolerance).` };
|
|
1396
1491
|
}
|
|
1397
1492
|
return { ok: true, mime, meta: { duration, fileSizeBytes: stat.size } };
|
|
1398
1493
|
}
|
|
@@ -1474,16 +1569,19 @@ function validateVideoFile(videoPath, options = {}) {
|
|
|
1474
1569
|
const maxSide = options.maxSide ?? Infinity;
|
|
1475
1570
|
const minAspect = options.minAspect ?? 0;
|
|
1476
1571
|
const maxAspect = options.maxAspect ?? Infinity;
|
|
1572
|
+
const allowedExtensions = options.allowedExtensions ?? ['mp4', 'mov', 'webm'];
|
|
1573
|
+
const maxFramePixels = options.maxFramePixels ?? MAX_VIDEO_FRAME_PIXELS;
|
|
1574
|
+
const maxFileSize = options.maxFileSize ?? MAX_VIDEO_UPLOAD_FILE_SIZE;
|
|
1477
1575
|
if (!fs.existsSync(videoPath)) {
|
|
1478
1576
|
return { ok: false, error: `Video file not found: ${videoPath}` };
|
|
1479
1577
|
}
|
|
1480
1578
|
const stat = fs.statSync(videoPath);
|
|
1481
|
-
if (stat.size >
|
|
1482
|
-
return { ok: false, error: `Video too large: ${(stat.size / 1024 / 1024).toFixed(1)}MB (max ${
|
|
1579
|
+
if (stat.size > maxFileSize) {
|
|
1580
|
+
return { ok: false, error: `Video too large: ${(stat.size / 1024 / 1024).toFixed(1)}MB (max ${(maxFileSize / 1024 / 1024).toFixed(0)}MB).` };
|
|
1483
1581
|
}
|
|
1484
1582
|
const ext = path.extname(videoPath).slice(1).toLowerCase();
|
|
1485
|
-
if (!
|
|
1486
|
-
return { ok: false, error: `Unsupported video format: .${ext}. Use
|
|
1583
|
+
if (!allowedExtensions.includes(ext)) {
|
|
1584
|
+
return { ok: false, error: `Unsupported video format: .${ext}. Use ${allowedExtensions.map(value => value.toUpperCase()).join(' or ')}.` };
|
|
1487
1585
|
}
|
|
1488
1586
|
const meta = probeLocalVideo(videoPath);
|
|
1489
1587
|
if (!meta) {
|
|
@@ -1492,8 +1590,8 @@ function validateVideoFile(videoPath, options = {}) {
|
|
|
1492
1590
|
if (meta.duration > maxDuration + durationTolerance) {
|
|
1493
1591
|
return { ok: false, error: `Video too long: ${formatSeconds(meta.duration)}s (max ${maxDuration}s, with ${durationTolerance}s metadata tolerance)` };
|
|
1494
1592
|
}
|
|
1495
|
-
if (meta.width * meta.height >
|
|
1496
|
-
return { ok: false, error: `Video resolution too high: ${meta.width}x${meta.height} (${meta.width * meta.height} px). Max is
|
|
1593
|
+
if (meta.width * meta.height > maxFramePixels) {
|
|
1594
|
+
return { ok: false, error: `Video resolution too high: ${meta.width}x${meta.height} (${meta.width * meta.height} px). Max is ${maxFramePixels} pixels.` };
|
|
1497
1595
|
}
|
|
1498
1596
|
const framePixels = meta.width * meta.height;
|
|
1499
1597
|
const aspect = meta.width / meta.height;
|
|
@@ -1588,6 +1686,8 @@ Commands:
|
|
|
1588
1686
|
credits Show current credit balance
|
|
1589
1687
|
list (ls) List all projects
|
|
1590
1688
|
project media <projectId> --json List timeline media for a project
|
|
1689
|
+
project media add <projectId> --source-url <url> --start-sec <n> --end-sec <n>
|
|
1690
|
+
Add an external source range without uploading video
|
|
1591
1691
|
create --image <file> Create project from local image
|
|
1592
1692
|
create --image-url <url> Create project from URL
|
|
1593
1693
|
create --title "name" Create empty project (text-to-image)
|
|
@@ -1596,6 +1696,8 @@ Commands:
|
|
|
1596
1696
|
chat --project <id> --skill <id> Use a built-in or marketplace skill
|
|
1597
1697
|
chat --project <id> --video <file> Attach video to conversation
|
|
1598
1698
|
chat --project <id> --audio <file> Attach song/beat/voice reference
|
|
1699
|
+
chat --project auto --media-manifest <file> "message"
|
|
1700
|
+
Create, import external ranges, and run Agent
|
|
1599
1701
|
chat --project <id> -b "message" Background: submit and print runId
|
|
1600
1702
|
chat --project <id> --stream "msg" Legacy: stream SSE in real-time
|
|
1601
1703
|
chat --project <id> --json "msg" Output structured JSON result
|
|
@@ -1703,9 +1805,12 @@ function printHelp(topic, subtopic) {
|
|
|
1703
1805
|
} else if (topic === 'credits' || topic === 'credit' || topic === 'balance') {
|
|
1704
1806
|
console.log('Usage: makaron credits [--json]');
|
|
1705
1807
|
} else if (topic === 'project' || topic === 'projects') {
|
|
1706
|
-
if (subtopic === 'media') console.log(
|
|
1808
|
+
if (subtopic === 'media') console.log(`Usage: makaron project media <projectId> [--json]
|
|
1809
|
+
makaron project media add <projectId> --source-url <url> --start-sec <n> --end-sec <n> [--source-uri <uri>] [--description <text>] [--json]
|
|
1810
|
+
makaron project media add <projectId> --input <ranges.json> [--json]`);
|
|
1707
1811
|
else console.log(`Project commands:
|
|
1708
1812
|
project media <projectId> --json List timeline media for a project
|
|
1813
|
+
project media add <projectId> ... Add external source_url + start_sec + end_sec media
|
|
1709
1814
|
`);
|
|
1710
1815
|
} else if (topic === 'abort') {
|
|
1711
1816
|
console.log('Usage: makaron abort <runId>');
|
|
@@ -1745,13 +1850,14 @@ Use with chat:
|
|
|
1745
1850
|
console.log('Usage: makaron analyze --video <file|url> ["question"]');
|
|
1746
1851
|
} else if (topic === 'video') {
|
|
1747
1852
|
if (subtopic === 'script') console.log('Usage: makaron video script --image <file> [--image <file>] [--lang en|zh] "direction"');
|
|
1748
|
-
else if (subtopic === 'create') console.log('Usage: makaron video create --script "..." [--image <url>
|
|
1853
|
+
else if (subtopic === 'create') console.log('Usage: makaron video create --script "..." [--image <url> ...] [--video <url> ...] [--audio <url> ...] [--duration 10] [--aspect 9:16] [--video-model seedance-fast|seedance-mini|seedance|seedance-2.5|kling|grok|google-omni|minimax-h3] [--operation generate|edit|extend] [--video-resolution auto|480p|720p|768p|1080p|2k|4k] [--keep-original-sound]');
|
|
1749
1854
|
else if (subtopic === 'status') console.log('Usage: makaron video status <taskId> | --snapshot <snapshotId> [--wait]');
|
|
1750
1855
|
else console.log(`Video commands:
|
|
1751
1856
|
video script --image <file> [--image <file>] "direction" Write video script
|
|
1752
1857
|
video create --script "..." --video-model seedance-fast Native text-to-video (no image required)
|
|
1858
|
+
video create --script "..." --video-model minimax-h3 --video-resolution 2k MiniMax H3 native 2K text-to-video
|
|
1753
1859
|
video create --script "..." --image <url> [--duration 10] Submit video task
|
|
1754
|
-
video create --script "..." --video <public-url> [--video-model seedance-fast|seedance-mini|seedance|kling|google-omni] Edit a video (
|
|
1860
|
+
video create --script "..." --video <public-url> [--video-model seedance-fast|seedance-mini|seedance|seedance-2.5|kling|google-omni|minimax-h3] Edit/reference a video (Grok does not support video refs)
|
|
1755
1861
|
video status <taskId> Check video status
|
|
1756
1862
|
video status --snapshot <snapshotId> [--wait] Check v2 video snapshot
|
|
1757
1863
|
`);
|
|
@@ -1862,18 +1968,29 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
|
|
|
1862
1968
|
let background = false;
|
|
1863
1969
|
let jsonOutput = false;
|
|
1864
1970
|
let activeSkill = undefined;
|
|
1865
|
-
let
|
|
1971
|
+
let mediaManifestPath = undefined;
|
|
1866
1972
|
for (let i = 1; i < args.length; i++) {
|
|
1867
1973
|
if (args[i] === '--project' && args[i + 1]) projectId = args[++i];
|
|
1868
1974
|
else if (args[i] === '--image' && args[i + 1]) chatImages.push(args[++i]);
|
|
1869
1975
|
else if (args[i] === '--video' && args[i + 1]) chatVideos.push(args[++i]);
|
|
1870
1976
|
else if (args[i] === '--audio' && args[i + 1]) chatAudios.push(args[++i]);
|
|
1977
|
+
else if (args[i] === '--media-manifest') {
|
|
1978
|
+
if (!args[i + 1] || args[i + 1].startsWith('--')) {
|
|
1979
|
+
process.stderr.write('❌ --media-manifest requires a JSON file path or -.\n');
|
|
1980
|
+
process.exit(1);
|
|
1981
|
+
}
|
|
1982
|
+
mediaManifestPath = args[++i];
|
|
1983
|
+
}
|
|
1984
|
+
else if (args[i].startsWith('--media-manifest=')) mediaManifestPath = args[i].slice('--media-manifest='.length);
|
|
1871
1985
|
else if (args[i] === '--skill' && args[i + 1]) activeSkill = args[++i];
|
|
1872
1986
|
else if (args[i].startsWith('--skill=')) activeSkill = args[i].slice('--skill='.length);
|
|
1873
1987
|
else if (args[i] === '--stream') useStream = true;
|
|
1874
1988
|
else if (args[i] === '--background' || args[i] === '-b') background = true;
|
|
1875
1989
|
else if (args[i] === '--json') jsonOutput = true;
|
|
1876
|
-
else if (args[i] === '--video-resolution'
|
|
1990
|
+
else if (args[i] === '--video-resolution' || args[i].startsWith('--video-resolution=')) {
|
|
1991
|
+
process.stderr.write('❌ makaron chat chooses video model and resolution together. Put the requested resolution in your chat message, for example: "use MiniMax H3 at 2K".\n');
|
|
1992
|
+
process.exit(1);
|
|
1993
|
+
}
|
|
1877
1994
|
else if (
|
|
1878
1995
|
['--agent-model', '--image-model', '--video-model', '--model'].includes(args[i])
|
|
1879
1996
|
|| ['--agent-model=', '--image-model=', '--video-model=', '--model='].some(prefix => args[i].startsWith(prefix))
|
|
@@ -1890,6 +2007,15 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
|
|
|
1890
2007
|
console.error('Run: makaron chat --help');
|
|
1891
2008
|
process.exit(1);
|
|
1892
2009
|
}
|
|
2010
|
+
let mediaManifest;
|
|
2011
|
+
if (mediaManifestPath) {
|
|
2012
|
+
try {
|
|
2013
|
+
mediaManifest = readMediaManifest(mediaManifestPath);
|
|
2014
|
+
} catch (error) {
|
|
2015
|
+
process.stderr.write(`❌ ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2016
|
+
process.exit(1);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
1893
2019
|
const { headers, baseUrl } = getAuth();
|
|
1894
2020
|
// Split images into URLs vs local files
|
|
1895
2021
|
const imageUrlList = chatImages.filter(p => p.startsWith('http://') || p.startsWith('https://'));
|
|
@@ -1946,13 +2072,31 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
|
|
|
1946
2072
|
const res = await fetch(`${baseUrl}/api/projects/create`, {
|
|
1947
2073
|
method: 'POST',
|
|
1948
2074
|
headers: { 'Content-Type': 'application/json', ...headers },
|
|
1949
|
-
body: JSON.stringify({ title: prompt.slice(0, 50) }),
|
|
2075
|
+
body: JSON.stringify({ title: mediaManifest?.title || prompt.slice(0, 50) }),
|
|
1950
2076
|
});
|
|
1951
2077
|
if (!res.ok) { process.stderr.write(`❌ Failed to create project: ${await res.text()}\n`); process.exit(1); }
|
|
1952
2078
|
const data = await res.json();
|
|
1953
2079
|
projectId = data.projectId;
|
|
1954
2080
|
process.stderr.write(`📦 Project created: ${projectId}\n`);
|
|
1955
2081
|
}
|
|
2082
|
+
let importedManifestMedia = [];
|
|
2083
|
+
if (mediaManifest) {
|
|
2084
|
+
const imported = await addProjectMediaSourceRanges(
|
|
2085
|
+
baseUrl,
|
|
2086
|
+
headers,
|
|
2087
|
+
projectId,
|
|
2088
|
+
mediaManifest.sourceRanges,
|
|
2089
|
+
{ silent: true },
|
|
2090
|
+
);
|
|
2091
|
+
importedManifestMedia = imported.media || [];
|
|
2092
|
+
if (importedManifestMedia.length !== mediaManifest.sourceRanges.length) {
|
|
2093
|
+
process.stderr.write(`❌ Imported ${importedManifestMedia.length}/${mediaManifest.sourceRanges.length} source range(s); aborting run.\n`);
|
|
2094
|
+
process.exit(1);
|
|
2095
|
+
}
|
|
2096
|
+
uploadedTurnMediaCount += importedManifestMedia.length;
|
|
2097
|
+
uploadedTurnVideoCount += importedManifestMedia.length;
|
|
2098
|
+
process.stderr.write(`📎 Imported ${importedManifestMedia.length} external source range(s) from media manifest\n`);
|
|
2099
|
+
}
|
|
1956
2100
|
// Upload additional images to existing project
|
|
1957
2101
|
if (imageFileList.length > 0 || imageUrlList.length > 0) {
|
|
1958
2102
|
const uploadedImageUrls = imageFileList.length
|
|
@@ -1988,7 +2132,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
|
|
|
1988
2132
|
const resolvedSkill = await resolveChatSkill(baseUrl, headers, activeSkill);
|
|
1989
2133
|
|
|
1990
2134
|
// Upload videos to project timeline (via /api/projects/create with videoUrls)
|
|
1991
|
-
|
|
2135
|
+
const finalPrompt = resolvedSkill ? `[Active skill: ${resolvedSkill}]\n${prompt}` : prompt;
|
|
1992
2136
|
let audioAttachments = [];
|
|
1993
2137
|
if (chatAudios.length > 0) {
|
|
1994
2138
|
const audioImports = [];
|
|
@@ -2079,7 +2223,6 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
|
|
|
2079
2223
|
if (useStream) {
|
|
2080
2224
|
// Legacy SSE mode
|
|
2081
2225
|
const { results } = await streamAgent(baseUrl, headers, projectId, finalPrompt, {
|
|
2082
|
-
videoResolution,
|
|
2083
2226
|
uploadedVideoCount: uploadedTurnVideoCount,
|
|
2084
2227
|
turnMediaCount: uploadedTurnMediaCount,
|
|
2085
2228
|
});
|
|
@@ -2092,7 +2235,6 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
|
|
|
2092
2235
|
} else {
|
|
2093
2236
|
// Default: fire-and-forget + poll
|
|
2094
2237
|
const { runId } = await submitRun(baseUrl, headers, projectId, finalPrompt, {
|
|
2095
|
-
videoResolution,
|
|
2096
2238
|
audioAttachments,
|
|
2097
2239
|
uploadedVideoCount: uploadedTurnVideoCount,
|
|
2098
2240
|
turnMediaCount: uploadedTurnMediaCount,
|
|
@@ -2100,7 +2242,20 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
|
|
|
2100
2242
|
if (background) {
|
|
2101
2243
|
// Just print runId and exit
|
|
2102
2244
|
if (jsonOutput) {
|
|
2103
|
-
console.log(JSON.stringify({
|
|
2245
|
+
console.log(JSON.stringify({
|
|
2246
|
+
runId,
|
|
2247
|
+
projectId,
|
|
2248
|
+
projectUrl: `${APP_URL}/projects/${projectId}`,
|
|
2249
|
+
status: 'running',
|
|
2250
|
+
...(importedManifestMedia.length ? {
|
|
2251
|
+
importedMedia: importedManifestMedia.map(item => ({
|
|
2252
|
+
ref: item.ref,
|
|
2253
|
+
source_url: item.source_url,
|
|
2254
|
+
start_sec: item.start_sec,
|
|
2255
|
+
end_sec: item.end_sec,
|
|
2256
|
+
})),
|
|
2257
|
+
} : {}),
|
|
2258
|
+
}));
|
|
2104
2259
|
} else {
|
|
2105
2260
|
console.log(runId);
|
|
2106
2261
|
}
|
|
@@ -2338,13 +2493,52 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
|
|
|
2338
2493
|
const { headers, baseUrl } = getAuth();
|
|
2339
2494
|
const sub = args[1];
|
|
2340
2495
|
if (sub === 'media') {
|
|
2341
|
-
const projectId = args[2];
|
|
2342
|
-
if (!projectId) { console.error('Usage: makaron project media <projectId> [--json]'); process.exit(1); }
|
|
2343
2496
|
const jsonOutput = args.includes('--json');
|
|
2344
|
-
|
|
2497
|
+
if (args[2] === 'add') {
|
|
2498
|
+
const projectId = args[3];
|
|
2499
|
+
if (!projectId) { console.error('Usage: makaron project media add <projectId> --source-url <url> --start-sec <n> --end-sec <n>'); process.exit(1); }
|
|
2500
|
+
const readOption = (name) => {
|
|
2501
|
+
const index = args.indexOf(name);
|
|
2502
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
2503
|
+
};
|
|
2504
|
+
const inputPath = readOption('--input');
|
|
2505
|
+
let ranges;
|
|
2506
|
+
if (inputPath) {
|
|
2507
|
+
const input = readJsonInput(inputPath);
|
|
2508
|
+
ranges = Array.isArray(input) ? input : input.source_ranges || input.sourceRanges;
|
|
2509
|
+
} else {
|
|
2510
|
+
const sourceUrl = readOption('--source-url');
|
|
2511
|
+
const startSec = Number(readOption('--start-sec'));
|
|
2512
|
+
const endSec = Number(readOption('--end-sec'));
|
|
2513
|
+
if (!sourceUrl || !Number.isFinite(startSec) || !Number.isFinite(endSec)) {
|
|
2514
|
+
console.error('Provide --source-url, --start-sec, and --end-sec, or --input <ranges.json>.');
|
|
2515
|
+
process.exit(1);
|
|
2516
|
+
}
|
|
2517
|
+
ranges = [{
|
|
2518
|
+
source_url: sourceUrl,
|
|
2519
|
+
start_sec: startSec,
|
|
2520
|
+
end_sec: endSec,
|
|
2521
|
+
...(readOption('--source-uri') ? { source_uri: readOption('--source-uri') } : {}),
|
|
2522
|
+
...(readOption('--project-id') ? { project_id: readOption('--project-id') } : {}),
|
|
2523
|
+
...(readOption('--asset-id') ? { asset_id: readOption('--asset-id') } : {}),
|
|
2524
|
+
...(readOption('--file-name') ? { file_name: readOption('--file-name') } : {}),
|
|
2525
|
+
...(readOption('--description') ? { description: readOption('--description') } : {}),
|
|
2526
|
+
}];
|
|
2527
|
+
}
|
|
2528
|
+
if (!Array.isArray(ranges) || !ranges.length) {
|
|
2529
|
+
console.error('Input must contain a non-empty source_ranges array.');
|
|
2530
|
+
process.exit(1);
|
|
2531
|
+
}
|
|
2532
|
+
await addProjectMediaSourceRanges(baseUrl, headers, projectId, ranges, { json: jsonOutput });
|
|
2533
|
+
} else {
|
|
2534
|
+
const projectId = args[2];
|
|
2535
|
+
if (!projectId) { console.error('Usage: makaron project media <projectId> [--json]'); process.exit(1); }
|
|
2536
|
+
await listProjectMedia(baseUrl, headers, projectId, { json: jsonOutput });
|
|
2537
|
+
}
|
|
2345
2538
|
} else {
|
|
2346
2539
|
console.log(`Project commands:
|
|
2347
2540
|
project media <projectId> --json List timeline media for a project
|
|
2541
|
+
project media add <projectId> ... Add external source_url + start_sec + end_sec media
|
|
2348
2542
|
`);
|
|
2349
2543
|
}
|
|
2350
2544
|
} else if (command === 'abort') {
|
|
@@ -2422,11 +2616,15 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
|
|
|
2422
2616
|
|
|
2423
2617
|
} else if (sub === 'create') {
|
|
2424
2618
|
const images = [];
|
|
2619
|
+
const videos = [];
|
|
2620
|
+
const audios = [];
|
|
2425
2621
|
let script = '', duration = undefined, aspectRatio = undefined, videoModel = undefined, videoResolution = undefined, wait = false;
|
|
2426
|
-
let
|
|
2622
|
+
let keepOriginalSound = false, videoOperation = undefined, extendDirection = undefined, outputFormat = undefined;
|
|
2623
|
+
let generateAudio = undefined, contentFilter = undefined, webSearch = false;
|
|
2427
2624
|
for (let i = 2; i < args.length; i++) {
|
|
2428
2625
|
if (args[i] === '--image' && args[i + 1]) images.push(args[++i]);
|
|
2429
|
-
else if (args[i] === '--video' && args[i + 1])
|
|
2626
|
+
else if (args[i] === '--video' && args[i + 1]) videos.push(args[++i]);
|
|
2627
|
+
else if (args[i] === '--audio' && args[i + 1]) audios.push(args[++i]);
|
|
2430
2628
|
else if (args[i] === '--script' && args[i + 1]) script = args[++i];
|
|
2431
2629
|
else if (args[i] === '--script-file' && args[i + 1]) script = fs.readFileSync(args[++i], 'utf-8');
|
|
2432
2630
|
else if (args[i] === '--duration' && args[i + 1]) duration = Number(args[++i]);
|
|
@@ -2439,6 +2637,13 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
|
|
|
2439
2637
|
else if (args[i] === '--video-resolution' && args[i + 1]) videoResolution = args[++i];
|
|
2440
2638
|
else if (args[i] === '--resolution' && args[i + 1]) videoResolution = args[++i];
|
|
2441
2639
|
else if (args[i] === '--keep-original-sound') keepOriginalSound = true;
|
|
2640
|
+
else if (args[i] === '--video-operation' && args[i + 1]) videoOperation = args[++i];
|
|
2641
|
+
else if (args[i] === '--extend-direction' && args[i + 1]) extendDirection = args[++i];
|
|
2642
|
+
else if (args[i] === '--output-format' && args[i + 1]) outputFormat = args[++i];
|
|
2643
|
+
else if (args[i] === '--no-generated-audio') generateAudio = false;
|
|
2644
|
+
else if (args[i] === '--generated-audio') generateAudio = true;
|
|
2645
|
+
else if (args[i] === '--relaxed-content-filter') contentFilter = false;
|
|
2646
|
+
else if (args[i] === '--web-search') webSearch = true;
|
|
2442
2647
|
else if (args[i] === '--project') {
|
|
2443
2648
|
console.error('Usage: video create no longer supports --project. Use: makaron chat --project <id> --video <file|url> "your request"');
|
|
2444
2649
|
process.exit(1);
|
|
@@ -2446,27 +2651,53 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
|
|
|
2446
2651
|
else if (args[i] === '--wait') wait = true;
|
|
2447
2652
|
}
|
|
2448
2653
|
const selectedVideoModel = videoModel || 'seedance-fast';
|
|
2449
|
-
const
|
|
2450
|
-
|
|
2451
|
-
|
|
2654
|
+
const isSeedance25 = selectedVideoModel === 'seedance-2.5';
|
|
2655
|
+
const isSeedanceModel = selectedVideoModel === 'seedance-fast' || selectedVideoModel === 'seedance-mini' || selectedVideoModel === 'seedance' || isSeedance25;
|
|
2656
|
+
const isMinimaxH3 = selectedVideoModel === 'minimax-h3';
|
|
2657
|
+
const supportsNativeTextToVideo = isSeedanceModel || isMinimaxH3;
|
|
2658
|
+
if (!script || (!images.length && !videos.length && !audios.length && !supportsNativeTextToVideo)) {
|
|
2659
|
+
console.error('Usage: makaron video create --script "..." [--image <url>] [--video <file|url>] [--audio <file|url>] [--duration 30] [--video-model seedance-2.5|minimax-h3]');
|
|
2452
2660
|
process.exit(1);
|
|
2453
2661
|
}
|
|
2662
|
+
if (isSeedance25 && images.length > 30) { console.error('Seedance 2.5 supports at most 30 image references.'); process.exit(1); }
|
|
2663
|
+
if (isSeedance25 && videos.length > 10) { console.error('Seedance 2.5 supports at most 10 video references.'); process.exit(1); }
|
|
2664
|
+
if (isSeedance25 && audios.length > 10) { console.error('Seedance 2.5 supports at most 10 audio references.'); process.exit(1); }
|
|
2665
|
+
if (isMinimaxH3 && images.length > 9) { console.error('MiniMax H3 supports at most 9 image references.'); process.exit(1); }
|
|
2666
|
+
if (isMinimaxH3 && videos.length > 3) { console.error('MiniMax H3 supports at most 3 video references.'); process.exit(1); }
|
|
2667
|
+
if (isMinimaxH3 && audios.length > 3) { console.error('MiniMax H3 supports at most 3 audio references.'); process.exit(1); }
|
|
2668
|
+
if (videoOperation && !['generate', 'edit', 'extend'].includes(videoOperation)) { console.error('--video-operation must be generate, edit, or extend.'); process.exit(1); }
|
|
2669
|
+
if (extendDirection && !['forward', 'backward'].includes(extendDirection)) { console.error('--extend-direction must be forward or backward.'); process.exit(1); }
|
|
2670
|
+
if (outputFormat && !['mp4', 'mov'].includes(outputFormat)) { console.error('--output-format must be mp4 or mov.'); process.exit(1); }
|
|
2454
2671
|
|
|
2455
2672
|
if (wait) {
|
|
2456
2673
|
console.error('Usage: --wait is only supported for project timeline tasks. Use chat --project for project video generation, or poll the returned taskId with video status.');
|
|
2457
2674
|
process.exit(1);
|
|
2458
2675
|
}
|
|
2459
2676
|
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2677
|
+
const providerMaxDuration = isSeedance25 ? SEEDANCE25_MAX_VIDEO_REFERENCE_DURATION : MAX_VIDEO_PROVIDER_REFERENCE_DURATION;
|
|
2678
|
+
const providerMaxPixels = isMinimaxH3 ? Infinity : isSeedance25 ? SEEDANCE25_MAX_VIDEO_FRAME_PIXELS : MAX_VIDEO_FRAME_PIXELS;
|
|
2679
|
+
const localImages = images.filter(image => !isHttpUrl(image));
|
|
2680
|
+
if (localImages.length) {
|
|
2681
|
+
const uploadedImages = await uploadImageFilesViaSignedUrl(baseUrl, headers, undefined, localImages);
|
|
2682
|
+
const uploadedByPath = new Map(localImages.map((image, index) => [image, uploadedImages[index]]));
|
|
2683
|
+
images = images.map(image => isHttpUrl(image) ? image : uploadedByPath.get(image)).filter(Boolean);
|
|
2464
2684
|
}
|
|
2465
|
-
|
|
2685
|
+
const videoUrls = [];
|
|
2686
|
+
let inputVideoMeta = null;
|
|
2687
|
+
for (const video of videos) {
|
|
2688
|
+
if (isHttpUrl(video)) { videoUrls.push(video); continue; }
|
|
2466
2689
|
const valid = validateVideoFile(video, {
|
|
2467
|
-
maxDuration:
|
|
2690
|
+
maxDuration: providerMaxDuration,
|
|
2468
2691
|
durationTolerance: MAX_VIDEO_PROVIDER_REFERENCE_DURATION_TOLERANCE,
|
|
2469
|
-
|
|
2692
|
+
maxFramePixels: providerMaxPixels,
|
|
2693
|
+
maxFileSize: isSeedance25 ? 200 * 1024 * 1024 : MAX_VIDEO_UPLOAD_FILE_SIZE,
|
|
2694
|
+
...(selectedVideoModel === 'minimax-h3' ? {
|
|
2695
|
+
allowedExtensions: ['mp4', 'mov'],
|
|
2696
|
+
minSide: MINIMAX_H3_MIN_VIDEO_SIDE,
|
|
2697
|
+
maxSide: MINIMAX_H3_MAX_VIDEO_SIDE,
|
|
2698
|
+
minAspect: SEEDANCE_MIN_VIDEO_ASPECT,
|
|
2699
|
+
maxAspect: SEEDANCE_MAX_VIDEO_ASPECT,
|
|
2700
|
+
} : isSeedanceModel ? {
|
|
2470
2701
|
minFramePixels: SEEDANCE_MIN_VIDEO_FRAME_PIXELS,
|
|
2471
2702
|
minSide: SEEDANCE_MIN_VIDEO_SIDE,
|
|
2472
2703
|
maxSide: SEEDANCE_MAX_VIDEO_SIDE,
|
|
@@ -2475,22 +2706,38 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
|
|
|
2475
2706
|
} : {}),
|
|
2476
2707
|
});
|
|
2477
2708
|
if (!valid.ok) { console.error(`❌ ${valid.error}`); process.exit(1); }
|
|
2478
|
-
inputVideoMeta
|
|
2709
|
+
inputVideoMeta ||= valid.meta;
|
|
2479
2710
|
process.stderr.write(`📹 Uploading ${path.basename(video)} (${(fs.statSync(video).size/1024/1024).toFixed(1)}MB)...\n`);
|
|
2480
|
-
|
|
2481
|
-
if (!
|
|
2711
|
+
const uploaded = await uploadFileViaSignedUrl(baseUrl, headers, undefined, video, valid.mime);
|
|
2712
|
+
if (!uploaded) process.exit(1);
|
|
2713
|
+
videoUrls.push(uploaded);
|
|
2482
2714
|
process.stderr.write(`📹 Uploaded: ${path.basename(video)}\n`);
|
|
2483
2715
|
}
|
|
2716
|
+
const audioUrls = [];
|
|
2717
|
+
for (const audio of audios) {
|
|
2718
|
+
if (isHttpUrl(audio)) { audioUrls.push(audio); continue; }
|
|
2719
|
+
const valid = validateAudioReferenceFile(audio, { maxDuration: isSeedance25 ? 30 : MAX_AUDIO_REFERENCE_DURATION });
|
|
2720
|
+
if (!valid.ok) { console.error(`❌ ${valid.error}`); process.exit(1); }
|
|
2721
|
+
process.stderr.write(`🎵 Uploading ${path.basename(audio)}...\n`);
|
|
2722
|
+
const uploaded = await uploadFileViaSignedUrl(baseUrl, headers, undefined, audio, valid.mime, { uploadKind: 'audio' });
|
|
2723
|
+
if (!uploaded) process.exit(1);
|
|
2724
|
+
audioUrls.push(uploaded);
|
|
2725
|
+
}
|
|
2484
2726
|
// Standalone MCP tool (no project timeline write)
|
|
2485
2727
|
process.stderr.write('🎬 Submitting video...\n');
|
|
2486
|
-
const
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2728
|
+
const resolvedOperation = videoOperation || (isSeedance25 && videoUrls.length ? 'edit' : 'generate');
|
|
2729
|
+
const vArgs = isSeedance25
|
|
2730
|
+
? { script, images, videoUrls, audioUrls, videoModel: selectedVideoModel, videoResolution, operation: resolvedOperation, extendDirection, outputFormat, generateAudio, contentFilter, webSearch }
|
|
2731
|
+
: isMinimaxH3
|
|
2732
|
+
? { script, images, videoUrls, audioUrls, videoModel: selectedVideoModel, videoResolution }
|
|
2733
|
+
: videoUrls[0]
|
|
2734
|
+
? { videoUrl: videoUrls[0], editPrompt: script, images, videoModel: selectedVideoModel, videoResolution, referType: isSeedanceModel ? 'feature' : 'base' }
|
|
2735
|
+
: { script, images, videoModel: selectedVideoModel, videoResolution };
|
|
2736
|
+
const effectiveDuration = resolvedOperation === 'edit' ? undefined : duration || (inputVideoMeta?.duration ? Math.min(providerMaxDuration, Math.round(inputVideoMeta.duration)) : undefined);
|
|
2490
2737
|
if (effectiveDuration) vArgs.duration = effectiveDuration;
|
|
2491
2738
|
if (aspectRatio) vArgs.aspectRatio = aspectRatio;
|
|
2492
|
-
if (keepOriginalSound &&
|
|
2493
|
-
const result = await callMcpTool(baseUrl, headers,
|
|
2739
|
+
if (keepOriginalSound && videoUrls.length && !isSeedance25) vArgs.keepOriginalSound = true;
|
|
2740
|
+
const result = await callMcpTool(baseUrl, headers, videoUrls.length && !isSeedance25 && !isMinimaxH3 ? 'makaron_edit_video' : 'makaron_create_video', vArgs);
|
|
2494
2741
|
const text = result?.content?.find(c => c.type === 'text')?.text;
|
|
2495
2742
|
if (text) {
|
|
2496
2743
|
console.log(text);
|
|
@@ -2534,8 +2781,9 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
|
|
|
2534
2781
|
console.log(`Video commands:
|
|
2535
2782
|
video script --image <file> [--image <file>] "direction" Write video script
|
|
2536
2783
|
video create --script "..." --video-model seedance-fast Native text-to-video (no image required)
|
|
2784
|
+
video create --script "..." --video-model minimax-h3 --video-resolution 2k MiniMax H3 native 2K text-to-video
|
|
2537
2785
|
video create --script "..." --image <url> [--duration 10] Submit video task
|
|
2538
|
-
video create --script "..." --video <public-url> [--video-model seedance-fast|seedance-mini|seedance|kling|google-omni] Edit a video (
|
|
2786
|
+
video create --script "..." --video <public-url> [--video-model seedance-fast|seedance-mini|seedance|seedance-2.5|kling|google-omni|minimax-h3] Edit/reference a video (Grok does not support video refs)
|
|
2539
2787
|
video status <taskId> Check video status
|
|
2540
2788
|
video status --snapshot <snapshotId> [--wait] Check v2 video snapshot
|
|
2541
2789
|
`);
|
package/package.json
CHANGED
package/skills/makaron/SKILL.md
CHANGED
|
@@ -102,7 +102,7 @@ Returns immediately:
|
|
|
102
102
|
| Fix one moment in a video from a screenshot | `npx makaron-cli chat --project <id> --image screenshot.png "@4 this frame should be Paris; only fix this moment"` |
|
|
103
103
|
| Cut or assemble video | `npx makaron-cli chat --project <id> --video clip.mp4 "cut out the dead air and keep the best 20 seconds"` |
|
|
104
104
|
| Add music | `npx makaron-cli chat --project <id> "add calm piano background music"` |
|
|
105
|
-
| Beat-sync video from audio | `npx makaron-cli chat --project auto --audio beat.mp3
|
|
105
|
+
| Beat-sync video from audio | `npx makaron-cli chat --project auto --audio beat.mp3 "use Seedance Mini at 480p to make a beat-synced video"` |
|
|
106
106
|
| Create motion design | `npx makaron-cli chat --project <id> "make an animated Instagram story with this image"` |
|
|
107
107
|
|
|
108
108
|
### Marketplace skills
|
|
@@ -149,6 +149,21 @@ npx makaron-cli project media <projectId> --json
|
|
|
149
149
|
|
|
150
150
|
This is project-scoped. `responses get <runId> --pick output` only returns artifacts from one run; `project media` returns the whole project timeline: original uploads, references, generated images, video snapshots, and editable compositions.
|
|
151
151
|
|
|
152
|
+
Publish an external video interval directly into that Media List without uploading the original or a derivative MP4:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npx makaron-cli project media add <projectId> --source-url "https://cdn.example.com/source.mp4" --start-sec 12.5 --end-sec 19 --source-uri "dam://project/asset" --description "Racket frame molding"
|
|
156
|
+
npx makaron-cli project media add <projectId> --input ranges.json --json
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The JSON input may be an array or `{ "source_ranges": [...] }`. Each item uses `source_url + start_sec + end_sec`; optional `source_uri`, `project_id`, and `asset_id` preserve durable source identity. Put existing media understanding (summary, editorial purpose, scene evidence, confidence, and limitations) in `description`. Makaron reads that provider-neutral Media List field before deciding whether any additional image/video analysis is needed.
|
|
160
|
+
|
|
161
|
+
For one-call orchestration, use `chat --project auto --media-manifest plan.json`. Makaron validates the manifest, creates the project, imports its ranges, and starts the Agent. If an upstream service returns multiple plans, the caller should start one independent Makaron task per plan instead of passing the provider-specific batch response into Makaron.
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npx makaron-cli chat --project auto --media-manifest set-01.json --json -b "Make a 30-second 9:16 TikTok with English VO and captions"
|
|
165
|
+
```
|
|
166
|
+
|
|
152
167
|
### With video input (edit, compose, extend)
|
|
153
168
|
|
|
154
169
|
```bash
|
|
@@ -176,12 +191,13 @@ Attach a short song, beat, or voice recording when the video should follow audio
|
|
|
176
191
|
```bash
|
|
177
192
|
npx makaron-cli chat --project auto \
|
|
178
193
|
--audio beat.mp3 \
|
|
179
|
-
|
|
180
|
-
-b "make a 15s beat-synced video"
|
|
194
|
+
-b "use Seedance Mini at 480p to make a 15s beat-synced video"
|
|
181
195
|
```
|
|
182
196
|
|
|
183
197
|
`--audio` accepts repeatable local files or public URLs. Local MP3/WAV files must be 2-15s and <=15MB; reference audio currently works with Seedance video generation.
|
|
184
198
|
|
|
199
|
+
`chat` intentionally has no video model or resolution flags. State both in the chat message so the Agent selects a compatible provider and resolution together. Use `video create` only when you explicitly need direct provider controls.
|
|
200
|
+
|
|
185
201
|
### Fix one video moment from a screenshot
|
|
186
202
|
|
|
187
203
|
When a video is mostly good but one moment needs a local fix, attach a screenshot of the problem frame and describe the correction in normal language:
|
|
@@ -262,8 +278,9 @@ npx makaron-cli analyze --video input.mp4 "describe the key actions and pacing"
|
|
|
262
278
|
npx makaron-cli video create --script "Shot 1 (5s): <<<image_1>>> ..." --image https://...jpg --duration 5 --video-model kling
|
|
263
279
|
npx makaron-cli video create --script "Shot 1 (15s): <<<image_1>>> and <<<image_2>>> build a neon one-person studio" --image https://...jpg --image https://...webp --duration 15 --video-model seedance-mini --video-resolution 480p --aspect 9:16
|
|
264
280
|
|
|
265
|
-
# 3b. Native SeeDance text-to-video (no image required)
|
|
281
|
+
# 3b. Native SeeDance or MiniMax H3 text-to-video (no image required)
|
|
266
282
|
npx makaron-cli video create --script "Shot 1 (5s): A neon one-person studio wakes at dawn" --duration 5 --video-model seedance-fast --aspect 16:9
|
|
283
|
+
npx makaron-cli video create --script "Shot 1 (15s): A premium creative editor comes alive" --duration 15 --video-model minimax-h3 --video-resolution 2k --aspect 16:9
|
|
267
284
|
|
|
268
285
|
# 3c. Edit a video from a local file or public URL
|
|
269
286
|
npx makaron-cli video create --script "make it funny" --video input.mp4 --duration 5 --video-model seedance-fast
|
|
@@ -279,9 +296,11 @@ npx makaron-cli video status <taskId>
|
|
|
279
296
|
npx makaron-cli chat --project <id|auto> --video input.mp4 -b "make it funny"
|
|
280
297
|
```
|
|
281
298
|
|
|
282
|
-
Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to
|
|
299
|
+
Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to the selected model limit), `--video <file|url>` and `--audio <file|url>` (repeatable where supported), `--duration <seconds>`, `--aspect 9:16|16:9|1:1`, `--video-model seedance-fast|seedance-mini|seedance|seedance-2.5|kling|grok|google-omni|minimax-h3`, `--video-resolution auto|480p|720p|768p|1080p|2k|4k`. Default model is `seedance-fast`. SeeDance accepts native text-to-video with no image and integer output duration 4-15s (default 5s); `seedance-mini` supports 480p/720p and is best for cheaper drafts/multi-size tests; MiniMax H3 accepts native text-to-video, 4-15s output, and up to 9 image, up to 3 video, and up to 3 audio references through Makaron Agent/chat. H3 defaults to public 2k output; 768p requires provider preview access plus `MINIMAX_H3_ENABLE_768P=true` on the Makaron server. Kling supports 5-15s; Grok 1.5 supports 1-15s single-image-to-video only; Gemini Omni supports 3-10s fast 720p image/video generation and editing with native generated audio, including up to 6 image references when no video reference is provided. For `--video-model grok`, forced `--aspect` is ignored to avoid xAI stretching the source image; pad/create the image at the target shape first or use another model.
|
|
300
|
+
|
|
301
|
+
Seedance 2.5 uses `--video-model seedance-2.5` and supports 4-30s at 480p/720p, up to 30 images + 10 videos + 10 audios, repeatable local/URL references, `--video-operation generate|edit|extend`, `--extend-direction`, `--output-format mp4|mov`, and `--web-search`. The Evolink route does not currently expose 4K output.
|
|
283
302
|
|
|
284
|
-
Video edit model behavior: `--video-model kling --video` uses Kling base/direct edit internally; `--video-model seedance-fast --video`, `--video-model seedance-mini --video`, or `--video-model seedance --video` uses the SeeDance video-reference path and requires target <=15s, <=50MB, width/height 300-6000px, aspect ratio 0.4-2.5, and frame pixels 409,600-2,086,876. `--video-model google-omni --video` uses Gemini Omni direct video editing and accepts one reference video in Makaron. Output duration is clamped to 3-10s. Grok does not support video references.
|
|
303
|
+
Video edit model behavior: `--video-model kling --video` uses Kling base/direct edit internally; `--video-model seedance-fast --video`, `--video-model seedance-mini --video`, or `--video-model seedance --video` uses the SeeDance video-reference path and requires target <=15s, <=50MB, width/height 300-6000px, aspect ratio 0.4-2.5, and frame pixels 409,600-2,086,876. `--video-model minimax-h3 --video` uses H3 feature/reference mode: up to 3 video references totaling <=15s, each <=50MB with width/height 256-5760px and aspect ratio 0.4-2.5. `--video-model google-omni --video` uses Gemini Omni direct video editing and accepts one reference video in Makaron. Output duration is clamped to 3-10s. Grok does not support video references.
|
|
285
304
|
|
|
286
305
|
### `music` — Music generation
|
|
287
306
|
|
|
@@ -412,7 +431,7 @@ send_message "All done!"
|
|
|
412
431
|
## Important Notes
|
|
413
432
|
|
|
414
433
|
- One project = one conversation thread. All history is preserved.
|
|
415
|
-
- One
|
|
434
|
+
- One active Agent Run at a time per project. A new message received while it is active is appended to that same Agent Run and processed at a durable work-unit boundary; it does not interrupt the execution or create a second owner for an in-progress Studio workflow.
|
|
416
435
|
- Multi-image: `create --image a.jpg --image b.jpg` or `chat --image ref.jpg`.
|
|
417
436
|
- Provider-generated videos can take 3-5 minutes; Grok is usually around 30-40 seconds; Gemini Omni is usually around 30-70 seconds plus Storage handoff. Remotion compositions should be converted with `materialize` / `responses get --materialize`, and timing should be read from `duration_seconds`, `render_seconds`, and `realtime_ratio`.
|
|
418
437
|
- Music takes ~60 seconds. Appears in output when done.
|