makaron-cli 0.7.2 → 0.7.6
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 +22 -6
- package/SKILL.md +22 -5
- package/bin/makaron.mjs +231 -58
- package/package.json +1 -1
- package/skills/makaron/SKILL.md +22 -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
|
@@ -110,8 +110,9 @@ npx makaron-cli chat --project <id> --video party.mp4 --image kid.jpg -b "make t
|
|
|
110
110
|
npx makaron-cli chat --project <id> --video clip1.mp4 --video clip2.mp4 -b "splice these into one seamless video"
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
Video files are uploaded via signed URL
|
|
114
|
-
The agent understands video content natively — it can analyze scenes, edit, extend, and compose videos.
|
|
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, target max 15s with 0.5s metadata tolerance, 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.
|
|
115
116
|
|
|
116
117
|
### Check status (single query)
|
|
117
118
|
|
|
@@ -169,20 +170,35 @@ npx makaron-cli edit --image photo.jpg --out result.jpg "make it dramatic"
|
|
|
169
170
|
|
|
170
171
|
Options: `--image`, `--model gemini|qwen|openai|pony|wai`, `--skill enhance|creative|wild|captions`, `--ref <file>` (up to 3), `--aspect <ratio>`, `--out <path>`
|
|
171
172
|
|
|
172
|
-
### `video` —
|
|
173
|
+
### `video` — Standalone video tools (no project timeline)
|
|
173
174
|
|
|
174
175
|
```bash
|
|
175
176
|
# 1. Write script from images
|
|
176
177
|
npx makaron-cli video script --image img1.jpg "cinematic story"
|
|
177
178
|
|
|
178
|
-
# 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)
|
|
179
183
|
npx makaron-cli video create --script "Shot 1 (5s): <<<image_1>>> ..." --image https://...jpg --duration 5 --model kling
|
|
180
184
|
|
|
181
|
-
#
|
|
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
|
|
182
190
|
npx makaron-cli video status <taskId>
|
|
183
191
|
```
|
|
184
192
|
|
|
185
|
-
|
|
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 target <=15s, <=1080p input. Tiny metadata padding up to 15.5s is accepted and output duration is clamped to 15s.
|
|
186
202
|
|
|
187
203
|
### `music` — Music generation
|
|
188
204
|
|
package/SKILL.md
CHANGED
|
@@ -102,7 +102,9 @@ npx makaron-cli chat --project <id> --video clip1.mp4 --video clip2.mp4 -b "comb
|
|
|
102
102
|
npx makaron-cli chat --project auto --video https://example.com/dance.mp4 -b "extend this to 15 seconds"
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
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, target max 15s with 0.5s metadata tolerance, 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.
|
|
106
108
|
|
|
107
109
|
### Check status (single query)
|
|
108
110
|
|
|
@@ -160,20 +162,35 @@ npx makaron-cli edit --image photo.jpg --out result.jpg "make it dramatic"
|
|
|
160
162
|
|
|
161
163
|
Options: `--image`, `--model gemini|qwen|openai|pony|wai`, `--skill enhance|creative|wild|captions`, `--ref <file>` (up to 3), `--aspect <ratio>`, `--out <path>`
|
|
162
164
|
|
|
163
|
-
### `video` —
|
|
165
|
+
### `video` — Standalone video tools (no project timeline)
|
|
164
166
|
|
|
165
167
|
```bash
|
|
166
168
|
# 1. Write script from images
|
|
167
169
|
npx makaron-cli video script --image img1.jpg "cinematic story"
|
|
168
170
|
|
|
169
|
-
# 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)
|
|
170
175
|
npx makaron-cli video create --script "Shot 1 (5s): <<<image_1>>> ..." --image https://...jpg --duration 5 --model kling
|
|
171
176
|
|
|
172
|
-
#
|
|
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
|
|
173
182
|
npx makaron-cli video status <taskId>
|
|
174
183
|
```
|
|
175
184
|
|
|
176
|
-
|
|
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 target <=15s, <=1080p input. Tiny metadata padding up to 15.5s is accepted and output duration is clamped to 15s.
|
|
177
194
|
|
|
178
195
|
### `music` — Music generation
|
|
179
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,16 @@ 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_DURATION_TOLERANCE = 0.5;
|
|
33
|
+
const MAX_VIDEO_FRAME_PIXELS = 2_086_876;
|
|
34
|
+
|
|
35
|
+
function formatSeconds(seconds) {
|
|
36
|
+
if (!Number.isFinite(seconds)) return String(seconds);
|
|
37
|
+
return Number.isInteger(seconds) ? String(seconds) : seconds.toFixed(1).replace(/\.0$/, '');
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
// ─── Auth ────────────────────────────────────────────────────────────────────
|
|
30
41
|
|
|
31
42
|
function loadAuth() {
|
|
@@ -116,6 +127,15 @@ function getAuth() {
|
|
|
116
127
|
};
|
|
117
128
|
}
|
|
118
129
|
|
|
130
|
+
function normalizeRunResponse(data) {
|
|
131
|
+
const projectId = data.projectId || data.project_id;
|
|
132
|
+
if (projectId) {
|
|
133
|
+
data.projectId = projectId;
|
|
134
|
+
data.projectUrl = `${APP_URL}/projects/${projectId}`;
|
|
135
|
+
}
|
|
136
|
+
return data;
|
|
137
|
+
}
|
|
138
|
+
|
|
119
139
|
// ─── SSE Consumer ────────────────────────────────────────────────────────────
|
|
120
140
|
|
|
121
141
|
async function abortRun(baseUrl, headers, runId) {
|
|
@@ -351,8 +371,7 @@ async function pollRun(baseUrl, headers, runId, opts = {}) {
|
|
|
351
371
|
|
|
352
372
|
if (json) {
|
|
353
373
|
// Structured JSON output — add projectUrl
|
|
354
|
-
data
|
|
355
|
-
console.log(JSON.stringify(data, null, 2));
|
|
374
|
+
console.log(JSON.stringify(normalizeRunResponse(data), null, 2));
|
|
356
375
|
} else {
|
|
357
376
|
process.stderr.write('\n━━━ Results ━━━\n');
|
|
358
377
|
if (data.result) {
|
|
@@ -360,6 +379,7 @@ async function pollRun(baseUrl, headers, runId, opts = {}) {
|
|
|
360
379
|
for (const d of data.result.designs || []) process.stderr.write(`🎨 Design (${d.width}x${d.height})\n`);
|
|
361
380
|
for (const v of data.result.videos || []) {
|
|
362
381
|
if (v.videoUrl) process.stderr.write(`🎬 Video: ${v.videoUrl}\n`);
|
|
382
|
+
else if (v.status === 'failed') process.stderr.write(`🎬 Video ${v.taskId}: failed${v.error ? ` — ${v.error}` : ''}\n`);
|
|
363
383
|
else process.stderr.write(`🎬 Video ${v.taskId}: ${v.status || 'submitted'}\n`);
|
|
364
384
|
}
|
|
365
385
|
for (const m of data.result.music || []) {
|
|
@@ -432,7 +452,7 @@ async function watchRun(baseUrl, headers, runId, opts = {}) {
|
|
|
432
452
|
} else if (JSON.stringify(prev) !== JSON.stringify(item)) {
|
|
433
453
|
// Updated item (e.g. video status changed)
|
|
434
454
|
if (jsonl) console.log(JSON.stringify({ event: 'output.updated', item }));
|
|
435
|
-
else process.stderr.write(`~ [${item.type}] ${item.status} ${item.url || ''}\n`);
|
|
455
|
+
else process.stderr.write(`~ [${item.type}] ${item.status} ${item.url || item.error || ''}\n`);
|
|
436
456
|
}
|
|
437
457
|
}
|
|
438
458
|
|
|
@@ -468,7 +488,7 @@ async function pollVideo(baseUrl, headers, taskId, snapshotId) {
|
|
|
468
488
|
if (!res.ok) continue;
|
|
469
489
|
const data = await res.json();
|
|
470
490
|
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; }
|
|
491
|
+
if (data.status === 'failed' || data.status === 'abandoned') { process.stderr.write(`\r🎬 Video ${data.status} (${elapsed}s)${data.error ? `: ${data.error}` : ''}\n`); return null; }
|
|
472
492
|
process.stderr.write(`\r🎬 Video rendering... ${elapsed}s`);
|
|
473
493
|
} catch { /* retry */ }
|
|
474
494
|
if (elapsed > 600) { process.stderr.write(`\r🎬 Video timeout (${elapsed}s)\n`); return null; }
|
|
@@ -652,6 +672,104 @@ function imageToArg(imgPath) {
|
|
|
652
672
|
return readImageAsDataUrl(imgPath);
|
|
653
673
|
}
|
|
654
674
|
|
|
675
|
+
function isHttpUrl(value) {
|
|
676
|
+
return value?.startsWith('http://') || value?.startsWith('https://');
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function probeVideoWithFfprobe(videoPath) {
|
|
680
|
+
try {
|
|
681
|
+
const out = execFileSync('ffprobe', [
|
|
682
|
+
'-v', 'error',
|
|
683
|
+
'-select_streams', 'v:0',
|
|
684
|
+
'-show_entries', 'stream=width,height:format=duration',
|
|
685
|
+
'-of', 'json',
|
|
686
|
+
videoPath,
|
|
687
|
+
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
688
|
+
const data = JSON.parse(out);
|
|
689
|
+
const stream = data.streams?.[0] || {};
|
|
690
|
+
const duration = Number(data.format?.duration);
|
|
691
|
+
const width = Number(stream.width);
|
|
692
|
+
const height = Number(stream.height);
|
|
693
|
+
if (Number.isFinite(duration) && width > 0 && height > 0) {
|
|
694
|
+
return { duration, width, height };
|
|
695
|
+
}
|
|
696
|
+
} catch { /* ffprobe unavailable or file unsupported */ }
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function probeVideoWithFfmpeg(videoPath) {
|
|
701
|
+
try {
|
|
702
|
+
const out = execFileSync('ffmpeg', ['-i', videoPath], {
|
|
703
|
+
encoding: 'utf8',
|
|
704
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
705
|
+
});
|
|
706
|
+
return parseFfmpegProbe(out);
|
|
707
|
+
} catch (e) {
|
|
708
|
+
const text = `${e.stdout || ''}\n${e.stderr || ''}`;
|
|
709
|
+
return parseFfmpegProbe(text);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function parseFfmpegProbe(text) {
|
|
714
|
+
const durationMatch = text.match(/Duration:\s*(\d+):(\d+):(\d+(?:\.\d+)?)/);
|
|
715
|
+
const sizeMatch = text.match(/Video:.*?(\d{2,5})x(\d{2,5})/);
|
|
716
|
+
if (!durationMatch || !sizeMatch) return null;
|
|
717
|
+
const duration = Number(durationMatch[1]) * 3600 + Number(durationMatch[2]) * 60 + Number(durationMatch[3]);
|
|
718
|
+
const width = Number(sizeMatch[1]);
|
|
719
|
+
const height = Number(sizeMatch[2]);
|
|
720
|
+
if (!Number.isFinite(duration) || width <= 0 || height <= 0) return null;
|
|
721
|
+
return { duration, width, height };
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function probeLocalVideo(videoPath) {
|
|
725
|
+
return probeVideoWithFfprobe(videoPath) || probeVideoWithFfmpeg(videoPath);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function validateVideoFile(videoPath) {
|
|
729
|
+
if (!fs.existsSync(videoPath)) {
|
|
730
|
+
return { ok: false, error: `Video file not found: ${videoPath}` };
|
|
731
|
+
}
|
|
732
|
+
const stat = fs.statSync(videoPath);
|
|
733
|
+
if (stat.size > MAX_VIDEO_FILE_SIZE) {
|
|
734
|
+
return { ok: false, error: `Video too large: ${(stat.size / 1024 / 1024).toFixed(1)}MB (max 200MB)` };
|
|
735
|
+
}
|
|
736
|
+
const ext = path.extname(videoPath).slice(1).toLowerCase();
|
|
737
|
+
if (!['mp4', 'mov', 'webm'].includes(ext)) {
|
|
738
|
+
return { ok: false, error: `Unsupported video format: .${ext}. Use MP4, MOV, or WebM.` };
|
|
739
|
+
}
|
|
740
|
+
const meta = probeLocalVideo(videoPath);
|
|
741
|
+
if (!meta) {
|
|
742
|
+
return { ok: false, error: 'Cannot read video duration/resolution. Install ffmpeg/ffprobe or use the normal frontend upload flow.' };
|
|
743
|
+
}
|
|
744
|
+
if (meta.duration > MAX_VIDEO_DURATION + MAX_VIDEO_DURATION_TOLERANCE) {
|
|
745
|
+
return { ok: false, error: `Video too long: ${formatSeconds(meta.duration)}s (max ${MAX_VIDEO_DURATION}s, with ${MAX_VIDEO_DURATION_TOLERANCE}s metadata tolerance)` };
|
|
746
|
+
}
|
|
747
|
+
if (meta.width * meta.height > MAX_VIDEO_FRAME_PIXELS) {
|
|
748
|
+
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.` };
|
|
749
|
+
}
|
|
750
|
+
const mime = ext === 'mov' ? 'video/quicktime' : ext === 'webm' ? 'video/webm' : 'video/mp4';
|
|
751
|
+
return { ok: true, mime, meta };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function validateVideoFileForAnalysis(videoPath) {
|
|
755
|
+
if (!fs.existsSync(videoPath)) {
|
|
756
|
+
return { ok: false, error: `Video file not found: ${videoPath}` };
|
|
757
|
+
}
|
|
758
|
+
const stat = fs.statSync(videoPath);
|
|
759
|
+
if (stat.size === 0) {
|
|
760
|
+
return { ok: false, error: `Video file is empty: ${videoPath}` };
|
|
761
|
+
}
|
|
762
|
+
if (stat.size > MAX_VIDEO_FILE_SIZE) {
|
|
763
|
+
return { ok: false, error: `Video too large: ${(stat.size / 1024 / 1024).toFixed(1)}MB (max 200MB)` };
|
|
764
|
+
}
|
|
765
|
+
const ext = path.extname(videoPath).slice(1).toLowerCase();
|
|
766
|
+
if (!['mp4', 'mov', 'webm'].includes(ext)) {
|
|
767
|
+
return { ok: false, error: `Unsupported video format: .${ext}. Use MP4, MOV, or WebM.` };
|
|
768
|
+
}
|
|
769
|
+
const mime = ext === 'mov' ? 'video/quicktime' : ext === 'webm' ? 'video/webm' : 'video/mp4';
|
|
770
|
+
return { ok: true, mime };
|
|
771
|
+
}
|
|
772
|
+
|
|
655
773
|
function saveMcpImage(result, outputPath) {
|
|
656
774
|
const content = result?.content || [];
|
|
657
775
|
const textBlock = content.find(c => c.type === 'text');
|
|
@@ -667,6 +785,29 @@ function saveMcpImage(result, outputPath) {
|
|
|
667
785
|
return null;
|
|
668
786
|
}
|
|
669
787
|
|
|
788
|
+
async function analyzeVideoCli(baseUrl, headers, rawVideo, questionParts) {
|
|
789
|
+
if (!rawVideo) {
|
|
790
|
+
console.error('Usage: makaron analyze --video <file|url> ["question"]');
|
|
791
|
+
process.exit(1);
|
|
792
|
+
}
|
|
793
|
+
let videoUrl = isHttpUrl(rawVideo) ? rawVideo : null;
|
|
794
|
+
if (videoUrl) {
|
|
795
|
+
process.stderr.write('📹 Using public video URL for analysis; provider limits apply.\n');
|
|
796
|
+
} else {
|
|
797
|
+
const valid = validateVideoFileForAnalysis(rawVideo);
|
|
798
|
+
if (!valid.ok) { console.error(`❌ ${valid.error}`); process.exit(1); }
|
|
799
|
+
process.stderr.write(`📹 Uploading ${path.basename(rawVideo)} (${(fs.statSync(rawVideo).size/1024/1024).toFixed(1)}MB)...\n`);
|
|
800
|
+
videoUrl = await uploadFileViaSignedUrl(baseUrl, headers, undefined, rawVideo, valid.mime);
|
|
801
|
+
if (!videoUrl) process.exit(1);
|
|
802
|
+
process.stderr.write(`📹 Uploaded: ${path.basename(rawVideo)}\n`);
|
|
803
|
+
}
|
|
804
|
+
process.stderr.write('🔎 Analyzing video...\n');
|
|
805
|
+
const question = questionParts.join(' ').trim() || undefined;
|
|
806
|
+
const result = await callMcpTool(baseUrl, headers, 'makaron_analyze_video', { videoUrl, question });
|
|
807
|
+
const text = result?.content?.find(c => c.type === 'text')?.text;
|
|
808
|
+
if (text) console.log(text);
|
|
809
|
+
}
|
|
810
|
+
|
|
670
811
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
671
812
|
|
|
672
813
|
const args = process.argv.slice(2);
|
|
@@ -723,6 +864,17 @@ if (command === 'login') {
|
|
|
723
864
|
// Split images into URLs vs local files
|
|
724
865
|
const imageUrlList = chatImages.filter(p => p.startsWith('http://') || p.startsWith('https://'));
|
|
725
866
|
const imageFileList = chatImages.filter(p => !p.startsWith('http://') && !p.startsWith('https://'));
|
|
867
|
+
const prevalidatedVideoUrlList = chatVideos.filter(p => p.startsWith('http://') || p.startsWith('https://'));
|
|
868
|
+
const prevalidatedVideoFileList = chatVideos.filter(p => !p.startsWith('http://') && !p.startsWith('https://'));
|
|
869
|
+
const prevalidatedVideoMetas = new Map();
|
|
870
|
+
for (const videoPath of prevalidatedVideoFileList) {
|
|
871
|
+
const valid = validateVideoFile(videoPath);
|
|
872
|
+
if (!valid.ok) {
|
|
873
|
+
process.stderr.write(`❌ ${valid.error}\n`);
|
|
874
|
+
process.exit(1);
|
|
875
|
+
}
|
|
876
|
+
prevalidatedVideoMetas.set(videoPath, valid.meta);
|
|
877
|
+
}
|
|
726
878
|
|
|
727
879
|
// --project auto: create a new project (with images/videos if provided)
|
|
728
880
|
if (!projectId || projectId === 'auto') {
|
|
@@ -788,40 +940,36 @@ if (command === 'login') {
|
|
|
788
940
|
// Upload videos to project timeline (via /api/projects/create with videoUrls)
|
|
789
941
|
let finalPrompt = prompt;
|
|
790
942
|
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
943
|
// Upload local files via signed URL (no size limit, works with API key auth)
|
|
806
|
-
const uploadedVideoUrls = [...
|
|
807
|
-
|
|
944
|
+
const uploadedVideoUrls = [...prevalidatedVideoUrlList];
|
|
945
|
+
const uploadedVideoMetas = prevalidatedVideoUrlList.map(() => null);
|
|
946
|
+
if (prevalidatedVideoUrlList.length) {
|
|
947
|
+
process.stderr.write(`📹 Assuming public video URL(s) already match Makaron upload limits: ≤${MAX_VIDEO_DURATION}s, ≤200MB, ≤1080p.\n`);
|
|
948
|
+
}
|
|
949
|
+
for (const videoPath of prevalidatedVideoFileList) {
|
|
808
950
|
process.stderr.write(`📹 Uploading ${path.basename(videoPath)} (${(fs.statSync(videoPath).size/1024/1024).toFixed(1)}MB)...\n`);
|
|
809
951
|
const ext = path.extname(videoPath).slice(1).toLowerCase();
|
|
810
952
|
const mime = ext === 'mov' ? 'video/quicktime' : ext === 'webm' ? 'video/webm' : 'video/mp4';
|
|
811
953
|
const url = await uploadFileViaSignedUrl(baseUrl, headers, projectId, videoPath, mime);
|
|
812
954
|
if (url) {
|
|
813
955
|
uploadedVideoUrls.push(url);
|
|
956
|
+
uploadedVideoMetas.push(prevalidatedVideoMetas.get(videoPath) || null);
|
|
814
957
|
process.stderr.write(`📹 Uploaded: ${path.basename(videoPath)}\n`);
|
|
815
958
|
}
|
|
816
959
|
}
|
|
817
960
|
|
|
818
961
|
// Add videos to project via projects/create (same as images)
|
|
962
|
+
if (uploadedVideoUrls.length === 0) {
|
|
963
|
+
process.stderr.write(`❌ No valid videos were uploaded. Local videos must be MP4/MOV/WebM, ≤${MAX_VIDEO_DURATION}s, ≤200MB, and ≤1080p.\n`);
|
|
964
|
+
process.exit(1);
|
|
965
|
+
}
|
|
966
|
+
|
|
819
967
|
if (uploadedVideoUrls.length > 0) {
|
|
820
|
-
if (
|
|
968
|
+
if (prevalidatedVideoUrlList.length) process.stderr.write(`📹 Adding ${uploadedVideoUrls.length} video(s) to timeline...\n`);
|
|
821
969
|
const res = await fetch(`${baseUrl}/api/projects/create`, {
|
|
822
970
|
method: 'POST',
|
|
823
971
|
headers: { 'Content-Type': 'application/json', ...headers },
|
|
824
|
-
body: JSON.stringify({ _addToProject: projectId, videoUrls: uploadedVideoUrls }),
|
|
972
|
+
body: JSON.stringify({ _addToProject: projectId, videoUrls: uploadedVideoUrls, videoMetas: uploadedVideoMetas }),
|
|
825
973
|
});
|
|
826
974
|
if (res.ok) {
|
|
827
975
|
const data = await res.json();
|
|
@@ -881,6 +1029,7 @@ if (command === 'login') {
|
|
|
881
1029
|
const res = await fetch(`${baseUrl}/api/agent/run/${runId}`, { headers });
|
|
882
1030
|
if (!res.ok) { process.stderr.write(`Error ${res.status}: ${await res.text()}\n`); process.exit(1); }
|
|
883
1031
|
const data = await res.json();
|
|
1032
|
+
normalizeRunResponse(data);
|
|
884
1033
|
if (pick) {
|
|
885
1034
|
const picked = applyPick(data, pick);
|
|
886
1035
|
if (picked !== undefined) console.log(typeof picked === 'string' ? picked : JSON.stringify(picked));
|
|
@@ -970,6 +1119,16 @@ if (command === 'login') {
|
|
|
970
1119
|
const result = await callMcpTool(baseUrl, headers, 'makaron_edit_image', editArgs);
|
|
971
1120
|
saveMcpImage(result, outputPath);
|
|
972
1121
|
|
|
1122
|
+
} else if (command === 'analyze') {
|
|
1123
|
+
const { headers, baseUrl } = getAuth();
|
|
1124
|
+
let video = null;
|
|
1125
|
+
const questionParts = [];
|
|
1126
|
+
for (let i = 1; i < args.length; i++) {
|
|
1127
|
+
if (args[i] === '--video' && args[i + 1]) video = args[++i];
|
|
1128
|
+
else questionParts.push(args[i]);
|
|
1129
|
+
}
|
|
1130
|
+
await analyzeVideoCli(baseUrl, headers, video, questionParts);
|
|
1131
|
+
|
|
973
1132
|
} else if (command === 'video') {
|
|
974
1133
|
const { headers, baseUrl } = getAuth();
|
|
975
1134
|
const sub = args[1];
|
|
@@ -991,50 +1150,62 @@ if (command === 'login') {
|
|
|
991
1150
|
|
|
992
1151
|
} else if (sub === 'create') {
|
|
993
1152
|
const images = [];
|
|
994
|
-
let script = '', duration = undefined, aspectRatio = undefined, videoModel = undefined,
|
|
1153
|
+
let script = '', duration = undefined, aspectRatio = undefined, videoModel = undefined, wait = false;
|
|
1154
|
+
let video = null, keepOriginalSound = false;
|
|
995
1155
|
for (let i = 2; i < args.length; i++) {
|
|
996
1156
|
if (args[i] === '--image' && args[i + 1]) images.push(args[++i]);
|
|
1157
|
+
else if (args[i] === '--video' && args[i + 1]) video = args[++i];
|
|
997
1158
|
else if (args[i] === '--script' && args[i + 1]) script = args[++i];
|
|
998
1159
|
else if (args[i] === '--script-file' && args[i + 1]) script = fs.readFileSync(args[++i], 'utf-8');
|
|
999
1160
|
else if (args[i] === '--duration' && args[i + 1]) duration = Number(args[++i]);
|
|
1000
1161
|
else if (args[i] === '--aspect' && args[i + 1]) aspectRatio = args[++i];
|
|
1001
1162
|
else if (args[i] === '--model' && args[i + 1]) videoModel = args[++i];
|
|
1002
|
-
else if (args[i] === '--
|
|
1163
|
+
else if (args[i] === '--keep-original-sound') keepOriginalSound = true;
|
|
1164
|
+
else if (args[i] === '--project') {
|
|
1165
|
+
console.error('Usage: video create no longer supports --project. Use: makaron chat --project <id> --video <file|url> "your request"');
|
|
1166
|
+
process.exit(1);
|
|
1167
|
+
}
|
|
1003
1168
|
else if (args[i] === '--wait') wait = true;
|
|
1004
1169
|
}
|
|
1005
|
-
if (!images.length
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
});
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
if (
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1170
|
+
if ((!images.length && !video) || !script) {
|
|
1171
|
+
console.error('Usage: makaron video create --script "..." (--image <url> | --video <public-url>) [--duration 10] [--aspect 9:16] [--model kling|seedance] [--keep-original-sound]');
|
|
1172
|
+
process.exit(1);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
if (wait) {
|
|
1176
|
+
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.');
|
|
1177
|
+
process.exit(1);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
let videoUrl = isHttpUrl(video) ? video : null;
|
|
1181
|
+
let inputVideoMeta = null;
|
|
1182
|
+
if (videoUrl) {
|
|
1183
|
+
process.stderr.write(`📹 Assuming public video URL already matches Makaron upload limits: ≤${MAX_VIDEO_DURATION}s, ≤200MB, ≤1080p.\n`);
|
|
1184
|
+
}
|
|
1185
|
+
if (video && !videoUrl) {
|
|
1186
|
+
const valid = validateVideoFile(video);
|
|
1187
|
+
if (!valid.ok) { console.error(`❌ ${valid.error}`); process.exit(1); }
|
|
1188
|
+
inputVideoMeta = valid.meta;
|
|
1189
|
+
process.stderr.write(`📹 Uploading ${path.basename(video)} (${(fs.statSync(video).size/1024/1024).toFixed(1)}MB)...\n`);
|
|
1190
|
+
videoUrl = await uploadFileViaSignedUrl(baseUrl, headers, undefined, video, valid.mime);
|
|
1191
|
+
if (!videoUrl) process.exit(1);
|
|
1192
|
+
process.stderr.write(`📹 Uploaded: ${path.basename(video)}\n`);
|
|
1193
|
+
}
|
|
1194
|
+
// Standalone MCP tool (no project timeline write)
|
|
1195
|
+
process.stderr.write('🎬 Submitting video...\n');
|
|
1196
|
+
const vArgs = videoUrl
|
|
1197
|
+
? { videoUrl, editPrompt: script, images, videoModel: videoModel || 'kling', referType: (videoModel || 'kling') === 'seedance' ? 'feature' : 'base' }
|
|
1198
|
+
: { script, images };
|
|
1199
|
+
const effectiveDuration = duration || (inputVideoMeta?.duration ? Math.min(MAX_VIDEO_DURATION, Math.round(inputVideoMeta.duration)) : undefined);
|
|
1200
|
+
if (effectiveDuration) vArgs.duration = effectiveDuration;
|
|
1201
|
+
if (aspectRatio) vArgs.aspectRatio = aspectRatio;
|
|
1202
|
+
if (videoModel && !videoUrl) vArgs.videoModel = videoModel;
|
|
1203
|
+
if (keepOriginalSound && videoUrl) vArgs.keepOriginalSound = true;
|
|
1204
|
+
const result = await callMcpTool(baseUrl, headers, videoUrl ? 'makaron_edit_video' : 'makaron_create_video', vArgs);
|
|
1205
|
+
const text = result?.content?.find(c => c.type === 'text')?.text;
|
|
1206
|
+
if (text) {
|
|
1207
|
+
console.log(text);
|
|
1208
|
+
if (!text.includes('Task ID:')) process.exit(1);
|
|
1038
1209
|
}
|
|
1039
1210
|
|
|
1040
1211
|
} else if (sub === 'status') {
|
|
@@ -1074,6 +1245,7 @@ if (command === 'login') {
|
|
|
1074
1245
|
console.log(`Video commands:
|
|
1075
1246
|
video script --image <file> [--image <file>] "direction" Write video script
|
|
1076
1247
|
video create --script "..." --image <url> [--duration 10] Submit video task
|
|
1248
|
+
video create --script "..." --video <public-url> [--model kling|seedance] Edit a video (standalone)
|
|
1077
1249
|
video status <taskId> Check video status
|
|
1078
1250
|
video status --snapshot <snapshotId> [--wait] Check v2 video snapshot
|
|
1079
1251
|
`);
|
|
@@ -1341,7 +1513,8 @@ Commands:
|
|
|
1341
1513
|
abort <runId> Abort a running Agent
|
|
1342
1514
|
|
|
1343
1515
|
edit [--image <file>] "prompt" AI image edit / text-to-image
|
|
1344
|
-
video
|
|
1516
|
+
analyze --video <file|url> Analyze video content
|
|
1517
|
+
video script|create|status Video generation
|
|
1345
1518
|
music create|status Music generation
|
|
1346
1519
|
|
|
1347
1520
|
admin Admin commands (skills, upload, set-admin)
|
package/package.json
CHANGED
package/skills/makaron/SKILL.md
CHANGED
|
@@ -102,7 +102,9 @@ npx makaron-cli chat --project <id> --video clip1.mp4 --video clip2.mp4 -b "comb
|
|
|
102
102
|
npx makaron-cli chat --project auto --video https://example.com/dance.mp4 -b "extend this to 15 seconds"
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
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, target max 15s with 0.5s metadata tolerance, 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.
|
|
106
108
|
|
|
107
109
|
### Check status (single query)
|
|
108
110
|
|
|
@@ -160,20 +162,35 @@ npx makaron-cli edit --image photo.jpg --out result.jpg "make it dramatic"
|
|
|
160
162
|
|
|
161
163
|
Options: `--image`, `--model gemini|qwen|openai|pony|wai`, `--skill enhance|creative|wild|captions`, `--ref <file>` (up to 3), `--aspect <ratio>`, `--out <path>`
|
|
162
164
|
|
|
163
|
-
### `video` —
|
|
165
|
+
### `video` — Standalone video tools (no project timeline)
|
|
164
166
|
|
|
165
167
|
```bash
|
|
166
168
|
# 1. Write script from images
|
|
167
169
|
npx makaron-cli video script --image img1.jpg "cinematic story"
|
|
168
170
|
|
|
169
|
-
# 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)
|
|
170
175
|
npx makaron-cli video create --script "Shot 1 (5s): <<<image_1>>> ..." --image https://...jpg --duration 5 --model kling
|
|
171
176
|
|
|
172
|
-
#
|
|
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
|
|
173
182
|
npx makaron-cli video status <taskId>
|
|
174
183
|
```
|
|
175
184
|
|
|
176
|
-
|
|
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 target <=15s, <=1080p input. Tiny metadata padding up to 15.5s is accepted and output duration is clamped to 15s.
|
|
177
194
|
|
|
178
195
|
### `music` — Music generation
|
|
179
196
|
|