Spaces:
Running
Running
| # Public Data API β roadmap & design reference | |
| > **Status: forward-looking. Not built yet.** Unlike the other reference docs (which | |
| > describe what *is*), this captures the agreed *target shape* and the *why*, so future | |
| > work doesn't re-derive it. When the API starts shipping, split the built parts into a | |
| > normal what-is reference and keep the unbuilt parts here. | |
| ## Why this exists (the problem) | |
| QUA already ships its data three ways, all **adapters over the canonical bucket** (see | |
| [`dataset-and-releases.md`](dataset-and-releases.md)): | |
| - **GitHub Releases** β versioned JSON tiers + per-reciter zips + `manifest.json` + checksums. | |
| - **HF dataset** β parquet, queryable via `datasets` / the HF datasets-server. | |
| - **Inspector app** β the editor/admin surface. | |
| The data is available; the **barrier is format-literacy**. To use it a developer must learn | |
| the zip layout, the three timestamp tiers, the manifest schema, the 42-token letter vocab, | |
| the dedup semantics, and then stitch things together themselves (pair audio URLs to timings, | |
| do per-ayah/per-surah lookups, poll for updates). That's a lot of reading for "play ayah | |
| 2:255 with word highlighting." | |
| **The API's job is to erase that barrier**: call functions, not file formats. | |
| ## End-goal vision | |
| > **A typed Python/JS client for the QUA dataset β call functions, not file formats.** It | |
| > fetches only the slice you ask for and caches it, serves the latest data by default (no | |
| > manual update checks) while letting you pin a version or vendor a full offline snapshot, | |
| > and encodes the schemas as type hints so you never reverse-engineer the release layout. | |
| Pillars (the honest value prop β see [Non-claims](#non-claims--pitfalls) for what NOT to overstate): | |
| - **One interface, not formats** β abstracts GH releases / HF dataset; call `reciter().ayah()` | |
| instead of learning zips, tiers, manifests, the char vocab. | |
| - **Typed** β IDE autocomplete + models generated from the same schemas the data ships with. | |
| - **Fetch-what-you-need + cached** β granular slices, instant on repeat. | |
| - **Fresh by default, reproducible on demand** β always-latest automatically; pin a version | |
| when you need stability. | |
| - **Online or offline, same API** β lazy remote by default; vendor a snapshot for air-gapped / CI / bulk. | |
| - **Correct by construction** β the dedup, tokenization, and audio pairing we use, not a | |
| consumer reimplementation. | |
| ## Architecture β three layers | |
| ``` | |
| A. Canonical static data on existing free hosting β the "backend" (no server to run) | |
| (HF dataset repo + GitHub Releases = the CDN; produced by the bucketβadapter pipeline) | |
| β² HTTP GET (granular, versioned, cacheable) | |
| B. SDKs (pip + npm) β THE product surface | |
| typed methods Β· lazy fetch + cache Β· latest-by-default + pin + vendor Β· reuse dedup/tokenization | |
| β² only for global/compute ops | |
| C. Optional compute/query service (HF Space) β built ONLY if demand appears | |
| cross-reciter queries Β· search Β· aggregations Β· on-the-fly audio clipping | |
| ``` | |
| ### Layer A β canonical static data (the backend is just files) | |
| The bucketβadapter pipeline already emits versioned data to **public, free hosting**. Make the | |
| layout **granular** (per-reciter, and per-surah shards β formalize the `shard.py` idea into the | |
| on-disk layout) so a client fetches a small slice, not a whole archive. Add a small `latest` | |
| pointer + an index/`openapi.json` for discovery. | |
| ### Layer B β the SDKs (this is the product) | |
| Rich, ergonomic, typed methods. The "cool functionality" (ayah/surah lookup) is **client-side | |
| indexing over fetched slices**, not server compute β so it needs no server. Illustrative surface | |
| (not final): | |
| ``` | |
| qua.reciters(riwayah="hafs", style="murattal") # filter the catalog (local) | |
| qua.reciter("mishary_...").surah(36).words # fetch+index a surah shard | |
| qua.reciter("...").ayah(2, 255).letters # ayah lookup (local) | |
| ayah.audio_url / ayah.clip() # CDN url / byte-range (never proxied bytes) | |
| qua.check_updates(...) # folds in check_updates.py logic | |
| Qua(data_version="v0.3.0") / Qua(data_dir="./snap") # pin / vendor | |
| ``` | |
| ### Layer C β optional compute service (the HF Space) | |
| Stand up a *server* **only** for what a client genuinely can't do locally without downloading | |
| everything: cross-reciter / global queries, search, aggregations, dynamic audio clipping. The | |
| SDK routes *only those* methods to it and does everything else local-static β the dev never | |
| knows which. β οΈ Free HF Spaces **sleep / cold-start** and have limited concurrency: **never | |
| route bulk static reads through the Space**, or simple lookups become slow/flaky. Static reads | |
| stay on the CDN; the Space handles compute only. (The HF datasets-server already gives a free | |
| REST + DuckDB query API over the parquet β may cover much of the query use case for free.) | |
| ## Why this shape (rationale) | |
| - **Data is static + immutable per version** β a stateful API server is the *wrong default* | |
| (cost, latency, SPOF, scaling) for data a CDN serves free at infinite scale. | |
| - **Rich per-ayah/surah functionality is client-side indexing**, not server compute β no server | |
| needed for the things that feel like "a powerful API". | |
| - **Audio bytes never flow through your API** (bandwidth trap) β the API returns CDN *URLs*; | |
| bytes come from the CDN; clips via edge/byte-range. | |
| - **One interface over many formats** kills the real barrier (format-literacy) and prevents | |
| buggy reimplementations of dedup/tokenization. | |
| ## Hosting & cost | |
| You **don't pay for or stand up new infra** for Layers A/B. Your existing **public** surfaces | |
| *are* the CDN: HF hosts public datasets free (Xet-backed), GitHub hosts release assets free β | |
| both over their CDNs. The SDK does HTTP GETs against those. The **private pipeline bucket is the | |
| canonical *source*, NOT the public read surface**; the HF dataset + GH releases are the published | |
| read replicas. A dedicated CDN (Cloudflare R2/Pages, etc.) is only needed if you outgrow HF/GH. | |
| ## Caching model | |
| The cache lives **wherever the SDK process runs**: | |
| | Runtime | Cache location | Whose disk | | |
| |---|---|---| | |
| | pip in a notebook/script | `~/.cache/qua/` | the dev's machine | | |
| | pip / npm(Node) in a backend | server filesystem (shared across requests) | the dev's server | | |
| | CI / training cluster | runner disk (ephemeral unless persisted) | the dev's infra | | |
| | npm in the browser | browser HTTP cache / IndexedDB | the **end-user's** device | | |
| **Who controls policy β three layers, the real lever is yours:** | |
| 1. **You β HTTP cache headers (the lever).** Versioned/pinned URLs are immutable β serve | |
| `Cache-Control: immutable, max-age=1y`. The only mutable thing is the tiny `latest` pointer β | |
| short TTL + `ETag`. This governs freshness everywhere for free, even with zero SDK cache code. | |
| 2. **The dev β local persistence + size.** SDK defaults (cache dir, max size, eviction, TTL, | |
| on/off), overridable (`QUA_CACHE_DIR`, constructor args). Tuned per environment. | |
| 3. **The end-user β nothing** beyond clearing their browser cache. | |
| **How much:** lazy mode caches only *accessed* slices (a few MB typical; per-surah shard β KBβtens | |
| of KB, a full reciter's letters β a few MB gz), bounded by a default cap + LRU. Immutable/pinned | |
| entries live forever; `latest` entries are `ETag`-revalidated, not re-downloaded. Vendored mode = | |
| whatever snapshot the dev pulled (full corpus β low hundreds of MB) β an explicit choice. Safe to | |
| cache hard because versioned data is content-addressed & immutable. | |
| ## Versioning model β two decoupled axes | |
| | Axis | Versions | Bumps when | | |
| |---|---|---| | |
| | **Package version** (pip/npm semver) | the **code** β methods, fixes, features | `pip install --upgrade qua` | | |
| | **Data version** (release tags / HF revisions) | the **data** β new/refreshed reciters | every cut; **no package upgrade** | | |
| - **Latest data by default** β SDK resolves the `latest` pointer, so devs get current data without | |
| upgrading the package. Data updates β package releases. | |
| - **Pin for reproducibility** β `Qua(data_version="v0.3.0")` locks an immutable snapshot (GH tags / | |
| HF revisions). "Latest" and "pin" aren't contradictory: latest is default, pin is opt-in (researchers | |
| will want it). | |
| - **Compatibility guard** β the package declares which data `schema_version`s it understands β old | |
| package + new data fails *loud*, never mis-parses. | |
| - **`check_updates`** folds into `qua.check_updates()` β tells a dev whether their pinned/used reciters | |
| changed upstream. | |
| ## Data access modes (online/offline parity) | |
| `remote-lazy` (default) Β· `vendored/offline` (`data_dir` / env) Β· `pre-warm snapshot`. **Same API | |
| across all** β the dev chooses the source; the methods are identical. Offline mode is a first-class | |
| feature (reproducible ML, air-gapped clusters, CI), not a fallback. | |
| ## Non-claims & pitfalls | |
| Things to *not* overstate or get wrong: | |
| - β "lower latency than a fetch" β a **cold** first call β a normal download. The wins are | |
| **granularity** (fetch a slice, not a whole archive) + **caching** (instant repeat). Say that. | |
| - β don't serve **audio bytes** through your API β return CDN URLs; clips via edge/range. | |
| - β don't route **bulk static reads** through the HF Space (cold-start/concurrency) β Space = compute only. | |
| - β don't republish the **package** on every **data** change β the axes are decoupled. | |
| - β don't let an **old package mis-parse new data** β the `schema_version` compatibility guard is mandatory. | |
| - β the API must stay an **adapter over the canonical bucket**, never a 4th source of truth β same | |
| generation path as `cut_release` / `publish_hf`. | |
| ## What this builds on (already in place) | |
| - **bucket-as-canonical + adapter pipeline** (`qua_jobs/cut_release.py`, `qua_jobs/publish_hf.py`). | |
| - **versioned releases + `manifest.json` + per-reciter `content_hash`** β the primitives for | |
| pinning, "latest" resolution, and update detection. | |
| - **Pydantic schemas β TS codegen** (`qua_shared/schemas/` β `scripts/codegen/regen_fe_types.py`) β | |
| free typed models for *both* SDKs. | |
| - **canonical dedup + tokenization in `qua_shared`** (`timestamps_dedup.py`, `letter_vocab.py`) β | |
| reuse for correct-by-construction reads; do not reimplement client-side. | |
| - **`check_updates.py`** logic β folds into the SDK. | |
| ## Roadmap / sequencing | |
| 1. **Formalize Layer A** on the existing HF/GH surfaces: granular per-reciter/per-surah layout, a | |
| versioned URL scheme, a `latest` pointer, immutable cache headers, and an index/`openapi.json`. | |
| (Largely a layout + headers exercise over data you already publish.) | |
| 2. **Python SDK (pip)** β richest leverage from existing schemas; serves the research/HF audience. | |
| Methods over static, lazy + cache, pin/vendor, `check_updates`, typed models. | |
| 3. **JS SDK (npm)** β the web playback/highlighting audience; reuse the codegen'd TS types. | |
| 4. **Layer C compute service (HF Space)** β *only if* demand: global queries/search/aggregations/ | |
| dynamic clips. SDK routes those transparently. (Re-evaluate against the free HF datasets-server first.) | |
| 5. **Dedicated CDN** β only if HF/GH limits are hit. | |
| ### Positioning recommendation | |
| Lead with one path so newcomers aren't paralyzed, then progressively disclose the rest: | |
| | On-ramp | Recommend it when⦠| | |
| |------------------|---------------------------------------------------------------------------------------------------| | |
| | **SDK (pip/npm) β default** | Building an app/tooling in Python or JS; want ergonomics, types, caching, auto-updates. | | |
| | **HF dataset** | ML training/data pipelines, bulk/SQL analytics, audio-embedded, zero-install exploration. | | |
| | **GitHub Releases** | Any other language, zero-dependency, offline/vendored, auditable, citable, simple one-offs. | | |
| ## Open decisions (defer until building) | |
| - Sharding granularity (per-surah vs per-reciter) + exact URL scheme. | |
| - Whether to expose `tokenize(text)` in the SDK (the earlier "tokenizer helper" question) β if shipped, | |
| source it from `qua_shared/letter_vocab.py` so it can't drift; ship on demand, not speculatively. | |
| - REST vs GraphQL for Layer C (only relevant if Layer C is built). | |
| - Audio clip strategy: edge function + byte-range vs precomputed. | |
| - Auth / rate-limiting β only if a public compute service is exposed. | |
| ## Related | |
| - [`dataset-and-releases.md`](dataset-and-releases.md) β the adapter model + release/dataset formats this reads from. | |
| - [`data-migrations.md`](data-migrations.md) β schema codegen rationale (writer/reader drift). | |
| - `qua_shared/letter_vocab.py` β the letter-tier tokenization the SDK should reuse, not reimplement. | |