- Python 89.6%
- HTML 4.6%
- JavaScript 2.7%
- CSS 2.6%
- Dockerfile 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
ci / build-image (push) Successful in 20s
Whisper decoding loaded the whole audiobook into RAM, so peak memory scaled with length (~8-10GB for a 15h book) — which OOM-killed the pod. Now the audio is decoded and transcribed in fixed windows (cadence/alignment/audio.py, default 30 min via CADENCE_ALIGNMENT__CHUNK_SECONDS) with per-chunk timestamps offset back to absolute time. Measured on the 61-min test book: peak RSS 2367MB -> 1196MB, anchors 258 -> 283 (positions unchanged), per-chunk audio fixed at ~38MB (10-min) regardless of book length. A 15h book now peaks ~1.3GB instead of ~8GB. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VF6ueCUaDQ2aKVsmmwF6Mx |
||
| .forgejo/workflows | ||
| cadence | ||
| scripts | ||
| tests | ||
| .dockerignore | ||
| .gitignore | ||
| config.example.yaml | ||
| Containerfile | ||
| docker-compose.yml | ||
| pyproject.toml | ||
| README.md | ||
Cadence
Cadence synchronises reading positions between Audiobookshelf and Grimmory.
It does this by transcribing the audiobooks, aligning them with the ebooks and acting as a bridge between the two: listen for a while, then pick up the ebook exactly where the narrator left off — and vice versa.
Secrets: server URLs and tokens live in
config.yaml(git-ignored), not here. Copyconfig.example.yamltoconfig.yamland fill it in, or supply secrets viaCADENCE_*environment variables.
How it works
The two apps track your place in incompatible units — Audiobookshelf in audio time (seconds), Grimmory in text location (EPUB CFI / percentage). A flat percentage copy is inaccurate (front/back matter, chapter openers, and narration asides shift the mapping). So per book, Cadence:
- Transcribes the audiobook with Whisper (
faster-whisper). - Extracts the ebook's linear text and CFI markers from the EPUB.
- Force-aligns the transcript to the ebook text (monotonic fuzzy match) to
build a table of anchors:
audio_time ↔ char_offset ↔ CFI. - Bridges the two live: a progress change on one side is translated through that map and written to the other. Sync is bidirectional with last-write-wins by timestamp.
Audiobookshelf ⇄ [ connectors → sync engine → position map ] ⇄ Grimmory
(socket + REST) SQLite: pairings, anchors, state (REST)
Architecture
| Module | Responsibility |
|---|---|
cadence/connectors/abs.py |
ABS REST (progress GET/PATCH, items) + Socket.IO change events |
cadence/connectors/grimmory.py |
Grimmory REST (books, read/write progress) |
cadence/pairing.py |
Match ABS items ↔ Grimmory books (ISBN/ASIN, then fuzzy title+author) |
cadence/alignment/ |
Transcribe, EPUB text/CFI extraction, aligner, and the PositionMap |
cadence/reconcile.py |
Pure last-write-wins + echo-suppression decision logic |
cadence/sync.py |
Reads progress, translates via the map, writes the other side |
cadence/web/ |
FastAPI management UI (dashboard, discover, align, manual pair) |
cadence/jobs.py |
Background alignment queue with live progress |
cadence/service.py |
Runs the web UI + socket sync + poll in one event loop |
cadence/db.py |
SQLite state (pairings, anchors, sync state) |
Usage
pip install -e '.[transcribe]' # omit [transcribe] for the bridge only
cp config.example.yaml config.yaml # then edit URLs/tokens
cadence run # start the service: web UI (:8080) + sync engine
Then open http://localhost:8080 and manage everything from the browser:
- Discover pairings — auto-match the ABS and Grimmory libraries.
- Confirm a pairing to enable syncing; Align transcribes + aligns the book with a live progress bar; Manual pair links books by hand.
- Confirmed + aligned books then sync automatically (ABS socket + poll).
The CLI subcommands (cadence pair, pairings, confirm, align, unpair)
remain for scripting/debugging. Alignment reads the audiobook + EPUB directly
from the mounted media volumes (paths auto-resolved from ABS/Grimmory metadata);
it's slow on CPU but the transcript is cached so re-alignment is cheap.
Configuration
See config.example.yaml. Any value can be overridden by environment variable
CADENCE_<SECTION>__<KEY> (e.g. CADENCE_AUDIOBOOKSHELF__TOKEN); prefer env for
secrets.
Deployment
docker-compose.yml runs Cadence beside ABS and Grimmory. Mount the media
volumes read-only and supply tokens via env.
Status
Verified end-to-end live against a real Audiobookshelf + Grimmory deployment, on The Butcher of Anderson Station (61-min audiobook + EPUB):
- Alignment — Whisper (
base) transcribed the audiobook and the fuzzy aligner produced 258 anchors in ~6 min on CPU. Spot-checks confirmed audio timestamps land on the exact narrated lines (0:30 → the opening sentence, 30:00 → 66% of the text, etc.). - Bidirectional sync — driven through the real connectors:
- A→B: ABS position 30:00 → Grimmory
epubProgressCFI at 66.2%. - B→A: Grimmory at 12% → ABS
currentTime≈ 9.2 min.
- A→B: ABS position 30:00 → Grimmory
- Connectors — ABS (libraries/items/metadata, progress GET/PATCH) and
Grimmory (JWT login,
list_books, progressPOST /api/v1/books/progress↔epubProgress+lastReadTime). Pairing auto-matched 72 books.
Known limitations / deferred:
- Un-narrated tail — audio past the last anchor (e.g. back-matter the narration skips) clamps to the final aligned position. Expected; affects only the very end of a book.
- ABS progress delete — ABS exposes no delete-progress route; Cadence only
ever writes positions (never needs to delete), so this only affected test
cleanup (reset to
currentTime=0). - OIDC — the connector supports client-credentials auth (
OIDCConfig), but the live instance currently uses local login; OIDC to be revisited later. - CFI fidelity — CFIs generated from the EPUB are approximate; percentage is
sent alongside as the exact channel, and
positionDataaccepts the CFI as-is.
Development
pip install -e '.[dev]'
pytest # pairing, position map, reconcile logic, and alignment are covered offline