From ee1c02929e986365934ea825ef503b4fdfde0238 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Tue, 20 Oct 2026 23:17:46 +0000 Subject: [PATCH] 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 --- core/http/react-ui/src/router.jsx | 10 ++--- core/http/react-ui/src/utils/legacyRoutes.js | 6 +-- docs/content/features/addons.md | 38 +++++++++++-------- docs/content/features/text-to-audio.md | 2 +- docs/content/features/voice-recognition.md | 4 +- .../2026-10-16-addons-navigation-design.md | 8 ++-- 6 files changed, 38 insertions(+), 30 deletions(-) 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 |