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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +24 -2
- package/bin/makaron.mjs +214 -10
- package/package.json +1 -1
- package/skills/makaron/SKILL.md +24 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "makaron-cli",
|
|
3
|
-
"version": "0.11.
|
|
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.
|
|
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
|
|
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
package/skills/makaron/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|