fix(voices): archetype language filter accepts ISO codes

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-10-04 18:29:28 +02:00
parent 5dbe8c8998
commit 8fed5eeda8
3 changed files with 24 additions and 1 deletions

View File

@ -10,6 +10,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
- **Voice list rows no longer show the "Source unknown" badge** (it crowded the play buttons). The badge stays in the voice inspector, next to the Source & consent panel.
### Fixed
- **Archetype `language` filter accepted only full names.** `GET /api/archetypes?language=de` returned 0 items; it now accepts ISO codes and names, case-insensitive (`de`, `DE`, `German`); unknown values return an empty list.
- **LibriVox search returned 502 "unavailable" after 20-30 s.** Cause: the LibriVox API is erratic (10-40 s for any request, measured from host and container; egress/DNS/TLS fine) and the old call asked for `extended=1` (all chapters of 15 books) with a 30 s timeout, mapping every failure to one generic message. Now the list is light (no `extended`), the first chapter is fetched per book on demand (`GET /api/voice-sources/librivox/book/{id}`, UI "Load preview" / on import), timeout 25 s with one retry and backoff, failures cached 30 s, distinct messages (timeout / unreachable / HTTP status / unreadable) and a privacy-safe log line (exception class and query length only). Host/redirect rules unchanged.
- **`export_provenance_default` missing from settings defaults.** It is now in the defaults (`true`), so `GET /api/settings` exposes it.
### Added

View File

@ -5,6 +5,8 @@ DESIGN_CHIPS taxonomy of static/js/voice-clone.js. Validated at import.
"""
from __future__ import annotations
from core.open_voice_sources import LANG_NAMES
FACET_VOCAB = {
"gender": {"male", "female", "neutral"},
"age": {"child", "teenager", "young adult", "middle-aged", "elderly"},
@ -147,15 +149,22 @@ BY_ID = {a["id"]: a for a in ARCHETYPES}
MAX_LIMIT = 100
def _lang_name(v: str) -> str:
"""ISO code or full name, case-insensitive -> canonical name; unknown values pass through (match nothing)."""
v = (v or "").strip()
return LANG_NAMES.get(v.lower()) or next((n for n in LANG_NAMES.values() if n.lower() == v.lower()), v)
def list_archetypes(gender="", age="", pitch="", accent="", language="", category="", q="",
offset=0, limit=24) -> dict:
flt = {"gender": gender, "age": age, "pitch": pitch, "accent": accent}
ql = (q or "").strip().lower()[:100]
lang = _lang_name(language)
rows = []
for a in ARCHETYPES:
if any(v and a["facets"][k] != v for k, v in flt.items()):
continue
if language and a["language"] != language or category and a["category"] != category:
if lang and a["language"] != lang or category and a["category"] != category:
continue
if ql and ql not in f'{a["name_de"]} {a["name_en"]} {a["instruct"]} {a["category"]}'.lower():
continue

View File

@ -51,6 +51,19 @@ class Catalog(unittest.TestCase):
self.assertNotEqual(va.preview_key(a, "m"), va.preview_key(a, "m", 7))
def test_language_filter_codes_and_names(self):
n = lambda l: va.list_archetypes(language=l)["total"]
de = n("German")
self.assertGreater(de, 0)
self.assertEqual(n("de"), de)
self.assertEqual(n("DE"), de)
self.assertEqual(n("german"), de)
self.assertEqual(n("en"), n("English"))
self.assertGreater(n("en"), 0)
self.assertEqual(n("xx"), 0)
self.assertEqual(n("French"), 0)
class UseRoute(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory(); self.addCleanup(self.tmp.cleanup)