videodraft 0.20.0 → 0.22.0
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 +44 -15
- package/dist/client.d.ts +49 -1
- package/dist/client.js +550 -9
- package/dist/index.js +1477 -246
- package/package.json +6 -3
- package/skills/index.json +14 -9
- package/skills/videodraft/SKILL.md +8 -6
- package/skills/videodraft/references/3d.md +46 -0
- package/skills/videodraft/references/examples.md +9 -1
- package/skills/videodraft/references/models.md +35 -9
- package/skills/videodraft/references/pipeline.md +30 -29
package/dist/client.js
CHANGED
|
@@ -643,8 +643,8 @@ var VideoDraftClient = class {
|
|
|
643
643
|
* MCP tool by design). Uses the same bearer token + 401 refresh as rpc().
|
|
644
644
|
* Returns the parsed JSON body; throws on non-2xx with the server's error.
|
|
645
645
|
*/
|
|
646
|
-
async restRequest(method,
|
|
647
|
-
const doFetch = (token2) => this.fetchImpl(`${this.baseUrl}${
|
|
646
|
+
async restRequest(method, path6, body) {
|
|
647
|
+
const doFetch = (token2) => this.fetchImpl(`${this.baseUrl}${path6}`, {
|
|
648
648
|
method,
|
|
649
649
|
headers: {
|
|
650
650
|
"content-type": "application/json",
|
|
@@ -1406,6 +1406,7 @@ var MIME_BY_EXT = {
|
|
|
1406
1406
|
jpg: "image/jpeg",
|
|
1407
1407
|
jpeg: "image/jpeg",
|
|
1408
1408
|
webp: "image/webp",
|
|
1409
|
+
avif: "image/avif",
|
|
1409
1410
|
gif: "image/gif",
|
|
1410
1411
|
heic: "image/heic",
|
|
1411
1412
|
mp4: "video/mp4",
|
|
@@ -1426,11 +1427,30 @@ function guessContentType(filename) {
|
|
|
1426
1427
|
return MIME_BY_EXT[ext];
|
|
1427
1428
|
}
|
|
1428
1429
|
async function uploadFile(client, localPath, options = {}) {
|
|
1430
|
+
return upload(client, localPath, options, false);
|
|
1431
|
+
}
|
|
1432
|
+
async function upload3DFile(client, localPath, options = {}) {
|
|
1433
|
+
if (path4.extname(localPath).toLowerCase() !== ".glb") {
|
|
1434
|
+
throw new CliError(
|
|
1435
|
+
"3D rigging uploads require a self-contained .glb file."
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
return upload(
|
|
1439
|
+
client,
|
|
1440
|
+
localPath,
|
|
1441
|
+
{ ...options, contentType: "model/gltf-binary" },
|
|
1442
|
+
true
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
async function upload(client, localPath, options, model3d) {
|
|
1429
1446
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1430
1447
|
const resolved = path4.resolve(localPath);
|
|
1431
1448
|
if (!fs4.existsSync(resolved)) {
|
|
1432
1449
|
throw new CliError(`File not found: ${resolved}`);
|
|
1433
1450
|
}
|
|
1451
|
+
if (!fs4.statSync(resolved).isFile()) {
|
|
1452
|
+
throw new CliError(`Expected a file: ${resolved}`);
|
|
1453
|
+
}
|
|
1434
1454
|
const filename = path4.basename(resolved);
|
|
1435
1455
|
const contentType = options.contentType ?? guessContentType(filename);
|
|
1436
1456
|
if (!contentType) {
|
|
@@ -1438,21 +1458,59 @@ async function uploadFile(client, localPath, options = {}) {
|
|
|
1438
1458
|
`Cannot infer media type for "${filename}". Pass --content-type (image/*, video/* or audio/*).`
|
|
1439
1459
|
);
|
|
1440
1460
|
}
|
|
1441
|
-
const
|
|
1461
|
+
const createTool = model3d ? "create_3d_upload" : "create_media_upload";
|
|
1462
|
+
const finalizeTool = model3d ? "finalize_3d_upload" : "finalize_media_upload";
|
|
1463
|
+
const created = await client.callTool(createTool, {
|
|
1442
1464
|
filename,
|
|
1443
1465
|
content_type: contentType
|
|
1444
1466
|
});
|
|
1445
1467
|
const uploadUrl = created?.upload_url;
|
|
1446
1468
|
const filePath = created?.file_path;
|
|
1447
1469
|
if (!uploadUrl || !filePath) {
|
|
1448
|
-
throw new CliError(
|
|
1449
|
-
"create_media_upload did not return upload_url/file_path."
|
|
1450
|
-
);
|
|
1470
|
+
throw new CliError(`${createTool} did not return upload_url/file_path.`);
|
|
1451
1471
|
}
|
|
1452
1472
|
const { size } = fs4.statSync(resolved);
|
|
1473
|
+
const headers = {
|
|
1474
|
+
"content-type": contentType,
|
|
1475
|
+
"content-length": String(size)
|
|
1476
|
+
};
|
|
1477
|
+
if (model3d) {
|
|
1478
|
+
const maxBytes = created?.max_bytes;
|
|
1479
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
|
|
1480
|
+
throw new CliError(
|
|
1481
|
+
"create_3d_upload did not return a valid max_bytes limit."
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
if (size > maxBytes) {
|
|
1485
|
+
throw new CliError(
|
|
1486
|
+
`3D upload is ${size} bytes; the server allows at most ${maxBytes} bytes.`
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
if (!created.headers || typeof created.headers !== "object" || Array.isArray(created.headers)) {
|
|
1490
|
+
throw new CliError(
|
|
1491
|
+
"create_3d_upload did not return the required signed PUT headers."
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
for (const [name, value] of Object.entries(created.headers)) {
|
|
1495
|
+
if (typeof value !== "string")
|
|
1496
|
+
throw new CliError(
|
|
1497
|
+
`create_3d_upload returned an invalid ${name} header.`
|
|
1498
|
+
);
|
|
1499
|
+
headers[name.toLowerCase()] = value;
|
|
1500
|
+
}
|
|
1501
|
+
headers["content-length"] = String(size);
|
|
1502
|
+
const range = /^(\d+),(\d+)$/.exec(
|
|
1503
|
+
headers["x-goog-content-length-range"] ?? ""
|
|
1504
|
+
);
|
|
1505
|
+
if (range && (size < Number(range[1]) || size > Number(range[2]))) {
|
|
1506
|
+
throw new CliError(
|
|
1507
|
+
`3D upload must contain between ${range[1]} and ${range[2]} bytes.`
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1453
1511
|
const putRes = await fetchImpl(uploadUrl, {
|
|
1454
1512
|
method: "PUT",
|
|
1455
|
-
headers
|
|
1513
|
+
headers,
|
|
1456
1514
|
body: Readable2.toWeb(
|
|
1457
1515
|
fs4.createReadStream(resolved)
|
|
1458
1516
|
),
|
|
@@ -1465,16 +1523,494 @@ async function uploadFile(client, localPath, options = {}) {
|
|
|
1465
1523
|
`Upload PUT failed (HTTP ${putRes.status}). The presigned URL may have expired \u2014 retry.`
|
|
1466
1524
|
);
|
|
1467
1525
|
}
|
|
1468
|
-
const finalized = await client.callTool(
|
|
1526
|
+
const finalized = await client.callTool(finalizeTool, {
|
|
1469
1527
|
file_path: filePath,
|
|
1470
1528
|
original_filename: filename
|
|
1471
1529
|
});
|
|
1472
1530
|
const url = finalized?.url ?? finalized?.cdn_url;
|
|
1473
1531
|
if (!url) {
|
|
1474
|
-
throw new CliError(
|
|
1532
|
+
throw new CliError(`${finalizeTool} did not return a public url.`);
|
|
1475
1533
|
}
|
|
1476
1534
|
return { ...finalized, url, file_path: filePath };
|
|
1477
1535
|
}
|
|
1536
|
+
|
|
1537
|
+
// src/core/model3d.ts
|
|
1538
|
+
import fs5 from "fs";
|
|
1539
|
+
import path5 from "path";
|
|
1540
|
+
import { randomUUID } from "crypto";
|
|
1541
|
+
|
|
1542
|
+
// src/core/media.ts
|
|
1543
|
+
var IMAGE_EXTS = /* @__PURE__ */ new Set([
|
|
1544
|
+
"png",
|
|
1545
|
+
"jpg",
|
|
1546
|
+
"jpeg",
|
|
1547
|
+
"webp",
|
|
1548
|
+
"gif",
|
|
1549
|
+
"bmp",
|
|
1550
|
+
"heic",
|
|
1551
|
+
"heif",
|
|
1552
|
+
"avif",
|
|
1553
|
+
"svg"
|
|
1554
|
+
]);
|
|
1555
|
+
var VIDEO_EXTS = /* @__PURE__ */ new Set(["mp4", "webm", "mov", "m4v"]);
|
|
1556
|
+
var AUDIO_EXTS = /* @__PURE__ */ new Set([
|
|
1557
|
+
"mp3",
|
|
1558
|
+
"wav",
|
|
1559
|
+
"pcm",
|
|
1560
|
+
"opus",
|
|
1561
|
+
"m4a",
|
|
1562
|
+
"ogg",
|
|
1563
|
+
"flac",
|
|
1564
|
+
"aac"
|
|
1565
|
+
]);
|
|
1566
|
+
function kindOf(url, typeHint) {
|
|
1567
|
+
const t = (typeHint ?? "").toLowerCase();
|
|
1568
|
+
if (t.includes("image")) return "image";
|
|
1569
|
+
if (t.includes("video")) return "video";
|
|
1570
|
+
if (t.includes("audio") || t.includes("music") || t.includes("sound") || t.includes("voice") || t.includes("speech") || t.includes("tts")) {
|
|
1571
|
+
return "audio";
|
|
1572
|
+
}
|
|
1573
|
+
const path6 = url.split(/[?#]/)[0]?.toLowerCase() ?? "";
|
|
1574
|
+
if (path6.includes("/img/")) return "image";
|
|
1575
|
+
if (path6.includes("/vid/")) return "video";
|
|
1576
|
+
if (path6.includes("/aud/")) return "audio";
|
|
1577
|
+
const ext = path6.match(/\.([a-z0-9]+)$/)?.[1];
|
|
1578
|
+
if (ext) {
|
|
1579
|
+
if (IMAGE_EXTS.has(ext)) return "image";
|
|
1580
|
+
if (VIDEO_EXTS.has(ext)) return "video";
|
|
1581
|
+
if (AUDIO_EXTS.has(ext)) return "audio";
|
|
1582
|
+
}
|
|
1583
|
+
return null;
|
|
1584
|
+
}
|
|
1585
|
+
function buildMediaDescriptors(urls, typeHint) {
|
|
1586
|
+
if (!Array.isArray(urls)) return [];
|
|
1587
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1588
|
+
const out = [];
|
|
1589
|
+
for (const url of urls) {
|
|
1590
|
+
if (typeof url !== "string" || !/^https?:\/\//i.test(url) || seen.has(url))
|
|
1591
|
+
continue;
|
|
1592
|
+
const kind = kindOf(url, typeHint);
|
|
1593
|
+
if (!kind) continue;
|
|
1594
|
+
seen.add(url);
|
|
1595
|
+
out.push({ kind, url });
|
|
1596
|
+
}
|
|
1597
|
+
return out;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// src/core/model3d.ts
|
|
1601
|
+
function isModel3DResult(payload) {
|
|
1602
|
+
return payload?.type === "model3d";
|
|
1603
|
+
}
|
|
1604
|
+
function warningStrings(value) {
|
|
1605
|
+
return Array.isArray(value) ? value.filter(
|
|
1606
|
+
(warning) => typeof warning === "string" && Boolean(warning.trim())
|
|
1607
|
+
) : [];
|
|
1608
|
+
}
|
|
1609
|
+
function model3DWarnings(payload, localWarnings = []) {
|
|
1610
|
+
return [
|
|
1611
|
+
.../* @__PURE__ */ new Set([
|
|
1612
|
+
...warningStrings(payload?.artifact_warnings),
|
|
1613
|
+
...warningStrings(payload?.package_warnings),
|
|
1614
|
+
...localWarnings
|
|
1615
|
+
])
|
|
1616
|
+
];
|
|
1617
|
+
}
|
|
1618
|
+
function model3DArtifacts(payload) {
|
|
1619
|
+
const artifacts = [];
|
|
1620
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1621
|
+
for (const item of Array.isArray(payload?.artifacts) ? payload.artifacts : []) {
|
|
1622
|
+
if (!item || typeof item.url !== "string" || !/^https?:\/\//i.test(item.url))
|
|
1623
|
+
continue;
|
|
1624
|
+
const filename = typeof item.filename === "string" ? item.filename : "";
|
|
1625
|
+
const key = `${item.url}
|
|
1626
|
+
${filename}`;
|
|
1627
|
+
if (seen.has(key)) continue;
|
|
1628
|
+
seen.add(key);
|
|
1629
|
+
artifacts.push({
|
|
1630
|
+
id: String(item.id ?? `artifact-${artifacts.length}`),
|
|
1631
|
+
role: String(item.role ?? "other"),
|
|
1632
|
+
url: item.url,
|
|
1633
|
+
filename: filename || `artifact-${artifacts.length}.${extFromUrl(item.url)}`,
|
|
1634
|
+
...typeof item.content_type === "string" ? { content_type: item.content_type } : {},
|
|
1635
|
+
...typeof item.bytes === "number" ? { bytes: item.bytes } : {},
|
|
1636
|
+
...Array.isArray(item.aliases) ? {
|
|
1637
|
+
aliases: item.aliases.filter(
|
|
1638
|
+
(alias) => typeof alias === "string"
|
|
1639
|
+
)
|
|
1640
|
+
} : {}
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
const urls = Array.isArray(payload?.output_urls) ? payload.output_urls : payload?.outputs;
|
|
1644
|
+
for (const url of Array.isArray(urls) ? urls : []) {
|
|
1645
|
+
if (typeof url !== "string" || !/^https?:\/\//i.test(url) || artifacts.some((a) => a.url === url))
|
|
1646
|
+
continue;
|
|
1647
|
+
artifacts.push({
|
|
1648
|
+
id: `model-${artifacts.length}`,
|
|
1649
|
+
role: "model",
|
|
1650
|
+
url,
|
|
1651
|
+
filename: `model-${artifacts.length}.${extFromUrl(url)}`
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
if (typeof payload?.preview_url === "string" && /^https?:\/\//i.test(payload.preview_url) && !artifacts.some((a) => a.url === payload.preview_url)) {
|
|
1655
|
+
artifacts.push({
|
|
1656
|
+
id: "preview",
|
|
1657
|
+
role: "preview",
|
|
1658
|
+
url: payload.preview_url,
|
|
1659
|
+
filename: `preview.${extFromUrl(payload.preview_url, "png")}`
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
return artifacts;
|
|
1663
|
+
}
|
|
1664
|
+
function model3DOutputFields(payload) {
|
|
1665
|
+
const artifacts = model3DArtifacts(payload);
|
|
1666
|
+
const previews = artifacts.filter((a) => a.role === "preview");
|
|
1667
|
+
return {
|
|
1668
|
+
output_files: artifacts,
|
|
1669
|
+
artifact_warnings: warningStrings(payload?.artifact_warnings),
|
|
1670
|
+
package_warnings: model3DWarnings(payload),
|
|
1671
|
+
output_media: previews.flatMap(
|
|
1672
|
+
(a) => buildMediaDescriptors([a.url], a.content_type)
|
|
1673
|
+
)
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
function safeName(value, fallback) {
|
|
1677
|
+
const name = value.replace(/[^a-zA-Z0-9._ -]/g, "_").replace(/^\.+/, "");
|
|
1678
|
+
return name && name !== "." && name !== ".." ? name : fallback;
|
|
1679
|
+
}
|
|
1680
|
+
function safeRelativeFilename(filename, index) {
|
|
1681
|
+
return normalizeThreeDArtifactFilename(filename || `artifact-${index}.bin`);
|
|
1682
|
+
}
|
|
1683
|
+
function truncateUtf8(value, maximum) {
|
|
1684
|
+
let result = "";
|
|
1685
|
+
let bytes = 0;
|
|
1686
|
+
for (const character of Array.from(value)) {
|
|
1687
|
+
const size = Buffer.byteLength(character);
|
|
1688
|
+
if (bytes + size > maximum) break;
|
|
1689
|
+
result += character;
|
|
1690
|
+
bytes += size;
|
|
1691
|
+
}
|
|
1692
|
+
return result;
|
|
1693
|
+
}
|
|
1694
|
+
function normalizeThreeDArtifactFilename(name, extension, suffix = "") {
|
|
1695
|
+
const cleaned = name.replace(/\\/g, "/").replace(/^[A-Za-z]:\//, "").replace(/^\/+/, "");
|
|
1696
|
+
const parts = cleaned.split("/").filter((part) => part && part !== "." && part !== "..");
|
|
1697
|
+
const safeParts = parts.map((part, index) => {
|
|
1698
|
+
const sanitized = part.replace(/[\x00-\x1f\x7f<>:"|?*]/g, "_");
|
|
1699
|
+
const last = index === parts.length - 1;
|
|
1700
|
+
const originalExtension = path5.posix.extname(sanitized);
|
|
1701
|
+
const keptExtension = last && extension ? `.${extension.replace(/^\./, "")}` : truncateUtf8(originalExtension, 24);
|
|
1702
|
+
const keptSuffix = last ? truncateUtf8(suffix, 64) : "";
|
|
1703
|
+
const base = originalExtension ? sanitized.slice(0, -originalExtension.length) : sanitized;
|
|
1704
|
+
const stem = truncateUtf8(
|
|
1705
|
+
base,
|
|
1706
|
+
180 - Buffer.byteLength(keptExtension + keptSuffix)
|
|
1707
|
+
);
|
|
1708
|
+
return `${stem || "artifact"}${keptSuffix}${keptExtension}`;
|
|
1709
|
+
});
|
|
1710
|
+
while (safeParts.length > 1 && Buffer.byteLength(safeParts.join("/")) > 700)
|
|
1711
|
+
safeParts.shift();
|
|
1712
|
+
return safeParts.join("/") || `artifact.${extension || "bin"}`;
|
|
1713
|
+
}
|
|
1714
|
+
function model3DPackageDirectory(payload, target) {
|
|
1715
|
+
const id = safeName(
|
|
1716
|
+
String(payload?.asset_id ?? payload?.job_id ?? payload?.id ?? "asset"),
|
|
1717
|
+
"asset"
|
|
1718
|
+
);
|
|
1719
|
+
if (target === true) return path5.join("media", "3d", id);
|
|
1720
|
+
if (/\{(?:index|ext|name)\}/.test(target)) {
|
|
1721
|
+
throw new CliError(
|
|
1722
|
+
"3D downloads need a package directory. Use --download './media/3d/{job_id}' (no {index}, {name}, or {ext}).",
|
|
1723
|
+
EXIT.USAGE
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
const expanded = target.replaceAll("{job_id}", safeName(String(payload?.job_id ?? id), "job")).replaceAll("{asset_id}", id);
|
|
1727
|
+
if (/\.(?:glb|gltf|fbx|obj|mtl|stl|usdz|zip|bin)$/i.test(expanded)) {
|
|
1728
|
+
throw new CliError(
|
|
1729
|
+
"3D downloads contain a model, dependencies, and a manifest. Pass a directory to --download, such as ./media/3d.",
|
|
1730
|
+
EXIT.USAGE
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
return /\{(?:job_id|asset_id)\}/.test(target) ? expanded : path5.join(expanded, id);
|
|
1734
|
+
}
|
|
1735
|
+
function writeDependencyAliases(directory, artifacts) {
|
|
1736
|
+
const occupied = new Map(
|
|
1737
|
+
artifacts.map((artifact) => [
|
|
1738
|
+
artifact.local_path.toLowerCase(),
|
|
1739
|
+
artifact.url
|
|
1740
|
+
])
|
|
1741
|
+
);
|
|
1742
|
+
occupied.set("manifest.json", "manifest");
|
|
1743
|
+
const warnings = [];
|
|
1744
|
+
const files = [];
|
|
1745
|
+
for (const artifact of artifacts) {
|
|
1746
|
+
const aliases = [];
|
|
1747
|
+
for (const original of artifact.aliases ?? []) {
|
|
1748
|
+
const relative = original.replace(/\\/g, "/");
|
|
1749
|
+
const segments = relative.split("/");
|
|
1750
|
+
if (!relative || path5.posix.isAbsolute(relative) || segments.some(
|
|
1751
|
+
(segment) => !segment || segment === "." || segment === ".." || /[\x00-\x1f\x7f<>:"|?*]/.test(segment)
|
|
1752
|
+
)) {
|
|
1753
|
+
warnings.push(
|
|
1754
|
+
`${artifact.local_path}: unsafe dependency alias was not saved: ${original}`
|
|
1755
|
+
);
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1758
|
+
if (relative === artifact.local_path) {
|
|
1759
|
+
aliases.push(relative);
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
const lower = relative.toLowerCase();
|
|
1763
|
+
const conflict = Array.from(occupied).some(
|
|
1764
|
+
([name, url]) => name === lower && url !== artifact.url || name.startsWith(lower + "/") || lower.startsWith(name + "/")
|
|
1765
|
+
);
|
|
1766
|
+
if (conflict) {
|
|
1767
|
+
warnings.push(
|
|
1768
|
+
`${artifact.local_path}: dependency alias collides with another artifact: ${relative}`
|
|
1769
|
+
);
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
const dest = path5.join(directory, relative);
|
|
1773
|
+
if (!fs5.existsSync(dest)) {
|
|
1774
|
+
fs5.mkdirSync(path5.dirname(dest), { recursive: true });
|
|
1775
|
+
fs5.copyFileSync(
|
|
1776
|
+
path5.join(directory, artifact.local_path),
|
|
1777
|
+
dest,
|
|
1778
|
+
fs5.constants.COPYFILE_EXCL
|
|
1779
|
+
);
|
|
1780
|
+
files.push({
|
|
1781
|
+
url: artifact.url,
|
|
1782
|
+
path: dest,
|
|
1783
|
+
bytes: artifact.downloaded_bytes
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
aliases.push(relative);
|
|
1787
|
+
occupied.set(lower, artifact.url);
|
|
1788
|
+
}
|
|
1789
|
+
if (aliases.length) artifact.local_aliases = aliases;
|
|
1790
|
+
}
|
|
1791
|
+
return { files, warnings };
|
|
1792
|
+
}
|
|
1793
|
+
function localizeDependencies(directory, artifacts) {
|
|
1794
|
+
const warnings = [];
|
|
1795
|
+
const matching = (source, reference) => {
|
|
1796
|
+
let decoded = reference;
|
|
1797
|
+
try {
|
|
1798
|
+
decoded = decodeURIComponent(reference);
|
|
1799
|
+
} catch {
|
|
1800
|
+
}
|
|
1801
|
+
const sourceRelative = path5.posix.normalize(
|
|
1802
|
+
path5.posix.join(
|
|
1803
|
+
path5.posix.dirname(source.source_filename.replace(/\\/g, "/")),
|
|
1804
|
+
decoded.replace(/\\/g, "/")
|
|
1805
|
+
)
|
|
1806
|
+
);
|
|
1807
|
+
const absoluteUrl = (() => {
|
|
1808
|
+
try {
|
|
1809
|
+
return new URL(reference, source.url).href;
|
|
1810
|
+
} catch {
|
|
1811
|
+
return "";
|
|
1812
|
+
}
|
|
1813
|
+
})();
|
|
1814
|
+
const precise = artifacts.filter(
|
|
1815
|
+
(a) => a.url === reference || a.url === absoluteUrl || a.source_filename.replace(/\\/g, "/") === sourceRelative
|
|
1816
|
+
);
|
|
1817
|
+
if (precise.length === 1) return precise[0];
|
|
1818
|
+
const byFilename = artifacts.filter(
|
|
1819
|
+
(a) => path5.posix.basename(a.source_filename.replace(/\\/g, "/")) === path5.posix.basename(decoded.replace(/\\/g, "/"))
|
|
1820
|
+
);
|
|
1821
|
+
return byFilename.length === 1 ? byFilename[0] : void 0;
|
|
1822
|
+
};
|
|
1823
|
+
const rewrite = (source, reference, encode) => {
|
|
1824
|
+
if (/^data:/i.test(reference)) return reference;
|
|
1825
|
+
const found = matching(source, reference);
|
|
1826
|
+
if (!found) {
|
|
1827
|
+
warnings.push(
|
|
1828
|
+
`${source.local_path}: dependency could not be matched to a saved artifact: ${reference}`
|
|
1829
|
+
);
|
|
1830
|
+
return reference;
|
|
1831
|
+
}
|
|
1832
|
+
const relative = path5.posix.relative(
|
|
1833
|
+
path5.posix.dirname(source.local_path),
|
|
1834
|
+
found.local_path
|
|
1835
|
+
);
|
|
1836
|
+
return encode ? relative.split("/").map(encodeURIComponent).join("/") : relative;
|
|
1837
|
+
};
|
|
1838
|
+
for (const artifact of artifacts) {
|
|
1839
|
+
const filename = path5.join(directory, artifact.local_path);
|
|
1840
|
+
const ext = path5.extname(artifact.local_path).toLowerCase();
|
|
1841
|
+
if (ext === ".gltf") {
|
|
1842
|
+
try {
|
|
1843
|
+
const document = JSON.parse(fs5.readFileSync(filename, "utf8"));
|
|
1844
|
+
const visit = (value) => {
|
|
1845
|
+
if (Array.isArray(value)) {
|
|
1846
|
+
value.forEach(visit);
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
if (!value || typeof value !== "object") return;
|
|
1850
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1851
|
+
if (key === "uri" && typeof child === "string")
|
|
1852
|
+
value[key] = rewrite(
|
|
1853
|
+
artifact,
|
|
1854
|
+
child,
|
|
1855
|
+
true
|
|
1856
|
+
);
|
|
1857
|
+
else visit(child);
|
|
1858
|
+
}
|
|
1859
|
+
};
|
|
1860
|
+
visit(document);
|
|
1861
|
+
fs5.writeFileSync(filename, JSON.stringify(document, null, 2) + "\n");
|
|
1862
|
+
} catch (error) {
|
|
1863
|
+
warnings.push(
|
|
1864
|
+
`${artifact.local_path}: could not resolve glTF dependencies (${error instanceof Error ? error.message : String(error)}).`
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
} else if (ext === ".obj" || ext === ".mtl") {
|
|
1868
|
+
const contents = fs5.readFileSync(filename, "utf8");
|
|
1869
|
+
const updated = contents.replace(
|
|
1870
|
+
/^(\s*(?:mtllib|map_[a-z0-9_]+|bump|disp|decal|norm|refl)\s+)(.+)$/gim,
|
|
1871
|
+
(line, prefix, raw) => {
|
|
1872
|
+
const reference = raw.trim();
|
|
1873
|
+
if (reference.startsWith("-")) {
|
|
1874
|
+
const candidates = artifacts.filter(
|
|
1875
|
+
(a) => reference.endsWith(a.url) || reference.endsWith(a.source_filename) || reference.endsWith(path5.posix.basename(a.source_filename))
|
|
1876
|
+
);
|
|
1877
|
+
if (candidates.length === 1) {
|
|
1878
|
+
const candidate = candidates[0];
|
|
1879
|
+
const name = reference.endsWith(candidate.url) ? candidate.url : reference.endsWith(candidate.source_filename) ? candidate.source_filename : path5.posix.basename(candidate.source_filename);
|
|
1880
|
+
return prefix + reference.slice(0, -name.length) + rewrite(artifact, name, false);
|
|
1881
|
+
}
|
|
1882
|
+
warnings.push(
|
|
1883
|
+
`${artifact.local_path}: could not resolve material reference: ${reference}`
|
|
1884
|
+
);
|
|
1885
|
+
return line;
|
|
1886
|
+
}
|
|
1887
|
+
if (/mtllib/i.test(prefix) && !matching(artifact, reference)) {
|
|
1888
|
+
const references = [...reference.matchAll(/"([^"]+)"|(\S+)/g)].map(
|
|
1889
|
+
(match) => match[1] ?? match[2]
|
|
1890
|
+
);
|
|
1891
|
+
if (references.length > 1 && references.every((ref) => matching(artifact, ref))) {
|
|
1892
|
+
return references.map((ref) => prefix + rewrite(artifact, ref, false)).join("\n");
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
return prefix + rewrite(artifact, reference, false);
|
|
1896
|
+
}
|
|
1897
|
+
);
|
|
1898
|
+
fs5.writeFileSync(filename, updated);
|
|
1899
|
+
}
|
|
1900
|
+
artifact.downloaded_bytes = fs5.statSync(filename).size;
|
|
1901
|
+
}
|
|
1902
|
+
return [...new Set(warnings)];
|
|
1903
|
+
}
|
|
1904
|
+
async function downloadModel3DPackage(payload, target, fetchImpl = fetch) {
|
|
1905
|
+
try {
|
|
1906
|
+
return await downloadPackage(payload, target, fetchImpl);
|
|
1907
|
+
} catch (error) {
|
|
1908
|
+
const id = payload?.asset_id ?? payload?.job_id ?? payload?.id;
|
|
1909
|
+
throw new CliError(
|
|
1910
|
+
error instanceof Error ? error.message : String(error),
|
|
1911
|
+
error instanceof CliError ? error.exitCode : EXIT.ERROR,
|
|
1912
|
+
id ? `The saved 3D asset remains available. Retry downloading with: videodraft assets 3d get ${id} --download` : void 0,
|
|
1913
|
+
{
|
|
1914
|
+
asset_id: payload?.asset_id,
|
|
1915
|
+
job_id: payload?.job_id,
|
|
1916
|
+
request_id: payload?.request_id
|
|
1917
|
+
}
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
async function downloadPackage(payload, target, fetchImpl) {
|
|
1922
|
+
const artifacts = model3DArtifacts(payload);
|
|
1923
|
+
if (artifacts.length === 0)
|
|
1924
|
+
throw new CliError("The 3D asset has no downloadable artifacts yet.");
|
|
1925
|
+
const preferredDirectory = model3DPackageDirectory(payload, target);
|
|
1926
|
+
fs5.mkdirSync(path5.dirname(preferredDirectory), { recursive: true });
|
|
1927
|
+
let directory = preferredDirectory;
|
|
1928
|
+
for (let suffix = 2; ; suffix++) {
|
|
1929
|
+
try {
|
|
1930
|
+
fs5.mkdirSync(directory);
|
|
1931
|
+
break;
|
|
1932
|
+
} catch (error) {
|
|
1933
|
+
if (error.code !== "EEXIST") throw error;
|
|
1934
|
+
directory = `${preferredDirectory}-${suffix}`;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
const used = /* @__PURE__ */ new Set(["manifest.json"]);
|
|
1938
|
+
const local = artifacts.map((artifact, index) => {
|
|
1939
|
+
const original = safeRelativeFilename(artifact.filename, index);
|
|
1940
|
+
let relative = original;
|
|
1941
|
+
let suffix = 1;
|
|
1942
|
+
while (Array.from(used).some(
|
|
1943
|
+
(name) => name === relative.toLowerCase() || name.startsWith(relative.toLowerCase() + "/") || relative.toLowerCase().startsWith(name + "/")
|
|
1944
|
+
)) {
|
|
1945
|
+
relative = normalizeThreeDArtifactFilename(
|
|
1946
|
+
path5.posix.basename(original),
|
|
1947
|
+
void 0,
|
|
1948
|
+
`-${suffix++}`
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
used.add(relative.toLowerCase());
|
|
1952
|
+
return {
|
|
1953
|
+
...artifact,
|
|
1954
|
+
local_path: relative,
|
|
1955
|
+
source_filename: artifact.filename,
|
|
1956
|
+
downloaded_bytes: 0
|
|
1957
|
+
};
|
|
1958
|
+
});
|
|
1959
|
+
const files = [];
|
|
1960
|
+
for (const artifact of local) {
|
|
1961
|
+
const dest = path5.join(directory, artifact.local_path);
|
|
1962
|
+
const temporary = `${dest}.partial-${randomUUID()}`;
|
|
1963
|
+
try {
|
|
1964
|
+
const file = await downloadUrl(artifact.url, temporary, fetchImpl);
|
|
1965
|
+
fs5.renameSync(temporary, dest);
|
|
1966
|
+
artifact.downloaded_bytes = file.bytes;
|
|
1967
|
+
files.push({ ...file, path: dest });
|
|
1968
|
+
} finally {
|
|
1969
|
+
fs5.rmSync(temporary, { force: true });
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
const warnings = localizeDependencies(directory, local);
|
|
1973
|
+
for (let index = 0; index < files.length; index++) {
|
|
1974
|
+
const file = files[index];
|
|
1975
|
+
file.bytes = local[index].downloaded_bytes;
|
|
1976
|
+
if (local[index].role === "preview") {
|
|
1977
|
+
const preview = await writePreview(
|
|
1978
|
+
file.path,
|
|
1979
|
+
file.url,
|
|
1980
|
+
file.bytes,
|
|
1981
|
+
fetchImpl
|
|
1982
|
+
);
|
|
1983
|
+
if (preview) file.preview = preview;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
const aliases = writeDependencyAliases(directory, local);
|
|
1987
|
+
files.push(...aliases.files);
|
|
1988
|
+
warnings.push(...aliases.warnings);
|
|
1989
|
+
const manifestPath = path5.join(directory, "manifest.json");
|
|
1990
|
+
const temporaryManifest = `${manifestPath}.partial-${randomUUID()}`;
|
|
1991
|
+
const manifest = {
|
|
1992
|
+
version: 1,
|
|
1993
|
+
type: "model3d",
|
|
1994
|
+
asset_id: payload?.asset_id ?? payload?.id,
|
|
1995
|
+
job_id: payload?.job_id,
|
|
1996
|
+
model: payload?.model,
|
|
1997
|
+
credits: payload?.credits,
|
|
1998
|
+
byok: payload?.byok,
|
|
1999
|
+
complete: warnings.length === 0,
|
|
2000
|
+
complete_scope: "returned_artifacts",
|
|
2001
|
+
artifact_warnings: warningStrings(payload?.artifact_warnings),
|
|
2002
|
+
warnings: model3DWarnings(payload, warnings),
|
|
2003
|
+
artifacts: local
|
|
2004
|
+
};
|
|
2005
|
+
fs5.writeFileSync(temporaryManifest, JSON.stringify(manifest, null, 2) + "\n");
|
|
2006
|
+
fs5.renameSync(temporaryManifest, manifestPath);
|
|
2007
|
+
return {
|
|
2008
|
+
downloaded_files: files,
|
|
2009
|
+
package_directory: directory,
|
|
2010
|
+
manifest_path: manifestPath,
|
|
2011
|
+
package_warnings: model3DWarnings(payload, warnings)
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
1478
2014
|
export {
|
|
1479
2015
|
AuthError,
|
|
1480
2016
|
CliError,
|
|
@@ -1493,6 +2029,7 @@ export {
|
|
|
1493
2029
|
configPath,
|
|
1494
2030
|
createConnectionSessionStore,
|
|
1495
2031
|
createPkcePair,
|
|
2032
|
+
downloadModel3DPackage,
|
|
1496
2033
|
downloadOutputs,
|
|
1497
2034
|
downloadUrl,
|
|
1498
2035
|
exchangeCode,
|
|
@@ -1500,6 +2037,9 @@ export {
|
|
|
1500
2037
|
extractOutputUrls,
|
|
1501
2038
|
getProfile,
|
|
1502
2039
|
guessContentType,
|
|
2040
|
+
isModel3DResult,
|
|
2041
|
+
model3DArtifacts,
|
|
2042
|
+
model3DOutputFields,
|
|
1503
2043
|
nextPollDelay,
|
|
1504
2044
|
pollExport,
|
|
1505
2045
|
pollGeneration,
|
|
@@ -1517,6 +2057,7 @@ export {
|
|
|
1517
2057
|
sessionScope,
|
|
1518
2058
|
staticTokenProvider,
|
|
1519
2059
|
updateConfig,
|
|
2060
|
+
upload3DFile,
|
|
1520
2061
|
uploadFile,
|
|
1521
2062
|
writeConfig
|
|
1522
2063
|
};
|