feat: enhance TTS language detection with frequency scoring and explicit overrides

This commit is contained in:
mARTin-B78 2026-07-07 03:59:25 +02:00
parent ce3ae79132
commit 46ef73bbae
2 changed files with 38 additions and 20 deletions

View File

@ -178,23 +178,39 @@ def _request_app_name(request: Request) -> str:
# ── Language detection ────────────────────────────────────────────────────────
def _detect_text_language(text: str) -> str:
low = f" {text.lower()} "
if re.search(r"[äöüß]", low) or re.search(r"\b(und|der|die|das|ich|nicht|mit|ist|ein|eine|auf|für)\b", low):
return "DE"
if re.search(r"[éèêàçùœ]", low) or re.search(r"\b(et|le|la|les|des|une|avec|pour|est|pas|que)\b", low):
return "FR"
if re.search(r"[áéíóúñ¿¡]", low) or re.search(r"\b(el|la|los|las|una|con|para|que|pero|está|hola)\b", low):
return "ES"
if re.search(r"\b(il|lo|gli|una|con|per|che|ciao|grazie|sono|della)\b", low):
return "IT"
if re.search(r"[ãõç]", low) or re.search(r"\b(com|para|uma|que|não|está|obrigado)\b", low):
return "PT"
if re.search(r"\b(het|een|niet|met|voor|zijn|maar|dank|goede)\b", low):
return "NL"
if re.search(r"[ąćęłńóśźż]", low) or re.search(r"\b(jest|nie|tak|dla|oraz|dzień|dziękuję)\b", low):
return "PL"
words = re.findall(r"\b[a-zäöüßéèêàçùœáíóúñąćęłńóśźż]+\b", text.lower())
if not words:
return "EN"
scores = {"EN": 0, "DE": 0, "FR": 0, "ES": 0, "IT": 0, "PT": 0, "NL": 0, "PL": 0}
if re.search(r"[äöüß]", text.lower()):
scores["DE"] += 3
if re.search(r"[ąćęłńóśźż]", text.lower()):
scores["PL"] += 3
if re.search(r"[ãõ]", text.lower()):
scores["PT"] += 3
if re.search(r"[ñ¿¡]", text.lower()):
scores["ES"] += 3
stopwords = {
"EN": {"the", "be", "to", "of", "and", "a", "in", "that", "have", "i", "it", "for", "not", "on", "with", "he", "as", "you", "do", "at", "this", "but", "his", "by", "from", "they", "we", "say", "her", "she", "or", "an", "will", "my", "one", "all", "would", "there", "their", "what", "so", "up", "out", "if", "about", "who", "get", "which", "go", "me", "when", "make", "can", "like", "time", "no", "just", "him", "know", "take", "people", "into", "year", "your", "good", "some", "could", "them", "see", "other", "than", "then", "now", "look", "only", "come", "its", "over", "think", "also", "back", "after", "use", "two", "how", "our", "work", "first", "well", "way", "even", "new", "want", "because", "any", "these", "give", "day", "most", "us", "hello", "hi", "yes", "thanks", "please"},
"DE": {"und", "der", "die", "das", "ich", "nicht", "mit", "ist", "ein", "eine", "auf", "für", "ja", "nein", "gut", "morgen", "hallo", "danke", "bitte", "wie", "was", "warum", "wer", "wo", "hier", "da", "dann", "wenn", "so", "nur", "auch", "aber", "oder", "als", "um", "zu", "von", "aus", "bei", "nach", "vor", "an", "im", "am", "über", "unter", "doch", "schon", "sehr", "viel", "mehr", "immer", "wieder", "heute", "jetzt", "machen", "tun", "sagen", "gehen", "kommen", "sehen", "wissen", "sind", "wir", "ihr", "sie", "ihnen", "mir", "mich", "dir", "dich", "uns", "euch"},
"FR": {"et", "le", "la", "les", "des", "une", "avec", "pour", "est", "pas", "que", "je", "tu", "il", "elle", "nous", "vous", "ils", "elles", "qui", "quoi", "quand", "", "pourquoi", "comment", "oui", "non", "merci", "bonjour", "bien", "très", "tout", "plus", "moins", "dans", "sur", "sous", "devant", "derrière", "avant", "après", "ici", "", "aujourd'hui", "maintenant", "faire", "dire", "aller", "venir", "voir", "savoir", "un", "en", "au", "aux", "ce", "ces", "se", "sa", "son", "ses"},
"ES": {"el", "la", "los", "las", "una", "con", "para", "que", "pero", "está", "hola", "y", "o", "no", "", "gracias", "por", "favor", "bien", "mal", "muy", "mucho", "poco", "más", "menos", "todo", "nada", "algo", "aquí", "allí", "ahora", "hoy", "mañana", "ayer", "siempre", "nunca", "hacer", "decir", "ir", "venir", "ver", "saber", "poder", "querer", "tener", "ser", "estar", "un", "en", "su", "sus", "te", "me", "se", "nos"},
"IT": {"il", "lo", "gli", "una", "con", "per", "che", "ciao", "grazie", "sono", "della", "e", "o", "non", "", "prego", "bene", "male", "molto", "poco", "più", "meno", "tutto", "niente", "qualcosa", "qui", "", "ora", "oggi", "domani", "ieri", "sempre", "mai", "fare", "dire", "andare", "venire", "vedere", "sapere", "potere", "volere", "avere", "essere", "un", "in", "su", "di", "da", "al", "ai", "mi", "ti", "si", "ci", "vi"},
}
for word in words:
for lang, words_set in stopwords.items():
if word in words_set:
scores[lang] += 1
best_lang = max(scores, key=scores.get)
if scores[best_lang] == 0:
return "EN"
return best_lang
# ── Route matching ────────────────────────────────────────────────────────────
@ -217,8 +233,8 @@ def _route_specificity(rule: dict, app: str, voice: str, lang: str) -> tuple[int
)
def _resolve_tts_route(app: str, voice: str, text: str) -> tuple[str, dict | None]:
lang = _detect_text_language(text)
def _resolve_tts_route(app: str, voice: str, text: str, explicit_lang: str = "") -> tuple[str, dict | None]:
lang = explicit_lang.upper() if explicit_lang else _detect_text_language(text)
best: tuple[tuple[int, int, int, int], dict] | None = None
for idx, rule in enumerate(_load_tts_routes()):
spec = _route_specificity(rule, app, voice, lang)

View File

@ -741,9 +741,10 @@ async def tts_route_test(request: Request):
text = str(data.get("input") or data.get("text") or "").strip()
voice = str(data.get("voice") or "default").strip()
app_name = _canonical_app_name(str(data.get("app") or data.get("client") or "Open WebUI").strip())
explicit_lang = str(data.get("language") or data.get("lang") or "").strip()
if not text:
raise HTTPException(400, "input is required")
routed_voice, route = _resolve_tts_route(app_name, voice, text)
routed_voice, route = _resolve_tts_route(app_name, voice, text, explicit_lang)
backend = _route_backend(route, routed_voice)
settings = _load_settings()
scan_dir = Path(settings.get("voices_scan_dir", _VOICES_DIR_DEFAULT))
@ -872,7 +873,8 @@ async def openai_speech_proxy(request: Request):
)
raise HTTPException(400, "voice is required")
voice = fallback
voice, route = _resolve_tts_route(request_app, voice, text)
explicit_lang = str(data.get("language") or data.get("lang") or "").strip()
voice, route = _resolve_tts_route(request_app, voice, text, explicit_lang)
backend = _route_backend(route, voice)
style_instruction = str(data.get("instruct") or data.get("style_instruction") or "")
virtual = _resolve_virtual_voice(voice)