Files
UmbraAudio/specs/09-web-review-ui.md
B.D. Murphy c86e323d7a Initial commit: UmbraAudio audiobook generation program
Quality-first multi-pass audiobook generation from English manuscripts.

Includes:
- umbraaudio/ core package (pipeline, config, voices, web review UI)
- scripts/ CLI utilities + model download script
- tts_server/ remote TTS server
- specs/ design docs, tests/, pyproject.toml + uv.lock
- Qwen3-TTS git submodule (pinned 022e286) with local demo.py UI patch
  saved at scripts/qwen3_tts_demo_umbra_patch.patch
- .env with runtime config/secrets

Excluded (see .gitignore): models/ (downloaded via scripts/download_models.sh),
output/, logs/, .venv, .umbra runtime data, and book projects
(DealHunterShip_*), which are committed separately.
2026-08-30 07:58:24 -07:00

18 KiB

Spec: Web Review UI

Overview

FastAPI + HTMX web interface for human review of generated audiobooks. Provides dashboards, voice review, chapter review with audio playback, and quality reports.

Technology Stack

  • Framework: FastAPI 0.104+
  • Templating: Jinja2 3.1+
  • Interactive: HTMX 1.9 (CDN)
  • Styling: Custom dark theme (no CSS framework)
  • Database: Synchronous sqlite3 (not aiosqlite — web runs in sync context)

App Factory

# umbraaudio/web/__init__.py
def create_app(config: AppConfig) -> FastAPI
  • Creates FastAPI app (title: "UmbraAudio Review", version: "0.1.0")
  • Sets up Jinja2Templates from templates/ directory
  • Mounts static files if static/ directory exists
  • Includes router from umbraaudio.web.routes
  • Stores config on app.state.config
  • Singleton pattern via global _app

Routes

HTML Pages (GET)

Route Function Template Description
GET / index() index.html Dashboard with stats, scan results, pipeline status, export files
GET /voices voice_review() voice_review.html Voice review page
GET /voices/partial voice_list_partial() _voice_list.html HTMX partial
GET /voices/{name}/history voice_history() _voice_history.html Per-character voice review history
GET /speakers speakers_review() speakers.html Speaker management (CRUD, aliases, merge)
GET /speakers/partial speakers_list_partial() _speakers_list.html HTMX partial
GET /proper-names proper_names_review() proper_names.html Proper-noun pronunciation review (/character-names is an alias)
GET /orphaned-utterances orphaned_utterances_page() orphaned_utterances.html Utterances whose speaker is unknown or __unassigned__
GET /voice-tests voice_tests_review() voice_tests.html Verification-phrase test samples
GET /voice-tests/partial voice_tests_partial() _voice_tests_list.html HTMX partial
GET /chapters/{id} chapter_review() chapter_review.html Chapter review
GET /chapters/{id}/partial chapter_utterances_partial() _utterance_list.html HTMX partial
GET /chapters-audio chapters_audio_review() chapters_audio.html Per-chapter audio listing
GET /chapters-audio/{chapter_dir} chapter_audio_review() chapter_audio_detail.html Utterance WAV review with speaker/text editing
GET /quality quality_report() quality_report.html Quality dashboard
GET /registry registry_index() registry.html Cross-project voice registry
GET /registry/partial registry_list_partial() _registry_list.html HTMX partial
GET /registry/search?q= registry_search() — (JSON) Search registry by character or alias

Audio Serving (GET)

Route Description
GET /audio/{chapter_id}/{filename} Serve WAV (prefers audio_normalized, falls back to audio)
GET /voice-audio/{filename} Serve voice reference WAVs from .umbra/voices
GET /voice-test-audio/{filename} Serve test sample (prefers .umbra/voices, falls back to .umbra/voice_tests)
GET /chapter-audio-file/{chapter_dir}/{filename} Serve per-utterance chapter WAV
GET /registry-audio/{registry_id}/{filename} Serve a registered voice reference from the registry DB path

API

Route Description
POST /api/upload-manuscript Upload .docx/.epub/.txt/.md/.pdf, parse chapters, rebuild utterances; archives prior state in mode=new, else keeps speakers
POST /api/speakers Create speaker (form fields: name, gender, type, aliases, traits, register, description, sample dialogue, narrator/group flags, accent)
PUT /api/speakers/{index} Update speaker
DELETE /api/speakers/{index} Delete speaker; flags their utterances __unassigned__
POST /api/speakers/{index}/alias Add alias
DELETE /api/speakers/{index}/alias/{alias} Remove alias
POST /api/speakers/{index}/alias/{alias}/move/{target} Move alias to another speaker
POST /api/speakers/{index}/alias/{alias}/create Promote alias to a new speaker
POST /api/speakers/{source}/merge/{target} Merge speakers (attributes, aliases, chapters merged; utterances repointed)
POST /api/speakers/voice-status Set voice_status.json entry; on approved, upsert into voice_registry
POST /api/speakers/regenerate-voice Regenerate one speaker's voice reference (background)
POST /api/speakers/generate-all-voices Generate all missing voice references (background, skips existing)
PUT /api/phonetic/{speaker_name} Set/edit phonetic spelling; stored as an unconfirmed user candidate, regenerates preview audio (clearing the value deletes the record + audio)
GET /api/pronunciation/{name} Look up the most common pronunciation (dictionaryapi.dev for common words, Wiktionary API for names) and return the candidate without saving
POST /api/pronunciations/{name}/lookup Internet lookup → candidate record in phonetic_names.json + generated preview audio; dictionary hits auto-confirm as common, Wiktionary hits are stored needs review
POST /api/pronunciations/{name}/confirm Mark a candidate confirmed (only confirmed spellings are applied to TTS)
POST /api/pronunciations/{name}/clear Delete the record and its preview audio
GET /api/pronunciations/audio?name= Serve the preview WAV from .umbra/pronunciation_audio/{sanitized}.wav
GET /api/pronunciations/status Read batch progress from pronunciation_status.json ({status, detail, done, total})
POST /api/auto-pronunciations Background batch lookup over all proper nouns; auto-confirms common words, generates preview audio for Wiktionary hits, re-looks-up legacy records, writes progress to pronunciation_status.json
POST /api/voices/{id}/approve Approve character voice (records voice_review_history)
POST /api/voices/{id}/reject Reject character voice
POST /api/voices/{id}/edit Queue a voice edit prompt (stored in history)
POST /api/chapters/{id}/approve Approve chapter (review_decisions)
POST /api/chapters/{id}/regenerate Stub (returns {"status": "queued"})
PUT /api/utterance/{utt_id}/speaker Reassign utterance speaker; deletes stale WAV and regenerates via Edge fallback
PUT /api/utterance/{utt_id}/text Edit utterance text; deletes stale WAV
DELETE /api/utterance/{utt_id} Delete utterance; renumber chapter; regenerate chapter WAVs via Qwen3
POST /api/chapter/{chapter_id}/utterance Append utterance; generate its WAV in background
POST /api/orphaned-utterances/{utt_id}/reassign Reassign one orphaned utterance
POST /api/orphaned-utterances/reassign-all Reassign all utterances that had the given old names
POST /api/analyze Run character discovery in background (analysis_status.json)
GET /api/analysis-status Read analysis_status.json
POST /api/segment-utterances Run segmentation + LLM attribution + emotion analysis (skips attribution if LM Studio offline)
POST /api/generate-audiobook Run full Qwen3-TTS generation in background (scripts.generate_all_audio.main)
GET /api/generation/status Live generation progress (items, success/failure counts, interrupted flag)
GET /api/pipeline/status Pipeline state machine status
POST /api/pipeline/status Update pipeline status fields
POST /api/pipeline/start Start pipeline (run_pipeline_background)
POST /api/pipeline/pause Pause pipeline
POST /api/pipeline/resume Resume paused pipeline
POST /api/pipeline/cancel Cancel pipeline
POST /api/pipeline/reset?step= Reset discover (speakers/voices/phonetics) / voices / segment (utterances)
POST /api/pipeline/run-all Full pipeline: discover → voices → segment+attribute → generate → quality gate → export M4B/MP3
POST /api/chapter/{id}/build Stitch chapter WAVs into ch{id}_full.wav after quality gate
GET /api/quality/check Quality-check all chapters (JSON)
GET /api/quality/check/{chapter_id} Quality-check one chapter (JSON)
GET /api/export/files List export directory files with download URLs
GET /api/export/download/{path} Download export file (path-traversal protected)

Pages

Dashboard (/)

Stats displayed:

  • Project name, source path, and format (from project row + scan_results.json)
  • Chapter count, character count (non-narrator), utterance count, audio segment count
  • Scene count (<<<>>> markers across chapter source text)
  • Speaker count (from speakers.json) and voice reference count (*_ref.wav in .umbra/voices)
  • Pipeline status (current step, progress, error)
  • Export files (name + size, with download links)

Voice Review (/voices)

HTMX-powered page that loads character data on demand.

_voice_list.html partial:

Column Content
Name Character name
Description Truncated voice_description JSON
Role Badge: Narrator / Character
Status Badge: voiced_approved / rejected / pending
Energy params Derived exaggeration / cfg_weight from ATTRIBUTE_PARAM_MAP[energy] (deterministic from name hash; narrator uses fixed values)
Audio HTML5 <audio> player (if voice_ref exists)
Actions Approve / Reject / Edit-prompt buttons (HTMX POST)

Approve/Reject flow:

  1. HTMX POST to /api/voices/{id}/approve or /api/voices/{id}/reject
  2. Inserts a voice_review_history record (action, prompt, resulting exaggeration/cfg weight)
  3. Updates characters.status to voiced_approved or rejected

Speakers (/speakers)

Speaker management backed by .umbra/speakers.json (not the DB characters table). HTMX _speakers_list.html partial.

Columns: primary name, aliases (with move/promote/remove actions), demographics (type/gender), vocal traits, register, flagged status (no traits + unknown gender + no description → flagged for review), voice status, audio player.

Operations:

  • Add / edit / delete speaker
  • Add / remove / move / promote aliases
  • Merge one speaker into another (attributes and aliases merged; utterances repointed to the target)
  • Delete flags the speaker's utterances as __unassigned__ for later review
  • Set voice status (pending/approved); approving upserts the reference into the cross-project voice_registry
  • Regenerate voice reference (single speaker or all missing)

Proper Names (/proper-names)

Extracts capitalized noun phrases from chapter source text, filters titles/stopwords, counts frequency, and flags which are known speakers. Pronunciation is reviewed per name through a lookup → listen → confirm flow:

  • Lookup (POST /api/pronunciations/{name}/lookup): searches dictionaryapi.dev (on the name and its lowercase form) then the Wiktionary API. Dictionary hits are well-understood common words — auto-confirmed and marked Common. Wiktionary hits are real names/places — stored as unconfirmed candidates (Needs review).
  • Preview audio: the phonetic spelling is synthesized with the narrator's cloned voice (.umbra/pronunciation_audio/{sanitized}.wav) so the reviewer hears exactly what the substitution will sound like. TTS failures (e.g. transient server errors) are captured into audio_error on the record and surfaced in the UI without blocking manual entry.
  • Confirm: after listening, the reviewer confirms the candidate; only confirmed spellings are returned by load_phonetic_names() and substituted into utterance text before synthesis.
  • Edit: changing the spelling stores it as an unconfirmed user candidate and regenerates the preview audio.
  • Batch ("Look Up All Pronunciations"): background pass over every proper noun, skipping records that already have a real source, re-looking-up legacy records, with progress polled from /api/pronunciations/status.
  • Fictional/invented names with no dictionary or Wiktionary entry show "not found" and are set manually.
  • The page offers a text filter and a status filter (Needs review / Confirmed / Common / No pronunciation) with per-row status badges.

Shared logic lives in umbraaudio/pronunciation.py (record load/save with legacy {name: "SPELLING"} migration, IPA→ASCII respelling, lookups, preview synthesis). scripts/generate_all_audio.py loads confirmed spellings once and applies them (longest-name-first) to every utterance before synthesis.

Orphaned Utterances (/orphaned-utterances)

Lists utterances whose speaker_name is (a) not a known speaker name/alias, or (b) __unassigned__. Each can be reassigned to any known speaker; "reassign all" repoints every utterance that previously used a given old name.

Voice Tests (/voice-tests)

Lists verification-phrase test samples from .umbra/voice_tests/*.wav joined with voice_assignments.json and speakers.json for voice/demographics context. Audio served via /voice-test-audio/{filename} (falls back to .umbra/voices).

Chapter Audio Review (/chapters-audio)

  • Listing page: one row per chapter (title, utterance count, directory ch{id})
  • Detail page (/chapters-audio/ch{id}): every utterance with index, speaker, truncated text, resolved voice, and an audio player for utt_{index:04d}.wav (if present)
  • Per-utterance actions: reassign speaker (PUT /api/utterance/{id}/speaker), edit text (PUT /api/utterance/{id}/text), delete (DELETE /api/utterance/{id}), append utterance (POST /api/chapter/{chapter_id}/utterance); edits delete the stale WAV and trigger background regeneration
  • Speaker dropdown canonicalization: chapter_audio_review builds a lower_to_canonical map from the primary names, all aliases, and the first word of each primary name, so an utterance whose stored speaker is an alias/first-word (e.g. kainda for Kainda Walters Foster Murphy) still selects the correct dropdown option instead of defaulting to AI-1
  • Chapter build (POST /api/chapter/{id}/build): quality-gates then stitches into ch{id}_full.wav

Registry (/registry)

Cross-project voice reference registry (voice_registry_path DB, default ~/.umbra/voice_registry.db). Lists approved characters with their reference paths and audio, searchable by name or alias.

Chapter Review (/chapters/{id})

Features:

  • Chapter navigation links (all 35 chapters, highlights current)
  • Approve Chapter button (HTMX POST)
  • HTMX loads _utterance_list.html partial

_utterance_list.html partial:

For each utterance:

  • Speaker name (from speaker_name column)
  • Type badge (narration / dialogue)
  • Utterance text
  • Audio player (if audio exists)

Audio playback:

<audio controls preload="none">
    <source src="/audio/{chapter_id}/{filename}" type="audio/wav">
</audio>

Quality Report (/quality)

Sections:

  1. Defects table — All logged quality issues with severity badges
  2. Chapter summary — Per-chapter audio count, MOS scores, similarity

SQL for chapter summary:

SELECT ch.id, ch.title,
       COUNT(ga.id) as audio_count,
       MIN(ga.mos_score) as min_mos,
       MAX(ga.mos_score) as max_mos,
       AVG(ga.speaker_similarity) as avg_sim
FROM chapters ch
LEFT JOIN utterances u ON u.chapter_id = ch.id
LEFT JOIN generated_audio ga ON ga.utterance_id = u.id
GROUP BY ch.id ORDER BY ch.chapter_index

Database Access

The web routes use synchronous sqlite3 (not aiosqlite) and run SCHEMA_SQL (CREATE TABLE IF NOT EXISTS for project, chapters, characters, voice_review_history, pronunciation_guide, utterances, generated_audio, quality_reports, review_decisions, plus indexes) on every connection:

def _get_db(request: Request) -> sqlite3.Connection:
    config = request.app.state.config
    db_path = config.data_dir / "project.db"
    db = sqlite3.connect(str(db_path))
    db.row_factory = sqlite3.Row
    db.executescript(SCHEMA_SQL)
    return db

State JSON files are also read/written directly by routes: speakers.json, voice_assignments.json, voice_status.json, phonetic_names.json, pipeline_status.json, analysis_status.json, scan_results.json, defects.json.

Background Job Pattern

Long-running work (analysis, segmentation, voice generation, audiobook generation, pronunciation lookup, chapter builds, utterance regeneration) runs in daemon threading.Threads. Status surfaces through:

  • pipeline_status.json — coarse step/status/detail/progress for pipeline flows
  • analysis_status.json — character discovery progress
  • generation_manager — item-level live progress for /api/generation/status (rejects concurrent runs with HTTP 409)
  • WAV file existence — the generation script's natural resume marker

Theme

Dark theme with custom CSS:

  • Background: #1a1a2e
  • Text: #e0e0e0
  • Accent: #7c3aed (purple)
  • Card background: #16213e
  • Button approve: #22c55e (green)
  • Button reject: #ef4444 (red)

HTMX Integration

Pages use HTMX for partial updates:

  • hx-trigger="load" — Load content on page load
  • hx-get="/voices/partial" — Fetch partial HTML
  • hx-target="#voice-list" — Target element for replacement
  • hx-swap="innerHTML" — Replace content

Starting the Server

# Via CLI
umbraaudio review --host 0.0.0.0 --port 8420

# Via run_review.sh
setsid /home/bm/Apps/UmbraAudio/run_review.sh > /tmp/umbraaudio_review.log 2>&1 &

Known Limitations

  • POST /api/chapters/{id}/regenerate is a stub (returns {"status": "queued"}); real regeneration is file-targeted via utterance edit/delete endpoints
  • POST /api/utterance/{id}/speaker regenerates the utterance with the Edge-TTS fallback, not Qwen3-TTS (inconsistent with bulk generation)
  • No authentication/authorization
  • No WebSocket for real-time updates (long-running jobs poll JSON status files)
  • No pagination for large utterance lists
  • Database connection opened per-request (no connection pooling)
  • Dashboard speaker/voice counts read speakers.json/.umbra/voices, which may drift from the DB characters table