mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-22 07:10:28 +02:00
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
This commit is contained in:
+3
-4
@@ -2235,11 +2235,10 @@ class abogen(QWidget):
|
|||||||
self.current_queue_index = 0 # Reset for next time
|
self.current_queue_index = 0 # Reset for next time
|
||||||
|
|
||||||
def get_voice_formula(self) -> str:
|
def get_voice_formula(self) -> str:
|
||||||
|
from abogen.voice_formulas import pairs_to_formula
|
||||||
|
|
||||||
if self.mixed_voice_state:
|
if self.mixed_voice_state:
|
||||||
formula_components = [
|
return pairs_to_formula(self.mixed_voice_state) or ""
|
||||||
f"{name}*{weight}" for name, weight in self.mixed_voice_state
|
|
||||||
]
|
|
||||||
return " + ".join(filter(None, formula_components))
|
|
||||||
else:
|
else:
|
||||||
return self.selected_voice
|
return self.selected_voice
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import re
|
import re
|
||||||
from typing import List, Tuple
|
from typing import Iterable, List, Optional, Tuple
|
||||||
|
|
||||||
from abogen.tts_plugin.utils import get_voices
|
from abogen.tts_plugin.utils import get_voices
|
||||||
|
|
||||||
@@ -72,6 +72,33 @@ def parse_voice_formula(pipeline, formula):
|
|||||||
return weighted_sum
|
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):
|
def calculate_sum_from_formula(formula):
|
||||||
weights = re.findall(r"\* *([\d.]+)", formula)
|
weights = re.findall(r"\* *([\d.]+)", formula)
|
||||||
total_sum = sum(float(weight) for weight in weights)
|
total_sum = sum(float(weight) for weight in weights)
|
||||||
|
|||||||
@@ -548,19 +548,12 @@ def prepare_speaker_metadata(
|
|||||||
|
|
||||||
|
|
||||||
def formula_from_profile(entry: Dict[str, Any]) -> Optional[str]:
|
def formula_from_profile(entry: Dict[str, Any]) -> Optional[str]:
|
||||||
|
from abogen.voice_formulas import pairs_to_formula
|
||||||
|
|
||||||
voices = entry.get("voices") or []
|
voices = entry.get("voices") or []
|
||||||
if not voices:
|
if not voices:
|
||||||
return None
|
return None
|
||||||
total = sum(weight for _, weight in voices)
|
return pairs_to_formula(voices)
|
||||||
if total <= 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _format_weight(value: float) -> str:
|
|
||||||
normalized = value / total if total else 0.0
|
|
||||||
return (f"{normalized:.4f}").rstrip("0").rstrip(".") or "0"
|
|
||||||
|
|
||||||
parts = [f"{name}*{_format_weight(weight)}" for name, weight in voices if weight > 0]
|
|
||||||
return "+".join(parts) if parts else None
|
|
||||||
|
|
||||||
|
|
||||||
def template_options() -> Dict[str, Any]:
|
def template_options() -> Dict[str, Any]:
|
||||||
@@ -710,19 +703,8 @@ def sanitize_voice_entries(entries: Iterable[Any]) -> List[Dict[str, Any]]:
|
|||||||
|
|
||||||
|
|
||||||
def pairs_to_formula(pairs: Iterable[Tuple[str, float]]) -> Optional[str]:
|
def pairs_to_formula(pairs: Iterable[Tuple[str, float]]) -> Optional[str]:
|
||||||
voices = [(voice, float(weight)) for voice, weight in pairs if float(weight) > 0]
|
from abogen.voice_formulas import pairs_to_formula as _pairs_to_formula
|
||||||
if not voices:
|
return _pairs_to_formula(pairs)
|
||||||
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 profiles_payload() -> Dict[str, Any]:
|
def profiles_payload() -> Dict[str, Any]:
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""Tests for voice_formulas.py — pairs_to_formula."""
|
||||||
|
|
||||||
|
from abogen.voice_formulas import pairs_to_formula
|
||||||
|
|
||||||
|
|
||||||
|
class TestPairsToFormula:
|
||||||
|
def test_basic_pair(self):
|
||||||
|
result = pairs_to_formula([("A", 1.0), ("B", 1.0)])
|
||||||
|
assert result == "A*0.5+B*0.5"
|
||||||
|
|
||||||
|
def test_unequal_weights(self):
|
||||||
|
result = pairs_to_formula([("A", 3.0), ("B", 1.0)])
|
||||||
|
assert result == "A*0.75+B*0.25"
|
||||||
|
|
||||||
|
def test_single_voice(self):
|
||||||
|
result = pairs_to_formula([("A", 1.0)])
|
||||||
|
assert result == "A*1"
|
||||||
|
|
||||||
|
def test_filters_zero_weight(self):
|
||||||
|
result = pairs_to_formula([("A", 1.0), ("B", 0.0)])
|
||||||
|
assert result == "A*1"
|
||||||
|
|
||||||
|
def test_all_zero_returns_none(self):
|
||||||
|
result = pairs_to_formula([("A", 0.0), ("B", 0.0)])
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_empty_returns_none(self):
|
||||||
|
result = pairs_to_formula([])
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_none_values_filtered(self):
|
||||||
|
result = pairs_to_formula([("A", 1.0), ("B", None)])
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
|
def test_weight_normalization(self):
|
||||||
|
result = pairs_to_formula([("A", 2.0), ("B", 2.0)])
|
||||||
|
assert result == "A*0.5+B*0.5"
|
||||||
|
|
||||||
|
def test_three_voices(self):
|
||||||
|
result = pairs_to_formula([("A", 1.0), ("B", 1.0), ("C", 1.0)])
|
||||||
|
assert "A*" in result
|
||||||
|
assert "B*" in result
|
||||||
|
assert "C*" in result
|
||||||
|
assert "+" in result
|
||||||
Reference in New Issue
Block a user