Files
abogen/abogen/voice_formulas.py
T
Artem Akymenko a99cf58c79 refactor: consolidate voice formula building into voice_formulas.py
- voice_formulas.py: add pairs_to_formula() as canonical implementation
- webui/routes/utils/voice.py: formula_from_profile() and pairs_to_formula()
  now delegate to voice_formulas.pairs_to_formula()
- pyqt/gui.py: get_voice_formula() now uses voice_formulas.pairs_to_formula()
  instead of inline string formatting
- Eliminates 3 duplicate implementations of voice*weight formula building
- +9 tests
- 1178 tests pass
2026-07-19 10:02:14 +00:00

110 lines
3.6 KiB
Python

import re
from typing import Iterable, List, Optional, Tuple
from abogen.tts_plugin.utils import get_voices
# Calls parsing and loads the voice to gpu or cpu
def get_new_voice(pipeline, formula, use_gpu):
try:
weighted_voice = parse_voice_formula(pipeline, formula)
# device = "cuda" if use_gpu else "cpu"
# Setting the device "cuda" gives "Error occurred: split_with_sizes(): argument 'split_sizes' (position 2)"
# error when the device is gpu. So disabling this for now.
device = "cpu"
return weighted_voice.to(device)
except Exception as e:
raise ValueError(f"Failed to create voice: {str(e)}")
def parse_formula_terms(formula: str) -> List[Tuple[str, float]]:
if not formula or not formula.strip():
raise ValueError("Empty voice formula")
terms: List[Tuple[str, float]] = []
kokoro_voices = get_voices("kokoro")
for segment in formula.split("+"):
part = segment.strip()
if not part:
continue
if "*" not in part:
raise ValueError("Each component must be in the form voice*weight")
voice_name, raw_weight = part.split("*", 1)
voice_name = voice_name.strip()
if voice_name not in kokoro_voices:
raise ValueError(f"Unknown voice: {voice_name}")
try:
weight = float(raw_weight.strip())
except ValueError as exc:
raise ValueError(f"Invalid weight for {voice_name}") from exc
if weight <= 0:
raise ValueError(f"Weight for {voice_name} must be positive")
terms.append((voice_name, weight))
if not terms:
raise ValueError("Voice weights must sum to a positive value")
return terms
def parse_voice_formula(pipeline, formula):
terms = parse_formula_terms(formula)
total_weight = sum(weight for _, weight in terms)
if total_weight <= 0:
raise ValueError("Voice weights must sum to a positive value")
weighted_sum = None
for voice_name, weight in terms:
normalized_weight = weight / total_weight if total_weight > 0 else weight
voice_tensor = pipeline.load_single_voice(voice_name)
if weighted_sum is None:
weighted_sum = normalized_weight * voice_tensor
else:
weighted_sum += normalized_weight * voice_tensor
if weighted_sum is None:
raise ValueError("Voice formula produced no components")
return weighted_sum
def pairs_to_formula(pairs: Iterable[Tuple[str, float]]) -> Optional[str]:
"""Build a voice formula string from (voice_name, weight) pairs.
Normalizes weights to sum to 1.0 and formats as "voice1*0.5+voice2*0.5".
Args:
pairs: Iterable of (voice_name, weight) tuples. Zero-weight entries
are filtered out.
Returns:
Formula string, or None if no valid entries.
"""
voices = [(voice, float(weight)) for voice, weight in pairs if weight is not None and float(weight) > 0]
if not voices:
return None
total = sum(weight for _, weight in voices)
if total <= 0:
return None
def _format_value(value: float) -> str:
normalized = value / total if total else 0.0
return (f"{normalized:.4f}").rstrip("0").rstrip(".") or "0"
parts = [f"{voice}*{_format_value(weight)}" for voice, weight in voices]
return "+".join(parts)
def calculate_sum_from_formula(formula):
weights = re.findall(r"\* *([\d.]+)", formula)
total_sum = sum(float(weight) for weight in weights)
return total_sum
def extract_voice_ids(formula: str) -> List[str]:
return [voice for voice, _ in parse_formula_terms(formula)]