ngano 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ngano-0.1.0/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .mypy_cache/
5
+ dist/
6
+ build/
7
+ *.egg-info/
@@ -0,0 +1,75 @@
1
+ # Changelog
2
+
3
+ All notable changes to the `ngano` Python package are recorded here. The format
4
+ follows Keep a Changelog, and the project uses semantic versioning.
5
+
6
+ ## [0.1.0] - 2026-09-13
7
+
8
+ First release.
9
+
10
+ ### Added
11
+
12
+ - `Catalogue`, reading the snapshot bundled in the wheel with no network and no
13
+ Hugging Face token, or reading live from `https://ngano.dev/api/v1` through
14
+ `Catalogue.from_api()`. 611 dataset records, 58 countries, 315 language tags.
15
+ - `Catalogue.datasets()`, `get()`, `find()`, `search()`, `countries()`,
16
+ `country()`, `languages()`, `language()`, `language_codes()`,
17
+ `resolve_language()` and `stats()`. Every hours total
18
+ excludes records flagged `unverified_size`, and `stats()` reports how many
19
+ records and hours were excluded.
20
+ - ISO 639-3 language keying. Every language is a BCP 47 tag whose primary
21
+ subtag is a three-letter code, with an optional region subtag for a
22
+ country-specific variety, so Shona is `sna` and Nigerian English is `eng-NG`.
23
+ `ngano.languages` bundles the registry as `language_codes.json` and resolves a
24
+ filter value in one fixed order: an exact tag case insensitively, then a bare
25
+ code that exists only as regional varieties, then a name from the registry
26
+ aliases in its plain or slugified spelling. A tag always beats a name, a bare
27
+ code widens to its varieties only with `include_varieties=True`, and an
28
+ unresolvable value matches nothing rather than being guessed at.
29
+ - `LanguageCode`, `LanguageRegistry`, `resolve_language()`,
30
+ `resolve_languages()`, `canonicalise_tag()` and `slugify_name()`, exported
31
+ from the package root.
32
+ - `DatasetRecord.language_tags`, `.language_codes` and `.language_note`, and a
33
+ `LanguageRecord` carrying `tag`, `iso639_3`, `region`, `scope`, `type`,
34
+ `aliases`, `country_codes` and `tasks` alongside its counts.
35
+ - `Row.language_tag`, resolved from the row's own `language_tag`, `language_iso`
36
+ or `language` column, taking the first that resolves to exactly one tag, and
37
+ otherwise from the catalogue record only when that record names exactly one
38
+ language, so a row of a multilingual corpus is never attributed to whichever
39
+ language the record happens to list first. `Row.language_iso` is that tag's
40
+ bare code, or the code the source stated kept verbatim when ngano cannot place
41
+ it, and `Row.language` keeps the source's own value.
42
+ - `Filter`, mirroring the `/datasets` query parameters, buildable from keyword
43
+ arguments or fluently, with `matches()`, `apply()` and `to_params()`.
44
+ `commercial=True` means `"Yes"`, and `include_purchasable=True` widens it to
45
+ `"Yes, if purchased"`. `language` and `iso` accept tags, bare codes and names
46
+ alike and are OR'd into one set of tags, and
47
+ `Filter.resolved_language_tags()` reports what a filter will match.
48
+ - `load()`, streaming canonical rows across every matching dataset at once, with
49
+ `round_robin`, `sequential` and `weighted_by_hours` interleaving, a row
50
+ `limit`, and `stream.matched` and `stream.loadable` counts. It warns once when
51
+ far fewer matched records are streamable than matched.
52
+ - `load_dataset()`, streaming one Hugging Face repo with the same canonical rows,
53
+ including repos that are not in the catalogue.
54
+ - `Row` and `AudioRef` dataclasses for the canonical schema shared by every ngano
55
+ SDK. `AudioRef.read()` fetches bytes only when called, and `AudioRef.decode()`
56
+ raises a clear error naming the `ngano[audio]` extra when soundfile is missing.
57
+ - Column mapping from `field_map.json` in the specified order: repo overrides,
58
+ runtime alias matching that ignores case, underscores and hyphens, unit hint
59
+ conversions, extras preserved in `Row.extra`, and the drop list discarded. An
60
+ unrecognised schema never raises; the reason lands in
61
+ `Row.extra["_ngano_unmapped"]`.
62
+ - Token resolution from the `hf_token` argument, then `HF_TOKEN`, then
63
+ `HUGGING_FACE_HUB_TOKEN`, then anonymous access, and `GatedDatasetError`
64
+ naming the repo and its access request URL instead of a raw 401.
65
+ - `ngano` command line interface built on argparse with `search`, `show`,
66
+ `countries`, `languages`, `stats` and `load`, and `--json` on every command.
67
+ `ngano languages` lists tags with their codes and names, `ngano show` prints a
68
+ record's tags, and `--include-varieties` widens a bare code.
69
+ - `sync_data.py` and a hatchling build hook that copy the repository data files
70
+ into the package at build time, with a `--check` mode for CI.
71
+ - `snippets.json`, the copy-pasteable Python examples the website and the MCP
72
+ `get_loader_snippet` tool serve, checked in CI against the shipped API.
73
+ - Full type hints, a `py.typed` marker, and an offline pytest suite.
74
+
75
+ [0.1.0]: https://github.com/thisisisheanesu/ngano/releases/tag/python-v0.1.0
ngano-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,376 @@
1
+ Metadata-Version: 2.5
2
+ Name: ngano
3
+ Version: 0.1.0
4
+ Summary: Open catalogue and streaming loader for African-language speech datasets
5
+ Project-URL: Homepage, https://ngano.dev
6
+ Project-URL: Documentation, https://ngano.dev/docs
7
+ Project-URL: API, https://ngano.dev/api/v1
8
+ Project-URL: Source, https://github.com/thisisisheanesu/ngano
9
+ Author: Isheanesu Nigel Misi
10
+ License: MIT
11
+ Keywords: africa,african languages,asr,corpus,datasets,huggingface,speech,tts
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: Implementation :: CPython
24
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
25
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.9
28
+ Requires-Dist: datasets>=2.14
29
+ Requires-Dist: requests>=2.28
30
+ Provides-Extra: all
31
+ Requires-Dist: numpy>=1.21; extra == 'all'
32
+ Requires-Dist: soundfile>=0.12; extra == 'all'
33
+ Provides-Extra: audio
34
+ Requires-Dist: numpy>=1.21; extra == 'audio'
35
+ Requires-Dist: soundfile>=0.12; extra == 'audio'
36
+ Provides-Extra: dev
37
+ Requires-Dist: mypy>=1.8; extra == 'dev'
38
+ Requires-Dist: pytest>=7.4; extra == 'dev'
39
+ Requires-Dist: types-requests>=2.28; extra == 'dev'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # ngano
43
+
44
+ **Open catalogue and streaming loader for African-language speech datasets.**
45
+
46
+ `ngano` (Shona: folk stories, the oral tradition) catalogues 611 speech and text
47
+ corpora for African languages, and streams the 283 of them that live on Hugging
48
+ Face through one canonical row schema. One loop can read across datasets that
49
+ agree on nothing: different column names, different units, different splits.
50
+
51
+ The catalogue ships inside the package. `Catalogue()` works offline, with no
52
+ network and no Hugging Face token.
53
+
54
+ ```bash
55
+ pip install ngano # catalogue plus streaming loader
56
+ pip install "ngano[audio]" # adds soundfile and numpy for AudioRef.decode()
57
+ pip install "ngano[all]" # everything
58
+ ```
59
+
60
+ ## The catalogue
61
+
62
+ ```python
63
+ from ngano import Catalogue
64
+
65
+ cat = Catalogue() # bundled snapshot, offline
66
+ cat = Catalogue.from_api() # live from https://ngano.dev/api/v1
67
+
68
+ cat.datasets(language="sna", commercial=True, task="ASR")
69
+ cat.get("waxal-corpus-paper") # also accepts a repo id: cat.get("google/fleurs")
70
+ cat.search("parliament")
71
+ cat.countries()
72
+ cat.languages()
73
+ cat.language_codes() # the ISO 639-3 registry filters resolve through
74
+ cat.resolve_language("isiZulu") # -> "zul"
75
+ cat.stats()
76
+ ```
77
+
78
+ Every record is a `DatasetRecord` with the catalogue fields: `id, name, task,
79
+ variety, languages, languages_clean, language_tags, language_codes, iso,
80
+ countries, country_codes, regions, hours, hours_num, speakers, recording_type,
81
+ quality, labelled, domain, licence, licence_class, commercial, access, host,
82
+ url, hf_repo, year, notes, language_note, unverified_size`.
83
+
84
+ `languages` keeps each source's own spelling, `languages_clean` the canonical
85
+ display names, and `language_tags` the BCP 47 tags ngano filters on.
86
+ `language_codes` holds the bare ISO 639-3 codes behind those tags. The eleven
87
+ records whose source describes its coverage in prose, for example "~340 African
88
+ languages", carry no tags and explain themselves in `language_note`.
89
+
90
+ ## Languages
91
+
92
+ A language is a BCP 47 tag whose primary subtag is an ISO 639-3 three-letter
93
+ code. An optional region subtag marks a country-specific variety, so Shona is
94
+ `sna`, Nigerian English is `eng-NG` and Mozambican Portuguese is `por-MZ`. There
95
+ are 315 tags, 27 of them regional.
96
+
97
+ Any filter, lookup or CLI flag that takes a language accepts three spellings,
98
+ resolved in this order, first match wins:
99
+
100
+ 1. an exact tag, case insensitively, canonicalised to a lowercase primary
101
+ subtag and an uppercase region, so `sna`, `SNA`, `eng-NG` and `eng-ng` all
102
+ land on the same key;
103
+ 2. a bare ISO 639-3 code that exists only as regional varieties, which resolves
104
+ to those varieties, since there is nothing else it could mean;
105
+ 3. a name from the registry aliases, case insensitively, in its plain or its
106
+ slugified spelling, so `language="isiZulu"` still finds `zul`.
107
+
108
+ A tag always beats a name. The one collision in the catalogue is `tem`, which is
109
+ the tag of Timne and also the name of `kdh`, and the tag wins.
110
+
111
+ A bare code never widens to its regional varieties on its own. Ask for that with
112
+ `include_varieties=True`, which returns the bare tag first and then every
113
+ `<code>-*` in registry order. A value that already names a regional variety has
114
+ nothing to widen to, so `include_varieties` leaves it alone, whether it was given
115
+ as a tag or as a name. A value that resolves to nothing matches nothing, because
116
+ ngano does not guess.
117
+
118
+ ```python
119
+ from ngano import resolve_language
120
+
121
+ resolve_language("SNA") # ["sna"]
122
+ resolve_language("isiZulu") # ["zul"]
123
+ resolve_language("eng", include_varieties=True) # ["eng", "eng-NG", "eng-ZA", ...]
124
+ resolve_language("Nigerian English", include_varieties=True) # ["eng-NG"], nothing to widen
125
+ resolve_language("Many African languages") # []
126
+
127
+ cat.datasets(language="eng-NG") # Nigerian English only
128
+ cat.datasets(language="eng", include_varieties=True)
129
+ ```
130
+
131
+ `cat.languages()` returns a `LanguageRecord` per tag with `tag, iso639_3,
132
+ region, name, scope, type, aliases, slug, datasets, hours, countries,
133
+ country_codes, tasks`. Hours are apportioned evenly across each record's tags
134
+ and exclude the unverified figures. `cat.language("Shona")`, `cat.language("sna")`
135
+ and `cat.language("SNA")` all return the same record.
136
+
137
+ Hours are reproduced as each source published them. Self-reported figures of
138
+ 20,000 hours or more carry `unverified_size: true` and are excluded from every
139
+ total that ngano computes, in `stats()`, in `countries()` and in `languages()`.
140
+ `record.countable_hours` is the figure that may be summed.
141
+
142
+ ## Filters
143
+
144
+ A `Filter` mirrors the query parameters of `GET /api/v1/datasets`. Values inside
145
+ one parameter are OR'd, and parameters are AND'd. Build it with keywords or
146
+ fluently, whichever reads better:
147
+
148
+ ```python
149
+ from ngano import Filter
150
+
151
+ Filter(language=["sna", "nde"], country="ZW", task="ASR", commercial=True)
152
+ Filter().language("sna", "nde").country("ZW").task("ASR").commercial()
153
+ ```
154
+
155
+ `commercial=True` means `commercial == "Yes"`. Add `include_purchasable=True` to
156
+ also accept `"Yes, if purchased"`:
157
+
158
+ ```python
159
+ cat.datasets(commercial=True, include_purchasable=True)
160
+ ```
161
+
162
+ Other parameters: `q, iso, region, variety, licence_class, access, labelled,
163
+ quality, domain, host, hf_only, min_hours, max_hours, has_hours,
164
+ include_varieties, sort`. `iso` is the code-shaped spelling of `language`, and
165
+ the two are OR'd into one set of tags rather than AND'd.
166
+ `Filter.resolved_language_tags()` shows exactly which tags a filter will match,
167
+ and `Filter.to_params()` serialises to the HTTP query string if you want to call
168
+ the API yourself.
169
+
170
+ ## Streaming
171
+
172
+ ```python
173
+ from ngano import load
174
+
175
+ stream = load(
176
+ language=["Shona", "Ndebele"],
177
+ country="ZW",
178
+ commercial=True,
179
+ task="ASR",
180
+ split="train",
181
+ streaming=True,
182
+ hf_token=None,
183
+ limit=None,
184
+ interleave="round_robin",
185
+ )
186
+
187
+ print(stream.matched, "datasets matched,", stream.loadable, "can be streamed")
188
+
189
+ for row in stream:
190
+ print(row.transcript, row.language, row.duration_s)
191
+ if row.audio:
192
+ print(row.audio.url, row.audio.path, row.audio.sampling_rate)
193
+ break # nothing further is fetched
194
+ ```
195
+
196
+ Nothing is materialised. Each dataset is opened with
197
+ `datasets.load_dataset(..., streaming=True)`, and rows are mapped as they
198
+ arrive. Breaking out of the loop leaves the rest of every dataset untouched.
199
+
200
+ `stream.matched` is how many catalogue records satisfied the filter, and
201
+ `stream.loadable` is how many of those have a Hugging Face repo. Only those can
202
+ be streamed; the rest are catalogued sources you have to obtain from their host.
203
+ When the gap is large, `load()` warns once. Pass `hf_only=True` to filter them
204
+ out up front.
205
+
206
+ ### Interleaving
207
+
208
+ - `round_robin` (default): one row from each dataset in turn.
209
+ - `sequential`: one dataset to exhaustion, then the next.
210
+ - `weighted_by_hours`: rows in proportion to each corpus's published hours,
211
+ deterministic, no randomness.
212
+
213
+ `datasets.interleave_datasets` is used when the datasets share one schema, which
214
+ is what that function requires. Otherwise ngano round robins them itself. Either
215
+ way the merge is lazy.
216
+
217
+ ### One repo at a time
218
+
219
+ ```python
220
+ from ngano import load_dataset
221
+
222
+ for row in load_dataset("google/fleurs", config="sw_ke", limit=5):
223
+ print(row.language_tag, row.language_iso, row.transcript)
224
+ ```
225
+
226
+ A repo that is not in the catalogue still streams. `row.dataset_id`,
227
+ `row.licence` and the other catalogue fields are then `None`.
228
+
229
+ ## The canonical row
230
+
231
+ Every row is a `Row` dataclass, identical across the Python, JavaScript and Rust
232
+ SDKs:
233
+
234
+ | field | type | meaning |
235
+ | --- | --- | --- |
236
+ | `audio` | `AudioRef \| None` | lazy handle, never decoded by default |
237
+ | `transcript` | `str \| None` | reference text |
238
+ | `language` | `str \| None` | language name as the source labels it, else the catalogue's canonical name |
239
+ | `language_iso` | `str \| None` | bare ISO 639-3 code, see below |
240
+ | `language_tag` | `str \| None` | BCP 47 tag, for example `sna` or `eng-NG`, see below |
241
+ | `country` | `str \| None` | ISO 3166-1 alpha-2 |
242
+ | `speaker_id` | `str \| None` | speaker id within the dataset |
243
+ | `gender` | `str \| None` | as stated by the source, never inferred |
244
+ | `age` | `str \| None` | as stated by the source, never inferred |
245
+ | `duration_s` | `float \| None` | seconds |
246
+ | `sampling_rate` | `int \| None` | Hz |
247
+ | `domain` | `str \| None` | recording domain |
248
+ | `split` | `str` | split name |
249
+ | `dataset_id` | `str \| None` | ngano catalogue id |
250
+ | `hf_repo` | `str \| None` | Hugging Face repo |
251
+ | `licence` | `str \| None` | licence from the catalogue |
252
+ | `source_url` | `str \| None` | canonical dataset URL |
253
+ | `extra` | `dict` | every column ngano did not map |
254
+
255
+ `row.to_dict()` gives a JSON-serialisable dict. Audio bytes are never included.
256
+
257
+ ### How a row gets its language
258
+
259
+ `language_tag` is, in order:
260
+
261
+ 1. the row's own `language_tag`, `language_iso` or `language` column, taking the
262
+ first that resolves to exactly one tag;
263
+ 2. otherwise the catalogue record's tag, but only when that record names exactly
264
+ one language;
265
+ 3. otherwise `None`.
266
+
267
+ A row from a forty language corpus is not attributed to one language just
268
+ because the record lists that language first, so a silent row of a multilingual
269
+ record has `language_tag=None`.
270
+
271
+ `language_iso` is the bare primary subtag of `language_tag` when the row's own
272
+ value resolved, otherwise the code the source stated kept verbatim when ngano
273
+ cannot place it, so an ISO 639-1 `sw` survives as `"sw"`, otherwise the record's
274
+ code when it names one language. `language` always keeps the source's own
275
+ spelling, falling back to the canonical name of a single-language record.
276
+
277
+ ## Lazy audio
278
+
279
+ ```python
280
+ row.audio.url # remote location, if the source gave one
281
+ row.audio.path # local or archive-relative path
282
+ row.audio.sampling_rate # Hz, as stated by the source
283
+
284
+ data = row.audio.read() # fetches bytes only now
285
+ array, sr = row.audio.decode() # needs pip install "ngano[audio]"
286
+ ```
287
+
288
+ `decode()` raises a clear `ImportError` naming the `ngano[audio]` extra when
289
+ soundfile is not installed. Building a row never reads, downloads or decodes
290
+ anything.
291
+
292
+ ## Column mapping
293
+
294
+ Datasets do not agree on column names, so ngano maps them in one fixed order,
295
+ the same in every SDK:
296
+
297
+ 1. `overrides[hf_repo]` from `field_map.json`, for repos that were checked by hand.
298
+ 2. runtime inspection of the dataset's real columns against `aliases`, matched
299
+ case-insensitively and ignoring underscores and hyphens, so `Client_ID`,
300
+ `client-id` and `clientid` are the same column.
301
+ 3. `unit_hints` conversions, so a `duration_ms` column of 2500 becomes
302
+ `duration_s == 2.5`.
303
+ 4. anything still unmapped is preserved in `row.extra`, never dropped silently.
304
+ 5. columns in the `drop` list are discarded rather than kept as extras.
305
+
306
+ An unknown schema never raises. `transcript` and `audio` stay `None` and the
307
+ reason is recorded in `row.extra["_ngano_unmapped"]`, naming the columns that
308
+ were seen. `stream.plans` shows how each repo was mapped once it has opened.
309
+
310
+ ## Authentication
311
+
312
+ ngano itself needs no credentials. A token is only used for gated Hugging Face
313
+ repos, resolved in this order: the `hf_token` argument, then `HF_TOKEN`, then
314
+ `HUGGING_FACE_HUB_TOKEN`, then anonymous access. A gated repo raises
315
+ `GatedDatasetError` naming the repo and the page where access is requested,
316
+ rather than a raw 401.
317
+
318
+ ## Command line
319
+
320
+ ```bash
321
+ ngano search parliament --limit 5
322
+ ngano search --language sna --task ASR --commercial --json
323
+ ngano search --language isiZulu # a name works too
324
+ ngano search --language eng --include-varieties # eng plus every eng-* variety
325
+ ngano show google/fleurs
326
+ ngano countries --region "Southern Africa"
327
+ ngano languages --min-datasets 10 # tag, code, name, counts
328
+ ngano stats --json
329
+ ngano load --language sna --hf-only --limit 20 --out rows.jsonl
330
+ ngano load --language sna --hf-only --dry-run
331
+ ```
332
+
333
+ `--json` works on every command, before or after the subcommand. `--api` reads
334
+ the live API instead of the bundled snapshot. `ngano languages` lists one line
335
+ per tag, with the bare ISO 639-3 code and the canonical name, and `ngano show`
336
+ prints a record's `language_tags` and `language_codes`.
337
+
338
+ ## Updating the bundled data
339
+
340
+ The wheel carries a snapshot of `catalogue.json`, `countries.json`,
341
+ `languages.json`, `language_codes.json` and `field_map.json` copied from the
342
+ repository root. The
343
+ hatchling hook in `hatch_build.py` runs the copy during `python -m build`, so a
344
+ normal build is always current. To refresh it by hand, or to check for drift in
345
+ CI:
346
+
347
+ ```bash
348
+ python sync_data.py # copy data/*.json into src/ngano/data/
349
+ python sync_data.py --check # exit 1 if the snapshot is stale
350
+ ```
351
+
352
+ ## Snippets for the website
353
+
354
+ `snippets.json` holds the Python examples that ngano.dev and the MCP
355
+ `get_loader_snippet` tool serve, with `{{LANGUAGE}}`, `{{COUNTRY_ISO2}}`,
356
+ `{{COUNTRY_NAME}}`, `{{DATASET_ID}}`, `{{HF_REPO}}`, `{{CONFIG}}` and `{{TASK}}`
357
+ as the substitution tokens. Each token is described in `placeholder_docs`.
358
+ `{{LANGUAGE}}` is a BCP 47 tag such as `sna`, never a display name, because that
359
+ is what the filters take, and the website and the MCP server substitute a tag.
360
+ The test suite substitutes real values and compiles every snippet, and runs the
361
+ offline ones, so they cannot drift from this API.
362
+
363
+ ## Development
364
+
365
+ ```bash
366
+ pip install -e ".[dev,audio]"
367
+ pytest # the whole suite is offline, no network, no token
368
+ mypy # strict, on the public surface
369
+ ```
370
+
371
+ ## Licences and credits
372
+
373
+ Code MIT. Catalogue data CC-BY-4.0. By Isheanesu Nigel Misi.
374
+ The catalogue is a list of sources, with figures as published by each source and
375
+ unverified ones flagged. It is not a claim that every corpus is usable, legal
376
+ for your purpose, or of the quality its publisher states.