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 +44 -3
- package/bin/makaron.mjs +305 -24
- package/package.json +1 -1
- package/skills/makaron/SKILL.md +24 -3
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.
|
|
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
|
-
-
|
|
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
|
|
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 {
|
|
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, {
|
|
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
|
-
|
|
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
|
|
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
package/skills/makaron/SKILL.md
CHANGED
|
@@ -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.
|
|
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
|
-
-
|
|
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.
|