aboutsummaryrefslogtreecommitdiff
path: root/app/backends/audiocpp.py
diff options
context:
space:
mode:
Diffstat (limited to 'app/backends/audiocpp.py')
-rwxr-xr-xapp/backends/audiocpp.py465
1 files changed, 361 insertions, 104 deletions
diff --git a/app/backends/audiocpp.py b/app/backends/audiocpp.py
index a502c8f..f74f726 100755
--- a/app/backends/audiocpp.py
+++ b/app/backends/audiocpp.py
@@ -26,12 +26,20 @@ Usage:
With no flags and a terminal, the TUI wizard runs. Without a terminal
(or with all flags supplied), it runs non-interactively from the flags;
any missing required value is a hard error with a remediation hint.
+
+When the target ``server.json`` already exists, the TUI wizard runs as a
+"modify": it loads the existing models, host, port, backend, lazy-load
+and voice directory and pre-fills the screens with them (the model tree
+opens with the installed models already checked) instead of prompting to
+overwrite, and offers to delete already-downloaded models that are no
+longer selected.
"""
import argparse
import json
import os
import re
+import shutil
import subprocess
import sys
import urllib.parse
@@ -44,7 +52,6 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from backends import (
BackendStatus,
- ConfigureAction,
ServerSpec,
common,
format_launch_hint,
@@ -721,14 +728,18 @@ def _offer_config_model_id_sync(model_id: str, accepted: Optional[bool]) -> None
def _build_entries(family_keys: List[str], chosen: Dict[str, List[dict]],
catalog_by_family: Dict[str, dict],
task_picker: Callable[[str], str],
- id_picker: Callable[[str, str, str], str]
+ id_picker: Callable[[str, str, str], str],
+ known_tasks: Optional[Dict[Tuple[str, str], str]] = None
) -> Tuple[List[dict], List[str], List[Tuple[str, str]],
List[str], bool]:
"""Build server.json model entries from the selected families/packages.
TASK_PICKER is called for each design package to choose vdes/tts;
- ID_PICKER resolves a duplicate server entry id. Returns (model_entries,
- entry_ids, install_guidance, design_entry_ids, include_clone).
+ ID_PICKER resolves a duplicate server entry id. KNOWN_TASKS maps
+ ``(family, target_directory)`` to a previously-stored task ("tts" or
+ "vdes") so a modify run preserves how a design package was hosted
+ instead of re-asking. Returns (model_entries, entry_ids,
+ install_guidance, design_entry_ids, include_clone).
"""
model_entries: List[dict] = []
entry_ids: List[str] = []
@@ -739,7 +750,13 @@ def _build_entries(family_keys: List[str], chosen: Dict[str, List[dict]],
entry = catalog_by_family[family]
include_clone = include_clone or entry["clone_capable"]
for opt in chosen[family]:
- task = task_picker(opt["install_id"]) if opt["design"] else TASK_TTS
+ if opt["design"]:
+ task = known_tasks.get((family, opt["target_directory"])) \
+ if known_tasks else None
+ if task is None:
+ task = task_picker(opt["install_id"])
+ else:
+ task = TASK_TTS
base_id = (f"{entry['preferred_id']}-design"
if task == TASK_VDES else entry["preferred_id"])
model_id = base_id
@@ -861,8 +878,6 @@ def _build_tree_families(catalog: List[dict]) -> List[dict]:
if "design" in entry["tasks"]:
capabilities.append("design")
name = entry["display_name"]
- if name != entry["family"]:
- name = f"{name} ({entry['family']})"
options = []
for opt in package_dir_options(entry):
options.append({
@@ -899,21 +914,15 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser
step = 0
while True:
if step == 0:
- # Checkout browser + the output path/overwrite confirmation. The
- # browser asks for the checkout root and finds model_specs/ inside
- # it (picking the model_specs directory itself works too — its
- # parent is used). A highlighted subdirectory named "audio.cpp"
- # that already contains model_specs/ is auto-accepted on
- # Enter/Right, skipping the "[ Use this directory ]" step.
- # Pressing Esc on an overwrite confirmation returns here instead
- # of aborting: the browser then restarts inside the previously
- # accepted checkout with auto-accept disabled, so a wrong guess
- # can be corrected. An explicit --audiocpp-dir flag has no
- # browser to return to, so Esc still aborts there. Esc on the
- # browser itself is the first step, so it aborts the wizard.
+ # Checkout browser. The browser asks for the checkout root and
+ # finds model_specs/ inside it (picking the model_specs directory
+ # itself works too — its parent is used). A highlighted
+ # subdirectory named "audio.cpp" that already contains
+ # model_specs/ is auto-accepted on Enter/Right, skipping the
+ # "[ Use this directory ]" step. Esc on the browser is the first
+ # step, so it aborts the wizard.
auto_accept = True
browser_start: Path = Path.cwd()
- force_browse = False
def do_browse():
return tui.browse_directory(
@@ -932,44 +941,36 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser
while True:
audiocpp_dir = args.audiocpp_dir
- if audiocpp_dir is None and not force_browse:
+ if audiocpp_dir is None:
audiocpp_dir = find_local_checkout()
- if force_browse:
- audiocpp_dir = None
if audiocpp_dir is None:
- if force_browse:
- # Esc on an overwrite confirmation came back here: go
- # straight back into the browser inside the previously
- # accepted checkout (auto-accept disabled).
- audiocpp_dir = do_browse()
+ # No checkout found anywhere: offer to clone one into
+ # ./app/audio.cpp or browse for an existing checkout.
+ # Esc on this first menu aborts the wizard.
+ choice = tui.menu(
+ stdscr, "No audio.cpp checkout found",
+ [(f"Clone into ./app/{AUDIOCPP_DIR_NAME} "
+ f"(from {AUDIOCPP_GIT_URL})", "clone"),
+ ("Browse for an existing checkout", "browse")],
+ help_lines=[
+ "audio.cpp hosts the TTS model families "
+ "this generator uses.",
+ "Clone it into the project's app "
+ "directory, or point at an existing "
+ "checkout."])
+ if choice == "clone":
+ target = APP_DIR / AUDIOCPP_DIR_NAME
+ with tui.suspend(stdscr):
+ rc = common.git_clone(AUDIOCPP_GIT_URL,
+ target)
+ if rc != 0:
+ raise _TuiError(
+ f"git clone failed (exit {rc}). Clone "
+ f"audio.cpp manually: git clone "
+ f"{AUDIOCPP_GIT_URL} {target}")
+ audiocpp_dir = target
else:
- # No checkout found anywhere: offer to clone one into
- # ./app/audio.cpp or browse for an existing checkout.
- # Esc on this first menu aborts the wizard.
- choice = tui.menu(
- stdscr, "No audio.cpp checkout found",
- [(f"Clone into ./app/{AUDIOCPP_DIR_NAME} "
- f"(from {AUDIOCPP_GIT_URL})", "clone"),
- ("Browse for an existing checkout", "browse")],
- help_lines=[
- "audio.cpp hosts the TTS model families "
- "this generator uses.",
- "Clone it into the project's app "
- "directory, or point at an existing "
- "checkout."])
- if choice == "clone":
- target = APP_DIR / AUDIOCPP_DIR_NAME
- with tui.suspend(stdscr):
- rc = common.git_clone(AUDIOCPP_GIT_URL,
- target)
- if rc != 0:
- raise _TuiError(
- f"git clone failed (exit {rc}). Clone "
- f"audio.cpp manually: git clone "
- f"{AUDIOCPP_GIT_URL} {target}")
- audiocpp_dir = target
- else:
- audiocpp_dir = do_browse()
+ audiocpp_dir = do_browse()
audiocpp_dir = Path(audiocpp_dir).resolve()
if not audiocpp_dir.is_dir():
raise _TuiError(f"audio.cpp checkout not found: "
@@ -993,37 +994,28 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser
output_path = args.output if args.output is not None \
else audiocpp_dir / "server.json"
- esc_back = args.audiocpp_dir is None
- went_back = False
- if not args.force and output_path.exists():
- decision = tui.confirm(
- stdscr, f"{output_path} already exists. Overwrite?",
- default=True,
- cancel_value=_GO_BACK if esc_back else None)
- if decision is _GO_BACK:
- went_back = True
- elif decision is False:
- if args.output is None:
- output_path = Path.cwd() / "server.json"
- if output_path.exists():
- decision = tui.confirm(
- stdscr,
- f"{output_path} already exists. "
- "Overwrite?",
- default=True,
- cancel_value=_GO_BACK if esc_back else None)
- if decision is _GO_BACK:
- went_back = True
- elif decision is False:
- return None
- else:
- return None
- if went_back:
- auto_accept = False
- browser_start = audiocpp_dir
- force_browse = True
- continue
break
+
+ # Modify flow: an existing server.json seeds the wizard's
+ # screens instead of being overwritten from scratch (an explicit
+ # --force still starts fresh).
+ existing_config = load_server_config(output_path) \
+ if not args.force else None
+ if existing_config is not None:
+ existing_selected, existing_tasks = \
+ server_config_selections(existing_config, catalog)
+ else:
+ existing_selected, existing_tasks = {}, {}
+ existing_host = existing_config.get("host") \
+ if existing_config else None
+ existing_port = existing_config.get("port") \
+ if existing_config else None
+ existing_backend = existing_config.get("backend") \
+ if existing_config else None
+ existing_lazy = existing_config.get("lazy_load") \
+ if existing_config else None
+ existing_voice_dir = existing_config.get("voice_dir") \
+ if existing_config else None
detected_backend = detect_backend(audiocpp_dir)
step = 1
continue
@@ -1048,10 +1040,24 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser
catalog_by_family[family]) if opt["recommended"]]
else:
tree_families = _build_tree_families(catalog)
+ # Modify flow: pre-check the models an existing server.json
+ # hosts, so the tree opens as a "modify" list rather than a
+ # fresh one.
+ checked_set = set()
+ for family, dirs in existing_selected.items():
+ if family not in catalog_by_family:
+ continue
+ family_index = catalog.index(catalog_by_family[family])
+ valid_dirs = {opt["target_directory"]
+ for opt in package_dir_options(
+ catalog_by_family[family])}
+ for target in dirs:
+ if target in valid_dirs:
+ checked_set.add((family_index, target))
picked = tui.checkbox_tree(
stdscr, "Select TTS model families to host",
tree_families, expand_all=args.all_packages,
- back_value=_GO_BACK)
+ back_value=_GO_BACK, checked=checked_set)
if picked is _GO_BACK:
step = 0
continue
@@ -1100,7 +1106,7 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser
model_entries, entry_ids, install_guidance, \
design_entry_ids, include_clone = _build_entries(
family_keys, chosen, catalog_by_family,
- task_picker, id_picker)
+ task_picker, id_picker, known_tasks=existing_tasks)
except _GoBack:
step = 1
continue
@@ -1114,7 +1120,9 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser
host = args.host
else:
host = tui.line_edit(
- stdscr, "Bind host", DEFAULT_HOST,
+ stdscr, "Bind host",
+ existing_host if isinstance(existing_host, str)
+ else DEFAULT_HOST,
help_lines=["The IP address audiocpp will be hosted on",
"127.0.0.1 (this machine) is probably "
"correct"], back_value=_GO_BACK)
@@ -1125,7 +1133,9 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser
port = args.port
else:
port_text = tui.line_edit(
- stdscr, "Port", str(config_port()),
+ stdscr, "Port",
+ str(existing_port) if isinstance(existing_port, int)
+ else str(config_port()),
validate=lambda s: None if (s.isdigit()
and 1 <= int(s) <= 65535)
else "Enter a port number between 1 and 65535",
@@ -1154,6 +1164,11 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser
# Already built: use the detected backend, no menu, no build.
backend = detected_backend
build = False
+ elif existing_backend in BACKENDS:
+ # Modify flow: keep the backend an existing server.json
+ # records (already configured, no rebuild needed).
+ backend = existing_backend
+ build = False
else:
backend_options, backend_default = _backend_options(None)
backend = tui.menu(
@@ -1173,6 +1188,8 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser
step = 2
continue
default_lazy = len(model_entries) > 1
+ if isinstance(existing_lazy, bool):
+ default_lazy = existing_lazy
if args.lazy_load:
lazy_load = True
else:
@@ -1192,6 +1209,10 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser
wav_dir = args.input_dir
elif include_clone:
wav_start = detect_wav_dir(audiocpp_dir, TTS_ROOT)
+ # Modify flow: an existing voice_dir seeds the browser so the
+ # user can accept it on Enter instead of re-navigating.
+ if isinstance(existing_voice_dir, str) and existing_voice_dir:
+ wav_start = Path(existing_voice_dir)
wav_dir = tui.browse_directory(
stdscr, "Select the directory with your .wav voices",
info=_wav_dir_info, preview=_wav_dir_preview,
@@ -1240,16 +1261,35 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser
if sync_model_ids is _GO_BACK:
step = 5
continue
+ step = 7
+ continue
+
+ if step == 7:
+ # Delete unused models: already-downloaded models that the new
+ # selection no longer hosts. Only offered in the modify flow (an
+ # existing config was loaded), since a fresh --force run is an
+ # explicit overwrite. Esc falls back to the model-id sync (6).
+ new_paths = {entry["path"] for entry in model_entries}
+ unused_entries = unused_installed_entries(output_path, new_paths) \
+ if existing_config is not None else []
+ delete_unused = False
+ if unused_entries:
+ delete_unused = tui.confirm(
+ stdscr, "Delete unused models?", default=False,
+ cancel_value=_GO_BACK)
+ if delete_unused is _GO_BACK:
+ step = 6
+ continue
step = 8
continue
if step == 8:
# Automatic model download (or print the install commands). Esc
- # falls back to the model-id sync (step 6).
+ # falls back to the delete-unused step (7).
try:
download = _decide_download(audiocpp_dir, ask_confirm)
except _GoBack:
- step = 6
+ step = 7
continue
return {
"audiocpp_dir": audiocpp_dir,
@@ -1273,9 +1313,64 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser
"wav_dir": wav_dir,
"plan": plan,
"download": download,
+ "delete_unused": delete_unused,
+ "unused_entries": unused_entries,
}
+def load_server_config(server_json: Path) -> Optional[dict]:
+ """Read server.json into a dict, or None when it cannot be used.
+
+ Returns None for a missing file, unreadable content, or a non-dict
+ document. Used by the wizard's modify flow to pre-fill its screens
+ from an existing config instead of prompting to overwrite it.
+ """
+ if not server_json.exists():
+ return None
+ try:
+ data = json.loads(server_json.read_text(encoding="utf-8"))
+ except (OSError, ValueError):
+ return None
+ if not isinstance(data, dict):
+ return None
+ return data
+
+
+def server_config_selections(server_config: dict,
+ catalog: List[dict]
+ ) -> Tuple[Dict[str, List[str]],
+ Dict[Tuple[str, str], str]]:
+ """Map an existing server.json's models back to catalog selections.
+
+ Returns ``(selected_dirs, tasks)``: ``selected_dirs`` maps a catalog
+ family to the target directories it hosts (``models/<target>`` paths
+ with the ``models/`` prefix stripped, in server.json order), and
+ ``tasks`` maps ``(family, target_directory)`` to the entry's task
+ (``"tts"`` or ``"vdes"``) so the wizard can preserve how design
+ packages were hosted. Entries whose family is not in the CATALOG are
+ ignored — the wizard cannot offer them again.
+ """
+ families = {entry["family"] for entry in catalog}
+ selected_dirs: Dict[str, List[str]] = {}
+ tasks: Dict[Tuple[str, str], str] = {}
+ for entry in server_config.get("models") or []:
+ if not isinstance(entry, dict):
+ continue
+ family = entry.get("family")
+ if not isinstance(family, str) or family not in families:
+ continue
+ path = entry.get("path")
+ if not isinstance(path, str):
+ continue
+ target = path[len("models/"):] if path.startswith("models/") else path
+ if family not in selected_dirs:
+ selected_dirs[family] = []
+ if target not in selected_dirs[family]:
+ selected_dirs[family].append(target)
+ tasks[(family, target)] = str(entry.get("task") or TASK_TTS)
+ return selected_dirs, tasks
+
+
def _model_path_present(path: Path) -> bool:
"""True when a server.json model path holds actual model files.
@@ -1322,6 +1417,70 @@ def missing_model_entries(server_json: Path) -> List[dict]:
return missing
+def _install_id_by_path(audiocpp_dir: Path) -> Dict[str, str]:
+ """Map ``models/<target_directory>`` -> catalog install id.
+
+ The catalog package that installs a model is derived from the
+ ``default_path`` of each TTS family; an entry whose path matches no
+ catalog package has no install id.
+ """
+ by_path: Dict[str, str] = {}
+ try:
+ for entry in load_model_catalog(audiocpp_dir):
+ by_path[entry["default_path"]] = entry["install_id"]
+ except (NotADirectoryError, OSError):
+ pass
+ return by_path
+
+
+def installed_model_entries(server_json: Path) -> List[dict]:
+ """Return the server.json model entries whose files ARE on disk.
+
+ The complement of ``missing_model_entries``: each returned entry carries
+ the entry ``id`` and ``rel`` (the configured path string), resolved
+ exactly like ``missing_model_entries`` (relative against the server.json's
+ directory). Used by the wizard's "Delete unused models?" step to find
+ already-downloaded models that were unselected.
+ """
+ try:
+ data = json.loads(server_json.read_text(encoding="utf-8"))
+ except (OSError, ValueError):
+ return []
+ if not isinstance(data, dict):
+ return []
+ base = server_json.parent
+ installed: List[dict] = []
+ for entry in data.get("models") or []:
+ if not isinstance(entry, dict):
+ continue
+ rel = entry.get("path")
+ if not isinstance(rel, str) or not rel:
+ continue
+ path = Path(rel) if Path(rel).is_absolute() else base / rel
+ if _model_path_present(path):
+ installed.append({"id": str(entry.get("id") or rel), "rel": rel})
+ return installed
+
+
+def missing_model_install_guidance(audiocpp_dir: Path,
+ missing: List[dict]) -> List[Tuple[str, str]]:
+ """Map MISSING model entries to (display name, install id) pairs.
+
+ The install id is derived from each entry's configured path via the
+ catalog (see ``_install_id_by_path``); entries whose path matches no
+ catalog package are skipped (there is no ``model_manager_v2.py install``
+ command for them). Feeds ``_install_models`` for the "Download Missing
+ Models" action.
+ """
+ by_path = _install_id_by_path(audiocpp_dir)
+ guidance: List[Tuple[str, str]] = []
+ for item in missing:
+ install_id = by_path.get(item["rel"])
+ if install_id:
+ guidance.append((item["id"], install_id))
+ return guidance
+
+
def model_install_hints(audiocpp_dir: Path,
missing: List[dict]) -> List[str]:
"""Remediation lines for MISSING model entries (see missing_model_entries).
@@ -1331,12 +1490,7 @@ def model_install_hints(audiocpp_dir: Path,
carries the exact ``model_manager_v2.py install`` command; entries whose
directory matches no catalog package just name the path.
"""
- by_path: Dict[str, str] = {}
- try:
- for entry in load_model_catalog(audiocpp_dir):
- by_path[entry["default_path"]] = entry["install_id"]
- except (NotADirectoryError, OSError):
- pass
+ by_path = _install_id_by_path(audiocpp_dir)
hints: List[str] = []
for item in missing:
install_id = by_path.get(item["rel"])
@@ -1348,6 +1502,104 @@ def model_install_hints(audiocpp_dir: Path,
return hints
+def install_models(audiocpp_dir: Path,
+ guidance: List[Tuple[str, str]]) -> None:
+ """Download the (display name, install id) models via the helper script.
+
+ Runs ``model_manager_v2.py install`` for each de-duped install id in the
+ checkout, streaming to the console; a failing install is reported as a
+ warning and does not abort the rest. Used by the hub's "Download Missing
+ Models" action (see ``missing_model_install_guidance`` for the mapping).
+ """
+ _install_models(audiocpp_dir, guidance, download=True)
+
+
+def hand_install_guidance(audiocpp_dir: Path,
+ missing: List[dict]) -> str:
+ """Explain how to install MISSING model entries by hand.
+
+ Returns a multi-line message listing each missing model's id and the
+ path its files must be placed in (``rel``, resolved against the
+ AUDIOCPP_DIR checkout). Used when the missing models cannot be mapped to
+ a ``model_manager_v2.py install`` command, so the user still knows what
+ to download and where to put it.
+ """
+ lines = [
+ "None of the missing models map to a model_manager_v2.py install "
+ "command.",
+ "Download them by hand and place the files at these paths:",
+ ]
+ for item in missing:
+ lines.append(f" {item['id']} -> {item['rel']}")
+ lines.append(f"(paths are relative to {audiocpp_dir})")
+ return "\n".join(lines)
+
+
+def unused_installed_entries(server_json: Path,
+ new_paths: Set[str]) -> List[dict]:
+ """Return installed server.json entries whose path is not in NEW_PATHS.
+
+ The already-downloaded models (see ``installed_model_entries``) that the
+ new selection does not host any more — the candidates for the wizard's
+ "Delete unused models?" prompt. Entries whose files are not on disk are
+ never listed (there is nothing to delete).
+ """
+ return [entry for entry in installed_model_entries(server_json)
+ if entry["rel"] not in new_paths]
+
+
+def delete_model_files(server_json: Path, entries: List[dict]) -> int:
+ """Remove the on-disk model files for ENTRIES ({id, rel}) from disk.
+
+ Each entry's ``rel`` is resolved exactly like the server resolves it
+ (relative against ``server_json``'s directory; absolute paths honored),
+ then removed as a directory tree or a single file. Missing entries are
+ ignored. Returns the number of paths removed. Used by the wizard's
+ "Delete unused models?" step — the regenerated server.json already only
+ lists the kept models, so no entry cleanup is needed here.
+ """
+ base = server_json.parent
+ removed = 0
+ for item in entries:
+ rel = item.get("rel")
+ if not isinstance(rel, str) or not rel:
+ continue
+ path = Path(rel) if Path(rel).is_absolute() else base / rel
+ try:
+ if not path.exists():
+ continue
+ if path.is_dir():
+ shutil.rmtree(path, ignore_errors=True)
+ else:
+ path.unlink()
+ except OSError as exc:
+ print(f"[WARNING] Could not remove {path}: {exc}")
+ continue
+ print(f"[OK] Removed unused model {path}")
+ removed += 1
+ return removed
+
+
+def uninstall() -> int:
+ """Remove the audio.cpp backend entirely: stop its server, delete the checkout.
+
+ The checkout (``app/audio.cpp``, or wherever ``find_local_checkout``
+ resolves it) holds the built binary, the downloaded models, and the
+ server.json, so removing the directory uninstalls the backend. A running
+ server this tool started is stopped first (best-effort). Returns the exit
+ code.
+ """
+ servers.stop("audiocpp")
+ checkout = find_local_checkout()
+ if checkout is None:
+ print("[INFO] No audio.cpp checkout to remove.")
+ return 0
+ print(f"[INFO] Removing audio.cpp checkout {checkout}...")
+ shutil.rmtree(checkout, ignore_errors=True)
+ print("[OK] audio.cpp removed.")
+ return 0
+
+
def find_local_checkout() -> Optional[Path]:
"""Best-effort location of an audio.cpp checkout with model_specs.
@@ -1555,6 +1807,15 @@ def _execute(settings: dict, args: argparse.Namespace) -> int:
settings["host"], settings["port"], settings["backend"],
settings["lazy_load"], transcripts, write_prompt)
+ # Delete-unused cleanup (modify flow): remove the already-downloaded
+ # models the new selection dropped. The regenerated server.json already
+ # only lists the kept entries.
+ if settings.get("delete_unused"):
+ removed = delete_model_files(settings["output_path"],
+ settings["unused_entries"])
+ print(f"[OK] Deleted {removed} unused model "
+ f"{'entry' if removed == 1 else 'entries'} from disk.")
+
if len(settings["entry_ids"]) == 1:
_offer_config_model_id_sync(settings["entry_ids"][0],
settings["sync_model_ids"])
@@ -1806,7 +2067,9 @@ def build_parser() -> argparse.ArgumentParser:
"(default: base)")
parser.add_argument("--force", action="store_true",
help="Overwrite the output file (and prompt_text) "
- "without prompting")
+ "without prompting; in the TUI, start the "
+ "wizard fresh instead of loading the existing "
+ "server.json")
parser.add_argument("--download", action="store_true",
help="Run model_manager_v2.py install for each hosted "
"model automatically (default: print the commands "
@@ -1893,12 +2156,6 @@ def _detect_remote(managed: bool = False) -> Tuple[bool, dict]:
return False, {}
-configure_actions: List[ConfigureAction] = [
- ConfigureAction("Reconfigure audio.cpp (models, voices, server.json)",
- run_tui),
-]
-
-
def main() -> int:
parser = build_parser()
args = parser.parse_args()