pixelkiln 0.57.0 → 0.59.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 +463 -28
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +465 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +653 -74
- package/dist/index.d.ts +653 -74
- package/dist/index.js +464 -28
- package/dist/index.js.map +1 -1
- package/docs/ENDPOINTS.md +51 -14
- package/docs/GENERATORS.md +58 -0
- package/docs/MANIFEST.md +8 -2
- package/docs/REVISIONS.md +103 -27
- package/package.json +1 -1
- package/schema/manifest.schema.json +51 -1
- package/skills/pixelkiln/references/pixellab-roadmap.md +53 -28
- package/skills/pixelkiln/references/pixellab.md +39 -9
- package/skills/pixelkiln/references/revisions.md +20 -5
package/dist/cli.js
CHANGED
|
@@ -703,6 +703,11 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
703
703
|
// A mirror's bytes come from its source's recorded outputs; the plan
|
|
704
704
|
// compares those directly, so only the choice of source is identity.
|
|
705
705
|
mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
|
|
706
|
+
// The whole group's ordered descriptions, not just this asset's own —
|
|
707
|
+
// any sibling's prompt changing, or a member being added or removed,
|
|
708
|
+
// must mark every member stale together, since they all ride on one
|
|
709
|
+
// submitted job with no way to amend it after the fact.
|
|
710
|
+
batch: spec.batch ? { role: spec.batch.role, itemDescriptions: spec.batch.itemDescriptions, index: spec.batch.index } : void 0,
|
|
706
711
|
revision: spec.revision ? {
|
|
707
712
|
mode: spec.revision.mode,
|
|
708
713
|
from: spec.revision.sourceAssetId,
|
|
@@ -712,7 +717,12 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
712
717
|
numColors: spec.revision.numColors,
|
|
713
718
|
paletteImageSha256: spec.revision.paletteImageSha256,
|
|
714
719
|
dithering: spec.revision.dithering,
|
|
715
|
-
ditheringStrength: spec.revision.ditheringStrength
|
|
720
|
+
ditheringStrength: spec.revision.ditheringStrength,
|
|
721
|
+
frames: spec.revision.frames,
|
|
722
|
+
fps: spec.revision.fps,
|
|
723
|
+
lastFrameSha256: spec.revision.lastFrameSha256,
|
|
724
|
+
direction: spec.revision.direction,
|
|
725
|
+
enhancePrompt: spec.revision.enhancePrompt
|
|
716
726
|
} : void 0
|
|
717
727
|
})
|
|
718
728
|
);
|
|
@@ -732,7 +742,15 @@ import { z } from "zod";
|
|
|
732
742
|
var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
|
|
733
743
|
var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro"]);
|
|
734
744
|
var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
|
|
735
|
-
var RevisionModeSchema = z.enum([
|
|
745
|
+
var RevisionModeSchema = z.enum([
|
|
746
|
+
"image-to-image",
|
|
747
|
+
"inpaint",
|
|
748
|
+
"outpaint",
|
|
749
|
+
"reduce-colors",
|
|
750
|
+
"correct-pixelart",
|
|
751
|
+
"animate",
|
|
752
|
+
"animate-pixminimax"
|
|
753
|
+
]);
|
|
736
754
|
function tileVariationCount(descriptions) {
|
|
737
755
|
return Math.max(1, descriptions) * 4;
|
|
738
756
|
}
|
|
@@ -844,6 +862,12 @@ var CharacterProportionsSchema = z.union([
|
|
|
844
862
|
}).strict()
|
|
845
863
|
]);
|
|
846
864
|
var CharacterAnimationModeSchema = z.enum(["template", "v3", "pro"]);
|
|
865
|
+
var AssetBatchSchema = z.object({
|
|
866
|
+
/** The batch leader: another `1dir` asset in the same style. */
|
|
867
|
+
of: z.string().min(1),
|
|
868
|
+
/** 1-based slot in the batch (the leader is implicitly slot 0). Unique and contiguous among siblings. */
|
|
869
|
+
index: z.number().int().min(1)
|
|
870
|
+
}).strict();
|
|
847
871
|
var CharacterStateSchema = z.object({
|
|
848
872
|
/** The base character, or another state, in the same style. */
|
|
849
873
|
of: z.string().min(1),
|
|
@@ -943,7 +967,21 @@ var RevisionSchema = z.object({
|
|
|
943
967
|
/** `reduce-colors` only: ordered dithering matrix size. */
|
|
944
968
|
dithering: RevisionDitheringSchema.optional(),
|
|
945
969
|
/** `reduce-colors` only: dithering intensity; ignored when `dithering` is "none" or unset. */
|
|
946
|
-
ditheringStrength: z.number().min(0).max(10).optional()
|
|
970
|
+
ditheringStrength: z.number().min(0).max(10).optional(),
|
|
971
|
+
/**
|
|
972
|
+
* `animate`/`animate-pixminimax` only: frames to generate, even, 4 to 40
|
|
973
|
+
* (the provider-specific ceiling — 16 for `animate` — is enforced at
|
|
974
|
+
* the provider layer, since it differs by mode).
|
|
975
|
+
*/
|
|
976
|
+
frames: z.number().int().min(4).max(40).optional(),
|
|
977
|
+
/** `animate`/`animate-pixminimax` only: playback rate recorded with the frames; PixelLab does not store one. */
|
|
978
|
+
fps: z.number().int().min(1).max(60).optional(),
|
|
979
|
+
/** `animate`/`animate-pixminimax` only: manifest-relative image pinning where the motion ends (interpolation instead of open-ended animation). */
|
|
980
|
+
lastFrame: z.string().min(1).optional(),
|
|
981
|
+
/** `animate-pixminimax` only: facing direction, used only alongside `enhancePrompt` to hold the sprite's facing. */
|
|
982
|
+
direction: CharacterDirectionSchema.optional(),
|
|
983
|
+
/** `animate`/`animate-pixminimax` only: let PixelLab expand the action into a fuller motion description first. */
|
|
984
|
+
enhancePrompt: z.boolean().optional()
|
|
947
985
|
}).strict().superRefine((revision, context) => {
|
|
948
986
|
if (revision.mode === "inpaint" && !revision.mask) {
|
|
949
987
|
context.addIssue({
|
|
@@ -982,6 +1020,32 @@ var RevisionSchema = z.object({
|
|
|
982
1020
|
path: ["strength"]
|
|
983
1021
|
});
|
|
984
1022
|
}
|
|
1023
|
+
if (revision.strength !== void 0 && (revision.mode === "animate" || revision.mode === "animate-pixminimax")) {
|
|
1024
|
+
context.addIssue({
|
|
1025
|
+
code: z.ZodIssueCode.custom,
|
|
1026
|
+
message: `${revision.mode} revisions do not take a strength; use enhancePrompt`,
|
|
1027
|
+
path: ["strength"]
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
for (const field of ["frames", "fps", "lastFrame", "enhancePrompt"]) {
|
|
1031
|
+
if (revision[field] !== void 0 && revision.mode !== "animate" && revision.mode !== "animate-pixminimax") {
|
|
1032
|
+
context.addIssue({
|
|
1033
|
+
code: z.ZodIssueCode.custom,
|
|
1034
|
+
message: `${field} applies to animate/animate-pixminimax revisions only`,
|
|
1035
|
+
path: [field]
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (revision.direction !== void 0 && revision.mode !== "animate-pixminimax") {
|
|
1040
|
+
context.addIssue({
|
|
1041
|
+
code: z.ZodIssueCode.custom,
|
|
1042
|
+
message: "direction applies to animate-pixminimax revisions only",
|
|
1043
|
+
path: ["direction"]
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
if (revision.frames !== void 0 && revision.frames % 2 !== 0) {
|
|
1047
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
|
|
1048
|
+
}
|
|
985
1049
|
});
|
|
986
1050
|
var StyleObjectSchema = z.object({
|
|
987
1051
|
/** Generation backend for this style. Omit to inherit the manifest default. */
|
|
@@ -1377,6 +1441,12 @@ var AssetSchema = z.object({
|
|
|
1377
1441
|
* `prompt` instead.
|
|
1378
1442
|
*/
|
|
1379
1443
|
animation: CharacterAnimationSchema.optional(),
|
|
1444
|
+
/**
|
|
1445
|
+
* `1dir` styles only: this asset rides along on another `1dir` asset's
|
|
1446
|
+
* batch submission instead of generating on its own. See
|
|
1447
|
+
* `AssetBatchSchema`.
|
|
1448
|
+
*/
|
|
1449
|
+
batch: AssetBatchSchema.optional(),
|
|
1380
1450
|
/** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
|
|
1381
1451
|
proportions: CharacterProportionsSchema.optional(),
|
|
1382
1452
|
/**
|
|
@@ -1453,7 +1523,7 @@ var AssetSchema = z.object({
|
|
|
1453
1523
|
path: ["revision"]
|
|
1454
1524
|
});
|
|
1455
1525
|
}
|
|
1456
|
-
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror"].filter(Boolean);
|
|
1526
|
+
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch"].filter(Boolean);
|
|
1457
1527
|
if (shapes.length > 1) {
|
|
1458
1528
|
context.addIssue({
|
|
1459
1529
|
code: z.ZodIssueCode.custom,
|
|
@@ -1592,13 +1662,24 @@ var LockEntrySchema = z.object({
|
|
|
1592
1662
|
numColors: z.number().int().min(2).max(256).optional(),
|
|
1593
1663
|
paletteImageSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
|
|
1594
1664
|
dithering: RevisionDitheringSchema.optional(),
|
|
1595
|
-
ditheringStrength: z.number().min(0).max(10).optional()
|
|
1665
|
+
ditheringStrength: z.number().min(0).max(10).optional(),
|
|
1666
|
+
frames: z.number().int().min(4).max(40).optional(),
|
|
1667
|
+
fps: z.number().int().min(1).max(60).optional(),
|
|
1668
|
+
lastFrameSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
|
|
1669
|
+
direction: CharacterDirectionSchema.optional(),
|
|
1670
|
+
enhancePrompt: z.boolean().optional()
|
|
1596
1671
|
}).strict().nullable().default(null),
|
|
1597
1672
|
/** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
|
|
1598
1673
|
mirror: z.object({
|
|
1599
1674
|
sourceAssetId: z.string().min(1),
|
|
1600
1675
|
sourceSha256: z.string().regex(/^[0-9a-f]{64}$/)
|
|
1601
1676
|
}).strict().nullable().default(null),
|
|
1677
|
+
/** For a `1dir` batch member: which leader and slot it rode along on. */
|
|
1678
|
+
batch: z.object({
|
|
1679
|
+
role: z.enum(["leader", "member"]),
|
|
1680
|
+
leaderAssetId: z.string().min(1).optional(),
|
|
1681
|
+
index: z.number().int().min(1).optional()
|
|
1682
|
+
}).strict().nullable().default(null),
|
|
1602
1683
|
/**
|
|
1603
1684
|
* Output hashes owned by the previous generation while its replacement is
|
|
1604
1685
|
* pending. They authorize replacing only unchanged PixelKiln-owned files.
|
|
@@ -4667,8 +4748,16 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
4667
4748
|
supports(generator) {
|
|
4668
4749
|
return generator === "map" || generator === "frames";
|
|
4669
4750
|
}
|
|
4670
|
-
|
|
4671
|
-
|
|
4751
|
+
/**
|
|
4752
|
+
* Everything else resolves generically to whatever the user's own
|
|
4753
|
+
* workflow does with `bindings.sourceImage`. `animate`/`animate-pixminimax`
|
|
4754
|
+
* are the one exception: they produce an ordered frame set, and this
|
|
4755
|
+
* adapter's revision path always writes a single output image (see
|
|
4756
|
+
* `submit`/`fetch` below) — a real structural gap, not a missing binding,
|
|
4757
|
+
* so it is rejected here rather than only failing once `validate` runs.
|
|
4758
|
+
*/
|
|
4759
|
+
supportsRevision(mode2) {
|
|
4760
|
+
return mode2 !== "animate" && mode2 !== "animate-pixminimax";
|
|
4672
4761
|
}
|
|
4673
4762
|
estimate(spec) {
|
|
4674
4763
|
return {
|
|
@@ -6153,14 +6242,15 @@ var PixelLabClient = class {
|
|
|
6153
6242
|
/**
|
|
6154
6243
|
* `/reduce-colors`, PixelLab's "Cleanup" tier: quantize one image onto a
|
|
6155
6244
|
* smaller palette, synchronously — no `background_job_id`, the result
|
|
6156
|
-
* comes back in this same response, like `createImagePixflux`.
|
|
6157
|
-
*
|
|
6158
|
-
*
|
|
6159
|
-
*
|
|
6160
|
-
*
|
|
6161
|
-
*
|
|
6162
|
-
*
|
|
6163
|
-
*
|
|
6245
|
+
* comes back in this same response, like `createImagePixflux`. The
|
|
6246
|
+
* schema's own response example is dollar-denominated (`usage: {type:
|
|
6247
|
+
* "usd", usd: 0.02}`), which — matching this codebase's repeated
|
|
6248
|
+
* experience with PixelLab's documented-vs-billed cost mismatches
|
|
6249
|
+
* (`isometricTile`, `objectPro`) — did not hold: confirmed live against a
|
|
6250
|
+
* Tier 2 account at exactly 0.1 generations for a 32x32 source
|
|
6251
|
+
* (docs/REVISIONS.md). `numColors` and `paletteImage` are mutually
|
|
6252
|
+
* exclusive upstream; the manifest schema already enforces that before
|
|
6253
|
+
* this is ever called.
|
|
6164
6254
|
*/
|
|
6165
6255
|
async reduceColors(args) {
|
|
6166
6256
|
const body = { images: [args.image] };
|
|
@@ -6183,8 +6273,9 @@ var PixelLabClient = class {
|
|
|
6183
6273
|
/**
|
|
6184
6274
|
* `/correct-pixelart`, PixelLab's "Cleanup" tier: sharpen edges and drop
|
|
6185
6275
|
* stray pixels without resizing, synchronously — same shape as
|
|
6186
|
-
* `reduceColors` above, no background job. Cost is likewise
|
|
6187
|
-
*
|
|
6276
|
+
* `reduceColors` above, no background job. Cost is likewise confirmed
|
|
6277
|
+
* live at 0.1 generations for a 32x32 source, not the schema's own
|
|
6278
|
+
* dollar-denominated example (`usage: {type: "usd", usd: 0.02}`).
|
|
6188
6279
|
*/
|
|
6189
6280
|
async correctPixelart(args) {
|
|
6190
6281
|
const body = { images: [args.image] };
|
|
@@ -6196,6 +6287,50 @@ var PixelLabClient = class {
|
|
|
6196
6287
|
);
|
|
6197
6288
|
return { png: Buffer.from(res.images[0].base64, "base64"), usage: res.usage };
|
|
6198
6289
|
}
|
|
6290
|
+
/**
|
|
6291
|
+
* `/animate-with-text-v3`: animate a loose image from a text description,
|
|
6292
|
+
* no PixelLab character/object resource required — unlike `animateCharacter`
|
|
6293
|
+
* / `animateObject`, `firstFrame` is whatever bytes the caller has on hand.
|
|
6294
|
+
* A plain background job, like every other PixelLab async submission;
|
|
6295
|
+
* unlike `inpaintV3`/`editImagesV2`, its completed shape has not been
|
|
6296
|
+
* exercised against a live account (request/response fields here come from
|
|
6297
|
+
* the live OpenAPI document, not an observed call — `pollAnimateRevision`
|
|
6298
|
+
* in pixellab.ts checks several plausible field names for the frame list
|
|
6299
|
+
* defensively, the same as `pollRevision` already does for image edits).
|
|
6300
|
+
*/
|
|
6301
|
+
async animateWithTextV3(args) {
|
|
6302
|
+
const body = { first_frame: args.firstFrame, action: args.action };
|
|
6303
|
+
if (args.lastFrame) body.last_frame = args.lastFrame;
|
|
6304
|
+
if (args.frameCount != null) body.frame_count = args.frameCount;
|
|
6305
|
+
if (args.seed != null) body.seed = args.seed;
|
|
6306
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
6307
|
+
if (args.enhancePrompt != null) body.enhance_prompt = args.enhancePrompt;
|
|
6308
|
+
return validateResponse(
|
|
6309
|
+
RevisionJobSubmitSchema,
|
|
6310
|
+
await this.request("/animate-with-text-v3", { method: "POST", body: JSON.stringify(body) }),
|
|
6311
|
+
"animate-with-text-v3"
|
|
6312
|
+
);
|
|
6313
|
+
}
|
|
6314
|
+
/**
|
|
6315
|
+
* `/animate-pixminimax`, beta (tier 1 subscription or higher): PixMiniMax's
|
|
6316
|
+
* richer take on the same idea — `direction` and `enhancePrompt` steer
|
|
6317
|
+
* facing for aimed motions. Same unverified-completed-shape caveat as
|
|
6318
|
+
* `animateWithTextV3` above.
|
|
6319
|
+
*/
|
|
6320
|
+
async animatePixminimax(args) {
|
|
6321
|
+
const body = { first_frame: args.firstFrame, description: args.description };
|
|
6322
|
+
if (args.lastFrame) body.last_frame = args.lastFrame;
|
|
6323
|
+
if (args.frameCount != null) body.frame_count = args.frameCount;
|
|
6324
|
+
if (args.seed != null) body.seed = args.seed;
|
|
6325
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
6326
|
+
if (args.enhancePrompt != null) body.enhance_prompt = args.enhancePrompt;
|
|
6327
|
+
if (args.direction) body.direction = args.direction;
|
|
6328
|
+
return validateResponse(
|
|
6329
|
+
RevisionJobSubmitSchema,
|
|
6330
|
+
await this.request("/animate-pixminimax", { method: "POST", body: JSON.stringify(body) }),
|
|
6331
|
+
"animate-pixminimax"
|
|
6332
|
+
);
|
|
6333
|
+
}
|
|
6199
6334
|
async getBackgroundJob(jobId) {
|
|
6200
6335
|
const raw = await this.request(`/background-jobs/${encodeURIComponent(jobId)}`);
|
|
6201
6336
|
return validateResponse(BackgroundJobSchema, raw, "background-jobs/{id}");
|
|
@@ -6378,12 +6513,15 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6378
6513
|
* no mask) both exist on PixelLab, and so do `reduce-colors`
|
|
6379
6514
|
* (`/reduce-colors`) and `correct-pixelart` (`/correct-pixelart`) — PixelLab's
|
|
6380
6515
|
* "Cleanup" tier, a mechanical post-process on the source's own pixels
|
|
6381
|
-
* rather than a described edit
|
|
6382
|
-
*
|
|
6383
|
-
*
|
|
6516
|
+
* rather than a described edit — and `animate`/`animate-pixminimax`
|
|
6517
|
+
* (`/animate-with-text-v3`, `/animate-pixminimax`), which animate any loose
|
|
6518
|
+
* image from a text description with no character/object resource
|
|
6519
|
+
* required. `outpaint` does not: there is no canvas-expansion endpoint in
|
|
6520
|
+
* the API, matching docs/REVISIONS.md's note that no provider ships a
|
|
6521
|
+
* tested outpaint path yet.
|
|
6384
6522
|
*/
|
|
6385
6523
|
supportsRevision(mode2) {
|
|
6386
|
-
return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart";
|
|
6524
|
+
return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "animate" || mode2 === "animate-pixminimax";
|
|
6387
6525
|
}
|
|
6388
6526
|
/** PixelLab's own constraints: submissions must be >2s apart, and
|
|
6389
6527
|
* background jobs in flight are capped by subscription tier (Tier 1=8,
|
|
@@ -6405,9 +6543,19 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6405
6543
|
return dir;
|
|
6406
6544
|
}
|
|
6407
6545
|
estimate(spec) {
|
|
6546
|
+
if (spec.batch?.role === "member") {
|
|
6547
|
+
return { unit: "generations", amount: 0, candidates: 1 };
|
|
6548
|
+
}
|
|
6408
6549
|
if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
|
|
6409
6550
|
return { unit: "generations", amount: 0.1, candidates: 1 };
|
|
6410
6551
|
}
|
|
6552
|
+
if (spec.revision?.mode === "animate" || spec.revision?.mode === "animate-pixminimax") {
|
|
6553
|
+
const width = spec.revision.sourceWidth ?? spec.width;
|
|
6554
|
+
const height = spec.revision.sourceHeight ?? spec.height;
|
|
6555
|
+
const frames = spec.revision.frames ?? 8;
|
|
6556
|
+
const base = Math.max(1, Math.ceil(width * height * frames / 65536));
|
|
6557
|
+
return { unit: "generations", amount: spec.revision.enhancePrompt ? base + 0.05 : base, candidates: 1 };
|
|
6558
|
+
}
|
|
6411
6559
|
if (spec.revision) {
|
|
6412
6560
|
const width = spec.revision.sourceWidth ?? spec.width;
|
|
6413
6561
|
const height = spec.revision.sourceHeight ?? spec.height;
|
|
@@ -6449,6 +6597,20 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6449
6597
|
throw new Error(`PixelLab correct-pixelart source is ${width}x${height}; the API takes at most 1024 pixels per side`);
|
|
6450
6598
|
}
|
|
6451
6599
|
}
|
|
6600
|
+
if (spec.revision?.mode === "animate" || spec.revision?.mode === "animate-pixminimax") {
|
|
6601
|
+
const { sourceWidth: width, sourceHeight: height, mode: mode2, frames, lastFrameWidth, lastFrameHeight } = spec.revision;
|
|
6602
|
+
if (width != null && height != null && (width > 256 || height > 256)) {
|
|
6603
|
+
throw new Error(`PixelLab ${mode2} source is ${width}x${height}; the API takes at most 256 pixels per side`);
|
|
6604
|
+
}
|
|
6605
|
+
if (mode2 === "animate" && frames != null && frames > 16) {
|
|
6606
|
+
throw new Error(`PixelLab animate takes 4 to 16 frames; ${frames} is too many (use animate-pixminimax for up to 40)`);
|
|
6607
|
+
}
|
|
6608
|
+
if (lastFrameWidth != null && lastFrameHeight != null && width != null && height != null && (lastFrameWidth !== width || lastFrameHeight !== height)) {
|
|
6609
|
+
throw new Error(
|
|
6610
|
+
`PixelLab ${mode2} lastFrame is ${lastFrameWidth}x${lastFrameHeight}; source is ${width}x${height} \u2014 they must match`
|
|
6611
|
+
);
|
|
6612
|
+
}
|
|
6613
|
+
}
|
|
6452
6614
|
if (spec.generator === "1dir" && (spec.width < 32 || spec.width > 256)) {
|
|
6453
6615
|
throw new Error("PixelLab 1dir dimensions must be between 32 and 256 pixels");
|
|
6454
6616
|
}
|
|
@@ -7182,11 +7344,21 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7182
7344
|
return { jobId: res2.background_job_id };
|
|
7183
7345
|
}
|
|
7184
7346
|
if (spec.generator === "1dir") {
|
|
7347
|
+
if (spec.batch?.role === "member") {
|
|
7348
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: a batch member cannot submit on its own`);
|
|
7349
|
+
}
|
|
7185
7350
|
const res2 = await this.client.create1Direction({
|
|
7186
7351
|
description: spec.prompt,
|
|
7187
7352
|
size: spec.size,
|
|
7188
7353
|
view: spec.view === "sidescroller" ? "sidescroller" : "top-down",
|
|
7189
|
-
styleImages
|
|
7354
|
+
styleImages,
|
|
7355
|
+
// The leader's own subject is item_descriptions[0], so `description`
|
|
7356
|
+
// matches it exactly. Confirmed live: item_descriptions[0] owns
|
|
7357
|
+
// candidate slot 0 (a 3-item batch returned exactly the declared
|
|
7358
|
+
// items at frames 0/1/2, in order; docs/GENERATORS.md#1dir), so
|
|
7359
|
+
// sending the same text as `description` too is redundant but
|
|
7360
|
+
// harmless, not a hedge against ambiguity.
|
|
7361
|
+
itemDescriptions: spec.batch?.role === "leader" ? spec.batch.itemDescriptions : void 0
|
|
7190
7362
|
});
|
|
7191
7363
|
return { jobId: res2.object_id, metadata: { backgroundJobId: res2.background_job_id } };
|
|
7192
7364
|
}
|
|
@@ -7270,6 +7442,42 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7270
7442
|
writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), res2.png);
|
|
7271
7443
|
return { jobId, metadata: { revisionUsage: res2.usage } };
|
|
7272
7444
|
}
|
|
7445
|
+
if (revision.mode === "animate" || revision.mode === "animate-pixminimax") {
|
|
7446
|
+
let lastFrame;
|
|
7447
|
+
if (revision.lastFrameFile) {
|
|
7448
|
+
if (!revision.lastFrameSha256 || !revision.lastFrameFormat) {
|
|
7449
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame is not ready`);
|
|
7450
|
+
}
|
|
7451
|
+
const lastFrameBytes = readFileSync2(revision.lastFrameFile);
|
|
7452
|
+
if (sha256(lastFrameBytes) !== revision.lastFrameSha256) {
|
|
7453
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame changed after the manifest was resolved`);
|
|
7454
|
+
}
|
|
7455
|
+
lastFrame = { base64: lastFrameBytes.toString("base64"), format: revision.lastFrameFormat };
|
|
7456
|
+
}
|
|
7457
|
+
if (revision.mode === "animate") {
|
|
7458
|
+
const res3 = await this.client.animateWithTextV3({
|
|
7459
|
+
firstFrame: image,
|
|
7460
|
+
lastFrame,
|
|
7461
|
+
action: spec.prompt,
|
|
7462
|
+
frameCount: revision.frames,
|
|
7463
|
+
seed: spec.seed,
|
|
7464
|
+
noBackground: spec.noBackground,
|
|
7465
|
+
enhancePrompt: revision.enhancePrompt
|
|
7466
|
+
});
|
|
7467
|
+
return { jobId: res3.background_job_id };
|
|
7468
|
+
}
|
|
7469
|
+
const res2 = await this.client.animatePixminimax({
|
|
7470
|
+
firstFrame: image,
|
|
7471
|
+
lastFrame,
|
|
7472
|
+
description: spec.prompt,
|
|
7473
|
+
frameCount: revision.frames,
|
|
7474
|
+
seed: spec.seed,
|
|
7475
|
+
noBackground: spec.noBackground,
|
|
7476
|
+
enhancePrompt: revision.enhancePrompt,
|
|
7477
|
+
direction: revision.direction
|
|
7478
|
+
});
|
|
7479
|
+
return { jobId: res2.background_job_id };
|
|
7480
|
+
}
|
|
7273
7481
|
if (revision.strength != null) {
|
|
7274
7482
|
throw new Error(
|
|
7275
7483
|
`${spec.styleId}/${spec.assetId}: PixelLab image-to-image revisions take no strength; edit-images-v2 always applies the full instruction`
|
|
@@ -7305,6 +7513,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7305
7513
|
if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart") {
|
|
7306
7514
|
return this.pollCachedRevision(jobId, context);
|
|
7307
7515
|
}
|
|
7516
|
+
if (revisionMode === "animate" || revisionMode === "animate-pixminimax") {
|
|
7517
|
+
return this.pollAnimateRevision(jobId, context);
|
|
7518
|
+
}
|
|
7308
7519
|
if (context?.spec?.revision) return this.pollRevision(jobId);
|
|
7309
7520
|
if (generator === "pixflux") {
|
|
7310
7521
|
const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
@@ -7361,6 +7572,71 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7361
7572
|
const usage = context?.metadata?.revisionUsage;
|
|
7362
7573
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed: billedFromUsage(usage) };
|
|
7363
7574
|
}
|
|
7575
|
+
/**
|
|
7576
|
+
* `animate`/`animate-pixminimax` revisions: a plain background job, like
|
|
7577
|
+
* `pollRevision` above, but completing with an ORDERED FRAME LIST instead
|
|
7578
|
+
* of a single image — the same `review-set` shape `pollCharacterAnimation`
|
|
7579
|
+
* returns, minus any character/object resource to look the result up on
|
|
7580
|
+
* (there is none; this operates on a loose image). Unlike every other
|
|
7581
|
+
* PixelLab call this adapter makes, the completed shape for
|
|
7582
|
+
* `/animate-with-text-v3`/`/animate-pixminimax` has never been exercised
|
|
7583
|
+
* against a live account — the checks below are an informed guess (hosted
|
|
7584
|
+
* URLs under a plausible key name, the same `storage_urls.frames` shape
|
|
7585
|
+
* character animations use, or inline base64 per frame, decoded to a local
|
|
7586
|
+
* cache file the same way `pollRevision`'s single-image case already is)
|
|
7587
|
+
* rather than an observed shape, and fail loudly, naming the keys actually
|
|
7588
|
+
* received, for whatever the real shape turns out to be.
|
|
7589
|
+
*/
|
|
7590
|
+
async pollAnimateRevision(jobId, context) {
|
|
7591
|
+
const job = await this.client.getBackgroundJob(jobId);
|
|
7592
|
+
if (job.status === "failed") return { status: "failed", error: "animation job failed upstream" };
|
|
7593
|
+
if (job.status !== "completed") return { status: "processing" };
|
|
7594
|
+
const billed = billedFromUsage(job.usage);
|
|
7595
|
+
const done = job.last_response ?? {};
|
|
7596
|
+
const fps = context?.spec?.revision?.fps ?? 8;
|
|
7597
|
+
const review = (frameUrls) => ({
|
|
7598
|
+
status: "review-set",
|
|
7599
|
+
objectId: jobId,
|
|
7600
|
+
frameUrls,
|
|
7601
|
+
sources: frameUrls.map((url, index) => ({ url, role: `frame-${String(index).padStart(2, "0")}` })),
|
|
7602
|
+
fps,
|
|
7603
|
+
metadata: { frameSet: { fps, count: frameUrls.length } },
|
|
7604
|
+
billed
|
|
7605
|
+
});
|
|
7606
|
+
for (const key of ["frame_urls", "frames", "images"]) {
|
|
7607
|
+
const list = done[key];
|
|
7608
|
+
if (!Array.isArray(list) || !list.length) continue;
|
|
7609
|
+
const urls = [];
|
|
7610
|
+
for (const [index, item] of list.entries()) {
|
|
7611
|
+
if (typeof item === "string" && item) {
|
|
7612
|
+
urls.push(item);
|
|
7613
|
+
continue;
|
|
7614
|
+
}
|
|
7615
|
+
const url = item?.url;
|
|
7616
|
+
if (typeof url === "string" && url) {
|
|
7617
|
+
urls.push(url);
|
|
7618
|
+
continue;
|
|
7619
|
+
}
|
|
7620
|
+
const base64 = extractBase64(item);
|
|
7621
|
+
if (base64) {
|
|
7622
|
+
const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
7623
|
+
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
7624
|
+
urls.push(`file://${file}`);
|
|
7625
|
+
continue;
|
|
7626
|
+
}
|
|
7627
|
+
break;
|
|
7628
|
+
}
|
|
7629
|
+
if (urls.length === list.length) return review(urls);
|
|
7630
|
+
}
|
|
7631
|
+
const storageFrames = done.storage_urls?.frames;
|
|
7632
|
+
if (Array.isArray(storageFrames) && storageFrames.length && storageFrames.every((f) => typeof f === "string")) {
|
|
7633
|
+
return review(storageFrames);
|
|
7634
|
+
}
|
|
7635
|
+
return {
|
|
7636
|
+
status: "failed",
|
|
7637
|
+
error: `Invalid PixelLab response for animate job ${jobId}: completed with no recognized frame list (got: ${Object.keys(done).join(", ") || "no keys"}); update pollAnimateRevision in src/providers/pixellab.ts with the real shape`
|
|
7638
|
+
};
|
|
7639
|
+
}
|
|
7364
7640
|
/**
|
|
7365
7641
|
* `/inpaint-v3` and `/edit-images-v2` both hand back a plain background
|
|
7366
7642
|
* job with no resource of its own, polled generically at
|
|
@@ -9101,6 +9377,14 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9101
9377
|
};
|
|
9102
9378
|
styleSpecs.set(assetId, resolved);
|
|
9103
9379
|
}
|
|
9380
|
+
const batchMembersByLeader = /* @__PURE__ */ new Map();
|
|
9381
|
+
for (const id of styleSpecs.keys()) {
|
|
9382
|
+
const batch = manifest.assets[id].batch;
|
|
9383
|
+
if (!batch) continue;
|
|
9384
|
+
const members = batchMembersByLeader.get(batch.of) ?? [];
|
|
9385
|
+
members.push({ id, index: batch.index });
|
|
9386
|
+
batchMembersByLeader.set(batch.of, members);
|
|
9387
|
+
}
|
|
9104
9388
|
const finalized = /* @__PURE__ */ new Set();
|
|
9105
9389
|
const finalize = async (assetId) => {
|
|
9106
9390
|
const resolved = styleSpecs.get(assetId);
|
|
@@ -9217,6 +9501,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9217
9501
|
}
|
|
9218
9502
|
const paletteImageFile = asset.revision.paletteImage ? path13.resolve(root, asset.revision.paletteImage) : void 0;
|
|
9219
9503
|
const paletteImage = paletteImageFile ? await optionalRevisionImage(paletteImageFile, "revision palette image", "png") : null;
|
|
9504
|
+
const lastFrameFile = asset.revision.lastFrame ? path13.resolve(root, asset.revision.lastFrame) : void 0;
|
|
9505
|
+
const lastFrame = lastFrameFile ? await optionalRevisionImage(lastFrameFile, "revision last frame") : null;
|
|
9220
9506
|
resolved.revision = {
|
|
9221
9507
|
mode: asset.revision.mode,
|
|
9222
9508
|
sourceAssetId: asset.revision.from,
|
|
@@ -9243,9 +9529,70 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9243
9529
|
paletteImageFormat: paletteImage?.format ?? null
|
|
9244
9530
|
} : {},
|
|
9245
9531
|
...asset.revision.dithering ? { dithering: asset.revision.dithering } : {},
|
|
9246
|
-
...asset.revision.ditheringStrength == null ? {} : { ditheringStrength: asset.revision.ditheringStrength }
|
|
9532
|
+
...asset.revision.ditheringStrength == null ? {} : { ditheringStrength: asset.revision.ditheringStrength },
|
|
9533
|
+
...asset.revision.frames == null ? {} : { frames: asset.revision.frames },
|
|
9534
|
+
...asset.revision.fps == null ? {} : { fps: asset.revision.fps },
|
|
9535
|
+
...lastFrameFile ? {
|
|
9536
|
+
lastFrameFile,
|
|
9537
|
+
lastFrameSha256: lastFrame?.hash ?? null,
|
|
9538
|
+
lastFrameWidth: lastFrame?.width ?? null,
|
|
9539
|
+
lastFrameHeight: lastFrame?.height ?? null,
|
|
9540
|
+
lastFrameFormat: lastFrame?.format ?? null
|
|
9541
|
+
} : {},
|
|
9542
|
+
...asset.revision.direction ? { direction: asset.revision.direction } : {},
|
|
9543
|
+
...asset.revision.enhancePrompt == null ? {} : { enhancePrompt: asset.revision.enhancePrompt }
|
|
9247
9544
|
};
|
|
9248
9545
|
}
|
|
9546
|
+
if (asset.batch) {
|
|
9547
|
+
if (asset.batch.of === assetId) throw new Error(`assets.${assetId}: a batch cannot name itself as its own leader`);
|
|
9548
|
+
const leaderSpec = await finalize(asset.batch.of);
|
|
9549
|
+
if (!leaderSpec.batch || leaderSpec.batch.role !== "leader") {
|
|
9550
|
+
throw new Error(`assets.${assetId}: ${asset.batch.of} is not a 1dir batch leader`);
|
|
9551
|
+
}
|
|
9552
|
+
if (resolved.size !== leaderSpec.size) {
|
|
9553
|
+
throw new Error(
|
|
9554
|
+
`assets.${assetId}: batch members share the leader's canvas size; ${asset.batch.of} is ${leaderSpec.size}px, this is ${resolved.size}px`
|
|
9555
|
+
);
|
|
9556
|
+
}
|
|
9557
|
+
resolved.batch = {
|
|
9558
|
+
role: "member",
|
|
9559
|
+
itemDescriptions: leaderSpec.batch.itemDescriptions,
|
|
9560
|
+
index: asset.batch.index,
|
|
9561
|
+
leaderAssetId: asset.batch.of,
|
|
9562
|
+
leaderSpec
|
|
9563
|
+
};
|
|
9564
|
+
} else {
|
|
9565
|
+
const members = batchMembersByLeader.get(assetId);
|
|
9566
|
+
if (members) {
|
|
9567
|
+
if (style.generator !== "1dir") {
|
|
9568
|
+
throw new Error(`assets.${assetId}: only a 1dir asset can lead a batch`);
|
|
9569
|
+
}
|
|
9570
|
+
const sorted = [...members].sort((a, b) => a.index - b.index);
|
|
9571
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9572
|
+
for (const { index } of sorted) {
|
|
9573
|
+
if (seen.has(index)) throw new Error(`assets.${assetId}: two batch members both claim index ${index}`);
|
|
9574
|
+
seen.add(index);
|
|
9575
|
+
}
|
|
9576
|
+
const expectedIndices = sorted.map((_, i) => i + 1).join(",");
|
|
9577
|
+
if (sorted.map((m) => m.index).join(",") !== expectedIndices) {
|
|
9578
|
+
throw new Error(
|
|
9579
|
+
`assets.${assetId}: batch member indices must be 1..${sorted.length} with no gaps; got ${sorted.map((m) => m.index).join(",")}`
|
|
9580
|
+
);
|
|
9581
|
+
}
|
|
9582
|
+
const total = 1 + sorted.length;
|
|
9583
|
+
const limit = candidateCount(resolved.size);
|
|
9584
|
+
if (total > limit) {
|
|
9585
|
+
throw new Error(
|
|
9586
|
+
`assets.${assetId}: a ${resolved.size}px batch holds at most ${limit} items (1 leader + ${limit - 1} members); this one declares ${total}`
|
|
9587
|
+
);
|
|
9588
|
+
}
|
|
9589
|
+
resolved.batch = {
|
|
9590
|
+
role: "leader",
|
|
9591
|
+
itemDescriptions: [resolved.prompt, ...sorted.map(({ id }) => styleSpecs.get(id).prompt)],
|
|
9592
|
+
memberAssetIds: sorted.map((m) => m.id)
|
|
9593
|
+
};
|
|
9594
|
+
}
|
|
9595
|
+
}
|
|
9249
9596
|
resolved.specHash = specHash(
|
|
9250
9597
|
resolved,
|
|
9251
9598
|
styleImageHashes,
|
|
@@ -13513,6 +13860,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
13513
13860
|
for (const styleId of new Set(items.map((i) => i.spec.styleId))) {
|
|
13514
13861
|
styleImages.set(styleId, await resolveStyleImages(loaded, styleId));
|
|
13515
13862
|
}
|
|
13863
|
+
const byKey = new Map(items.map((item) => [item.key, item]));
|
|
13516
13864
|
let submitted = 0;
|
|
13517
13865
|
let failed = 0;
|
|
13518
13866
|
let spent = 0;
|
|
@@ -13542,8 +13890,62 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
13542
13890
|
await pruneInFlight();
|
|
13543
13891
|
}
|
|
13544
13892
|
}
|
|
13893
|
+
function writeBatchMembers(leaderSpec, leaderKey, error) {
|
|
13894
|
+
if (leaderSpec.batch?.role !== "leader") return;
|
|
13895
|
+
const leaderEntry = lock.entries[leaderKey];
|
|
13896
|
+
for (const memberAssetId of leaderSpec.batch.memberAssetIds ?? []) {
|
|
13897
|
+
const memberKey = lockKey(leaderSpec.styleId, memberAssetId);
|
|
13898
|
+
const memberItem = byKey.get(memberKey);
|
|
13899
|
+
if (!memberItem) {
|
|
13900
|
+
throw new Error(
|
|
13901
|
+
`${leaderKey}: batch member "${memberAssetId}" must be submitted in the same run as its leader; run without --only, or include every sibling`
|
|
13902
|
+
);
|
|
13903
|
+
}
|
|
13904
|
+
const memberSpec = memberItem.spec;
|
|
13905
|
+
const memberEstimate = estimates.get(memberKey);
|
|
13906
|
+
const previousMemberEntry = lock.entries[memberKey];
|
|
13907
|
+
const memberSubmittedAt = leaderEntry.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
13908
|
+
upsert(lock, memberKey, {
|
|
13909
|
+
styleId: memberSpec.styleId,
|
|
13910
|
+
assetId: memberSpec.assetId,
|
|
13911
|
+
specHash: memberSpec.specHash,
|
|
13912
|
+
generator: memberSpec.generator,
|
|
13913
|
+
prompt: memberSpec.prompt,
|
|
13914
|
+
width: memberSpec.width,
|
|
13915
|
+
height: memberSpec.height,
|
|
13916
|
+
batch: { role: "member", leaderAssetId: leaderSpec.assetId, index: memberSpec.batch.index },
|
|
13917
|
+
status: error ? "failed" : "processing",
|
|
13918
|
+
error,
|
|
13919
|
+
jobId: error ? null : leaderEntry.jobId,
|
|
13920
|
+
submissionComplete: error ? void 0 : true,
|
|
13921
|
+
reviewObjectId: error ? null : leaderEntry.reviewObjectId,
|
|
13922
|
+
objectId: null,
|
|
13923
|
+
candidateIndex: null,
|
|
13924
|
+
outputs: [],
|
|
13925
|
+
supersededOutputs: previousMemberEntry?.outputs.length ? previousMemberEntry.outputs : previousMemberEntry?.supersededOutputs ?? [],
|
|
13926
|
+
providerMetadata: leaderEntry.providerMetadata,
|
|
13927
|
+
sourceUrl: null,
|
|
13928
|
+
sourceUrls: [],
|
|
13929
|
+
submittedAt: memberSubmittedAt,
|
|
13930
|
+
history: historyAfterReplacing(previousMemberEntry, historyLimit(loaded.manifest), memberSubmittedAt),
|
|
13931
|
+
cost: memberEstimate.amount,
|
|
13932
|
+
costUnit: memberEstimate.unit,
|
|
13933
|
+
provider: provider.id,
|
|
13934
|
+
downloadedAt: null
|
|
13935
|
+
});
|
|
13936
|
+
if (error) failed++;
|
|
13937
|
+
else {
|
|
13938
|
+
submitted++;
|
|
13939
|
+
spent += memberEstimate.amount;
|
|
13940
|
+
log2(` ${memberKey} \u2192 ${leaderEntry.jobId} (rides on ${leaderKey}'s batch)`);
|
|
13941
|
+
}
|
|
13942
|
+
}
|
|
13943
|
+
}
|
|
13545
13944
|
for (const { spec, key } of items) {
|
|
13546
13945
|
const estimate = estimates.get(key);
|
|
13946
|
+
if (spec.batch?.role === "member") {
|
|
13947
|
+
continue;
|
|
13948
|
+
}
|
|
13547
13949
|
if (spec.mirror) {
|
|
13548
13950
|
try {
|
|
13549
13951
|
await requireRevisionReady(spec, lock);
|
|
@@ -13576,6 +13978,14 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
13576
13978
|
const since = Date.now() - lastSubmitAt;
|
|
13577
13979
|
if (since < spacing) await sleep2(spacing - since);
|
|
13578
13980
|
await requireRevisionReady(spec, lock);
|
|
13981
|
+
if (spec.batch?.role === "leader") {
|
|
13982
|
+
const missing = (spec.batch.memberAssetIds ?? []).filter((id) => !byKey.has(lockKey(spec.styleId, id)));
|
|
13983
|
+
if (missing.length) {
|
|
13984
|
+
throw new Error(
|
|
13985
|
+
`${key}: batch member(s) ${missing.join(", ")} must be submitted in the same run as this leader; run without --only, or include every sibling`
|
|
13986
|
+
);
|
|
13987
|
+
}
|
|
13988
|
+
}
|
|
13579
13989
|
const previousEntry = lock.entries[key];
|
|
13580
13990
|
const resumesCheckpoint = Boolean(
|
|
13581
13991
|
previousEntry?.specHash === spec.specHash && previousEntry.provider === provider.id && previousEntry.generator === spec.generator && previousEntry.submissionComplete === false && previousEntry.jobId
|
|
@@ -13603,7 +14013,12 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
13603
14013
|
...spec.revision.numColors == null ? {} : { numColors: spec.revision.numColors },
|
|
13604
14014
|
...spec.revision.paletteImageSha256 ? { paletteImageSha256: spec.revision.paletteImageSha256 } : {},
|
|
13605
14015
|
...spec.revision.dithering ? { dithering: spec.revision.dithering } : {},
|
|
13606
|
-
...spec.revision.ditheringStrength == null ? {} : { ditheringStrength: spec.revision.ditheringStrength }
|
|
14016
|
+
...spec.revision.ditheringStrength == null ? {} : { ditheringStrength: spec.revision.ditheringStrength },
|
|
14017
|
+
...spec.revision.frames == null ? {} : { frames: spec.revision.frames },
|
|
14018
|
+
...spec.revision.fps == null ? {} : { fps: spec.revision.fps },
|
|
14019
|
+
...spec.revision.lastFrameSha256 ? { lastFrameSha256: spec.revision.lastFrameSha256 } : {},
|
|
14020
|
+
...spec.revision.direction ? { direction: spec.revision.direction } : {},
|
|
14021
|
+
...spec.revision.enhancePrompt == null ? {} : { enhancePrompt: spec.revision.enhancePrompt }
|
|
13607
14022
|
} : null,
|
|
13608
14023
|
status: "pending",
|
|
13609
14024
|
jobId: previousJobId ?? null,
|
|
@@ -13676,6 +14091,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
13676
14091
|
inFlight.set(jobId, spec);
|
|
13677
14092
|
submitted++;
|
|
13678
14093
|
spent += estimate.amount;
|
|
14094
|
+
writeBatchMembers(spec, key, null);
|
|
13679
14095
|
log2(
|
|
13680
14096
|
` ${key} \u2192 ${jobId} (${spec.width}x${spec.height}` + (estimate.candidates > 1 ? `, ${estimate.candidates} ${spec.tileFeature ? "outputs" : "candidates"}` : "") + `, ${estimate.amount})`
|
|
13681
14097
|
);
|
|
@@ -13687,6 +14103,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
13687
14103
|
inFlight.set(entry.jobId, spec);
|
|
13688
14104
|
submitted++;
|
|
13689
14105
|
spent += estimate.amount;
|
|
14106
|
+
writeBatchMembers(spec, key, null);
|
|
13690
14107
|
log2(` ${key} \u2192 ${entry.jobId} (recovered from completed checkpoint)`);
|
|
13691
14108
|
} else {
|
|
13692
14109
|
failed++;
|
|
@@ -13695,6 +14112,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
13695
14112
|
error: message6,
|
|
13696
14113
|
cost: entry.jobId ? estimate.amount : 0
|
|
13697
14114
|
});
|
|
14115
|
+
writeBatchMembers(spec, key, message6);
|
|
13698
14116
|
log2(` FAILED ${key}: ${message6}`);
|
|
13699
14117
|
}
|
|
13700
14118
|
}
|
|
@@ -13895,6 +14313,9 @@ function renderSheet(groups, options = {}) {
|
|
|
13895
14313
|
.cand.active { border-color:var(--dim); box-shadow:0 0 0 2px color-mix(in srgb, var(--dim) 20%, transparent); }
|
|
13896
14314
|
.cand.sel { border-color:var(--ok); box-shadow:0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent); }
|
|
13897
14315
|
.cand.set-frame { cursor:pointer; }
|
|
14316
|
+
.cand.recommended:not(.sel) { border-color:var(--accent); }
|
|
14317
|
+
.cand.recommended .badge { position:absolute; top:-1px; left:-1px; font-size:9.5px; font-weight:650;
|
|
14318
|
+
text-transform:uppercase; letter-spacing:.03em; color:var(--bg); background:var(--accent); padding:1px 5px; }
|
|
13898
14319
|
.cand img { image-rendering:pixelated; display:block;
|
|
13899
14320
|
background-image:
|
|
13900
14321
|
linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%),
|
|
@@ -14052,14 +14473,17 @@ GROUPS.forEach((g, gi) => {
|
|
|
14052
14473
|
}).observe(loop);
|
|
14053
14474
|
}
|
|
14054
14475
|
}
|
|
14476
|
+
const recommended = g.recommendedIndex ?? 0;
|
|
14055
14477
|
g.frameUrls.forEach((url, i) => {
|
|
14056
14478
|
const c = document.createElement('button');
|
|
14057
14479
|
c.type = 'button';
|
|
14058
|
-
c.className = 'cand' + (g.mode === 'frame-set' ? ' set-frame' : '')
|
|
14480
|
+
c.className = 'cand' + (g.mode === 'frame-set' ? ' set-frame' : '') +
|
|
14481
|
+
(g.recommendedIndex != null && i === recommended ? ' recommended' : '');
|
|
14059
14482
|
c.tabIndex = -1;
|
|
14060
|
-
c.setAttribute('aria-label', g.mode === 'frame-set'
|
|
14483
|
+
c.setAttribute('aria-label', (g.mode === 'frame-set'
|
|
14061
14484
|
? 'Accept ordered frame set from frame ' + (i + 1)
|
|
14062
|
-
: 'Choose candidate ' + (i + 1) + ' of ' + g.frameUrls.length)
|
|
14485
|
+
: 'Choose candidate ' + (i + 1) + ' of ' + g.frameUrls.length) +
|
|
14486
|
+
(g.recommendedIndex != null && i === recommended ? ' (this asset\u2019s declared slot)' : ''));
|
|
14063
14487
|
|
|
14064
14488
|
const preview = document.createElement('img');
|
|
14065
14489
|
preview.className = 'preview';
|
|
@@ -14072,6 +14496,12 @@ GROUPS.forEach((g, gi) => {
|
|
|
14072
14496
|
index.textContent = (g.frameLabels?.[i] || String(i + 1)) + ' \xB7 ' +
|
|
14073
14497
|
g.width + '\xD7' + g.height + ' \xB7 ' + scaleLabel;
|
|
14074
14498
|
c.append(preview, index);
|
|
14499
|
+
if (g.recommendedIndex != null && i === recommended) {
|
|
14500
|
+
const badge = document.createElement('span');
|
|
14501
|
+
badge.className = 'badge';
|
|
14502
|
+
badge.textContent = 'declared';
|
|
14503
|
+
c.append(badge);
|
|
14504
|
+
}
|
|
14075
14505
|
if (displayScale > 1 && largestSide <= 96) {
|
|
14076
14506
|
const actual = document.createElement('img');
|
|
14077
14507
|
actual.className = 'actual';
|
|
@@ -14096,6 +14526,7 @@ GROUPS.forEach((g, gi) => {
|
|
|
14096
14526
|
});
|
|
14097
14527
|
frames.dataset.active = '0';
|
|
14098
14528
|
frames.querySelector('.cand')?.classList.add('active');
|
|
14529
|
+
if (recommended > 0) activate(recommended, frames);
|
|
14099
14530
|
root.appendChild(el);
|
|
14100
14531
|
});
|
|
14101
14532
|
|
|
@@ -14270,7 +14701,11 @@ async function prepareReview(provider, lock, opts = {}) {
|
|
|
14270
14701
|
height: spec.revision.sourceHeight
|
|
14271
14702
|
}
|
|
14272
14703
|
} : {},
|
|
14273
|
-
...current && currentRoute ? { current: { url: currentRoute, width: current.width, height: current.height } } : {}
|
|
14704
|
+
...current && currentRoute ? { current: { url: currentRoute, width: current.width, height: current.height } } : {},
|
|
14705
|
+
// A batch member's declared slot in the shared candidate set — the
|
|
14706
|
+
// leader's own slot 0 is already the sheet's ordinary default, so
|
|
14707
|
+
// only a member (whose slot is 1+) needs to say so explicitly.
|
|
14708
|
+
...entry.batch?.index != null ? { recommendedIndex: entry.batch.index } : {}
|
|
14274
14709
|
});
|
|
14275
14710
|
} catch (err) {
|
|
14276
14711
|
log2(` could not load candidates for ${key}: ${err instanceof Error ? err.message : String(err)}`);
|