feat(ui): send the old voice routes to the Voice tabs

/app/voice, /app/voice-library and /app/voice-library/new now open the
matching tab of the Voice add-on page while the add-on is installed and
running, and the core pages otherwise. The moved-page helper compares
the first segment of the target with the declared pages, so a target
can name a tab. The docs and the navigation spec list the new addresses.

Assisted-by: Claude Code:claude-sonnet-5-5 node
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto committed 2026-10-20 23:18:08 +00:00
1 parent 7b7cde274f
commit ee1c02929e
6 files changed
+38 -30

No files matched your search

+5 -5
View File
@@ -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: <MovedPage addon="quantization" page="jobs"><Feature feature="quantization" disabled={<ToolOff tool="quantize" />}><Quantize /></Feature></MovedPage> },
{ path: 'face', element: <MovedPage addon="face" page="recognition"><Feature feature="face_recognition" disabled={<FeatureOff feature="face" />}><FaceRecognition /></Feature></MovedPage> },
{ path: 'face/:model', element: <MovedPage addon="face" page="recognition" withModel><Feature feature="face_recognition" disabled={<FeatureOff feature="face" />}><FaceRecognition /></Feature></MovedPage> },
{ path: 'voice', element: <MovedPage addon="voice" page="recognition"><Feature feature="voice_recognition" disabled={<FeatureOff feature="voice" />}><VoiceRecognition /></Feature></MovedPage> },
{ path: 'voice/:model', element: <MovedPage addon="voice" page="recognition" withModel><Feature feature="voice_recognition" disabled={<FeatureOff feature="voice" />}><VoiceRecognition /></Feature></MovedPage> },
{ path: 'voice-library', element: <Admin><MovedPage addon="voice" page="library"><VoiceRecognition tab="speech" /></MovedPage></Admin> },
{ path: 'voice', element: <MovedPage addon="voice" page="voice/recognition"><Feature feature="voice_recognition" disabled={<FeatureOff feature="voice" />}><VoiceRecognition /></Feature></MovedPage> },
{ path: 'voice/:model', element: <MovedPage addon="voice" page="voice/recognition" withModel><Feature feature="voice_recognition" disabled={<FeatureOff feature="voice" />}><VoiceRecognition /></Feature></MovedPage> },
{ path: 'voice-library', element: <Admin><MovedPage addon="voice" page="voice/library"><VoiceRecognition tab="speech" /></MovedPage></Admin> },
{ path: 'import-model', element: <Admin><ImportModel /></Admin> },
],
},
@@ -363,7 +363,7 @@ const appChildren = [
children: [{ path: ':id', element: <ModelPage /> }],
},
{ path: 'manage', element: <Admin><ManageRedirect /></Admin> },
{ path: 'voice-library/new', element: <Admin><MovedPage addon="voice" page="create"><VoiceProfileCreate /></MovedPage></Admin> },
{ path: 'voice-library/new', element: <Admin><MovedPage addon="voice" page="voice/library/new"><VoiceProfileCreate /></MovedPage></Admin> },
{ path: 'model-editor', element: <Admin><ModelEditor /></Admin> },
{ path: 'model-editor/:name', element: <Admin><ModelEditor /></Admin> },
{ path: '*', element: <NotFound /> },
+3 -3
View File
@@ -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' },
])
+23 -15
View File
@@ -1741,17 +1741,17 @@ query string and the hash:
| `/app/transform` | `/app/addons/studio/studio/transform` |
| `/app/face` | `/app/addons/face/recognition` |
| `/app/face/<model>` | `/app/addons/face/recognition?model=<model>` |
| `/app/voice` | `/app/addons/voice/recognition` |
| `/app/voice/<model>` | `/app/addons/voice/recognition?model=<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/<model>` | `/app/addons/voice/voice/recognition?model=<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/<model>`) opens `/app/addons/studio/studio/images?model=<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
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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.
@@ -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 |