mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(noise-removal): SOTA noise removal with 4 quality tiers (#57)
* feat(noise-removal): register tool in shared constants and i18n * feat(noise-removal): add SCUNet and NAFNet model architectures * feat(noise-removal): add Python denoising engine with 4 quality tiers * feat(noise-removal): add TypeScript bridge for Python sidecar * feat(noise-removal): add frontend settings with 4-tier selector * feat(noise-removal): register in tool registry and pipeline * feat(noise-removal): add Fastify API route with Zod validation * feat(noise-removal): add SCUNet and NAFNet model downloads to Docker build * test(noise-removal): add to e2e tool page rendering tests * test(noise-removal): add integration tests for API endpoint * style: fix biome formatting and import ordering * fix(noise-removal): use correct model download URLs NAFNet model is hosted on HuggingFace, not GitHub releases. Also align SCUNet URL to use the KAIR releases (same as Docker build). * fix(noise-removal): remove emojis from tier selector, simplify labels Drop emoji icons from Quick/Balanced/Quality/Maximum buttons. Replace technical algorithm names with plain descriptions users can understand. --------- Co-authored-by: stirling-image <stirling-image@users.noreply.github.com>
This commit is contained in:
co-authored by
stirling-image
parent
61794dca2d
commit
dfffc0a8cc
@@ -40,6 +40,7 @@ const TOOLS_WITH_DROPZONE = [
|
||||
{ id: "svg-to-raster", name: "SVG to Raster" },
|
||||
{ id: "vectorize", name: "Image to SVG" },
|
||||
{ id: "gif-tools", name: "GIF" },
|
||||
{ id: "noise-removal", name: "Noise Removal" },
|
||||
];
|
||||
|
||||
const TOOLS_WITHOUT_DROPZONE = [{ id: "qr-generate", name: "QR Code" }];
|
||||
|
||||
@@ -2720,6 +2720,7 @@ describe("Pipeline", () => {
|
||||
expect(toolIds).toContain("remove-background");
|
||||
expect(toolIds).toContain("upscale");
|
||||
expect(toolIds).toContain("blur-faces");
|
||||
expect(toolIds).toContain("noise-removal");
|
||||
});
|
||||
|
||||
it("excludes tools that are not pipeline-compatible", async () => {
|
||||
@@ -4056,3 +4057,112 @@ describe("Image Enhancement", () => {
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// NOISE REMOVAL
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
describe("Noise Removal", () => {
|
||||
it("POST /api/v1/tools/noise-removal processes with quick tier", async () => {
|
||||
const { body: payload, contentType } = createMultipartPayload([
|
||||
{ name: "file", filename: "noisy.png", contentType: "image/png", content: PNG_200x150 },
|
||||
{
|
||||
name: "settings",
|
||||
content: JSON.stringify({ tier: "quick", strength: 50 }),
|
||||
},
|
||||
]);
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/noise-removal",
|
||||
headers: {
|
||||
authorization: `Bearer ${adminToken}`,
|
||||
"content-type": contentType,
|
||||
},
|
||||
payload,
|
||||
});
|
||||
// Either succeeds or Python sidecar unavailable in test env
|
||||
expect(res.statusCode).not.toBe(400);
|
||||
expect([200, 422]).toContain(res.statusCode);
|
||||
if (res.statusCode === 200) {
|
||||
expect(res.headers["content-type"]).toMatch(/^image\//);
|
||||
expect(res.headers["content-disposition"]).toMatch(/attachment/);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns 400 when no file is provided", async () => {
|
||||
const { body: payload, contentType } = createMultipartPayload([
|
||||
{
|
||||
name: "settings",
|
||||
content: JSON.stringify({ tier: "quick", strength: 50 }),
|
||||
},
|
||||
]);
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/noise-removal",
|
||||
headers: {
|
||||
authorization: `Bearer ${adminToken}`,
|
||||
"content-type": contentType,
|
||||
},
|
||||
payload,
|
||||
});
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 400 for empty file", async () => {
|
||||
const { body: payload, contentType } = createMultipartPayload([
|
||||
{ name: "file", filename: "empty.png", contentType: "image/png", content: Buffer.alloc(0) },
|
||||
{
|
||||
name: "settings",
|
||||
content: JSON.stringify({ tier: "quick" }),
|
||||
},
|
||||
]);
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/noise-removal",
|
||||
headers: {
|
||||
authorization: `Bearer ${adminToken}`,
|
||||
"content-type": contentType,
|
||||
},
|
||||
payload,
|
||||
});
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it("accepts JPEG input", async () => {
|
||||
const { body: payload, contentType } = createMultipartPayload([
|
||||
{ name: "file", filename: "noisy.jpg", contentType: "image/jpeg", content: JPG_100x100 },
|
||||
{
|
||||
name: "settings",
|
||||
content: JSON.stringify({ tier: "quick", strength: 30 }),
|
||||
},
|
||||
]);
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/noise-removal",
|
||||
headers: {
|
||||
authorization: `Bearer ${adminToken}`,
|
||||
"content-type": contentType,
|
||||
},
|
||||
payload,
|
||||
});
|
||||
expect(res.statusCode).not.toBe(400);
|
||||
expect([200, 422]).toContain(res.statusCode);
|
||||
});
|
||||
|
||||
it("uses default settings when none provided", async () => {
|
||||
const { body: payload, contentType } = createMultipartPayload([
|
||||
{ name: "file", filename: "noisy.png", contentType: "image/png", content: PNG_200x150 },
|
||||
]);
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/noise-removal",
|
||||
headers: {
|
||||
authorization: `Bearer ${adminToken}`,
|
||||
"content-type": contentType,
|
||||
},
|
||||
payload,
|
||||
});
|
||||
// Defaults should be accepted without validation errors
|
||||
expect(res.statusCode).not.toBe(400);
|
||||
expect([200, 422]).toContain(res.statusCode);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user