makaron-cli 0.11.2 → 0.11.4

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.11.2",
3
+ "version": "0.11.4",
4
4
  "description": "AI image editing, video generation, music creation, and marketplace skill workflows via CLI. Agents can self-register, install skills, 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.11.2",
3
+ "version": "0.11.4",
4
4
  "description": "AI image editing, video generation, music creation, and marketplace skill workflows via CLI. Agents can self-register, install skills, 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
@@ -107,6 +107,7 @@ Returns immediately:
107
107
  | Fix one moment in a video from a screenshot | `npx makaron-cli chat --project <id> --image screenshot.png "@4 this frame should be Paris; only fix this moment"` |
108
108
  | Cut or assemble video | `npx makaron-cli chat --project <id> --video clip.mp4 "cut out the dead air and keep the best 20 seconds"` |
109
109
  | Add music | `npx makaron-cli chat --project <id> "add calm piano background music"` |
110
+ | Beat-sync video from audio | `npx makaron-cli chat --project auto --audio beat.mp3 --video-model seedance-fast --video-resolution 480p "make a beat-synced video"` |
110
111
  | Create motion design | `npx makaron-cli chat --project <id> "make an animated Instagram story with this image"` |
111
112
 
112
113
  ### Marketplace skills
@@ -159,6 +160,26 @@ Video files are uploaded via signed URL. CLI local video uploads support `.mp4`,
159
160
  The agent understands video content natively — it can analyze scenes, edit, extend, and compose videos. Seedance video-reference editing is still limited to ~15s provider references, so longer uploaded videos should be split/prepared by the agent before model submission; Kling remains the base/direct edit path.
160
161
  Use `chat --project <id|auto> --video ...` for any project/timeline video work. Direct `video create` is standalone and does not write timeline entries.
161
162
 
163
+ ### With reference audio (MP3/WAV)
164
+
165
+ Use `--audio` when a song, beat, or voice recording should guide the video generation. Audio is imported into the project music library and passed to the agent as `audio_1`, `audio_2`, etc.; it does not become a timeline media item.
166
+
167
+ ```bash
168
+ # Local audio: validate, upload, import, then generate with Seedance
169
+ npx makaron-cli chat --project auto \
170
+ --audio beat.mp3 \
171
+ --video-model seedance-fast \
172
+ --video-resolution 480p \
173
+ -b "make a 15s beat-synced video from this music"
174
+
175
+ # Public audio URL: store the URL directly, no mirroring
176
+ npx makaron-cli chat --project <id> \
177
+ --audio https://example.com/voice.wav \
178
+ -b "use this voice recording as the pacing reference for the video"
179
+ ```
180
+
181
+ Local audio supports MP3/WAV, 2-15s duration, and max 15MB. Reference audio is currently supported for Seedance video generation; Kling and Grok will return a clear unsupported-model error if audio refs are used.
182
+
162
183
  ### Fix one video moment from a screenshot
163
184
 
164
185
  When a video is mostly good but one moment needs a local fix, attach a screenshot of the problem frame and describe the correction in normal language:
@@ -238,6 +259,7 @@ npx makaron-cli analyze --video input.mp4 "describe the key actions and pacing"
238
259
  # 3a. Submit image-to-video rendering (images must be public URLs from step 1 or uploaded)
239
260
  npx makaron-cli video create --script "Shot 1 (5s): <<<image_1>>> ..." --image https://...jpg --duration 5 --model kling
240
261
  npx makaron-cli video create --script "Shot 1 (5s): <<<image_1>>> slow cinematic push-in with native ambience" --image https://...jpg --duration 5 --model grok
262
+ npx makaron-cli video create --script "Shot 1 (15s): <<<image_1>>> and <<<image_2>>> build a neon one-person studio" --image https://...jpg --image https://...webp --duration 15 --model seedance-mini --video-resolution 480p --aspect 9:16
241
263
 
242
264
  # 3b. Edit a video from a local file or public URL
243
265
  npx makaron-cli video create --script "make it funny" --video input.mp4 --duration 5 --model seedance-fast
@@ -253,9 +275,9 @@ For project/timeline video editing, use:
253
275
  npx makaron-cli chat --project <id|auto> --video input.mp4 -b "make it funny"
254
276
  ```
255
277
 
256
- Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--video <file|url>`, `--duration <seconds>`, `--aspect 9:16|16:9|1:1`, `--model seedance-fast|seedance|kling|grok`, `--video-resolution auto|480p|720p|1080p|4k`. Default model is `seedance-fast`. SeeDance accepts integer output duration 4-15s (default 5s); Kling supports 5-15s; Grok 1.5 supports 1-15s single-image-to-video only. For `--model grok`, forced `--aspect` is ignored to avoid xAI stretching the source image; pad/create the image at the target shape first or use another model.
278
+ Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--video <file|url>`, `--duration <seconds>`, `--aspect 9:16|16:9|1:1`, `--model seedance-fast|seedance-mini|seedance|kling|grok`, `--video-resolution auto|480p|720p|1080p|4k`. Default model is `seedance-fast`. SeeDance accepts integer output duration 4-15s (default 5s); `seedance-mini` supports 480p/720p and is best for cheaper drafts/multi-size tests; Kling supports 5-15s; Grok 1.5 supports 1-15s single-image-to-video only. For `--model grok`, forced `--aspect` is ignored to avoid xAI stretching the source image; pad/create the image at the target shape first or use another model.
257
279
 
258
- Video edit model behavior: `--model kling --video` uses Kling base/direct edit internally; `--model seedance-fast --video` or `--model seedance --video` uses the SeeDance video-reference path and requires target <=15s, <=50MB, width/height 300-6000px, aspect ratio 0.4-2.5, and frame pixels 409,600-2,086,876. Tiny metadata padding up to 15.5s is accepted and output duration is clamped to 15s. Grok does not support video references.
280
+ Video edit model behavior: `--model kling --video` uses Kling base/direct edit internally; `--model seedance-fast --video`, `--model seedance-mini --video`, or `--model seedance --video` uses the SeeDance video-reference path and requires target <=15s, <=50MB, width/height 300-6000px, aspect ratio 0.4-2.5, and frame pixels 409,600-2,086,876. Tiny metadata padding up to 15.5s is accepted and output duration is clamped to 15s. Grok does not support video references.
259
281
 
260
282
  ### `music` — Music generation
261
283
 
package/bin/makaron.mjs CHANGED
@@ -38,6 +38,11 @@ const MAX_VIDEO_UPLOAD_DURATION = 120;
38
38
  const MAX_VIDEO_UPLOAD_DURATION_TOLERANCE = 1;
39
39
  const MAX_VIDEO_PROVIDER_REFERENCE_DURATION = 15;
40
40
  const MAX_VIDEO_PROVIDER_REFERENCE_DURATION_TOLERANCE = 0.5;
41
+ const MIN_AUDIO_REFERENCE_DURATION = 2;
42
+ const MAX_AUDIO_REFERENCE_DURATION = 15;
43
+ const MAX_AUDIO_REFERENCE_DURATION_TOLERANCE = 0.5;
44
+ const MAX_AUDIO_REFERENCE_FILE_SIZE_MB = 15;
45
+ const MAX_AUDIO_REFERENCE_FILE_SIZE = MAX_AUDIO_REFERENCE_FILE_SIZE_MB * 1024 * 1024;
41
46
  const MAX_VIDEO_FRAME_PIXELS = 2_086_876;
42
47
  const SEEDANCE_MIN_VIDEO_FRAME_PIXELS = 409_600;
43
48
  const SEEDANCE_MIN_VIDEO_SIDE = 300;
@@ -274,9 +279,10 @@ Options:
274
279
  --project <id|auto> Project to work in. Use "auto" to create one.
275
280
  --image <file|url> Attach a reference image or screenshot. Repeatable.
276
281
  --video <file|url> Attach a video to the project timeline. Repeatable.
282
+ --audio <file|url> Attach a song, beat, or voice reference. MP3/WAV, repeatable.
277
283
  --skill <id|label|name> Use an installed skill or auto-install a matched marketplace skill.
278
284
  --model <name> Preferred image/model route.
279
- --video-model <name> Preferred video model: seedance-fast, seedance, kling, or grok.
285
+ --video-model <name> Preferred video model: seedance-fast, seedance-mini, seedance, kling, or grok.
280
286
  --video-resolution <res> Video resolution: auto, 480p, 720p, 1080p, or 4k.
281
287
  --background, -b Submit and print a runId.
282
288
  --json Output structured JSON.
@@ -305,6 +311,10 @@ What you can ask:
305
311
  Music
306
312
  makaron chat --project <id> "add calm piano background music"
307
313
 
314
+ Reference audio / beat sync
315
+ makaron chat --project auto --audio beat.mp3 --video-model seedance-fast --video-resolution 480p "用这个音乐做卡点视频"
316
+ makaron chat --project <id> --audio https://example.com/beat.mp3 "add this as the soundtrack"
317
+
308
318
  Motion design
309
319
  makaron chat --project <id> "make an animated Instagram story with this image"
310
320
 
@@ -450,6 +460,7 @@ async function submitRun(baseUrl, headers, projectId, prompt, opts = {}) {
450
460
  if (opts.videoResolution) body.videoResolution = opts.videoResolution;
451
461
  if (opts.currentSnapshotIndex != null) body.currentSnapshotIndex = opts.currentSnapshotIndex;
452
462
  if (opts.isNsfw) body.isNsfw = opts.isNsfw;
463
+ if (opts.audioAttachments?.length) body.audioAttachments = opts.audioAttachments;
453
464
 
454
465
  const res = await fetch(`${baseUrl}/api/agent/run`, {
455
466
  method: 'POST',
@@ -1000,13 +1011,13 @@ function readImageAsDataUrl(filePath) {
1000
1011
  * 2. PUT file directly to Supabase Storage (no Vercel body limit)
1001
1012
  * Returns public URL on success, null on failure.
1002
1013
  */
1003
- async function uploadFileViaSignedUrl(baseUrl, headers, projectId, filePath, contentType) {
1014
+ async function uploadFileViaSignedUrl(baseUrl, headers, projectId, filePath, contentType, options = {}) {
1004
1015
  const filename = path.basename(filePath);
1005
1016
  // Step 1: get signed upload URL
1006
1017
  const urlRes = await fetch(`${baseUrl}/api/storage/upload-url`, {
1007
1018
  method: 'POST',
1008
1019
  headers: { 'Content-Type': 'application/json', ...headers },
1009
- body: JSON.stringify({ projectId, filename, contentType }),
1020
+ body: JSON.stringify({ projectId, filename, contentType, uploadKind: options.uploadKind }),
1010
1021
  });
1011
1022
  if (!urlRes.ok) {
1012
1023
  process.stderr.write(`⚠️ Failed to get upload URL: ${await urlRes.text()}\n`);
@@ -1108,6 +1119,125 @@ function probeLocalVideo(videoPath) {
1108
1119
  return probeVideoWithFfprobe(videoPath) || probeVideoWithFfmpeg(videoPath);
1109
1120
  }
1110
1121
 
1122
+ function probeAudioDurationWithFfprobe(audioPath) {
1123
+ try {
1124
+ const out = execFileSync('ffprobe', [
1125
+ '-v', 'error',
1126
+ '-show_entries', 'format=duration',
1127
+ '-of', 'json',
1128
+ audioPath,
1129
+ ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
1130
+ const data = JSON.parse(out);
1131
+ const duration = Number(data.format?.duration);
1132
+ if (Number.isFinite(duration) && duration > 0) return duration;
1133
+ } catch { /* ffprobe unavailable or file unsupported */ }
1134
+ return null;
1135
+ }
1136
+
1137
+ function getAudioMimeFromExt(ext) {
1138
+ if (ext === 'mp3') return 'audio/mpeg';
1139
+ if (ext === 'wav') return 'audio/wav';
1140
+ return null;
1141
+ }
1142
+
1143
+ function validateAudioReferenceFile(audioPath) {
1144
+ if (!fs.existsSync(audioPath)) {
1145
+ return { ok: false, error: `Audio file not found: ${audioPath}` };
1146
+ }
1147
+ const stat = fs.statSync(audioPath);
1148
+ if (stat.size === 0) {
1149
+ return { ok: false, error: `Audio file is empty: ${audioPath}` };
1150
+ }
1151
+ if (stat.size > MAX_AUDIO_REFERENCE_FILE_SIZE) {
1152
+ return { ok: false, error: `Audio too large: ${(stat.size / 1024 / 1024).toFixed(1)}MB (max ${MAX_AUDIO_REFERENCE_FILE_SIZE_MB}MB).` };
1153
+ }
1154
+ const ext = path.extname(audioPath).slice(1).toLowerCase();
1155
+ const mime = getAudioMimeFromExt(ext);
1156
+ if (!mime) {
1157
+ return { ok: false, error: `Unsupported audio format: .${ext || 'unknown'}. Use MP3 or WAV.` };
1158
+ }
1159
+ const duration = probeAudioDurationWithFfprobe(audioPath);
1160
+ if (duration == null) {
1161
+ return { ok: false, error: 'Cannot read audio duration. Install ffprobe or use a public MP3/WAV URL.' };
1162
+ }
1163
+ if (duration < MIN_AUDIO_REFERENCE_DURATION) {
1164
+ return { ok: false, error: `Audio too short: ${formatSeconds(duration)}s (min ${MIN_AUDIO_REFERENCE_DURATION}s).` };
1165
+ }
1166
+ if (duration > MAX_AUDIO_REFERENCE_DURATION + MAX_AUDIO_REFERENCE_DURATION_TOLERANCE) {
1167
+ return { ok: false, error: `Audio too long: ${formatSeconds(duration)}s (max ${MAX_AUDIO_REFERENCE_DURATION}s, with ${MAX_AUDIO_REFERENCE_DURATION_TOLERANCE}s metadata tolerance).` };
1168
+ }
1169
+ return { ok: true, mime, meta: { duration, fileSizeBytes: stat.size } };
1170
+ }
1171
+
1172
+ function validateAudioReferenceUrl(audioUrl) {
1173
+ let parsed;
1174
+ try {
1175
+ parsed = new URL(audioUrl);
1176
+ } catch {
1177
+ return { ok: false, error: `Invalid audio URL: ${audioUrl}` };
1178
+ }
1179
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
1180
+ return { ok: false, error: `Invalid audio URL protocol: ${parsed.protocol}` };
1181
+ }
1182
+ const ext = path.extname(parsed.pathname).slice(1).toLowerCase();
1183
+ const mime = getAudioMimeFromExt(ext);
1184
+ if (!mime) {
1185
+ return { ok: false, error: `Unsupported audio URL format: .${ext || 'unknown'}. Use an MP3 or WAV URL.` };
1186
+ }
1187
+ return { ok: true, mime };
1188
+ }
1189
+
1190
+ async function probeAudioReferenceUrl(audioUrl) {
1191
+ const controller = new AbortController();
1192
+ const timer = setTimeout(() => controller.abort(), 6000);
1193
+ try {
1194
+ const res = await fetch(audioUrl, {
1195
+ method: 'HEAD',
1196
+ signal: controller.signal,
1197
+ headers: { Accept: 'audio/mpeg,audio/wav,audio/wave,audio/x-wav,*/*' },
1198
+ });
1199
+ if (!res.ok) return { ok: true, warning: `Could not HEAD probe audio URL (${res.status}); storing external URL as-is.` };
1200
+ const contentLength = Number(res.headers.get('content-length') || 0);
1201
+ if (contentLength > MAX_AUDIO_REFERENCE_FILE_SIZE) {
1202
+ return { ok: false, error: `Audio URL appears too large: ${(contentLength / 1024 / 1024).toFixed(1)}MB (max ${MAX_AUDIO_REFERENCE_FILE_SIZE_MB}MB).` };
1203
+ }
1204
+ const contentType = (res.headers.get('content-type') || '').toLowerCase();
1205
+ if (contentType && !contentType.includes('audio/') && !contentType.includes('octet-stream')) {
1206
+ return { ok: false, error: `Audio URL content-type is not audio: ${contentType}` };
1207
+ }
1208
+ return { ok: true, fileSizeBytes: contentLength || undefined };
1209
+ } catch {
1210
+ return { ok: true, warning: 'Could not HEAD probe audio URL; storing external URL as-is.' };
1211
+ } finally {
1212
+ clearTimeout(timer);
1213
+ }
1214
+ }
1215
+
1216
+ function titleFromAudioInput(value) {
1217
+ if (isHttpUrl(value)) {
1218
+ try {
1219
+ const name = decodeURIComponent(new URL(value).pathname.split('/').filter(Boolean).pop() || '');
1220
+ return name || 'Reference audio';
1221
+ } catch {
1222
+ return 'Reference audio';
1223
+ }
1224
+ }
1225
+ return path.basename(value);
1226
+ }
1227
+
1228
+ async function importAudioTracks(baseUrl, headers, projectId, audios) {
1229
+ const res = await fetch(`${baseUrl}/api/music/import`, {
1230
+ method: 'POST',
1231
+ headers: { 'Content-Type': 'application/json', ...headers },
1232
+ body: JSON.stringify({ projectId, audios }),
1233
+ });
1234
+ if (!res.ok) {
1235
+ process.stderr.write(`❌ Failed to import audio: ${await res.text()}\n`);
1236
+ process.exit(1);
1237
+ }
1238
+ return await res.json();
1239
+ }
1240
+
1111
1241
  function validateVideoFile(videoPath, options = {}) {
1112
1242
  const maxDuration = options.maxDuration ?? MAX_VIDEO_UPLOAD_DURATION;
1113
1243
  const durationTolerance = options.durationTolerance ?? MAX_VIDEO_UPLOAD_DURATION_TOLERANCE;
@@ -1236,6 +1366,7 @@ Commands:
1236
1366
  chat --project <id> "message" Chat (non-blocking, polls for result)
1237
1367
  chat --project <id> --skill <id> Use or auto-install a marketplace skill
1238
1368
  chat --project <id> --video <file> Attach video to conversation
1369
+ chat --project <id> --audio <file> Attach song/beat/voice reference
1239
1370
  chat --project <id> -b "message" Background: submit and print runId
1240
1371
  chat --project <id> --stream "msg" Legacy: stream SSE in real-time
1241
1372
  chat --project <id> --json "msg" Output structured JSON result
@@ -1353,12 +1484,12 @@ Use with chat:
1353
1484
  console.log('Usage: makaron analyze --video <file|url> ["question"]');
1354
1485
  } else if (topic === 'video') {
1355
1486
  if (subtopic === 'script') console.log('Usage: makaron video script --image <file> [--image <file>] [--lang en|zh] "direction"');
1356
- else if (subtopic === 'create') console.log('Usage: makaron video create --script "..." (--image <url> | --video <public-url>) [--duration 10] [--aspect 9:16] [--model seedance-fast|seedance|kling|grok] [--video-resolution auto|480p|720p|1080p|4k] [--keep-original-sound]');
1487
+ else if (subtopic === 'create') console.log('Usage: makaron video create --script "..." (--image <url> | --video <public-url>) [--duration 10] [--aspect 9:16] [--model seedance-fast|seedance-mini|seedance|kling|grok] [--video-resolution auto|480p|720p|1080p|4k] [--keep-original-sound]');
1357
1488
  else if (subtopic === 'status') console.log('Usage: makaron video status <taskId> | --snapshot <snapshotId> [--wait]');
1358
1489
  else console.log(`Video commands:
1359
1490
  video script --image <file> [--image <file>] "direction" Write video script
1360
1491
  video create --script "..." --image <url> [--duration 10] Submit video task
1361
- video create --script "..." --video <public-url> [--model seedance-fast|seedance|kling] Edit a video (standalone; Grok does not support video refs)
1492
+ video create --script "..." --video <public-url> [--model seedance-fast|seedance-mini|seedance|kling] Edit a video (standalone; Grok does not support video refs)
1362
1493
  video status <taskId> Check video status
1363
1494
  video status --snapshot <snapshotId> [--wait] Check v2 video snapshot
1364
1495
  `);
@@ -1435,6 +1566,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1435
1566
  let projectId = null;
1436
1567
  const chatImages = [];
1437
1568
  const chatVideos = [];
1569
+ const chatAudios = [];
1438
1570
  const promptParts = [];
1439
1571
  let useStream = false;
1440
1572
  let background = false;
@@ -1447,6 +1579,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1447
1579
  if (args[i] === '--project' && args[i + 1]) projectId = args[++i];
1448
1580
  else if (args[i] === '--image' && args[i + 1]) chatImages.push(args[++i]);
1449
1581
  else if (args[i] === '--video' && args[i + 1]) chatVideos.push(args[++i]);
1582
+ else if (args[i] === '--audio' && args[i + 1]) chatAudios.push(args[++i]);
1450
1583
  else if (args[i] === '--skill' && args[i + 1]) activeSkill = args[++i];
1451
1584
  else if (args[i].startsWith('--skill=')) activeSkill = args[i].slice('--skill='.length);
1452
1585
  else if (args[i] === '--stream') useStream = true;
@@ -1469,6 +1602,8 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1469
1602
  const imageFileList = chatImages.filter(p => !p.startsWith('http://') && !p.startsWith('https://'));
1470
1603
  const prevalidatedVideoUrlList = chatVideos.filter(p => p.startsWith('http://') || p.startsWith('https://'));
1471
1604
  const prevalidatedVideoFileList = chatVideos.filter(p => !p.startsWith('http://') && !p.startsWith('https://'));
1605
+ const prevalidatedAudioUrlList = chatAudios.filter(p => p.startsWith('http://') || p.startsWith('https://'));
1606
+ const prevalidatedAudioFileList = chatAudios.filter(p => !p.startsWith('http://') && !p.startsWith('https://'));
1472
1607
  const prevalidatedVideoMetas = new Map();
1473
1608
  for (const videoPath of prevalidatedVideoFileList) {
1474
1609
  const valid = validateVideoFile(videoPath);
@@ -1478,6 +1613,33 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1478
1613
  }
1479
1614
  prevalidatedVideoMetas.set(videoPath, valid.meta);
1480
1615
  }
1616
+ const prevalidatedAudioMetas = new Map();
1617
+ for (const audioUrl of prevalidatedAudioUrlList) {
1618
+ const valid = validateAudioReferenceUrl(audioUrl);
1619
+ if (!valid.ok) {
1620
+ process.stderr.write(`❌ ${valid.error}\n`);
1621
+ process.exit(1);
1622
+ }
1623
+ const probed = await probeAudioReferenceUrl(audioUrl);
1624
+ if (!probed.ok) {
1625
+ process.stderr.write(`❌ ${probed.error}\n`);
1626
+ process.exit(1);
1627
+ }
1628
+ if (probed.warning) process.stderr.write(`⚠️ ${probed.warning}\n`);
1629
+ prevalidatedAudioMetas.set(audioUrl, { ...valid, ...probed });
1630
+ }
1631
+ for (const audioPath of prevalidatedAudioFileList) {
1632
+ const valid = validateAudioReferenceFile(audioPath);
1633
+ if (!valid.ok) {
1634
+ process.stderr.write(`❌ ${valid.error}\n`);
1635
+ process.exit(1);
1636
+ }
1637
+ prevalidatedAudioMetas.set(audioPath, valid);
1638
+ }
1639
+ if (useStream && chatAudios.length > 0) {
1640
+ process.stderr.write('❌ --audio is supported in the default async chat path. Remove --stream and retry.\n');
1641
+ process.exit(1);
1642
+ }
1481
1643
 
1482
1644
  // --project auto: create a new project (with images/videos if provided)
1483
1645
  if (!projectId || projectId === 'auto') {
@@ -1529,6 +1691,48 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1529
1691
 
1530
1692
  // Upload videos to project timeline (via /api/projects/create with videoUrls)
1531
1693
  let finalPrompt = resolvedSkill ? `[Active skill: ${resolvedSkill}]\n${prompt}` : prompt;
1694
+ let audioAttachments = [];
1695
+ if (chatAudios.length > 0) {
1696
+ const audioImports = [];
1697
+ for (const audioUrl of prevalidatedAudioUrlList) {
1698
+ const valid = prevalidatedAudioMetas.get(audioUrl);
1699
+ audioImports.push({
1700
+ audioUrl,
1701
+ title: titleFromAudioInput(audioUrl),
1702
+ mimeType: valid.mime,
1703
+ fileSizeBytes: valid.fileSizeBytes,
1704
+ source: 'cli_url',
1705
+ });
1706
+ }
1707
+ for (const audioPath of prevalidatedAudioFileList) {
1708
+ const valid = prevalidatedAudioMetas.get(audioPath);
1709
+ process.stderr.write(`🎵 Uploading ${path.basename(audioPath)} (${(fs.statSync(audioPath).size / 1024 / 1024).toFixed(1)}MB)...\n`);
1710
+ const url = await uploadFileViaSignedUrl(baseUrl, headers, projectId, audioPath, valid.mime, { uploadKind: 'audio' });
1711
+ if (!url) {
1712
+ process.stderr.write(`❌ Failed to upload audio: ${audioPath}\n`);
1713
+ process.exit(1);
1714
+ }
1715
+ audioImports.push({
1716
+ audioUrl: url,
1717
+ title: titleFromAudioInput(audioPath),
1718
+ duration: valid.meta.duration,
1719
+ mimeType: valid.mime,
1720
+ fileSizeBytes: valid.meta.fileSizeBytes,
1721
+ source: 'cli_upload',
1722
+ });
1723
+ process.stderr.write(`🎵 Uploaded: ${path.basename(audioPath)}\n`);
1724
+ }
1725
+
1726
+ const imported = await importAudioTracks(baseUrl, headers, projectId, audioImports);
1727
+ audioAttachments = (imported.tracks || []).map(track => ({
1728
+ audioUrl: track.audioUrl,
1729
+ title: track.title,
1730
+ duration: track.duration,
1731
+ trackIndex: track.trackIndex,
1732
+ }));
1733
+ process.stderr.write(`🎵 Imported ${audioAttachments.length} reference audio track(s)\n`);
1734
+ }
1735
+
1532
1736
  if (chatVideos.length > 0) {
1533
1737
  // Upload local files via signed URL (no size limit, works with API key auth)
1534
1738
  const uploadedVideoUrls = [...prevalidatedVideoUrlList];
@@ -1586,7 +1790,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1586
1790
  for (const task of results.musicTasks) await pollMusic(baseUrl, headers, task.taskId);
1587
1791
  } else {
1588
1792
  // Default: fire-and-forget + poll
1589
- const { runId } = await submitRun(baseUrl, headers, projectId, finalPrompt, { videoModel, videoResolution, preferredModel });
1793
+ const { runId } = await submitRun(baseUrl, headers, projectId, finalPrompt, { videoModel, videoResolution, preferredModel, audioAttachments });
1590
1794
  if (background) {
1591
1795
  // Just print runId and exit
1592
1796
  if (jsonOutput) {
@@ -1820,7 +2024,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1820
2024
  else if (args[i] === '--wait') wait = true;
1821
2025
  }
1822
2026
  if ((!images.length && !video) || !script) {
1823
- console.error('Usage: makaron video create --script "..." (--image <url> | --video <public-url>) [--duration 10] [--aspect 9:16] [--model seedance-fast|seedance|kling|grok] [--video-resolution auto|480p|720p|1080p|4k] [--keep-original-sound]');
2027
+ console.error('Usage: makaron video create --script "..." (--image <url> | --video <public-url>) [--duration 10] [--aspect 9:16] [--model seedance-fast|seedance-mini|seedance|kling|grok] [--video-resolution auto|480p|720p|1080p|4k] [--keep-original-sound]');
1824
2028
  process.exit(1);
1825
2029
  }
1826
2030
 
@@ -1839,7 +2043,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1839
2043
  const valid = validateVideoFile(video, {
1840
2044
  maxDuration: MAX_VIDEO_PROVIDER_REFERENCE_DURATION,
1841
2045
  durationTolerance: MAX_VIDEO_PROVIDER_REFERENCE_DURATION_TOLERANCE,
1842
- ...(selectedVideoModel === 'seedance' || selectedVideoModel === 'seedance-fast' ? {
2046
+ ...(selectedVideoModel === 'seedance' || selectedVideoModel === 'seedance-fast' || selectedVideoModel === 'seedance-mini' ? {
1843
2047
  minFramePixels: SEEDANCE_MIN_VIDEO_FRAME_PIXELS,
1844
2048
  minSide: SEEDANCE_MIN_VIDEO_SIDE,
1845
2049
  maxSide: SEEDANCE_MAX_VIDEO_SIDE,
@@ -1857,7 +2061,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1857
2061
  // Standalone MCP tool (no project timeline write)
1858
2062
  process.stderr.write('🎬 Submitting video...\n');
1859
2063
  const vArgs = videoUrl
1860
- ? { videoUrl, editPrompt: script, images, videoModel: selectedVideoModel, videoResolution, referType: (selectedVideoModel === 'seedance' || selectedVideoModel === 'seedance-fast') ? 'feature' : 'base' }
2064
+ ? { videoUrl, editPrompt: script, images, videoModel: selectedVideoModel, videoResolution, referType: (selectedVideoModel === 'seedance' || selectedVideoModel === 'seedance-fast' || selectedVideoModel === 'seedance-mini') ? 'feature' : 'base' }
1861
2065
  : { script, images, videoModel: selectedVideoModel, videoResolution };
1862
2066
  const effectiveDuration = duration || (inputVideoMeta?.duration ? Math.min(MAX_VIDEO_PROVIDER_REFERENCE_DURATION, Math.round(inputVideoMeta.duration)) : undefined);
1863
2067
  if (effectiveDuration) vArgs.duration = effectiveDuration;
@@ -1907,7 +2111,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1907
2111
  console.log(`Video commands:
1908
2112
  video script --image <file> [--image <file>] "direction" Write video script
1909
2113
  video create --script "..." --image <url> [--duration 10] Submit video task
1910
- video create --script "..." --video <public-url> [--model seedance-fast|seedance|kling] Edit a video (standalone; Grok does not support video refs)
2114
+ video create --script "..." --video <public-url> [--model seedance-fast|seedance-mini|seedance|kling] Edit a video (standalone; Grok does not support video refs)
1911
2115
  video status <taskId> Check video status
1912
2116
  video status --snapshot <snapshotId> [--wait] Check v2 video snapshot
1913
2117
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
4
4
  "description": "Talk to Makaron Agent from the terminal — create projects, edit images, generate videos",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -94,6 +94,7 @@ Returns immediately:
94
94
  | Fix one moment in a video from a screenshot | `npx makaron-cli chat --project <id> --image screenshot.png "@4 this frame should be Paris; only fix this moment"` |
95
95
  | Cut or assemble video | `npx makaron-cli chat --project <id> --video clip.mp4 "cut out the dead air and keep the best 20 seconds"` |
96
96
  | Add music | `npx makaron-cli chat --project <id> "add calm piano background music"` |
97
+ | Beat-sync video from audio | `npx makaron-cli chat --project auto --audio beat.mp3 --video-model seedance-fast --video-resolution 480p "make a beat-synced video"` |
97
98
  | Create motion design | `npx makaron-cli chat --project <id> "make an animated Instagram story with this image"` |
98
99
 
99
100
  ### Marketplace skills
@@ -158,6 +159,26 @@ Supported formats: MP4, MOV, WebM. CLI local video uploads support max 50MB, max
158
159
 
159
160
  Use `chat --project <id|auto> --video ...` for any project/timeline video work. Direct video commands are standalone raw-tool calls.
160
161
 
162
+ ### With reference audio (MP3/WAV)
163
+
164
+ Use `--audio` when a song, beat, or voice recording should guide the video generation. Audio is imported into the project music library and passed to the agent as `audio_1`, `audio_2`, etc.; it does not become a timeline media item.
165
+
166
+ ```bash
167
+ # Local audio: validate, upload, import, then generate with Seedance
168
+ npx makaron-cli chat --project auto \
169
+ --audio beat.mp3 \
170
+ --video-model seedance-fast \
171
+ --video-resolution 480p \
172
+ -b "make a 15s beat-synced video from this music"
173
+
174
+ # Public audio URL: store the URL directly, no mirroring
175
+ npx makaron-cli chat --project <id> \
176
+ --audio https://example.com/voice.wav \
177
+ -b "use this voice recording as the pacing reference for the video"
178
+ ```
179
+
180
+ Local audio supports MP3/WAV, 2-15s duration, and max 15MB. Reference audio is currently supported for Seedance video generation; Kling and Grok will return a clear unsupported-model error if audio refs are used.
181
+
161
182
  ### Fix one video moment from a screenshot
162
183
 
163
184
  When a video is mostly good but one moment needs a local fix, attach a screenshot of the problem frame and describe the correction in normal language:
@@ -236,6 +257,7 @@ npx makaron-cli analyze --video input.mp4 "describe the key actions and pacing"
236
257
 
237
258
  # 3a. Submit image-to-video rendering (images must be public URLs from step 1 or uploaded)
238
259
  npx makaron-cli video create --script "Shot 1 (5s): <<<image_1>>> ..." --image https://...jpg --duration 5 --model kling
260
+ npx makaron-cli video create --script "Shot 1 (15s): <<<image_1>>> and <<<image_2>>> build a neon one-person studio" --image https://...jpg --image https://...webp --duration 15 --model seedance-mini --video-resolution 480p --aspect 9:16
239
261
 
240
262
  # 3b. Edit a video from a local file or public URL
241
263
  npx makaron-cli video create --script "make it funny" --video input.mp4 --duration 5 --model seedance-fast
@@ -251,9 +273,9 @@ npx makaron-cli video status <taskId>
251
273
  npx makaron-cli chat --project <id|auto> --video input.mp4 -b "make it funny"
252
274
  ```
253
275
 
254
- Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--video <file|url>`, `--duration <seconds>`, `--aspect 9:16|16:9|1:1`, `--model seedance-fast|seedance|kling|grok`, `--video-resolution auto|480p|720p|1080p|4k`. Default model is `seedance-fast`. SeeDance accepts integer output duration 4-15s (default 5s); Kling supports 5-15s; Grok 1.5 supports 1-15s single-image-to-video only. For `--model grok`, forced `--aspect` is ignored to avoid xAI stretching the source image; pad/create the image at the target shape first or use another model.
276
+ Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--video <file|url>`, `--duration <seconds>`, `--aspect 9:16|16:9|1:1`, `--model seedance-fast|seedance-mini|seedance|kling|grok`, `--video-resolution auto|480p|720p|1080p|4k`. Default model is `seedance-fast`. SeeDance accepts integer output duration 4-15s (default 5s); `seedance-mini` supports 480p/720p and is best for cheaper drafts/multi-size tests; Kling supports 5-15s; Grok 1.5 supports 1-15s single-image-to-video only. For `--model grok`, forced `--aspect` is ignored to avoid xAI stretching the source image; pad/create the image at the target shape first or use another model.
255
277
 
256
- Video edit model behavior: `--model kling --video` uses Kling base/direct edit internally; `--model seedance-fast --video` or `--model seedance --video` uses the SeeDance video-reference path and requires target <=15s, <=50MB, width/height 300-6000px, aspect ratio 0.4-2.5, and frame pixels 409,600-2,086,876. Tiny metadata padding up to 15.5s is accepted and output duration is clamped to 15s. Grok does not support video references.
278
+ Video edit model behavior: `--model kling --video` uses Kling base/direct edit internally; `--model seedance-fast --video`, `--model seedance-mini --video`, or `--model seedance --video` uses the SeeDance video-reference path and requires target <=15s, <=50MB, width/height 300-6000px, aspect ratio 0.4-2.5, and frame pixels 409,600-2,086,876. Tiny metadata padding up to 15.5s is accepted and output duration is clamped to 15s. Grok does not support video references.
257
279
 
258
280
  ### `music` — Music generation
259
281