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.
5.1 KiB
5.1 KiB
Spec: Database Schema
Overview
UmbraAudio uses SQLite for all project state. The database tracks the full lifecycle from parsed manuscript through generated audio. Character discovery and voice assignments are stored in JSON files for flexibility.
Current Implementation
Actual Schema (project.db)
-- Project metadata
CREATE TABLE IF NOT EXISTS project (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
source_path TEXT NOT NULL,
source_format TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
status TEXT NOT NULL DEFAULT 'initialized'
);
-- Chapters with source text
CREATE TABLE IF NOT EXISTS chapters (
id INTEGER PRIMARY KEY,
project_id INTEGER NOT NULL,
chapter_index INTEGER NOT NULL,
title TEXT,
source_text TEXT NOT NULL,
word_count INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'parsed',
audio_path TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Utterances (narration and dialogue)
CREATE TABLE IF NOT EXISTS utterances (
id INTEGER PRIMARY KEY,
chapter_id INTEGER NOT NULL,
character_id INTEGER,
utterance_index INTEGER NOT NULL,
text TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'narration',
context_before TEXT,
context_after TEXT,
emotion TEXT,
pace TEXT,
emphasis_words TEXT,
pause_before_ms INTEGER,
pause_after_ms INTEGER,
instruct TEXT,
speaker_name TEXT,
status TEXT NOT NULL DEFAULT 'pending'
);
-- Generated audio files
CREATE TABLE IF NOT EXISTS generated_audio (
id INTEGER PRIMARY KEY,
utterance_id INTEGER NOT NULL,
character_id INTEGER,
audio_path TEXT NOT NULL,
duration_ms INTEGER,
mos_score REAL,
wer_score REAL,
speaker_similarity REAL,
artifact_score REAL,
generation_params TEXT,
status TEXT NOT NULL DEFAULT 'generated',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
JSON Data Files
speakers.json
{
"speakers": [
{
"primary_name": "Kainda",
"aliases": ["Princess Kainda", "Princess", "Captain", "XO Capek"],
"demographics": {
"type": "Human",
"gender": "Female"
},
"vocal_traits": ["assertive", "clear", "young"],
"communication_style": {
"primary_register": "Formal",
"style_shifts_or_notes": "Shifts to casual with close allies"
},
"is_narrator": false,
"is_group": false,
"description": "Princess and communication specialist",
"sample_dialogue": "I'm on a yacht.",
"chapters_appeared": [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
}
]
}
voice_assignments.json
{
"Narrator": "en-US-GuyNeural:-10",
"Kainda": "en-US-JennyNeural",
"Captain Weston": "en-US-ChristopherNeural",
"Hunter": "en-US-JasonNeural",
"Nanny": "en-GB-SoniaNeural",
"Aunt Vicky": "en-US-AvaNeural",
"Admiral Wake": "en-US-AndrewNeural",
"Princess Kainda": "en-US-JennyNeural",
"Doctor": "en-US-GuyNeural",
"Guards": "en-US-GuyNeural:+48"
}
Entity Relationships
project (1) ──< chapters (many)
chapters (1) ──< utterances (many)
utterances (1) ──< generated_audio (many)
speakers.json ──< (character profiles)
voice_assignments.json ──< (voice mappings)
Key Differences from Original Spec
- No characters table: Character profiles stored in
speakers.json - No pronunciation_guide table: Not currently implemented
- No quality_reports table: Quality tracking via web UI
- No review_decisions table: Review via web UI endpoints
- speaker_name column: Added to utterances for speaker attribution
- JSON-based configuration: speakers.json and voice_assignments.json
Database Class API
class Database:
def __init__(self, db_path: Path) -> None
def connect(self) -> None
def close(self) -> None
Query Methods
| Method | Returns | Description |
|---|---|---|
get_chapters() |
list[dict] |
All chapters ordered by index |
get_utterances(chapter_id) |
list[dict] |
Utterances for a chapter |
get_audio_for_chapter(chapter_id) |
list[dict] |
Generated audio files |
Data Flow
- Parsing: Manuscript → chapters table (source_text)
- Character Discovery: LLM analysis → speakers.json
- Voice Assignment: Manual/automated → voice_assignments.json
- Segmentation: Chapter text + speakers.json → utterances table
- Audio Generation: utterances + voice_assignments.json → chapter_audio/
Indexes
CREATE INDEX IF NOT EXISTS idx_utterances_chapter ON utterances(chapter_id);
CREATE INDEX IF NOT EXISTS idx_generated_audio_utterance ON generated_audio(utterance_id);
CREATE INDEX IF NOT EXISTS idx_chapters_project ON chapters(project_id);