- Added a new 'Table View' button to the library sort bar. - Implemented a CSS grid layout to display voice properties in a high-density table. - Added sortable column headers (Name, Lang, Gender, Speed, dBFS, Length, Rating, Source, Seed, Note, Tags, Active). - Aligned CSS grid to account for the bulk edit checkbox injection. - Added drag-and-drop profile image support to the Inspector's large avatar. - Ensured picture updates instantly synchronize across the List, Table, and Inspector views.
298 lines
12 KiB
Python
298 lines
12 KiB
Python
"""Settings management: load, save, normalize, TTS stability helpers."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import requests
|
|
|
|
from core.constants import (
|
|
CONFIG_DIR, CONFIG_FILE,
|
|
_VOICES_DIR_DEFAULT, _OUTPUT_DIR_DEFAULT,
|
|
_WHISPER_DEFAULT, _TTS_DEFAULT, _TTS_STREAM_DEFAULT,
|
|
_CUSTOMVOICE_DEFAULT, _VOICE_DESIGN_DEFAULT,
|
|
_NVIDIA_ROUTER_DEFAULT, _NVIDIA_TTS_DEFAULT, _NVIDIA_ASR_DEFAULT,
|
|
_NVIDIA_CLONE_DEFAULT, _NVIDIA_ZEROSHOT_DEFAULT, _NVIDIA_FLOW_DEFAULT,
|
|
_FASTER_WHISPER_DEFAULT, _WHISPER_CPP_DEFAULT,
|
|
_KOKORO_DEFAULT, _VIBEVOICE_DEFAULT, _XTTS_DEFAULT, _FISHSPEECH_DEFAULT,
|
|
_TTS_STREAM_DEFAULT,
|
|
)
|
|
|
|
# ── Settings key whitelist ────────────────────────────────────────────────────
|
|
|
|
_SETTINGS_KEYS = {
|
|
"whisper_url", "tts_url", "tts_stream_url", "tts_stream_mode", "tts_backend",
|
|
"output_dir", "voices_scan_dir", "voice_design_url", "customvoice_url",
|
|
"nvidia_router_url", "nvidia_tts_url", "nvidia_asr_url", "nvidia_clone_url",
|
|
"nvidia_zeroshot_url", "nvidia_flow_url",
|
|
"faster_whisper_url", "whisper_cpp_url", "groq_api_key", "kokoro_url", "vibevoice_url", "xtts_url",
|
|
"whisper_api_key", "tts_api_key", "voice_design_api_key", "elevenlabs_api_key",
|
|
"tts_stability_enabled", "tts_extra_params", "tts_extra_params_by_backend",
|
|
# Captures settings
|
|
"stt_language", "stt_preferred_backend",
|
|
"auto_refine", "refine_model",
|
|
"refine_fillers", "refine_repetitions", "refine_corrections", "refine_punctuation",
|
|
"captures_default_voice",
|
|
"client_voice_bindings",
|
|
"llm_url", "llm_model",
|
|
# Browser-persistent UI state
|
|
"engine_local_urls", "engine_container_names", "custom_engine_cards",
|
|
"refine_llm_url", "conv_llm_url", "seed_finder_text",
|
|
}
|
|
|
|
# ── TTS stability defaults ────────────────────────────────────────────────────
|
|
|
|
_TTS_STABILITY_DEFAULT = {"temperature": 0.1, "top_p": 0.8, "seed": 0}
|
|
_TTS_STABILITY_BY_BACKEND_DEFAULT = {
|
|
"voice_clone": dict(_TTS_STABILITY_DEFAULT),
|
|
"streaming": dict(_TTS_STABILITY_DEFAULT),
|
|
"customvoice": dict(_TTS_STABILITY_DEFAULT),
|
|
"voice_design": dict(_TTS_STABILITY_DEFAULT),
|
|
"nvidia_magpie": {},
|
|
"nvidia_zeroshot": {},
|
|
"nvidia_flow": {},
|
|
"kokoro": {},
|
|
"vibevoice": {},
|
|
"xtts": {},
|
|
"fishspeech": {},
|
|
}
|
|
_TTS_PAYLOAD_CORE_KEYS = {"model", "input", "voice", "response_format", "instruct", "language"}
|
|
|
|
|
|
def _settings_bool(value, default: bool = True) -> bool:
|
|
if value is None:
|
|
return default
|
|
if isinstance(value, bool):
|
|
return value
|
|
return str(value).strip().lower() not in {"0", "false", "off", "no"}
|
|
|
|
|
|
def _clean_tts_param_dict(raw, fallback: dict | None = None) -> dict:
|
|
if raw in (None, ""):
|
|
raw = fallback or {}
|
|
if isinstance(raw, str):
|
|
try:
|
|
raw = json.loads(raw)
|
|
except Exception:
|
|
raw = fallback or {}
|
|
if not isinstance(raw, dict):
|
|
return {}
|
|
params = {}
|
|
for key, value in raw.items():
|
|
if key in _TTS_PAYLOAD_CORE_KEYS or str(key).startswith("_"):
|
|
continue
|
|
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
params[str(key)] = value
|
|
return params
|
|
|
|
|
|
def _clean_preview_backend(value: str) -> str:
|
|
key = re.sub(r"[^a-z0-9]+", "_", str(value or "voice_clone").lower()).strip("_")
|
|
aliases = {
|
|
"clone": "voice_clone",
|
|
"base": "voice_clone",
|
|
"voiceclone": "voice_clone",
|
|
"voice_clone_base": "voice_clone",
|
|
"stream": "streaming",
|
|
"tts_streaming": "streaming",
|
|
"custom": "customvoice",
|
|
"custom_voice": "customvoice",
|
|
"voice_design": "voice_design",
|
|
"voicedesign": "voice_design",
|
|
"design": "voice_design",
|
|
"nvidia": "nvidia_magpie",
|
|
"magpie": "nvidia_magpie",
|
|
"nvidia_tts": "nvidia_magpie",
|
|
"nvidia_magpie_tts": "nvidia_magpie",
|
|
"nvidia_clone": "nvidia_zeroshot",
|
|
"nvidia_zeroshot_tts": "nvidia_zeroshot",
|
|
"magpie_zeroshot": "nvidia_zeroshot",
|
|
"zeroshot": "nvidia_zeroshot",
|
|
"zero_shot": "nvidia_zeroshot",
|
|
"nvidia_flow_tts": "nvidia_flow",
|
|
"magpie_flow": "nvidia_flow",
|
|
"flow": "nvidia_flow",
|
|
"kokoro_fastapi": "kokoro",
|
|
"kokoro_tts": "kokoro",
|
|
"kokoro_local": "kokoro",
|
|
"vibevoice_service": "vibevoice",
|
|
"vibe_voice": "vibevoice",
|
|
"vibetts": "vibevoice",
|
|
"xtts_v2": "xtts",
|
|
"xtts2": "xtts",
|
|
"coqui_xtts": "xtts",
|
|
"fish": "fishspeech",
|
|
"fish_speech": "fishspeech",
|
|
"fishaudio": "fishspeech",
|
|
"openaudio": "fishspeech",
|
|
}
|
|
key = aliases.get(key, key)
|
|
return key if key in {"voice_clone", "streaming", "customvoice", "voice_design", "nvidia_magpie", "nvidia_zeroshot", "nvidia_flow", "kokoro", "vibevoice", "xtts", "fishspeech"} else "voice_clone"
|
|
|
|
|
|
def _tts_extra_params(settings: dict, backend: str = "voice_clone") -> dict:
|
|
if not _settings_bool(settings.get("tts_stability_enabled"), True):
|
|
return {}
|
|
backend = _clean_preview_backend(backend)
|
|
by_backend = settings.get("tts_extra_params_by_backend")
|
|
if isinstance(by_backend, str):
|
|
try:
|
|
by_backend = json.loads(by_backend)
|
|
except Exception:
|
|
by_backend = None
|
|
if isinstance(by_backend, dict):
|
|
fallback = _TTS_STABILITY_BY_BACKEND_DEFAULT.get(backend, _TTS_STABILITY_DEFAULT)
|
|
if backend in by_backend:
|
|
return _clean_tts_param_dict(by_backend.get(backend), fallback)
|
|
return _clean_tts_param_dict(fallback, fallback)
|
|
return _clean_tts_param_dict(settings.get("tts_extra_params", _TTS_STABILITY_BY_BACKEND_DEFAULT.get(backend, _TTS_STABILITY_DEFAULT)), _TTS_STABILITY_BY_BACKEND_DEFAULT.get(backend, _TTS_STABILITY_DEFAULT))
|
|
|
|
|
|
def _apply_tts_extra_params(payload: dict, settings: dict, backend: str = "voice_clone") -> dict:
|
|
params = _tts_extra_params(settings, backend)
|
|
if params:
|
|
payload.update(params)
|
|
return payload
|
|
|
|
|
|
def _strip_tts_extra_params(payload: dict) -> dict:
|
|
return {k: v for k, v in payload.items() if k in _TTS_PAYLOAD_CORE_KEYS}
|
|
|
|
|
|
def _post_tts_with_fallback(endpoint: str, payload: dict, headers: dict, **kwargs) -> requests.Response:
|
|
resp = requests.post(endpoint, json=payload, headers=headers, **kwargs)
|
|
fallback = _strip_tts_extra_params(payload)
|
|
if resp.status_code in {400, 404, 415, 422} and fallback != payload:
|
|
try:
|
|
resp.close()
|
|
except Exception:
|
|
pass
|
|
import logging
|
|
logging.getLogger("uvicorn.error").warning("TTS backend rejected extra generation params; retrying without them")
|
|
return requests.post(endpoint, json=fallback, headers=headers, **kwargs)
|
|
return resp
|
|
|
|
|
|
def _preview_backend_base_url(settings: dict, backend: str) -> str:
|
|
backend = _clean_preview_backend(backend)
|
|
if backend == "streaming":
|
|
return settings.get("tts_stream_url") or settings.get("tts_url") or _TTS_STREAM_DEFAULT
|
|
if backend == "customvoice":
|
|
return settings.get("customvoice_url") or _CUSTOMVOICE_DEFAULT
|
|
if backend == "voice_design":
|
|
return settings.get("voice_design_url") or _VOICE_DESIGN_DEFAULT
|
|
if backend == "nvidia_magpie":
|
|
return settings.get("nvidia_tts_url") or settings.get("nvidia_router_url") or _NVIDIA_TTS_DEFAULT
|
|
if backend == "nvidia_zeroshot":
|
|
return settings.get("nvidia_zeroshot_url") or settings.get("nvidia_clone_url") or settings.get("nvidia_router_url") or _NVIDIA_ZEROSHOT_DEFAULT
|
|
if backend == "nvidia_flow":
|
|
return settings.get("nvidia_flow_url") or settings.get("nvidia_clone_url") or settings.get("nvidia_router_url") or _NVIDIA_FLOW_DEFAULT
|
|
if backend == "kokoro":
|
|
return settings.get("kokoro_url") or _KOKORO_DEFAULT
|
|
if backend == "vibevoice":
|
|
return settings.get("vibevoice_url") or _VIBEVOICE_DEFAULT
|
|
if backend == "xtts":
|
|
return settings.get("xtts_url") or _XTTS_DEFAULT
|
|
if backend == "fishspeech":
|
|
return settings.get("fishspeech_url") or _FISHSPEECH_DEFAULT
|
|
return settings.get("tts_url") or _TTS_DEFAULT
|
|
|
|
|
|
def _normalize_settings(s: dict) -> dict:
|
|
scan_dir = Path(s.get("voices_scan_dir", _VOICES_DIR_DEFAULT))
|
|
output_dir = Path(s.get("output_dir", _OUTPUT_DIR_DEFAULT))
|
|
legacy_output_dirs = {
|
|
Path("/voices/speakers"),
|
|
scan_dir / "speakers",
|
|
}
|
|
if output_dir in legacy_output_dirs:
|
|
s["output_dir"] = str(scan_dir / "active_voices")
|
|
if s.get("tts_stream_mode") not in {"auto", "streaming", "buffered"}:
|
|
s["tts_stream_mode"] = "auto"
|
|
if not isinstance(s.get("engine_local_urls"), dict):
|
|
s["engine_local_urls"] = {}
|
|
if not isinstance(s.get("engine_container_names"), dict):
|
|
s["engine_container_names"] = {}
|
|
if not isinstance(s.get("custom_engine_cards"), list):
|
|
s["custom_engine_cards"] = []
|
|
return s
|
|
|
|
|
|
_settings_cache: dict | None = None
|
|
_settings_cache_mtime: float = -1.0
|
|
|
|
|
|
def _load_settings() -> dict:
|
|
global _settings_cache, _settings_cache_mtime
|
|
mtime = CONFIG_FILE.stat().st_mtime if CONFIG_FILE.exists() else 0.0
|
|
if _settings_cache is not None and mtime == _settings_cache_mtime:
|
|
return dict(_settings_cache)
|
|
defaults = {
|
|
"whisper_url": _WHISPER_DEFAULT,
|
|
"tts_url": _TTS_DEFAULT,
|
|
"tts_stream_url": _TTS_STREAM_DEFAULT,
|
|
"tts_stream_mode": "auto",
|
|
"tts_backend": "openai",
|
|
"output_dir": _OUTPUT_DIR_DEFAULT,
|
|
"voices_scan_dir": _VOICES_DIR_DEFAULT,
|
|
"voice_design_url": _VOICE_DESIGN_DEFAULT,
|
|
"customvoice_url": _CUSTOMVOICE_DEFAULT,
|
|
"nvidia_router_url": _NVIDIA_ROUTER_DEFAULT,
|
|
"nvidia_tts_url": _NVIDIA_TTS_DEFAULT,
|
|
"nvidia_asr_url": _NVIDIA_ASR_DEFAULT,
|
|
"nvidia_clone_url": _NVIDIA_CLONE_DEFAULT,
|
|
"nvidia_zeroshot_url": _NVIDIA_ZEROSHOT_DEFAULT,
|
|
"nvidia_flow_url": _NVIDIA_FLOW_DEFAULT,
|
|
"faster_whisper_url": _FASTER_WHISPER_DEFAULT,
|
|
"whisper_cpp_url": _WHISPER_CPP_DEFAULT,
|
|
"groq_api_key": "",
|
|
"kokoro_url": _KOKORO_DEFAULT,
|
|
"vibevoice_url": _VIBEVOICE_DEFAULT,
|
|
"xtts_url": _XTTS_DEFAULT,
|
|
"fishspeech_url": _FISHSPEECH_DEFAULT,
|
|
"whisper_api_key": "",
|
|
"tts_api_key": "",
|
|
"voice_design_api_key": "",
|
|
"tts_stability_enabled": True,
|
|
"tts_extra_params": _TTS_STABILITY_DEFAULT,
|
|
"tts_extra_params_by_backend": _TTS_STABILITY_BY_BACKEND_DEFAULT,
|
|
# Captures
|
|
"stt_language": "",
|
|
"stt_preferred_backend": "",
|
|
"auto_refine": "off",
|
|
"refine_model": "",
|
|
"refine_fillers": True,
|
|
"refine_repetitions": True,
|
|
"refine_corrections": True,
|
|
"refine_punctuation": True,
|
|
"captures_default_voice": "",
|
|
"client_voice_bindings": {},
|
|
"llm_url": "http://localhost:11434/v1",
|
|
"llm_model": "",
|
|
"engine_local_urls": {},
|
|
"engine_container_names": {},
|
|
"custom_engine_cards": [],
|
|
"refine_llm_url": "",
|
|
"conv_llm_url": "",
|
|
"seed_finder_text": "",
|
|
}
|
|
if CONFIG_FILE.exists():
|
|
try:
|
|
saved = json.loads(CONFIG_FILE.read_text())
|
|
defaults.update({k: v for k, v in saved.items() if k in _SETTINGS_KEYS})
|
|
except Exception:
|
|
pass
|
|
result = _normalize_settings(defaults)
|
|
_settings_cache = result
|
|
_settings_cache_mtime = mtime
|
|
return dict(result)
|
|
|
|
|
|
def _save_settings(s: dict) -> None:
|
|
global _settings_cache, _settings_cache_mtime
|
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
CONFIG_FILE.write_text(json.dumps(s, indent=2))
|
|
_settings_cache = s
|
|
_settings_cache_mtime = CONFIG_FILE.stat().st_mtime
|