pixelkiln 0.3.0 → 0.4.1
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 +4 -2
- package/schema/manifest.schema.json +15 -1
- package/skills/pixelkiln/SKILL.md +15 -8
package/dist/index.cjs
CHANGED
|
@@ -40,9 +40,11 @@ __export(index_exports, {
|
|
|
40
40
|
MAX_DOWNLOAD_BYTES: () => MAX_DOWNLOAD_BYTES,
|
|
41
41
|
MAX_RETRIES: () => MAX_RETRIES,
|
|
42
42
|
ManifestSchema: () => ManifestSchema,
|
|
43
|
+
MediaType: () => MediaType,
|
|
43
44
|
PixelLabClient: () => PixelLabClient,
|
|
44
45
|
PixelLabError: () => PixelLabError,
|
|
45
46
|
PixelLabProvider: () => PixelLabProvider,
|
|
47
|
+
RetroDiffusionProvider: () => RetroDiffusionProvider,
|
|
46
48
|
StyleSchema: () => StyleSchema,
|
|
47
49
|
UnsupportedCapabilityError: () => UnsupportedCapabilityError,
|
|
48
50
|
WorkspaceProjectSchema: () => WorkspaceProjectSchema,
|
|
@@ -50,16 +52,20 @@ __export(index_exports, {
|
|
|
50
52
|
adopt: () => adopt,
|
|
51
53
|
applyTags: () => applyTags,
|
|
52
54
|
auditStyle: () => auditStyle,
|
|
55
|
+
availableProviders: () => availableProviders,
|
|
53
56
|
backoffMs: () => backoffMs,
|
|
54
57
|
buildManifest: () => buildManifest,
|
|
55
58
|
buildPlan: () => buildPlan,
|
|
59
|
+
cacheFileName: () => cacheFileName,
|
|
56
60
|
candidateCount: () => candidateCount,
|
|
57
61
|
clientFromEnv: () => clientFromEnv,
|
|
58
62
|
colorDistance: () => colorDistance,
|
|
59
63
|
countNumberedDescriptions: () => countNumberedDescriptions,
|
|
60
64
|
createArtifactBundleManifest: () => createArtifactBundleManifest,
|
|
65
|
+
createProvider: () => createProvider,
|
|
61
66
|
currentEntryOutputPath: () => currentEntryOutputPath,
|
|
62
67
|
currentOutputPath: () => currentOutputPath,
|
|
68
|
+
detectMediaType: () => detectMediaType,
|
|
63
69
|
doctor: () => doctor,
|
|
64
70
|
evaluateAudit: () => evaluateAudit,
|
|
65
71
|
expectedOutputPath: () => expectedOutputPath,
|
|
@@ -82,6 +88,8 @@ __export(index_exports, {
|
|
|
82
88
|
lockKey: () => lockKey,
|
|
83
89
|
matchOrphanStyle: () => matchOrphanStyle,
|
|
84
90
|
measureBalanceChange: () => measureBalanceChange,
|
|
91
|
+
mediaExtension: () => mediaExtension,
|
|
92
|
+
mediaTypeFromExtension: () => mediaTypeFromExtension,
|
|
85
93
|
mergePalettes: () => mergePalettes,
|
|
86
94
|
mountSprites: () => mountSprites,
|
|
87
95
|
mountStyle: () => mountStyle,
|
|
@@ -98,12 +106,16 @@ __export(index_exports, {
|
|
|
98
106
|
poll: () => poll,
|
|
99
107
|
portableOutputPath: () => portableOutputPath,
|
|
100
108
|
primaryOutput: () => primaryOutput,
|
|
109
|
+
providerFactory: () => providerFactory,
|
|
101
110
|
pushTags: () => pushTags,
|
|
111
|
+
registerProvider: () => registerProvider,
|
|
102
112
|
remove: () => remove,
|
|
103
113
|
renderSalvageSheet: () => renderSalvageSheet,
|
|
104
114
|
renderSheet: () => renderSheet,
|
|
115
|
+
requireBalance: () => requireBalance,
|
|
105
116
|
requireDelete: () => requireDelete,
|
|
106
117
|
requireList: () => requireList,
|
|
118
|
+
requireSelectCandidate: () => requireSelectCandidate,
|
|
107
119
|
resolveEntryOutputs: () => resolveEntryOutputs,
|
|
108
120
|
resolveOutputPath: () => resolveOutputPath,
|
|
109
121
|
resolvePackInputs: () => resolvePackInputs,
|
|
@@ -137,6 +149,7 @@ __export(index_exports, {
|
|
|
137
149
|
totalSpend: () => totalSpend,
|
|
138
150
|
upsert: () => upsert,
|
|
139
151
|
validateCostEstimate: () => validateCostEstimate,
|
|
152
|
+
validateMedia: () => validateMedia,
|
|
140
153
|
validateWorkspace: () => validateWorkspace,
|
|
141
154
|
verifyArtifactBundle: () => verifyArtifactBundle,
|
|
142
155
|
withArtifactManifest: () => withArtifactManifest,
|
|
@@ -149,7 +162,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
149
162
|
|
|
150
163
|
// src/types.ts
|
|
151
164
|
var import_zod = require("zod");
|
|
152
|
-
var
|
|
165
|
+
var MediaTypeSchema = import_zod.z.enum(["image/png", "image/gif"]);
|
|
166
|
+
var GeneratorSchema = import_zod.z.enum(["1dir", "map", "pixflux", "tiles", "animation"]);
|
|
153
167
|
function tileVariationCount(descriptions) {
|
|
154
168
|
return Math.max(1, descriptions) * 4;
|
|
155
169
|
}
|
|
@@ -181,7 +195,7 @@ function tilesCost(tileSize, variations) {
|
|
|
181
195
|
return 40;
|
|
182
196
|
}
|
|
183
197
|
var StyleImageSchema = import_zod.z.object({
|
|
184
|
-
/** Path to a PNG/JPEG, relative to the manifest
|
|
198
|
+
/** Path to a PNG/JPEG, relative to the manifest; the active provider validates limits. */
|
|
185
199
|
path: import_zod.z.string()
|
|
186
200
|
});
|
|
187
201
|
var StyleSchema = import_zod.z.object({
|
|
@@ -292,7 +306,9 @@ var StyleSchema = import_zod.z.object({
|
|
|
292
306
|
out: import_zod.z.string()
|
|
293
307
|
}).strict().optional(),
|
|
294
308
|
/** Tags applied to every object generated in this style, for server-side filtering. */
|
|
295
|
-
tags: import_zod.z.array(import_zod.z.string()).default([])
|
|
309
|
+
tags: import_zod.z.array(import_zod.z.string()).default([]),
|
|
310
|
+
/** Adapter-owned settings, keyed by provider id. */
|
|
311
|
+
providerOptions: import_zod.z.record(import_zod.z.record(import_zod.z.unknown())).default({})
|
|
296
312
|
}).strict().refine((s) => !(s.tileFeature && s.styleImages.length), {
|
|
297
313
|
message: "tileFeature and styleImages cannot be combined \u2014 a connectable set derives its own tile geometry, so remove one or the other",
|
|
298
314
|
path: ["tileFeature"]
|
|
@@ -307,7 +323,7 @@ var AssetSchema = import_zod.z.object({
|
|
|
307
323
|
height: import_zod.z.number().int().min(16).max(400).optional(),
|
|
308
324
|
/** Overrides the style default. `1dir` generator only. */
|
|
309
325
|
size: import_zod.z.number().int().min(32).max(256).optional(),
|
|
310
|
-
/** Explicit output path relative to outDir.
|
|
326
|
+
/** Explicit output path relative to outDir. Media-aware providers may replace its extension. */
|
|
311
327
|
file: import_zod.z.string().optional(),
|
|
312
328
|
/**
|
|
313
329
|
* Grid cell this asset owns in a mounted style, as [column, row].
|
|
@@ -359,6 +375,8 @@ var AssetSchema = import_zod.z.object({
|
|
|
359
375
|
var ManifestSchema = import_zod.z.object({
|
|
360
376
|
$schema: import_zod.z.string().optional(),
|
|
361
377
|
name: import_zod.z.string(),
|
|
378
|
+
/** Generation backend. Existing manifests remain PixelLab by default. */
|
|
379
|
+
provider: import_zod.z.string().min(1).default("pixellab"),
|
|
362
380
|
styles: import_zod.z.record(StyleSchema),
|
|
363
381
|
assets: import_zod.z.record(AssetSchema)
|
|
364
382
|
}).strict();
|
|
@@ -396,7 +414,8 @@ var LockEntrySchema = import_zod.z.object({
|
|
|
396
414
|
sourceUrls: import_zod.z.array(
|
|
397
415
|
import_zod.z.object({
|
|
398
416
|
url: import_zod.z.string(),
|
|
399
|
-
role: import_zod.z.string().optional()
|
|
417
|
+
role: import_zod.z.string().optional(),
|
|
418
|
+
mediaType: MediaTypeSchema.optional()
|
|
400
419
|
})
|
|
401
420
|
).default([]),
|
|
402
421
|
/**
|
|
@@ -411,7 +430,8 @@ var LockEntrySchema = import_zod.z.object({
|
|
|
411
430
|
import_zod.z.object({
|
|
412
431
|
path: import_zod.z.string(),
|
|
413
432
|
sha256: import_zod.z.string(),
|
|
414
|
-
role: import_zod.z.string().optional()
|
|
433
|
+
role: import_zod.z.string().optional(),
|
|
434
|
+
mediaType: MediaTypeSchema.optional()
|
|
415
435
|
})
|
|
416
436
|
).default([]),
|
|
417
437
|
/**
|
|
@@ -425,7 +445,7 @@ var LockEntrySchema = import_zod.z.object({
|
|
|
425
445
|
/** Successful-submission estimate in `costUnit`; may be fractional USD. */
|
|
426
446
|
cost: import_zod.z.number().finite().nonnegative().default(0),
|
|
427
447
|
/** Unit for `cost`. Defaults preserve pre-unit PixelLab lockfiles. */
|
|
428
|
-
costUnit: import_zod.z.
|
|
448
|
+
costUnit: import_zod.z.string().min(1).default("generations"),
|
|
429
449
|
/** Which provider produced this. Absent on entries written before providers. */
|
|
430
450
|
provider: import_zod.z.string().default("pixellab")
|
|
431
451
|
});
|
|
@@ -799,7 +819,7 @@ function validateCostEstimate(providerId, value) {
|
|
|
799
819
|
throw new Error(`Provider "${providerId}" returned an invalid cost estimate`);
|
|
800
820
|
}
|
|
801
821
|
const estimate = value;
|
|
802
|
-
if (
|
|
822
|
+
if (typeof estimate.unit !== "string" || !estimate.unit.trim()) {
|
|
803
823
|
throw new Error(`Provider "${providerId}" returned an invalid cost unit`);
|
|
804
824
|
}
|
|
805
825
|
if (!Number.isFinite(estimate.amount) || estimate.amount < 0) {
|
|
@@ -841,18 +861,23 @@ function requireDelete(provider) {
|
|
|
841
861
|
if (!provider.delete) throw new UnsupportedCapabilityError(provider.id, "deleting remote assets");
|
|
842
862
|
return provider.delete.bind(provider);
|
|
843
863
|
}
|
|
864
|
+
function requireSelectCandidate(provider) {
|
|
865
|
+
if (!provider.selectCandidate) {
|
|
866
|
+
throw new UnsupportedCapabilityError(provider.id, "candidate selection");
|
|
867
|
+
}
|
|
868
|
+
return provider.selectCandidate.bind(provider);
|
|
869
|
+
}
|
|
870
|
+
function requireBalance(provider) {
|
|
871
|
+
if (!provider.balance) throw new UnsupportedCapabilityError(provider.id, "account balance");
|
|
872
|
+
return provider.balance.bind(provider);
|
|
873
|
+
}
|
|
844
874
|
function formatCost(unit, amount) {
|
|
845
875
|
if (unit === "free") return "free";
|
|
846
876
|
if (unit === "usd") return `$${amount.toFixed(2)}`;
|
|
847
|
-
return `${amount} generation${amount === 1 ? "" : "s"}`;
|
|
877
|
+
if (unit === "generations") return `${amount} generation${amount === 1 ? "" : "s"}`;
|
|
878
|
+
return `${amount} ${unit}`;
|
|
848
879
|
}
|
|
849
880
|
|
|
850
|
-
// src/providers/pixellab.ts
|
|
851
|
-
var import_node_fs = require("fs");
|
|
852
|
-
var import_node_crypto = require("crypto");
|
|
853
|
-
var import_node_os = __toESM(require("os"), 1);
|
|
854
|
-
var import_node_path = __toESM(require("path"), 1);
|
|
855
|
-
|
|
856
881
|
// src/png.ts
|
|
857
882
|
var import_node_zlib = require("zlib");
|
|
858
883
|
var SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
@@ -1210,7 +1235,96 @@ function parseHex(hex2) {
|
|
|
1210
1235
|
return { r: n >> 16 & 255, g: n >> 8 & 255, b: n & 255 };
|
|
1211
1236
|
}
|
|
1212
1237
|
|
|
1238
|
+
// src/media.ts
|
|
1239
|
+
var MediaType = {
|
|
1240
|
+
PNG: "image/png",
|
|
1241
|
+
GIF: "image/gif"
|
|
1242
|
+
};
|
|
1243
|
+
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
1244
|
+
function mediaExtension(mediaType) {
|
|
1245
|
+
return mediaType === MediaType.GIF ? ".gif" : ".png";
|
|
1246
|
+
}
|
|
1247
|
+
function mediaTypeFromExtension(file) {
|
|
1248
|
+
const lower = file.toLowerCase();
|
|
1249
|
+
if (lower.endsWith(".png")) return MediaType.PNG;
|
|
1250
|
+
if (lower.endsWith(".gif")) return MediaType.GIF;
|
|
1251
|
+
return null;
|
|
1252
|
+
}
|
|
1253
|
+
function detectMediaType(bytes) {
|
|
1254
|
+
if (bytes.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) return MediaType.PNG;
|
|
1255
|
+
const header = bytes.subarray(0, 6).toString("ascii");
|
|
1256
|
+
if (header === "GIF87a" || header === "GIF89a") return MediaType.GIF;
|
|
1257
|
+
return null;
|
|
1258
|
+
}
|
|
1259
|
+
function validateMedia(bytes, expected) {
|
|
1260
|
+
const actual = detectMediaType(bytes);
|
|
1261
|
+
if (!actual) throw new Error(`response was not a supported PNG or GIF (${bytes.length} bytes)`);
|
|
1262
|
+
if (expected && actual !== expected) {
|
|
1263
|
+
throw new Error(`response was ${actual}, expected ${expected}`);
|
|
1264
|
+
}
|
|
1265
|
+
if (actual === MediaType.PNG) {
|
|
1266
|
+
decodePng(bytes);
|
|
1267
|
+
} else {
|
|
1268
|
+
validateGif(bytes);
|
|
1269
|
+
}
|
|
1270
|
+
return actual;
|
|
1271
|
+
}
|
|
1272
|
+
function validateGif(bytes) {
|
|
1273
|
+
if (bytes.length < 14) throw new Error("invalid GIF: truncated logical screen descriptor");
|
|
1274
|
+
const width = bytes.readUInt16LE(6);
|
|
1275
|
+
const height = bytes.readUInt16LE(8);
|
|
1276
|
+
if (!width || !height) throw new Error("invalid GIF: zero-sized logical screen");
|
|
1277
|
+
const packed = bytes[10];
|
|
1278
|
+
let offset = 13;
|
|
1279
|
+
if (packed & 128) offset += 3 * 2 ** ((packed & 7) + 1);
|
|
1280
|
+
if (offset > bytes.length) throw new Error("invalid GIF: truncated global color table");
|
|
1281
|
+
let sawImage = false;
|
|
1282
|
+
while (offset < bytes.length) {
|
|
1283
|
+
const marker = bytes[offset];
|
|
1284
|
+
if (marker === 59) {
|
|
1285
|
+
if (!sawImage) throw new Error("invalid GIF: contains no image frame");
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (marker === 44) {
|
|
1289
|
+
if (offset + 10 > bytes.length) throw new Error("invalid GIF: truncated image descriptor");
|
|
1290
|
+
const imagePacked = bytes[offset + 9];
|
|
1291
|
+
offset += 10;
|
|
1292
|
+
if (imagePacked & 128) offset += 3 * 2 ** ((imagePacked & 7) + 1);
|
|
1293
|
+
if (offset >= bytes.length) throw new Error("invalid GIF: missing image data");
|
|
1294
|
+
offset++;
|
|
1295
|
+
offset = skipSubBlocks(bytes, offset);
|
|
1296
|
+
sawImage = true;
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
if (marker === 33) {
|
|
1300
|
+
if (offset + 2 > bytes.length) throw new Error("invalid GIF: truncated extension");
|
|
1301
|
+
offset = skipSubBlocks(bytes, offset + 2);
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
throw new Error(`invalid GIF: unexpected block marker 0x${marker.toString(16)}`);
|
|
1305
|
+
}
|
|
1306
|
+
throw new Error("invalid GIF: missing trailer");
|
|
1307
|
+
}
|
|
1308
|
+
function skipSubBlocks(bytes, start) {
|
|
1309
|
+
let offset = start;
|
|
1310
|
+
for (; ; ) {
|
|
1311
|
+
if (offset >= bytes.length) throw new Error("invalid GIF: truncated data blocks");
|
|
1312
|
+
const size = bytes[offset];
|
|
1313
|
+
offset++;
|
|
1314
|
+
if (size === 0) return offset;
|
|
1315
|
+
offset += size;
|
|
1316
|
+
if (offset > bytes.length) throw new Error("invalid GIF: truncated data block");
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
function cacheFileName(hash, mediaType = MediaType.PNG) {
|
|
1320
|
+
return `${hash}${mediaExtension(mediaType)}`;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1213
1323
|
// src/providers/pixellab.ts
|
|
1324
|
+
var import_node_fs = require("fs");
|
|
1325
|
+
var import_node_crypto = require("crypto");
|
|
1326
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
1327
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
1214
1328
|
var PixelLabProvider = class _PixelLabProvider {
|
|
1215
1329
|
constructor(client) {
|
|
1216
1330
|
this.client = client;
|
|
@@ -1260,7 +1374,44 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
1260
1374
|
candidates: spec.generator === "1dir" ? candidateCount(spec.size) : 1
|
|
1261
1375
|
};
|
|
1262
1376
|
}
|
|
1377
|
+
validate(spec, styleImages) {
|
|
1378
|
+
if (spec.generator === "map") {
|
|
1379
|
+
requirePixelLabOption("view", spec.view, ["low top-down", "high top-down", "side"]);
|
|
1380
|
+
requirePixelLabOption("outline", spec.outline, [
|
|
1381
|
+
"single color outline",
|
|
1382
|
+
"selective outline",
|
|
1383
|
+
"lineless"
|
|
1384
|
+
]);
|
|
1385
|
+
requirePixelLabOption("shading", spec.shading, [
|
|
1386
|
+
"flat shading",
|
|
1387
|
+
"basic shading",
|
|
1388
|
+
"medium shading",
|
|
1389
|
+
"detailed shading"
|
|
1390
|
+
]);
|
|
1391
|
+
requirePixelLabOption("detail", spec.detail, [
|
|
1392
|
+
"low detail",
|
|
1393
|
+
"medium detail",
|
|
1394
|
+
"high detail"
|
|
1395
|
+
]);
|
|
1396
|
+
}
|
|
1397
|
+
if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
|
|
1398
|
+
throw new Error(`PixelLab ${spec.generator} does not support style images`);
|
|
1399
|
+
}
|
|
1400
|
+
for (const image of styleImages) {
|
|
1401
|
+
if (image.width > 256 || image.height > 256) {
|
|
1402
|
+
throw new Error(
|
|
1403
|
+
`Style image exceeds PixelLab's 256x256 limit (${image.width}x${image.height})`
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
if (spec.tags.length > 20) {
|
|
1408
|
+
throw new Error(
|
|
1409
|
+
`${spec.styleId}/${spec.assetId} resolves to ${spec.tags.length} tags, but PixelLab allows at most 20`
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1263
1413
|
async submit(spec, styleImages) {
|
|
1414
|
+
this.validate(spec, styleImages);
|
|
1264
1415
|
if (spec.generator === "pixflux") {
|
|
1265
1416
|
const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
1266
1417
|
const { png } = await this.client.createImagePixflux({
|
|
@@ -1446,6 +1597,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
1446
1597
|
await this.client.deleteObject(assetId);
|
|
1447
1598
|
}
|
|
1448
1599
|
};
|
|
1600
|
+
function requirePixelLabOption(name, value, allowed) {
|
|
1601
|
+
if (value != null && !allowed.includes(value)) {
|
|
1602
|
+
throw new Error(`PixelLab map ${name} must be one of: ${allowed.join(", ")}`);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1449
1605
|
function tilesInIndexOrder(urls) {
|
|
1450
1606
|
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);
|
|
1451
1607
|
}
|
|
@@ -1454,6 +1610,319 @@ function firstUrl(urls) {
|
|
|
1454
1610
|
return Object.values(urls).find((u) => typeof u === "string") ?? null;
|
|
1455
1611
|
}
|
|
1456
1612
|
|
|
1613
|
+
// src/providers/retrodiffusion.ts
|
|
1614
|
+
var DEFAULT_BASE_URL = "https://api.retrodiffusion.ai/v1";
|
|
1615
|
+
var RetroDiffusionClient = class {
|
|
1616
|
+
constructor(token, baseUrl = DEFAULT_BASE_URL, request = fetch) {
|
|
1617
|
+
this.token = token;
|
|
1618
|
+
this.baseUrl = baseUrl;
|
|
1619
|
+
this.request = request;
|
|
1620
|
+
}
|
|
1621
|
+
token;
|
|
1622
|
+
baseUrl;
|
|
1623
|
+
request;
|
|
1624
|
+
async submit(body) {
|
|
1625
|
+
const response = await this.call("/inferences", { method: "POST", body: JSON.stringify(body) });
|
|
1626
|
+
const taskId = response.task_id;
|
|
1627
|
+
if (typeof taskId !== "string" || !taskId) {
|
|
1628
|
+
throw new Error("Retro Diffusion did not return an async task id");
|
|
1629
|
+
}
|
|
1630
|
+
return taskId;
|
|
1631
|
+
}
|
|
1632
|
+
async quote(body) {
|
|
1633
|
+
const response = await this.call("/inferences", {
|
|
1634
|
+
method: "POST",
|
|
1635
|
+
body: JSON.stringify({ ...body, check_cost: true })
|
|
1636
|
+
});
|
|
1637
|
+
const amount = response.balance_cost;
|
|
1638
|
+
if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) {
|
|
1639
|
+
throw new Error("Retro Diffusion returned an invalid cost quote");
|
|
1640
|
+
}
|
|
1641
|
+
return amount;
|
|
1642
|
+
}
|
|
1643
|
+
async task(id) {
|
|
1644
|
+
return await this.call(`/inferences/tasks/${encodeURIComponent(id)}`);
|
|
1645
|
+
}
|
|
1646
|
+
async balance() {
|
|
1647
|
+
const response = await this.call("/inferences/credits");
|
|
1648
|
+
const balance = response.balance;
|
|
1649
|
+
if (typeof balance !== "number" || !Number.isFinite(balance)) {
|
|
1650
|
+
throw new Error("Retro Diffusion returned an invalid balance");
|
|
1651
|
+
}
|
|
1652
|
+
return balance;
|
|
1653
|
+
}
|
|
1654
|
+
async call(path17, init = {}) {
|
|
1655
|
+
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
1656
|
+
const response = await this.request(`${this.baseUrl}${path17}`, {
|
|
1657
|
+
...init,
|
|
1658
|
+
headers: {
|
|
1659
|
+
"Content-Type": "application/json",
|
|
1660
|
+
"X-RD-Token": this.token,
|
|
1661
|
+
...init.headers
|
|
1662
|
+
}
|
|
1663
|
+
});
|
|
1664
|
+
const text = await response.text();
|
|
1665
|
+
let value = null;
|
|
1666
|
+
try {
|
|
1667
|
+
value = text ? JSON.parse(text) : null;
|
|
1668
|
+
} catch {
|
|
1669
|
+
value = text;
|
|
1670
|
+
}
|
|
1671
|
+
if (!response.ok) {
|
|
1672
|
+
const retry = response.headers.get("retry-after");
|
|
1673
|
+
const detail = retroError(value);
|
|
1674
|
+
throw new Error(
|
|
1675
|
+
`Retro Diffusion request failed (${response.status}): ${detail}` + (retry ? `; retry after ${retry}s` : "")
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
return value;
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
var RetroDiffusionProvider = class _RetroDiffusionProvider {
|
|
1682
|
+
constructor(client) {
|
|
1683
|
+
this.client = client;
|
|
1684
|
+
}
|
|
1685
|
+
client;
|
|
1686
|
+
id = "retrodiffusion";
|
|
1687
|
+
static fromEnv() {
|
|
1688
|
+
return new _RetroDiffusionProvider(new RetroDiffusionClient(process.env.RD_API_KEY));
|
|
1689
|
+
}
|
|
1690
|
+
static forOffline() {
|
|
1691
|
+
return new _RetroDiffusionProvider(new RetroDiffusionClient(void 0));
|
|
1692
|
+
}
|
|
1693
|
+
static forDownloads() {
|
|
1694
|
+
return _RetroDiffusionProvider.forOffline();
|
|
1695
|
+
}
|
|
1696
|
+
supports(generator) {
|
|
1697
|
+
return generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "animation";
|
|
1698
|
+
}
|
|
1699
|
+
estimate(spec) {
|
|
1700
|
+
const options = retroOptions(spec);
|
|
1701
|
+
const style = resolvedPromptStyle(spec, options);
|
|
1702
|
+
const count = options.numImages ?? 1;
|
|
1703
|
+
const pixels = spec.width * spec.height;
|
|
1704
|
+
let each;
|
|
1705
|
+
if (style.startsWith("rd_advanced_animation__")) {
|
|
1706
|
+
each = /__(?:custom_action|subtle_motion)$/.test(style) ? 0.25 : 0.14;
|
|
1707
|
+
} else if (style.startsWith("rd_animation__")) {
|
|
1708
|
+
each = /__(?:any_animation|8_dir_rotation)$/.test(style) ? 0.25 : 0.07;
|
|
1709
|
+
} else if (/^rd_tile__tileset(?:_advanced)?$/.test(style)) {
|
|
1710
|
+
each = 0.1;
|
|
1711
|
+
} else if (style.startsWith("rd_pro__")) {
|
|
1712
|
+
each = 0.18;
|
|
1713
|
+
} else if (style.startsWith("rd_fast__")) {
|
|
1714
|
+
each = Math.max(0.015, (pixels + 1e5) / 6e6);
|
|
1715
|
+
} else if (isLowResolutionStyle(style)) {
|
|
1716
|
+
each = Math.max(0.02, (pixels + 13700) / 6e5);
|
|
1717
|
+
} else {
|
|
1718
|
+
each = Math.max(0.025, (pixels + 5e4) / 2e6);
|
|
1719
|
+
}
|
|
1720
|
+
return { unit: "usd", amount: roundUsdEstimate(each * count), candidates: count };
|
|
1721
|
+
}
|
|
1722
|
+
validate(spec, styleImages) {
|
|
1723
|
+
const options = retroOptions(spec);
|
|
1724
|
+
const promptStyle = resolvedPromptStyle(spec, options);
|
|
1725
|
+
const count = options.numImages ?? 1;
|
|
1726
|
+
const isAnimation = /^(?:rd_animation__|rd_advanced_animation__)/.test(promptStyle);
|
|
1727
|
+
const isTile = promptStyle.startsWith("rd_tile__");
|
|
1728
|
+
if (!promptStyle || spec.generator === "animation" && !isAnimation || spec.generator === "tiles" && !isTile || spec.generator !== "animation" && spec.generator !== "tiles" && (isAnimation || isTile)) {
|
|
1729
|
+
throw new Error(
|
|
1730
|
+
`Retro Diffusion style "${promptStyle}" does not match generator "${spec.generator}"`
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
if (!Number.isInteger(count) || count < 1 || count > 16) {
|
|
1734
|
+
throw new Error("Retro Diffusion numImages must be a whole number from 1 to 16");
|
|
1735
|
+
}
|
|
1736
|
+
if (spec.width < 16 || spec.height < 16 || spec.width > 512 || spec.height > 512) {
|
|
1737
|
+
throw new Error("Retro Diffusion output dimensions must be between 16 and 512 pixels");
|
|
1738
|
+
}
|
|
1739
|
+
if (styleImages.length > 9) {
|
|
1740
|
+
throw new Error("Retro Diffusion accepts at most 9 reference images");
|
|
1741
|
+
}
|
|
1742
|
+
if (spec.generator === "animation") {
|
|
1743
|
+
if (count !== 1) throw new Error("Retro Diffusion animations currently require numImages: 1");
|
|
1744
|
+
validateAnimation(promptStyle, spec, styleImages, options);
|
|
1745
|
+
} else if (spec.generator === "tiles") {
|
|
1746
|
+
validateTile(promptStyle, spec, styleImages, count, options);
|
|
1747
|
+
} else if (styleImages.length && !/^(?:rd_pro__|user__)/.test(promptStyle)) {
|
|
1748
|
+
throw new Error(
|
|
1749
|
+
`Retro Diffusion style "${promptStyle}" does not accept reference_images; use an RD Pro or user style`
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
async submit(spec, styleImages) {
|
|
1754
|
+
this.validate(spec, styleImages);
|
|
1755
|
+
const options = retroOptions(spec);
|
|
1756
|
+
const promptStyle = resolvedPromptStyle(spec, options);
|
|
1757
|
+
const animation = spec.generator === "animation";
|
|
1758
|
+
const tiles = spec.generator === "tiles";
|
|
1759
|
+
const body = {
|
|
1760
|
+
prompt: spec.prompt,
|
|
1761
|
+
prompt_style: promptStyle,
|
|
1762
|
+
width: spec.width,
|
|
1763
|
+
height: spec.height,
|
|
1764
|
+
num_images: options.numImages ?? 1,
|
|
1765
|
+
...!animation && !tiles ? { remove_bg: options.removeBg ?? spec.noBackground } : {},
|
|
1766
|
+
...spec.seed != null ? { seed: spec.seed } : {},
|
|
1767
|
+
...animation || tiles ? styleImages[0] ? { input_image: styleImages[0].base64 } : {} : styleImages.length ? { reference_images: styleImages.map((image) => image.base64) } : {},
|
|
1768
|
+
...tiles && styleImages[1] ? { extra_input_image: styleImages[1].base64 } : {},
|
|
1769
|
+
...tiles && options.extraPrompt ? { extra_prompt: options.extraPrompt } : {},
|
|
1770
|
+
...animation && options.framesDuration ? { frames_duration: options.framesDuration } : {},
|
|
1771
|
+
...animation && options.returnSpritesheet ? { return_spritesheet: true } : {},
|
|
1772
|
+
...!animation && options.tileX != null ? { tile_x: options.tileX } : {},
|
|
1773
|
+
...!animation && options.tileY != null ? { tile_y: options.tileY } : {},
|
|
1774
|
+
...spec.palette.length ? { input_palette: paletteSwatch(spec.palette).toString("base64") } : {}
|
|
1775
|
+
};
|
|
1776
|
+
const quoted = await this.client.quote(body);
|
|
1777
|
+
const estimated = this.estimate(spec).amount;
|
|
1778
|
+
if (quoted > estimated + 1e-6) {
|
|
1779
|
+
throw new Error(
|
|
1780
|
+
`Retro Diffusion quoted $${quoted.toFixed(6)}, above the offline estimate $${estimated.toFixed(6)}; no paid request was sent`
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
return {
|
|
1784
|
+
jobId: await this.client.submit({ ...body, async: true, upload_outputs: true })
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
async poll(jobId, generator, context) {
|
|
1788
|
+
const task = await this.client.task(jobId);
|
|
1789
|
+
if (task.status === "pending" || task.status === "running") return { status: "processing" };
|
|
1790
|
+
if (task.status === "failed") return { status: "failed", error: retroError(task.error) };
|
|
1791
|
+
if (task.status !== "succeeded") {
|
|
1792
|
+
return { status: "failed", error: `Retro Diffusion returned unknown task status "${String(task.status)}"` };
|
|
1793
|
+
}
|
|
1794
|
+
const options = context?.spec ? retroOptions(context.spec) : {};
|
|
1795
|
+
const mediaType = generator === "animation" && !options.returnSpritesheet ? MediaType.GIF : MediaType.PNG;
|
|
1796
|
+
const sources = resultSources(task.result, mediaType);
|
|
1797
|
+
const urls = sources.map((source) => source.url);
|
|
1798
|
+
if (!urls.length) return { status: "failed", error: "Retro Diffusion task returned no images" };
|
|
1799
|
+
if (urls.length > 1) return { status: "review", candidateUrls: urls };
|
|
1800
|
+
return {
|
|
1801
|
+
status: "ready",
|
|
1802
|
+
objectId: `${jobId}#0`,
|
|
1803
|
+
sourceUrl: urls[0],
|
|
1804
|
+
sources,
|
|
1805
|
+
metadata: {
|
|
1806
|
+
balanceCost: task.result?.balance_cost ?? null,
|
|
1807
|
+
remainingBalance: task.result?.remaining_balance ?? null,
|
|
1808
|
+
mediaType,
|
|
1809
|
+
kind: generator === "animation" ? "animation" : generator === "tiles" ? "tileset" : "image",
|
|
1810
|
+
...context?.spec ? {
|
|
1811
|
+
promptStyle: resolvedPromptStyle(context.spec, options),
|
|
1812
|
+
width: context.spec.width,
|
|
1813
|
+
height: context.spec.height
|
|
1814
|
+
} : {}
|
|
1815
|
+
}
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
async selectCandidate(jobId, index) {
|
|
1819
|
+
const task = await this.client.task(jobId);
|
|
1820
|
+
if (task.status !== "succeeded") throw new Error(`Retro Diffusion task ${jobId} is not ready`);
|
|
1821
|
+
const url = resultSources(task.result, MediaType.PNG)[index]?.url;
|
|
1822
|
+
if (!url) throw new Error(`Retro Diffusion task ${jobId} has no candidate at index ${index}`);
|
|
1823
|
+
return { objectId: `${jobId}#${index}`, sourceUrl: url };
|
|
1824
|
+
}
|
|
1825
|
+
async download(url) {
|
|
1826
|
+
const data = /^data:[^;]+;base64,(.+)$/.exec(url)?.[1];
|
|
1827
|
+
if (data) return Buffer.from(data, "base64");
|
|
1828
|
+
const response = await fetch(url);
|
|
1829
|
+
if (!response.ok) throw new Error(`Retro Diffusion download failed (${response.status})`);
|
|
1830
|
+
return Buffer.from(await response.arrayBuffer());
|
|
1831
|
+
}
|
|
1832
|
+
async balance() {
|
|
1833
|
+
return { unit: "usd", remaining: await this.client.balance() };
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
function retroOptions(spec) {
|
|
1837
|
+
return spec.providerOptions;
|
|
1838
|
+
}
|
|
1839
|
+
function resultSources(result, mediaType) {
|
|
1840
|
+
const hosted = (result?.output_urls ?? []).filter((url) => typeof url === "string" && url.length > 0).map((url) => ({ url, mediaType }));
|
|
1841
|
+
if (hosted.length) return hosted;
|
|
1842
|
+
return (result?.base64_images ?? []).filter((data) => typeof data === "string" && data.length > 0).map((data) => ({
|
|
1843
|
+
url: `data:${mediaType};base64,${data}`,
|
|
1844
|
+
mediaType
|
|
1845
|
+
}));
|
|
1846
|
+
}
|
|
1847
|
+
function resolvedPromptStyle(spec, options) {
|
|
1848
|
+
return options.promptStyle ?? (spec.generator === "animation" ? "rd_animation__any_animation" : spec.generator === "tiles" ? "rd_tile__tileset" : "rd_plus__default");
|
|
1849
|
+
}
|
|
1850
|
+
function validateAnimation(style, spec, styleImages, options) {
|
|
1851
|
+
if (spec.width !== spec.height) throw new Error("Retro Diffusion animations must be square");
|
|
1852
|
+
if (style.startsWith("rd_advanced_animation__")) {
|
|
1853
|
+
if (styleImages.length !== 1) {
|
|
1854
|
+
throw new Error(`Retro Diffusion advanced animation style "${style}" requires one input image`);
|
|
1855
|
+
}
|
|
1856
|
+
if (spec.width < 32 || spec.width > 256) {
|
|
1857
|
+
throw new Error("Retro Diffusion advanced animations require dimensions from 32 to 256 pixels");
|
|
1858
|
+
}
|
|
1859
|
+
} else if (styleImages.length > 1) {
|
|
1860
|
+
throw new Error("Retro Diffusion prompt animations accept at most one input image");
|
|
1861
|
+
}
|
|
1862
|
+
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;
|
|
1863
|
+
if (exact && spec.width !== exact) {
|
|
1864
|
+
throw new Error(`Retro Diffusion style "${style}" requires ${exact}x${exact} dimensions`);
|
|
1865
|
+
}
|
|
1866
|
+
if (style.endsWith("__vfx") && (spec.width < 24 || spec.width > 96)) {
|
|
1867
|
+
throw new Error("Retro Diffusion VFX animations require dimensions from 24 to 96 pixels");
|
|
1868
|
+
}
|
|
1869
|
+
if (options.framesDuration != null && ![4, 6, 8, 10, 12, 16].includes(options.framesDuration)) {
|
|
1870
|
+
throw new Error("Retro Diffusion framesDuration must be 4, 6, 8, 10, 12, or 16");
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
function validateTile(style, spec, styleImages, count, options) {
|
|
1874
|
+
if (spec.width !== spec.height) throw new Error("Retro Diffusion tiles must be square");
|
|
1875
|
+
const size = spec.width;
|
|
1876
|
+
if (/^rd_tile__tileset(?:_advanced)?$/.test(style)) {
|
|
1877
|
+
if (size < 16 || size > 32) throw new Error("Retro Diffusion tilesets require 16\u201332px tiles");
|
|
1878
|
+
if (count !== 1) throw new Error("Retro Diffusion tilesets require numImages: 1");
|
|
1879
|
+
} else if (style === "rd_tile__single_tile" && (size < 16 || size > 64)) {
|
|
1880
|
+
throw new Error("Retro Diffusion single tiles require dimensions from 16 to 64 pixels");
|
|
1881
|
+
} else if (style === "rd_tile__tile_variation") {
|
|
1882
|
+
if (size < 16 || size > 128) throw new Error("Retro Diffusion tile variations require 16\u2013128px tiles");
|
|
1883
|
+
if (styleImages.length !== 1) throw new Error("Retro Diffusion tile variations require one input image");
|
|
1884
|
+
} else if (style === "rd_tile__tile_object" && (size < 16 || size > 96)) {
|
|
1885
|
+
throw new Error("Retro Diffusion tile objects require dimensions from 16 to 96 pixels");
|
|
1886
|
+
} else if (style === "rd_tile__scene_object" && (size < 64 || size > 384)) {
|
|
1887
|
+
throw new Error("Retro Diffusion tile scene objects require dimensions from 64 to 384 pixels");
|
|
1888
|
+
}
|
|
1889
|
+
if (style === "rd_tile__tileset" && styleImages.length > 1) {
|
|
1890
|
+
throw new Error("Retro Diffusion basic tilesets accept at most one input image");
|
|
1891
|
+
}
|
|
1892
|
+
if (style === "rd_tile__tileset_advanced") {
|
|
1893
|
+
if (styleImages.length > 2) throw new Error("Retro Diffusion advanced tilesets accept at most two input images");
|
|
1894
|
+
if (!options.extraPrompt && styleImages.length < 2) {
|
|
1895
|
+
throw new Error("Retro Diffusion advanced tilesets require extraPrompt or a second input image");
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
function isLowResolutionStyle(style) {
|
|
1900
|
+
return /(?:^|__)(?:mc_|low_res|classic|skill_icon|topdown_item)/.test(style);
|
|
1901
|
+
}
|
|
1902
|
+
function roundUsdEstimate(value) {
|
|
1903
|
+
return Math.ceil((value - 1e-9) * 1e3) / 1e3;
|
|
1904
|
+
}
|
|
1905
|
+
function retroError(value) {
|
|
1906
|
+
if (typeof value === "string") return value || "unknown error";
|
|
1907
|
+
if (!value || typeof value !== "object") return "unknown error";
|
|
1908
|
+
const record = value;
|
|
1909
|
+
if (typeof record.message === "string") return record.message;
|
|
1910
|
+
if (typeof record.detail === "string") return record.detail;
|
|
1911
|
+
if (Array.isArray(record.detail)) {
|
|
1912
|
+
const details = record.detail.map((item) => {
|
|
1913
|
+
if (!item || typeof item !== "object") return String(item);
|
|
1914
|
+
const detail = item;
|
|
1915
|
+
return typeof detail.msg === "string" ? detail.msg : JSON.stringify(item);
|
|
1916
|
+
}).filter(Boolean);
|
|
1917
|
+
if (details.length) return details.join("; ");
|
|
1918
|
+
}
|
|
1919
|
+
if (record.detail && typeof record.detail === "object") {
|
|
1920
|
+
const message2 = record.detail.message;
|
|
1921
|
+
if (typeof message2 === "string") return message2;
|
|
1922
|
+
}
|
|
1923
|
+
return JSON.stringify(value);
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1457
1926
|
// src/providers/fake.ts
|
|
1458
1927
|
var import_node_crypto2 = require("crypto");
|
|
1459
1928
|
var FAKE_PNG = Buffer.from(
|
|
@@ -1598,6 +2067,47 @@ var FakeProvider = class {
|
|
|
1598
2067
|
}
|
|
1599
2068
|
};
|
|
1600
2069
|
|
|
2070
|
+
// src/providers/registry.ts
|
|
2071
|
+
var factories = /* @__PURE__ */ new Map();
|
|
2072
|
+
function registerProvider(factory) {
|
|
2073
|
+
const id = factory.id.trim();
|
|
2074
|
+
if (!id) throw new Error("Provider id cannot be empty");
|
|
2075
|
+
if (factories.has(id)) throw new Error(`Provider "${id}" is already registered`);
|
|
2076
|
+
factories.set(id, factory);
|
|
2077
|
+
}
|
|
2078
|
+
function providerFactory(id) {
|
|
2079
|
+
const factory = factories.get(id);
|
|
2080
|
+
if (!factory) {
|
|
2081
|
+
const available = [...factories.keys()].sort().join(", ") || "(none)";
|
|
2082
|
+
throw new Error(`Unknown provider "${id}". Available providers: ${available}`);
|
|
2083
|
+
}
|
|
2084
|
+
return factory;
|
|
2085
|
+
}
|
|
2086
|
+
function createProvider(id, mode2) {
|
|
2087
|
+
return providerFactory(id).create(mode2);
|
|
2088
|
+
}
|
|
2089
|
+
function availableProviders() {
|
|
2090
|
+
return [...factories.keys()].sort();
|
|
2091
|
+
}
|
|
2092
|
+
registerProvider({
|
|
2093
|
+
id: "pixellab",
|
|
2094
|
+
credentialEnv: "PIXELLAB_API_KEY",
|
|
2095
|
+
create(mode2) {
|
|
2096
|
+
if (mode2 === "online") return PixelLabProvider.fromEnv();
|
|
2097
|
+
if (mode2 === "downloads") return PixelLabProvider.forDownloads();
|
|
2098
|
+
return PixelLabProvider.forOffline();
|
|
2099
|
+
}
|
|
2100
|
+
});
|
|
2101
|
+
registerProvider({
|
|
2102
|
+
id: "retrodiffusion",
|
|
2103
|
+
credentialEnv: "RD_API_KEY",
|
|
2104
|
+
create(mode2) {
|
|
2105
|
+
if (mode2 === "online") return RetroDiffusionProvider.fromEnv();
|
|
2106
|
+
if (mode2 === "downloads") return RetroDiffusionProvider.forDownloads();
|
|
2107
|
+
return RetroDiffusionProvider.forOffline();
|
|
2108
|
+
}
|
|
2109
|
+
});
|
|
2110
|
+
|
|
1601
2111
|
// src/manifest.ts
|
|
1602
2112
|
var import_promises2 = require("fs/promises");
|
|
1603
2113
|
var import_node_fs2 = require("fs");
|
|
@@ -1615,6 +2125,10 @@ async function sha256File(path17) {
|
|
|
1615
2125
|
function specHash(spec, styleImageHashes) {
|
|
1616
2126
|
return sha256(
|
|
1617
2127
|
JSON.stringify({
|
|
2128
|
+
// Preserve every existing PixelLab hash while making a provider switch
|
|
2129
|
+
// invalidate the spec. Older manifests implicitly mean pixellab.
|
|
2130
|
+
provider: spec.provider === "pixellab" ? void 0 : spec.provider,
|
|
2131
|
+
providerOptions: Object.keys(spec.providerOptions).length > 0 ? spec.providerOptions : void 0,
|
|
1618
2132
|
generator: spec.generator,
|
|
1619
2133
|
prompt: spec.prompt,
|
|
1620
2134
|
width: spec.width,
|
|
@@ -1628,7 +2142,7 @@ function specHash(spec, styleImageHashes) {
|
|
|
1628
2142
|
// `noBackground` only reaches the wire for pixflux; the tile fields are
|
|
1629
2143
|
// undefined for every other generator. `tileSize` is intentionally
|
|
1630
2144
|
// absent — width/height are derived from it, so it is already covered.
|
|
1631
|
-
noBackground: spec.generator === "pixflux" ? spec.noBackground : void 0,
|
|
2145
|
+
noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
|
|
1632
2146
|
tileType: spec.tileType,
|
|
1633
2147
|
tileView: spec.tileView,
|
|
1634
2148
|
tileFeature: spec.tileFeature,
|
|
@@ -1668,6 +2182,7 @@ ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
|
1668
2182
|
}
|
|
1669
2183
|
async function resolveSpecs(loaded, filter) {
|
|
1670
2184
|
const { manifest, root } = loaded;
|
|
2185
|
+
const activeProvider = filter?.provider ?? createProvider(manifest.provider, "offline");
|
|
1671
2186
|
const specs = [];
|
|
1672
2187
|
const styleIds = Object.keys(manifest.styles).filter(
|
|
1673
2188
|
(id) => !filter?.styles?.length || filter.styles.includes(id)
|
|
@@ -1693,10 +2208,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1693
2208
|
const buf = await (0, import_promises2.readFile)(abs);
|
|
1694
2209
|
const metadata = imageMetadata(buf);
|
|
1695
2210
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
|
|
1696
|
-
if (metadata.width < 1 || metadata.height < 1
|
|
1697
|
-
throw new Error(
|
|
1698
|
-
`Style image exceeds the API's 256x256 limit: ${abs} (${metadata.width}x${metadata.height})`
|
|
1699
|
-
);
|
|
2211
|
+
if (metadata.width < 1 || metadata.height < 1) {
|
|
2212
|
+
throw new Error(`Style image has invalid dimensions: ${abs}`);
|
|
1700
2213
|
}
|
|
1701
2214
|
hit = { base64: buf.toString("base64"), hash: sha256(buf), ...metadata };
|
|
1702
2215
|
styleImageCache.set(abs, hit);
|
|
@@ -1716,8 +2229,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1716
2229
|
if (filter?.assets?.length && !filter.assets.includes(assetId)) continue;
|
|
1717
2230
|
if (asset.styles.length && !asset.styles.includes(styleId)) continue;
|
|
1718
2231
|
const generator = style.generator;
|
|
1719
|
-
if (
|
|
1720
|
-
throw new Error(`Provider "${
|
|
2232
|
+
if (!activeProvider.supports(generator)) {
|
|
2233
|
+
throw new Error(`Provider "${activeProvider.id}" does not support generator "${generator}"`);
|
|
1721
2234
|
}
|
|
1722
2235
|
let width;
|
|
1723
2236
|
let height;
|
|
@@ -1744,6 +2257,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1744
2257
|
const base = {
|
|
1745
2258
|
styleId,
|
|
1746
2259
|
assetId,
|
|
2260
|
+
provider: activeProvider.id,
|
|
2261
|
+
providerOptions: style.providerOptions[activeProvider.id] ?? {},
|
|
1747
2262
|
generator,
|
|
1748
2263
|
prompt,
|
|
1749
2264
|
width,
|
|
@@ -1775,11 +2290,6 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1775
2290
|
`style:${styleId}`
|
|
1776
2291
|
])
|
|
1777
2292
|
];
|
|
1778
|
-
if (tags.length > 20) {
|
|
1779
|
-
throw new Error(
|
|
1780
|
-
`${styleId}/${assetId} resolves to ${tags.length} tags, but PixelLab allows at most 20`
|
|
1781
|
-
);
|
|
1782
|
-
}
|
|
1783
2293
|
const resolved = {
|
|
1784
2294
|
...base,
|
|
1785
2295
|
root,
|
|
@@ -1788,12 +2298,15 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1788
2298
|
source: asset.source,
|
|
1789
2299
|
specHash: specHash(base, styleImageHashes)
|
|
1790
2300
|
};
|
|
1791
|
-
|
|
1792
|
-
const
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2301
|
+
const resolvedImages = style.styleImages.map((image) => {
|
|
2302
|
+
const hit = styleImageCache.get(import_node_path2.default.resolve(root, image.path));
|
|
2303
|
+
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
2304
|
+
});
|
|
2305
|
+
activeProvider.validate?.(resolved, resolvedImages);
|
|
2306
|
+
const estimate = validateCostEstimate(activeProvider.id, activeProvider.estimate(resolved));
|
|
2307
|
+
resolved.cost = estimate.amount;
|
|
2308
|
+
resolved.costUnit = estimate.unit;
|
|
2309
|
+
resolved.candidates = estimate.candidates;
|
|
1797
2310
|
specs.push(resolved);
|
|
1798
2311
|
}
|
|
1799
2312
|
}
|
|
@@ -1817,10 +2330,8 @@ async function resolveStyleImages(loaded, styleId) {
|
|
|
1817
2330
|
const buf = await (0, import_promises2.readFile)(file);
|
|
1818
2331
|
const metadata = imageMetadata(buf);
|
|
1819
2332
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
1820
|
-
if (metadata.width < 1 || metadata.height < 1
|
|
1821
|
-
throw new Error(
|
|
1822
|
-
`Style image exceeds the API's 256x256 limit: ${file} (${metadata.width}x${metadata.height})`
|
|
1823
|
-
);
|
|
2333
|
+
if (metadata.width < 1 || metadata.height < 1) {
|
|
2334
|
+
throw new Error(`Style image has invalid dimensions: ${file}`);
|
|
1824
2335
|
}
|
|
1825
2336
|
out.push({ base64: buf.toString("base64"), ...metadata });
|
|
1826
2337
|
}
|
|
@@ -2022,12 +2533,13 @@ async function acquireFileLock(file) {
|
|
|
2022
2533
|
function spendByUnit(lock) {
|
|
2023
2534
|
const totals = { generations: 0, usd: 0, free: 0 };
|
|
2024
2535
|
for (const entry of Object.values(lock.entries)) {
|
|
2025
|
-
|
|
2536
|
+
const unit = entry.costUnit ?? "generations";
|
|
2537
|
+
totals[unit] = (totals[unit] ?? 0) + (entry.cost ?? 0);
|
|
2026
2538
|
}
|
|
2027
2539
|
return totals;
|
|
2028
2540
|
}
|
|
2029
2541
|
function totalSpend(lock, unit = "generations") {
|
|
2030
|
-
return spendByUnit(lock)[unit];
|
|
2542
|
+
return spendByUnit(lock)[unit] ?? 0;
|
|
2031
2543
|
}
|
|
2032
2544
|
|
|
2033
2545
|
// src/outputs.ts
|
|
@@ -2044,16 +2556,16 @@ function resolveOutputPath(recordedPath, manifestDir) {
|
|
|
2044
2556
|
if (import_node_path4.default.win32.isAbsolute(recordedPath)) return recordedPath;
|
|
2045
2557
|
return import_node_path4.default.resolve(manifestDir, recordedPath.split(/[\\/]/).join(import_node_path4.default.sep));
|
|
2046
2558
|
}
|
|
2047
|
-
function expectedOutputPath(spec, role, index, total) {
|
|
2048
|
-
if (total === 1) return spec.outFile;
|
|
2559
|
+
function expectedOutputPath(spec, role, index, total, mediaType) {
|
|
2049
2560
|
const originalExt = import_node_path4.default.extname(spec.outFile);
|
|
2050
|
-
const ext = originalExt || ".png";
|
|
2561
|
+
const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
|
|
2051
2562
|
const stem = originalExt ? spec.outFile.slice(0, -originalExt.length) : spec.outFile;
|
|
2563
|
+
if (total === 1) return `${stem}${ext}`;
|
|
2052
2564
|
const safeRole = (role ?? fallbackOutputRole(index)).replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
2053
2565
|
return `${stem}-${safeRole}${ext}`;
|
|
2054
2566
|
}
|
|
2055
2567
|
function currentOutputPath(output, spec, index, total) {
|
|
2056
|
-
return expectedOutputPath(spec, output.role, index, total);
|
|
2568
|
+
return expectedOutputPath(spec, output.role, index, total, output.mediaType);
|
|
2057
2569
|
}
|
|
2058
2570
|
function currentEntryOutputPath(entry, spec, index) {
|
|
2059
2571
|
const output = entry.outputs[index];
|
|
@@ -2901,7 +3413,6 @@ function cachePathFor(lockPath) {
|
|
|
2901
3413
|
}
|
|
2902
3414
|
|
|
2903
3415
|
// src/pipeline/cache-health.ts
|
|
2904
|
-
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
2905
3416
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
2906
3417
|
if (options.prune && !(0, import_node_fs7.existsSync)(lockPath)) {
|
|
2907
3418
|
throw new Error(`Refusing to prune without an existing lockfile at ${import_node_path9.default.resolve(lockPath)}`);
|
|
@@ -2970,7 +3481,8 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
2970
3481
|
}
|
|
2971
3482
|
report.files++;
|
|
2972
3483
|
const file = import_node_path9.default.join(contentDir, entry.name);
|
|
2973
|
-
const
|
|
3484
|
+
const mediaType = mediaTypeFromExtension(entry.name);
|
|
3485
|
+
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
2974
3486
|
let bytes;
|
|
2975
3487
|
try {
|
|
2976
3488
|
bytes = await (0, import_promises7.readFile)(file);
|
|
@@ -2983,11 +3495,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
2983
3495
|
continue;
|
|
2984
3496
|
}
|
|
2985
3497
|
if (!isSha256Hash(expected)) {
|
|
2986
|
-
report.invalid.push({ name: entry.name, reason: "filename is not <sha256>.png" });
|
|
2987
|
-
continue;
|
|
2988
|
-
}
|
|
2989
|
-
if (!bytes.subarray(0, 8).equals(PNG_SIGNATURE)) {
|
|
2990
|
-
report.invalid.push({ name: entry.name, reason: "not a PNG" });
|
|
3498
|
+
report.invalid.push({ name: entry.name, reason: "filename is not <sha256>.png or <sha256>.gif" });
|
|
2991
3499
|
continue;
|
|
2992
3500
|
}
|
|
2993
3501
|
if (sha256(bytes) !== expected) {
|
|
@@ -2995,11 +3503,11 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
2995
3503
|
continue;
|
|
2996
3504
|
}
|
|
2997
3505
|
try {
|
|
2998
|
-
|
|
3506
|
+
validateMedia(bytes, mediaType);
|
|
2999
3507
|
} catch (err) {
|
|
3000
3508
|
report.invalid.push({
|
|
3001
3509
|
name: entry.name,
|
|
3002
|
-
reason: `invalid PNG: ${err instanceof Error ? err.message : String(err)}`
|
|
3510
|
+
reason: `invalid ${mediaType === "image/gif" ? "GIF" : "PNG"}: ${err instanceof Error ? err.message : String(err)}`
|
|
3003
3511
|
});
|
|
3004
3512
|
continue;
|
|
3005
3513
|
}
|
|
@@ -3588,7 +4096,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
3588
4096
|
async function pruneInFlight() {
|
|
3589
4097
|
for (const [id, spec] of [...inFlight]) {
|
|
3590
4098
|
try {
|
|
3591
|
-
const state = await provider.poll(id, spec.generator, spec);
|
|
4099
|
+
const state = await provider.poll(id, spec.generator, { spec, tileFeature: spec.tileFeature });
|
|
3592
4100
|
if (state.status !== "processing") inFlight.delete(id);
|
|
3593
4101
|
lastSlotError = null;
|
|
3594
4102
|
} catch (err) {
|
|
@@ -3640,7 +4148,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
3640
4148
|
await saveLock(lockPath, lock);
|
|
3641
4149
|
lastSubmitAt = Date.now();
|
|
3642
4150
|
try {
|
|
3643
|
-
const refs =
|
|
4151
|
+
const refs = styleImages.get(spec.styleId) ?? [];
|
|
3644
4152
|
const { jobId } = await provider.submit(spec, refs);
|
|
3645
4153
|
upsert(lock, key, {
|
|
3646
4154
|
jobId,
|
|
@@ -3690,7 +4198,8 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
3690
4198
|
try {
|
|
3691
4199
|
const currentSpec = specByKey.get(key);
|
|
3692
4200
|
const state = await provider.poll(entry.jobId, entry.generator, {
|
|
3693
|
-
tileFeature: entry.tileFeature ?? currentSpec?.tileFeature
|
|
4201
|
+
tileFeature: entry.tileFeature ?? currentSpec?.tileFeature,
|
|
4202
|
+
spec: currentSpec
|
|
3694
4203
|
});
|
|
3695
4204
|
if (state.status === "review") {
|
|
3696
4205
|
upsert(lock, key, { status: "review", reviewObjectId: entry.jobId });
|
|
@@ -3728,7 +4237,6 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
3728
4237
|
var import_node_fs9 = require("fs");
|
|
3729
4238
|
var import_promises8 = require("fs/promises");
|
|
3730
4239
|
var import_node_path11 = __toESM(require("path"), 1);
|
|
3731
|
-
var PNG_SIGNATURE2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
3732
4240
|
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
3733
4241
|
const log = opts.onProgress ?? (() => {
|
|
3734
4242
|
});
|
|
@@ -3760,7 +4268,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3760
4268
|
result.skipped++;
|
|
3761
4269
|
continue;
|
|
3762
4270
|
}
|
|
3763
|
-
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 })) : [];
|
|
4271
|
+
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 })) : [];
|
|
3764
4272
|
if (!sources.length) {
|
|
3765
4273
|
upsert(lock, key, {
|
|
3766
4274
|
status: "download-failed",
|
|
@@ -3776,7 +4284,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3776
4284
|
const recorded = entry.outputs.find(
|
|
3777
4285
|
(o) => source.role ? o.role === source.role : !o.role && sources.length === 1
|
|
3778
4286
|
);
|
|
3779
|
-
const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length);
|
|
4287
|
+
const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length, source.mediaType);
|
|
3780
4288
|
if ((0, import_node_fs9.existsSync)(target)) {
|
|
3781
4289
|
if (!recorded) {
|
|
3782
4290
|
throw new Error(`refusing to overwrite untracked output ${target}`);
|
|
@@ -3784,11 +4292,19 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3784
4292
|
if (await sha256File(target) !== recorded.sha256) {
|
|
3785
4293
|
throw new Error(`refusing to overwrite modified output ${target}`);
|
|
3786
4294
|
}
|
|
3787
|
-
if (cacheDir)
|
|
4295
|
+
if (cacheDir) {
|
|
4296
|
+
await cacheMedia(
|
|
4297
|
+
cacheDir,
|
|
4298
|
+
await (0, import_promises8.readFile)(target),
|
|
4299
|
+
recorded.mediaType ?? MediaType.PNG,
|
|
4300
|
+
recorded.sha256
|
|
4301
|
+
);
|
|
4302
|
+
}
|
|
3788
4303
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
3789
4304
|
continue;
|
|
3790
4305
|
}
|
|
3791
|
-
|
|
4306
|
+
const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
|
|
4307
|
+
let buf = recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null;
|
|
3792
4308
|
if (buf) {
|
|
3793
4309
|
log(` cached ${import_node_path11.default.relative(process.cwd(), target)}`);
|
|
3794
4310
|
} else {
|
|
@@ -3797,24 +4313,25 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3797
4313
|
}
|
|
3798
4314
|
buf = await provider.download(source.url);
|
|
3799
4315
|
}
|
|
3800
|
-
|
|
3801
|
-
throw new Error(`response for ${source.role ?? "asset"} was not a PNG (${buf.length} bytes)`);
|
|
3802
|
-
}
|
|
4316
|
+
let mediaType;
|
|
3803
4317
|
try {
|
|
3804
|
-
|
|
4318
|
+
mediaType = validateMedia(buf, expectedMediaType);
|
|
3805
4319
|
} catch (err) {
|
|
4320
|
+
const label = expectedMediaType === MediaType.GIF ? "GIF" : "PNG";
|
|
4321
|
+
const mismatch = detectMediaType(buf) !== expectedMediaType;
|
|
3806
4322
|
throw new Error(
|
|
3807
|
-
`response for ${source.role ?? "asset"} was not a valid
|
|
4323
|
+
`response for ${source.role ?? "asset"} was not ${mismatch ? "a" : "a valid"} ${label}` + (mismatch ? ` (${buf.length} bytes)` : `: ${err instanceof Error ? err.message : String(err)}`)
|
|
3808
4324
|
);
|
|
3809
4325
|
}
|
|
3810
|
-
if (cacheDir) await
|
|
4326
|
+
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
3811
4327
|
await (0, import_promises8.mkdir)(import_node_path11.default.dirname(target), { recursive: true });
|
|
3812
4328
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
3813
4329
|
await (0, import_promises8.writeFile)(tmp, buf);
|
|
3814
4330
|
outputs.push({
|
|
3815
4331
|
path: portableOutputPath(target, spec.root),
|
|
3816
4332
|
sha256: sha256(buf),
|
|
3817
|
-
...source.role ? { role: source.role } : {}
|
|
4333
|
+
...source.role ? { role: source.role } : {},
|
|
4334
|
+
mediaType
|
|
3818
4335
|
});
|
|
3819
4336
|
upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
|
|
3820
4337
|
await saveLock(lockPath, lock);
|
|
@@ -3851,22 +4368,23 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3851
4368
|
await saveLock(lockPath, lock);
|
|
3852
4369
|
return result;
|
|
3853
4370
|
}
|
|
3854
|
-
async function
|
|
3855
|
-
const file = import_node_path11.default.join(cacheDir,
|
|
4371
|
+
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
4372
|
+
const file = import_node_path11.default.join(cacheDir, cacheFileName(hash, mediaType));
|
|
3856
4373
|
if (!(0, import_node_fs9.existsSync)(file)) return null;
|
|
3857
4374
|
try {
|
|
3858
4375
|
const buf = await (0, import_promises8.readFile)(file);
|
|
3859
|
-
if (
|
|
3860
|
-
|
|
4376
|
+
if (sha256(buf) !== hash) return null;
|
|
4377
|
+
validateMedia(buf, mediaType);
|
|
3861
4378
|
return buf;
|
|
3862
4379
|
} catch {
|
|
3863
4380
|
return null;
|
|
3864
4381
|
}
|
|
3865
4382
|
}
|
|
3866
|
-
async function
|
|
4383
|
+
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
3867
4384
|
const hash = knownHash ?? sha256(buf);
|
|
3868
|
-
|
|
3869
|
-
|
|
4385
|
+
validateMedia(buf, mediaType);
|
|
4386
|
+
const file = import_node_path11.default.join(cacheDir, cacheFileName(hash, mediaType));
|
|
4387
|
+
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
3870
4388
|
await (0, import_promises8.mkdir)(cacheDir, { recursive: true });
|
|
3871
4389
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3872
4390
|
await (0, import_promises8.writeFile)(tmp, buf);
|
|
@@ -3971,7 +4489,10 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
3971
4489
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
3972
4490
|
let hasCache = false;
|
|
3973
4491
|
for (const output of entry.outputs) {
|
|
3974
|
-
const cached = import_node_path12.default.join(
|
|
4492
|
+
const cached = import_node_path12.default.join(
|
|
4493
|
+
cacheDir,
|
|
4494
|
+
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
4495
|
+
);
|
|
3975
4496
|
if ((0, import_node_fs10.existsSync)(cached) && await sha256File(cached) === output.sha256) {
|
|
3976
4497
|
hasCache = true;
|
|
3977
4498
|
break;
|
|
@@ -4001,11 +4522,14 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
4001
4522
|
add(
|
|
4002
4523
|
"provider",
|
|
4003
4524
|
"error",
|
|
4004
|
-
opts.apiKeyPresent === false ? "PIXELLAB_API_KEY is not configured
|
|
4525
|
+
opts.apiKeyPresent === false ? `${opts.credentialEnv ?? "PIXELLAB_API_KEY"} is not configured` : "provider is not configured"
|
|
4005
4526
|
);
|
|
4527
|
+
} else if (!opts.provider.balance) {
|
|
4528
|
+
add("provider", "ok", `${opts.provider.id} configured; balance reporting unavailable`);
|
|
4006
4529
|
} else {
|
|
4530
|
+
const balanceFn = opts.provider.balance.bind(opts.provider);
|
|
4007
4531
|
try {
|
|
4008
|
-
const balance = await
|
|
4532
|
+
const balance = await balanceFn();
|
|
4009
4533
|
add("provider", "ok", `${opts.provider.id} reachable; ${balance.remaining} ${balance.unit} remaining`);
|
|
4010
4534
|
} catch (err) {
|
|
4011
4535
|
add("provider", "error", `provider connectivity failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -4496,7 +5020,7 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
4496
5020
|
const entry = lock.entries[key];
|
|
4497
5021
|
if (!group || !entry?.reviewObjectId) continue;
|
|
4498
5022
|
if (!Number.isInteger(index) || index < 0 || index >= group.frameUrls.length) continue;
|
|
4499
|
-
const { objectId, sourceUrl } = await provider
|
|
5023
|
+
const { objectId, sourceUrl } = await requireSelectCandidate(provider)(
|
|
4500
5024
|
entry.reviewObjectId,
|
|
4501
5025
|
index,
|
|
4502
5026
|
`asset:${entry.assetId}`,
|
|
@@ -4586,6 +5110,7 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
4586
5110
|
}
|
|
4587
5111
|
return {
|
|
4588
5112
|
name,
|
|
5113
|
+
provider: "pixellab",
|
|
4589
5114
|
styles: {
|
|
4590
5115
|
[styleId]: {
|
|
4591
5116
|
generator,
|
|
@@ -4598,7 +5123,8 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
4598
5123
|
styleImages: [],
|
|
4599
5124
|
palette: [],
|
|
4600
5125
|
outDir,
|
|
4601
|
-
tags: [name]
|
|
5126
|
+
tags: [name],
|
|
5127
|
+
providerOptions: {}
|
|
4602
5128
|
}
|
|
4603
5129
|
},
|
|
4604
5130
|
assets
|
|
@@ -4956,7 +5482,7 @@ refresh();
|
|
|
4956
5482
|
}
|
|
4957
5483
|
|
|
4958
5484
|
// src/pick/salvage-server.ts
|
|
4959
|
-
var
|
|
5485
|
+
var PNG_SIGNATURE2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
4960
5486
|
async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
4961
5487
|
const log = opts.onProgress ?? (() => {
|
|
4962
5488
|
});
|
|
@@ -4988,7 +5514,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
4988
5514
|
if (decision.action === "import") {
|
|
4989
5515
|
try {
|
|
4990
5516
|
const buf = await provider.download(orphan.previewUrl);
|
|
4991
|
-
if (!buf.subarray(0, 8).equals(
|
|
5517
|
+
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
4992
5518
|
decodePng(buf);
|
|
4993
5519
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
4994
5520
|
const rel = import_node_path16.default.join("_salvaged", `${assetId}.png`);
|
|
@@ -5221,7 +5747,6 @@ function emptyStateCounts() {
|
|
|
5221
5747
|
}
|
|
5222
5748
|
async function workspaceStatus(ws, dir) {
|
|
5223
5749
|
const diagnostics = validateWorkspace(ws, dir);
|
|
5224
|
-
const provider = PixelLabProvider.forOffline();
|
|
5225
5750
|
const projects = [];
|
|
5226
5751
|
const totalsByState = emptyStateCounts();
|
|
5227
5752
|
const totalsSpend = { generations: 0, usd: 0, free: 0 };
|
|
@@ -5235,6 +5760,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
5235
5760
|
lock: lockPath
|
|
5236
5761
|
};
|
|
5237
5762
|
try {
|
|
5763
|
+
const provider = createProvider(project.provider, "offline");
|
|
5238
5764
|
const loaded = await loadManifest(manifestPath);
|
|
5239
5765
|
const specs = await resolveSpecs(loaded, { provider });
|
|
5240
5766
|
const lock = await loadLock(lockPath);
|
|
@@ -5246,7 +5772,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
5246
5772
|
totalsByState[state] += byState[state];
|
|
5247
5773
|
}
|
|
5248
5774
|
for (const unit of Object.keys(spend)) {
|
|
5249
|
-
totalsSpend[unit]
|
|
5775
|
+
totalsSpend[unit] = (totalsSpend[unit] ?? 0) + (spend[unit] ?? 0);
|
|
5250
5776
|
}
|
|
5251
5777
|
projects.push({
|
|
5252
5778
|
...base,
|
|
@@ -5291,9 +5817,11 @@ async function workspaceStatus(ws, dir) {
|
|
|
5291
5817
|
MAX_DOWNLOAD_BYTES,
|
|
5292
5818
|
MAX_RETRIES,
|
|
5293
5819
|
ManifestSchema,
|
|
5820
|
+
MediaType,
|
|
5294
5821
|
PixelLabClient,
|
|
5295
5822
|
PixelLabError,
|
|
5296
5823
|
PixelLabProvider,
|
|
5824
|
+
RetroDiffusionProvider,
|
|
5297
5825
|
StyleSchema,
|
|
5298
5826
|
UnsupportedCapabilityError,
|
|
5299
5827
|
WorkspaceProjectSchema,
|
|
@@ -5301,16 +5829,20 @@ async function workspaceStatus(ws, dir) {
|
|
|
5301
5829
|
adopt,
|
|
5302
5830
|
applyTags,
|
|
5303
5831
|
auditStyle,
|
|
5832
|
+
availableProviders,
|
|
5304
5833
|
backoffMs,
|
|
5305
5834
|
buildManifest,
|
|
5306
5835
|
buildPlan,
|
|
5836
|
+
cacheFileName,
|
|
5307
5837
|
candidateCount,
|
|
5308
5838
|
clientFromEnv,
|
|
5309
5839
|
colorDistance,
|
|
5310
5840
|
countNumberedDescriptions,
|
|
5311
5841
|
createArtifactBundleManifest,
|
|
5842
|
+
createProvider,
|
|
5312
5843
|
currentEntryOutputPath,
|
|
5313
5844
|
currentOutputPath,
|
|
5845
|
+
detectMediaType,
|
|
5314
5846
|
doctor,
|
|
5315
5847
|
evaluateAudit,
|
|
5316
5848
|
expectedOutputPath,
|
|
@@ -5333,6 +5865,8 @@ async function workspaceStatus(ws, dir) {
|
|
|
5333
5865
|
lockKey,
|
|
5334
5866
|
matchOrphanStyle,
|
|
5335
5867
|
measureBalanceChange,
|
|
5868
|
+
mediaExtension,
|
|
5869
|
+
mediaTypeFromExtension,
|
|
5336
5870
|
mergePalettes,
|
|
5337
5871
|
mountSprites,
|
|
5338
5872
|
mountStyle,
|
|
@@ -5349,12 +5883,16 @@ async function workspaceStatus(ws, dir) {
|
|
|
5349
5883
|
poll,
|
|
5350
5884
|
portableOutputPath,
|
|
5351
5885
|
primaryOutput,
|
|
5886
|
+
providerFactory,
|
|
5352
5887
|
pushTags,
|
|
5888
|
+
registerProvider,
|
|
5353
5889
|
remove,
|
|
5354
5890
|
renderSalvageSheet,
|
|
5355
5891
|
renderSheet,
|
|
5892
|
+
requireBalance,
|
|
5356
5893
|
requireDelete,
|
|
5357
5894
|
requireList,
|
|
5895
|
+
requireSelectCandidate,
|
|
5358
5896
|
resolveEntryOutputs,
|
|
5359
5897
|
resolveOutputPath,
|
|
5360
5898
|
resolvePackInputs,
|
|
@@ -5388,6 +5926,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
5388
5926
|
totalSpend,
|
|
5389
5927
|
upsert,
|
|
5390
5928
|
validateCostEstimate,
|
|
5929
|
+
validateMedia,
|
|
5391
5930
|
validateWorkspace,
|
|
5392
5931
|
verifyArtifactBundle,
|
|
5393
5932
|
withArtifactManifest,
|