mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Add Python bridge (packages/ai/src/bridge.ts) that calls Python scripts via child_process with venv-first fallback to system python3. Implements 6 AI-powered tools: - Remove Background: rembg-based with U2-Net/IS-Net models - Image Upscaling: Real-ESRGAN with Lanczos fallback - OCR/Text Extraction: Tesseract + PaddleOCR engines - Face/PII Blur: MediaPipe face detection with configurable blur - Object Eraser: LaMa inpainting with mask-based input - Smart Crop: Sharp attention-based entropy cropping (no Python needed) Each tool includes: Python script, TypeScript wrapper, API route, and React settings component. All Python scripts handle ImportError gracefully with clear installation messages.
87 lines
2.8 KiB
TypeScript
87 lines
2.8 KiB
TypeScript
import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
|
import { randomUUID } from "node:crypto";
|
|
import { writeFile } from "node:fs/promises";
|
|
import { join, basename } from "node:path";
|
|
import { upscale } from "@stirling-image/ai";
|
|
import { createWorkspace } from "../../lib/workspace.js";
|
|
|
|
/**
|
|
* AI image upscaling route.
|
|
* Uses Real-ESRGAN when available, falls back to Lanczos.
|
|
*/
|
|
export function registerUpscale(app: FastifyInstance) {
|
|
app.post(
|
|
"/api/v1/tools/upscale",
|
|
async (request: FastifyRequest, reply: FastifyReply) => {
|
|
let fileBuffer: Buffer | null = null;
|
|
let filename = "image";
|
|
let settingsRaw: string | null = null;
|
|
|
|
try {
|
|
const parts = request.parts();
|
|
for await (const part of parts) {
|
|
if (part.type === "file") {
|
|
const chunks: Buffer[] = [];
|
|
for await (const chunk of part.file) {
|
|
chunks.push(chunk);
|
|
}
|
|
fileBuffer = Buffer.concat(chunks);
|
|
filename = basename(part.filename ?? "image");
|
|
} else if (part.fieldname === "settings") {
|
|
settingsRaw = part.value as string;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
return reply.status(400).send({
|
|
error: "Failed to parse multipart request",
|
|
details: err instanceof Error ? err.message : String(err),
|
|
});
|
|
}
|
|
|
|
if (!fileBuffer || fileBuffer.length === 0) {
|
|
return reply.status(400).send({ error: "No image file provided" });
|
|
}
|
|
|
|
try {
|
|
const settings = settingsRaw ? JSON.parse(settingsRaw) : {};
|
|
const scale = Number(settings.scale) || 2;
|
|
|
|
const jobId = randomUUID();
|
|
const workspacePath = await createWorkspace(jobId);
|
|
|
|
// Save input
|
|
const inputPath = join(workspacePath, "input", filename);
|
|
await writeFile(inputPath, fileBuffer);
|
|
|
|
// Process
|
|
const result = await upscale(
|
|
fileBuffer,
|
|
join(workspacePath, "output"),
|
|
{ scale },
|
|
);
|
|
|
|
// Save output
|
|
const outputFilename =
|
|
filename.replace(/\.[^.]+$/, "") + `_${scale}x.png`;
|
|
const outputPath = join(workspacePath, "output", outputFilename);
|
|
await writeFile(outputPath, result.buffer);
|
|
|
|
return reply.send({
|
|
jobId,
|
|
downloadUrl: `/api/v1/download/${jobId}/${encodeURIComponent(outputFilename)}`,
|
|
originalSize: fileBuffer.length,
|
|
processedSize: result.buffer.length,
|
|
width: result.width,
|
|
height: result.height,
|
|
method: result.method,
|
|
});
|
|
} catch (err) {
|
|
return reply.status(422).send({
|
|
error: "Upscaling failed",
|
|
details: err instanceof Error ? err.message : "Unknown error",
|
|
});
|
|
}
|
|
},
|
|
);
|
|
}
|