fix: update tool installation checks and refactor stdout JSON parsing in AI modules

This commit is contained in:
ashim-hq
2026-04-19 12:13:26 +08:00
parent dc7ba119f8
commit 12c4d4de6f
14 changed files with 43 additions and 39 deletions
+1 -1
View File
@@ -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);
} }
+11 -12
View File
@@ -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 -2
View File
@@ -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");
} }
+8 -3
View File
@@ -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]);
}
+2 -2
View File
@@ -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");
} }
+3 -3
View File
@@ -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");
} }
+2 -2
View File
@@ -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");
} }
+2 -2
View File
@@ -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");
} }
+2 -2
View File
@@ -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");
} }
+2 -2
View File
@@ -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");
} }
+2 -2
View File
@@ -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");
} }
+2 -2
View File
@@ -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");
} }
+2 -2
View File
@@ -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");
} }
+2 -2
View File
@@ -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");
} }