fix(passport-photo): require the face-detection bundle, not just background-removal (#329)

* fix(passport-photo): require the face-detection bundle, not just background-removal

Passport Photo runs face-landmark detection (face_landmarks.py, gated to the
face-detection bundle) before background removal (background-removal bundle),
but it was only declared under and guarded against background-removal. A user
who installed only Background Removal passed every JS-side check, then hit a
late "feature_not_installed" from the Python dispatcher gate when the analyze
step ran face landmarks, and the UI never told them Face Detection was needed.

- shared: add TOOL_EXTRA_BUNDLES + getRequiredBundlesForTool so a tool can
  declare more than one required bundle (passport-photo needs background-removal
  and face-detection). enablesTools is untouched, so the one-tool-per-bundle
  invariant still holds.
- api: isToolInstalled() now checks every required bundle; add
  getFirstMissingBundleForTool() so the analyze and base routes, pipeline (both
  guards) and batch report the bundle the user actually still needs.
- web: the proactive install prompt (tool-page) and features-store treat a tool
  as installed only when all required bundles are present, and point the prompt
  at the first missing one (sequential install, no new UI).

Refs #327

* test(passport-photo): deterministic integration coverage for the two-bundle guard

Boots the real API with an isolated DATA_DIR and controls installed.json to
prove the HTTP route behavior end-to-end:
- nothing installed -> 501 naming background-removal
- only background-removal installed -> 501 naming face-detection (issue #327)
- both installed -> guard passes (not 501)
- base route reports face-detection too

Refs #327
This commit is contained in:
SnapOtter
2026-06-22 23:31:18 +08:00
committed by GitHub
parent 8952e9ba47
commit 32c1192d63
11 changed files with 346 additions and 43 deletions
+25
View File
@@ -93,3 +93,28 @@ export function getBundleForTool(toolId: string): FeatureBundleInfo | null {
export function getToolsForBundle(bundleId: string): string[] {
return FEATURE_BUNDLES[bundleId]?.enablesTools ?? [];
}
/**
* Tools that need AI models from MORE THAN ONE feature bundle.
*
* The "primary" bundle (the one whose `enablesTools` lists the tool) is in
* TOOL_BUNDLE_MAP. Any ADDITIONAL bundles the tool's processing requires are
* listed here. Example: Passport Photo removes the background (its primary
* `background-removal` bundle) but first runs face-landmark detection, which
* is gated to the separate `face-detection` bundle.
*/
export const TOOL_EXTRA_BUNDLES: Record<string, string[]> = {
"passport-photo": ["face-detection"],
};
/**
* Every feature bundle a tool needs installed before it can run: its primary
* bundle plus any extras from TOOL_EXTRA_BUNDLES. Order is [primary, ...extras],
* deduped. Returns [] for tools that need no AI bundle.
*/
export function getRequiredBundlesForTool(toolId: string): string[] {
const primary = TOOL_BUNDLE_MAP[toolId];
if (!primary) return [];
const extras = TOOL_EXTRA_BUNDLES[toolId] ?? [];
return [...new Set([primary, ...extras])];
}