Spaces:
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):
- 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 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:
- You β HTTP cache headers (the lever). Versioned/pinned URLs are immutable β serve
Cache-Control: immutable, max-age=1y. The only mutable thing is the tinylatestpointer β short TTL +ETag. This governs freshness everywhere for free, even with zero SDK cache code. - The dev β local persistence + size. SDK defaults (cache dir, max size, eviction, TTL,
on/off), overridable (
QUA_CACHE_DIR, constructor args). Tuned per environment. - 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
latestpointer, 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_versions it understands β old package + new data fails loud, never mis-parses. check_updatesfolds intoqua.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_versioncompatibility 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-recitercontent_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.pylogic β folds into the SDK.
Roadmap / sequencing
- Formalize Layer A on the existing HF/GH surfaces: granular per-reciter/per-surah layout, a
versioned URL scheme, a
latestpointer, immutable cache headers, and an index/openapi.json. (Largely a layout + headers exercise over data you already publish.) - Python SDK (pip) β richest leverage from existing schemas; serves the research/HF audience.
Methods over static, lazy + cache, pin/vendor,
check_updates, typed models. - JS SDK (npm) β the web playback/highlighting audience; reuse the codegen'd TS types.
- 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.)
- 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 fromqua_shared/letter_vocab.pyso 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β the adapter model + release/dataset formats this reads from.data-migrations.mdβ schema codegen rationale (writer/reader drift).qua_shared/letter_vocab.pyβ the letter-tier tokenization the SDK should reuse, not reimplement.