pixelkiln 0.3.0 → 0.4.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/CONTRIBUTING.md +2 -1
- package/PROVIDERS.md +128 -90
- package/README.md +37 -9
- package/SECURITY.md +4 -3
- package/dist/cli.js +667 -146
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +622 -83
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +380 -228
- package/dist/index.d.ts +380 -228
- package/dist/index.js +609 -83
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +12 -5
- package/docs/ARCHITECTURE.md +25 -15
- package/docs/CLI.md +23 -17
- package/docs/GENERATORS.md +5 -0
- package/docs/GETTING_STARTED.md +24 -5
- package/docs/LIBRARY.md +4 -4
- package/docs/MANIFEST.md +103 -5
- package/docs/PIXELLAB.md +100 -0
- package/docs/PROVIDER_BENCHMARK.md +134 -0
- package/docs/README.md +4 -1
- package/docs/RECOVERY.md +8 -7
- package/docs/RETRO_DIFFUSION.md +110 -0
- package/package.json +3 -1
- package/schema/manifest.schema.json +15 -1
- package/skills/pixelkiln/SKILL.md +15 -8
package/dist/cli.js
CHANGED
|
@@ -38,6 +38,71 @@ function applyEnv(contents) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// src/provider.ts
|
|
42
|
+
function validateCostEstimate(providerId, value) {
|
|
43
|
+
if (!value || typeof value !== "object") {
|
|
44
|
+
throw new Error(`Provider "${providerId}" returned an invalid cost estimate`);
|
|
45
|
+
}
|
|
46
|
+
const estimate = value;
|
|
47
|
+
if (typeof estimate.unit !== "string" || !estimate.unit.trim()) {
|
|
48
|
+
throw new Error(`Provider "${providerId}" returned an invalid cost unit`);
|
|
49
|
+
}
|
|
50
|
+
if (!Number.isFinite(estimate.amount) || estimate.amount < 0) {
|
|
51
|
+
throw new Error(`Provider "${providerId}" returned an invalid cost amount`);
|
|
52
|
+
}
|
|
53
|
+
if (estimate.unit === "free" && estimate.amount !== 0) {
|
|
54
|
+
throw new Error(`Provider "${providerId}" returned a nonzero amount with the free cost unit`);
|
|
55
|
+
}
|
|
56
|
+
if (!Number.isInteger(estimate.candidates) || estimate.candidates < 1) {
|
|
57
|
+
throw new Error(`Provider "${providerId}" returned an invalid candidate count`);
|
|
58
|
+
}
|
|
59
|
+
return estimate;
|
|
60
|
+
}
|
|
61
|
+
function measureBalanceChange(before, after) {
|
|
62
|
+
if (before.unit !== after.unit || !Number.isFinite(before.remaining) || !Number.isFinite(after.remaining)) return null;
|
|
63
|
+
const delta = before.remaining - after.remaining;
|
|
64
|
+
return {
|
|
65
|
+
unit: before.unit,
|
|
66
|
+
before: before.remaining,
|
|
67
|
+
after: after.remaining,
|
|
68
|
+
spent: Math.max(0, delta),
|
|
69
|
+
credited: Math.max(0, -delta)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
var DEFAULT_RATE_LIMIT = { spacingMs: 2500, maxInFlight: 8 };
|
|
73
|
+
var UnsupportedCapabilityError = class extends Error {
|
|
74
|
+
constructor(providerId, capability) {
|
|
75
|
+
super(
|
|
76
|
+
`Provider "${providerId}" does not support ${capability}. That command is unavailable with this backend.`
|
|
77
|
+
);
|
|
78
|
+
this.name = "UnsupportedCapabilityError";
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
function requireList(provider) {
|
|
82
|
+
if (!provider.list) throw new UnsupportedCapabilityError(provider.id, "listing remote assets");
|
|
83
|
+
return provider.list.bind(provider);
|
|
84
|
+
}
|
|
85
|
+
function requireDelete(provider) {
|
|
86
|
+
if (!provider.delete) throw new UnsupportedCapabilityError(provider.id, "deleting remote assets");
|
|
87
|
+
return provider.delete.bind(provider);
|
|
88
|
+
}
|
|
89
|
+
function requireSelectCandidate(provider) {
|
|
90
|
+
if (!provider.selectCandidate) {
|
|
91
|
+
throw new UnsupportedCapabilityError(provider.id, "candidate selection");
|
|
92
|
+
}
|
|
93
|
+
return provider.selectCandidate.bind(provider);
|
|
94
|
+
}
|
|
95
|
+
function requireBalance(provider) {
|
|
96
|
+
if (!provider.balance) throw new UnsupportedCapabilityError(provider.id, "account balance");
|
|
97
|
+
return provider.balance.bind(provider);
|
|
98
|
+
}
|
|
99
|
+
function formatCost(unit, amount) {
|
|
100
|
+
if (unit === "free") return "free";
|
|
101
|
+
if (unit === "usd") return `$${amount.toFixed(2)}`;
|
|
102
|
+
if (unit === "generations") return `${amount} generation${amount === 1 ? "" : "s"}`;
|
|
103
|
+
return `${amount} ${unit}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
41
106
|
// src/providers/pixellab.ts
|
|
42
107
|
import { mkdirSync, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
43
108
|
import { randomUUID } from "crypto";
|
|
@@ -748,7 +813,8 @@ function parseHex(hex2) {
|
|
|
748
813
|
|
|
749
814
|
// src/types.ts
|
|
750
815
|
import { z as z2 } from "zod";
|
|
751
|
-
var
|
|
816
|
+
var MediaTypeSchema = z2.enum(["image/png", "image/gif"]);
|
|
817
|
+
var GeneratorSchema = z2.enum(["1dir", "map", "pixflux", "tiles", "animation"]);
|
|
752
818
|
function tileVariationCount(descriptions) {
|
|
753
819
|
return Math.max(1, descriptions) * 4;
|
|
754
820
|
}
|
|
@@ -780,7 +846,7 @@ function tilesCost(tileSize, variations) {
|
|
|
780
846
|
return 40;
|
|
781
847
|
}
|
|
782
848
|
var StyleImageSchema = z2.object({
|
|
783
|
-
/** Path to a PNG/JPEG, relative to the manifest
|
|
849
|
+
/** Path to a PNG/JPEG, relative to the manifest; the active provider validates limits. */
|
|
784
850
|
path: z2.string()
|
|
785
851
|
});
|
|
786
852
|
var StyleSchema = z2.object({
|
|
@@ -891,7 +957,9 @@ var StyleSchema = z2.object({
|
|
|
891
957
|
out: z2.string()
|
|
892
958
|
}).strict().optional(),
|
|
893
959
|
/** Tags applied to every object generated in this style, for server-side filtering. */
|
|
894
|
-
tags: z2.array(z2.string()).default([])
|
|
960
|
+
tags: z2.array(z2.string()).default([]),
|
|
961
|
+
/** Adapter-owned settings, keyed by provider id. */
|
|
962
|
+
providerOptions: z2.record(z2.record(z2.unknown())).default({})
|
|
895
963
|
}).strict().refine((s) => !(s.tileFeature && s.styleImages.length), {
|
|
896
964
|
message: "tileFeature and styleImages cannot be combined \u2014 a connectable set derives its own tile geometry, so remove one or the other",
|
|
897
965
|
path: ["tileFeature"]
|
|
@@ -906,7 +974,7 @@ var AssetSchema = z2.object({
|
|
|
906
974
|
height: z2.number().int().min(16).max(400).optional(),
|
|
907
975
|
/** Overrides the style default. `1dir` generator only. */
|
|
908
976
|
size: z2.number().int().min(32).max(256).optional(),
|
|
909
|
-
/** Explicit output path relative to outDir.
|
|
977
|
+
/** Explicit output path relative to outDir. Media-aware providers may replace its extension. */
|
|
910
978
|
file: z2.string().optional(),
|
|
911
979
|
/**
|
|
912
980
|
* Grid cell this asset owns in a mounted style, as [column, row].
|
|
@@ -958,6 +1026,8 @@ var AssetSchema = z2.object({
|
|
|
958
1026
|
var ManifestSchema = z2.object({
|
|
959
1027
|
$schema: z2.string().optional(),
|
|
960
1028
|
name: z2.string(),
|
|
1029
|
+
/** Generation backend. Existing manifests remain PixelLab by default. */
|
|
1030
|
+
provider: z2.string().min(1).default("pixellab"),
|
|
961
1031
|
styles: z2.record(StyleSchema),
|
|
962
1032
|
assets: z2.record(AssetSchema)
|
|
963
1033
|
}).strict();
|
|
@@ -995,7 +1065,8 @@ var LockEntrySchema = z2.object({
|
|
|
995
1065
|
sourceUrls: z2.array(
|
|
996
1066
|
z2.object({
|
|
997
1067
|
url: z2.string(),
|
|
998
|
-
role: z2.string().optional()
|
|
1068
|
+
role: z2.string().optional(),
|
|
1069
|
+
mediaType: MediaTypeSchema.optional()
|
|
999
1070
|
})
|
|
1000
1071
|
).default([]),
|
|
1001
1072
|
/**
|
|
@@ -1010,7 +1081,8 @@ var LockEntrySchema = z2.object({
|
|
|
1010
1081
|
z2.object({
|
|
1011
1082
|
path: z2.string(),
|
|
1012
1083
|
sha256: z2.string(),
|
|
1013
|
-
role: z2.string().optional()
|
|
1084
|
+
role: z2.string().optional(),
|
|
1085
|
+
mediaType: MediaTypeSchema.optional()
|
|
1014
1086
|
})
|
|
1015
1087
|
).default([]),
|
|
1016
1088
|
/**
|
|
@@ -1024,7 +1096,7 @@ var LockEntrySchema = z2.object({
|
|
|
1024
1096
|
/** Successful-submission estimate in `costUnit`; may be fractional USD. */
|
|
1025
1097
|
cost: z2.number().finite().nonnegative().default(0),
|
|
1026
1098
|
/** Unit for `cost`. Defaults preserve pre-unit PixelLab lockfiles. */
|
|
1027
|
-
costUnit: z2.
|
|
1099
|
+
costUnit: z2.string().min(1).default("generations"),
|
|
1028
1100
|
/** Which provider produced this. Absent on entries written before providers. */
|
|
1029
1101
|
provider: z2.string().default("pixellab")
|
|
1030
1102
|
});
|
|
@@ -1094,7 +1166,44 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
1094
1166
|
candidates: spec.generator === "1dir" ? candidateCount(spec.size) : 1
|
|
1095
1167
|
};
|
|
1096
1168
|
}
|
|
1169
|
+
validate(spec, styleImages) {
|
|
1170
|
+
if (spec.generator === "map") {
|
|
1171
|
+
requirePixelLabOption("view", spec.view, ["low top-down", "high top-down", "side"]);
|
|
1172
|
+
requirePixelLabOption("outline", spec.outline, [
|
|
1173
|
+
"single color outline",
|
|
1174
|
+
"selective outline",
|
|
1175
|
+
"lineless"
|
|
1176
|
+
]);
|
|
1177
|
+
requirePixelLabOption("shading", spec.shading, [
|
|
1178
|
+
"flat shading",
|
|
1179
|
+
"basic shading",
|
|
1180
|
+
"medium shading",
|
|
1181
|
+
"detailed shading"
|
|
1182
|
+
]);
|
|
1183
|
+
requirePixelLabOption("detail", spec.detail, [
|
|
1184
|
+
"low detail",
|
|
1185
|
+
"medium detail",
|
|
1186
|
+
"high detail"
|
|
1187
|
+
]);
|
|
1188
|
+
}
|
|
1189
|
+
if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
|
|
1190
|
+
throw new Error(`PixelLab ${spec.generator} does not support style images`);
|
|
1191
|
+
}
|
|
1192
|
+
for (const image of styleImages) {
|
|
1193
|
+
if (image.width > 256 || image.height > 256) {
|
|
1194
|
+
throw new Error(
|
|
1195
|
+
`Style image exceeds PixelLab's 256x256 limit (${image.width}x${image.height})`
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
if (spec.tags.length > 20) {
|
|
1200
|
+
throw new Error(
|
|
1201
|
+
`${spec.styleId}/${spec.assetId} resolves to ${spec.tags.length} tags, but PixelLab allows at most 20`
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1097
1205
|
async submit(spec, styleImages) {
|
|
1206
|
+
this.validate(spec, styleImages);
|
|
1098
1207
|
if (spec.generator === "pixflux") {
|
|
1099
1208
|
const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
1100
1209
|
const { png } = await this.client.createImagePixflux({
|
|
@@ -1280,6 +1389,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
1280
1389
|
await this.client.deleteObject(assetId);
|
|
1281
1390
|
}
|
|
1282
1391
|
};
|
|
1392
|
+
function requirePixelLabOption(name, value, allowed) {
|
|
1393
|
+
if (value != null && !allowed.includes(value)) {
|
|
1394
|
+
throw new Error(`PixelLab map ${name} must be one of: ${allowed.join(", ")}`);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1283
1397
|
function tilesInIndexOrder(urls) {
|
|
1284
1398
|
return Object.entries(urls).map(([key, url]) => ({ index: Number(key.replace(/^tile_/, "")), url })).filter((tile) => Number.isFinite(tile.index)).sort((a, b) => a.index - b.index);
|
|
1285
1399
|
}
|
|
@@ -1288,60 +1402,442 @@ function firstUrl(urls) {
|
|
|
1288
1402
|
return Object.values(urls).find((u) => typeof u === "string") ?? null;
|
|
1289
1403
|
}
|
|
1290
1404
|
|
|
1291
|
-
// src/
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1405
|
+
// src/media.ts
|
|
1406
|
+
var MediaType = {
|
|
1407
|
+
PNG: "image/png",
|
|
1408
|
+
GIF: "image/gif"
|
|
1409
|
+
};
|
|
1410
|
+
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
1411
|
+
function mediaExtension(mediaType) {
|
|
1412
|
+
return mediaType === MediaType.GIF ? ".gif" : ".png";
|
|
1413
|
+
}
|
|
1414
|
+
function mediaTypeFromExtension(file) {
|
|
1415
|
+
const lower = file.toLowerCase();
|
|
1416
|
+
if (lower.endsWith(".png")) return MediaType.PNG;
|
|
1417
|
+
if (lower.endsWith(".gif")) return MediaType.GIF;
|
|
1418
|
+
return null;
|
|
1419
|
+
}
|
|
1420
|
+
function detectMediaType(bytes) {
|
|
1421
|
+
if (bytes.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) return MediaType.PNG;
|
|
1422
|
+
const header = bytes.subarray(0, 6).toString("ascii");
|
|
1423
|
+
if (header === "GIF87a" || header === "GIF89a") return MediaType.GIF;
|
|
1424
|
+
return null;
|
|
1425
|
+
}
|
|
1426
|
+
function validateMedia(bytes, expected) {
|
|
1427
|
+
const actual = detectMediaType(bytes);
|
|
1428
|
+
if (!actual) throw new Error(`response was not a supported PNG or GIF (${bytes.length} bytes)`);
|
|
1429
|
+
if (expected && actual !== expected) {
|
|
1430
|
+
throw new Error(`response was ${actual}, expected ${expected}`);
|
|
1295
1431
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1432
|
+
if (actual === MediaType.PNG) {
|
|
1433
|
+
decodePng(bytes);
|
|
1434
|
+
} else {
|
|
1435
|
+
validateGif(bytes);
|
|
1436
|
+
}
|
|
1437
|
+
return actual;
|
|
1438
|
+
}
|
|
1439
|
+
function validateGif(bytes) {
|
|
1440
|
+
if (bytes.length < 14) throw new Error("invalid GIF: truncated logical screen descriptor");
|
|
1441
|
+
const width = bytes.readUInt16LE(6);
|
|
1442
|
+
const height = bytes.readUInt16LE(8);
|
|
1443
|
+
if (!width || !height) throw new Error("invalid GIF: zero-sized logical screen");
|
|
1444
|
+
const packed = bytes[10];
|
|
1445
|
+
let offset = 13;
|
|
1446
|
+
if (packed & 128) offset += 3 * 2 ** ((packed & 7) + 1);
|
|
1447
|
+
if (offset > bytes.length) throw new Error("invalid GIF: truncated global color table");
|
|
1448
|
+
let sawImage = false;
|
|
1449
|
+
while (offset < bytes.length) {
|
|
1450
|
+
const marker = bytes[offset];
|
|
1451
|
+
if (marker === 59) {
|
|
1452
|
+
if (!sawImage) throw new Error("invalid GIF: contains no image frame");
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (marker === 44) {
|
|
1456
|
+
if (offset + 10 > bytes.length) throw new Error("invalid GIF: truncated image descriptor");
|
|
1457
|
+
const imagePacked = bytes[offset + 9];
|
|
1458
|
+
offset += 10;
|
|
1459
|
+
if (imagePacked & 128) offset += 3 * 2 ** ((imagePacked & 7) + 1);
|
|
1460
|
+
if (offset >= bytes.length) throw new Error("invalid GIF: missing image data");
|
|
1461
|
+
offset++;
|
|
1462
|
+
offset = skipSubBlocks(bytes, offset);
|
|
1463
|
+
sawImage = true;
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
if (marker === 33) {
|
|
1467
|
+
if (offset + 2 > bytes.length) throw new Error("invalid GIF: truncated extension");
|
|
1468
|
+
offset = skipSubBlocks(bytes, offset + 2);
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
throw new Error(`invalid GIF: unexpected block marker 0x${marker.toString(16)}`);
|
|
1299
1472
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1473
|
+
throw new Error("invalid GIF: missing trailer");
|
|
1474
|
+
}
|
|
1475
|
+
function skipSubBlocks(bytes, start) {
|
|
1476
|
+
let offset = start;
|
|
1477
|
+
for (; ; ) {
|
|
1478
|
+
if (offset >= bytes.length) throw new Error("invalid GIF: truncated data blocks");
|
|
1479
|
+
const size = bytes[offset];
|
|
1480
|
+
offset++;
|
|
1481
|
+
if (size === 0) return offset;
|
|
1482
|
+
offset += size;
|
|
1483
|
+
if (offset > bytes.length) throw new Error("invalid GIF: truncated data block");
|
|
1302
1484
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1485
|
+
}
|
|
1486
|
+
function cacheFileName(hash, mediaType = MediaType.PNG) {
|
|
1487
|
+
return `${hash}${mediaExtension(mediaType)}`;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// src/providers/retrodiffusion.ts
|
|
1491
|
+
var DEFAULT_BASE_URL = "https://api.retrodiffusion.ai/v1";
|
|
1492
|
+
var RetroDiffusionClient = class {
|
|
1493
|
+
constructor(token, baseUrl = DEFAULT_BASE_URL, request = fetch) {
|
|
1494
|
+
this.token = token;
|
|
1495
|
+
this.baseUrl = baseUrl;
|
|
1496
|
+
this.request = request;
|
|
1497
|
+
}
|
|
1498
|
+
token;
|
|
1499
|
+
baseUrl;
|
|
1500
|
+
request;
|
|
1501
|
+
async submit(body) {
|
|
1502
|
+
const response = await this.call("/inferences", { method: "POST", body: JSON.stringify(body) });
|
|
1503
|
+
const taskId = response.task_id;
|
|
1504
|
+
if (typeof taskId !== "string" || !taskId) {
|
|
1505
|
+
throw new Error("Retro Diffusion did not return an async task id");
|
|
1506
|
+
}
|
|
1507
|
+
return taskId;
|
|
1508
|
+
}
|
|
1509
|
+
async quote(body) {
|
|
1510
|
+
const response = await this.call("/inferences", {
|
|
1511
|
+
method: "POST",
|
|
1512
|
+
body: JSON.stringify({ ...body, check_cost: true })
|
|
1513
|
+
});
|
|
1514
|
+
const amount = response.balance_cost;
|
|
1515
|
+
if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) {
|
|
1516
|
+
throw new Error("Retro Diffusion returned an invalid cost quote");
|
|
1517
|
+
}
|
|
1518
|
+
return amount;
|
|
1305
1519
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1520
|
+
async task(id) {
|
|
1521
|
+
return await this.call(`/inferences/tasks/${encodeURIComponent(id)}`);
|
|
1308
1522
|
}
|
|
1309
|
-
|
|
1523
|
+
async balance() {
|
|
1524
|
+
const response = await this.call("/inferences/credits");
|
|
1525
|
+
const balance = response.balance;
|
|
1526
|
+
if (typeof balance !== "number" || !Number.isFinite(balance)) {
|
|
1527
|
+
throw new Error("Retro Diffusion returned an invalid balance");
|
|
1528
|
+
}
|
|
1529
|
+
return balance;
|
|
1530
|
+
}
|
|
1531
|
+
async call(path19, init = {}) {
|
|
1532
|
+
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
1533
|
+
const response = await this.request(`${this.baseUrl}${path19}`, {
|
|
1534
|
+
...init,
|
|
1535
|
+
headers: {
|
|
1536
|
+
"Content-Type": "application/json",
|
|
1537
|
+
"X-RD-Token": this.token,
|
|
1538
|
+
...init.headers
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
const text = await response.text();
|
|
1542
|
+
let value = null;
|
|
1543
|
+
try {
|
|
1544
|
+
value = text ? JSON.parse(text) : null;
|
|
1545
|
+
} catch {
|
|
1546
|
+
value = text;
|
|
1547
|
+
}
|
|
1548
|
+
if (!response.ok) {
|
|
1549
|
+
const retry = response.headers.get("retry-after");
|
|
1550
|
+
const detail = retroError(value);
|
|
1551
|
+
throw new Error(
|
|
1552
|
+
`Retro Diffusion request failed (${response.status}): ${detail}` + (retry ? `; retry after ${retry}s` : "")
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
return value;
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
var RetroDiffusionProvider = class _RetroDiffusionProvider {
|
|
1559
|
+
constructor(client) {
|
|
1560
|
+
this.client = client;
|
|
1561
|
+
}
|
|
1562
|
+
client;
|
|
1563
|
+
id = "retrodiffusion";
|
|
1564
|
+
static fromEnv() {
|
|
1565
|
+
return new _RetroDiffusionProvider(new RetroDiffusionClient(process.env.RD_API_KEY));
|
|
1566
|
+
}
|
|
1567
|
+
static forOffline() {
|
|
1568
|
+
return new _RetroDiffusionProvider(new RetroDiffusionClient(void 0));
|
|
1569
|
+
}
|
|
1570
|
+
static forDownloads() {
|
|
1571
|
+
return _RetroDiffusionProvider.forOffline();
|
|
1572
|
+
}
|
|
1573
|
+
supports(generator) {
|
|
1574
|
+
return generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "animation";
|
|
1575
|
+
}
|
|
1576
|
+
estimate(spec) {
|
|
1577
|
+
const options = retroOptions(spec);
|
|
1578
|
+
const style = resolvedPromptStyle(spec, options);
|
|
1579
|
+
const count = options.numImages ?? 1;
|
|
1580
|
+
const pixels = spec.width * spec.height;
|
|
1581
|
+
let each;
|
|
1582
|
+
if (style.startsWith("rd_advanced_animation__")) {
|
|
1583
|
+
each = /__(?:custom_action|subtle_motion)$/.test(style) ? 0.25 : 0.14;
|
|
1584
|
+
} else if (style.startsWith("rd_animation__")) {
|
|
1585
|
+
each = /__(?:any_animation|8_dir_rotation)$/.test(style) ? 0.25 : 0.07;
|
|
1586
|
+
} else if (/^rd_tile__tileset(?:_advanced)?$/.test(style)) {
|
|
1587
|
+
each = 0.1;
|
|
1588
|
+
} else if (style.startsWith("rd_pro__")) {
|
|
1589
|
+
each = 0.18;
|
|
1590
|
+
} else if (style.startsWith("rd_fast__")) {
|
|
1591
|
+
each = Math.max(0.015, (pixels + 1e5) / 6e6);
|
|
1592
|
+
} else if (isLowResolutionStyle(style)) {
|
|
1593
|
+
each = Math.max(0.02, (pixels + 13700) / 6e5);
|
|
1594
|
+
} else {
|
|
1595
|
+
each = Math.max(0.025, (pixels + 5e4) / 2e6);
|
|
1596
|
+
}
|
|
1597
|
+
return { unit: "usd", amount: roundUsdEstimate(each * count), candidates: count };
|
|
1598
|
+
}
|
|
1599
|
+
validate(spec, styleImages) {
|
|
1600
|
+
const options = retroOptions(spec);
|
|
1601
|
+
const promptStyle = resolvedPromptStyle(spec, options);
|
|
1602
|
+
const count = options.numImages ?? 1;
|
|
1603
|
+
const isAnimation = /^(?:rd_animation__|rd_advanced_animation__)/.test(promptStyle);
|
|
1604
|
+
const isTile = promptStyle.startsWith("rd_tile__");
|
|
1605
|
+
if (!promptStyle || spec.generator === "animation" && !isAnimation || spec.generator === "tiles" && !isTile || spec.generator !== "animation" && spec.generator !== "tiles" && (isAnimation || isTile)) {
|
|
1606
|
+
throw new Error(
|
|
1607
|
+
`Retro Diffusion style "${promptStyle}" does not match generator "${spec.generator}"`
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
if (!Number.isInteger(count) || count < 1 || count > 16) {
|
|
1611
|
+
throw new Error("Retro Diffusion numImages must be a whole number from 1 to 16");
|
|
1612
|
+
}
|
|
1613
|
+
if (spec.width < 16 || spec.height < 16 || spec.width > 512 || spec.height > 512) {
|
|
1614
|
+
throw new Error("Retro Diffusion output dimensions must be between 16 and 512 pixels");
|
|
1615
|
+
}
|
|
1616
|
+
if (styleImages.length > 9) {
|
|
1617
|
+
throw new Error("Retro Diffusion accepts at most 9 reference images");
|
|
1618
|
+
}
|
|
1619
|
+
if (spec.generator === "animation") {
|
|
1620
|
+
if (count !== 1) throw new Error("Retro Diffusion animations currently require numImages: 1");
|
|
1621
|
+
validateAnimation(promptStyle, spec, styleImages, options);
|
|
1622
|
+
} else if (spec.generator === "tiles") {
|
|
1623
|
+
validateTile(promptStyle, spec, styleImages, count, options);
|
|
1624
|
+
} else if (styleImages.length && !/^(?:rd_pro__|user__)/.test(promptStyle)) {
|
|
1625
|
+
throw new Error(
|
|
1626
|
+
`Retro Diffusion style "${promptStyle}" does not accept reference_images; use an RD Pro or user style`
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
async submit(spec, styleImages) {
|
|
1631
|
+
this.validate(spec, styleImages);
|
|
1632
|
+
const options = retroOptions(spec);
|
|
1633
|
+
const promptStyle = resolvedPromptStyle(spec, options);
|
|
1634
|
+
const animation = spec.generator === "animation";
|
|
1635
|
+
const tiles = spec.generator === "tiles";
|
|
1636
|
+
const body = {
|
|
1637
|
+
prompt: spec.prompt,
|
|
1638
|
+
prompt_style: promptStyle,
|
|
1639
|
+
width: spec.width,
|
|
1640
|
+
height: spec.height,
|
|
1641
|
+
num_images: options.numImages ?? 1,
|
|
1642
|
+
...!animation && !tiles ? { remove_bg: options.removeBg ?? spec.noBackground } : {},
|
|
1643
|
+
...spec.seed != null ? { seed: spec.seed } : {},
|
|
1644
|
+
...animation || tiles ? styleImages[0] ? { input_image: styleImages[0].base64 } : {} : styleImages.length ? { reference_images: styleImages.map((image) => image.base64) } : {},
|
|
1645
|
+
...tiles && styleImages[1] ? { extra_input_image: styleImages[1].base64 } : {},
|
|
1646
|
+
...tiles && options.extraPrompt ? { extra_prompt: options.extraPrompt } : {},
|
|
1647
|
+
...animation && options.framesDuration ? { frames_duration: options.framesDuration } : {},
|
|
1648
|
+
...animation && options.returnSpritesheet ? { return_spritesheet: true } : {},
|
|
1649
|
+
...!animation && options.tileX != null ? { tile_x: options.tileX } : {},
|
|
1650
|
+
...!animation && options.tileY != null ? { tile_y: options.tileY } : {},
|
|
1651
|
+
...spec.palette.length ? { input_palette: paletteSwatch(spec.palette).toString("base64") } : {}
|
|
1652
|
+
};
|
|
1653
|
+
const quoted = await this.client.quote(body);
|
|
1654
|
+
const estimated = this.estimate(spec).amount;
|
|
1655
|
+
if (quoted > estimated + 1e-6) {
|
|
1656
|
+
throw new Error(
|
|
1657
|
+
`Retro Diffusion quoted $${quoted.toFixed(6)}, above the offline estimate $${estimated.toFixed(6)}; no paid request was sent`
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
return {
|
|
1661
|
+
jobId: await this.client.submit({ ...body, async: true, upload_outputs: true })
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
async poll(jobId, generator, context) {
|
|
1665
|
+
const task = await this.client.task(jobId);
|
|
1666
|
+
if (task.status === "pending" || task.status === "running") return { status: "processing" };
|
|
1667
|
+
if (task.status === "failed") return { status: "failed", error: retroError(task.error) };
|
|
1668
|
+
if (task.status !== "succeeded") {
|
|
1669
|
+
return { status: "failed", error: `Retro Diffusion returned unknown task status "${String(task.status)}"` };
|
|
1670
|
+
}
|
|
1671
|
+
const options = context?.spec ? retroOptions(context.spec) : {};
|
|
1672
|
+
const mediaType = generator === "animation" && !options.returnSpritesheet ? MediaType.GIF : MediaType.PNG;
|
|
1673
|
+
const sources = resultSources(task.result, mediaType);
|
|
1674
|
+
const urls = sources.map((source) => source.url);
|
|
1675
|
+
if (!urls.length) return { status: "failed", error: "Retro Diffusion task returned no images" };
|
|
1676
|
+
if (urls.length > 1) return { status: "review", candidateUrls: urls };
|
|
1677
|
+
return {
|
|
1678
|
+
status: "ready",
|
|
1679
|
+
objectId: `${jobId}#0`,
|
|
1680
|
+
sourceUrl: urls[0],
|
|
1681
|
+
sources,
|
|
1682
|
+
metadata: {
|
|
1683
|
+
balanceCost: task.result?.balance_cost ?? null,
|
|
1684
|
+
remainingBalance: task.result?.remaining_balance ?? null,
|
|
1685
|
+
mediaType,
|
|
1686
|
+
kind: generator === "animation" ? "animation" : generator === "tiles" ? "tileset" : "image",
|
|
1687
|
+
...context?.spec ? {
|
|
1688
|
+
promptStyle: resolvedPromptStyle(context.spec, options),
|
|
1689
|
+
width: context.spec.width,
|
|
1690
|
+
height: context.spec.height
|
|
1691
|
+
} : {}
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
async selectCandidate(jobId, index) {
|
|
1696
|
+
const task = await this.client.task(jobId);
|
|
1697
|
+
if (task.status !== "succeeded") throw new Error(`Retro Diffusion task ${jobId} is not ready`);
|
|
1698
|
+
const url = resultSources(task.result, MediaType.PNG)[index]?.url;
|
|
1699
|
+
if (!url) throw new Error(`Retro Diffusion task ${jobId} has no candidate at index ${index}`);
|
|
1700
|
+
return { objectId: `${jobId}#${index}`, sourceUrl: url };
|
|
1701
|
+
}
|
|
1702
|
+
async download(url) {
|
|
1703
|
+
const data = /^data:[^;]+;base64,(.+)$/.exec(url)?.[1];
|
|
1704
|
+
if (data) return Buffer.from(data, "base64");
|
|
1705
|
+
const response = await fetch(url);
|
|
1706
|
+
if (!response.ok) throw new Error(`Retro Diffusion download failed (${response.status})`);
|
|
1707
|
+
return Buffer.from(await response.arrayBuffer());
|
|
1708
|
+
}
|
|
1709
|
+
async balance() {
|
|
1710
|
+
return { unit: "usd", remaining: await this.client.balance() };
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
function retroOptions(spec) {
|
|
1714
|
+
return spec.providerOptions;
|
|
1715
|
+
}
|
|
1716
|
+
function resultSources(result, mediaType) {
|
|
1717
|
+
const hosted = (result?.output_urls ?? []).filter((url) => typeof url === "string" && url.length > 0).map((url) => ({ url, mediaType }));
|
|
1718
|
+
if (hosted.length) return hosted;
|
|
1719
|
+
return (result?.base64_images ?? []).filter((data) => typeof data === "string" && data.length > 0).map((data) => ({
|
|
1720
|
+
url: `data:${mediaType};base64,${data}`,
|
|
1721
|
+
mediaType
|
|
1722
|
+
}));
|
|
1310
1723
|
}
|
|
1311
|
-
function
|
|
1312
|
-
|
|
1313
|
-
const delta = before.remaining - after.remaining;
|
|
1314
|
-
return {
|
|
1315
|
-
unit: before.unit,
|
|
1316
|
-
before: before.remaining,
|
|
1317
|
-
after: after.remaining,
|
|
1318
|
-
spent: Math.max(0, delta),
|
|
1319
|
-
credited: Math.max(0, -delta)
|
|
1320
|
-
};
|
|
1724
|
+
function resolvedPromptStyle(spec, options) {
|
|
1725
|
+
return options.promptStyle ?? (spec.generator === "animation" ? "rd_animation__any_animation" : spec.generator === "tiles" ? "rd_tile__tileset" : "rd_plus__default");
|
|
1321
1726
|
}
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
`
|
|
1327
|
-
|
|
1328
|
-
|
|
1727
|
+
function validateAnimation(style, spec, styleImages, options) {
|
|
1728
|
+
if (spec.width !== spec.height) throw new Error("Retro Diffusion animations must be square");
|
|
1729
|
+
if (style.startsWith("rd_advanced_animation__")) {
|
|
1730
|
+
if (styleImages.length !== 1) {
|
|
1731
|
+
throw new Error(`Retro Diffusion advanced animation style "${style}" requires one input image`);
|
|
1732
|
+
}
|
|
1733
|
+
if (spec.width < 32 || spec.width > 256) {
|
|
1734
|
+
throw new Error("Retro Diffusion advanced animations require dimensions from 32 to 256 pixels");
|
|
1735
|
+
}
|
|
1736
|
+
} else if (styleImages.length > 1) {
|
|
1737
|
+
throw new Error("Retro Diffusion prompt animations accept at most one input image");
|
|
1738
|
+
}
|
|
1739
|
+
const exact = style.includes("four_angle_walking") ? 48 : style.endsWith("__small_sprites") ? 32 : style.endsWith("__any_animation") ? 64 : style.endsWith("__big_animation") ? 128 : style.endsWith("__8_dir_rotation") ? 80 : null;
|
|
1740
|
+
if (exact && spec.width !== exact) {
|
|
1741
|
+
throw new Error(`Retro Diffusion style "${style}" requires ${exact}x${exact} dimensions`);
|
|
1742
|
+
}
|
|
1743
|
+
if (style.endsWith("__vfx") && (spec.width < 24 || spec.width > 96)) {
|
|
1744
|
+
throw new Error("Retro Diffusion VFX animations require dimensions from 24 to 96 pixels");
|
|
1745
|
+
}
|
|
1746
|
+
if (options.framesDuration != null && ![4, 6, 8, 10, 12, 16].includes(options.framesDuration)) {
|
|
1747
|
+
throw new Error("Retro Diffusion framesDuration must be 4, 6, 8, 10, 12, or 16");
|
|
1329
1748
|
}
|
|
1330
|
-
};
|
|
1331
|
-
function requireList(provider) {
|
|
1332
|
-
if (!provider.list) throw new UnsupportedCapabilityError(provider.id, "listing remote assets");
|
|
1333
|
-
return provider.list.bind(provider);
|
|
1334
1749
|
}
|
|
1335
|
-
function
|
|
1336
|
-
if (
|
|
1337
|
-
|
|
1750
|
+
function validateTile(style, spec, styleImages, count, options) {
|
|
1751
|
+
if (spec.width !== spec.height) throw new Error("Retro Diffusion tiles must be square");
|
|
1752
|
+
const size = spec.width;
|
|
1753
|
+
if (/^rd_tile__tileset(?:_advanced)?$/.test(style)) {
|
|
1754
|
+
if (size < 16 || size > 32) throw new Error("Retro Diffusion tilesets require 16\u201332px tiles");
|
|
1755
|
+
if (count !== 1) throw new Error("Retro Diffusion tilesets require numImages: 1");
|
|
1756
|
+
} else if (style === "rd_tile__single_tile" && (size < 16 || size > 64)) {
|
|
1757
|
+
throw new Error("Retro Diffusion single tiles require dimensions from 16 to 64 pixels");
|
|
1758
|
+
} else if (style === "rd_tile__tile_variation") {
|
|
1759
|
+
if (size < 16 || size > 128) throw new Error("Retro Diffusion tile variations require 16\u2013128px tiles");
|
|
1760
|
+
if (styleImages.length !== 1) throw new Error("Retro Diffusion tile variations require one input image");
|
|
1761
|
+
} else if (style === "rd_tile__tile_object" && (size < 16 || size > 96)) {
|
|
1762
|
+
throw new Error("Retro Diffusion tile objects require dimensions from 16 to 96 pixels");
|
|
1763
|
+
} else if (style === "rd_tile__scene_object" && (size < 64 || size > 384)) {
|
|
1764
|
+
throw new Error("Retro Diffusion tile scene objects require dimensions from 64 to 384 pixels");
|
|
1765
|
+
}
|
|
1766
|
+
if (style === "rd_tile__tileset" && styleImages.length > 1) {
|
|
1767
|
+
throw new Error("Retro Diffusion basic tilesets accept at most one input image");
|
|
1768
|
+
}
|
|
1769
|
+
if (style === "rd_tile__tileset_advanced") {
|
|
1770
|
+
if (styleImages.length > 2) throw new Error("Retro Diffusion advanced tilesets accept at most two input images");
|
|
1771
|
+
if (!options.extraPrompt && styleImages.length < 2) {
|
|
1772
|
+
throw new Error("Retro Diffusion advanced tilesets require extraPrompt or a second input image");
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1338
1775
|
}
|
|
1339
|
-
function
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1776
|
+
function isLowResolutionStyle(style) {
|
|
1777
|
+
return /(?:^|__)(?:mc_|low_res|classic|skill_icon|topdown_item)/.test(style);
|
|
1778
|
+
}
|
|
1779
|
+
function roundUsdEstimate(value) {
|
|
1780
|
+
return Math.ceil((value - 1e-9) * 1e3) / 1e3;
|
|
1781
|
+
}
|
|
1782
|
+
function retroError(value) {
|
|
1783
|
+
if (typeof value === "string") return value || "unknown error";
|
|
1784
|
+
if (!value || typeof value !== "object") return "unknown error";
|
|
1785
|
+
const record = value;
|
|
1786
|
+
if (typeof record.message === "string") return record.message;
|
|
1787
|
+
if (typeof record.detail === "string") return record.detail;
|
|
1788
|
+
if (Array.isArray(record.detail)) {
|
|
1789
|
+
const details = record.detail.map((item) => {
|
|
1790
|
+
if (!item || typeof item !== "object") return String(item);
|
|
1791
|
+
const detail = item;
|
|
1792
|
+
return typeof detail.msg === "string" ? detail.msg : JSON.stringify(item);
|
|
1793
|
+
}).filter(Boolean);
|
|
1794
|
+
if (details.length) return details.join("; ");
|
|
1795
|
+
}
|
|
1796
|
+
if (record.detail && typeof record.detail === "object") {
|
|
1797
|
+
const message2 = record.detail.message;
|
|
1798
|
+
if (typeof message2 === "string") return message2;
|
|
1799
|
+
}
|
|
1800
|
+
return JSON.stringify(value);
|
|
1343
1801
|
}
|
|
1344
1802
|
|
|
1803
|
+
// src/providers/registry.ts
|
|
1804
|
+
var factories = /* @__PURE__ */ new Map();
|
|
1805
|
+
function registerProvider(factory) {
|
|
1806
|
+
const id = factory.id.trim();
|
|
1807
|
+
if (!id) throw new Error("Provider id cannot be empty");
|
|
1808
|
+
if (factories.has(id)) throw new Error(`Provider "${id}" is already registered`);
|
|
1809
|
+
factories.set(id, factory);
|
|
1810
|
+
}
|
|
1811
|
+
function providerFactory(id) {
|
|
1812
|
+
const factory = factories.get(id);
|
|
1813
|
+
if (!factory) {
|
|
1814
|
+
const available = [...factories.keys()].sort().join(", ") || "(none)";
|
|
1815
|
+
throw new Error(`Unknown provider "${id}". Available providers: ${available}`);
|
|
1816
|
+
}
|
|
1817
|
+
return factory;
|
|
1818
|
+
}
|
|
1819
|
+
function createProvider(id, mode2) {
|
|
1820
|
+
return providerFactory(id).create(mode2);
|
|
1821
|
+
}
|
|
1822
|
+
registerProvider({
|
|
1823
|
+
id: "pixellab",
|
|
1824
|
+
credentialEnv: "PIXELLAB_API_KEY",
|
|
1825
|
+
create(mode2) {
|
|
1826
|
+
if (mode2 === "online") return PixelLabProvider.fromEnv();
|
|
1827
|
+
if (mode2 === "downloads") return PixelLabProvider.forDownloads();
|
|
1828
|
+
return PixelLabProvider.forOffline();
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
registerProvider({
|
|
1832
|
+
id: "retrodiffusion",
|
|
1833
|
+
credentialEnv: "RD_API_KEY",
|
|
1834
|
+
create(mode2) {
|
|
1835
|
+
if (mode2 === "online") return RetroDiffusionProvider.fromEnv();
|
|
1836
|
+
if (mode2 === "downloads") return RetroDiffusionProvider.forDownloads();
|
|
1837
|
+
return RetroDiffusionProvider.forOffline();
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1345
1841
|
// src/manifest.ts
|
|
1346
1842
|
import { readFile as readFile2 } from "fs/promises";
|
|
1347
1843
|
import { existsSync as existsSync3 } from "fs";
|
|
@@ -1359,6 +1855,10 @@ async function sha256File(path19) {
|
|
|
1359
1855
|
function specHash(spec, styleImageHashes) {
|
|
1360
1856
|
return sha256(
|
|
1361
1857
|
JSON.stringify({
|
|
1858
|
+
// Preserve every existing PixelLab hash while making a provider switch
|
|
1859
|
+
// invalidate the spec. Older manifests implicitly mean pixellab.
|
|
1860
|
+
provider: spec.provider === "pixellab" ? void 0 : spec.provider,
|
|
1861
|
+
providerOptions: Object.keys(spec.providerOptions).length > 0 ? spec.providerOptions : void 0,
|
|
1362
1862
|
generator: spec.generator,
|
|
1363
1863
|
prompt: spec.prompt,
|
|
1364
1864
|
width: spec.width,
|
|
@@ -1372,7 +1872,7 @@ function specHash(spec, styleImageHashes) {
|
|
|
1372
1872
|
// `noBackground` only reaches the wire for pixflux; the tile fields are
|
|
1373
1873
|
// undefined for every other generator. `tileSize` is intentionally
|
|
1374
1874
|
// absent — width/height are derived from it, so it is already covered.
|
|
1375
|
-
noBackground: spec.generator === "pixflux" ? spec.noBackground : void 0,
|
|
1875
|
+
noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
|
|
1376
1876
|
tileType: spec.tileType,
|
|
1377
1877
|
tileView: spec.tileView,
|
|
1378
1878
|
tileFeature: spec.tileFeature,
|
|
@@ -1412,6 +1912,7 @@ ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
|
1412
1912
|
}
|
|
1413
1913
|
async function resolveSpecs(loaded, filter) {
|
|
1414
1914
|
const { manifest, root } = loaded;
|
|
1915
|
+
const activeProvider = filter?.provider ?? createProvider(manifest.provider, "offline");
|
|
1415
1916
|
const specs = [];
|
|
1416
1917
|
const styleIds = Object.keys(manifest.styles).filter(
|
|
1417
1918
|
(id) => !filter?.styles?.length || filter.styles.includes(id)
|
|
@@ -1437,10 +1938,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1437
1938
|
const buf = await readFile2(abs);
|
|
1438
1939
|
const metadata = imageMetadata(buf);
|
|
1439
1940
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
|
|
1440
|
-
if (metadata.width < 1 || metadata.height < 1
|
|
1441
|
-
throw new Error(
|
|
1442
|
-
`Style image exceeds the API's 256x256 limit: ${abs} (${metadata.width}x${metadata.height})`
|
|
1443
|
-
);
|
|
1941
|
+
if (metadata.width < 1 || metadata.height < 1) {
|
|
1942
|
+
throw new Error(`Style image has invalid dimensions: ${abs}`);
|
|
1444
1943
|
}
|
|
1445
1944
|
hit = { base64: buf.toString("base64"), hash: sha256(buf), ...metadata };
|
|
1446
1945
|
styleImageCache.set(abs, hit);
|
|
@@ -1460,8 +1959,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1460
1959
|
if (filter?.assets?.length && !filter.assets.includes(assetId)) continue;
|
|
1461
1960
|
if (asset.styles.length && !asset.styles.includes(styleId)) continue;
|
|
1462
1961
|
const generator = style.generator;
|
|
1463
|
-
if (
|
|
1464
|
-
throw new Error(`Provider "${
|
|
1962
|
+
if (!activeProvider.supports(generator)) {
|
|
1963
|
+
throw new Error(`Provider "${activeProvider.id}" does not support generator "${generator}"`);
|
|
1465
1964
|
}
|
|
1466
1965
|
let width;
|
|
1467
1966
|
let height;
|
|
@@ -1488,6 +1987,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1488
1987
|
const base = {
|
|
1489
1988
|
styleId,
|
|
1490
1989
|
assetId,
|
|
1990
|
+
provider: activeProvider.id,
|
|
1991
|
+
providerOptions: style.providerOptions[activeProvider.id] ?? {},
|
|
1491
1992
|
generator,
|
|
1492
1993
|
prompt,
|
|
1493
1994
|
width,
|
|
@@ -1519,11 +2020,6 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1519
2020
|
`style:${styleId}`
|
|
1520
2021
|
])
|
|
1521
2022
|
];
|
|
1522
|
-
if (tags.length > 20) {
|
|
1523
|
-
throw new Error(
|
|
1524
|
-
`${styleId}/${assetId} resolves to ${tags.length} tags, but PixelLab allows at most 20`
|
|
1525
|
-
);
|
|
1526
|
-
}
|
|
1527
2023
|
const resolved = {
|
|
1528
2024
|
...base,
|
|
1529
2025
|
root,
|
|
@@ -1532,12 +2028,15 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1532
2028
|
source: asset.source,
|
|
1533
2029
|
specHash: specHash(base, styleImageHashes)
|
|
1534
2030
|
};
|
|
1535
|
-
|
|
1536
|
-
const
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
2031
|
+
const resolvedImages = style.styleImages.map((image) => {
|
|
2032
|
+
const hit = styleImageCache.get(path3.resolve(root, image.path));
|
|
2033
|
+
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
2034
|
+
});
|
|
2035
|
+
activeProvider.validate?.(resolved, resolvedImages);
|
|
2036
|
+
const estimate = validateCostEstimate(activeProvider.id, activeProvider.estimate(resolved));
|
|
2037
|
+
resolved.cost = estimate.amount;
|
|
2038
|
+
resolved.costUnit = estimate.unit;
|
|
2039
|
+
resolved.candidates = estimate.candidates;
|
|
1541
2040
|
specs.push(resolved);
|
|
1542
2041
|
}
|
|
1543
2042
|
}
|
|
@@ -1561,10 +2060,8 @@ async function resolveStyleImages(loaded, styleId) {
|
|
|
1561
2060
|
const buf = await readFile2(file);
|
|
1562
2061
|
const metadata = imageMetadata(buf);
|
|
1563
2062
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
1564
|
-
if (metadata.width < 1 || metadata.height < 1
|
|
1565
|
-
throw new Error(
|
|
1566
|
-
`Style image exceeds the API's 256x256 limit: ${file} (${metadata.width}x${metadata.height})`
|
|
1567
|
-
);
|
|
2063
|
+
if (metadata.width < 1 || metadata.height < 1) {
|
|
2064
|
+
throw new Error(`Style image has invalid dimensions: ${file}`);
|
|
1568
2065
|
}
|
|
1569
2066
|
out.push({ base64: buf.toString("base64"), ...metadata });
|
|
1570
2067
|
}
|
|
@@ -1770,7 +2267,8 @@ async function acquireFileLock(file) {
|
|
|
1770
2267
|
function spendByUnit(lock) {
|
|
1771
2268
|
const totals = { generations: 0, usd: 0, free: 0 };
|
|
1772
2269
|
for (const entry of Object.values(lock.entries)) {
|
|
1773
|
-
|
|
2270
|
+
const unit = entry.costUnit ?? "generations";
|
|
2271
|
+
totals[unit] = (totals[unit] ?? 0) + (entry.cost ?? 0);
|
|
1774
2272
|
}
|
|
1775
2273
|
return totals;
|
|
1776
2274
|
}
|
|
@@ -1788,16 +2286,16 @@ function resolveOutputPath(recordedPath, manifestDir) {
|
|
|
1788
2286
|
if (path5.win32.isAbsolute(recordedPath)) return recordedPath;
|
|
1789
2287
|
return path5.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path5.sep));
|
|
1790
2288
|
}
|
|
1791
|
-
function expectedOutputPath(spec, role, index, total) {
|
|
1792
|
-
if (total === 1) return spec.outFile;
|
|
2289
|
+
function expectedOutputPath(spec, role, index, total, mediaType) {
|
|
1793
2290
|
const originalExt = path5.extname(spec.outFile);
|
|
1794
|
-
const ext = originalExt || ".png";
|
|
2291
|
+
const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
|
|
1795
2292
|
const stem = originalExt ? spec.outFile.slice(0, -originalExt.length) : spec.outFile;
|
|
2293
|
+
if (total === 1) return `${stem}${ext}`;
|
|
1796
2294
|
const safeRole = (role ?? fallbackOutputRole(index)).replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
1797
2295
|
return `${stem}-${safeRole}${ext}`;
|
|
1798
2296
|
}
|
|
1799
2297
|
function currentOutputPath(output, spec, index, total) {
|
|
1800
|
-
return expectedOutputPath(spec, output.role, index, total);
|
|
2298
|
+
return expectedOutputPath(spec, output.role, index, total, output.mediaType);
|
|
1801
2299
|
}
|
|
1802
2300
|
function currentEntryOutputPath(entry, spec, index) {
|
|
1803
2301
|
const output = entry.outputs[index];
|
|
@@ -2255,7 +2753,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
2255
2753
|
async function pruneInFlight() {
|
|
2256
2754
|
for (const [id, spec] of [...inFlight]) {
|
|
2257
2755
|
try {
|
|
2258
|
-
const state = await provider.poll(id, spec.generator, spec);
|
|
2756
|
+
const state = await provider.poll(id, spec.generator, { spec, tileFeature: spec.tileFeature });
|
|
2259
2757
|
if (state.status !== "processing") inFlight.delete(id);
|
|
2260
2758
|
lastSlotError = null;
|
|
2261
2759
|
} catch (err) {
|
|
@@ -2307,7 +2805,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
2307
2805
|
await saveLock(lockPath, lock);
|
|
2308
2806
|
lastSubmitAt = Date.now();
|
|
2309
2807
|
try {
|
|
2310
|
-
const refs =
|
|
2808
|
+
const refs = styleImages.get(spec.styleId) ?? [];
|
|
2311
2809
|
const { jobId } = await provider.submit(spec, refs);
|
|
2312
2810
|
upsert(lock, key, {
|
|
2313
2811
|
jobId,
|
|
@@ -2357,7 +2855,8 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
2357
2855
|
try {
|
|
2358
2856
|
const currentSpec = specByKey.get(key);
|
|
2359
2857
|
const state = await provider.poll(entry.jobId, entry.generator, {
|
|
2360
|
-
tileFeature: entry.tileFeature ?? currentSpec?.tileFeature
|
|
2858
|
+
tileFeature: entry.tileFeature ?? currentSpec?.tileFeature,
|
|
2859
|
+
spec: currentSpec
|
|
2361
2860
|
});
|
|
2362
2861
|
if (state.status === "review") {
|
|
2363
2862
|
upsert(lock, key, { status: "review", reviewObjectId: entry.jobId });
|
|
@@ -2395,7 +2894,6 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
2395
2894
|
import { existsSync as existsSync6 } from "fs";
|
|
2396
2895
|
import { mkdir as mkdir2, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
2397
2896
|
import path8 from "path";
|
|
2398
|
-
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
2399
2897
|
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
2400
2898
|
const log2 = opts.onProgress ?? (() => {
|
|
2401
2899
|
});
|
|
@@ -2427,7 +2925,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
2427
2925
|
result.skipped++;
|
|
2428
2926
|
continue;
|
|
2429
2927
|
}
|
|
2430
|
-
const sources = entry.sourceUrls?.length ? entry.sourceUrls : entry.sourceUrl ? [{ url: entry.sourceUrl }] : opts.repair && entry.outputs.length ? entry.outputs.map((o) => ({ url: "", role: o.role })) : [];
|
|
2928
|
+
const sources = entry.sourceUrls?.length ? entry.sourceUrls : entry.sourceUrl ? [{ url: entry.sourceUrl }] : opts.repair && entry.outputs.length ? entry.outputs.map((o) => ({ url: "", role: o.role, mediaType: o.mediaType })) : [];
|
|
2431
2929
|
if (!sources.length) {
|
|
2432
2930
|
upsert(lock, key, {
|
|
2433
2931
|
status: "download-failed",
|
|
@@ -2443,7 +2941,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
2443
2941
|
const recorded = entry.outputs.find(
|
|
2444
2942
|
(o) => source.role ? o.role === source.role : !o.role && sources.length === 1
|
|
2445
2943
|
);
|
|
2446
|
-
const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length);
|
|
2944
|
+
const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length, source.mediaType);
|
|
2447
2945
|
if (existsSync6(target)) {
|
|
2448
2946
|
if (!recorded) {
|
|
2449
2947
|
throw new Error(`refusing to overwrite untracked output ${target}`);
|
|
@@ -2451,11 +2949,19 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
2451
2949
|
if (await sha256File(target) !== recorded.sha256) {
|
|
2452
2950
|
throw new Error(`refusing to overwrite modified output ${target}`);
|
|
2453
2951
|
}
|
|
2454
|
-
if (cacheDir)
|
|
2952
|
+
if (cacheDir) {
|
|
2953
|
+
await cacheMedia(
|
|
2954
|
+
cacheDir,
|
|
2955
|
+
await readFile4(target),
|
|
2956
|
+
recorded.mediaType ?? MediaType.PNG,
|
|
2957
|
+
recorded.sha256
|
|
2958
|
+
);
|
|
2959
|
+
}
|
|
2455
2960
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
2456
2961
|
continue;
|
|
2457
2962
|
}
|
|
2458
|
-
|
|
2963
|
+
const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
|
|
2964
|
+
let buf = recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null;
|
|
2459
2965
|
if (buf) {
|
|
2460
2966
|
log2(` cached ${path8.relative(process.cwd(), target)}`);
|
|
2461
2967
|
} else {
|
|
@@ -2464,24 +2970,25 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
2464
2970
|
}
|
|
2465
2971
|
buf = await provider.download(source.url);
|
|
2466
2972
|
}
|
|
2467
|
-
|
|
2468
|
-
throw new Error(`response for ${source.role ?? "asset"} was not a PNG (${buf.length} bytes)`);
|
|
2469
|
-
}
|
|
2973
|
+
let mediaType;
|
|
2470
2974
|
try {
|
|
2471
|
-
|
|
2975
|
+
mediaType = validateMedia(buf, expectedMediaType);
|
|
2472
2976
|
} catch (err) {
|
|
2977
|
+
const label = expectedMediaType === MediaType.GIF ? "GIF" : "PNG";
|
|
2978
|
+
const mismatch = detectMediaType(buf) !== expectedMediaType;
|
|
2473
2979
|
throw new Error(
|
|
2474
|
-
`response for ${source.role ?? "asset"} was not a valid
|
|
2980
|
+
`response for ${source.role ?? "asset"} was not ${mismatch ? "a" : "a valid"} ${label}` + (mismatch ? ` (${buf.length} bytes)` : `: ${err instanceof Error ? err.message : String(err)}`)
|
|
2475
2981
|
);
|
|
2476
2982
|
}
|
|
2477
|
-
if (cacheDir) await
|
|
2983
|
+
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
2478
2984
|
await mkdir2(path8.dirname(target), { recursive: true });
|
|
2479
2985
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
2480
2986
|
await writeFile2(tmp, buf);
|
|
2481
2987
|
outputs.push({
|
|
2482
2988
|
path: portableOutputPath(target, spec.root),
|
|
2483
2989
|
sha256: sha256(buf),
|
|
2484
|
-
...source.role ? { role: source.role } : {}
|
|
2990
|
+
...source.role ? { role: source.role } : {},
|
|
2991
|
+
mediaType
|
|
2485
2992
|
});
|
|
2486
2993
|
upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
|
|
2487
2994
|
await saveLock(lockPath, lock);
|
|
@@ -2518,22 +3025,23 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
2518
3025
|
await saveLock(lockPath, lock);
|
|
2519
3026
|
return result;
|
|
2520
3027
|
}
|
|
2521
|
-
async function
|
|
2522
|
-
const file = path8.join(cacheDir,
|
|
3028
|
+
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
3029
|
+
const file = path8.join(cacheDir, cacheFileName(hash, mediaType));
|
|
2523
3030
|
if (!existsSync6(file)) return null;
|
|
2524
3031
|
try {
|
|
2525
3032
|
const buf = await readFile4(file);
|
|
2526
|
-
if (
|
|
2527
|
-
|
|
3033
|
+
if (sha256(buf) !== hash) return null;
|
|
3034
|
+
validateMedia(buf, mediaType);
|
|
2528
3035
|
return buf;
|
|
2529
3036
|
} catch {
|
|
2530
3037
|
return null;
|
|
2531
3038
|
}
|
|
2532
3039
|
}
|
|
2533
|
-
async function
|
|
3040
|
+
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
2534
3041
|
const hash = knownHash ?? sha256(buf);
|
|
2535
|
-
|
|
2536
|
-
|
|
3042
|
+
validateMedia(buf, mediaType);
|
|
3043
|
+
const file = path8.join(cacheDir, cacheFileName(hash, mediaType));
|
|
3044
|
+
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
2537
3045
|
await mkdir2(cacheDir, { recursive: true });
|
|
2538
3046
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
2539
3047
|
await writeFile2(tmp, buf);
|
|
@@ -2638,7 +3146,10 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
2638
3146
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
2639
3147
|
let hasCache = false;
|
|
2640
3148
|
for (const output of entry.outputs) {
|
|
2641
|
-
const cached = path9.join(
|
|
3149
|
+
const cached = path9.join(
|
|
3150
|
+
cacheDir,
|
|
3151
|
+
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
3152
|
+
);
|
|
2642
3153
|
if (existsSync7(cached) && await sha256File(cached) === output.sha256) {
|
|
2643
3154
|
hasCache = true;
|
|
2644
3155
|
break;
|
|
@@ -2668,11 +3179,14 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
2668
3179
|
add(
|
|
2669
3180
|
"provider",
|
|
2670
3181
|
"error",
|
|
2671
|
-
opts.apiKeyPresent === false ? "PIXELLAB_API_KEY is not configured
|
|
3182
|
+
opts.apiKeyPresent === false ? `${opts.credentialEnv ?? "PIXELLAB_API_KEY"} is not configured` : "provider is not configured"
|
|
2672
3183
|
);
|
|
3184
|
+
} else if (!opts.provider.balance) {
|
|
3185
|
+
add("provider", "ok", `${opts.provider.id} configured; balance reporting unavailable`);
|
|
2673
3186
|
} else {
|
|
3187
|
+
const balanceFn = opts.provider.balance.bind(opts.provider);
|
|
2674
3188
|
try {
|
|
2675
|
-
const balance = await
|
|
3189
|
+
const balance = await balanceFn();
|
|
2676
3190
|
add("provider", "ok", `${opts.provider.id} reachable; ${balance.remaining} ${balance.unit} remaining`);
|
|
2677
3191
|
} catch (err) {
|
|
2678
3192
|
add("provider", "error", `provider connectivity failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -3259,7 +3773,7 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
3259
3773
|
const entry = lock.entries[key];
|
|
3260
3774
|
if (!group || !entry?.reviewObjectId) continue;
|
|
3261
3775
|
if (!Number.isInteger(index) || index < 0 || index >= group.frameUrls.length) continue;
|
|
3262
|
-
const { objectId, sourceUrl } = await provider
|
|
3776
|
+
const { objectId, sourceUrl } = await requireSelectCandidate(provider)(
|
|
3263
3777
|
entry.reviewObjectId,
|
|
3264
3778
|
index,
|
|
3265
3779
|
`asset:${entry.assetId}`,
|
|
@@ -3349,6 +3863,7 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
3349
3863
|
}
|
|
3350
3864
|
return {
|
|
3351
3865
|
name,
|
|
3866
|
+
provider: "pixellab",
|
|
3352
3867
|
styles: {
|
|
3353
3868
|
[styleId]: {
|
|
3354
3869
|
generator,
|
|
@@ -3361,7 +3876,8 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
3361
3876
|
styleImages: [],
|
|
3362
3877
|
palette: [],
|
|
3363
3878
|
outDir,
|
|
3364
|
-
tags: [name]
|
|
3879
|
+
tags: [name],
|
|
3880
|
+
providerOptions: {}
|
|
3365
3881
|
}
|
|
3366
3882
|
},
|
|
3367
3883
|
assets
|
|
@@ -3727,7 +4243,6 @@ function emptyStateCounts() {
|
|
|
3727
4243
|
}
|
|
3728
4244
|
async function workspaceStatus(ws, dir) {
|
|
3729
4245
|
const diagnostics = validateWorkspace(ws, dir);
|
|
3730
|
-
const provider = PixelLabProvider.forOffline();
|
|
3731
4246
|
const projects = [];
|
|
3732
4247
|
const totalsByState = emptyStateCounts();
|
|
3733
4248
|
const totalsSpend = { generations: 0, usd: 0, free: 0 };
|
|
@@ -3741,6 +4256,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
3741
4256
|
lock: lockPath
|
|
3742
4257
|
};
|
|
3743
4258
|
try {
|
|
4259
|
+
const provider = createProvider(project.provider, "offline");
|
|
3744
4260
|
const loaded = await loadManifest(manifestPath);
|
|
3745
4261
|
const specs = await resolveSpecs(loaded, { provider });
|
|
3746
4262
|
const lock = await loadLock(lockPath);
|
|
@@ -3752,7 +4268,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
3752
4268
|
totalsByState[state] += byState[state];
|
|
3753
4269
|
}
|
|
3754
4270
|
for (const unit of Object.keys(spend)) {
|
|
3755
|
-
totalsSpend[unit]
|
|
4271
|
+
totalsSpend[unit] = (totalsSpend[unit] ?? 0) + (spend[unit] ?? 0);
|
|
3756
4272
|
}
|
|
3757
4273
|
projects.push({
|
|
3758
4274
|
...base,
|
|
@@ -3945,7 +4461,6 @@ function hex(c) {
|
|
|
3945
4461
|
import { existsSync as existsSync14 } from "fs";
|
|
3946
4462
|
import { readFile as readFile11, readdir as readdir2, rm as rm5 } from "fs/promises";
|
|
3947
4463
|
import path15 from "path";
|
|
3948
|
-
var PNG_SIGNATURE2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
3949
4464
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
3950
4465
|
if (options.prune && !existsSync14(lockPath)) {
|
|
3951
4466
|
throw new Error(`Refusing to prune without an existing lockfile at ${path15.resolve(lockPath)}`);
|
|
@@ -4014,7 +4529,8 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
4014
4529
|
}
|
|
4015
4530
|
report.files++;
|
|
4016
4531
|
const file = path15.join(contentDir, entry.name);
|
|
4017
|
-
const
|
|
4532
|
+
const mediaType = mediaTypeFromExtension(entry.name);
|
|
4533
|
+
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
4018
4534
|
let bytes;
|
|
4019
4535
|
try {
|
|
4020
4536
|
bytes = await readFile11(file);
|
|
@@ -4027,11 +4543,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
4027
4543
|
continue;
|
|
4028
4544
|
}
|
|
4029
4545
|
if (!isSha256Hash(expected)) {
|
|
4030
|
-
report.invalid.push({ name: entry.name, reason: "filename is not <sha256>.png" });
|
|
4031
|
-
continue;
|
|
4032
|
-
}
|
|
4033
|
-
if (!bytes.subarray(0, 8).equals(PNG_SIGNATURE2)) {
|
|
4034
|
-
report.invalid.push({ name: entry.name, reason: "not a PNG" });
|
|
4546
|
+
report.invalid.push({ name: entry.name, reason: "filename is not <sha256>.png or <sha256>.gif" });
|
|
4035
4547
|
continue;
|
|
4036
4548
|
}
|
|
4037
4549
|
if (sha256(bytes) !== expected) {
|
|
@@ -4039,11 +4551,11 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
4039
4551
|
continue;
|
|
4040
4552
|
}
|
|
4041
4553
|
try {
|
|
4042
|
-
|
|
4554
|
+
validateMedia(bytes, mediaType);
|
|
4043
4555
|
} catch (err) {
|
|
4044
4556
|
report.invalid.push({
|
|
4045
4557
|
name: entry.name,
|
|
4046
|
-
reason: `invalid PNG: ${err instanceof Error ? err.message : String(err)}`
|
|
4558
|
+
reason: `invalid ${mediaType === "image/gif" ? "GIF" : "PNG"}: ${err instanceof Error ? err.message : String(err)}`
|
|
4047
4559
|
});
|
|
4048
4560
|
continue;
|
|
4049
4561
|
}
|
|
@@ -4499,7 +5011,7 @@ refresh();
|
|
|
4499
5011
|
}
|
|
4500
5012
|
|
|
4501
5013
|
// src/pick/salvage-server.ts
|
|
4502
|
-
var
|
|
5014
|
+
var PNG_SIGNATURE2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
4503
5015
|
async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
4504
5016
|
const log2 = opts.onProgress ?? (() => {
|
|
4505
5017
|
});
|
|
@@ -4531,7 +5043,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
4531
5043
|
if (decision.action === "import") {
|
|
4532
5044
|
try {
|
|
4533
5045
|
const buf = await provider.download(orphan.previewUrl);
|
|
4534
|
-
if (!buf.subarray(0, 8).equals(
|
|
5046
|
+
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
4535
5047
|
decodePng(buf);
|
|
4536
5048
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
4537
5049
|
const rel = path16.join("_salvaged", `${assetId}.png`);
|
|
@@ -5234,7 +5746,7 @@ function parseArgs(argv) {
|
|
|
5234
5746
|
account: get("--account")
|
|
5235
5747
|
};
|
|
5236
5748
|
}
|
|
5237
|
-
var HELP = `pixelkiln \u2014 manifest-driven pixel art generation
|
|
5749
|
+
var HELP = `pixelkiln \u2014 manifest-driven pixel art generation
|
|
5238
5750
|
|
|
5239
5751
|
pixelkiln <command> [options]
|
|
5240
5752
|
|
|
@@ -5383,8 +5895,9 @@ async function main() {
|
|
|
5383
5895
|
if (args.command === "balance") {
|
|
5384
5896
|
loadEnvFiles(path18.dirname(path18.resolve(args.manifest)));
|
|
5385
5897
|
loadEnvFiles(process.cwd());
|
|
5386
|
-
const
|
|
5387
|
-
const
|
|
5898
|
+
const loaded2 = await loadManifest(args.manifest);
|
|
5899
|
+
const p = createProvider(loaded2.manifest.provider, "online");
|
|
5900
|
+
const b = await requireBalance(p)();
|
|
5388
5901
|
log(` provider: ${p.id}`);
|
|
5389
5902
|
log(` plan: ${b.plan ?? "n/a"}`);
|
|
5390
5903
|
log(` remaining: ${formatCost(b.unit, b.remaining)}${b.total ? ` of ${b.total}` : ""}`);
|
|
@@ -5506,7 +6019,7 @@ async function main() {
|
|
|
5506
6019
|
id,
|
|
5507
6020
|
manifest: toPortablePath(dir, manifestPath),
|
|
5508
6021
|
lock: toPortablePath(dir, lockPath),
|
|
5509
|
-
provider: args.provider ??
|
|
6022
|
+
provider: args.provider ?? loadedTarget.manifest.provider,
|
|
5510
6023
|
...args.account ? { account: args.account } : {}
|
|
5511
6024
|
};
|
|
5512
6025
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
@@ -5576,15 +6089,15 @@ async function main() {
|
|
|
5576
6089
|
${p.id} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
5577
6090
|
log(` ${p.entries} lock entries`);
|
|
5578
6091
|
for (const [state, n] of Object.entries(p.byState)) if (n) log(` ${state.padEnd(12)} ${n}`);
|
|
5579
|
-
for (const unit of
|
|
5580
|
-
if (
|
|
6092
|
+
for (const [unit, amount] of Object.entries(p.spendByUnit).sort()) {
|
|
6093
|
+
if (amount) log(` spend: ${formatCost(unit, amount)}`);
|
|
5581
6094
|
}
|
|
5582
6095
|
}
|
|
5583
6096
|
log(`
|
|
5584
6097
|
totals:`);
|
|
5585
6098
|
for (const [state, n] of Object.entries(report.totals.byState)) if (n) log(` ${state.padEnd(12)} ${n}`);
|
|
5586
|
-
for (const unit of
|
|
5587
|
-
if (
|
|
6099
|
+
for (const [unit, amount] of Object.entries(report.totals.spendByUnit).sort()) {
|
|
6100
|
+
if (amount) log(` spend: ${formatCost(unit, amount)}`);
|
|
5588
6101
|
}
|
|
5589
6102
|
log(` claims: ${report.totals.claims}`);
|
|
5590
6103
|
for (const d of report.diagnostics) log(` ${d.level === "error" ? "ERROR" : "WARN "} ${d.id.padEnd(18)} ${d.message}`);
|
|
@@ -5618,7 +6131,7 @@ async function main() {
|
|
|
5618
6131
|
const envFiles = [...loadEnvFiles(manifestDir)];
|
|
5619
6132
|
if (path18.resolve(process.cwd()) !== manifestDir) envFiles.push(...loadEnvFiles(process.cwd()));
|
|
5620
6133
|
const loaded = await loadManifest(args.manifest);
|
|
5621
|
-
const estimator =
|
|
6134
|
+
const estimator = createProvider(loaded.manifest.provider, "offline");
|
|
5622
6135
|
const specs = await resolveSpecs(loaded, {
|
|
5623
6136
|
styles: args.styles,
|
|
5624
6137
|
assets: args.assets,
|
|
@@ -5637,9 +6150,9 @@ async function main() {
|
|
|
5637
6150
|
for (const [s, n] of Object.entries(byStatus).sort()) log(` ${s.padEnd(12)} ${n}`);
|
|
5638
6151
|
const spend = spendByUnit(lock);
|
|
5639
6152
|
let reported = false;
|
|
5640
|
-
for (const unit of
|
|
5641
|
-
if (
|
|
5642
|
-
log(` recorded successful submissions: ${formatCost(unit,
|
|
6153
|
+
for (const [unit, amount] of Object.entries(spend).sort()) {
|
|
6154
|
+
if (amount) {
|
|
6155
|
+
log(` recorded successful submissions: ${formatCost(unit, amount)}`);
|
|
5643
6156
|
reported = true;
|
|
5644
6157
|
}
|
|
5645
6158
|
}
|
|
@@ -5648,12 +6161,14 @@ async function main() {
|
|
|
5648
6161
|
}
|
|
5649
6162
|
const plan = await buildPlan(specs, lock, { force: args.force });
|
|
5650
6163
|
if (args.command === "doctor") {
|
|
5651
|
-
const
|
|
5652
|
-
const
|
|
6164
|
+
const factory = providerFactory(loaded.manifest.provider);
|
|
6165
|
+
const apiKeyPresent = !factory.credentialEnv || Boolean(process.env[factory.credentialEnv]);
|
|
6166
|
+
const provider2 = !args.dryRun && apiKeyPresent ? createProvider(loaded.manifest.provider, "online") : void 0;
|
|
5653
6167
|
const report = await doctor(loaded, specs, lock, args.lock, {
|
|
5654
6168
|
provider: provider2,
|
|
5655
6169
|
offline: args.dryRun,
|
|
5656
|
-
apiKeyPresent
|
|
6170
|
+
apiKeyPresent,
|
|
6171
|
+
credentialEnv: factory.credentialEnv
|
|
5657
6172
|
});
|
|
5658
6173
|
if (args.json) {
|
|
5659
6174
|
log(JSON.stringify(report, null, 2));
|
|
@@ -5953,7 +6468,7 @@ async function main() {
|
|
|
5953
6468
|
}
|
|
5954
6469
|
return;
|
|
5955
6470
|
}
|
|
5956
|
-
const provider = args.command === "restore" || args.command === "fetch" && !args.tag ?
|
|
6471
|
+
const provider = args.command === "restore" || args.command === "fetch" && !args.tag ? createProvider(loaded.manifest.provider, "downloads") : createProvider(loaded.manifest.provider, "online");
|
|
5957
6472
|
if (args.command === "adopt") {
|
|
5958
6473
|
log(`
|
|
5959
6474
|
Reconciling account objects against files already on disk\u2026`);
|
|
@@ -6196,7 +6711,7 @@ async function main() {
|
|
|
6196
6711
|
return;
|
|
6197
6712
|
}
|
|
6198
6713
|
log(`
|
|
6199
|
-
This permanently deletes them from your
|
|
6714
|
+
This permanently deletes them from your ${provider.id} account.`);
|
|
6200
6715
|
log(` Any local files already downloaded are untouched, but the objects`);
|
|
6201
6716
|
log(` and their URLs are gone and cannot be re-downloaded.`);
|
|
6202
6717
|
if (!await confirm(` Delete ${doomed.length} object(s)?`, args.yes)) {
|
|
@@ -6231,21 +6746,26 @@ async function main() {
|
|
|
6231
6746
|
return;
|
|
6232
6747
|
}
|
|
6233
6748
|
if (plan.actionable.length) {
|
|
6234
|
-
const balance = await provider.balance();
|
|
6235
|
-
|
|
6749
|
+
const balance = provider.balance ? await provider.balance() : null;
|
|
6750
|
+
if (balance) {
|
|
6751
|
+
log(`
|
|
6236
6752
|
balance: ${formatCost(balance.unit, balance.remaining)} remaining (${provider.id})`);
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6753
|
+
if (balance.unit !== plan.costUnit) {
|
|
6754
|
+
throw new Error(
|
|
6755
|
+
`Provider estimate unit ${plan.costUnit} does not match balance unit ${balance.unit}.`
|
|
6756
|
+
);
|
|
6757
|
+
}
|
|
6758
|
+
if (balance.unit !== "free" && plan.cost > balance.remaining) {
|
|
6759
|
+
throw new Error(
|
|
6760
|
+
`This run needs ${formatCost(balance.unit, plan.cost)} but only ${formatCost(balance.unit, balance.remaining)} remain.`
|
|
6761
|
+
);
|
|
6762
|
+
}
|
|
6763
|
+
} else {
|
|
6764
|
+
log(`
|
|
6765
|
+
${provider.id} does not expose an account balance; enforcing the explicit run budget`);
|
|
6246
6766
|
}
|
|
6247
6767
|
const ok = await confirm(
|
|
6248
|
-
` Spend ${formatCost(
|
|
6768
|
+
` Spend ${formatCost(plan.costUnit, plan.cost)} on ${plan.actionable.length} asset(s)?`,
|
|
6249
6769
|
args.yes
|
|
6250
6770
|
);
|
|
6251
6771
|
if (!ok) {
|
|
@@ -6263,7 +6783,8 @@ async function main() {
|
|
|
6263
6783
|
submitted ${res.submitted}, failed ${res.failed}, estimated ${formatCost(res.unit, res.spent)}`
|
|
6264
6784
|
);
|
|
6265
6785
|
try {
|
|
6266
|
-
const after = await provider.balance();
|
|
6786
|
+
const after = balance && provider.balance ? await provider.balance() : null;
|
|
6787
|
+
if (!balance || !after) throw new Error("balance reporting is unsupported");
|
|
6267
6788
|
const measured = measureBalanceChange(balance, after);
|
|
6268
6789
|
if (measured) {
|
|
6269
6790
|
const movement = measured.credited ? `${formatCost(measured.unit, measured.credited)} credited` : `${formatCost(measured.unit, measured.spent)} consumed`;
|