A local, modular harness for experimenting with realtime conversational AI stacks.
The implementation provides a runnable scaffold with clear provider boundaries:
- VAD: Silero VAD (ONNX on CPU) with configurable speech threshold, hysteresis, hangover, and minimum speech duration gating.
- ASR: faster-whisper with CUDA or CPU support, mock fallback included.
- LLM: llama.cpp OpenAI-compatible adapter with runtime-adjustable sampler settings.
- TTS: Pocket TTS and Kokoro ONNX voice output, mock tone fallback included.
- UI: local browser app with Chat, Companion, and Settings tabs, text/voice turns, event stream, and latency metrics.
- Companion mode: separate prompt, names, sampler overrides, transcript, and optional Markdown memory backed by
memory.md.
The default start profile is profiles/llamacpp-cuda-asr.json, which uses Silero VAD, CUDA faster-whisper ASR, llama.cpp, and Pocket TTS. A CPU ASR fallback is available at profiles/llamacpp-local.json. A no-model smoke-test profile is at profiles/mock-local.json.
Windows:
.\install.ps1
.\start.ps1Linux/macOS:
./install.sh
./start.shThen open http://127.0.0.1:7860.
start.* now runs a guided launcher. It checks the selected profile, required Python packages, the harness port, provider status, and llama.cpp readiness before starting uvicorn. In an interactive terminal it can prompt for the profile and ask before opening the browser. For non-interactive use, pass --no-prompt --no-browser.
On Windows, you can also start a Cloudflare quick tunnel from the launcher:
.\start.ps1 -CloudflareTunnelThe tunnel prints a public Cloudflare URL in the terminal, and the launcher reprints it as Cloudflare tunnel URL: ... when detected. Use this only when you intentionally want remote access to your local harness.
If the local readiness check times out but the server process is still running, the launcher still starts the Cloudflare tunnel and warns that the public URL may show a temporary error until the harness finishes warming up.
- Python 3.11, 3.12, or 3.13 (Python 3.14 is not supported by
kokoro-onnx==0.5.0; the project venv uses whatever Python created it) - llama.cpp server running separately for LLM inference (see llama.cpp section)
- CUDA 12+ (optional, for GPU ASR — the harness falls back to CPU if unavailable)
The CUDA ASR profile requires CTranslate2 to find CUDA 12 runtime DLLs. If your system CUDA toolkit is v13+, cublas64_12.dll will be missing. The fix:
.\.venv\Scripts\pip install nvidia-cublas-cu12The start.ps1 script automatically adds the NVIDIA bin directories from .venv to PATH before launching. If you see Library cublas64_12.dll is not found or cannot be loaded, verify this package is installed.
The web UI captures microphone input and streams it to the backend as framed PCM:
- encoding:
pcm_s16le - sample rate: profile
audio.sample_rate, default16000 - channels: profile
audio.channels, default1 - frame size: profile
audio.frame_ms, default30
Silero VAD runs on this stream and emits vad.probability, vad.speech_start, and vad.speech_end. Speech start cancels active TTS playback for barge-in support. A minimum speech duration gate (min_speech_duration_ms) filters out short noise bursts like keystrokes before they reach ASR.
The default CUDA profile uses faster-whisper with large-v3-turbo on GPU:
"asr": {
"provider": "faster-whisper",
"model": "large-v3-turbo",
"device": "cuda",
"compute_type": "float16"
}A CPU fallback profile is available:
$env:HARNESS_PROFILE="profiles/llamacpp-local.json"
.\start.ps1The model starts loading when you click Connect, so the first spoken utterance does not pay the model-load cost. If it is not already cached, faster-whisper/Hugging Face may download it at that point. After Silero emits vad.speech_end, the harness trims quiet leading/trailing audio, applies low-energy rejection guards, transcribes the buffered utterance, and feeds the transcript into the active mode's llama.cpp turn pipeline.
During ASR the UI reports progress stages such as queued, loading, loaded, segment, trim, and complete. If first use appears slow, check server.err.log for Hugging Face download/cache warnings.
The included faster-whisper profiles also use conservative anti-hallucination settings for large-v3-turbo, including condition_on_previous_text: false, temperature: 0, no_speech_threshold: 0.2, tighter Silero VAD thresholds, short/quiet utterance rejection, and silence trimming before ASR.
The default profile uses Pocket TTS with the voice azelma:
"tts": {
"provider": "pocket-tts",
"voice": "azelma"
}Pocket TTS loads the model and voice state on first use, then reuses them for later turns. The model is gated on Hugging Face, so accept the model terms for kyutai/pocket-tts and make sure your Hugging Face token is available if first use reports an access error.
Pocket TTS 2.1.0 supports int8 quantization through the quantize profile option. The default Pocket preset keeps full precision, and the runtime selector also includes Pocket TTS INT8 for lower memory use and faster CPU inference:
"quantize": trueFor smoother playback, the local profiles feed Pocket TTS larger phrase-sized chunks and the browser schedules decoded PCM chunks on a shared Web Audio timeline instead of playing each chunk as a separate audio element.
An additional profile is included for Kokoro v1.0 ONNX on CPU:
$env:HARNESS_PROFILE="profiles/llamacpp-kokoro-onnx.json"
.\doctor.ps1
.\start.ps1Kokoro uses local ONNX files and downloads the current v1.0 assets on first use into model-cache/kokoro. The runtime TTS selector in the web UI can switch between Pocket TTS and Kokoro without restarting the harness.
Kokoro is tuned separately from Pocket TTS because its CPU ONNX path benefits from shorter phrase chunks. The Kokoro profile and preset use lower tts_chunk_chars / min_tts_chars values, prewarm English G2P on model load, and expose optional CPU threading controls:
"onnx_intra_op_num_threads": 4,
"onnx_inter_op_num_threads": 1,
"preload_g2p": trueThe UI has Chat, Companion, and Settings tabs in the topbar. Chat is the standard harness conversation, Companion is a separate persistent companion workspace, and Settings controls the standard chat defaults and character-card workflow.
Companion mode is separate from standard Chat mode:
- It has its own transcript, composer, user name, AI name, system prompt, and sampler overrides.
- Text and voice turns are routed to the active tab, so Companion speech and Chat speech keep separate histories.
- Character-card first-message seeding applies only to standard Chat mode.
- Companion prompt assembly uses the Companion names, Companion system prompt, optional memory, and Companion sampler overrides.
Companion memory is stored in project-root memory.md, which is ignored by git. The file is created on first memory write, not on startup.
Memory is manual-save in this version:
- Save Memory writes the current memory editor text to
memory.md. - Summarize Chat sends the visible Companion transcript to the active LLM and appends a dated Markdown summary to
memory.md. - Clear Memory removes the saved memory file.
- The Memory checkbox controls whether saved memory is injected into Companion LLM requests.
Saved memory is injected only in Companion mode when memory is enabled. It is not injected into standard Chat or character-card conversations.
The Settings tab has three sections:
Set a display name for yourself and the AI. Names are used:
- In chat bubble labels (replacing "You" and "Assistant")
- In the system prompt sent to the LLM
Adjust llama.cpp sampler parameters at runtime without restarting:
| Parameter | Description |
|---|---|
| Temperature | Sampling randomness (0.0 - 2.0) |
| Top K | Limit to top K tokens |
| Top P | Nucleus sampling threshold |
| Min P | Minimum probability threshold |
| Repeat Penalty | Discourages token repetition |
| Frequency Penalty | Penalizes frequent tokens |
| Presence Penalty | Penalizes present tokens |
| Max Tokens | Max tokens per turn (-1 = unlimited) |
Settings are pre-filled from the llama.cpp server's current defaults on startup. Edits are saved to user_settings.json and persist across restarts. The "Reset to Defaults" button clears all overrides.
Import TavernAI V2 character cards (.png with embedded data or standalone .json). The card's name, description, personality, scenario, and example dialogue are assembled into the system prompt, replacing any automatic name-based prompt. Template variables {{user}} and {{char}} are resolved to the configured names.
You can add extra instructions in the "Additional instructions" textarea below the card, which are appended after the character data. The manual system prompt textarea on the Chat tab can also supplement the card.
The Chat tab has a "Continue" button next to Send. When the LLM response is cut off (by hitting the context limit or max tokens), clicking Continue asks the model to pick up where it left off. The last assistant message stays in the conversation history as a prefix, and new tokens are appended to the existing bubble.
| Endpoint | Method | Purpose |
|---|---|---|
/ |
GET | Serves the web UI |
/api/status |
GET | Profile, providers, TTS runtime, settings |
/api/settings |
GET | Current runtime settings |
/api/settings |
PATCH | Update sampler/names/additional prompt |
/api/settings/character |
POST | Import character card (JSON body) |
/api/settings/character/upload |
POST | Upload character card file (PNG or JSON) |
/api/settings/character |
DELETE | Clear active character card |
/api/companion/memory |
GET | Read Companion memory text and metadata |
/api/companion/memory |
PUT | Save edited Companion memory text |
/api/companion/memory/summarize |
POST | Summarize Companion transcript into memory.md |
/api/companion/memory |
DELETE | Clear Companion memory |
/api/tts/select |
POST | Select TTS preset |
/api/tts/load |
POST | Load TTS model |
/api/tts/unload |
POST | Unload TTS model |
/api/tts/voice |
POST | Select TTS voice |
/ws/events |
WebSocket | Bidirectional event stream |
install.*: verifies Python 3.11+, creates or reuses.venv, installs Python dependencies, and prints next steps.start.*: launches the guided startup flow and then runs the local web/backend server in the foreground. It honorsHARNESS_PROFILEandHARNESS_PORT, refuses to start if the target port is already occupied, checks provider/backend readiness, and prints the local URL. Windows also adds NVIDIA package bin directories from.venvtoPATHfor CUDA ASR.stop.*: stops this harness if it is listening onHARNESS_PORT(default7860). It leaves unrelated processes alone and force-stops only if graceful shutdown times out.doctor.*: checks Python, optional GPU tooling, optional llama.cpp server, and profile status.update.*: updates this checkout fromorigin/mainonly. It refuses to run with uncommitted changes, uses fast-forward-only merge behavior, and reinstalls requirements whenrequirements.txtchanged.
Examples:
.\start.ps1 -Port 7861 -Profile profiles/mock-local.json
.\start.ps1 -ListProfiles
.\start.ps1 -NoPrompt -NoBrowser
.\start.ps1 -CloudflareTunnel
$env:PYTHONPATH = "$(Get-Location)\app"; .\.venv\Scripts\python -m conversational_harness.launch --cloudflare-tunnel
.\update.ps1HARNESS_PORT=7861 HARNESS_PROFILE=profiles/mock-local.json ./start.sh
./start.sh --list-profiles
./start.sh --no-prompt --no-browser
./update.shThe update scripts intentionally do not pull from the current branch if it is Beta or another working branch; origin/main is the only update source.
Profiles live in profiles/*.json. Available profiles:
| Profile | ASR | TTS | Notes |
|---|---|---|---|
llamacpp-cuda-asr.json |
CUDA float16 | Pocket TTS | Default. Requires CUDA 12 runtime. |
llamacpp-local.json |
CPU int8 | Pocket TTS | CPU fallback. No CUDA needed. |
llamacpp-kokoro-onnx.json |
CPU int8 | Kokoro ONNX | Kokoro TTS on CPU. |
mock-local.json |
Mock | Mock | No-model smoke test for UI and orchestration. |
To use a non-default profile:
$env:HARNESS_PROFILE="profiles/mock-local.json"
.\start.ps1The harness talks to llama.cpp through its OpenAI-compatible server API. Start llama-server separately, then use any llamacpp-* profile.
Example CUDA launch:
llama-server -m C:\models\your-model.gguf --host 127.0.0.1 --port 8080 --alias local-gguf -ngl 999Then verify:
.\doctor.ps1
.\start.ps1Notes:
- The harness uses
/v1/chat/completionswith streaming enabled. doctorchecks/healthand/v1/modelsso missing or still-loading models are visible.- On startup, the harness fetches
/propsfrom llama.cpp to learn the server's current sampler defaults and pre-fills the Settings UI. - Sampler resolution is server defaults, then profile defaults, then the active mode's runtime overrides. Companion mode has its own override set.
- Vulkan can be useful for llama.cpp on non-CUDA systems, but it is LLM-only here and should be treated as experimental for this voice stack.
- The default profile uses
"model": "auto"and selects the first model reported by/v1/models. - If you prefer a fixed ID, set
llm.modelto the exact model ID reported by/v1/models, or launchllama-serverwith a matching--alias.
The server runs a lifespan handler that cleans up on Ctrl+C or SIGTERM:
- Cancels all active TTS playback
- Calls
unload()on all providers (releases CUDA GPU memory from ASR, drops VAD model) - Discards all active WebSocket queues
stop.ps1 tries graceful shutdown first with a 5-second timeout, then force-kills if needed.
Run tests:
.\.venv\Scripts\python -m pytestOptional live llama.cpp integration test:
$env:HARNESS_TEST_LLAMA_CPP="1"
$env:HARNESS_LLAMA_CPP_MODEL="auto"
.\.venv\Scripts\python -m pytest tests\test_llamacpp_live.py