A static Modern Eastern Armenian reading trainer for Russian-speaking beginners. Type a reading in Cyrillic or Latin, press Enter to check, and Enter again for the next word. “Не знаю” reveals the reading and records a skip. This is a complete client-side training product: it includes adaptive selection, progress tracking, typography and font practice, flash recognition, metadata hints, bilingual UI, and local persistence.
Try it online at vank.alexesprit.com.
Node.js 24 or later:
npm ci
npm run build:seed # required once in a fresh checkout
npm run dev
npm run test:run
npm run build
npm run previewweb/data/*.json are minified build artifacts and are not stored in Git.
Run npm run build:seed first after cloning (or after removing the artifact),
then local development and tests use that deterministic 79-word dictionary.
The downloader resolves enabled assets from the latest GitHub Release and
verifies each asset against GitHub's SHA-256 digest. Production deployment is
owned by .github/workflows/deploy.yml: after successful CI on main, it
rebuilds curated packs without AI, publishes them alongside the current main
dictionary, then deploys with the Vercel CLI. Release publishing is
content-addressed, so unchanged assets do not create another release. Vercel's
Git-triggered deployments are disabled for all branches; GitHub Actions handles
production deployment with the Vercel CLI.
Add these repository Actions secrets for production deployment:
VERCEL_TOKEN, VERCEL_ORG_ID, and VERCEL_PROJECT_ID.
The AI-enriched words target stays a manual build and publish. The CI deploy
downloads the latest release, carries its words asset forward unchanged, and
updates only curated packs. After changing the main dictionary locally, build
and publish the complete release; publishing dispatches a deploy-only run:
npm run dict:download -- --force
npm run dict:build -- --target words
npm run dict:publishdict:publish reruns the production quality gate from
builder/config.json before publishing and dispatching the deployment workflow.
That dispatch deploys the release as published without rebuilding packs. The
CI-gated workflow publishes packs first and uses --no-deploy to avoid
dispatching itself a second time.
The release contains only the runtime files. Rich provenance and enrichment data
remain in the builder intermediates, including builder/data/words.json.
Deterministic letter decomposition is also omitted and reconstructed when the
app validates the downloaded dictionary.
Deploy web/dist/ to any static host. Asset URLs are relative, including the
versioned dictionary, so deployment under a GitHub Pages repository path works.
No backend, account, or runtime AI. Progress and learner answers never leave
the browser. Optional, cookie-free Umami analytics sends page views and limited
per-visit practice milestone/return events in production; development logs the
same events to the console. Both modes require opt-in and are blocked by Do Not Track.
Optional Armenian fonts are fetched lazily from Google Fonts only when selected
for training.
The small i18next runtime dependency is intentional: it provides locale
fallbacks and pluralization without maintaining a second interpolation system.
Interface icons use the pinned, SRI-verified Lucide asset loaded from unpkg.
Progress and raw attempt events stay in IndexedDB on the current browser origin.
npm test runs Vitest in watch mode. Tests use offline fixtures and
fake-indexeddb; they do not call Wiktionary or OpenRouter.
The dictionary targets Russian-recognizable vocabulary, up to the configured maximum. Familiarity scores are curated or AI estimates, not learner-study results. A separate 79-word curated seed remains available for offline development.
npm run build:seed # reproducible curated-only runtime dictionary
npm run fetch # download/cache source exports and save raw.json
npm run normalize # re-read raw.json; normalize and merge fields
npm run derive # read normalized.json; derive technical metadata
npm run enrich # read deterministic.json; cached provider batches
npm run eval:enrichment # run the no-cache provider evaluation fixture
npm run validate # apply overrides, compose, validate, emit words.json
npm run dict:build # build all enabled targets from builder/targets.json
npm run dict:build -- --pack # build only targets marked pack in builder/targets.json
npm run dict:publish # publish target assets in one GitHub release
npm run dict:download # use existing outputs, or download missing targets
npm run dict:download -- --force # refresh all enabled targets from the latest releasebuilder/targets.json defines each dictionary target: its builder config,
intermediate directory, runtime output, release asset name, enrichment mode,
and validation checks. The manifest ships words, toponyms, countries,
names, food, numbers, and cars; the latter six are finite curated packs built
from matching builder/data/curated-*.json sources.
To update a pack manually, run npm run dict:download -- --force, build with
npm run dict:build -- --pack, inspect the target intermediates and generated
runtime JSON, then publish all enabled assets with npm run dict:publish.
Changes pushed to main are built and deployed by the CI-gated deployment
workflow.
Word IDs are derived from canonical Armenian forms. Identical forms therefore share global letter and word progress across modes; each dictionary keeps its own metadata and selection order.
Configure sources, explicit priorities, learner languages, and the output ceiling in
builder/config.json. Source paths are relative to the repository working
directory. The Wiktionary adapter reads structured Wiktextract JSONL extracted
from Wikimedia dumps; it does not scrape HTML. A local export can replace the
configured download. Curated records are first-class vocabulary sources and can
introduce words absent from Wiktionary. Higher-priority sources win only fields
they supply, including individual language-map values. Same-priority duplicate
records use the last supplied field value; definitions and provenance accumulate.
For live enrichment, create an ignored .env with the variable names in
.env.example, or export AI_PROVIDER, AI_MODEL, and the provider credentials.
The default provider is openrouter; ollama uses the local
http://localhost:11434/v1/chat/completions endpoint and does not require a key.
The model must support structured JSON-schema responses. AI_ENDPOINT overrides
the provider endpoint. AI_CONCURRENCY and AI_BATCH_SIZE control parallel
requests and words per request; legacy OPENROUTER_* variables remain supported
as fallbacks.
ru is the default enrichment language; adding languages is a configuration change.
The configured audience policy plans words before AI enrichment:
builder/data/recognizable-ru.jsonpairs attested Armenian spellings with recognition hints and an optionalpurpose(familiarorverification). Hints are hypotheses, not guaranteed familiarity scores. Add reviewed concepts here to expand the vocabulary.- Curated words are assessed first, followed by imported candidates in
reviewed-list order, then the remaining Wiktionary vocabulary by source
frequency and length. Missing reviewed spellings are reported, never invented.
Enrichment stops once the output ceiling and audience-quality checks are satisfied.
selected-names.jsonexplicitly allows source-attested names through the otherwise name-excluding Wiktionary adapter. - AI receives source definitions, parts of speech, and recognition hints. It checks current usage, meaning, recognizability, and beginner usefulness.
- Final composition follows the configured maximum, familiar-word minimum, and familiar-share requirement. Imported words need AI confidence >= 0.8; flagged words are excluded. Usefulness ranks familiar words. Verification words must be explicitly selected or curated, have familiarity <= 0.4, and imported verification words also need usefulness >= 0.5. Composition includes verification words only while the familiar share remains at least 70%.
- Every written Armenian letter must occur in at least two selected words.
The CLI,
audience-report.json, and GitHub release notes report coverage per letter; the JSON report also separates familiar and verification coverage.
maxWords is a ceiling, not a target to pad with arbitrary words. Quality-gate
failure leaves the previous runtime dictionary intact. candidates.json records
the shortlist and missing candidates. The concept list is reviewed and intentionally finite; it does not exhaust
all Armenian loanwords. The optional node builder/scripts/discover.ts command
uses the configured AI model to screen derived source entries in cached batches
and writes builder/data/discovery-proposals.json. Review proposals before
adding them to the candidate list; discovery never promotes them automatically.
The curated ԿՈՄԲՈ entry is attested at https://artlunch.am/menu.
node builder/src/cli.ts build --curated-only --no-ai --output /tmp/vank-review/words.json
npm run enrich -- --verbose
npm run validate -- --quiet--no-ai explicitly skips AI for seed/fixture/review builds. It does not claim
missing semantic scores have been estimated. A full audience build without AI
can fail its familiarity gate; use --curated-only for the offline seed. All commands support --quiet,
--verbose, --config FILE, --data-dir DIR, and --output FILE.
--curated-only excludes external sources. --help lists stages and options.
Intermediate files (raw, normalized, deterministic, enriched, words,
rejected) remain separate under the selected data directory. Successful AI
items are cached individually in the sibling cache/ directory, keyed by word
identity/reading, source definitions, parts of speech, recognition hints, model,
learner languages, prompt version, and AI schema version.
Changing batch size or restarting after a failed batch reuses successful items.
Invalid AI JSON, mismatched IDs, and invalid category/score data are rejected.
Transient failures have bounded retries and request timeouts. Progress reports
counts, percentage when the total is known, batches, cache/API work, rejected
entries, execution/API failures, retries, elapsed time, and ETA based on newly
processed work rather than cache hits. Source exclusions and flagged words
count as rejections, not failures; repeated validation does not duplicate them.
The enrichment evaluation fixture under builder/evals/enrichment/ intentionally
does not cache results. It uses the same schema and dictionary validation as the
build and compares provider output with human-reviewed references. The 30-word
fixture covers common vocabulary, loans, names, categories, and deliberate
recognition-hint false friends; ԳՐԱԲԱՐ is flagged because it is historical
language content outside the beginner/current-word target.
Run it against either provider, for example:
AI_PROVIDER=ollama OLLAMA_MODEL=qwen3-coder:30b npm run eval:enrichment
AI_PROVIDER=openrouter OPENROUTER_MODEL=... npm run eval:enrichmentUse --output FILE to save the JSON metrics and per-word diff report;
--source, --reference, --language, --batch-size, --concurrency, and
--max-retries are also available for custom evaluation sets.
Put corrections in builder/data/overrides.json, keyed by the stable word ID:
{
"hy-54f-531-554-54d-53b": {
"familiarity": {"ru": 1},
"acceptedLatin": ["taksi", "taxi"],
"categories": ["transport"]
}
}Overrides run after enrichment and before composition/validation. Curated technical fields such as IDs, letters, units, and canonical readings are derived locally, even if an AI authored the source file. Curated semantic corrections are retained during enrichment. Optional pronunciation corrections must retain consistent source units and accepted readings; invalid output fails the build without replacing the previous runtime dictionary.
Builder intermediates retain source URLs, licensing, definitions, per-field
metadata provenance, and AI version/model metadata. The minified runtime output
omits those build-only fields. The builder writes attribution beside its output
as an ignored local sidecar. The committed
web/public/ATTRIBUTION.txt is copied into every static deployment. Licensing
references and the changes to imported material are recorded there.
The application source code is available under the MIT License; see
LICENSE.md.
The bundled seed vocabulary is project-owned curated data. Dictionaries rebuilt
with Wiktionary records include adapted material under Creative Commons
Attribution-ShareAlike 4.0 where indicated. See
web/public/ATTRIBUTION.txt for source credits,
licensing references, and a description of the dataset changes.