pixelkiln 0.72.0 → 0.74.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/dist/cli.js +773 -516
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +16 -0
- package/dist/client/gallery.js +452 -0
- package/dist/index.cjs +476 -266
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1876 -259
- package/dist/index.d.ts +1876 -259
- package/dist/index.js +459 -249
- package/dist/index.js.map +1 -1
- package/docs/CHARACTERS.md +12 -0
- package/docs/CLI.md +4 -1
- package/docs/GENERATORS.md +1 -1
- package/docs/MANIFEST.md +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1001,7 +1001,7 @@ var AssetSchema = z.object({
|
|
|
1001
1001
|
path: ["mirror"]
|
|
1002
1002
|
});
|
|
1003
1003
|
}
|
|
1004
|
-
if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit) {
|
|
1004
|
+
if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit && !asset.animation?.template) {
|
|
1005
1005
|
context.addIssue({
|
|
1006
1006
|
code: z.ZodIssueCode.custom,
|
|
1007
1007
|
message: "Required",
|
|
@@ -1736,9 +1736,9 @@ var PixelLabClient = class {
|
|
|
1736
1736
|
apiKey;
|
|
1737
1737
|
http;
|
|
1738
1738
|
/** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
|
|
1739
|
-
async request(
|
|
1739
|
+
async request(path36, init) {
|
|
1740
1740
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
1741
|
-
const res = await this.http(`${BASE}${
|
|
1741
|
+
const res = await this.http(`${BASE}${path36}`, {
|
|
1742
1742
|
...init,
|
|
1743
1743
|
headers: {
|
|
1744
1744
|
Authorization: auth,
|
|
@@ -1748,13 +1748,13 @@ var PixelLabClient = class {
|
|
|
1748
1748
|
});
|
|
1749
1749
|
const text = await res.text();
|
|
1750
1750
|
if (!res.ok) {
|
|
1751
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
1751
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path36} \u2192 ${res.status}`, res.status, text);
|
|
1752
1752
|
}
|
|
1753
1753
|
if (!text) return {};
|
|
1754
1754
|
try {
|
|
1755
1755
|
return JSON.parse(text);
|
|
1756
1756
|
} catch {
|
|
1757
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
1757
|
+
throw new Error(`${init?.method ?? "GET"} ${path36} returned invalid JSON`);
|
|
1758
1758
|
}
|
|
1759
1759
|
}
|
|
1760
1760
|
async balance() {
|
|
@@ -2146,10 +2146,10 @@ var PixelLabClient = class {
|
|
|
2146
2146
|
const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
|
|
2147
2147
|
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
2148
2148
|
const south = args.reference?.south;
|
|
2149
|
-
let
|
|
2149
|
+
let path36;
|
|
2150
2150
|
let body;
|
|
2151
2151
|
if (args.mode === "v3") {
|
|
2152
|
-
|
|
2152
|
+
path36 = "/create-character-v3";
|
|
2153
2153
|
body = {
|
|
2154
2154
|
description: args.description,
|
|
2155
2155
|
image_size: imageSize,
|
|
@@ -2163,7 +2163,7 @@ var PixelLabClient = class {
|
|
|
2163
2163
|
...args.enhancePrompt ? { enhance_prompt: true } : {}
|
|
2164
2164
|
};
|
|
2165
2165
|
} else if (args.mode === "pro-flash") {
|
|
2166
|
-
|
|
2166
|
+
path36 = "/create-character-pro-flash";
|
|
2167
2167
|
const traits = args.styleTraits;
|
|
2168
2168
|
body = {
|
|
2169
2169
|
description: args.description,
|
|
@@ -2187,7 +2187,7 @@ var PixelLabClient = class {
|
|
|
2187
2187
|
} : {}
|
|
2188
2188
|
};
|
|
2189
2189
|
} else if (args.mode === "pro") {
|
|
2190
|
-
|
|
2190
|
+
path36 = "/create-character-pro";
|
|
2191
2191
|
body = {
|
|
2192
2192
|
description: args.description,
|
|
2193
2193
|
image_size: imageSize,
|
|
@@ -2201,7 +2201,7 @@ var PixelLabClient = class {
|
|
|
2201
2201
|
...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
|
|
2202
2202
|
};
|
|
2203
2203
|
} else {
|
|
2204
|
-
|
|
2204
|
+
path36 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
|
|
2205
2205
|
body = {
|
|
2206
2206
|
description: args.description,
|
|
2207
2207
|
image_size: imageSize,
|
|
@@ -2218,8 +2218,8 @@ var PixelLabClient = class {
|
|
|
2218
2218
|
...palette
|
|
2219
2219
|
};
|
|
2220
2220
|
}
|
|
2221
|
-
const raw = await this.request(
|
|
2222
|
-
return validateResponse(CharacterSubmitSchema, raw,
|
|
2221
|
+
const raw = await this.request(path36, { method: "POST", body: JSON.stringify(body) });
|
|
2222
|
+
return validateResponse(CharacterSubmitSchema, raw, path36.slice(1));
|
|
2223
2223
|
}
|
|
2224
2224
|
/** A text edit of an existing character, applied to every direction and stored as a sibling. */
|
|
2225
2225
|
async createCharacterState(args) {
|
|
@@ -3405,10 +3405,10 @@ import { createReadStream } from "fs";
|
|
|
3405
3405
|
function sha256(data) {
|
|
3406
3406
|
return createHash("sha256").update(data).digest("hex");
|
|
3407
3407
|
}
|
|
3408
|
-
async function sha256File(
|
|
3408
|
+
async function sha256File(path36) {
|
|
3409
3409
|
return new Promise((resolve, reject) => {
|
|
3410
3410
|
const hash = createHash("sha256");
|
|
3411
|
-
const stream = createReadStream(
|
|
3411
|
+
const stream = createReadStream(path36);
|
|
3412
3412
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
3413
3413
|
stream.on("error", reject);
|
|
3414
3414
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -3578,7 +3578,11 @@ function characterCost(spec) {
|
|
|
3578
3578
|
return 20;
|
|
3579
3579
|
}
|
|
3580
3580
|
if (character.mode === "standard") return 1;
|
|
3581
|
-
if (character.mode === "v3")
|
|
3581
|
+
if (character.mode === "v3") {
|
|
3582
|
+
const south = character.reference?.south;
|
|
3583
|
+
if (south) return Math.max(1, Math.ceil(south.width * south.height * 8 / 65536));
|
|
3584
|
+
return 1 + Math.max(1, Math.ceil(px * 8 / 65536));
|
|
3585
|
+
}
|
|
3582
3586
|
if (character.mode === "pro-flash") {
|
|
3583
3587
|
const south = character.reference?.south;
|
|
3584
3588
|
return south ? proFlashCharacterCost(south.width, south.height, true) : proFlashCharacterCost(spec.width, spec.height, false);
|
|
@@ -5772,9 +5776,9 @@ var RetroDiffusionClient = class {
|
|
|
5772
5776
|
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
5773
5777
|
return Buffer.from(await response.arrayBuffer());
|
|
5774
5778
|
}
|
|
5775
|
-
async call(
|
|
5779
|
+
async call(path36, init = {}) {
|
|
5776
5780
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
5777
|
-
const response = await this.request(`${this.baseUrl}${
|
|
5781
|
+
const response = await this.request(`${this.baseUrl}${path36}`, {
|
|
5778
5782
|
...init,
|
|
5779
5783
|
headers: {
|
|
5780
5784
|
"Content-Type": "application/json",
|
|
@@ -12063,10 +12067,10 @@ function isSha256Hash(value) {
|
|
|
12063
12067
|
function parseCache(value) {
|
|
12064
12068
|
return HashCacheSchema.parse(value);
|
|
12065
12069
|
}
|
|
12066
|
-
async function loadCache(
|
|
12067
|
-
if (!existsSync13(
|
|
12070
|
+
async function loadCache(path36) {
|
|
12071
|
+
if (!existsSync13(path36)) return { version: 1, hashes: {} };
|
|
12068
12072
|
try {
|
|
12069
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile11(
|
|
12073
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile11(path36, "utf8")));
|
|
12070
12074
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
12071
12075
|
return {
|
|
12072
12076
|
version: 1,
|
|
@@ -12078,18 +12082,18 @@ async function loadCache(path35) {
|
|
|
12078
12082
|
return { version: 1, hashes: {} };
|
|
12079
12083
|
}
|
|
12080
12084
|
}
|
|
12081
|
-
async function saveCache(
|
|
12085
|
+
async function saveCache(path36, cache) {
|
|
12082
12086
|
const sorted = {};
|
|
12083
12087
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
12084
12088
|
const hash = cache.hashes[key];
|
|
12085
12089
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
12086
12090
|
sorted[key] = hash;
|
|
12087
12091
|
}
|
|
12088
|
-
await mkdir5(pathModule.dirname(pathModule.resolve(
|
|
12089
|
-
const tmp = `${
|
|
12092
|
+
await mkdir5(pathModule.dirname(pathModule.resolve(path36)), { recursive: true });
|
|
12093
|
+
const tmp = `${path36}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
12090
12094
|
try {
|
|
12091
12095
|
await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
12092
|
-
await rename4(tmp,
|
|
12096
|
+
await rename4(tmp, path36);
|
|
12093
12097
|
} finally {
|
|
12094
12098
|
await rm4(tmp, { force: true });
|
|
12095
12099
|
}
|
|
@@ -14894,9 +14898,9 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
14894
14898
|
|
|
14895
14899
|
// src/gallery/snapshot.ts
|
|
14896
14900
|
import { createHash as createHash4 } from "crypto";
|
|
14897
|
-
import { existsSync as
|
|
14901
|
+
import { existsSync as existsSync25 } from "fs";
|
|
14898
14902
|
import { readFile as readFile22, stat as stat3 } from "fs/promises";
|
|
14899
|
-
import
|
|
14903
|
+
import path31 from "path";
|
|
14900
14904
|
|
|
14901
14905
|
// src/workspace.ts
|
|
14902
14906
|
import { mkdir as mkdir7, readFile as readFile18, rename as rename6, rm as rm7, writeFile as writeFile9 } from "fs/promises";
|
|
@@ -15032,8 +15036,8 @@ function validateWorkspace(ws, dir) {
|
|
|
15032
15036
|
}
|
|
15033
15037
|
|
|
15034
15038
|
// src/gallery/edit.ts
|
|
15035
|
-
import
|
|
15036
|
-
import { z as
|
|
15039
|
+
import path30 from "path";
|
|
15040
|
+
import { z as z12 } from "zod";
|
|
15037
15041
|
|
|
15038
15042
|
// src/manifest-edit.ts
|
|
15039
15043
|
import { readFile as readFile19, rename as rename7, rm as rm8, writeFile as writeFile10 } from "fs/promises";
|
|
@@ -15064,13 +15068,16 @@ var NewAnimationSchema = z8.object({
|
|
|
15064
15068
|
direction: CharacterDirectionSchema.optional(),
|
|
15065
15069
|
frames: z8.number().int().min(4).max(16).optional(),
|
|
15066
15070
|
fps: z8.number().int().min(1).max(60).optional(),
|
|
15071
|
+
/** One loop per named direction, plus free mirrors; expanded when the manifest loads. */
|
|
15072
|
+
directions: z8.array(CharacterDirectionSchema).min(1).optional(),
|
|
15067
15073
|
mode: CharacterAnimationModeSchema.optional(),
|
|
15068
15074
|
/** What is being animated, when the parent's own description would mislead the model (`character` only). */
|
|
15069
15075
|
subject: z8.string().min(1).optional(),
|
|
15070
15076
|
enhancePrompt: z8.boolean().optional()
|
|
15071
15077
|
}).strict();
|
|
15072
15078
|
var NewAssetSchema = z8.object({
|
|
15073
|
-
|
|
15079
|
+
/** A portrait, an outfit, or a mirror has none of its own; the loader says where one is needed. */
|
|
15080
|
+
prompt: z8.string().optional(),
|
|
15074
15081
|
category: z8.string().optional(),
|
|
15075
15082
|
width: z8.number().int().optional(),
|
|
15076
15083
|
height: z8.number().int().optional(),
|
|
@@ -15105,7 +15112,24 @@ var NewAssetSchema = z8.object({
|
|
|
15105
15112
|
/** A pose or outfit of an existing `character`/`objectPro` base or state. */
|
|
15106
15113
|
state: NewStateSchema.optional(),
|
|
15107
15114
|
/** A loop of an existing `character`/`objectPro` base or state. */
|
|
15108
|
-
animation: NewAnimationSchema.optional()
|
|
15115
|
+
animation: NewAnimationSchema.optional(),
|
|
15116
|
+
/** A bust of an existing character. */
|
|
15117
|
+
portrait: CharacterPortraitSchema.optional(),
|
|
15118
|
+
/** An existing loop re-clothed from a reference image (uploaded first). */
|
|
15119
|
+
outfit: CharacterOutfitSchema.optional(),
|
|
15120
|
+
/** Another asset of the same style, flipped; free. */
|
|
15121
|
+
mirror: z8.string().min(1).optional(),
|
|
15122
|
+
/**
|
|
15123
|
+
* `character`/`objectPro` bases: the subject's own sprite (uploaded
|
|
15124
|
+
* first), or one per direction, which PixelLab rotates instead of drawing.
|
|
15125
|
+
*/
|
|
15126
|
+
reference: z8.union([z8.string().min(1), z8.record(CharacterDirectionSchema, z8.string().min(1))]).optional(),
|
|
15127
|
+
/** `character` pro bases: a concept image that seeds the design. */
|
|
15128
|
+
concept: z8.string().min(1).optional(),
|
|
15129
|
+
/** `character` pro bases: another character in the style whose look this one follows. */
|
|
15130
|
+
styleCharacter: z8.string().min(1).optional(),
|
|
15131
|
+
/** `character` standard humanoid bases: this character's proportions, over the style's. */
|
|
15132
|
+
proportions: CharacterProportionsSchema.optional()
|
|
15109
15133
|
}).strict().refine((asset) => !(asset.revision && (asset.state || asset.animation)), {
|
|
15110
15134
|
message: "an asset is a revision, a state, or an animation \u2014 not more than one"
|
|
15111
15135
|
}).refine((asset) => !(asset.state && asset.animation), {
|
|
@@ -15116,59 +15140,95 @@ var CANDIDATE_OPTION = {
|
|
|
15116
15140
|
comfyui: "numImages",
|
|
15117
15141
|
scenario: "numOutputs"
|
|
15118
15142
|
};
|
|
15143
|
+
var NewStyleSchema = z8.object({
|
|
15144
|
+
generator: z8.enum(["character", "objectPro"]),
|
|
15145
|
+
/** Where the style's art is written, inside the project. */
|
|
15146
|
+
outDir: z8.string().min(1).refine((dir) => !dir.split(/[\\/]/).includes("..") && !/^([a-z]:)?[\\/]/i.test(dir), {
|
|
15147
|
+
message: "outDir must be a path inside the project"
|
|
15148
|
+
}),
|
|
15149
|
+
size: z8.number().int().min(16).max(256).optional(),
|
|
15150
|
+
mode: CharacterModeSchema.optional(),
|
|
15151
|
+
directions: z8.union([z8.literal(4), z8.literal(8)]).optional(),
|
|
15152
|
+
template: z8.string().min(1).optional(),
|
|
15153
|
+
view: z8.string().min(1).max(64).optional(),
|
|
15154
|
+
proportions: CharacterProportionsSchema.optional(),
|
|
15155
|
+
isometric: z8.boolean().optional(),
|
|
15156
|
+
textGuidanceScale: z8.number().min(1).max(20).optional(),
|
|
15157
|
+
enhancePrompt: z8.boolean().optional(),
|
|
15158
|
+
promptPrefix: z8.string().optional(),
|
|
15159
|
+
promptSuffix: z8.string().optional(),
|
|
15160
|
+
palette: z8.array(z8.string().regex(/^#?[0-9a-f]{6}$/i, "expected a six-digit hex colour")).max(256).optional()
|
|
15161
|
+
}).strict();
|
|
15162
|
+
var PatchAssetEdit = z8.object({
|
|
15163
|
+
action: z8.literal("patch-asset"),
|
|
15164
|
+
assetId: z8.string().min(1),
|
|
15165
|
+
patch: AssetPatchSchema
|
|
15166
|
+
}).strict();
|
|
15167
|
+
var AddAssetEdit = z8.object({
|
|
15168
|
+
action: z8.literal("add-asset"),
|
|
15169
|
+
assetId: z8.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
|
|
15170
|
+
asset: NewAssetSchema
|
|
15171
|
+
}).strict();
|
|
15172
|
+
var SetSourceEdit = z8.object({
|
|
15173
|
+
action: z8.literal("set-source"),
|
|
15174
|
+
assetId: z8.string().min(1),
|
|
15175
|
+
styleId: z8.string().min(1),
|
|
15176
|
+
/** Manifest-relative path of the art that stands in for this asset in this style. */
|
|
15177
|
+
source: z8.string().min(1)
|
|
15178
|
+
}).strict();
|
|
15179
|
+
var ClearSourceEdit = z8.object({
|
|
15180
|
+
action: z8.literal("clear-source"),
|
|
15181
|
+
assetId: z8.string().min(1),
|
|
15182
|
+
styleId: z8.string().min(1)
|
|
15183
|
+
}).strict();
|
|
15184
|
+
var PatchStyleEdit = z8.object({
|
|
15185
|
+
action: z8.literal("patch-style"),
|
|
15186
|
+
styleId: z8.string().min(1),
|
|
15187
|
+
patch: z8.object({
|
|
15188
|
+
/** Candidates per generation, written to the provider's own option. */
|
|
15189
|
+
candidates: z8.number().int().min(1).max(64).optional(),
|
|
15190
|
+
/** Empty clears the style's own value (inherit, or the default). */
|
|
15191
|
+
promptPrefix: z8.string().optional(),
|
|
15192
|
+
promptSuffix: z8.string().optional(),
|
|
15193
|
+
/** `#rrggbb` values; null or empty clears the style's own palette. */
|
|
15194
|
+
palette: z8.array(z8.string().regex(/^#?[0-9a-f]{6}$/i, "expected a six-digit hex colour")).max(256).nullable().optional(),
|
|
15195
|
+
/** Snap downloaded art to the palette; false clears the style's own value. */
|
|
15196
|
+
enforcePalette: z8.boolean().optional(),
|
|
15197
|
+
/** Provider view name (PixelLab: low top-down, high top-down, side); empty clears. */
|
|
15198
|
+
view: z8.string().max(64).nullable().optional(),
|
|
15199
|
+
/** pixflux only; null clears the style's own value. */
|
|
15200
|
+
noBackground: z8.boolean().nullable().optional()
|
|
15201
|
+
}).strict().refine((patch) => Object.keys(patch).length > 0, { message: "nothing to change" })
|
|
15202
|
+
}).strict();
|
|
15203
|
+
var AddStyleEdit = z8.object({
|
|
15204
|
+
action: z8.literal("add-style"),
|
|
15205
|
+
styleId: z8.string().min(1).regex(/^[^/\\:]+$/, "style ids cannot contain slashes or colons"),
|
|
15206
|
+
style: NewStyleSchema
|
|
15207
|
+
}).strict();
|
|
15208
|
+
var Envelope = {
|
|
15209
|
+
/** Workspace project id; omitted for a single-project gallery. */
|
|
15210
|
+
project: z8.string().min(1).optional(),
|
|
15211
|
+
/** Manifest bytes the page last saw; a mismatch refuses the write. */
|
|
15212
|
+
expectedSha256: HexSha
|
|
15213
|
+
};
|
|
15214
|
+
var SingleEdit = z8.discriminatedUnion("action", [PatchAssetEdit, AddAssetEdit, SetSourceEdit, ClearSourceEdit, PatchStyleEdit, AddStyleEdit]);
|
|
15119
15215
|
var ManifestEditSchema = z8.discriminatedUnion("action", [
|
|
15216
|
+
PatchAssetEdit.extend(Envelope),
|
|
15217
|
+
AddAssetEdit.extend(Envelope),
|
|
15218
|
+
SetSourceEdit.extend(Envelope),
|
|
15219
|
+
ClearSourceEdit.extend(Envelope),
|
|
15220
|
+
PatchStyleEdit.extend(Envelope),
|
|
15221
|
+
AddStyleEdit.extend(Envelope),
|
|
15222
|
+
/**
|
|
15223
|
+
* Several edits as one write: a new character's style, base, and loops
|
|
15224
|
+
* land together or not at all, validated once as the finished manifest.
|
|
15225
|
+
* Each edit sees the ones before it, so a base can name a style the same
|
|
15226
|
+
* batch adds.
|
|
15227
|
+
*/
|
|
15120
15228
|
z8.object({
|
|
15121
|
-
action: z8.literal("
|
|
15122
|
-
|
|
15123
|
-
|
|
15124
|
-
assetId: z8.string().min(1),
|
|
15125
|
-
/** Manifest bytes the page last saw; a mismatch refuses the write. */
|
|
15126
|
-
expectedSha256: HexSha,
|
|
15127
|
-
patch: AssetPatchSchema
|
|
15128
|
-
}).strict(),
|
|
15129
|
-
z8.object({
|
|
15130
|
-
action: z8.literal("add-asset"),
|
|
15131
|
-
project: z8.string().min(1).optional(),
|
|
15132
|
-
assetId: z8.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
|
|
15133
|
-
expectedSha256: HexSha,
|
|
15134
|
-
asset: NewAssetSchema
|
|
15135
|
-
}).strict(),
|
|
15136
|
-
z8.object({
|
|
15137
|
-
action: z8.literal("set-source"),
|
|
15138
|
-
project: z8.string().min(1).optional(),
|
|
15139
|
-
assetId: z8.string().min(1),
|
|
15140
|
-
styleId: z8.string().min(1),
|
|
15141
|
-
/** Manifest-relative path of the art that stands in for this asset in this style. */
|
|
15142
|
-
source: z8.string().min(1),
|
|
15143
|
-
expectedSha256: HexSha
|
|
15144
|
-
}).strict(),
|
|
15145
|
-
z8.object({
|
|
15146
|
-
action: z8.literal("clear-source"),
|
|
15147
|
-
project: z8.string().min(1).optional(),
|
|
15148
|
-
assetId: z8.string().min(1),
|
|
15149
|
-
styleId: z8.string().min(1),
|
|
15150
|
-
expectedSha256: HexSha
|
|
15151
|
-
}).strict(),
|
|
15152
|
-
z8.object({
|
|
15153
|
-
action: z8.literal("patch-style"),
|
|
15154
|
-
project: z8.string().min(1).optional(),
|
|
15155
|
-
styleId: z8.string().min(1),
|
|
15156
|
-
expectedSha256: HexSha,
|
|
15157
|
-
patch: z8.object({
|
|
15158
|
-
/** Candidates per generation, written to the provider's own option. */
|
|
15159
|
-
candidates: z8.number().int().min(1).max(64).optional(),
|
|
15160
|
-
/** Empty clears the style's own value (inherit, or the default). */
|
|
15161
|
-
promptPrefix: z8.string().optional(),
|
|
15162
|
-
promptSuffix: z8.string().optional(),
|
|
15163
|
-
/** `#rrggbb` values; null or empty clears the style's own palette. */
|
|
15164
|
-
palette: z8.array(z8.string().regex(/^#?[0-9a-f]{6}$/i, "expected a six-digit hex colour")).max(256).nullable().optional(),
|
|
15165
|
-
/** Snap downloaded art to the palette; false clears the style's own value. */
|
|
15166
|
-
enforcePalette: z8.boolean().optional(),
|
|
15167
|
-
/** Provider view name (PixelLab: low top-down, high top-down, side); empty clears. */
|
|
15168
|
-
view: z8.string().max(64).nullable().optional(),
|
|
15169
|
-
/** pixflux only; null clears the style's own value. */
|
|
15170
|
-
noBackground: z8.boolean().nullable().optional()
|
|
15171
|
-
}).strict().refine((patch) => Object.keys(patch).length > 0, { message: "nothing to change" })
|
|
15229
|
+
action: z8.literal("batch"),
|
|
15230
|
+
...Envelope,
|
|
15231
|
+
edits: z8.array(SingleEdit).min(1).max(64)
|
|
15172
15232
|
}).strict()
|
|
15173
15233
|
]);
|
|
15174
15234
|
var ManifestDriftError = class extends Error {
|
|
@@ -15217,6 +15277,18 @@ function undeclaredAsset(raw, assetId) {
|
|
|
15217
15277
|
}
|
|
15218
15278
|
function applyEdit(raw, edit) {
|
|
15219
15279
|
raw.assets ??= {};
|
|
15280
|
+
if (edit.action === "batch") {
|
|
15281
|
+
for (const each of edit.edits) applyEdit(raw, each);
|
|
15282
|
+
return;
|
|
15283
|
+
}
|
|
15284
|
+
if (edit.action === "add-style") {
|
|
15285
|
+
raw.styles ??= {};
|
|
15286
|
+
if (Object.hasOwn(raw.styles, edit.styleId)) throw new ManifestEditError(`style "${edit.styleId}" already exists`);
|
|
15287
|
+
const style = { ...edit.style };
|
|
15288
|
+
if (edit.style.palette) style.palette = edit.style.palette.map((color) => "#" + color.replace(/^#/, "").toLowerCase());
|
|
15289
|
+
raw.styles[edit.styleId] = style;
|
|
15290
|
+
return;
|
|
15291
|
+
}
|
|
15220
15292
|
if (edit.action === "set-source" || edit.action === "clear-source") {
|
|
15221
15293
|
const asset2 = Object.hasOwn(raw.assets, edit.assetId) ? raw.assets[edit.assetId] : void 0;
|
|
15222
15294
|
if (!asset2) throw undeclaredAsset(raw, edit.assetId);
|
|
@@ -15294,6 +15366,15 @@ function applyEdit(raw, edit) {
|
|
|
15294
15366
|
if (edit.asset.animation && !declaredAssets(raw).has(edit.asset.animation.of)) {
|
|
15295
15367
|
throw new ManifestEditError(`animation parent "${edit.asset.animation.of}" is not declared by the manifest`);
|
|
15296
15368
|
}
|
|
15369
|
+
for (const [what, parent] of [
|
|
15370
|
+
["portrait", edit.asset.portrait?.of],
|
|
15371
|
+
["outfit", edit.asset.outfit?.of],
|
|
15372
|
+
["mirror", edit.asset.mirror]
|
|
15373
|
+
]) {
|
|
15374
|
+
if (parent && !declaredAssets(raw).has(parent)) {
|
|
15375
|
+
throw new ManifestEditError(`${what} source "${parent}" is not declared by the manifest`);
|
|
15376
|
+
}
|
|
15377
|
+
}
|
|
15297
15378
|
raw.assets[edit.assetId] = asset2;
|
|
15298
15379
|
return;
|
|
15299
15380
|
}
|
|
@@ -15321,11 +15402,10 @@ function applyEdit(raw, edit) {
|
|
|
15321
15402
|
else delete asset.tags;
|
|
15322
15403
|
}
|
|
15323
15404
|
}
|
|
15324
|
-
async function
|
|
15405
|
+
async function withCandidate(manifestPath, edit, use, unchanged) {
|
|
15325
15406
|
const absolute = path26.resolve(manifestPath);
|
|
15326
15407
|
const original = await readFile19(absolute, "utf8");
|
|
15327
|
-
|
|
15328
|
-
if (before !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
15408
|
+
if (sha256(original) !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
15329
15409
|
let raw;
|
|
15330
15410
|
try {
|
|
15331
15411
|
raw = JSON.parse(original);
|
|
@@ -15336,25 +15416,35 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
15336
15416
|
}
|
|
15337
15417
|
applyEdit(raw, edit);
|
|
15338
15418
|
const next = serializeLike(original, raw);
|
|
15339
|
-
if (next === original) return
|
|
15419
|
+
if (next === original) return unchanged(absolute, original);
|
|
15340
15420
|
const tmp = path26.join(
|
|
15341
15421
|
path26.dirname(absolute),
|
|
15342
15422
|
`.${path26.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
|
|
15343
15423
|
);
|
|
15344
15424
|
try {
|
|
15345
15425
|
await writeFile10(tmp, next);
|
|
15426
|
+
let specs;
|
|
15346
15427
|
try {
|
|
15347
|
-
|
|
15348
|
-
await resolveSpecs(loaded);
|
|
15428
|
+
specs = await resolveSpecs(await loadManifest(tmp));
|
|
15349
15429
|
} catch (error) {
|
|
15350
15430
|
const message6 = error instanceof Error ? error.message : String(error);
|
|
15351
15431
|
throw new ManifestEditError(message6.replace(tmp, absolute));
|
|
15352
15432
|
}
|
|
15353
|
-
await
|
|
15433
|
+
return await use({ absolute, original, next, tmp, specs });
|
|
15354
15434
|
} finally {
|
|
15355
15435
|
await rm8(tmp, { force: true });
|
|
15356
15436
|
}
|
|
15357
|
-
|
|
15437
|
+
}
|
|
15438
|
+
async function applyManifestEdit(manifestPath, edit) {
|
|
15439
|
+
return withCandidate(
|
|
15440
|
+
manifestPath,
|
|
15441
|
+
edit,
|
|
15442
|
+
async ({ absolute, next, tmp }) => {
|
|
15443
|
+
await rename7(tmp, absolute);
|
|
15444
|
+
return { manifestPath: absolute, sha256: sha256(next), changed: true };
|
|
15445
|
+
},
|
|
15446
|
+
(absolute, original) => ({ manifestPath: absolute, sha256: sha256(original), changed: false })
|
|
15447
|
+
);
|
|
15358
15448
|
}
|
|
15359
15449
|
|
|
15360
15450
|
// src/pipeline/hand-edit.ts
|
|
@@ -15605,62 +15695,103 @@ function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
|
|
|
15605
15695
|
return openExternal(file, command);
|
|
15606
15696
|
}
|
|
15607
15697
|
|
|
15608
|
-
// src/gallery/
|
|
15698
|
+
// src/gallery/upload.ts
|
|
15609
15699
|
import { existsSync as existsSync23 } from "fs";
|
|
15610
|
-
import { mkdir as mkdir9,
|
|
15700
|
+
import { mkdir as mkdir9, writeFile as writeFile12 } from "fs/promises";
|
|
15611
15701
|
import path28 from "path";
|
|
15612
15702
|
import { z as z10 } from "zod";
|
|
15613
|
-
var
|
|
15703
|
+
var MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
|
|
15704
|
+
var UploadImageSchema = z10.object({
|
|
15705
|
+
action: z10.literal("upload-image"),
|
|
15614
15706
|
project: z10.string().min(1).optional(),
|
|
15615
|
-
|
|
15616
|
-
|
|
15617
|
-
|
|
15707
|
+
/** Manifest-relative destination, ending .png, .jpg, or .jpeg. */
|
|
15708
|
+
path: z10.string().min(1),
|
|
15709
|
+
base64: z10.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_UPLOAD_BYTES / 3) * 4),
|
|
15710
|
+
/** Replace a file already at `path`. */
|
|
15711
|
+
overwrite: z10.boolean().optional()
|
|
15618
15712
|
}).strict();
|
|
15619
|
-
|
|
15620
|
-
|
|
15621
|
-
|
|
15622
|
-
|
|
15623
|
-
|
|
15713
|
+
async function saveProjectImage(root, request) {
|
|
15714
|
+
const file = path28.resolve(root, request.path);
|
|
15715
|
+
const relative = path28.relative(root, file);
|
|
15716
|
+
if (relative.startsWith("..") || path28.isAbsolute(relative)) {
|
|
15717
|
+
throw new ManifestEditError("an upload must stay inside the project");
|
|
15718
|
+
}
|
|
15719
|
+
const extension = path28.extname(file).toLowerCase();
|
|
15720
|
+
const expected = extension === ".png" ? "png" : extension === ".jpg" || extension === ".jpeg" ? "jpeg" : null;
|
|
15721
|
+
if (!expected) throw new ManifestEditError("an upload must be a .png, .jpg, or .jpeg path");
|
|
15722
|
+
const bytes = Buffer.from(request.base64, "base64");
|
|
15723
|
+
if (!bytes.length || bytes.length > MAX_UPLOAD_BYTES) {
|
|
15724
|
+
throw new ManifestEditError(`an upload must be between 1 byte and ${MAX_UPLOAD_BYTES / 1024 / 1024} MB`);
|
|
15725
|
+
}
|
|
15726
|
+
const metadata = imageMetadata(bytes);
|
|
15727
|
+
if (!metadata) throw new ManifestEditError("the upload is not a readable PNG or JPEG");
|
|
15728
|
+
if (metadata.format !== expected) {
|
|
15729
|
+
throw new ManifestEditError(`the upload is a ${metadata.format.toUpperCase()}; name it ${metadata.format === "png" ? ".png" : ".jpg"}`);
|
|
15730
|
+
}
|
|
15731
|
+
if (existsSync23(file) && !request.overwrite) {
|
|
15732
|
+
throw new ManifestEditError(`${relative.split(path28.sep).join("/")} already exists; choose another name, or replace it`);
|
|
15733
|
+
}
|
|
15734
|
+
await mkdir9(path28.dirname(file), { recursive: true });
|
|
15735
|
+
await writeFile12(file, bytes);
|
|
15736
|
+
return relative.split(path28.sep).join("/");
|
|
15737
|
+
}
|
|
15738
|
+
|
|
15739
|
+
// src/gallery/skeleton.ts
|
|
15740
|
+
import { existsSync as existsSync24 } from "fs";
|
|
15741
|
+
import { mkdir as mkdir10, readFile as readFile21, rm as rm10, writeFile as writeFile13 } from "fs/promises";
|
|
15742
|
+
import path29 from "path";
|
|
15743
|
+
import { z as z11 } from "zod";
|
|
15744
|
+
var SkeletonEstimateRequestSchema = z11.object({
|
|
15745
|
+
project: z11.string().min(1).optional(),
|
|
15746
|
+
styleId: z11.string().min(1),
|
|
15747
|
+
assetId: z11.string().min(1),
|
|
15748
|
+
frames: z11.number().int().min(3).max(15).optional()
|
|
15749
|
+
}).strict();
|
|
15750
|
+
var SkeletonAnimationRequestSchema = z11.object({
|
|
15751
|
+
action: z11.literal("create-skeleton-animation"),
|
|
15752
|
+
project: z11.string().min(1).optional(),
|
|
15753
|
+
expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/),
|
|
15754
|
+
styleId: z11.string().min(1),
|
|
15624
15755
|
/** The sprite the animation poses. */
|
|
15625
|
-
from:
|
|
15626
|
-
assetId:
|
|
15756
|
+
from: z11.string().min(1),
|
|
15757
|
+
assetId: z11.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
|
|
15627
15758
|
/** The motion, sent as PixelLab's `action`. */
|
|
15628
|
-
prompt:
|
|
15759
|
+
prompt: z11.string().min(1),
|
|
15629
15760
|
direction: CharacterDirectionSchema,
|
|
15630
|
-
description:
|
|
15631
|
-
skeletonTemplate:
|
|
15761
|
+
description: z11.string().min(1).optional(),
|
|
15762
|
+
skeletonTemplate: z11.string().min(1).optional(),
|
|
15632
15763
|
/** Manifest-relative JSON path inside the project. */
|
|
15633
|
-
keypointsFile:
|
|
15764
|
+
keypointsFile: z11.string().min(1),
|
|
15634
15765
|
/** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
|
|
15635
15766
|
set: SkeletonSetSchema.optional(),
|
|
15636
15767
|
/** Replace an existing `keypointsFile` with `set`. */
|
|
15637
|
-
overwrite:
|
|
15768
|
+
overwrite: z11.boolean().optional()
|
|
15638
15769
|
}).strict();
|
|
15639
|
-
var SkeletonKeypointsUpdateSchema =
|
|
15640
|
-
action:
|
|
15641
|
-
project:
|
|
15642
|
-
styleId:
|
|
15643
|
-
assetId:
|
|
15770
|
+
var SkeletonKeypointsUpdateSchema = z11.object({
|
|
15771
|
+
action: z11.literal("update-skeleton-keypoints"),
|
|
15772
|
+
project: z11.string().min(1).optional(),
|
|
15773
|
+
styleId: z11.string().min(1),
|
|
15774
|
+
assetId: z11.string().min(1),
|
|
15644
15775
|
/** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
|
|
15645
|
-
expectedSha256:
|
|
15776
|
+
expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/),
|
|
15646
15777
|
set: SkeletonSetSchema
|
|
15647
15778
|
}).strict();
|
|
15648
15779
|
async function createSkeletonAnimation(manifestPath, request) {
|
|
15649
|
-
const root =
|
|
15650
|
-
const file =
|
|
15651
|
-
const relative =
|
|
15652
|
-
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") ||
|
|
15780
|
+
const root = path29.dirname(path29.resolve(manifestPath));
|
|
15781
|
+
const file = path29.resolve(root, request.keypointsFile);
|
|
15782
|
+
const relative = path29.relative(root, file);
|
|
15783
|
+
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path29.isAbsolute(relative)) {
|
|
15653
15784
|
throw new ManifestEditError("the keypoints file must be a .json path inside the project");
|
|
15654
15785
|
}
|
|
15655
|
-
const existed =
|
|
15786
|
+
const existed = existsSync24(file);
|
|
15656
15787
|
let previous = null;
|
|
15657
15788
|
if (request.set) {
|
|
15658
15789
|
if (existed && !request.overwrite) {
|
|
15659
15790
|
throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
|
|
15660
15791
|
}
|
|
15661
15792
|
if (existed) previous = await readFile21(file);
|
|
15662
|
-
await
|
|
15663
|
-
await
|
|
15793
|
+
await mkdir10(path29.dirname(file), { recursive: true });
|
|
15794
|
+
await writeFile13(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
15664
15795
|
} else if (!existed) {
|
|
15665
15796
|
throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
|
|
15666
15797
|
}
|
|
@@ -15675,7 +15806,7 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
15675
15806
|
revision: {
|
|
15676
15807
|
mode: "animate-skeleton",
|
|
15677
15808
|
from: request.from,
|
|
15678
|
-
keypointsFile: relative.split(
|
|
15809
|
+
keypointsFile: relative.split(path29.sep).join("/"),
|
|
15679
15810
|
direction: request.direction,
|
|
15680
15811
|
...request.description ? { description: request.description } : {},
|
|
15681
15812
|
...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
|
|
@@ -15684,7 +15815,7 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
15684
15815
|
});
|
|
15685
15816
|
} catch (error) {
|
|
15686
15817
|
if (request.set) {
|
|
15687
|
-
if (previous) await
|
|
15818
|
+
if (previous) await writeFile13(file, previous);
|
|
15688
15819
|
else await rm10(file, { force: true });
|
|
15689
15820
|
}
|
|
15690
15821
|
throw error;
|
|
@@ -15696,56 +15827,56 @@ async function updateSkeletonKeypoints(ctx, request) {
|
|
|
15696
15827
|
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
15697
15828
|
const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
|
|
15698
15829
|
if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
|
|
15699
|
-
const relative =
|
|
15700
|
-
if (relative.startsWith("..") ||
|
|
15830
|
+
const relative = path29.relative(ctx.loaded.root, file);
|
|
15831
|
+
if (relative.startsWith("..") || path29.isAbsolute(relative)) {
|
|
15701
15832
|
throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
|
|
15702
15833
|
}
|
|
15703
|
-
const current =
|
|
15834
|
+
const current = existsSync24(file) ? sha256(await readFile21(file)) : null;
|
|
15704
15835
|
if (current !== request.expectedSha256) {
|
|
15705
15836
|
throw Object.assign(
|
|
15706
|
-
new Error(`${relative.split(
|
|
15837
|
+
new Error(`${relative.split(path29.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
|
|
15707
15838
|
{ status: 409 }
|
|
15708
15839
|
);
|
|
15709
15840
|
}
|
|
15710
|
-
await
|
|
15711
|
-
return relative.split(
|
|
15841
|
+
await writeFile13(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
15842
|
+
return relative.split(path29.sep).join("/");
|
|
15712
15843
|
}
|
|
15713
15844
|
|
|
15714
15845
|
// src/gallery/edit.ts
|
|
15715
|
-
var Base64 =
|
|
15716
|
-
var HandEditRequestSchema =
|
|
15717
|
-
|
|
15718
|
-
action:
|
|
15719
|
-
project:
|
|
15720
|
-
assetId:
|
|
15721
|
-
styleId:
|
|
15722
|
-
expectedSha256:
|
|
15846
|
+
var Base64 = z12.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
|
|
15847
|
+
var HandEditRequestSchema = z12.discriminatedUnion("action", [
|
|
15848
|
+
z12.object({
|
|
15849
|
+
action: z12.literal("start-edit"),
|
|
15850
|
+
project: z12.string().min(1).optional(),
|
|
15851
|
+
assetId: z12.string().min(1),
|
|
15852
|
+
styleId: z12.string().min(1),
|
|
15853
|
+
expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/),
|
|
15723
15854
|
/** Also launch the author's editor on the file. */
|
|
15724
|
-
open:
|
|
15855
|
+
open: z12.boolean().optional()
|
|
15725
15856
|
}).strict(),
|
|
15726
|
-
|
|
15727
|
-
action:
|
|
15728
|
-
project:
|
|
15729
|
-
assetId:
|
|
15730
|
-
styleId:
|
|
15731
|
-
expectedSha256:
|
|
15857
|
+
z12.object({
|
|
15858
|
+
action: z12.literal("detach-edit"),
|
|
15859
|
+
project: z12.string().min(1).optional(),
|
|
15860
|
+
assetId: z12.string().min(1),
|
|
15861
|
+
styleId: z12.string().min(1),
|
|
15862
|
+
expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/)
|
|
15732
15863
|
}).strict(),
|
|
15733
15864
|
/** From the in-browser editor: the page holds the bytes, the server validates and writes. */
|
|
15734
|
-
|
|
15735
|
-
action:
|
|
15736
|
-
project:
|
|
15737
|
-
assetId:
|
|
15738
|
-
styleId:
|
|
15739
|
-
expectedSha256:
|
|
15865
|
+
z12.object({
|
|
15866
|
+
action: z12.literal("save-edit"),
|
|
15867
|
+
project: z12.string().min(1).optional(),
|
|
15868
|
+
assetId: z12.string().min(1),
|
|
15869
|
+
styleId: z12.string().min(1),
|
|
15870
|
+
expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/),
|
|
15740
15871
|
/** The flattened image; for a frame set, `frames` instead. */
|
|
15741
15872
|
png: Base64.min(1).optional(),
|
|
15742
15873
|
/** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
|
|
15743
|
-
frames:
|
|
15874
|
+
frames: z12.array(z12.object({ role: z12.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
|
|
15744
15875
|
/** The editor's layered project file, kept beside the edit. */
|
|
15745
15876
|
pxo: Base64.optional(),
|
|
15746
15877
|
/** Editor identity from the bridge's ready message. */
|
|
15747
|
-
editor:
|
|
15748
|
-
protocol:
|
|
15878
|
+
editor: z12.string().min(1).max(200),
|
|
15879
|
+
protocol: z12.number().int().positive()
|
|
15749
15880
|
}).strict()
|
|
15750
15881
|
]);
|
|
15751
15882
|
function createGalleryEditHandler(opts) {
|
|
@@ -15786,6 +15917,21 @@ function createGalleryEditHandler(opts) {
|
|
|
15786
15917
|
}
|
|
15787
15918
|
return opts.reload();
|
|
15788
15919
|
}
|
|
15920
|
+
const upload = UploadImageSchema.safeParse(body);
|
|
15921
|
+
if (upload.success) {
|
|
15922
|
+
let manifestPath2;
|
|
15923
|
+
try {
|
|
15924
|
+
manifestPath2 = await opts.manifestFor(upload.data.project);
|
|
15925
|
+
} catch (error) {
|
|
15926
|
+
throw new ManifestEditError(error instanceof Error ? error.message : String(error));
|
|
15927
|
+
}
|
|
15928
|
+
const written = await saveProjectImage(path30.dirname(path30.resolve(manifestPath2)), upload.data);
|
|
15929
|
+
log(` image added: ${written}`);
|
|
15930
|
+
return opts.reload();
|
|
15931
|
+
}
|
|
15932
|
+
if (body?.action === "upload-image") {
|
|
15933
|
+
throw new ManifestEditError("invalid upload: " + upload.error.issues.slice(0, 2).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; "));
|
|
15934
|
+
}
|
|
15789
15935
|
const poses = SkeletonKeypointsUpdateSchema.safeParse(body);
|
|
15790
15936
|
if (poses.success) {
|
|
15791
15937
|
if (!opts.loadProject) throw new ManifestEditError("pose edits are not available in this gallery");
|
|
@@ -15829,19 +15975,35 @@ function createGalleryEditHandler(opts) {
|
|
|
15829
15975
|
}
|
|
15830
15976
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
15831
15977
|
if (result.changed) {
|
|
15832
|
-
const relative =
|
|
15978
|
+
const relative = path30.relative(process.cwd(), result.manifestPath);
|
|
15833
15979
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
15834
|
-
|
|
15835
|
-
log(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
|
|
15980
|
+
log(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
|
|
15836
15981
|
}
|
|
15837
15982
|
return opts.reload();
|
|
15838
15983
|
};
|
|
15839
15984
|
}
|
|
15985
|
+
function describeEdit(edit) {
|
|
15986
|
+
switch (edit.action) {
|
|
15987
|
+
case "batch":
|
|
15988
|
+
return `${edit.edits.length} changes (${edit.edits.map(describeEdit).join("; ")})`;
|
|
15989
|
+
case "add-style":
|
|
15990
|
+
return `added style ${edit.styleId}`;
|
|
15991
|
+
case "patch-style":
|
|
15992
|
+
return `changed style ${edit.styleId}`;
|
|
15993
|
+
case "set-source":
|
|
15994
|
+
case "clear-source":
|
|
15995
|
+
return `changed ${edit.styleId}/${edit.assetId} source`;
|
|
15996
|
+
case "add-asset":
|
|
15997
|
+
return `added ${edit.assetId}`;
|
|
15998
|
+
case "patch-asset":
|
|
15999
|
+
return `changed ${edit.assetId}`;
|
|
16000
|
+
}
|
|
16001
|
+
}
|
|
15840
16002
|
|
|
15841
16003
|
// src/gallery/snapshot.ts
|
|
15842
16004
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
15843
16005
|
function galleryMediaId(absolutePath) {
|
|
15844
|
-
return createHash4("sha256").update(
|
|
16006
|
+
return createHash4("sha256").update(path31.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
15845
16007
|
}
|
|
15846
16008
|
function galleryMediaRoute(id) {
|
|
15847
16009
|
return `/media/${id}`;
|
|
@@ -15860,9 +16022,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
15860
16022
|
return (entry.history ?? []).map((generation, i) => {
|
|
15861
16023
|
const outputs = generation.outputs.map((output) => {
|
|
15862
16024
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
15863
|
-
const file =
|
|
16025
|
+
const file = path31.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
15864
16026
|
let url = null;
|
|
15865
|
-
if (
|
|
16027
|
+
if (existsSync25(file)) {
|
|
15866
16028
|
const id = galleryMediaId(file);
|
|
15867
16029
|
media.set(id, { path: file, contentType: mediaType });
|
|
15868
16030
|
url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
|
|
@@ -15992,10 +16154,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
15992
16154
|
async function describeQuality(media, root, inspection) {
|
|
15993
16155
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
15994
16156
|
const outputs = await Promise.all(
|
|
15995
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
16157
|
+
outputPaths.map((file) => describeOutput(media, root, path31.resolve(root, file)))
|
|
15996
16158
|
);
|
|
15997
|
-
const recordPath =
|
|
15998
|
-
const recordExists =
|
|
16159
|
+
const recordPath = path31.resolve(root, inspection.record);
|
|
16160
|
+
const recordExists = existsSync25(recordPath);
|
|
15999
16161
|
let options = null;
|
|
16000
16162
|
let check = null;
|
|
16001
16163
|
if (recordExists) {
|
|
@@ -16014,7 +16176,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
16014
16176
|
return {
|
|
16015
16177
|
state: inspection.state,
|
|
16016
16178
|
reason: inspection.reason,
|
|
16017
|
-
output: portableOutputPath(
|
|
16179
|
+
output: portableOutputPath(path31.resolve(root, inspection.output), root),
|
|
16018
16180
|
record: portableOutputPath(recordPath, root),
|
|
16019
16181
|
recordExists,
|
|
16020
16182
|
outputs,
|
|
@@ -16054,7 +16216,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
16054
16216
|
const items = [];
|
|
16055
16217
|
const manifestText = await readFile22(loaded.path, "utf8");
|
|
16056
16218
|
const manifestSha256 = sha256(manifestText);
|
|
16057
|
-
const cacheDir =
|
|
16219
|
+
const cacheDir = path31.resolve(path31.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
16058
16220
|
let rawStyles = {};
|
|
16059
16221
|
try {
|
|
16060
16222
|
const raw = JSON.parse(manifestText);
|
|
@@ -16078,12 +16240,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
16078
16240
|
)
|
|
16079
16241
|
);
|
|
16080
16242
|
if (spec.source) {
|
|
16081
|
-
const editPath =
|
|
16243
|
+
const editPath = path31.resolve(root, spec.source);
|
|
16082
16244
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
16083
16245
|
const editShas = await Promise.all(
|
|
16084
16246
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
16085
16247
|
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
16086
|
-
return { file, sha256:
|
|
16248
|
+
return { file, sha256: existsSync25(file) ? await sha256File(file) : null };
|
|
16087
16249
|
})
|
|
16088
16250
|
);
|
|
16089
16251
|
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
@@ -16091,7 +16253,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
16091
16253
|
const companion = await readHandEditCompanion(editPath);
|
|
16092
16254
|
if (companion) {
|
|
16093
16255
|
const projectPath = handEditProjectPath(editPath);
|
|
16094
|
-
const project = companion.project &&
|
|
16256
|
+
const project = companion.project && existsSync25(projectPath) ? await fileInfo(projectPath) : null;
|
|
16095
16257
|
let projectUrl = null;
|
|
16096
16258
|
if (project) {
|
|
16097
16259
|
const id = galleryMediaId(projectPath);
|
|
@@ -16127,8 +16289,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
16127
16289
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
16128
16290
|
}
|
|
16129
16291
|
} else if (spec.source) {
|
|
16130
|
-
outputs = [await describeOutput(media, root,
|
|
16131
|
-
} else if (!entry &&
|
|
16292
|
+
outputs = [await describeOutput(media, root, path31.resolve(root, spec.source))];
|
|
16293
|
+
} else if (!entry && existsSync25(spec.outFile)) {
|
|
16132
16294
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
16133
16295
|
} else {
|
|
16134
16296
|
outputs = [];
|
|
@@ -16329,7 +16491,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
16329
16491
|
id: loaded.manifest.name,
|
|
16330
16492
|
name: loaded.manifest.name,
|
|
16331
16493
|
manifest: loaded.path,
|
|
16332
|
-
lock:
|
|
16494
|
+
lock: path31.resolve(opts.lockPath),
|
|
16333
16495
|
root,
|
|
16334
16496
|
provider: loaded.manifest.provider,
|
|
16335
16497
|
account: null,
|
|
@@ -16355,7 +16517,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
16355
16517
|
return { snapshot, media };
|
|
16356
16518
|
}
|
|
16357
16519
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
16358
|
-
const dir =
|
|
16520
|
+
const dir = path31.dirname(path31.resolve(opts.workspacePath));
|
|
16359
16521
|
const media = /* @__PURE__ */ new Map();
|
|
16360
16522
|
const projects = [];
|
|
16361
16523
|
const items = [];
|
|
@@ -16398,7 +16560,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
16398
16560
|
id: project.id,
|
|
16399
16561
|
name: loaded.manifest.name,
|
|
16400
16562
|
manifest: manifestPath,
|
|
16401
|
-
lock:
|
|
16563
|
+
lock: path31.resolve(lockPath),
|
|
16402
16564
|
root: loaded.root,
|
|
16403
16565
|
provider: loaded.manifest.provider,
|
|
16404
16566
|
account: project.account ?? null,
|
|
@@ -16414,8 +16576,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
16414
16576
|
id: project.id,
|
|
16415
16577
|
name: project.id,
|
|
16416
16578
|
manifest: manifestPath,
|
|
16417
|
-
lock:
|
|
16418
|
-
root:
|
|
16579
|
+
lock: path31.resolve(lockPath),
|
|
16580
|
+
root: path31.dirname(manifestPath),
|
|
16419
16581
|
provider: project.provider,
|
|
16420
16582
|
account: project.account ?? null,
|
|
16421
16583
|
manifestSha256: null,
|
|
@@ -16442,7 +16604,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
16442
16604
|
version: 1,
|
|
16443
16605
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
16444
16606
|
project: null,
|
|
16445
|
-
workspace: { path:
|
|
16607
|
+
workspace: { path: path31.resolve(opts.workspacePath), dir, projects },
|
|
16446
16608
|
filter,
|
|
16447
16609
|
totals: {
|
|
16448
16610
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -16459,7 +16621,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
16459
16621
|
}
|
|
16460
16622
|
|
|
16461
16623
|
// src/gallery/page.ts
|
|
16462
|
-
import { existsSync as
|
|
16624
|
+
import { existsSync as existsSync26, readFileSync as readFileSync5 } from "fs";
|
|
16463
16625
|
|
|
16464
16626
|
// src/gallery/client-bundle.ts
|
|
16465
16627
|
import { createRequire } from "module";
|
|
@@ -16500,7 +16662,7 @@ function clientAssets() {
|
|
|
16500
16662
|
const prebuilt = new URL("gallery.js", CLIENT_DIR);
|
|
16501
16663
|
client ??= {
|
|
16502
16664
|
css: readFileSync5(new URL("gallery.css", CLIENT_DIR), "utf8"),
|
|
16503
|
-
js:
|
|
16665
|
+
js: existsSync26(prebuilt) ? readFileSync5(prebuilt, "utf8") : bundleGalleryClient()
|
|
16504
16666
|
};
|
|
16505
16667
|
return client;
|
|
16506
16668
|
}
|
|
@@ -16542,6 +16704,7 @@ ${css}</style>
|
|
|
16542
16704
|
<option value="none">Group: none</option>
|
|
16543
16705
|
</select>
|
|
16544
16706
|
<span id="editing" class="editing" hidden title="This gallery can write the manifest. It never contacts a provider.">editing</span>
|
|
16707
|
+
<button id="studio" type="button" hidden title="Draft a new character: its style, base, loops, and portrait, priced as you go">+ New character</button>
|
|
16545
16708
|
<button id="refresh" type="button" title="Re-read the manifest, lockfile, and disk">Refresh</button>
|
|
16546
16709
|
<label class="chip" title="Refresh every 5 seconds while this tab is visible"><input id="auto" type="checkbox"> auto</label>
|
|
16547
16710
|
</div>
|
|
@@ -16627,10 +16790,10 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
|
|
|
16627
16790
|
|
|
16628
16791
|
// src/editor/install.ts
|
|
16629
16792
|
import { createHash as createHash5 } from "crypto";
|
|
16630
|
-
import { existsSync as
|
|
16631
|
-
import { mkdir as
|
|
16793
|
+
import { existsSync as existsSync27 } from "fs";
|
|
16794
|
+
import { mkdir as mkdir11, readFile as readFile23, rename as rename9, rm as rm11, stat as stat4, writeFile as writeFile14 } from "fs/promises";
|
|
16632
16795
|
import os2 from "os";
|
|
16633
|
-
import
|
|
16796
|
+
import path32 from "path";
|
|
16634
16797
|
var EditorInstallError = class extends Error {
|
|
16635
16798
|
constructor(message6, file) {
|
|
16636
16799
|
super(message6);
|
|
@@ -16641,15 +16804,15 @@ var EditorInstallError = class extends Error {
|
|
|
16641
16804
|
};
|
|
16642
16805
|
function editorToolsRoot() {
|
|
16643
16806
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
16644
|
-
if (override) return
|
|
16645
|
-
if (process.platform === "darwin") return
|
|
16807
|
+
if (override) return path32.resolve(override);
|
|
16808
|
+
if (process.platform === "darwin") return path32.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
16646
16809
|
if (process.platform === "win32") {
|
|
16647
|
-
return
|
|
16810
|
+
return path32.join(process.env.LOCALAPPDATA ?? path32.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
16648
16811
|
}
|
|
16649
|
-
return
|
|
16812
|
+
return path32.join(process.env.XDG_CACHE_HOME ?? path32.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
16650
16813
|
}
|
|
16651
16814
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
16652
|
-
return
|
|
16815
|
+
return path32.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
16653
16816
|
}
|
|
16654
16817
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
16655
16818
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -16672,8 +16835,8 @@ async function editorStatus(opts = {}) {
|
|
|
16672
16835
|
let totalBytes = 0;
|
|
16673
16836
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
16674
16837
|
totalBytes += expected.bytes;
|
|
16675
|
-
const file =
|
|
16676
|
-
const info =
|
|
16838
|
+
const file = path32.join(dir, name);
|
|
16839
|
+
const info = existsSync27(file) ? await stat4(file).catch(() => null) : null;
|
|
16677
16840
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
16678
16841
|
installedBytes += expected.bytes;
|
|
16679
16842
|
} else {
|
|
@@ -16702,7 +16865,7 @@ async function installEditor(opts = {}) {
|
|
|
16702
16865
|
);
|
|
16703
16866
|
}
|
|
16704
16867
|
if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
|
|
16705
|
-
await
|
|
16868
|
+
await mkdir11(dir, { recursive: true });
|
|
16706
16869
|
const downloaded = [];
|
|
16707
16870
|
const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
|
|
16708
16871
|
let fetchedBytes = before.installedBytes;
|
|
@@ -16730,10 +16893,10 @@ async function installEditor(opts = {}) {
|
|
|
16730
16893
|
if (digest2 !== expected.sha256) {
|
|
16731
16894
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
16732
16895
|
}
|
|
16733
|
-
const final =
|
|
16896
|
+
const final = path32.join(dir, name);
|
|
16734
16897
|
const tmp = `${final}.${process.pid}.part`;
|
|
16735
16898
|
try {
|
|
16736
|
-
await
|
|
16899
|
+
await writeFile14(tmp, bytes);
|
|
16737
16900
|
await rename9(tmp, final);
|
|
16738
16901
|
} finally {
|
|
16739
16902
|
await rm11(tmp, { force: true });
|
|
@@ -16776,12 +16939,12 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
16776
16939
|
".json": "application/json; charset=utf-8"
|
|
16777
16940
|
};
|
|
16778
16941
|
function editorContentType(name) {
|
|
16779
|
-
return EDITOR_CONTENT_TYPES[
|
|
16942
|
+
return EDITOR_CONTENT_TYPES[path32.extname(name).toLowerCase()] ?? null;
|
|
16780
16943
|
}
|
|
16781
16944
|
|
|
16782
16945
|
// src/gallery/editor.ts
|
|
16783
16946
|
import { readFile as readFile24 } from "fs/promises";
|
|
16784
|
-
import
|
|
16947
|
+
import path33 from "path";
|
|
16785
16948
|
function editorRoute(release, name = "index.html") {
|
|
16786
16949
|
return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
|
|
16787
16950
|
}
|
|
@@ -16854,7 +17017,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
16854
17017
|
if (!status.installed) return null;
|
|
16855
17018
|
let bytes;
|
|
16856
17019
|
try {
|
|
16857
|
-
bytes = await readFile24(
|
|
17020
|
+
bytes = await readFile24(path33.join(dir, name));
|
|
16858
17021
|
} catch {
|
|
16859
17022
|
verified = null;
|
|
16860
17023
|
return null;
|
|
@@ -16899,7 +17062,7 @@ async function readJsonBody(req) {
|
|
|
16899
17062
|
async function serveGallery(opts) {
|
|
16900
17063
|
const log = opts.onProgress ?? (() => {
|
|
16901
17064
|
});
|
|
16902
|
-
const session = opts.edit || opts.generate || opts.editor || opts.skeleton ? randomBytes(16).toString("hex") : null;
|
|
17065
|
+
const session = opts.edit || opts.generate || opts.editor || opts.skeleton || opts.price ? randomBytes(16).toString("hex") : null;
|
|
16903
17066
|
let media = /* @__PURE__ */ new Map();
|
|
16904
17067
|
let loading = null;
|
|
16905
17068
|
const reviewAssets = /* @__PURE__ */ new Map();
|
|
@@ -16974,6 +17137,17 @@ async function serveGallery(opts) {
|
|
|
16974
17137
|
}
|
|
16975
17138
|
return;
|
|
16976
17139
|
}
|
|
17140
|
+
if (req.method === "POST" && url2.pathname === "/api/price") {
|
|
17141
|
+
if (!opts.price || !session) return fail(405, "pricing an edit needs the gallery started with --edit");
|
|
17142
|
+
try {
|
|
17143
|
+
const body = await guardedBody("price requests");
|
|
17144
|
+
if (body === void 0) return;
|
|
17145
|
+
json(200, await opts.price(body));
|
|
17146
|
+
} catch (err) {
|
|
17147
|
+
reportError("price", err);
|
|
17148
|
+
}
|
|
17149
|
+
return;
|
|
17150
|
+
}
|
|
16977
17151
|
if (req.method === "POST" && url2.pathname === "/api/skeleton/estimate") {
|
|
16978
17152
|
if (!opts.skeleton || !session) return fail(405, "skeleton estimates need the gallery started with --edit");
|
|
16979
17153
|
try {
|
|
@@ -17168,27 +17342,27 @@ async function serveGallery(opts) {
|
|
|
17168
17342
|
|
|
17169
17343
|
// src/gallery/generate.ts
|
|
17170
17344
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
17171
|
-
import { z as
|
|
17172
|
-
var GenerateRequestSchema =
|
|
17173
|
-
project:
|
|
17174
|
-
keys:
|
|
17345
|
+
import { z as z13 } from "zod";
|
|
17346
|
+
var GenerateRequestSchema = z13.object({
|
|
17347
|
+
project: z13.string().min(1).optional(),
|
|
17348
|
+
keys: z13.array(z13.string().min(1)).min(1).max(500),
|
|
17175
17349
|
/**
|
|
17176
17350
|
* Regenerate work that is up to date, as `gen --force` does, and let the
|
|
17177
17351
|
* fetch replace a locally modified or untracked file. With `restore`, put
|
|
17178
17352
|
* the recorded bytes back over a modified file.
|
|
17179
17353
|
*/
|
|
17180
|
-
force:
|
|
17354
|
+
force: z13.boolean().optional(),
|
|
17181
17355
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
17182
|
-
resume:
|
|
17356
|
+
resume: z13.boolean().optional(),
|
|
17183
17357
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
17184
|
-
refresh:
|
|
17358
|
+
refresh: z13.boolean().optional(),
|
|
17185
17359
|
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
17186
|
-
restore:
|
|
17360
|
+
restore: z13.boolean().optional(),
|
|
17187
17361
|
/**
|
|
17188
17362
|
* Bring a replaced generation back: 1-based position in the record's
|
|
17189
17363
|
* history (newest first) or an output hash prefix. One key at a time.
|
|
17190
17364
|
*/
|
|
17191
|
-
revert:
|
|
17365
|
+
revert: z13.string().min(1).max(64).optional()
|
|
17192
17366
|
}).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
|
|
17193
17367
|
message: "resume, refresh, restore, and revert are exclusive"
|
|
17194
17368
|
}).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
|
|
@@ -17312,6 +17486,70 @@ function createGenerateHandlers(opts) {
|
|
|
17312
17486
|
job.phase = "done";
|
|
17313
17487
|
job.finishedAt = now().toISOString();
|
|
17314
17488
|
}
|
|
17489
|
+
async function submitGroups(job, ctx, groups) {
|
|
17490
|
+
job.phase = "submitting";
|
|
17491
|
+
for (const group of groups) {
|
|
17492
|
+
const provider = opts.providerFor(job.project ?? void 0, group.provider);
|
|
17493
|
+
const ceiling = remainingFor(group, groups.length, opts.budget, spent);
|
|
17494
|
+
if (provider.balance) {
|
|
17495
|
+
const balance = await provider.balance();
|
|
17496
|
+
say(job, `balance: ${formatCost(balance.unit, balance.remaining)} remaining (${group.provider})`);
|
|
17497
|
+
if (balance.unit !== group.costUnit) {
|
|
17498
|
+
throw new Error(
|
|
17499
|
+
`Provider ${group.provider} estimate unit ${group.costUnit} does not match balance unit ${balance.unit}.`
|
|
17500
|
+
);
|
|
17501
|
+
}
|
|
17502
|
+
if (balance.unit !== "free" && group.cost > balance.remaining) {
|
|
17503
|
+
throw new Error(
|
|
17504
|
+
`${group.provider} needs ${formatCost(balance.unit, group.cost)} but only ${formatCost(balance.unit, balance.remaining)} remain.`
|
|
17505
|
+
);
|
|
17506
|
+
}
|
|
17507
|
+
}
|
|
17508
|
+
const res = await submit(provider, ctx.loaded, group.actionable, ctx.lock, ctx.lockPath, {
|
|
17509
|
+
budget: ceiling,
|
|
17510
|
+
onProgress: (line) => say(job, line),
|
|
17511
|
+
spacingMs: opts.submitSpacingMs
|
|
17512
|
+
});
|
|
17513
|
+
spent[group.provider] = (spent[group.provider] ?? 0) + res.spent;
|
|
17514
|
+
units[group.provider] = res.unit;
|
|
17515
|
+
job.spent[group.provider] = (job.spent[group.provider] ?? 0) + res.spent;
|
|
17516
|
+
job.counts.submitted += res.submitted;
|
|
17517
|
+
job.counts.failed += res.failed;
|
|
17518
|
+
say(job, `${group.provider}: submitted ${res.submitted}, failed ${res.failed}, estimated ${formatCost(res.unit, res.spent)}`);
|
|
17519
|
+
}
|
|
17520
|
+
}
|
|
17521
|
+
async function generateInWaves(job, ctx, specs, groups) {
|
|
17522
|
+
for (let wave = 1; ; wave++) {
|
|
17523
|
+
const submittedBefore = job.counts.submitted;
|
|
17524
|
+
await submitGroups(job, ctx, groups);
|
|
17525
|
+
await settle(job, ctx, specs);
|
|
17526
|
+
if (job.phase !== "done" || job.counts.submitted === submittedBefore) return;
|
|
17527
|
+
const finishedAt = job.finishedAt;
|
|
17528
|
+
const finish = () => {
|
|
17529
|
+
job.phase = "done";
|
|
17530
|
+
job.finishedAt = finishedAt;
|
|
17531
|
+
};
|
|
17532
|
+
job.phase = "queued";
|
|
17533
|
+
job.finishedAt = null;
|
|
17534
|
+
const next = await opts.loadProject(job.project ?? void 0);
|
|
17535
|
+
const byKey = new Map(next.specs.map((spec) => [lockKey(spec.styleId, spec.assetId), spec]));
|
|
17536
|
+
const nextSpecs = job.keys.map((key) => byKey.get(key)).filter((spec) => Boolean(spec));
|
|
17537
|
+
const plan = await buildPlan(nextSpecs, next.lock);
|
|
17538
|
+
if (!plan.actionable.length) return finish();
|
|
17539
|
+
for (const group of plan.groups) {
|
|
17540
|
+
const remaining = remainingFor(group, plan.groups.length, opts.budget, spent);
|
|
17541
|
+
if (group.cost > remaining) {
|
|
17542
|
+
say(job, `wave ${wave + 1} would spend ${formatCost(group.costUnit, group.cost)} on ${group.provider} but only ${formatCost(group.costUnit, remaining)} of this session's budget remain; stopping here`);
|
|
17543
|
+
return finish();
|
|
17544
|
+
}
|
|
17545
|
+
}
|
|
17546
|
+
say(job, `wave ${wave + 1}: ${plan.actionable.length} asset(s) whose parents are now on disk: ` + plan.groups.map((g) => `${g.provider} ${formatCost(g.costUnit, g.cost)}`).join("; "));
|
|
17547
|
+
contexts.set(job.id, next);
|
|
17548
|
+
ctx = next;
|
|
17549
|
+
specs = nextSpecs;
|
|
17550
|
+
groups = plan.groups;
|
|
17551
|
+
}
|
|
17552
|
+
}
|
|
17315
17553
|
async function run(job, ctx, specs, groups) {
|
|
17316
17554
|
try {
|
|
17317
17555
|
if (job.mode === "refresh") {
|
|
@@ -17339,36 +17577,8 @@ function createGenerateHandlers(opts) {
|
|
|
17339
17577
|
return;
|
|
17340
17578
|
}
|
|
17341
17579
|
if (job.mode === "generate") {
|
|
17342
|
-
job
|
|
17343
|
-
|
|
17344
|
-
const provider = opts.providerFor(job.project ?? void 0, group.provider);
|
|
17345
|
-
const ceiling = remainingFor(group, groups.length, opts.budget, spent);
|
|
17346
|
-
if (provider.balance) {
|
|
17347
|
-
const balance = await provider.balance();
|
|
17348
|
-
say(job, `balance: ${formatCost(balance.unit, balance.remaining)} remaining (${group.provider})`);
|
|
17349
|
-
if (balance.unit !== group.costUnit) {
|
|
17350
|
-
throw new Error(
|
|
17351
|
-
`Provider ${group.provider} estimate unit ${group.costUnit} does not match balance unit ${balance.unit}.`
|
|
17352
|
-
);
|
|
17353
|
-
}
|
|
17354
|
-
if (balance.unit !== "free" && group.cost > balance.remaining) {
|
|
17355
|
-
throw new Error(
|
|
17356
|
-
`${group.provider} needs ${formatCost(balance.unit, group.cost)} but only ${formatCost(balance.unit, balance.remaining)} remain.`
|
|
17357
|
-
);
|
|
17358
|
-
}
|
|
17359
|
-
}
|
|
17360
|
-
const res = await submit(provider, ctx.loaded, group.actionable, ctx.lock, ctx.lockPath, {
|
|
17361
|
-
budget: ceiling,
|
|
17362
|
-
onProgress: (line) => say(job, line),
|
|
17363
|
-
spacingMs: opts.submitSpacingMs
|
|
17364
|
-
});
|
|
17365
|
-
spent[group.provider] = (spent[group.provider] ?? 0) + res.spent;
|
|
17366
|
-
units[group.provider] = res.unit;
|
|
17367
|
-
job.spent[group.provider] = (job.spent[group.provider] ?? 0) + res.spent;
|
|
17368
|
-
job.counts.submitted += res.submitted;
|
|
17369
|
-
job.counts.failed += res.failed;
|
|
17370
|
-
say(job, `${group.provider}: submitted ${res.submitted}, failed ${res.failed}, estimated ${formatCost(res.unit, res.spent)}`);
|
|
17371
|
-
}
|
|
17580
|
+
await generateInWaves(job, ctx, specs, groups);
|
|
17581
|
+
return;
|
|
17372
17582
|
}
|
|
17373
17583
|
await settle(job, ctx, specs);
|
|
17374
17584
|
} catch (error) {
|
|
@@ -17499,15 +17709,15 @@ function createGenerateHandlers(opts) {
|
|
|
17499
17709
|
|
|
17500
17710
|
// src/pipeline/salvage.ts
|
|
17501
17711
|
import { readFile as readFile26 } from "fs/promises";
|
|
17502
|
-
import { existsSync as
|
|
17503
|
-
import
|
|
17712
|
+
import { existsSync as existsSync28 } from "fs";
|
|
17713
|
+
import path34 from "path";
|
|
17504
17714
|
function providerClaimId(provider, objectId) {
|
|
17505
17715
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
17506
17716
|
}
|
|
17507
17717
|
async function loadClaims(lockPaths, opts = {}) {
|
|
17508
17718
|
const claimed = /* @__PURE__ */ new Set();
|
|
17509
17719
|
for (const p of lockPaths) {
|
|
17510
|
-
if (!
|
|
17720
|
+
if (!existsSync28(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
17511
17721
|
let parsed;
|
|
17512
17722
|
try {
|
|
17513
17723
|
parsed = parseLock(JSON.parse(await readFile26(p, "utf8")));
|
|
@@ -17614,20 +17824,20 @@ function matchOrphanStyle(prompt, manifest) {
|
|
|
17614
17824
|
return matchStyleByPattern(prompt, manifest);
|
|
17615
17825
|
}
|
|
17616
17826
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
17617
|
-
const own =
|
|
17827
|
+
const own = path34.resolve(ownManifestPath);
|
|
17618
17828
|
const siblingManifestPaths = [
|
|
17619
17829
|
.../* @__PURE__ */ new Set([
|
|
17620
17830
|
...workspaceManifestPaths,
|
|
17621
|
-
...claimPaths.map((c) =>
|
|
17831
|
+
...claimPaths.map((c) => path34.join(path34.dirname(path34.resolve(c)), "pixelkiln.manifest.json"))
|
|
17622
17832
|
])
|
|
17623
17833
|
];
|
|
17624
17834
|
const siblings = [];
|
|
17625
17835
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
17626
|
-
if (
|
|
17627
|
-
if (!
|
|
17836
|
+
if (path34.resolve(siblingManifestPath) === own) continue;
|
|
17837
|
+
if (!existsSync28(siblingManifestPath)) continue;
|
|
17628
17838
|
try {
|
|
17629
17839
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
17630
|
-
siblings.push({ label:
|
|
17840
|
+
siblings.push({ label: path34.basename(path34.dirname(siblingManifestPath)), manifest });
|
|
17631
17841
|
} catch {
|
|
17632
17842
|
}
|
|
17633
17843
|
}
|
|
@@ -17747,8 +17957,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
17747
17957
|
}
|
|
17748
17958
|
|
|
17749
17959
|
// src/pick/salvage-server.ts
|
|
17750
|
-
import { mkdir as
|
|
17751
|
-
import
|
|
17960
|
+
import { mkdir as mkdir12, writeFile as writeFile15, readFile as readFile27 } from "fs/promises";
|
|
17961
|
+
import path35 from "path";
|
|
17752
17962
|
|
|
17753
17963
|
// src/pick/salvage-sheet.ts
|
|
17754
17964
|
var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -17914,7 +18124,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
17914
18124
|
});
|
|
17915
18125
|
const html = renderSalvageSheet(orphans, {
|
|
17916
18126
|
styleId: ctx.styleId,
|
|
17917
|
-
importDir:
|
|
18127
|
+
importDir: path35.relative(process.cwd(), ctx.importDir) || "."
|
|
17918
18128
|
});
|
|
17919
18129
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
17920
18130
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -17970,10 +18180,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
17970
18180
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
17971
18181
|
decodePng(buf);
|
|
17972
18182
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
17973
|
-
const rel =
|
|
17974
|
-
const outFile =
|
|
17975
|
-
await
|
|
17976
|
-
await
|
|
18183
|
+
const rel = path35.join("_salvaged", `${assetId}.png`);
|
|
18184
|
+
const outFile = path35.resolve(ctx.importDir, rel);
|
|
18185
|
+
await mkdir12(path35.dirname(outFile), { recursive: true });
|
|
18186
|
+
await writeFile15(outFile, buf);
|
|
17977
18187
|
ctx.manifest.assets[assetId] = {
|
|
17978
18188
|
prompt: orphan.prompt,
|
|
17979
18189
|
promptByStyle: {},
|
|
@@ -18005,7 +18215,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
18005
18215
|
sourceUrl: durableSource,
|
|
18006
18216
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
18007
18217
|
outputs: [{
|
|
18008
|
-
path: portableOutputPath(outFile,
|
|
18218
|
+
path: portableOutputPath(outFile, path35.dirname(ctx.manifestPath)),
|
|
18009
18219
|
sha256: sha256(buf)
|
|
18010
18220
|
}],
|
|
18011
18221
|
submittedAt: orphan.createdAt,
|
|
@@ -18031,7 +18241,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
18031
18241
|
await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
|
|
18032
18242
|
const raw = JSON.parse(await readFile27(ctx.manifestPath, "utf8"));
|
|
18033
18243
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
18034
|
-
await
|
|
18244
|
+
await writeFile15(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
18035
18245
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
18036
18246
|
if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
|
|
18037
18247
|
return result;
|