Merge pull request 'docs(openwebui): the browser-kokoro switch is the user's settings row, not browser storage' (#141) from docs/openwebui-tts-kokoro-user-row into main

This commit was merged in pull request #141.
This commit is contained in:
dank committed 2026-11-04 12:14:35 -08:00
commit 085a5d570c
2 files changed
+55 -28

No files matched your search

+1 -1
View File
@@ -222,5 +222,5 @@ harness bundle and measured the day of the flip:
| Endpoint answers, synthesis fails | the engine: `podman healthcheck run frank-tts-engine`, then `systemctl --user status frank-tts-engine.service` | | Endpoint answers, synthesis fails | the engine: `podman healthcheck run frank-tts-engine`, then `systemctl --user status frank-tts-engine.service` |
| First request of the day is ~0.6 s slower than the rest — or every name returns 400 | the warm pass did not run; it carries `-`, so it cannot fail the unit. `journalctl --user -u frank-tts-engine` for the warmer's lines, then `podman exec frank-tts-engine /app/warm-voices.sh` — the registry is memory-only (law 1 of [[Frank TTS Engine]]) | | First request of the day is ~0.6 s slower than the rest — or every name returns 400 | the warm pass did not run; it carries `-`, so it cannot fail the unit. `journalctl --user -u frank-tts-engine` for the warmer's lines, then `podman exec frank-tts-engine /app/warm-voices.sh` — the registry is memory-only (law 1 of [[Frank TTS Engine]]) |
| The unit never appears after a deploy | the generator dropped it: `frank-tts-engine.network` is missing from `~/.config/containers/systemd/` | | The unit never appears after a deploy | the generator dropped it: `frank-tts-engine.network` is missing from `~/.config/containers/systemd/` |
| A consumer rejects `wilwheaton` by listing 29 names (`af_heart … bm_fable`) and no request ever reaches this endpoint | that consumer is synthesizing **in the browser** with Kokoro.js, whose voice pack is fixed — OpenWebUI 0.11.4 ships it as the `browser-kokoro` TTS engine; see [[OpenWebUI]] | | A consumer rejects `wilwheaton` by listing 29 names (`af_heart … bm_fable`) and no request ever reaches this endpoint | that consumer is synthesizing **in the browser** with Kokoro.js, whose voice pack is fixed. In OpenWebUI the `browser-kokoro` choice is that user's own `user.settings` row (`ui.audio.tts.engine`) and the SPA re-serves it on every load, so flipping the dropdown does not stick; see [[OpenWebUI]] |
--- ---
+54 -27
View File
@@ -21,7 +21,7 @@ https://github.com/open-webui/open-webui
| Item | Value | | Item | Value |
|------|-------| |------|-------|
| Image | `ghcr.io/open-webui/open-webui:main` (verified from the live quadlet unit) | | Image | `ghcr.io/open-webui/open-webui:main` (verified from the live quadlet unit) |
| Deployed build | `0.11.4` — `GET http://127.0.0.1:3005/api/version` → `{"version":"0.11.4"}` (2026-10-18). `:main` is the rolling image, not a stable pin, so the running build lags upstream: releases `0.12.0` (2026-10-10) through `0.12.3` (2026-10-16) exist and are not deployed (https://github.com/open-webui/open-webui/releases). The 2026-09-21 `0.11.4` security/access-control advisory is what pinned this era of the page. Imagegen endpoint is `POST /api/v1/images/generations` (the `comfyui` engine is still upstream). | | Deployed build | `0.12.5` — `GET http://127.0.0.1:3005/api/version` → `{"version":"0.12.5"}` (re-measured 2026-11-04). The repo pin `@sha256:0b399a8b…` (`dankclaw/open-webui/quadlet/open-webui.container`, bumped 2026-10-30) is what makes that build reproducible; `:main` is a rolling tag, so re-measure `/api/version` instead of quoting a build from this page. Imagegen endpoint is `POST /api/v1/images/generations` (the `comfyui` engine is still upstream). |
| Port | 3005 (host) → 8080 (container). Port 3000 is Gitea's HTTP, so it is not usable. | | Port | 3005 (host) → 8080 (container). Port 3000 is Gitea's HTTP, so it is not usable. |
| Data volume | `/home/dank/Containers/open-webui/data` → `/app/backend/data` | | Data volume | `/home/dank/Containers/open-webui/data` → `/app/backend/data` |
| Backends | The picker's content is `webui.db` state, not repo state: as recorded on 2026-10-07 it exposes **one** model, `frank-zealot`, with `frank-dragoon`/`frank-archon` configured but disabled — *unverified 2026-10-18* (the DB is the authority and is not readable from the repo). What the repo does fix is the container's own OpenAI target: `OPENAI_API_BASE_URL=http://192.168.0.143:7770` (`dankclaw/open-webui/quadlet/open-webui.container:32`, same value in the live container env) — see [The LLM backends](#the-llm-backends) | | Backends | The picker's content is `webui.db` state, not repo state: as recorded on 2026-10-07 it exposes **one** model, `frank-zealot`, with `frank-dragoon`/`frank-archon` configured but disabled — *unverified 2026-10-18* (the DB is the authority and is not readable from the repo). What the repo does fix is the container's own OpenAI target: `OPENAI_API_BASE_URL=http://192.168.0.143:7770` (`dankclaw/open-webui/quadlet/open-webui.container:32`, same value in the live container env) — see [The LLM backends](#the-llm-backends) |
@@ -163,14 +163,11 @@ account under Profile → API Keys). Anonymous web visitors get the `pending`
role (`ui.default_user_role=pending`), which cannot chat. Verified role (`ui.default_user_role=pending`), which cannot chat. Verified
2026-10-07 from loopback `:3005` and over the public vhost: 2026-10-07 from loopback `:3005` and over the public vhost:
`GET /api/v1/models/list`, `GET /api/v1/models` and `GET /api/v1/models/list`, `GET /api/v1/models` and
`POST /api/chat/completions` all return **401** with no key (also with a
bogus bearer token); only `/api/config`, `/api/version` and `/health`
`POST /api/chat/completions` all return **401** with no key (also with a bogus bearer `POST /api/chat/completions` all return **401** with no key (also with a bogus bearer
token); only `/api/config`, `/api/version` and `/health` answer publicly, and none of token); only `/api/config`, `/api/version` and `/health` answer publicly, and none of
them expose the model list. An external program needs an account + API key — there is them expose the model list. An external program needs an account + API key — there is
no keyless access. Re-verified 2026-10-18 from loopback: `/health` → `{"status":true}` no keyless access. Re-measured 2026-11-04 from loopback: `/api/version` → `0.12.5`,
`/api/version` → 0.11.4, `/api/v1/models/list`, `/api/v1/models` and and `/api/config` publishes
`POST /api/chat/completions` → 401; `/api/config` additionally publishes
`features.enable_login_form=false`, `features.enable_signup=false` and `features.enable_login_form=false`, `features.enable_signup=false` and
`oauth.providers.oidc="authentik"` — the SSO-only posture, visible from outside. `oauth.providers.oidc="authentik"` — the SSO-only posture, visible from outside.
@@ -305,28 +302,58 @@ port is the rootless quadlet `frank-tts` (this repo:
400 listing the voices that exist, never a silent fallback. 400 listing the voices that exist, never a silent fallback.
**The 29-voice `af_* … bm_fable` list is NOT frank-tts.** That enum belongs **The 29-voice `af_* … bm_fable` list is NOT frank-tts.** That enum belongs
to **Kokoro.js running in the browser**: the deployed front end (0.11.4) to **Kokoro.js running in the browser**: the deployed front end ships an
ships an in-page Kokoro implementation (chunk carrying a frozen voice table in-page Kokoro implementation (frozen voice table plus `_validate_voice`,
`{af_heart:{name:"Heart",…}}` and `_validate_voice`, model model `onnx-community/Kokoro-82M-v1.0-ONNX`, and a personal *Kokoro.js dtype*
`onnx-community/Kokoro-82M-v1.0-ONNX`, plus a personal *Kokoro.js dtype* setting). Its engine select takes `""` (Default) or `browser-kokoro`; with
setting). Its engine select takes `""` (Default) or `browser-kokoro`; when `browser-kokoro` the page synthesizes locally and rejects any voice outside
`browser-kokoro` is chosen the page synthesizes locally and rejects any its fixed pack, so `wilwheaton` fails with `Voice: "wilwheaton" not found.
voice outside its fixed pack — so `wilwheaton` fails with Should be one of: af_heart, …` **without a single request leaving the
`Voice: "wilwheaton" not found. Should be one of: af_heart, …` **without a browser**. Kokoro cannot clone — fixed pack only.
single request reaching the server**. Personal audio settings are
browser: no `user.settings` row and no container env carries an audio
override, yet the toast still appears ⇒ the choice lives in that browser's
own storage. Fix in the UI: Settings → Audio → TTS Engine → **Default**,
which hands synthesis to the admin config (frank-tts); picking `af_heart`
instead keeps Kokoro, and Kokoro cannot clone — fixed pack only.
Verified 2026-10-18 from inside the container with the exact OpenWebUI body **The switch is a DB row, not browser storage.** Personal audio settings are
(`{"model":"frank-tts","input":…,"voice":"narrator1", the `settings` JSON column of table `user`, and in 0.12.x they nest under
"response_format":"mp3","speed":1.0}`) → **200, `audio/mpeg`, 7,629 B**, `ui`: `ui.audio.tts.engine`, `.voice`, `.defaultVoice`. In 0.11.x the same
while `podman logs open-webui` recorded **zero** `audio/speech` requests for keys sat at the top level (`audio.tts.*`), and the 2026-10-18 probe listed
the failing click — that contrast is the instrument: a server-path failure only top-level keys, found no audio key, and concluded the choice lived in
leaves a log line and a frank-tts error, a `browser-kokoro` failure leaves that browser's own storage — a probe-depth bug. The row carried the override
neither. the whole way; the 2026-10-30 pin bump to 0.12.5 moved the keys, not the bug.
The SPA reads that row at load (`GET /api/v1/users/user/settings`) and posts
the **whole** blob back whenever any preference changes
(`POST …/settings/update`, body `{...settings}`). So the dropdown can fix it —
but a second tab still holding the old copy writes `browser-kokoro` straight
back, which is the loop that makes the toast return after it was "fixed". The
user row is the authority here; the admin `config` table was never the problem
(its `audio.tts.engine` has been `openai` throughout). Repair
`settings.ui.audio.tts` with the image's stdlib `sqlite3` (`mode=ro`; quote
identifiers, `user` and `group` are reserved words — there is no `sqlite3` CLI
in the image or on the host), then reload every tab of that account.
**Repair the engine *and* the voice.** With engine `""` the front end still
sends its stored voice in the request body, so a Kokoro-era `voice`/
`defaultVoice` survives the engine fix as frank-tts's own rejection —
measured 2026-11-04: `voice: "alloy"` → **400 `unknown voice 'alloy';
available voices: dr.kern, garrickhagon, jeffhays, vaati, wilwheaton`**. Set
`ui.audio.tts` to `{"engine": "", "voice": "wilwheaton", "defaultVoice":
"wilwheaton"}` and reload. Engine `""` means "the admin config decides", and
the handler fills `voice` from `audio.tts.voice` only when the body omits it.
**Two tells settle the case in one line:**
- a session that fetched `/wasm/ort-wasm-simd-threaded.jsep.wasm` loaded
onnxruntime, so Kokoro.js is running in that browser — the server path
never asks for that file;
- a server-path failure leaves a uvicorn request line for
`/api/v1/audio/speech` **and** a frank-tts log line; a `browser-kokoro`
failure leaves neither. uvicorn logs every request, so silence is evidence.
Verified 2026-11-04: the handler's own outbound shape
(`{"model":"frank-tts","input":…,"voice":"wilwheaton"}` with
`Authorization: Bearer none`, the configured `audio.tts.openai.api_key`) →
**200 `audio/wav`** — a body without `response_format` comes back WAV and
either MIME is browser-playable, so OpenWebUI accepts it. The full route as a
real user over the vhost, `POST /api/v1/audio/speech` → **200 `audio/mpeg`**,
with frank-tts logging `speech voice=wilwheaton`.
## Retired instances ## Retired instances