Skip to content

feat: generic per-language text corpora (data/corpora) - #63

Merged
willwade merged 6 commits into
mainfrom
feat/text-corpora
Aug 29, 2026
Merged

feat: generic per-language text corpora (data/corpora)#63
willwade merged 6 commits into
mainfrom
feat/text-corpora

Conversation

@willwade

Copy link
Copy Markdown
Collaborator

New language asset alongside freq/audio/alphabets — nothing consumer-specific (this replaces the closed #62, which wrongly put a Dasher-format exporter here; the Dasher-side consumer now lives in dasher-project/DasherCore#73).

What

scripts/build_text_corpora.py builds data/corpora/<lang>.txt — natural text, one sentence per line, ~400KB per language — for every language with freq data (193 on disk).

  • Synthetic mode today: Zipf-weighted sampling from the ranked freq/top1000 lists — preserves character- and word-shape statistics
  • Sentence sources later: CommonVoice (CC-0) and Tatoeba (CC-BY 2.0 FR) become the priority inputs once wired
  • Provenance: SOURCES.json per corpus — generator, pipeline, license note, and verify: true until the per-language source chain is confirmed clean (freq pipeline mixes Leipzig research-use terms with clean sources). Consumers respect the flag.

Checks

ruff check (0.16.5) and mypy clean on the new script — run locally before pushing this time.

DCO signed.

New language asset alongside freq/audio/alphabets: data/corpora/<lang>.txt
— natural text, one sentence per line, ~400KB per language — with
SOURCES.json provenance per corpus (generator, mode, pipeline, license
note, verify flag).

Synthetic mode today: Zipf-weighted sampling from the ranked word lists
in data/freq/top1000 (193 languages on disk), which preserves the
character- and word-shape statistics of the source corpus. Sentence
sources take precedence once wired: CommonVoice transcripts (CC-0) and
Tatoeba (CC-BY 2.0 FR) are the priority inputs.

Licensing: every corpus is verify:true until its per-language source
chain is confirmed clean — the freq pipeline mixes Leipzig (research-use
terms) with clean sources. Consumers must respect the flag; DasherCore's
importer (dasher-project/DasherCore#73) refuses verify:true corpora by
default.

ruff + mypy clean.

Signed-off-by: will wade <willwade@gmail.com>
Full run of scripts/build_text_corpora.py: one corpus per language with
freq data (400KB synthetic text each, 74MB total — comparable to the
existing data/audio asset). All carry verify:true in SOURCES.json until
their per-language source chain is reviewed; consumers (e.g.
dasher-project/DasherCore#73) refuse verify:true corpora by default.

Signed-off-by: will wade <willwade@gmail.com>
…nifest)

Same access pattern as the existing freq/alphabets data, across the
package's entry points:

- index.js (CJS/Node): loadCorpus(code) reads data/corpora/<code>.txt
  from disk; listCorpora() reads SOURCES.json (always shipped in the
  npm tarball) — lang, mode, verify flag, license note
- index.mjs (ESM/browser): listCorpora() from a generated
  dist/browser-corpora.mjs MANIFEST bundle (the 74MB of text is
  deliberately not embedded — same reasoning as audio); loadCorpus()
  works under Node ESM via dynamic fs import and throws descriptive
  guidance in the browser
- index.d.ts: CorpusInfo / Corpus types
- package.json files: corpus .txt excluded from the npm tarball
  (!data/corpora/*.txt) with SOURCES.json shipped — listCorpora() works
  from npm; the text lives in the repo / vendored checkouts
- jest: 3 tests (manifest listing, repo-checkout load, descriptive
  error) — full suite 54 passed

The Python package (src/worldalphabets) gets its accessors alongside
the lint baseline merge (follow-up commit).

Signed-off-by: will wade <willwade@gmail.com>
@willwade

Copy link
Copy Markdown
Collaborator Author

Now includes the consumer API (npm/Node CJS, Node ESM, browser manifest, TS types) — listCorpora() ships everywhere via SOURCES.json, loadCorpus(code) reads the text from repo checkouts, and the 74MB of .txt is excluded from the npm tarball like audio. Jest suite: 54 passed.

CI note: Test Python on this branch will keep failing on the pre-existing ruff violations until #64 (the pinned-ruff baseline) merges — merge #64 first, then this rebases/merges green.

The first cut built corpora by sampling words from the frequency lists
with Zipf weights — plausible character statistics, but the sentences
were nonsense ('A a múlt oh a gyernek a ez a benned a!'). For a
text-entry corpus that's the wrong trade: word-order and inter-word
context matter, and the label 'natural text' was simply false.

build_text_corpora.py now downloads the Tatoeba per-language exports
(cached under .cache/tatoeba/):

- CC0 subset used where a language has substantive coverage (>=500
  sentences), otherwise the full export — CC-BY 2.0 FR with
  attribution (https://tatoeba.org) recorded in every SOURCES.json
  entry and in the README
- verify:false for Tatoeba-derived corpora: licensing-clean WITH
  attribution, so downstream importers (DasherCore #73) can consume
  them without the review gate
- languages without Tatoeba coverage are skipped and reported —
  a missing corpus is honest; a synthetic one is not. The synthetic
  mode is removed, deliberately
- 167 languages covered (was 193 synthetic); 27MB (was 74MB)
- dist/browser-corpora.mjs manifest regenerated

README: new 'Text corpora' section documenting access from every
interface (Python get_corpus/list_corpora incl. WA_CORPORA_DIR and
path= resolution; JS loadCorpus/listCorpora incl. browser manifest
behaviour) and the licensing/attribution requirements.

Re-verified in DasherCore: Hungarian with the real-sentence corpus
gives 29 letter boxes / 5.3x spread (synthetic: 24 / 2.3x; maintained
English reference: 25 / 2.4x) — real sentences train a genuinely
better model, as expected.

Signed-off-by: will wade <willwade@gmail.com>
Completes the consumer wiring for data/corpora (JS/TS landed in the
previous commit; earlier Python work was lost to a stash mishap and is
re-applied here):

Python (pip install worldalphabets):
- worldalphabets.list_corpora() — manifest from the packaged
  SOURCES.json (always shipped; 167 Tatoeba entries)
- worldalphabets.get_corpus(lang, path=None) — resolves the text via
  path=, WA_CORPORA_DIR, then package data; descriptive error listing
  available languages when the files aren't present (27MB is
  deliberately not packaged)
- sync_package_data.py syncs the manifest ONLY (stale .txt swept);
  MANIFEST.in belt-and-braces excludes corpus text from the wheel

C (c library):
- wa_corpus_info struct + wa_corpus_count / wa_list_corpora /
  wa_get_corpus_info — a static MANIFEST table generated from
  SOURCES.json (wa_data_corpora.c). The text is never embedded; the
  header documents why and carries the CC-BY attribution requirement
- generator extended; stale generated artifacts (morphology-branch
  leftovers referencing undeclared types) regenerated clean

Verified end to end:
- C: full cmake build + smoke suite pass; corpus check binary prints
  167 entries, hu -> tatoeba-sentences verify=0, missing -> NULL
- Python: list_corpora 167; get_corpus via path 368KB of real Hungarian
- JS: jest 54/54 (corpus tests from previous commit)
- ruff 0.16.5 clean; mypy clean; pytest 4/4

Signed-off-by: will wade <willwade@gmail.com>
@willwade
willwade merged commit fe7bda1 into main Aug 29, 2026
5 checks passed
@willwade
willwade deleted the feat/text-corpora branch August 29, 2026 08:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant