Spaces:
Running
Inspector backend architecture β START HERE
The Inspector is a single-worker Flask app serving a Svelte 5 SPA. It manages the lifecycle of Quran-recitation forced-alignment deliveries (catalogue β align β review β release β publish) and lets editors trim/split/merge/re-reference word-level segments. The deployed app is a Docker HF Space; its source of truth is a container-local SQLite DB (inspector.db) full-file synced to an HF bucket after every committed write. Per-reciter content (detailed.json, segments.json, timestamps/<ch>.json, peaks, audio) stays JSON/binary on the bucket, read on demand.
Layering
HTTP ββΆ routes/<domain>/* thin Flask blueprints: parse β service β jsonify
β (primary Flask layer; only services/ exception is auth/auth.py)
βΌ
services/<domain>/* business logic, Flask-free, returns plain dicts
β db/ storage/ audio/ auth/ state/ segments/
β validation/ activity/ reference/ quran_foundation/
βΌ
domain/ adapters/ utils/ pure model Β· JSONβdomain conversion Β· helpers
services/storage/cache.py single owner of every mutable cache var
config.py Β· constants.py env-overridable tunables Β· domain literals
Invariants:
- Routes are the Flask layer. Services accept params, return dicts, raise typed errors;
app.pymaps those errors to the{error: str}JSON envelope. The soleservices/exception isservices/auth/auth.pyβ authlib'sflask_clientOAuth integration (_CacheStateOAuth/_CacheStateIntegrationsubclass Flask integration) cannot be abstracted away. Theservices-flask-freeCI guard enforces this withauth.pyas the only allow-listed file. - Single worker.
app.py::_assert_single_workerrefuses to boot under any multi-worker signal. State store, per-slug locks, role cache, OAuth-state cookie, and all in-memory caches assume one process. - SQLite is the source of truth for the 7 former bucket JSON stores + audit. Reads go through
services/db/repo_*; writes commit inservices/db/connection.py::transaction()then sync viaservices/db/sync.py. - Bucket-first reads for reciter content go through
services/storage/data_dir.py+storage_paths.py, never rawPath.read_text(). services/__init__.pyre-exports every submodule at the legacy flat name (from services.cache import β¦still resolves toservices/storage/cache.py) viasys.modulesaliasing. New modules drop into a subpackage + one re-export line.
Services subpackage map
| Subpackage | Role | Key files |
|---|---|---|
db/ |
SQLite substrate β source of truth for state/catalog/access/claims/requests/activity + transitions (audit). WAL single-writer, full-file bucket sync. | connection.py (txn primitives, transaction(), get_writer, current_db_seq), migrate.py (PRAGMA user_version runner over migrations/NNNN_*.sql), sync.py (bucket pull/push + CAS guard), repo_state.py, repo_catalog.py, repo_access.py, repo_claims.py, repo_requests.py, repo_activity.py, repo_transitions.py, _serde.py, errors.py |
storage/ |
Bucket/filesystem backend, per-reciter content IO, the cache registry, local auto-mount, external-file validation. | cache.py (all cache vars + invalidation), data_dir.py (slugβbytes/dict under reciters/<slug>/), storage_paths.py (pure on-bucket path strings), data_loader.py (cached loaders for segs/verses/probe/reference), hf_bucket.py (BucketBackend/FilesystemBackend), auto_mount.py (local hf-mount FUSE), bucket_audit.py (Flask-free per-reciters/<slug>/ artefact validator against qua_shared/schemas; backs the per-slug CLI scripts/bucket/audit_bucket_reciter.py, the whole-bucket gate scripts/diagnostics/validate_bucket.py, and /healthz?deep=1 via sample_validation) |
audio/ |
Everything bytes-on-disk β <audio>: source resolution, fetch, peaks, manifest. |
audio_source.py (resolve bucket/CDN + VBR), audio_fetch.py (bucket audio + peaks reads), audio_meta.py (per-chapter sidecar), peaks.py (ffmpeg compute), peaks_slim.py (canonical schema-v3 packed format), peaks_history.py (per-op JSONL) |
auth/ |
HF OAuth + signed identity cookie, roles, pure authorization predicates. | auth.py (OAuth + inspector_session cookie + dev-mode), access.py (roles facade over DB), permissions.py (pure predicates + normalize_reason), predicates.py (/api/reciter-task can-edit predicates), hf_users.py (loginβhf_user_id proxy), secrets_guard.py |
state/ |
Reciter lifecycle state machine, catalog, requests, audit facade. | state.py (transition(slug, event, actor), _HANDLERS, typed errors), catalog.py (snapshot() β ReciterCatalog, mutations), pending_requests.py, request_archive.py, audit.py (legacy append() facade β repo_transitions) |
segments/ |
Save / undo / read queries for the Segments editor, plus auto-detect + auto-split. | save.py (write-through, history append, rebuild segments.json), undo.py (batch reversal + snapshot verify), segments_query.py (read-only data), auto_detect.py (reconciler: reciters/ dir β alignment_completed), auto_split.py (precomputed cursor lookup), qalqala.py (persisted qalqala_letter) |
validation/ |
Registry-backed segment validation + chapter counts (Flask-free). | registry.py (IssueRegistry, category sets β keep in lockstep with FE registry.ts), classifier.py, snapshot_classifier.py, detail.py, _missing.py, _structural.py; package __init__.py exposes validate_reciter_segments |
activity/ |
Audit-event classification + public activity feed + history queries + stats. The admin notifications rail was retired β admin awareness lives in the Admin dashboard tabs now. | activity_classification.py (event β public/hidden), activity_state.py (global-tombstone facade), public_activity.py, history_query.py (edit-history read + split-group/resolved-by-edit indexes), stats.py, search_normalize.py (mirrors FE normalizeArabic) |
reference/ |
Static Quran reference payloads + timestamps server + public dashboard mapper. | quran_refs.py (DK words + verse word counts, one immutable asset), timestamps.py (manifest + per-chapter shard server, gzip LRU), public_state.py (ReciterRow β six-bucket public taxonomy, strips identity) |
quran_foundation/ |
Quran.Foundation API integration (OAuth user APIs + content APIs + bookmarks). | oauth.py, content.py, bookmarks.py, reciter_map.py, session.py (signed qf_session cookie), config.py β see Quran Foundation integration |
Routes registry
Blueprints register in routes/__init__.py::register_blueprints. Subpackage __init__.py files are empty; each module defines its own Blueprint.
| URL prefix | Blueprint | Module | Purpose |
|---|---|---|---|
/healthz, /readyz |
health |
routes/auth/health.py |
Liveness/readiness; reports SQLite + bucket-mount status (503 when degraded) |
/api (/api/auth/*) |
auth |
routes/auth/auth.py |
HF OAuth login/callback/logout; sets inspector_session identity cookie |
/api/dev |
dev |
routes/auth/dev.py |
Dev-only: flip inspector_dev_role cookie (404 when dev mode off) |
/api |
claims |
routes/claims/claims.py |
Claim/release/mark/unmark + /api/reciter-task/<slug> (via state.transition) |
/api |
requests |
routes/claims/requests.py |
Request flow β user submit + admin review/reject/undiscard |
/api/public |
public |
routes/public/public.py |
Public read-only dashboard data (identity-redacted) |
/api/static |
static_data |
routes/public/static.py |
Static projections (catalog.json, quran-refs.json, β¦) for the FE |
/api/admin/access |
access_admin |
routes/admin/access.py |
Role grant/revoke/update (mirrors services/access.py) |
/api/admin |
admin_actions |
routes/admin/actions.py |
Admin override transitions (force-release, reassign, force-set-state, send-back) |
/api/public/activity |
public_activity_admin |
routes/admin/activity.py |
Owner-only DELETE β global tombstone for a public-feed card |
/api/admin |
admin_reconcile |
routes/admin/reconcile.py |
Manual trigger for the auto-detect reconciler |
/api/ts |
ts |
routes/timestamps/timestamps.py |
Timestamps tab: /manifest, /shard/<reciter>/<chapter>, /config |
/api/seg |
seg_data |
routes/segments/data.py |
Segments tab read-only data (/all, refs, β¦) |
/api/seg |
seg_edit |
routes/segments/edit.py |
Segments save/undo (@require_same_origin β @require_edit_lock) |
/api/seg |
seg_val |
routes/segments/validation.py |
Validate, stats, edit-history |
/api/seg |
peaks |
routes/segments/peaks.py |
/peaks, /segment-peaks, /history-peaks |
/api/seg |
audio_proxy |
routes/audio/proxy.py |
/audio-proxy/<reciter> β bucket-resident audio; CDN stream-through fallback (same-origin 200/206 + ACAO, not a 302) |
/api/audio |
audio_meta |
routes/audio/metadata.py |
Audio tab metadata |
/api/seg |
segment_clip |
routes/audio/clip.py |
/segment-clip β ffmpeg MP3 window clip (VBR-safe seek) |
/api/qf |
qf_auth |
routes/qf_auth.py |
Quran.Foundation OAuth2 (pre-prod user APIs) |
/api/qf/content |
qf_content |
routes/qf_content.py |
QF Content-API read proxy (word-by-word translations) |
/api/bookmarks |
bookmarks |
routes/bookmarks.py |
QF bookmarks proxy ([] for anonymous) |
/ + static |
(Flask static) | app.py |
SPA shell frontend/dist/index.html; /api/surah-info cross-tab route |
_admin_helpers.py (package root, not a blueprint) is shared by admin/ + claims/ routes.
domain / adapters / utils
inspector/domain/ β pure model, no Flask, no IO:
| File | Role |
|---|---|
segment.py |
Frozen Segment dataclass β one aligned segment as stored in detailed.json |
command.py |
SegmentPatch β change set produced by a command; undo applies its inverse |
identity.py |
Deterministic segment_uid (uuid5 over NAMESPACE_INSPECTOR); must match the TS impl |
inspector/adapters/ β JSON β domain conversion:
| File | Role |
|---|---|
detailed_json.py |
detailed.json entries list β segment dicts with backfilled UIDs (load_entries) |
segments_json.py |
Rebuild verse-aggregated segments.json from detailed entries |
save_payload.py |
Incoming save payload β canonical segment dicts (make_seg) |
inspector/utils/ β pure utilities + cross-cutting decorators:
| File | Role |
|---|---|
decorators.py |
require_same_origin (CSRF), require_edit_lock (signed-in + active claim + editable, admin_bypass) |
json_response.py |
orjson_response β fast JSON for big-payload routes |
arabic_text.py |
Strip Quranic decoration, last-letter extraction |
references.py |
Reference-string parsing + segment classification helpers |
repetitions.py |
Repetition-segment helpers over wrap_word_ranges |
formatting.py |
Display formatting |
io.py |
Atomic writes + safe filenames |
uuid7.py |
UUIDv7 (time-ordered) generator |
app.py boot sequence
Runs at module import (so python3 inspector/app.py and gunicorn inspector.app:app follow the same path):
- Insert repo root on
sys.path(resolvesqua_shared.*). _load_dotenv_for_local_dev()β hydrate env from<repo>/.envtheninspector/.env(process env wins).- Auto-enable
INSPECTOR_DEV_MODE=1when not behind proxy and not under pytest. auto_mount()β local hf-mount FUSE of the bucket (silent degrade to API path)._configure_logging()β plain single-line formatter; silence httpx/urllib3/hf noise._assert_single_worker()β abort on any-w >1/WEB_CONCURRENCY/GUNICORN_*signal.- Build
Flask(static_folder=frontend/dist); wrap withCompress(gzip/brotli). ProxyFixifINSPECTOR_BEHIND_PROXY=1; set session cookie SameSite/Secure accordingly.app.secret_key = get_session_secret()(warns, doesn't crash, if missing).auth_service.init_oauth(app)β register HF OAuth provider with Authlib.register_blueprints(app)β all route blueprints._boot_substrate()(skipped under pytest):sync.pull()bucket DB β local βinit_db()(open writer + run migrations) βauto_detect.hydrate_initial_seen()boot-scan insidedeferred_sync()(one coalesced upload). Then the opt-in auto-detect loop (INSPECTOR_AUTO_DETECT=1). Deployed-mode init failure leaves app importable withdb_open:false; local failure raises.- Register error handlers (
HTTPException,UnknownReciter,InvalidTransition,NotAuthorizedForTransition, catch-all) β{error: β¦}envelope. - Register
/(SPA shell) +/api/surah-info. if __name__ == "__main__": parse--port, run dev server (debug/reloader only underFLASK_ENV=development).
No eager warms of reciter data β timestamps + per-reciter caches are lazy on first request.
Caching
All mutable cache vars live in services/storage/cache.py (services.cache); no global for caches elsewhere. Classes: _SingletonCache (one nullable value), _KeyedCache (LRU dict, ceiling _KEYED_CACHE_LRU_MAX=20).
- Eager (singletons, immutable post-boot): word counts, single-word verses, QPC/DK data, surah-info-lite β filled on first use, never invalidated.
- Lazy per-reciter (LRU-bounded): segments, seg meta/verses, probe-v2, auto-split, pipeline-meta, history batches, split-group index, validate/stats results, audio manifest.
- db_seq-keyed:
public_recitersandcatalog_snapshotcache on the monotoniccurrent_db_seq()β any committed DB write bumps the seq and transparently invalidates them (no per-mutation hook). - Peaks:
_PEAKS_RESPONSE_CACHE(global LRU, 50 entries, serialized JSON bytes), thread-safe with explicit locks.
Invalidation hook: invalidate_seg_caches(reciter) runs on every save/undo. The surgical variant pop_seg_caches_affected_by_segment_edit preserves append-in-place caches (history batches, split-group index) and immutable ones (pipeline-meta). The chapter-peaks LRU (_PEAKS_RESPONSE_CACHE) is deliberately NOT evicted on save β peaks track immutable audio bytes, not segment edits; it sheds only via its own 50-entry LRU and an explicit pop_reciter_peaks_response_cache wherever a future path rewrites bucket peaks. Add a new cache here with its invalidation tied to the mutation that dirties it.
Quran Foundation integration
services/quran_foundation/ integrates the Quran.Foundation (QF) APIs. No standalone doc β this is the reference.
Backend-proxy pattern: per-user OAuth2 tokens stay server-side in a signed qf_session cookie; the browser never sees them. Outbound QF calls inject x-auth-token + x-client-id. Two-environment split (never mix tokens): User APIs (bookmarks) use the pre-prod client + apis-prelive base; Content APIs use the prod client (client_credentials, HTTP Basic). All modules Flask-free; only routes/qf_* + routes/bookmarks.py import Flask.
| File | Role |
|---|---|
oauth.py |
authorization_code + PKCE helpers (pre-prod); token endpoint requires client_secret_basic |
content.py |
Content API (client_credentials) β full-surah audio URLs, word-by-word translations |
bookmarks.py |
Bookmarks proxy β QF /auth/v1/bookmarks + dev in-memory store; normalizes to {surah, ayah, key} |
reciter_map.py |
(reciter_id, style) β QF chapter-reciter id for verified reciters |
session.py |
Signed qf_session cookie holding the per-user token server-side (itsdangerous) |
config.py |
Env-driven endpoints + credentials (pre-prod OAuth issuer, user-API base, auth method) |
Routes: routes/qf_auth.py (/api/qf β OAuth2 login/callback + dev-stub login), routes/qf_content.py (/api/qf/content β read-only word-by-word for the Timestamps Analysis view), routes/bookmarks.py (/api/bookmarks β returns [] for anonymous so the FE silently stays app-local). QF caches (content token, chapter URLs, token cooldown, WBW translations) live in services/storage/cache.py.
Reference index β which sibling doc per task
| Doc | When to read |
|---|---|
database.md |
Working on the SQLite substrate β schema, migrations, repos, WAL/sync, CAS |
state-machine.md |
Lifecycle states, events, transition() handlers, admin overrides |
auth-permissions.md |
OAuth, identity cookie, roles, predicates, edit-lock/CSRF gating |
catalog.md |
Reciter taxonomy, ReciterCatalog, audio URL templates, accept flow |
segments-editor.md |
Save/undo flow, segment domain model, adapters, auto-split |
validation.md |
Validation registry, classifier categories, accordion lockstep with FE |
frontend.md |
Svelte 5 SPA β stores, tabs (dashboard/timestamps/segments), editGate |
accordion-guides.md |
Per-category guide copy for the validation accordion |
config-deploy.md |
Env vars, runtime profiles (dev/dev-Space/prod), HF Space deploy |
data-migrations.md |
Schema/data migration procedures, backfill scripts, drift checks |