pixelkiln 0.13.0 → 0.14.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/PROVIDERS.md +63 -27
- package/README.md +25 -25
- package/SECURITY.md +2 -1
- package/dist/cli.js +533 -14
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +536 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -8
- package/dist/index.d.ts +91 -8
- package/dist/index.js +534 -16
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +14 -10
- package/docs/ARCHITECTURE.md +11 -5
- package/docs/CLI.md +1 -1
- package/docs/GETTING_STARTED.md +20 -4
- package/docs/LIBRARY.md +2 -1
- package/docs/MANIFEST.md +47 -1
- package/docs/MIXED_PROVIDERS.md +5 -3
- package/docs/PROVIDER_BENCHMARK.md +4 -0
- package/docs/README.md +1 -0
- package/docs/SCENARIO.md +202 -0
- package/package.json +2 -1
- package/skills/pixelkiln/SKILL.md +6 -3
- package/skills/pixelkiln/references/scenario.md +35 -0
package/dist/cli.js
CHANGED
|
@@ -2315,7 +2315,494 @@ function retroError(value) {
|
|
|
2315
2315
|
return JSON.stringify(value);
|
|
2316
2316
|
}
|
|
2317
2317
|
|
|
2318
|
+
// src/providers/scenario.ts
|
|
2319
|
+
var DEFAULT_BASE_URL3 = "https://api.cloud.scenario.com/v1";
|
|
2320
|
+
var SOURCE_PROTOCOL2 = "scenario:";
|
|
2321
|
+
var PROTECTED_PARAMETERS = /* @__PURE__ */ new Set([
|
|
2322
|
+
"dryRun",
|
|
2323
|
+
"height",
|
|
2324
|
+
"maxComputeUnits",
|
|
2325
|
+
"numOutputs",
|
|
2326
|
+
"projectId",
|
|
2327
|
+
"prompt",
|
|
2328
|
+
"seed",
|
|
2329
|
+
"width"
|
|
2330
|
+
]);
|
|
2331
|
+
var ScenarioClient = class {
|
|
2332
|
+
constructor(apiKey, apiSecret, baseUrl = DEFAULT_BASE_URL3, request = fetch) {
|
|
2333
|
+
this.apiKey = apiKey;
|
|
2334
|
+
this.apiSecret = apiSecret;
|
|
2335
|
+
this.baseUrl = baseUrl;
|
|
2336
|
+
this.request = request;
|
|
2337
|
+
}
|
|
2338
|
+
apiKey;
|
|
2339
|
+
apiSecret;
|
|
2340
|
+
baseUrl;
|
|
2341
|
+
request;
|
|
2342
|
+
async quote(modelId, body, projectId) {
|
|
2343
|
+
const response = await this.call(
|
|
2344
|
+
`/generate/custom/${encodeURIComponent(modelId)}`,
|
|
2345
|
+
{ method: "POST", body: JSON.stringify(body) },
|
|
2346
|
+
{ dryRun: "true", projectId }
|
|
2347
|
+
);
|
|
2348
|
+
const creativeUnitsCost = finiteNonnegative(
|
|
2349
|
+
response?.creativeUnitsCost,
|
|
2350
|
+
"Scenario dry run returned an invalid creativeUnitsCost"
|
|
2351
|
+
);
|
|
2352
|
+
const costDetails = numericRecord(response?.costDetails);
|
|
2353
|
+
const ipDetectionCost = optionalFiniteNonnegative(
|
|
2354
|
+
response?.ipDetection?.creativeUnitsCharged,
|
|
2355
|
+
"Scenario dry run returned an invalid IP-detection cost"
|
|
2356
|
+
);
|
|
2357
|
+
return {
|
|
2358
|
+
total: creativeUnitsCost + ipDetectionCost,
|
|
2359
|
+
creativeUnitsCost,
|
|
2360
|
+
costDetails,
|
|
2361
|
+
ipDetectionCost
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
async submit(modelId, body, projectId) {
|
|
2365
|
+
const response = await this.call(
|
|
2366
|
+
`/generate/custom/${encodeURIComponent(modelId)}`,
|
|
2367
|
+
{ method: "POST", body: JSON.stringify(body) },
|
|
2368
|
+
{ projectId }
|
|
2369
|
+
);
|
|
2370
|
+
const job = response?.job;
|
|
2371
|
+
const jobId = readJobId(job);
|
|
2372
|
+
if (!jobId) throw new Error("Scenario did not return an async job id");
|
|
2373
|
+
return jobId;
|
|
2374
|
+
}
|
|
2375
|
+
async job(id, projectId) {
|
|
2376
|
+
const response = await this.call(`/jobs/${encodeURIComponent(id)}`, {}, { projectId });
|
|
2377
|
+
const raw = response?.job;
|
|
2378
|
+
if (!raw || typeof raw !== "object") throw new Error("Scenario returned an invalid job");
|
|
2379
|
+
const record = raw;
|
|
2380
|
+
const jobId = readJobId(raw);
|
|
2381
|
+
if (!jobId || typeof record.status !== "string") {
|
|
2382
|
+
throw new Error("Scenario returned an invalid job");
|
|
2383
|
+
}
|
|
2384
|
+
return {
|
|
2385
|
+
jobId,
|
|
2386
|
+
status: record.status,
|
|
2387
|
+
...typeof record.progress === "number" ? { progress: record.progress } : {},
|
|
2388
|
+
...record.metadata && typeof record.metadata === "object" ? { metadata: record.metadata } : {},
|
|
2389
|
+
...record.billing && typeof record.billing === "object" ? { billing: record.billing } : {}
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
async asset(id, projectId) {
|
|
2393
|
+
const response = await this.call(
|
|
2394
|
+
`/assets/${encodeURIComponent(id)}`,
|
|
2395
|
+
{},
|
|
2396
|
+
{ originalAssets: "true", projectId }
|
|
2397
|
+
);
|
|
2398
|
+
const raw = response?.asset;
|
|
2399
|
+
if (!raw || typeof raw !== "object") throw new Error(`Scenario returned an invalid asset ${id}`);
|
|
2400
|
+
const asset = raw;
|
|
2401
|
+
const assetId = typeof asset.id === "string" && asset.id ? asset.id : id;
|
|
2402
|
+
const url = typeof asset.url === "string" ? asset.url : "";
|
|
2403
|
+
const originalFileUrl = typeof asset.originalFileUrl === "string" ? asset.originalFileUrl : void 0;
|
|
2404
|
+
if (!originalFileUrl && !url) throw new Error(`Scenario asset ${assetId} has no download URL`);
|
|
2405
|
+
const metadata = isRecord(asset.metadata) ? asset.metadata : {};
|
|
2406
|
+
const rawOutputIndex = asset.outputIndex ?? metadata.outputIndex;
|
|
2407
|
+
const outputIndex = Number.isInteger(rawOutputIndex) && Number(rawOutputIndex) >= 0 ? Number(rawOutputIndex) : Number.MAX_SAFE_INTEGER;
|
|
2408
|
+
const originalMimeType = typeof asset.originalMimeType === "string" ? asset.originalMimeType : typeof asset.mimeType === "string" ? asset.mimeType : void 0;
|
|
2409
|
+
return {
|
|
2410
|
+
id: assetId,
|
|
2411
|
+
url,
|
|
2412
|
+
...originalFileUrl ? { originalFileUrl } : {},
|
|
2413
|
+
...originalMimeType ? { originalMimeType } : {},
|
|
2414
|
+
outputIndex
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
async download(url) {
|
|
2418
|
+
const parsed = new URL(url);
|
|
2419
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
2420
|
+
throw new Error("Scenario downloads must use HTTP or HTTPS");
|
|
2421
|
+
}
|
|
2422
|
+
const response = await this.request(parsed);
|
|
2423
|
+
if (!response.ok) throw new Error(`Scenario download failed (${response.status})`);
|
|
2424
|
+
const declared = Number(response.headers.get("content-length"));
|
|
2425
|
+
if (Number.isFinite(declared) && declared > MAX_DOWNLOAD_BYTES) {
|
|
2426
|
+
throw new Error(`Scenario download exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
|
|
2427
|
+
}
|
|
2428
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
2429
|
+
if (bytes.length > MAX_DOWNLOAD_BYTES) {
|
|
2430
|
+
throw new Error(`Scenario download exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
|
|
2431
|
+
}
|
|
2432
|
+
return bytes;
|
|
2433
|
+
}
|
|
2434
|
+
async checkConnection() {
|
|
2435
|
+
await this.call("/models", {}, { pageSize: "1" });
|
|
2436
|
+
}
|
|
2437
|
+
async call(pathname, init = {}, query = {}) {
|
|
2438
|
+
if (!this.apiKey || !this.apiSecret) {
|
|
2439
|
+
const missing = [
|
|
2440
|
+
!this.apiKey ? "SCENARIO_SDK_API_KEY" : null,
|
|
2441
|
+
!this.apiSecret ? "SCENARIO_SDK_API_SECRET" : null
|
|
2442
|
+
].filter(Boolean);
|
|
2443
|
+
throw new Error(`${missing.join(" and ")} ${missing.length > 1 ? "are" : "is"} not set`);
|
|
2444
|
+
}
|
|
2445
|
+
const url = new URL(`${this.baseUrl.replace(/\/$/, "")}${pathname}`);
|
|
2446
|
+
for (const [key, value2] of Object.entries(query)) {
|
|
2447
|
+
if (value2 != null) url.searchParams.set(key, value2);
|
|
2448
|
+
}
|
|
2449
|
+
const auth = Buffer.from(`${this.apiKey}:${this.apiSecret}`).toString("base64");
|
|
2450
|
+
const response = await this.request(url, {
|
|
2451
|
+
...init,
|
|
2452
|
+
headers: {
|
|
2453
|
+
Accept: "application/json",
|
|
2454
|
+
Authorization: `Basic ${auth}`,
|
|
2455
|
+
...init.body ? { "Content-Type": "application/json" } : {},
|
|
2456
|
+
...init.headers
|
|
2457
|
+
}
|
|
2458
|
+
});
|
|
2459
|
+
const text = await response.text();
|
|
2460
|
+
let value = null;
|
|
2461
|
+
try {
|
|
2462
|
+
value = text ? JSON.parse(text) : null;
|
|
2463
|
+
} catch {
|
|
2464
|
+
value = text;
|
|
2465
|
+
}
|
|
2466
|
+
if (!response.ok) {
|
|
2467
|
+
const retry = response.headers.get("retry-after");
|
|
2468
|
+
const detail = redactScenarioError(value, [this.apiKey, this.apiSecret, auth]);
|
|
2469
|
+
throw new Error(
|
|
2470
|
+
`Scenario request failed (${response.status}): ${detail}` + (retry ? `; retry after ${retry}s` : "")
|
|
2471
|
+
);
|
|
2472
|
+
}
|
|
2473
|
+
return value;
|
|
2474
|
+
}
|
|
2475
|
+
};
|
|
2476
|
+
var ScenarioProvider = class _ScenarioProvider {
|
|
2477
|
+
constructor(client) {
|
|
2478
|
+
this.client = client;
|
|
2479
|
+
}
|
|
2480
|
+
client;
|
|
2481
|
+
id = "scenario";
|
|
2482
|
+
static fromEnv() {
|
|
2483
|
+
return new _ScenarioProvider(new ScenarioClient(
|
|
2484
|
+
process.env.SCENARIO_SDK_API_KEY,
|
|
2485
|
+
process.env.SCENARIO_SDK_API_SECRET,
|
|
2486
|
+
scenarioBaseUrl(process.env.SCENARIO_API_BASE_URL ?? DEFAULT_BASE_URL3)
|
|
2487
|
+
));
|
|
2488
|
+
}
|
|
2489
|
+
static forOffline() {
|
|
2490
|
+
return new _ScenarioProvider(new ScenarioClient(void 0, void 0));
|
|
2491
|
+
}
|
|
2492
|
+
static forDownloads() {
|
|
2493
|
+
return _ScenarioProvider.fromEnv();
|
|
2494
|
+
}
|
|
2495
|
+
supports(generator) {
|
|
2496
|
+
return generator === "map";
|
|
2497
|
+
}
|
|
2498
|
+
estimate(spec) {
|
|
2499
|
+
const options = scenarioOptions(spec);
|
|
2500
|
+
return {
|
|
2501
|
+
unit: "compute-units",
|
|
2502
|
+
amount: options.maxComputeUnits,
|
|
2503
|
+
candidates: options.numOutputs ?? 1
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
validate(spec, styleImages) {
|
|
2507
|
+
const options = scenarioOptions(spec);
|
|
2508
|
+
if (typeof options.modelId !== "string" || !options.modelId.trim()) {
|
|
2509
|
+
throw new Error("Scenario modelId is required");
|
|
2510
|
+
}
|
|
2511
|
+
if (!Number.isFinite(options.maxComputeUnits) || options.maxComputeUnits <= 0) {
|
|
2512
|
+
throw new Error("Scenario maxComputeUnits must be a positive number");
|
|
2513
|
+
}
|
|
2514
|
+
const count = options.numOutputs ?? 1;
|
|
2515
|
+
if (!Number.isInteger(count) || count < 1 || count > 4) {
|
|
2516
|
+
throw new Error("Scenario numOutputs must be a whole number from 1 to 4");
|
|
2517
|
+
}
|
|
2518
|
+
if (spec.width < 128 || spec.width > 2048 || spec.width % 16 !== 0 || spec.height < 128 || spec.height > 2048 || spec.height % 16 !== 0) {
|
|
2519
|
+
throw new Error("Scenario width and height must be multiples of 16 from 128 to 2048");
|
|
2520
|
+
}
|
|
2521
|
+
if (styleImages.length) {
|
|
2522
|
+
throw new Error("Scenario styleImages are not supported yet; use a reusable Scenario model");
|
|
2523
|
+
}
|
|
2524
|
+
if (options.projectId != null && (typeof options.projectId !== "string" || !options.projectId.trim())) {
|
|
2525
|
+
throw new Error("Scenario projectId must be a non-empty string");
|
|
2526
|
+
}
|
|
2527
|
+
validateParameters(options.parameters ?? {});
|
|
2528
|
+
}
|
|
2529
|
+
rateLimit() {
|
|
2530
|
+
return { spacingMs: 1e3, maxInFlight: 4 };
|
|
2531
|
+
}
|
|
2532
|
+
async submit(spec, styleImages) {
|
|
2533
|
+
this.validate(spec, styleImages);
|
|
2534
|
+
const options = scenarioOptions(spec);
|
|
2535
|
+
const body = requestBody(spec, options);
|
|
2536
|
+
const quote = await this.client.quote(options.modelId, body, options.projectId);
|
|
2537
|
+
if (quote.total > options.maxComputeUnits + Number.EPSILON) {
|
|
2538
|
+
throw new Error(
|
|
2539
|
+
`Scenario quoted ${quote.total} compute-units, above maxComputeUnits ${options.maxComputeUnits}; no paid request was sent`
|
|
2540
|
+
);
|
|
2541
|
+
}
|
|
2542
|
+
const apiJobId = await this.client.submit(options.modelId, body, options.projectId);
|
|
2543
|
+
return {
|
|
2544
|
+
jobId: jobRef(apiJobId, options.projectId),
|
|
2545
|
+
metadata: {
|
|
2546
|
+
modelId: options.modelId,
|
|
2547
|
+
projectId: options.projectId ?? null,
|
|
2548
|
+
maxComputeUnits: options.maxComputeUnits,
|
|
2549
|
+
quotedComputeUnits: quote.total,
|
|
2550
|
+
creativeUnitsCost: quote.creativeUnitsCost,
|
|
2551
|
+
costDetails: quote.costDetails,
|
|
2552
|
+
ipDetectionCost: quote.ipDetectionCost
|
|
2553
|
+
}
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
async poll(jobReference, _generator, context) {
|
|
2557
|
+
const reference = parseJobRef(jobReference);
|
|
2558
|
+
const job = await this.client.job(reference.jobId, reference.projectId);
|
|
2559
|
+
const metadata = jobMetadata(job, context?.spec ? scenarioOptions(context.spec) : void 0);
|
|
2560
|
+
if (["pending", "queued", "warming-up", "in-progress", "finalizing", "processing"].includes(job.status)) {
|
|
2561
|
+
return {
|
|
2562
|
+
status: "processing",
|
|
2563
|
+
progressPercent: validProgress(job.progress)
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
if (["failure", "failed", "canceled"].includes(job.status)) {
|
|
2567
|
+
return { status: "failed", error: scenarioJobError(job) };
|
|
2568
|
+
}
|
|
2569
|
+
if (job.status !== "success") {
|
|
2570
|
+
return { status: "failed", error: `Scenario returned unknown job status "${job.status}"` };
|
|
2571
|
+
}
|
|
2572
|
+
const assetIds = scenarioAssetIds(job);
|
|
2573
|
+
if (!assetIds.length) return { status: "failed", error: "Scenario job returned no asset IDs" };
|
|
2574
|
+
const assets = await this.assets(assetIds, reference.projectId);
|
|
2575
|
+
if (assets.length > 1) {
|
|
2576
|
+
return {
|
|
2577
|
+
status: "review",
|
|
2578
|
+
candidateUrls: assets.map(assetDownloadUrl),
|
|
2579
|
+
metadata: { ...metadata, assetIds: assets.map((asset2) => asset2.id) }
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
const asset = assets[0];
|
|
2583
|
+
return {
|
|
2584
|
+
status: "ready",
|
|
2585
|
+
objectId: asset.id,
|
|
2586
|
+
sourceUrl: assetRef(asset.id, reference.projectId),
|
|
2587
|
+
sources: [{ url: assetRef(asset.id, reference.projectId), mediaType: MediaType.PNG }],
|
|
2588
|
+
metadata: {
|
|
2589
|
+
...metadata,
|
|
2590
|
+
assetId: asset.id,
|
|
2591
|
+
outputIndex: asset.outputIndex === Number.MAX_SAFE_INTEGER ? null : asset.outputIndex
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
async selectCandidate(jobReference, index) {
|
|
2596
|
+
const reference = parseJobRef(jobReference);
|
|
2597
|
+
const job = await this.client.job(reference.jobId, reference.projectId);
|
|
2598
|
+
if (job.status !== "success") throw new Error(`Scenario job ${reference.jobId} is not ready`);
|
|
2599
|
+
const assets = await this.assets(scenarioAssetIds(job), reference.projectId);
|
|
2600
|
+
const asset = assets[index];
|
|
2601
|
+
if (!asset) throw new Error(`Scenario job ${reference.jobId} has no candidate at index ${index}`);
|
|
2602
|
+
return {
|
|
2603
|
+
objectId: asset.id,
|
|
2604
|
+
sourceUrl: assetRef(asset.id, reference.projectId),
|
|
2605
|
+
metadata: {
|
|
2606
|
+
...jobMetadata(job),
|
|
2607
|
+
assetId: asset.id,
|
|
2608
|
+
outputIndex: asset.outputIndex === Number.MAX_SAFE_INTEGER ? null : asset.outputIndex
|
|
2609
|
+
}
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
async download(source) {
|
|
2613
|
+
if (!source.startsWith(`${SOURCE_PROTOCOL2}//`)) return this.client.download(source);
|
|
2614
|
+
const reference = parseAssetRef(source);
|
|
2615
|
+
const asset = await this.client.asset(reference.assetId, reference.projectId);
|
|
2616
|
+
assertPngAsset(asset);
|
|
2617
|
+
return this.client.download(assetDownloadUrl(asset));
|
|
2618
|
+
}
|
|
2619
|
+
async checkConnection() {
|
|
2620
|
+
await this.client.checkConnection();
|
|
2621
|
+
}
|
|
2622
|
+
async assets(ids, projectId) {
|
|
2623
|
+
const assets = await Promise.all(ids.map((id) => this.client.asset(id, projectId)));
|
|
2624
|
+
const jobOrder = new Map(ids.map((id, index) => [id, index]));
|
|
2625
|
+
for (const asset of assets) assertPngAsset(asset);
|
|
2626
|
+
return assets.sort((left, right) => left.outputIndex - right.outputIndex || (jobOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (jobOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER) || left.id.localeCompare(right.id));
|
|
2627
|
+
}
|
|
2628
|
+
};
|
|
2629
|
+
function scenarioOptions(spec) {
|
|
2630
|
+
return spec.providerOptions;
|
|
2631
|
+
}
|
|
2632
|
+
function scenarioBaseUrl(value) {
|
|
2633
|
+
let url;
|
|
2634
|
+
try {
|
|
2635
|
+
url = new URL(value);
|
|
2636
|
+
} catch {
|
|
2637
|
+
throw new Error("SCENARIO_API_BASE_URL must be an absolute HTTP(S) URL");
|
|
2638
|
+
}
|
|
2639
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
2640
|
+
throw new Error("SCENARIO_API_BASE_URL must use HTTP or HTTPS");
|
|
2641
|
+
}
|
|
2642
|
+
return url.href.replace(/\/$/, "");
|
|
2643
|
+
}
|
|
2644
|
+
function requestBody(spec, options) {
|
|
2645
|
+
return {
|
|
2646
|
+
...options.parameters ?? {},
|
|
2647
|
+
prompt: spec.prompt,
|
|
2648
|
+
width: spec.width,
|
|
2649
|
+
height: spec.height,
|
|
2650
|
+
numOutputs: options.numOutputs ?? 1,
|
|
2651
|
+
...spec.seed != null ? { seed: spec.seed } : {}
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
function validateParameters(parameters) {
|
|
2655
|
+
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) {
|
|
2656
|
+
throw new Error("Scenario parameters must be a JSON object");
|
|
2657
|
+
}
|
|
2658
|
+
for (const key of Object.keys(parameters)) {
|
|
2659
|
+
if (PROTECTED_PARAMETERS.has(key)) {
|
|
2660
|
+
throw new Error(`Scenario parameters.${key} is managed by PixelKiln and cannot be overridden`);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
assertJsonValue(parameters, "Scenario parameters");
|
|
2664
|
+
}
|
|
2665
|
+
function assertJsonValue(value, label, seen = /* @__PURE__ */ new Set()) {
|
|
2666
|
+
if (value == null || typeof value === "string" || typeof value === "boolean") return;
|
|
2667
|
+
if (typeof value === "number") {
|
|
2668
|
+
if (!Number.isFinite(value)) throw new Error(`${label} contains a non-finite number`);
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
if (typeof value !== "object") throw new Error(`${label} must contain only JSON values`);
|
|
2672
|
+
if (!Array.isArray(value)) {
|
|
2673
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2674
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
2675
|
+
throw new Error(`${label} must contain only plain JSON objects`);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
if (seen.has(value)) throw new Error(`${label} contains a circular value`);
|
|
2679
|
+
seen.add(value);
|
|
2680
|
+
if (Array.isArray(value)) {
|
|
2681
|
+
value.forEach((item, index) => assertJsonValue(item, `${label}[${index}]`, seen));
|
|
2682
|
+
} else {
|
|
2683
|
+
for (const [key, item] of Object.entries(value)) {
|
|
2684
|
+
assertJsonValue(item, `${label}.${key}`, seen);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
seen.delete(value);
|
|
2688
|
+
}
|
|
2689
|
+
function jobRef(jobId, projectId) {
|
|
2690
|
+
const url = new URL("scenario://job");
|
|
2691
|
+
url.searchParams.set("id", jobId);
|
|
2692
|
+
if (projectId) url.searchParams.set("projectId", projectId);
|
|
2693
|
+
return url.href;
|
|
2694
|
+
}
|
|
2695
|
+
function parseJobRef(value) {
|
|
2696
|
+
if (!value.startsWith(`${SOURCE_PROTOCOL2}//`)) return { jobId: value };
|
|
2697
|
+
const url = new URL(value);
|
|
2698
|
+
const jobId = url.searchParams.get("id");
|
|
2699
|
+
if (url.protocol !== SOURCE_PROTOCOL2 || url.hostname !== "job" || !jobId) {
|
|
2700
|
+
throw new Error("Invalid durable Scenario job reference");
|
|
2701
|
+
}
|
|
2702
|
+
const projectId = url.searchParams.get("projectId") || void 0;
|
|
2703
|
+
return { jobId, ...projectId ? { projectId } : {} };
|
|
2704
|
+
}
|
|
2705
|
+
function assetRef(assetId, projectId) {
|
|
2706
|
+
const url = new URL("scenario://asset");
|
|
2707
|
+
url.searchParams.set("id", assetId);
|
|
2708
|
+
if (projectId) url.searchParams.set("projectId", projectId);
|
|
2709
|
+
return url.href;
|
|
2710
|
+
}
|
|
2711
|
+
function parseAssetRef(value) {
|
|
2712
|
+
const url = new URL(value);
|
|
2713
|
+
const assetId = url.searchParams.get("id");
|
|
2714
|
+
if (url.protocol !== SOURCE_PROTOCOL2 || url.hostname !== "asset" || !assetId) {
|
|
2715
|
+
throw new Error("Invalid durable Scenario asset reference");
|
|
2716
|
+
}
|
|
2717
|
+
const projectId = url.searchParams.get("projectId") || void 0;
|
|
2718
|
+
return { assetId, ...projectId ? { projectId } : {} };
|
|
2719
|
+
}
|
|
2720
|
+
function readJobId(value) {
|
|
2721
|
+
if (!value || typeof value !== "object") return null;
|
|
2722
|
+
const job = value;
|
|
2723
|
+
const id = job.jobId ?? job.id;
|
|
2724
|
+
return typeof id === "string" && id ? id : null;
|
|
2725
|
+
}
|
|
2726
|
+
function isRecord(value) {
|
|
2727
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2728
|
+
}
|
|
2729
|
+
function scenarioAssetIds(job) {
|
|
2730
|
+
const ids = job.metadata?.assetIds;
|
|
2731
|
+
return Array.isArray(ids) ? [...new Set(ids.filter((id) => typeof id === "string" && id.length > 0))] : [];
|
|
2732
|
+
}
|
|
2733
|
+
function assetDownloadUrl(asset) {
|
|
2734
|
+
return asset.originalFileUrl || asset.url;
|
|
2735
|
+
}
|
|
2736
|
+
function assertPngAsset(asset) {
|
|
2737
|
+
if (asset.originalMimeType && asset.originalMimeType !== MediaType.PNG) {
|
|
2738
|
+
throw new Error(
|
|
2739
|
+
`Scenario asset ${asset.id} is ${asset.originalMimeType}; PixelKiln's Scenario MVP requires PNG output`
|
|
2740
|
+
);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
function jobMetadata(job, options) {
|
|
2744
|
+
const billing = job.billing;
|
|
2745
|
+
const billedMain = optionalFiniteNonnegative(billing?.cuCost, "Scenario job returned invalid CU billing");
|
|
2746
|
+
const billedAddons = Object.values(numericRecord(billing?.cuCostDetails)).reduce((sum, amount) => sum + amount, 0);
|
|
2747
|
+
return {
|
|
2748
|
+
...options ? {
|
|
2749
|
+
modelId: options.modelId,
|
|
2750
|
+
projectId: options.projectId ?? null,
|
|
2751
|
+
maxComputeUnits: options.maxComputeUnits
|
|
2752
|
+
} : {},
|
|
2753
|
+
apiJobId: job.jobId,
|
|
2754
|
+
billedComputeUnits: billedMain + billedAddons,
|
|
2755
|
+
billing: billing ?? null
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
function scenarioJobError(job) {
|
|
2759
|
+
const raw = job.metadata?.error;
|
|
2760
|
+
const detail = typeof raw === "string" ? raw : raw && typeof raw === "object" && typeof raw.message === "string" ? String(raw.message) : "job did not complete";
|
|
2761
|
+
return `Scenario job ${job.status}: ${detail.slice(0, 500)}`;
|
|
2762
|
+
}
|
|
2763
|
+
function validProgress(progress) {
|
|
2764
|
+
if (typeof progress !== "number" || !Number.isFinite(progress) || progress < 0 || progress > 1) {
|
|
2765
|
+
return null;
|
|
2766
|
+
}
|
|
2767
|
+
return Math.round(progress * 100);
|
|
2768
|
+
}
|
|
2769
|
+
function finiteNonnegative(value, message5) {
|
|
2770
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new Error(message5);
|
|
2771
|
+
return value;
|
|
2772
|
+
}
|
|
2773
|
+
function optionalFiniteNonnegative(value, message5) {
|
|
2774
|
+
if (value == null) return 0;
|
|
2775
|
+
return finiteNonnegative(value, message5);
|
|
2776
|
+
}
|
|
2777
|
+
function numericRecord(value) {
|
|
2778
|
+
if (value == null) return {};
|
|
2779
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2780
|
+
throw new Error("Scenario returned invalid cost details");
|
|
2781
|
+
}
|
|
2782
|
+
const out = {};
|
|
2783
|
+
for (const [key, amount] of Object.entries(value)) {
|
|
2784
|
+
out[key] = finiteNonnegative(amount, `Scenario returned invalid cost detail "${key}"`);
|
|
2785
|
+
}
|
|
2786
|
+
return out;
|
|
2787
|
+
}
|
|
2788
|
+
function redactScenarioError(value, secrets) {
|
|
2789
|
+
let detail;
|
|
2790
|
+
if (typeof value === "string") detail = value;
|
|
2791
|
+
else if (value && typeof value === "object") {
|
|
2792
|
+
const record = value;
|
|
2793
|
+
const nested = record.error && typeof record.error === "object" ? record.error.message : null;
|
|
2794
|
+
detail = typeof record.message === "string" ? record.message : typeof nested === "string" ? nested : typeof record.detail === "string" ? record.detail : "request failed";
|
|
2795
|
+
} else detail = "request failed";
|
|
2796
|
+
for (const secret of secrets) {
|
|
2797
|
+
if (secret) detail = detail.split(secret).join("[redacted]");
|
|
2798
|
+
}
|
|
2799
|
+
return detail.slice(0, 500);
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2318
2802
|
// src/providers/registry.ts
|
|
2803
|
+
function providerCredentialEnvs(factory) {
|
|
2804
|
+
return factory.credentialEnvs ?? (factory.credentialEnv ? [factory.credentialEnv] : []);
|
|
2805
|
+
}
|
|
2319
2806
|
var factories = /* @__PURE__ */ new Map();
|
|
2320
2807
|
function registerProvider(factory) {
|
|
2321
2808
|
const id = factory.id.trim();
|
|
@@ -2360,6 +2847,15 @@ registerProvider({
|
|
|
2360
2847
|
return ComfyUIProvider.forOffline();
|
|
2361
2848
|
}
|
|
2362
2849
|
});
|
|
2850
|
+
registerProvider({
|
|
2851
|
+
id: "scenario",
|
|
2852
|
+
credentialEnvs: ["SCENARIO_SDK_API_KEY", "SCENARIO_SDK_API_SECRET"],
|
|
2853
|
+
create(mode2) {
|
|
2854
|
+
if (mode2 === "online") return ScenarioProvider.fromEnv();
|
|
2855
|
+
if (mode2 === "downloads") return ScenarioProvider.forDownloads();
|
|
2856
|
+
return ScenarioProvider.forOffline();
|
|
2857
|
+
}
|
|
2858
|
+
});
|
|
2363
2859
|
|
|
2364
2860
|
// src/manifest.ts
|
|
2365
2861
|
import { readFile as readFile2 } from "fs/promises";
|
|
@@ -3333,13 +3829,14 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
3333
3829
|
lastSubmitAt = Date.now();
|
|
3334
3830
|
try {
|
|
3335
3831
|
const refs = styleImages.get(spec.styleId) ?? [];
|
|
3336
|
-
const { jobId } = await provider.submit(spec, refs);
|
|
3832
|
+
const { jobId, metadata } = await provider.submit(spec, refs);
|
|
3337
3833
|
upsert(lock, key, {
|
|
3338
3834
|
jobId,
|
|
3339
3835
|
// A multi-candidate generator routes through review; record the parent
|
|
3340
3836
|
// so `pick` knows where to look.
|
|
3341
3837
|
reviewObjectId: estimate.candidates > 1 && !spec.tileFeature ? jobId : null,
|
|
3342
|
-
status: "processing"
|
|
3838
|
+
status: "processing",
|
|
3839
|
+
providerMetadata: metadata ? { ...lock.entries[key].providerMetadata, [provider.id]: metadata } : lock.entries[key].providerMetadata
|
|
3343
3840
|
});
|
|
3344
3841
|
inFlight.set(jobId, spec);
|
|
3345
3842
|
submitted++;
|
|
@@ -3389,7 +3886,13 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
3389
3886
|
upsert(lock, key, {
|
|
3390
3887
|
status: "review",
|
|
3391
3888
|
reviewObjectId: entry.jobId,
|
|
3392
|
-
providerMetadata: state.metadata ? {
|
|
3889
|
+
providerMetadata: state.metadata ? {
|
|
3890
|
+
...entry.providerMetadata,
|
|
3891
|
+
[provider.id]: {
|
|
3892
|
+
...entry.providerMetadata[provider.id],
|
|
3893
|
+
...state.metadata
|
|
3894
|
+
}
|
|
3895
|
+
} : entry.providerMetadata
|
|
3393
3896
|
});
|
|
3394
3897
|
result.review++;
|
|
3395
3898
|
log2(` review ${key} (${state.candidateUrls.length} candidates)`);
|
|
@@ -3400,7 +3903,13 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
3400
3903
|
objectId: state.objectId,
|
|
3401
3904
|
sourceUrl: sourceUrls[0]?.url ?? null,
|
|
3402
3905
|
sourceUrls,
|
|
3403
|
-
providerMetadata: state.metadata ? {
|
|
3906
|
+
providerMetadata: state.metadata ? {
|
|
3907
|
+
...entry.providerMetadata,
|
|
3908
|
+
[provider.id]: {
|
|
3909
|
+
...entry.providerMetadata[provider.id],
|
|
3910
|
+
...state.metadata
|
|
3911
|
+
}
|
|
3912
|
+
} : entry.providerMetadata,
|
|
3404
3913
|
error: null
|
|
3405
3914
|
});
|
|
3406
3915
|
result.completed++;
|
|
@@ -3760,7 +4269,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
3760
4269
|
add(
|
|
3761
4270
|
checkId,
|
|
3762
4271
|
"error",
|
|
3763
|
-
!target.apiKeyPresent ? `${target.credentialEnv ?? (legacyTarget ? "PIXELLAB_API_KEY" : `${target.id} credential`)} is not configured` : `${target.id} is not configured`
|
|
4272
|
+
!target.apiKeyPresent ? `${target.missingCredentialEnvs?.length ? target.missingCredentialEnvs.join(", ") : target.credentialEnv ?? (legacyTarget ? "PIXELLAB_API_KEY" : `${target.id} credential`)} ${target.missingCredentialEnvs && target.missingCredentialEnvs.length > 1 ? "are" : "is"} not configured` : `${target.id} is not configured`
|
|
3764
4273
|
);
|
|
3765
4274
|
} else if (!target.provider.balance && !target.provider.checkConnection) {
|
|
3766
4275
|
add(checkId, "ok", `${target.id} configured; connectivity check unavailable`);
|
|
@@ -4381,7 +4890,7 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
4381
4890
|
const entry = lock.entries[key];
|
|
4382
4891
|
if (!group || !entry?.reviewObjectId) continue;
|
|
4383
4892
|
if (!Number.isInteger(index) || index < 0 || index >= group.frameUrls.length) continue;
|
|
4384
|
-
const { objectId, sourceUrl } = await requireSelectCandidate(provider)(
|
|
4893
|
+
const { objectId, sourceUrl, metadata } = await requireSelectCandidate(provider)(
|
|
4385
4894
|
entry.reviewObjectId,
|
|
4386
4895
|
index,
|
|
4387
4896
|
`asset:${entry.assetId}`,
|
|
@@ -4393,7 +4902,14 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
4393
4902
|
candidateIndex: index,
|
|
4394
4903
|
sourceUrl: sourceUrl ?? group.frameUrls[index] ?? null,
|
|
4395
4904
|
sourceUrls: [],
|
|
4396
|
-
provider: provider.id
|
|
4905
|
+
provider: provider.id,
|
|
4906
|
+
providerMetadata: metadata ? {
|
|
4907
|
+
...entry.providerMetadata,
|
|
4908
|
+
[provider.id]: {
|
|
4909
|
+
...entry.providerMetadata[provider.id],
|
|
4910
|
+
...metadata
|
|
4911
|
+
}
|
|
4912
|
+
} : entry.providerMetadata
|
|
4397
4913
|
});
|
|
4398
4914
|
selected++;
|
|
4399
4915
|
log2(` picked ${key} \u2192 candidate ${index + 1}`);
|
|
@@ -6038,16 +6554,16 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
6038
6554
|
|
|
6039
6555
|
// src/pipeline/tileset-export.ts
|
|
6040
6556
|
function normalizeTileRules(raw) {
|
|
6041
|
-
if (!
|
|
6557
|
+
if (!isRecord2(raw)) return null;
|
|
6042
6558
|
const ruleType = raw.rule_type;
|
|
6043
6559
|
const arity = raw.arity;
|
|
6044
6560
|
if (ruleType !== "edge" && ruleType !== "corner" && ruleType !== "outline") return null;
|
|
6045
6561
|
if (arity !== 4 && arity !== 6) return null;
|
|
6046
6562
|
const masks = {};
|
|
6047
|
-
if (
|
|
6563
|
+
if (isRecord2(raw.tiles)) {
|
|
6048
6564
|
for (const [key, value] of Object.entries(raw.tiles)) {
|
|
6049
6565
|
const index = tileIndex(key);
|
|
6050
|
-
const mask = typeof value === "number" ? value :
|
|
6566
|
+
const mask = typeof value === "number" ? value : isRecord2(value) && typeof value.bitmask === "number" ? value.bitmask : isRecord2(value) && typeof value.mask === "number" ? value.mask : null;
|
|
6051
6567
|
if (index !== null && mask !== null && Number.isInteger(mask) && mask >= 0) masks[index] = mask;
|
|
6052
6568
|
}
|
|
6053
6569
|
}
|
|
@@ -6078,7 +6594,7 @@ function exportTileset(entry, spec, options) {
|
|
|
6078
6594
|
);
|
|
6079
6595
|
}
|
|
6080
6596
|
const metadata = entry.providerMetadata?.[entry.provider];
|
|
6081
|
-
const rawRules =
|
|
6597
|
+
const rawRules = isRecord2(metadata) ? metadata.tileRules : void 0;
|
|
6082
6598
|
const rules = normalizeTileRules(rawRules);
|
|
6083
6599
|
const frames = packed.atlas.frames;
|
|
6084
6600
|
const generic = buildGeneric(
|
|
@@ -6086,7 +6602,7 @@ function exportTileset(entry, spec, options) {
|
|
|
6086
6602
|
options.imageName,
|
|
6087
6603
|
packed.atlas,
|
|
6088
6604
|
frames,
|
|
6089
|
-
|
|
6605
|
+
isRecord2(rawRules) ? rawRules : null,
|
|
6090
6606
|
rules
|
|
6091
6607
|
);
|
|
6092
6608
|
if (options.format === "generic") {
|
|
@@ -6288,7 +6804,7 @@ function tileIndex(key) {
|
|
|
6288
6804
|
const match = key.match(/(?:^|[_-])(\d+)$/);
|
|
6289
6805
|
return match ? Number(match[1]) : null;
|
|
6290
6806
|
}
|
|
6291
|
-
function
|
|
6807
|
+
function isRecord2(value) {
|
|
6292
6808
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6293
6809
|
}
|
|
6294
6810
|
|
|
@@ -8330,11 +8846,14 @@ async function main() {
|
|
|
8330
8846
|
)].sort();
|
|
8331
8847
|
const providers2 = providerIds.map((id) => {
|
|
8332
8848
|
const factory = providerFactory(id);
|
|
8333
|
-
const
|
|
8849
|
+
const credentialEnvs = providerCredentialEnvs(factory);
|
|
8850
|
+
const missingCredentialEnvs = credentialEnvs.filter((name) => !process.env[name]);
|
|
8851
|
+
const apiKeyPresent = missingCredentialEnvs.length === 0;
|
|
8334
8852
|
return {
|
|
8335
8853
|
id,
|
|
8336
8854
|
apiKeyPresent,
|
|
8337
8855
|
credentialEnv: factory.credentialEnv,
|
|
8856
|
+
missingCredentialEnvs,
|
|
8338
8857
|
provider: !args.dryRun && apiKeyPresent ? createProvider(id, "online") : void 0
|
|
8339
8858
|
};
|
|
8340
8859
|
});
|