Files
church-translate/app/_ctxprobe.mjs
T

99 lines
5.1 KiB
JavaScript
Raw Normal View History

import { pipeline, env } from '@huggingface/transformers';
env.allowLocalModels = false;
env.cacheDir = '/Users/Studio/Library/Application Support/Church Translate Models';
// --- chrF (character n-gram F-score, beta=2), written from scratch: no npm pkg exists for node
function ngrams(s, n) {
const t = s.replace(/\s+/g, '');
const m = new Map();
for (let i = 0; i + n <= t.length; i++) { const g = t.slice(i, i + n); m.set(g, (m.get(g) || 0) + 1); }
return m;
}
function chrF(hyp, ref, N = 6, beta = 2) {
let pS = 0, rS = 0, cnt = 0;
for (let n = 1; n <= N; n++) {
const H = ngrams(hyp, n), R = ngrams(ref, n);
let hs = 0, rs = 0, ov = 0;
for (const [g, c] of H) { hs += c; ov += Math.min(c, R.get(g) || 0); }
for (const [, c] of R) rs += c;
if (hs === 0 || rs === 0) continue;
pS += ov / hs; rS += ov / rs; cnt++;
}
if (!cnt) return 0;
const p = pS / cnt, r = rS / cnt;
if (p + r === 0) return 0;
return (1 + beta * beta) * p * r / (beta * beta * p + r);
}
const PAIRS = [
["Habakkuk stood upon his watchtower and waited to see what the Lord would say.", "And the Lord answered him and told him to write the vision plainly upon tablets."],
["Turn with me to Second Chronicles chapter seven verse fourteen.", "If my people who are called by my name will humble themselves and pray, I will heal their land."],
["For by grace you have been saved through faith, and that not of yourselves.", "It is the gift of God, not of works, so that no one may boast."],
["Melchizedek, king of Salem, brought out bread and wine.", "He was priest of God Most High, and he blessed Abram that day."],
["The Lord is my shepherd; I shall not want.", "He maketh me to lie down in green pastures and leadeth me beside the still waters."],
["Jesus wept at the tomb of Lazarus.", "Then he cried with a loud voice and called his friend out of the grave."],
["Paul wrote to the church at Philippi from a prison cell.", "He told them to rejoice in the Lord always, and again he said rejoice."],
["The elders fell down before the Lamb with harps and golden bowls of incense.", "And they sang a new song, saying, worthy art thou to take the scroll."],
];
const LANGS = [['spa_Latn','Spanish'],['hat_Latn','Haitian'],['arb_Arab','Arabic'],['kor_Hang','Korean'],['vie_Latn','Vietnamese']];
const t0 = Date.now();
const pipe = await pipeline('translation', 'Xenova/nllb-200-distilled-600M', { dtype: 'fp32' });
console.log(`MODEL LOAD: ${((Date.now()-t0)/1000).toFixed(1)}s`);
const tr = async (text, tgt) => {
const t = Date.now();
const r = await pipe(text, { src_lang: 'eng_Latn', tgt_lang: tgt });
return { text: (Array.isArray(r)?r[0]:r).translation_text.trim(), ms: Date.now()-t };
};
await tr(PAIRS[0][0], 'spa_Latn'); // warmup
const rows = [];
const tStart = Date.now();
for (const [code, name] of LANGS) {
for (const [s1, s2] of PAIRS) {
const a = await tr(s1, code);
const b = await tr(s2, code);
const c = await tr(s1 + ' ' + s2, code);
const simA = chrF(c.text, a.text);
const covB = chrF(c.text, b.text);
const lenRatio = c.text.length / a.text.length;
// clean-prefix test: does a.text appear verbatim at the head of c.text?
const prefixExact = c.text.startsWith(a.text);
// fuzzy prefix: chrF of the first a.text.length chars of c vs a
const prefixFuzzy = chrF(c.text.slice(0, a.text.length), a.text);
const dropped = simA > 0.85 && lenRatio < 1.35;
rows.push({ name, code, s1: s1.slice(0,28), a: a.text, b: b.text, c: c.text,
simA:+simA.toFixed(3), covB:+covB.toFixed(3), lenRatio:+lenRatio.toFixed(2),
prefixExact, prefixFuzzy:+prefixFuzzy.toFixed(3), dropped,
msA:a.ms, msB:b.ms, msC:c.ms });
}
}
const wall = (Date.now()-tStart)/1000;
console.log(`\nWALL CLOCK for ${rows.length*3} calls (${PAIRS.length} pairs x ${LANGS.length} langs x 3): ${wall.toFixed(1)}s`);
console.log(`=> per-call avg ${(wall*1000/(rows.length*3)).toFixed(0)}ms\n`);
console.log('lang dropRate meanCovB prefixExact prefixFuzzy meanLenRatio msA msC ratio');
for (const [code,name] of LANGS) {
const R = rows.filter(r=>r.code===code);
const f=(k)=> (R.reduce((s,r)=>s+(typeof r[k]==='boolean'? (r[k]?1:0) : r[k]),0)/R.length);
console.log(
name.padEnd(12) +
(f('dropped')*100).toFixed(0).padStart(6)+'% ' +
f('covB').toFixed(3).padStart(8) + ' ' +
(f('prefixExact')*100).toFixed(0).padStart(10)+'% ' +
f('prefixFuzzy').toFixed(3).padStart(10) + ' ' +
f('lenRatio').toFixed(2).padStart(12) + ' ' +
f('msA').toFixed(0).padStart(4) + ' ' + f('msC').toFixed(0).padStart(4) + ' ' +
(f('msC')/f('msA')).toFixed(2)
);
}
console.log('\n--- samples ---');
for (const r of rows.filter(r=>r.code==='hat_Latn'||r.code==='arb_Arab').slice(0,6)) {
console.log(`\n[${r.name}] ${r.s1}... dropped=${r.dropped} covB=${r.covB} prefixExact=${r.prefixExact}`);
console.log(' a:', r.a); console.log(' b:', r.b); console.log(' c:', r.c);
}
import fs from 'fs';
fs.writeFileSync('/private/tmp/claude-502/-Users-Studio-Documents-BandLineupDisplay/cee248f6-d861-4845-bd13-03867cf46596/scratchpad/rows.json', JSON.stringify(rows,null,1));