fix(wp7-02): normalize generated image dimensions
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 2m17s
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 2m17s
This commit is contained in:
@@ -9,7 +9,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "13.0.1",
|
"better-sqlite3": "13.0.1",
|
||||||
"drizzle-orm": "0.45.2"
|
"drizzle-orm": "0.45.2",
|
||||||
|
"sharp": "0.35.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "7.6.13",
|
"@types/better-sqlite3": "7.6.13",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { GenerationAdapterRequest, NormalizedGenerationOutput } from "./ai-adapter-contract.js";
|
import type { GenerationAdapterRequest, NormalizedGenerationOutput } from "./ai-adapter-contract.js";
|
||||||
|
import { normalizeImageOutputToRatio } from "./image-output-normalizer.mjs";
|
||||||
import {
|
import {
|
||||||
AdapterContractError, type AdapterStartResult, type AdapterTransport, balanceSignalFromResponse,
|
AdapterContractError, type AdapterStartResult, type AdapterTransport, balanceSignalFromResponse,
|
||||||
classifyAdapterError, dimensionsForRatio, mockPngBytes, validateAdapterRequest, validateMockContract,
|
classifyAdapterError, dimensionsForRatio, mockPngBytes, validateAdapterRequest, validateMockContract,
|
||||||
@@ -41,7 +42,8 @@ export class GeminiFlashAdapter implements ModelAdapter {
|
|||||||
const classified = this.classifyError("error" in response ? response.error : undefined, (request.configSnapshot.error_mapping_profile as Record<string, string> | undefined) ?? {});
|
const classified = this.classifyError("error" in response ? response.error : undefined, (request.configSnapshot.error_mapping_profile as Record<string, string> | undefined) ?? {});
|
||||||
return { ...classified, status: "failed" };
|
return { ...classified, status: "failed" };
|
||||||
}
|
}
|
||||||
return { outputs: [this.normalizeOutput(response)], status: "completed" };
|
const output = this.normalizeOutput(response);
|
||||||
|
return { outputs: [await normalizeImageOutputToRatio({ ...output, ratio: request.ratio })], status: "completed" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const classified = error instanceof AdapterContractError
|
const classified = error instanceof AdapterContractError
|
||||||
? { category: "gateway_contract_invalid" as const, sourceCategory: error.sourceCategory }
|
? { category: "gateway_contract_invalid" as const, sourceCategory: error.sourceCategory }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { GenerationAdapterRequest, NormalizedGenerationOutput } from "./ai-adapter-contract.js";
|
import type { GenerationAdapterRequest, NormalizedGenerationOutput } from "./ai-adapter-contract.js";
|
||||||
|
import { normalizeImageOutputToRatio } from "./image-output-normalizer.mjs";
|
||||||
import {
|
import {
|
||||||
AdapterContractError, type AdapterStartResult, type AdapterTransport, balanceSignalFromResponse,
|
AdapterContractError, type AdapterStartResult, type AdapterTransport, balanceSignalFromResponse,
|
||||||
classifyAdapterError, dimensionsForRatio, mockPngBytes, validateAdapterRequest, validateMockContract,
|
classifyAdapterError, dimensionsForRatio, mockPngBytes, validateAdapterRequest, validateMockContract,
|
||||||
@@ -37,7 +38,7 @@ export class GeminiProAdapter implements ModelAdapter {
|
|||||||
try {
|
try {
|
||||||
validateAdapterRequest(request, this.modelId);
|
validateAdapterRequest(request, this.modelId);
|
||||||
const response = await this.transport.start({ operation: "start", modelId: this.modelId, prompt: request.prompt, ratio: request.ratio, referenceAssetIds: request.referenceAssetIds });
|
const response = await this.transport.start({ operation: "start", modelId: this.modelId, prompt: request.prompt, ratio: request.ratio, referenceAssetIds: request.referenceAssetIds });
|
||||||
return this.interpret(response, request.configSnapshot.error_mapping_profile as Record<string, string> ?? {});
|
return await this.interpret(response, request.configSnapshot.error_mapping_profile as Record<string, string> ?? {}, request.ratio);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const classified = error instanceof AdapterContractError
|
const classified = error instanceof AdapterContractError
|
||||||
? { category: "gateway_contract_invalid" as const, sourceCategory: error.sourceCategory }
|
? { category: "gateway_contract_invalid" as const, sourceCategory: error.sourceCategory }
|
||||||
@@ -49,7 +50,7 @@ export class GeminiProAdapter implements ModelAdapter {
|
|||||||
async poll(upstreamJobReference: string): Promise<AdapterStartResult> {
|
async poll(upstreamJobReference: string): Promise<AdapterStartResult> {
|
||||||
try {
|
try {
|
||||||
const response = await this.transport.poll({ operation: "poll", modelId: this.modelId, upstreamJobReference });
|
const response = await this.transport.poll({ operation: "poll", modelId: this.modelId, upstreamJobReference });
|
||||||
return this.interpret(response, {});
|
return await this.interpret(response, {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const classified = error instanceof AdapterContractError
|
const classified = error instanceof AdapterContractError
|
||||||
? { category: "gateway_contract_invalid" as const, sourceCategory: error.sourceCategory }
|
? { category: "gateway_contract_invalid" as const, sourceCategory: error.sourceCategory }
|
||||||
@@ -58,7 +59,7 @@ export class GeminiProAdapter implements ModelAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private interpret(response: unknown, mappingProfile: Readonly<Record<string, string>>): AdapterStartResult {
|
private async interpret(response: unknown, mappingProfile: Readonly<Record<string, string>>, ratio?: GenerationAdapterRequest["ratio"]): Promise<AdapterStartResult> {
|
||||||
if (!response || typeof response !== "object" || !("operation" in response) || !response.operation || typeof response.operation !== "object") {
|
if (!response || typeof response !== "object" || !("operation" in response) || !response.operation || typeof response.operation !== "object") {
|
||||||
return { category: "gateway_contract_invalid", sourceCategory: "response_shape_invalid", status: "failed" };
|
return { category: "gateway_contract_invalid", sourceCategory: "response_shape_invalid", status: "failed" };
|
||||||
}
|
}
|
||||||
@@ -72,7 +73,8 @@ export class GeminiProAdapter implements ModelAdapter {
|
|||||||
return reference ? { status: "pending", upstreamJobReference: reference } : { category: "gateway_contract_invalid", sourceCategory: "upstream_reference_missing", status: "failed" };
|
return reference ? { status: "pending", upstreamJobReference: reference } : { category: "gateway_contract_invalid", sourceCategory: "upstream_reference_missing", status: "failed" };
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return { outputs: [this.normalizeOutput("response" in operation ? operation.response : undefined)], status: "completed" };
|
const output = this.normalizeOutput("response" in operation ? operation.response : undefined);
|
||||||
|
return { outputs: [ratio ? await normalizeImageOutputToRatio({ ...output, ratio }) : output], status: "completed" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { category: "gateway_contract_invalid", sourceCategory: error instanceof AdapterContractError ? error.sourceCategory : "response_shape_invalid", status: "failed" };
|
return { category: "gateway_contract_invalid", sourceCategory: error instanceof AdapterContractError ? error.sourceCategory : "response_shape_invalid", status: "failed" };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { GenerationAdapterRequest, NormalizedGenerationOutput } from "./ai-adapter-contract.js";
|
import type { GenerationAdapterRequest, NormalizedGenerationOutput } from "./ai-adapter-contract.js";
|
||||||
|
import { normalizeImageOutputToRatio } from "./image-output-normalizer.mjs";
|
||||||
import {
|
import {
|
||||||
AdapterContractError, type AdapterStartResult, type AdapterTransport, balanceSignalFromResponse,
|
AdapterContractError, type AdapterStartResult, type AdapterTransport, balanceSignalFromResponse,
|
||||||
classifyAdapterError, dimensionsForRatio, mockPngBytes, validateAdapterRequest, validateMockContract,
|
classifyAdapterError, dimensionsForRatio, mockPngBytes, validateAdapterRequest, validateMockContract,
|
||||||
@@ -41,7 +42,8 @@ export class GptImageAdapter implements ModelAdapter {
|
|||||||
const classified = this.classifyError("error" in response ? response.error : undefined, (request.configSnapshot.error_mapping_profile as Record<string, string> | undefined) ?? {});
|
const classified = this.classifyError("error" in response ? response.error : undefined, (request.configSnapshot.error_mapping_profile as Record<string, string> | undefined) ?? {});
|
||||||
return { ...classified, status: "failed" };
|
return { ...classified, status: "failed" };
|
||||||
}
|
}
|
||||||
return { outputs: [this.normalizeOutput(response)], status: "completed" };
|
const output = this.normalizeOutput(response);
|
||||||
|
return { outputs: [await normalizeImageOutputToRatio({ ...output, ratio: request.ratio })], status: "completed" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const classified = error instanceof AdapterContractError
|
const classified = error instanceof AdapterContractError
|
||||||
? { category: "gateway_contract_invalid" as const, sourceCategory: error.sourceCategory }
|
? { category: "gateway_contract_invalid" as const, sourceCategory: error.sourceCategory }
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import sharp from "sharp";
|
||||||
|
|
||||||
|
const productDimensions = Object.freeze({
|
||||||
|
"3:4": Object.freeze({ pixelHeight: 1440, pixelWidth: 1080 }),
|
||||||
|
"1:1": Object.freeze({ pixelHeight: 1080, pixelWidth: 1080 }),
|
||||||
|
"4:3": Object.freeze({ pixelHeight: 1080, pixelWidth: 1440 }),
|
||||||
|
"9:16": Object.freeze({ pixelHeight: 1920, pixelWidth: 1080 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const gptImageRequestSizes = Object.freeze({
|
||||||
|
"3:4": "1056x1408",
|
||||||
|
"1:1": "1088x1088",
|
||||||
|
"4:3": "1408x1056",
|
||||||
|
"9:16": "1008x1792",
|
||||||
|
});
|
||||||
|
|
||||||
|
const allowedMimeTypes = new Set(["image/jpeg", "image/png", "image/webp"]);
|
||||||
|
const maximumInputBytes = 20 * 1024 * 1024;
|
||||||
|
|
||||||
|
function assertRatio(ratio) {
|
||||||
|
if (!(ratio in productDimensions)) throw new Error("image_output_ratio_unsupported");
|
||||||
|
return ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function productDimensionsForRatio(ratio) {
|
||||||
|
return { ...productDimensions[assertRatio(ratio)] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gptImageRequestSizeForRatio(ratio) {
|
||||||
|
return gptImageRequestSizes[assertRatio(ratio)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function normalizeImageOutputToRatio(input) {
|
||||||
|
const ratio = assertRatio(input?.ratio);
|
||||||
|
if (!Buffer.isBuffer(input?.bytes) || input.bytes.length === 0 || input.bytes.length > maximumInputBytes
|
||||||
|
|| !allowedMimeTypes.has(input?.mimeType)) {
|
||||||
|
throw new Error("image_output_media_invalid");
|
||||||
|
}
|
||||||
|
const target = productDimensions[ratio];
|
||||||
|
if (input.pixelWidth === target.pixelWidth && input.pixelHeight === target.pixelHeight) {
|
||||||
|
return {
|
||||||
|
bytes: Buffer.from(input.bytes),
|
||||||
|
mimeType: input.mimeType,
|
||||||
|
normalized: false,
|
||||||
|
...target,
|
||||||
|
upstreamPixelHeight: input.pixelHeight,
|
||||||
|
upstreamPixelWidth: input.pixelWidth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = sharp(input.bytes, { failOn: "error", limitInputPixels: 40_000_000 });
|
||||||
|
const metadata = await image.metadata();
|
||||||
|
if (!metadata.width || !metadata.height) throw new Error("image_output_dimensions_missing");
|
||||||
|
const requestedRatio = target.pixelWidth / target.pixelHeight;
|
||||||
|
const upstreamRatio = metadata.width / metadata.height;
|
||||||
|
if (Math.abs(upstreamRatio - requestedRatio) / requestedRatio > 0.02) {
|
||||||
|
throw new Error("image_output_aspect_ratio_mismatch");
|
||||||
|
}
|
||||||
|
const { data, info } = await image
|
||||||
|
.resize(target.pixelWidth, target.pixelHeight, { fit: "fill", kernel: sharp.kernel.lanczos3 })
|
||||||
|
.png({ compressionLevel: 9 })
|
||||||
|
.toBuffer({ resolveWithObject: true });
|
||||||
|
if (info.width !== target.pixelWidth || info.height !== target.pixelHeight || info.format !== "png") {
|
||||||
|
throw new Error("image_output_normalization_failed");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
bytes: data,
|
||||||
|
mimeType: "image/png",
|
||||||
|
normalized: true,
|
||||||
|
...target,
|
||||||
|
upstreamPixelHeight: metadata.height,
|
||||||
|
upstreamPixelWidth: metadata.width,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"lib": ["ES2024"],
|
"lib": ["ES2024"],
|
||||||
|
|||||||
Generated
+3
@@ -133,6 +133,9 @@ importers:
|
|||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: 0.45.2
|
specifier: 0.45.2
|
||||||
version: 0.45.2(@types/better-sqlite3@7.6.13)(better-sqlite3@13.0.1)
|
version: 0.45.2(@types/better-sqlite3@7.6.13)(better-sqlite3@13.0.1)
|
||||||
|
sharp:
|
||||||
|
specifier: 0.35.3
|
||||||
|
version: 0.35.3(@types/node@24.13.3)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/better-sqlite3':
|
'@types/better-sqlite3':
|
||||||
specifier: 7.6.13
|
specifier: 7.6.13
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export const frozenPackages = {
|
|||||||
dependencies: {
|
dependencies: {
|
||||||
"better-sqlite3": "13.0.1",
|
"better-sqlite3": "13.0.1",
|
||||||
"drizzle-orm": "0.45.2",
|
"drizzle-orm": "0.45.2",
|
||||||
|
sharp: "0.35.3",
|
||||||
},
|
},
|
||||||
devDependencies: {
|
devDependencies: {
|
||||||
typescript: "7.0.2",
|
typescript: "7.0.2",
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
|
|
||||||
|
import {
|
||||||
|
gptImageRequestSizeForRatio,
|
||||||
|
normalizeImageOutputToRatio,
|
||||||
|
} from "../../apps/worker/src/image-output-normalizer.mjs";
|
||||||
import { WP7_02_MODEL_IDS, buildModelContractPlan } from "./wp7-02-external-contract.mjs";
|
import { WP7_02_MODEL_IDS, buildModelContractPlan } from "./wp7-02-external-contract.mjs";
|
||||||
|
|
||||||
export const WP7_02_CONTROLLED_REAL_LIMIT = 120;
|
export const WP7_02_CONTROLLED_REAL_LIMIT = 120;
|
||||||
|
|
||||||
const ratios = ["3:4", "1:1", "4:3", "9:16"];
|
const ratios = ["3:4", "1:1", "4:3", "9:16"];
|
||||||
const openAiSizes = Object.freeze({
|
|
||||||
"3:4": "1080x1440",
|
|
||||||
"1:1": "1080x1080",
|
|
||||||
"4:3": "1440x1080",
|
|
||||||
"9:16": "1080x1920",
|
|
||||||
});
|
|
||||||
const allowedMimeTypes = new Set(["image/jpeg", "image/png", "image/webp"]);
|
const allowedMimeTypes = new Set(["image/jpeg", "image/png", "image/webp"]);
|
||||||
const forbiddenEvidenceKeys = /(?:^|_)(?:absolute_path|authorization|body|credential|credential_value|image_bytes|image_data|original_image|password|path|prompt|raw|raw_provider_payload|raw_prompt|secret|token)(?:_|$)/i;
|
const forbiddenEvidenceKeys = /(?:^|_)(?:absolute_path|authorization|body|credential|credential_value|image_bytes|image_data|original_image|password|path|prompt|raw|raw_provider_payload|raw_prompt|secret|token)(?:_|$)/i;
|
||||||
|
|
||||||
@@ -87,7 +85,7 @@ export function buildProviderRequest({ modelConfig, prompt, ratio, reference })
|
|||||||
model: config.model_id,
|
model: config.model_id,
|
||||||
prompt,
|
prompt,
|
||||||
response_format: "b64_json",
|
response_format: "b64_json",
|
||||||
size: openAiSizes[ratio],
|
size: gptImageRequestSizeForRatio(ratio),
|
||||||
};
|
};
|
||||||
if (reference) body.image = `data:${reference.mime_type};base64,${reference.bytes.toString("base64")}`;
|
if (reference) body.image = `data:${reference.mime_type};base64,${reference.bytes.toString("base64")}`;
|
||||||
return { body, headers, method: "POST", url: config.route_profile.endpoint };
|
return { body, headers, method: "POST", url: config.route_profile.endpoint };
|
||||||
@@ -221,6 +219,7 @@ export function buildSanitizedResponseEvidence(normalized) {
|
|||||||
dimensions: structuredClone(normalized.dimensions),
|
dimensions: structuredClone(normalized.dimensions),
|
||||||
evidence_hash: normalized.evidence_hash,
|
evidence_hash: normalized.evidence_hash,
|
||||||
mime: normalized.mime,
|
mime: normalized.mime,
|
||||||
|
...(normalized.normalization ? { normalization: structuredClone(normalized.normalization) } : {}),
|
||||||
usage_summary: structuredClone(normalized.usage_summary),
|
usage_summary: structuredClone(normalized.usage_summary),
|
||||||
};
|
};
|
||||||
return validateSanitizedEvidence(evidence);
|
return validateSanitizedEvidence(evidence);
|
||||||
@@ -264,7 +263,25 @@ export async function executeProviderRequest({ fetchImpl = fetch, modelConfig, p
|
|||||||
const providerResponse = await response.json();
|
const providerResponse = await response.json();
|
||||||
let normalized;
|
let normalized;
|
||||||
try {
|
try {
|
||||||
normalized = normalizeProviderResponse(modelConfig, providerResponse);
|
const providerNormalized = normalizeProviderResponse(modelConfig, providerResponse);
|
||||||
|
const adapted = await normalizeImageOutputToRatio({
|
||||||
|
bytes: providerNormalized.bytes,
|
||||||
|
mimeType: providerNormalized.mime,
|
||||||
|
pixelHeight: providerNormalized.dimensions.height,
|
||||||
|
pixelWidth: providerNormalized.dimensions.width,
|
||||||
|
ratio,
|
||||||
|
});
|
||||||
|
normalized = {
|
||||||
|
...providerNormalized,
|
||||||
|
bytes: adapted.bytes,
|
||||||
|
dimensions: { height: adapted.pixelHeight, width: adapted.pixelWidth },
|
||||||
|
evidence_hash: `sha256:${sha256(adapted.bytes)}`,
|
||||||
|
mime: adapted.mimeType,
|
||||||
|
normalization: {
|
||||||
|
applied: adapted.normalized,
|
||||||
|
upstream_dimensions: { height: adapted.upstreamPixelHeight, width: adapted.upstreamPixelWidth },
|
||||||
|
},
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && /^WP7_02_[A-Z0-9_]+$/.test(error.message)) {
|
if (error instanceof Error && /^WP7_02_[A-Z0-9_]+$/.test(error.message)) {
|
||||||
error.safe_response_shape = describeProviderResponseShape(providerResponse);
|
error.safe_response_shape = describeProviderResponseShape(providerResponse);
|
||||||
|
|||||||
@@ -135,6 +135,11 @@ test("TDD-WP7-EXT-001 confines the credential to the request header and discards
|
|||||||
token: credentialMarker,
|
token: credentialMarker,
|
||||||
});
|
});
|
||||||
assert.equal(success.http_status, 200);
|
assert.equal(success.http_status, 200);
|
||||||
|
assert.deepEqual(success.response_evidence.dimensions, { height: 1080, width: 1080 });
|
||||||
|
assert.deepEqual(success.response_evidence.normalization, {
|
||||||
|
applied: true,
|
||||||
|
upstream_dimensions: { height: 1, width: 1 },
|
||||||
|
});
|
||||||
assert.doesNotMatch(JSON.stringify({ ...success, normalized: undefined }), new RegExp(credentialMarker));
|
assert.doesNotMatch(JSON.stringify({ ...success, normalized: undefined }), new RegExp(credentialMarker));
|
||||||
|
|
||||||
await assert.rejects(() => executeProviderRequest({
|
await assert.rejects(() => executeProviderRequest({
|
||||||
@@ -166,7 +171,7 @@ test("TDD-WP7-EXT-001 executes only five real success probes per model and keeps
|
|||||||
assert.equal(fetchCalls, 5);
|
assert.equal(fetchCalls, 5);
|
||||||
assert.equal(execution.real_calls, 5);
|
assert.equal(execution.real_calls, 5);
|
||||||
assert.equal(execution.status, "externally_blocked");
|
assert.equal(execution.status, "externally_blocked");
|
||||||
assert.equal(execution.calls.filter((call) => call.status === "passed").length, 0);
|
assert.equal(execution.calls.filter((call) => call.status === "passed").length, 2);
|
||||||
assert.doesNotMatch(JSON.stringify(execution), /controlled-secret|fixture prompt|iVBOR/i);
|
assert.doesNotMatch(JSON.stringify(execution), /controlled-secret|fixture prompt|iVBOR/i);
|
||||||
await assert.rejects(() => runControlledRealScenarios({ maxRealCalls: 121, modelConfig: models[0], token: "not-used" }), /WP7_02_REAL_CALL_LIMIT_INVALID/);
|
await assert.rejects(() => runControlledRealScenarios({ maxRealCalls: 121, modelConfig: models[0], token: "not-used" }), /WP7_02_REAL_CALL_LIMIT_INVALID/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { GeminiFlashAdapter } from "../../apps/worker/src/ai-adapter-gemini-flash.js";
|
||||||
|
import { GeminiProAdapter } from "../../apps/worker/src/ai-adapter-gemini-pro.js";
|
||||||
|
import { GptImageAdapter } from "../../apps/worker/src/ai-adapter-gpt-image.js";
|
||||||
import {
|
import {
|
||||||
gptImageRequestSizeForRatio,
|
gptImageRequestSizeForRatio,
|
||||||
normalizeImageOutputToRatio,
|
normalizeImageOutputToRatio,
|
||||||
productDimensionsForRatio,
|
productDimensionsForRatio,
|
||||||
} from "../../apps/worker/src/image-output-normalizer.js";
|
} from "../../apps/worker/src/image-output-normalizer.mjs";
|
||||||
|
|
||||||
const onePixelPng = Buffer.from(
|
const onePixelPng = Buffer.from(
|
||||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
|
||||||
@@ -39,4 +42,29 @@ describe("TDD-WP7-EXT-001 exact image output normalization", () => {
|
|||||||
expect(output.bytes.subarray(0, 8).toString("hex")).toBe("89504e470d0a1a0a");
|
expect(output.bytes.subarray(0, 8).toString("hex")).toBe("89504e470d0a1a0a");
|
||||||
expect(productDimensionsForRatio("9:16")).toEqual({ pixelHeight: 1920, pixelWidth: 1080 });
|
expect(productDimensionsForRatio("9:16")).toEqual({ pixelHeight: 1920, pixelWidth: 1080 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("is used by all three production adapter boundaries", async () => {
|
||||||
|
const encoded = onePixelPng.toString("base64");
|
||||||
|
const adapters = [
|
||||||
|
new GeminiFlashAdapter({ transport: {
|
||||||
|
async start() { return { candidates: [{ inline_data: { data: encoded, mime_type: "image/png" }, pixelHeight: 1, pixelWidth: 1 }] }; },
|
||||||
|
async poll() { return {}; },
|
||||||
|
} }),
|
||||||
|
new GeminiProAdapter({ transport: {
|
||||||
|
async start() { return { operation: { done: true, response: { candidates: [{ inline_data: { data: encoded, mime_type: "image/png" }, pixelHeight: 1, pixelWidth: 1 }] } } }; },
|
||||||
|
async poll() { return {}; },
|
||||||
|
} }),
|
||||||
|
new GptImageAdapter({ transport: {
|
||||||
|
async start() { return { data: [{ b64_json: encoded, pixelHeight: 1, pixelWidth: 1 }] }; },
|
||||||
|
async poll() { return {}; },
|
||||||
|
} }),
|
||||||
|
];
|
||||||
|
for (const adapter of adapters) {
|
||||||
|
const result = await adapter.start({
|
||||||
|
configSnapshot: {}, generationId: `normalization-${adapter.modelId}`, modelId: adapter.modelId,
|
||||||
|
prompt: "sanitized fixture", ratio: "1:1", referenceAssetIds: [],
|
||||||
|
});
|
||||||
|
expect(result).toMatchObject({ status: "completed", outputs: [{ mimeType: "image/png", pixelHeight: 1080, pixelWidth: 1080 }] });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user