spych

Spych

PyPI version License: MIT PyPI Downloads

Spych (pronounced "speech"): Talk with your computer like it's your personal assistant without sending your voice to the cloud.

A lightweight, fully offline Python toolkit for wake word detection, audio transcription, spoken AI responses, and AI integrations. Built on faster-whisper, PvRecorder, and Kokoro.

API Docs: https://connor-makowski.github.io/spych/spych.html


Installation

uv tool install spych

Alternative: pip

pip install spych

TTS Extras

By default, Spych automatically installs the right TTS backend for your Python version. You can also install explicitly:

uv tool install "spych[kokoro]"       # Fast, lightweight (Python < 3.13 recommended)
uv tool install "spych[chatterbox]"   # High-quality voice cloning (Python >= 3.13 required)

Quick Start

# Navigate to your project directory first
cd ~/my_project

# Voice-control Claude Code β€” say "hey claude" to trigger
spych claude

# Use a personality preset β€” say "hey jarvis" to trigger
spych claude --personality jarvis

# Voice-control a local Ollama model β€” say "hey llama" to trigger
spych ollama --model llama3.2:latest

πŸ’‘ Pro tip: Saying "Hey Claude" or "Hey Llama" tends to trigger more reliably than the bare wake word.

Say "terminate" (or press Ctrl+C) to stop any session.


CLI

Available Agents

All agents require their respective CLI tool to be installed and authenticated before use.

Command Alias Description Default wake words
spych claude_code_cli β€” Voice-control Claude Code via the CLI claude, clod, cloud, clawed
spych claude_code_sdk spych claude Voice-control Claude Code via the Agent SDK claude, clod, cloud, clawed
spych codex_cli spych codex Voice-control the OpenAI Codex agent codex
spych gemini_cli spych gemini Voice-control the Google Gemini agent gemini, google
spych opencode_cli spych opencode Voice-control the OpenCode agent opencode, open code
spych ollama β€” Talk to a local Ollama model llama, ollama, lama

Available Utilities

The following utilities are also available as CLI commands. They don't use wake words, but serve various auxiliary functions like live transcription and voice profiling.

Command Description
spych --version Print the version number and exit
spych --help Show detailed usage instructions and exit
spych live Continuous speech-to-text transcription to file
spych live-translation (Beta Mode) Continuous speech transcription + translation (bilingual input and output)
spych multi Run multiple agents simultaneously
spych users Manage user profiles and global settings
spych profile_my_voice Record a voice sample for TTS cloning

Global Flags

These must be placed before the agent name:

spych --theme light claude
Flag Options Default Description
--theme dark, light, solarized, mono dark Terminal colour theme

πŸ’‘ TUI Dashboard: Spych launches a rich terminal interface by default. Use the --verbose flag (e.g., spych --verbose claude) to switch to a simpler, non-interactive scrollable output.


Common Flags

All agent subcommands accept these flags:

Flag Default Description
--personality NAME β€” Apply a named preset (sets wake words, voice, name, style)
--name NAME (agent default) Custom display name shown in the terminal
--wake-words WORD [...] (agent default) One or more words that trigger the agent
--terminate-words WORD [...] terminate Words that stop the listener
--listen-duration SECONDS 0 (VAD auto) Seconds to record after wake word
--follow-up-listen-duration SECONDS 0 Seconds to listen for a follow-up answer
--inactivity-timeout SECONDS 4.0 Seconds of silence before returning to wake word
--use-speaker BOOL true Speak responses aloud via TTS
--speaker-voice VOICE af_heart Voice name for spoken responses
--speaker-backend BACKEND (auto) chatterbox or kokoro
--response-style STYLE β€” Style preset or custom instruction for spoken output
--intermediate-responses BOOL true Enable intermediate response chaining for long-running tasks

Coding agents (claude, codex, gemini, opencode) also accept:

Flag Default Description
--continue-conversation BOOL true Resume the most recent session
--show-tool-events BOOL true Print live tool start/end events

Agent-specific flags:

Agent Flag Default Description
ollama --model llama3.2:latest Ollama model name
ollama --history-length 10 Past interactions to include in context
ollama --host http://localhost:11434 Ollama instance URL
opencode_cli --model β€” Model in provider/model format
claude_code_sdk --setting-sources user project local Claude Code settings sources

Personalities

Personalities are named presets that bundle a wake word list, voice, display name, and response style into a single flag. Any explicit flag overrides the preset.

spych claude --personality jarvis
# equivalent to:
spych claude --name "JARVIS" --wake-words jarvis jarves              --speaker-voice bm_george --use-speaker true              --response-style jarvis
Name Wake words Voice Style
assistant assistant, helper, computer af_heart assistant β€” helpful, precise, informative
friend friend, buddy, pal af_amy friendly β€” warm and simple
jarvis jarvis, jarves, jargus, jervis bm_george jarvis β€” precise, dry wit, "sir"
pirate blackbeard, pirate, ahoy am_michael pirate β€” pirate speak, colorful
news_anchor bella, news anchor, anchor af_bella news_anchor β€” professional broadcast tone
robot rob, robot am_adam robot β€” monotone, literal
caveman er, ur, caveman, cave man am_onyx caveman β€” very simple, direct

User Management

Spych supports multiple user profiles, allowing agents to provide more personalized responses based on your name, age, and other context.

# Launch the interactive user management menu
spych users

The users utility allows you to:

  • Create, edit, and delete user profiles.
  • Set a default user for all agents.
  • Change the global terminal theme (dark, light, solarized, mono).

You can also specify a user for a specific session:

spych claude --user Connor

Response Styles

The --response-style flag shapes how the agent formats its spoken output.

Style Description
assistant Helpful and precise, concise and informative
concise Key points only, direct
friendly Warm, approachable, simple language
military Brevity-style, short sentences
five_year_old Simple words, very short
fast As brief as reasonably possible
pirate Pirate speak, colorful
news_anchor Professional broadcast tone
haiku 5-7-5 haiku form
shakespearean Elizabethan English
robot Monotone, literal
caveman Very simple, direct
yoda Inverted sentence structure
jarvis JARVIS from Iron Man β€” precise, dry wit, addresses user as "sir" or "ma'am"

You can also pass any custom instruction string directly: --response-style "Reply in exactly one sentence.".


Text-to-Speech & Voices

Spoken responses are enabled by default for personality presets and when --use-speaker true is set.

spych claude --use-speaker true --speaker-voice bm_george
spych claude --use-speaker true --speaker-backend kokoro
spych claude --use-speaker false   # disable TTS

When TTS is active, short responses are spoken verbatim; longer ones use the agent's short summary. If the response ends with a question, Spych automatically listens for a follow-up β€” no wake word required.

TTS Backends

Backend Best for Python support
Chatterbox (default priority) Natural voices, zero-shot voice cloning 3.11+ (required for 3.13+)
Kokoro (lightweight fallback) Fast, low-resource devices (e.g. Raspberry Pi) 3.11–3.12 recommended

Spych tries Chatterbox first, then Kokoro. Use --speaker-backend to force one explicitly.

Available Voices

The same voice names work for both backends.

American English (am_ / af_):

Voice Gender Grade
af_heart F A (default)
af_bella F A-
af_nicole F B-
am_michael M C+
am_fenrir M C+
am_puck M C+

British English (bm_ / bf_):

Voice Gender Grade
bf_emma F B-
bf_isabella F C
bm_george M C

Voice Cloning

Record a 10-second sample of your voice, then use it as the speaker voice. Requires the Chatterbox backend.

# Step 1: record your profile
spych profile_my_voice --name my_voice

# Step 2: use it
spych claude --use-speaker true --speaker-voice my_voice --speaker-backend chatterbox

# Or use any .wav file directly
spych claude --use-speaker true --speaker-voice /path/to/my_voice.wav --speaker-backend chatterbox

Live Transcription

spych live continuously records from the microphone using VAD and writes the transcript to disk in real time. No wake word required β€” it transcribes everything until stopped.

CLI

spych live                                                 # writes transcript.srt
spych live --output-path meeting --output-format both
spych live --terminate-words "stop recording"
spych live --no-timestamps --whisper-model medium.en

Stop by pressing the stop key (default: q + Enter), saying a terminate word, or pressing Ctrl+C.

Parameters

Flag Default Description
--output-path PATH transcript Base output file path without extension
--output-format FORMAT srt txt, srt, or both
--no-timestamps false Omit timestamps from terminal and .txt output
--stop-key KEY q Key (then Enter) to stop the session
--terminate-words WORD [...] β€” Spoken words that stop the session
--device-index N -1 Microphone device index; -1 uses system default
--whisper-model MODEL base.en faster-whisper model name
--whisper-device DEVICE cpu cpu or cuda
--whisper-compute-type TYPE int8 int8, float16, or float32
--no-speech-threshold FLOAT 0.3 Whisper segments above this no_speech_prob are dropped
--speech-threshold FLOAT 0.5 VAD speech onset probability
--silence-threshold FLOAT 0.35 VAD silence probability during speech
--silence-frames N 20 Consecutive silent frames to end a segment (~32ms each)
--speech-pad-frames N 5 Pre-roll frames and onset confirmation count
--max-speech-duration SECONDS 30.0 Hard cap on a single segment
--context-words N 32 Trailing words passed as whisper initial_prompt

Python

from spych.live import SpychLive

SpychLive(
    output_format="srt",          # "txt", "srt", or "both"
    output_path="my_transcript",  # written to my_transcript.srt
    show_timestamps=True,
    stop_key="q",                 # type q + Enter to stop
    terminate_words=["stop recording"],
).start()

SpychLive Parameters

Parameter Default Description
output_format "srt" Output format(s): "txt", "srt", or "both"
output_path "transcript" Base path without extension
show_timestamps True Prepend [HH:MM:SS] timestamps to terminal and .txt output
stop_key "q" Key (then Enter) to stop the session
terminate_words None Spoken words that stop the session
on_terminate None No-argument callback executed when a terminate word fires
device_index -1 Microphone device index; -1 uses system default
whisper_model "base.en" faster-whisper model name
whisper_device "cpu" Device for inference: "cpu" or "cuda"
whisper_compute_type "int8" Compute precision: "int8", "float16", or "float32"
no_speech_threshold 0.4 Whisper segments above this are discarded
speech_threshold 0.5 Silero VAD onset probability
silence_threshold 0.35 Silero VAD silence probability during speech
silence_frames_threshold 20 Consecutive silent frames to close a segment
speech_pad_frames 5 Pre-roll frame count and onset confirmation threshold
max_speech_duration_s 30.0 Hard cap on a single segment in seconds
context_words 32 Trailing transcript words passed as initial_prompt

Live Translation

Currently in beta: expect some rough edges.

spych live-translation starts a bidirectional live translation session between two languages. Either participant can speak in either language β€” Whisper transcribes each utterance, Ollama detects which language was spoken and translates it to the other, and each segment is shown as two lines in real time. The translated text is also spoken aloud via TTS by default.

Note: This is currently in beta mode and the API may change without a major version bump. Feedback is very welcome!

Requires Ollama running locally with the translation model pulled. By default, Spych uses the llama3.2 model, but you can specify any Ollama model with --ollama-translation-model.

CLI

# English ↔ Spanish conversation
spych live-translation --languages en es

# English ↔ French, disable TTS
spych live-translation --languages en fr --no-speaker

# Use a larger Ollama model and save a bilingual transcript
spych live-translation --languages en de     --ollama-translation-model mistral --output-format both

Stop by pressing the stop key (default: q + Enter), saying a terminate word, or pressing Ctrl+C.

Each utterance is shown as two lines β€” the original and the translation:

[00:00:05](es) Hola, ΒΏcΓ³mo estΓ‘s?
[00:00:05](en) Hello, how are you?

If Ollama is unreachable, the session continues and the translation line shows [translation unavailable] for that segment.

Parameters

Flag Default Description
--languages LANG LANG (required) Two BCP-47 language codes for the conversation pair (e.g. en es)
--ollama-host URL http://localhost:11434 Ollama HTTP base URL
--ollama-translation-model MODEL llama3.2 Ollama model used for translation
--no-speaker false Disable TTS β€” speaker is on by default
--speaker-voice VOICE (model default) Wave voice name for zero-shot cloning; omit to use the model's built-in default voice
--output-path PATH transcript Base output file path without extension
--output-format FORMAT (none) Save transcript to file: txt, srt, or both; omit for terminal-only output
--no-timestamps false Omit timestamps from terminal and .txt output
--stop-key KEY q Key (then Enter) to stop the session
--terminate-words WORD [...] β€” Spoken words that stop the session
--device-index N -1 Microphone device index; -1 uses system default
--whisper-model MODEL small faster-whisper model name; .en suffix is stripped automatically
--whisper-device DEVICE auto auto, cpu, or cuda; auto selects cuda when available on Python ≀3.13
--whisper-compute-type TYPE int8 int8, float16, or float32
--no-speech-threshold FLOAT 0.3 Whisper segments above this no_speech_prob are dropped
--speech-threshold FLOAT 0.5 VAD speech onset probability
--silence-threshold FLOAT 0.35 VAD silence probability during speech
--silence-frames N 20 Consecutive silent frames to end a segment (~32ms each)
--speech-pad-frames N 5 Pre-roll frames and onset confirmation count
--max-speech-duration SECONDS 30.0 Hard cap on a single segment

Python

from spych.live_translation import SpychLiveTranslation

SpychLiveTranslation(
    lang_a="en",
    lang_b="es",
    output_format="both",          # "txt", "srt", or "both"; "" for terminal only
    output_path="my_translation",  # written to my_translation.txt + .srt
    show_timestamps=True,
    stop_key="q",
    terminate_words=["terminate"],
    ollama_host="http://localhost:11434",
    ollama_translation_model="llama3.2",
    use_speaker=True,
    speaker_voice="",              # "" uses the model's built-in default voice
).start()

SpychLiveTranslation Parameters

Parameter Default Description
lang_a (required) BCP-47 code of the first language (e.g. "en")
lang_b (required) BCP-47 code of the second language (e.g. "es")
output_format "" Output format(s): "txt", "srt", or "both"; "" disables file output
output_path "transcript" Base path without extension
show_timestamps True Prepend [HH:MM:SS] timestamps to terminal and .txt output
stop_key "q" Key (then Enter) to stop the session
terminate_words None Spoken words that stop the session
device_index -1 Microphone device index; -1 uses system default
whisper_model "base" faster-whisper model name; .en suffix stripped automatically
whisper_device "auto" Device for inference: "auto", "cpu", or "cuda"
whisper_compute_type "int8" Compute precision: "int8", "float16", or "float32"
no_speech_threshold 0.4 Whisper segments above this are discarded
speech_threshold 0.5 Silero VAD onset probability
silence_threshold 0.35 Silero VAD silence probability during speech
silence_frames_threshold 20 Consecutive silent frames to close a segment
speech_pad_frames 5 Pre-roll frame count and onset confirmation threshold
max_speech_duration_s 30.0 Hard cap on a single segment in seconds
ollama_host "http://localhost:11434" Ollama HTTP base URL for translation
ollama_translation_model "llama3.2" Ollama model used for translation
use_speaker True Speak each translated segment aloud via TTS
speaker_voice "" Wave voice name for zero-shot cloning; "" uses the model's built-in default voice

Multi-agent

Run several agents simultaneously under a single listener, each bound to its own wake words. Say "hey claude" to talk to Claude, "hey llama" to talk to Ollama β€” all in the same terminal session.

CLI

# Two agents, default wake words
spych multi --agents claude gemini

# Include Ollama with a specific model
spych multi --agents claude ollama --ollama-model llama3.2:latest

# Tune listen duration across all agents
spych multi --agents claude codex --listen-duration 8

Multi-agent CLI Flags

Flag Default Description
--agents AGENT [...] (required) Agents to run: claude (claude_code_cli), claude_sdk (claude_code_sdk), codex (codex_cli), gemini (gemini_cli), opencode (opencode_cli), ollama
--terminate-words WORD [...] terminate Words that stop all agents
--listen-duration SECONDS 5 Seconds to listen after a wake word
--follow-up-listen-duration SECONDS 0 Seconds to listen for follow-up answers
--inactivity-timeout SECONDS 4.0 Seconds of silence before returning to wake word
--continue-conversation BOOL true Resume the most recent session for each coding agent
--show-tool-events BOOL true Print live tool start/end events
--use-speaker BOOL true Speak responses aloud via TTS
--speaker-backend BACKEND (auto) chatterbox or kokoro
--intermediate-responses BOOL true Enable intermediate response chaining for long-running tasks
--ollama-model MODEL llama3.2:latest Only used when ollama is in --agents
--ollama-host URL http://localhost:11434 Only used when ollama is in --agents
--ollama-history-length N 10 Only used when ollama is in --agents
--opencode-model MODEL β€” provider/model format. Only used when opencode_cli is in --agents
--setting-sources SOURCE [...] user project local Only used when claude_code_sdk is in --agents

Python

from spych.core import Spych
from spych.orchestrator import SpychOrchestrator
from spych.agents.claude import LocalClaudeCodeCLIResponder
from spych.agents.ollama import OllamaResponder

spych_object = Spych(whisper_model="base.en")

SpychOrchestrator(
    entries=[
        {
            "responder": LocalClaudeCodeCLIResponder(spych_object=spych_object),
            "wake_words": ["claude", "clod", "cloud", "clawed"],
            "terminate_words": ["terminate"],
        },
        {
            "responder": OllamaResponder(spych_object=spych_object, model="llama3.2:latest"),
            "wake_words": ["llama", "ollama", "lama"],
        },
    ]
).start()

OrchestratorEntry Keys

Key Required Default Description
responder βœ“ β€” A BaseResponder instance
wake_words βœ“ β€” Words that trigger this responder. Must be unique across all entries
terminate_words ["terminate"] Words that stop the entire orchestrator

SpychOrchestrator Parameters

Parameter Default Description
entries (required) List of OrchestratorEntry dicts
spych_wake_kwargs None Extra kwargs forwarded to SpychWake

Python β€” Built-in Agents

The same agents available from the CLI can be used directly from Python.

Claude Code CLI

from spych.agents import claude_code_cli

# Say "hey claude" to trigger
claude_code_cli()

Claude Code SDK

from spych.agents import claude_code_sdk

# Say "hey claude" to trigger
claude_code_sdk()

Codex CLI

from spych.agents import codex_cli

# Say "hey codex" to trigger
codex_cli()

Gemini CLI

from spych.agents import gemini_cli

# Say "hey gemini" to trigger
gemini_cli()

OpenCode CLI

from spych.agents import opencode_cli

# Say "hey opencode" to trigger
opencode_cli()

Ollama

from spych.agents import ollama

# Pull the model first: ollama pull llama3.2:latest
# Say "hey llama" to trigger
ollama(model="llama3.2:latest")

Coding Agent Parameters

Parameter claude_code_cli claude_code_sdk codex_cli gemini_cli opencode_cli Description
name Claude Claude Codex Gemini OpenCode Custom display name
wake_words ["claude", "clod", "cloud", "clawed"] ["claude", "clod", "cloud", "clawed"] ["codex"] ["gemini", "google"] ["opencode", "open code"] Words that trigger the agent
terminate_words ["terminate"] ["terminate"] ["terminate"] ["terminate"] ["terminate"] Words that stop the listener
model β€” β€” β€” β€” None Model in provider/model format
listen_duration 0 0 0 0 0 Seconds to listen (0 = VAD auto)
continue_conversation True True True True True Resume the most recent session
setting_sources β€” ["user", "project", "local"] β€” β€” β€” Claude Code settings sources
show_tool_events True True True True True Print live tool start/end events
use_speaker False False False False False Speak responses aloud via TTS
speaker_voice "af_heart" "af_heart" "af_heart" "af_heart" "af_heart" Voice name for TTS
response_style "" "" "" "" "" Style preset or custom instruction
allow_intermediate_responses True True True True True Enable intermediate response chaining
spych_kwargs β€” β€” β€” β€” β€” Extra kwargs passed to Spych
spych_wake_kwargs β€” β€” β€” β€” β€” Extra kwargs passed to SpychWake

Ollama Parameters

Parameter Default Description
name "Ollama" Custom display name
wake_words ["llama", "ollama", "lama"] Words that trigger the agent
terminate_words ["terminate"] Words that stop the listener
model "llama3.2:latest" Ollama model name
listen_duration 0 Seconds to listen (0 = VAD auto)
history_length 10 Past interactions to include in context
host "http://localhost:11434" Ollama instance URL
use_speaker False Speak responses aloud via TTS
speaker_voice "af_heart" Voice name for TTS
response_style "" Style preset or custom instruction
allow_intermediate_responses True Enable intermediate response chaining
spych_kwargs None Extra kwargs passed to Spych
spych_wake_kwargs None Extra kwargs passed to SpychWake

Python: Building Your Own Agent

Subclass BaseResponder, implement respond, and Spych handles the rest: wake word detection, transcription, spinner UI, timing, TTS, error handling.

respond() must return an AgentResponse. Use self.format_prompt() to inject the JSON schema into your prompt and self.parse_output() to parse the result:

from spych.responders import BaseResponder, AgentResponse

class MyResponder(BaseResponder):
    def respond(self, user_input: str) -> AgentResponse:
        raw = call_my_llm(self.format_prompt(user_input))
        return self.parse_output(raw)

A complete working example with a custom wake word:

from spych import Spych, SpychOrchestrator
from spych.responders import BaseResponder, AgentResponse

class EchoResponder(BaseResponder):
    def respond(self, user_input: str) -> AgentResponse:
        return AgentResponse(
            response=f"'{self.name}' heard: {user_input}",
            summary=f"Heard: {user_input}",
            requires_user_feedback=False,
        )

SpychOrchestrator(
    entries=[
        {
            "responder": EchoResponder(
                spych_object=Spych(whisper_model="base.en"),
                listen_duration=5,
                name="TestResponder",
            ),
            "wake_words": ["test"],
            "terminate_words": ["terminate"],
        }
    ]
).start()

You can also subclass a built-in agent. For example, a translation agent that routes to Ollama:

from spych import Spych, SpychOrchestrator
from spych.agents import OllamaResponder
from spych.responders import AgentResponse

class Spanish(OllamaResponder):
    def respond(self, user_input: str) -> AgentResponse:
        user_input = f"Translate the following to Spanish and return only the translated text: '{user_input}'"
        return super().respond(user_input)

class German(OllamaResponder):
    def respond(self, user_input: str) -> AgentResponse:
        user_input = f"Translate the following to German and return only the translated text: '{user_input}'"
        return super().respond(user_input)

spych_object = Spych(whisper_model="base.en")

SpychOrchestrator(
    entries=[
        {
            "responder": Spanish(spych_object=spych_object, name="SpanishTranslator", model="llama3.2:latest"),
            "wake_words": ["spanish"],
            "terminate_words": ["terminate"],
        },
        {
            "responder": German(spych_object=spych_object, name="GermanTranslator", model="llama3.2:latest"),
            "wake_words": ["german"],
            "terminate_words": ["terminate"],
        },
    ]
).start()

Think your agent would be useful to others? Open a PR or file a feature request via a GitHub issue.


Python: Lower-Level API

Need more control? Use Spych and SpychWake directly.

Transcription

from spych import Spych

spych = Spych(
    whisper_model="base.en",  # tiny, small, medium, large β€” all faster-whisper models work
    whisper_device="cpu",     # use "cuda" for Nvidia GPU
)

print(spych.listen(duration=5))

See: https://connor-makowski.github.io/spych/spych/core.html

Wake Word Detection

from spych import SpychWake, Spych

spych = Spych(whisper_model="base.en", whisper_device="cpu")

def on_wake():
    print("Wake word detected! Listening...")
    print(spych.listen(duration=5))

SpychWake(
    wake_word_map={"speech": on_wake},
    whisper_model="tiny.en",
    whisper_device="cpu",
).start()

See: https://connor-makowski.github.io/spych/spych/wake.html


API Reference

Full docs including all parameters and methods: https://connor-makowski.github.io/spych/spych.html


Support

Found a bug or want a new feature? Open an issue on GitHub.


Contributing

Contributions are welcome!

  1. Fork the repo and clone it locally.
  2. Make your changes.
  3. Run tests and make sure they pass.
  4. Commit atomically with clear messages.
  5. Submit a pull request.

Virtual environment setup:

python3.11 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
./utils/test.sh
  1"""
  2# Spych
  3[![PyPI version](https://badge.fury.io/py/spych.svg)](https://badge.fury.io/py/spych)
  4[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
  5[![PyPI Downloads](https://static.pepy.tech/badge/spych/month)](https://pepy.tech/project/spych)
  6
  7**Spych** (pronounced "speech"): Talk with your computer like it's your personal assistant without sending your voice to the cloud.
  8
  9A lightweight, fully offline Python toolkit for wake word detection, audio transcription, spoken AI responses, and AI integrations. Built on [faster-whisper](https://github.com/SYSTRAN/faster-whisper), [PvRecorder](https://github.com/Picovoice/pvrecorder), and [Kokoro](https://github.com/hexgrad/kokoro).
 10
 11**API Docs**: https://connor-makowski.github.io/spych/spych.html
 12
 13---
 14
 15# Installation
 16
 17### Recommended: uv
 18
 19```bash
 20uv tool install spych
 21```
 22
 23### Alternative: pip
 24
 25```bash
 26pip install spych
 27```
 28
 29### TTS Extras
 30
 31By default, Spych automatically installs the right TTS backend for your Python version. You can also install explicitly:
 32
 33```bash
 34uv tool install "spych[kokoro]"       # Fast, lightweight (Python < 3.13 recommended)
 35uv tool install "spych[chatterbox]"   # High-quality voice cloning (Python >= 3.13 required)
 36```
 37
 38---
 39
 40# Quick Start
 41
 42```bash
 43# Navigate to your project directory first
 44cd ~/my_project
 45
 46# Voice-control Claude Code β€” say "hey claude" to trigger
 47spych claude
 48
 49# Use a personality preset β€” say "hey jarvis" to trigger
 50spych claude --personality jarvis
 51
 52# Voice-control a local Ollama model β€” say "hey llama" to trigger
 53spych ollama --model llama3.2:latest
 54```
 55
 56> πŸ’‘ **Pro tip:** Saying "Hey Claude" or "Hey Llama" tends to trigger more reliably than the bare wake word.
 57
 58Say **"terminate"** (or press `Ctrl+C`) to stop any session.
 59
 60---
 61
 62# CLI
 63
 64## Available Agents
 65
 66All agents require their respective CLI tool to be installed and authenticated before use.
 67
 68| Command | Alias | Description | Default wake words |
 69|---|---|---|---|
 70| `spych claude_code_cli` | β€” | Voice-control Claude Code via the CLI | `claude`, `clod`, `cloud`, `clawed` |
 71| `spych claude_code_sdk` | `spych claude` | Voice-control Claude Code via the Agent SDK | `claude`, `clod`, `cloud`, `clawed` |
 72| `spych codex_cli` | `spych codex` | Voice-control the OpenAI Codex agent | `codex` |
 73| `spych gemini_cli` | `spych gemini` | Voice-control the Google Gemini agent | `gemini`, `google` |
 74| `spych opencode_cli` | `spych opencode` | Voice-control the OpenCode agent | `opencode`, `open code` |
 75| `spych ollama` | β€” | Talk to a local Ollama model | `llama`, `ollama`, `lama` |
 76
 77## Available Utilities
 78
 79The following utilities are also available as CLI commands. They don't use wake words, but serve various auxiliary functions like live transcription and voice profiling.
 80
 81| Command | Description |
 82|---|---|
 83| `spych --version` | Print the version number and exit |
 84| `spych --help` | Show detailed usage instructions and exit |
 85| `spych live` | Continuous speech-to-text transcription to file |
 86| `spych live-translation` | (Beta Mode) Continuous speech transcription + translation (bilingual input and output) |
 87| `spych multi` | Run multiple agents simultaneously |
 88| `spych users` | Manage user profiles and global settings |
 89| `spych profile_my_voice` | Record a voice sample for TTS cloning |
 90
 91## Global Flags
 92
 93These must be placed **before** the agent name:
 94
 95```bash
 96spych --theme light claude
 97```
 98
 99| Flag | Options | Default | Description |
100|---|---|---|---|
101| `--theme` | `dark`, `light`, `solarized`, `mono` | `dark` | Terminal colour theme |
102
103> πŸ’‘ **TUI Dashboard:** Spych launches a rich terminal interface by default. Use the `--verbose` flag (e.g., `spych --verbose claude`) to switch to a simpler, non-interactive scrollable output.
104
105---
106
107## Common Flags
108
109All agent subcommands accept these flags:
110
111| Flag | Default | Description |
112|---|---|---|
113| `--personality NAME` | β€” | Apply a named preset (sets wake words, voice, name, style) |
114| `--name NAME` | *(agent default)* | Custom display name shown in the terminal |
115| `--wake-words WORD [...]` | *(agent default)* | One or more words that trigger the agent |
116| `--terminate-words WORD [...]` | `terminate` | Words that stop the listener |
117| `--listen-duration SECONDS` | `0` (VAD auto) | Seconds to record after wake word |
118| `--follow-up-listen-duration SECONDS` | `0` | Seconds to listen for a follow-up answer |
119| `--inactivity-timeout SECONDS` | `4.0` | Seconds of silence before returning to wake word |
120| `--use-speaker BOOL` | `true` | Speak responses aloud via TTS |
121| `--speaker-voice VOICE` | `af_heart` | Voice name for spoken responses |
122| `--speaker-backend BACKEND` | *(auto)* | `chatterbox` or `kokoro` |
123| `--response-style STYLE` | β€” | Style preset or custom instruction for spoken output |
124| `--intermediate-responses BOOL` | `true` | Enable intermediate response chaining for long-running tasks |
125
126Coding agents (`claude`, `codex`, `gemini`, `opencode`) also accept:
127
128| Flag | Default | Description |
129|---|---|---|
130| `--continue-conversation BOOL` | `true` | Resume the most recent session |
131| `--show-tool-events BOOL` | `true` | Print live tool start/end events |
132
133Agent-specific flags:
134
135| Agent | Flag | Default | Description |
136|---|---|---|---|
137| `ollama` | `--model` | `llama3.2:latest` | Ollama model name |
138| `ollama` | `--history-length` | `10` | Past interactions to include in context |
139| `ollama` | `--host` | `http://localhost:11434` | Ollama instance URL |
140| `opencode_cli` | `--model` | β€” | Model in `provider/model` format |
141| `claude_code_sdk` | `--setting-sources` | `user project local` | Claude Code settings sources |
142
143---
144
145# Personalities
146
147Personalities are named presets that bundle a wake word list, voice, display name, and response style into a single flag. Any explicit flag overrides the preset.
148
149```bash
150spych claude --personality jarvis
151# equivalent to:
152spych claude --name "JARVIS" --wake-words jarvis jarves \
153             --speaker-voice bm_george --use-speaker true \
154             --response-style jarvis
155```
156
157| Name | Wake words | Voice | Style |
158|---|---|---|---|
159| `assistant` | `assistant`, `helper`, `computer` | `af_heart` | `assistant` β€” helpful, precise, informative |
160| `friend` | `friend`, `buddy`, `pal` | `af_amy` | `friendly` β€” warm and simple |
161| `jarvis` | `jarvis`, `jarves`, `jargus`, `jervis` | `bm_george` | `jarvis` β€” precise, dry wit, "sir" |
162| `pirate` | `blackbeard`, `pirate`, `ahoy` | `am_michael` | `pirate` β€” pirate speak, colorful |
163| `news_anchor` | `bella`, `news anchor`, `anchor` | `af_bella` | `news_anchor` β€” professional broadcast tone |
164| `robot` | `rob`, `robot` | `am_adam` | `robot` β€” monotone, literal |
165| `caveman` | `er`, `ur`, `caveman`, `cave man` | `am_onyx` | `caveman` β€” very simple, direct |
166
167---
168
169# User Management
170
171Spych supports multiple user profiles, allowing agents to provide more personalized responses based on your name, age, and other context.
172
173```bash
174# Launch the interactive user management menu
175spych users
176```
177
178The `users` utility allows you to:
179- Create, edit, and delete user profiles.
180- Set a default user for all agents.
181- Change the global terminal theme (`dark`, `light`, `solarized`, `mono`).
182
183You can also specify a user for a specific session:
184```bash
185spych claude --user Connor
186```
187
188---
189
190# Response Styles
191
192The `--response-style` flag shapes how the agent formats its spoken output.
193
194| Style | Description |
195|---|---|
196| `assistant` | Helpful and precise, concise and informative |
197| `concise` | Key points only, direct |
198| `friendly` | Warm, approachable, simple language |
199| `military` | Brevity-style, short sentences |
200| `five_year_old` | Simple words, very short |
201| `fast` | As brief as reasonably possible |
202| `pirate` | Pirate speak, colorful |
203| `news_anchor` | Professional broadcast tone |
204| `haiku` | 5-7-5 haiku form |
205| `shakespearean` | Elizabethan English |
206| `robot` | Monotone, literal |
207| `caveman` | Very simple, direct |
208| `yoda` | Inverted sentence structure |
209| `jarvis` | JARVIS from Iron Man β€” precise, dry wit, addresses user as "sir" or "ma'am" |
210
211You can also pass any custom instruction string directly: `--response-style "Reply in exactly one sentence."`.
212
213---
214
215# Text-to-Speech & Voices
216
217Spoken responses are enabled by default for personality presets and when `--use-speaker true` is set.
218
219```bash
220spych claude --use-speaker true --speaker-voice bm_george
221spych claude --use-speaker true --speaker-backend kokoro
222spych claude --use-speaker false   # disable TTS
223```
224
225When TTS is active, short responses are spoken verbatim; longer ones use the agent's short `summary`. If the response ends with a question, Spych automatically listens for a follow-up β€” no wake word required.
226
227### TTS Backends
228
229| Backend | Best for | Python support |
230|---|---|---|
231| **Chatterbox** (default priority) | Natural voices, zero-shot voice cloning | 3.11+ (required for 3.13+) |
232| **Kokoro** (lightweight fallback) | Fast, low-resource devices (e.g. Raspberry Pi) | 3.11–3.12 recommended |
233
234Spych tries Chatterbox first, then Kokoro. Use `--speaker-backend` to force one explicitly.
235
236### Available Voices
237
238The same voice names work for both backends.
239
240- Chatterbox wave voices: https://github.com/connor-makowski/spych/tree/main/voices/wave
241- Kokoro pt voices (56 total): https://github.com/connor-makowski/spych/tree/main/voices/pt
242
243American English (`am_` / `af_`):
244
245| Voice | Gender | Grade |
246|---|---|---|
247| `af_heart` | F | A (default) |
248| `af_bella` | F | A- |
249| `af_nicole` | F | B- |
250| `am_michael` | M | C+ |
251| `am_fenrir` | M | C+ |
252| `am_puck` | M | C+ |
253
254British English (`bm_` / `bf_`):
255
256| Voice | Gender | Grade |
257|---|---|---|
258| `bf_emma` | F | B- |
259| `bf_isabella` | F | C |
260| `bm_george` | M | C |
261
262### Voice Cloning
263
264Record a 10-second sample of your voice, then use it as the speaker voice. Requires the Chatterbox backend.
265
266```bash
267# Step 1: record your profile
268spych profile_my_voice --name my_voice
269
270# Step 2: use it
271spych claude --use-speaker true --speaker-voice my_voice --speaker-backend chatterbox
272
273# Or use any .wav file directly
274spych claude --use-speaker true --speaker-voice /path/to/my_voice.wav --speaker-backend chatterbox
275```
276
277---
278
279# Live Transcription
280
281`spych live` continuously records from the microphone using VAD and writes the transcript to disk in real time. No wake word required β€” it transcribes everything until stopped.
282
283## CLI
284
285```bash
286spych live                                                 # writes transcript.srt
287spych live --output-path meeting --output-format both
288spych live --terminate-words "stop recording"
289spych live --no-timestamps --whisper-model medium.en
290```
291
292Stop by pressing the stop key (default: `q` + Enter), saying a terminate word, or pressing `Ctrl+C`.
293
294### Parameters
295
296| Flag | Default | Description |
297|---|---|---|
298| `--output-path PATH` | `transcript` | Base output file path without extension |
299| `--output-format FORMAT` | `srt` | `txt`, `srt`, or `both` |
300| `--no-timestamps` | false | Omit timestamps from terminal and `.txt` output |
301| `--stop-key KEY` | `q` | Key (then Enter) to stop the session |
302| `--terminate-words WORD [...]` | β€” | Spoken words that stop the session |
303| `--device-index N` | `-1` | Microphone device index; -1 uses system default |
304| `--whisper-model MODEL` | `base.en` | faster-whisper model name |
305| `--whisper-device DEVICE` | `cpu` | `cpu` or `cuda` |
306| `--whisper-compute-type TYPE` | `int8` | `int8`, `float16`, or `float32` |
307| `--no-speech-threshold FLOAT` | `0.3` | Whisper segments above this `no_speech_prob` are dropped |
308| `--speech-threshold FLOAT` | `0.5` | VAD speech onset probability |
309| `--silence-threshold FLOAT` | `0.35` | VAD silence probability during speech |
310| `--silence-frames N` | `20` | Consecutive silent frames to end a segment (~32ms each) |
311| `--speech-pad-frames N` | `5` | Pre-roll frames and onset confirmation count |
312| `--max-speech-duration SECONDS` | `30.0` | Hard cap on a single segment |
313| `--context-words N` | `32` | Trailing words passed as whisper `initial_prompt` |
314
315## Python
316
317```python
318from spych.live import SpychLive
319
320SpychLive(
321    output_format="srt",          # "txt", "srt", or "both"
322    output_path="my_transcript",  # written to my_transcript.srt
323    show_timestamps=True,
324    stop_key="q",                 # type q + Enter to stop
325    terminate_words=["stop recording"],
326).start()
327```
328
329### `SpychLive` Parameters
330
331| Parameter | Default | Description |
332|---|---|---|
333| `output_format` | `"srt"` | Output format(s): `"txt"`, `"srt"`, or `"both"` |
334| `output_path` | `"transcript"` | Base path without extension |
335| `show_timestamps` | `True` | Prepend `[HH:MM:SS]` timestamps to terminal and `.txt` output |
336| `stop_key` | `"q"` | Key (then Enter) to stop the session |
337| `terminate_words` | `None` | Spoken words that stop the session |
338| `on_terminate` | `None` | No-argument callback executed when a terminate word fires |
339| `device_index` | `-1` | Microphone device index; `-1` uses system default |
340| `whisper_model` | `"base.en"` | faster-whisper model name |
341| `whisper_device` | `"cpu"` | Device for inference: `"cpu"` or `"cuda"` |
342| `whisper_compute_type` | `"int8"` | Compute precision: `"int8"`, `"float16"`, or `"float32"` |
343| `no_speech_threshold` | `0.4` | Whisper segments above this are discarded |
344| `speech_threshold` | `0.5` | Silero VAD onset probability |
345| `silence_threshold` | `0.35` | Silero VAD silence probability during speech |
346| `silence_frames_threshold` | `20` | Consecutive silent frames to close a segment |
347| `speech_pad_frames` | `5` | Pre-roll frame count and onset confirmation threshold |
348| `max_speech_duration_s` | `30.0` | Hard cap on a single segment in seconds |
349| `context_words` | `32` | Trailing transcript words passed as `initial_prompt` |
350
351---
352
353# Live Translation
354
355**Currently in beta: expect some rough edges.**
356
357`spych live-translation` starts a **bidirectional** live translation session between two languages. Either participant can speak in either language β€” Whisper transcribes each utterance, Ollama detects which language was spoken and translates it to the other, and each segment is shown as two lines in real time. The translated text is also spoken aloud via TTS by default.
358
359Note: This is currently in beta mode and the API may change without a major version bump. Feedback is very welcome!
360
361Requires [Ollama](https://ollama.com) running locally with the translation model pulled. By default, Spych uses the `llama3.2` model, but you can specify any Ollama model with `--ollama-translation-model`.
362
363## CLI
364
365```bash
366# English ↔ Spanish conversation
367spych live-translation --languages en es
368
369# English ↔ French, disable TTS
370spych live-translation --languages en fr --no-speaker
371
372# Use a larger Ollama model and save a bilingual transcript
373spych live-translation --languages en de \
374    --ollama-translation-model mistral --output-format both
375```
376
377Stop by pressing the stop key (default: `q` + Enter), saying a terminate word, or pressing `Ctrl+C`.
378
379Each utterance is shown as two lines β€” the original and the translation:
380
381```
382[00:00:05](es) Hola, ΒΏcΓ³mo estΓ‘s?
383[00:00:05](en) Hello, how are you?
384```
385
386If Ollama is unreachable, the session continues and the translation line shows `[translation unavailable]` for that segment.
387
388### Parameters
389
390| Flag | Default | Description |
391|---|---|---|
392| `--languages LANG LANG` | *(required)* | Two BCP-47 language codes for the conversation pair (e.g. `en es`) |
393| `--ollama-host URL` | `http://localhost:11434` | Ollama HTTP base URL |
394| `--ollama-translation-model MODEL` | `llama3.2` | Ollama model used for translation |
395| `--no-speaker` | false | Disable TTS β€” speaker is on by default |
396| `--speaker-voice VOICE` | *(model default)* | Wave voice name for zero-shot cloning; omit to use the model's built-in default voice |
397| `--output-path PATH` | `transcript` | Base output file path without extension |
398| `--output-format FORMAT` | *(none)* | Save transcript to file: `txt`, `srt`, or `both`; omit for terminal-only output |
399| `--no-timestamps` | false | Omit timestamps from terminal and `.txt` output |
400| `--stop-key KEY` | `q` | Key (then Enter) to stop the session |
401| `--terminate-words WORD [...]` | β€” | Spoken words that stop the session |
402| `--device-index N` | `-1` | Microphone device index; -1 uses system default |
403| `--whisper-model MODEL` | `small` | faster-whisper model name; `.en` suffix is stripped automatically |
404| `--whisper-device DEVICE` | `auto` | `auto`, `cpu`, or `cuda`; `auto` selects `cuda` when available on Python ≀3.13 |
405| `--whisper-compute-type TYPE` | `int8` | `int8`, `float16`, or `float32` |
406| `--no-speech-threshold FLOAT` | `0.3` | Whisper segments above this `no_speech_prob` are dropped |
407| `--speech-threshold FLOAT` | `0.5` | VAD speech onset probability |
408| `--silence-threshold FLOAT` | `0.35` | VAD silence probability during speech |
409| `--silence-frames N` | `20` | Consecutive silent frames to end a segment (~32ms each) |
410| `--speech-pad-frames N` | `5` | Pre-roll frames and onset confirmation count |
411| `--max-speech-duration SECONDS` | `30.0` | Hard cap on a single segment |
412
413## Python
414
415```python
416from spych.live_translation import SpychLiveTranslation
417
418SpychLiveTranslation(
419    lang_a="en",
420    lang_b="es",
421    output_format="both",          # "txt", "srt", or "both"; "" for terminal only
422    output_path="my_translation",  # written to my_translation.txt + .srt
423    show_timestamps=True,
424    stop_key="q",
425    terminate_words=["terminate"],
426    ollama_host="http://localhost:11434",
427    ollama_translation_model="llama3.2",
428    use_speaker=True,
429    speaker_voice="",              # "" uses the model's built-in default voice
430).start()
431```
432
433### `SpychLiveTranslation` Parameters
434
435| Parameter | Default | Description |
436|---|---|---|
437| `lang_a` | *(required)* | BCP-47 code of the first language (e.g. `"en"`) |
438| `lang_b` | *(required)* | BCP-47 code of the second language (e.g. `"es"`) |
439| `output_format` | `""` | Output format(s): `"txt"`, `"srt"`, or `"both"`; `""` disables file output |
440| `output_path` | `"transcript"` | Base path without extension |
441| `show_timestamps` | `True` | Prepend `[HH:MM:SS]` timestamps to terminal and `.txt` output |
442| `stop_key` | `"q"` | Key (then Enter) to stop the session |
443| `terminate_words` | `None` | Spoken words that stop the session |
444| `device_index` | `-1` | Microphone device index; `-1` uses system default |
445| `whisper_model` | `"base"` | faster-whisper model name; `.en` suffix stripped automatically |
446| `whisper_device` | `"auto"` | Device for inference: `"auto"`, `"cpu"`, or `"cuda"` |
447| `whisper_compute_type` | `"int8"` | Compute precision: `"int8"`, `"float16"`, or `"float32"` |
448| `no_speech_threshold` | `0.4` | Whisper segments above this are discarded |
449| `speech_threshold` | `0.5` | Silero VAD onset probability |
450| `silence_threshold` | `0.35` | Silero VAD silence probability during speech |
451| `silence_frames_threshold` | `20` | Consecutive silent frames to close a segment |
452| `speech_pad_frames` | `5` | Pre-roll frame count and onset confirmation threshold |
453| `max_speech_duration_s` | `30.0` | Hard cap on a single segment in seconds |
454| `ollama_host` | `"http://localhost:11434"` | Ollama HTTP base URL for translation |
455| `ollama_translation_model` | `"llama3.2"` | Ollama model used for translation |
456| `use_speaker` | `True` | Speak each translated segment aloud via TTS |
457| `speaker_voice` | `""` | Wave voice name for zero-shot cloning; `""` uses the model's built-in default voice |
458
459---
460
461# Multi-agent
462
463Run several agents simultaneously under a single listener, each bound to its own wake words. Say "hey claude" to talk to Claude, "hey llama" to talk to Ollama β€” all in the same terminal session.
464
465## CLI
466
467```bash
468# Two agents, default wake words
469spych multi --agents claude gemini
470
471# Include Ollama with a specific model
472spych multi --agents claude ollama --ollama-model llama3.2:latest
473
474# Tune listen duration across all agents
475spych multi --agents claude codex --listen-duration 8
476```
477
478### Multi-agent CLI Flags
479
480| Flag | Default | Description |
481|---|---|---|
482| `--agents AGENT [...]` | *(required)* | Agents to run: `claude` (`claude_code_cli`), `claude_sdk` (`claude_code_sdk`), `codex` (`codex_cli`), `gemini` (`gemini_cli`), `opencode` (`opencode_cli`), `ollama` |
483| `--terminate-words WORD [...]` | `terminate` | Words that stop all agents |
484| `--listen-duration SECONDS` | `5` | Seconds to listen after a wake word |
485| `--follow-up-listen-duration SECONDS` | `0` | Seconds to listen for follow-up answers |
486| `--inactivity-timeout SECONDS` | `4.0` | Seconds of silence before returning to wake word |
487| `--continue-conversation BOOL` | `true` | Resume the most recent session for each coding agent |
488| `--show-tool-events BOOL` | `true` | Print live tool start/end events |
489| `--use-speaker BOOL` | `true` | Speak responses aloud via TTS |
490| `--speaker-backend BACKEND` | *(auto)* | `chatterbox` or `kokoro` |
491| `--intermediate-responses BOOL` | `true` | Enable intermediate response chaining for long-running tasks |
492| `--ollama-model MODEL` | `llama3.2:latest` | Only used when `ollama` is in `--agents` |
493| `--ollama-host URL` | `http://localhost:11434` | Only used when `ollama` is in `--agents` |
494| `--ollama-history-length N` | `10` | Only used when `ollama` is in `--agents` |
495| `--opencode-model MODEL` | β€” | `provider/model` format. Only used when `opencode_cli` is in `--agents` |
496| `--setting-sources SOURCE [...]` | `user project local` | Only used when `claude_code_sdk` is in `--agents` |
497
498## Python
499
500```python
501from spych.core import Spych
502from spych.orchestrator import SpychOrchestrator
503from spych.agents.claude import LocalClaudeCodeCLIResponder
504from spych.agents.ollama import OllamaResponder
505
506spych_object = Spych(whisper_model="base.en")
507
508SpychOrchestrator(
509    entries=[
510        {
511            "responder": LocalClaudeCodeCLIResponder(spych_object=spych_object),
512            "wake_words": ["claude", "clod", "cloud", "clawed"],
513            "terminate_words": ["terminate"],
514        },
515        {
516            "responder": OllamaResponder(spych_object=spych_object, model="llama3.2:latest"),
517            "wake_words": ["llama", "ollama", "lama"],
518        },
519    ]
520).start()
521```
522
523### `OrchestratorEntry` Keys
524
525| Key | Required | Default | Description |
526|---|---|---|---|
527| `responder` | βœ“ | β€” | A `BaseResponder` instance |
528| `wake_words` | βœ“ | β€” | Words that trigger this responder. Must be unique across all entries |
529| `terminate_words` | | `["terminate"]` | Words that stop the entire orchestrator |
530
531### `SpychOrchestrator` Parameters
532
533| Parameter | Default | Description |
534|---|---|---|
535| `entries` | *(required)* | List of `OrchestratorEntry` dicts |
536| `spych_wake_kwargs` | `None` | Extra kwargs forwarded to `SpychWake` |
537
538---
539
540# Python β€” Built-in Agents
541
542The same agents available from the CLI can be used directly from Python.
543
544## Claude Code CLI
545
546```python
547from spych.agents import claude_code_cli
548
549# Say "hey claude" to trigger
550claude_code_cli()
551```
552
553## Claude Code SDK
554
555```python
556from spych.agents import claude_code_sdk
557
558# Say "hey claude" to trigger
559claude_code_sdk()
560```
561
562## Codex CLI
563
564```python
565from spych.agents import codex_cli
566
567# Say "hey codex" to trigger
568codex_cli()
569```
570
571## Gemini CLI
572
573```python
574from spych.agents import gemini_cli
575
576# Say "hey gemini" to trigger
577gemini_cli()
578```
579
580## OpenCode CLI
581
582```python
583from spych.agents import opencode_cli
584
585# Say "hey opencode" to trigger
586opencode_cli()
587```
588
589## Ollama
590
591```python
592from spych.agents import ollama
593
594# Pull the model first: ollama pull llama3.2:latest
595# Say "hey llama" to trigger
596ollama(model="llama3.2:latest")
597```
598
599### Coding Agent Parameters
600
601| Parameter | `claude_code_cli` | `claude_code_sdk` | `codex_cli` | `gemini_cli` | `opencode_cli` | Description |
602|---|---|---|---|---|---|---|
603| `name` | `Claude` | `Claude` | `Codex` | `Gemini` | `OpenCode` | Custom display name |
604| `wake_words` | `["claude", "clod", "cloud", "clawed"]` | `["claude", "clod", "cloud", "clawed"]` | `["codex"]` | `["gemini", "google"]` | `["opencode", "open code"]` | Words that trigger the agent |
605| `terminate_words` | `["terminate"]` | `["terminate"]` | `["terminate"]` | `["terminate"]` | `["terminate"]` | Words that stop the listener |
606| `model` | β€” | β€” | β€” | β€” | `None` | Model in `provider/model` format |
607| `listen_duration` | `0` | `0` | `0` | `0` | `0` | Seconds to listen (0 = VAD auto) |
608| `continue_conversation` | `True` | `True` | `True` | `True` | `True` | Resume the most recent session |
609| `setting_sources` | β€” | `["user", "project", "local"]` | β€” | β€” | β€” | Claude Code settings sources |
610| `show_tool_events` | `True` | `True` | `True` | `True` | `True` | Print live tool start/end events |
611| `use_speaker` | `False` | `False` | `False` | `False` | `False` | Speak responses aloud via TTS |
612| `speaker_voice` | `"af_heart"` | `"af_heart"` | `"af_heart"` | `"af_heart"` | `"af_heart"` | Voice name for TTS |
613| `response_style` | `""` | `""` | `""` | `""` | `""` | Style preset or custom instruction |
614| `allow_intermediate_responses` | `True` | `True` | `True` | `True` | `True` | Enable intermediate response chaining |
615| `spych_kwargs` | β€” | β€” | β€” | β€” | β€” | Extra kwargs passed to `Spych` |
616| `spych_wake_kwargs` | β€” | β€” | β€” | β€” | β€” | Extra kwargs passed to `SpychWake` |
617
618### Ollama Parameters
619
620| Parameter | Default | Description |
621|---|---|---|
622| `name` | `"Ollama"` | Custom display name |
623| `wake_words` | `["llama", "ollama", "lama"]` | Words that trigger the agent |
624| `terminate_words` | `["terminate"]` | Words that stop the listener |
625| `model` | `"llama3.2:latest"` | Ollama model name |
626| `listen_duration` | `0` | Seconds to listen (0 = VAD auto) |
627| `history_length` | `10` | Past interactions to include in context |
628| `host` | `"http://localhost:11434"` | Ollama instance URL |
629| `use_speaker` | `False` | Speak responses aloud via TTS |
630| `speaker_voice` | `"af_heart"` | Voice name for TTS |
631| `response_style` | `""` | Style preset or custom instruction |
632| `allow_intermediate_responses` | `True` | Enable intermediate response chaining |
633| `spych_kwargs` | `None` | Extra kwargs passed to `Spych` |
634| `spych_wake_kwargs` | `None` | Extra kwargs passed to `SpychWake` |
635
636---
637
638# Python: Building Your Own Agent
639
640Subclass `BaseResponder`, implement `respond`, and Spych handles the rest: wake word detection, transcription, spinner UI, timing, TTS, error handling.
641
642`respond()` must return an `AgentResponse`. Use `self.format_prompt()` to inject the JSON schema into your prompt and `self.parse_output()` to parse the result:
643
644```python
645from spych.responders import BaseResponder, AgentResponse
646
647class MyResponder(BaseResponder):
648    def respond(self, user_input: str) -> AgentResponse:
649        raw = call_my_llm(self.format_prompt(user_input))
650        return self.parse_output(raw)
651```
652
653A complete working example with a custom wake word:
654
655```python
656from spych import Spych, SpychOrchestrator
657from spych.responders import BaseResponder, AgentResponse
658
659class EchoResponder(BaseResponder):
660    def respond(self, user_input: str) -> AgentResponse:
661        return AgentResponse(
662            response=f"'{self.name}' heard: {user_input}",
663            summary=f"Heard: {user_input}",
664            requires_user_feedback=False,
665        )
666
667SpychOrchestrator(
668    entries=[
669        {
670            "responder": EchoResponder(
671                spych_object=Spych(whisper_model="base.en"),
672                listen_duration=5,
673                name="TestResponder",
674            ),
675            "wake_words": ["test"],
676            "terminate_words": ["terminate"],
677        }
678    ]
679).start()
680```
681
682You can also subclass a built-in agent. For example, a translation agent that routes to Ollama:
683
684```python
685from spych import Spych, SpychOrchestrator
686from spych.agents import OllamaResponder
687from spych.responders import AgentResponse
688
689class Spanish(OllamaResponder):
690    def respond(self, user_input: str) -> AgentResponse:
691        user_input = f"Translate the following to Spanish and return only the translated text: '{user_input}'"
692        return super().respond(user_input)
693
694class German(OllamaResponder):
695    def respond(self, user_input: str) -> AgentResponse:
696        user_input = f"Translate the following to German and return only the translated text: '{user_input}'"
697        return super().respond(user_input)
698
699spych_object = Spych(whisper_model="base.en")
700
701SpychOrchestrator(
702    entries=[
703        {
704            "responder": Spanish(spych_object=spych_object, name="SpanishTranslator", model="llama3.2:latest"),
705            "wake_words": ["spanish"],
706            "terminate_words": ["terminate"],
707        },
708        {
709            "responder": German(spych_object=spych_object, name="GermanTranslator", model="llama3.2:latest"),
710            "wake_words": ["german"],
711            "terminate_words": ["terminate"],
712        },
713    ]
714).start()
715```
716
717Think your agent would be useful to others? Open a PR or file a feature request via a [GitHub issue](https://github.com/connor-makowski/spych/issues).
718
719---
720
721# Python: Lower-Level API
722
723Need more control? Use `Spych` and `SpychWake` directly.
724
725## Transcription
726
727```python
728from spych import Spych
729
730spych = Spych(
731    whisper_model="base.en",  # tiny, small, medium, large β€” all faster-whisper models work
732    whisper_device="cpu",     # use "cuda" for Nvidia GPU
733)
734
735print(spych.listen(duration=5))
736```
737
738See: https://connor-makowski.github.io/spych/spych/core.html
739
740## Wake Word Detection
741
742```python
743from spych import SpychWake, Spych
744
745spych = Spych(whisper_model="base.en", whisper_device="cpu")
746
747def on_wake():
748    print("Wake word detected! Listening...")
749    print(spych.listen(duration=5))
750
751SpychWake(
752    wake_word_map={"speech": on_wake},
753    whisper_model="tiny.en",
754    whisper_device="cpu",
755).start()
756```
757
758See: https://connor-makowski.github.io/spych/spych/wake.html
759
760---
761
762# API Reference
763
764Full docs including all parameters and methods: https://connor-makowski.github.io/spych/spych.html
765
766---
767
768# Support
769
770Found a bug or want a new feature? [Open an issue on GitHub](https://github.com/connor-makowski/spych/issues).
771
772---
773
774# Contributing
775
776Contributions are welcome!
777
7781. Fork the repo and clone it locally.
7792. Make your changes.
7803. Run tests and make sure they pass.
7814. Commit atomically with clear messages.
7825. Submit a pull request.
783
784**Virtual environment setup:**
785```bash
786python3.11 -m venv venv
787source venv/bin/activate
788pip install -r requirements.txt
789./utils/test.sh
790```
791"""
792
793from .core import Spych
794from .wake import SpychWake
795from .orchestrator import SpychOrchestrator