docs(openwebui): the browser-kokoro switch is the user's settings row, not browser storage

`wilwheaton` kept failing in OpenWebUI with the 29-name Kokoro enum even
after the dropdown was set to Default. The page's 2026-10-18 conclusion —
"no user.settings row carries an audio override, so the choice lives in that
browser's own storage" — was a probe-depth bug: the personal audio settings
live in table `user`, and the 0.12.x front end nests them under `ui`
(`settings.ui.audio.tts.engine`). The row did carry `browser-kokoro`, and the
SPA re-applies the server copy on every load, so the UI fix never stuck.

Corrected facts on the owner page:
- where the override really lives, and the version-dependent nesting;
- repair BOTH engine and voice: engine "" still sends the stored voice, so a
  Kokoro-era `voice: "alloy"` returns as frank-tts's own 400;
- the one-line tells: an `/wasm/ort-wasm-simd-threaded.jsep.wasm` fetch means
  Kokoro.js is live in that browser, and uvicorn logs every request so a
  missing `/api/v1/audio/speech` line is evidence;
- deployed build is 0.12.5 (pin `0b399a8b`, bumped 2026-10-30) — the page
  still claimed 0.11.4 — and the 401 paragraph's duplicated sentence is gone.

Verified 2026-11-04: with the row repaired, `POST /api/v1/audio/speech` as a
real user over the vhost returns 200 audio/mpeg and frank-tts logs
`speech voice=wilwheaton`.
This commit is contained in:
Frank committed 2026-11-04 12:09:57 -08:00
1 parent 7e9bfbc2be
commit 64b06b9426
2 files changed
+55 -28

No files matched your search

+54 -27
View File
@@ -21,7 +21,7 @@ https://github.com/open-webui/open-webui
| Item | Value |
|------|-------|
| 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. |
| 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) |
@@ -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
2026-10-07 from loopback `:3005` and over the public vhost:
`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
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
no keyless access. Re-verified 2026-10-18 from loopback: `/health` → `{"status":true}`
`/api/version` → 0.11.4, `/api/v1/models/list`, `/api/v1/models` and
`POST /api/chat/completions` → 401; `/api/config` additionally publishes
no keyless access. Re-measured 2026-11-04 from loopback: `/api/version` → `0.12.5`,
and `/api/config` publishes
`features.enable_login_form=false`, `features.enable_signup=false` and
`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.
**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)
ships an in-page Kokoro implementation (chunk carrying a frozen voice table
`{af_heart:{name:"Heart",…}}` and `_validate_voice`, model
`onnx-community/Kokoro-82M-v1.0-ONNX`, plus a personal *Kokoro.js dtype*
setting). Its engine select takes `""` (Default) or `browser-kokoro`; when
`browser-kokoro` is chosen the page synthesizes locally and rejects any
voice outside its fixed pack — so `wilwheaton` fails with
`Voice: "wilwheaton" not found. Should be one of: af_heart, …` **without a
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.
to **Kokoro.js running in the browser**: the deployed front end ships an
in-page Kokoro implementation (frozen voice table plus `_validate_voice`,
model `onnx-community/Kokoro-82M-v1.0-ONNX`, and a personal *Kokoro.js dtype*
setting). Its engine select takes `""` (Default) or `browser-kokoro`; with
`browser-kokoro` the page synthesizes locally and rejects any voice outside
its fixed pack, so `wilwheaton` fails with `Voice: "wilwheaton" not found.
Should be one of: af_heart, …` **without a single request leaving the
browser**. Kokoro cannot clone — fixed pack only.
Verified 2026-10-18 from inside the container with the exact OpenWebUI body
(`{"model":"frank-tts","input":…,"voice":"narrator1",
"response_format":"mp3","speed":1.0}`) → **200, `audio/mpeg`, 7,629 B**,
while `podman logs open-webui` recorded **zero** `audio/speech` requests for
the failing click — that contrast is the instrument: a server-path failure
leaves a log line and a frank-tts error, a `browser-kokoro` failure leaves
neither.
**The switch is a DB row, not browser storage.** Personal audio settings are
the `settings` JSON column of table `user`, and in 0.12.x they nest under
`ui`: `ui.audio.tts.engine`, `.voice`, `.defaultVoice`. In 0.11.x the same
keys sat at the top level (`audio.tts.*`), and the 2026-10-18 probe listed
only top-level keys, found no audio key, and concluded the choice lived in
that browser's own storage — a probe-depth bug. The row carried the override
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