zst50's picture
Update README.md
878a301 verified
|
Raw
History Blame Contribute Delete
11 kB
> The .litertlm models on this card already use the QAT that is discussed in the blog post. The most popular file, `gemma-4-E2B-it.litertlm`, uses a mixture of int2, int4 and int8 to keep it small, fast and efficient.
https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/discussions/30#6a2c2e1ac539f50158631020
# Voice Chatbot (Android) β€” interruptible, on-device voice chat pipeline
A Kotlin/Compose Android app implementing a fully local, **interruptible**
voice chatbot:
```
mic ─► Silero VAD (sherpa-onnx)
β”‚ endpointing + barge-in
β–Ό
Whisper tiny.en Q4 (whisper.cpp) ──► transcript
β–Ό
LLM (streaming):
β”œβ”€ Local: LiteRT-LM (`.litertlm`, e.g. Qwen3-0.6B)
└─ Remote: OpenAI-compatible `/v1/chat/completions` (SSE)
β–Ό
SentenceSplitter ──► per-sentence VITS TTS (sherpa-onnx + Piper voice)
β–Ό
AudioTrack (playback; barge-in stops it instantly)
```
**Barge-in:** speak while the assistant is talking β†’ playback and generation
stop immediately and your new utterance becomes the next turn.
## Components
| Piece | Runtime | Model | Size |
|---|---|---|---|
| STT | whisper.cpp (JNI, built via NDK/CMake) | `ggml-tiny.en-q4_0.bin` (Q4) | ~45 MB |
| VAD | sherpa-onnx AAR (official prebuilt `sherpa-onnx-1.13.4.aar`) | `silero_vad.onnx` | ~2 MB |
| TTS | sherpa-onnx VITS (Piper) | `vits-piper-en_US-lessac-medium` (+ `espeak-ng-data`) | ~62 MB |
| LLM (local) | LiteRT-LM `com.google.ai.edge.litertlm:litertlm-android:0.15.0` | `litert-community/Qwen3-0.6B` β†’ `Qwen3-0.6B.litertlm` (dynamic INT8) | 586 MB |
| LLM (remote) | OkHttp + kotlinx.serialization (OpenAI-compatible chat completions) | any remote model, e.g. `qwen2.5-0.5b-instruct` | n/a |
All models are Apache-2.0 or MIT except the Piper voice/GPL espeak-ng data
(bundled in the sherpa-onnx tts-model tarball) β€” fine for local testing.
## Prerequisites
- macOS/Linux with Android SDK (platform 35, build-tools 36, NDK `27.2.12479018`, CMake 3.22.1),
JDK 17, and a connected Android device (arm64, 8 GB RAM recommended).
- `adb` on PATH.
## Build
Toolchain: **AGP 8.13.2 Β· Gradle 8.13 (wrapper) Β· Kotlin 2.3.21 Β· JDK 17**.
whisper.cpp is vendored under `third_party/` (committed), so the project builds
out-of-the-box once the SDK/NDK are present.
```bash
# 0. JDK 17 (Gradle 8.13 does not support newer JDKs)
# macOS/Homebrew: brew install openjdk@17
export JAVA_HOME=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
# 0b. Point Gradle at the Android SDK (or set ANDROID_HOME)
echo "sdk.dir=$HOME/Library/Android/sdk" > local.properties
# 1. (first time) install NDK/cmake if missing
"$ANDROID_HOME"/cmdline-tools/latest/bin/sdkmanager \
"platforms;android-35" "ndk;27.2.12479018" "cmake;3.22.1"
# 2. Build (Gradle downloads the sherpa-onnx AAR into app/libs on first run)
./gradlew :app:assembleDebug
# 3. Install
adb install -r app/build/outputs/apk/debug/app-debug.apk
```
Unit tests: `./gradlew :app:testDebugUnitTest`
> Only if `third_party/whisper.cpp` is ever missing (e.g. a fresh export that
> dropped it): `tools/fetch-deps.sh` re-clones it.
## Models
**Models are bundled in the APK** (`assets/models/`) and copied to the app's
private internal storage on first launch β€” no adb push needed. Storage used on
the device: `~240 MB` under `/data/data/com.example.voicechatbot/files/models/`.
> The APK is ~500 MB because of the bundled models; devices need **~1.5 GB free**
> to install it.
**Preinstalled:** SmolLM2 135M (LLM) + Whisper tiny.en Q4 + Silero VAD + Piper TTS.
**Download in-app:** bigger LLMs (SmolLM2 360M, Qwen3 0.6B, Gemma 4 E2B/E4B/12B)
and Whisper models are selected in Settings via **preset chips that
auto-download the file if it isn't already saved** (progress shown on the chip).
| id | Model | Size | Preinstalled | Thinking | License |
|---|---|---|---|---|---|
| `smollm2-135m` | SmolLM2 135M | 136 MB | βœ… | no | Apache-2.0 |
| `smollm2-360m` | SmolLM2 360M | 356 MB | download | no | Apache-2.0 |
| `qwen3-0.6b` | Qwen3 0.6B int8 | 586 MB | download | yes (stripped) | Apache-2.0 |
| `gemma-4-e2b` | Gemma 4 E2B (mobile 2/4/8-bit) | 2583 MB | download | yes (stripped) | Apache-2.0 |
| `gemma-4-e4b` | Gemma 4 E4B (mobile 2/4/8-bit) | 3654 MB | download | yes (stripped) | Apache-2.0 |
| `gemma-4-12b` | Gemma 4 12B | 6548 MB | download | yes (stripped) | Apache-2.0 |
To rebuild with bundled models, the files must be present in `./models/`:
```bash
tools/download-models.sh # downloads all LLM presets + whisper/VAD/TTS
tools/download-models.sh smollm2-360m # or just one
tools/download-models.sh list # show presets
```
(`tools/push-models.sh` is obsolete β€” models are in the APK now.)
> **LFM-700M:** there is no `.litertlm` build of LFM2-700M on HF yet, and a
> conversion requires the `litert-torch` generic-HF-export pipeline (heavy, not
> guaranteed for the architecture). The SmolLM2 presets are the practical small,
> non-thinking, Apache-2.0 alternatives.
> **Gemma 4 E2B / E4B:** official `litert-community` LiteRT-LM builds of
> `google/gemma-4-E2B-it` / `google/gemma-4-E4B-it` (Apache-2.0), using Google's
> "Gemma-4 mobile" 2/4/8-bit (LUT) quantization. LiteRT-LM **memory-maps the
> weights straight from the `.litertlm` file on disk** β€” the embedding tables are
> mmap'd and never fully loaded into RAM β€” so the ~2.6 GB / ~3.7 GB files don't
> need to fit in memory. Both support up to 32k context. Android benchmarks
> (S26 Ultra, 2048 ctx): E2B ~1.7 GB CPU / ~0.7 GB GPU process RAM at
> ~47–52 tok/s; E4B ~3.3 GB CPU / ~0.7 GB GPU at ~18–22 tok/s β€” E4B really wants
> a 12 GB-class phone **and** the GPU backend. Settings β†’ *Context window*
> (LiteRT-LM `maxNumTokens`) caps the KV cache so bigger models fit in less RAM.
> **Gemma 4 12B** (`gemma-4-12B-it.litertlm`, ~6.5 GB) is the largest option β€”
> realistically it needs a 16 GB-class device with the GPU backend.
> **TTS voices:** only the bundled `en_US-lessac-medium` voice is used. In-app
> voice downloads were removed; Settings β†’ *Voice (VITS)* just shows the
> preinstalled voice (blank the model path for text-only chat).
> **Whisper (STT):** the bundled model is `ggml-tiny.en-q4_0.bin` (~45 MB).
> Better variants are available as Settings β†’ *Whisper model* presets
> (auto-downloaded on selection): unquantized tiny.en / tiny (FP16, ~75 MB),
> tiny.en Q8_0 (~42 MB), or base.en Q8_0 (~78 MB). Q4_0 builds are **not**
> published on Hugging Face (`tools/download-models.sh` quantizes them locally),
> so Q8_0 is the closest official base.en quantized download. Larger models are
> slower but more accurate, especially for non-English speech (use the
> multilingual `tiny`).
## Running & testing
1. Grant the **Record audio** permission when prompted β€” listening starts
automatically, so there is no Start button.
2. Say something. You should see `Listening β†’ Transcribing β†’ Thinking β†’
Speaking`, and hear the reply sentence-by-sentence.
3. **Barge-in:** start talking mid-reply. Playback stops at once and your
question is picked up as the next turn.
4. **Remote LLM:** open Settings β†’ *Remote (OpenAI API)* β†’ set base URL
(e.g. `http://<your-server>:8000/v1`) + model name, save & restart.
Works with llama.cpp server, vLLM, Ollama, etc.
**Context length:** Settings β†’ *Context length* (512…32768). The app sends
it as `n_ctx`, which llama.cpp honours per-request. "Server default" sends
nothing β€” the server decides (vLLM/Ollama manage context server-side).
5. **Stop** cancels TTS + generation and re-arms listening.
6. **Visualizer mode:** tap the eye icon in the top bar to hide the transcript.
The screen then shows only a minimal audio-level bar visualizer and the
sentence that is currently being spoken, rendered in a cursive glowing font.
7. **Mini screen + submit photo:** tap the PiP icon to enable a small draggable
floating screen (grant the *Display over other apps* permission) that shows
the currently-spoken sentence in the glowing cursive font, with a distinct
submit-photo button (opens the system photo picker, like the chat's photo
icon). While the mini screen is active a **foreground service with a
persistent "listening" notification** keeps the microphone running in the
background (Android 14+). Image input only works with a **Gemma 4** model on
the local LiteRT-LM backend; anything else shows an error.
8. **Sampling:** Settings β†’ *Sampling (local)* (temperature / top-p / top-k for
LiteRT-LM) and *Sampling (remote)* (temperature / top-p / max tokens for the
OpenAI-compatible endpoint).
Tips:
- Use headphones or a quiet room: without acoustic echo cancellation support
the assistant may "barge in" on itself. The capture uses
`VOICE_COMMUNICATION` + `AcousticEchoCanceler` when available.
- TTS can be disabled by blanking the TTS model path in Settings (text-only
chat) for quick LLM testing.
- GPU backend: for the **local** LiteRT-LM backend this is LiteRT's GPU
delegate ("ML Drift", OpenCL on Android) β€” roughly doubles decode speed and
cuts process RAM a lot (Gemma E4B: ~3.3 GB β†’ ~0.7 GB). For the **remote**
backend the GPU is on the server (llama.cpp CUDA/Metal/OpenCL, vLLM, …), not
in the app. Enable/disable it in Settings β†’ *GPU backend*.
## Project layout
```
app/src/main/java/com/example/voicechatbot/
MainActivity.kt Compose UI + settings dialog
config/AppConfig.kt persisted settings & model paths
pipeline/VoicePipelineEngine.kt state machine, barge-in, sentence streaming
audio/AudioRecorder.kt 16 kHz capture (+AEC)
audio/TtsPlayer.kt AudioTrack queue with generation-based barge-in
vad/VadEngine.kt Silero VAD (sherpa-onnx)
stt/WhisperTranscriber.kt whisper.cpp JNI wrapper
stt/WhisperNative.kt JNI declarations
llm/ChatClient.kt backend interface
llm/LitertChatClient.kt LiteRT-LM local inference (Flow streaming)
llm/OpenAiChatClient.kt OpenAI-compatible remote client (SSE)
llm/SentenceSplitter.kt token stream β†’ sentence boundaries
llm/PromptBuilder.kt system prompt
tts/TtsEngine.kt VITS/Piper synthesis (sherpa-onnx)
app/src/main/cpp/ whisper.cpp CMake + JNI bridge
third_party/whisper.cpp vendored (committed, v1.7.5)
tools/download-models.sh model fetch + Q4 quantize (local)
tools/push-models.sh adb push models to device
```
## Notes & caveats
- **LiteRT-LM** requires a high-end device (Pixel 8 / S23 class). Engine init
takes a few seconds and is done off the UI thread.
- On interrupt, the current conversation context is dropped (clean slate for
the next turn).
- This is a test app: no wake word, no streaming partial transcription, no
foreground service (screen must stay on), single ABI (`arm64-v8a`).