mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Merge branch 'feat/remove-bg-improvements'
This commit is contained in:
@@ -1,5 +1,26 @@
|
|||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
|
|
||||||
|
export type BgOutputFormat = "png" | "webp" | "avif";
|
||||||
|
|
||||||
|
export const BG_OUTPUT_FORMATS: BgOutputFormat[] = ["png", "webp", "avif"];
|
||||||
|
|
||||||
|
export const BG_FORMAT_CONTENT_TYPES: Record<BgOutputFormat, string> = {
|
||||||
|
png: "image/png",
|
||||||
|
webp: "image/webp",
|
||||||
|
avif: "image/avif",
|
||||||
|
};
|
||||||
|
|
||||||
|
function toOutputFormat(pipeline: sharp.Sharp, format: BgOutputFormat): Promise<Buffer> {
|
||||||
|
switch (format) {
|
||||||
|
case "webp":
|
||||||
|
return pipeline.webp({ lossless: true }).toBuffer();
|
||||||
|
case "avif":
|
||||||
|
return pipeline.avif({ lossless: true }).toBuffer();
|
||||||
|
default:
|
||||||
|
return pipeline.png().toBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Background removal post-processing effects.
|
* Background removal post-processing effects.
|
||||||
* All effects use Sharp (libvips) for fast server-side image manipulation.
|
* All effects use Sharp (libvips) for fast server-side image manipulation.
|
||||||
@@ -182,6 +203,7 @@ export async function applyEffects(
|
|||||||
blurIntensity?: number;
|
blurIntensity?: number;
|
||||||
shadowEnabled?: boolean;
|
shadowEnabled?: boolean;
|
||||||
shadowOpacity?: number;
|
shadowOpacity?: number;
|
||||||
|
outputFormat?: BgOutputFormat;
|
||||||
},
|
},
|
||||||
): Promise<Buffer> {
|
): Promise<Buffer> {
|
||||||
const meta = await sharp(subjectBuffer).metadata();
|
const meta = await sharp(subjectBuffer).metadata();
|
||||||
@@ -238,13 +260,12 @@ export async function applyEffects(
|
|||||||
}
|
}
|
||||||
// else: transparent - no background layer
|
// else: transparent - no background layer
|
||||||
|
|
||||||
|
const fmt = settings.outputFormat ?? "png";
|
||||||
|
|
||||||
// Step 3: Composite subject onto background
|
// Step 3: Composite subject onto background
|
||||||
if (background) {
|
if (background) {
|
||||||
return sharp(background)
|
return toOutputFormat(sharp(background).composite([{ input: subject, blend: "over" }]), fmt);
|
||||||
.composite([{ input: subject, blend: "over" }])
|
|
||||||
.png()
|
|
||||||
.toBuffer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return subject;
|
return toOutputFormat(sharp(subject), fmt);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import { getBundleForTool, TOOL_BUNDLE_MAP } from "@snapotter/shared";
|
|||||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { autoOrient } from "../../lib/auto-orient.js";
|
import { autoOrient } from "../../lib/auto-orient.js";
|
||||||
import { applyEffects } from "../../lib/bg-effects.js";
|
import {
|
||||||
|
applyEffects,
|
||||||
|
BG_FORMAT_CONTENT_TYPES,
|
||||||
|
type BgOutputFormat,
|
||||||
|
} from "../../lib/bg-effects.js";
|
||||||
import { formatZodErrors } from "../../lib/errors.js";
|
import { formatZodErrors } from "../../lib/errors.js";
|
||||||
import { isToolInstalled } from "../../lib/feature-status.js";
|
import { isToolInstalled } from "../../lib/feature-status.js";
|
||||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||||
@@ -28,6 +32,9 @@ const settingsSchema = z.object({
|
|||||||
blurIntensity: z.number().min(0).max(100).optional(),
|
blurIntensity: z.number().min(0).max(100).optional(),
|
||||||
shadowEnabled: z.boolean().optional(),
|
shadowEnabled: z.boolean().optional(),
|
||||||
shadowOpacity: z.number().min(0).max(100).optional(),
|
shadowOpacity: z.number().min(0).max(100).optional(),
|
||||||
|
outputFormat: z.enum(["png", "webp", "avif"]).optional(),
|
||||||
|
edgeRefine: z.number().int().min(0).max(3).optional(),
|
||||||
|
decontaminate: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -176,7 +183,11 @@ export function registerRemoveBackground(app: FastifyInstance) {
|
|||||||
const transparentResult = await removeBackground(
|
const transparentResult = await removeBackground(
|
||||||
fileBuffer,
|
fileBuffer,
|
||||||
join(workspacePath, "output"),
|
join(workspacePath, "output"),
|
||||||
{ model: settings.model },
|
{
|
||||||
|
model: settings.model,
|
||||||
|
edgeRefine: settings.edgeRefine,
|
||||||
|
decontaminate: settings.decontaminate,
|
||||||
|
},
|
||||||
onProgress,
|
onProgress,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -265,6 +276,7 @@ export function registerRemoveBackground(app: FastifyInstance) {
|
|||||||
blurIntensity: z.number().min(0).max(100).optional(),
|
blurIntensity: z.number().min(0).max(100).optional(),
|
||||||
shadowEnabled: z.boolean().optional(),
|
shadowEnabled: z.boolean().optional(),
|
||||||
shadowOpacity: z.number().min(0).max(100).optional(),
|
shadowOpacity: z.number().min(0).max(100).optional(),
|
||||||
|
outputFormat: z.enum(["png", "webp", "avif"]).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -308,6 +320,7 @@ export function registerRemoveBackground(app: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply effects using cached mask + original
|
// Apply effects using cached mask + original
|
||||||
|
const fmt = (settings.outputFormat ?? "png") as BgOutputFormat;
|
||||||
const resultBuffer = await applyEffects(maskBuffer, originalBuffer, {
|
const resultBuffer = await applyEffects(maskBuffer, originalBuffer, {
|
||||||
backgroundType: settings.backgroundType,
|
backgroundType: settings.backgroundType,
|
||||||
backgroundColor: settings.backgroundColor,
|
backgroundColor: settings.backgroundColor,
|
||||||
@@ -319,10 +332,11 @@ export function registerRemoveBackground(app: FastifyInstance) {
|
|||||||
blurIntensity: settings.blurIntensity,
|
blurIntensity: settings.blurIntensity,
|
||||||
shadowEnabled: settings.shadowEnabled,
|
shadowEnabled: settings.shadowEnabled,
|
||||||
shadowOpacity: settings.shadowOpacity,
|
shadowOpacity: settings.shadowOpacity,
|
||||||
|
outputFormat: fmt,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save the final output
|
// Save the final output
|
||||||
const outputFilename = `${baseName}_nobg.png`;
|
const outputFilename = `${baseName}_nobg.${fmt}`;
|
||||||
const outputPath = join(workspacePath, "output", outputFilename);
|
const outputPath = join(workspacePath, "output", outputFilename);
|
||||||
await writeFile(outputPath, resultBuffer);
|
await writeFile(outputPath, resultBuffer);
|
||||||
|
|
||||||
@@ -354,9 +368,10 @@ export function registerRemoveBackground(app: FastifyInstance) {
|
|||||||
const transparentResult = await removeBackground(
|
const transparentResult = await removeBackground(
|
||||||
orientedBuffer,
|
orientedBuffer,
|
||||||
join(workspacePath, "output"),
|
join(workspacePath, "output"),
|
||||||
{ model: s.model },
|
{ model: s.model, edgeRefine: s.edgeRefine, decontaminate: s.decontaminate },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fmt = (s.outputFormat ?? "png") as BgOutputFormat;
|
||||||
const resultBuffer = await applyEffects(transparentResult, orientedBuffer, {
|
const resultBuffer = await applyEffects(transparentResult, orientedBuffer, {
|
||||||
backgroundType: s.backgroundType,
|
backgroundType: s.backgroundType,
|
||||||
backgroundColor: s.backgroundColor,
|
backgroundColor: s.backgroundColor,
|
||||||
@@ -367,10 +382,15 @@ export function registerRemoveBackground(app: FastifyInstance) {
|
|||||||
blurIntensity: s.blurIntensity,
|
blurIntensity: s.blurIntensity,
|
||||||
shadowEnabled: s.shadowEnabled,
|
shadowEnabled: s.shadowEnabled,
|
||||||
shadowOpacity: s.shadowOpacity,
|
shadowOpacity: s.shadowOpacity,
|
||||||
|
outputFormat: fmt,
|
||||||
});
|
});
|
||||||
|
|
||||||
const outputFilename = `${filename.replace(/\.[^.]+$/, "")}_nobg.png`;
|
const outputFilename = `${filename.replace(/\.[^.]+$/, "")}_nobg.${fmt}`;
|
||||||
return { buffer: resultBuffer, filename: outputFilename, contentType: "image/png" };
|
return {
|
||||||
|
buffer: resultBuffer,
|
||||||
|
filename: outputFilename,
|
||||||
|
contentType: BG_FORMAT_CONTENT_TYPES[fmt],
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type BackgroundType = "transparent" | "color" | "gradient" | "image";
|
|||||||
type BgModel =
|
type BgModel =
|
||||||
| "birefnet-general"
|
| "birefnet-general"
|
||||||
| "birefnet-general-lite"
|
| "birefnet-general-lite"
|
||||||
|
| "birefnet-hr-matting"
|
||||||
| "birefnet-matting"
|
| "birefnet-matting"
|
||||||
| "birefnet-portrait"
|
| "birefnet-portrait"
|
||||||
| "bria-rmbg"
|
| "bria-rmbg"
|
||||||
@@ -31,8 +32,8 @@ const MODEL_MAP: Record<SubjectType, Partial<Record<Quality, BgModel>>> = {
|
|||||||
people: {
|
people: {
|
||||||
fast: "u2net",
|
fast: "u2net",
|
||||||
balanced: "birefnet-portrait",
|
balanced: "birefnet-portrait",
|
||||||
best: "birefnet-portrait",
|
best: "birefnet-matting",
|
||||||
ultra: "birefnet-matting",
|
ultra: "birefnet-hr-matting",
|
||||||
},
|
},
|
||||||
products: { fast: "u2net", balanced: "bria-rmbg", best: "birefnet-general" },
|
products: { fast: "u2net", balanced: "bria-rmbg", best: "birefnet-general" },
|
||||||
general: { fast: "u2net", balanced: "birefnet-general-lite", best: "birefnet-general" },
|
general: { fast: "u2net", balanced: "birefnet-general-lite", best: "birefnet-general" },
|
||||||
@@ -105,6 +106,13 @@ export function RemoveBgControls({ settings: _settings, onChange }: RemoveBgCont
|
|||||||
const [shadowEnabled, setShadowEnabled] = useState(false);
|
const [shadowEnabled, setShadowEnabled] = useState(false);
|
||||||
const [shadowOpacity, setShadowOpacity] = useState(35);
|
const [shadowOpacity, setShadowOpacity] = useState(35);
|
||||||
|
|
||||||
|
// Post-processing
|
||||||
|
const [edgeRefine, setEdgeRefine] = useState(0);
|
||||||
|
const [decontaminate, setDecontaminate] = useState(false);
|
||||||
|
|
||||||
|
// Output
|
||||||
|
const [outputFormat, setOutputFormat] = useState<"png" | "webp" | "avif">("png");
|
||||||
|
|
||||||
// Expandable sections
|
// Expandable sections
|
||||||
const [effectsOpen, setEffectsOpen] = useState(false);
|
const [effectsOpen, setEffectsOpen] = useState(false);
|
||||||
|
|
||||||
@@ -152,6 +160,10 @@ export function RemoveBgControls({ settings: _settings, onChange }: RemoveBgCont
|
|||||||
next._bgImageFile = bgImageFile;
|
next._bgImageFile = bgImageFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (edgeRefine > 0) next.edgeRefine = edgeRefine;
|
||||||
|
if (decontaminate) next.decontaminate = true;
|
||||||
|
if (outputFormat !== "png") next.outputFormat = outputFormat;
|
||||||
|
|
||||||
onChangeRef.current(next);
|
onChangeRef.current(next);
|
||||||
}, [
|
}, [
|
||||||
model,
|
model,
|
||||||
@@ -165,6 +177,9 @@ export function RemoveBgControls({ settings: _settings, onChange }: RemoveBgCont
|
|||||||
blurIntensity,
|
blurIntensity,
|
||||||
shadowEnabled,
|
shadowEnabled,
|
||||||
shadowOpacity,
|
shadowOpacity,
|
||||||
|
edgeRefine,
|
||||||
|
decontaminate,
|
||||||
|
outputFormat,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -387,6 +402,25 @@ export function RemoveBgControls({ settings: _settings, onChange }: RemoveBgCont
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Output format */}
|
||||||
|
<SectionLabel>{t.toolSettings["remove-background"].outputFormat}</SectionLabel>
|
||||||
|
<div className="grid grid-cols-3 gap-1.5">
|
||||||
|
{(["png", "webp", "avif"] as const).map((fmt) => (
|
||||||
|
<button
|
||||||
|
key={fmt}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOutputFormat(fmt)}
|
||||||
|
className={`py-2 px-2 rounded-lg border text-xs font-medium uppercase transition-colors ${
|
||||||
|
outputFormat === fmt
|
||||||
|
? "border-primary bg-primary/10 text-primary"
|
||||||
|
: "border-border text-muted-foreground hover:border-primary/50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{fmt}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Effects */}
|
{/* Effects */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -395,7 +429,7 @@ export function RemoveBgControls({ settings: _settings, onChange }: RemoveBgCont
|
|||||||
>
|
>
|
||||||
{effectsOpen ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
{effectsOpen ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
||||||
Effects
|
Effects
|
||||||
{(blurEnabled || shadowEnabled) && (
|
{(blurEnabled || shadowEnabled || edgeRefine > 0 || decontaminate) && (
|
||||||
<span className="ms-auto text-primary text-[10px] normal-case font-normal">active</span>
|
<span className="ms-auto text-primary text-[10px] normal-case font-normal">active</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@@ -467,6 +501,48 @@ export function RemoveBgControls({ settings: _settings, onChange }: RemoveBgCont
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Edge smoothing */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{t.toolSettings["remove-background"].edgeSmoothing}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-mono text-foreground tabular-nums">
|
||||||
|
{edgeRefine === 0
|
||||||
|
? t.toolSettings["remove-background"].edgeSmoothingOff
|
||||||
|
: edgeRefine === 1
|
||||||
|
? t.toolSettings["remove-background"].edgeSmoothingLight
|
||||||
|
: edgeRefine === 2
|
||||||
|
? t.toolSettings["remove-background"].edgeSmoothingMedium
|
||||||
|
: t.toolSettings["remove-background"].edgeSmoothingStrong}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={0}
|
||||||
|
max={3}
|
||||||
|
step={1}
|
||||||
|
value={edgeRefine}
|
||||||
|
onChange={(e) => setEdgeRefine(Number(e.target.value))}
|
||||||
|
className="w-full mt-0.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Color decontamination */}
|
||||||
|
<div>
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={decontaminate}
|
||||||
|
onChange={(e) => setDecontaminate(e.target.checked)}
|
||||||
|
className="rounded border-border accent-primary"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{t.toolSettings["remove-background"].colorDecontamination}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -742,6 +818,7 @@ export function RemoveBgSettings({ onBgPreview }: RemoveBgSettingsProps = {}) {
|
|||||||
blurIntensity: settings.blurIntensity,
|
blurIntensity: settings.blurIntensity,
|
||||||
shadowEnabled: settings.shadowEnabled,
|
shadowEnabled: settings.shadowEnabled,
|
||||||
shadowOpacity: settings.shadowOpacity,
|
shadowOpacity: settings.shadowOpacity,
|
||||||
|
outputFormat: settings.outputFormat,
|
||||||
};
|
};
|
||||||
formData.append("settings", JSON.stringify(effectSettings));
|
formData.append("settings", JSON.stringify(effectSettings));
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,77 @@ import os
|
|||||||
|
|
||||||
|
|
||||||
def emit_progress(percent, stage):
|
def emit_progress(percent, stage):
|
||||||
"""Emit structured progress to stderr for bridge.ts to capture."""
|
|
||||||
print(json.dumps({"progress": percent, "stage": stage}), file=sys.stderr, flush=True)
|
print(json.dumps({"progress": percent, "stage": stage}), file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _refine_edges(image_bytes, level):
|
||||||
|
"""Morphological mask refinement to reduce gray halos on edges.
|
||||||
|
|
||||||
|
level: 1=light, 2=medium, 3=strong
|
||||||
|
"""
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
import io
|
||||||
|
|
||||||
|
img = Image.open(io.BytesIO(image_bytes)).convert("RGBA")
|
||||||
|
arr = np.array(img)
|
||||||
|
alpha = arr[:, :, 3]
|
||||||
|
|
||||||
|
kernel_size = 1 + level
|
||||||
|
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
|
||||||
|
alpha = cv2.morphologyEx(alpha, cv2.MORPH_CLOSE, kernel)
|
||||||
|
|
||||||
|
sigma = 0.3 + level * 0.3
|
||||||
|
alpha = cv2.GaussianBlur(alpha, (0, 0), sigma)
|
||||||
|
|
||||||
|
arr[:, :, 3] = alpha
|
||||||
|
out = Image.fromarray(arr, "RGBA")
|
||||||
|
buf = io.BytesIO()
|
||||||
|
out.save(buf, format="PNG")
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def _decontaminate_edges(image_bytes):
|
||||||
|
"""Remove background color spill from semi-transparent edge pixels."""
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
import io
|
||||||
|
|
||||||
|
img = Image.open(io.BytesIO(image_bytes)).convert("RGBA")
|
||||||
|
arr = np.array(img, dtype=np.float32)
|
||||||
|
alpha = arr[:, :, 3] / 255.0
|
||||||
|
rgb = arr[:, :, :3]
|
||||||
|
|
||||||
|
bg_mask = alpha < 0.04
|
||||||
|
if not np.any(bg_mask):
|
||||||
|
return image_bytes
|
||||||
|
|
||||||
|
bg_color = np.zeros(3, dtype=np.float32)
|
||||||
|
for c in range(3):
|
||||||
|
channel = rgb[:, :, c]
|
||||||
|
bg_pixels = channel[bg_mask]
|
||||||
|
if len(bg_pixels) > 0:
|
||||||
|
bg_color[c] = np.median(bg_pixels)
|
||||||
|
|
||||||
|
edge_mask = (alpha > 0.04) & (alpha < 0.96)
|
||||||
|
if not np.any(edge_mask):
|
||||||
|
return image_bytes
|
||||||
|
|
||||||
|
a = alpha[edge_mask, np.newaxis]
|
||||||
|
fg = rgb[edge_mask]
|
||||||
|
corrected = (fg - bg_color[np.newaxis, :] * (1.0 - a)) / np.maximum(a, 0.01)
|
||||||
|
corrected = np.clip(corrected, 0, 255)
|
||||||
|
rgb[edge_mask] = corrected
|
||||||
|
|
||||||
|
arr[:, :, :3] = rgb
|
||||||
|
result = np.clip(arr, 0, 255).astype(np.uint8)
|
||||||
|
out = Image.fromarray(result, "RGBA")
|
||||||
|
buf = io.BytesIO()
|
||||||
|
out.save(buf, format="PNG")
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
ALLOWED_MODELS = {
|
ALLOWED_MODELS = {
|
||||||
"u2net",
|
"u2net",
|
||||||
"isnet-general-use",
|
"isnet-general-use",
|
||||||
@@ -167,6 +234,17 @@ def main():
|
|||||||
|
|
||||||
emit_progress(80, "Background removed")
|
emit_progress(80, "Background removed")
|
||||||
|
|
||||||
|
edge_refine = settings.get("edgeRefine", 0)
|
||||||
|
decontaminate = settings.get("decontaminate", False)
|
||||||
|
|
||||||
|
if edge_refine and edge_refine > 0:
|
||||||
|
emit_progress(85, "Refining edges")
|
||||||
|
output_data = _refine_edges(output_data, int(edge_refine))
|
||||||
|
|
||||||
|
if decontaminate:
|
||||||
|
emit_progress(90, "Removing color spill")
|
||||||
|
output_data = _decontaminate_edges(output_data)
|
||||||
|
|
||||||
# Always return transparent PNG. All background compositing
|
# Always return transparent PNG. All background compositing
|
||||||
# (solid color, gradient, blur, shadow) is handled by Node.js/Sharp.
|
# (solid color, gradient, blur, shadow) is handled by Node.js/Sharp.
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { type ProgressCallback, parseStdoutJson, runPythonWithProgress } from ".
|
|||||||
export interface RemoveBackgroundOptions {
|
export interface RemoveBackgroundOptions {
|
||||||
model?: string;
|
model?: string;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
|
edgeRefine?: number;
|
||||||
|
decontaminate?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_REMBG_PX = Number(process.env.MAX_REMBG_PX) || 2048;
|
const MAX_REMBG_PX = Number(process.env.MAX_REMBG_PX) || 2048;
|
||||||
|
|||||||
@@ -587,6 +587,13 @@ export const ar: TranslationKeys = {
|
|||||||
intensity: "الشدة",
|
intensity: "الشدة",
|
||||||
addShadow: "إضافة ظل",
|
addShadow: "إضافة ظل",
|
||||||
opacity: "الشفافية",
|
opacity: "الشفافية",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "جاري المعالجة...",
|
rendering: "جاري المعالجة...",
|
||||||
submit: "إزالة الخلفية",
|
submit: "إزالة الخلفية",
|
||||||
submitBatch: "إزالة الخلفية ({count} ملف)",
|
submitBatch: "إزالة الخلفية ({count} ملف)",
|
||||||
|
|||||||
@@ -595,6 +595,13 @@ export const de: TranslationKeys = {
|
|||||||
intensity: "Intensitaet",
|
intensity: "Intensitaet",
|
||||||
addShadow: "Schatten hinzufuegen",
|
addShadow: "Schatten hinzufuegen",
|
||||||
opacity: "Deckkraft",
|
opacity: "Deckkraft",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "Wird gerendert...",
|
rendering: "Wird gerendert...",
|
||||||
submit: "Hintergrund entfernen",
|
submit: "Hintergrund entfernen",
|
||||||
submitBatch: "Hintergrund entfernen ({count} Dateien)",
|
submitBatch: "Hintergrund entfernen ({count} Dateien)",
|
||||||
|
|||||||
@@ -545,6 +545,13 @@ export const en = {
|
|||||||
intensity: "Intensity",
|
intensity: "Intensity",
|
||||||
addShadow: "Add Shadow",
|
addShadow: "Add Shadow",
|
||||||
opacity: "Opacity",
|
opacity: "Opacity",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "Rendering...",
|
rendering: "Rendering...",
|
||||||
submit: "Remove Background",
|
submit: "Remove Background",
|
||||||
submitBatch: "Remove Background ({count} files)",
|
submitBatch: "Remove Background ({count} files)",
|
||||||
|
|||||||
@@ -579,6 +579,13 @@ export const es: TranslationKeys = {
|
|||||||
intensity: "Intensidad",
|
intensity: "Intensidad",
|
||||||
addShadow: "Agregar sombra",
|
addShadow: "Agregar sombra",
|
||||||
opacity: "Opacidad",
|
opacity: "Opacidad",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "Renderizando...",
|
rendering: "Renderizando...",
|
||||||
submit: "Eliminar fondo",
|
submit: "Eliminar fondo",
|
||||||
submitBatch: "Eliminar fondo ({count} archivos)",
|
submitBatch: "Eliminar fondo ({count} archivos)",
|
||||||
|
|||||||
@@ -596,6 +596,13 @@ export const fr: TranslationKeys = {
|
|||||||
intensity: "Intensite",
|
intensity: "Intensite",
|
||||||
addShadow: "Ajouter une ombre",
|
addShadow: "Ajouter une ombre",
|
||||||
opacity: "Opacite",
|
opacity: "Opacite",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "Rendu en cours...",
|
rendering: "Rendu en cours...",
|
||||||
submit: "Supprimer l'arriere-plan",
|
submit: "Supprimer l'arriere-plan",
|
||||||
submitBatch: "Supprimer l'arriere-plan ({count} fichiers)",
|
submitBatch: "Supprimer l'arriere-plan ({count} fichiers)",
|
||||||
|
|||||||
@@ -583,6 +583,13 @@ export const hi: TranslationKeys = {
|
|||||||
intensity: "तीव्रता",
|
intensity: "तीव्रता",
|
||||||
addShadow: "शैडो जोड़ें",
|
addShadow: "शैडो जोड़ें",
|
||||||
opacity: "ओपेसिटी",
|
opacity: "ओपेसिटी",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "रेंडर हो रहा है...",
|
rendering: "रेंडर हो रहा है...",
|
||||||
submit: "बैकग्राउंड हटाएं",
|
submit: "बैकग्राउंड हटाएं",
|
||||||
submitBatch: "बैकग्राउंड हटाएं ({count} फाइलें)",
|
submitBatch: "बैकग्राउंड हटाएं ({count} फाइलें)",
|
||||||
|
|||||||
@@ -593,6 +593,13 @@ export const id: TranslationKeys = {
|
|||||||
intensity: "Intensitas",
|
intensity: "Intensitas",
|
||||||
addShadow: "Tambah Bayangan",
|
addShadow: "Tambah Bayangan",
|
||||||
opacity: "Opasitas",
|
opacity: "Opasitas",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "Merender...",
|
rendering: "Merender...",
|
||||||
submit: "Hapus Latar Belakang",
|
submit: "Hapus Latar Belakang",
|
||||||
submitBatch: "Hapus Latar Belakang ({count} file)",
|
submitBatch: "Hapus Latar Belakang ({count} file)",
|
||||||
|
|||||||
@@ -592,6 +592,13 @@ export const it: TranslationKeys = {
|
|||||||
intensity: "Intensita",
|
intensity: "Intensita",
|
||||||
addShadow: "Aggiungi ombra",
|
addShadow: "Aggiungi ombra",
|
||||||
opacity: "Opacita",
|
opacity: "Opacita",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "Rendering...",
|
rendering: "Rendering...",
|
||||||
submit: "Rimuovi sfondo",
|
submit: "Rimuovi sfondo",
|
||||||
submitBatch: "Rimuovi sfondo ({count} file)",
|
submitBatch: "Rimuovi sfondo ({count} file)",
|
||||||
|
|||||||
@@ -552,6 +552,13 @@ export const ja: TranslationKeys = {
|
|||||||
intensity: "強度",
|
intensity: "強度",
|
||||||
addShadow: "シャドウ追加",
|
addShadow: "シャドウ追加",
|
||||||
opacity: "不透明度",
|
opacity: "不透明度",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "レンダリング中...",
|
rendering: "レンダリング中...",
|
||||||
submit: "背景を除去",
|
submit: "背景を除去",
|
||||||
submitBatch: "背景を除去({count}ファイル)",
|
submitBatch: "背景を除去({count}ファイル)",
|
||||||
|
|||||||
@@ -539,6 +539,13 @@ export const ko: TranslationKeys = {
|
|||||||
intensity: "강도",
|
intensity: "강도",
|
||||||
addShadow: "그림자 추가",
|
addShadow: "그림자 추가",
|
||||||
opacity: "불투명도",
|
opacity: "불투명도",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "렌더링 중...",
|
rendering: "렌더링 중...",
|
||||||
submit: "배경 제거",
|
submit: "배경 제거",
|
||||||
submitBatch: "배경 제거 ({count}개 파일)",
|
submitBatch: "배경 제거 ({count}개 파일)",
|
||||||
|
|||||||
@@ -593,6 +593,13 @@ export const nl: TranslationKeys = {
|
|||||||
intensity: "Intensiteit",
|
intensity: "Intensiteit",
|
||||||
addShadow: "Schaduw toevoegen",
|
addShadow: "Schaduw toevoegen",
|
||||||
opacity: "Dekking",
|
opacity: "Dekking",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "Renderen...",
|
rendering: "Renderen...",
|
||||||
submit: "Achtergrond verwijderen",
|
submit: "Achtergrond verwijderen",
|
||||||
submitBatch: "Achtergrond verwijderen ({count} bestanden)",
|
submitBatch: "Achtergrond verwijderen ({count} bestanden)",
|
||||||
|
|||||||
@@ -596,6 +596,13 @@ export const pl: TranslationKeys = {
|
|||||||
intensity: "Intensywność",
|
intensity: "Intensywność",
|
||||||
addShadow: "Dodaj cień",
|
addShadow: "Dodaj cień",
|
||||||
opacity: "Krycie",
|
opacity: "Krycie",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "Renderowanie...",
|
rendering: "Renderowanie...",
|
||||||
submit: "Usuń tło",
|
submit: "Usuń tło",
|
||||||
submitBatch: "Usuń tło ({count} plików)",
|
submitBatch: "Usuń tło ({count} plików)",
|
||||||
|
|||||||
@@ -592,6 +592,13 @@ export const ptBR: TranslationKeys = {
|
|||||||
intensity: "Intensidade",
|
intensity: "Intensidade",
|
||||||
addShadow: "Adicionar sombra",
|
addShadow: "Adicionar sombra",
|
||||||
opacity: "Opacidade",
|
opacity: "Opacidade",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "Renderizando...",
|
rendering: "Renderizando...",
|
||||||
submit: "Remover fundo",
|
submit: "Remover fundo",
|
||||||
submitBatch: "Remover fundo ({count} arquivos)",
|
submitBatch: "Remover fundo ({count} arquivos)",
|
||||||
|
|||||||
@@ -594,6 +594,13 @@ export const ru: TranslationKeys = {
|
|||||||
intensity: "Интенсивность",
|
intensity: "Интенсивность",
|
||||||
addShadow: "Добавить тень",
|
addShadow: "Добавить тень",
|
||||||
opacity: "Непрозрачность",
|
opacity: "Непрозрачность",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "Рендеринг...",
|
rendering: "Рендеринг...",
|
||||||
submit: "Удалить фон",
|
submit: "Удалить фон",
|
||||||
submitBatch: "Удалить фон ({count} файлов)",
|
submitBatch: "Удалить фон ({count} файлов)",
|
||||||
|
|||||||
@@ -591,6 +591,13 @@ export const sv: TranslationKeys = {
|
|||||||
intensity: "Intensitet",
|
intensity: "Intensitet",
|
||||||
addShadow: "Lagg till skugga",
|
addShadow: "Lagg till skugga",
|
||||||
opacity: "Opacitet",
|
opacity: "Opacitet",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "Renderar...",
|
rendering: "Renderar...",
|
||||||
submit: "Ta bort bakgrund",
|
submit: "Ta bort bakgrund",
|
||||||
submitBatch: "Ta bort bakgrund ({count} filer)",
|
submitBatch: "Ta bort bakgrund ({count} filer)",
|
||||||
|
|||||||
@@ -583,6 +583,13 @@ export const th: TranslationKeys = {
|
|||||||
intensity: "ความเข้ม",
|
intensity: "ความเข้ม",
|
||||||
addShadow: "เพิ่มเงา",
|
addShadow: "เพิ่มเงา",
|
||||||
opacity: "ความทึบ",
|
opacity: "ความทึบ",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "กำลังเรนเดอร์...",
|
rendering: "กำลังเรนเดอร์...",
|
||||||
submit: "ลบพื้นหลัง",
|
submit: "ลบพื้นหลัง",
|
||||||
submitBatch: "ลบพื้นหลัง ({count} ไฟล์)",
|
submitBatch: "ลบพื้นหลัง ({count} ไฟล์)",
|
||||||
|
|||||||
@@ -595,6 +595,13 @@ export const tr: TranslationKeys = {
|
|||||||
intensity: "Yoğunluk",
|
intensity: "Yoğunluk",
|
||||||
addShadow: "Gölge Ekle",
|
addShadow: "Gölge Ekle",
|
||||||
opacity: "Opaklık",
|
opacity: "Opaklık",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "İşleniyor...",
|
rendering: "İşleniyor...",
|
||||||
submit: "Arka Planı Kaldır",
|
submit: "Arka Planı Kaldır",
|
||||||
submitBatch: "Arka Planı Kaldır ({count} dosya)",
|
submitBatch: "Arka Planı Kaldır ({count} dosya)",
|
||||||
|
|||||||
@@ -594,6 +594,13 @@ export const uk: TranslationKeys = {
|
|||||||
intensity: "Інтенсивність",
|
intensity: "Інтенсивність",
|
||||||
addShadow: "Додати тінь",
|
addShadow: "Додати тінь",
|
||||||
opacity: "Непрозорість",
|
opacity: "Непрозорість",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "Рендеринг...",
|
rendering: "Рендеринг...",
|
||||||
submit: "Видалити фон",
|
submit: "Видалити фон",
|
||||||
submitBatch: "Видалити фон ({count} файлів)",
|
submitBatch: "Видалити фон ({count} файлів)",
|
||||||
|
|||||||
@@ -594,6 +594,13 @@ export const vi: TranslationKeys = {
|
|||||||
intensity: "Cường độ",
|
intensity: "Cường độ",
|
||||||
addShadow: "Thêm bóng đổ",
|
addShadow: "Thêm bóng đổ",
|
||||||
opacity: "Độ mờ",
|
opacity: "Độ mờ",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "Đang kết xuất...",
|
rendering: "Đang kết xuất...",
|
||||||
submit: "Xóa nền",
|
submit: "Xóa nền",
|
||||||
submitBatch: "Xóa nền ({count} tệp)",
|
submitBatch: "Xóa nền ({count} tệp)",
|
||||||
|
|||||||
@@ -537,6 +537,13 @@ export const zhCN: TranslationKeys = {
|
|||||||
intensity: "强度",
|
intensity: "强度",
|
||||||
addShadow: "添加阴影",
|
addShadow: "添加阴影",
|
||||||
opacity: "不透明度",
|
opacity: "不透明度",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "渲染中...",
|
rendering: "渲染中...",
|
||||||
submit: "移除背景",
|
submit: "移除背景",
|
||||||
submitBatch: "移除背景({count} 个文件)",
|
submitBatch: "移除背景({count} 个文件)",
|
||||||
|
|||||||
@@ -536,6 +536,13 @@ export const zhTW: TranslationKeys = {
|
|||||||
intensity: "強度",
|
intensity: "強度",
|
||||||
addShadow: "加入陰影",
|
addShadow: "加入陰影",
|
||||||
opacity: "不透明度",
|
opacity: "不透明度",
|
||||||
|
outputFormat: "Output Format",
|
||||||
|
edgeSmoothing: "Edge Smoothing",
|
||||||
|
edgeSmoothingOff: "Off",
|
||||||
|
edgeSmoothingLight: "Light",
|
||||||
|
edgeSmoothingMedium: "Medium",
|
||||||
|
edgeSmoothingStrong: "Strong",
|
||||||
|
colorDecontamination: "Color Decontamination",
|
||||||
rendering: "算繪中...",
|
rendering: "算繪中...",
|
||||||
submit: "移除背景",
|
submit: "移除背景",
|
||||||
submitBatch: "移除背景({count}個檔案)",
|
submitBatch: "移除背景({count}個檔案)",
|
||||||
|
|||||||
@@ -387,6 +387,102 @@ describe("Remove Background", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts edge refinement and decontamination settings", async () => {
|
||||||
|
const { body, contentType } = createMultipartPayload([
|
||||||
|
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
|
||||||
|
{
|
||||||
|
name: "settings",
|
||||||
|
content: JSON.stringify({ edgeRefine: 2, decontaminate: true }),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: "/api/v1/tools/remove-background",
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${adminToken}`,
|
||||||
|
"content-type": contentType,
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect([202, 501]).toContain(res.statusCode);
|
||||||
|
}, 60_000);
|
||||||
|
|
||||||
|
it("accepts output format settings", async () => {
|
||||||
|
const { body, contentType } = createMultipartPayload([
|
||||||
|
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
|
||||||
|
{
|
||||||
|
name: "settings",
|
||||||
|
content: JSON.stringify({ outputFormat: "webp" }),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: "/api/v1/tools/remove-background",
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${adminToken}`,
|
||||||
|
"content-type": contentType,
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect([202, 501]).toContain(res.statusCode);
|
||||||
|
}, 60_000);
|
||||||
|
|
||||||
|
it("rejects edgeRefine out of range", async () => {
|
||||||
|
const { body, contentType } = createMultipartPayload([
|
||||||
|
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
|
||||||
|
{
|
||||||
|
name: "settings",
|
||||||
|
content: JSON.stringify({ edgeRefine: 5 }),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: "/api/v1/tools/remove-background",
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${adminToken}`,
|
||||||
|
"content-type": contentType,
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect([400, 501]).toContain(res.statusCode);
|
||||||
|
if (res.statusCode === 400) {
|
||||||
|
const result = JSON.parse(res.body);
|
||||||
|
expect(result.error).toMatch(/invalid settings/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid output format", async () => {
|
||||||
|
const { body, contentType } = createMultipartPayload([
|
||||||
|
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
|
||||||
|
{
|
||||||
|
name: "settings",
|
||||||
|
content: JSON.stringify({ outputFormat: "gif" }),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: "/api/v1/tools/remove-background",
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${adminToken}`,
|
||||||
|
"content-type": contentType,
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect([400, 501]).toContain(res.statusCode);
|
||||||
|
if (res.statusCode === 400) {
|
||||||
|
const result = JSON.parse(res.body);
|
||||||
|
expect(result.error).toMatch(/invalid settings/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects unauthenticated requests", async () => {
|
it("rejects unauthenticated requests", async () => {
|
||||||
const { body, contentType } = createMultipartPayload([
|
const { body, contentType } = createMultipartPayload([
|
||||||
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
|
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
|
||||||
|
|||||||
@@ -97,6 +97,35 @@ describe("removeBackground", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("serializes edgeRefine option into the args JSON", async () => {
|
||||||
|
await removeBackground(FAKE_INPUT, FAKE_OUTPUT_DIR, { edgeRefine: 2 });
|
||||||
|
|
||||||
|
const args = vi.mocked(runPythonWithProgress).mock.calls[0][1];
|
||||||
|
expect(JSON.parse(args[2])).toEqual({ edgeRefine: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("serializes decontaminate option into the args JSON", async () => {
|
||||||
|
await removeBackground(FAKE_INPUT, FAKE_OUTPUT_DIR, { decontaminate: true });
|
||||||
|
|
||||||
|
const args = vi.mocked(runPythonWithProgress).mock.calls[0][1];
|
||||||
|
expect(JSON.parse(args[2])).toEqual({ decontaminate: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("serializes all post-processing options together", async () => {
|
||||||
|
await removeBackground(FAKE_INPUT, FAKE_OUTPUT_DIR, {
|
||||||
|
model: "birefnet-matting",
|
||||||
|
edgeRefine: 1,
|
||||||
|
decontaminate: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const args = vi.mocked(runPythonWithProgress).mock.calls[0][1];
|
||||||
|
expect(JSON.parse(args[2])).toEqual({
|
||||||
|
model: "birefnet-matting",
|
||||||
|
edgeRefine: 1,
|
||||||
|
decontaminate: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("converts input to PNG via sharp before writing to disk", async () => {
|
it("converts input to PNG via sharp before writing to disk", async () => {
|
||||||
await removeBackground(FAKE_INPUT, FAKE_OUTPUT_DIR);
|
await removeBackground(FAKE_INPUT, FAKE_OUTPUT_DIR);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user