diff options
Diffstat (limited to 'app/ui/hub.py')
| -rw-r--r-- | app/ui/hub.py | 78 |
1 files changed, 60 insertions, 18 deletions
diff --git a/app/ui/hub.py b/app/ui/hub.py index 9bd136a..76187d6 100644 --- a/app/ui/hub.py +++ b/app/ui/hub.py @@ -67,6 +67,7 @@ from converter.clients import ( QWEN3_TTS_SPEAKERS, audiocpp_entry_supports_design, audiocpp_entry_voice_capability, + audiocpp_family_narrates, audiocpp_family_voice_policy, normalize_language, ) @@ -1285,6 +1286,23 @@ def _audiocpp_fields(stdscr, api_url: Optional[str] = None, return any(entry_capability(m) == AUDIOCPP_VOICE_DESIGN for m in models) + def narration_models() -> list: + """The configured entries that can synthesize narration from text. + + Families whose model spec has no text-synthesis task (e.g. + PersonaPlex, speech-to-speech-only) can only fail an "All" run, so + they are skipped there (with a run notice) and refused as an + All-of-nothing pick in all_voice_problem. Entries of families the + local specs do not describe stay included (unknown = capable). + """ + return [m for m in models + if audiocpp_family_narrates(m.get("family") or "") is not False] + + def non_narrating_models() -> list: + """The configured entries that cannot synthesize text (see above).""" + return [m for m in models + if audiocpp_family_narrates(m.get("family") or "") is False] + def all_voice_union() -> list: """Every voice an "All" run can offer. @@ -1343,16 +1361,23 @@ def _audiocpp_fields(stdscr, api_url: Optional[str] = None, """Why an "All" run cannot start with the current settings, or None. Refuses when a voice design model is configured without the - Instructions text its voice comes from, and when a clone-only - model has neither server voices nor an instruction-defined voice. - Every other mismatch is resolved by all_voice_for's per-model - fallback instead. + Instructions text its voice comes from, when a clone-only + model has neither server voices nor an instruction-defined voice, + and when every configured model is non-narrating (nothing left to + run after the non-narrating skip). Every other mismatch is + resolved by all_voice_for's per-model fallback instead. """ instructions_value = str(_field_value(fields, prefix + "instructions") or "").strip() if any_design_model() and not instructions_value: return ("The 'All' run includes a voice design model — " "describe the voice in Instructions") + skipped = non_narrating_models() + if len(skipped) == len(models): + names = ", ".join(str(m.get("id")) for m in skipped) + return ("None of the configured models can synthesize text " + f"({names} only transform audio) — there is nothing " + "for an 'All' run to generate with") for m in models: if entry_capability(m) != AUDIOCPP_VOICE_CLONE: continue @@ -1678,12 +1703,20 @@ def _audiocpp_fields(stdscr, api_url: Optional[str] = None, # model, each with the picked voice where the model accepts # it and its per-model fallback where it does not # (see all_voice_for). audiobook.convert unloads loaded - # models between the per-model conversions. + # models between the per-model conversions. Non-narrating + # families (speech-to-speech-only etc.) are skipped — they + # would fail every request — and reported on the run view's + # notice line. picked = result.get(prefix + "audiocpp_voice") or "" - kwargs["model_ids"] = [m.get("id") for m in models] + kwargs["model_ids"] = [m.get("id") for m in narration_models()] kwargs["model_voices"] = { model_id: all_voice_for(model_id, picked) for model_id in kwargs["model_ids"]} + skipped = non_narrating_models() + if skipped: + kwargs["run_notice"] = ( + "skipped non-TTS model(s): " + + ", ".join(str(m.get("id")) for m in skipped)) return ("convert", BACKEND_AUDIOCPP, kwargs) model_id = result[prefix + "model_id"] # The picked voice (a built-in speaker name on a CustomVoice entry, @@ -2178,6 +2211,9 @@ def _prepare_run_config(backend: str, kwargs: dict # The run-view behavior toggle (not a converter kwarg): stop the server # and quit the TUI once the generation ends. stop_and_exit = bool(kwargs.pop("stop_and_exit", False)) + # A pre-flight warning the convert form recorded (e.g. the "All" run's + # skipped non-narrating models): shown under the progress panel. + run_notice = str(kwargs.pop("run_notice", "") or "") # book_files/planned travel on the dedicated RunConfig fields; keeping # them in kwargs too would collide with convert()'s named parameters. book_files = kwargs.pop("book_files", None) or [] @@ -2191,16 +2227,19 @@ def _prepare_run_config(backend: str, kwargs: dict kwargs=kwargs, book_files=book_files, planned=planned, server_url=api_url, server_identity=identity, - log_path=log_path, stop_and_exit=stop_and_exit) + log_path=log_path, notice=run_notice, + stop_and_exit=stop_and_exit) status = next((s for s in detect_all(refresh=True) if s.key == backend), None) - notice = "" + # Notices accumulate (the run form's pre-flight warning, config + # repairs, server fallbacks) instead of each overwriting the last. + notices = [run_notice] if rehosted: - notice = ('re-hosted clone-only audio.cpp model(s) with task ' - '"clon" in server.json' - + ("; the managed server is restarted to load it" - if restart_name else "")) + notices.append('re-hosted clone-only audio.cpp model(s) with task ' + '"clon" in server.json' + + ("; the managed server is restarted to load it" + if restart_name else "")) spec: Optional[ServerSpec] = None if autostart: spec = _find_spec(autostart) @@ -2208,17 +2247,18 @@ def _prepare_run_config(backend: str, kwargs: dict spec = _select_spec(status, kwargs) if spec is not None and common.server_running(spec.url) \ and not servers.alive(spec.name): - notice = (f"a server this tool did not start is running at " - f"{spec.url} — the conversion will talk to it") + notices.append(f"a server this tool did not start is running " + f"at {spec.url} — the conversion will talk to it") if autostart and spec is None: # The recorded server vanished (backend reconfigured meanwhile): # converting without it is still meaningful, so continue. - notice = (f"no server named '{autostart}' — starting it was skipped") + notices.append(f"no server named '{autostart}' — starting it was " + "skipped") if restart_name and spec is None: spec = _find_spec(restart_name) if spec is None: - notice = (f"no server named '{restart_name}' — the model " - "switch restart was skipped") + notices.append(f"no server named '{restart_name}' — the model " + "switch restart was skipped") if spec is not None and backend == BACKEND_QWEN: # One demo server hosts one model: aim the spec at the model this # run selected (same URL/port, matching probe identity), so an @@ -2233,7 +2273,9 @@ def _prepare_run_config(backend: str, kwargs: dict server_identity=spec.identity if spec is not None else None, autostart_spec=spec if (autostart or restart_name) else None, restart_first=bool(restart_name) and spec is not None, - log_path=log_path, notice=notice, stop_and_exit=stop_and_exit) + log_path=log_path, + notice="; ".join(n for n in notices if n), + stop_and_exit=stop_and_exit) def _qwen_wanted_model(kwargs: dict) -> str: |
