8000 feat(voice): read answers and podcasts aloud with Kokoro on this machine by adeelahmad · Pull Request #538 · adeelahmad/lense · GitHub
[go: up one dir, main page]

Skip to content

feat(voice): read answers and podcasts aloud with Kokoro on this machine - #538

Merged
adeelahmad merged 24 commits into
mainfrom
claude/project-thread-hky2fo
Oct 18, 2026
Merged

adeelahmad merged 24 commits into
mainfrom
claude/project-thread-hky2fo

Conversation

@adeelahmad
@adeelahmad adeelahmad commented Oct 18, 2026 •
Copy link
Copy Markdown
Owner

Requested by Adeel · project thread

Description

Before: spoken answers and podcast episodes needed a speech provider (OpenAI-compatible, ElevenLabs, Deepgram, Gemini), a Mac for say, or the separate StyleTTS2 service; a fully local install with no extra container had no voice.

After: Kokoro (54 voices, nine languages) is a text-to-speech provider in Settings → AI assistant → Voice. Pick it and save: a worker fetches it in the background (Settings → Components shows how far it got), then answers and podcast hosts are read on the server's own CPU, with nothing sent anywhere and nothing billed. The voice picker lists all 54 voices before the fetch, with a Hear button; the model size (full 326 MB or 8-bit 114 MB) is a setting. A podcast's second host gets a voice in the first host's language (am_michael beside af_heart, em_alex beside ef_dora) unless podcasts.voice_b names one.

How: kokoro.py runs Kokoro v1.0 (hexgrad's Kokoro-82M, Apache-2.0) through kokoro-onnx (MIT) on ONNX Runtime, in-process, as voice.tts_provider kokoro; the model is loaded on first use and let go after ten minutes of quiet. components.py gains a Kokoro component that fetches the kokoro extra at uv.lock's versions and the model and voices files from kokoro-onnx's model-files-v1.1 release, checked against their SHA-256, as soon as the provider is picked; podcast jobs wait while it's being fetched. Each read is a model.speech row in the activity ledger (kokoro:kokoro-v1.0).

espeak-ng and phonemizer (which kokoro-onnx phonemizes with) are GPL-3.0, so they live only in the opt-in kokoro extra, never in the default install.

Motivation and Context

Adeel asked for Kokoro v1.0's model and voice assets for TTS and podcast generation, "making it 100% local supported". StyleTTS2 already read locally but as its own 2 GB service; Kokoro is a 330 MB download into the data folder with no service to start, so a Raspberry Pi with cloud models can read aloud on its own.

Screenshots (if appropriate):

n/a (a select and a paragraph in the existing Voice settings).

Steps to reproduce (if appropriate):

  1. Settings → AI assistant → Voice → Spoken answers by → Kokoro, Save.
  2. Settings → Components: Kokoro goes fetching → ready (about 450 MB in all).
  3. Back in Voice, pick a voice and press Hear; ask the assistant something by voice.
  4. Library → pick an asset → Podcast: the episode is read in two Kokoro voices.

Measured (one 5-second sentence, fp32): 2.8 s on a 4-core x64 container (int8 4.6 s), 3.2 s on GitHub's arm64 runner in kokoro.yml, loading included; on x64 loading the model takes 1.8–5 s. kokoro-onnx's fp16 export isn't offered: on arm64, ONNX Runtime's CPU provider read nothing with it (an empty waveform), and on x64 it was no faster than fp32.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change; ! in the title)

Checklist:

  • The title is a Conventional Commit.
  • My code follows the code style of this project (make hooks checks it on commit).
  • I added or updated tests.
  • My change requires documentation updates, and I have updated docs/ accordingly.
  • Users need more than the title to understand it, so I wrote it under ## Unreleased in CHANGELOG.md.

Done questions (AGENTS.md)

  • Goal: Kokoro reads answers and podcasts fully locally; merged on green CI.
  • Agents: PUT /api/v1/settings/voice (tts_provider, kokoro_model, tts_voice) and the assistant's change_settings tool pick it; GET /api/v1/voice/voices?provider=kokoro lists the voices; POST /api/v1/voice/speak reads; podcasts through the API and MCP as before.
  • Zero setup: nothing new to install; picking it fetches the extra and the files into the data folder.
  • Fewest clicks: one select and Save; the voice list is there before the fetch; the second host's voice is chosen for you.
  • Configurable: voice.tts_provider, voice.kokoro_model (fp32, the default, or int8) and voice.tts_voice in the web app; nothing in the setup wizard.
  • Light: off by default (the provider stays openai); about 450 MB fetched only when picked; int8 for a small disk; the model is unloaded after ten minutes of quiet.
  • Private and secure: text never leaves the machine; files are pinned by SHA-256; threat-model.md has a row for it (the voices .npz is read with pickles off).
  • Cost: no model provider call; each read is a model.speech ledger row.
  • User decides: existing defaults unchanged; nothing happens until an admin picks it.
  • Upgrades: n/a, no stored-data change (voice.kokoro_model is a new setting with a default).
  • Honest: built; the README, docs and requirements say Kokoro is opt-in and fetched once picked; the licenses are named in Components.
  • Tests: tests/domain/test_kokoro.py (7, on a tiny Kokoro-shaped ONNX model so no download), settings-kokoro.test.tsx, a Kokoro case in settings-voice-picker.test.tsx; kokoro.yml fetches the real files and reads with both models on x64 and arm64; make openapi regenerated; mkdocs build --strict clean.
  • Open: the model is Apache-2.0 and kokoro-onnx MIT; the GPL-3.0 espeak-ng and phonemizer are in the opt-in extra only, so Lens stays MIT.
  • Reported in the thread with timings; refine-later items (misaki G2P, speed, voice blends, streaming, hosts' voices in the podcast dialog) are in DEVLOG.md.
  • Reuse: kokoro-onnx and ONNX Runtime do the reading; components.py's fetch-by-URL-with-checksum, speech.py's provider dispatch, the voice picker and the podcast audio step are reused unchanged in shape.

Local CI (ci/local-ci.sh on 3ded53f, main through #534 merged in; no Docker here, so the SurrealDB-server and upgrade jobs run on GitHub):

Job Result Time
FastAPI (embedded SurrealDB) pass 985s
Next.js pass 566s
pre-commit pass 117s
PR title pass 0s

After main through #549 was merged in (a changelog-only conflict), the Kokoro, podcast, speech and components backend tests, the voice settings frontend tests, ci/check_requirements.py, ci/validate_threat_model.py --base origin/main and uv lock --check were run again. The worker's own fetch path was replayed earlier against a bare venv with only the base dependencies: components.pip_install(cfg, "kokoro") put 10 packages into data_dir/python, kokoro_onnx imported from there, and Kokoro read an English and a Spanish sentence.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH

Kokoro v1.0 (hexgrad's Kokoro-82M, Apache-2.0) as a text-to-speech provider, voice.tts_provider "kokoro": spoken
answers and podcast hosts are read on the server's own CPU through kokoro-onnx on ONNX Runtime, in any of its 54
voices across nine languages, with nothing sent anywhere and nothing billed. It's opt-in and fetched once it's picked:
the `kokoro` extra (espeak-ng and phonemizer are GPL-3.0, so they stay out of the default install), and the model
(voice.kokoro_model: fp32, fp16 or int8) and voices files from kokoro-onnx's release, checked against their SHA-256.
Settings → AI assistant → Voice offers it with the model size and the voice picker; a podcast's second host gets a
voice in the first host's language. kokoro.yml fetches and reads with each model on x64 and arm64.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
main's conversation titles (#531) added ai.auto_title to the section the Kokoro settings test renders, so its fixture
names it too; without it Save sent an ai update beside the voice one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
@adeelahmad adeelahmad self-assigned this Oct 18, 2026
Keeps both the Kokoro and the Moondream components, both changelog, devlog and requirements entries, and the README sentence with Kokoro and read-any-message-aloud; uv.lock's provides-extras lists both extras.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
The threat model workflow's validate job wants a change to threat-model.md to change a companion too (#507): the Kokoro row is documented, so the JSON's documented count goes up by one, as #518 did for Moondream.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
On GitHub's arm64 runner, ONNX Runtime's CPU provider returned an empty waveform for kokoro-onnx's fp16 export, and kokoro-onnx's pause insertion then failed on a zero-size array. On x64 it was no faster than fp32, so voice.kokoro_model is now fp32 (the default) or int8, and the fp16 file is no longer pinned.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
Keeps both changelog entries (Kokoro and deep research) and counts the Kokoro threat-model row on top of main's documented count (255, now 256).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
…ture

Main's deep research (#517) added a research section to Settings › AI assistant, so the fixture carries its defaults and Save sends only the voice change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
Keeps both changelog entries (Kokoro and the upgrade agent starting the code sandbox's gateway).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
Keeps Kokoro in the README's chat sentence alongside main's automatic summarising of long conversations.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
Main added ai.compact_at (summarise long conversations early), so the fixture carries its default and Save sends only the voice change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
Copy link
Copy Markdown
Owner Author

Clicks and keystrokes per feature is red on a02d70a for one journey only: chat-ask on the phone run timed out waiting for the answer (locator.waitFor: Timeout 60000ms). Every other journey passed, including chat-ask on the computer run of the same job. The same phone chat-ask passed on this PR's previous run, and this PR changes no chat UI or chat API: it adds a speech provider that is off by default, so the clicks run never loads Kokoro. Other PRs on the same main (#560, #561, #562) are green on this check.

So this looks like a slow answer on the runner, not this PR's. I'm re-running the failed job once; if it fails again I'll treat it as real and dig in.


Generated by Claude Code

claude added 13 commits October 18, 2026 07:29
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
…is released

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
…entry as a changelog fragment

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
…-hky2fo

# Conflicts:
#	fastapi_backend/app/domain/speech.py
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
…-hky2fo

# Conflicts:
#	docs/components.md
#	fastapi_backend/pyproject.toml
#	fastapi_backend/uv.lock
#	threat-model.json
…-hky2fo

# Conflicts:
#	fastapi_backend/uv.lock
…-hky2fo

# Conflicts:
#	README.md
#	fastapi_backend/pyproject.toml
#	fastapi_backend/uv.lock
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzm4YHdHn2qZy7aXer3KeH
@adeelahmad
adeelahmad merged commit 2deac7a into main Oct 18, 2026
23 checks passed
@adeelahmad
adeelahmad deleted the claude/project-thread-hky2fo branch October 18, 2026 15:18
adeelahmad pushed a commit that referenced this pull request Oct 18, 2026
…swers setting

#622 added ai.answer_comments after #538's test was written, so the test's view
lacked it and Save also saved the AI section, failing on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QFUNzy1QZy5HhKAyJxTAYJ
adeelahmad pushed a commit that referenced this pull request Oct 18, 2026
#622 added ai.answer_comments (answer when tagged in a comment) after #538
wrote the Kokoro settings test's view of the section, so Save sent an ai
update beside the voice one and main's Next.js check went red at 2deac7a.
The fixture names the default, the same line #621 and #624 carry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NY27LFMbLezbvQQQeHT19a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0