mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Enhance logging and error handling across tools; add full tool audit and Playwright tests
- Added model mismatch warnings in colorize, enhance-faces, and upscale routes. - Improved error handling in colorize, enhance_faces, remove_bg, restore, and upscale scripts with detailed logging. - Updated Dockerfile to align NCCL versions for compatibility. - Introduced a new full tool audit script to test all tools for functionality and GPU usage. - Created Playwright E2E tests for GPU-dependent tools to ensure proper functionality and performance.
This commit is contained in:
+48
-10
@@ -32,11 +32,26 @@ function extractPythonError(error: unknown): string {
|
||||
if (trimmed && !trimmed.startsWith("Traceback")) {
|
||||
return trimmed;
|
||||
}
|
||||
// Extract the last meaningful line from a Python Traceback
|
||||
if (trimmed) {
|
||||
const lines = trimmed
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean);
|
||||
const lastLine = lines[lines.length - 1];
|
||||
if (lastLine && lastLine !== "Traceback (most recent call last):") {
|
||||
return lastLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (execError.message) return execError.message;
|
||||
// Return empty string for {stdout, stderr} objects with no useful content
|
||||
// so the caller's fallback message (e.g. exit code) kicks in.
|
||||
return "";
|
||||
}
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
}
|
||||
|
||||
@@ -85,6 +100,9 @@ function startDispatcher(): ChildProcess | null {
|
||||
if (parsed.ready === true) {
|
||||
dispatcherReady = true;
|
||||
dispatcherGpuAvailable = parsed.gpu === true;
|
||||
console.log(
|
||||
`[bridge] Python dispatcher ready (GPU: ${parsed.gpu === true})`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -97,7 +115,11 @@ function startDispatcher(): ChildProcess | null {
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Not JSON - collect as error output for pending requests
|
||||
// Not JSON - forward diagnostic messages to Node.js logger,
|
||||
// collect the rest as error output for pending requests.
|
||||
if (trimmed.startsWith("[")) {
|
||||
console.log(`[python] ${trimmed}`);
|
||||
}
|
||||
for (const req of pendingRequests.values()) {
|
||||
req.stderrLines.push(trimmed);
|
||||
}
|
||||
@@ -121,14 +143,17 @@ function startDispatcher(): ChildProcess | null {
|
||||
if (pending) {
|
||||
pendingRequests.delete(reqId);
|
||||
if (response.exitCode !== 0) {
|
||||
pending.reject(
|
||||
new Error(
|
||||
extractPythonError({
|
||||
stdout: response.stdout,
|
||||
stderr: pending.stderrLines.join("\n"),
|
||||
}) || `Python script exited with code ${response.exitCode}`,
|
||||
),
|
||||
);
|
||||
const errText =
|
||||
extractPythonError({
|
||||
stdout: response.stdout,
|
||||
stderr: pending.stderrLines.join("\n"),
|
||||
}) ||
|
||||
(response.exitCode === 137
|
||||
? "Process killed (out of memory) — try a lighter model or smaller image"
|
||||
: response.exitCode === 139
|
||||
? "Process crashed (segmentation fault)"
|
||||
: `Python script exited with code ${response.exitCode}`);
|
||||
pending.reject(new Error(errText));
|
||||
} else {
|
||||
pending.resolve({
|
||||
stdout: response.stdout || "",
|
||||
@@ -143,6 +168,7 @@ function startDispatcher(): ChildProcess | null {
|
||||
});
|
||||
|
||||
child.on("error", (err: NodeJS.ErrnoException) => {
|
||||
console.error(`[bridge] Dispatcher error: ${err.message} (code: ${err.code})`);
|
||||
if (err.code === "ENOENT") {
|
||||
// Venv python not found - mark as failed, will fall back to per-request
|
||||
dispatcherFailed = true;
|
||||
@@ -307,7 +333,7 @@ function runPythonPerRequest(
|
||||
}
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
child.on("close", (code, signal) => {
|
||||
clearTimeout(timer);
|
||||
|
||||
if (stderrBuffer.trim()) {
|
||||
@@ -322,7 +348,16 @@ function runPythonPerRequest(
|
||||
const stderr = stderrLines.join("\n");
|
||||
|
||||
if (code !== 0) {
|
||||
// When the process was killed by a signal, use a clear message
|
||||
// instead of surfacing unrelated stderr (e.g. CUDA warnings).
|
||||
const signalMsg =
|
||||
signal === "SIGKILL" || code === 137
|
||||
? "Process killed (out of memory) — try a lighter model or smaller image"
|
||||
: signal === "SIGSEGV" || code === 139
|
||||
? "Process crashed (segmentation fault)"
|
||||
: null;
|
||||
const errorText =
|
||||
signalMsg ||
|
||||
extractPythonError({ stdout: stdout.trim(), stderr }) ||
|
||||
`Python script exited with code ${code}`;
|
||||
rejectPromise(new Error(errorText));
|
||||
@@ -361,6 +396,9 @@ export function runPythonWithProgress(
|
||||
// Retry in an isolated per-request process which starts clean and has
|
||||
// more available memory than the warm dispatcher.
|
||||
if (err.message === "Python dispatcher exited unexpectedly") {
|
||||
console.warn(
|
||||
`[bridge] Dispatcher crashed during ${scriptName}, retrying with per-request process`,
|
||||
);
|
||||
return runPythonPerRequest(scriptName, args, options);
|
||||
}
|
||||
throw err;
|
||||
|
||||
Reference in New Issue
Block a user