makaron-cli 0.11.3 → 0.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +15 -0
- package/bin/makaron.mjs +207 -3
- package/package.json +1 -1
- package/skills/makaron/SKILL.md +15 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "makaron-cli",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.5",
|
|
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.5",
|
|
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,20 @@ 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
|
+
Attach a short song, beat, or voice recording when the video should follow audio pacing:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
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"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`--audio` accepts repeatable local files or public URLs. Local MP3/WAV files must be 2-15s and <=15MB; reference audio currently works with Seedance video generation.
|
|
176
|
+
|
|
162
177
|
### Fix one video moment from a screenshot
|
|
163
178
|
|
|
164
179
|
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:
|
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,6 +279,7 @@ 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
285
|
--video-model <name> Preferred video model: seedance-fast, seedance-mini, seedance, kling, or grok.
|
|
@@ -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
|
|
@@ -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) {
|
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,20 @@ 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
|
+
Attach a short song, beat, or voice recording when the video should follow audio pacing:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npx makaron-cli chat --project auto \
|
|
168
|
+
--audio beat.mp3 \
|
|
169
|
+
--video-model seedance-fast \
|
|
170
|
+
--video-resolution 480p \
|
|
171
|
+
-b "make a 15s beat-synced video"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`--audio` accepts repeatable local files or public URLs. Local MP3/WAV files must be 2-15s and <=15MB; reference audio currently works with Seedance video generation.
|
|
175
|
+
|
|
161
176
|
### Fix one video moment from a screenshot
|
|
162
177
|
|
|
163
178
|
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:
|