diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 36ed3935..02da9c17 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -86,6 +86,12 @@ const app = Fastify({ routerOptions: { maxParamLength: 500 }, }); +// Image processing (especially AI batch) can run for tens of minutes. +// Node.js defaults to a 5-minute requestTimeout which kills long-running +// connections. Set a generous default; per-route overrides disable it entirely. +app.server.requestTimeout = 30 * 60 * 1000; +app.server.headersTimeout = 60 * 1000; + app.removeContentTypeParser("application/json"); app.addContentTypeParser("application/json", { parseAs: "string" }, (_request, body, done) => { try { diff --git a/apps/api/src/lib/format-decoders.ts b/apps/api/src/lib/format-decoders.ts index dd2ea6f5..608ac57a 100644 --- a/apps/api/src/lib/format-decoders.ts +++ b/apps/api/src/lib/format-decoders.ts @@ -235,16 +235,29 @@ async function decodeTga(buffer: Buffer): Promise { * because EXR files are typically stored in linear light. */ async function decodeExr(buffer: Buffer): Promise { - const cmd = await findMagickCmd(); const id = randomUUID(); const inputPath = join(tmpdir(), `exr-in-${id}.exr`); const outputPath = join(tmpdir(), `exr-out-${id}.png`); try { await writeFile(inputPath, buffer); + + // ImageMagick needs the OpenEXR delegate which is often missing on macOS + try { + const cmd = await findMagickCmd(); + await execFileAsync( + cmd, + magickArgs(cmd, [inputPath, "-colorspace", "sRGB", "-depth", "8", `png:${outputPath}`]), + { timeout: 120_000 }, + ); + return await readFile(outputPath); + } catch { + // ImageMagick failed, try ffmpeg + } + await execFileAsync( - cmd, - magickArgs(cmd, [inputPath, "-colorspace", "sRGB", `png:${outputPath}`]), + "ffmpeg", + ["-y", "-i", inputPath, "-pix_fmt", "rgba", "-update", "1", outputPath], { timeout: 120_000 }, ); return await readFile(outputPath); @@ -267,7 +280,7 @@ async function decodeHdr(buffer: Buffer): Promise { await writeFile(inputPath, buffer); await execFileAsync( cmd, - magickArgs(cmd, [inputPath, "-colorspace", "sRGB", `png:${outputPath}`]), + magickArgs(cmd, [inputPath, "-colorspace", "sRGB", "-depth", "8", `png:${outputPath}`]), { timeout: 120_000 }, ); return await readFile(outputPath); diff --git a/apps/api/src/routes/batch.ts b/apps/api/src/routes/batch.ts index 72f6102c..622ea747 100644 --- a/apps/api/src/routes/batch.ts +++ b/apps/api/src/routes/batch.ts @@ -37,6 +37,11 @@ export async function registerBatchRoutes(app: FastifyInstance): Promise { async (request: FastifyRequest<{ Params: { toolId: string } }>, reply: FastifyReply) => { const { toolId } = request.params; + // Batch processing (especially with AI) can take tens of minutes. + // Disable the Node.js HTTP socket timeout so the connection is not + // dropped while images are still being processed. + request.raw.socket?.setTimeout?.(0); + // Look up the tool config from the registry const toolConfig = getToolConfig(toolId); if (!toolConfig) {