Skip to content

Commit 0212202

Browse files
committed
plugins/ml: projector/scorer registry, mirror/syzygy/motion projectors, VLM GAN-check scorer
Tried: a plugin layer beside the kernel (Node, not freestanding) with a typed params contract; synthetic 12-frame feeds with rendered ground truth; a VLM critic (Qwen3-VL-30B via DeepInfra) that reconstructs the scene from text. Found: VLM reading of the real frames vs truth = 0.919 mean (the ceiling); from text, mirror 0.068, mirror x2 frames 0.219, syzygy edge glyphs 0.306, motion 0.224 with motion recall 0.50 vs 0.00. The free tonal-inverse scorer ranks syzygy last, so it cannot stand in for the critic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014B7KcHL9b1jwug3L7dmsV3
1 parent 1d756e1 commit 0212202

40 files changed

Lines changed: 1088 additions & 0 deletions

‎plugins/ml/apis.mjs‎

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// =============================================================================
2+
// apis.mjs — the crew of external models, each behind one small function. Every
3+
// call is (a) cached on disk by a hash of its request, so a re-run of a playtest
4+
// replays for free and is reproducible, and (b) appended to a call log with its
5+
// latency and token usage, so every number in the docs can point to the raw
6+
// response that produced it.
7+
//
8+
// vlm(messages, opts) DeepInfra OpenAI-compatible chat (vision models)
9+
// llm(provider, messages) deepseek | kimi | zai | deepinfra text models
10+
// systemOne(state, questions) TypeSafe Jev typed answers (noul/choice/score)
11+
// quantumBytes(n) MothQuantum comet-qrng-v1 (emu mode by default)
12+
//
13+
// Keys come only from the environment; nothing here writes a key to disk.
14+
// Set SYZ_ML_OFFLINE=1 to forbid network calls (cache hits still work).
15+
// =============================================================================
16+
import { createHash } from 'node:crypto';
17+
import fs from 'node:fs';
18+
import path from 'node:path';
19+
import { fileURLToPath } from 'node:url';
20+
21+
const HERE = path.dirname(fileURLToPath(import.meta.url));
22+
export const CACHE_DIR = process.env.SYZ_ML_CACHE || path.join(HERE, 'cache');
23+
export const LOG_FILE = process.env.SYZ_ML_LOG || path.join(HERE, 'logs', 'calls.jsonl');
24+
const OFFLINE = () => process.env.SYZ_ML_OFFLINE === '1';
25+
26+
export const PROVIDERS = {
27+
deepinfra: { url: 'https://api.deepinfra.com/v1/openai/chat/completions', key: 'DEEPINFRA_KEY', model: 'Qwen/Qwen3-VL-30B-A3B-Instruct' },
28+
deepseek: { url: 'https://api.deepseek.com/chat/completions', key: 'DEEPSEEK_KEY', model: 'deepseek-chat' },
29+
kimi: { url: 'https://api.moonshot.ai/v1/chat/completions', key: 'KIMIAI_KEY', model: 'kimi-k2.6' },
30+
zai: { url: 'https://api.z.ai/api/paas/v4/chat/completions', key: 'ZAI_KEY', model: 'glm-4.5-flash',
31+
extra: { thinking: { type: 'disabled' } } },
32+
};
33+
34+
export const stats = { calls: 0, cacheHits: 0, byProvider: {} };
35+
36+
function cacheKey(obj) { return createHash('sha256').update(JSON.stringify(obj)).digest('hex').slice(0, 24); }
37+
function logCall(rec) {
38+
fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
39+
fs.appendFileSync(LOG_FILE, JSON.stringify(rec) + '\n');
40+
}
41+
// Strip base64 image payloads from what we log (they are reproducible from the scene).
42+
function redactImages(messages) {
43+
return JSON.parse(JSON.stringify(messages, (k, v) =>
44+
typeof v === 'string' && v.startsWith('data:image') ? `<image ${v.length}B sha=${cacheKey(v)}>` : v));
45+
}
46+
47+
async function cached(kind, request, doFetch, { tag = '', nocache = false } = {}) {
48+
const key = cacheKey({ kind, request });
49+
const file = path.join(CACHE_DIR, kind, key + '.json');
50+
if (!nocache && fs.existsSync(file)) { stats.cacheHits++; return { ...JSON.parse(fs.readFileSync(file, 'utf8')), cached: true }; }
51+
if (OFFLINE()) throw new Error(`offline: no cache for ${kind} ${key}`);
52+
const t0 = Date.now();
53+
const out = await doFetch();
54+
out.ms = Date.now() - t0;
55+
stats.calls++; stats.byProvider[kind] = (stats.byProvider[kind] || 0) + 1;
56+
fs.mkdirSync(path.dirname(file), { recursive: true });
57+
fs.writeFileSync(file, JSON.stringify(out));
58+
logCall({ at: new Date().toISOString(), kind, tag, key, ms: out.ms, usage: out.usage || null,
59+
model: out.model || null, text: out.text?.slice(0, 4000) ?? null, error: out.error || null });
60+
return { ...out, cached: false };
61+
}
62+
63+
async function postJSON(url, headers, body, timeoutMs = 120000, tries = 3) {
64+
let last;
65+
for (let i = 0; i < tries; i++) {
66+
try {
67+
const r = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers },
68+
body: JSON.stringify(body), signal: AbortSignal.timeout(timeoutMs) });
69+
const txt = await r.text();
70+
if (r.status === 429 || r.status >= 500) { last = new Error(`${r.status} ${txt.slice(0, 200)}`); await new Promise((s) => setTimeout(s, 2000 * (i + 1))); continue; }
71+
if (!r.ok) throw new Error(`${r.status} ${txt.slice(0, 400)}`);
72+
return JSON.parse(txt);
73+
} catch (e) { last = e; if (i < tries - 1) await new Promise((s) => setTimeout(s, 2000 * (i + 1))); }
74+
}
75+
throw last;
76+
}
77+
78+
// OpenAI-compatible chat. Returns { text, usage, model }.
79+
export async function chat(provider, messages, { model, max_tokens = 800, temperature = 0, tag, nocache, json = false } = {}) {
80+
const p = PROVIDERS[provider];
81+
if (!p) throw new Error('unknown provider ' + provider);
82+
const key = process.env[p.key];
83+
const body = { model: model || p.model, messages, max_tokens, temperature, ...(p.extra || {}),
84+
...(json ? { response_format: { type: 'json_object' } } : {}) };
85+
return cached(provider, { ...body, messages: body.messages }, async () => {
86+
if (!key) throw new Error(`missing ${p.key}`);
87+
const d = await postJSON(p.url, { Authorization: `Bearer ${key}` }, body);
88+
const text = d.choices?.[0]?.message?.content ?? '';
89+
return { text, usage: d.usage || null, model: d.model || body.model, request: redactImages(messages) };
90+
}, { tag, nocache });
91+
}
92+
93+
export const vlm = (messages, opts = {}) => chat('deepinfra', messages, opts);
94+
export const llm = (provider, messages, opts = {}) => chat(provider, messages, opts);
95+
96+
// TypeSafe Jev: typed answers. questions = { name: {type:'noul'|'choice'|'score', instructions, criteria?} }
97+
export async function systemOne(state, questions, { model = 'jev-latest', tag, nocache } = {}) {
98+
const body = { model, state, questions };
99+
return cached('typesafe', body, async () => {
100+
const key = process.env.TYPESAFEAI_KEY; if (!key) throw new Error('missing TYPESAFEAI_KEY');
101+
const d = await postJSON('https://api.typesafe.ai/v1/systemone', { Authorization: `Bearer ${key}` }, body, 60000);
102+
return { answers: d.answers, usage: d.usage, model: d.model, text: JSON.stringify(d.answers) };
103+
}, { tag, nocache });
104+
}
105+
106+
// MothQuantum comet-qrng-v1. mode 'emu' = Aer simulator (uncertified); 'qpu' = IBM hardware.
107+
// Draws are never cached (a replayed random stream would defeat the point), but are logged.
108+
export async function quantumBytes(n = 32, { mode = 'emu', tag } = {}) {
109+
if (OFFLINE()) throw new Error('offline: quantum draws are never cached');
110+
const base = process.env.MOTHQUANTUM_BASE || 'https://api.mothquantum.com/api/v1';
111+
const H = { Authorization: `Bearer ${process.env.MOTHQUANTUM_KEY}` };
112+
const t0 = Date.now();
113+
const sub = await postJSON(`${base}/engines/comet-qrng-v1/process`, H,
114+
{ mode, params: { num_qubits: 12, shots: Math.max(256, n * 8), output_bytes: n, bell_witness: false } }, 60000);
115+
for (let i = 0; i < 40; i++) {
116+
await new Promise((s) => setTimeout(s, 1500));
117+
const st = await (await fetch(`${base}/jobs/${sub.job_id}/status`, { headers: H })).json();
118+
if (st.status === 'failed') throw new Error('qrng job failed: ' + JSON.stringify(st.error));
119+
if (st.status === 'completed') {
120+
const res = await (await fetch(`${base}/jobs/${sub.job_id}/result`, { headers: H })).json();
121+
const out = res.result.output;
122+
const rec = { hex: out.random.hex, bytes: out.random.bytes, job_id: sub.job_id, mode,
123+
commit: out.commitment?.commit, ms: Date.now() - t0 };
124+
stats.calls++; stats.byProvider.quantum = (stats.byProvider.quantum || 0) + 1;
125+
logCall({ at: new Date().toISOString(), kind: 'quantum', tag, key: sub.job_id, ms: rec.ms, text: rec.hex, mode });
126+
return rec;
127+
}
128+
}
129+
throw new Error('qrng job timed out');
130+
}
131+
132+
// Pull the first JSON object out of a model reply (models wrap JSON in prose/fences).
133+
export function extractJSON(text) {
134+
if (!text) return null;
135+
const t = text.replace(/```(?:json)?/g, '');
136+
const s = t.indexOf('{'); if (s < 0) return null;
137+
let depth = 0, inStr = false, esc = false;
138+
for (let i = s; i < t.length; i++) {
139+
const c = t[i];
140+
if (inStr) { if (esc) esc = false; else if (c === '\\') esc = true; else if (c === '"') inStr = false; continue; }
141+
if (c === '"') inStr = true; else if (c === '{') depth++;
142+
else if (c === '}' && --depth === 0) { try { return JSON.parse(t.slice(s, i + 1)); } catch { return null; } }
143+
}
144+
return null;
145+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"text":"{\"setting\": \"outdoor-night\", \"scene\": \"A red car is on a road in front of two tall buildings under a crescent moon.\", \"objects\": [{\"label\": \"car\", \"category\": \"vehicle\", \"region\": \"bottom-right\", \"box\": [0.64, 0.75, 0.84, 0.85], \"moving\": \"left\"}, {\"label\": \"building\", \"category\": \"building\", \"region\": \"center\", \"box\": [0.48, 0.25, 0.56, 0.7], \"moving\": \"none\"}, {\"label\": \"building\", \"category\": \"building\", \"region\": \"right\", \"box\": [0.78, 0.25, 0.86, 0.7], \"moving\": \"none\"}, {\"label\": \"moon\", \"category\": \"celestial\", \"region\": \"top-left\", \"box\": [0.05, 0.15, 0.15, 0.35], \"moving\": \"none\"}]}","usage":{"prompt_tokens":460,"total_tokens":701,"completion_tokens":241,"estimated_cost":0.00021360000000000001,"prompt_tokens_details":null},"model":"Qwen/Qwen3-VL-30B-A3B-Instruct","request":[{"role":"user","content":[{"type":"text","text":"You are looking at two frames from the same fixed camera, a moment apart (the first image is earlier, the second is the latest).\nDescribe the scene as it is in the LATEST frame, and for each object say which way it moved between the frames (\"none\" if it did not move).\nReturn ONLY a JSON object, no prose:\n{\"setting\": \"outdoor-day\" | \"outdoor-night\" | \"indoor\" | \"unknown\",\n \"scene\": \"<one short sentence>\",\n \"objects\": [{\"label\": \"<noun>\", \"category\": \"person\" | \"animal\" | \"vehicle\" | \"building\" | \"plant\" | \"celestial\" | \"furniture\" | \"shape\" | \"weather\" | \"other\",\n \"region\": \"top-left\" | \"top\" | \"top-right\" | \"left\" | \"center\" | \"right\" | \"bottom-left\" | \"bottom\" | \"bottom-right\",\n \"box\": [x0, y0, x1, y1],\n \"moving\": \"left\" | \"right\" | \"up\" | \"down\" | \"none\"}]}\nbox is the object's bounding box as fractions of the image width/height (0 = left/top, 1 = right/bottom).\nList each distinct object once (at most 8). Do not list ground, sky, walls, floor or other background surfaces as objects."},{"type":"image_url","image_url":{"url":"<image 1742B sha=3d72f12fa75091fd608560de>"}},{"type":"image_url","image_url":{"url":"<image 1754B sha=05a08269d652078c37ec4c19>"}}]}],"ms":6073}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"text":"{\"setting\": \"outdoor-day\", \"scene\": \"a landscape with a road and a vehicle moving from left to right\", \"objects\": [{\"label\": \"vehicle\", \"category\": \"vehicle\", \"region\": \"center\", \"box\": [0.4, 0.5, 0.6, 0.6], \"moving\": \"right\"}]}","usage":{"prompt_tokens":703,"total_tokens":781,"completion_tokens":78,"estimated_cost":0.00015225000000000001,"prompt_tokens_details":null},"model":"Qwen/Qwen3-VL-30B-A3B-Instruct","request":[{"role":"user","content":"The text below is a character-art rendering of ONE view from a fixed camera. Each character stands for a small patch of the image: denser characters are brighter or darker patches depending on the renderer, lines may mark edges, and some renderings mark motion with arrow characters (> < ^ v or → ← ↑ ↓) and '~' for where something just was. It may contain several frames of the same view separated by blank lines, oldest first.\nReconstruct the scene in the latest frame: what objects are there, where, and which way each is moving (\"none\" if you cannot tell or it is still).\nReturn ONLY a JSON object, no prose:\n{\"setting\": \"outdoor-day\" | \"outdoor-night\" | \"indoor\" | \"unknown\",\n \"scene\": \"<one short sentence>\",\n \"objects\": [{\"label\": \"<noun>\", \"category\": \"person\" | \"animal\" | \"vehicle\" | \"building\" | \"plant\" | \"celestial\" | \"furniture\" | \"shape\" | \"weather\" | \"other\",\n \"region\": \"top-left\" | \"top\" | \"top-right\" | \"left\" | \"center\" | \"right\" | \"bottom-left\" | \"bottom\" | \"bottom-right\",\n \"box\": [x0, y0, x1, y1],\n \"moving\": \"left\" | \"right\" | \"up\" | \"down\" | \"none\"}]}\nbox is the object's bounding box as fractions of the image width/height (0 = left/top, 1 = right/bottom).\nList each distinct object once (at most 8). Do not list ground, sky, walls, floor or other background surfaces as objects.\n\n```\n****************************************************************\n****************************************************##%%%#******\n***************************************************#%%%%%%%*****\n***************************************************%%%%%%%%*****\n*************+--=***********************************#%%%%#******\n**********+=::::::-+********************************************\n********=-::::::::::-=************************+=----=+**********\n*****+=::::::::::::::::-+********************----------*********\n######*++++++++++++++++*####################=----------=########\n######*+++++++++++*@@#+*####################+----------=########\n######*++++++-::-+****+*#####################+--------=#########\n######*++++++:..-++++++*#######################*=::=+*##########\n******+++++++-::-+++++++************************+::+************\n=================================================::=============\n===============###*-============================================\n============------------========================================\n============-...----:..-========================================\n=============:.:====-.:=========================================\n================================================================\n\n****************************************************************\n****************************************************##%%%#******\n***************************************************#%%%%%%%*****\n***************************************************%%%%%%%%*****\n*************+--=***********************************#%%%%#******\n**********+=::::::-+********************************************\n********=-::::::::::-=************************+=----=+**********\n*****+=::::::::::::::::-+********************----------*********\n######*++++++++++++++++*####################=----------=########\n######*+++++++++++*@@#+*####################+----------=########\n######*++++++-::-+****+*#####################+--------=#########\n######*++++++:..-++++++*#######################*=::=+*##########\n******+++++++-::-+++++++************************+::+************\n=================================================::=============\n============================================-+###+-=============\n===========================================------------=========\n===========================================-..:----...-=========\n============================================:.-====:.:==========\n================================================================\n```"}],"ms":4710}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"text":"{\"setting\": \"outdoor-night\", \"scene\": \"A red car is on a road in front of two tall buildings under a crescent moon.\", \"objects\": [{\"label\": \"car\", \"category\": \"vehicle\", \"region\": \"bottom-left\", \"box\": [0.24, 0.75, 0.44, 0.85], \"moving\": \"left\"}, {\"label\": \"building\", \"category\": \"building\", \"region\": \"top-right\", \"box\": [0.78, 0.25, 0.88, 0.75], \"moving\": \"none\"}, {\"label\": \"building\", \"category\": \"building\", \"region\": \"top-center\", \"box\": [0.47, 0.25, 0.57, 0.75], \"moving\": \"none\"}, {\"label\": \"moon\", \"category\": \"celestial\", \"region\": \"top-left\", \"box\": [0.05, 0.12, 0.15, 0.3], \"moving\": \"none\"}]}","usage":{"prompt_tokens":460,"total_tokens":704,"completion_tokens":244,"estimated_cost":0.0002154,"prompt_tokens_details":null},"model":"Qwen/Qwen3-VL-30B-A3B-Instruct","request":[{"role":"user","content":[{"type":"text","text":"You are looking at two frames from the same fixed camera, a moment apart (the first image is earlier, the second is the latest).\nDescribe the scene as it is in the LATEST frame, and for each object say which way it moved between the frames (\"none\" if it did not move).\nReturn ONLY a JSON object, no prose:\n{\"setting\": \"outdoor-day\" | \"outdoor-night\" | \"indoor\" | \"unknown\",\n \"scene\": \"<one short sentence>\",\n \"objects\": [{\"label\": \"<noun>\", \"category\": \"person\" | \"animal\" | \"vehicle\" | \"building\" | \"plant\" | \"celestial\" | \"furniture\" | \"shape\" | \"weather\" | \"other\",\n \"region\": \"top-left\" | \"top\" | \"top-right\" | \"left\" | \"center\" | \"right\" | \"bottom-left\" | \"bottom\" | \"bottom-right\",\n \"box\": [x0, y0, x1, y1],\n \"moving\": \"left\" | \"right\" | \"up\" | \"down\" | \"none\"}]}\nbox is the object's bounding box as fractions of the image width/height (0 = left/top, 1 = right/bottom).\nList each distinct object once (at most 8). Do not list ground, sky, walls, floor or other background surfaces as objects."},{"type":"image_url","image_url":{"url":"<image 1754B sha=7a1795c4a4d290dc41c60bac>"}},{"type":"image_url","image_url":{"url":"<image 1754B sha=05a08269d652078c37ec4c19>"}}]}],"ms":7039}

0 commit comments

Comments
 (0)