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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.13.3",
4
- "description": "AI image editing, video generation, music creation, and marketplace skill workflows via CLI. Agents can self-register, install skills, create projects, and produce creative media.",
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": "Makaron AI",
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.3",
4
- "description": "AI image editing, video generation, music creation, and marketplace skill workflows via CLI. Agents can self-register, install skills, create projects, and produce creative media.",
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": "Makaron AI",
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": ["ai", "image-editing", "video", "music", "agent", "creative", "skills"],
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
- "websiteURL": "https://www.makaron.app",
20
- "brandColor": "#d946ef"
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 --video-resolution 480p "make a beat-synced video"` |
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
- --video-resolution 480p \
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 7), `--video <file|url>`, `--duration <seconds>`, `--aspect 9:16|16:9|1:1`, `--video-model seedance-fast|seedance-mini|seedance|kling|grok|google-omni`, `--video-resolution auto|480p|720p|1080p|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; 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.
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 run at a time per project. New message interrupts previous run.
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 --video-resolution 480p "用这个音乐做卡点视频"
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 > MAX_AUDIO_REFERENCE_DURATION + MAX_AUDIO_REFERENCE_DURATION_TOLERANCE) {
1395
- return { ok: false, error: `Audio too long: ${formatSeconds(duration)}s (max ${MAX_AUDIO_REFERENCE_DURATION}s, with ${MAX_AUDIO_REFERENCE_DURATION_TOLERANCE}s metadata tolerance).` };
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 > MAX_VIDEO_UPLOAD_FILE_SIZE) {
1482
- return { ok: false, error: `Video too large: ${(stat.size / 1024 / 1024).toFixed(1)}MB (max ${MAX_VIDEO_UPLOAD_FILE_SIZE_MB}MB). The CLI uploads directly to Storage; use the frontend to transcode larger videos first.` };
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 (!['mp4', 'mov', 'webm'].includes(ext)) {
1486
- return { ok: false, error: `Unsupported video format: .${ext}. Use MP4, MOV, or WebM.` };
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 > MAX_VIDEO_FRAME_PIXELS) {
1496
- return { ok: false, error: `Video resolution too high: ${meta.width}x${meta.height} (${meta.width * meta.height} px). Max is <=1080p (${MAX_VIDEO_FRAME_PIXELS} px). Re-upload through the frontend to transcode, or export a smaller video.` };
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('Usage: makaron project media <projectId> [--json]');
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> | --video <public-url>] [--duration 10] [--aspect 9:16] [--video-model seedance-fast|seedance-mini|seedance|kling|grok|google-omni] [--video-resolution auto|480p|720p|1080p|4k] [--keep-original-sound]');
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 (standalone; Grok does not support video refs)
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 videoResolution = undefined;
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' && args[i + 1]) videoResolution = args[++i];
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
- let finalPrompt = resolvedSkill ? `[Active skill: ${resolvedSkill}]\n${prompt}` : prompt;
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({ runId, projectId, projectUrl: `${APP_URL}/projects/${projectId}`, status: 'running' }));
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
- await listProjectMedia(baseUrl, headers, projectId, { json: jsonOutput });
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 video = null, keepOriginalSound = false;
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]) video = args[++i];
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 isSeedanceModel = selectedVideoModel === 'seedance-fast' || selectedVideoModel === 'seedance-mini' || selectedVideoModel === 'seedance';
2450
- if (!script || (!images.length && !video && !isSeedanceModel)) {
2451
- console.error('Usage: makaron video create --script "..." [--image <url> | --video <public-url>] [--duration 10] [--aspect 9:16] [--video-model seedance-fast|seedance-mini|seedance|kling|grok|google-omni] [--video-resolution auto|480p|720p|1080p|4k] [--keep-original-sound]');
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
- let videoUrl = isHttpUrl(video) ? video : null;
2461
- let inputVideoMeta = null;
2462
- if (videoUrl) {
2463
- process.stderr.write(`📹 Assuming public video URL already matches provider reference limits. Seedance requires ≤${MAX_VIDEO_PROVIDER_REFERENCE_DURATION}s, ≤50MB, sides 300-6000px, frame pixels 409,600-${MAX_VIDEO_FRAME_PIXELS}; Kling requires ≤200MB and ≤2K; Google Omni accepts one reference video in Makaron; Grok does not support video references.\n`);
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
- if (video && !videoUrl) {
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: MAX_VIDEO_PROVIDER_REFERENCE_DURATION,
2690
+ maxDuration: providerMaxDuration,
2468
2691
  durationTolerance: MAX_VIDEO_PROVIDER_REFERENCE_DURATION_TOLERANCE,
2469
- ...(selectedVideoModel === 'seedance' || selectedVideoModel === 'seedance-fast' || selectedVideoModel === 'seedance-mini' ? {
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 = valid.meta;
2709
+ inputVideoMeta ||= valid.meta;
2479
2710
  process.stderr.write(`📹 Uploading ${path.basename(video)} (${(fs.statSync(video).size/1024/1024).toFixed(1)}MB)...\n`);
2480
- videoUrl = await uploadFileViaSignedUrl(baseUrl, headers, undefined, video, valid.mime);
2481
- if (!videoUrl) process.exit(1);
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 vArgs = videoUrl
2487
- ? { videoUrl, editPrompt: script, images, videoModel: selectedVideoModel, videoResolution, referType: (selectedVideoModel === 'seedance' || selectedVideoModel === 'seedance-fast' || selectedVideoModel === 'seedance-mini') ? 'feature' : 'base' }
2488
- : { script, images, videoModel: selectedVideoModel, videoResolution };
2489
- const effectiveDuration = duration || (inputVideoMeta?.duration ? Math.min(MAX_VIDEO_PROVIDER_REFERENCE_DURATION, Math.round(inputVideoMeta.duration)) : undefined);
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 && videoUrl) vArgs.keepOriginalSound = true;
2493
- const result = await callMcpTool(baseUrl, headers, videoUrl ? 'makaron_edit_video' : 'makaron_create_video', vArgs);
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 (standalone; Grok does not support video refs)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.13.3",
3
+ "version": "0.13.5",
4
4
  "description": "Talk to Makaron Agent from the terminal — create projects, edit images, generate videos",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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 --video-resolution 480p "make a beat-synced video"` |
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
- --video-resolution 480p \
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 7), `--video <file|url>`, `--duration <seconds>`, `--aspect 9:16|16:9|1:1`, `--video-model seedance-fast|seedance-mini|seedance|kling|grok|google-omni`, `--video-resolution auto|480p|720p|1080p|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; 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.
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 run at a time per project. New message interrupts previous run.
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.