makaron-cli 0.7.1 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +39 -4
- package/SKILL.md +32 -12
- package/bin/makaron.mjs +222 -58
- package/package.json +1 -1
- package/skills/makaron/SKILL.md +40 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "makaron-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "AI image editing, video generation, and music creation via CLI. Agents can self-register, create projects, and produce creative media.",
|
|
5
5
|
"displayName": "Makaron",
|
|
6
6
|
"shortDescription": "AI image/video/music creation from the terminal",
|
package/README.md
CHANGED
|
@@ -97,6 +97,23 @@ Returns immediately:
|
|
|
97
97
|
npx makaron-cli chat --project <id> --image ref1.jpg --image ref2.jpg -b "use these as style reference"
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
### With video input (MP4/MOV/WebM)
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Upload a video and ask the agent to edit it
|
|
104
|
+
npx makaron-cli chat --project auto --video clip.mp4 -b "put Iron Man armor on me in this video"
|
|
105
|
+
|
|
106
|
+
# Combine video + image references
|
|
107
|
+
npx makaron-cli chat --project <id> --video party.mp4 --image kid.jpg -b "make this kid appear in the party"
|
|
108
|
+
|
|
109
|
+
# Multiple videos (for composition / continuation)
|
|
110
|
+
npx makaron-cli chat --project <id> --video clip1.mp4 --video clip2.mp4 -b "splice these into one seamless video"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Video files are uploaded via signed URL. CLI local video uploads follow the same compatibility contract as the normal frontend flow: `.mp4`, `.mov`, or `.webm`, max 200MB, max 15s, and <=1080p / 2,086,876 frame pixels. The frontend can transcode oversized videos; the CLI rejects them so later Seedance editing does not fail.
|
|
114
|
+
The agent understands video content natively — it can analyze scenes, edit, extend, and compose videos. Seedance video-reference editing is supported for <=15s videos that meet the same upload limits; Kling remains the base/direct edit path.
|
|
115
|
+
Use `chat --project <id|auto> --video ...` for any project/timeline video work. Direct `video create` is standalone and does not write timeline entries.
|
|
116
|
+
|
|
100
117
|
### Check status (single query)
|
|
101
118
|
|
|
102
119
|
```bash
|
|
@@ -153,20 +170,35 @@ npx makaron-cli edit --image photo.jpg --out result.jpg "make it dramatic"
|
|
|
153
170
|
|
|
154
171
|
Options: `--image`, `--model gemini|qwen|openai|pony|wai`, `--skill enhance|creative|wild|captions`, `--ref <file>` (up to 3), `--aspect <ratio>`, `--out <path>`
|
|
155
172
|
|
|
156
|
-
### `video` —
|
|
173
|
+
### `video` — Standalone video tools (no project timeline)
|
|
157
174
|
|
|
158
175
|
```bash
|
|
159
176
|
# 1. Write script from images
|
|
160
177
|
npx makaron-cli video script --image img1.jpg "cinematic story"
|
|
161
178
|
|
|
162
|
-
# 2.
|
|
179
|
+
# 2. Analyze a video (standalone, no timeline write)
|
|
180
|
+
npx makaron-cli analyze --video input.mp4 "describe the key actions and pacing"
|
|
181
|
+
|
|
182
|
+
# 3a. Submit image-to-video rendering (images must be public URLs from step 1 or uploaded)
|
|
163
183
|
npx makaron-cli video create --script "Shot 1 (5s): <<<image_1>>> ..." --image https://...jpg --duration 5 --model kling
|
|
164
184
|
|
|
165
|
-
#
|
|
185
|
+
# 3b. Edit a video from a local file or public URL
|
|
186
|
+
npx makaron-cli video create --script "make it funny" --video input.mp4 --duration 5 --model seedance
|
|
187
|
+
npx makaron-cli video create --script "make it warmer and cinematic" --video https://example.com/input.mp4 --duration 5 --model seedance
|
|
188
|
+
|
|
189
|
+
# 4. Check status
|
|
166
190
|
npx makaron-cli video status <taskId>
|
|
167
191
|
```
|
|
168
192
|
|
|
169
|
-
|
|
193
|
+
For project/timeline video editing, use:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
npx makaron-cli chat --project <id|auto> --video input.mp4 -b "make it funny"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--video <file|url>`, `--duration 3|5|7|10|15`, `--aspect 9:16|16:9|1:1`, `--model kling|seedance`
|
|
200
|
+
|
|
201
|
+
Video edit model behavior: `--model kling --video` uses Kling base/direct edit internally; `--model seedance --video` uses the Seedance video-reference path and requires <=15s, <=1080p input.
|
|
170
202
|
|
|
171
203
|
### `music` — Music generation
|
|
172
204
|
|
|
@@ -224,6 +256,9 @@ type MakaronOutput =
|
|
|
224
256
|
| Text-to-image | "generate a cyberpunk cityscape" |
|
|
225
257
|
| Video from image | "create a 5 second video of her walking" |
|
|
226
258
|
| Video with model | "use seedance model, make a 5s video" |
|
|
259
|
+
| **Edit video** | **"put Iron Man armor on me in this video"** |
|
|
260
|
+
| **Compose videos** | **"combine @1 and @2 into one party video"** |
|
|
261
|
+
| **Extend video** | **"continue the story for 10 more seconds"** |
|
|
227
262
|
| Background music | "add calm piano music" |
|
|
228
263
|
| Motion design | "create an Instagram story with animated text" |
|
|
229
264
|
| Multi-step | "edit the photo then make a video from it" |
|
package/SKILL.md
CHANGED
|
@@ -86,20 +86,25 @@ Returns immediately:
|
|
|
86
86
|
npx makaron-cli chat --project <id> --image ref1.jpg --image ref2.jpg -b "use these as style reference"
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
### With video input
|
|
89
|
+
### With video input (edit, compose, extend)
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
|
-
# Upload a video
|
|
93
|
-
npx makaron-cli chat --project
|
|
92
|
+
# Upload a video and transform it — Agent understands video content natively
|
|
93
|
+
npx makaron-cli chat --project auto --video selfie.mp4 -b "put Iron Man armor on me"
|
|
94
94
|
|
|
95
|
-
#
|
|
96
|
-
npx makaron-cli chat --project
|
|
95
|
+
# Combine a person's photo with a video scene
|
|
96
|
+
npx makaron-cli chat --project <id> --video party.mp4 --image kid.jpg -b "make this kid join the party"
|
|
97
|
+
|
|
98
|
+
# Multiple videos — compose or splice
|
|
99
|
+
npx makaron-cli chat --project <id> --video clip1.mp4 --video clip2.mp4 -b "combine into one seamless video"
|
|
97
100
|
|
|
98
|
-
#
|
|
99
|
-
npx makaron-cli chat --project auto --
|
|
101
|
+
# Video URL (public, downloadable)
|
|
102
|
+
npx makaron-cli chat --project auto --video https://example.com/dance.mp4 -b "extend this to 15 seconds"
|
|
100
103
|
```
|
|
101
104
|
|
|
102
|
-
Supported formats: MP4, MOV, WebM
|
|
105
|
+
Supported formats: MP4, MOV, WebM. CLI local video uploads follow the same compatibility contract as the normal frontend flow: max 200MB, max 15s, and <=1080p / 2,086,876 frame pixels. The frontend can transcode oversized videos; the CLI rejects them so later Seedance editing does not fail. Videos are uploaded to the project timeline. The Agent can analyze scenes, edit content, compose multiple clips, extend duration, and add effects — all via natural language. Seedance video-reference editing is supported for <=15s videos that meet these upload limits; Kling remains the base/direct edit path.
|
|
106
|
+
|
|
107
|
+
Use `chat --project <id|auto> --video ...` for any project/timeline video work. Direct video commands are standalone raw-tool calls.
|
|
103
108
|
|
|
104
109
|
### Check status (single query)
|
|
105
110
|
|
|
@@ -157,20 +162,35 @@ npx makaron-cli edit --image photo.jpg --out result.jpg "make it dramatic"
|
|
|
157
162
|
|
|
158
163
|
Options: `--image`, `--model gemini|qwen|openai|pony|wai`, `--skill enhance|creative|wild|captions`, `--ref <file>` (up to 3), `--aspect <ratio>`, `--out <path>`
|
|
159
164
|
|
|
160
|
-
### `video` —
|
|
165
|
+
### `video` — Standalone video tools (no project timeline)
|
|
161
166
|
|
|
162
167
|
```bash
|
|
163
168
|
# 1. Write script from images
|
|
164
169
|
npx makaron-cli video script --image img1.jpg "cinematic story"
|
|
165
170
|
|
|
166
|
-
# 2.
|
|
171
|
+
# 2. Analyze a video (standalone, no timeline write)
|
|
172
|
+
npx makaron-cli analyze --video input.mp4 "describe the key actions and pacing"
|
|
173
|
+
|
|
174
|
+
# 3a. Submit image-to-video rendering (images must be public URLs from step 1 or uploaded)
|
|
167
175
|
npx makaron-cli video create --script "Shot 1 (5s): <<<image_1>>> ..." --image https://...jpg --duration 5 --model kling
|
|
168
176
|
|
|
169
|
-
#
|
|
177
|
+
# 3b. Edit a video from a local file or public URL
|
|
178
|
+
npx makaron-cli video create --script "make it funny" --video input.mp4 --duration 5 --model seedance
|
|
179
|
+
npx makaron-cli video create --script "make it warmer and cinematic" --video https://example.com/input.mp4 --duration 5 --model seedance
|
|
180
|
+
|
|
181
|
+
# 4. Check status
|
|
170
182
|
npx makaron-cli video status <taskId>
|
|
171
183
|
```
|
|
172
184
|
|
|
173
|
-
|
|
185
|
+
`video create` returns a provider task id and does not create or update a Makaron project timeline. For project/timeline video editing, use:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npx makaron-cli chat --project <id|auto> --video input.mp4 -b "make it funny"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--video <file|url>`, `--duration 3|5|7|10|15`, `--aspect 9:16|16:9|1:1`, `--model kling|seedance`
|
|
192
|
+
|
|
193
|
+
Video edit model behavior: `--model kling --video` uses Kling base/direct edit internally; `--model seedance --video` uses the Seedance video-reference path and requires <=15s, <=1080p input.
|
|
174
194
|
|
|
175
195
|
### `music` — Music generation
|
|
176
196
|
|
package/bin/makaron.mjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import fs from 'fs';
|
|
15
15
|
import path from 'path';
|
|
16
16
|
import { createInterface } from 'readline';
|
|
17
|
+
import { execFileSync } from 'child_process';
|
|
17
18
|
|
|
18
19
|
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
19
20
|
|
|
@@ -26,6 +27,10 @@ const APP_URL = process.env.MAKARON_APP_URL || DEFAULT_URL;
|
|
|
26
27
|
const SUPABASE_URL = 'https://sdyrtztrjgmmpnirswxt.supabase.co';
|
|
27
28
|
const SUPABASE_ANON_KEY = 'sb_publishable_FJFN2YYaWaQjABUKLqxQcA_fhxPLFDY';
|
|
28
29
|
|
|
30
|
+
const MAX_VIDEO_FILE_SIZE = 200 * 1024 * 1024;
|
|
31
|
+
const MAX_VIDEO_DURATION = 15;
|
|
32
|
+
const MAX_VIDEO_FRAME_PIXELS = 2_086_876;
|
|
33
|
+
|
|
29
34
|
// ─── Auth ────────────────────────────────────────────────────────────────────
|
|
30
35
|
|
|
31
36
|
function loadAuth() {
|
|
@@ -116,6 +121,15 @@ function getAuth() {
|
|
|
116
121
|
};
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
function normalizeRunResponse(data) {
|
|
125
|
+
const projectId = data.projectId || data.project_id;
|
|
126
|
+
if (projectId) {
|
|
127
|
+
data.projectId = projectId;
|
|
128
|
+
data.projectUrl = `${APP_URL}/projects/${projectId}`;
|
|
129
|
+
}
|
|
130
|
+
return data;
|
|
131
|
+
}
|
|
132
|
+
|
|
119
133
|
// ─── SSE Consumer ────────────────────────────────────────────────────────────
|
|
120
134
|
|
|
121
135
|
async function abortRun(baseUrl, headers, runId) {
|
|
@@ -351,8 +365,7 @@ async function pollRun(baseUrl, headers, runId, opts = {}) {
|
|
|
351
365
|
|
|
352
366
|
if (json) {
|
|
353
367
|
// Structured JSON output — add projectUrl
|
|
354
|
-
data
|
|
355
|
-
console.log(JSON.stringify(data, null, 2));
|
|
368
|
+
console.log(JSON.stringify(normalizeRunResponse(data), null, 2));
|
|
356
369
|
} else {
|
|
357
370
|
process.stderr.write('\n━━━ Results ━━━\n');
|
|
358
371
|
if (data.result) {
|
|
@@ -360,6 +373,7 @@ async function pollRun(baseUrl, headers, runId, opts = {}) {
|
|
|
360
373
|
for (const d of data.result.designs || []) process.stderr.write(`🎨 Design (${d.width}x${d.height})\n`);
|
|
361
374
|
for (const v of data.result.videos || []) {
|
|
362
375
|
if (v.videoUrl) process.stderr.write(`🎬 Video: ${v.videoUrl}\n`);
|
|
376
|
+
else if (v.status === 'failed') process.stderr.write(`🎬 Video ${v.taskId}: failed${v.error ? ` — ${v.error}` : ''}\n`);
|
|
363
377
|
else process.stderr.write(`🎬 Video ${v.taskId}: ${v.status || 'submitted'}\n`);
|
|
364
378
|
}
|
|
365
379
|
for (const m of data.result.music || []) {
|
|
@@ -432,7 +446,7 @@ async function watchRun(baseUrl, headers, runId, opts = {}) {
|
|
|
432
446
|
} else if (JSON.stringify(prev) !== JSON.stringify(item)) {
|
|
433
447
|
// Updated item (e.g. video status changed)
|
|
434
448
|
if (jsonl) console.log(JSON.stringify({ event: 'output.updated', item }));
|
|
435
|
-
else process.stderr.write(`~ [${item.type}] ${item.status} ${item.url || ''}\n`);
|
|
449
|
+
else process.stderr.write(`~ [${item.type}] ${item.status} ${item.url || item.error || ''}\n`);
|
|
436
450
|
}
|
|
437
451
|
}
|
|
438
452
|
|
|
@@ -468,7 +482,7 @@ async function pollVideo(baseUrl, headers, taskId, snapshotId) {
|
|
|
468
482
|
if (!res.ok) continue;
|
|
469
483
|
const data = await res.json();
|
|
470
484
|
if (data.videoUrl) { process.stderr.write(`\r🎬 Video done (${elapsed}s): ${data.videoUrl}\n`); return data.videoUrl; }
|
|
471
|
-
if (data.status === 'failed' || data.status === 'abandoned') { process.stderr.write(`\r🎬 Video ${data.status} (${elapsed}s)\n`); return null; }
|
|
485
|
+
if (data.status === 'failed' || data.status === 'abandoned') { process.stderr.write(`\r🎬 Video ${data.status} (${elapsed}s)${data.error ? `: ${data.error}` : ''}\n`); return null; }
|
|
472
486
|
process.stderr.write(`\r🎬 Video rendering... ${elapsed}s`);
|
|
473
487
|
} catch { /* retry */ }
|
|
474
488
|
if (elapsed > 600) { process.stderr.write(`\r🎬 Video timeout (${elapsed}s)\n`); return null; }
|
|
@@ -652,6 +666,104 @@ function imageToArg(imgPath) {
|
|
|
652
666
|
return readImageAsDataUrl(imgPath);
|
|
653
667
|
}
|
|
654
668
|
|
|
669
|
+
function isHttpUrl(value) {
|
|
670
|
+
return value?.startsWith('http://') || value?.startsWith('https://');
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function probeVideoWithFfprobe(videoPath) {
|
|
674
|
+
try {
|
|
675
|
+
const out = execFileSync('ffprobe', [
|
|
676
|
+
'-v', 'error',
|
|
677
|
+
'-select_streams', 'v:0',
|
|
678
|
+
'-show_entries', 'stream=width,height:format=duration',
|
|
679
|
+
'-of', 'json',
|
|
680
|
+
videoPath,
|
|
681
|
+
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
682
|
+
const data = JSON.parse(out);
|
|
683
|
+
const stream = data.streams?.[0] || {};
|
|
684
|
+
const duration = Number(data.format?.duration);
|
|
685
|
+
const width = Number(stream.width);
|
|
686
|
+
const height = Number(stream.height);
|
|
687
|
+
if (Number.isFinite(duration) && width > 0 && height > 0) {
|
|
688
|
+
return { duration, width, height };
|
|
689
|
+
}
|
|
690
|
+
} catch { /* ffprobe unavailable or file unsupported */ }
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function probeVideoWithFfmpeg(videoPath) {
|
|
695
|
+
try {
|
|
696
|
+
const out = execFileSync('ffmpeg', ['-i', videoPath], {
|
|
697
|
+
encoding: 'utf8',
|
|
698
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
699
|
+
});
|
|
700
|
+
return parseFfmpegProbe(out);
|
|
701
|
+
} catch (e) {
|
|
702
|
+
const text = `${e.stdout || ''}\n${e.stderr || ''}`;
|
|
703
|
+
return parseFfmpegProbe(text);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function parseFfmpegProbe(text) {
|
|
708
|
+
const durationMatch = text.match(/Duration:\s*(\d+):(\d+):(\d+(?:\.\d+)?)/);
|
|
709
|
+
const sizeMatch = text.match(/Video:.*?(\d{2,5})x(\d{2,5})/);
|
|
710
|
+
if (!durationMatch || !sizeMatch) return null;
|
|
711
|
+
const duration = Number(durationMatch[1]) * 3600 + Number(durationMatch[2]) * 60 + Number(durationMatch[3]);
|
|
712
|
+
const width = Number(sizeMatch[1]);
|
|
713
|
+
const height = Number(sizeMatch[2]);
|
|
714
|
+
if (!Number.isFinite(duration) || width <= 0 || height <= 0) return null;
|
|
715
|
+
return { duration, width, height };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function probeLocalVideo(videoPath) {
|
|
719
|
+
return probeVideoWithFfprobe(videoPath) || probeVideoWithFfmpeg(videoPath);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function validateVideoFile(videoPath) {
|
|
723
|
+
if (!fs.existsSync(videoPath)) {
|
|
724
|
+
return { ok: false, error: `Video file not found: ${videoPath}` };
|
|
725
|
+
}
|
|
726
|
+
const stat = fs.statSync(videoPath);
|
|
727
|
+
if (stat.size > MAX_VIDEO_FILE_SIZE) {
|
|
728
|
+
return { ok: false, error: `Video too large: ${(stat.size / 1024 / 1024).toFixed(1)}MB (max 200MB)` };
|
|
729
|
+
}
|
|
730
|
+
const ext = path.extname(videoPath).slice(1).toLowerCase();
|
|
731
|
+
if (!['mp4', 'mov', 'webm'].includes(ext)) {
|
|
732
|
+
return { ok: false, error: `Unsupported video format: .${ext}. Use MP4, MOV, or WebM.` };
|
|
733
|
+
}
|
|
734
|
+
const meta = probeLocalVideo(videoPath);
|
|
735
|
+
if (!meta) {
|
|
736
|
+
return { ok: false, error: 'Cannot read video duration/resolution. Install ffmpeg/ffprobe or use the normal frontend upload flow.' };
|
|
737
|
+
}
|
|
738
|
+
if (meta.duration > MAX_VIDEO_DURATION) {
|
|
739
|
+
return { ok: false, error: `Video too long: ${Math.round(meta.duration)}s (max ${MAX_VIDEO_DURATION}s)` };
|
|
740
|
+
}
|
|
741
|
+
if (meta.width * meta.height > MAX_VIDEO_FRAME_PIXELS) {
|
|
742
|
+
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.` };
|
|
743
|
+
}
|
|
744
|
+
const mime = ext === 'mov' ? 'video/quicktime' : ext === 'webm' ? 'video/webm' : 'video/mp4';
|
|
745
|
+
return { ok: true, mime, meta };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function validateVideoFileForAnalysis(videoPath) {
|
|
749
|
+
if (!fs.existsSync(videoPath)) {
|
|
750
|
+
return { ok: false, error: `Video file not found: ${videoPath}` };
|
|
751
|
+
}
|
|
752
|
+
const stat = fs.statSync(videoPath);
|
|
753
|
+
if (stat.size === 0) {
|
|
754
|
+
return { ok: false, error: `Video file is empty: ${videoPath}` };
|
|
755
|
+
}
|
|
756
|
+
if (stat.size > MAX_VIDEO_FILE_SIZE) {
|
|
757
|
+
return { ok: false, error: `Video too large: ${(stat.size / 1024 / 1024).toFixed(1)}MB (max 200MB)` };
|
|
758
|
+
}
|
|
759
|
+
const ext = path.extname(videoPath).slice(1).toLowerCase();
|
|
760
|
+
if (!['mp4', 'mov', 'webm'].includes(ext)) {
|
|
761
|
+
return { ok: false, error: `Unsupported video format: .${ext}. Use MP4, MOV, or WebM.` };
|
|
762
|
+
}
|
|
763
|
+
const mime = ext === 'mov' ? 'video/quicktime' : ext === 'webm' ? 'video/webm' : 'video/mp4';
|
|
764
|
+
return { ok: true, mime };
|
|
765
|
+
}
|
|
766
|
+
|
|
655
767
|
function saveMcpImage(result, outputPath) {
|
|
656
768
|
const content = result?.content || [];
|
|
657
769
|
const textBlock = content.find(c => c.type === 'text');
|
|
@@ -667,6 +779,29 @@ function saveMcpImage(result, outputPath) {
|
|
|
667
779
|
return null;
|
|
668
780
|
}
|
|
669
781
|
|
|
782
|
+
async function analyzeVideoCli(baseUrl, headers, rawVideo, questionParts) {
|
|
783
|
+
if (!rawVideo) {
|
|
784
|
+
console.error('Usage: makaron analyze --video <file|url> ["question"]');
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
let videoUrl = isHttpUrl(rawVideo) ? rawVideo : null;
|
|
788
|
+
if (videoUrl) {
|
|
789
|
+
process.stderr.write('📹 Using public video URL for analysis; provider limits apply.\n');
|
|
790
|
+
} else {
|
|
791
|
+
const valid = validateVideoFileForAnalysis(rawVideo);
|
|
792
|
+
if (!valid.ok) { console.error(`❌ ${valid.error}`); process.exit(1); }
|
|
793
|
+
process.stderr.write(`📹 Uploading ${path.basename(rawVideo)} (${(fs.statSync(rawVideo).size/1024/1024).toFixed(1)}MB)...\n`);
|
|
794
|
+
videoUrl = await uploadFileViaSignedUrl(baseUrl, headers, undefined, rawVideo, valid.mime);
|
|
795
|
+
if (!videoUrl) process.exit(1);
|
|
796
|
+
process.stderr.write(`📹 Uploaded: ${path.basename(rawVideo)}\n`);
|
|
797
|
+
}
|
|
798
|
+
process.stderr.write('🔎 Analyzing video...\n');
|
|
799
|
+
const question = questionParts.join(' ').trim() || undefined;
|
|
800
|
+
const result = await callMcpTool(baseUrl, headers, 'makaron_analyze_video', { videoUrl, question });
|
|
801
|
+
const text = result?.content?.find(c => c.type === 'text')?.text;
|
|
802
|
+
if (text) console.log(text);
|
|
803
|
+
}
|
|
804
|
+
|
|
670
805
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
671
806
|
|
|
672
807
|
const args = process.argv.slice(2);
|
|
@@ -723,6 +858,17 @@ if (command === 'login') {
|
|
|
723
858
|
// Split images into URLs vs local files
|
|
724
859
|
const imageUrlList = chatImages.filter(p => p.startsWith('http://') || p.startsWith('https://'));
|
|
725
860
|
const imageFileList = chatImages.filter(p => !p.startsWith('http://') && !p.startsWith('https://'));
|
|
861
|
+
const prevalidatedVideoUrlList = chatVideos.filter(p => p.startsWith('http://') || p.startsWith('https://'));
|
|
862
|
+
const prevalidatedVideoFileList = chatVideos.filter(p => !p.startsWith('http://') && !p.startsWith('https://'));
|
|
863
|
+
const prevalidatedVideoMetas = new Map();
|
|
864
|
+
for (const videoPath of prevalidatedVideoFileList) {
|
|
865
|
+
const valid = validateVideoFile(videoPath);
|
|
866
|
+
if (!valid.ok) {
|
|
867
|
+
process.stderr.write(`❌ ${valid.error}\n`);
|
|
868
|
+
process.exit(1);
|
|
869
|
+
}
|
|
870
|
+
prevalidatedVideoMetas.set(videoPath, valid.meta);
|
|
871
|
+
}
|
|
726
872
|
|
|
727
873
|
// --project auto: create a new project (with images/videos if provided)
|
|
728
874
|
if (!projectId || projectId === 'auto') {
|
|
@@ -788,40 +934,36 @@ if (command === 'login') {
|
|
|
788
934
|
// Upload videos to project timeline (via /api/projects/create with videoUrls)
|
|
789
935
|
let finalPrompt = prompt;
|
|
790
936
|
if (chatVideos.length > 0) {
|
|
791
|
-
const videoUrlList = chatVideos.filter(p => p.startsWith('http://') || p.startsWith('https://'));
|
|
792
|
-
const videoFileList = chatVideos.filter(p => !p.startsWith('http://') && !p.startsWith('https://'));
|
|
793
|
-
|
|
794
|
-
// Validate local video files
|
|
795
|
-
const validVideoFiles = [];
|
|
796
|
-
for (const videoPath of videoFileList) {
|
|
797
|
-
if (!fs.existsSync(videoPath)) { process.stderr.write(`⚠️ Video file not found: ${videoPath}\n`); continue; }
|
|
798
|
-
const stat = fs.statSync(videoPath);
|
|
799
|
-
if (stat.size > 200 * 1024 * 1024) { process.stderr.write(`⚠️ Video too large: ${(stat.size/1024/1024).toFixed(1)}MB (max 200MB)\n`); continue; }
|
|
800
|
-
const ext = path.extname(videoPath).slice(1).toLowerCase();
|
|
801
|
-
if (!['mp4', 'mov', 'webm'].includes(ext)) { process.stderr.write(`⚠️ Unsupported video format: .${ext}. Use MP4, MOV, or WebM.\n`); continue; }
|
|
802
|
-
validVideoFiles.push(videoPath);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
937
|
// Upload local files via signed URL (no size limit, works with API key auth)
|
|
806
|
-
const uploadedVideoUrls = [...
|
|
807
|
-
|
|
938
|
+
const uploadedVideoUrls = [...prevalidatedVideoUrlList];
|
|
939
|
+
const uploadedVideoMetas = prevalidatedVideoUrlList.map(() => null);
|
|
940
|
+
if (prevalidatedVideoUrlList.length) {
|
|
941
|
+
process.stderr.write(`📹 Assuming public video URL(s) already match Makaron upload limits: ≤${MAX_VIDEO_DURATION}s, ≤200MB, ≤1080p.\n`);
|
|
942
|
+
}
|
|
943
|
+
for (const videoPath of prevalidatedVideoFileList) {
|
|
808
944
|
process.stderr.write(`📹 Uploading ${path.basename(videoPath)} (${(fs.statSync(videoPath).size/1024/1024).toFixed(1)}MB)...\n`);
|
|
809
945
|
const ext = path.extname(videoPath).slice(1).toLowerCase();
|
|
810
946
|
const mime = ext === 'mov' ? 'video/quicktime' : ext === 'webm' ? 'video/webm' : 'video/mp4';
|
|
811
947
|
const url = await uploadFileViaSignedUrl(baseUrl, headers, projectId, videoPath, mime);
|
|
812
948
|
if (url) {
|
|
813
949
|
uploadedVideoUrls.push(url);
|
|
950
|
+
uploadedVideoMetas.push(prevalidatedVideoMetas.get(videoPath) || null);
|
|
814
951
|
process.stderr.write(`📹 Uploaded: ${path.basename(videoPath)}\n`);
|
|
815
952
|
}
|
|
816
953
|
}
|
|
817
954
|
|
|
818
955
|
// Add videos to project via projects/create (same as images)
|
|
956
|
+
if (uploadedVideoUrls.length === 0) {
|
|
957
|
+
process.stderr.write(`❌ No valid videos were uploaded. Local videos must be MP4/MOV/WebM, ≤${MAX_VIDEO_DURATION}s, ≤200MB, and ≤1080p.\n`);
|
|
958
|
+
process.exit(1);
|
|
959
|
+
}
|
|
960
|
+
|
|
819
961
|
if (uploadedVideoUrls.length > 0) {
|
|
820
|
-
if (
|
|
962
|
+
if (prevalidatedVideoUrlList.length) process.stderr.write(`📹 Adding ${uploadedVideoUrls.length} video(s) to timeline...\n`);
|
|
821
963
|
const res = await fetch(`${baseUrl}/api/projects/create`, {
|
|
822
964
|
method: 'POST',
|
|
823
965
|
headers: { 'Content-Type': 'application/json', ...headers },
|
|
824
|
-
body: JSON.stringify({ _addToProject: projectId, videoUrls: uploadedVideoUrls }),
|
|
966
|
+
body: JSON.stringify({ _addToProject: projectId, videoUrls: uploadedVideoUrls, videoMetas: uploadedVideoMetas }),
|
|
825
967
|
});
|
|
826
968
|
if (res.ok) {
|
|
827
969
|
const data = await res.json();
|
|
@@ -881,6 +1023,7 @@ if (command === 'login') {
|
|
|
881
1023
|
const res = await fetch(`${baseUrl}/api/agent/run/${runId}`, { headers });
|
|
882
1024
|
if (!res.ok) { process.stderr.write(`Error ${res.status}: ${await res.text()}\n`); process.exit(1); }
|
|
883
1025
|
const data = await res.json();
|
|
1026
|
+
normalizeRunResponse(data);
|
|
884
1027
|
if (pick) {
|
|
885
1028
|
const picked = applyPick(data, pick);
|
|
886
1029
|
if (picked !== undefined) console.log(typeof picked === 'string' ? picked : JSON.stringify(picked));
|
|
@@ -970,6 +1113,16 @@ if (command === 'login') {
|
|
|
970
1113
|
const result = await callMcpTool(baseUrl, headers, 'makaron_edit_image', editArgs);
|
|
971
1114
|
saveMcpImage(result, outputPath);
|
|
972
1115
|
|
|
1116
|
+
} else if (command === 'analyze') {
|
|
1117
|
+
const { headers, baseUrl } = getAuth();
|
|
1118
|
+
let video = null;
|
|
1119
|
+
const questionParts = [];
|
|
1120
|
+
for (let i = 1; i < args.length; i++) {
|
|
1121
|
+
if (args[i] === '--video' && args[i + 1]) video = args[++i];
|
|
1122
|
+
else questionParts.push(args[i]);
|
|
1123
|
+
}
|
|
1124
|
+
await analyzeVideoCli(baseUrl, headers, video, questionParts);
|
|
1125
|
+
|
|
973
1126
|
} else if (command === 'video') {
|
|
974
1127
|
const { headers, baseUrl } = getAuth();
|
|
975
1128
|
const sub = args[1];
|
|
@@ -991,52 +1144,61 @@ if (command === 'login') {
|
|
|
991
1144
|
|
|
992
1145
|
} else if (sub === 'create') {
|
|
993
1146
|
const images = [];
|
|
994
|
-
let script = '', duration = undefined, aspectRatio = undefined, videoModel = undefined,
|
|
1147
|
+
let script = '', duration = undefined, aspectRatio = undefined, videoModel = undefined, wait = false;
|
|
1148
|
+
let video = null, keepOriginalSound = false;
|
|
995
1149
|
for (let i = 2; i < args.length; i++) {
|
|
996
1150
|
if (args[i] === '--image' && args[i + 1]) images.push(args[++i]);
|
|
1151
|
+
else if (args[i] === '--video' && args[i + 1]) video = args[++i];
|
|
997
1152
|
else if (args[i] === '--script' && args[i + 1]) script = args[++i];
|
|
998
1153
|
else if (args[i] === '--script-file' && args[i + 1]) script = fs.readFileSync(args[++i], 'utf-8');
|
|
999
1154
|
else if (args[i] === '--duration' && args[i + 1]) duration = Number(args[++i]);
|
|
1000
1155
|
else if (args[i] === '--aspect' && args[i + 1]) aspectRatio = args[++i];
|
|
1001
1156
|
else if (args[i] === '--model' && args[i + 1]) videoModel = args[++i];
|
|
1002
|
-
else if (args[i] === '--
|
|
1157
|
+
else if (args[i] === '--keep-original-sound') keepOriginalSound = true;
|
|
1158
|
+
else if (args[i] === '--project') {
|
|
1159
|
+
console.error('Usage: video create no longer supports --project. Use: makaron chat --project <id> --video <file|url> "your request"');
|
|
1160
|
+
process.exit(1);
|
|
1161
|
+
}
|
|
1003
1162
|
else if (args[i] === '--wait') wait = true;
|
|
1004
1163
|
}
|
|
1005
|
-
if (!images.length
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
if (videoModel) body.videoModel = videoModel;
|
|
1014
|
-
const res = await fetch(`${baseUrl}/api/video-snapshot`, {
|
|
1015
|
-
method: 'POST',
|
|
1016
|
-
headers: { 'Content-Type': 'application/json', ...headers },
|
|
1017
|
-
body: JSON.stringify(body),
|
|
1018
|
-
});
|
|
1019
|
-
if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
|
|
1020
|
-
const data = await res.json();
|
|
1021
|
-
process.stderr.write(`✅ Video snapshot: ${data.snapshotId} (task: ${data.taskId})\n`);
|
|
1022
|
-
if (wait) {
|
|
1023
|
-
const url = await pollVideo(baseUrl, headers, data.taskId, data.snapshotId);
|
|
1024
|
-
if (url) console.log(url);
|
|
1025
|
-
} else {
|
|
1026
|
-
console.log(JSON.stringify(data, null, 2));
|
|
1027
|
-
}
|
|
1028
|
-
} else {
|
|
1029
|
-
// No project: use MCP tool (standalone, no timeline write)
|
|
1030
|
-
process.stderr.write('🎬 Submitting video...\n');
|
|
1031
|
-
const vArgs = { script, images };
|
|
1032
|
-
if (duration) vArgs.duration = duration;
|
|
1033
|
-
if (aspectRatio) vArgs.aspectRatio = aspectRatio;
|
|
1034
|
-
if (videoModel) vArgs.videoModel = videoModel;
|
|
1035
|
-
const result = await callMcpTool(baseUrl, headers, 'makaron_create_video', vArgs);
|
|
1036
|
-
const text = result?.content?.find(c => c.type === 'text')?.text;
|
|
1037
|
-
if (text) console.log(text);
|
|
1164
|
+
if ((!images.length && !video) || !script) {
|
|
1165
|
+
console.error('Usage: makaron video create --script "..." (--image <url> | --video <public-url>) [--duration 10] [--aspect 9:16] [--model kling|seedance] [--keep-original-sound]');
|
|
1166
|
+
process.exit(1);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (wait) {
|
|
1170
|
+
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.');
|
|
1171
|
+
process.exit(1);
|
|
1038
1172
|
}
|
|
1039
1173
|
|
|
1174
|
+
let videoUrl = isHttpUrl(video) ? video : null;
|
|
1175
|
+
let inputVideoMeta = null;
|
|
1176
|
+
if (videoUrl) {
|
|
1177
|
+
process.stderr.write(`📹 Assuming public video URL already matches Makaron upload limits: ≤${MAX_VIDEO_DURATION}s, ≤200MB, ≤1080p.\n`);
|
|
1178
|
+
}
|
|
1179
|
+
if (video && !videoUrl) {
|
|
1180
|
+
const valid = validateVideoFile(video);
|
|
1181
|
+
if (!valid.ok) { console.error(`❌ ${valid.error}`); process.exit(1); }
|
|
1182
|
+
inputVideoMeta = valid.meta;
|
|
1183
|
+
process.stderr.write(`📹 Uploading ${path.basename(video)} (${(fs.statSync(video).size/1024/1024).toFixed(1)}MB)...\n`);
|
|
1184
|
+
videoUrl = await uploadFileViaSignedUrl(baseUrl, headers, undefined, video, valid.mime);
|
|
1185
|
+
if (!videoUrl) process.exit(1);
|
|
1186
|
+
process.stderr.write(`📹 Uploaded: ${path.basename(video)}\n`);
|
|
1187
|
+
}
|
|
1188
|
+
// Standalone MCP tool (no project timeline write)
|
|
1189
|
+
process.stderr.write('🎬 Submitting video...\n');
|
|
1190
|
+
const vArgs = videoUrl
|
|
1191
|
+
? { videoUrl, editPrompt: script, images, videoModel: videoModel || 'kling', referType: (videoModel || 'kling') === 'seedance' ? 'feature' : 'base' }
|
|
1192
|
+
: { script, images };
|
|
1193
|
+
const effectiveDuration = duration || (inputVideoMeta?.duration ? Math.round(inputVideoMeta.duration) : undefined);
|
|
1194
|
+
if (effectiveDuration) vArgs.duration = effectiveDuration;
|
|
1195
|
+
if (aspectRatio) vArgs.aspectRatio = aspectRatio;
|
|
1196
|
+
if (videoModel && !videoUrl) vArgs.videoModel = videoModel;
|
|
1197
|
+
if (keepOriginalSound && videoUrl) vArgs.keepOriginalSound = true;
|
|
1198
|
+
const result = await callMcpTool(baseUrl, headers, videoUrl ? 'makaron_edit_video' : 'makaron_create_video', vArgs);
|
|
1199
|
+
const text = result?.content?.find(c => c.type === 'text')?.text;
|
|
1200
|
+
if (text) console.log(text);
|
|
1201
|
+
|
|
1040
1202
|
} else if (sub === 'status') {
|
|
1041
1203
|
let taskId = null, snapshotId = null, wait = false;
|
|
1042
1204
|
for (let i = 2; i < args.length; i++) {
|
|
@@ -1074,6 +1236,7 @@ if (command === 'login') {
|
|
|
1074
1236
|
console.log(`Video commands:
|
|
1075
1237
|
video script --image <file> [--image <file>] "direction" Write video script
|
|
1076
1238
|
video create --script "..." --image <url> [--duration 10] Submit video task
|
|
1239
|
+
video create --script "..." --video <public-url> [--model kling|seedance] Edit a video (standalone)
|
|
1077
1240
|
video status <taskId> Check video status
|
|
1078
1241
|
video status --snapshot <snapshotId> [--wait] Check v2 video snapshot
|
|
1079
1242
|
`);
|
|
@@ -1341,7 +1504,8 @@ Commands:
|
|
|
1341
1504
|
abort <runId> Abort a running Agent
|
|
1342
1505
|
|
|
1343
1506
|
edit [--image <file>] "prompt" AI image edit / text-to-image
|
|
1344
|
-
video
|
|
1507
|
+
analyze --video <file|url> Analyze video content
|
|
1508
|
+
video script|create|status Video generation
|
|
1345
1509
|
music create|status Music generation
|
|
1346
1510
|
|
|
1347
1511
|
admin Admin commands (skills, upload, set-admin)
|
package/package.json
CHANGED
package/skills/makaron/SKILL.md
CHANGED
|
@@ -86,6 +86,26 @@ Returns immediately:
|
|
|
86
86
|
npx makaron-cli chat --project <id> --image ref1.jpg --image ref2.jpg -b "use these as style reference"
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
### With video input (edit, compose, extend)
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Upload a video and transform it — Agent understands video content natively
|
|
93
|
+
npx makaron-cli chat --project auto --video selfie.mp4 -b "put Iron Man armor on me"
|
|
94
|
+
|
|
95
|
+
# Combine a person's photo with a video scene
|
|
96
|
+
npx makaron-cli chat --project <id> --video party.mp4 --image kid.jpg -b "make this kid join the party"
|
|
97
|
+
|
|
98
|
+
# Multiple videos — compose or splice
|
|
99
|
+
npx makaron-cli chat --project <id> --video clip1.mp4 --video clip2.mp4 -b "combine into one seamless video"
|
|
100
|
+
|
|
101
|
+
# Video URL (public, downloadable)
|
|
102
|
+
npx makaron-cli chat --project auto --video https://example.com/dance.mp4 -b "extend this to 15 seconds"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Supported formats: MP4, MOV, WebM. CLI local video uploads follow the same compatibility contract as the normal frontend flow: max 200MB, max 15s, and <=1080p / 2,086,876 frame pixels. The frontend can transcode oversized videos; the CLI rejects them so later Seedance editing does not fail. Videos are uploaded to the project timeline. The Agent can analyze scenes, edit content, compose multiple clips, extend duration, and add effects — all via natural language. Seedance video-reference editing is supported for <=15s videos that meet these upload limits; Kling remains the base/direct edit path.
|
|
106
|
+
|
|
107
|
+
Use `chat --project <id|auto> --video ...` for any project/timeline video work. Direct video commands are standalone raw-tool calls.
|
|
108
|
+
|
|
89
109
|
### Check status (single query)
|
|
90
110
|
|
|
91
111
|
```bash
|
|
@@ -142,20 +162,35 @@ npx makaron-cli edit --image photo.jpg --out result.jpg "make it dramatic"
|
|
|
142
162
|
|
|
143
163
|
Options: `--image`, `--model gemini|qwen|openai|pony|wai`, `--skill enhance|creative|wild|captions`, `--ref <file>` (up to 3), `--aspect <ratio>`, `--out <path>`
|
|
144
164
|
|
|
145
|
-
### `video` —
|
|
165
|
+
### `video` — Standalone video tools (no project timeline)
|
|
146
166
|
|
|
147
167
|
```bash
|
|
148
168
|
# 1. Write script from images
|
|
149
169
|
npx makaron-cli video script --image img1.jpg "cinematic story"
|
|
150
170
|
|
|
151
|
-
# 2.
|
|
171
|
+
# 2. Analyze a video (standalone, no timeline write)
|
|
172
|
+
npx makaron-cli analyze --video input.mp4 "describe the key actions and pacing"
|
|
173
|
+
|
|
174
|
+
# 3a. Submit image-to-video rendering (images must be public URLs from step 1 or uploaded)
|
|
152
175
|
npx makaron-cli video create --script "Shot 1 (5s): <<<image_1>>> ..." --image https://...jpg --duration 5 --model kling
|
|
153
176
|
|
|
154
|
-
#
|
|
177
|
+
# 3b. Edit a video from a local file or public URL
|
|
178
|
+
npx makaron-cli video create --script "make it funny" --video input.mp4 --duration 5 --model seedance
|
|
179
|
+
npx makaron-cli video create --script "make it warmer and cinematic" --video https://example.com/input.mp4 --duration 5 --model seedance
|
|
180
|
+
|
|
181
|
+
# 4. Check status
|
|
155
182
|
npx makaron-cli video status <taskId>
|
|
156
183
|
```
|
|
157
184
|
|
|
158
|
-
|
|
185
|
+
`video create` returns a provider task id and does not create or update a Makaron project timeline. For project/timeline video editing, use:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npx makaron-cli chat --project <id|auto> --video input.mp4 -b "make it funny"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--video <file|url>`, `--duration 3|5|7|10|15`, `--aspect 9:16|16:9|1:1`, `--model kling|seedance`
|
|
192
|
+
|
|
193
|
+
Video edit model behavior: `--model kling --video` uses Kling base/direct edit internally; `--model seedance --video` uses the Seedance video-reference path and requires <=15s, <=1080p input.
|
|
159
194
|
|
|
160
195
|
### `music` — Music generation
|
|
161
196
|
|
|
@@ -184,7 +219,7 @@ type MakaronOutput =
|
|
|
184
219
|
| { id: string; type: "text"; status: "completed"; content: string }
|
|
185
220
|
| { id: string; type: "image"; status: "completed"; url: string; snapshot_id: string }
|
|
186
221
|
| { id: string; type: "design"; status: "completed"; url: string; width: number; height: number; animated: boolean; duration?: number }
|
|
187
|
-
| { id: string; type: "video"; status: "queued"|"rendering"|"completed"|"failed"; task_id: string; url?: string; elapsed_seconds?: number }
|
|
222
|
+
| { id: string; type: "video"; status: "queued"|"rendering"|"completed"|"failed"; task_id: string; snapshot_id?: string; url?: string; elapsed_seconds?: number; width?: number; height?: number }
|
|
188
223
|
| { id: string; type: "music"; status: "queued"|"rendering"|"completed"|"failed"; task_id: string; url?: string; elapsed_seconds?: number }
|
|
189
224
|
```
|
|
190
225
|
|