mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: support downloadFn-based model manifests and improve install error messages
Add support for models defined via downloadFn/args (rembg_session, hf_snapshot) in bundle verification, recovery, and uninstall paths. Previously only path-based models were tracked, so bundles using rembg or HF snapshot downloads appeared broken after install. Also improve pip install error messages with user-friendly hints for common failures (basicsr build issues, OOM, disk full) and add better error context for rembg session download failures.
This commit is contained in:
@@ -219,6 +219,8 @@ export function setInstallProgress(
|
||||
interface ManifestModel {
|
||||
id: string;
|
||||
path?: string;
|
||||
downloadFn?: string;
|
||||
args?: string[];
|
||||
minSize?: number;
|
||||
}
|
||||
|
||||
@@ -309,25 +311,42 @@ export function recoverInterruptedInstalls(): void {
|
||||
if (!manifestBundle) continue;
|
||||
|
||||
for (const model of manifestBundle.models) {
|
||||
if (!model.path) continue;
|
||||
const modelPath = join(MODELS_DIR, model.path);
|
||||
if (!existsSync(modelPath)) {
|
||||
console.warn(`[feature-status] Bundle "${bundleId}" missing model file: ${model.path}`);
|
||||
// Don't remove from installed.json — getFeatureStates will surface the error
|
||||
break;
|
||||
}
|
||||
if (model.minSize != null && model.minSize > 0) {
|
||||
try {
|
||||
const st = statSync(modelPath);
|
||||
if (st.size < model.minSize) {
|
||||
if (model.path) {
|
||||
const modelPath = join(MODELS_DIR, model.path);
|
||||
if (!existsSync(modelPath)) {
|
||||
console.warn(`[feature-status] Bundle "${bundleId}" missing model file: ${model.path}`);
|
||||
break;
|
||||
}
|
||||
if (model.minSize != null && model.minSize > 0) {
|
||||
try {
|
||||
const st = statSync(modelPath);
|
||||
if (st.size < model.minSize) {
|
||||
console.warn(
|
||||
`[feature-status] Bundle "${bundleId}" model "${model.path}" is undersized (${st.size} < ${model.minSize})`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
console.warn(
|
||||
`[feature-status] Bundle "${bundleId}" model "${model.path}" is undersized (${st.size} < ${model.minSize})`,
|
||||
`[feature-status] Bundle "${bundleId}" cannot stat model: ${model.path}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// stat failed, treat as missing
|
||||
console.warn(`[feature-status] Bundle "${bundleId}" cannot stat model: ${model.path}`);
|
||||
}
|
||||
} else if (model.downloadFn === "rembg_session" && model.args?.[0]) {
|
||||
const filePath = join(MODELS_DIR, "rembg", `${model.args[0]}.onnx`);
|
||||
if (!existsSync(filePath)) {
|
||||
console.warn(
|
||||
`[feature-status] Bundle "${bundleId}" missing rembg model: ${model.args[0]}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
} else if (model.downloadFn === "hf_snapshot" && model.args?.[1]) {
|
||||
const dirPath = join(MODELS_DIR, model.args[1]);
|
||||
if (!existsSync(dirPath)) {
|
||||
console.warn(
|
||||
`[feature-status] Bundle "${bundleId}" missing model directory: ${model.args[1]}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -348,19 +367,30 @@ function verifyBundleModels(bundleId: string): string | null {
|
||||
if (!manifestBundle) return null;
|
||||
|
||||
for (const model of manifestBundle.models) {
|
||||
if (!model.path) continue;
|
||||
const modelPath = join(MODELS_DIR, model.path);
|
||||
if (!existsSync(modelPath)) {
|
||||
return `Missing model file: ${model.path}`;
|
||||
}
|
||||
if (model.minSize != null && model.minSize > 0) {
|
||||
try {
|
||||
const st = statSync(modelPath);
|
||||
if (st.size < model.minSize) {
|
||||
return `Model "${model.path}" is undersized (${st.size} < ${model.minSize})`;
|
||||
if (model.path) {
|
||||
const modelPath = join(MODELS_DIR, model.path);
|
||||
if (!existsSync(modelPath)) {
|
||||
return `Missing model file: ${model.path}`;
|
||||
}
|
||||
if (model.minSize != null && model.minSize > 0) {
|
||||
try {
|
||||
const st = statSync(modelPath);
|
||||
if (st.size < model.minSize) {
|
||||
return `Model "${model.path}" is undersized (${st.size} < ${model.minSize})`;
|
||||
}
|
||||
} catch {
|
||||
return `Cannot read model file: ${model.path}`;
|
||||
}
|
||||
} catch {
|
||||
return `Cannot read model file: ${model.path}`;
|
||||
}
|
||||
} else if (model.downloadFn === "rembg_session" && model.args?.[0]) {
|
||||
const filePath = join(MODELS_DIR, "rembg", `${model.args[0]}.onnx`);
|
||||
if (!existsSync(filePath)) {
|
||||
return `Missing rembg model: ${model.args[0]}`;
|
||||
}
|
||||
} else if (model.downloadFn === "hf_snapshot" && model.args?.[1]) {
|
||||
const dirPath = join(MODELS_DIR, model.args[1]);
|
||||
if (!existsSync(dirPath)) {
|
||||
return `Missing model directory: ${model.args[1]}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,15 @@
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import crypto from "node:crypto";
|
||||
import { type Dirent, existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
|
||||
import {
|
||||
type Dirent,
|
||||
existsSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
unlinkSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { shutdownDispatcher } from "@snapotter/ai";
|
||||
import { ANALYTICS_EVENTS, FEATURE_BUNDLES } from "@snapotter/shared";
|
||||
@@ -42,6 +50,8 @@ interface BundleIdParams {
|
||||
interface ManifestModel {
|
||||
id: string;
|
||||
path?: string;
|
||||
downloadFn?: string;
|
||||
args?: string[];
|
||||
}
|
||||
|
||||
interface ManifestBundle {
|
||||
@@ -277,30 +287,43 @@ export async function registerFeatureRoutes(app: FastifyInstance): Promise<void>
|
||||
return reply.status(409).send({ error: `Bundle "${bundleId}" is not installed` });
|
||||
}
|
||||
|
||||
// Read manifest to find model files to delete, but skip models
|
||||
// that are still needed by another installed bundle.
|
||||
const manifest = readManifest();
|
||||
if (manifest) {
|
||||
const manifestBundle = manifest.bundles[bundleId];
|
||||
if (manifestBundle) {
|
||||
// Collect model paths that OTHER installed bundles still need
|
||||
const sharedPaths = new Set<string>();
|
||||
const protectedFiles = new Set<string>();
|
||||
const protectedDirs = new Set<string>();
|
||||
for (const [otherId, otherBundle] of Object.entries(manifest.bundles)) {
|
||||
if (otherId === bundleId) continue;
|
||||
if (!isFeatureInstalled(otherId)) continue;
|
||||
if (otherId === bundleId || !isFeatureInstalled(otherId)) continue;
|
||||
for (const m of otherBundle.models ?? []) {
|
||||
if (m.path) sharedPaths.add(m.path);
|
||||
if (m.path) protectedFiles.add(m.path);
|
||||
if (m.downloadFn === "rembg_session" && m.args?.[0]) {
|
||||
protectedFiles.add(`rembg/${m.args[0]}.onnx`);
|
||||
}
|
||||
if (m.downloadFn === "hf_snapshot" && m.args?.[1]) {
|
||||
protectedDirs.add(m.args[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const modelsDir = getModelsDir();
|
||||
for (const model of manifestBundle.models) {
|
||||
if (!model.path) continue;
|
||||
if (sharedPaths.has(model.path)) continue; // still needed
|
||||
const modelPath = join(modelsDir, model.path);
|
||||
try {
|
||||
if (existsSync(modelPath)) {
|
||||
unlinkSync(modelPath);
|
||||
if (model.path && !protectedFiles.has(model.path)) {
|
||||
const modelPath = join(modelsDir, model.path);
|
||||
if (existsSync(modelPath)) unlinkSync(modelPath);
|
||||
} else if (model.downloadFn === "rembg_session" && model.args?.[0]) {
|
||||
const relPath = `rembg/${model.args[0]}.onnx`;
|
||||
if (!protectedFiles.has(relPath)) {
|
||||
const filePath = join(modelsDir, relPath);
|
||||
if (existsSync(filePath)) unlinkSync(filePath);
|
||||
}
|
||||
} else if (!model.path && model.downloadFn === "hf_snapshot" && model.args?.[1]) {
|
||||
const subdir = model.args[1];
|
||||
if (!protectedDirs.has(subdir)) {
|
||||
const dirPath = join(modelsDir, subdir);
|
||||
if (existsSync(dirPath)) rmSync(dirPath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Best-effort deletion
|
||||
|
||||
Reference in New Issue
Block a user