import { spawn } from 'node:child_process'; import { createWriteStream } from 'node:fs'; import { access, mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { extname, resolve } from 'node:path'; import { pipeline } from 'node:stream/promises'; const VIDEO_EXTENSIONS = new Set(['.mp4', '.mov', '.webm', '.m4v']); function run(command, args, options = {}) { return new Promise((resolveRun, reject) => { const child = spawn(command, args, { ...options, stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; child.stdout.on('data', (chunk) => { stdout = (stdout + chunk).slice(-16000); }); child.stderr.on('data', (chunk) => { stderr = (stderr + chunk).slice(-32000); }); child.once('error', reject); child.once('close', (code) => { if (code === 0) resolveRun({ stdout: stdout.trim(), stderr: stderr.trim() }); else reject(new Error((stderr || stdout || `${command} exited with ${code}`).trim())); }); }); } export function freeMocapBridge({ projectRoot }) { const bundledPython = resolve(projectRoot, '.freemocap-venv/bin/python'); const worker = resolve(projectRoot, 'tools/freemocap_worker.py'); const python = process.env.TILT_FREEMOCAP_PYTHON || bundledPython; async function workerStatus() { try { await access(python); const result = await run(python, ['-c', 'import freemocap; print(freemocap.__version__)']); const version = result.stdout.split('\n').at(-1)?.replace(/\x1b\[[0-9;]*m/g, '') || 'installed'; return { available: true, version }; } catch (error) { return { available: false, message: `FreeMoCap worker is not installed (${error.message.split('\n').at(-1)})`, }; } } return { name: 'tilt-freemocap-bridge', configureServer(server) { server.middlewares.use('/api/freemocap/status', async (_request, response) => { const status = await workerStatus(); response.statusCode = status.available ? 200 : 503; response.setHeader('Content-Type', 'application/json'); response.end(JSON.stringify(status)); }); server.middlewares.use('/api/freemocap/process', async (request, response) => { if (request.method !== 'POST') { response.statusCode = 405; response.end('POST a video file to this endpoint'); return; } const status = await workerStatus(); if (!status.available) { response.statusCode = 503; response.end(`${status.message}. Run npm run freemocap:setup.`); return; } const url = new URL(request.url || '/', 'http://localhost'); const requestedName = url.searchParams.get('filename') || 'reference.mp4'; const requestedExtension = extname(requestedName).toLowerCase(); const extension = VIDEO_EXTENSIONS.has(requestedExtension) ? requestedExtension : '.mp4'; const fps = Math.max(1, Math.min(60, Number(url.searchParams.get('fps')) || 30)); const jobRoot = await mkdtemp(resolve(tmpdir(), 'tilt-freemocap-')); const input = resolve(jobRoot, `upload${extension}`); const recording = resolve(jobRoot, 'recording'); const output = resolve(jobRoot, 'result.csv'); const metadata = resolve(jobRoot, 'result.json'); try { await pipeline(request, createWriteStream(input)); server.config.logger.info(`FreeMoCap processing ${requestedName}…`); await run(python, [ worker, '--input', input, '--recording', recording, '--output', output, '--metadata', metadata, '--fps', String(fps), ], { cwd: projectRoot }); const csv = await readFile(output); const resultMetadata = JSON.parse(await readFile(metadata, 'utf8')); response.statusCode = 200; response.setHeader('Content-Type', 'text/csv; charset=utf-8'); response.setHeader('X-Tilt-Mocap-Backend', `FreeMoCap ${status.version}`); response.setHeader('X-Tilt-Source-Fps', String(resultMetadata.fps || fps)); response.end(csv); } catch (error) { server.config.logger.error(error.stack || error.message); response.statusCode = 500; response.setHeader('Content-Type', 'text/plain; charset=utf-8'); response.end(error.message); } finally { await rm(jobRoot, { recursive: true, force: true }); } }); }, }; }