ima2-gen 1.1.19 → 1.1.21
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/README.md +24 -25
- package/bin/commands/capabilities.js +2 -2
- package/bin/commands/capabilities.ts +2 -2
- package/bin/commands/defaults.js +2 -2
- package/bin/commands/defaults.ts +2 -2
- package/bin/commands/doctor.js +3 -3
- package/bin/commands/doctor.ts +3 -3
- package/bin/commands/edit.js +1 -1
- package/bin/commands/edit.ts +1 -1
- package/bin/commands/gen.js +1 -1
- package/bin/commands/gen.ts +1 -1
- package/bin/commands/grok.js +25 -22
- package/bin/commands/grok.ts +26 -22
- package/bin/commands/multimode.js +1 -1
- package/bin/commands/multimode.ts +1 -1
- package/bin/commands/observability.js +2 -2
- package/bin/commands/observability.ts +2 -2
- package/bin/commands/video.js +335 -13
- package/bin/commands/video.ts +249 -12
- package/bin/ima2.js +9 -9
- package/bin/ima2.ts +9 -9
- package/bin/lib/error-hints.js +2 -2
- package/bin/lib/error-hints.ts +2 -2
- package/docs/API.md +112 -3
- package/docs/CLI.md +61 -7
- package/docs/FAQ.ko.md +15 -20
- package/docs/FAQ.md +14 -19
- package/docs/NPX_QUICKSTART.md +40 -0
- package/docs/PROMPT_STUDIO.ko.md +1 -1
- package/docs/PROMPT_STUDIO.md +1 -1
- package/docs/README.ja.md +6 -16
- package/docs/README.ko.md +10 -20
- package/docs/README.zh-CN.md +7 -17
- package/docs/migration/runtime-test-inventory.md +9 -1
- package/lib/agentGenerationPlanner.js +20 -1
- package/lib/agentGenerationPlanner.ts +25 -1
- package/lib/agentRuntime.js +24 -8
- package/lib/agentRuntime.ts +23 -8
- package/lib/capabilities.js +1 -1
- package/lib/capabilities.ts +1 -1
- package/lib/generationErrors.js +1 -1
- package/lib/generationErrors.ts +1 -1
- package/lib/grokProxyLauncher.js +26 -3
- package/lib/grokProxyLauncher.ts +27 -3
- package/lib/grokVideoAdapter.js +18 -89
- package/lib/grokVideoAdapter.ts +27 -88
- package/lib/grokVideoCanvas.js +25 -0
- package/lib/grokVideoCanvas.ts +26 -0
- package/lib/grokVideoDownload.js +58 -0
- package/lib/grokVideoDownload.ts +59 -0
- package/lib/grokVideoPlannerPrompt.js +64 -0
- package/lib/grokVideoPlannerPrompt.ts +67 -0
- package/lib/historyList.js +7 -1
- package/lib/historyList.ts +5 -1
- package/lib/oauthLauncher.js +21 -6
- package/lib/oauthLauncher.ts +22 -6
- package/lib/videoContinuity.js +149 -0
- package/lib/videoContinuity.ts +180 -0
- package/lib/videoFrameExtract.js +80 -0
- package/lib/videoFrameExtract.ts +78 -0
- package/node_modules/progrok/dist/index.js +187 -88
- package/node_modules/progrok/dist/index.js.map +1 -1
- package/node_modules/progrok/package.json +1 -1
- package/node_modules/progrok/skills/progrok/SKILL.md +33 -4
- package/package.json +2 -2
- package/routes/index.js +4 -0
- package/routes/index.ts +4 -0
- package/routes/quota.js +66 -0
- package/routes/quota.ts +89 -0
- package/routes/video.js +77 -15
- package/routes/video.ts +82 -14
- package/routes/videoExtended.js +293 -0
- package/routes/videoExtended.ts +284 -0
- package/server.js +6 -2
- package/server.ts +5 -2
- package/skills/ima2/SKILL.md +381 -3
- package/ui/dist/.vite/manifest.json +12 -12
- package/ui/dist/assets/{AgentWorkspace-DE_wg90f.js → AgentWorkspace-B_hq9CLg.js} +2 -2
- package/ui/dist/assets/{CardNewsWorkspace--Myc5pAp.js → CardNewsWorkspace-wD12J7qk.js} +1 -1
- package/ui/dist/assets/{NodeCanvas-4U5oOT2y.js → NodeCanvas-CI_wuPMf.js} +1 -1
- package/ui/dist/assets/{PromptBuilderPanel-DNW1U8zI.js → PromptBuilderPanel-CUTujJUV.js} +1 -1
- package/ui/dist/assets/{PromptImportDialog-o-4Sqki1.js → PromptImportDialog-CUi66jPK.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-BAbrRP8B.js → PromptImportDiscoverySection-Cm3vrjY4.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-L-XI2noz.js → PromptImportFolderSection-DOtWTD9n.js} +1 -1
- package/ui/dist/assets/{PromptLibraryPanel-CrW9LYGD.js → PromptLibraryPanel-BMjQegRa.js} +2 -2
- package/ui/dist/assets/SettingsWorkspace-PiaVnsdA.js +1 -0
- package/ui/dist/assets/{index-BONbNNIi.js → index-31uVIdt4.js} +1 -1
- package/ui/dist/assets/index-CjgnNtgt.css +1 -0
- package/ui/dist/assets/index-Da2s4_-5.js +36 -0
- package/ui/dist/index.html +2 -2
- package/vendor/progrok-0.2.0.tgz +0 -0
- package/ui/dist/assets/SettingsWorkspace-Dn4SYTyZ.js +0 -1
- package/ui/dist/assets/index-B6tcw_UF.css +0 -1
- package/ui/dist/assets/index-CeSZ2L3-.js +0 -32
- package/vendor/progrok-0.1.1.tgz +0 -0
|
@@ -1247,101 +1247,200 @@ function imageToDataUri(filePath) {
|
|
|
1247
1247
|
const ext = abs.toLowerCase().endsWith(".png") ? "png" : "jpeg";
|
|
1248
1248
|
return `data:image/${ext};base64,${buf.toString("base64")}`;
|
|
1249
1249
|
}
|
|
1250
|
+
async function pollUntilDone(requestId, bearer, timeout, json) {
|
|
1251
|
+
const deadline = Date.now() + timeout;
|
|
1252
|
+
let lastProgress = -1;
|
|
1253
|
+
while (Date.now() < deadline) {
|
|
1254
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
1255
|
+
const poll = await fetch(`${XAI_API_BASE_URL}/videos/${requestId}`, {
|
|
1256
|
+
headers: { Authorization: `Bearer ${bearer}` }
|
|
1257
|
+
});
|
|
1258
|
+
if (!poll.ok) {
|
|
1259
|
+
throw new Error(`Poll HTTP ${poll.status}: ${(await poll.text()).slice(0, 200)}`);
|
|
1260
|
+
}
|
|
1261
|
+
const data = await poll.json();
|
|
1262
|
+
const status = data.status;
|
|
1263
|
+
if (status === "done") return data;
|
|
1264
|
+
if (status === "failed") {
|
|
1265
|
+
const err = data.error;
|
|
1266
|
+
throw new Error(`Generation failed: ${err?.code ?? "unknown"} \u2014 ${err?.message ?? ""}`);
|
|
1267
|
+
}
|
|
1268
|
+
if (status === "expired") throw new Error("Generation expired");
|
|
1269
|
+
if (!json && typeof data.progress === "number" && data.progress !== lastProgress) {
|
|
1270
|
+
lastProgress = data.progress;
|
|
1271
|
+
process.stdout.write(`\r Progress: ${Math.round(lastProgress * 100)}%`);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
throw new Error(`Timeout after ${timeout / 1e3}s \u2014 video still pending`);
|
|
1275
|
+
}
|
|
1276
|
+
async function downloadAndSave(videoUrl, outPath) {
|
|
1277
|
+
const videoRes = await fetch(videoUrl);
|
|
1278
|
+
if (!videoRes.ok) throw new Error(`Download failed: HTTP ${videoRes.status}`);
|
|
1279
|
+
const videoBuffer = Buffer.from(await videoRes.arrayBuffer());
|
|
1280
|
+
writeFileSync2(outPath, videoBuffer);
|
|
1281
|
+
}
|
|
1250
1282
|
function videoCommand() {
|
|
1251
|
-
|
|
1252
|
-
`Generate video with Grok Imagine Video
|
|
1253
|
-
|
|
1283
|
+
const cmd = new Command10("video").description(
|
|
1284
|
+
`Generate, edit, or extend video with Grok Imagine Video.
|
|
1285
|
+
|
|
1286
|
+
Subcommands:
|
|
1287
|
+
generate (default) Text-to-video or image-to-video
|
|
1288
|
+
edit Edit existing video with text prompt (V2V)
|
|
1289
|
+
extend Continue video from its last frame
|
|
1254
1290
|
|
|
1255
1291
|
Examples:
|
|
1256
1292
|
$ progrok video "A cat playing piano"
|
|
1257
|
-
$ progrok video "Animate this
|
|
1258
|
-
$ progrok video "
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1293
|
+
$ progrok video "Animate this" --image photo.jpg
|
|
1294
|
+
$ progrok video edit "Make it sunset colors" --video input.mp4
|
|
1295
|
+
$ progrok video extend "Camera pulls back slowly" --video input.mp4 --duration 5`
|
|
1296
|
+
);
|
|
1297
|
+
cmd.argument("[prompt]", "video generation prompt").option("--model <id>", "video model", DEFAULT_VIDEO_MODEL).option("--duration <s>", "duration in seconds (1-15)", "5").option("--aspect <ratio>", "aspect ratio", "16:9").option("--resolution <r>", "480p or 720p", "480p").option("--image <path>", "source image for image-to-video").option("--output <path>", "output file path").option("--json", "output structured JSON").option("--timeout <s>", "polling timeout in seconds", "600").action(async (prompt, opts) => {
|
|
1298
|
+
if (!prompt) {
|
|
1299
|
+
cmd.help();
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
await generateAction(prompt, opts);
|
|
1303
|
+
});
|
|
1304
|
+
cmd.command("edit <prompt>").description("Edit existing video with a text prompt (real V2V). Model: grok-imagine-video only.").requiredOption("--video <url-or-path>", "source video URL or local .mp4 path").option("--model <id>", "video model (must be grok-imagine-video)", DEFAULT_VIDEO_MODEL).option("--output <path>", "output file path").option("--json", "output structured JSON").option("--timeout <s>", "polling timeout in seconds", "600").action(async (prompt, opts) => {
|
|
1305
|
+
await editAction(prompt, opts);
|
|
1306
|
+
});
|
|
1307
|
+
cmd.command("extend <prompt>").description("Extend video from its last frame. Model: grok-imagine-video only.").requiredOption("--video <url-or-path>", "source video URL or local .mp4 path").option("--model <id>", "video model (must be grok-imagine-video)", DEFAULT_VIDEO_MODEL).option("--duration <s>", "extension duration 2-10s (default 6)", "6").option("--output <path>", "output file path").option("--json", "output structured JSON").option("--timeout <s>", "polling timeout in seconds", "600").action(async (prompt, opts) => {
|
|
1308
|
+
await extendAction(prompt, opts);
|
|
1309
|
+
});
|
|
1310
|
+
return cmd;
|
|
1311
|
+
}
|
|
1312
|
+
async function generateAction(prompt, opts) {
|
|
1313
|
+
try {
|
|
1314
|
+
const bearer = await getValidBearer();
|
|
1315
|
+
const duration = parseInt(opts.duration ?? "5", 10);
|
|
1316
|
+
const timeout = parseInt(opts.timeout ?? "600", 10) * 1e3;
|
|
1317
|
+
const body = {
|
|
1318
|
+
model: opts.model ?? DEFAULT_VIDEO_MODEL,
|
|
1319
|
+
prompt,
|
|
1320
|
+
duration,
|
|
1321
|
+
aspect_ratio: opts.aspect ?? "16:9",
|
|
1322
|
+
resolution: opts.resolution ?? "480p"
|
|
1323
|
+
};
|
|
1324
|
+
if (opts.image) {
|
|
1325
|
+
const uri = opts.image.startsWith("data:") ? opts.image : imageToDataUri(opts.image);
|
|
1326
|
+
body.image = { url: uri };
|
|
1327
|
+
} else if (opts.model?.includes("1.5") && !opts.image) {
|
|
1328
|
+
body.image = { url: WHITE_PIXEL_PNG };
|
|
1329
|
+
body.prompt = `${prompt}
|
|
1277
1330
|
|
|
1278
1331
|
This is not a start frame \u2014 generate freely as a new video.`;
|
|
1279
|
-
}
|
|
1280
|
-
const res = await fetch(`${XAI_API_BASE_URL}/videos/generations`, {
|
|
1281
|
-
method: "POST",
|
|
1282
|
-
headers: {
|
|
1283
|
-
Authorization: `Bearer ${bearer}`,
|
|
1284
|
-
"Content-Type": "application/json"
|
|
1285
|
-
},
|
|
1286
|
-
body: JSON.stringify(body)
|
|
1287
|
-
});
|
|
1288
|
-
if (!res.ok) {
|
|
1289
|
-
const errBody = await res.text();
|
|
1290
|
-
throw new Error(`HTTP ${res.status}: ${errBody.slice(0, 300)}`);
|
|
1291
|
-
}
|
|
1292
|
-
const { request_id } = await res.json();
|
|
1293
|
-
if (!opts.json) {
|
|
1294
|
-
log.info(`Video generation started (${request_id})`);
|
|
1295
|
-
log.dim(`Model: ${body.model} | ${duration}s | ${opts.aspect} | ${opts.resolution}`);
|
|
1296
|
-
}
|
|
1297
|
-
const deadline = Date.now() + timeout;
|
|
1298
|
-
let lastProgress = -1;
|
|
1299
|
-
while (Date.now() < deadline) {
|
|
1300
|
-
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
1301
|
-
const poll = await fetch(`${XAI_API_BASE_URL}/videos/${request_id}`, {
|
|
1302
|
-
headers: { Authorization: `Bearer ${bearer}` }
|
|
1303
|
-
});
|
|
1304
|
-
if (!poll.ok) {
|
|
1305
|
-
throw new Error(`Poll HTTP ${poll.status}: ${(await poll.text()).slice(0, 200)}`);
|
|
1306
|
-
}
|
|
1307
|
-
const data = await poll.json();
|
|
1308
|
-
if (data.status === "done" && data.video) {
|
|
1309
|
-
if (opts.json) {
|
|
1310
|
-
console.log(JSON.stringify(data, null, 2));
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
const videoRes = await fetch(data.video.url);
|
|
1314
|
-
if (!videoRes.ok) throw new Error(`Download failed: HTTP ${videoRes.status}`);
|
|
1315
|
-
const videoBuffer = Buffer.from(await videoRes.arrayBuffer());
|
|
1316
|
-
const outPath = opts.output ?? `progrok-video-${request_id.slice(0, 8)}.mp4`;
|
|
1317
|
-
writeFileSync2(outPath, videoBuffer);
|
|
1318
|
-
log.success(`
|
|
1319
|
-
Video saved: ${outPath}`);
|
|
1320
|
-
log.info(`Duration: ${data.video.duration}s`);
|
|
1321
|
-
if (data.usage?.cost_in_usd_ticks) {
|
|
1322
|
-
log.dim(`Cost: $${(data.usage.cost_in_usd_ticks / 1e10).toFixed(4)}`);
|
|
1323
|
-
}
|
|
1324
|
-
return;
|
|
1325
|
-
}
|
|
1326
|
-
if (data.status === "failed") {
|
|
1327
|
-
throw new Error(
|
|
1328
|
-
`Generation failed: ${data.error?.code ?? "unknown"} \u2014 ${data.error?.message ?? ""}`
|
|
1329
|
-
);
|
|
1330
|
-
}
|
|
1331
|
-
if (data.status === "expired") {
|
|
1332
|
-
throw new Error("Generation expired");
|
|
1333
|
-
}
|
|
1334
|
-
if (!opts.json && data.progress !== void 0 && data.progress !== lastProgress) {
|
|
1335
|
-
lastProgress = data.progress;
|
|
1336
|
-
process.stdout.write(`\r Progress: ${Math.round(data.progress * 100)}%`);
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
throw new Error(`Timeout after ${timeout / 1e3}s \u2014 video still pending`);
|
|
1340
|
-
} catch (err) {
|
|
1341
|
-
log.error(err.message);
|
|
1342
|
-
process.exit(1);
|
|
1343
1332
|
}
|
|
1344
|
-
|
|
1333
|
+
const res = await fetch(`${XAI_API_BASE_URL}/videos/generations`, {
|
|
1334
|
+
method: "POST",
|
|
1335
|
+
headers: { Authorization: `Bearer ${bearer}`, "Content-Type": "application/json" },
|
|
1336
|
+
body: JSON.stringify(body)
|
|
1337
|
+
});
|
|
1338
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`);
|
|
1339
|
+
const { request_id } = await res.json();
|
|
1340
|
+
if (!opts.json) {
|
|
1341
|
+
log.info(`Video generation started (${request_id})`);
|
|
1342
|
+
log.dim(`Model: ${body.model} | ${duration}s | ${opts.aspect} | ${opts.resolution}`);
|
|
1343
|
+
}
|
|
1344
|
+
const data = await pollUntilDone(request_id, bearer, timeout, opts.json);
|
|
1345
|
+
if (opts.json) {
|
|
1346
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
const video = data.video;
|
|
1350
|
+
if (!video) throw new Error("No video in response");
|
|
1351
|
+
const outPath = opts.output ?? `progrok-video-${request_id.slice(0, 8)}.mp4`;
|
|
1352
|
+
await downloadAndSave(video.url, outPath);
|
|
1353
|
+
if (!opts.json) process.stdout.write("\n");
|
|
1354
|
+
log.success(`Video saved: ${outPath}`);
|
|
1355
|
+
log.info(`Duration: ${video.duration}s`);
|
|
1356
|
+
const usage = data.usage;
|
|
1357
|
+
if (usage?.cost_in_usd_ticks) log.dim(`Cost: $${(usage.cost_in_usd_ticks / 1e10).toFixed(4)}`);
|
|
1358
|
+
} catch (err) {
|
|
1359
|
+
log.error(err.message);
|
|
1360
|
+
process.exit(1);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
async function editAction(prompt, opts) {
|
|
1364
|
+
try {
|
|
1365
|
+
const bearer = await getValidBearer();
|
|
1366
|
+
const timeout = parseInt(opts.timeout ?? "600", 10) * 1e3;
|
|
1367
|
+
const model = opts.model ?? DEFAULT_VIDEO_MODEL;
|
|
1368
|
+
if (model.includes("1.5")) {
|
|
1369
|
+
throw new Error("Video editing is only supported by grok-imagine-video (not 1.5-preview).");
|
|
1370
|
+
}
|
|
1371
|
+
const videoUrl = opts.video.startsWith("http") ? opts.video : (() => {
|
|
1372
|
+
throw new Error("--video must be a URL (local file upload not yet supported for editing).");
|
|
1373
|
+
})();
|
|
1374
|
+
const res = await fetch(`${XAI_API_BASE_URL}/videos/edits`, {
|
|
1375
|
+
method: "POST",
|
|
1376
|
+
headers: { Authorization: `Bearer ${bearer}`, "Content-Type": "application/json" },
|
|
1377
|
+
body: JSON.stringify({ model, prompt, video: { url: videoUrl } })
|
|
1378
|
+
});
|
|
1379
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`);
|
|
1380
|
+
const { request_id } = await res.json();
|
|
1381
|
+
if (!opts.json) {
|
|
1382
|
+
log.info(`Video edit started (${request_id})`);
|
|
1383
|
+
log.dim(`Model: ${model} | Edit: "${prompt.slice(0, 60)}"`);
|
|
1384
|
+
}
|
|
1385
|
+
const data = await pollUntilDone(request_id, bearer, timeout, opts.json);
|
|
1386
|
+
if (opts.json) {
|
|
1387
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
const video = data.video;
|
|
1391
|
+
if (!video) throw new Error("No video in response");
|
|
1392
|
+
const outPath = opts.output ?? `progrok-edit-${request_id.slice(0, 8)}.mp4`;
|
|
1393
|
+
await downloadAndSave(video.url, outPath);
|
|
1394
|
+
if (!opts.json) process.stdout.write("\n");
|
|
1395
|
+
log.success(`Edited video saved: ${outPath}`);
|
|
1396
|
+
log.info(`Duration: ${video.duration}s`);
|
|
1397
|
+
} catch (err) {
|
|
1398
|
+
log.error(err.message);
|
|
1399
|
+
process.exit(1);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
async function extendAction(prompt, opts) {
|
|
1403
|
+
try {
|
|
1404
|
+
const bearer = await getValidBearer();
|
|
1405
|
+
const timeout = parseInt(opts.timeout ?? "600", 10) * 1e3;
|
|
1406
|
+
const duration = parseInt(opts.duration ?? "6", 10);
|
|
1407
|
+
const model = opts.model ?? DEFAULT_VIDEO_MODEL;
|
|
1408
|
+
if (model.includes("1.5")) {
|
|
1409
|
+
throw new Error("Video extension is only supported by grok-imagine-video (not 1.5-preview).");
|
|
1410
|
+
}
|
|
1411
|
+
if (duration < 2 || duration > 10) {
|
|
1412
|
+
throw new Error("Extension duration must be 2-10 seconds.");
|
|
1413
|
+
}
|
|
1414
|
+
const videoUrl = opts.video.startsWith("http") ? opts.video : (() => {
|
|
1415
|
+
throw new Error("--video must be a URL (local file upload not yet supported for extension).");
|
|
1416
|
+
})();
|
|
1417
|
+
const res = await fetch(`${XAI_API_BASE_URL}/videos/extensions`, {
|
|
1418
|
+
method: "POST",
|
|
1419
|
+
headers: { Authorization: `Bearer ${bearer}`, "Content-Type": "application/json" },
|
|
1420
|
+
body: JSON.stringify({ model, prompt, duration, video: { url: videoUrl } })
|
|
1421
|
+
});
|
|
1422
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`);
|
|
1423
|
+
const { request_id } = await res.json();
|
|
1424
|
+
if (!opts.json) {
|
|
1425
|
+
log.info(`Video extension started (${request_id})`);
|
|
1426
|
+
log.dim(`Model: ${model} | Extend: +${duration}s | "${prompt.slice(0, 60)}"`);
|
|
1427
|
+
}
|
|
1428
|
+
const data = await pollUntilDone(request_id, bearer, timeout, opts.json);
|
|
1429
|
+
if (opts.json) {
|
|
1430
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
const video = data.video;
|
|
1434
|
+
if (!video) throw new Error("No video in response");
|
|
1435
|
+
const outPath = opts.output ?? `progrok-extend-${request_id.slice(0, 8)}.mp4`;
|
|
1436
|
+
await downloadAndSave(video.url, outPath);
|
|
1437
|
+
if (!opts.json) process.stdout.write("\n");
|
|
1438
|
+
log.success(`Extended video saved: ${outPath}`);
|
|
1439
|
+
log.info(`Total duration: ${video.duration}s (original + ${duration}s extension)`);
|
|
1440
|
+
} catch (err) {
|
|
1441
|
+
log.error(err.message);
|
|
1442
|
+
process.exit(1);
|
|
1443
|
+
}
|
|
1345
1444
|
}
|
|
1346
1445
|
|
|
1347
1446
|
// src/commands/image.ts
|