diff --git a/apps/api/src/routes/tools/image-enhancement.ts b/apps/api/src/routes/tools/image-enhancement.ts index ca5e6135..47938851 100644 --- a/apps/api/src/routes/tools/image-enhancement.ts +++ b/apps/api/src/routes/tools/image-enhancement.ts @@ -1,12 +1,18 @@ +import { randomUUID } from "node:crypto"; +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import { noiseRemoval } from "@snapotter/ai"; import { analyzeImage, applyCorrections } from "@snapotter/image-engine"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import sharp from "sharp"; import { z } from "zod"; import { autoOrient } from "../../lib/auto-orient.js"; +import { isToolInstalled } from "../../lib/feature-status.js"; import { validateImageBuffer } from "../../lib/file-validation.js"; import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js"; import { decodeHeic } from "../../lib/heic-converter.js"; import { resolveOutputFormat } from "../../lib/output-format.js"; +import { createWorkspace } from "../../lib/workspace.js"; import { createToolRoute } from "../tool-factory.js"; const settingsSchema = z.object({ @@ -22,6 +28,7 @@ const settingsSchema = z.object({ denoise: z.boolean().default(true), }) .default({}), + deepEnhance: z.boolean().default(false), }); type EnhancementSettings = z.infer; @@ -45,10 +52,28 @@ async function processImageEnhancement( { width: meta.width ?? 1, height: meta.height ?? 1 }, ); - const buffer = await image + let buffer = await image .toFormat(outputFormat.format, { quality: outputFormat.quality }) .toBuffer(); + if (settings.deepEnhance && isToolInstalled("noise-removal")) { + try { + const jobId = randomUUID(); + const workspacePath = await createWorkspace(jobId); + const outputDir = join(workspacePath, "output"); + await mkdir(outputDir, { recursive: true }); + const result = await noiseRemoval(buffer, outputDir, { + tier: "quality", + strength: 35, + detailPreservation: 70, + colorNoise: 20, + }); + buffer = result.buffer; + } catch { + // SCUNet unavailable -- fall back to Sharp-only result + } + } + return { buffer, filename, contentType: outputFormat.contentType }; } diff --git a/tests/integration/image-enhancement.test.ts b/tests/integration/image-enhancement.test.ts index ca2c3a07..1611ae23 100644 --- a/tests/integration/image-enhancement.test.ts +++ b/tests/integration/image-enhancement.test.ts @@ -900,3 +900,21 @@ describe("Large file with modes", () => { expect(result.processedSize).toBeGreaterThan(0); }); }); + +// ── Deep Enhance ─────────────────────────────────────────────── +describe("Deep Enhance", () => { + it("accepts deepEnhance setting and returns 200", async () => { + const res = await postTool({ deepEnhance: true }); + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + expect(result.processedSize).toBeGreaterThan(0); + }); + + it("works without deepEnhance (default false)", async () => { + const res = await postTool({}); + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + }); +});