diff --git a/core/http/react-ui/src/router.jsx b/core/http/react-ui/src/router.jsx
index 48073d8b0..29f892e28 100644
--- a/core/http/react-ui/src/router.jsx
+++ b/core/http/react-ui/src/router.jsx
@@ -213,7 +213,7 @@ function MovedPage({ addon, page, withModel, children }) {
// The list is read only for a user the server lets see add-ons.
useEffect(() => (allowed ? acquire() : undefined), [acquire, allowed])
const app = apps.find(a => a.id === addon)
- if (appStatus(app) === 'ready' && app.ui.pages.includes(page)) {
+ if (appStatus(app) === 'ready' && app.ui.pages.includes(page.split('/')[0])) {
const params = new URLSearchParams(search)
if (withModel && model) params.set('model', model)
const query = params.toString()
@@ -306,9 +306,9 @@ const appChildren = [
{ path: 'quantize', element: }> },
{ path: 'face', element: }> },
{ path: 'face/:model', element: }> },
- { path: 'voice', element: }> },
- { path: 'voice/:model', element: }> },
- { path: 'voice-library', element: },
+ { path: 'voice', element: }> },
+ { path: 'voice/:model', element: }> },
+ { path: 'voice-library', element: },
{ path: 'import-model', element: },
],
},
@@ -363,7 +363,7 @@ const appChildren = [
children: [{ path: ':id', element: }],
},
{ path: 'manage', element: },
- { path: 'voice-library/new', element: },
+ { path: 'voice-library/new', element: },
{ path: 'model-editor', element: },
{ path: 'model-editor/:name', element: },
{ path: '*', element: },
diff --git a/core/http/react-ui/src/utils/legacyRoutes.js b/core/http/react-ui/src/utils/legacyRoutes.js
index cd2818dbb..7080d8b3f 100644
--- a/core/http/react-ui/src/utils/legacyRoutes.js
+++ b/core/http/react-ui/src/utils/legacyRoutes.js
@@ -58,9 +58,9 @@ export const MIGRATION_ROUTES = Object.freeze([
{ from: '/app/diarization', to: '/app/addons/studio/studio/diarization', kind: 'studio-tab' },
{ from: '/app/transform', to: '/app/addons/studio/studio/transform', kind: 'studio-tab' },
{ from: '/app/face', to: '/app/addons/face/recognition', kind: 'addon-page' },
- { from: '/app/voice', to: '/app/addons/voice/recognition', kind: 'addon-page' },
- { from: '/app/voice-library', to: '/app/addons/voice/library', kind: 'addon-page' },
- { from: '/app/voice-library/new', to: '/app/addons/voice/create', kind: 'addon-page' },
+ { from: '/app/voice', to: '/app/addons/voice/voice/recognition', kind: 'addon-page' },
+ { from: '/app/voice-library', to: '/app/addons/voice/voice/library', kind: 'addon-page' },
+ { from: '/app/voice-library/new', to: '/app/addons/voice/voice/library/new', kind: 'addon-page' },
{ from: '/app/fine-tune', to: '/app/addons/fine-tuning/jobs', kind: 'addon-page' },
{ from: '/app/quantize', to: '/app/addons/quantization/jobs', kind: 'addon-page' },
])
diff --git a/docs/content/features/addons.md b/docs/content/features/addons.md
index 472208643..99550bda8 100644
--- a/docs/content/features/addons.md
+++ b/docs/content/features/addons.md
@@ -1741,17 +1741,17 @@ query string and the hash:
| `/app/transform` | `/app/addons/studio/studio/transform` |
| `/app/face` | `/app/addons/face/recognition` |
| `/app/face/` | `/app/addons/face/recognition?model=` |
-| `/app/voice` | `/app/addons/voice/recognition` |
-| `/app/voice/` | `/app/addons/voice/recognition?model=` |
-| `/app/voice-library` | `/app/addons/voice/library` |
-| `/app/voice-library/new` | `/app/addons/voice/create` |
+| `/app/voice` | `/app/addons/voice/voice/recognition` |
+| `/app/voice/` | `/app/addons/voice/voice/recognition?model=` |
+| `/app/voice-library` | `/app/addons/voice/voice/library` |
+| `/app/voice-library/new` | `/app/addons/voice/voice/library/new` |
The two face links and the four voice links open their add-on only while it is installed and usable. Without
it, they open the same pages in the core web UI, so nobody loses a page they can open today.
The Studio and Talk links have no core page to fall back to: without the add-on they open
the add-on page, which says the add-on is not available.
A model id can hold `/` or `:`, which an add-on path segment refuses, so the model travels
-as `?model=` (for example `/app/voice/org%2Fname` opens `/app/addons/voice/recognition?model=org%2Fname`; the face links and the
+as `?model=` (for example `/app/voice/org%2Fname` opens `/app/addons/voice/voice/recognition?model=org%2Fname`; the face links and the
Studio links do the same).
A link with a model (`/app/image/`) opens `/app/addons/studio/studio/images?model=`. The
@@ -1806,12 +1806,19 @@ The Voice add-on (`voice`) is a packaged add-on. It is not shipped with LocalAI:
from **Browse add-ons** in the drawer and start it. Once it is usable, it appears in the
drawer and can be pinned. The Build hub has no Voices tab.
-| Page | Path | Who sees it |
+The add-on has one page, `/app/addons/voice/voice`, and one entry in the drawer. Its
+sections are tabs of that page, and the tab is the next segment of the address:
+
+| Tab | Path | Who sees it |
|---|---|---|
-| Recognition (main page) | `/app/addons/voice/recognition` | users with the `voice_recognition` feature; the page shows an off notice for anyone else |
-| Speech (text to speech) | `/app/addons/voice/speech` | users with the `addons` feature; the server refuses the speech call without `audio_speech` |
-| Library | `/app/addons/voice/library` | administrators |
-| Create | `/app/addons/voice/create` | administrators |
+| Recognition (default) | `/app/addons/voice/voice/recognition` | users with the `voice_recognition` feature; the tab shows an off notice for anyone else |
+| Library | `/app/addons/voice/voice/library` (the form to add a voice is `.../library/new`) | administrators |
+| Text to speech | `/app/addons/voice/voice/speech` | users with the `addons` feature; the server refuses the speech call without `audio_speech` |
+| From a recording | `/app/addons/voice/voice/recording` | users with the `addons` feature |
+
+The earlier page addresses (`/app/addons/voice/recognition`, `.../library`, `.../create`
+and `.../speech`) still open: each one redirects to its tab and keeps the query string
+(`?model=`, `?selected=`, `?voice=`).
All of these also need the `addons` feature to see the tile. The pages call core routes
with the signed-in user's own session (see "Core routes from add-ons"): `/v1/voice/*` for
@@ -1820,11 +1827,12 @@ for the library and `/v1/audio/speech` for the speech page. The core keeps the v
registry and these routes; the server checks every call again, so an add-on page never
widens what a user may do.
-The recognition page has three tabs. **Speakers** is the workbench: who is this, same
-person, the people this browser enrolled, and the analyze and embedding tools under **More
-tools**. **Speech voices** opens the library page and is shown to administrators only.
-**From a recording** links to the diarization workspace of Studio. The Face add-on shares
-the same views, built from one source directory into each add-on.
+The **Recognition** tab is the workbench: who is this, same person, the people this
+browser enrolled, and the analyze and embedding tools under **More tools**. **Library**
+manages the reference recordings that text-to-speech can use and is shown to
+administrators only. **Text to speech** speaks text with a voice model. **From a
+recording** links to the diarization workspace of Studio. The Face add-on shares the same
+views, built from one source directory into each add-on.
### The Face add-on
diff --git a/docs/content/features/text-to-audio.md b/docs/content/features/text-to-audio.md
index ad2b97307..180d4eaea 100644
--- a/docs/content/features/text-to-audio.md
+++ b/docs/content/features/text-to-audio.md
@@ -80,7 +80,7 @@ this control may ignore it.
## Voice Library
-Administrators can manage reusable voice-cloning references from **Speech voices** on the Recognition page of the Voice add-on (**Add-ons → Voice**, or the **Library** page of the same add-on) in the LocalAI WebUI. (Speakers, the first tab of that page, is a separate feature: voiceprints for recognising who is speaking. See [Voice recognition](/features/voice-recognition/).) The library replaces per-model filesystem and YAML setup for supported cloning backends:
+Administrators can manage reusable voice-cloning references from the **Library** tab of the Voice add-on (**Add-ons → Voice**) in the LocalAI WebUI. (Recognition, the first tab of that page, is a separate feature: voiceprints for recognising who is speaking. See [Voice recognition](/features/voice-recognition/).) The library replaces per-model filesystem and YAML setup for supported cloning backends:
1. Select **Add a speech voice** and upload or record a clear reference clip. A speech voice can also be made while enrolling a speaker, from the enrol sheet.
2. Enter the exact words spoken in the clip. Add more audio/transcript pairs when the personality needs more examples.
diff --git a/docs/content/features/voice-recognition.md b/docs/content/features/voice-recognition.md
index 8a98338b2..4a935cfa3 100644
--- a/docs/content/features/voice-recognition.md
+++ b/docs/content/features/voice-recognition.md
@@ -559,9 +559,9 @@ default only applies when omitted.
## The WebUI page
-The page is the **Recognition** page of the Voice add-on (**Add-ons → Voice**, `/app/addons/voice/recognition`). Install the add-on from **Browse add-ons** and start it; the old `/app/voice` link opens the same page in the core web UI until then. The page calls the `/v1/voice/*` routes described on this page with your own session. It has three tabs. **Speakers** is this feature. **Speech voices** is the [Voice Library](/features/text-to-audio/#voice-library) for text-to-speech, a different store with its own recordings and transcripts. **From a recording** links to the diarization workspace, where a speaker can be named and remembered.
+The page is the **Recognition** tab of the Voice add-on (**Add-ons → Voice**, `/app/addons/voice/voice/recognition`). Install the add-on from **Browse add-ons** and start it; the old `/app/voice` link opens the same page in the core web UI until then. The page calls the `/v1/voice/*` routes described on this page with your own session. The Voice page has four tabs. **Recognition** is this feature. **Library** is the [Voice Library](/features/text-to-audio/#voice-library) for text-to-speech, a different store with its own recordings and transcripts. **Text to speech** speaks text with a voice model. **From a recording** links to the diarization workspace, where a speaker can be named and remembered.
-On Speakers:
+On the Recognition tab:
- **Who is this** matches a clip against the people you enrolled (`POST /v1/voice/identify`). The answer is a sentence with the real distance and the cut-off, a word for how far inside the cut-off it sits (strong, likely, close call), and a scale with the cut-off drawn on it. The cut-off slider re-reads the answer in the browser; it does not call the server again. The page sends the cut-off in the request, 0.25 by default.
- **Same person?** compares two clips (`POST /v1/voice/verify`) and uses the threshold the model returns. The word is not a probability, and the page says so.
diff --git a/docs/superpowers/specs/2026-10-16-addons-navigation-design.md b/docs/superpowers/specs/2026-10-16-addons-navigation-design.md
index e921b43f9..e8cdf2fc4 100644
--- a/docs/superpowers/specs/2026-10-16-addons-navigation-design.md
+++ b/docs/superpowers/specs/2026-10-16-addons-navigation-design.md
@@ -338,10 +338,10 @@ Routes below come from `core/http/react-ui/src/utils/destinations.js`, `core/htt
| `/app/fine-tune` | Build hub | Fine-tuning add-on, page `jobs` (`/app/addons/fine-tuning/jobs`) while it is installed and running; the core page otherwise. |
| `/app/quantize` | Build hub | Quantization add-on, page `jobs` (`/app/addons/quantization/jobs`) while it is installed and running; the core page otherwise. |
| `/app/face`, `/app/face/:model` | Build hub | Face add-on, page `recognition` (`/app/addons/face/recognition`) while it is installed and running; the core page otherwise. The model travels as `?model=`. |
-| `/app/voice`, `/app/voice/:model` | Build hub | Voice add-on, page `recognition` (`/app/addons/voice/recognition`) while it is installed and running; the core page otherwise. The model travels as `?model=`. |
-| `/app/voice-library` | Build hub (match) | Voice add-on, page `library` (`/app/addons/voice/library`) while it is installed and running; the core page otherwise. Whether it also has a drawer tile of its own is open, see question (c). |
-| `/app/voice-library/new` | page | Voice add-on, page `create` (`/app/addons/voice/create`) while it is installed and running; the core page otherwise. |
-| (new) | none | Voice add-on, page `speech`: text-to-speech generation. No old route. |
+| `/app/voice`, `/app/voice/:model` | Build hub | Voice add-on, page `voice`, tab `recognition` (`/app/addons/voice/voice/recognition`) while it is installed and running; the core page otherwise. The model travels as `?model=`. |
+| `/app/voice-library` | Build hub (match) | Voice add-on, page `voice`, tab `library` (`/app/addons/voice/voice/library`) while it is installed and running; the core page otherwise. Whether it also has a drawer tile of its own is open, see question (c). |
+| `/app/voice-library/new` | page | Voice add-on, page `voice`, tab `library`, view `new` (`/app/addons/voice/voice/library/new`) while it is installed and running; the core page otherwise. |
+| (new) | none | Voice add-on, page `voice`, tab `speech`: text-to-speech generation. No old route. The Voice add-on is one page with tabs; the older page names stay declared and redirect to their tab. |
| `/app/addons` | Build hub tab | Add-ons (top level), drawer page. |
| `/app/addons/:addonId/*` | add-on pages | unchanged |
| `/app/build`, `/app/agents`, `/app/skills`, `/app/collections`, `/app/agent-jobs`, `/app/import-model` | Build hub | unchanged; out of scope |