B.D. Murphy 0909d050d9 Probe the route correctly: credentials, CSRF ordering, and a real dry run
The controller's ingest endpoint came up between runs and this tool misreported
it three more times, each in the same direction — naming the state it was in
when the check was written rather than the state it was reading.

* POST was probed without a credential, so a live route kept answering 401 and
  the verdict stayed AUTH_REQUIRED after auth was configured and T02 passed.
  The probe now sends whatever headers exist, because an unauthenticated probe
  answers "is there a door", never "does it open".

* The rule for reading a status was wrong twice. First it accepted only
  200/201/405 as routed, so a 401 on a live route read as absent. Then it was
  widened to "anything but 404", which would have read a CSRF 403 as a route:
  the controller checks CSRF in middleware before routing (app.py:246-248) and
  exempts only /api/node (app.py:89), so POST /api/compute/audio/import answers
  403 whether or not a route exists — and none does. Now: on a safe method, 404
  means unrouted and anything else means routed; on an unsafe method a 403 off
  an exempt prefix is discarded and recorded as csrf_masked_probe.

* T04 dropped csrf_masked_probe from its FAIL evidence, which is exactly the
  case where the next reader needs it. Found by the new tests, not by reading.

* T05 returned BLOCKED unconditionally, so the one test that mattered was the
  least willing to run. It now executes a real dry run against --bundle.
  dry_run short-circuits before store.commit(), so running it cannot write.

* authenticate() sent X-Node-Key, but require_node parses only "bearer "
  (routes_compute.py:477) — a valid key reported as rejected. It also probed
  /api/auth/me, which resolves a browser session, so no node credential could
  ever pass it. Now Bearer on /api/node/agent, and 403 there counts as
  authenticated-but-unscoped, since the scope check runs after resolution.

* _summarise reduced a plain-text body to "<str>", discarding "Internal Server
  Error" and every handler message — the detail worth reading.

* Four verdict names, including INGEST_ROUTED_NEEDS_RESTART, kept describing a
  restart that had already happened. _readiness now derives the name from
  evidence held by this run, and a real dry run outranks every transport
  observation: the current verdict is DRY_RUN_REJECTED.

Measured against the controller: migration 0008 applied (27 -> 31 tables, guard
proven to fire, idempotent on re-run), service restarted to PID 392511, endpoint
routed and parsing. The dry run executes and is rejected on content_hash,
because the HTTP path re-serializes records (json.dumps default separators,
ensure_ascii=True) while their own read_bundle() hashes the file bytes — the
same bundle yields d0011ae6 over HTTP and 21ce5687 on disk, and their validator
reports 0 problems on disk. Reported to the controller; bundle hash unchanged.

Also reported: routes_compute.py:1809 constructs InMemoryAudioImportStore,
whose commit() appends to a list, and no PostgreSQL adapter exists — so a
successful commit would return a uuid4() project_id while all four
translation_* tables stayed empty. Read from source, not reproduced.

30 new tests pin the routing rule, the CSRF ordering, the credential header,
and every verdict branch. Full suite 799 passed / 8 failed (pre-existing
voice_profile_manager + test_pronunciation). ruff and mypy clean.
2026-10-15 20:41:05 -07:00

UmbraAudio

Quality-first multi-pass audiobook generation from English manuscripts.

Features

  • 5-pass pipeline: Analysis → Voice Design → Mapping → Test → Generate
  • Multi-engine TTS: Chatterbox (primary), Qwen3-TTS (secondary)
  • LLM-enhanced: BookNLP for NLP, LLM for profiling/emotion/pronunciation
  • Web review: FastAPI + HTMX at checkpoints for human approval

Quick Start

python -m umbraaudio.cli init <manuscript.epub> --name 'My Book'
python -m umbraaudio.cli analyze
python -m umbraaudio.cli voices
python -m umbraaudio.cli review

Requirements

  • Python 3.11+
  • NVIDIA GPU with CUDA support (RTX 3060 or better recommended)
  • ffmpeg

Development

pip install -e ".[dev]"
ruff check umbraaudio/
pytest tests/ -v

Commit cadence: commit at least once every Sunday. Never leave a week (let alone a month) of work uncommitted — a single accidental git checkout on 2026-09-20 wiped out a month of changes to umbraaudio/web/routes.py. See AGENTS.md → Commit Cadence.

scripts/commit_check.sh enforces the nudge: it exits non-zero once the tree has been dirty longer than a week. The LaunchAgent com.umbraaudio.commitcheck runs it every Sunday at 18:00 and posts a macOS notification when there is uncommitted work.

scripts/commit_check.sh            # warn if dirty > 7 days
scripts/commit_check.sh --days 3   # tighter threshold
scripts/commit_check.sh --any      # warn on any uncommitted change

Rebuilding from source

The repo tracks source only; large runtime data is reconstructed as needed.

  1. Models: weights are not committed (see .gitignore). Download them:

    bash scripts/download_models.sh   # ~9 GB, targets ./models
    
  2. Qwen3-TTS is a git submodule pinned to upstream commit 022e286:

    git submodule update --init --recursive
    

    The tts_server/web-demo backend includes a local UI customization to qwen_tts/cli/demo.py. It is preserved as a patch — reapply it after checking out the submodule:

    cd Qwen3-TTS && git apply ../scripts/qwen3_tts_demo_umbra_patch.patch
    
  3. Environment: copy/keep .env (contains secrets) and set UMBRA_TTS__* model paths under models/.

Book projects (e.g. DealHunterShip_...) are committed separately under their own project directories and are not part of this program repository.

Description
Quality-first multi-pass audiobook generation pipeline (5-pass: Analysis -> Voice Design -> Mapping -> Test -> Generate). FastAPI+HTMX review UI, Edge-TTS/Qwen3-TTS multi-engine voice, SQLite state.
Readme Apache-2.0 37 MiB
Languages
Python 84.8%
HTML 11.4%
Shell 3%
PLpgSQL 0.7%