makaron-cli 0.11.5 → 0.12.1

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/README.md CHANGED
@@ -143,6 +143,47 @@ npx makaron-cli project media <projectId> --json
143
143
 
144
144
  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.
145
145
 
146
+ ### Export editable Remotion compositions
147
+
148
+ 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:
149
+
150
+ ```bash
151
+ npx makaron-cli materialize --project <projectId> --media <N> --pick url
152
+ npx makaron-cli materialize --project <projectId> --design-json composition.json --pick url
153
+ npx makaron-cli composition export --project <projectId> --media <N> --wait
154
+ npx makaron-cli composition export --project <projectId> --snapshot <snapshotId> --wait
155
+ npx makaron-cli composition status <jobId> --wait
156
+ ```
157
+
158
+ `materialize` is the preferred high-level command for Remotion-to-MP4. It defaults to `--wait`, `--publish`, and the `fast_720p` profile (short side 720, no upscale), so the completed MP4 is also added back to the project timeline like CUI. Use `--no-publish` only when you need a file URL without a new timeline video. Use `--profile source` only when full source resolution is required.
159
+
160
+ For a run that produced an animated composition, materialize before picking the video URL:
161
+
162
+ ```bash
163
+ npx makaron-cli responses get <runId> --materialize --wait --pick first_video_url
164
+ npx makaron-cli responses get <runId> --export-compositions --wait --pick first_video_url
165
+ ```
166
+
167
+ To turn a Makaron Remotion design JSON file directly into an MP4, use `--design-json`. The JSON must be a Makaron/Remotion composition payload, not a provider-video task response. Always pass the destination project because published exports and storage paths are project-scoped:
168
+
169
+ ```bash
170
+ npx makaron-cli materialize --project <projectId> --design-json composition.json --pick url
171
+ cat composition.json | npx makaron-cli materialize --project <projectId> --design-json - --pick url
172
+ ```
173
+
174
+ This JSON-to-MP4 path uses the same defaults as timeline materialize: `--wait`, `--publish`, and `fast_720p`. Add `--no-publish` only when another agent needs the MP4 URL but should not add a timeline video.
175
+
176
+ The completed export reports `duration_seconds`, `render_seconds`, and `realtime_ratio` so agents can compare video length against export time. Do not apply provider-video ETA rules to Remotion materialize; with a warm exporter it is often near video length to tens of seconds, while cold starts can be longer.
177
+
178
+ In production, run the exporter as a separate warm worker:
179
+
180
+ ```bash
181
+ REMOTION_EXPORT_INLINE_AFTER=false npm run worker:remotion-export:check
182
+ REMOTION_EXPORT_INLINE_AFTER=false npm run worker:remotion-export
183
+ ```
184
+
185
+ Keeping this worker warm avoids paying sandbox cold-start cost on every CLI or service call.
186
+
146
187
  ### With video input (MP4/MOV/WebM)
147
188
 
148
189
  ```bash
@@ -269,9 +310,9 @@ For project/timeline video editing, use:
269
310
  npx makaron-cli chat --project <id|auto> --video input.mp4 -b "make it funny"
270
311
  ```
271
312
 
272
- 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`, `--model seedance-fast|seedance-mini|seedance|kling|grok`, `--video-resolution auto|480p|720p|1080p|4k`. Default model is `seedance-fast`. SeeDance accepts 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. For `--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.
313
+ 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`, `--model seedance-fast|seedance-mini|seedance|kling|grok|google-omni`, `--video-resolution auto|480p|720p|1080p|4k`. Default model is `seedance-fast`. SeeDance accepts 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. For `--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.
273
314
 
274
- Video edit model behavior: `--model kling --video` uses Kling base/direct edit internally; `--model seedance-fast --video`, `--model seedance-mini --video`, or `--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. Tiny metadata padding up to 15.5s is accepted and output duration is clamped to 15s. Grok does not support video references.
315
+ Video edit model behavior: `--model kling --video` uses Kling base/direct edit internally; `--model seedance-fast --video`, `--model seedance-mini --video`, or `--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. `--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.
275
316
 
276
317
  ### `music` — Music generation
277
318
 
@@ -386,7 +427,7 @@ send_message "All done!"
386
427
  - One project = one conversation thread. All history is preserved.
387
428
  - One run at a time per project. New message interrupts previous run.
388
429
  - Multi-image: `create --image a.jpg --image b.jpg` or `chat --image ref.jpg`.
389
- - Most videos take 3-5 minutes; Grok is usually around 30-40 seconds. Use `responses get <runId> --wait --json` for the default customer-service path.
430
+ - 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`.
390
431
  - Music takes ~60 seconds. Appears in output when done.
391
432
  - Images are typically ready in 15-30 seconds.
392
433
  - stdout is always machine-readable JSON/text. Human-friendly logs go to stderr.
package/bin/makaron.mjs CHANGED
@@ -142,6 +142,11 @@ function formatSeconds(seconds) {
142
142
  return Number.isInteger(seconds) ? String(seconds) : seconds.toFixed(1).replace(/\.0$/, '');
143
143
  }
144
144
 
145
+ function readJsonInput(filePath) {
146
+ const raw = filePath === '-' ? fs.readFileSync(0, 'utf-8') : fs.readFileSync(filePath, 'utf-8');
147
+ return JSON.parse(raw);
148
+ }
149
+
145
150
  // ─── Auth ────────────────────────────────────────────────────────────────────
146
151
 
147
152
  function loadAuth() {
@@ -195,15 +200,6 @@ async function login() {
195
200
  console.error(` Token saved to ${AUTH_FILE}`);
196
201
  }
197
202
 
198
- function getAuthCookie() {
199
- const auth = loadAuth();
200
- if (!auth) {
201
- console.error('Not logged in. Run: npx makaron-cli login');
202
- process.exit(1);
203
- }
204
- return { cookie: buildCookie(auth), baseUrl: process.env.MAKARON_URL || auth._baseUrl || BASE_URL };
205
- }
206
-
207
203
  function getAuth() {
208
204
  const apiKey = process.env.MAKARON_API_KEY;
209
205
  if (apiKey) {
@@ -282,7 +278,7 @@ Options:
282
278
  --audio <file|url> Attach a song, beat, or voice reference. MP3/WAV, repeatable.
283
279
  --skill <id|label|name> Use an installed skill or auto-install a matched marketplace skill.
284
280
  --model <name> Preferred image/model route.
285
- --video-model <name> Preferred video model: seedance-fast, seedance-mini, seedance, kling, or grok.
281
+ --video-model <name> Preferred video model: seedance-fast, seedance-mini, seedance, kling, grok, or google-omni.
286
282
  --video-resolution <res> Video resolution: auto, 480p, 720p, 1080p, or 4k.
287
283
  --background, -b Submit and print a runId.
288
284
  --json Output structured JSON.
@@ -478,7 +474,14 @@ async function submitRun(baseUrl, headers, projectId, prompt, opts = {}) {
478
474
  }
479
475
 
480
476
  async function pollRun(baseUrl, headers, runId, opts = {}) {
481
- const { json = false, waitForArtifacts = false, background = false } = opts;
477
+ const {
478
+ json = false,
479
+ waitForArtifacts = false,
480
+ background = false,
481
+ exportCompositions = false,
482
+ publishExports = false,
483
+ returnDataOnly = false,
484
+ } = opts;
482
485
  if (background) return;
483
486
 
484
487
  let lastSeq = -1;
@@ -559,11 +562,17 @@ async function pollRun(baseUrl, headers, runId, opts = {}) {
559
562
  // Check terminal status
560
563
  if (data.status === 'completed' || data.status === 'failed' || data.status === 'aborted') {
561
564
  if (printedText && !json) process.stdout.write('\n');
565
+ if (data.status === 'completed' && exportCompositions) {
566
+ data = await exportAnimatedCompositionsFromRun(baseUrl, headers, data, {
567
+ publish: publishExports,
568
+ quiet: json || returnDataOnly,
569
+ });
570
+ }
562
571
 
563
- if (json) {
572
+ if (json && !returnDataOnly) {
564
573
  // Structured JSON output — add projectUrl
565
574
  console.log(JSON.stringify(normalizeRunResponse(data), null, 2));
566
- } else {
575
+ } else if (!returnDataOnly) {
567
576
  process.stderr.write('\n━━━ Results ━━━\n');
568
577
  if (data.result) {
569
578
  for (const img of data.result.images || []) process.stderr.write(`🖼️ Image: ${img.imageUrl}\n`);
@@ -777,7 +786,7 @@ async function listProjectMedia(baseUrl, headers, projectId, opts = {}) {
777
786
  if (!res.ok) { console.error('Project media failed:', await res.text()); process.exit(1); }
778
787
  const data = await res.json();
779
788
  if (opts.json) {
780
- console.log(JSON.stringify(data, null, 2));
789
+ if (!opts.silent) console.log(JSON.stringify(data, null, 2));
781
790
  return data;
782
791
  }
783
792
 
@@ -800,6 +809,158 @@ async function listProjectMedia(baseUrl, headers, projectId, opts = {}) {
800
809
  return data;
801
810
  }
802
811
 
812
+ async function pollRemotionExport(baseUrl, headers, jobId, opts = {}) {
813
+ const start = Date.now();
814
+ while (true) {
815
+ const res = await fetch(`${baseUrl}/api/remotion/export/${jobId}`, { headers });
816
+ if (!res.ok) { process.stderr.write(`Export status failed ${res.status}: ${await res.text()}\n`); process.exit(1); }
817
+ const data = await res.json();
818
+ if (data.status === 'completed' || data.status === 'failed') {
819
+ if (opts.json) {
820
+ console.log(JSON.stringify(data, null, 2));
821
+ } else if (data.status === 'completed') {
822
+ const duration = typeof data.duration_seconds === 'number' ? `${data.duration_seconds.toFixed(2)}s` : 'unknown';
823
+ const render = typeof data.render_seconds === 'number' ? `${data.render_seconds.toFixed(2)}s` : 'unknown';
824
+ const ratio = typeof data.realtime_ratio === 'number' ? data.realtime_ratio.toFixed(2) : 'unknown';
825
+ if (!opts.quiet) {
826
+ process.stderr.write(`✅ Export complete\n`);
827
+ process.stderr.write(` Video duration: ${duration}\n`);
828
+ process.stderr.write(` Export time: ${render}\n`);
829
+ process.stderr.write(` Duration/export ratio: ${ratio}:1\n`);
830
+ console.log(data.url || data.storageUrl || '');
831
+ }
832
+ } else if (!opts.quiet) {
833
+ process.stderr.write(`❌ Export failed: ${data.error || 'unknown error'}\n`);
834
+ }
835
+ if (data.status === 'failed') process.exit(1);
836
+ return data;
837
+ }
838
+
839
+ const elapsed = Math.round((Date.now() - start) / 1000);
840
+ const pct = typeof data.progress === 'number' ? ` ${(data.progress * 100).toFixed(0)}%` : '';
841
+ if (!opts.quiet) process.stderr.write(`\r🎬 Export ${data.status}${pct} (${elapsed}s)`);
842
+ await new Promise(r => setTimeout(r, data.next_poll_after_ms || 3000));
843
+ }
844
+ }
845
+
846
+ async function exportComposition(baseUrl, headers, opts = {}) {
847
+ const body = {
848
+ projectId: opts.projectId,
849
+ snapshotId: opts.snapshotId,
850
+ designPath: opts.designPath,
851
+ design: opts.design,
852
+ outputType: opts.outputType || 'video',
853
+ renderProfile: opts.renderProfile || 'fast_720p',
854
+ publish: opts.publish === true,
855
+ name: opts.name,
856
+ };
857
+
858
+ if (opts.mediaIndex) {
859
+ if (!body.projectId) {
860
+ process.stderr.write('Usage: makaron materialize --project <id> --media <N> [--wait] [--publish] [--name <slug>] [--json]\n');
861
+ process.exit(1);
862
+ }
863
+ const mediaData = await listProjectMedia(baseUrl, headers, opts.projectId, { json: true, silent: true });
864
+ const item = (mediaData.media || []).find(m => Number(m.index) === Number(opts.mediaIndex));
865
+ if (!item) {
866
+ process.stderr.write(`No media item at index ${opts.mediaIndex}\n`);
867
+ process.exit(1);
868
+ }
869
+ if (item.type !== 'composition' && !item.codePath) {
870
+ process.stderr.write(`Media ${opts.mediaIndex} is ${item.type}, not a Remotion composition.\n`);
871
+ process.exit(1);
872
+ }
873
+ body.snapshotId = item.snapshotId || item.snapshot_id;
874
+ body.designPath = item.codePath || item.designPath || body.designPath;
875
+ }
876
+
877
+ if (!body.projectId || (!body.snapshotId && !body.designPath && !body.design)) {
878
+ process.stderr.write('Usage: makaron materialize --project <id> (--media <N> | --snapshot <snapshotId> | --design-path <path> | --design-json <file|->) [--wait] [--publish] [--name <slug>] [--json]\n');
879
+ process.exit(1);
880
+ }
881
+
882
+ const res = await fetch(`${baseUrl}/api/remotion/export`, {
883
+ method: 'POST',
884
+ headers: { 'Content-Type': 'application/json', ...headers },
885
+ body: JSON.stringify(body),
886
+ });
887
+ const data = await res.json();
888
+ if (!res.ok) {
889
+ process.stderr.write(`Export failed ${res.status}: ${JSON.stringify(data)}\n`);
890
+ process.exit(1);
891
+ }
892
+
893
+ if (opts.wait) {
894
+ if (!opts.quiet) process.stderr.write(`🎬 Export queued: ${data.jobId || data.id}\n`);
895
+ return pollRemotionExport(baseUrl, headers, data.jobId || data.id, { json: opts.json, quiet: opts.quiet });
896
+ }
897
+
898
+ if (!opts.quiet) {
899
+ if (opts.json) console.log(JSON.stringify(data, null, 2));
900
+ else console.log(data.jobId || data.id);
901
+ }
902
+ return data;
903
+ }
904
+
905
+ function pickExportValue(data, pick) {
906
+ if (!pick) return undefined;
907
+ if (pick === 'url' || pick === 'video_url' || pick === 'first_video_url') return data.url || data.storageUrl || data.storage_url;
908
+ if (pick === 'job_id' || pick === 'id') return data.jobId || data.id;
909
+ if (pick === 'workspace_path') return data.workspacePath || data.workspace_path;
910
+ if (pick === 'status') return data.status;
911
+ if (pick === 'output') return data;
912
+ return data[pick];
913
+ }
914
+
915
+ async function exportAnimatedCompositionsFromRun(baseUrl, headers, data, opts = {}) {
916
+ const projectId = data.projectId || data.project_id;
917
+ if (!projectId) return data;
918
+ const designs = (data.output || []).filter(o => o.type === 'design' && o.animated && o.snapshot_id);
919
+ if (!designs.length) return data;
920
+
921
+ const exported = [];
922
+ for (const design of designs) {
923
+ if (!opts.quiet) process.stderr.write(`\n🎬 Exporting composition snapshot ${design.snapshot_id}...\n`);
924
+ const job = await exportComposition(baseUrl, headers, {
925
+ projectId,
926
+ snapshotId: design.snapshot_id,
927
+ wait: true,
928
+ publish: opts.publish === true,
929
+ name: `run-${data.id || 'composition'}-${design.snapshot_id}`,
930
+ quiet: opts.quiet,
931
+ });
932
+ exported.push(job);
933
+ if (job?.url || job?.storageUrl) {
934
+ data.output = data.output || [];
935
+ data.output.push({
936
+ id: `export_${job.id}`,
937
+ type: 'video',
938
+ status: 'completed',
939
+ url: job.url || job.storageUrl,
940
+ export_job_id: job.id,
941
+ source_snapshot_id: design.snapshot_id,
942
+ duration: job.duration_seconds,
943
+ render_seconds: job.render_seconds,
944
+ realtime_ratio: job.realtime_ratio,
945
+ width: job.width,
946
+ height: job.height,
947
+ });
948
+ data.result = data.result || {};
949
+ data.result.videos = data.result.videos || [];
950
+ data.result.videos.push({
951
+ taskId: `remotion-export-${job.id}`,
952
+ status: 'completed',
953
+ videoUrl: job.url || job.storageUrl,
954
+ duration: job.duration_seconds,
955
+ renderSeconds: job.render_seconds,
956
+ realtimeRatio: job.realtime_ratio,
957
+ });
958
+ }
959
+ }
960
+ data.remotion_exports = exported;
961
+ return data;
962
+ }
963
+
803
964
  function timeSince(date) {
804
965
  const s = Math.floor((Date.now() - date.getTime()) / 1000);
805
966
  if (s < 60) return 'just now';
@@ -1373,6 +1534,12 @@ Commands:
1373
1534
 
1374
1535
  responses get <runId> Get run status and results
1375
1536
  responses get <runId> --wait Poll until completed
1537
+ responses get <runId> --materialize --wait --pick first_video_url
1538
+ Export and publish Remotion compositions as MP4
1539
+ materialize --project <id> --media <N> --pick url
1540
+ Convert editable composition/JSON to MP4
1541
+ composition export --project <id> --media <N> --wait
1542
+ Export editable Remotion composition to MP4
1376
1543
  responses list --project <id> List runs for a project
1377
1544
  abort <runId> Abort a running Agent
1378
1545
  skills list|search|show|install Browse and install marketplace skills
@@ -1441,13 +1608,17 @@ function printHelp(topic, subtopic) {
1441
1608
  } else if (topic === 'chat') {
1442
1609
  printChatHelp();
1443
1610
  } else if (topic === 'responses' || topic === 'run') {
1444
- if (subtopic === 'get') console.log('Usage: makaron responses get <runId> [--wait] [--json] [--pick <field>]');
1611
+ if (subtopic === 'get') console.log('Usage: makaron responses get <runId> [--wait] [--json] [--pick <field>] [--materialize|--export-compositions] [--publish-exports]');
1445
1612
  else if (subtopic === 'watch') console.log('Usage: makaron responses watch <runId> [--jsonl] [--interval <ms>]');
1446
1613
  else if (subtopic === 'list') console.log('Usage: makaron responses list --project <id>');
1447
1614
  else console.log(`Responses commands:
1448
1615
  responses get <runId> Get status and output (JSON)
1449
1616
  responses get <runId> --wait Poll until completed
1450
1617
  responses get <runId> --pick <field> Extract: first_image_url, first_video_url, project_url, output
1618
+ responses get <runId> --export-compositions --wait --pick first_video_url
1619
+ Export animated compositions before picking video URL
1620
+ responses get <runId> --materialize --wait --pick first_video_url
1621
+ Export and publish animated compositions as MP4
1451
1622
  responses watch <runId> --jsonl Watch until done (incremental events)
1452
1623
  responses list --project <id> List runs for a project
1453
1624
  `);
@@ -1477,6 +1648,16 @@ function printHelp(topic, subtopic) {
1477
1648
 
1478
1649
  Use with chat:
1479
1650
  makaron chat --project auto --skill <id|label> "your request"
1651
+ `);
1652
+ } else if (topic === 'materialize') {
1653
+ console.log(`Usage: makaron materialize --project <id> (--media <N> | --snapshot <snapshotId> | --design-path <path> | --design-json <file|->) [--wait] [--publish|--no-publish] [--profile fast_720p|source] [--pick url|job_id|status]`);
1654
+ } else if (topic === 'composition' || topic === 'compositions') {
1655
+ console.log(`Composition commands:
1656
+ composition export --project <id> --media <N> --wait
1657
+ composition export --project <id> --snapshot <snapshotId> --wait
1658
+ composition export --project <id> --design-path <path> --wait
1659
+ composition export --project <id> --design-json composition.json --wait
1660
+ composition status <jobId> [--wait] [--json]
1480
1661
  `);
1481
1662
  } else if (topic === 'edit') {
1482
1663
  console.log('Usage: makaron edit [--image <file|url>] [--model gemini|qwen|openai] [--skill enhance|creative|wild|captions] [--ref <file>] [--out <file>] "prompt"');
@@ -1484,12 +1665,12 @@ Use with chat:
1484
1665
  console.log('Usage: makaron analyze --video <file|url> ["question"]');
1485
1666
  } else if (topic === 'video') {
1486
1667
  if (subtopic === 'script') console.log('Usage: makaron video script --image <file> [--image <file>] [--lang en|zh] "direction"');
1487
- else if (subtopic === 'create') console.log('Usage: makaron video create --script "..." (--image <url> | --video <public-url>) [--duration 10] [--aspect 9:16] [--model seedance-fast|seedance-mini|seedance|kling|grok] [--video-resolution auto|480p|720p|1080p|4k] [--keep-original-sound]');
1668
+ else if (subtopic === 'create') console.log('Usage: makaron video create --script "..." (--image <url> | --video <public-url>) [--duration 10] [--aspect 9:16] [--model seedance-fast|seedance-mini|seedance|kling|grok|google-omni] [--video-resolution auto|480p|720p|1080p|4k] [--keep-original-sound]');
1488
1669
  else if (subtopic === 'status') console.log('Usage: makaron video status <taskId> | --snapshot <snapshotId> [--wait]');
1489
1670
  else console.log(`Video commands:
1490
1671
  video script --image <file> [--image <file>] "direction" Write video script
1491
1672
  video create --script "..." --image <url> [--duration 10] Submit video task
1492
- video create --script "..." --video <public-url> [--model seedance-fast|seedance-mini|seedance|kling] Edit a video (standalone; Grok does not support video refs)
1673
+ video create --script "..." --video <public-url> [--model seedance-fast|seedance-mini|seedance|kling|google-omni] Edit a video (standalone; Grok does not support video refs)
1493
1674
  video status <taskId> Check video status
1494
1675
  video status --snapshot <snapshotId> [--wait] Check v2 video snapshot
1495
1676
  `);
@@ -1809,20 +1990,41 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1809
1990
 
1810
1991
  if (sub === 'get') {
1811
1992
  const runId = args[2];
1812
- if (!runId) { console.error('Usage: makaron responses get <runId> [--wait] [--json] [--pick <field>]'); process.exit(1); }
1813
- let wait = false, pick = null;
1993
+ if (!runId) { console.error('Usage: makaron responses get <runId> [--wait] [--json] [--pick <field>] [--materialize|--export-compositions]'); process.exit(1); }
1994
+ let wait = false, jsonOutput = false, pick = null, exportCompositions = false, publishExports = false;
1814
1995
  for (let i = 3; i < args.length; i++) {
1815
1996
  if (args[i] === '--wait') wait = true;
1997
+ if (args[i] === '--json') jsonOutput = true;
1998
+ if (args[i] === '--export-compositions') exportCompositions = true;
1999
+ if (args[i] === '--materialize') {
2000
+ exportCompositions = true;
2001
+ publishExports = true;
2002
+ }
2003
+ if (args[i] === '--publish-exports') publishExports = true;
1816
2004
  if (args[i] === '--pick' && args[i + 1]) pick = args[++i];
1817
2005
  }
1818
2006
 
1819
2007
  if (wait) {
1820
- await pollRun(baseUrl, headers, runId, { json: true });
2008
+ const data = await pollRun(baseUrl, headers, runId, {
2009
+ json: true,
2010
+ exportCompositions,
2011
+ publishExports,
2012
+ returnDataOnly: !!pick || !jsonOutput,
2013
+ });
2014
+ if (pick) {
2015
+ const picked = applyPick(data, pick);
2016
+ if (picked !== undefined) console.log(typeof picked === 'string' ? picked : JSON.stringify(picked));
2017
+ } else if (!jsonOutput) {
2018
+ console.log(JSON.stringify(data, null, 2));
2019
+ }
1821
2020
  } else {
1822
2021
  const res = await fetch(`${baseUrl}/api/agent/run/${runId}`, { headers });
1823
2022
  if (!res.ok) { process.stderr.write(`Error ${res.status}: ${await res.text()}\n`); process.exit(1); }
1824
- const data = await res.json();
2023
+ let data = await res.json();
1825
2024
  normalizeRunResponse(data);
2025
+ if (exportCompositions && data.status === 'completed') {
2026
+ data = await exportAnimatedCompositionsFromRun(baseUrl, headers, data, { publish: publishExports, quiet: jsonOutput || !!pick });
2027
+ }
1826
2028
  if (pick) {
1827
2029
  const picked = applyPick(data, pick);
1828
2030
  if (picked !== undefined) console.log(typeof picked === 'string' ? picked : JSON.stringify(picked));
@@ -1865,6 +2067,10 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1865
2067
  responses get <runId> Get status and output (JSON)
1866
2068
  responses get <runId> --wait Poll until completed
1867
2069
  responses get <runId> --pick <field> Extract: first_image_url, first_video_url, project_url, output
2070
+ responses get <runId> --export-compositions --wait --pick first_video_url
2071
+ Export animated compositions before picking video URL
2072
+ responses get <runId> --materialize --wait --pick first_video_url
2073
+ Export and publish animated compositions as MP4
1868
2074
  responses watch <runId> --jsonl Watch until done (incremental events)
1869
2075
  responses list --project <id> List runs for a project
1870
2076
  `);
@@ -1872,6 +2078,81 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1872
2078
  } else if (command === 'list' || command === 'ls') {
1873
2079
  const { headers, baseUrl } = getAuth();
1874
2080
  await listProjects(baseUrl, headers);
2081
+ } else if (command === 'materialize') {
2082
+ const { headers, baseUrl } = getAuth();
2083
+ const opts = { wait: true, publish: true, json: false, outputType: 'video', renderProfile: 'fast_720p', pick: null, quiet: false };
2084
+ for (let i = 1; i < args.length; i++) {
2085
+ if (args[i] === '--project' && args[i + 1]) opts.projectId = args[++i];
2086
+ else if (args[i] === '--media' && args[i + 1]) opts.mediaIndex = Number(args[++i]);
2087
+ else if (args[i] === '--snapshot' && args[i + 1]) opts.snapshotId = args[++i];
2088
+ else if (args[i] === '--design-path' && args[i + 1]) opts.designPath = args[++i];
2089
+ else if (args[i] === '--design-json' && args[i + 1]) opts.design = readJsonInput(args[++i]);
2090
+ else if (args[i] === '--name' && args[i + 1]) opts.name = args[++i];
2091
+ else if (args[i] === '--type' && args[i + 1]) opts.outputType = args[++i];
2092
+ else if (args[i] === '--profile' && args[i + 1]) opts.renderProfile = args[++i];
2093
+ else if (args[i] === '--render-profile' && args[i + 1]) opts.renderProfile = args[++i];
2094
+ else if (args[i] === '--no-publish') opts.publish = false;
2095
+ else if (args[i] === '--publish') opts.publish = true;
2096
+ else if (args[i] === '--no-wait') opts.wait = false;
2097
+ else if (args[i] === '--wait') opts.wait = true;
2098
+ else if (args[i] === '--json') opts.json = true;
2099
+ else if (args[i] === '--pick' && args[i + 1]) opts.pick = args[++i];
2100
+ }
2101
+ if (opts.pick) opts.quiet = true;
2102
+ const data = await exportComposition(baseUrl, headers, opts);
2103
+ if (opts.pick) {
2104
+ const picked = pickExportValue(data, opts.pick);
2105
+ if (picked !== undefined) console.log(typeof picked === 'string' ? picked : JSON.stringify(picked));
2106
+ }
2107
+ } else if (command === 'composition' || command === 'compositions') {
2108
+ const { headers, baseUrl } = getAuth();
2109
+ const sub = args[1];
2110
+ if (sub === 'export') {
2111
+ const opts = { wait: false, publish: false, json: false, outputType: 'video', renderProfile: 'fast_720p', pick: null, quiet: false };
2112
+ for (let i = 2; i < args.length; i++) {
2113
+ if (args[i] === '--project' && args[i + 1]) opts.projectId = args[++i];
2114
+ else if (args[i] === '--media' && args[i + 1]) opts.mediaIndex = Number(args[++i]);
2115
+ else if (args[i] === '--snapshot' && args[i + 1]) opts.snapshotId = args[++i];
2116
+ else if (args[i] === '--design-path' && args[i + 1]) opts.designPath = args[++i];
2117
+ else if (args[i] === '--design-json' && args[i + 1]) opts.design = readJsonInput(args[++i]);
2118
+ else if (args[i] === '--name' && args[i + 1]) opts.name = args[++i];
2119
+ else if (args[i] === '--type' && args[i + 1]) opts.outputType = args[++i];
2120
+ else if (args[i] === '--profile' && args[i + 1]) opts.renderProfile = args[++i];
2121
+ else if (args[i] === '--render-profile' && args[i + 1]) opts.renderProfile = args[++i];
2122
+ else if (args[i] === '--publish') opts.publish = true;
2123
+ else if (args[i] === '--wait') opts.wait = true;
2124
+ else if (args[i] === '--json') opts.json = true;
2125
+ else if (args[i] === '--pick' && args[i + 1]) opts.pick = args[++i];
2126
+ }
2127
+ if (opts.pick) opts.quiet = true;
2128
+ const data = await exportComposition(baseUrl, headers, opts);
2129
+ if (opts.pick) {
2130
+ const picked = pickExportValue(data, opts.pick);
2131
+ if (picked !== undefined) console.log(typeof picked === 'string' ? picked : JSON.stringify(picked));
2132
+ }
2133
+ } else if (sub === 'status') {
2134
+ const jobId = args[2];
2135
+ if (!jobId) { console.error('Usage: makaron composition status <jobId> [--wait] [--json]'); process.exit(1); }
2136
+ const wait = args.includes('--wait');
2137
+ const json = args.includes('--json');
2138
+ if (wait) await pollRemotionExport(baseUrl, headers, jobId, { json });
2139
+ else {
2140
+ const res = await fetch(`${baseUrl}/api/remotion/export/${jobId}`, { headers });
2141
+ if (!res.ok) { process.stderr.write(`Export status failed ${res.status}: ${await res.text()}\n`); process.exit(1); }
2142
+ const data = await res.json();
2143
+ if (json) console.log(JSON.stringify(data, null, 2));
2144
+ else console.log(`${data.id} ${data.status} ${data.url || ''}`);
2145
+ if (data.status === 'failed') process.exit(1);
2146
+ }
2147
+ } else {
2148
+ console.log(`Composition commands:
2149
+ composition export --project <id> --media <N> --wait
2150
+ composition export --project <id> --snapshot <snapshotId> --wait
2151
+ composition export --project <id> --design-path <path> --wait
2152
+ composition export --project <id> --design-json composition.json --wait
2153
+ composition status <jobId> [--wait] [--json]
2154
+ `);
2155
+ }
1875
2156
  } else if (command === 'skills') {
1876
2157
  const sub = args[1] || 'list';
1877
2158
  const baseUrl = process.env.MAKARON_URL || DEFAULT_URL;
@@ -2024,7 +2305,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2024
2305
  else if (args[i] === '--wait') wait = true;
2025
2306
  }
2026
2307
  if ((!images.length && !video) || !script) {
2027
- console.error('Usage: makaron video create --script "..." (--image <url> | --video <public-url>) [--duration 10] [--aspect 9:16] [--model seedance-fast|seedance-mini|seedance|kling|grok] [--video-resolution auto|480p|720p|1080p|4k] [--keep-original-sound]');
2308
+ console.error('Usage: makaron video create --script "..." (--image <url> | --video <public-url>) [--duration 10] [--aspect 9:16] [--model seedance-fast|seedance-mini|seedance|kling|grok|google-omni] [--video-resolution auto|480p|720p|1080p|4k] [--keep-original-sound]');
2028
2309
  process.exit(1);
2029
2310
  }
2030
2311
 
@@ -2037,7 +2318,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2037
2318
  let inputVideoMeta = null;
2038
2319
  const selectedVideoModel = videoModel || 'seedance-fast';
2039
2320
  if (videoUrl) {
2040
- 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. Grok does not support video references.\n`);
2321
+ 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`);
2041
2322
  }
2042
2323
  if (video && !videoUrl) {
2043
2324
  const valid = validateVideoFile(video, {
@@ -2111,7 +2392,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2111
2392
  console.log(`Video commands:
2112
2393
  video script --image <file> [--image <file>] "direction" Write video script
2113
2394
  video create --script "..." --image <url> [--duration 10] Submit video task
2114
- video create --script "..." --video <public-url> [--model seedance-fast|seedance-mini|seedance|kling] Edit a video (standalone; Grok does not support video refs)
2395
+ video create --script "..." --video <public-url> [--model seedance-fast|seedance-mini|seedance|kling|google-omni] Edit a video (standalone; Grok does not support video refs)
2115
2396
  video status <taskId> Check video status
2116
2397
  video status --snapshot <snapshotId> [--wait] Check v2 video snapshot
2117
2398
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.11.5",
3
+ "version": "0.12.1",
4
4
  "description": "Talk to Makaron Agent from the terminal — create projects, edit images, generate videos",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -267,9 +267,9 @@ npx makaron-cli video status <taskId>
267
267
  npx makaron-cli chat --project <id|auto> --video input.mp4 -b "make it funny"
268
268
  ```
269
269
 
270
- 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`, `--model seedance-fast|seedance-mini|seedance|kling|grok`, `--video-resolution auto|480p|720p|1080p|4k`. Default model is `seedance-fast`. SeeDance accepts 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. For `--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.
270
+ 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`, `--model seedance-fast|seedance-mini|seedance|kling|grok|google-omni`, `--video-resolution auto|480p|720p|1080p|4k`. Default model is `seedance-fast`. SeeDance accepts 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. For `--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.
271
271
 
272
- Video edit model behavior: `--model kling --video` uses Kling base/direct edit internally; `--model seedance-fast --video`, `--model seedance-mini --video`, or `--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. Tiny metadata padding up to 15.5s is accepted and output duration is clamped to 15s. Grok does not support video references.
272
+ Video edit model behavior: `--model kling --video` uses Kling base/direct edit internally; `--model seedance-fast --video`, `--model seedance-mini --video`, or `--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. `--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.
273
273
 
274
274
  ### `music` — Music generation
275
275
 
@@ -340,6 +340,27 @@ type CompletionAction = {
340
340
  | Motion design | "create an Instagram story with animated text" |
341
341
  | Multi-step | "edit the photo then make a video from it" |
342
342
 
343
+ ## Export editable Remotion compositions
344
+
345
+ Animated Remotion compositions are saved as editable timeline/code artifacts first. Use `materialize` as the preferred high-level Remotion-to-MP4 command:
346
+
347
+ ```bash
348
+ npx makaron-cli materialize --project <projectId> --media <N> --pick url
349
+ npx makaron-cli materialize --project <projectId> --design-json composition.json --pick url
350
+ npx makaron-cli responses get <runId> --materialize --wait --pick first_video_url
351
+ ```
352
+
353
+ `materialize` defaults to `--wait`, `--publish`, and `fast_720p`, so the completed MP4 is added back to the timeline like CUI. Use `--no-publish` only when you need a file URL without a new timeline video. The completed export reports `duration_seconds`, `render_seconds`, and `realtime_ratio`; use those metrics instead of provider-video ETA rules.
354
+
355
+ For JSON-to-MP4, pass a Makaron/Remotion composition JSON with `--design-json`. This is the correct CLI path when another agent already has the composition JSON and only needs the exported video:
356
+
357
+ ```bash
358
+ npx makaron-cli materialize --project <projectId> --design-json composition.json --pick url
359
+ cat composition.json | npx makaron-cli materialize --project <projectId> --design-json - --pick url
360
+ ```
361
+
362
+ Keep `--project` because exports are project-scoped and publish back to the timeline by default. Use `--no-publish` only when you want the MP4 URL without adding a timeline video.
363
+
343
364
  ## Recommended Pattern: Service Flow (Feishu/OpenClaw/Group Chat)
344
365
 
345
366
  When serving end-users in a chat environment (Feishu, Slack, Discord), use this proactive message pattern:
@@ -381,7 +402,7 @@ send_message "All done!"
381
402
  - One project = one conversation thread. All history is preserved.
382
403
  - One run at a time per project. New message interrupts previous run.
383
404
  - Multi-image: `create --image a.jpg --image b.jpg` or `chat --image ref.jpg`.
384
- - Most videos take 3-5 minutes; Grok is usually around 30-40 seconds. Use `responses get <runId> --wait --json` for the default customer-service path.
405
+ - 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`.
385
406
  - Music takes ~60 seconds. Appears in output when done.
386
407
  - Images are typically ready in 15-30 seconds.
387
408
  - stdout is always machine-readable JSON/text. Human-friendly logs go to stderr.