mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Adds an opt-in High Quality mode to the Object Eraser, backed by a new inpaint-hq feature bundle (Stable Diffusion 1.5 inpainting via diffusers). The default fast LaMa path is unchanged. Both arch archives are published to deepsafe/feature-bundles and the manifest carries their real sha256/sizes. Verified end to end: a fresh container pulls the bundle from HuggingFace, checksum-verifies it, extracts torch/diffusers plus the fp16 model, and the HQ sidecar erases a large object with a plausible fill. Refs #141
197 lines
7.7 KiB
TypeScript
197 lines
7.7 KiB
TypeScript
import {
|
|
FEATURE_BUNDLES,
|
|
getBundleForTool,
|
|
getOptionalBundleForTool,
|
|
getRequiredBundlesForTool,
|
|
getToolsForBundle,
|
|
PYTHON_SIDECAR_TOOLS,
|
|
TOOL_BUNDLE_MAP,
|
|
TOOL_EXTRA_BUNDLES,
|
|
TOOL_OPTIONAL_BUNDLE_MAP,
|
|
} from "@snapotter/shared";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
describe("Feature bundles", () => {
|
|
it("every PYTHON_SIDECAR_TOOL maps to one mandatory or optional bundle", () => {
|
|
for (const toolId of PYTHON_SIDECAR_TOOLS) {
|
|
const bundle = getBundleForTool(toolId) ?? getOptionalBundleForTool(toolId);
|
|
expect(bundle, `${toolId} has no bundle`).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it("getBundleForTool returns null for non-AI tools", () => {
|
|
expect(getBundleForTool("resize")).toBeNull();
|
|
expect(getBundleForTool("crop")).toBeNull();
|
|
});
|
|
|
|
it("getToolsForBundle returns correct tools", () => {
|
|
const tools = getToolsForBundle("background-removal");
|
|
expect(tools).toContain("remove-background");
|
|
expect(tools).toContain("passport-photo");
|
|
expect(tools).not.toContain("upscale");
|
|
});
|
|
|
|
it("all 8 bundles are defined", () => {
|
|
expect(Object.keys(FEATURE_BUNDLES)).toHaveLength(8);
|
|
expect(FEATURE_BUNDLES["background-removal"]).toBeDefined();
|
|
expect(FEATURE_BUNDLES["face-detection"]).toBeDefined();
|
|
expect(FEATURE_BUNDLES["object-eraser-colorize"]).toBeDefined();
|
|
expect(FEATURE_BUNDLES["inpaint-hq"]).toBeDefined();
|
|
expect(FEATURE_BUNDLES["upscale-enhance"]).toBeDefined();
|
|
expect(FEATURE_BUNDLES["photo-restoration"]).toBeDefined();
|
|
expect(FEATURE_BUNDLES.ocr).toBeDefined();
|
|
expect(FEATURE_BUNDLES.transcription).toBeDefined();
|
|
});
|
|
|
|
it("every sidecar tool is reachable; only built-in-fast tools skip the required map", () => {
|
|
const mappedTools = Object.keys(TOOL_BUNDLE_MAP);
|
|
for (const toolId of PYTHON_SIDECAR_TOOLS) {
|
|
// Reachable via a required primary and/or an optional upgrade pack.
|
|
expect(
|
|
getBundleForTool(toolId) !== null || getOptionalBundleForTool(toolId) !== null,
|
|
`${toolId} has no bundle at all`,
|
|
).toBe(true);
|
|
// A tool ABSENT from TOOL_BUNDLE_MAP must be a built-in-fast tool whose only
|
|
// bundle is an optional pack (e.g. OCR's Fast tier + accurate pack). A tool
|
|
// with a required base stays mapped even if it also has an optional upgrade
|
|
// pack (e.g. erase-object's LaMa base + inpaint-hq diffusion pack).
|
|
if (!mappedTools.includes(toolId)) {
|
|
expect(
|
|
getOptionalBundleForTool(toolId),
|
|
`${toolId} is neither required-mapped nor a built-in-fast optional-pack tool`,
|
|
).not.toBeNull();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("Feature bundle edge cases", () => {
|
|
it("no tool appears in two non-optional bundles (an optional pack may re-list its tool)", () => {
|
|
const firstBundle = new Map<string, string>();
|
|
for (const bundle of Object.values(FEATURE_BUNDLES)) {
|
|
for (const tool of bundle.enablesTools) {
|
|
const prior = firstBundle.get(tool);
|
|
if (prior === undefined) {
|
|
firstBundle.set(tool, bundle.id);
|
|
continue;
|
|
}
|
|
// The only allowed overlap: a tool's optional upgrade pack re-lists a
|
|
// tool its primary bundle already enables (e.g. inpaint-hq over
|
|
// erase-object). Any other pairing is an accidental duplicate.
|
|
const optional = TOOL_OPTIONAL_BUNDLE_MAP[tool];
|
|
expect(
|
|
optional !== undefined && (prior === optional || bundle.id === optional),
|
|
`Tool ${tool} appears in two non-optional bundles (${prior}, ${bundle.id})`,
|
|
).toBe(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("every bundle has a non-empty estimated size", () => {
|
|
for (const bundle of Object.values(FEATURE_BUNDLES)) {
|
|
expect(bundle.estimatedSize.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it("getToolsForBundle returns empty array for unknown bundle", () => {
|
|
expect(getToolsForBundle("nonexistent")).toEqual([]);
|
|
});
|
|
|
|
it("getBundleForTool returns null for unknown tool", () => {
|
|
expect(getBundleForTool("nonexistent-tool")).toBeNull();
|
|
});
|
|
|
|
it("TOOL_BUNDLE_MAP has no undefined values", () => {
|
|
for (const [tool, bundle] of Object.entries(TOOL_BUNDLE_MAP)) {
|
|
expect(bundle, `Tool ${tool} has undefined bundle`).toBeDefined();
|
|
expect(
|
|
FEATURE_BUNDLES[bundle],
|
|
`Bundle ${bundle} for tool ${tool} not in FEATURE_BUNDLES`,
|
|
).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it("every bundle id matches its key in FEATURE_BUNDLES", () => {
|
|
for (const [key, bundle] of Object.entries(FEATURE_BUNDLES)) {
|
|
expect(bundle.id).toBe(key);
|
|
}
|
|
});
|
|
|
|
it("every bundle has a non-empty name and description", () => {
|
|
for (const bundle of Object.values(FEATURE_BUNDLES)) {
|
|
expect(bundle.name.length).toBeGreaterThan(0);
|
|
expect(bundle.description.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it("every bundle has at least one tool", () => {
|
|
for (const [id, bundle] of Object.entries(FEATURE_BUNDLES)) {
|
|
expect(bundle.enablesTools.length, `Bundle ${id} has no tools`).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("getRequiredBundlesForTool", () => {
|
|
it("returns the primary bundle for a single-bundle tool", () => {
|
|
expect(getRequiredBundlesForTool("remove-background")).toEqual(["background-removal"]);
|
|
});
|
|
|
|
it("returns [] for non-AI tools", () => {
|
|
expect(getRequiredBundlesForTool("resize")).toEqual([]);
|
|
expect(getRequiredBundlesForTool("nonexistent-tool")).toEqual([]);
|
|
});
|
|
|
|
it("includes the primary bundle plus extras for cross-bundle tools", () => {
|
|
// Passport Photo runs face-landmark detection (face-detection) on top of
|
|
// background removal (its primary bundle).
|
|
expect(getRequiredBundlesForTool("passport-photo")).toEqual([
|
|
"background-removal",
|
|
"face-detection",
|
|
]);
|
|
});
|
|
|
|
it("lists the primary bundle first", () => {
|
|
for (const toolId of PYTHON_SIDECAR_TOOLS) {
|
|
const required = getRequiredBundlesForTool(toolId);
|
|
if (required.length > 0) {
|
|
expect(required[0]).toBe(TOOL_BUNDLE_MAP[toolId]);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("never lists a bundle twice", () => {
|
|
for (const toolId of PYTHON_SIDECAR_TOOLS) {
|
|
const required = getRequiredBundlesForTool(toolId);
|
|
expect(new Set(required).size).toBe(required.length);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("TOOL_EXTRA_BUNDLES", () => {
|
|
it("references only real bundles and never the tool's own primary bundle", () => {
|
|
for (const [toolId, extras] of Object.entries(TOOL_EXTRA_BUNDLES)) {
|
|
const primary = TOOL_BUNDLE_MAP[toolId];
|
|
for (const bundleId of extras) {
|
|
expect(FEATURE_BUNDLES[bundleId], `Unknown extra bundle ${bundleId}`).toBeDefined();
|
|
expect(bundleId, `${toolId} lists its primary bundle as an extra`).not.toBe(primary);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("inpaint-hq optional upgrade for erase-object", () => {
|
|
it("keeps object-eraser-colorize as the required primary; inpaint-hq stays optional", () => {
|
|
// The HQ diffusion pack upgrades Object Eraser but must not gate it: the base
|
|
// LaMa bundle remains the tool's required primary, and HQ is a separate,
|
|
// explicit install check (mirrors OCR's Fast tier + optional accurate pack).
|
|
expect(FEATURE_BUNDLES["inpaint-hq"]).toBeDefined();
|
|
expect(TOOL_BUNDLE_MAP["erase-object"]).toBe("object-eraser-colorize");
|
|
expect(TOOL_OPTIONAL_BUNDLE_MAP["erase-object"]).toBe("inpaint-hq");
|
|
expect(getBundleForTool("erase-object")?.id).toBe("object-eraser-colorize");
|
|
expect(getOptionalBundleForTool("erase-object")?.id).toBe("inpaint-hq");
|
|
// erase-object must NOT require inpaint-hq (fast path works without it).
|
|
expect(getRequiredBundlesForTool("erase-object")).toEqual(["object-eraser-colorize"]);
|
|
expect(getRequiredBundlesForTool("erase-object")).not.toContain("inpaint-hq");
|
|
});
|
|
});
|