pixelkiln 0.48.0 → 0.50.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 +289 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +289 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +834 -4
- package/dist/index.d.ts +834 -4
- package/dist/index.js +289 -5
- package/dist/index.js.map +1 -1
- package/docs/ENDPOINTS.md +61 -3
- package/docs/PIXELLAB.md +36 -8
- package/docs/REVISIONS.md +72 -2
- package/package.json +1 -1
- package/skills/pixelkiln/SKILL.md +11 -1
- package/skills/pixelkiln/references/pixellab-fidelity.md +186 -0
- package/skills/pixelkiln/references/pixellab-roadmap.md +176 -0
- package/skills/pixelkiln/references/pixellab.md +135 -6
package/dist/cli.js
CHANGED
|
@@ -938,17 +938,37 @@ var StyleObjectSchema = z.object({
|
|
|
938
938
|
shading: z.string().optional(),
|
|
939
939
|
detail: z.string().optional(),
|
|
940
940
|
/**
|
|
941
|
-
* `tiles` generator only. Edge length of one tile, 16-
|
|
941
|
+
* `tiles` generator only. Edge length of one tile, 16-128 (the API's own
|
|
942
|
+
* range; connectable sets narrow it further per shape, e.g. square
|
|
943
|
+
* top-down roads are fixed at 32).
|
|
942
944
|
*
|
|
943
945
|
* Ignored when `styleImages` is set: style mode takes the tile's shape
|
|
944
946
|
* and dimensions from the reference image, which is the whole reason to
|
|
945
947
|
* use it against an existing sheet.
|
|
946
948
|
*/
|
|
947
|
-
tileSize: z.number().int().min(16).max(
|
|
949
|
+
tileSize: z.number().int().min(16).max(128).optional(),
|
|
950
|
+
/** `tiles` generator only. Height in pixels (16-256) for a non-square
|
|
951
|
+
* tile (e.g. a tall building wall); omit to compute it from `tileType`
|
|
952
|
+
* geometry and the view angle. */
|
|
953
|
+
tileHeight: z.number().int().min(16).max(256).optional(),
|
|
948
954
|
/** `tiles` generator only. Defaults to the API's `isometric`. */
|
|
949
955
|
tileType: z.enum(["hex", "hex_pointy", "isometric", "oblique", "octagon", "square_topdown"]).optional(),
|
|
950
956
|
/** `tiles` generator only. Defaults to the API's `low top-down`. */
|
|
951
957
|
tileView: z.enum(["top-down", "high top-down", "low top-down", "side"]).optional(),
|
|
958
|
+
/** `tiles` generator only. Continuous view angle in degrees (0 = side,
|
|
959
|
+
* 90 = top-down), overriding `tileView` when set. */
|
|
960
|
+
tileViewAngle: z.number().min(0).max(90).optional(),
|
|
961
|
+
/** `tiles` generator only. Tile depth/thickness as a ratio (0-1) of the
|
|
962
|
+
* tile's height, overriding the default the API computes from the
|
|
963
|
+
* view. This is the tutorials' "thickness" control. */
|
|
964
|
+
tileDepthRatio: z.number().min(0).max(1).optional(),
|
|
965
|
+
/** `tiles` generator only, `tileType: "isometric"`. Top/bottom cap
|
|
966
|
+
* width in pixels: 2 for the classic look, 4 for a more modern one. */
|
|
967
|
+
tileFlatTopPx: z.number().int().min(2).max(8).optional(),
|
|
968
|
+
/** `tiles` generator only, `tileType: "oblique"` (ground tiles or
|
|
969
|
+
* building walls). Horizontal shear per pixel of height (0-1); 0.5 is
|
|
970
|
+
* a classic cabinet projection (~27°), 1.0 a full 45° diagonal. */
|
|
971
|
+
obliqueLean: z.number().min(0).max(1).optional(),
|
|
952
972
|
/**
|
|
953
973
|
* `tiles` generator only. Asks for a connectable set instead of
|
|
954
974
|
* independent variations:
|
|
@@ -963,6 +983,28 @@ var StyleObjectSchema = z.object({
|
|
|
963
983
|
* is load-bearing; do not sort a connectable set by anything else.
|
|
964
984
|
*/
|
|
965
985
|
tileFeature: z.enum(["roads", "tileset", "building"]).optional(),
|
|
986
|
+
/** `tileFeature: "building"` only. Wall height in tiles (1-3); the API
|
|
987
|
+
* defaults to 2. */
|
|
988
|
+
buildingWallTiles: z.number().int().min(1).max(3).optional(),
|
|
989
|
+
/** `tileFeature: "building"` only. `"grid"` paints each shaped piece
|
|
990
|
+
* individually (richer, the isometric default); `"materials"` paints
|
|
991
|
+
* flat swatches and renders pieces from them (more consistent for
|
|
992
|
+
* square top-down building kits). */
|
|
993
|
+
buildingLayout: z.enum(["grid", "materials"]).optional(),
|
|
994
|
+
/** `tileFeature: "building"` only. Wall material, e.g. "stone brick
|
|
995
|
+
* walls" — more reliable than relying on the main `prompt` being split
|
|
996
|
+
* into wall/floor parts. */
|
|
997
|
+
buildingWallDescription: z.string().min(1).max(500).optional(),
|
|
998
|
+
/** `tileFeature: "building"` only. Floor material, e.g. "wooden plank
|
|
999
|
+
* floor". */
|
|
1000
|
+
buildingFloorDescription: z.string().min(1).max(500).optional(),
|
|
1001
|
+
/** `tileFeature: "building"` only. Upper-storey or roof surface;
|
|
1002
|
+
* defaults to the wall material when omitted. */
|
|
1003
|
+
buildingFloor2Description: z.string().min(1).max(500).optional(),
|
|
1004
|
+
/** `tileFeature: "building"` with `tileType: "square_topdown"` only.
|
|
1005
|
+
* Wall storey height as its own camera angle in degrees (5-90),
|
|
1006
|
+
* decoupled from the ground's pitch. */
|
|
1007
|
+
buildingWallAngle: z.number().min(5).max(90).optional(),
|
|
966
1008
|
/**
|
|
967
1009
|
* `tiles` generator only. How tile edges are drawn.
|
|
968
1010
|
*
|
|
@@ -5125,6 +5167,10 @@ var TilesProSchema = z2.object({
|
|
|
5125
5167
|
kind: z2.string().nullable().default(null),
|
|
5126
5168
|
tile_rules: z2.record(z2.unknown()).nullable().optional()
|
|
5127
5169
|
}).passthrough();
|
|
5170
|
+
var RevisionJobSubmitSchema = z2.object({
|
|
5171
|
+
background_job_id: z2.string().min(1),
|
|
5172
|
+
status: z2.string().default("processing")
|
|
5173
|
+
}).passthrough();
|
|
5128
5174
|
var PixelLabObjectSchema = z2.object({
|
|
5129
5175
|
id: z2.string().min(1),
|
|
5130
5176
|
name: z2.string().nullable().default(null),
|
|
@@ -5341,9 +5387,20 @@ var PixelLabClient = class {
|
|
|
5341
5387
|
async createTilesPro(args) {
|
|
5342
5388
|
const body = { description: args.description };
|
|
5343
5389
|
if (args.tileSize != null) body.tile_size = args.tileSize;
|
|
5390
|
+
if (args.tileHeight != null) body.tile_height = args.tileHeight;
|
|
5344
5391
|
if (args.tileType) body.tile_type = args.tileType;
|
|
5345
5392
|
if (args.tileView) body.tile_view = args.tileView;
|
|
5393
|
+
if (args.tileViewAngle != null) body.tile_view_angle = args.tileViewAngle;
|
|
5394
|
+
if (args.tileDepthRatio != null) body.tile_depth_ratio = args.tileDepthRatio;
|
|
5395
|
+
if (args.tileFlatTopPx != null) body.tile_flat_top_px = args.tileFlatTopPx;
|
|
5396
|
+
if (args.obliqueLean != null) body.oblique_lean = args.obliqueLean;
|
|
5346
5397
|
if (args.tileFeature) body.tile_feature = args.tileFeature;
|
|
5398
|
+
if (args.buildingWallTiles != null) body.building_wall_tiles = args.buildingWallTiles;
|
|
5399
|
+
if (args.buildingLayout) body.building_layout = args.buildingLayout;
|
|
5400
|
+
if (args.buildingWallDescription) body.building_wall_description = args.buildingWallDescription;
|
|
5401
|
+
if (args.buildingFloorDescription) body.building_floor_description = args.buildingFloorDescription;
|
|
5402
|
+
if (args.buildingFloor2Description) body.building_floor2_description = args.buildingFloor2Description;
|
|
5403
|
+
if (args.buildingWallAngle != null) body.building_wall_angle = args.buildingWallAngle;
|
|
5347
5404
|
if (args.outlineMode) body.outline_mode = args.outlineMode;
|
|
5348
5405
|
if (args.seed != null) body.seed = args.seed;
|
|
5349
5406
|
if (args.styleImages?.length) body.style_images = args.styleImages;
|
|
@@ -5596,6 +5653,71 @@ var PixelLabClient = class {
|
|
|
5596
5653
|
if (page.characters.length === 0 || offset >= page.total) return;
|
|
5597
5654
|
}
|
|
5598
5655
|
}
|
|
5656
|
+
/**
|
|
5657
|
+
* Masked inpaint, PixelLab's `/inpaint-v3` (the endpoint its own docs list
|
|
5658
|
+
* first in the Inpaint section, its convention for "reach for this by
|
|
5659
|
+
* default"). Exercised live at nine sizes, all returning `last_response.
|
|
5660
|
+
* image` as a single `{type, base64, width, height}` (the first shape
|
|
5661
|
+
* `pollRevision` in pixellab.ts checks, so no change was ever needed):
|
|
5662
|
+
* 32x32 (1024px², billed 20, this adapter's tiering floor) through
|
|
5663
|
+
* 256x256 (65536px²) all billed 20 (the tiering wrongly predicts 25 from
|
|
5664
|
+
* 1024px² up, see estimate() in pixellab.ts); 288x288 (82944px²) and
|
|
5665
|
+
* 320x320 (102400px²) billed 25 — the middle tier is real, just starting
|
|
5666
|
+
* far higher than this tiering assumes; 352x352 (123904px²), 384x384
|
|
5667
|
+
* (147456px²), and 512x512 (262144px², the tiering ceiling) all billed
|
|
5668
|
+
* 40. Both breakpoints are now tightly bracketed: 20->25 in
|
|
5669
|
+
* (65536px², 82944px²], 25->40 in (102400px², 123904px²].
|
|
5670
|
+
* `editImagesV2` below returns the same fields under `images`, plural
|
|
5671
|
+
* and array-wrapped, not `image` — do not assume the two endpoints share
|
|
5672
|
+
* one response shape. See docs/ENDPOINTS.md and docs/REVISIONS.md for
|
|
5673
|
+
* the full shape and cost picture, including what is still unconfirmed.
|
|
5674
|
+
*
|
|
5675
|
+
* `crop_to_mask` defaults true upstream (confirmed in the schema): PixelLab
|
|
5676
|
+
* otherwise blends generated pixels outside the mask edge to "fit
|
|
5677
|
+
* naturally," which is the opposite of what a mask boundary is for.
|
|
5678
|
+
*/
|
|
5679
|
+
async inpaintV3(args) {
|
|
5680
|
+
const size = { width: args.width, height: args.height };
|
|
5681
|
+
const body = {
|
|
5682
|
+
description: args.description,
|
|
5683
|
+
inpainting_image: { image: args.image, size },
|
|
5684
|
+
mask_image: { image: args.maskImage, size }
|
|
5685
|
+
};
|
|
5686
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
5687
|
+
if (args.cropToMask != null) body.crop_to_mask = args.cropToMask;
|
|
5688
|
+
if (args.seed != null) body.seed = args.seed;
|
|
5689
|
+
return validateResponse(
|
|
5690
|
+
RevisionJobSubmitSchema,
|
|
5691
|
+
await this.request("/inpaint-v3", { method: "POST", body: JSON.stringify(body) }),
|
|
5692
|
+
"inpaint-v3"
|
|
5693
|
+
);
|
|
5694
|
+
}
|
|
5695
|
+
/**
|
|
5696
|
+
* Whole-image edit with no mask, PixelLab's `/edit-images-v2`. `edit_images`
|
|
5697
|
+
* takes an array (the endpoint supports editing several images with one
|
|
5698
|
+
* instruction); pixelkiln's `revision` model is one parent per child, so
|
|
5699
|
+
* this always sends exactly one. Exercised live at its floor size (32x32):
|
|
5700
|
+
* billed exactly the 20-generation estimate this adapter borrows from
|
|
5701
|
+
* `1dir`, matching `inpaintV3`'s floor exactly. The completed response is
|
|
5702
|
+
* `last_response.images`, an *array* of `{type, base64, width, height}` —
|
|
5703
|
+
* plural and array-wrapped, unlike `inpaintV3`'s singular `image` above,
|
|
5704
|
+
* even though exactly one image is ever sent or expected here.
|
|
5705
|
+
*/
|
|
5706
|
+
async editImagesV2(args) {
|
|
5707
|
+
const body = {
|
|
5708
|
+
method: "edit_with_text",
|
|
5709
|
+
description: args.description,
|
|
5710
|
+
edit_images: [{ image: args.image, width: args.width, height: args.height }],
|
|
5711
|
+
image_size: { width: args.width, height: args.height }
|
|
5712
|
+
};
|
|
5713
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
5714
|
+
if (args.seed != null) body.seed = args.seed;
|
|
5715
|
+
return validateResponse(
|
|
5716
|
+
RevisionJobSubmitSchema,
|
|
5717
|
+
await this.request("/edit-images-v2", { method: "POST", body: JSON.stringify(body) }),
|
|
5718
|
+
"edit-images-v2"
|
|
5719
|
+
);
|
|
5720
|
+
}
|
|
5599
5721
|
async getBackgroundJob(jobId) {
|
|
5600
5722
|
const raw = await this.request(`/background-jobs/${encodeURIComponent(jobId)}`);
|
|
5601
5723
|
return validateResponse(BackgroundJobSchema, raw, "background-jobs/{id}");
|
|
@@ -5736,6 +5858,15 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5736
5858
|
supports(generator) {
|
|
5737
5859
|
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "character";
|
|
5738
5860
|
}
|
|
5861
|
+
/**
|
|
5862
|
+
* `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
|
|
5863
|
+
* no mask) both exist on PixelLab. `outpaint` does not: there is no
|
|
5864
|
+
* canvas-expansion endpoint in the API, matching docs/REVISIONS.md's note
|
|
5865
|
+
* that no provider ships a tested outpaint path yet.
|
|
5866
|
+
*/
|
|
5867
|
+
supportsRevision(mode2) {
|
|
5868
|
+
return mode2 === "inpaint" || mode2 === "image-to-image";
|
|
5869
|
+
}
|
|
5739
5870
|
/** PixelLab's own constraints: submissions must be >2s apart, and
|
|
5740
5871
|
* background jobs in flight are capped by subscription tier (Tier 1=8,
|
|
5741
5872
|
* Tier 2=10, Tier 3=20); 8 is the safe floor across every tier. */
|
|
@@ -5756,6 +5887,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5756
5887
|
return dir;
|
|
5757
5888
|
}
|
|
5758
5889
|
estimate(spec) {
|
|
5890
|
+
if (spec.revision) {
|
|
5891
|
+
const width = spec.revision.sourceWidth ?? spec.width;
|
|
5892
|
+
const height = spec.revision.sourceHeight ?? spec.height;
|
|
5893
|
+
return { unit: "generations", amount: generationCost(width, height, "1dir"), candidates: 1 };
|
|
5894
|
+
}
|
|
5759
5895
|
if (spec.generator === "tiles") {
|
|
5760
5896
|
return { unit: "generations", amount: spec.cost, candidates: spec.candidates };
|
|
5761
5897
|
}
|
|
@@ -5769,6 +5905,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5769
5905
|
};
|
|
5770
5906
|
}
|
|
5771
5907
|
validate(spec, styleImages) {
|
|
5908
|
+
if (spec.revision?.mode === "inpaint") {
|
|
5909
|
+
const { sourceWidth: width, sourceHeight: height } = spec.revision;
|
|
5910
|
+
if (width != null && height != null && (width < 32 || height < 32 || width > 512 || height > 512)) {
|
|
5911
|
+
throw new Error(`PixelLab inpaint source is ${width}x${height}; the API takes 32 to 512 pixels per side`);
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5772
5914
|
if (spec.generator === "1dir" && (spec.width < 32 || spec.width > 256)) {
|
|
5773
5915
|
throw new Error("PixelLab 1dir dimensions must be between 32 and 256 pixels");
|
|
5774
5916
|
}
|
|
@@ -6118,6 +6260,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6118
6260
|
}
|
|
6119
6261
|
async submit(spec, styleImages, context) {
|
|
6120
6262
|
this.validate(spec, styleImages);
|
|
6263
|
+
if (spec.revision) return this.submitRevision(spec);
|
|
6121
6264
|
if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
|
|
6122
6265
|
if (spec.generator === "pixflux") {
|
|
6123
6266
|
const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
@@ -6137,9 +6280,20 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6137
6280
|
const res2 = await this.client.createTilesPro({
|
|
6138
6281
|
description: spec.prompt,
|
|
6139
6282
|
tileSize: spec.tileSize,
|
|
6283
|
+
tileHeight: spec.tileHeight,
|
|
6140
6284
|
tileType: spec.tileType,
|
|
6141
6285
|
tileView: spec.tileView,
|
|
6286
|
+
tileViewAngle: spec.tileViewAngle,
|
|
6287
|
+
tileDepthRatio: spec.tileDepthRatio,
|
|
6288
|
+
tileFlatTopPx: spec.tileFlatTopPx,
|
|
6289
|
+
obliqueLean: spec.obliqueLean,
|
|
6142
6290
|
tileFeature: spec.tileFeature,
|
|
6291
|
+
buildingWallTiles: spec.buildingWallTiles,
|
|
6292
|
+
buildingLayout: spec.buildingLayout,
|
|
6293
|
+
buildingWallDescription: spec.buildingWallDescription,
|
|
6294
|
+
buildingFloorDescription: spec.buildingFloorDescription,
|
|
6295
|
+
buildingFloor2Description: spec.buildingFloor2Description,
|
|
6296
|
+
buildingWallAngle: spec.buildingWallAngle,
|
|
6143
6297
|
outlineMode: spec.outlineMode,
|
|
6144
6298
|
seed: spec.seed,
|
|
6145
6299
|
// TilesProStyleImage is flat and wants the reference's real dimensions,
|
|
@@ -6169,6 +6323,60 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6169
6323
|
});
|
|
6170
6324
|
return { jobId: res.object_id, metadata: { backgroundJobId: res.background_job_id } };
|
|
6171
6325
|
}
|
|
6326
|
+
/**
|
|
6327
|
+
* `inpaint` reads the mask as PixelLab's own convention: white marks the
|
|
6328
|
+
* area to generate, black the area to preserve (`InpaintV3Request`'s own
|
|
6329
|
+
* field description). Unlike ComfyUI's revision graph, there is no
|
|
6330
|
+
* configurable side to this — document it as fixed rather than leave an
|
|
6331
|
+
* agent to guess, the way docs/REVISIONS.md tells a ComfyUI author to test
|
|
6332
|
+
* their own graph.
|
|
6333
|
+
*/
|
|
6334
|
+
async submitRevision(spec) {
|
|
6335
|
+
const revision = spec.revision;
|
|
6336
|
+
if (!revision.sourceSha256 || !revision.sourceFormat || revision.sourceWidth == null || revision.sourceHeight == null) {
|
|
6337
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
|
|
6338
|
+
}
|
|
6339
|
+
const sourceBytes = readFileSync2(revision.sourceFile);
|
|
6340
|
+
if (sha256(sourceBytes) !== revision.sourceSha256) {
|
|
6341
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
|
|
6342
|
+
}
|
|
6343
|
+
const image = { base64: sourceBytes.toString("base64"), format: revision.sourceFormat };
|
|
6344
|
+
const width = revision.sourceWidth;
|
|
6345
|
+
const height = revision.sourceHeight;
|
|
6346
|
+
if (revision.mode === "inpaint") {
|
|
6347
|
+
if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
|
|
6348
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
|
|
6349
|
+
}
|
|
6350
|
+
const maskBytes = readFileSync2(revision.maskFile);
|
|
6351
|
+
if (sha256(maskBytes) !== revision.maskSha256) {
|
|
6352
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision mask changed after the manifest was resolved`);
|
|
6353
|
+
}
|
|
6354
|
+
const res2 = await this.client.inpaintV3({
|
|
6355
|
+
description: spec.prompt,
|
|
6356
|
+
image,
|
|
6357
|
+
width,
|
|
6358
|
+
height,
|
|
6359
|
+
maskImage: { base64: maskBytes.toString("base64"), format: revision.maskFormat },
|
|
6360
|
+
noBackground: spec.noBackground,
|
|
6361
|
+
seed: spec.seed
|
|
6362
|
+
});
|
|
6363
|
+
return { jobId: res2.background_job_id };
|
|
6364
|
+
}
|
|
6365
|
+
if (revision.strength != null) {
|
|
6366
|
+
throw new Error(
|
|
6367
|
+
`${spec.styleId}/${spec.assetId}: PixelLab image-to-image revisions take no strength; edit-images-v2 always applies the full instruction`
|
|
6368
|
+
);
|
|
6369
|
+
}
|
|
6370
|
+
const res = await this.client.editImagesV2({
|
|
6371
|
+
description: spec.prompt,
|
|
6372
|
+
image,
|
|
6373
|
+
width,
|
|
6374
|
+
height,
|
|
6375
|
+
noBackground: spec.noBackground,
|
|
6376
|
+
seed: spec.seed
|
|
6377
|
+
});
|
|
6378
|
+
return { jobId: res.background_job_id };
|
|
6379
|
+
}
|
|
6172
6380
|
/**
|
|
6173
6381
|
* What a background job actually billed, once its own record is still
|
|
6174
6382
|
* around to ask; see `billedFromUsage`. A stale or already-cleaned-up job
|
|
@@ -6185,6 +6393,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6185
6393
|
}
|
|
6186
6394
|
}
|
|
6187
6395
|
async poll(jobId, generator, context) {
|
|
6396
|
+
if (context?.spec?.revision) return this.pollRevision(jobId);
|
|
6188
6397
|
if (generator === "pixflux") {
|
|
6189
6398
|
const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
6190
6399
|
if (existsSync7(file)) {
|
|
@@ -6215,6 +6424,59 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6215
6424
|
etaSeconds: obj.eta_seconds ?? null
|
|
6216
6425
|
};
|
|
6217
6426
|
}
|
|
6427
|
+
/**
|
|
6428
|
+
* `/inpaint-v3` and `/edit-images-v2` both hand back a plain background
|
|
6429
|
+
* job with no resource of its own, polled generically at
|
|
6430
|
+
* `GET /background-jobs/{id}`. Confirmed live for both, and the two do not
|
|
6431
|
+
* match: a completed `inpaint-v3` job's `last_response.image` is a single
|
|
6432
|
+
* `{type: "base64", base64, width, height}`, matched by the `imageKeys`
|
|
6433
|
+
* branch below; a completed `edit-images-v2` job's is `last_response.images`,
|
|
6434
|
+
* an *array* of that same shape, matched by the `done.images` branch below
|
|
6435
|
+
* instead — exactly why both branches exist rather than just the first one
|
|
6436
|
+
* (docs/ENDPOINTS.md, docs/REVISIONS.md). Beyond these two confirmed
|
|
6437
|
+
* shapes, this also checks a nested `{image: {base64, format}}` and a
|
|
6438
|
+
* hosted URL under a handful of plausible keys, and fails loudly, naming
|
|
6439
|
+
* the keys it actually got, rather than guess wrong silently, for whatever
|
|
6440
|
+
* shape still isn't covered.
|
|
6441
|
+
*/
|
|
6442
|
+
async pollRevision(jobId) {
|
|
6443
|
+
const job = await this.client.getBackgroundJob(jobId);
|
|
6444
|
+
if (job.status === "failed") return { status: "failed", error: "revision job failed upstream" };
|
|
6445
|
+
if (job.status !== "completed") return { status: "processing" };
|
|
6446
|
+
const billed = billedFromUsage(job.usage);
|
|
6447
|
+
const done = job.last_response ?? {};
|
|
6448
|
+
const urlKeys = ["image_url", "download_url", "url", "output_url"];
|
|
6449
|
+
for (const key of urlKeys) {
|
|
6450
|
+
const url = done[key];
|
|
6451
|
+
if (typeof url === "string" && url) {
|
|
6452
|
+
return { status: "ready", objectId: jobId, sourceUrl: url, sources: [{ url }], billed };
|
|
6453
|
+
}
|
|
6454
|
+
}
|
|
6455
|
+
const imageKeys = ["image", "output_image", "result_image", "edited_image"];
|
|
6456
|
+
for (const key of imageKeys) {
|
|
6457
|
+
const candidate = done[key];
|
|
6458
|
+
const base64 = extractBase64(candidate);
|
|
6459
|
+
if (base64) {
|
|
6460
|
+
const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
6461
|
+
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
6462
|
+
const sourceUrl = `file://${file}`;
|
|
6463
|
+
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
6464
|
+
}
|
|
6465
|
+
}
|
|
6466
|
+
if (Array.isArray(done.images) && done.images.length) {
|
|
6467
|
+
const base64 = extractBase64(done.images[0]);
|
|
6468
|
+
if (base64) {
|
|
6469
|
+
const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
6470
|
+
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
6471
|
+
const sourceUrl = `file://${file}`;
|
|
6472
|
+
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6475
|
+
return {
|
|
6476
|
+
status: "failed",
|
|
6477
|
+
error: `Invalid PixelLab response for revision job ${jobId}: completed with no recognized image field (got: ${Object.keys(done).join(", ") || "no keys"}); update pollRevision in src/providers/pixellab.ts with the real shape`
|
|
6478
|
+
};
|
|
6479
|
+
}
|
|
6218
6480
|
/**
|
|
6219
6481
|
* Map objects need their own path because the `/map-objects/{id}` record is
|
|
6220
6482
|
* deleted upstream roughly 8 hours after creation while the image survives in
|
|
@@ -6406,6 +6668,15 @@ function firstUrl(urls) {
|
|
|
6406
6668
|
if (!urls) return null;
|
|
6407
6669
|
return Object.values(urls).find((u) => typeof u === "string") ?? null;
|
|
6408
6670
|
}
|
|
6671
|
+
function extractBase64(value) {
|
|
6672
|
+
if (typeof value === "string" && value) return value;
|
|
6673
|
+
if (value && typeof value === "object") {
|
|
6674
|
+
const obj = value;
|
|
6675
|
+
if (typeof obj.base64 === "string" && obj.base64) return obj.base64;
|
|
6676
|
+
if (obj.image) return extractBase64(obj.image);
|
|
6677
|
+
}
|
|
6678
|
+
return null;
|
|
6679
|
+
}
|
|
6409
6680
|
function readReference(spec, reference) {
|
|
6410
6681
|
const images = {};
|
|
6411
6682
|
for (const [direction, image] of Object.entries(reference)) {
|
|
@@ -7663,9 +7934,20 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7663
7934
|
enforcePalette: style.enforcePalette,
|
|
7664
7935
|
noBackground: style.noBackground,
|
|
7665
7936
|
tileSize: generator === "tiles" ? tileSize : void 0,
|
|
7937
|
+
tileHeight: generator === "tiles" ? style.tileHeight : void 0,
|
|
7666
7938
|
tileType: generator === "tiles" ? style.tileType : void 0,
|
|
7667
7939
|
tileView: generator === "tiles" ? style.tileView : void 0,
|
|
7940
|
+
tileViewAngle: generator === "tiles" ? style.tileViewAngle : void 0,
|
|
7941
|
+
tileDepthRatio: generator === "tiles" ? style.tileDepthRatio : void 0,
|
|
7942
|
+
tileFlatTopPx: generator === "tiles" ? style.tileFlatTopPx : void 0,
|
|
7943
|
+
obliqueLean: generator === "tiles" ? style.obliqueLean : void 0,
|
|
7668
7944
|
tileFeature: generator === "tiles" ? style.tileFeature : void 0,
|
|
7945
|
+
buildingWallTiles: generator === "tiles" ? style.buildingWallTiles : void 0,
|
|
7946
|
+
buildingLayout: generator === "tiles" ? style.buildingLayout : void 0,
|
|
7947
|
+
buildingWallDescription: generator === "tiles" ? style.buildingWallDescription : void 0,
|
|
7948
|
+
buildingFloorDescription: generator === "tiles" ? style.buildingFloorDescription : void 0,
|
|
7949
|
+
buildingFloor2Description: generator === "tiles" ? style.buildingFloor2Description : void 0,
|
|
7950
|
+
buildingWallAngle: generator === "tiles" ? style.buildingWallAngle : void 0,
|
|
7669
7951
|
outlineMode: generator === "tiles" ? style.outlineMode : void 0,
|
|
7670
7952
|
...generator === "character" ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
7671
7953
|
cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generationCost(width, height, generator),
|
|
@@ -10926,7 +11208,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
10926
11208
|
retiredAt: generation.retiredAt,
|
|
10927
11209
|
outputs,
|
|
10928
11210
|
cached: outputs.length > 0 && outputs.every((output) => output.url !== null),
|
|
10929
|
-
|
|
11211
|
+
// A revision's jobId is a plain background job id, not a PixelLab
|
|
11212
|
+
// account object: there is no `/create-object` page to point at.
|
|
11213
|
+
upstreamUrl: generation.provider === "pixellab" && !entry.revision ? pixelLabObjectUrl(generation.generator, generation.objectId) : null
|
|
10930
11214
|
};
|
|
10931
11215
|
});
|
|
10932
11216
|
}
|
|
@@ -11191,7 +11475,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
11191
11475
|
editChanged,
|
|
11192
11476
|
editStatus,
|
|
11193
11477
|
editMeta,
|
|
11194
|
-
upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
11478
|
+
upstreamUrl: entry?.provider === "pixellab" && !spec.revision ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
11195
11479
|
refreshable: Boolean(entry && entry.status === "downloaded" && entry.outputs.length && (entry.sourceUrls?.length || entry.sourceUrl)),
|
|
11196
11480
|
history: entry ? describeHistory(media, entry, cacheDir) : [],
|
|
11197
11481
|
tags: spec.tags,
|
|
@@ -11255,7 +11539,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
11255
11539
|
editChanged: [],
|
|
11256
11540
|
editStatus: null,
|
|
11257
11541
|
editMeta: null,
|
|
11258
|
-
upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
11542
|
+
upstreamUrl: entry.provider === "pixellab" && !entry.revision ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
11259
11543
|
refreshable: false,
|
|
11260
11544
|
history: describeHistory(media, entry, cacheDir),
|
|
11261
11545
|
tags: [],
|