hetchyy's picture
deploy: inspector prod @ ab17458
f2d4e06 verified
|
Raw
History Blame Contribute Delete
12.9 kB

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:

  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_versions 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 β€” 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.