Files
UmbraAudio/umbraaudio/analysis/__init__.py
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

177 lines
6.2 KiB
Python

from __future__ import annotations
from dataclasses import dataclass, field
from typing import Literal
PitchRegister = Literal[
"very_low", "low", "mid_low", "mid", "mid_high", "high", "very_high", "ultra_high"
]
PaceBaseline = Literal["very_slow", "slow", "moderate", "brisk", "fast", "rapid"]
Energy = Literal[
"whispered", "subdued", "soft_spoken", "measured", "assertive",
"commanding", "volatile", "explosive"
]
Timbre = Literal[
"warm", "breathy", "nasal", "gravelly", "clear", "resonant", "thin",
"metallic", "velvety"
]
Texture = Literal["smooth", "raspy", "clipped", "melodic", "monotone", "staccato", "lilting"]
Formality = Literal["casual", "conversational", "formal", "archaic", "military", "academic"]
VALID_PITCH_REGISTERS = set(PitchRegister.__args__) # type: ignore[attr-defined]
VALID_PACE_BASELINES = set(PaceBaseline.__args__) # type: ignore[attr-defined]
VALID_ENERGIES = set(Energy.__args__) # type: ignore[attr-defined]
VALID_TIMBRES = set(Timbre.__args__) # type: ignore[attr-defined]
VALID_TEXTURES = set(Texture.__args__) # type: ignore[attr-defined]
VALID_FORMALITIES = set(Formality.__args__) # type: ignore[attr-defined]
TAXONOMY_ENUMS = {
"pitch_register": VALID_PITCH_REGISTERS,
"pace_baseline": VALID_PACE_BASELINES,
"energy": VALID_ENERGIES,
"timbre": VALID_TIMBRES,
"texture": VALID_TEXTURES,
"formality": VALID_FORMALITIES,
}
TAXONOMY_DEFAULTS = {
"pitch_register": "mid",
"pace_baseline": "moderate",
"energy": "measured",
"timbre": "clear",
"texture": "smooth",
"formality": "conversational",
}
HASH_VARY_PITCH: dict[str, list[str]] = {
"Male": ["low", "mid_low", "mid", "mid_high"],
"Female": ["mid_high", "high", "mid", "mid_low"],
"Unknown": ["mid_low", "mid", "mid_high", "low"],
}
HASH_VARY_ENERGY: list[str] = [
"subdued", "soft_spoken", "measured", "assertive", "commanding", "volatile",
"whispered", "explosive",
]
HASH_VARY_TIMBRE: dict[str, list[str]] = {
"Male": ["clear", "resonant", "gravelly", "warm", "metallic"],
"Female": ["warm", "clear", "breathy", "velvety", "resonant"],
"Unknown": ["clear", "resonant", "warm", "thin", "breathy"],
}
HASH_VARY_TEXTURE: list[str] = [
"smooth", "raspy", "clipped", "melodic", "monotone", "staccato", "lilting",
]
HASH_VARY_FORMALITY: list[str] = [
"casual", "conversational", "formal", "archaic", "military", "academic",
]
@dataclass
class CharacterProfile:
name: str
aliases: list[str] = field(default_factory=list)
gender: str | None = None
age_range: str | None = None
is_narrator: bool = False
personality: str | None = None
voice_description: str | None = None
emotion_tendencies: list[str] = field(default_factory=list)
speech_patterns: str | None = None
narrator_style: str | None = None
pitch_register: PitchRegister = "mid"
pace_baseline: PaceBaseline = "moderate"
energy: Energy = "measured"
timbre: Timbre = "clear"
texture: Texture = "smooth"
formality: Formality = "conversational"
emotional_baseline: str = "neutral_calm"
distinguishing_trait: str | None = None
accent_marker: str = "none"
accent_strength: str = "medium"
def to_dict(self) -> dict:
return {
"name": self.name,
"aliases": self.aliases,
"gender": self.gender,
"age_range": self.age_range,
"is_narrator": self.is_narrator,
"personality": self.personality,
"voice_description": self.voice_description,
"emotion_tendencies": self.emotion_tendencies,
"speech_patterns": self.speech_patterns,
"narrator_style": self.narrator_style,
"pitch_register": self.pitch_register,
"pace_baseline": self.pace_baseline,
"energy": self.energy,
"timbre": self.timbre,
"texture": self.texture,
"formality": self.formality,
"emotional_baseline": self.emotional_baseline,
"distinguishing_trait": self.distinguishing_trait,
"accent_marker": self.accent_marker,
"accent_strength": self.accent_strength,
}
def taxonomy_tuple(self) -> tuple[str, str, str, str, str, str]:
return (
self.pitch_register,
self.pace_baseline,
self.energy,
self.timbre,
self.texture,
self.formality,
)
@classmethod
def from_dict(cls, data: dict) -> CharacterProfile:
validated = {}
for field_name, valid_set in TAXONOMY_ENUMS.items():
raw = data.get(field_name, TAXONOMY_DEFAULTS[field_name])
if raw in valid_set:
validated[field_name] = raw
else:
validated[field_name] = TAXONOMY_DEFAULTS[field_name]
return cls(
name=data.get("name", ""),
aliases=data.get("aliases", []),
gender=data.get("gender"),
age_range=data.get("age_range"),
is_narrator=data.get("is_narrator", False),
personality=data.get("personality"),
voice_description=data.get("voice_description"),
emotion_tendencies=data.get("emotion_tendencies", []),
speech_patterns=data.get("speech_patterns"),
narrator_style=data.get("narrator_style"),
**validated,
emotional_baseline=data.get("emotional_baseline", "neutral_calm"),
distinguishing_trait=data.get("distinguishing_trait"),
accent_marker=data.get("accent_marker", "none"),
accent_strength=data.get("accent_strength", "medium"),
)
NARRATOR_DEFAULT_PROFILE = CharacterProfile(
name="narrator",
is_narrator=True,
pitch_register="mid",
pace_baseline="moderate",
energy="measured",
timbre="clear",
texture="smooth",
formality="conversational",
emotional_baseline="neutral_calm",
distinguishing_trait="even, unhurried, minimal emphasis",
accent_marker="none",
voice_description=(
"A calm, clear, even-toned narrator. Minimal emotional coloring, "
"no shouting, no dramatic swings in pace or pitch. Reads "
"descriptive prose steadily regardless of how dramatic the "
"events being described are."
),
)