Files
tilt/tools/freemocap-vite-plugin.mjs
2026-08-03 16:04:46 -05:00

107 lines
4.5 KiB
JavaScript

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 });
}
});
},
};
}