Files
UmbraAudio/specs/15-multi-project.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

435 lines
14 KiB
Markdown

# Spec: Multi-Project Support
## Overview
UmbraAudio currently operates on a single project stored in `.umbra/`. This spec adds first-class support for **multiple independent book projects**, each isolated with its own data, configuration, and outputs. Projects are identified by a slug (e.g., `deal-hunter`, `the-great-adventure`) and can be created, listed, switched, and removed via CLI and Web UI.
## Architecture
### Project Root Layout
```
UmbraAudio/ # Repository root
├── projects/ # Project registry (new)
│ ├── index.json # Master index of all projects
│ ├── deal-hunter/ # Project slug directory
│ │ ├── .umbra/ # Project data (was .umbra/)
│ │ ├── config.yaml # Project-local overrides
│ │ └── manifest.json # Project metadata
│ ├── the-great-adventure/
│ │ ├── .umbra/
│ │ ├── config.yaml
│ │ └── manifest.json
│ └── ...
├── .umbra/ # DEPRECATED: legacy single-project location
└── umbraaudio/ # Package code (unchanged)
```
### Project Identity
| Field | Type | Description |
|-------|------|-------------|
| `slug` | string | URL-safe identifier (kebab-case), unique, immutable |
| `title` | string | Human-readable book title |
| `author` | string | Author name |
| `created_at` | ISO8601 | Creation timestamp |
| `updated_at` | ISO8601 | Last activity timestamp |
| `status` | enum | `active`, `archived`, `error` |
| `source_format` | string | `docx`, `epub`, `pdf`, `txt`, `md` |
| `source_path` | string | Path to original manuscript (relative to project dir or absolute) |
| `chapters` | int | Number of parsed chapters |
| `speakers` | int | Number of discovered speakers |
| `pipeline_step` | string | Current pipeline stage (`parsed`, `analyzed`, `designed`, `mapped`, `tested`, `generated`, `exported`) |
### Project Index (`projects/index.json`)
```json
{
"version": 1,
"default_project": "deal-hunter",
"projects": [
{
"slug": "deal-hunter",
"title": "Deal Hunter",
"author": "B.D. Murphy",
"created_at": "2026-08-01T04:22:45",
"updated_at": "2026-08-06T10:30:00",
"status": "active",
"source_format": "docx",
"source_path": "uploads/DealHunterShip_4.0e_whole-to-audio.docx",
"chapters": 35,
"speakers": 61,
"pipeline_step": "generated"
},
{
"slug": "the-great-adventure",
"title": "The Great Adventure",
"author": "John Smith",
"created_at": "2026-08-06T14:00:00",
"updated_at": "2026-08-06T14:00:00",
"status": "active",
"source_format": "epub",
"source_path": "uploads/the-great-adventure.epub",
"chapters": 12,
"speakers": 8,
"pipeline_step": "parsed"
}
]
}
```
## Data Isolation
Each project is **completely independent**:
| Resource | Location | Shared? |
|----------|----------|---------|
| SQLite database (`project.db`) | `projects/{slug}/.umbra/project.db` | No |
| Speakers JSON | `projects/{slug}/.umbra/speakers.json` | No |
| Voice assignments | `projects/{slug}/.umbra/voice_assignments.json` | No |
| Voice references | `projects/{slug}/.umbra/voices/` | No |
| Chapter audio | `projects/{slug}/.umbra/chapter_audio/` | No |
| Voice tests | `projects/{slug}/.umbra/voice_tests/` | No |
| Pronunciation guide | `projects/{slug}/.umbra/pronunciation_guide/` | No |
| Exports | `projects/{slug}/export/` | No |
| Logs | `projects/{slug}/.umbra/logs/` | No |
| **Voice Registry DB** | `.umbra/voice_registry.db` | **Yes** (global) |
| **Phonetic names** | `.umbra/phonetic_names.json` | **Yes** (global) |
| **Qwen TTS models** | `models/` | **Yes** (global) |
| **LM Studio** | external | **Yes** (global) |
**Shared resources** are global infrastructure; **project resources** are fully isolated.
## Configuration
### Global Config (`config.yaml` at repo root)
```yaml
# Global settings (apply to all projects unless overridden)
tts:
engine: qwen
device: cuda
sample_rate: 24000
qwen_tts_url: "http://localhost:8000"
voicedesign_model: "/mnt/models/qwen3-tts/Qwen3-TTS-12Hz-1.7B-VoiceDesign"
base_model: "/mnt/models/qwen3-tts/Qwen3-TTS-12Hz-1.7B-Base"
llm:
provider: auto
lmstudio_base_url: "http://localhost:1234/v1"
lmstudio_model: "google/gemma-4-e4b"
temperature: 0.3
max_tokens: 4096
web:
host: "0.0.0.0"
port: 8420
auto_open: false
quality:
min_mos: 3.8
min_test_mos: 4.0
max_wer: 0.03
min_speaker_similarity: 0.80
audio:
target_rms_db: -20.0
peak_limit_db: -3.0
```
### Project-Local Config (`projects/{slug}/config.yaml`)
```yaml
# Optional overrides for this project only
project:
slug: "deal-hunter"
title: "Deal Hunter"
author: "B.D. Murphy"
tts:
engine: qwen
# device inherits from global
# qwen_tts_url inherits from global
llm:
# lmstudio_model can override per project
# temperature can override per project
quality:
min_mos: 4.0 # stricter for this project
audio:
target_rms_db: -18.0 # louder master
```
**Merge precedence**: project config → global config → defaults.
## CLI Interface
### Project Management Commands
```bash
# List all projects
python -m umbraaudio.cli project list
# Show current project
python -m umbraaudio.cli project current
# Create new project from manuscript
python -m umbraaudio.cli project create \
--title "The Great Adventure" \
--author "John Smith" \
--source /path/to/book.epub \
--slug the-great-adventure
# Switch active project
python -m umbraaudio.cli project switch the-great-adventure
# Show project details
python -m umbraaudio.cli project info the-great-adventure
# Archive project (keeps data, marks inactive)
python -m umbraaudio.cli project archive the-great-adventure
# Restore archived project
python -m umbraaudio.cli project restore the-great-adventure
# Delete project (removes all data - requires confirmation)
python -m umbraaudio.cli project delete the-great-adventure --confirm
# Run pipeline commands on specific project
python -m umbraaudio.cli analyze --project the-great-adventure
python -m umbraaudio.cli generate --project the-great-adventure
python -m umbraaudio.cli pipeline --project the-great-adventure -c all
```
### Default Project Behavior
- If `--project` not specified, uses `default_project` from `projects/index.json`
- If no default set, uses first project in index
- Legacy single-project mode: if `projects/` doesn't exist but `.umbra/` does, auto-migrate on first run
## Web UI
### Project Selector
- Top-left dropdown in navbar showing current project
- Clicking opens modal with project cards (title, author, status, last updated)
- "New Project" button in modal → upload form
- "Switch" action reloads page with new project context
- "Create New" navigates to `/projects/new`
### Project-Aware Endpoints
All existing endpoints become project-scoped:
| Old Path | New Path | Notes |
|----------|----------|-------|
| `/` | `/projects/{slug}/` | Dashboard |
| `/voices` | `/projects/{slug}/voices` | Voice review |
| `/speakers` | `/projects/{slug}/speakers` | Speaker review |
| `/chapters-audio` | `/projects/{slug}/chapters-audio` | Audio review |
| `/proper-names` | `/projects/{slug}/proper-names` | Pronunciation |
| `/api/upload-manuscript` | `/projects/{slug}/api/upload-manuscript` | Upload |
| `/api/tts/health` | `/api/tts/health` | Global (unchanged) |
| `/api/models/status` | `/api/models/status` | Global (unchanged) |
### Project Creation Flow (Web)
1. User clicks "New Project" → `/projects/new`
2. Form: title, author, file upload, optional slug (auto-generated from title)
3. POST `/projects` → creates project directory, saves manuscript, returns project slug
4. Redirect to `/projects/{slug}/` (dashboard)
5. Background parsing starts automatically
### API Endpoints
```python
# Project CRUD
GET /api/projects # List all projects
POST /api/projects # Create new project
GET /api/projects/{slug} # Get project info
PUT /api/projects/{slug} # Update project metadata
DELETE /api/projects/{slug} # Delete project (with ?confirm=true)
POST /api/projects/{slug}/archive # Archive project
POST /api/projects/{slug}/restore # Restore project
POST /api/projects/{slug}/set-default # Set as default
# Project-scoped existing endpoints (all prefixed with /projects/{slug})
GET /projects/{slug}/api/voices
POST /projects/{slug}/api/voices/regenerate
GET /projects/{slug}/api/chapters
# ... etc
```
## Database Changes
### Project Registry Table (Global)
```sql
-- In a new global SQLite DB: .umbra/projects.db
CREATE TABLE IF NOT EXISTS projects (
slug TEXT PRIMARY KEY,
title TEXT NOT NULL,
author TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
status TEXT NOT NULL DEFAULT 'active', -- active, archived, error
source_format TEXT,
source_path TEXT,
chapters INTEGER DEFAULT 0,
speakers INTEGER DEFAULT 0,
pipeline_step TEXT DEFAULT 'initialized',
config_json TEXT, -- project-local config overrides
manifest_json TEXT -- extended metadata
);
CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);
CREATE INDEX IF NOT EXISTS idx_projects_updated ON projects(updated_at);
```
### Per-Project Database (Unchanged)
Each project's `project.db` retains the exact same schema as today:
- `project` table (single row per project)
- `chapters`, `characters`, `utterances`, `generated_audio`, etc.
- No cross-project foreign keys
### Migration from Legacy Single-Project
```python
# On first run if .umbra/ exists but projects/ doesn't:
1. Create projects/ directory
2. Generate slug from project name in .umbra/project.db
3. Move .umbra/ → projects/{slug}/.umbra/
4. Create projects/{slug}/config.yaml (empty/inherit all)
5. Create projects/{slug}/manifest.json
6. Create projects/index.json with the migrated project as default
7. Update .env UMBRA_DATA_DIR to point to projects/{slug}/.umbra (or use runtime resolution)
```
## Config Resolution
```python
def load_config(project_slug: str | None = None) -> AppConfig:
# 1. Load global config.yaml
global_cfg = load_yaml("config.yaml")
# 2. Resolve project slug
if project_slug is None:
project_slug = get_default_project_slug() # from index.json
# 3. Load project config
project_cfg = load_yaml(f"projects/{project_slug}/config.yaml") if exists else {}
# 4. Merge: project overrides global
merged = deep_merge(global_cfg, project_cfg)
merged["project_slug"] = project_slug
merged["data_dir"] = Path(f"projects/{project_slug}/.umbra")
merged["export_dir"] = Path(f"projects/{project_slug}/export")
return AppConfig(**merged)
```
## Pipeline Execution
### Per-Project Pipeline State
Each project tracks its own pipeline status in `projects/{slug}/.umbra/pipeline_status.json`:
```json
{
"step": "generation",
"status": "running",
"detail": "Chapter 12 of 35",
"progress": 34,
"started_at": "2026-08-06T10:00:00",
"updated_at": "2026-08-06T10:15:00"
}
```
### Concurrent Execution
- Multiple projects can run pipeline steps **simultaneously** if resources allow
- Qwen TTS server is shared; requests from different projects queued
- LM Studio is shared; requests from different projects queued
- Web UI shows status for active project only; other projects' status visible in project list
## Voice Registry Integration
The global voice registry (`voice_registry.db`) tracks voices **across all projects**:
```sql
-- Extended schema
ALTER TABLE voice_registry ADD COLUMN source_projects TEXT; -- JSON array of project slugs
ALTER TABLE voice_registry ADD COLUMN last_used_project TEXT;
ALTER TABLE voice_registry ADD COLUMN last_used_at TEXT;
```
- A voice created in Project A can be reused in Project B
- Web UI "Voice Registry" page shows all voices with project badges
- When generating voice reference, if voice exists in registry and matches description, reuse it
## Testing Strategy
### Unit Tests
```python
# tests/test_multi_project.py
def test_create_project_creates_directory_structure()
def test_project_index_persists_and_loads()
def test_config_merge_precedence()
def test_load_config_resolves_data_dir_per_project()
def test_migrate_legacy_single_project()
def test_project_slug_uniqueness()
def test_archive_restore_cycle()
def test_delete_project_removes_all_data()
```
### Integration Tests
```python
# tests/test_integration_multi_project.py
def test_two_projects_independent_pipeline()
def test_shared_voice_registry_reuse()
def test_web_ui_project_switching()
def test_concurrent_generation_different_projects()
```
## Migration Checklist
- [ ] Create `projects/` directory structure
- [ ] Add `ProjectManager` class (`umbraaudio/project_manager.py`)
- [ ] Update `load_config()` to accept project_slug
- [ ] Add `project` command group to CLI
- [ ] Update all CLI commands to accept `--project` flag
- [ ] Update web routes to be project-scoped
- [ ] Add project selector to web UI navbar
- [ ] Create `/projects/new` page
- [ ] Create `/api/projects` endpoints
- [ ] Add global `projects.db` for registry
- [ ] Implement legacy migration on startup
- [ ] Update `PipelineManager` to use project-scoped data_dir
- [ ] Update `GenerationManager` to use project-scoped data_dir
- [ ] Update all scripts to accept `--project` argument
- [ ] Add tests for multi-project isolation
- [ ] Update documentation
## Backward Compatibility
| Scenario | Behavior |
|----------|----------|
| Legacy `.umbra/` exists, no `projects/` | Auto-migrate on first run |
| `UMBRA_DATA_DIR` env var set | Used as-is (legacy mode) |
| `--project` not specified | Uses default from index.json |
| Scripts without `--project` | Use default project |
| Web UI accessed at `/` | Redirects to `/projects/{default}/` |
## Future Considerations
1. **Project Templates**: Pre-defined voice configs, quality settings per genre
2. **Project Cloning**: Duplicate project structure for series books
3. **Shared Character Library**: Export/import characters across projects
4. **Multi-Book Series**: Parent project with child projects (volumes)
5. **Remote Project Sync**: Push/pull project data to cloud storage
6. **Project Permissions**: Read-only, collaborator, admin roles for web UI