Security fixes: - Block /proc /sys /dev /run /boot in /api/browse-dirs (path traversal) - Verify yt-dlp output stays inside TEMP_DIR before registration - Remove Access-Control-Allow-Origin: * from /api/proxy-audio - TTL-based temp file registry (default 2h) to prevent disk fill Performance: - Cache settings + routing rules in memory (mtime-checked); eliminates per-request disk reads on every TTS call UI: - Add container name (optional) field to Docker stack TTS/STT engine cards (Qwen3 Voice Clone, Voice Design, Custom Voice, Streaming, NVIDIA Magpie, Parakeet) — enables Stop/Start/Restart buttons on all engine cards, matching the existing Other Local TTS/STT cards Refactor — backend: - server.py: 5560 lines → 43-line entry point - core/ package: constants, registry, validation, docker_client, config, routing, audio, voice, presets, tts_helpers - routes/ package: admin, settings, library, stt, sources, docker, tts, conversation (FastAPI APIRouter modules) - Dockerfile + docker-compose.yml updated to include core/ and routes/ Refactor — frontend: - static/app.js: 8744 lines → 16 modules in static/js/ utils, voice-inspector, voice-sources, integrations, routing, settings, voice-clone, voice-library, tts-preview, benchmark, stt, init, engines, ai-backends, generation, conversation - static/loader.js updated to load modules sequentially Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
96 lines
4.3 KiB
Python
96 lines
4.3 KiB
Python
"""Voice design presets: load, save, slug helpers."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
|
|
from core.constants import CONFIG_DIR, DESIGN_PRESETS_FILE
|
|
|
|
_DEFAULT_DESIGN_PRESETS = {
|
|
"EN_M_Young_Energetic": {
|
|
"description": "Young adult male voice, clear English, bright and energetic, moderately high pitch, quick but controlled speaking rate, confident and friendly, suitable for tutorials or streaming.",
|
|
"sample_text": "Hey everyone, welcome back. Today we are going to move quickly, keep it clear, and make this setup feel easy.",
|
|
"language": "English",
|
|
"gender": "M",
|
|
},
|
|
"EN_F_Warm_Narrator": {
|
|
"description": "Adult female English narrator, warm and smooth, medium pitch, calm pace, gentle emotion, clear articulation, suited for audiobooks and voice assistant responses.",
|
|
"sample_text": "The room grew quiet as the morning light touched the window, and for a moment everything felt simple and kind.",
|
|
"language": "English",
|
|
"gender": "F",
|
|
},
|
|
"DE_M_Elderly_Documentary": {
|
|
"description": "Aeltere maennliche deutsche Stimme, tief und resonant, langsam und gelassen, klar artikuliert, ruhig und dokumentarisch, mit serioeser und vertrauensvoller Praesenz.",
|
|
"sample_text": "Seit vielen Jahren beobachten wir diesen Ort, seine Geschichte und die Menschen, die ihn mit Leben fuellen.",
|
|
"language": "German",
|
|
"gender": "M",
|
|
},
|
|
"DE_F_Young_Friendly": {
|
|
"description": "Junge weibliche deutsche Stimme, hell und freundlich, natuerliche Sprechgeschwindigkeit, klare Aussprache, leicht optimistisch und nahbar, passend fuer Assistenten und kurze Erklaerungen.",
|
|
"sample_text": "Hallo, schoen dass du da bist. Ich zeige dir kurz, wie alles funktioniert, Schritt fuer Schritt.",
|
|
"language": "German",
|
|
"gender": "F",
|
|
},
|
|
"EN_N_Old_Wise_Assistant": {
|
|
"description": "Older neutral English voice, gentle and wise, slightly low pitch, slow measured pace, soothing tone, very clear pronunciation, calm personality for guidance and reflective narration.",
|
|
"sample_text": "Take a slow breath. We will look at the facts carefully, choose the next step, and keep moving.",
|
|
"language": "English",
|
|
"gender": "N",
|
|
},
|
|
}
|
|
|
|
|
|
def _slug_voice_design_name(name: str) -> str:
|
|
slug = re.sub(r"[^A-Za-z0-9_.-]+", "_", name.strip()).strip("._-")
|
|
return slug or "VoiceDesign"
|
|
|
|
|
|
def _load_design_presets() -> dict:
|
|
presets = dict(_DEFAULT_DESIGN_PRESETS)
|
|
if DESIGN_PRESETS_FILE.exists():
|
|
try:
|
|
saved = json.loads(DESIGN_PRESETS_FILE.read_text())
|
|
if isinstance(saved, dict):
|
|
for name, preset in saved.items():
|
|
if isinstance(preset, dict):
|
|
presets[_slug_voice_design_name(str(name))] = {
|
|
"description": str(preset.get("description", "")),
|
|
"sample_text": str(preset.get("sample_text", preset.get("text", ""))),
|
|
"language": str(preset.get("language", "Auto")),
|
|
"gender": str(preset.get("gender", "N")),
|
|
}
|
|
except Exception:
|
|
pass
|
|
return presets
|
|
|
|
|
|
def _save_design_presets(presets: dict) -> None:
|
|
cleaned = {}
|
|
for name, preset in presets.items():
|
|
if not isinstance(preset, dict):
|
|
continue
|
|
key = _slug_voice_design_name(str(name))
|
|
cleaned[key] = {
|
|
"description": str(preset.get("description", "")),
|
|
"sample_text": str(preset.get("sample_text", preset.get("text", ""))),
|
|
"language": str(preset.get("language", "Auto")),
|
|
"gender": str(preset.get("gender", "N")),
|
|
}
|
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
DESIGN_PRESETS_FILE.write_text(json.dumps(cleaned, indent=2))
|
|
|
|
|
|
def _virtual_voice_id(name: str) -> str:
|
|
return f"vd_{_slug_voice_design_name(name)}"
|
|
|
|
|
|
def _resolve_virtual_voice(voice: str) -> tuple[str, dict] | None:
|
|
if not voice.startswith("vd_"):
|
|
return None
|
|
wanted = _slug_voice_design_name(voice[3:])
|
|
presets = _load_design_presets()
|
|
for name, preset in presets.items():
|
|
if _slug_voice_design_name(name) == wanted:
|
|
return name, preset
|
|
return None
|