diff options
Diffstat (limited to 'app/ui/hub.py')
| -rw-r--r-- | app/ui/hub.py | 99 |
1 files changed, 47 insertions, 52 deletions
diff --git a/app/ui/hub.py b/app/ui/hub.py index afb8a30..e883996 100644 --- a/app/ui/hub.py +++ b/app/ui/hub.py @@ -988,10 +988,9 @@ def _help_lines() -> list: (None = body). Numbered steps start at the margin; every other line is indented three spaces — Frame's two-space indent unit plus a leading space in the row's first segment — so it lines up - with the step text after the "N. " prefixes. The - input/output folders are read from the converter module at call - time, so a Settings change this session is reflected without a - restart. + with the step text after the "N. " prefixes. The input/output and + clone .wav folders are read at call time, so a Settings change this + session is reflected without a restart. """ return [ ([("1. ", "title"), @@ -1000,7 +999,7 @@ def _help_lines() -> list: "", ([("2. ", "title"), ("Put any .wavs of voices to clone here:", None)], 0), - ([(" " + str(common.VOICES_DIR), "input")], 1), + ([(" " + str(_clone_wav_dir()), "input")], 1), "", ([("3. ", "title"), ("If no backend is installed, go to ", None), @@ -1948,8 +1947,9 @@ def _qwen_fields(remote_modes: Optional[list] = None, Returns ``(fields, mapper)`` where FIELDS are the qwen options — which model the demo server hosts (Base (voice cloning) / CustomVoice (built-in voices) / VoiceDesign (design)), plus the per-model controls: Speaker on - CustomVoice, a Clone .wav directory browser (default ./voices) + Voice- - to-clone .wav picker on Base, Instructions on VoiceDesign — and + CustomVoice, a Voice-to-clone .wav picker on Base (listing the reference + .wavs in the configured Clone .wav directory — a Settings entry), + Instructions on VoiceDesign — and MAPPER turns a submitted form values dict into the qwen converter kwargs. None (form omitted) when a filtered remote model list comes back empty — no known mode matched what the remote demo reported. @@ -1990,29 +1990,18 @@ def _qwen_fields(remote_modes: Optional[list] = None, speakers = list(qwen_backend.QWEN_SPEAKERS) default_speaker = speakers[0] - # Voice cloning references: the directory the .wavs live in — browsed - # with the directory widget, defaulting to the project's ./voices (the - # folder the Help screen points at) — plus a picker of the .wav files - # found there (the same directory + voice picker the audio.cpp form - # uses; the demo uploads exactly one reference file). + # Voice cloning reference: the .wav picker lists the files in the + # configured Clone .wav directory (a Settings entry, defaulting to the + # project's ./voices — the folder the Help screen points at); the demo + # uploads exactly one reference file. def clone_wav_choices(fs) -> list: """(file name, full path) pairs for the clone directory's .wavs.""" - return [(p.name, str(p)) for p in _list_wavs( - _field_value(fs, prefix + "clone_dir"))] - - def reset_clone_wav(fs) -> None: - """Re-point the .wav picker at the newly chosen directory.""" - wav_field = next(f for f in fields - if f.get("key") == prefix + "clone") - wav_field["value"] = next( - (path for _name, path in clone_wav_choices(fs)), "") + return [(p.name, str(p)) for p in _list_wavs(_clone_wav_dir())] def no_wavs_hint(_fs=None) -> str: """Why the .wav picker is empty (validate echoes it on Generate!).""" - directory = next((f.get("value") for f in fields - if f.get("key") == prefix + "clone_dir"), None) - return (f"No .wav files in {directory} — put a reference .wav " - "there or pick another directory.") + return (f"No .wav files in {_clone_wav_dir()}. " + "Check directory in Settings.") def clone_wav_validate(value) -> Optional[str]: """Refuse Generate! when no reference .wav is available to clone.""" @@ -2020,7 +2009,7 @@ def _qwen_fields(remote_modes: Optional[list] = None, return None return no_wavs_hint() - initial_wavs = _list_wavs(common.VOICES_DIR) + initial_wavs = _list_wavs(_clone_wav_dir()) initial_clone = str(initial_wavs[0]) if initial_wavs else "" fields = [ {"key": prefix + "mode", "label": "Model", "kind": "choice", @@ -2030,11 +2019,6 @@ def _qwen_fields(remote_modes: Optional[list] = None, {"key": prefix + "speaker", "label": "Speaker", "kind": "choice", "value": default_speaker, "choices": speakers, "visible": lambda fs: _field_value(fs, prefix + "mode") == "custom"}, - {"key": prefix + "clone_dir", "label": "Clone .wav directory", - "kind": "dir", "value": common.VOICES_DIR, - "info": common.wav_dir_info, "preview": common.wav_dir_preview, - "on_change": reset_clone_wav, - "visible": lambda fs: _field_value(fs, prefix + "mode") == "clone"}, {"key": prefix + "clone", "label": "Voice to clone", "kind": "choice", "value": initial_clone, "choices": clone_wav_choices, "on_empty_choices": no_wavs_hint, @@ -2141,11 +2125,11 @@ def _sglomni_fields(stdscr, api_url: Optional[str] = None, (preset voices on speaker models; on clone models the model's built-in default voice on top when it can narrate without a reference, then the server's uploaded named voices, then the - reference .wavs from the Clone .wav directory browser — picking a - .wav clones it, and on models that cannot narrate without a - reference a .wav pick is required), Instructions on the VoiceDesign - model — and MAPPER turns a submitted form values dict into the - sglomni converter kwargs. Returns None when + reference .wavs in the configured Clone .wav directory — a Settings + entry — picking a .wav clones it, and on models that cannot narrate + without a reference a .wav pick is required), Instructions on the + VoiceDesign model — and MAPPER turns a submitted form values dict + into the sglomni converter kwargs. Returns None when the entry's options cannot be gathered (a flash explains why), so the caller drops SGLang-Omni from the Backend choices. PREFIX namespaces the field keys ("" for the managed entry) so two entries of this @@ -2188,11 +2172,11 @@ def _sglomni_fields(stdscr, api_url: Optional[str] = None, def model_capability(fs) -> str: return model_entry(fs).capability - # Voice-clone references: the qwen form's directory + .wav picker. + # Voice-clone references: the qwen form's .wav picker, fed by the + # configured Clone .wav directory (a Settings entry). def clone_wav_choices(fs) -> list: - """The reference .wavs offered by the clone-directory field.""" - return [(p.name, str(p)) for p in _list_wavs( - _field_value(fs, prefix + "clone_dir"))] + """The reference .wavs offered by the clone directory.""" + return [(p.name, str(p)) for p in _list_wavs(_clone_wav_dir())] def voice_choices(fs): """One Voice menu per capability. @@ -2218,10 +2202,8 @@ def _sglomni_fields(stdscr, api_url: Optional[str] = None, return choices def no_wavs_hint(_fs=None) -> str: - directory = next((f.get("value") for f in fields - if f.get("key") == prefix + "clone_dir"), None) - return (f"No .wav files in {directory} — put a reference .wav " - "there or pick another directory.") + return (f"No .wav files in {_clone_wav_dir()}. " + "Check directory in Settings.") def voice_validate(value) -> Optional[str]: """Blank is the built-in default voice — unless the model needs @@ -2313,7 +2295,7 @@ def _sglomni_fields(stdscr, api_url: Optional[str] = None, initial_voice = voices[0] if voices else "" elif (default_entry.capability == "clone" and default_entry.requires_reference): - initial_wavs = _list_wavs(common.VOICES_DIR) + initial_wavs = _list_wavs(_clone_wav_dir()) initial_voice = str(initial_wavs[0]) if initial_wavs else "" fields = [ @@ -2328,11 +2310,6 @@ def _sglomni_fields(stdscr, api_url: Optional[str] = None, "validate": voice_validate, "visible": lambda fs: model_capability(fs) in ("speaker", "clone")}, - {"key": prefix + "clone_dir", "label": "Clone .wav directory", - "kind": "dir", "value": common.VOICES_DIR, - "info": common.wav_dir_info, "preview": common.wav_dir_preview, - "on_change": reset_voice_fields, - "visible": lambda fs: model_capability(fs) == "clone"}, {"key": prefix + "instructions", "label": "Instructions", "kind": "text", "value": "", "help": ["Describe the voice to design, e.g.", @@ -2351,8 +2328,7 @@ def _sglomni_fields(stdscr, api_url: Optional[str] = None, elif entry.capability == "clone": # A .wav pick clones it; a named (uploaded) voice reuses a # server-side voice; blank uses the model's built-in default. - wavs = {str(p) for p in _list_wavs( - result.get(prefix + "clone_dir"))} + wavs = {str(p) for p in _list_wavs(_clone_wav_dir())} if pick in wavs: kwargs["clone"] = pick elif pick: @@ -2370,6 +2346,17 @@ def _sglomni_fields(stdscr, api_url: Optional[str] = None, # Settings menu (global output options -> app/converter/config.py) # --------------------------------------------------------------------------- +def _clone_wav_dir() -> Path: + """The configured Clone .wav directory (a Settings entry). + + The directory the qwen-tts Base / SGLang-Omni voice pickers list + their reference .wavs from. Resolved like the input/output folders: + relative paths resolve against the project root, blank falls back + to the project's ./voices. + """ + return converter_mod.resolve_dir(config.CLONE_WAV_DIR, "voices") + + def _settings_changed(fields: list, original: dict) -> bool: """True when any field's current value differs from its ORIGINAL. @@ -2409,6 +2396,10 @@ def _settings_fields() -> list: {"key": "output_dir", "label": "Output Directory", "kind": "dir", "value": converter_mod.resolve_dir(config.OUTPUT_DIR, "output"), "validate": _validate_dir}, + {"key": "clone_wav_dir", "label": "Clone .wav directory", + "kind": "dir", "value": _clone_wav_dir(), + "info": common.wav_dir_info, "preview": common.wav_dir_preview, + "validate": _validate_dir}, {"key": "speed", "label": "Speed", "kind": "text", "value": str(config.SPEED), "validate": _validate_speed}, {"key": "debug", "label": "Debug", "kind": "bool", @@ -2550,10 +2541,13 @@ def _apply_settings(values: dict) -> None: raise ValueError("Speed must be a positive number") input_dir = str(values["input_dir"]).strip() output_dir = str(values["output_dir"]).strip() + clone_dir = str(values["clone_wav_dir"]).strip() if not input_dir: raise ValueError("Input Directory must not be empty") if not output_dir: raise ValueError("Output Directory must not be empty") + if not clone_dir: + raise ValueError("Clone .wav directory must not be empty") ports = { "qwen_port": _read_port(values, "qwen_port"), @@ -2578,6 +2572,7 @@ def _apply_settings(values: dict) -> None: "CHUNK_SIZE": chunk_size, "INPUT_DIR": input_dir, "OUTPUT_DIR": output_dir, + "CLONE_WAV_DIR": clone_dir, "SPEED": speed, "DEBUG": bool(values["debug"]), "STOP_SERVER_AND_EXIT": bool(values["stop_and_exit"]), |
