- Keep the map footer links permanently visible; remove the hide toggle. - Publish inline founder promote/demote from the member row. - Add encrypted MLS reaction/delete/read control messages and UI. - Add the minute Cordn join watcher: NIP-17 DMs + encrypted bell notices. - Add the browser translation-matrix runner and a low-end performance finding.
166 lines
8.2 KiB
JavaScript
Executable file
166 lines
8.2 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
/**
|
|
* Browser translation matrix (on-device worker).
|
|
*
|
|
* Serves the production dist, instantiates the built translate.worker chunk in
|
|
* headless Chromium, and round-trips one short sentence through every app
|
|
* language (en→xx then xx→en). Verifies the worker returns text and records
|
|
* latency. This is a hardware-reality check, not a CI test: it downloads the
|
|
* ~617 MB multilingual model on first use.
|
|
*
|
|
* Usage: node scripts/translation-matrix.mjs
|
|
* Output: /tmp/opencode/translation-matrix.json (+ a short summary on stdout)
|
|
*/
|
|
import { createServer } from 'node:http';
|
|
import { spawn } from 'node:child_process';
|
|
import { readFile, readdir } from 'node:fs/promises';
|
|
import { existsSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { dirname, extname, join, normalize } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { tmpdir } from 'node:os';
|
|
import net from 'node:net';
|
|
import WebSocket from 'ws';
|
|
|
|
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
const dist = join(root, 'dist');
|
|
const chromePath = process.env.CHROME_PATH || '/snap/bin/chromium';
|
|
const timeoutMs = Number(process.env.MATRIX_TIMEOUT_MS || 30 * 60 * 1000);
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
const MIME = {
|
|
'.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript',
|
|
'.css': 'text/css', '.json': 'application/json', '.wasm': 'application/wasm',
|
|
'.png': 'image/png', '.svg': 'image/svg+xml', '.woff2': 'font/woff2',
|
|
};
|
|
|
|
const workerFile = (await readdir(join(dist, 'assets'))).find((file) => /^translate\.worker-.*\.js$/.test(file));
|
|
if (!workerFile) throw new Error('translate.worker chunk not found — build first');
|
|
if (!existsSync(chromePath)) throw new Error(`Chromium not found at ${chromePath}`);
|
|
|
|
const freePort = () => new Promise((resolve) => {
|
|
const s = net.createServer();
|
|
s.listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); });
|
|
});
|
|
const port = await freePort();
|
|
const workerUrl = `http://127.0.0.1:${port}/assets/${workerFile}`;
|
|
const server = createServer(async (req, res) => {
|
|
const url = decodeURIComponent((req.url || '/').split('?')[0]);
|
|
if (url === '/matrix') {
|
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
res.end('<!doctype html><meta charset="utf-8"><title>translation matrix</title><body>ready</body>');
|
|
return;
|
|
}
|
|
let file = join(dist, normalize(url).replace(/^(\.\.[/\\])+/, ''));
|
|
if (url === '/' || !extname(file)) file = join(dist, 'index.html');
|
|
try {
|
|
const body = await readFile(file);
|
|
res.writeHead(200, { 'Content-Type': MIME[extname(file)] || 'application/octet-stream' });
|
|
res.end(body);
|
|
} catch {
|
|
res.writeHead(404); res.end('not found');
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(port, '127.0.0.1', resolve));
|
|
|
|
const cdpPort = await freePort();
|
|
const profile = join(tmpdir(), `skatespots-matrix-${process.pid}-${cdpPort}`);
|
|
const chrome = spawn(chromePath, [
|
|
'--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage',
|
|
'--disable-extensions', '--no-first-run', '--no-default-browser-check',
|
|
'--js-flags=--max-old-space-size=4096',
|
|
`--user-data-dir=${profile}`, `--remote-debugging-port=${cdpPort}`,
|
|
`http://127.0.0.1:${port}/matrix`,
|
|
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
chrome.stderr.on('data', () => {});
|
|
const cleanup = () => { try { chrome.kill('SIGKILL'); } catch {} try { server.close(); } catch {} try { rmSync(profile, { recursive: true, force: true }); } catch {} };
|
|
process.on('exit', cleanup);
|
|
|
|
async function pageTarget() {
|
|
for (let i = 0; i < 80; i++) {
|
|
try {
|
|
const list = await (await fetch(`http://127.0.0.1:${cdpPort}/json`)).json();
|
|
const page = list.find((t) => t.type === 'page' && t.url.includes('/matrix'));
|
|
if (page) return page;
|
|
} catch {}
|
|
await sleep(300);
|
|
}
|
|
throw new Error('no Chromium page target');
|
|
}
|
|
const target = await pageTarget();
|
|
const ws = new WebSocket(target.webSocketDebuggerUrl, { maxPayload: 64 * 1024 * 1024 });
|
|
let id = 0; const pending = new Map(); const exceptions = [];
|
|
ws.on('message', (raw) => {
|
|
const message = JSON.parse(raw);
|
|
if (message.id && pending.has(message.id)) { pending.get(message.id)(message); pending.delete(message.id); return; }
|
|
if (message.method === 'Runtime.exceptionThrown') {
|
|
exceptions.push(message.params.exceptionDetails?.exception?.description || message.params.exceptionDetails?.text || 'exception');
|
|
}
|
|
});
|
|
const send = (method, params = {}) => new Promise((resolve) => {
|
|
const requestId = ++id; pending.set(requestId, resolve);
|
|
ws.send(JSON.stringify({ id: requestId, method, params }));
|
|
});
|
|
await new Promise((resolve) => ws.on('open', resolve));
|
|
await send('Runtime.enable');
|
|
await send('Page.enable');
|
|
const evalJS = async (expression, awaitPromise = false) => (await send('Runtime.evaluate', {
|
|
expression, returnByValue: true, awaitPromise,
|
|
}))?.result?.result?.value;
|
|
|
|
const init = await evalJS(`(() => {
|
|
try {
|
|
const worker = new Worker(${JSON.stringify(workerUrl)}, { type: 'module' });
|
|
window.__matrix = { worker, seq: 0, pending: new Map() };
|
|
worker.onerror = (event) => { window.__matrixError = String(event.message || event); };
|
|
worker.onmessage = (event) => {
|
|
const data = event.data;
|
|
if (!data || typeof data.id !== 'number') return;
|
|
if (data.type === 'progress') return;
|
|
const p = window.__matrix.pending.get(data.id);
|
|
if (!p) return;
|
|
window.__matrix.pending.delete(data.id);
|
|
if (data.type === 'result') p.resolve(data.text);
|
|
else p.reject(new Error(data.code || 'FAILED'));
|
|
};
|
|
window.__matrix.translate = (text, source, target) => new Promise((resolve, reject) => {
|
|
const requestId = ++window.__matrix.seq;
|
|
window.__matrix.pending.set(requestId, { resolve, reject });
|
|
worker.postMessage({ type: 'translate', id: requestId, text, source, target });
|
|
});
|
|
return 'ok';
|
|
} catch (error) {
|
|
window.__matrixError = String(error);
|
|
return 'error';
|
|
}
|
|
})()`);
|
|
if (init !== 'ok') throw new Error(`worker init failed: ${await evalJS('String(window.__matrixError || "unknown")')}`);
|
|
|
|
const ALL_LANGUAGES = ['en', 'es', 'ca', 'eu', 'gl', 'hi', 'th', 'ja', 'pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru', 'ko', 'zh-CN', 'zh-TW', 'uk', 'tr', 'vi', 'id', 'sv', 'cs', 'ro', 'el'];
|
|
const selected = (process.env.MATRIX_LANGUAGES ?? '').split(',').map((v) => v.trim()).filter(Boolean);
|
|
const languages = selected.length ? ['en', ...selected.filter((code) => code !== 'en')] : ALL_LANGUAGES;
|
|
const sourceText = 'The skater dropped into the bowl and landed a clean trick.';
|
|
const results = [];
|
|
const started = Date.now();
|
|
for (const language of languages.filter((code) => code !== 'en')) {
|
|
const t0 = Date.now();
|
|
let ok = false;
|
|
let forward = '';
|
|
let back = '';
|
|
let error = '';
|
|
try {
|
|
forward = await evalJS(`window.__matrix.translate(${JSON.stringify(sourceText)}, ${JSON.stringify('en')}, ${JSON.stringify(language)})`, true) ?? '';
|
|
back = await evalJS(`window.__matrix.translate(${JSON.stringify(forward)}, ${JSON.stringify(language)}, ${JSON.stringify('en')})`, true) ?? '';
|
|
ok = typeof forward === 'string' && forward.trim().length > 0 && typeof back === 'string' && back.trim().length > 0;
|
|
} catch (err) {
|
|
error = err instanceof Error ? err.message : String(err);
|
|
}
|
|
const ms = Date.now() - t0;
|
|
results.push({ language, ok, ms, forwardLength: forward.length, backLength: back.length, error });
|
|
writeFileSync('/tmp/opencode/translation-matrix.partial.json', JSON.stringify({ results }, null, 2));
|
|
console.log(`${language}: ${ok ? 'ok' : 'FAIL'} ${ms}ms forward=${forward.length} back=${back.length}${error ? ` (${error})` : ''}`);
|
|
}
|
|
const elapsed = Date.now() - started;
|
|
const heap = await evalJS('performance.memory ? performance.memory.usedJSHeapSize : null');
|
|
const report = { workerFile, languages: languages.length, results, elapsedMs: elapsed, jsHeapBytes: heap, exceptions };
|
|
writeFileSync('/tmp/opencode/translation-matrix.json', JSON.stringify(report, null, 2));
|
|
console.log(JSON.stringify({ ok: results.every((r) => r.ok), languages: results.length, elapsedMs: elapsed, jsHeapBytes: heap, exceptions: exceptions.length }));
|
|
ws.close(); cleanup(); process.exit(report.ok && exceptions.length === 0 ? 0 : 1);
|