No description
  • Python 89.6%
  • HTML 4.6%
  • JavaScript 2.7%
  • CSS 2.6%
  • Dockerfile 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jonathan Cremin cf60c0ed99
All checks were successful
ci / build-image (push) Successful in 20s
Chunk transcription to bound memory for long audiobooks
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
2026-07-18 22:50:34 +01:00
.forgejo/workflows Add Forgejo CI + containerisation, k8s media-path alignment 2026-07-18 22:13:31 +01:00
cadence Chunk transcription to bound memory for long audiobooks 2026-07-18 22:50:34 +01:00
scripts Chunk transcription to bound memory for long audiobooks 2026-07-18 22:50:34 +01:00
tests Chunk transcription to bound memory for long audiobooks 2026-07-18 22:50:34 +01:00
.dockerignore Initial commit 2026-07-17 22:15:40 +01:00
.gitignore Initial commit 2026-07-17 22:15:40 +01:00
config.example.yaml Initial commit 2026-07-17 22:15:40 +01:00
Containerfile Add Forgejo CI + containerisation, k8s media-path alignment 2026-07-18 22:13:31 +01:00
docker-compose.yml Add Forgejo CI + containerisation, k8s media-path alignment 2026-07-18 22:13:31 +01:00
pyproject.toml Add Forgejo CI + containerisation, k8s media-path alignment 2026-07-18 22:13:31 +01:00
README.md Add Forgejo CI + containerisation, k8s media-path alignment 2026-07-18 22:13:31 +01:00

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. Copy config.example.yaml to config.yaml and fill it in, or supply secrets via CADENCE_* 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:

  1. Transcribes the audiobook with Whisper (faster-whisper).
  2. Extracts the ebook's linear text and CFI markers from the EPUB.
  3. Force-aligns the transcript to the ebook text (monotonic fuzzy match) to build a table of anchors: audio_time ↔ char_offset ↔ CFI.
  4. 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 epubProgress CFI at 66.2%.
    • B→A: Grimmory at 12% → ABS currentTime ≈ 9.2 min.
  • Connectors — ABS (libraries/items/metadata, progress GET/PATCH) and Grimmory (JWT login, list_books, progress POST /api/v1/books/progressepubProgress+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 positionData accepts the CFI as-is.

Development

pip install -e '.[dev]'
pytest        # pairing, position map, reconcile logic, and alignment are covered offline