mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: update tool installation checks and refactor stdout JSON parsing in AI modules
This commit is contained in:
@@ -100,7 +100,7 @@ export function isFeatureInstalled(bundleId: string): boolean {
|
|||||||
|
|
||||||
export function isToolInstalled(toolId: string): boolean {
|
export function isToolInstalled(toolId: string): boolean {
|
||||||
const bundleId = TOOL_BUNDLE_MAP[toolId];
|
const bundleId = TOOL_BUNDLE_MAP[toolId];
|
||||||
if (!bundleId) return false;
|
if (!bundleId) return true;
|
||||||
return isFeatureInstalled(bundleId);
|
return isFeatureInstalled(bundleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ const settingsSchema = z.object({
|
|||||||
*/
|
*/
|
||||||
export function registerRestorePhoto(app: FastifyInstance) {
|
export function registerRestorePhoto(app: FastifyInstance) {
|
||||||
app.post("/api/v1/tools/restore-photo", async (request: FastifyRequest, reply: FastifyReply) => {
|
app.post("/api/v1/tools/restore-photo", async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
if (!isToolInstalled("restore-photo")) {
|
||||||
|
const bundle = getBundleForTool("restore-photo");
|
||||||
|
return reply.status(501).send({
|
||||||
|
error: "Feature not installed",
|
||||||
|
code: "FEATURE_NOT_INSTALLED",
|
||||||
|
feature: "photo-restoration",
|
||||||
|
featureName: bundle?.name ?? "Photo Restoration",
|
||||||
|
estimatedSize: bundle?.estimatedSize ?? "unknown",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let fileBuffer: Buffer | null = null;
|
let fileBuffer: Buffer | null = null;
|
||||||
let filename = "image";
|
let filename = "image";
|
||||||
let settingsRaw: string | null = null;
|
let settingsRaw: string | null = null;
|
||||||
@@ -69,18 +80,6 @@ export function registerRestorePhoto(app: FastifyInstance) {
|
|||||||
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
|
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guard: check if the photo restoration feature bundle is installed
|
|
||||||
if (!isToolInstalled("restore-photo")) {
|
|
||||||
const bundle = getBundleForTool("restore-photo");
|
|
||||||
return reply.status(501).send({
|
|
||||||
error: "Feature not installed",
|
|
||||||
code: "FEATURE_NOT_INSTALLED",
|
|
||||||
feature: "photo-restoration",
|
|
||||||
featureName: bundle?.name ?? "Photo Restoration",
|
|
||||||
estimatedSize: bundle?.estimatedSize ?? "unknown",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings = settingsSchema.parse(settingsRaw ? JSON.parse(settingsRaw) : {});
|
const settings = settingsSchema.parse(settingsRaw ? JSON.parse(settingsRaw) : {});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|||||||
import { readFile, unlink, writeFile } from "node:fs/promises";
|
import { readFile, unlink, writeFile } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { type ProgressCallback, runPythonWithProgress } from "./bridge.js";
|
import { type ProgressCallback, parseStdoutJson, runPythonWithProgress } from "./bridge.js";
|
||||||
|
|
||||||
export interface RemoveBackgroundOptions {
|
export interface RemoveBackgroundOptions {
|
||||||
model?: string;
|
model?: string;
|
||||||
@@ -29,7 +29,7 @@ export async function removeBackground(
|
|||||||
{ onProgress, timeout },
|
{ onProgress, timeout },
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = JSON.parse(stdout);
|
const result = parseStdoutJson(stdout);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || "Background removal failed");
|
throw new Error(result.error || "Background removal failed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,9 +100,7 @@ function startDispatcher(): ChildProcess | null {
|
|||||||
if (parsed.ready === true) {
|
if (parsed.ready === true) {
|
||||||
dispatcherReady = true;
|
dispatcherReady = true;
|
||||||
dispatcherGpuAvailable = parsed.gpu === true;
|
dispatcherGpuAvailable = parsed.gpu === true;
|
||||||
console.log(
|
console.log(`[bridge] Python dispatcher ready (GPU: ${parsed.gpu === true})`);
|
||||||
`[bridge] Python dispatcher ready (GPU: ${parsed.gpu === true})`,
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,3 +406,10 @@ export function runPythonWithProgress(
|
|||||||
// Fall back to per-request spawning
|
// Fall back to per-request spawning
|
||||||
return runPythonPerRequest(scriptName, args, options);
|
return runPythonPerRequest(scriptName, args, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: matches JSON.parse return type
|
||||||
|
export function parseStdoutJson(stdout: string): any {
|
||||||
|
const match = stdout.match(/\{[\s\S]*\}$/);
|
||||||
|
if (!match) throw new Error("No JSON response from Python script");
|
||||||
|
return JSON.parse(match[0]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { readFile, writeFile } from "node:fs/promises";
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { type ProgressCallback, runPythonWithProgress } from "./bridge.js";
|
import { type ProgressCallback, parseStdoutJson, runPythonWithProgress } from "./bridge.js";
|
||||||
|
|
||||||
export interface ColorizeOptions {
|
export interface ColorizeOptions {
|
||||||
intensity?: number;
|
intensity?: number;
|
||||||
@@ -30,7 +30,7 @@ export async function colorize(
|
|||||||
{ onProgress },
|
{ onProgress },
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = JSON.parse(stdout);
|
const result = parseStdoutJson(stdout);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || "Colorization failed");
|
throw new Error(result.error || "Colorization failed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { readFile, unlink, writeFile } from "node:fs/promises";
|
import { readFile, unlink, writeFile } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { type ProgressCallback, runPythonWithProgress } from "./bridge.js";
|
import { type ProgressCallback, parseStdoutJson, runPythonWithProgress } from "./bridge.js";
|
||||||
|
|
||||||
export interface BlurFacesOptions {
|
export interface BlurFacesOptions {
|
||||||
blurRadius?: number;
|
blurRadius?: number;
|
||||||
@@ -46,7 +46,7 @@ export async function blurFaces(
|
|||||||
{ onProgress },
|
{ onProgress },
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = JSON.parse(stdout);
|
const result = parseStdoutJson(stdout);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || "Face detection failed");
|
throw new Error(result.error || "Face detection failed");
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ export async function detectFaces(
|
|||||||
{ onProgress },
|
{ onProgress },
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = JSON.parse(stdout);
|
const result = parseStdoutJson(stdout);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || "Face detection failed");
|
throw new Error(result.error || "Face detection failed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { readFile, writeFile } from "node:fs/promises";
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { type ProgressCallback, runPythonWithProgress } from "./bridge.js";
|
import { type ProgressCallback, parseStdoutJson, runPythonWithProgress } from "./bridge.js";
|
||||||
|
|
||||||
export interface EnhanceFacesOptions {
|
export interface EnhanceFacesOptions {
|
||||||
model?: "auto" | "gfpgan" | "codeformer";
|
model?: "auto" | "gfpgan" | "codeformer";
|
||||||
@@ -32,7 +32,7 @@ export async function enhanceFaces(
|
|||||||
{ onProgress },
|
{ onProgress },
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = JSON.parse(stdout);
|
const result = parseStdoutJson(stdout);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || "Face enhancement failed");
|
throw new Error(result.error || "Face enhancement failed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { unlink, writeFile } from "node:fs/promises";
|
import { unlink, writeFile } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { type ProgressCallback, runPythonWithProgress } from "./bridge.js";
|
import { type ProgressCallback, parseStdoutJson, runPythonWithProgress } from "./bridge.js";
|
||||||
|
|
||||||
export interface FaceLandmarkPoint {
|
export interface FaceLandmarkPoint {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -40,7 +40,7 @@ export async function detectFaceLandmarks(
|
|||||||
{ onProgress },
|
{ onProgress },
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = JSON.parse(stdout);
|
const result = parseStdoutJson(stdout);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || "Face landmark detection failed");
|
throw new Error(result.error || "Face landmark detection failed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { readFile, writeFile } from "node:fs/promises";
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { type ProgressCallback, runPythonWithProgress } from "./bridge.js";
|
import { type ProgressCallback, parseStdoutJson, runPythonWithProgress } from "./bridge.js";
|
||||||
|
|
||||||
export async function inpaint(
|
export async function inpaint(
|
||||||
inputBuffer: Buffer,
|
inputBuffer: Buffer,
|
||||||
@@ -19,7 +19,7 @@ export async function inpaint(
|
|||||||
onProgress,
|
onProgress,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = JSON.parse(stdout);
|
const result = parseStdoutJson(stdout);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || "Inpainting failed");
|
throw new Error(result.error || "Inpainting failed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { readFile, writeFile } from "node:fs/promises";
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { type ProgressCallback, runPythonWithProgress } from "./bridge.js";
|
import { type ProgressCallback, parseStdoutJson, runPythonWithProgress } from "./bridge.js";
|
||||||
|
|
||||||
export interface NoiseRemovalOptions {
|
export interface NoiseRemovalOptions {
|
||||||
tier?: string;
|
tier?: string;
|
||||||
@@ -35,7 +35,7 @@ export async function noiseRemoval(
|
|||||||
{ onProgress },
|
{ onProgress },
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = JSON.parse(stdout);
|
const result = parseStdoutJson(stdout);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || "Noise removal failed");
|
throw new Error(result.error || "Noise removal failed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { writeFile } from "node:fs/promises";
|
import { writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import { type ProgressCallback, runPythonWithProgress } from "./bridge.js";
|
import { type ProgressCallback, parseStdoutJson, runPythonWithProgress } from "./bridge.js";
|
||||||
|
|
||||||
export type OcrQuality = "fast" | "balanced" | "best";
|
export type OcrQuality = "fast" | "balanced" | "best";
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ export async function extractText(
|
|||||||
timeout: 600_000, // 10 min timeout for VLM on CPU
|
timeout: 600_000, // 10 min timeout for VLM on CPU
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = JSON.parse(stdout);
|
const result = parseStdoutJson(stdout);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || "OCR failed");
|
throw new Error(result.error || "OCR failed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { readFile, writeFile } from "node:fs/promises";
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { type ProgressCallback, runPythonWithProgress } from "./bridge.js";
|
import { type ProgressCallback, parseStdoutJson, runPythonWithProgress } from "./bridge.js";
|
||||||
|
|
||||||
export interface RedEyeRemovalOptions {
|
export interface RedEyeRemovalOptions {
|
||||||
sensitivity?: number;
|
sensitivity?: number;
|
||||||
@@ -34,7 +34,7 @@ export async function removeRedEye(
|
|||||||
{ onProgress },
|
{ onProgress },
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = JSON.parse(stdout);
|
const result = parseStdoutJson(stdout);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || "Red eye removal failed");
|
throw new Error(result.error || "Red eye removal failed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { readFile, writeFile } from "node:fs/promises";
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { type ProgressCallback, runPythonWithProgress } from "./bridge.js";
|
import { type ProgressCallback, parseStdoutJson, runPythonWithProgress } from "./bridge.js";
|
||||||
|
|
||||||
export interface RestorePhotoOptions {
|
export interface RestorePhotoOptions {
|
||||||
mode?: string;
|
mode?: string;
|
||||||
@@ -39,7 +39,7 @@ export async function restorePhoto(
|
|||||||
{ onProgress },
|
{ onProgress },
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = JSON.parse(stdout);
|
const result = parseStdoutJson(stdout);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || "Photo restoration failed");
|
throw new Error(result.error || "Photo restoration failed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { readFile, writeFile } from "node:fs/promises";
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { type ProgressCallback, runPythonWithProgress } from "./bridge.js";
|
import { type ProgressCallback, parseStdoutJson, runPythonWithProgress } from "./bridge.js";
|
||||||
|
|
||||||
export interface UpscaleOptions {
|
export interface UpscaleOptions {
|
||||||
scale?: number;
|
scale?: number;
|
||||||
@@ -35,7 +35,7 @@ export async function upscale(
|
|||||||
{ onProgress },
|
{ onProgress },
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = JSON.parse(stdout);
|
const result = parseStdoutJson(stdout);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || "Upscaling failed");
|
throw new Error(result.error || "Upscaling failed");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user