From 0a2c225797a13367d513a499160ec786d07bdc3a Mon Sep 17 00:00:00 2001 From: Simplereally Date: Wed, 11 Mar 2026 20:50:33 +1100 Subject: [PATCH 1/4] feat: add dirtberry image model Made-with: Cursor --- lib/config/models.test.ts | 70 ++++++++++++++++++- lib/config/models.ts | 29 ++++++++ .../pollinations-pricing.schema.test.ts | 51 ++++++++++++++ lib/schemas/pollinations-pricing.schema.ts | 15 ++++ lib/schemas/pollinations.schema.ts | 2 + 5 files changed, 165 insertions(+), 2 deletions(-) diff --git a/lib/config/models.test.ts b/lib/config/models.test.ts index 7a190f0..52a90e8 100644 --- a/lib/config/models.test.ts +++ b/lib/config/models.test.ts @@ -38,6 +38,7 @@ describe("Model Registry", () => { "imagen-4", "grok-imagine", "flux-2-dev", + "dirtberry", ] for (const modelId of expectedImageModels) { @@ -76,6 +77,7 @@ describe("Model Registry", () => { expect(MODEL_REGISTRY["grok-imagine"].displayName).toBe("Grok Imagine") expect(MODEL_REGISTRY["grok-video"].displayName).toBe("Grok Video") expect(MODEL_REGISTRY["flux-2-dev"].displayName).toBe("FLUX.2 Dev") + expect(MODEL_REGISTRY["dirtberry"].displayName).toBe("Dirtberry") }) it("should have valid constraints for all models", () => { @@ -187,11 +189,11 @@ describe("Model Registry", () => { describe("Model Lists", () => { it("should have all model IDs", () => { - expect(ALL_MODEL_IDS.length).toBe(20) + expect(ALL_MODEL_IDS.length).toBe(21) }) it("should have correct image model IDs", () => { - expect(IMAGE_MODEL_IDS.length).toBe(15) + expect(IMAGE_MODEL_IDS.length).toBe(16) expect(IMAGE_MODEL_IDS).toContain("zimage") expect(IMAGE_MODEL_IDS).toContain("gptimage") expect(IMAGE_MODEL_IDS).toContain("flux") @@ -199,6 +201,7 @@ describe("Model Registry", () => { expect(IMAGE_MODEL_IDS).toContain("imagen-4") expect(IMAGE_MODEL_IDS).toContain("grok-imagine") expect(IMAGE_MODEL_IDS).toContain("flux-2-dev") + expect(IMAGE_MODEL_IDS).toContain("dirtberry") expect(IMAGE_MODEL_IDS).not.toContain("veo") }) @@ -224,6 +227,12 @@ describe("Model Registry", () => { it("should include flux-2-dev in UNRESTRICTED_MODEL_IDS (isUnrestricted: true)", () => { expect(UNRESTRICTED_MODEL_IDS.has("flux-2-dev")).toBe(true) }) + + it("should include dirtberry in ACTIVE_IMAGE_MODEL_IDS and not in LEGACY_MODEL_IDS or UNRESTRICTED_MODEL_IDS", () => { + expect(ACTIVE_IMAGE_MODEL_IDS).toContain("dirtberry") + expect(LEGACY_MODEL_IDS).not.toContain("dirtberry") + expect(UNRESTRICTED_MODEL_IDS.has("dirtberry")).toBe(false) + }) }) }) @@ -576,6 +585,36 @@ describe("Model Constraints", () => { expect(model.constraints.supportedTiers).toEqual(["hd"]) }) }) + + describe("Dirtberry", () => { + it("should have fixed dimensions (DALL-E 3 standard)", () => { + const model = getModel("dirtberry")! + expect(model.constraints.dimensionsEnabled).toBe(false) + expect(model.constraints.outputCertainty).toBe("exact") + expect(model.constraints.minDimension).toBe(1024) + expect(model.constraints.maxDimension).toBe(1792) + expect(model.constraints.defaultDimensions).toEqual({ width: 1024, height: 1024 }) + }) + + it("should not support negative prompts or reference images", () => { + const model = getModel("dirtberry")! + expect(model.supportsNegativePrompt).toBe(false) + expect(model.supportsReferenceImage).toBe(false) + }) + + it("should have correct provider metadata", () => { + const model = getModel("dirtberry")! + expect(model.icon).toBe("sparkles") + expect(model.logo).toBe("/image-models/flux.svg") + expect(model.isLegacy).toBeUndefined() + expect(model.isUnrestricted).toBeUndefined() + }) + + it("should only support HD tier", () => { + const model = getModel("dirtberry")! + expect(model.constraints.supportedTiers).toEqual(["hd"]) + }) + }) }) describe("Aspect Ratio Presets", () => { @@ -755,6 +794,33 @@ describe("Aspect Ratio Presets", () => { const grokRatios = getModelAspectRatios("grok-imagine")! expect(imagenRatios).toEqual(grokRatios) }) + + it("should have Dirtberry limited to 3 presets (no custom) with DALL-E 3 standard dimensions", () => { + const ratios = getModelAspectRatios("dirtberry")! + expect(ratios.length).toBe(3) + expect(ratios.every(r => r.value !== "custom")).toBe(true) + + const square = ratios.find(r => r.value === "1:1") + expect(square?.width).toBe(1024) + expect(square?.height).toBe(1024) + + const landscape = ratios.find(r => r.value === "16:9") + expect(landscape?.width).toBe(1792) + expect(landscape?.height).toBe(1024) + + const portrait = ratios.find(r => r.value === "9:16") + expect(portrait?.width).toBe(1024) + expect(portrait?.height).toBe(1792) + }) + + it("should have Dirtberry share identical aspect ratios with Imagen 4 and Grok Imagine", () => { + const imagenRatios = getModelAspectRatios("imagen-4")! + const dirtberryRatios = getModelAspectRatios("dirtberry")! + const grokRatios = getModelAspectRatios("grok-imagine")! + + expect(dirtberryRatios).toEqual(imagenRatios) + expect(dirtberryRatios).toEqual(grokRatios) + }) }) describe("Video Model Properties", () => { diff --git a/lib/config/models.ts b/lib/config/models.ts index 3850d2c..7751c3c 100644 --- a/lib/config/models.ts +++ b/lib/config/models.ts @@ -523,6 +523,35 @@ export const MODEL_REGISTRY: Record = { modelPricing: IMAGE_MODEL_PRICING["grok-imagine"], }, + dirtberry: { + id: "dirtberry", + displayName: "Dirtberry", + type: "image", + icon: "sparkles", + logo: "/image-models/flux.svg", + description: "Quick realistic image generation via api.airforce", + constraints: { + maxPixels: Infinity, + minPixels: 0, + minDimension: 1024, + maxDimension: 1792, + step: 1, + defaultDimensions: { width: 1024, height: 1024 }, + dimensionsEnabled: false, + // Seed not supported: api.airforce buildRequestBody only sends model, prompt, n, size. + // See: image.pollinations.ai/src/models/airforceModel.ts + supportsSeed: false, + supportedTiers: ["hd"], + outputCertainty: "exact", + dimensionWarning: "Dimensions are fixed for this model", + }, + aspectRatios: DALLE3_STANDARD_ASPECT_RATIOS, + supportsNegativePrompt: false, + supportsReferenceImage: false, + // Dirtberry is kept safe-only in Bloom Studio (no NSFW/unrestricted flag) + modelPricing: IMAGE_MODEL_PRICING["dirtberry"], + }, + "gptimage-large": { id: "gptimage-large", displayName: "GPT 1.5", diff --git a/lib/schemas/pollinations-pricing.schema.test.ts b/lib/schemas/pollinations-pricing.schema.test.ts index b1f446f..4d8c2b0 100644 --- a/lib/schemas/pollinations-pricing.schema.test.ts +++ b/lib/schemas/pollinations-pricing.schema.test.ts @@ -27,6 +27,14 @@ describe("pollinations-pricing.schema", () => { expect(pricing?.approximatePerPollen).toBe(1000); }); + it("should return pricing for dirtberry", () => { + const pricing = getModelPricing("dirtberry"); + expect(pricing).toBeDefined(); + expect(pricing?.type).toBe("image"); + expect(pricing?.modelId).toBe("dirtberry"); + expect(pricing?.approximatePerPollen).toBe(1000); + }); + it("should return pricing for a valid video model", () => { const pricing = getModelPricing("veo"); expect(pricing).toBeDefined(); @@ -68,6 +76,14 @@ describe("pollinations-pricing.schema", () => { expect(cost).toBe(30.0 / 1_000_000); }); + it("should calculate cost for dirtberry at $0.001/image", () => { + const cost1 = estimateImageCost("dirtberry", 1); + expect(cost1).toBe(0.001); + + const cost10 = estimateImageCost("dirtberry", 10); + expect(cost10).toBe(0.001 * 10); + }); + it("should return undefined for video models", () => { const cost = estimateImageCost("veo"); expect(cost).toBeUndefined(); @@ -141,6 +157,7 @@ describe("pollinations-pricing.schema", () => { it("should return false for models that do not support reference image", () => { expect(modelSupportsReferenceImage("flux")).toBe(false); expect(modelSupportsReferenceImage("flux-2-dev")).toBe(false); + expect(modelSupportsReferenceImage("dirtberry")).toBe(false); }); it("should return false for unknown models", () => { @@ -152,6 +169,7 @@ describe("pollinations-pricing.schema", () => { it("should return true for alpha models", () => { expect(isModelAlpha("veo")).toBe(true); expect(isModelAlpha("grok-video")).toBe(true); + expect(isModelAlpha("dirtberry")).toBe(true); }); it("should return false for stable models", () => { @@ -198,3 +216,36 @@ describe("FLUX.2 Dev pricing specifics", () => { expect(pricing.tokenPricing).toBeUndefined(); }); }); + +describe("Dirtberry pricing specifics", () => { + it("should exist in IMAGE_MODEL_PRICING", () => { + expect(IMAGE_MODEL_PRICING["dirtberry"]).toBeDefined(); + }); + + it("should have per-image pricing at $0.001", () => { + const pricing = IMAGE_MODEL_PRICING["dirtberry"]; + expect(pricing.imagePricing?.perImage).toBe(0.001); + }); + + it("should have approximatePerPollen of 1000 (consistent with perImage)", () => { + const pricing = IMAGE_MODEL_PRICING["dirtberry"]; + expect(pricing.approximatePerPollen).toBe(1000); + const expectedEfficiency = 1 / pricing.imagePricing!.perImage; + expect(pricing.approximatePerPollen).toBe(expectedEfficiency); + }); + + it("should not support reference image", () => { + const pricing = IMAGE_MODEL_PRICING["dirtberry"]; + expect(pricing.supportsReferenceImage).toBe(false); + }); + + it("should be alpha", () => { + const pricing = IMAGE_MODEL_PRICING["dirtberry"]; + expect(pricing.isAlpha).toBe(true); + }); + + it("should not have token pricing (uses simple per-image pricing)", () => { + const pricing = IMAGE_MODEL_PRICING["dirtberry"]; + expect(pricing.tokenPricing).toBeUndefined(); + }); +}); diff --git a/lib/schemas/pollinations-pricing.schema.ts b/lib/schemas/pollinations-pricing.schema.ts index 9ce096c..31b8455 100644 --- a/lib/schemas/pollinations-pricing.schema.ts +++ b/lib/schemas/pollinations-pricing.schema.ts @@ -349,6 +349,21 @@ export const IMAGE_MODEL_PRICING: Record = { perImage: 0.0025, // completionImageTokens from API }, }, + + /** + * Dirtberry - Quick realistic image generation via api.airforce + * Good efficiency: ~1000 images per pollen (same as FLUX.2 Dev) + */ + dirtberry: { + modelId: "dirtberry", + type: "image", + approximatePerPollen: 1000, + supportsReferenceImage: false, + isAlpha: true, + imagePricing: { + perImage: 0.001, // completionImageTokens from API (~free tier) + }, + }, } as const; // ============================================================================ diff --git a/lib/schemas/pollinations.schema.ts b/lib/schemas/pollinations.schema.ts index c689b13..4b9ee97 100644 --- a/lib/schemas/pollinations.schema.ts +++ b/lib/schemas/pollinations.schema.ts @@ -19,6 +19,8 @@ export const KnownImageModelSchema = z.enum([ "klein-large", "imagen-4", "grok-imagine", + "flux-2-dev", + "dirtberry", ]); export const ImageModelSchema = z.union([KnownImageModelSchema, z.string()]); From 27fbd78d1ba3ae1af645cea5e08033832229c434 Mon Sep 17 00:00:00 2001 From: Simplereally Date: Wed, 11 Mar 2026 22:36:36 +1100 Subject: [PATCH 2/4] feat: persist cropped Dirtberry media consistently Crop Dirtberry generations before upload and storage so every surface uses the same dimensions, and add supporting tests/docs for the new pipeline behavior. Made-with: Cursor --- DIRTBERRY-CROPPING.md | 48 ++++++++++ convex/_generated/api.d.ts | 2 + convex/batchProcessor.ts | 60 ++++++++++-- convex/lib/dirtberryCrop.test.ts | 73 +++++++++++++++ convex/lib/dirtberryCrop.ts | 130 ++++++++++++++++++++++++++ convex/lib/index.ts | 13 +++ convex/singleGenerationProcessor.ts | 60 ++++++++++-- hooks/queries/use-generate-image.ts | 7 +- hooks/use-generation-settings.test.ts | 13 +++ hooks/use-generation-settings.ts | 15 ++- lib/config/models.test.ts | 33 ++----- lib/config/models.ts | 15 ++- 12 files changed, 417 insertions(+), 52 deletions(-) create mode 100644 DIRTBERRY-CROPPING.md create mode 100644 convex/lib/dirtberryCrop.test.ts create mode 100644 convex/lib/dirtberryCrop.ts diff --git a/DIRTBERRY-CROPPING.md b/DIRTBERRY-CROPPING.md new file mode 100644 index 0000000..4e10a9a --- /dev/null +++ b/DIRTBERRY-CROPPING.md @@ -0,0 +1,48 @@ +# Dirtberry Cropping Implementation Notes + +## Goal + +- Make `dirtberry` portrait-only and show **post-crop dimensions** in Studio (`832x1144`). +- Remove Dirtberry corner marks by cropping **before** persistence. +- Keep downstream rendering/export simple by storing a pre-cropped source. + +## Final Approach (Server-Side, Pre-Upload) + +Cropping is applied in Convex after Pollinations returns image bytes and before R2 upload: + +1. Fetch Pollinations image bytes in `convex/singleGenerationProcessor.ts`. +2. If `model` resolves to Dirtberry: + - Crop 3% from top and 3% from bottom using `convex/lib/dirtberryCrop.ts`. +3. Upload the cropped buffer to R2. +4. Persist cropped dimensions in `generatedImages` (and Dirtberry generation params). + +Result: Gallery, canvas, lightbox, and downloads all use the same already-cropped asset. + +## Crop Math + +- `TRIM_FRACTION = 0.03` +- `trimPixels = round(H * 0.03)` +- `croppedHeight = H - (2 * trimPixels)` +- For source `H = 1216`: `trimPixels = 36`, `croppedHeight = 1144` + +## Required Code Touchpoints + +- `lib/config/models.ts` + - Dirtberry aspect ratios reduced to one preset (`9:16`, `832x1144` display dimensions) + - Dirtberry default dimensions updated to `832x1144` +- `hooks/use-generation-settings.ts` + - Fixed single-tier models use exact preset dimensions on model switch +- `convex/lib/dirtberryCrop.ts` + - Crop-region math + buffer crop utility +- `convex/singleGenerationProcessor.ts` + - Force Dirtberry upstream request dimensions to source size (`832x1216`) + - Wire Dirtberry crop before `uploadMediaWithThumbnail(...)` +- `convex/batchProcessor.ts` + - Apply the same Dirtberry source-dimension + pre-upload crop logic for batch jobs + +## Validation Checklist + +1. Select `dirtberry` -> one aspect option with displayed dimensions `832x1144`. +2. Generate Dirtberry image -> no visible corner watermark in Studio surfaces. +3. Download image -> dimensions reflect cropped output (`832x1144`). +4. Generate non-Dirtberry model -> no behavior change. diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 3dffb4d..2a96fbf 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -19,6 +19,7 @@ import type * as follows from "../follows.js"; import type * as generatedImages from "../generatedImages.js"; import type * as http from "../http.js"; import type * as lib_crypto from "../lib/crypto.js"; +import type * as lib_dirtberryCrop from "../lib/dirtberryCrop.js"; import type * as lib_groq from "../lib/groq.js"; import type * as lib_index from "../lib/index.js"; import type * as lib_nsfwDetection from "../lib/nsfwDetection.js"; @@ -73,6 +74,7 @@ declare const fullApi: ApiFromModules<{ generatedImages: typeof generatedImages; http: typeof http; "lib/crypto": typeof lib_crypto; + "lib/dirtberryCrop": typeof lib_dirtberryCrop; "lib/groq": typeof lib_groq; "lib/index": typeof lib_index; "lib/nsfwDetection": typeof lib_nsfwDetection; diff --git a/convex/batchProcessor.ts b/convex/batchProcessor.ts index e742f48..baacfec 100644 --- a/convex/batchProcessor.ts +++ b/convex/batchProcessor.ts @@ -20,6 +20,9 @@ import { internalAction } from "./_generated/server" import { buildPollinationsUrl, classifyApiError, + cropDirtberryImageBuffer, + getDirtberrySourceDimensions, + isDirtberryModel, generateR2Key, generateThumbnailKey, uploadMediaWithThumbnail, @@ -130,6 +133,8 @@ export const processBatchItem = internalAction({ } try { + const shouldCropDirtberry = isDirtberryModel(batchJob.generationParams.model) + const dirtberrySourceDimensions = shouldCropDirtberry ? getDirtberrySourceDimensions() : null // Pollinations API only accepts seeds up to int32 max (2147483647) const INT32_MAX = 2147483647 const rawSeed = batchJob.generationParams.seed ?? Math.floor(Math.random() * INT32_MAX) @@ -140,8 +145,8 @@ export const processBatchItem = internalAction({ prompt: batchJob.generationParams.prompt, negativePrompt: batchJob.generationParams.negativePrompt, model: batchJob.generationParams.model, - width: batchJob.generationParams.width, - height: batchJob.generationParams.height, + width: dirtberrySourceDimensions?.width ?? batchJob.generationParams.width, + height: dirtberrySourceDimensions?.height ?? batchJob.generationParams.height, seed, enhance: batchJob.generationParams.enhance, private: batchJob.generationParams.private, @@ -191,12 +196,42 @@ export const processBatchItem = internalAction({ const imageBuffer = Buffer.from(await response.arrayBuffer()) const contentType = response.headers.get("content-type") || "image/jpeg" + let uploadBuffer = imageBuffer + let outputWidth = batchJob.generationParams.width ?? 1024 + let outputHeight = batchJob.generationParams.height ?? 1024 + + // Crop Dirtberry outputs before upload/persistence so every surface + // (canvas, gallery, downloads, lightbox) uses the same native asset. + if (shouldCropDirtberry) { + try { + const cropped = await cropDirtberryImageBuffer(imageBuffer) + uploadBuffer = Buffer.from(cropped.buffer) + outputWidth = cropped.width + outputHeight = cropped.height + + if (cropped.wasCropped) { + console.log( + `${logger} Applied Dirtberry crop (${cropped.processor}): ${cropped.inputWidth}x${cropped.inputHeight} -> ${cropped.width}x${cropped.height} (trim=${cropped.trimPixels}px)` + ) + } else { + console.log( + `${logger} Dirtberry crop skipped: source=${cropped.inputWidth}x${cropped.inputHeight} (image too small to trim safely)` + ) + } + } catch (cropError) { + console.error( + `${logger} Dirtberry crop failed, falling back to original image:`, + cropError + ) + } + } + // Upload to R2 (and thumbnail for images — videos defer secondary assets) const r2Key = generateR2Key(batchJob.ownerId, contentType) console.log(`${logger} Uploading to R2: ${r2Key}`) const { media: uploadResult, thumbnail: thumbnailResult } = await uploadMediaWithThumbnail( - imageBuffer, + uploadBuffer, r2Key, contentType ) @@ -217,16 +252,23 @@ export const processBatchItem = internalAction({ previewR2Key: undefined, previewUrl: undefined, prompt: batchJob.generationParams.prompt, - width: batchJob.generationParams.width ?? 1024, - height: batchJob.generationParams.height ?? 1024, + width: outputWidth, + height: outputHeight, model: batchJob.generationParams.model ?? "flux", seed, contentType, sizeBytes: uploadResult.sizeBytes, - generationParams: { - ...batchJob.generationParams, - seed, - }, + generationParams: shouldCropDirtberry + ? { + ...batchJob.generationParams, + seed, + width: outputWidth, + height: outputHeight, + } + : { + ...batchJob.generationParams, + seed, + }, visibility: batchJob.generationParams.private ? "unlisted" : "public", }) diff --git a/convex/lib/dirtberryCrop.test.ts b/convex/lib/dirtberryCrop.test.ts new file mode 100644 index 0000000..cd29f64 --- /dev/null +++ b/convex/lib/dirtberryCrop.test.ts @@ -0,0 +1,73 @@ +import sharp from "sharp" +import { describe, expect, it } from "vitest" +import { + DIRTBERRY_OUTPUT_HEIGHT, + DIRTBERRY_SOURCE_HEIGHT, + DIRTBERRY_SOURCE_WIDTH, + DIRTBERRY_TRIM_FRACTION, + calculateDirtberryCropRegion, + cropDirtberryImageBuffer, +} from "./dirtberryCrop" + +describe("calculateDirtberryCropRegion", () => { + it("calculates 3% top/bottom crop for source height", () => { + const region = calculateDirtberryCropRegion(DIRTBERRY_SOURCE_HEIGHT) + expect(region).toEqual({ + top: Math.round(DIRTBERRY_SOURCE_HEIGHT * DIRTBERRY_TRIM_FRACTION), + height: DIRTBERRY_OUTPUT_HEIGHT, + }) + }) + + it("returns null when crop would be invalid", () => { + expect(calculateDirtberryCropRegion(0)).toBeNull() + expect(calculateDirtberryCropRegion(1)).toBeNull() + expect(calculateDirtberryCropRegion(10)).toBeNull() + }) +}) + +describe("cropDirtberryImageBuffer", () => { + it("crops an image buffer to remove top/bottom strips", async () => { + const input = await sharp({ + create: { + width: DIRTBERRY_SOURCE_WIDTH, + height: DIRTBERRY_SOURCE_HEIGHT, + channels: 3, + background: { r: 255, g: 0, b: 0 }, + }, + }) + .jpeg() + .toBuffer() + + const result = await cropDirtberryImageBuffer(input) + expect(result.wasCropped).toBe(true) + expect(result.width).toBe(DIRTBERRY_SOURCE_WIDTH) + expect(result.height).toBe(DIRTBERRY_OUTPUT_HEIGHT) + expect(result.inputWidth).toBe(DIRTBERRY_SOURCE_WIDTH) + expect(result.inputHeight).toBe(DIRTBERRY_SOURCE_HEIGHT) + expect(result.trimPixels).toBe(Math.round(DIRTBERRY_SOURCE_HEIGHT * DIRTBERRY_TRIM_FRACTION)) + + const metadata = await sharp(result.buffer).metadata() + expect(metadata.width).toBe(DIRTBERRY_SOURCE_WIDTH) + expect(metadata.height).toBe(DIRTBERRY_OUTPUT_HEIGHT) + }) + + it("still crops when upstream returns shorter-than-expected heights", async () => { + const input = await sharp({ + create: { + width: DIRTBERRY_SOURCE_WIDTH, + height: DIRTBERRY_OUTPUT_HEIGHT, + channels: 3, + background: { r: 0, g: 255, b: 0 }, + }, + }) + .jpeg() + .toBuffer() + + const result = await cropDirtberryImageBuffer(input) + const expectedTrim = Math.round(DIRTBERRY_OUTPUT_HEIGHT * DIRTBERRY_TRIM_FRACTION) + expect(result.wasCropped).toBe(true) + expect(result.width).toBe(DIRTBERRY_SOURCE_WIDTH) + expect(result.height).toBe(DIRTBERRY_OUTPUT_HEIGHT - expectedTrim * 2) + expect(result.trimPixels).toBe(expectedTrim) + }) +}) diff --git a/convex/lib/dirtberryCrop.ts b/convex/lib/dirtberryCrop.ts new file mode 100644 index 0000000..3b65962 --- /dev/null +++ b/convex/lib/dirtberryCrop.ts @@ -0,0 +1,130 @@ +"use node" + +export const DIRTBERRY_TRIM_FRACTION = 0.03 +export const DIRTBERRY_SOURCE_WIDTH = 832 +export const DIRTBERRY_SOURCE_HEIGHT = 1216 +export const DIRTBERRY_OUTPUT_HEIGHT = + DIRTBERRY_SOURCE_HEIGHT - Math.round(DIRTBERRY_SOURCE_HEIGHT * DIRTBERRY_TRIM_FRACTION) * 2 + +const JIMP_ENCODABLE_MIME_TYPES = new Set([ + "image/bmp", + "image/x-ms-bmp", + "image/gif", + "image/jpeg", + "image/png", + "image/tiff", +] as const) + +type JimpEncodableMime = "image/bmp" | "image/x-ms-bmp" | "image/gif" | "image/jpeg" | "image/png" | "image/tiff" + +type DirtberryCropRegion = { + top: number + height: number +} + +export type DirtberryCropResult = { + buffer: Buffer + width: number + height: number + wasCropped: boolean + inputWidth: number + inputHeight: number + trimPixels: number + processor: "jimp" | "none" +} + +export function getDirtberrySourceDimensions(): { width: number; height: number } { + return { + width: DIRTBERRY_SOURCE_WIDTH, + height: DIRTBERRY_SOURCE_HEIGHT, + } +} + +export function isDirtberryModel(model?: string): boolean { + const normalized = model?.trim().toLowerCase() + if (!normalized) { + return false + } + return normalized === "dirtberry" || normalized.includes("dirtberry") +} + +/** + * Calculate Dirtberry's watermark-removal crop region. + * Trims 3% from top and bottom when the resulting crop is valid. + */ +export function calculateDirtberryCropRegion(height: number): DirtberryCropRegion | null { + if (!Number.isFinite(height) || height <= 0) return null + + const trimPixels = Math.round(height * DIRTBERRY_TRIM_FRACTION) + const croppedHeight = height - trimPixels * 2 + + if (trimPixels <= 0 || croppedHeight <= 0) { + return null + } + + return { + top: trimPixels, + height: croppedHeight, + } +} + +/** + * Crop Dirtberry images to remove corner watermarks before persistence. + * Returns original data if crop is not applicable. + */ +export async function cropDirtberryImageBuffer( + imageBuffer: Buffer +): Promise { + // Use jimp only (pure JS) so Convex linux-arm64 deployments don't fail + // on native sharp module loading. + const { Jimp } = await import("jimp") + const image = await Jimp.read(imageBuffer) + const width = image.bitmap.width + const height = image.bitmap.height + + if (!width || !height) { + throw new Error("Unable to read Dirtberry image dimensions") + } + + const region = calculateDirtberryCropRegion(height) + if (!region) { + return { + buffer: imageBuffer, + width, + height, + wasCropped: false, + inputWidth: width, + inputHeight: height, + trimPixels: 0, + processor: "none", + } + } + + image.crop({ + x: 0, + y: region.top, + w: width, + h: region.height, + }) + + const inputMime = image.mime + const outputMime: JimpEncodableMime = + inputMime && JIMP_ENCODABLE_MIME_TYPES.has(inputMime as JimpEncodableMime) + ? (inputMime as JimpEncodableMime) + : "image/jpeg" + const croppedBuffer = + outputMime === "image/jpeg" + ? Buffer.from(await image.getBuffer(outputMime, { quality: 100 })) + : Buffer.from(await image.getBuffer(outputMime)) + + return { + buffer: croppedBuffer, + width, + height: region.height, + wasCropped: true, + inputWidth: width, + inputHeight: height, + trimPixels: region.top, + processor: "jimp", + } +} diff --git a/convex/lib/index.ts b/convex/lib/index.ts index 88576ed..03eafaa 100644 --- a/convex/lib/index.ts +++ b/convex/lib/index.ts @@ -22,6 +22,19 @@ export { type ErrorClassification, } from "./pollinations" +// Dirtberry processing utilities +export { + DIRTBERRY_TRIM_FRACTION, + DIRTBERRY_SOURCE_WIDTH, + DIRTBERRY_SOURCE_HEIGHT, + DIRTBERRY_OUTPUT_HEIGHT, + getDirtberrySourceDimensions, + isDirtberryModel, + calculateDirtberryCropRegion, + cropDirtberryImageBuffer, + type DirtberryCropResult, +} from "./dirtberryCrop" + // R2 storage utilities export { generateR2Key, diff --git a/convex/singleGenerationProcessor.ts b/convex/singleGenerationProcessor.ts index 2d0c5d1..9d5a07a 100644 --- a/convex/singleGenerationProcessor.ts +++ b/convex/singleGenerationProcessor.ts @@ -19,6 +19,9 @@ import { internalAction } from "./_generated/server" import { buildPollinationsUrl, classifyApiError, + cropDirtberryImageBuffer, + getDirtberrySourceDimensions, + isDirtberryModel, generateR2Key, generateThumbnailKey, uploadMediaWithThumbnail, @@ -119,6 +122,8 @@ export const processGeneration = internalAction({ try { const params = generation.generationParams + const shouldCropDirtberry = isDirtberryModel(params.model) + const dirtberrySourceDimensions = shouldCropDirtberry ? getDirtberrySourceDimensions() : null // Pollinations API only accepts seeds up to int32 max (2147483647) const INT32_MAX = 2147483647 const rawSeed = params.seed ?? Math.floor(Math.random() * INT32_MAX) @@ -134,8 +139,8 @@ export const processGeneration = internalAction({ prompt: params.prompt, negativePrompt: params.negativePrompt, model: params.model, - width: params.width, - height: params.height, + width: dirtberrySourceDimensions?.width ?? params.width, + height: dirtberrySourceDimensions?.height ?? params.height, seed, enhance: params.enhance, private: params.private, @@ -198,12 +203,42 @@ export const processGeneration = internalAction({ const imageBuffer = Buffer.from(await response.arrayBuffer()) const contentType = response.headers.get("content-type") || "image/jpeg" + let uploadBuffer = imageBuffer + let outputWidth = params.width ?? 1024 + let outputHeight = params.height ?? 1024 + + // Crop Dirtberry outputs before upload/persistence so every surface + // (canvas, gallery, downloads, lightbox) uses the same native asset. + if (shouldCropDirtberry) { + try { + const cropped = await cropDirtberryImageBuffer(imageBuffer) + uploadBuffer = Buffer.from(cropped.buffer) + outputWidth = cropped.width + outputHeight = cropped.height + + if (cropped.wasCropped) { + console.log( + `${logger} Applied Dirtberry crop (${cropped.processor}): ${cropped.inputWidth}x${cropped.inputHeight} -> ${cropped.width}x${cropped.height} (trim=${cropped.trimPixels}px)` + ) + } else { + console.log( + `${logger} Dirtberry crop skipped: source=${cropped.inputWidth}x${cropped.inputHeight} (image too small to trim safely)` + ) + } + } catch (cropError) { + console.error( + `${logger} Dirtberry crop failed, falling back to original image:`, + cropError + ) + } + } + // Upload to R2 (and thumbnail for images — videos defer secondary assets) const r2Key = generateR2Key(generation.ownerId, contentType) console.log(`${logger} Uploading to R2: ${r2Key}`) const { media: uploadResult, thumbnail: thumbnailResult } = await uploadMediaWithThumbnail( - imageBuffer, + uploadBuffer, r2Key, contentType ) @@ -240,16 +275,23 @@ export const processGeneration = internalAction({ previewR2Key: undefined, previewUrl: undefined, prompt: params.prompt, - width: params.width ?? 1024, - height: params.height ?? 1024, + width: outputWidth, + height: outputHeight, model: params.model ?? "flux", seed, contentType, sizeBytes: uploadResult.sizeBytes, - generationParams: { - ...params, - seed, - }, + generationParams: shouldCropDirtberry + ? { + ...params, + seed, + width: outputWidth, + height: outputHeight, + } + : { + ...params, + seed, + }, visibility: params.private ? "unlisted" : "public", }) diff --git a/hooks/queries/use-generate-image.ts b/hooks/queries/use-generate-image.ts index 929921d..c964d63 100644 --- a/hooks/queries/use-generate-image.ts +++ b/hooks/queries/use-generate-image.ts @@ -361,11 +361,16 @@ export function useGenerateImage( callbacksRef.current.onSettled?.(undefined, err, entry.params) entry.reject?.(err) } else if (generatedImage) { + const entryParams = entry.params as GeneratedImage["params"] const image: GeneratedImage = { id: generatedImage._id, url: generatedImage.url, prompt: generatedImage.prompt, - params: entry.params as GeneratedImage["params"], + params: { + ...entryParams, + width: generatedImage.width ?? entryParams.width, + height: generatedImage.height ?? entryParams.height, + }, timestamp: generatedImage.createdAt, r2Key: generatedImage.r2Key, sizeBytes: generatedImage.sizeBytes, diff --git a/hooks/use-generation-settings.test.ts b/hooks/use-generation-settings.test.ts index 82659ab..58d3fae 100644 --- a/hooks/use-generation-settings.test.ts +++ b/hooks/use-generation-settings.test.ts @@ -176,6 +176,19 @@ describe("useGenerationSettings", () => { expect(result.current.model).toBe("flux-realism") }) + it("applies exact fixed dimensions for dirtberry model", () => { + const { result } = renderHook(() => useGenerationSettings()) + + act(() => { + result.current.handleModelChange("dirtberry") + }) + + expect(result.current.model).toBe("dirtberry") + expect(result.current.aspectRatio).toBe("9:16") + expect(result.current.width).toBe(832) + expect(result.current.height).toBe(1144) + }) + it("provides aspectRatios based on model", () => { const { result } = renderHook(() => useGenerationSettings()) diff --git a/hooks/use-generation-settings.ts b/hooks/use-generation-settings.ts index 1763b42..5da817a 100644 --- a/hooks/use-generation-settings.ts +++ b/hooks/use-generation-settings.ts @@ -306,10 +306,17 @@ export function useGenerationSettings(): UseGenerationSettingsReturn { // For fixed-size models (dimensionsEnabled: false), use standard dimensions if (!newConstraints.dimensionsEnabled && ratios.length > 0) { const firstRatio = ratios[0] - // Use standard dimensions for this ratio at the tier - const standardDims = getStandardDimensionsWithFallback(firstRatio.value, tierToUse) - setWidth(standardDims.width) - setHeight(standardDims.height) + // Single-tier fixed models define exact output dimensions in the model preset. + // Multi-tier fixed models (e.g., video) still use tier-based standard dimensions. + const supportedTierCount = newConstraints.supportedTiers?.length ?? 1 + if (supportedTierCount === 1) { + setWidth(firstRatio.width) + setHeight(firstRatio.height) + } else { + const standardDims = getStandardDimensionsWithFallback(firstRatio.value, tierToUse) + setWidth(standardDims.width) + setHeight(standardDims.height) + } setAspectRatio(firstRatio.value) } else { // For other models, try to preserve aspect ratio with standard dimensions diff --git a/lib/config/models.test.ts b/lib/config/models.test.ts index 52a90e8..3f424f0 100644 --- a/lib/config/models.test.ts +++ b/lib/config/models.test.ts @@ -587,13 +587,13 @@ describe("Model Constraints", () => { }) describe("Dirtberry", () => { - it("should have fixed dimensions (DALL-E 3 standard)", () => { + it("should have fixed portrait-only dimensions", () => { const model = getModel("dirtberry")! expect(model.constraints.dimensionsEnabled).toBe(false) expect(model.constraints.outputCertainty).toBe("exact") - expect(model.constraints.minDimension).toBe(1024) - expect(model.constraints.maxDimension).toBe(1792) - expect(model.constraints.defaultDimensions).toEqual({ width: 1024, height: 1024 }) + expect(model.constraints.minDimension).toBe(832) + expect(model.constraints.maxDimension).toBe(1144) + expect(model.constraints.defaultDimensions).toEqual({ width: 832, height: 1144 }) }) it("should not support negative prompts or reference images", () => { @@ -795,31 +795,14 @@ describe("Aspect Ratio Presets", () => { expect(imagenRatios).toEqual(grokRatios) }) - it("should have Dirtberry limited to 3 presets (no custom) with DALL-E 3 standard dimensions", () => { + it("should have Dirtberry limited to a single portrait preset", () => { const ratios = getModelAspectRatios("dirtberry")! - expect(ratios.length).toBe(3) + expect(ratios.length).toBe(1) expect(ratios.every(r => r.value !== "custom")).toBe(true) - const square = ratios.find(r => r.value === "1:1") - expect(square?.width).toBe(1024) - expect(square?.height).toBe(1024) - - const landscape = ratios.find(r => r.value === "16:9") - expect(landscape?.width).toBe(1792) - expect(landscape?.height).toBe(1024) - const portrait = ratios.find(r => r.value === "9:16") - expect(portrait?.width).toBe(1024) - expect(portrait?.height).toBe(1792) - }) - - it("should have Dirtberry share identical aspect ratios with Imagen 4 and Grok Imagine", () => { - const imagenRatios = getModelAspectRatios("imagen-4")! - const dirtberryRatios = getModelAspectRatios("dirtberry")! - const grokRatios = getModelAspectRatios("grok-imagine")! - - expect(dirtberryRatios).toEqual(imagenRatios) - expect(dirtberryRatios).toEqual(grokRatios) + expect(portrait?.width).toBe(832) + expect(portrait?.height).toBe(1144) }) }) diff --git a/lib/config/models.ts b/lib/config/models.ts index 7751c3c..fd58cdb 100644 --- a/lib/config/models.ts +++ b/lib/config/models.ts @@ -200,6 +200,13 @@ const DALLE3_STANDARD_ASPECT_RATIOS: readonly AspectRatioOption[] = ( ] as const ).map(withAspectRatioTags); +/** Dirtberry fixed portrait-only preset */ +const DIRTBERRY_ASPECT_RATIOS: readonly AspectRatioOption[] = ( + [ + { label: "Portrait", value: "9:16", width: 832, height: 1144, icon: "rectangle-vertical", category: "portrait" }, + ] as const +).map(withAspectRatioTags); + /** GPT 1.5 uses the same DALL-E 3 standard aspect ratios (1024², 1792×1024, 1024×1792) */ const GPTIMAGE_LARGE_ASPECT_RATIOS = DALLE3_STANDARD_ASPECT_RATIOS; @@ -533,10 +540,10 @@ export const MODEL_REGISTRY: Record = { constraints: { maxPixels: Infinity, minPixels: 0, - minDimension: 1024, - maxDimension: 1792, + minDimension: 832, + maxDimension: 1144, step: 1, - defaultDimensions: { width: 1024, height: 1024 }, + defaultDimensions: { width: 832, height: 1144 }, dimensionsEnabled: false, // Seed not supported: api.airforce buildRequestBody only sends model, prompt, n, size. // See: image.pollinations.ai/src/models/airforceModel.ts @@ -545,7 +552,7 @@ export const MODEL_REGISTRY: Record = { outputCertainty: "exact", dimensionWarning: "Dimensions are fixed for this model", }, - aspectRatios: DALLE3_STANDARD_ASPECT_RATIOS, + aspectRatios: DIRTBERRY_ASPECT_RATIOS, supportsNegativePrompt: false, supportsReferenceImage: false, // Dirtberry is kept safe-only in Bloom Studio (no NSFW/unrestricted flag) From db11a87d645b18e9caa26b1ebe6b2db80397e77b Mon Sep 17 00:00:00 2001 From: Simplereally Date: Wed, 11 Mar 2026 22:38:04 +1100 Subject: [PATCH 3/4] fix: update Dirtberry logo path in model configuration --- lib/config/models.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/config/models.ts b/lib/config/models.ts index fd58cdb..edfdc4c 100644 --- a/lib/config/models.ts +++ b/lib/config/models.ts @@ -535,7 +535,7 @@ export const MODEL_REGISTRY: Record = { displayName: "Dirtberry", type: "image", icon: "sparkles", - logo: "/image-models/flux.svg", + logo: "/icon.png", description: "Quick realistic image generation via api.airforce", constraints: { maxPixels: Infinity, From 12057ddb1b38c6247b1288d7e29093298276bc24 Mon Sep 17 00:00:00 2001 From: Simplereally Date: Wed, 11 Mar 2026 22:43:39 +1100 Subject: [PATCH 4/4] chore: remove dirtberry research and test artifacts Delete temporary Dirtberry research/test files and revert incidental test-only changes so the branch only contains implementation code. Made-with: Cursor --- DIRTBERRY-CROPPING.md | 48 ------------ convex/lib/dirtberryCrop.test.ts | 73 ------------------- hooks/use-generation-settings.test.ts | 13 ---- lib/config/models.test.ts | 53 +------------- .../pollinations-pricing.schema.test.ts | 51 ------------- 5 files changed, 2 insertions(+), 236 deletions(-) delete mode 100644 DIRTBERRY-CROPPING.md delete mode 100644 convex/lib/dirtberryCrop.test.ts diff --git a/DIRTBERRY-CROPPING.md b/DIRTBERRY-CROPPING.md deleted file mode 100644 index 4e10a9a..0000000 --- a/DIRTBERRY-CROPPING.md +++ /dev/null @@ -1,48 +0,0 @@ -# Dirtberry Cropping Implementation Notes - -## Goal - -- Make `dirtberry` portrait-only and show **post-crop dimensions** in Studio (`832x1144`). -- Remove Dirtberry corner marks by cropping **before** persistence. -- Keep downstream rendering/export simple by storing a pre-cropped source. - -## Final Approach (Server-Side, Pre-Upload) - -Cropping is applied in Convex after Pollinations returns image bytes and before R2 upload: - -1. Fetch Pollinations image bytes in `convex/singleGenerationProcessor.ts`. -2. If `model` resolves to Dirtberry: - - Crop 3% from top and 3% from bottom using `convex/lib/dirtberryCrop.ts`. -3. Upload the cropped buffer to R2. -4. Persist cropped dimensions in `generatedImages` (and Dirtberry generation params). - -Result: Gallery, canvas, lightbox, and downloads all use the same already-cropped asset. - -## Crop Math - -- `TRIM_FRACTION = 0.03` -- `trimPixels = round(H * 0.03)` -- `croppedHeight = H - (2 * trimPixels)` -- For source `H = 1216`: `trimPixels = 36`, `croppedHeight = 1144` - -## Required Code Touchpoints - -- `lib/config/models.ts` - - Dirtberry aspect ratios reduced to one preset (`9:16`, `832x1144` display dimensions) - - Dirtberry default dimensions updated to `832x1144` -- `hooks/use-generation-settings.ts` - - Fixed single-tier models use exact preset dimensions on model switch -- `convex/lib/dirtberryCrop.ts` - - Crop-region math + buffer crop utility -- `convex/singleGenerationProcessor.ts` - - Force Dirtberry upstream request dimensions to source size (`832x1216`) - - Wire Dirtberry crop before `uploadMediaWithThumbnail(...)` -- `convex/batchProcessor.ts` - - Apply the same Dirtberry source-dimension + pre-upload crop logic for batch jobs - -## Validation Checklist - -1. Select `dirtberry` -> one aspect option with displayed dimensions `832x1144`. -2. Generate Dirtberry image -> no visible corner watermark in Studio surfaces. -3. Download image -> dimensions reflect cropped output (`832x1144`). -4. Generate non-Dirtberry model -> no behavior change. diff --git a/convex/lib/dirtberryCrop.test.ts b/convex/lib/dirtberryCrop.test.ts deleted file mode 100644 index cd29f64..0000000 --- a/convex/lib/dirtberryCrop.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import sharp from "sharp" -import { describe, expect, it } from "vitest" -import { - DIRTBERRY_OUTPUT_HEIGHT, - DIRTBERRY_SOURCE_HEIGHT, - DIRTBERRY_SOURCE_WIDTH, - DIRTBERRY_TRIM_FRACTION, - calculateDirtberryCropRegion, - cropDirtberryImageBuffer, -} from "./dirtberryCrop" - -describe("calculateDirtberryCropRegion", () => { - it("calculates 3% top/bottom crop for source height", () => { - const region = calculateDirtberryCropRegion(DIRTBERRY_SOURCE_HEIGHT) - expect(region).toEqual({ - top: Math.round(DIRTBERRY_SOURCE_HEIGHT * DIRTBERRY_TRIM_FRACTION), - height: DIRTBERRY_OUTPUT_HEIGHT, - }) - }) - - it("returns null when crop would be invalid", () => { - expect(calculateDirtberryCropRegion(0)).toBeNull() - expect(calculateDirtberryCropRegion(1)).toBeNull() - expect(calculateDirtberryCropRegion(10)).toBeNull() - }) -}) - -describe("cropDirtberryImageBuffer", () => { - it("crops an image buffer to remove top/bottom strips", async () => { - const input = await sharp({ - create: { - width: DIRTBERRY_SOURCE_WIDTH, - height: DIRTBERRY_SOURCE_HEIGHT, - channels: 3, - background: { r: 255, g: 0, b: 0 }, - }, - }) - .jpeg() - .toBuffer() - - const result = await cropDirtberryImageBuffer(input) - expect(result.wasCropped).toBe(true) - expect(result.width).toBe(DIRTBERRY_SOURCE_WIDTH) - expect(result.height).toBe(DIRTBERRY_OUTPUT_HEIGHT) - expect(result.inputWidth).toBe(DIRTBERRY_SOURCE_WIDTH) - expect(result.inputHeight).toBe(DIRTBERRY_SOURCE_HEIGHT) - expect(result.trimPixels).toBe(Math.round(DIRTBERRY_SOURCE_HEIGHT * DIRTBERRY_TRIM_FRACTION)) - - const metadata = await sharp(result.buffer).metadata() - expect(metadata.width).toBe(DIRTBERRY_SOURCE_WIDTH) - expect(metadata.height).toBe(DIRTBERRY_OUTPUT_HEIGHT) - }) - - it("still crops when upstream returns shorter-than-expected heights", async () => { - const input = await sharp({ - create: { - width: DIRTBERRY_SOURCE_WIDTH, - height: DIRTBERRY_OUTPUT_HEIGHT, - channels: 3, - background: { r: 0, g: 255, b: 0 }, - }, - }) - .jpeg() - .toBuffer() - - const result = await cropDirtberryImageBuffer(input) - const expectedTrim = Math.round(DIRTBERRY_OUTPUT_HEIGHT * DIRTBERRY_TRIM_FRACTION) - expect(result.wasCropped).toBe(true) - expect(result.width).toBe(DIRTBERRY_SOURCE_WIDTH) - expect(result.height).toBe(DIRTBERRY_OUTPUT_HEIGHT - expectedTrim * 2) - expect(result.trimPixels).toBe(expectedTrim) - }) -}) diff --git a/hooks/use-generation-settings.test.ts b/hooks/use-generation-settings.test.ts index 58d3fae..82659ab 100644 --- a/hooks/use-generation-settings.test.ts +++ b/hooks/use-generation-settings.test.ts @@ -176,19 +176,6 @@ describe("useGenerationSettings", () => { expect(result.current.model).toBe("flux-realism") }) - it("applies exact fixed dimensions for dirtberry model", () => { - const { result } = renderHook(() => useGenerationSettings()) - - act(() => { - result.current.handleModelChange("dirtberry") - }) - - expect(result.current.model).toBe("dirtberry") - expect(result.current.aspectRatio).toBe("9:16") - expect(result.current.width).toBe(832) - expect(result.current.height).toBe(1144) - }) - it("provides aspectRatios based on model", () => { const { result } = renderHook(() => useGenerationSettings()) diff --git a/lib/config/models.test.ts b/lib/config/models.test.ts index 3f424f0..7a190f0 100644 --- a/lib/config/models.test.ts +++ b/lib/config/models.test.ts @@ -38,7 +38,6 @@ describe("Model Registry", () => { "imagen-4", "grok-imagine", "flux-2-dev", - "dirtberry", ] for (const modelId of expectedImageModels) { @@ -77,7 +76,6 @@ describe("Model Registry", () => { expect(MODEL_REGISTRY["grok-imagine"].displayName).toBe("Grok Imagine") expect(MODEL_REGISTRY["grok-video"].displayName).toBe("Grok Video") expect(MODEL_REGISTRY["flux-2-dev"].displayName).toBe("FLUX.2 Dev") - expect(MODEL_REGISTRY["dirtberry"].displayName).toBe("Dirtberry") }) it("should have valid constraints for all models", () => { @@ -189,11 +187,11 @@ describe("Model Registry", () => { describe("Model Lists", () => { it("should have all model IDs", () => { - expect(ALL_MODEL_IDS.length).toBe(21) + expect(ALL_MODEL_IDS.length).toBe(20) }) it("should have correct image model IDs", () => { - expect(IMAGE_MODEL_IDS.length).toBe(16) + expect(IMAGE_MODEL_IDS.length).toBe(15) expect(IMAGE_MODEL_IDS).toContain("zimage") expect(IMAGE_MODEL_IDS).toContain("gptimage") expect(IMAGE_MODEL_IDS).toContain("flux") @@ -201,7 +199,6 @@ describe("Model Registry", () => { expect(IMAGE_MODEL_IDS).toContain("imagen-4") expect(IMAGE_MODEL_IDS).toContain("grok-imagine") expect(IMAGE_MODEL_IDS).toContain("flux-2-dev") - expect(IMAGE_MODEL_IDS).toContain("dirtberry") expect(IMAGE_MODEL_IDS).not.toContain("veo") }) @@ -227,12 +224,6 @@ describe("Model Registry", () => { it("should include flux-2-dev in UNRESTRICTED_MODEL_IDS (isUnrestricted: true)", () => { expect(UNRESTRICTED_MODEL_IDS.has("flux-2-dev")).toBe(true) }) - - it("should include dirtberry in ACTIVE_IMAGE_MODEL_IDS and not in LEGACY_MODEL_IDS or UNRESTRICTED_MODEL_IDS", () => { - expect(ACTIVE_IMAGE_MODEL_IDS).toContain("dirtberry") - expect(LEGACY_MODEL_IDS).not.toContain("dirtberry") - expect(UNRESTRICTED_MODEL_IDS.has("dirtberry")).toBe(false) - }) }) }) @@ -585,36 +576,6 @@ describe("Model Constraints", () => { expect(model.constraints.supportedTiers).toEqual(["hd"]) }) }) - - describe("Dirtberry", () => { - it("should have fixed portrait-only dimensions", () => { - const model = getModel("dirtberry")! - expect(model.constraints.dimensionsEnabled).toBe(false) - expect(model.constraints.outputCertainty).toBe("exact") - expect(model.constraints.minDimension).toBe(832) - expect(model.constraints.maxDimension).toBe(1144) - expect(model.constraints.defaultDimensions).toEqual({ width: 832, height: 1144 }) - }) - - it("should not support negative prompts or reference images", () => { - const model = getModel("dirtberry")! - expect(model.supportsNegativePrompt).toBe(false) - expect(model.supportsReferenceImage).toBe(false) - }) - - it("should have correct provider metadata", () => { - const model = getModel("dirtberry")! - expect(model.icon).toBe("sparkles") - expect(model.logo).toBe("/image-models/flux.svg") - expect(model.isLegacy).toBeUndefined() - expect(model.isUnrestricted).toBeUndefined() - }) - - it("should only support HD tier", () => { - const model = getModel("dirtberry")! - expect(model.constraints.supportedTiers).toEqual(["hd"]) - }) - }) }) describe("Aspect Ratio Presets", () => { @@ -794,16 +755,6 @@ describe("Aspect Ratio Presets", () => { const grokRatios = getModelAspectRatios("grok-imagine")! expect(imagenRatios).toEqual(grokRatios) }) - - it("should have Dirtberry limited to a single portrait preset", () => { - const ratios = getModelAspectRatios("dirtberry")! - expect(ratios.length).toBe(1) - expect(ratios.every(r => r.value !== "custom")).toBe(true) - - const portrait = ratios.find(r => r.value === "9:16") - expect(portrait?.width).toBe(832) - expect(portrait?.height).toBe(1144) - }) }) describe("Video Model Properties", () => { diff --git a/lib/schemas/pollinations-pricing.schema.test.ts b/lib/schemas/pollinations-pricing.schema.test.ts index 4d8c2b0..b1f446f 100644 --- a/lib/schemas/pollinations-pricing.schema.test.ts +++ b/lib/schemas/pollinations-pricing.schema.test.ts @@ -27,14 +27,6 @@ describe("pollinations-pricing.schema", () => { expect(pricing?.approximatePerPollen).toBe(1000); }); - it("should return pricing for dirtberry", () => { - const pricing = getModelPricing("dirtberry"); - expect(pricing).toBeDefined(); - expect(pricing?.type).toBe("image"); - expect(pricing?.modelId).toBe("dirtberry"); - expect(pricing?.approximatePerPollen).toBe(1000); - }); - it("should return pricing for a valid video model", () => { const pricing = getModelPricing("veo"); expect(pricing).toBeDefined(); @@ -76,14 +68,6 @@ describe("pollinations-pricing.schema", () => { expect(cost).toBe(30.0 / 1_000_000); }); - it("should calculate cost for dirtberry at $0.001/image", () => { - const cost1 = estimateImageCost("dirtberry", 1); - expect(cost1).toBe(0.001); - - const cost10 = estimateImageCost("dirtberry", 10); - expect(cost10).toBe(0.001 * 10); - }); - it("should return undefined for video models", () => { const cost = estimateImageCost("veo"); expect(cost).toBeUndefined(); @@ -157,7 +141,6 @@ describe("pollinations-pricing.schema", () => { it("should return false for models that do not support reference image", () => { expect(modelSupportsReferenceImage("flux")).toBe(false); expect(modelSupportsReferenceImage("flux-2-dev")).toBe(false); - expect(modelSupportsReferenceImage("dirtberry")).toBe(false); }); it("should return false for unknown models", () => { @@ -169,7 +152,6 @@ describe("pollinations-pricing.schema", () => { it("should return true for alpha models", () => { expect(isModelAlpha("veo")).toBe(true); expect(isModelAlpha("grok-video")).toBe(true); - expect(isModelAlpha("dirtberry")).toBe(true); }); it("should return false for stable models", () => { @@ -216,36 +198,3 @@ describe("FLUX.2 Dev pricing specifics", () => { expect(pricing.tokenPricing).toBeUndefined(); }); }); - -describe("Dirtberry pricing specifics", () => { - it("should exist in IMAGE_MODEL_PRICING", () => { - expect(IMAGE_MODEL_PRICING["dirtberry"]).toBeDefined(); - }); - - it("should have per-image pricing at $0.001", () => { - const pricing = IMAGE_MODEL_PRICING["dirtberry"]; - expect(pricing.imagePricing?.perImage).toBe(0.001); - }); - - it("should have approximatePerPollen of 1000 (consistent with perImage)", () => { - const pricing = IMAGE_MODEL_PRICING["dirtberry"]; - expect(pricing.approximatePerPollen).toBe(1000); - const expectedEfficiency = 1 / pricing.imagePricing!.perImage; - expect(pricing.approximatePerPollen).toBe(expectedEfficiency); - }); - - it("should not support reference image", () => { - const pricing = IMAGE_MODEL_PRICING["dirtberry"]; - expect(pricing.supportsReferenceImage).toBe(false); - }); - - it("should be alpha", () => { - const pricing = IMAGE_MODEL_PRICING["dirtberry"]; - expect(pricing.isAlpha).toBe(true); - }); - - it("should not have token pricing (uses simple per-image pricing)", () => { - const pricing = IMAGE_MODEL_PRICING["dirtberry"]; - expect(pricing.tokenPricing).toBeUndefined(); - }); -});