diff --git a/CHANGELOG.md b/CHANGELOG.md index fae44b1..ecc3176 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/core/voice_archetypes.py b/core/voice_archetypes.py index 141e1b7..aa33306 100644 --- a/core/voice_archetypes.py +++ b/core/voice_archetypes.py @@ -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 diff --git a/tests/test_voice_archetypes.py b/tests/test_voice_archetypes.py index c66df3a..aa3eec4 100644 --- a/tests/test_voice_archetypes.py +++ b/tests/test_voice_archetypes.py @@ -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)