mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: add Phase 4 AI tools with Python bridge and 6 new tools
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.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PYTHON_DIR = resolve(__dirname, "../python");
|
||||
|
||||
/** Try venv first, then system python. */
|
||||
function getPythonPath(): string {
|
||||
const venvPath = process.env.PYTHON_VENV_PATH || "/opt/venv";
|
||||
return `${venvPath}/bin/python3`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a user-friendly error from a Python process error.
|
||||
* Python scripts print JSON to stderr/stdout on failure — try to parse it.
|
||||
*/
|
||||
function extractPythonError(error: unknown): string {
|
||||
if (error && typeof error === "object") {
|
||||
const execError = error as {
|
||||
stderr?: string;
|
||||
stdout?: string;
|
||||
message?: string;
|
||||
};
|
||||
// Try to parse JSON error from stderr or stdout
|
||||
for (const output of [execError.stderr, execError.stdout]) {
|
||||
if (output) {
|
||||
try {
|
||||
const parsed = JSON.parse(output.trim());
|
||||
if (parsed.error) return parsed.error;
|
||||
} catch {
|
||||
// Not JSON, check for human-readable content
|
||||
const trimmed = output.trim();
|
||||
if (trimmed && !trimmed.startsWith("Traceback")) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (execError.message) return execError.message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a Python script from packages/ai/python/ with the given arguments.
|
||||
* Falls back to system python3 if the venv is not available.
|
||||
*/
|
||||
export async function runPythonScript(
|
||||
scriptName: string,
|
||||
args: string[],
|
||||
timeoutMs = 300000, // 5 min default
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
const scriptPath = resolve(PYTHON_DIR, scriptName);
|
||||
const pythonPath = getPythonPath();
|
||||
|
||||
const execOpts = {
|
||||
timeout: timeoutMs,
|
||||
maxBuffer: 50 * 1024 * 1024, // 50MB
|
||||
};
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execFileAsync(
|
||||
pythonPath,
|
||||
[scriptPath, ...args],
|
||||
execOpts,
|
||||
);
|
||||
return { stdout: stdout.trim(), stderr: stderr.trim() };
|
||||
} catch {
|
||||
// Try system python as fallback
|
||||
try {
|
||||
const { stdout, stderr } = await execFileAsync(
|
||||
"python3",
|
||||
[scriptPath, ...args],
|
||||
execOpts,
|
||||
);
|
||||
return { stdout: stdout.trim(), stderr: stderr.trim() };
|
||||
} catch (fallbackError: unknown) {
|
||||
const message = extractPythonError(fallbackError);
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user