aboutsummaryrefslogtreecommitdiff
path: root/app/ui/hub.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-31 14:08:36 -0400
committerhistoria <historiavg@proton.me>2026-08-31 14:08:36 -0400
commit3b109185d642319c2c1870815b25ae1c0ad49445 (patch)
tree475bdac0f98de68d99b1c8ce251c74a092d507cb /app/ui/hub.py
parenta0e3050c6e1e43df3941077afa4ade9a1c4d6ce4 (diff)
downloadtts-audiobook-generator-3b109185d642319c2c1870815b25ae1c0ad49445.tar.gz
feat: model capabilities lined up in tui menu
Diffstat (limited to 'app/ui/hub.py')
-rw-r--r--app/ui/hub.py67
1 files changed, 49 insertions, 18 deletions
diff --git a/app/ui/hub.py b/app/ui/hub.py
index 29d2229..abb375a 100644
--- a/app/ui/hub.py
+++ b/app/ui/hub.py
@@ -65,6 +65,7 @@ from converter.clients import (
BACKEND_QWEN,
LANGUAGE_CHOICES,
QWEN3_TTS_SPEAKERS,
+ audiocpp_entry_supports_design,
audiocpp_entry_voice_capability,
audiocpp_family_voice_policy,
normalize_language,
@@ -1221,8 +1222,10 @@ def _audiocpp_fields(stdscr, api_url: Optional[str] = None,
fields, prefix + "model_id"))]
if model_voice_policy(fields) == AUDIOCPP_VOICE_OPTIONAL:
# Mixed tts+clone family: the blank pick means plain TTS
- # (no reference voice), so it always leads the menu.
- return [("", "(built-in)")] + voices
+ # (the model's own built-in voice, no reference cloned), so
+ # it always leads the menu. The pair is (label, value): the
+ # readable label describes the blank value.
+ return [("<built-in> (no clone)", "")] + voices
return voices
return [] # design or pure TTS: the field is hidden
@@ -1282,25 +1285,53 @@ def _audiocpp_fields(stdscr, api_url: Optional[str] = None,
initial = voices_for(default_model)
initial_voice = initial[0] if initial else ""
- # The Model picker reads as a two-column table: pad every id to the
- # widest one so the (type) column starts on the same position.
+ # The Model picker reads as a table: pad every id to the widest one,
+ # then render each entry's capabilities as fixed columns (how plain
+ # synthesis is voiced | clone | design) so every capability word sits
+ # in its own column across rows — easy to scan at a glance.
id_width = max(len(entry.get("id") or "") for entry in models)
- def _label(entry: dict) -> str:
+ def _capabilities(entry: dict) -> tuple:
+ """The entry's capability words in fixed column order.
+
+ Column 1 voices plain synthesis ("speaker" for built-in speakers,
+ "tts" for families that need no voice at all), column 2 is
+ "clone" when the entry clones a reference, column 3 "design" when
+ it can design a voice from an Instructions description.
+ """
family = entry.get("family") or ""
- capability = audiocpp_entry_voice_capability(
- family, entry.get("task") or "tts", entry.get("id") or "")
- if capability == AUDIOCPP_VOICE_CLONE:
- # The generic clone capability is refined by the family's
- # voice policy: pure-TTS families need no voice at all, mixed
- # families may run with or without one, clone-only families
- # (and unknown families) always clone a reference.
- capability = {
- AUDIOCPP_VOICE_NONE: "tts",
- AUDIOCPP_VOICE_OPTIONAL: "tts/clone",
- AUDIOCPP_VOICE_REQUIRED: "clone",
- }[audiocpp_family_voice_policy(family)]
- return f"{entry.get('id') or '':<{id_width}} ({capability})"
+ task = entry.get("task") or "tts"
+ model_id = entry.get("id") or ""
+ capability = audiocpp_entry_voice_capability(family, task, model_id)
+ if capability == AUDIOCPP_VOICE_SPEAKER:
+ return ("speaker", "", "")
+ if capability == AUDIOCPP_VOICE_DESIGN:
+ return ("", "", "design")
+ # The generic clone capability is refined by the family's voice
+ # policy: pure-TTS families need no voice at all, mixed families
+ # may run with or without one, clone-only families (and unknown
+ # families) always clone a reference.
+ words = {
+ AUDIOCPP_VOICE_NONE: ("tts", "", ""),
+ AUDIOCPP_VOICE_OPTIONAL: ("tts", "clone", ""),
+ AUDIOCPP_VOICE_REQUIRED: ("", "clone", ""),
+ }[audiocpp_family_voice_policy(family)]
+ if audiocpp_entry_supports_design(family, task, model_id):
+ return words[:2] + ("design",)
+ return words
+
+ _capability_words = [_capabilities(entry) for entry in models]
+ _column_widths = [max((len(words[index])
+ for words in _capability_words), default=0)
+ for index in range(3)]
+
+ def _label(entry: dict) -> str:
+ words = _capabilities(entry)
+ row = f"{entry.get('id') or '':<{id_width}}"
+ for word, width in zip(words, _column_widths):
+ if width:
+ row += f" {word:<{width}}"
+ return row.rstrip()
def entry_supports_options(fs) -> bool:
"""True when the selected entry's family defines request options.