diff --git a/docs/DESKTOP_RELEASE.md b/docs/DESKTOP_RELEASE.md index 9268145d..2ceaa4c6 100644 --- a/docs/DESKTOP_RELEASE.md +++ b/docs/DESKTOP_RELEASE.md @@ -69,7 +69,7 @@ We exclude every `nvidia.*` wheel from the Apple Silicon build (saves ~2 GB) and For us: - **Base DMG ships MPS + MLX path only.** Excludes `nvidia.*`, `triton`, `flash-attn`, anything CUDA-specific in the spec. -- **Optional pack: VoxCPM2** (requires CUDA). Not installed by default. Model Catalogue → Engines → "Install VoxCPM2" triggers download from our `voxcpm2-cu128-v1.tar.gz` release asset. +- **Optional pack: VoxCPM2** (requires CUDA). Not installed by default. Model Catalogue → "Install VoxCPM2" triggers download from our `voxcpm2-cu128-v1.tar.gz` release asset. - **Optional pack: pyannote** (HF-token gated). Default off. Settings → Speaker diarisation → "Enable" prompts for HF token, downloads + installs. - **Optional pack: MOSS-TTS-Nano.** Same pattern. diff --git a/docs/downloading-models.md b/docs/downloading-models.md index e641aa03..8aaac6fb 100644 --- a/docs/downloading-models.md +++ b/docs/downloading-models.md @@ -60,7 +60,7 @@ State is reported at **Settings → About** / `GET /system/info`: - `fast_download.xet_installed` — `hf_xet` present (true) - `fast_download.xet_active` — whether Xet actually drives downloads (false by default, because of `HF_HUB_DISABLE_XET`) -- the **⚡ fast download** badge in **Model Catalogue → Models** appears only when Xet +- the **⚡ fast download** badge in **Model Catalogue → Downloaded weights** appears only when Xet is *active*. The backend logs one line at startup, e.g. @@ -148,7 +148,7 @@ failed download at once. Caveats: ## Cancelling a download -**Model Catalogue → Models** lets you cancel an in-flight install. Cancellation stops +**Model Catalogue → Downloaded weights** lets you cancel an in-flight install. Cancellation stops further retries and clears the failure cooldown so you can restart immediately. A file that's already streaming finishes first — cancellation takes effect at the next retry boundary. @@ -162,6 +162,6 @@ takes effect at the next retry boundary. High-performance mode only helps if RAM and bandwidth are plentiful. - **"download finished but no model weights were found"** — the download was interrupted and left a partial snapshot. Delete the model in - **Model Catalogue → Models** and install it again. + **Model Catalogue → Downloaded weights** and install it again. - **Out of disk** — model sizes are shown in the catalog; free space or change the cache location with `HF_HOME` / `HF_HUB_CACHE`. diff --git a/docs/engines/README.md b/docs/engines/README.md index e73e7b8d..37e67f81 100644 --- a/docs/engines/README.md +++ b/docs/engines/README.md @@ -1,7 +1,7 @@ # Engine guides One page per engine: what it's for, what it needs, how to enable it, and its -quirks. Select engines in **Model Catalogue → Engines** (or quick-switch with +quirks. Select engines in **Model Catalogue** (or quick-switch with Ctrl/Cmd+E), or pin one with `OMNIVOICE_TTS_BACKEND` / `OMNIVOICE_ASR_BACKEND`. diff --git a/docs/engines/audio-cpp.md b/docs/engines/audio-cpp.md index de1f5d8e..32934937 100644 --- a/docs/engines/audio-cpp.md +++ b/docs/engines/audio-cpp.md @@ -80,11 +80,11 @@ instead of that dedicated-VRAM floor. ``` Alternatively set `OMNIVOICE_AUDIOCPP_DIR` to the directory containing it. -3. Restart VoiceStudio, open **Model Catalogue → Models**, find +3. Restart VoiceStudio, open **Model Catalogue → Downloaded weights**, find **Breeze-TTS-2 Q8_0 for audio.cpp**, review its research/non-commercial license note, and click **Install**. Generation never starts this ~4.73 GiB download automatically. -4. Pick `audiocpp` in **Model Catalogue → Engines**. The server starts +4. Pick `audiocpp` in **Model Catalogue**. The server starts lazily on first generate (`server.json` + `server.log` live under the app data `audiocpp/` directory). @@ -131,7 +131,7 @@ The managed loopback port may be taken. Check `server.log` next to ### `Breeze-TTS-2 ... not installed` or `package ... not completely installed` -Install the model from **Model Catalogue → Models**. If an interrupted install +Install the model from **Model Catalogue → Downloaded weights**. If an interrupted install left it incomplete, use **Reinstall** there. If the error persists after a complete reinstall, file an issue with the package listing. diff --git a/docs/engines/confucius4-tts.md b/docs/engines/confucius4-tts.md index d478b4b1..9b20875f 100644 --- a/docs/engines/confucius4-tts.md +++ b/docs/engines/confucius4-tts.md @@ -56,7 +56,7 @@ Then point VoiceStudio at the clone and restart: - **macOS/Linux:** `export OMNIVOICE_CONFUCIUS4_TTS_DIR=/path/to/Confucius4-TTS` - **Windows (PowerShell):** `[Environment]::SetEnvironmentVariable("OMNIVOICE_CONFUCIUS4_TTS_DIR","C:\path\to\Confucius4-TTS","User")` -Select **Confucius4-TTS** in Model Catalogue → Engines. The first synthesize triggers +Select **Confucius4-TTS** in Model Catalogue. The first synthesize triggers the weight downloads above, then generates. ### Optional overrides diff --git a/docs/engines/dots-tts.md b/docs/engines/dots-tts.md index ae57cb5b..8dc5d131 100644 --- a/docs/engines/dots-tts.md +++ b/docs/engines/dots-tts.md @@ -11,14 +11,14 @@ pins `transformers>=5.3` — the same isolation primitive used by [IndexTTS-2](indextts.md) and [MOSS-TTS-v1.5](moss-tts-v15.md). > **Opt-in, and never a default.** dots.tts is selected explicitly in -> **Model Catalogue → Engines** (or `OMNIVOICE_TTS_BACKEND=dots-tts`). It is not +> **Model Catalogue** (or `OMNIVOICE_TTS_BACKEND=dots-tts`). It is not > part of the default install. ## Platform support - **Linux / macOS only.** dots.tts's upstream package declares Linux and macOS classifiers and has **no Windows install path**. On Windows the - engine reports itself unavailable in **Model Catalogue → Engines** with a clear + engine reports itself unavailable in **Model Catalogue** with a clear reason — run VoiceStudio under WSL2 or use a Linux/macOS host. - **No MPS.** Upstream device selection is CUDA-or-CPU with no Metal branch, so on Apple Silicon the official package runs on **CPU** (slow but @@ -60,7 +60,7 @@ dots.tts is **not** bundled (large checkpoint + conflicting `transformers`). source ~/.zshrc ``` -5. Restart VoiceStudio. dots.tts appears in **Model Catalogue → Engines** with +5. Restart VoiceStudio. dots.tts appears in **Model Catalogue** with `available: true` and `isolation_mode: subprocess`. ## Venv resolution order diff --git a/docs/engines/faster-whisper-isolated.md b/docs/engines/faster-whisper-isolated.md index e48d506d..44e9169c 100644 --- a/docs/engines/faster-whisper-isolated.md +++ b/docs/engines/faster-whisper-isolated.md @@ -13,7 +13,7 @@ only the process boundary is new. ## Selecting it -- **Model Catalogue → Engines**, ASR tab → **Use** on the crash-isolated row, or +- **Model Catalogue**, ASR tab → **Use** on the crash-isolated row, or - pin it with `OMNIVOICE_ASR_BACKEND=faster-whisper-isolated`. It is never picked by auto-detect — it's an explicit opt-in escape hatch. @@ -56,6 +56,6 @@ Weights download on first load — see "sidecar crashed" error and the backend stays up — retry to respawn. - **cuDNN 8 is still required on CUDA** — same CTranslate2 requirement as the in-process engine. It's checked up front so a missing cuDNN 8 shows as - "unavailable" in Model Catalogue → Engines instead of a sidecar that + "unavailable" in Model Catalogue instead of a sidecar that silently fails every transcribe ([#1371](https://github.com/debpalash/VoiceStudio/issues/1371)). diff --git a/docs/engines/faster-whisper.md b/docs/engines/faster-whisper.md index 35023d5f..e5cf30c8 100644 --- a/docs/engines/faster-whisper.md +++ b/docs/engines/faster-whisper.md @@ -7,7 +7,7 @@ fallback on non-Apple machines. ## Selecting it -- **Model Catalogue → Engines**, ASR tab → **Use** on the Faster-Whisper row, or +- **Model Catalogue**, ASR tab → **Use** on the Faster-Whisper row, or - pin it with `OMNIVOICE_ASR_BACKEND=faster-whisper`. Auto-detect only picks it when [whisperx](whisperx.md) is unavailable. diff --git a/docs/engines/funasr.md b/docs/engines/funasr.md index 89a60c10..13f8c4bb 100644 --- a/docs/engines/funasr.md +++ b/docs/engines/funasr.md @@ -10,7 +10,7 @@ WhisperX remains the cross-platform default. ## Selecting it - Install it into the app venv: `uv pip install funasr`. -- Then **Model Catalogue → Engines**, ASR tab → **Use** on the FunASR row, or +- Then **Model Catalogue**, ASR tab → **Use** on the FunASR row, or `OMNIVOICE_ASR_BACKEND=funasr`. Auto-detect never picks it; it's an explicit opt-in. diff --git a/docs/engines/gpt-sovits.md b/docs/engines/gpt-sovits.md index 0aa1f36d..72df3139 100644 --- a/docs/engines/gpt-sovits.md +++ b/docs/engines/gpt-sovits.md @@ -24,7 +24,7 @@ HTTP. python api_v2.py -a 127.0.0.1 -p 9880 -c GPT_SoVITS/configs/tts_infer.yaml ``` -2. Select the engine via **Model Catalogue → Engines** or +2. Select the engine via **Model Catalogue** or `OMNIVOICE_TTS_BACKEND=gpt-sovits`. VoiceStudio marks the engine available only when the server responds diff --git a/docs/engines/indextts.md b/docs/engines/indextts.md index 34ca0fe3..07c2845c 100644 --- a/docs/engines/indextts.md +++ b/docs/engines/indextts.md @@ -14,7 +14,7 @@ runtime. Existing user-managed IndexTTS-2 environments remain supported. IndexTTS 2.5 is not bundled because its source environment and model weights require substantial disk space. -1. Open **Model Catalogue → Engines**. +1. Open **Model Catalogue**. 2. Expand **IndexTTS 2.5** and select **Install**. 3. Keep VoiceStudio open while source, dependencies, and weights download. @@ -97,7 +97,7 @@ IndexTTS-2 installations continue receiving their `target_tokens` control. ### Engine unavailable -Use **Model Catalogue → Engines → IndexTTS 2.5 → Install**. For a manual install, +Use **Model Catalogue → IndexTTS 2.5 → Install**. For a manual install, confirm that the configured directory contains: ```text diff --git a/docs/engines/kittentts.md b/docs/engines/kittentts.md index 943a30fe..1e5631a9 100644 --- a/docs/engines/kittentts.md +++ b/docs/engines/kittentts.md @@ -20,7 +20,7 @@ only — but a much faster and much smaller install. pip install kittentts ``` -Then select the engine via **Model Catalogue → Engines** or +Then select the engine via **Model Catalogue** or `OMNIVOICE_TTS_BACKEND=kittentts`. ## Voices diff --git a/docs/engines/mlx-audio.md b/docs/engines/mlx-audio.md index 07a40e97..a5b4a455 100644 --- a/docs/engines/mlx-audio.md +++ b/docs/engines/mlx-audio.md @@ -19,7 +19,7 @@ platforms never reports as available pip install mlx-audio ``` -Then select the engine via **Model Catalogue → Engines** or +Then select the engine via **Model Catalogue** or `OMNIVOICE_TTS_BACKEND=mlx-audio`. ## Model selection @@ -36,7 +36,7 @@ One backend hosts many models. The curated set: | `melotts` | `mlx-community/MeloTTS-English-v3-MLX` | lightweight VITS | | `outetts` | `mlx-community/Llama-OuteTTS-1.0-1B-4bit` | LM-based | -Pick a model in the **Model Catalogue → Engines** curated picker +Pick a model in the **Model Catalogue** curated picker ([#981](https://github.com/debpalash/VoiceStudio/issues/981)) or set `OMNIVOICE_MLX_AUDIO_MODEL` to either a curated key (`kokoro`) or any full HF repo id. The env var overrides the persisted UI choice. diff --git a/docs/engines/mlx-whisper.md b/docs/engines/mlx-whisper.md index e4fddd92..eb8abcde 100644 --- a/docs/engines/mlx-whisper.md +++ b/docs/engines/mlx-whisper.md @@ -11,7 +11,7 @@ on every Apple Silicon machine ## Selecting it - Nothing to do on Apple Silicon — auto-detect prefers it there. -- Or explicitly: **Model Catalogue → Engines**, ASR tab → **Use**, or +- Or explicitly: **Model Catalogue**, ASR tab → **Use**, or `OMNIVOICE_ASR_BACKEND=mlx-whisper`. ## Best at diff --git a/docs/engines/moonshine.md b/docs/engines/moonshine.md index 02d85f59..0f02754f 100644 --- a/docs/engines/moonshine.md +++ b/docs/engines/moonshine.md @@ -11,7 +11,7 @@ where even int8 whisper-large is too slow. - Install one of the runtimes into the app venv: `uv pip install moonshine-onnx` (lighter, tried first) or `moonshine-voice`. -- Then **Model Catalogue → Engines**, ASR tab → **Use** on the Moonshine row, +- Then **Model Catalogue**, ASR tab → **Use** on the Moonshine row, or `OMNIVOICE_ASR_BACKEND=moonshine`. Auto-detect never picks it; it's an explicit opt-in. diff --git a/docs/engines/moss-tts-nano.md b/docs/engines/moss-tts-nano.md index 286891a5..3c3f884b 100644 --- a/docs/engines/moss-tts-nano.md +++ b/docs/engines/moss-tts-nano.md @@ -25,7 +25,7 @@ cd MOSS-TTS-Nano uv pip install -e . ``` -Then select the engine via **Model Catalogue → Engines** or +Then select the engine via **Model Catalogue** or `OMNIVOICE_TTS_BACKEND=moss-tts-nano`. ## Model selection diff --git a/docs/engines/moss-tts-v15.md b/docs/engines/moss-tts-v15.md index 63666db6..db566ffd 100644 --- a/docs/engines/moss-tts-v15.md +++ b/docs/engines/moss-tts-v15.md @@ -13,7 +13,7 @@ interpreter, so MOSS runs behind `backend/services/subprocess_backend.py::SubprocessBackend`. > **Opt-in, and never a default.** MOSS-TTS-v1.5 is selected explicitly in -> **Model Catalogue → Engines** (or `OMNIVOICE_TTS_BACKEND=moss-tts-v15`). It is +> **Model Catalogue** (or `OMNIVOICE_TTS_BACKEND=moss-tts-v15`). It is > not part of the default install and does not change VoiceStudio's > out-of-the-box behaviour on any platform. @@ -75,7 +75,7 @@ into an isolated venv on demand. [Environment]::SetEnvironmentVariable("OMNIVOICE_MOSS_TTS_V15_DIR","$env:USERPROFILE\code\MOSS-TTS","User") ``` -5. Restart VoiceStudio. MOSS-TTS-v1.5 appears in **Model Catalogue → Engines** with +5. Restart VoiceStudio. MOSS-TTS-v1.5 appears in **Model Catalogue** with `available: true` and `isolation_mode: subprocess`. ## Venv resolution order diff --git a/docs/engines/nemo-parakeet.md b/docs/engines/nemo-parakeet.md index 0277b18e..0d7cbe99 100644 --- a/docs/engines/nemo-parakeet.md +++ b/docs/engines/nemo-parakeet.md @@ -29,7 +29,7 @@ instead — same model family, no NeMo dependency: Only meaningful if you've set up `nemo_toolkit[asr]` in a **separate, dedicated Python environment** that runs the backend: -- **Model Catalogue → Engines**, ASR tab → **Use** on the Parakeet TDT row, or +- **Model Catalogue**, ASR tab → **Use** on the Parakeet TDT row, or - `OMNIVOICE_ASR_BACKEND=nemo-parakeet`. Auto-detect never picks it; it's an explicit opt-in. diff --git a/docs/engines/omnivoice-gguf.md b/docs/engines/omnivoice-gguf.md index dd551df3..bf5ce7b8 100644 --- a/docs/engines/omnivoice-gguf.md +++ b/docs/engines/omnivoice-gguf.md @@ -32,10 +32,10 @@ against the same table (an F32 reference quant, ~3.2 GB, is override-only). ## Setup Nothing to install: installer and CI builds bundle the binary for your -platform. Select the engine via **Model Catalogue → Engines** or +platform. Select the engine via **Model Catalogue** or `OMNIVOICE_TTS_BACKEND=omnivoice-gguf`. The quant weights download on first use (see [downloading-models.md](../downloading-models.md)) — install them -ahead of time from **Model Catalogue → Models** if you want the first +ahead of time from **Model Catalogue → Downloaded weights** if you want the first generation to be quick; a long first render is the download, not a hang. **Source checkouts:** the repo ships zero-byte placeholders in `bin/` — real diff --git a/docs/engines/omnivoice-subprocess.md b/docs/engines/omnivoice-subprocess.md index e08b28a3..0fb4fcb0 100644 --- a/docs/engines/omnivoice-subprocess.md +++ b/docs/engines/omnivoice-subprocess.md @@ -32,7 +32,7 @@ lower call overhead. ## Selecting it -- **Model Catalogue → Engines** on CUDA, ROCm, or CPU, or +- **Model Catalogue** on CUDA, ROCm, or CPU, or - `OMNIVOICE_TTS_BACKEND=omnivoice-subprocess` The explicit engine is opt-in on CUDA, ROCm, and CPU. On Apple Silicon it is diff --git a/docs/engines/omnivoice.md b/docs/engines/omnivoice.md index 19546cfa..8e5bee58 100644 --- a/docs/engines/omnivoice.md +++ b/docs/engines/omnivoice.md @@ -39,7 +39,7 @@ quantized native binary with a much smaller memory footprint. OmniVoice is the default, so normally there is nothing to do. If you switched away and want it back: -- **Model Catalogue → Engines**, or +- **Model Catalogue**, or - set `OMNIVOICE_TTS_BACKEND=omnivoice`. The env var overrides the persisted UI choice. @@ -103,7 +103,7 @@ The env var overrides the persisted UI choice. above — switch to OmniVoice GGUF or close other GPU apps. - First generation is slow: the first call downloads multi-GB weights. To keep the first render quick, install the model ahead of time from - **Model Catalogue → Models** — a long first generate is almost always the + **Model Catalogue → Downloaded weights** — a long first generate is almost always the download, not a hang. - General install issues: [install/troubleshooting.md](../install/troubleshooting.md). diff --git a/docs/engines/openai-compatible-asr.md b/docs/engines/openai-compatible-asr.md index decd3cbe..ee63190e 100644 --- a/docs/engines/openai-compatible-asr.md +++ b/docs/engines/openai-compatible-asr.md @@ -9,7 +9,7 @@ model installation and compute. ## Setup -Everything lives on one screen — **Model Catalogue → Engines**, **ASR** tab: +Everything lives on one screen — **Model Catalogue**, **ASR** tab: 1. The **OpenAI-compatible (remote server)** row shows as unavailable until a server is configured. The config panel appears **below the engine diff --git a/docs/engines/parakeet-mlx.md b/docs/engines/parakeet-mlx.md index 22e43a71..fca24e92 100644 --- a/docs/engines/parakeet-mlx.md +++ b/docs/engines/parakeet-mlx.md @@ -12,10 +12,10 @@ Apple Silicon source installs since 0.3.22**. ## Selecting it -- **Model Catalogue → Engines**, ASR tab → **Use** on the Parakeet TDT v3 +- **Model Catalogue**, ASR tab → **Use** on the Parakeet TDT v3 (MLX) row, or `OMNIVOICE_ASR_BACKEND=parakeet-mlx`. - **Dictation prefers it automatically**: once the model weights are - installed (Model Catalogue → Models — the auto-pick never triggers a + installed (Model Catalogue → Downloaded weights — the auto-pick never triggers a download), live dictation/capture uses it whenever your system language is one of the 25 covered European languages. Other languages keep the multilingual Whisper engine, so dictation coverage never regresses. diff --git a/docs/engines/pockettts.md b/docs/engines/pockettts.md index 73f54e02..681b87e0 100644 --- a/docs/engines/pockettts.md +++ b/docs/engines/pockettts.md @@ -24,18 +24,18 @@ for this model. uv sync --extra pockettts ``` - (Or enable it from **Model Catalogue → Engines**.) + (Or enable it from **Model Catalogue**.) 2. **Accept the license in-app** ([#1306](https://github.com/debpalash/VoiceStudio/issues/1306)). The code is MIT and the weights are CC-BY-4.0, but the weights are **gated on HuggingFace** behind an access agreement with an acceptable-use clause. VoiceStudio surfaces this before first use: the engine stays unavailable - until you review and accept in **Model Catalogue → Engines → PocketTTS**. + until you review and accept in **Model Catalogue → PocketTTS**. You also need HuggingFace access to the gated repo (see [downloading-models.md](../downloading-models.md) for token setup). -3. Select the engine via **Model Catalogue → Engines** or +3. Select the engine via **Model Catalogue** or `OMNIVOICE_TTS_BACKEND=pockettts`. ## Platform notes @@ -77,7 +77,7 @@ for this model. ## Troubleshooting - "pocket_tts package not installed": run the `uv sync` above. -- "license not accepted": open **Model Catalogue → Engines → PocketTTS** +- "license not accepted": open **Model Catalogue → PocketTTS** and review/accept. - Timeouts on a slow connection: raise `OMNIVOICE_POCKETTTS_RECV_TIMEOUT_S` for the first (download-heavy) run. diff --git a/docs/engines/pytorch-whisper.md b/docs/engines/pytorch-whisper.md index 8029f074..815a327e 100644 --- a/docs/engines/pytorch-whisper.md +++ b/docs/engines/pytorch-whisper.md @@ -9,7 +9,7 @@ genuinely uses **AMD ROCm** GPUs, so auto-detect picks it on ROCm hosts ## Selecting it -- **Model Catalogue → Engines**, ASR tab → **Use** on the PyTorch Whisper +- **Model Catalogue**, ASR tab → **Use** on the PyTorch Whisper row, or `OMNIVOICE_ASR_BACKEND=pytorch-whisper`. - `OMNIVOICE_ASR_BACKEND=omnivoice` is accepted as a compatibility alias and selects this same PyTorch-native ASR path on ROCm hosts. diff --git a/docs/engines/sherpa-onnx-asr.md b/docs/engines/sherpa-onnx-asr.md index 95a131f6..fbc74dab 100644 --- a/docs/engines/sherpa-onnx-asr.md +++ b/docs/engines/sherpa-onnx-asr.md @@ -10,8 +10,8 @@ partials either way. ## Selecting it - Ensure `sherpa-onnx` is installed (`uv add sherpa-onnx` on source installs). -- Pick a dictation model in the app (Model Catalogue → Models lists the - selectable set below), or **Model Catalogue → Engines**, ASR tab → **Use**, or +- Pick a dictation model in the app (Model Catalogue → Downloaded weights lists the + selectable set below), or **Model Catalogue**, ASR tab → **Use**, or pin `OMNIVOICE_ASR_BACKEND=sherpa-onnx-asr`. - `OMNIVOICE_SHERPA_ASR_MODEL` selects the model — default `sherpa-whisper-tiny`. diff --git a/docs/engines/sherpa-onnx.md b/docs/engines/sherpa-onnx.md index 6cd09aab..5313b1d4 100644 --- a/docs/engines/sherpa-onnx.md +++ b/docs/engines/sherpa-onnx.md @@ -30,7 +30,7 @@ TTS model directory. export OMNIVOICE_SHERPA_MODEL=/path/to/model-dir ``` -4. Select the engine via **Model Catalogue → Engines** or +4. Select the engine via **Model Catalogue** or `OMNIVOICE_TTS_BACKEND=sherpa-onnx`. The directory must contain `model.onnx` and `tokens.txt`. Sherpa-ONNX ships diff --git a/docs/engines/supertonic3.md b/docs/engines/supertonic3.md index f1db1744..27f0a3c9 100644 --- a/docs/engines/supertonic3.md +++ b/docs/engines/supertonic3.md @@ -19,16 +19,16 @@ crashes and cold init never block the rest of VoiceStudio. uv sync --extra supertonic ``` - (Or enable it from **Model Catalogue → Engines**, which installs the + (Or enable it from **Model Catalogue**, which installs the pinned `supertonic` wheel for you.) 2. **Accept the license in-app.** First use is gated behind an explicit acceptance dialog: the inference SDK is MIT, but the model weights are **OpenRAIL-M**, which carries use restrictions. The engine stays - unavailable until you review and accept in **Model Catalogue → Engines → + unavailable until you review and accept in **Model Catalogue → Supertonic-3**. -3. Select the engine via **Model Catalogue → Engines** or +3. Select the engine via **Model Catalogue** or `OMNIVOICE_TTS_BACKEND=supertonic3`. The first synthesis cold-downloads ~400 MB of model weights, pinned to an @@ -66,7 +66,7 @@ log line. - "supertonic package not installed": run the `uv sync` above or enable from the Model Catalogue. -- "license not accepted": open **Model Catalogue → Engines → Supertonic-3** +- "license not accepted": open **Model Catalogue → Supertonic-3** and accept. - Other issues: [install/troubleshooting.md](../install/troubleshooting.md). diff --git a/docs/engines/voxcpm2.md b/docs/engines/voxcpm2.md index 556c9b5d..0bea7920 100644 --- a/docs/engines/voxcpm2.md +++ b/docs/engines/voxcpm2.md @@ -31,7 +31,7 @@ pip install "voxcpm>=2.0.3" That is a version **floor**, not a pin — an older install still works, but the engine logs an upgrade hint at load time. Then select the engine via -**Model Catalogue → Engines** or `OMNIVOICE_TTS_BACKEND=voxcpm2`. +**Model Catalogue** or `OMNIVOICE_TTS_BACKEND=voxcpm2`. ## Model selection diff --git a/docs/engines/whisperx.md b/docs/engines/whisperx.md index 95c7a628..19474540 100644 --- a/docs/engines/whisperx.md +++ b/docs/engines/whisperx.md @@ -8,7 +8,7 @@ prefers it wherever CTranslate2 can use the GPU. ## Selecting it -- **Model Catalogue → Engines**, ASR tab → **Use** on the WhisperX row, or +- **Model Catalogue**, ASR tab → **Use** on the WhisperX row, or - pin it with `OMNIVOICE_ASR_BACKEND=whisperx` (the env var always wins over the Settings pick; with neither set, auto-detect chooses per-hardware). diff --git a/docs/expressive-speech.md b/docs/expressive-speech.md index f2288d89..32f43657 100644 --- a/docs/expressive-speech.md +++ b/docs/expressive-speech.md @@ -129,7 +129,7 @@ the default engine's taxonomy, so free-text instruct currently needs the API Setup: clone + install [CosyVoice](https://github.com/FunAudioLLM/CosyVoice) (non-trivial: `git clone --recursive`, its requirements, SoX), then set `OMNIVOICE_COSYVOICE_MODEL` to the model directory and select it in -Model Catalogue → Engines. CUDA or CPU; MPS is unverified upstream. +Model Catalogue. CUDA or CPU; MPS is unverified upstream. ### VoxCPM2 (opt-in) diff --git a/docs/install/docker.md b/docs/install/docker.md index 1f5fe554..2ee4e9d4 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -215,7 +215,7 @@ ROCm container `omnivoice-studio-rocm` (CPU: `omnivoice-studio`, NVIDIA: name means torch can see the GPU.) That check alone isn't proof the app is using it: **Settings → Performance & Device** shows the device VoiceStudio actually resolved. -**Model Catalogue → Engines** should report both `omnivoice` and +**Model Catalogue** should report both `omnivoice` and `omnivoice-subprocess` as accelerated on ROCm, rather than a CPU-fallback warning. If it reads `cpu` while the command above prints `True`, the backend log line diff --git a/docs/install/macos.md b/docs/install/macos.md index 5be288d0..fc00f950 100644 --- a/docs/install/macos.md +++ b/docs/install/macos.md @@ -157,7 +157,7 @@ without the quarantine step. - **Apple Silicon (M-series):** VoiceStudio automatically picks the `mlx-whisper` and `mlx-audio` backends where available — these use the Apple Neural Engine and Metal Performance Shaders for ~2× the throughput of the CPU path. - Installing the **Parakeet TDT v3 (MLX)** model from **Model Catalogue → Models** + Installing the **Parakeet TDT v3 (MLX)** model from **Model Catalogue → Downloaded weights** additionally makes dictation/capture prefer the `parakeet-mlx` engine (25 European languages, word timestamps, ~2 GB unified memory) — it is never downloaded without that explicit install, and it is only auto-preferred when @@ -170,7 +170,7 @@ without the quarantine step. works only when pointed at a remote backend (**Settings → Sharing → Remote backend**). -The picker in **Model Catalogue → Engines** shows which backend is active. +The picker in **Model Catalogue** shows which backend is active. ## Hugging Face token (optional but recommended) diff --git a/docs/install/troubleshooting.md b/docs/install/troubleshooting.md index c6aec3dd..29dab25d 100644 --- a/docs/install/troubleshooting.md +++ b/docs/install/troubleshooting.md @@ -89,7 +89,7 @@ uv pip install --reinstall transformers ``` Or, as a quick workaround, switch ASR to **faster-whisper** in -**Model Catalogue → Models**. If it recurs, add the backend **`.venv`** to your +**Model Catalogue → Downloaded weights**. If it recurs, add the backend **`.venv`** to your antivirus exclusions (see §1). Newer builds classify this error and show the reinstall hint directly instead of a bare path + "try restarting". @@ -141,7 +141,7 @@ uv pip install --reinstall pytorch-lightning If it recurs, add the backend **`.venv`** to your antivirus exclusions (see §1). Since this fix landed the app also degrades gracefully: WhisperX is -marked unavailable (Model Catalogue → Engines shows why, with this repair command) +marked unavailable (Model Catalogue shows why, with this repair command) and dubbing automatically falls through to **faster-whisper** instead of failing outright. @@ -205,7 +205,7 @@ itself does not grant access. license and prohibited-use conditions, share the requested contact details, and accept the conditions. 2. Open **Settings → API Keys** and save a read token from that same account. -3. Open **Model Catalogue → Engines**, review and accept the PocketTTS terms locally, +3. Open **Model Catalogue**, review and accept the PocketTTS terms locally, then retry. VoiceStudio stores this acknowledgement only on your machine. ## 3. Gatekeeper quarantine on macOS @@ -408,7 +408,7 @@ Intel-Mac wheels, so this entry only applies to historical installs (see ## 10. Windows: `Could not locate cudnn_ops_infer64_8.dll` during transcription **Symptom:** on Windows + NVIDIA, transcription/dubbing fails and the backend -log shows `Could not locate cudnn_ops_infer64_8.dll`. Model Catalogue → Models shows +log shows `Could not locate cudnn_ops_infer64_8.dll`. Model Catalogue → Downloaded weights shows WhisperX or faster-whisper selected. On builds before this was fixed, the failure looked much worse than a failed @@ -444,7 +444,7 @@ uv pip install --no-deps --python .venv\Scripts\python.exe --target .venv\Lib\si (On Linux the target is `.venv/lib/pythonX.Y/site-packages/cudnn8_compat`.) Or sidestep cuDNN 8 entirely: switch the ASR backend to **PyTorch Whisper** in -**Model Catalogue → Models**. It runs on PyTorch's own stack (cuDNN 9, bundled with +**Model Catalogue → Downloaded weights**. It runs on PyTorch's own stack (cuDNN 9, bundled with torch) and needs no cuDNN-8 DLL — it loads its Whisper pipeline on demand (no extra env var). @@ -600,12 +600,12 @@ did was `generate:start (audio)`, a dub, or a dictation. **Fix — reduce ASR load (any one of these):** -1. **Pick a smaller ASR model / engine** in **Model Catalogue → Models** — e.g. +1. **Pick a smaller ASR model / engine** in **Model Catalogue → Downloaded weights** — e.g. faster-whisper **medium** or **small**, instead of large-v3. Biggest win on low-VRAM GPUs. 2. **Free VRAM**: **Flush the TTS model** before dubbing so ASR isn't competing for memory (top toolbar → Flush → "Unload all + flush", or per-model from - Model Catalogue → Models — see [Flush caches / Unload resident model](../performance.md#flush-caches--unload-resident-model) + Model Catalogue → Downloaded weights — see [Flush caches / Unload resident model](../performance.md#flush-caches--unload-resident-model) for exactly what it frees and the API equivalents for scripts), or 3. **Run ASR on CPU** (slower but reliable) if your GPU is small. 4. **Test with a 10-second clip** first — if that returns quickly, it confirms a @@ -675,7 +675,7 @@ hammering — on a 1-worker machine, concurrent requests serialize by design. recovering the underlying hang — the wedged thread keeps its VRAM until the app exits. The error message will then recommend switching the ASR engine to **Faster-Whisper (crash-isolated subprocess)** (`faster-whisper-isolated`) in -**Model Catalogue → Engines**: it runs transcription in a separate process that can be +**Model Catalogue**: it runs transcription in a separate process that can be force-killed to reclaim a hung transcribe *and* its VRAM, at a small per-call overhead. It reuses your existing faster-whisper install (nothing extra to download). VoiceStudio never switches engines automatically — this stays your diff --git a/docs/migration/real-time-voice-cloning.md b/docs/migration/real-time-voice-cloning.md index 956f5c9f..89b48a2f 100644 --- a/docs/migration/real-time-voice-cloning.md +++ b/docs/migration/real-time-voice-cloning.md @@ -25,7 +25,7 @@ on your own machine: no accounts, no API keys, no cloud. |---|---|---| | Speaker encoder + reference utterance | Reference clip in the **Voice Clone** workflow ("From audio") | No separate embedding step — zero-shot engines condition on the clip directly | | Saved speaker embeddings (`.npy`) | **Voice Profiles** — save a clone once, reuse it everywhere | Exportable as portable `.ovsvoice` bundles | -| Synthesizer + vocoder choice (Tacotron 2 · WaveRNN / Griffin-Lim) | **TTS engine choice** — Model Catalogue → Engines | 14 engines, from CPU-realtime to GPU heavyweights; per-engine GPU preflight | +| Synthesizer + vocoder choice (Tacotron 2 · WaveRNN / Griffin-Lim) | **TTS engine choice** — Model Catalogue | 14 engines, from CPU-realtime to GPU heavyweights; per-engine GPU preflight | | The Toolbox GUI (`demo_toolbox.py`) | The app itself | Record or drop a clip, type text, synthesize — same loop, no `python demo_toolbox.py` | | `demo_cli.py` / scripting your own pipeline | Local REST API (OpenAI-compatible, `http://localhost:3900/v1`), `omnivoice-infer` CLI, MCP server | See the [API section of the README](../../README.md#openai-api) | | Training your own encoder / synthesizer / vocoder | Partial — see ["What RTVC did that VoiceStudio doesn't"](#what-rtvc-did-that-omnivoice-doesnt) | Fine-tuning the bundled model is documented; RTVC-style three-stage research training is not what this project is | diff --git a/docs/performance.md b/docs/performance.md index 347ff69e..2c41e8aa 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -38,7 +38,7 @@ Before touching any knob, check these — they account for most slowness reports - **Settings → About → Run self-check** (the `/system/diagnose` endpoint) warns explicitly: *"cpu (no GPU acceleration detected)"* with a hint about drivers. - - **Model Catalogue → Engines** shows a routing badge per engine — "GPU active", + - **Model Catalogue** shows a routing badge per engine — "GPU active", "CPU fallback", or "CPU" — with the *reason* shown as small text under the badge (full text on hover). Note: **GPU acceleration on Windows is NVIDIA/CUDA-only** — AMD and Intel @@ -183,7 +183,7 @@ drain, or restart the backend, and then Flush. - **Unload all + flush** — the above **plus** fully unloads the resident TTS model. Frees the most memory; the next generation pays the ~8 s reload. -- **Model Catalogue → Models** — rows whose weights are resident right now show an +- **Model Catalogue → Downloaded weights** — rows whose weights are resident right now show an "In memory" badge with the same per-model **Unload** button. **From a script** (the local API on port 3900), the same operations: @@ -234,7 +234,7 @@ CPU ASR, the crash-isolated ASR engine). to 3-4 concurrent generations (API/batch workloads); ≤10 GB deliberately serializes. - **CPU-only**: expect ~2x slower than MPS, more against CUDA. Prefer the - smaller/faster engines (see Model Catalogue → Engines) and short reference clips. + smaller/faster engines (see Model Catalogue) and short reference clips. ## Measuring instead of guessing diff --git a/frontend/src/components/EngineQuickSwitch.jsx b/frontend/src/components/EngineQuickSwitch.jsx index c128734e..03227b93 100644 --- a/frontend/src/components/EngineQuickSwitch.jsx +++ b/frontend/src/components/EngineQuickSwitch.jsx @@ -218,7 +218,7 @@ export default function EngineQuickSwitch({ type="button" onClick={() => { setOpen(false); - useAppStore.getState().openCatalogue({ pane: 'engines', family }); + useAppStore.getState().openCatalogue({ family }); }} className="mt-[3px] flex items-center gap-[3px] border-0 bg-transparent px-[7px] py-[5px] text-left text-[11px] text-[color:var(--chrome-fg-muted)] hover:text-[color:var(--chrome-fg)]" > diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index 4c9072f7..d1a4e11c 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -517,9 +517,7 @@ export default function Header({ className="flex items-center gap-1 border-0 bg-transparent p-2 text-xs text-[var(--chrome-fg-muted)] cursor-pointer hover:text-[var(--chrome-fg)]" onClick={() => { setFlushOpen(false); - useAppStore - .getState() - .openCatalogue({ pane: 'engines', family: engineFamily }); + useAppStore.getState().openCatalogue({ family: engineFamily }); }} > {t('header.label_catalogue')} diff --git a/frontend/src/components/catalogue/SetupSummary.jsx b/frontend/src/components/catalogue/SetupSummary.jsx new file mode 100644 index 00000000..dabce062 --- /dev/null +++ b/frontend/src/components/catalogue/SetupSummary.jsx @@ -0,0 +1,236 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'react-hot-toast'; +import { Download, RefreshCw } from 'lucide-react'; +import { apiJson } from '../../api/client'; +import { useEngines, useInstallModel, useRecommendations } from '../../api/hooks'; +import { useAppStore } from '../../store'; +import { Badge, Button } from '../../ui'; + +/** + * SetupSummary — the Model Catalogue's first screen: what the app will use + * right now for speech, transcription, dictation and the language model, one + * line each, and a "Change" that jumps to that family's engine list below. + * + * Most users take the recommended engines and never come back; this is the + * view built for them. Everything that used to compete for attention on the + * old two-pane page (engine matrix, model store, stats) now sits under it, + * reached only through Change. + */ +const DEVICE_LABEL = { cuda: 'CUDA', mps: 'MPS', rocm: 'ROCm', cpu: 'CPU', directml: 'DirectML' }; + +const STATUS_TONE = { + ready: 'success', + cpu: 'warn', + setup: 'warn', + off: 'neutral', + missing: 'warn', +}; + +/** Pure: reduce one family's /engines payload to {name, device, status}. */ +export function summarizeFamily(family, familyData) { + const active = familyData?.active; + const backends = familyData?.backends || []; + if (family === 'llm' && (!active || active === 'off')) { + return { name: null, device: null, status: 'off' }; + } + const entry = backends.find((b) => b.id === active); + const name = entry?.display_name || active || null; + if (!entry || entry.available === false) return { name, device: null, status: 'setup' }; + const routing = entry.routing_status; + const device = + routing === 'n/a' ? 'remote' : DEVICE_LABEL[entry.effective_device] || entry.effective_device; + return { + name, + device: device || null, + status: routing === 'cpu_fallback' ? 'cpu' : 'ready', + }; +} + +/** Pure: the dictation line from /dictation/models + the model_id pref. */ +export function summarizeDictation(data, modelId) { + const models = Array.isArray(data?.models) ? data.models : []; + const selected = + models.find((m) => m.id === modelId) || models.find((m) => m.recommended) || models[0]; + if (!selected) return { name: null, device: null, status: 'setup' }; + if (data?.engine_available === false) + return { name: selected.label, device: null, status: 'setup' }; + return { + name: selected.label, + device: DEVICE_LABEL.cpu, + status: selected.installed ? 'ready' : 'missing', + }; +} + +export default function SetupSummary({ onChange }) { + const { t } = useTranslation(); + const enginesQuery = useEngines(); + const recoQuery = useRecommendations(); + const installMutation = useInstallModel(); + const [installing, setInstalling] = useState(false); + + const modelId = useAppStore((s) => s.dictationModelId); + const loadPrefs = useAppStore((s) => s.loadDictationPrefs); + const dictationLoaded = useAppStore((s) => s.dictationLoaded); + useEffect(() => { + if (!dictationLoaded) loadPrefs?.(); + }, [dictationLoaded, loadPrefs]); + // Same query key as the quick-menu picker so both read one cache entry. + const dictationQuery = useQuery({ + queryKey: ['dictation-models'], + queryFn: () => apiJson('/dictation/models'), + staleTime: 10_000, + retry: false, + }); + + const engines = enginesQuery.data; + const rows = useMemo( + () => [ + { + key: 'speech', + family: 'tts', + label: t('header.speech'), + ...summarizeFamily('tts', engines?.tts), + }, + { + key: 'transcription', + family: 'asr', + label: t('projects.transcription'), + ...summarizeFamily('asr', engines?.asr), + }, + { + key: 'dictation', + family: 'asr', + label: t('settings.dictation'), + ...summarizeDictation(dictationQuery.data, modelId), + }, + { + key: 'llm', + family: 'llm', + label: t('models.role_llm'), + ...summarizeFamily('llm', engines?.llm), + }, + ], + [t, engines, dictationQuery.data, modelId], + ); + + const statusLabel = { + ready: t('models.ready_badge'), + cpu: t('catalogue.status_cpu'), + setup: t('catalogue.status_setup'), + off: t('catalogue.status_off'), + missing: t('models.not_installed'), + }; + + const reco = recoQuery.data; + const missing = reco?.models?.filter((m) => !m.installed) || []; + const installRest = async () => { + if (missing.length === 0) return; + setInstalling(true); + try { + await Promise.all(missing.map((m) => installMutation.mutateAsync(m.repo_id))); + toast.success(t('models.started_downloading', { count: missing.length })); + } catch (e) { + toast.error(t('models.install_failed', { message: e?.message || e })); + } finally { + setInstalling(false); + } + }; + + return ( +
+

+ {t('catalogue.summary_title')} +

+
+ {rows.map((row) => ( +
+
+ {row.label} +
+
+ {enginesQuery.isLoading && !engines && row.key !== 'dictation' ? ( + {t('common.loading')} + ) : ( + <> + {row.name && ( + + {row.name} + + )} + {row.device && ( + <> + {row.name && } + + {row.device === 'remote' ? t('catalogue.device_remote') : row.device} + + + )} + {(row.name || row.device) && } + + {statusLabel[row.status]} + + + )} +
+ +
+ ))} +
+ {reco && ( +
+ + {reco.all_installed || missing.length === 0 + ? t('models.reco_installed_for', { device: reco.device?.label }) + : t('catalogue.reco_line', { + installed: reco.models.length - missing.length, + total: reco.models.length, + device: reco.device?.label, + remaining: reco.download_gb_remaining, + })} + + {missing.length > 0 && ( + + )} +
+ )} +
+ ); +} diff --git a/frontend/src/components/catalogue/SetupSummary.test.jsx b/frontend/src/components/catalogue/SetupSummary.test.jsx new file mode 100644 index 00000000..83a966c3 --- /dev/null +++ b/frontend/src/components/catalogue/SetupSummary.test.jsx @@ -0,0 +1,208 @@ +import React from 'react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent, waitFor, within } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { I18nextProvider } from 'react-i18next'; +import i18n from '../../i18n'; + +vi.mock('react-hot-toast', () => ({ toast: { success: vi.fn(), error: vi.fn() } })); + +let enginesData = null; +let recoData = null; +const installMutate = vi.fn().mockResolvedValue({}); +vi.mock('../../api/hooks', () => ({ + useEngines: () => ({ data: enginesData, isLoading: false }), + useRecommendations: () => ({ data: recoData }), + useInstallModel: () => ({ mutateAsync: installMutate }), +})); +vi.mock('../../api/client', () => ({ apiJson: vi.fn() })); + +import { apiJson } from '../../api/client'; +import { useAppStore } from '../../store'; +import SetupSummary, { summarizeDictation, summarizeFamily } from './SetupSummary'; + +const t = i18n.t.bind(i18n); + +const ENGINES = { + tts: { + active: 'indextts', + backends: [ + { + id: 'indextts', + display_name: 'IndexTTS 2', + available: true, + effective_device: 'mps', + routing_status: 'accelerated', + }, + ], + }, + asr: { + active: 'faster-whisper', + backends: [ + { + id: 'faster-whisper', + display_name: 'Faster-Whisper', + available: true, + effective_device: 'cpu', + routing_status: 'cpu_fallback', + }, + ], + }, + llm: { active: 'off', backends: [] }, +}; + +const DICTATION = { + engine_available: true, + models: [ + { id: 'sherpa-whisper-tiny', label: 'Whisper Tiny', installed: true, recommended: true }, + { id: 'sherpa-parakeet-tdt-v3', label: 'Parakeet TDT v3', installed: false }, + ], +}; + +function mount(props = {}) { + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + return render( + + + + + , + ); +} + +describe('SetupSummary', () => { + beforeEach(() => { + vi.clearAllMocks(); + enginesData = ENGINES; + recoData = null; + apiJson.mockResolvedValue(DICTATION); + useAppStore.setState({ dictationModelId: 'sherpa-whisper-tiny', dictationLoaded: true }); + }); + + it('shows one line per capability: engine, device, and a plain status word', async () => { + mount(); + const speech = screen.getByTestId('setup-row-speech'); + expect(within(speech).getByTestId('setup-name-speech')).toHaveTextContent('IndexTTS 2'); + expect(speech).toHaveTextContent('MPS'); + expect(within(speech).getByTestId('setup-status-speech')).toHaveTextContent( + t('models.ready_badge'), + ); + + const asr = screen.getByTestId('setup-row-transcription'); + expect(asr).toHaveTextContent('Faster-Whisper'); + expect(within(asr).getByTestId('setup-status-transcription')).toHaveTextContent( + t('catalogue.status_cpu'), + ); + + const llm = screen.getByTestId('setup-row-llm'); + expect(within(llm).getByTestId('setup-status-llm')).toHaveTextContent( + t('catalogue.status_off'), + ); + expect(within(llm).queryByTestId('setup-name-llm')).toBeNull(); + + await waitFor(() => + expect(screen.getByTestId('setup-name-dictation')).toHaveTextContent('Whisper Tiny'), + ); + expect(screen.getByTestId('setup-status-dictation')).toHaveTextContent(t('models.ready_badge')); + }); + + it('flags an unavailable active engine and an undownloaded dictation model', async () => { + enginesData = { + ...ENGINES, + tts: { active: 'gone', backends: [{ id: 'gone', display_name: 'Gone', available: false }] }, + }; + useAppStore.setState({ dictationModelId: 'sherpa-parakeet-tdt-v3' }); + mount(); + expect(screen.getByTestId('setup-status-speech')).toHaveTextContent( + t('catalogue.status_setup'), + ); + await waitFor(() => + expect(screen.getByTestId('setup-status-dictation')).toHaveTextContent( + t('models.not_installed'), + ), + ); + }); + + it('Change hands the row’s engine family to the page (dictation lives under ASR)', () => { + const onChange = vi.fn(); + mount({ onChange }); + for (const [key, family] of [ + ['speech', 'tts'], + ['transcription', 'asr'], + ['dictation', 'asr'], + ['llm', 'llm'], + ]) { + fireEvent.click(screen.getByTestId(`setup-change-${key}`)); + expect(onChange).toHaveBeenLastCalledWith(family); + } + }); + + it('summarises the recommended preset in one line and installs the rest in one click', async () => { + recoData = { + device: { label: 'Apple M2 Pro' }, + all_installed: false, + download_gb_remaining: 2.4, + models: [ + { repo_id: 'a/have', installed: true, size_gb: 1 }, + { repo_id: 'b/need', installed: false, size_gb: 2.4 }, + ], + }; + mount(); + const line = screen.getByTestId('setup-reco'); + expect(line).toHaveTextContent( + t('catalogue.reco_line', { installed: 1, total: 2, device: 'Apple M2 Pro', remaining: 2.4 }), + ); + fireEvent.click(screen.getByTestId('setup-install-rest')); + await waitFor(() => expect(installMutate).toHaveBeenCalledWith('b/need')); + expect(installMutate).toHaveBeenCalledTimes(1); + }); + + it('drops the install button once the preset is complete', () => { + recoData = { + device: { label: 'Apple M2 Pro' }, + all_installed: true, + total_gb: 3.4, + models: [{ repo_id: 'a/have', installed: true, size_gb: 1 }], + }; + mount(); + expect(screen.getByTestId('setup-reco')).toHaveTextContent( + t('models.reco_installed_for', { device: 'Apple M2 Pro' }), + ); + expect(screen.queryByTestId('setup-install-rest')).toBeNull(); + }); +}); + +describe('summarizeFamily / summarizeDictation', () => { + it('maps routing and availability to the four status words', () => { + expect(summarizeFamily('tts', ENGINES.tts)).toEqual({ + name: 'IndexTTS 2', + device: 'MPS', + status: 'ready', + }); + expect(summarizeFamily('asr', ENGINES.asr).status).toBe('cpu'); + expect(summarizeFamily('llm', ENGINES.llm)).toEqual({ + name: null, + device: null, + status: 'off', + }); + expect(summarizeFamily('tts', { active: 'x', backends: [] })).toEqual({ + name: 'x', + device: null, + status: 'setup', + }); + expect( + summarizeFamily('asr', { + active: 'openai-compat', + backends: [{ id: 'openai-compat', available: true, routing_status: 'n/a' }], + }).device, + ).toBe('remote'); + }); + + it('falls back to the recommended dictation model and reports engine trouble', () => { + expect(summarizeDictation(DICTATION, 'unknown').name).toBe('Whisper Tiny'); + expect(summarizeDictation({ ...DICTATION, engine_available: false }, null).status).toBe( + 'setup', + ); + expect(summarizeDictation(null, null).status).toBe('setup'); + }); +}); diff --git a/frontend/src/components/settings/CataloguePointer.jsx b/frontend/src/components/settings/CataloguePointer.jsx deleted file mode 100644 index 5d7ae66f..00000000 --- a/frontend/src/components/settings/CataloguePointer.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import { Boxes } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; -import { useAppStore } from '../../store'; -import { Button } from '../../ui'; -import { SETTINGS_SECTION_SURFACE } from './primitives'; - -/** - * CataloguePointer — the Settings-side signpost for what moved to the Model - * Catalogue workspace. - * - * Engine selection and the model store are now a top-level workspace - * (`pages/ModelCatalogue.jsx`), not Settings categories. The categories stay in - * the Settings IA on purpose: "Engines" / "Models" is where users (and every - * doc written before the move) look first, and search still has to find them. - * They render this instead of the panels, so the trip is one click, not a hunt. - * - * @param {'engines'|'models'} area which pane to open - */ -export default function CataloguePointer({ area = 'engines' }) { - const { t } = useTranslation(); - const openCatalogue = useAppStore((s) => s.openCatalogue); - - return ( -
- -
-

- {t('catalogue.moved_title')} -

-

- {t(area === 'models' ? 'catalogue.moved_models_body' : 'catalogue.moved_engines_body')} -

-
- -
- ); -} diff --git a/frontend/src/components/settings/CataloguePointer.test.jsx b/frontend/src/components/settings/CataloguePointer.test.jsx deleted file mode 100644 index 0f42c293..00000000 --- a/frontend/src/components/settings/CataloguePointer.test.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import React from 'react'; -import { render, screen, fireEvent, act } from '@testing-library/react'; - -import { useAppStore } from '../../store'; -import CataloguePointer from './CataloguePointer'; - -describe('CataloguePointer', () => { - beforeEach(() => { - act(() => { - useAppStore.setState({ mode: 'settings', pendingCatalogueTab: null }); - }); - }); - - it('sends the Engines category to the catalogue engines pane', () => { - render(); - fireEvent.click(screen.getByRole('button', { name: 'Open Engines' })); - expect(useAppStore.getState().mode).toBe('catalogue'); - expect(useAppStore.getState().pendingCatalogueTab).toBe('engines'); - }); - - it('sends the Models category to the catalogue models pane', () => { - render(); - fireEvent.click(screen.getByRole('button', { name: 'Open Models' })); - expect(useAppStore.getState().mode).toBe('catalogue'); - expect(useAppStore.getState().pendingCatalogueTab).toBe('models'); - }); -}); diff --git a/frontend/src/components/settings/LLMProvidersPanel.jsx b/frontend/src/components/settings/LLMProvidersPanel.jsx index 9a41b351..c73319aa 100644 --- a/frontend/src/components/settings/LLMProvidersPanel.jsx +++ b/frontend/src/components/settings/LLMProvidersPanel.jsx @@ -236,7 +236,7 @@ export default function LLMProvidersPanel() { - )} - {!hfTokenSet && hfExpanded && ( -
- setHfToken(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') saveHfToken(); - if (e.key === 'Escape') setHfExpanded(false); - }} - autoFocus - /> - - { - e.preventDefault(); - openExternal('https://huggingface.co/settings/tokens'); - }} - title="Open huggingface.co/settings/tokens" - > - {t('models.get_token')}→ - -
- )} - {hfTokenSet && ( - - ✓ - - )} - - - - - - -
- +
+
+
- -
- {sections.map((group) => ( - - ))} -
- {/* Global empty state — every section filtered out. Same actionable - "Clear filters" affordance the table-level empty state used to carry. */} - {sections.length === 0 && allModels.length > 0 && ( -
- {t('models.no_matches')} - -
+ {info?.fast_download?.xet_enabled && ( + + )} - - {/* Downloaded previews belong next to downloaded models: same question - ("what has this install fetched?"), same tab. */} - - + + +
+ + + +
+ {sections.map((group) => ( + + ))} +
+ {/* Global empty state — every section filtered out. Same actionable + "Clear filters" affordance the table-level empty state carries. */} + {sections.length === 0 && allModels.length > 0 && ( +
+ {t('models.no_matches')} + +
+ )} + ); } diff --git a/frontend/src/components/settings/SettingsSidebar.test.jsx b/frontend/src/components/settings/SettingsSidebar.test.jsx index d9b58df8..9cabc910 100644 --- a/frontend/src/components/settings/SettingsSidebar.test.jsx +++ b/frontend/src/components/settings/SettingsSidebar.test.jsx @@ -77,7 +77,7 @@ describe('SettingsSidebar — keyboard navigation', () => { it('is a single tab stop: only the active category is tabbable', () => { render( {}} />); expect(screen.getByTestId('settings-nav-appearance')).toHaveAttribute('tabindex', '0'); - expect(screen.getByTestId('settings-nav-engines')).toHaveAttribute('tabindex', '-1'); + expect(screen.getByTestId('settings-nav-dictation')).toHaveAttribute('tabindex', '-1'); }); it('ArrowDown/ArrowUp move selection, crossing group boundaries', () => { @@ -86,12 +86,12 @@ describe('SettingsSidebar — keyboard navigation', () => { // the first item of Voice & Engines — arrowing must not stop at the seam. render(); fireEvent.keyDown(nav(), { key: 'ArrowDown' }); - expect(onSelect).toHaveBeenCalledWith('engines'); + expect(onSelect).toHaveBeenCalledWith('dictation'); onSelect.mockClear(); - render(); + render(); fireEvent.keyDown(screen.getAllByTestId('settings-nav-scroll')[1], { key: 'ArrowUp' }); - expect(onSelect).toHaveBeenCalledWith('engines'); + expect(onSelect).toHaveBeenCalledWith('dictation'); }); it('Home/End jump to the first and last visible category', () => { diff --git a/frontend/src/components/settings/StorageUsagePanel.jsx b/frontend/src/components/settings/StorageUsagePanel.jsx index e5c88002..76cd259c 100644 --- a/frontend/src/components/settings/StorageUsagePanel.jsx +++ b/frontend/src/components/settings/StorageUsagePanel.jsx @@ -132,7 +132,7 @@ function SmallButton({ children, onClick, title, testId, disabled }) { export default function StorageUsagePanel() { const { t } = useTranslation(); - const openSettingsTab = useAppStore((s) => s.openSettingsTab); + const openCatalogue = useAppStore((s) => s.openCatalogue); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); @@ -379,7 +379,7 @@ export default function StorageUsagePanel() { {cat.id === 'hf_cache' && ( openSettingsTab('models')} + onClick={() => openCatalogue()} title={t('settings.storage_manage_models_hint', { defaultValue: 'Reclaim space by removing models in the Model Store', })} diff --git a/frontend/src/components/settings/models/sections.js b/frontend/src/components/settings/models/sections.js index a6d0abcb..ba3e4bfc 100644 --- a/frontend/src/components/settings/models/sections.js +++ b/frontend/src/components/settings/models/sections.js @@ -68,3 +68,31 @@ export function groupModels(models, query) { }; }); } + +/** Which catalog sections belong to each engine family (one family's slice of the catalog). */ +export const FAMILY_SECTIONS = { + tts: ['tts'], + asr: ['asr', 'dictation', 'diarisation'], + llm: [], +}; + +/** + * Pure: narrow the device-wide recommendation preset to one family so the + * banner under a family's engines only talks about that family's weights. + * Totals are recomputed from the subset; null when nothing applies. + */ +export function scopeReco(reco, family) { + if (!reco || !family) return reco; + const keep = FAMILY_SECTIONS[family] || []; + const models = (reco.models || []).filter((m) => keep.includes(modelSectionKey(m))); + if (models.length === 0) return null; + const missing = models.filter((m) => !m.installed); + const gb = (xs) => Math.round(xs.reduce((sum, m) => sum + (m.size_gb || 0), 0) * 10) / 10; + return { + ...reco, + models, + all_installed: missing.length === 0, + total_gb: gb(models), + download_gb_remaining: gb(missing), + }; +} diff --git a/frontend/src/components/settings/settingsCategories.jsx b/frontend/src/components/settings/settingsCategories.jsx index 12f5f9e2..55edeab6 100644 --- a/frontend/src/components/settings/settingsCategories.jsx +++ b/frontend/src/components/settings/settingsCategories.jsx @@ -90,49 +90,6 @@ export const GROUPS = [ labelKey: 'settings.group_voice', defaultLabel: 'Voice & Engines', items: [ - { - id: 'engines', - labelKey: 'settings.engines', - defaultLabel: 'Engines', - icon: Plug, - // Engine selection lives in the Model Catalogue workspace now; the - // category stays so the old name still finds it (it renders a pointer). - keywords: [ - 'engine', - 'catalogue', - 'catalog', - 'model catalogue', - 'default engine', - 'tts engine', - 'indextts', - 'cosyvoice', - 'compatibility', - 'gpu', - 'asr', - 'transcription', - 'whisper', - 'openai-compatible', - 'remote asr', - ], - }, - { - id: 'models', - labelKey: 'settings.models', - defaultLabel: 'Models', - icon: Cpu, - restart: true, - keywords: [ - 'model', - 'download', - 'cache directory', - 'models directory', - 'hugging face mirror', - 'hf_endpoint', - 'catalogue', - 'catalog', - 'model store', - ], - }, { id: 'dictation', labelKey: 'settings.dictation', @@ -231,6 +188,8 @@ export const GROUPS = [ }, { id: 'storage', + // Hosts a restart-bound panel (models directory / HF mirror). + restart: true, labelKey: 'settings.storage', defaultLabel: 'Storage', icon: HardDrive, @@ -251,6 +210,13 @@ export const GROUPS = [ 'free space', 'disk space', 'model cache size', + 'models directory', + 'cache directory', + 'model store', + 'catalogue', + 'catalog', + 'download', + 'voice previews', 'engine venvs', 'temp files', 'clear logs', @@ -279,6 +245,8 @@ export const GROUPS = [ }, { id: 'network', + // Hosts a restart-bound panel (models directory / HF mirror). + restart: true, labelKey: 'settings.network', defaultLabel: 'Network', icon: Wifi, @@ -297,6 +265,9 @@ export const GROUPS = [ // settingsCategories.test.jsx like Models / Performance / Sharing. restart: true, keywords: [ + 'hugging face mirror', + 'hf_endpoint', + 'mirror', 'ffmpeg', 'ffprobe', 'ffmpeg path', diff --git a/frontend/src/components/settings/settingsCategories.test.jsx b/frontend/src/components/settings/settingsCategories.test.jsx index 4dbd4c01..ae1b5f4d 100644 --- a/frontend/src/components/settings/settingsCategories.test.jsx +++ b/frontend/src/components/settings/settingsCategories.test.jsx @@ -62,8 +62,8 @@ describe('restart flag ↔ RestartBadge lockstep', () => { // restart: true, or the sidebar ↻ glyph / header badge contract breaks // (that drift is exactly how Network shipped without its glyph). const PANEL_CATEGORY = { - 'StoragePanel.jsx': 'models', - 'HFMirrorPanel.jsx': 'models', + 'StoragePanel.jsx': 'storage', + 'HFMirrorPanel.jsx': 'network', 'RemoteBackendPanel.jsx': 'sharing', 'AudioToolsPanel.jsx': 'audio-tools', 'PerformancePanel.jsx': 'performance', diff --git a/frontend/src/i18n/locales/ar.json b/frontend/src/i18n/locales/ar.json index fb53cb32..6e11c24d 100644 --- a/frontend/src/i18n/locales/ar.json +++ b/frontend/src/i18n/locales/ar.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "كتالوج النماذج", - "tab_engines": "المحركات", - "tab_models": "النماذج", - "moved_title": "أصبح ضمن كتالوج النماذج", - "moved_engines_body": "انتقل اختيار المحرك الافتراضي للنطق والتفريغ النصي ونماذج اللغة الكبيرة إلى مساحة عمل كتالوج النماذج.", - "moved_models_body": "انتقل تصفح النماذج المنزَّلة وتثبيتها وإزالتها إلى مساحة عمل كتالوج النماذج.", - "open_engines": "فتح المحركات", - "open_models": "فتح النماذج" + "summary_title": "إعدادك", + "change": "تغيير", + "status_setup": "يحتاج إلى إعداد", + "status_cpu": "احتياطي CPU", + "status_off": "متوقف", + "weights": "الأوزان المنزَّلة", + "storage_link": "إعدادات التخزين", + "reco_line": "تم تثبيت {{installed}} من {{total}} نماذج موصى بها لـ {{device}} · يتبقى {{remaining}} GB", + "install_rest": "تثبيت الباقي", + "device_remote": "بعيد" }, "settings": { "remote_backend_test": "اختبار الاتصال", @@ -779,7 +782,7 @@ "configureProviders": "إعداد مقدمي الخدمة", "loading": "جارٍ تحميل المحركات…", "refresh": "تحديث", - "matrixTitle": "مصفوفة توافق المحرك", + "matrixTitle": "المحركات", "loadFailed": "فشل تحميل المحركات: {{message}}", "couldNotLoad": "لا يمكن تحميل المحركات: {{message}}", "retry": "أعد المحاولة", diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 5144988a..bfe9f805 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "Modellkatalog", - "tab_engines": "Engines", - "tab_models": "Modelle", - "moved_title": "Jetzt im Modellkatalog", - "moved_engines_body": "Die Auswahl der Standard-Engine für Sprache, Transkription und LLM befindet sich jetzt im Arbeitsbereich „Modellkatalog“.", - "moved_models_body": "Heruntergeladene Modelle durchsehen, installieren und entfernen befindet sich jetzt im Arbeitsbereich „Modellkatalog“.", - "open_engines": "Engines öffnen", - "open_models": "Modelle öffnen" + "summary_title": "Deine Einrichtung", + "change": "Ändern", + "status_setup": "Einrichtung nötig", + "status_cpu": "CPU-Fallback", + "status_off": "Aus", + "weights": "Heruntergeladene Gewichte", + "storage_link": "Speichereinstellungen", + "reco_line": "{{installed}} von {{total}} empfohlenen Modellen für {{device}} installiert · noch {{remaining}} GB", + "install_rest": "Rest installieren", + "device_remote": "Remote" }, "settings": { "remote_backend_test": "Verbindung testen", @@ -779,7 +782,7 @@ "configureProviders": "Anbieter konfigurieren", "loading": "Motoren werden geladen…", "refresh": "Aktualisieren", - "matrixTitle": "Engine-Kompatibilitätsmatrix", + "matrixTitle": "Engines", "loadFailed": "Fehler beim Laden der Engines: {{message}}", "couldNotLoad": "Engines konnten nicht geladen werden: {{message}}", "retry": "Versuchen Sie es noch einmal", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index f012f2ff..42327fcf 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -171,13 +171,16 @@ }, "catalogue": { "title": "Model Catalogue", - "tab_engines": "Engines", - "tab_models": "Models", - "moved_title": "Now in the Model Catalogue", - "moved_engines_body": "Choosing the default speech, transcription and LLM engine moved to the Model Catalogue workspace.", - "moved_models_body": "Browsing, installing and removing model downloads moved to the Model Catalogue workspace.", - "open_engines": "Open Engines", - "open_models": "Open Models" + "summary_title": "Your setup", + "change": "Change", + "status_setup": "Needs setup", + "status_cpu": "CPU fallback", + "status_off": "Off", + "weights": "Downloaded weights", + "storage_link": "Storage settings", + "reco_line": "{{installed}} of {{total}} recommended models for {{device}} installed · {{remaining}} GB to go", + "install_rest": "Install the rest", + "device_remote": "Remote" }, "audiobook": { "cast": "Cast", @@ -2118,7 +2121,7 @@ "configureProviders": "Configure providers", "loading": "Loading engines…", "refresh": "Refresh", - "matrixTitle": "Engine Compatibility Matrix", + "matrixTitle": "Engines", "familyMatrixTitle": "{{family}} Engines", "colEngine": "Engine", "colGpuCompat": "GPU compat", diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index 89a44ade..25d59159 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "Catálogo de modelos", - "tab_engines": "Motores", - "tab_models": "Modelos", - "moved_title": "Ahora en el catálogo de modelos", - "moved_engines_body": "Elegir el motor predeterminado de voz, transcripción y LLM se movió al espacio Catálogo de modelos.", - "moved_models_body": "Explorar, instalar y eliminar descargas de modelos se movió al espacio Catálogo de modelos.", - "open_engines": "Abrir motores", - "open_models": "Abrir modelos" + "summary_title": "Tu configuración", + "change": "Cambiar", + "status_setup": "Requiere configuración", + "status_cpu": "Alternativa CPU", + "status_off": "Desactivado", + "weights": "Pesos descargados", + "storage_link": "Ajustes de almacenamiento", + "reco_line": "{{installed}} de {{total}} modelos recomendados para {{device}} instalados · faltan {{remaining}} GB", + "install_rest": "Instalar el resto", + "device_remote": "Remoto" }, "settings": { "remote_backend_test": "Probar conexión", @@ -779,7 +782,7 @@ "configureProviders": "Configurar proveedores", "loading": "Cargando motores…", "refresh": "Actualizar", - "matrixTitle": "Matriz de compatibilidad de motores", + "matrixTitle": "Motores", "loadFailed": "No se pudieron cargar los motores: {{message}}", "couldNotLoad": "No se pudieron cargar los motores: {{message}}", "retry": "Reintentar", diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index 26a467e7..ac5ccbab 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "Catalogue de modèles", - "tab_engines": "Moteurs", - "tab_models": "Modèles", - "moved_title": "Désormais dans le catalogue de modèles", - "moved_engines_body": "Le choix du moteur par défaut pour la voix, la transcription et les LLM se trouve maintenant dans l’espace Catalogue de modèles.", - "moved_models_body": "Parcourir, installer et supprimer les modèles téléchargés se trouve maintenant dans l’espace Catalogue de modèles.", - "open_engines": "Ouvrir les moteurs", - "open_models": "Ouvrir les modèles" + "summary_title": "Votre configuration", + "change": "Modifier", + "status_setup": "Configuration requise", + "status_cpu": "Repli CPU", + "status_off": "Désactivé", + "weights": "Poids téléchargés", + "storage_link": "Paramètres de stockage", + "reco_line": "{{installed}} des {{total}} modèles recommandés pour {{device}} installés · {{remaining}} Go restants", + "install_rest": "Installer le reste", + "device_remote": "Distant" }, "settings": { "remote_backend_test": "Tester la connexion", @@ -779,7 +782,7 @@ "configureProviders": "Configurer les fournisseurs", "loading": "Chargement des moteurs…", "refresh": "Actualiser", - "matrixTitle": "Matrice de compatibilité des moteurs", + "matrixTitle": "Moteurs", "loadFailed": "Échec du chargement des moteurs : {{message}}", "couldNotLoad": "Impossible de charger les moteurs : {{message}}", "retry": "Réessayer", diff --git a/frontend/src/i18n/locales/hi.json b/frontend/src/i18n/locales/hi.json index 53ae0870..22ec2118 100644 --- a/frontend/src/i18n/locales/hi.json +++ b/frontend/src/i18n/locales/hi.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "मॉडल कैटलॉग", - "tab_engines": "इंजन", - "tab_models": "मॉडल", - "moved_title": "अब मॉडल कैटलॉग में", - "moved_engines_body": "वाक्, प्रतिलेखन और LLM का डिफ़ॉल्ट इंजन चुनना अब मॉडल कैटलॉग वर्कस्पेस में है।", - "moved_models_body": "डाउनलोड किए गए मॉडल देखना, इंस्टॉल करना और हटाना अब मॉडल कैटलॉग वर्कस्पेस में है।", - "open_engines": "इंजन खोलें", - "open_models": "मॉडल खोलें" + "summary_title": "आपका सेटअप", + "change": "बदलें", + "status_setup": "सेटअप आवश्यक", + "status_cpu": "CPU फ़ॉलबैक", + "status_off": "बंद", + "weights": "डाउनलोड किए गए वेट्स", + "storage_link": "स्टोरेज सेटिंग्स", + "reco_line": "{{device}} के लिए {{total}} में से {{installed}} अनुशंसित मॉडल इंस्टॉल · {{remaining}} GB शेष", + "install_rest": "बाकी इंस्टॉल करें", + "device_remote": "रिमोट" }, "settings": { "remote_backend_test": "कनेक्शन जाँचें", @@ -779,7 +782,7 @@ "configureProviders": "प्रदाता कॉन्फ़िगर करें", "loading": "इंजन लोड हो रहे हैं...", "refresh": "ताज़ा करें", - "matrixTitle": "इंजन अनुकूलता मैट्रिक्स", + "matrixTitle": "इंजन", "loadFailed": "इंजन लोड करने में विफल: {{message}}", "couldNotLoad": "इंजन लोड नहीं हो सका: {{message}}", "retry": "पुनः प्रयास करें", diff --git a/frontend/src/i18n/locales/id.json b/frontend/src/i18n/locales/id.json index 57ecc3c5..cebc6dbf 100644 --- a/frontend/src/i18n/locales/id.json +++ b/frontend/src/i18n/locales/id.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "Katalog model", - "tab_engines": "Engine", - "tab_models": "Model", - "moved_title": "Kini di Katalog model", - "moved_engines_body": "Pemilihan engine default untuk suara, transkripsi, dan LLM kini ada di ruang kerja Katalog model.", - "moved_models_body": "Menjelajahi, memasang, dan menghapus model yang diunduh kini ada di ruang kerja Katalog model.", - "open_engines": "Buka engine", - "open_models": "Buka model" + "summary_title": "Pengaturan Anda", + "change": "Ubah", + "status_setup": "Perlu penyiapan", + "status_cpu": "Fallback CPU", + "status_off": "Nonaktif", + "weights": "Bobot yang diunduh", + "storage_link": "Pengaturan penyimpanan", + "reco_line": "{{installed}} dari {{total}} model yang direkomendasikan untuk {{device}} terpasang · sisa {{remaining}} GB", + "install_rest": "Pasang sisanya", + "device_remote": "Jarak jauh" }, "settings": { "remote_backend_test": "Uji koneksi", @@ -779,7 +782,7 @@ "configureProviders": "Konfigurasikan penyedia", "loading": "Memuat mesin…", "refresh": "Segarkan", - "matrixTitle": "Matriks Kompatibilitas Mesin", + "matrixTitle": "Mesin", "loadFailed": "Gagal memuat mesin: {{message}}", "couldNotLoad": "Tidak dapat memuat mesin: {{message}}", "retry": "Coba lagi", diff --git a/frontend/src/i18n/locales/it.json b/frontend/src/i18n/locales/it.json index 50f7bd4c..d8a7a54b 100644 --- a/frontend/src/i18n/locales/it.json +++ b/frontend/src/i18n/locales/it.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "Catalogo modelli", - "tab_engines": "Motori", - "tab_models": "Modelli", - "moved_title": "Ora nel catalogo modelli", - "moved_engines_body": "La scelta del motore predefinito per voce, trascrizione e LLM si trova ora nello spazio Catalogo modelli.", - "moved_models_body": "Sfogliare, installare e rimuovere i modelli scaricati si trova ora nello spazio Catalogo modelli.", - "open_engines": "Apri motori", - "open_models": "Apri modelli" + "summary_title": "La tua configurazione", + "change": "Cambia", + "status_setup": "Da configurare", + "status_cpu": "Fallback CPU", + "status_off": "Disattivato", + "weights": "Pesi scaricati", + "storage_link": "Impostazioni archiviazione", + "reco_line": "{{installed}} di {{total}} modelli consigliati per {{device}} installati · mancano {{remaining}} GB", + "install_rest": "Installa il resto", + "device_remote": "Remoto" }, "settings": { "remote_backend_test": "Verifica connessione", @@ -779,7 +782,7 @@ "configureProviders": "Configura i provider", "loading": "Caricamento motori…", "refresh": "Aggiorna", - "matrixTitle": "Matrice di compatibilità del motore", + "matrixTitle": "Motori", "loadFailed": "Impossibile caricare i motori: {{message}}", "couldNotLoad": "Impossibile caricare i motori: {{message}}", "retry": "Riprova", diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json index 374ca78e..e7de5434 100644 --- a/frontend/src/i18n/locales/ja.json +++ b/frontend/src/i18n/locales/ja.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "モデルカタログ", - "tab_engines": "エンジン", - "tab_models": "モデル", - "moved_title": "モデルカタログに移動しました", - "moved_engines_body": "音声・文字起こし・LLM の既定エンジンの選択は「モデルカタログ」ワークスペースに移動しました。", - "moved_models_body": "モデルの閲覧・インストール・削除は「モデルカタログ」ワークスペースに移動しました。", - "open_engines": "エンジンを開く", - "open_models": "モデルを開く" + "summary_title": "現在の設定", + "change": "変更", + "status_setup": "設定が必要", + "status_cpu": "CPUフォールバック", + "status_off": "オフ", + "weights": "ダウンロード済みの重み", + "storage_link": "ストレージ設定", + "reco_line": "{{device}} 向けの推奨モデル {{total}} 件中 {{installed}} 件をインストール済み · 残り {{remaining}} GB", + "install_rest": "残りをインストール", + "device_remote": "リモート" }, "settings": { "remote_backend_test": "接続をテスト", @@ -779,7 +782,7 @@ "configureProviders": "プロバイダーを設定", "loading": "エンジンをロード中…", "refresh": "リフレッシュ", - "matrixTitle": "エンジン互換性マトリックス", + "matrixTitle": "エンジン", "loadFailed": "エンジンのロードに失敗しました: {{message}}", "couldNotLoad": "エンジンをロードできませんでした: {{message}}", "retry": "再試行", diff --git a/frontend/src/i18n/locales/ko.json b/frontend/src/i18n/locales/ko.json index 877eedf2..4645b98d 100644 --- a/frontend/src/i18n/locales/ko.json +++ b/frontend/src/i18n/locales/ko.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "모델 카탈로그", - "tab_engines": "엔진", - "tab_models": "모델", - "moved_title": "이제 모델 카탈로그에 있습니다", - "moved_engines_body": "음성, 전사, LLM 기본 엔진 선택이 모델 카탈로그 작업 공간으로 이동했습니다.", - "moved_models_body": "내려받은 모델을 살펴보고 설치·삭제하는 기능이 모델 카탈로그 작업 공간으로 이동했습니다.", - "open_engines": "엔진 열기", - "open_models": "모델 열기" + "summary_title": "내 설정", + "change": "변경", + "status_setup": "설정 필요", + "status_cpu": "CPU 대체", + "status_off": "꺼짐", + "weights": "다운로드한 가중치", + "storage_link": "저장소 설정", + "reco_line": "{{device}} 권장 모델 {{total}}개 중 {{installed}}개 설치됨 · {{remaining}} GB 남음", + "install_rest": "나머지 설치", + "device_remote": "원격" }, "settings": { "remote_backend_test": "연결 테스트", @@ -1023,7 +1026,7 @@ "configureProviders": "제공업체 설정", "loading": "엔진 로드 중…", "refresh": "새로고침", - "matrixTitle": "엔진 호환성 매트릭스", + "matrixTitle": "엔진", "loadFailed": "엔진을 로드하지 못했습니다: {{message}}", "couldNotLoad": "엔진을 로드할 수 없습니다: {{message}}", "retry": "재시도", diff --git a/frontend/src/i18n/locales/nl.json b/frontend/src/i18n/locales/nl.json index 805ce54e..ad701d15 100644 --- a/frontend/src/i18n/locales/nl.json +++ b/frontend/src/i18n/locales/nl.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "Modelcatalogus", - "tab_engines": "Engines", - "tab_models": "Modellen", - "moved_title": "Nu in de modelcatalogus", - "moved_engines_body": "Het kiezen van de standaard-engine voor spraak, transcriptie en LLM staat nu in de werkruimte Modelcatalogus.", - "moved_models_body": "Gedownloade modellen bekijken, installeren en verwijderen staat nu in de werkruimte Modelcatalogus.", - "open_engines": "Engines openen", - "open_models": "Modellen openen" + "summary_title": "Jouw configuratie", + "change": "Wijzigen", + "status_setup": "Instellen vereist", + "status_cpu": "CPU-fallback", + "status_off": "Uit", + "weights": "Gedownloade gewichten", + "storage_link": "Opslaginstellingen", + "reco_line": "{{installed}} van {{total}} aanbevolen modellen voor {{device}} geïnstalleerd · nog {{remaining}} GB", + "install_rest": "Rest installeren", + "device_remote": "Extern" }, "settings": { "remote_backend_test": "Verbinding testen", @@ -779,7 +782,7 @@ "configureProviders": "Providers configureren", "loading": "Motoren laden…", "refresh": "Vernieuwen", - "matrixTitle": "Motorcompatibiliteitsmatrix", + "matrixTitle": "Motoren", "loadFailed": "Kan motoren niet laden: {{message}}", "couldNotLoad": "Kan motoren niet laden: {{message}}", "retry": "Opnieuw proberen", diff --git a/frontend/src/i18n/locales/pl.json b/frontend/src/i18n/locales/pl.json index 17e35611..eedf48ec 100644 --- a/frontend/src/i18n/locales/pl.json +++ b/frontend/src/i18n/locales/pl.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "Katalog modeli", - "tab_engines": "Silniki", - "tab_models": "Modele", - "moved_title": "Teraz w katalogu modeli", - "moved_engines_body": "Wybór domyślnego silnika mowy, transkrypcji i LLM znajduje się teraz w obszarze Katalog modeli.", - "moved_models_body": "Przeglądanie, instalowanie i usuwanie pobranych modeli znajduje się teraz w obszarze Katalog modeli.", - "open_engines": "Otwórz silniki", - "open_models": "Otwórz modele" + "summary_title": "Twoja konfiguracja", + "change": "Zmień", + "status_setup": "Wymaga konfiguracji", + "status_cpu": "Tryb awaryjny CPU", + "status_off": "Wyłączone", + "weights": "Pobrane wagi", + "storage_link": "Ustawienia pamięci", + "reco_line": "Zainstalowano {{installed}} z {{total}} zalecanych modeli dla {{device}} · pozostało {{remaining}} GB", + "install_rest": "Zainstaluj resztę", + "device_remote": "Zdalny" }, "settings": { "remote_backend_test": "Testuj połączenie", @@ -779,7 +782,7 @@ "configureProviders": "Skonfiguruj dostawców", "loading": "Ładowanie silników…", "refresh": "Odśwież", - "matrixTitle": "Matryca kompatybilności silników", + "matrixTitle": "Silniki", "loadFailed": "Nie udało się załadować silników: {{message}}", "couldNotLoad": "Nie można załadować silników: {{message}}", "retry": "Spróbuj ponownie", diff --git a/frontend/src/i18n/locales/pt.json b/frontend/src/i18n/locales/pt.json index f53a0649..e782d2e3 100644 --- a/frontend/src/i18n/locales/pt.json +++ b/frontend/src/i18n/locales/pt.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "Catálogo de modelos", - "tab_engines": "Motores", - "tab_models": "Modelos", - "moved_title": "Agora no catálogo de modelos", - "moved_engines_body": "A escolha do motor padrão de voz, transcrição e LLM foi para o espaço Catálogo de modelos.", - "moved_models_body": "Explorar, instalar e remover modelos baixados foi para o espaço Catálogo de modelos.", - "open_engines": "Abrir motores", - "open_models": "Abrir modelos" + "summary_title": "Sua configuração", + "change": "Alterar", + "status_setup": "Requer configuração", + "status_cpu": "Alternativa CPU", + "status_off": "Desligado", + "weights": "Pesos baixados", + "storage_link": "Configurações de armazenamento", + "reco_line": "{{installed}} de {{total}} modelos recomendados para {{device}} instalados · faltam {{remaining}} GB", + "install_rest": "Instalar o restante", + "device_remote": "Remoto" }, "settings": { "remote_backend_test": "Testar conexão", @@ -779,7 +782,7 @@ "configureProviders": "Configurar provedores", "loading": "Carregando motores…", "refresh": "Atualizar", - "matrixTitle": "Matriz de compatibilidade do motor", + "matrixTitle": "Motores", "loadFailed": "Falha ao carregar motores: {{message}}", "couldNotLoad": "Não foi possível carregar os motores: {{message}}", "retry": "Tentar novamente", diff --git a/frontend/src/i18n/locales/ru.json b/frontend/src/i18n/locales/ru.json index be96f5bc..80a6ee0b 100644 --- a/frontend/src/i18n/locales/ru.json +++ b/frontend/src/i18n/locales/ru.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "Каталог моделей", - "tab_engines": "Движки", - "tab_models": "Модели", - "moved_title": "Теперь в каталоге моделей", - "moved_engines_body": "Выбор движка по умолчанию для синтеза речи, расшифровки и LLM перенесён в раздел «Каталог моделей».", - "moved_models_body": "Просмотр, установка и удаление загруженных моделей перенесены в раздел «Каталог моделей».", - "open_engines": "Открыть движки", - "open_models": "Открыть модели" + "summary_title": "Ваша конфигурация", + "change": "Изменить", + "status_setup": "Требуется настройка", + "status_cpu": "Резерв на CPU", + "status_off": "Выкл.", + "weights": "Загруженные веса", + "storage_link": "Настройки хранилища", + "reco_line": "Установлено {{installed}} из {{total}} рекомендуемых моделей для {{device}} · осталось {{remaining}} ГБ", + "install_rest": "Установить остальное", + "device_remote": "Удалённый" }, "settings": { "remote_backend_test": "Проверить подключение", @@ -779,7 +782,7 @@ "configureProviders": "Настроить поставщиков", "loading": "Загрузка двигателей…", "refresh": "Обновить", - "matrixTitle": "Матрица совместимости двигателей", + "matrixTitle": "Движки", "loadFailed": "Не удалось загрузить двигатели: {{message}}.", "couldNotLoad": "Не удалось загрузить двигатели: {{message}}", "retry": "Повторить попытку", diff --git a/frontend/src/i18n/locales/sv.json b/frontend/src/i18n/locales/sv.json index 1b8e1d16..617831f3 100644 --- a/frontend/src/i18n/locales/sv.json +++ b/frontend/src/i18n/locales/sv.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "Modellkatalog", - "tab_engines": "Motorer", - "tab_models": "Modeller", - "moved_title": "Nu i modellkatalogen", - "moved_engines_body": "Att välja standardmotor för tal, transkribering och LLM finns nu i arbetsytan Modellkatalog.", - "moved_models_body": "Att bläddra bland, installera och ta bort nedladdade modeller finns nu i arbetsytan Modellkatalog.", - "open_engines": "Öppna motorer", - "open_models": "Öppna modeller" + "summary_title": "Din konfiguration", + "change": "Ändra", + "status_setup": "Behöver konfigureras", + "status_cpu": "CPU-reserv", + "status_off": "Av", + "weights": "Nedladdade vikter", + "storage_link": "Lagringsinställningar", + "reco_line": "{{installed}} av {{total}} rekommenderade modeller för {{device}} installerade · {{remaining}} GB kvar", + "install_rest": "Installera resten", + "device_remote": "Fjärr" }, "settings": { "remote_backend_test": "Testa anslutningen", @@ -779,7 +782,7 @@ "configureProviders": "Konfigurera leverantörer", "loading": "Laddar motorer...", "refresh": "Uppdatera", - "matrixTitle": "Motorkompatibilitetsmatris", + "matrixTitle": "Motorer", "loadFailed": "Det gick inte att ladda motorer: {{message}}", "couldNotLoad": "Kunde inte ladda motorer: {{message}}", "retry": "Försök igen", diff --git a/frontend/src/i18n/locales/th.json b/frontend/src/i18n/locales/th.json index afba49c9..5f26d531 100644 --- a/frontend/src/i18n/locales/th.json +++ b/frontend/src/i18n/locales/th.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "แค็ตตาล็อกโมเดล", - "tab_engines": "เอนจิน", - "tab_models": "โมเดล", - "moved_title": "ย้ายไปที่แค็ตตาล็อกโมเดลแล้ว", - "moved_engines_body": "การเลือกเอนจินเริ่มต้นสำหรับเสียงพูด การถอดเสียง และ LLM ย้ายไปอยู่ในพื้นที่ทำงานแค็ตตาล็อกโมเดลแล้ว", - "moved_models_body": "การเรียกดู ติดตั้ง และลบโมเดลที่ดาวน์โหลดไว้ ย้ายไปอยู่ในพื้นที่ทำงานแค็ตตาล็อกโมเดลแล้ว", - "open_engines": "เปิดเอนจิน", - "open_models": "เปิดโมเดล" + "summary_title": "การตั้งค่าของคุณ", + "change": "เปลี่ยน", + "status_setup": "ต้องตั้งค่า", + "status_cpu": "สำรองด้วย CPU", + "status_off": "ปิด", + "weights": "น้ำหนักโมเดลที่ดาวน์โหลด", + "storage_link": "การตั้งค่าพื้นที่เก็บข้อมูล", + "reco_line": "ติดตั้งแล้ว {{installed}} จาก {{total}} โมเดลที่แนะนำสำหรับ {{device}} · เหลืออีก {{remaining}} GB", + "install_rest": "ติดตั้งส่วนที่เหลือ", + "device_remote": "ระยะไกล" }, "settings": { "remote_backend_test": "ทดสอบการเชื่อมต่อ", @@ -779,7 +782,7 @@ "configureProviders": "ตั้งค่าผู้ให้บริการ", "loading": "กำลังโหลดเครื่องยนต์...", "refresh": "รีเฟรช", - "matrixTitle": "เมทริกซ์ความเข้ากันได้ของเครื่องยนต์", + "matrixTitle": "เอนจิน", "loadFailed": "ไม่สามารถโหลดเอ็นจิ้น: {{message}}", "couldNotLoad": "ไม่สามารถโหลดเอ็นจิ้น: {{message}}", "retry": "ลองอีกครั้ง", diff --git a/frontend/src/i18n/locales/tr.json b/frontend/src/i18n/locales/tr.json index 5aa609c4..86371977 100644 --- a/frontend/src/i18n/locales/tr.json +++ b/frontend/src/i18n/locales/tr.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "Model kataloğu", - "tab_engines": "Motorlar", - "tab_models": "Modeller", - "moved_title": "Artık model kataloğunda", - "moved_engines_body": "Varsayılan ses, deşifre ve LLM motorunu seçme işlemi Model kataloğu çalışma alanına taşındı.", - "moved_models_body": "İndirilen modellere göz atma, yükleme ve kaldırma işlemleri Model kataloğu çalışma alanına taşındı.", - "open_engines": "Motorları aç", - "open_models": "Modelleri aç" + "summary_title": "Kurulumunuz", + "change": "Değiştir", + "status_setup": "Kurulum gerekli", + "status_cpu": "CPU yedeği", + "status_off": "Kapalı", + "weights": "İndirilen ağırlıklar", + "storage_link": "Depolama ayarları", + "reco_line": "{{device}} için önerilen {{total}} modelden {{installed}} tanesi yüklü · {{remaining}} GB kaldı", + "install_rest": "Kalanları yükle", + "device_remote": "Uzak" }, "settings": { "remote_backend_test": "Bağlantıyı test et", @@ -779,7 +782,7 @@ "configureProviders": "Sağlayıcıları yapılandır", "loading": "Motorlar yükleniyor…", "refresh": "Yenile", - "matrixTitle": "Motor Uyumluluk Matrisi", + "matrixTitle": "Motorlar", "loadFailed": "Motorlar yüklenemedi: {{message}}", "couldNotLoad": "Motorlar yüklenemedi: {{message}}", "retry": "Yeniden dene", diff --git a/frontend/src/i18n/locales/uk.json b/frontend/src/i18n/locales/uk.json index 0be52b4f..f7951fbf 100644 --- a/frontend/src/i18n/locales/uk.json +++ b/frontend/src/i18n/locales/uk.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "Каталог моделей", - "tab_engines": "Рушії", - "tab_models": "Моделі", - "moved_title": "Тепер у каталозі моделей", - "moved_engines_body": "Вибір типового рушія мовлення, розшифрування та LLM перенесено до розділу «Каталог моделей».", - "moved_models_body": "Перегляд, встановлення та видалення завантажених моделей перенесено до розділу «Каталог моделей».", - "open_engines": "Відкрити рушії", - "open_models": "Відкрити моделі" + "summary_title": "Ваша конфігурація", + "change": "Змінити", + "status_setup": "Потрібне налаштування", + "status_cpu": "Резерв на CPU", + "status_off": "Вимкнено", + "weights": "Завантажені ваги", + "storage_link": "Налаштування сховища", + "reco_line": "Встановлено {{installed}} з {{total}} рекомендованих моделей для {{device}} · залишилось {{remaining}} ГБ", + "install_rest": "Встановити решту", + "device_remote": "Віддалений" }, "settings": { "remote_backend_test": "Перевірити з’єднання", @@ -779,7 +782,7 @@ "configureProviders": "Налаштувати постачальників", "loading": "Завантаження двигунів…", "refresh": "Оновити", - "matrixTitle": "Матриця сумісності двигуна", + "matrixTitle": "Движки", "loadFailed": "Не вдалося завантажити двигуни: {{message}}", "couldNotLoad": "Не вдалося завантажити двигуни: {{message}}", "retry": "Повторіть спробу", diff --git a/frontend/src/i18n/locales/vi.json b/frontend/src/i18n/locales/vi.json index 3c6a1f78..9f7832f7 100644 --- a/frontend/src/i18n/locales/vi.json +++ b/frontend/src/i18n/locales/vi.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "Danh mục mô hình", - "tab_engines": "Engine", - "tab_models": "Mô hình", - "moved_title": "Đã chuyển sang Danh mục mô hình", - "moved_engines_body": "Việc chọn engine mặc định cho giọng nói, chuyển lời nói thành văn bản và LLM đã chuyển sang không gian làm việc Danh mục mô hình.", - "moved_models_body": "Việc duyệt, cài đặt và xoá mô hình đã tải đã chuyển sang không gian làm việc Danh mục mô hình.", - "open_engines": "Mở engine", - "open_models": "Mở mô hình" + "summary_title": "Thiết lập của bạn", + "change": "Thay đổi", + "status_setup": "Cần thiết lập", + "status_cpu": "Dự phòng CPU", + "status_off": "Tắt", + "weights": "Trọng số đã tải", + "storage_link": "Cài đặt lưu trữ", + "reco_line": "Đã cài {{installed}}/{{total}} mô hình đề xuất cho {{device}} · còn {{remaining}} GB", + "install_rest": "Cài phần còn lại", + "device_remote": "Từ xa" }, "settings": { "remote_backend_test": "Kiểm tra kết nối", @@ -779,7 +782,7 @@ "configureProviders": "Cấu hình nhà cung cấp", "loading": "Đang tải động cơ…", "refresh": "Làm mới", - "matrixTitle": "Ma trận tương thích động cơ", + "matrixTitle": "Công cụ", "loadFailed": "Không tải được động cơ: {{message}}", "couldNotLoad": "Không thể tải động cơ: {{message}}", "retry": "Thử lại", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 05e0fe1d..fa7659b1 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -152,13 +152,16 @@ }, "catalogue": { "title": "模型库", - "tab_engines": "引擎", - "tab_models": "模型", - "moved_title": "已移至模型库", - "moved_engines_body": "选择默认的语音合成、语音转写和大模型引擎已移至“模型库”工作区。", - "moved_models_body": "浏览、安装和删除已下载模型已移至“模型库”工作区。", - "open_engines": "打开引擎", - "open_models": "打开模型" + "summary_title": "当前配置", + "change": "更改", + "status_setup": "需要设置", + "status_cpu": "CPU 回退", + "status_off": "关闭", + "weights": "已下载的模型权重", + "storage_link": "存储设置", + "reco_line": "{{device}} 的推荐模型已安装 {{installed}}/{{total}} · 还需 {{remaining}} GB", + "install_rest": "安装其余模型", + "device_remote": "远程" }, "launchpad": { "greeting": "你好", @@ -975,7 +978,7 @@ "configureProviders": "配置提供商", "loading": "加载引擎中…", "refresh": "刷新", - "matrixTitle": "引擎兼容性矩阵", + "matrixTitle": "引擎", "loadFailed": "无法加载引擎:{{message}}", "couldNotLoad": "无法加载引擎:{{message}}", "retry": "重试", diff --git a/frontend/src/i18n/locales/zh-TW.json b/frontend/src/i18n/locales/zh-TW.json index 9f57938c..71f99236 100644 --- a/frontend/src/i18n/locales/zh-TW.json +++ b/frontend/src/i18n/locales/zh-TW.json @@ -33,13 +33,16 @@ }, "catalogue": { "title": "模型庫", - "tab_engines": "引擎", - "tab_models": "模型", - "moved_title": "已移至模型庫", - "moved_engines_body": "選擇預設的語音合成、語音轉寫與大型語言模型引擎已移至「模型庫」工作區。", - "moved_models_body": "瀏覽、安裝與移除已下載模型已移至「模型庫」工作區。", - "open_engines": "開啟引擎", - "open_models": "開啟模型" + "summary_title": "目前設定", + "change": "變更", + "status_setup": "需要設定", + "status_cpu": "CPU 回退", + "status_off": "關閉", + "weights": "已下載的模型權重", + "storage_link": "儲存設定", + "reco_line": "{{device}} 的推薦模型已安裝 {{installed}}/{{total}} · 尚需 {{remaining}} GB", + "install_rest": "安裝其餘模型", + "device_remote": "遠端" }, "settings": { "remote_backend_test": "測試連線", @@ -779,7 +782,7 @@ "configureProviders": "設定提供者", "loading": "正在載入引擎...", "refresh": "重新整理", - "matrixTitle": "引擎相容性矩陣", + "matrixTitle": "配音引擎", "loadFailed": "無法載入引擎:{{message}}", "couldNotLoad": "無法載入引擎:{{message}}", "retry": "重試", diff --git a/frontend/src/pages/ModelCatalogue.jsx b/frontend/src/pages/ModelCatalogue.jsx index 1e82a4a9..bdbcec30 100644 --- a/frontend/src/pages/ModelCatalogue.jsx +++ b/frontend/src/pages/ModelCatalogue.jsx @@ -1,48 +1,43 @@ /** - * ModelCatalogue — the workspace where engines and model weights are browsed - * and the app's defaults are chosen. + * ModelCatalogue — one page, one axis. * - * Engine picking and the model store used to be two Settings categories, which - * buried the single most consequential decision in the app (which TTS/ASR/LLM - * engine runs, and which weights are on disk) three clicks deep, split across - * two panes that constantly cross-reference each other. This promotes both to a - * first-class workspace with one pane switch between them; Settings keeps only - * the genuinely settings-shaped remainder (models directory, HF mirror) and - * points here for the rest. + * Reads top-down the way a user thinks about it: + * 1. SetupSummary — what speech, transcription, dictation and the language + * model use right now, one line each, with a Change button. + * 2. The engine list for ONE family (TTS / ASR / LLM), the same tested + * EngineCompatibilityMatrix the Engines pane hosted; Change on a summary + * row switches this family and scrolls here. + * 3. That family's downloadable weights (the former Models pane, filtered): + * TTS weights under TTS; offline ASR, streaming dictation and diarisation + * under ASR. LLM engines bring their own weights, so nothing renders there. + * 4. One storage line pointing at Settings → Storage. * - * Deliberately a COMPOSITION, not a rewrite: the panes mount the existing - * `EnginesTab` (engine matrix + the OpenAI-compatible ASR config) and - * `ModelStoreTab` unchanged, so their data contracts, tests and behaviour carry - * over untouched — the shared engine cache, the same install/delete flows, and - * the same env-var-wins semantics. + * The previous Engines | Models pane switch put the same decision on two axes + * (family tabs inside one pane, role sections inside the other), duplicated + * dictation in both, and parked storage stats, the HF token and the voice + * preview toggle on the model list. Those now live where they belong + * (Settings → Storage / Credentials), and there is no pane to pick. * - * `pendingCatalogueTab` is the one-shot deep-link hand-off (mirrors Settings' - * `pendingSettingsTab`): a caller sets the pane and navigates here, this page - * consumes it once and clears it so a later plain visit reopens the last pane. + * `pendingCatalogueFamily` is the one-shot deep-link hand-off (mirrors + * Settings' `pendingSettingsTab`): a caller names a family and navigates + * here; this page consumes it once and clears it so a later plain visit + * reopens the last family. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Boxes, CheckCircle, Cpu, HardDriveDownload, RefreshCw } from 'lucide-react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { Boxes } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useAppStore } from '../store'; -import { useModelStatus, useSystemInfo } from '../api/hooks'; -import { Badge, Tabs } from '../ui'; +import { useModels } from '../api/hooks'; +import SetupSummary from '../components/catalogue/SetupSummary'; import EnginesTab from '../components/settings/EnginesTab'; import ModelStoreTab from '../components/settings/ModelStoreTab'; +import { fmtBytes } from '../components/settings/models/format'; /** Persisted across visits so the workspace reopens where you left it. */ -const PANE_KEY = 'omnivoice.catalogue.pane'; const FAMILY_KEY = 'omnivoice.catalogue.engine-family'; -const PANES = ['engines', 'models']; const FAMILIES = ['tts', 'asr', 'llm']; - -function readStoredPane() { - try { - const stored = localStorage.getItem(PANE_KEY); - return PANES.includes(stored) ? stored : 'engines'; - } catch { - return 'engines'; - } -} +/** Families whose engines load catalogue weights (LLM engines bring their own). */ +const WEIGHT_FAMILIES = ['tts', 'asr']; function readStoredFamily() { try { @@ -55,27 +50,15 @@ function readStoredFamily() { export default function ModelCatalogue() { const { t } = useTranslation(); - const pendingCatalogueTab = useAppStore((s) => s.pendingCatalogueTab); const pendingCatalogueFamily = useAppStore((s) => s.pendingCatalogueFamily); - const setPendingCatalogueTab = useAppStore((s) => s.setPendingCatalogueTab); const setPendingCatalogueFamily = useAppStore((s) => s.setPendingCatalogueFamily); - // Seed from the deep-link so the first paint is already the requested pane — - // seeding from storage and correcting in an effect would flash the wrong one. - const [pane, setPaneRaw] = useState(() => - PANES.includes(pendingCatalogueTab) ? pendingCatalogueTab : readStoredPane(), - ); + const openSettingsTab = useAppStore((s) => s.openSettingsTab); + // Seed from the deep-link so the first paint is already the requested + // family — seeding from storage and correcting in an effect would flash. const [family, setFamilyRaw] = useState(() => FAMILIES.includes(pendingCatalogueFamily) ? pendingCatalogueFamily : readStoredFamily(), ); - const setPane = useCallback((next) => { - setPaneRaw(next); - try { - localStorage.setItem(PANE_KEY, next); - } catch { - /* private mode / quota — the pane still switches, it just won't persist */ - } - }, []); const setFamily = useCallback((next) => { setFamilyRaw(next); try { @@ -85,53 +68,26 @@ export default function ModelCatalogue() { } }, []); - // Consume the one-shot deep-link (including a repeat request for the pane - // we're already on, which must still clear). + // Consume the one-shot deep-link (including a repeat request for the + // family we're already on, which must still clear). useEffect(() => { - if (!pendingCatalogueTab) return; - if (PANES.includes(pendingCatalogueTab)) setPane(pendingCatalogueTab); + if (!pendingCatalogueFamily) return; if (FAMILIES.includes(pendingCatalogueFamily)) setFamily(pendingCatalogueFamily); - setPendingCatalogueTab(null); setPendingCatalogueFamily(null); - }, [ - pendingCatalogueTab, - pendingCatalogueFamily, - setPendingCatalogueFamily, - setPendingCatalogueTab, - setFamily, - setPane, - ]); + }, [pendingCatalogueFamily, setPendingCatalogueFamily, setFamily]); - const { data: info } = useSystemInfo(); - const { data: status } = useModelStatus(); - - // The loaded-model pill ModelStoreTab renders in its header. Lives here now - // that this page — not Settings — hosts the model store. - const modelBadge = - status?.status === 'ready' ? ( - - {t('models.ready_badge')} - - ) : status?.status === 'loading' ? ( - - {t('models.loading_badge')} - - ) : ( - {t('models.idle_badge')} - ); - - // Tabs, not a two-state Segmented: these are two workspaces of a catalogue, - // not one setting with an on/off reading, and the room to add a third pane - // later is free. Tabs also carry roving tabindex + role="tab" from the - // primitive, which the switch had to describe with an aria-label. - const paneItems = useMemo( - () => [ - { id: 'engines', label: t('catalogue.tab_engines'), icon: Cpu }, - { id: 'models', label: t('catalogue.tab_models'), icon: HardDriveDownload }, - ], - [t], + const enginesRef = useRef(null); + const changeFamily = useCallback( + (next) => { + setFamily(next); + enginesRef.current?.scrollIntoView?.({ behavior: 'smooth', block: 'start' }); + }, + [setFamily], ); + const { data: models } = useModels(); + const cacheDir = models?.hf_cache_dir?.replace(/^\/Users\/[^/]+/, '~'); + return ( // Same container-query shell as Settings: the app is zoom-scaled, so // viewport media queries fire at the wrong logical width under Tauri. @@ -140,48 +96,83 @@ export default function ModelCatalogue() { data-testid="model-catalogue" >
- {/* Treat this as a workspace, not a Settings card: one editorial title, - one quiet navigation line, then the data surface. */} -
-
- -

- {t('catalogue.title')} -

-
- +
+ +

+ {t('catalogue.title')} +

- {/* Full-bleed: engine rows and the model table are wide, data-dense + + + {/* Full-bleed: engine rows and the weights table are wide, data-dense surfaces — a reading-width column would only add horizontal scrolling inside them. */} -
- {pane === 'engines' ? ( - - ) : ( - + + + + {WEIGHT_FAMILIES.includes(family) && ( +
+

+ {t('catalogue.weights')} +

+ +
+ )} + +
+ {models && ( + <> + + {fmtBytes(models.total_installed_bytes) || '0 B'} + + {models.disk_free_gb != null && ( + <> + + + {t('models.disk_free', { size: `${models.disk_free_gb} GB` })} + + + )} + {cacheDir && ( + <> + + + {cacheDir} + + + )} + )} -
+ +
); diff --git a/frontend/src/pages/ModelCatalogue.test.jsx b/frontend/src/pages/ModelCatalogue.test.jsx index d47f7eae..faa98edf 100644 --- a/frontend/src/pages/ModelCatalogue.test.jsx +++ b/frontend/src/pages/ModelCatalogue.test.jsx @@ -1,160 +1,151 @@ -// Model Catalogue workspace — pane switching, deep-link hand-off, persistence. +// Model Catalogue workspace — one family axis, deep-link hand-off, persistence. // -// The two panes are mounted stand-ins for the real EnginesTab / ModelStoreTab -// (both of which have their own suites and both of which hit the network); this -// suite is about the workspace shell around them. +// The summary, engine list and weights list are mounted stand-ins for the real +// components (each has its own suite and hits the network); this suite is +// about the page around them: one family drives both lists, LLM has no +// weights, Change on the summary switches the family, and the old two-pane +// switch is gone. import { describe, it, expect, beforeEach, vi } from 'vitest'; import React from 'react'; import { render, screen, fireEvent, act } from '@testing-library/react'; vi.mock('../components/settings/EnginesTab', () => ({ - default: ({ initialFamily, catalogueLayout }) => ( + default: ({ initialFamily, onFamilyChange, catalogueLayout }) => (
{initialFamily} +
), })); vi.mock('../components/settings/ModelStoreTab', () => ({ - default: ({ modelBadge, catalogueLayout }) => ( -
- {modelBadge} + default: ({ family }) =>
{family}
, +})); +vi.mock('../components/catalogue/SetupSummary', () => ({ + default: ({ onChange }) => ( +
+
), })); vi.mock('../api/hooks', () => ({ - useSystemInfo: () => ({ data: { has_hf_token: false } }), - useModelStatus: () => ({ data: { status: 'ready' } }), + useModels: () => ({ + data: { + total_installed_bytes: 3 * 1024 ** 3, + disk_free_gb: 41, + hf_cache_dir: '/Users/dev/.cache/huggingface', + }, + }), })); import { useAppStore } from '../store'; import ModelCatalogue from './ModelCatalogue'; -// Radix Tabs activate on POINTER DOWN, not click — a bare fireEvent.click -// leaves the pane unchanged and reads as a broken switcher. Drive the tab the -// way a pointer does. -const clickTab = (name) => { - const tab = screen.getByRole('tab', { name }); - fireEvent.pointerDown(tab, { button: 0, ctrlKey: false, pointerType: 'mouse' }); - fireEvent.mouseDown(tab, { button: 0 }); - fireEvent.click(tab); -}; - describe('ModelCatalogue', () => { beforeEach(() => { localStorage.clear(); act(() => { - useAppStore.setState({ - mode: 'catalogue', - pendingCatalogueTab: null, - pendingCatalogueFamily: null, - }); + useAppStore.setState({ mode: 'catalogue', pendingCatalogueFamily: null }); }); }); - it('opens on the Engines pane and switches to Models', () => { + it('is one page: summary, then the TTS engines and TTS weights, no pane switch', () => { render(); - expect(screen.getByTestId('stub-engines')).toBeInTheDocument(); - expect(screen.getByRole('tabpanel', { name: 'Engines' })).toBeInTheDocument(); + expect(screen.getByTestId('stub-summary')).toBeInTheDocument(); + expect(screen.getByTestId('stub-engines')).toHaveTextContent('tts'); expect(screen.getByTestId('stub-engines')).toHaveAttribute('data-catalogue-layout', 'true'); - expect(screen.queryByTestId('stub-models')).toBeNull(); - - clickTab('Models'); - expect(screen.getByTestId('stub-models')).toBeInTheDocument(); - expect(screen.getByRole('tabpanel', { name: 'Models' })).toBeInTheDocument(); - expect(screen.getByTestId('stub-models')).toHaveAttribute('data-catalogue-layout', 'true'); - expect(screen.queryByTestId('stub-engines')).toBeNull(); + expect(screen.getByTestId('stub-weights')).toHaveTextContent('tts'); + expect(screen.queryByRole('tab')).toBeNull(); + // Reading order: summary above engines above weights. + const [summary, engines, weights] = ['stub-summary', 'stub-engines', 'stub-weights'].map((id) => + screen.getByTestId(id), + ); + expect( + summary.compareDocumentPosition(engines) & Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + expect( + engines.compareDocumentPosition(weights) & Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); }); - it('uses the wide workspace shell for data-heavy catalogue panes', () => { + it('Change on the summary switches the family for engines and weights together', () => { + render(); + fireEvent.click(screen.getByText('change-asr')); + expect(screen.getByTestId('stub-engines')).toHaveTextContent('asr'); + expect(screen.getByTestId('stub-weights')).toHaveTextContent('asr'); + }); + + it('LLM engines bring their own weights, so the weights block disappears', () => { + render(); + fireEvent.click(screen.getByText('to-llm')); + expect(screen.getByTestId('stub-engines')).toHaveTextContent('llm'); + expect(screen.queryByTestId('stub-weights')).toBeNull(); + }); + + it('uses the wide workspace shell for data-heavy lists', () => { render(); expect(screen.getByTestId('model-catalogue').firstElementChild).toHaveClass('max-w-[1500px]'); }); - it('associates each tab with its labelled panel', () => { + it('remembers the family across visits', () => { + const { unmount } = render(); + fireEvent.click(screen.getByText('change-asr')); + unmount(); render(); - const enginesTab = screen.getByRole('tab', { name: 'Engines' }); - const enginesPanel = screen.getByRole('tabpanel', { name: 'Engines' }); - expect(enginesTab).toHaveAttribute('aria-controls', enginesPanel.id); - expect(enginesPanel).toHaveAttribute('aria-labelledby', enginesTab.id); - - const modelsTab = screen.getByRole('tab', { name: 'Models' }); - clickTab('Models'); - const modelsPanel = screen.getByRole('tabpanel', { name: 'Models' }); - expect(modelsTab).toHaveAttribute('aria-controls', modelsPanel.id); - expect(modelsPanel).toHaveAttribute('aria-labelledby', modelsTab.id); + expect(screen.getByTestId('stub-engines')).toHaveTextContent('asr'); }); - it('remembers the pane across visits', () => { - const first = render(); - clickTab('Models'); - first.unmount(); - + it('honours a pending family deep-link on first paint and clears it', () => { + act(() => useAppStore.setState({ pendingCatalogueFamily: 'llm' })); render(); - expect(screen.getByTestId('stub-models')).toBeInTheDocument(); + expect(screen.getByTestId('stub-engines')).toHaveTextContent('llm'); + expect(useAppStore.getState().pendingCatalogueFamily).toBeNull(); }); - it('honours a pending deep-link pane on first paint and clears it', () => { + it('switches family when a deep-link arrives while already mounted', () => { + render(); + expect(screen.getByTestId('stub-engines')).toHaveTextContent('tts'); act(() => { - useAppStore.setState({ pendingCatalogueTab: 'models' }); + useAppStore.getState().openCatalogue('asr'); }); - render(); - expect(screen.getByTestId('stub-models')).toBeInTheDocument(); - expect(useAppStore.getState().pendingCatalogueTab).toBeNull(); - }); - - it('switches pane when a deep-link arrives while already mounted', () => { - render(); - expect(screen.getByTestId('stub-engines')).toBeInTheDocument(); - - act(() => { - useAppStore.getState().openCatalogue('models'); - }); - expect(screen.getByTestId('stub-models')).toBeInTheDocument(); - expect(useAppStore.getState().pendingCatalogueTab).toBeNull(); - }); - - it('honours an engine-family deep link and clears it after hand-off', () => { - act(() => { - useAppStore.getState().openCatalogue({ pane: 'engines', family: 'asr' }); - }); - render(); expect(screen.getByTestId('stub-engines')).toHaveTextContent('asr'); expect(useAppStore.getState().pendingCatalogueFamily).toBeNull(); }); - it('passes the loaded-model badge down to the model store pane', () => { - act(() => { - useAppStore.setState({ pendingCatalogueTab: 'models' }); - }); + it('shows one storage line and links it to Settings → Storage', () => { render(); - // 'ready' status → the ready badge renders inside the models pane. - expect(screen.getByTestId('stub-models').textContent).toBeTruthy(); + const footer = screen.getByTestId('catalogue-storage'); + expect(footer).toHaveTextContent('41 GB'); + expect(footer).toHaveTextContent('~/.cache/huggingface'); + fireEvent.click(screen.getByTestId('catalogue-storage-link')); + expect(useAppStore.getState().mode).toBe('settings'); + expect(useAppStore.getState().pendingSettingsTab).toBe('storage'); }); }); describe('openCatalogue', () => { - it('navigates to the catalogue workspace on the requested pane', () => { - act(() => { - useAppStore.setState({ mode: 'settings', pendingCatalogueTab: null }); - useAppStore.getState().openCatalogue('models'); - }); + beforeEach(() => { + act(() => useAppStore.setState({ mode: 'studio', pendingCatalogueFamily: null })); + }); + + it('navigates to the catalogue on the requested family', () => { + act(() => useAppStore.getState().openCatalogue('llm')); expect(useAppStore.getState().mode).toBe('catalogue'); - expect(useAppStore.getState().pendingCatalogueTab).toBe('models'); - }); - - it('defaults to the engines pane', () => { - act(() => { - useAppStore.setState({ pendingCatalogueTab: null }); - useAppStore.getState().openCatalogue(); - }); - expect(useAppStore.getState().pendingCatalogueTab).toBe('engines'); - }); - - it('accepts an object deep link with a family', () => { - act(() => { - useAppStore.getState().openCatalogue({ pane: 'engines', family: 'llm' }); - }); - expect(useAppStore.getState().pendingCatalogueTab).toBe('engines'); expect(useAppStore.getState().pendingCatalogueFamily).toBe('llm'); }); + + it('accepts the object form and ignores the retired pane key', () => { + act(() => useAppStore.getState().openCatalogue({ pane: 'models', family: 'asr' })); + expect(useAppStore.getState().mode).toBe('catalogue'); + expect(useAppStore.getState().pendingCatalogueFamily).toBe('asr'); + }); + + it('defaults to the last family when called bare', () => { + act(() => useAppStore.getState().openCatalogue()); + expect(useAppStore.getState().mode).toBe('catalogue'); + expect(useAppStore.getState().pendingCatalogueFamily).toBeNull(); + }); }); diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 69dd780f..0f767443 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -35,7 +35,7 @@ import UpdatesPanel from '../components/UpdatesPanel'; import GeneralTab from '../components/settings/GeneralTab'; // Engine selection + the model store moved to the Model Catalogue workspace // (pages/ModelCatalogue.jsx); these categories now signpost it. -import CataloguePointer from '../components/settings/CataloguePointer'; +import VoicePreviewsPanel from '../components/settings/VoicePreviewsPanel'; import HotkeyTab from '../components/settings/HotkeyTab'; import TranslationTab from '../components/settings/TranslationTab'; import NetworkTab from '../components/settings/NetworkTab'; @@ -399,20 +399,6 @@ export default function Settings() { ); - case 'engines': - return ; - case 'models': - // What stays here is the storage-shaped remainder: where weights live - // on disk and which mirror they come from (both restart-bound, hence - // the category's `restart: true`). Browsing / installing / removing - // them is the Model Catalogue's Models pane. - return ( - <> - - - - - ); case 'dictation': return ( <> @@ -436,16 +422,28 @@ export default function Settings() { ); case 'storage': + // Everything disk-shaped in one place: usage, the models directory + // (restart-bound), data/output paths, and the voice-preview cache + // toggle. Browsing / installing / removing weights is the Model + // Catalogue. return ( <> + + ); case 'permissions': return ; case 'network': - return ; + // The HF mirror is a network choice (which host downloads come from). + return ( + <> + + + + ); case 'audio-tools': return ; case 'sharing': diff --git a/frontend/src/store/uiSlice.ts b/frontend/src/store/uiSlice.ts index e0e04d5e..f550079e 100644 --- a/frontend/src/store/uiSlice.ts +++ b/frontend/src/store/uiSlice.ts @@ -33,8 +33,12 @@ export type AppMode = | 'settings'; /** Which pane the Model Catalogue workspace opens on. */ -export type CatalogueTab = 'engines' | 'models'; -export type CatalogueTarget = CatalogueTab | { pane?: CatalogueTab; family?: EngineFamily }; +/** + * Deep-link target for the Model Catalogue: the engine family to open on. The + * page has one axis now (TTS / ASR / LLM) — the old `pane` key is tolerated + * and ignored so stale callers keep navigating. + */ +export type CatalogueTarget = EngineFamily | { family?: EngineFamily | null; pane?: string }; /** * The Voice workspace's "Define voice" method (was the Clone/Design tab @@ -77,12 +81,9 @@ export interface UiSlice { */ pendingSettingsTab: string | null; /** - * One-shot hand-off for "open the Model Catalogue on a specific pane" — the - * catalogue twin of `pendingSettingsTab`, used by the Settings pointers that - * replaced the old Engines / Model Store panels. + * One-shot hand-off for "open the Model Catalogue on a specific engine + * family" — the catalogue twin of `pendingSettingsTab`. */ - pendingCatalogueTab: CatalogueTab | null; - /** Optional engine family to focus after entering the catalogue. */ pendingCatalogueFamily: EngineFamily | null; isSidebarCollapsed: boolean; isSidebarProjectsCollapsed: boolean; @@ -102,12 +103,11 @@ export interface UiSlice { setModeBeforeVoice: (mode: AppMode | null) => void; setPendingProfileId: (id: string | null) => void; setPendingSettingsTab: (tab: string | null) => void; - setPendingCatalogueTab: (tab: CatalogueTab | null) => void; setPendingCatalogueFamily: (family: EngineFamily | null) => void; /** Navigate to Settings on a specific tab in one call. */ openSettingsTab: (tab: string) => void; - /** Navigate to the Model Catalogue on a specific pane in one call. */ - openCatalogue: (target?: CatalogueTarget) => void; + /** Navigate to the Model Catalogue on a specific engine family in one call. */ + openCatalogue: (target?: CatalogueTarget | null) => void; setIsSidebarCollapsed: (collapsed: boolean) => void; setIsSidebarProjectsCollapsed: (collapsed: boolean) => void; setSidebarTab: (tab: SidebarTab) => void; @@ -132,7 +132,6 @@ export const createUiSlice: StateCreator = (set, get) modeBeforeVoice: null, pendingProfileId: null, pendingSettingsTab: null, - pendingCatalogueTab: null, pendingCatalogueFamily: null, isSidebarCollapsed: false, isSidebarProjectsCollapsed: false, @@ -154,13 +153,11 @@ export const createUiSlice: StateCreator = (set, get) setModeBeforeVoice: (mode) => set({ modeBeforeVoice: mode }), setPendingProfileId: (id) => set({ pendingProfileId: id }), setPendingSettingsTab: (tab) => set({ pendingSettingsTab: tab }), - setPendingCatalogueTab: (tab) => set({ pendingCatalogueTab: tab }), setPendingCatalogueFamily: (family) => set({ pendingCatalogueFamily: family }), openSettingsTab: (tab) => set({ pendingSettingsTab: tab, mode: 'settings' }), - openCatalogue: (target = 'engines') => { - const { pane = 'engines', family = null } = - typeof target === 'string' ? { pane: target } : target; - set({ pendingCatalogueTab: pane, pendingCatalogueFamily: family, mode: 'catalogue' }); + openCatalogue: (target = null) => { + const family = typeof target === 'string' ? target : (target?.family ?? null); + set({ pendingCatalogueFamily: family, mode: 'catalogue' }); }, setIsSidebarCollapsed: (collapsed) => set({ isSidebarCollapsed: collapsed }), setIsSidebarProjectsCollapsed: (collapsed) => set({ isSidebarProjectsCollapsed: collapsed }), diff --git a/frontend/src/test/EngineCompatibilityMatrix.test.jsx b/frontend/src/test/EngineCompatibilityMatrix.test.jsx index 8d122b60..4e997920 100644 --- a/frontend/src/test/EngineCompatibilityMatrix.test.jsx +++ b/frontend/src/test/EngineCompatibilityMatrix.test.jsx @@ -1153,7 +1153,7 @@ describe('EngineCompatibilityMatrix', () => { />, ); await waitFor(() => screen.getByText('OmniVoice (test)')); - expect(screen.getByText('Engine Compatibility Matrix')).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Engines' })).toBeInTheDocument(); expect(document.querySelectorAll('.engine-matrix__tab-family').length).toBe(3); const labels = document.querySelectorAll('.engine-matrix__tab-label'); expect(labels).toHaveLength(3); diff --git a/frontend/src/test/modelStoreGrouping.test.jsx b/frontend/src/test/modelStoreGrouping.test.jsx index 1a7305a8..e77fb78d 100644 --- a/frontend/src/test/modelStoreGrouping.test.jsx +++ b/frontend/src/test/modelStoreGrouping.test.jsx @@ -228,8 +228,9 @@ vi.mock('../components/settings/models/ModelsTable', () => ({ })); import ModelStoreTab from '../components/settings/ModelStoreTab'; +import { scopeReco } from '../components/settings/models/sections'; -function mountTab() { +function mountTab(props = {}) { global.EventSource = class { constructor() { this.onmessage = null; @@ -238,7 +239,7 @@ function mountTab() { }; return render( - + , ); } @@ -251,7 +252,7 @@ describe('Model Store — grouped catalog', () => { it('renders role sections in order with localized titles', async () => { mountTab(); await waitFor(() => expect(screen.getByTestId('models-section-tts')).toBeInTheDocument()); - expect(screen.getByTestId('model-list-panel')).toHaveClass('min-h-[95%]'); + expect(screen.getByTestId('model-list-panel')).toBeInTheDocument(); expect(screen.getByTestId('model-list-area')).toHaveClass('flex-1'); const keys = ['tts', 'asr', 'dictation', 'diarisation']; const sections = keys.map((k) => screen.getByTestId(`models-section-${k}`)); @@ -313,3 +314,50 @@ describe('Model Store — grouped catalog', () => { expect(search).toHaveValue(''); }); }); + +// ── Family scoping: the catalogue lists one family's weights under its engines ─ + +describe('Model Store — scoped to one engine family', () => { + it('ASR carries offline ASR, streaming dictation and diarisation; TTS only TTS', async () => { + mountTab({ family: 'asr' }); + await waitFor(() => expect(screen.getByTestId('models-section-asr')).toBeInTheDocument()); + expect(screen.getByTestId('models-section-dictation')).toBeInTheDocument(); + expect(screen.getByTestId('models-section-diarisation')).toBeInTheDocument(); + expect(screen.queryByTestId('models-section-tts')).not.toBeInTheDocument(); + expect(visibleLabels()).not.toContain('VoiceStudio TTS'); + }); + + it('TTS shows only TTS weights and searching within it stays scoped', async () => { + mountTab({ family: 'tts' }); + await waitFor(() => expect(screen.getByTestId('models-section-tts')).toBeInTheDocument()); + expect(screen.queryByTestId('models-section-asr')).not.toBeInTheDocument(); + const search = screen.getByRole('searchbox', { name: t('models.search_label') }); + fireEvent.change(search, { target: { value: 'parakeet' } }); + // Parakeet is an ASR row — no match inside TTS, so the scoped empty state shows. + await waitFor(() => expect(screen.getByText(t('models.no_matches'))).toBeInTheDocument()); + }); + + it('scopeReco narrows the device preset to the family and recomputes totals', () => { + const reco = { + device: { label: 'M2' }, + all_installed: false, + download_gb_remaining: 5.3, + total_gb: 7.7, + models: [ + { repo_id: 'a/tts', role: 'TTS', installed: true, size_gb: 2.4 }, + { repo_id: 'b/asr', role: 'ASR', installed: false, size_gb: 2.9 }, + { repo_id: 'c/dict', role: 'ASR', engine: 'sherpa-onnx', installed: false, size_gb: 2.4 }, + ], + }; + expect(scopeReco(reco, null)).toBe(reco); + const tts = scopeReco(reco, 'tts'); + expect(tts.models.map((m) => m.repo_id)).toEqual(['a/tts']); + expect(tts.all_installed).toBe(true); + expect(tts.download_gb_remaining).toBe(0); + const asr = scopeReco(reco, 'asr'); + expect(asr.models.map((m) => m.repo_id)).toEqual(['b/asr', 'c/dict']); + expect(asr.all_installed).toBe(false); + expect(asr.download_gb_remaining).toBe(5.3); + expect(scopeReco(reco, 'llm')).toBeNull(); + }); +}); diff --git a/frontend/src/test/modelStoreTokenPath.test.jsx b/frontend/src/test/modelStoreTokenPath.test.jsx deleted file mode 100644 index 2f654a6a..00000000 --- a/frontend/src/test/modelStoreTokenPath.test.jsx +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react'; -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { I18nextProvider } from 'react-i18next'; -import i18n from '../i18n'; - -// ── Enhancement: unify the two HF-token entry points ─────────────────────── -// The Models toolbar used to save the token via /system/set-env (env var + -// HF-CLI file) while Settings → Credentials saves to the encrypted app store — -// two stores with an asymmetric clear path (Credentials' "Clear" couldn't -// remove a toolbar-set token). This test locks the toolbar onto the SAME -// canonical app-store endpoint Credentials uses (/api/settings/hf-token), and -// guards against a regression back to /system/set-env. - -const apiPost = vi.fn(() => Promise.resolve({ active: 'app', sources: [] })); -vi.mock('../api/client', () => ({ - apiPost: (...a) => apiPost(...a), - apiFetch: vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve({}) })), -})); - -const refetch = vi.fn(); -vi.mock('../api/hooks', () => ({ - useModels: () => ({ - data: { - models: [], - total_installed_bytes: 0, - disk_free_gb: 42.5, - hf_cache_dir: '/home/u/.cache/huggingface', - }, - isLoading: false, - refetch, - }), - useRecommendations: () => ({ data: null, refetch }), - useInstallModel: () => ({ mutateAsync: vi.fn() }), - useDeleteModel: () => ({ mutateAsync: vi.fn() }), -})); - -vi.mock('../api/setup', () => ({ - setupDownloadStreamUrl: () => 'http://localhost/stream', - cancelInstallModel: vi.fn(), -})); -vi.mock('../api/external', () => ({ openExternal: vi.fn() })); -// Keep the render light + focused on the toolbar. -vi.mock('../components/settings/models/ModelsTable', () => ({ default: () => null })); -vi.mock('../components/settings/models/RecoBanner', () => ({ default: () => null })); - -import ModelStoreTab from '../components/settings/ModelStoreTab'; - -function withI18n(node) { - return {node}; -} - -describe('Model Store toolbar — HF token saves to the canonical app store', () => { - beforeEach(() => { - apiPost.mockClear(); - global.EventSource = class { - constructor() { - this.onmessage = null; - } - close() {} - }; - }); - afterEach(() => vi.restoreAllMocks()); - - async function openAndSave(token) { - render(withI18n()); - // The compact toolbar shows a "HF Token" trigger when no token is set. - fireEvent.click(screen.getByRole('button', { name: /HF Token/i })); - const input = screen.getByPlaceholderText(/hf_/i); - fireEvent.change(input, { target: { value: token } }); - fireEvent.click(screen.getByRole('button', { name: i18n.t('common.save') })); - } - - it('POSTs the token to /api/settings/hf-token (same store as Credentials)', async () => { - await openAndSave('hf_abc123'); - await waitFor(() => expect(apiPost).toHaveBeenCalledTimes(1)); - expect(apiPost).toHaveBeenCalledWith('/api/settings/hf-token', { token: 'hf_abc123' }); - }); - - it('never routes the token through the legacy /system/set-env store', async () => { - await openAndSave('hf_xyz'); - await waitFor(() => expect(apiPost).toHaveBeenCalled()); - for (const call of apiPost.mock.calls) { - expect(call[0]).not.toContain('/system/set-env'); - } - expect(apiPost).toHaveBeenCalledWith('/api/settings/hf-token', { token: 'hf_xyz' }); - }); -});