forked from akai/readest
b1346bf16d
* feat(wordlens): support en-en monolingual glosses Gloss difficult English words with a short English definition for learners reading English with English hints. - build: buildEnEn + shortDefGloss read ECDICT's English `definition` column (first/primary WordNet sense, POS-stripped, drop ;-example, <=24 word-boundary with trailing-connector trim). New `en-en` CLI branch; buildEnZh/buildEnEn now share a buildEnPack core. - gating: drop the hardcoded `hint === source` rejections (wordlensSection, WordLensPanel) so same-language packs are allowed; availability is decided by the manifest (resolvePack returns null when no pack exists). - data: data/wordlens/en-en.json (26,578 entries) + regenerated manifest.json. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(wordlens): gloss styling, derivation lemmas, display-time cap Builds on the en-en monolingual gloss support with refinements and regenerated packs. - settings: per-book gloss <rt> font size (em) and color in Settings > Language > Word Lens (getRubyStyles reads viewSettings). - en-en hints: WordNet hybrid (a simpler synonym, else a category hypernym, else the ECDICT definition) instead of raw verbose definitions. - lemmatization: gate difficulty by the lemma rank for every English source pair. enBaseFormCandidates now also covers -able/-ible suffixes and negative prefixes (un/in/im/ir/il), so insufferable resolves to suffer. A candidate is accepted when the English definition names the base OR the Chinese translations share a content character, which keeps true derivations (insufferable -> suffer) and rejects coincidental stems (capable -> cap). en-X packs inherit the en-en lemma table. - display cap: the max gloss length is applied at render time in cleanGloss (MAX_GLOSS_LEN), so the packs store the full hint and the cap can change without regenerating data. - tooling: pnpm wordlens:preview to sample pack entries; cache build corpora under data/wordlens/.sources (gitignored). - data: regenerate en-en, en-zh and en-de/es/fr/pt/ru plus the manifest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
67 lines
2.7 KiB
JavaScript
67 lines
2.7 KiB
JavaScript
// Preview sample entries from the generated Word Lens gloss packs, to eyeball gloss
|
|
// quality per language pair. Read-only; does not modify any pack.
|
|
//
|
|
// node scripts/preview-wordlens.mjs # a sample from every pack
|
|
// node scripts/preview-wordlens.mjs en-en # a larger sample from one pair
|
|
// node scripts/preview-wordlens.mjs en-en happy commence thickly # specific words
|
|
//
|
|
// (pnpm wordlens:preview [pair] [...words])
|
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
import { resolve, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const DATA_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../data/wordlens');
|
|
|
|
// Pick `count` entries spread evenly across the frequency-rank range (commonest
|
|
// first, rarest last) so a preview shows both easy and hard words. Deterministic.
|
|
export function sampleEntries(entries, count = 12) {
|
|
const sorted = Object.entries(entries).sort((a, b) => a[1].r - b[1].r);
|
|
if (sorted.length <= count) return sorted;
|
|
const step = (sorted.length - 1) / (count - 1);
|
|
return Array.from({ length: count }, (_, i) => sorted[Math.round(i * step)]);
|
|
}
|
|
|
|
function previewPack(pair, words, count) {
|
|
const path = resolve(DATA_DIR, `${pair}.json`);
|
|
if (!existsSync(path)) {
|
|
console.log(`\n${pair}: (no pack file)`);
|
|
return;
|
|
}
|
|
const { meta, entries, inflections } = JSON.parse(readFileSync(path, 'utf8'));
|
|
const n = Object.keys(entries).length;
|
|
console.log(`\n${pair} — ${meta.source}→${meta.target}, ${n} entries, metric=${meta.metric}`);
|
|
const rows = words.length
|
|
? words.map((w) => {
|
|
const key = w.toLowerCase();
|
|
const lemma = inflections[key];
|
|
const entry = entries[key] ?? (lemma ? entries[lemma] : null);
|
|
return [lemma ? `${key} → ${lemma}` : key, entry];
|
|
})
|
|
: sampleEntries(entries, count);
|
|
for (const [word, entry] of rows) {
|
|
if (!entry) console.log(` ${'—'.padStart(6)} ${String(word).padEnd(22)} (no entry)`);
|
|
else console.log(` ${String(entry.r).padStart(6)} ${String(word).padEnd(22)} ${entry.g}`);
|
|
}
|
|
if (!words.length) {
|
|
const infl = Object.entries(inflections)
|
|
.slice(0, 6)
|
|
.map(([f, l]) => `${f}→${l}`);
|
|
if (infl.length) console.log(` inflections: ${infl.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
function listPairs() {
|
|
return readdirSync(DATA_DIR)
|
|
.filter((f) => f.endsWith('.json') && f !== 'manifest.json')
|
|
.map((f) => f.replace(/\.json$/, ''))
|
|
.sort();
|
|
}
|
|
|
|
function main() {
|
|
const [pair, ...words] = process.argv.slice(2);
|
|
if (pair) previewPack(pair, words, words.length ? words.length : 25);
|
|
else for (const p of listPairs()) previewPack(p, [], 12);
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) main();
|