makaron-cli 0.7.2 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.6.2",
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
  "author": {
6
6
  "name": "Makaron AI",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.6.2",
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 (no size limit up to 200MB). Supported formats: `.mp4`, `.mov`, `.webm`.
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, 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.
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` — Video generation (3 steps)
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. Submit rendering (images must be public URLs from step 1 or uploaded)
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
- # 3. Check status
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
- Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--duration 3|5|7|10|15`, `--aspect 9:16|16:9|1:1`, `--model kling|seedance`
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.
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 (max 200MB, 15s per video for composition). 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.
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.
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` — Video generation (3 steps)
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. Submit rendering (images must be public URLs from step 1 or uploaded)
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
- # 3. Check status
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
- Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--duration 3|5|7|10|15`, `--aspect 9:16|16:9|1:1`, `--model kling|seedance`
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.
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,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.projectUrl = `${APP_URL}/projects/${data.projectId}`;
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 = [...videoUrlList];
807
- for (const videoPath of validVideoFiles) {
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 (videoUrlList.length) process.stderr.write(`📹 Adding ${uploadedVideoUrls.length} video(s) to timeline...\n`);
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, projectId = null, wait = false;
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] === '--project' && args[i + 1]) projectId = 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 || !script) { console.error('Usage: makaron video create --script "..." --image <url> [--project <id>] [--duration 10] [--aspect 9:16] [--model kling|seedance] [--wait]'); process.exit(1); }
1006
-
1007
- if (projectId) {
1008
- // v2: submit to /api/video-snapshot → writes into timeline
1009
- process.stderr.write('🎬 Submitting video to timeline...\n');
1010
- const body = { projectId, imageUrls: images, prompt: script };
1011
- if (duration) body.duration = duration;
1012
- if (aspectRatio) body.aspectRatio = aspectRatio;
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 script|create|status Video generation (v2 timeline support)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.7.2",
3
+ "version": "0.7.5",
4
4
  "description": "Talk to Makaron Agent from the terminal — create projects, edit images, generate videos",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 (max 200MB, 15s per video for composition). 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.
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.
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` — Video generation (3 steps)
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. Submit rendering (images must be public URLs from step 1 or uploaded)
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
- # 3. Check status
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
- Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--duration 3|5|7|10|15`, `--aspect 9:16|16:9|1:1`, `--model kling|seedance`
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.
177
194
 
178
195
  ### `music` — Music generation
179
196