import { mkdir, stat } from 'node:fs/promises'; import path from 'node:path'; import { spawn } from 'node:child_process'; export const MAX_LONG_EDGE = 1280; export class FrameNormalizer { constructor({ helper = path.resolve(new URL('./py/normalize_frame.py', import.meta.url).pathname), python = 'python3', spawnImpl = spawn, tmpDir = '/tmp/jarvis-cu', maxLongEdge = MAX_LONG_EDGE, quality = 70 } = {}) { Object.assign(this, { helper, python, spawnImpl, tmpDir, maxLongEdge, quality }); } async normalize(input, output = path.join(this.tmpDir, `frame-${Date.now()}.webp`), rect) { await mkdir(path.dirname(output), { recursive: true }); const crop = rect ? rect.map(Number).map(value => String(Math.round(value))) : []; let metadata = {}; await new Promise((resolve, reject) => { const child = this.spawnImpl(this.python, [this.helper, input, output, String(this.maxLongEdge), String(this.quality), ...crop], { stdio: ['ignore', 'pipe', 'pipe'] }); let stderr = '', stdout = ''; const timer = setTimeout(() => { child.kill('SIGTERM'); reject(new Error('frame normalization timed out')); }, 8000); child.stdout?.on('data', data => { stdout += data; }); child.stderr?.on('data', data => { stderr += data; }); child.on('error', error => { clearTimeout(timer); reject(error); }); child.on('close', code => { clearTimeout(timer); try { metadata = JSON.parse(stdout || '{}'); } catch {} code === 0 ? resolve() : reject(new Error(stderr || `frame normalization exited ${code}`)); }); }); const info = await stat(output); const ext = path.extname(output).toLowerCase(); const mime = metadata.mime || (ext === '.png' ? 'image/png' : ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg' : 'image/webp'); return { ...metadata, path: output, bytes: info.size, maxLongEdge: this.maxLongEdge, mime }; } }