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.
147 lines
5.0 KiB
Python
147 lines
5.0 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from umbraaudio.analysis import CharacterProfile
|
|
from umbraaudio.voices.registry import VoiceRegistry
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connect_creates_db(registry_db_path: Path):
|
|
registry = VoiceRegistry(registry_db_path)
|
|
await registry.connect()
|
|
assert registry_db_path.exists()
|
|
await registry.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_find_or_create_new(registry_db_path: Path, character_profile: CharacterProfile):
|
|
registry = VoiceRegistry(registry_db_path)
|
|
await registry.connect()
|
|
reg_id, created = await registry.find_or_create(character_profile, source_project="test")
|
|
assert created is True
|
|
assert reg_id > 0
|
|
await registry.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_find_or_create_duplicate(
|
|
registry_db_path: Path, character_profile: CharacterProfile
|
|
):
|
|
registry = VoiceRegistry(registry_db_path)
|
|
await registry.connect()
|
|
reg_id1, created1 = await registry.find_or_create(character_profile, source_project="test")
|
|
reg_id2, created2 = await registry.find_or_create(character_profile, source_project="test2")
|
|
assert created1 is True
|
|
assert created2 is False
|
|
assert reg_id1 == reg_id2
|
|
await registry.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_find_by_name(registry_db_path: Path, character_profile: CharacterProfile):
|
|
registry = VoiceRegistry(registry_db_path)
|
|
await registry.connect()
|
|
reg_id, _ = await registry.find_or_create(character_profile)
|
|
found = await registry.find_by_name("John Smith")
|
|
assert found is not None
|
|
assert found["id"] == reg_id
|
|
await registry.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_find_by_alias(registry_db_path: Path, character_profile: CharacterProfile):
|
|
registry = VoiceRegistry(registry_db_path)
|
|
await registry.connect()
|
|
await registry.find_or_create(character_profile)
|
|
found = await registry.find_by_name("John")
|
|
assert found is not None
|
|
assert found["character_name"] == "John Smith"
|
|
await registry.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_find_by_unknown_name(registry_db_path: Path):
|
|
registry = VoiceRegistry(registry_db_path)
|
|
await registry.connect()
|
|
found = await registry.find_by_name("Nobody")
|
|
assert found is None
|
|
await registry.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_voice_ref(registry_db_path: Path, character_profile: CharacterProfile):
|
|
registry = VoiceRegistry(registry_db_path)
|
|
await registry.connect()
|
|
reg_id, _ = await registry.find_or_create(character_profile)
|
|
await registry.update_voice_ref(reg_id, "/path/to/ref.wav")
|
|
found = await registry.find_by_name("John Smith")
|
|
assert found is not None
|
|
assert found["voice_ref_path"] == "/path/to/ref.wav"
|
|
assert found["status"] == "voiced"
|
|
await registry.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_profile(registry_db_path: Path, character_profile: CharacterProfile):
|
|
registry = VoiceRegistry(registry_db_path)
|
|
await registry.connect()
|
|
reg_id, _ = await registry.find_or_create(character_profile)
|
|
updated = CharacterProfile(
|
|
name="John Smith",
|
|
aliases=["John", "Johnny"],
|
|
gender="male",
|
|
age_range="adult",
|
|
energy="high",
|
|
timbre="bright",
|
|
)
|
|
await registry.update_profile(reg_id, updated)
|
|
found = await registry.find_by_name("John Smith")
|
|
assert found is not None
|
|
assert found["energy"] == "high"
|
|
assert found["timbre"] == "bright"
|
|
# new alias should be findable
|
|
found_by_new = await registry.find_by_name("Johnny")
|
|
assert found_by_new is not None
|
|
await registry.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_all_and_voiced(
|
|
registry_db_path: Path, character_profile: CharacterProfile, narrator_profile: CharacterProfile
|
|
):
|
|
registry = VoiceRegistry(registry_db_path)
|
|
await registry.connect()
|
|
await registry.find_or_create(character_profile)
|
|
await registry.find_or_create(narrator_profile)
|
|
all_chars = await registry.get_all()
|
|
assert len(all_chars) >= 2
|
|
voiced = await registry.get_all_voiced()
|
|
assert len(voiced) == 0 # none voiced yet
|
|
await registry.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search(registry_db_path: Path, character_profile: CharacterProfile):
|
|
registry = VoiceRegistry(registry_db_path)
|
|
await registry.connect()
|
|
await registry.find_or_create(character_profile)
|
|
results = await registry.search("Smith")
|
|
assert len(results) >= 1
|
|
assert results[0]["character_name"] == "John Smith"
|
|
results_none = await registry.search("xyzzy")
|
|
assert len(results_none) == 0
|
|
await registry.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_link_to_project(registry_db_path: Path, character_profile: CharacterProfile):
|
|
registry = VoiceRegistry(registry_db_path)
|
|
await registry.connect()
|
|
reg_id, _ = await registry.find_or_create(character_profile)
|
|
await registry.link_to_project(reg_id, 1, role_in_book="Protagonist")
|
|
await registry.link_to_project(reg_id, 2)
|
|
await registry.close()
|