pixelkiln 0.2.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 +50 -1
- package/NAMING.md +15 -15
- package/PROVIDERS.md +128 -90
- package/README.md +57 -16
- package/SECURITY.md +4 -3
- package/dist/cli.d.ts +18 -1
- package/dist/cli.js +1248 -281
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +911 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +547 -228
- package/dist/index.d.ts +547 -228
- package/dist/index.js +887 -102
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +13 -6
- package/docs/ARCHITECTURE.md +27 -15
- package/docs/CLI.md +57 -4
- package/docs/ENDPOINTS.md +39 -38
- package/docs/GENERATORS.md +6 -1
- 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 +46 -1
- package/docs/RETRO_DIFFUSION.md +110 -0
- package/docs/TILES.md +1 -1
- package/examples/minimal/README.md +2 -2
- package/package.json +3 -1
- package/schema/manifest.schema.json +15 -1
- package/schema/workspace.schema.json +54 -0
- package/skills/pixelkiln/SKILL.md +15 -8
package/dist/index.cjs
CHANGED
|
@@ -40,24 +40,32 @@ __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,
|
|
50
|
+
WorkspaceProjectSchema: () => WorkspaceProjectSchema,
|
|
51
|
+
WorkspaceSchema: () => WorkspaceSchema,
|
|
48
52
|
adopt: () => adopt,
|
|
49
53
|
applyTags: () => applyTags,
|
|
50
54
|
auditStyle: () => auditStyle,
|
|
55
|
+
availableProviders: () => availableProviders,
|
|
51
56
|
backoffMs: () => backoffMs,
|
|
52
57
|
buildManifest: () => buildManifest,
|
|
53
58
|
buildPlan: () => buildPlan,
|
|
59
|
+
cacheFileName: () => cacheFileName,
|
|
54
60
|
candidateCount: () => candidateCount,
|
|
55
61
|
clientFromEnv: () => clientFromEnv,
|
|
56
62
|
colorDistance: () => colorDistance,
|
|
57
63
|
countNumberedDescriptions: () => countNumberedDescriptions,
|
|
58
64
|
createArtifactBundleManifest: () => createArtifactBundleManifest,
|
|
65
|
+
createProvider: () => createProvider,
|
|
59
66
|
currentEntryOutputPath: () => currentEntryOutputPath,
|
|
60
67
|
currentOutputPath: () => currentOutputPath,
|
|
68
|
+
detectMediaType: () => detectMediaType,
|
|
61
69
|
doctor: () => doctor,
|
|
62
70
|
evaluateAudit: () => evaluateAudit,
|
|
63
71
|
expectedOutputPath: () => expectedOutputPath,
|
|
@@ -75,9 +83,13 @@ __export(index_exports, {
|
|
|
75
83
|
loadClaims: () => loadClaims,
|
|
76
84
|
loadLock: () => loadLock,
|
|
77
85
|
loadManifest: () => loadManifest,
|
|
86
|
+
loadSiblingManifests: () => loadSiblingManifests,
|
|
87
|
+
loadWorkspace: () => loadWorkspace,
|
|
78
88
|
lockKey: () => lockKey,
|
|
79
89
|
matchOrphanStyle: () => matchOrphanStyle,
|
|
80
90
|
measureBalanceChange: () => measureBalanceChange,
|
|
91
|
+
mediaExtension: () => mediaExtension,
|
|
92
|
+
mediaTypeFromExtension: () => mediaTypeFromExtension,
|
|
81
93
|
mergePalettes: () => mergePalettes,
|
|
82
94
|
mountSprites: () => mountSprites,
|
|
83
95
|
mountStyle: () => mountStyle,
|
|
@@ -89,19 +101,25 @@ __export(index_exports, {
|
|
|
89
101
|
packStyle: () => packStyle,
|
|
90
102
|
paletteDistance: () => paletteDistance,
|
|
91
103
|
parseLock: () => parseLock,
|
|
104
|
+
parseWorkspace: () => parseWorkspace,
|
|
92
105
|
pngSize: () => pngSize,
|
|
93
106
|
poll: () => poll,
|
|
94
107
|
portableOutputPath: () => portableOutputPath,
|
|
95
108
|
primaryOutput: () => primaryOutput,
|
|
109
|
+
providerFactory: () => providerFactory,
|
|
96
110
|
pushTags: () => pushTags,
|
|
111
|
+
registerProvider: () => registerProvider,
|
|
97
112
|
remove: () => remove,
|
|
98
113
|
renderSalvageSheet: () => renderSalvageSheet,
|
|
99
114
|
renderSheet: () => renderSheet,
|
|
115
|
+
requireBalance: () => requireBalance,
|
|
100
116
|
requireDelete: () => requireDelete,
|
|
101
117
|
requireList: () => requireList,
|
|
118
|
+
requireSelectCandidate: () => requireSelectCandidate,
|
|
102
119
|
resolveEntryOutputs: () => resolveEntryOutputs,
|
|
103
120
|
resolveOutputPath: () => resolveOutputPath,
|
|
104
121
|
resolvePackInputs: () => resolvePackInputs,
|
|
122
|
+
resolveProject: () => resolveProject,
|
|
105
123
|
resolveSpecEntryOutputs: () => resolveSpecEntryOutputs,
|
|
106
124
|
resolveSpecOutputs: () => resolveSpecOutputs,
|
|
107
125
|
resolveSpecs: () => resolveSpecs,
|
|
@@ -111,6 +129,7 @@ __export(index_exports, {
|
|
|
111
129
|
runPicker: () => runPicker,
|
|
112
130
|
runSalvage: () => runSalvage,
|
|
113
131
|
saveLock: () => saveLock,
|
|
132
|
+
saveWorkspace: () => saveWorkspace,
|
|
114
133
|
scanAssets: () => scanAssets,
|
|
115
134
|
selectEntryOutput: () => selectEntryOutput,
|
|
116
135
|
sha256: () => sha256,
|
|
@@ -126,11 +145,16 @@ __export(index_exports, {
|
|
|
126
145
|
tileFeatureOutputCount: () => tileFeatureOutputCount,
|
|
127
146
|
tileVariationCount: () => tileVariationCount,
|
|
128
147
|
tilesCost: () => tilesCost,
|
|
148
|
+
toPortablePath: () => toPortablePath,
|
|
129
149
|
totalSpend: () => totalSpend,
|
|
130
150
|
upsert: () => upsert,
|
|
131
151
|
validateCostEstimate: () => validateCostEstimate,
|
|
152
|
+
validateMedia: () => validateMedia,
|
|
153
|
+
validateWorkspace: () => validateWorkspace,
|
|
132
154
|
verifyArtifactBundle: () => verifyArtifactBundle,
|
|
133
155
|
withArtifactManifest: () => withArtifactManifest,
|
|
156
|
+
workspaceClaims: () => workspaceClaims,
|
|
157
|
+
workspaceStatus: () => workspaceStatus,
|
|
134
158
|
writeArtifactBundle: () => writeArtifactBundle,
|
|
135
159
|
writeManagedArtifactBundle: () => writeManagedArtifactBundle
|
|
136
160
|
});
|
|
@@ -138,7 +162,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
138
162
|
|
|
139
163
|
// src/types.ts
|
|
140
164
|
var import_zod = require("zod");
|
|
141
|
-
var
|
|
165
|
+
var MediaTypeSchema = import_zod.z.enum(["image/png", "image/gif"]);
|
|
166
|
+
var GeneratorSchema = import_zod.z.enum(["1dir", "map", "pixflux", "tiles", "animation"]);
|
|
142
167
|
function tileVariationCount(descriptions) {
|
|
143
168
|
return Math.max(1, descriptions) * 4;
|
|
144
169
|
}
|
|
@@ -170,7 +195,7 @@ function tilesCost(tileSize, variations) {
|
|
|
170
195
|
return 40;
|
|
171
196
|
}
|
|
172
197
|
var StyleImageSchema = import_zod.z.object({
|
|
173
|
-
/** Path to a PNG/JPEG, relative to the manifest
|
|
198
|
+
/** Path to a PNG/JPEG, relative to the manifest; the active provider validates limits. */
|
|
174
199
|
path: import_zod.z.string()
|
|
175
200
|
});
|
|
176
201
|
var StyleSchema = import_zod.z.object({
|
|
@@ -281,7 +306,9 @@ var StyleSchema = import_zod.z.object({
|
|
|
281
306
|
out: import_zod.z.string()
|
|
282
307
|
}).strict().optional(),
|
|
283
308
|
/** Tags applied to every object generated in this style, for server-side filtering. */
|
|
284
|
-
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({})
|
|
285
312
|
}).strict().refine((s) => !(s.tileFeature && s.styleImages.length), {
|
|
286
313
|
message: "tileFeature and styleImages cannot be combined \u2014 a connectable set derives its own tile geometry, so remove one or the other",
|
|
287
314
|
path: ["tileFeature"]
|
|
@@ -296,7 +323,7 @@ var AssetSchema = import_zod.z.object({
|
|
|
296
323
|
height: import_zod.z.number().int().min(16).max(400).optional(),
|
|
297
324
|
/** Overrides the style default. `1dir` generator only. */
|
|
298
325
|
size: import_zod.z.number().int().min(32).max(256).optional(),
|
|
299
|
-
/** Explicit output path relative to outDir.
|
|
326
|
+
/** Explicit output path relative to outDir. Media-aware providers may replace its extension. */
|
|
300
327
|
file: import_zod.z.string().optional(),
|
|
301
328
|
/**
|
|
302
329
|
* Grid cell this asset owns in a mounted style, as [column, row].
|
|
@@ -348,6 +375,8 @@ var AssetSchema = import_zod.z.object({
|
|
|
348
375
|
var ManifestSchema = import_zod.z.object({
|
|
349
376
|
$schema: import_zod.z.string().optional(),
|
|
350
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"),
|
|
351
380
|
styles: import_zod.z.record(StyleSchema),
|
|
352
381
|
assets: import_zod.z.record(AssetSchema)
|
|
353
382
|
}).strict();
|
|
@@ -385,7 +414,8 @@ var LockEntrySchema = import_zod.z.object({
|
|
|
385
414
|
sourceUrls: import_zod.z.array(
|
|
386
415
|
import_zod.z.object({
|
|
387
416
|
url: import_zod.z.string(),
|
|
388
|
-
role: import_zod.z.string().optional()
|
|
417
|
+
role: import_zod.z.string().optional(),
|
|
418
|
+
mediaType: MediaTypeSchema.optional()
|
|
389
419
|
})
|
|
390
420
|
).default([]),
|
|
391
421
|
/**
|
|
@@ -400,7 +430,8 @@ var LockEntrySchema = import_zod.z.object({
|
|
|
400
430
|
import_zod.z.object({
|
|
401
431
|
path: import_zod.z.string(),
|
|
402
432
|
sha256: import_zod.z.string(),
|
|
403
|
-
role: import_zod.z.string().optional()
|
|
433
|
+
role: import_zod.z.string().optional(),
|
|
434
|
+
mediaType: MediaTypeSchema.optional()
|
|
404
435
|
})
|
|
405
436
|
).default([]),
|
|
406
437
|
/**
|
|
@@ -414,7 +445,7 @@ var LockEntrySchema = import_zod.z.object({
|
|
|
414
445
|
/** Successful-submission estimate in `costUnit`; may be fractional USD. */
|
|
415
446
|
cost: import_zod.z.number().finite().nonnegative().default(0),
|
|
416
447
|
/** Unit for `cost`. Defaults preserve pre-unit PixelLab lockfiles. */
|
|
417
|
-
costUnit: import_zod.z.
|
|
448
|
+
costUnit: import_zod.z.string().min(1).default("generations"),
|
|
418
449
|
/** Which provider produced this. Absent on entries written before providers. */
|
|
419
450
|
provider: import_zod.z.string().default("pixellab")
|
|
420
451
|
});
|
|
@@ -547,11 +578,11 @@ var PixelLabClient = class {
|
|
|
547
578
|
* common than the failure mode of retrying (a duplicate object), and a
|
|
548
579
|
* duplicate is visible and free to delete whereas a silent gap is neither.
|
|
549
580
|
*/
|
|
550
|
-
async request(
|
|
581
|
+
async request(path17, init, attempt = 0) {
|
|
551
582
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
552
583
|
let res;
|
|
553
584
|
try {
|
|
554
|
-
res = await fetch(`${BASE}${
|
|
585
|
+
res = await fetch(`${BASE}${path17}`, {
|
|
555
586
|
...init,
|
|
556
587
|
signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
557
588
|
headers: {
|
|
@@ -563,24 +594,24 @@ var PixelLabClient = class {
|
|
|
563
594
|
} catch (err) {
|
|
564
595
|
if (attempt < MAX_RETRIES) {
|
|
565
596
|
await sleep(backoffMs(attempt));
|
|
566
|
-
return this.request(
|
|
597
|
+
return this.request(path17, init, attempt + 1);
|
|
567
598
|
}
|
|
568
599
|
throw err;
|
|
569
600
|
}
|
|
570
601
|
if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
|
|
571
602
|
const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
|
|
572
603
|
await sleep(waitMs);
|
|
573
|
-
return this.request(
|
|
604
|
+
return this.request(path17, init, attempt + 1);
|
|
574
605
|
}
|
|
575
606
|
const text = await res.text();
|
|
576
607
|
if (!res.ok) {
|
|
577
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
608
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path17} \u2192 ${res.status}`, res.status, text);
|
|
578
609
|
}
|
|
579
610
|
if (!text) return {};
|
|
580
611
|
try {
|
|
581
612
|
return JSON.parse(text);
|
|
582
613
|
} catch {
|
|
583
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
614
|
+
throw new Error(`${init?.method ?? "GET"} ${path17} returned invalid JSON`);
|
|
584
615
|
}
|
|
585
616
|
}
|
|
586
617
|
async balance() {
|
|
@@ -788,7 +819,7 @@ function validateCostEstimate(providerId, value) {
|
|
|
788
819
|
throw new Error(`Provider "${providerId}" returned an invalid cost estimate`);
|
|
789
820
|
}
|
|
790
821
|
const estimate = value;
|
|
791
|
-
if (
|
|
822
|
+
if (typeof estimate.unit !== "string" || !estimate.unit.trim()) {
|
|
792
823
|
throw new Error(`Provider "${providerId}" returned an invalid cost unit`);
|
|
793
824
|
}
|
|
794
825
|
if (!Number.isFinite(estimate.amount) || estimate.amount < 0) {
|
|
@@ -830,18 +861,23 @@ function requireDelete(provider) {
|
|
|
830
861
|
if (!provider.delete) throw new UnsupportedCapabilityError(provider.id, "deleting remote assets");
|
|
831
862
|
return provider.delete.bind(provider);
|
|
832
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
|
+
}
|
|
833
874
|
function formatCost(unit, amount) {
|
|
834
875
|
if (unit === "free") return "free";
|
|
835
876
|
if (unit === "usd") return `$${amount.toFixed(2)}`;
|
|
836
|
-
return `${amount} generation${amount === 1 ? "" : "s"}`;
|
|
877
|
+
if (unit === "generations") return `${amount} generation${amount === 1 ? "" : "s"}`;
|
|
878
|
+
return `${amount} ${unit}`;
|
|
837
879
|
}
|
|
838
880
|
|
|
839
|
-
// src/providers/pixellab.ts
|
|
840
|
-
var import_node_fs = require("fs");
|
|
841
|
-
var import_node_crypto = require("crypto");
|
|
842
|
-
var import_node_os = __toESM(require("os"), 1);
|
|
843
|
-
var import_node_path = __toESM(require("path"), 1);
|
|
844
|
-
|
|
845
881
|
// src/png.ts
|
|
846
882
|
var import_node_zlib = require("zlib");
|
|
847
883
|
var SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
@@ -1199,7 +1235,96 @@ function parseHex(hex2) {
|
|
|
1199
1235
|
return { r: n >> 16 & 255, g: n >> 8 & 255, b: n & 255 };
|
|
1200
1236
|
}
|
|
1201
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
|
+
|
|
1202
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);
|
|
1203
1328
|
var PixelLabProvider = class _PixelLabProvider {
|
|
1204
1329
|
constructor(client) {
|
|
1205
1330
|
this.client = client;
|
|
@@ -1249,7 +1374,44 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
1249
1374
|
candidates: spec.generator === "1dir" ? candidateCount(spec.size) : 1
|
|
1250
1375
|
};
|
|
1251
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
|
+
}
|
|
1252
1413
|
async submit(spec, styleImages) {
|
|
1414
|
+
this.validate(spec, styleImages);
|
|
1253
1415
|
if (spec.generator === "pixflux") {
|
|
1254
1416
|
const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
1255
1417
|
const { png } = await this.client.createImagePixflux({
|
|
@@ -1435,6 +1597,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
1435
1597
|
await this.client.deleteObject(assetId);
|
|
1436
1598
|
}
|
|
1437
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
|
+
}
|
|
1438
1605
|
function tilesInIndexOrder(urls) {
|
|
1439
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);
|
|
1440
1607
|
}
|
|
@@ -1443,6 +1610,319 @@ function firstUrl(urls) {
|
|
|
1443
1610
|
return Object.values(urls).find((u) => typeof u === "string") ?? null;
|
|
1444
1611
|
}
|
|
1445
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
|
+
|
|
1446
1926
|
// src/providers/fake.ts
|
|
1447
1927
|
var import_node_crypto2 = require("crypto");
|
|
1448
1928
|
var FAKE_PNG = Buffer.from(
|
|
@@ -1587,6 +2067,47 @@ var FakeProvider = class {
|
|
|
1587
2067
|
}
|
|
1588
2068
|
};
|
|
1589
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
|
+
|
|
1590
2111
|
// src/manifest.ts
|
|
1591
2112
|
var import_promises2 = require("fs/promises");
|
|
1592
2113
|
var import_node_fs2 = require("fs");
|
|
@@ -1598,12 +2119,16 @@ var import_promises = require("fs/promises");
|
|
|
1598
2119
|
function sha256(data) {
|
|
1599
2120
|
return (0, import_node_crypto3.createHash)("sha256").update(data).digest("hex");
|
|
1600
2121
|
}
|
|
1601
|
-
async function sha256File(
|
|
1602
|
-
return sha256(await (0, import_promises.readFile)(
|
|
2122
|
+
async function sha256File(path17) {
|
|
2123
|
+
return sha256(await (0, import_promises.readFile)(path17));
|
|
1603
2124
|
}
|
|
1604
2125
|
function specHash(spec, styleImageHashes) {
|
|
1605
2126
|
return sha256(
|
|
1606
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,
|
|
1607
2132
|
generator: spec.generator,
|
|
1608
2133
|
prompt: spec.prompt,
|
|
1609
2134
|
width: spec.width,
|
|
@@ -1617,7 +2142,7 @@ function specHash(spec, styleImageHashes) {
|
|
|
1617
2142
|
// `noBackground` only reaches the wire for pixflux; the tile fields are
|
|
1618
2143
|
// undefined for every other generator. `tileSize` is intentionally
|
|
1619
2144
|
// absent — width/height are derived from it, so it is already covered.
|
|
1620
|
-
noBackground: spec.generator === "pixflux" ? spec.noBackground : void 0,
|
|
2145
|
+
noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
|
|
1621
2146
|
tileType: spec.tileType,
|
|
1622
2147
|
tileView: spec.tileView,
|
|
1623
2148
|
tileFeature: spec.tileFeature,
|
|
@@ -1657,6 +2182,7 @@ ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
|
1657
2182
|
}
|
|
1658
2183
|
async function resolveSpecs(loaded, filter) {
|
|
1659
2184
|
const { manifest, root } = loaded;
|
|
2185
|
+
const activeProvider = filter?.provider ?? createProvider(manifest.provider, "offline");
|
|
1660
2186
|
const specs = [];
|
|
1661
2187
|
const styleIds = Object.keys(manifest.styles).filter(
|
|
1662
2188
|
(id) => !filter?.styles?.length || filter.styles.includes(id)
|
|
@@ -1682,10 +2208,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1682
2208
|
const buf = await (0, import_promises2.readFile)(abs);
|
|
1683
2209
|
const metadata = imageMetadata(buf);
|
|
1684
2210
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
|
|
1685
|
-
if (metadata.width < 1 || metadata.height < 1
|
|
1686
|
-
throw new Error(
|
|
1687
|
-
`Style image exceeds the API's 256x256 limit: ${abs} (${metadata.width}x${metadata.height})`
|
|
1688
|
-
);
|
|
2211
|
+
if (metadata.width < 1 || metadata.height < 1) {
|
|
2212
|
+
throw new Error(`Style image has invalid dimensions: ${abs}`);
|
|
1689
2213
|
}
|
|
1690
2214
|
hit = { base64: buf.toString("base64"), hash: sha256(buf), ...metadata };
|
|
1691
2215
|
styleImageCache.set(abs, hit);
|
|
@@ -1705,8 +2229,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1705
2229
|
if (filter?.assets?.length && !filter.assets.includes(assetId)) continue;
|
|
1706
2230
|
if (asset.styles.length && !asset.styles.includes(styleId)) continue;
|
|
1707
2231
|
const generator = style.generator;
|
|
1708
|
-
if (
|
|
1709
|
-
throw new Error(`Provider "${
|
|
2232
|
+
if (!activeProvider.supports(generator)) {
|
|
2233
|
+
throw new Error(`Provider "${activeProvider.id}" does not support generator "${generator}"`);
|
|
1710
2234
|
}
|
|
1711
2235
|
let width;
|
|
1712
2236
|
let height;
|
|
@@ -1733,6 +2257,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1733
2257
|
const base = {
|
|
1734
2258
|
styleId,
|
|
1735
2259
|
assetId,
|
|
2260
|
+
provider: activeProvider.id,
|
|
2261
|
+
providerOptions: style.providerOptions[activeProvider.id] ?? {},
|
|
1736
2262
|
generator,
|
|
1737
2263
|
prompt,
|
|
1738
2264
|
width,
|
|
@@ -1764,11 +2290,6 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1764
2290
|
`style:${styleId}`
|
|
1765
2291
|
])
|
|
1766
2292
|
];
|
|
1767
|
-
if (tags.length > 20) {
|
|
1768
|
-
throw new Error(
|
|
1769
|
-
`${styleId}/${assetId} resolves to ${tags.length} tags, but PixelLab allows at most 20`
|
|
1770
|
-
);
|
|
1771
|
-
}
|
|
1772
2293
|
const resolved = {
|
|
1773
2294
|
...base,
|
|
1774
2295
|
root,
|
|
@@ -1777,12 +2298,15 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1777
2298
|
source: asset.source,
|
|
1778
2299
|
specHash: specHash(base, styleImageHashes)
|
|
1779
2300
|
};
|
|
1780
|
-
|
|
1781
|
-
const
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
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;
|
|
1786
2310
|
specs.push(resolved);
|
|
1787
2311
|
}
|
|
1788
2312
|
}
|
|
@@ -1806,10 +2330,8 @@ async function resolveStyleImages(loaded, styleId) {
|
|
|
1806
2330
|
const buf = await (0, import_promises2.readFile)(file);
|
|
1807
2331
|
const metadata = imageMetadata(buf);
|
|
1808
2332
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
1809
|
-
if (metadata.width < 1 || metadata.height < 1
|
|
1810
|
-
throw new Error(
|
|
1811
|
-
`Style image exceeds the API's 256x256 limit: ${file} (${metadata.width}x${metadata.height})`
|
|
1812
|
-
);
|
|
2333
|
+
if (metadata.width < 1 || metadata.height < 1) {
|
|
2334
|
+
throw new Error(`Style image has invalid dimensions: ${file}`);
|
|
1813
2335
|
}
|
|
1814
2336
|
out.push({ base64: buf.toString("base64"), ...metadata });
|
|
1815
2337
|
}
|
|
@@ -2011,12 +2533,13 @@ async function acquireFileLock(file) {
|
|
|
2011
2533
|
function spendByUnit(lock) {
|
|
2012
2534
|
const totals = { generations: 0, usd: 0, free: 0 };
|
|
2013
2535
|
for (const entry of Object.values(lock.entries)) {
|
|
2014
|
-
|
|
2536
|
+
const unit = entry.costUnit ?? "generations";
|
|
2537
|
+
totals[unit] = (totals[unit] ?? 0) + (entry.cost ?? 0);
|
|
2015
2538
|
}
|
|
2016
2539
|
return totals;
|
|
2017
2540
|
}
|
|
2018
2541
|
function totalSpend(lock, unit = "generations") {
|
|
2019
|
-
return spendByUnit(lock)[unit];
|
|
2542
|
+
return spendByUnit(lock)[unit] ?? 0;
|
|
2020
2543
|
}
|
|
2021
2544
|
|
|
2022
2545
|
// src/outputs.ts
|
|
@@ -2033,16 +2556,16 @@ function resolveOutputPath(recordedPath, manifestDir) {
|
|
|
2033
2556
|
if (import_node_path4.default.win32.isAbsolute(recordedPath)) return recordedPath;
|
|
2034
2557
|
return import_node_path4.default.resolve(manifestDir, recordedPath.split(/[\\/]/).join(import_node_path4.default.sep));
|
|
2035
2558
|
}
|
|
2036
|
-
function expectedOutputPath(spec, role, index, total) {
|
|
2037
|
-
if (total === 1) return spec.outFile;
|
|
2559
|
+
function expectedOutputPath(spec, role, index, total, mediaType) {
|
|
2038
2560
|
const originalExt = import_node_path4.default.extname(spec.outFile);
|
|
2039
|
-
const ext = originalExt || ".png";
|
|
2561
|
+
const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
|
|
2040
2562
|
const stem = originalExt ? spec.outFile.slice(0, -originalExt.length) : spec.outFile;
|
|
2563
|
+
if (total === 1) return `${stem}${ext}`;
|
|
2041
2564
|
const safeRole = (role ?? fallbackOutputRole(index)).replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
2042
2565
|
return `${stem}-${safeRole}${ext}`;
|
|
2043
2566
|
}
|
|
2044
2567
|
function currentOutputPath(output, spec, index, total) {
|
|
2045
|
-
return expectedOutputPath(spec, output.role, index, total);
|
|
2568
|
+
return expectedOutputPath(spec, output.role, index, total, output.mediaType);
|
|
2046
2569
|
}
|
|
2047
2570
|
function currentEntryOutputPath(entry, spec, index) {
|
|
2048
2571
|
const output = entry.outputs[index];
|
|
@@ -2843,10 +3366,10 @@ function isSha256Hash(value) {
|
|
|
2843
3366
|
function parseCache(value) {
|
|
2844
3367
|
return HashCacheSchema.parse(value);
|
|
2845
3368
|
}
|
|
2846
|
-
async function loadCache(
|
|
2847
|
-
if (!(0, import_node_fs6.existsSync)(
|
|
3369
|
+
async function loadCache(path17) {
|
|
3370
|
+
if (!(0, import_node_fs6.existsSync)(path17)) return { version: 1, hashes: {} };
|
|
2848
3371
|
try {
|
|
2849
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises6.readFile)(
|
|
3372
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises6.readFile)(path17, "utf8")));
|
|
2850
3373
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
2851
3374
|
return {
|
|
2852
3375
|
version: 1,
|
|
@@ -2858,18 +3381,18 @@ async function loadCache(path15) {
|
|
|
2858
3381
|
return { version: 1, hashes: {} };
|
|
2859
3382
|
}
|
|
2860
3383
|
}
|
|
2861
|
-
async function saveCache(
|
|
3384
|
+
async function saveCache(path17, cache) {
|
|
2862
3385
|
const sorted = {};
|
|
2863
3386
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
2864
3387
|
const hash = cache.hashes[key];
|
|
2865
3388
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
2866
3389
|
sorted[key] = hash;
|
|
2867
3390
|
}
|
|
2868
|
-
await (0, import_promises6.mkdir)(import_node_path8.default.dirname(import_node_path8.default.resolve(
|
|
2869
|
-
const tmp = `${
|
|
3391
|
+
await (0, import_promises6.mkdir)(import_node_path8.default.dirname(import_node_path8.default.resolve(path17)), { recursive: true });
|
|
3392
|
+
const tmp = `${path17}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
2870
3393
|
try {
|
|
2871
3394
|
await (0, import_promises6.writeFile)(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
2872
|
-
await (0, import_promises6.rename)(tmp,
|
|
3395
|
+
await (0, import_promises6.rename)(tmp, path17);
|
|
2873
3396
|
} finally {
|
|
2874
3397
|
await (0, import_promises6.rm)(tmp, { force: true });
|
|
2875
3398
|
}
|
|
@@ -2890,7 +3413,6 @@ function cachePathFor(lockPath) {
|
|
|
2890
3413
|
}
|
|
2891
3414
|
|
|
2892
3415
|
// src/pipeline/cache-health.ts
|
|
2893
|
-
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
2894
3416
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
2895
3417
|
if (options.prune && !(0, import_node_fs7.existsSync)(lockPath)) {
|
|
2896
3418
|
throw new Error(`Refusing to prune without an existing lockfile at ${import_node_path9.default.resolve(lockPath)}`);
|
|
@@ -2959,7 +3481,8 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
2959
3481
|
}
|
|
2960
3482
|
report.files++;
|
|
2961
3483
|
const file = import_node_path9.default.join(contentDir, entry.name);
|
|
2962
|
-
const
|
|
3484
|
+
const mediaType = mediaTypeFromExtension(entry.name);
|
|
3485
|
+
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
2963
3486
|
let bytes;
|
|
2964
3487
|
try {
|
|
2965
3488
|
bytes = await (0, import_promises7.readFile)(file);
|
|
@@ -2972,11 +3495,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
2972
3495
|
continue;
|
|
2973
3496
|
}
|
|
2974
3497
|
if (!isSha256Hash(expected)) {
|
|
2975
|
-
report.invalid.push({ name: entry.name, reason: "filename is not <sha256>.png" });
|
|
2976
|
-
continue;
|
|
2977
|
-
}
|
|
2978
|
-
if (!bytes.subarray(0, 8).equals(PNG_SIGNATURE)) {
|
|
2979
|
-
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" });
|
|
2980
3499
|
continue;
|
|
2981
3500
|
}
|
|
2982
3501
|
if (sha256(bytes) !== expected) {
|
|
@@ -2984,11 +3503,11 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
2984
3503
|
continue;
|
|
2985
3504
|
}
|
|
2986
3505
|
try {
|
|
2987
|
-
|
|
3506
|
+
validateMedia(bytes, mediaType);
|
|
2988
3507
|
} catch (err) {
|
|
2989
3508
|
report.invalid.push({
|
|
2990
3509
|
name: entry.name,
|
|
2991
|
-
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)}`
|
|
2992
3511
|
});
|
|
2993
3512
|
continue;
|
|
2994
3513
|
}
|
|
@@ -3577,7 +4096,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
3577
4096
|
async function pruneInFlight() {
|
|
3578
4097
|
for (const [id, spec] of [...inFlight]) {
|
|
3579
4098
|
try {
|
|
3580
|
-
const state = await provider.poll(id, spec.generator, spec);
|
|
4099
|
+
const state = await provider.poll(id, spec.generator, { spec, tileFeature: spec.tileFeature });
|
|
3581
4100
|
if (state.status !== "processing") inFlight.delete(id);
|
|
3582
4101
|
lastSlotError = null;
|
|
3583
4102
|
} catch (err) {
|
|
@@ -3629,7 +4148,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
3629
4148
|
await saveLock(lockPath, lock);
|
|
3630
4149
|
lastSubmitAt = Date.now();
|
|
3631
4150
|
try {
|
|
3632
|
-
const refs =
|
|
4151
|
+
const refs = styleImages.get(spec.styleId) ?? [];
|
|
3633
4152
|
const { jobId } = await provider.submit(spec, refs);
|
|
3634
4153
|
upsert(lock, key, {
|
|
3635
4154
|
jobId,
|
|
@@ -3679,7 +4198,8 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
3679
4198
|
try {
|
|
3680
4199
|
const currentSpec = specByKey.get(key);
|
|
3681
4200
|
const state = await provider.poll(entry.jobId, entry.generator, {
|
|
3682
|
-
tileFeature: entry.tileFeature ?? currentSpec?.tileFeature
|
|
4201
|
+
tileFeature: entry.tileFeature ?? currentSpec?.tileFeature,
|
|
4202
|
+
spec: currentSpec
|
|
3683
4203
|
});
|
|
3684
4204
|
if (state.status === "review") {
|
|
3685
4205
|
upsert(lock, key, { status: "review", reviewObjectId: entry.jobId });
|
|
@@ -3717,7 +4237,6 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
3717
4237
|
var import_node_fs9 = require("fs");
|
|
3718
4238
|
var import_promises8 = require("fs/promises");
|
|
3719
4239
|
var import_node_path11 = __toESM(require("path"), 1);
|
|
3720
|
-
var PNG_SIGNATURE2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
3721
4240
|
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
3722
4241
|
const log = opts.onProgress ?? (() => {
|
|
3723
4242
|
});
|
|
@@ -3749,7 +4268,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3749
4268
|
result.skipped++;
|
|
3750
4269
|
continue;
|
|
3751
4270
|
}
|
|
3752
|
-
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 })) : [];
|
|
3753
4272
|
if (!sources.length) {
|
|
3754
4273
|
upsert(lock, key, {
|
|
3755
4274
|
status: "download-failed",
|
|
@@ -3765,7 +4284,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3765
4284
|
const recorded = entry.outputs.find(
|
|
3766
4285
|
(o) => source.role ? o.role === source.role : !o.role && sources.length === 1
|
|
3767
4286
|
);
|
|
3768
|
-
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);
|
|
3769
4288
|
if ((0, import_node_fs9.existsSync)(target)) {
|
|
3770
4289
|
if (!recorded) {
|
|
3771
4290
|
throw new Error(`refusing to overwrite untracked output ${target}`);
|
|
@@ -3773,11 +4292,19 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3773
4292
|
if (await sha256File(target) !== recorded.sha256) {
|
|
3774
4293
|
throw new Error(`refusing to overwrite modified output ${target}`);
|
|
3775
4294
|
}
|
|
3776
|
-
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
|
+
}
|
|
3777
4303
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
3778
4304
|
continue;
|
|
3779
4305
|
}
|
|
3780
|
-
|
|
4306
|
+
const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
|
|
4307
|
+
let buf = recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null;
|
|
3781
4308
|
if (buf) {
|
|
3782
4309
|
log(` cached ${import_node_path11.default.relative(process.cwd(), target)}`);
|
|
3783
4310
|
} else {
|
|
@@ -3786,24 +4313,25 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3786
4313
|
}
|
|
3787
4314
|
buf = await provider.download(source.url);
|
|
3788
4315
|
}
|
|
3789
|
-
|
|
3790
|
-
throw new Error(`response for ${source.role ?? "asset"} was not a PNG (${buf.length} bytes)`);
|
|
3791
|
-
}
|
|
4316
|
+
let mediaType;
|
|
3792
4317
|
try {
|
|
3793
|
-
|
|
4318
|
+
mediaType = validateMedia(buf, expectedMediaType);
|
|
3794
4319
|
} catch (err) {
|
|
4320
|
+
const label = expectedMediaType === MediaType.GIF ? "GIF" : "PNG";
|
|
4321
|
+
const mismatch = detectMediaType(buf) !== expectedMediaType;
|
|
3795
4322
|
throw new Error(
|
|
3796
|
-
`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)}`)
|
|
3797
4324
|
);
|
|
3798
4325
|
}
|
|
3799
|
-
if (cacheDir) await
|
|
4326
|
+
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
3800
4327
|
await (0, import_promises8.mkdir)(import_node_path11.default.dirname(target), { recursive: true });
|
|
3801
4328
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
3802
4329
|
await (0, import_promises8.writeFile)(tmp, buf);
|
|
3803
4330
|
outputs.push({
|
|
3804
4331
|
path: portableOutputPath(target, spec.root),
|
|
3805
4332
|
sha256: sha256(buf),
|
|
3806
|
-
...source.role ? { role: source.role } : {}
|
|
4333
|
+
...source.role ? { role: source.role } : {},
|
|
4334
|
+
mediaType
|
|
3807
4335
|
});
|
|
3808
4336
|
upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
|
|
3809
4337
|
await saveLock(lockPath, lock);
|
|
@@ -3840,22 +4368,23 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3840
4368
|
await saveLock(lockPath, lock);
|
|
3841
4369
|
return result;
|
|
3842
4370
|
}
|
|
3843
|
-
async function
|
|
3844
|
-
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));
|
|
3845
4373
|
if (!(0, import_node_fs9.existsSync)(file)) return null;
|
|
3846
4374
|
try {
|
|
3847
4375
|
const buf = await (0, import_promises8.readFile)(file);
|
|
3848
|
-
if (
|
|
3849
|
-
|
|
4376
|
+
if (sha256(buf) !== hash) return null;
|
|
4377
|
+
validateMedia(buf, mediaType);
|
|
3850
4378
|
return buf;
|
|
3851
4379
|
} catch {
|
|
3852
4380
|
return null;
|
|
3853
4381
|
}
|
|
3854
4382
|
}
|
|
3855
|
-
async function
|
|
4383
|
+
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
3856
4384
|
const hash = knownHash ?? sha256(buf);
|
|
3857
|
-
|
|
3858
|
-
|
|
4385
|
+
validateMedia(buf, mediaType);
|
|
4386
|
+
const file = import_node_path11.default.join(cacheDir, cacheFileName(hash, mediaType));
|
|
4387
|
+
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
3859
4388
|
await (0, import_promises8.mkdir)(cacheDir, { recursive: true });
|
|
3860
4389
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3861
4390
|
await (0, import_promises8.writeFile)(tmp, buf);
|
|
@@ -3960,7 +4489,10 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
3960
4489
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
3961
4490
|
let hasCache = false;
|
|
3962
4491
|
for (const output of entry.outputs) {
|
|
3963
|
-
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
|
+
);
|
|
3964
4496
|
if ((0, import_node_fs10.existsSync)(cached) && await sha256File(cached) === output.sha256) {
|
|
3965
4497
|
hasCache = true;
|
|
3966
4498
|
break;
|
|
@@ -3990,11 +4522,14 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
3990
4522
|
add(
|
|
3991
4523
|
"provider",
|
|
3992
4524
|
"error",
|
|
3993
|
-
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"
|
|
3994
4526
|
);
|
|
4527
|
+
} else if (!opts.provider.balance) {
|
|
4528
|
+
add("provider", "ok", `${opts.provider.id} configured; balance reporting unavailable`);
|
|
3995
4529
|
} else {
|
|
4530
|
+
const balanceFn = opts.provider.balance.bind(opts.provider);
|
|
3996
4531
|
try {
|
|
3997
|
-
const balance = await
|
|
4532
|
+
const balance = await balanceFn();
|
|
3998
4533
|
add("provider", "ok", `${opts.provider.id} reachable; ${balance.remaining} ${balance.unit} remaining`);
|
|
3999
4534
|
} catch (err) {
|
|
4000
4535
|
add("provider", "error", `provider connectivity failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -4485,7 +5020,7 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
4485
5020
|
const entry = lock.entries[key];
|
|
4486
5021
|
if (!group || !entry?.reviewObjectId) continue;
|
|
4487
5022
|
if (!Number.isInteger(index) || index < 0 || index >= group.frameUrls.length) continue;
|
|
4488
|
-
const { objectId, sourceUrl } = await provider
|
|
5023
|
+
const { objectId, sourceUrl } = await requireSelectCandidate(provider)(
|
|
4489
5024
|
entry.reviewObjectId,
|
|
4490
5025
|
index,
|
|
4491
5026
|
`asset:${entry.assetId}`,
|
|
@@ -4575,6 +5110,7 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
4575
5110
|
}
|
|
4576
5111
|
return {
|
|
4577
5112
|
name,
|
|
5113
|
+
provider: "pixellab",
|
|
4578
5114
|
styles: {
|
|
4579
5115
|
[styleId]: {
|
|
4580
5116
|
generator,
|
|
@@ -4587,7 +5123,8 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
4587
5123
|
styleImages: [],
|
|
4588
5124
|
palette: [],
|
|
4589
5125
|
outDir,
|
|
4590
|
-
tags: [name]
|
|
5126
|
+
tags: [name],
|
|
5127
|
+
providerOptions: {}
|
|
4591
5128
|
}
|
|
4592
5129
|
},
|
|
4593
5130
|
assets
|
|
@@ -4602,6 +5139,7 @@ function mode(values) {
|
|
|
4602
5139
|
// src/pipeline/salvage.ts
|
|
4603
5140
|
var import_promises12 = require("fs/promises");
|
|
4604
5141
|
var import_node_fs13 = require("fs");
|
|
5142
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
4605
5143
|
async function loadClaims(lockPaths) {
|
|
4606
5144
|
const claimed = /* @__PURE__ */ new Set();
|
|
4607
5145
|
for (const p of lockPaths) {
|
|
@@ -4654,6 +5192,26 @@ function matchOrphanStyle(prompt, manifest) {
|
|
|
4654
5192
|
if (styleIds.length <= 1) return styleIds[0] ?? null;
|
|
4655
5193
|
return matchStyleByPattern(prompt, manifest);
|
|
4656
5194
|
}
|
|
5195
|
+
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
5196
|
+
const own = import_node_path15.default.resolve(ownManifestPath);
|
|
5197
|
+
const siblingManifestPaths = [
|
|
5198
|
+
.../* @__PURE__ */ new Set([
|
|
5199
|
+
...workspaceManifestPaths,
|
|
5200
|
+
...claimPaths.map((c) => import_node_path15.default.join(import_node_path15.default.dirname(import_node_path15.default.resolve(c)), "pixelkiln.manifest.json"))
|
|
5201
|
+
])
|
|
5202
|
+
];
|
|
5203
|
+
const siblings = [];
|
|
5204
|
+
for (const siblingManifestPath of siblingManifestPaths) {
|
|
5205
|
+
if (import_node_path15.default.resolve(siblingManifestPath) === own) continue;
|
|
5206
|
+
if (!(0, import_node_fs13.existsSync)(siblingManifestPath)) continue;
|
|
5207
|
+
try {
|
|
5208
|
+
const { manifest } = await loadManifest(siblingManifestPath);
|
|
5209
|
+
siblings.push({ label: import_node_path15.default.basename(import_node_path15.default.dirname(siblingManifestPath)), manifest });
|
|
5210
|
+
} catch {
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
5213
|
+
return siblings;
|
|
5214
|
+
}
|
|
4657
5215
|
function groupOrphansByStyle(orphans, manifest, siblings = []) {
|
|
4658
5216
|
const matched = /* @__PURE__ */ new Map();
|
|
4659
5217
|
const elsewhere = /* @__PURE__ */ new Map();
|
|
@@ -4765,7 +5323,7 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
4765
5323
|
|
|
4766
5324
|
// src/pick/salvage-server.ts
|
|
4767
5325
|
var import_promises13 = require("fs/promises");
|
|
4768
|
-
var
|
|
5326
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
4769
5327
|
|
|
4770
5328
|
// src/pick/salvage-sheet.ts
|
|
4771
5329
|
var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -4924,13 +5482,13 @@ refresh();
|
|
|
4924
5482
|
}
|
|
4925
5483
|
|
|
4926
5484
|
// src/pick/salvage-server.ts
|
|
4927
|
-
var
|
|
5485
|
+
var PNG_SIGNATURE2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
4928
5486
|
async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
4929
5487
|
const log = opts.onProgress ?? (() => {
|
|
4930
5488
|
});
|
|
4931
5489
|
const html = renderSalvageSheet(orphans, {
|
|
4932
5490
|
styleId: ctx.styleId,
|
|
4933
|
-
importDir:
|
|
5491
|
+
importDir: import_node_path16.default.relative(process.cwd(), ctx.importDir) || "."
|
|
4934
5492
|
});
|
|
4935
5493
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
4936
5494
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -4956,12 +5514,12 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
4956
5514
|
if (decision.action === "import") {
|
|
4957
5515
|
try {
|
|
4958
5516
|
const buf = await provider.download(orphan.previewUrl);
|
|
4959
|
-
if (!buf.subarray(0, 8).equals(
|
|
5517
|
+
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
4960
5518
|
decodePng(buf);
|
|
4961
5519
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
4962
|
-
const rel =
|
|
4963
|
-
const outFile =
|
|
4964
|
-
await (0, import_promises13.mkdir)(
|
|
5520
|
+
const rel = import_node_path16.default.join("_salvaged", `${assetId}.png`);
|
|
5521
|
+
const outFile = import_node_path16.default.resolve(ctx.importDir, rel);
|
|
5522
|
+
await (0, import_promises13.mkdir)(import_node_path16.default.dirname(outFile), { recursive: true });
|
|
4965
5523
|
await (0, import_promises13.writeFile)(outFile, buf);
|
|
4966
5524
|
ctx.manifest.assets[assetId] = {
|
|
4967
5525
|
prompt: orphan.prompt,
|
|
@@ -4988,7 +5546,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
4988
5546
|
error: null,
|
|
4989
5547
|
sourceUrl: orphan.previewUrl,
|
|
4990
5548
|
outputs: [{
|
|
4991
|
-
path: portableOutputPath(outFile,
|
|
5549
|
+
path: portableOutputPath(outFile, import_node_path16.default.dirname(ctx.manifestPath)),
|
|
4992
5550
|
sha256: sha256(buf)
|
|
4993
5551
|
}],
|
|
4994
5552
|
submittedAt: orphan.createdAt,
|
|
@@ -5020,6 +5578,233 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5020
5578
|
}
|
|
5021
5579
|
});
|
|
5022
5580
|
}
|
|
5581
|
+
|
|
5582
|
+
// src/workspace.ts
|
|
5583
|
+
var import_promises14 = require("fs/promises");
|
|
5584
|
+
var import_node_fs14 = require("fs");
|
|
5585
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
5586
|
+
var import_zod4 = require("zod");
|
|
5587
|
+
var WorkspaceProjectSchema = import_zod4.z.object({
|
|
5588
|
+
id: import_zod4.z.string().min(1),
|
|
5589
|
+
/** Manifest path, relative to the catalog file's own directory. */
|
|
5590
|
+
manifest: import_zod4.z.string().min(1),
|
|
5591
|
+
/** Lockfile path, relative to the catalog file's own directory. */
|
|
5592
|
+
lock: import_zod4.z.string().min(1),
|
|
5593
|
+
provider: import_zod4.z.string().min(1).default("pixellab"),
|
|
5594
|
+
/** Free-form label for a shared account, e.g. distinguishing sandboxes. */
|
|
5595
|
+
account: import_zod4.z.string().optional()
|
|
5596
|
+
}).strict();
|
|
5597
|
+
var WorkspaceSchema = import_zod4.z.object({
|
|
5598
|
+
version: import_zod4.z.literal(1),
|
|
5599
|
+
projects: import_zod4.z.array(WorkspaceProjectSchema).default([])
|
|
5600
|
+
}).strict();
|
|
5601
|
+
function parseWorkspace(raw) {
|
|
5602
|
+
const parsed = WorkspaceSchema.safeParse(raw);
|
|
5603
|
+
if (parsed.success) return parsed.data;
|
|
5604
|
+
throw new Error(
|
|
5605
|
+
`Workspace catalog is not valid v1:
|
|
5606
|
+
${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")}`
|
|
5607
|
+
);
|
|
5608
|
+
}
|
|
5609
|
+
async function loadWorkspace(workspacePath) {
|
|
5610
|
+
if (!(0, import_node_fs14.existsSync)(workspacePath)) return { version: 1, projects: [] };
|
|
5611
|
+
let raw;
|
|
5612
|
+
try {
|
|
5613
|
+
raw = JSON.parse(await (0, import_promises14.readFile)(workspacePath, "utf8"));
|
|
5614
|
+
} catch (err) {
|
|
5615
|
+
throw new Error(
|
|
5616
|
+
`Workspace catalog at ${workspacePath} is malformed:
|
|
5617
|
+
${err instanceof Error ? err.message : String(err)}`
|
|
5618
|
+
);
|
|
5619
|
+
}
|
|
5620
|
+
return parseWorkspace(raw);
|
|
5621
|
+
}
|
|
5622
|
+
async function saveWorkspace(workspacePath, ws) {
|
|
5623
|
+
const sorted = {
|
|
5624
|
+
version: 1,
|
|
5625
|
+
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
5626
|
+
};
|
|
5627
|
+
await (0, import_promises14.mkdir)(import_node_path17.default.dirname(import_node_path17.default.resolve(workspacePath)), { recursive: true });
|
|
5628
|
+
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
5629
|
+
try {
|
|
5630
|
+
await (0, import_promises14.writeFile)(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
5631
|
+
await (0, import_promises14.rename)(tmp, workspacePath);
|
|
5632
|
+
} finally {
|
|
5633
|
+
await (0, import_promises14.rm)(tmp, { force: true });
|
|
5634
|
+
}
|
|
5635
|
+
}
|
|
5636
|
+
function toPortablePath(dir, absolute) {
|
|
5637
|
+
return import_node_path17.default.relative(dir, absolute).split(import_node_path17.default.sep).join("/");
|
|
5638
|
+
}
|
|
5639
|
+
function resolveProject(dir, project) {
|
|
5640
|
+
return {
|
|
5641
|
+
manifestPath: import_node_path17.default.resolve(dir, project.manifest.split("/").join(import_node_path17.default.sep)),
|
|
5642
|
+
lockPath: import_node_path17.default.resolve(dir, project.lock.split("/").join(import_node_path17.default.sep))
|
|
5643
|
+
};
|
|
5644
|
+
}
|
|
5645
|
+
function validateWorkspace(ws, dir) {
|
|
5646
|
+
const diagnostics = [];
|
|
5647
|
+
const idCounts = /* @__PURE__ */ new Map();
|
|
5648
|
+
const lockOwners = /* @__PURE__ */ new Map();
|
|
5649
|
+
const manifestOwners = /* @__PURE__ */ new Map();
|
|
5650
|
+
for (const project of ws.projects) {
|
|
5651
|
+
idCounts.set(project.id, (idCounts.get(project.id) ?? 0) + 1);
|
|
5652
|
+
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
5653
|
+
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
5654
|
+
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
5655
|
+
if (import_node_path17.default.isAbsolute(project.manifest) || import_node_path17.default.isAbsolute(project.lock)) {
|
|
5656
|
+
diagnostics.push({
|
|
5657
|
+
id: "absolute-path",
|
|
5658
|
+
level: "warning",
|
|
5659
|
+
message: `project "${project.id}" stores an absolute path \u2014 the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
|
|
5660
|
+
});
|
|
5661
|
+
}
|
|
5662
|
+
if (!(0, import_node_fs14.existsSync)(manifestPath)) {
|
|
5663
|
+
diagnostics.push({
|
|
5664
|
+
id: "missing-manifest",
|
|
5665
|
+
level: "error",
|
|
5666
|
+
message: `project "${project.id}" manifest not found: ${manifestPath}`
|
|
5667
|
+
});
|
|
5668
|
+
}
|
|
5669
|
+
if (!(0, import_node_fs14.existsSync)(lockPath)) {
|
|
5670
|
+
diagnostics.push({
|
|
5671
|
+
id: "missing-lock",
|
|
5672
|
+
level: "error",
|
|
5673
|
+
message: `project "${project.id}" lockfile not found: ${lockPath}`
|
|
5674
|
+
});
|
|
5675
|
+
}
|
|
5676
|
+
}
|
|
5677
|
+
for (const [id, count] of idCounts) {
|
|
5678
|
+
if (count > 1) {
|
|
5679
|
+
diagnostics.push({
|
|
5680
|
+
id: "duplicate-id",
|
|
5681
|
+
level: "error",
|
|
5682
|
+
message: `project id "${id}" is registered ${count} times`
|
|
5683
|
+
});
|
|
5684
|
+
}
|
|
5685
|
+
}
|
|
5686
|
+
for (const [lockPath, ids] of lockOwners) {
|
|
5687
|
+
if (ids.length > 1) {
|
|
5688
|
+
diagnostics.push({
|
|
5689
|
+
id: "duplicate-lock",
|
|
5690
|
+
level: "error",
|
|
5691
|
+
message: `${ids.join(", ")} all register the same lockfile: ${lockPath}`
|
|
5692
|
+
});
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5695
|
+
for (const [manifestPath, ids] of manifestOwners) {
|
|
5696
|
+
if (ids.length > 1) {
|
|
5697
|
+
diagnostics.push({
|
|
5698
|
+
id: "duplicate-manifest",
|
|
5699
|
+
level: "warning",
|
|
5700
|
+
message: `${ids.join(", ")} share manifest ${manifestPath} \u2014 expected only when they are variant lockfiles beside one manifest`
|
|
5701
|
+
});
|
|
5702
|
+
}
|
|
5703
|
+
}
|
|
5704
|
+
const providers = new Set(ws.projects.map((p) => p.provider));
|
|
5705
|
+
if (providers.size > 1) {
|
|
5706
|
+
diagnostics.push({
|
|
5707
|
+
id: "mixed-provider",
|
|
5708
|
+
level: "warning",
|
|
5709
|
+
message: `registered projects use different providers: ${[...providers].sort().join(", ")} \u2014 spend totals are kept separate per unit, but confirm this is intentional`
|
|
5710
|
+
});
|
|
5711
|
+
}
|
|
5712
|
+
return diagnostics;
|
|
5713
|
+
}
|
|
5714
|
+
|
|
5715
|
+
// src/pipeline/workspace.ts
|
|
5716
|
+
async function workspaceClaims(ws, dir) {
|
|
5717
|
+
const lockPaths = [];
|
|
5718
|
+
const byProject = {};
|
|
5719
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
5720
|
+
for (const project of ws.projects) {
|
|
5721
|
+
const { lockPath } = resolveProject(dir, project);
|
|
5722
|
+
lockPaths.push(lockPath);
|
|
5723
|
+
let projectClaims;
|
|
5724
|
+
try {
|
|
5725
|
+
projectClaims = await loadClaims([lockPath]);
|
|
5726
|
+
} catch (err) {
|
|
5727
|
+
throw new Error(
|
|
5728
|
+
`Project "${project.id}" lockfile is unreadable: ${err instanceof Error ? err.message : String(err)}`
|
|
5729
|
+
);
|
|
5730
|
+
}
|
|
5731
|
+
byProject[project.id] = projectClaims.size;
|
|
5732
|
+
for (const id of projectClaims) claimed.add(id);
|
|
5733
|
+
}
|
|
5734
|
+
return { claimed, byProject, lockPaths };
|
|
5735
|
+
}
|
|
5736
|
+
function emptyStateCounts() {
|
|
5737
|
+
return {
|
|
5738
|
+
ok: 0,
|
|
5739
|
+
missing: 0,
|
|
5740
|
+
untracked: 0,
|
|
5741
|
+
stale: 0,
|
|
5742
|
+
orphaned: 0,
|
|
5743
|
+
"in-flight": 0,
|
|
5744
|
+
recoverable: 0,
|
|
5745
|
+
failed: 0
|
|
5746
|
+
};
|
|
5747
|
+
}
|
|
5748
|
+
async function workspaceStatus(ws, dir) {
|
|
5749
|
+
const diagnostics = validateWorkspace(ws, dir);
|
|
5750
|
+
const projects = [];
|
|
5751
|
+
const totalsByState = emptyStateCounts();
|
|
5752
|
+
const totalsSpend = { generations: 0, usd: 0, free: 0 };
|
|
5753
|
+
for (const project of ws.projects) {
|
|
5754
|
+
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
5755
|
+
const base = {
|
|
5756
|
+
id: project.id,
|
|
5757
|
+
provider: project.provider,
|
|
5758
|
+
account: project.account ?? null,
|
|
5759
|
+
manifest: manifestPath,
|
|
5760
|
+
lock: lockPath
|
|
5761
|
+
};
|
|
5762
|
+
try {
|
|
5763
|
+
const provider = createProvider(project.provider, "offline");
|
|
5764
|
+
const loaded = await loadManifest(manifestPath);
|
|
5765
|
+
const specs = await resolveSpecs(loaded, { provider });
|
|
5766
|
+
const lock = await loadLock(lockPath);
|
|
5767
|
+
normalizeLockOutputPaths(lock, specs);
|
|
5768
|
+
const plan = await buildPlan(specs, lock);
|
|
5769
|
+
const byState = summarize(plan);
|
|
5770
|
+
const spend = spendByUnit(lock);
|
|
5771
|
+
for (const state of Object.keys(byState)) {
|
|
5772
|
+
totalsByState[state] += byState[state];
|
|
5773
|
+
}
|
|
5774
|
+
for (const unit of Object.keys(spend)) {
|
|
5775
|
+
totalsSpend[unit] = (totalsSpend[unit] ?? 0) + (spend[unit] ?? 0);
|
|
5776
|
+
}
|
|
5777
|
+
projects.push({
|
|
5778
|
+
...base,
|
|
5779
|
+
entries: Object.keys(lock.entries).length,
|
|
5780
|
+
byState,
|
|
5781
|
+
spendByUnit: spend,
|
|
5782
|
+
error: null
|
|
5783
|
+
});
|
|
5784
|
+
} catch (err) {
|
|
5785
|
+
projects.push({
|
|
5786
|
+
...base,
|
|
5787
|
+
entries: 0,
|
|
5788
|
+
byState: emptyStateCounts(),
|
|
5789
|
+
spendByUnit: { generations: 0, usd: 0, free: 0 },
|
|
5790
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5791
|
+
});
|
|
5792
|
+
}
|
|
5793
|
+
}
|
|
5794
|
+
let claims = 0;
|
|
5795
|
+
try {
|
|
5796
|
+
claims = (await workspaceClaims(ws, dir)).claimed.size;
|
|
5797
|
+
} catch {
|
|
5798
|
+
}
|
|
5799
|
+
return {
|
|
5800
|
+
version: 1,
|
|
5801
|
+
safe: !diagnostics.some((d) => d.level === "error") && projects.every((p) => !p.error),
|
|
5802
|
+
dir,
|
|
5803
|
+
projects,
|
|
5804
|
+
totals: { byState: totalsByState, spendByUnit: totalsSpend, claims },
|
|
5805
|
+
diagnostics
|
|
5806
|
+
};
|
|
5807
|
+
}
|
|
5023
5808
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5024
5809
|
0 && (module.exports = {
|
|
5025
5810
|
AssetSchema,
|
|
@@ -5032,24 +5817,32 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5032
5817
|
MAX_DOWNLOAD_BYTES,
|
|
5033
5818
|
MAX_RETRIES,
|
|
5034
5819
|
ManifestSchema,
|
|
5820
|
+
MediaType,
|
|
5035
5821
|
PixelLabClient,
|
|
5036
5822
|
PixelLabError,
|
|
5037
5823
|
PixelLabProvider,
|
|
5824
|
+
RetroDiffusionProvider,
|
|
5038
5825
|
StyleSchema,
|
|
5039
5826
|
UnsupportedCapabilityError,
|
|
5827
|
+
WorkspaceProjectSchema,
|
|
5828
|
+
WorkspaceSchema,
|
|
5040
5829
|
adopt,
|
|
5041
5830
|
applyTags,
|
|
5042
5831
|
auditStyle,
|
|
5832
|
+
availableProviders,
|
|
5043
5833
|
backoffMs,
|
|
5044
5834
|
buildManifest,
|
|
5045
5835
|
buildPlan,
|
|
5836
|
+
cacheFileName,
|
|
5046
5837
|
candidateCount,
|
|
5047
5838
|
clientFromEnv,
|
|
5048
5839
|
colorDistance,
|
|
5049
5840
|
countNumberedDescriptions,
|
|
5050
5841
|
createArtifactBundleManifest,
|
|
5842
|
+
createProvider,
|
|
5051
5843
|
currentEntryOutputPath,
|
|
5052
5844
|
currentOutputPath,
|
|
5845
|
+
detectMediaType,
|
|
5053
5846
|
doctor,
|
|
5054
5847
|
evaluateAudit,
|
|
5055
5848
|
expectedOutputPath,
|
|
@@ -5067,9 +5860,13 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5067
5860
|
loadClaims,
|
|
5068
5861
|
loadLock,
|
|
5069
5862
|
loadManifest,
|
|
5863
|
+
loadSiblingManifests,
|
|
5864
|
+
loadWorkspace,
|
|
5070
5865
|
lockKey,
|
|
5071
5866
|
matchOrphanStyle,
|
|
5072
5867
|
measureBalanceChange,
|
|
5868
|
+
mediaExtension,
|
|
5869
|
+
mediaTypeFromExtension,
|
|
5073
5870
|
mergePalettes,
|
|
5074
5871
|
mountSprites,
|
|
5075
5872
|
mountStyle,
|
|
@@ -5081,19 +5878,25 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5081
5878
|
packStyle,
|
|
5082
5879
|
paletteDistance,
|
|
5083
5880
|
parseLock,
|
|
5881
|
+
parseWorkspace,
|
|
5084
5882
|
pngSize,
|
|
5085
5883
|
poll,
|
|
5086
5884
|
portableOutputPath,
|
|
5087
5885
|
primaryOutput,
|
|
5886
|
+
providerFactory,
|
|
5088
5887
|
pushTags,
|
|
5888
|
+
registerProvider,
|
|
5089
5889
|
remove,
|
|
5090
5890
|
renderSalvageSheet,
|
|
5091
5891
|
renderSheet,
|
|
5892
|
+
requireBalance,
|
|
5092
5893
|
requireDelete,
|
|
5093
5894
|
requireList,
|
|
5895
|
+
requireSelectCandidate,
|
|
5094
5896
|
resolveEntryOutputs,
|
|
5095
5897
|
resolveOutputPath,
|
|
5096
5898
|
resolvePackInputs,
|
|
5899
|
+
resolveProject,
|
|
5097
5900
|
resolveSpecEntryOutputs,
|
|
5098
5901
|
resolveSpecOutputs,
|
|
5099
5902
|
resolveSpecs,
|
|
@@ -5103,6 +5906,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5103
5906
|
runPicker,
|
|
5104
5907
|
runSalvage,
|
|
5105
5908
|
saveLock,
|
|
5909
|
+
saveWorkspace,
|
|
5106
5910
|
scanAssets,
|
|
5107
5911
|
selectEntryOutput,
|
|
5108
5912
|
sha256,
|
|
@@ -5118,11 +5922,16 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5118
5922
|
tileFeatureOutputCount,
|
|
5119
5923
|
tileVariationCount,
|
|
5120
5924
|
tilesCost,
|
|
5925
|
+
toPortablePath,
|
|
5121
5926
|
totalSpend,
|
|
5122
5927
|
upsert,
|
|
5123
5928
|
validateCostEstimate,
|
|
5929
|
+
validateMedia,
|
|
5930
|
+
validateWorkspace,
|
|
5124
5931
|
verifyArtifactBundle,
|
|
5125
5932
|
withArtifactManifest,
|
|
5933
|
+
workspaceClaims,
|
|
5934
|
+
workspaceStatus,
|
|
5126
5935
|
writeArtifactBundle,
|
|
5127
5936
|
writeManagedArtifactBundle
|
|
5128
5937
|
});
|