mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-22 07:10:28 +02:00
feat: Implement voice mixer UI and functionality
- Added new styles for the voice mixer components in styles.css. - Updated base.html to include a block for scripts. - Refactored voices.html to create a structured voice mixer interface with profile management features. - Introduced voices.js to handle voice mixer logic, including profile creation, editing, and previewing. - Implemented actions for importing and exporting voice profiles. - Enhanced user experience with loading states and status messages.
This commit is contained in:
+108
-1
@@ -1,5 +1,8 @@
|
||||
import os
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, Iterable, List, Tuple
|
||||
|
||||
from abogen.constants import VOICES_INTERNAL
|
||||
from abogen.utils import get_user_config_path
|
||||
|
||||
|
||||
@@ -57,3 +60,107 @@ def export_profiles(export_path):
|
||||
profiles = load_profiles()
|
||||
with open(export_path, "w", encoding="utf-8") as f:
|
||||
json.dump({"abogen_voice_profiles": profiles}, f, indent=2)
|
||||
|
||||
|
||||
def serialize_profiles() -> Dict[str, Dict[str, Iterable[Tuple[str, float]]]]:
|
||||
"""Return profiles in canonical dictionary form."""
|
||||
return load_profiles()
|
||||
|
||||
|
||||
def _normalize_voice_entries(entries: Iterable) -> List[Tuple[str, float]]:
|
||||
normalized: List[Tuple[str, float]] = []
|
||||
for item in entries or []:
|
||||
if isinstance(item, dict):
|
||||
voice = item.get("id") or item.get("voice")
|
||||
weight = item.get("weight")
|
||||
elif isinstance(item, (list, tuple)) and len(item) >= 2:
|
||||
voice, weight = item[0], item[1]
|
||||
else:
|
||||
continue
|
||||
if voice not in VOICES_INTERNAL:
|
||||
continue
|
||||
if weight is None:
|
||||
continue
|
||||
try:
|
||||
weight_val = float(weight)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if weight_val <= 0:
|
||||
continue
|
||||
normalized.append((voice, weight_val))
|
||||
return normalized
|
||||
|
||||
|
||||
def normalize_voice_entries(entries: Iterable) -> List[Tuple[str, float]]:
|
||||
"""Public helper to normalize voice-weight pairs from arbitrary payloads."""
|
||||
|
||||
return _normalize_voice_entries(entries)
|
||||
|
||||
|
||||
def save_profile(name: str, *, language: str, voices: Iterable) -> None:
|
||||
"""Persist a single profile after validating its data."""
|
||||
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
raise ValueError("Profile name is required")
|
||||
|
||||
normalized = _normalize_voice_entries(voices)
|
||||
if not normalized:
|
||||
raise ValueError("At least one voice with a weight above zero is required")
|
||||
|
||||
if not language:
|
||||
language = "a"
|
||||
|
||||
profiles = load_profiles()
|
||||
profiles[name] = {"language": language, "voices": normalized}
|
||||
save_profiles(profiles)
|
||||
|
||||
|
||||
def remove_profile(name: str) -> None:
|
||||
delete_profile(name)
|
||||
|
||||
|
||||
def import_profiles_data(data: Dict, *, replace_existing: bool = False) -> List[str]:
|
||||
"""Merge profiles from a dictionary structure and persist them.
|
||||
|
||||
Returns the list of profile names that were added or updated.
|
||||
"""
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("Invalid profile payload")
|
||||
|
||||
if "abogen_voice_profiles" in data:
|
||||
data = data["abogen_voice_profiles"]
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("Invalid profile payload")
|
||||
|
||||
current = load_profiles()
|
||||
updated: List[str] = []
|
||||
for name, entry in data.items():
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
voices = _normalize_voice_entries(entry.get("voices", []))
|
||||
if not voices:
|
||||
continue
|
||||
language = entry.get("language", "a")
|
||||
if name in current and not replace_existing:
|
||||
# skip duplicates unless explicit replacement is requested
|
||||
continue
|
||||
current[name] = {"language": language, "voices": voices}
|
||||
updated.append(name)
|
||||
|
||||
if updated:
|
||||
save_profiles(current)
|
||||
return updated
|
||||
|
||||
|
||||
def export_profiles_payload(names: Iterable[str] | None = None) -> Dict[str, Dict]:
|
||||
"""Return profiles limited to the provided names for download/export."""
|
||||
|
||||
profiles = load_profiles()
|
||||
if names is None:
|
||||
subset = profiles
|
||||
else:
|
||||
subset = {name: profiles[name] for name in names if name in profiles}
|
||||
return {"abogen_voice_profiles": subset}
|
||||
|
||||
Reference in New Issue
Block a user