From 3d96087f531b8bcb5b97d04e50685636811cc3e6 Mon Sep 17 00:00:00 2001 From: musingfox Date: Thu, 1 Oct 2026 17:59:58 +0800 Subject: [PATCH 1/9] fix(cli): list xai in --provider help and drop the model example The factory accepts xai/grok, and the gemini-2.5-flash example implied a default the config owns. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/cli.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 70aaba8..8c27066 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -69,11 +69,11 @@ pub struct SumArgs { /// Text to summarize (use "-" to read from stdin) pub text: String, - /// LLM provider: google, anthropic, openai, ollama + /// LLM provider: google, anthropic, openai, xai, ollama #[arg(long)] pub provider: Option, - /// Model name (e.g., gemini-2.5-flash, gpt-4o-mini) + /// Model name; replaces the model configured for the chosen provider #[arg(long)] pub model: Option, From abc0ccae262daaf182988048a1bb46ac30469dbf Mon Sep 17 00:00:00 2001 From: musingfox Date: Thu, 1 Oct 2026 18:00:11 +0800 Subject: [PATCH 2/9] docs(config): fix voice and model claims, drop pricing from recommended.toml xAI and ElevenLabs require a voice (src/tts/mod.rs), and the ElevenLabs model id is eleven_v3. Pricing and the unenforced macOS rate range go, since they rot. Co-Authored-By: Claude Opus 5.5 (1M context) --- config/recommended.toml | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/config/recommended.toml b/config/recommended.toml index 0acd582..45abf9a 100644 --- a/config/recommended.toml +++ b/config/recommended.toml @@ -17,7 +17,7 @@ disable_thinking = false # Set true to reduce token usage # Uncomment and configure the providers you want to use # Edit api_key = "${PROVIDER_API_KEY}" with your actual API key -# Google Gemini (Recommended: fast, cheap, reliable) +# Google Gemini (Recommended: fast and reliable) [[llm.providers]] name = "google" model = "gemini-3.1-flash-lite" @@ -41,7 +41,7 @@ api_key = "${PROVIDER_API_KEY}" # Get from: https://platform.openai.com # base_url = "https://api.openai.com/v1" # Optional: for Azure OpenAI or compatible APIs timeout = 10 -# xAI Grok (OpenAI-compatible API; very cheap) +# xAI Grok (OpenAI-compatible API) # Get API key: https://console.x.ai # Uncomment to enable: # [[llm.providers]] @@ -70,7 +70,7 @@ timeout = 60 # Local inference needs more time name = "macos" # api_key not required - macOS TTS is built-in, no API key needed # voice = "Daniel" # Optional: specify voice (run 'say -v ?' to list available voices) -rate = 200 # Speech rate (90-300, default: 200) +rate = 200 # Speech rate in words per minute (default: 200) # volume = 100 # Playback volume (0-100); synthesis is rendered to a file and # played by the platform player, so the knob applies here too @@ -125,36 +125,30 @@ rate = 200 # Speech rate (90-300, default: 200) # volume = 50 # Playback volume (0-100, default: 100) # # Gemini TTS output is loud by default, 40-60 is recommended -# xAI TTS (Natural speech, 5 voices, requires API key) -# Pricing: $4.20 per 1M characters (Beta) -# Voices: eve (default), ara, rex, sal, leo +# xAI TTS (requires API key; `voice` is required, there is no default) # Get API key: https://console.x.ai # Uncomment to enable: # [[tts.providers]] # name = "xai" -# voice = "eve" # Options: eve, ara, rex, sal, leo +# voice = "eve" # Required. Options: eve, ara, rex, sal, leo # language_code = "auto" # Language: "auto", "en", "zh", "ja", etc. # api_key = "${XAI_API_KEY}" # volume = 100 # Playback volume (0-100, default: 100) -# ElevenLabs TTS (Premium voices, very natural, requires API key) -# Pricing: -# Flash v2.5 / Turbo v2.5: $0.06 per 1K characters (~75ms latency) -# Multilingual v2 / v3: $0.12 per 1K characters (~300ms latency, higher quality) +# ElevenLabs TTS (requires API key; `model` and `voice` are both required) # Get API key: https://elevenlabs.io/app/settings/api-keys # Browse voices: https://elevenlabs.io/app/voice-library # Uncomment to enable: # [[tts.providers]] # name = "elevenlabs" -# model = "eleven_flash_v2_5" # Options: eleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_multilingual_v3 -# voice = "21m00Tcm4TlvDq8ikWAM" # Voice ID (default: Rachel). Other stock voices: +# model = "eleven_flash_v2_5" # Options: eleven_flash_v2_5, eleven_turbo_v2_5, eleven_multilingual_v2, eleven_v3 +# voice = "21m00Tcm4TlvDq8ikWAM" # Voice ID (required; Rachel here). Other stock voices: # # JBFqnCBsd6RMkjVDRZzb (George), TX3LPaxmHKxFdv7VOQHJ (Liam), # # pNInz6obpgDQGcFmaJgB (Adam), EXAVITQu4vr4xnSDxMaL (Bella) # api_key = "${ELEVENLABS_API_KEY}" # volume = 100 # Playback volume (0-100, default: 100) # OpenAI TTS (gpt-4o-mini-tts, steerable style via instructions, requires API key) -# Pricing: ~$0.015 per minute of audio (gpt-4o-mini-tts) # Voices: alloy, ash, ballad, coral, echo, fable, nova, onyx, sage, shimmer # Get API key: https://platform.openai.com # Uncomment to enable: @@ -169,9 +163,6 @@ rate = 200 # Speech rate (90-300, default: 200) # api_key = "${OPENAI_API_KEY}" # Google Cloud TTS (Professional-grade, requires service account) -# Pricing: -# Standard voices: $4 per 1M characters (first 4M/month free) -# WaveNet voices: $16 per 1M characters (first 1M/month free) # Setup: https://cloud.google.com/text-to-speech/docs/before-you-begin # Uncomment to enable: # [[tts.providers]] @@ -185,9 +176,6 @@ rate = 200 # Speech rate (90-300, default: 200) # Gemini-TTS (expressive voices via the same Cloud TTS endpoint) # Set `model` to switch cloud_tts into Gemini-TTS mode: bare voice names # (Kore, Charon, Puck, ...) plus an optional `style_prompt` for tone control. -# Pricing: -# Token-based, NOT per-character: $10 per 1M audio output tokens -# (25 tokens/sec of audio). No free tier. Coarsely ~4x the cost of Standard. # Setup: https://cloud.google.com/text-to-speech/docs/gemini-tts # Uncomment to enable: # [[tts.providers]] From 2633034ef6d2fa55491ed6193f6372777c3aab1c Mon Sep 17 00:00:00 2001 From: musingfox Date: Thu, 1 Oct 2026 18:00:43 +0800 Subject: [PATCH 3/9] docs: cut README to pitch, setup, troubleshooting and menu bar CLI, provider, config-structure and env-var reference now point to `sumvox --help` and config/recommended.toml, which also removes the wrong default-voice claims and the three-file watcher claim. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 518 ++++++------------------------------------------------ 1 file changed, 57 insertions(+), 461 deletions(-) diff --git a/README.md b/README.md index 06cda62..054c61d 100644 --- a/README.md +++ b/README.md @@ -8,44 +8,32 @@ without switching context. Runs on macOS and Linux. [![CI](https://github.com/musingfox/sumvox/actions/workflows/ci.yml/badge.svg)](https://github.com/musingfox/sumvox/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![crates.io](https://img.shields.io/crates/v/sumvox.svg)](https://crates.io/crates/sumvox) -## โœจ Features +## Features -- โšก **Single Rust binary** โ€” no runtime, no dependencies to install -- ๐Ÿง  **Multi-model LLM**: Google Gemini (recommended), Anthropic Claude, OpenAI GPT, xAI Grok, Ollama -- ๐Ÿ”Š **Eight TTS engines**: macOS `say`, espeak-ng, piper (local/offline) plus Google TTS, - Google Cloud TTS, xAI, OpenAI and ElevenLabs (cloud) -- ๐Ÿ”„ **Smart fallback**: providers are tried in order until one succeeds โ€” for both LLM and TTS -- ๐Ÿ”‰ **Volume control everywhere**: one 0โ€“100 knob applies to every engine on both platforms -- ๐ŸŽจ **TOML configuration** with inline comments -- ๐Ÿ“ **Localization**: works in any language the chosen engines support -- ๐Ÿช **Claude Code hooks**: Notification and Stop events out of the box -- ๐Ÿ–ฅ๏ธ **Optional macOS menu bar app**: mute toggle, notification history, talking orb +- Single Rust binary, no runtime to install +- LLM summaries from Gemini, Anthropic, OpenAI, xAI Grok or Ollama +- Eight TTS engines: macOS `say`, espeak-ng and piper (local), Google TTS, Google Cloud TTS, xAI, + OpenAI and ElevenLabs (cloud) +- Fallback chains: providers are tried in order until one succeeds, for both LLM and TTS +- Claude Code hooks for Notification and Stop events +- Optional macOS menu bar app: mute toggle, notification history, talking orb -## ๐Ÿš€ Quick Start +## Quick Start -> **In a hurry?** [QUICKSTART.md](QUICKSTART.md) is the 5-minute version. - -### Installation +### Install ```bash # Homebrew (macOS and Linux) brew tap musingfox/sumvox brew install sumvox -# Cargo -cargo install sumvox +# Cargo (from source; sumvox is not published on crates.io) +cargo install --git https://github.com/musingfox/sumvox ``` -Or grab a binary from [GitHub Releases](https://github.com/musingfox/sumvox/releases/latest) โ€” -`sumvox-macos-aarch64`, `sumvox-macos-x86_64`, `sumvox-linux-x86_64`, `sumvox-linux-aarch64`: - -```bash -curl -LO https://github.com/musingfox/sumvox/releases/latest/download/sumvox-macos-aarch64.tar.gz -tar xzf sumvox-macos-aarch64.tar.gz -sudo mv sumvox /usr/local/bin/ -``` +Prebuilt binaries for macOS and Linux (aarch64 and x86_64) are on +[GitHub Releases](https://github.com/musingfox/sumvox/releases/latest). ### Step 1: Initialize configuration @@ -53,33 +41,24 @@ sudo mv sumvox /usr/local/bin/ sumvox init ``` -This writes `~/.config/sumvox/config.toml` with: - -- **LLM**: Google Gemini โ†’ Anthropic โ†’ OpenAI โ†’ Ollama (local last) -- **TTS**: macOS `say` โ†’ Google TTS. On Linux, replace `macos` with `espeak` or `piper` โ€” - see [Local TTS on Linux](#local-tts-on-linux) - -An existing `config.yaml` or `config.json` from an older release is migrated to TOML on first run -(the original is kept as a timestamped backup). +This writes `~/.config/sumvox/config.toml` with the built-in defaults: an LLM chain of Google +Gemini, Anthropic, OpenAI and Ollama, and a TTS chain of the platform's local engine (`macos` on +macOS, `espeak` elsewhere) followed by Google TTS. ### Step 2: Set an API key -```bash -$EDITOR ~/.config/sumvox/config.toml -``` - -Replace the `${PROVIDER_API_KEY}` placeholder with a real key. The placeholder is not expanded โ€” -a provider whose key still looks like `${...}` is treated as unconfigured and skipped: +Edit the config and replace the `${PROVIDER_API_KEY}` placeholder with a real key. A key that +still starts with `${` counts as unset and that provider is skipped. ```toml [[llm.providers]] name = "google" model = "gemini-3.1-flash-lite" -api_key = "AIza..." # Get one from https://ai.google.dev +api_key = "AIza..." # https://ai.google.dev ``` -Alternatively, leave the placeholder and export the key as an environment variable โ€” see -[Environment Variables](#environment-variables). +Instead of writing the key into the file, you can export the provider's usual variable +(`GEMINI_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `XAI_API_KEY`, `ELEVENLABS_API_KEY`). ### Step 3: Test playback @@ -87,8 +66,6 @@ Alternatively, leave the placeholder and export the key as an environment variab sumvox say "Hello, this is a test" ``` -If you hear nothing, see [Troubleshooting](#troubleshooting). - ### Step 4: Register the Claude Code hook Add to `~/.claude/settings.json` (find your path with `which sumvox`): @@ -108,440 +85,59 @@ Add to `~/.claude/settings.json` (find your path with `which sumvox`): } ``` -Start a Claude Code session and you should hear instant alerts on **Notification** events -("Permission required") and an LLM-written summary on **Stop** events. - -## ๐Ÿ“– Configuration - -Config lives at `~/.config/sumvox/config.toml`. -[config/recommended.toml](config/recommended.toml) is a fully commented reference with every -provider block, including the ones that ship commented out. - -### What `sumvox init` writes - -Abridged below โ€” the generated file also carries the default `prompt_template`, and puts an -`api_key = "${PROVIDER_API_KEY}"` placeholder on *every* provider, including the ones that need no -key (`ollama`, `macos`). Leave those alone or delete them; a placeholder key is treated as unset. - -```toml -[[llm.providers]] -name = "google" -model = "gemini-3.1-flash-lite" -api_key = "${PROVIDER_API_KEY}" -timeout = 10 - -[[llm.providers]] -name = "anthropic" -model = "claude-haiku-4-5-20251001" -api_key = "${PROVIDER_API_KEY}" -timeout = 10 - -[[llm.providers]] -name = "openai" -model = "gpt-5-nano" -api_key = "${PROVIDER_API_KEY}" -timeout = 10 - -[[llm.providers]] -name = "ollama" -model = "llama3.2" -timeout = 60 - -[llm.parameters] -max_tokens = 10000 -temperature = 0.3 -disable_thinking = false - -[[tts.providers]] -name = "macos" -rate = 200 - -[[tts.providers]] -name = "google" -model = "gemini-2.5-flash-preview-tts" -voice = "Aoede" -api_key = "${PROVIDER_API_KEY}" - -[summarization] -content_source = "transcript" -turns = 1 -system_message = "You are a voice notification assistant. Generate concise summaries suitable for voice playback." -fallback_message = "Task completed" - -[hooks.claude_code] -notification_filter = ["permission_prompt", "idle_prompt", "elicitation_dialog"] -notification_tts_provider = "macos" -stop_tts_provider = "auto" -``` - -### Fallback chains - -`[[llm.providers]]` and `[[tts.providers]]` are ordered lists. Each entry is tried in turn until -one succeeds; an entry that is missing its API key, binary or voice model is skipped rather than -failing the chain. If every LLM fails, `summarization.fallback_message` is spoken. If every TTS -fails, SumVox stays silent instead of crashing. - -### Per-hook TTS selection - -```toml -[hooks.claude_code] -notification_tts_provider = "macos" # short alerts: fastest engine -stop_tts_provider = "auto" # summaries: whole fallback chain -notification_volume = 80 # 0-100 -stop_volume = 100 -``` - -A provider selector is either `auto` (walk the whole chain) or the name of one configured entry: -`macos`, `espeak`, `piper`, `google`, `cloud_tts`, `gemini_tts`, `xai`, `openai`, `elevenlabs`, -`audio_file`. - -### Common setups - -**Free and offline** โ€” no API keys, no network: +Notification events are spoken as short alerts. Stop events are summarized by the LLM first. -```toml -[[llm.providers]] -name = "ollama" -model = "llama3.2" -timeout = 60 +## Reference -[[tts.providers]] -name = "macos" # Linux: "espeak", or "piper" with a downloaded .onnx voice -``` +- `sumvox --help` and `sumvox --help` list every command and flag. +- [config/recommended.toml](config/recommended.toml) documents every config key and every + provider, including the ones that ship commented out. Local TTS on Linux (espeak-ng, piper) and + the Linux audio players are covered there. +- Set `SUMVOX_DISABLE` to any value to make SumVox do nothing, for example + `SUMVOX_DISABLE=1 claude`. +- Set `RUST_LOG=debug` for logs. -Slower summaries (30โ€“60 s), but zero cost and nothing leaves the machine. +## Troubleshooting -**Cloud LLM with a local fallback** (recommended) โ€” fast summaries, free speech: +**"No API key found"**: an `api_key` still reading `${PROVIDER_API_KEY}` counts as unset. Paste +the real key or export the matching environment variable. -```toml -[[llm.providers]] -name = "google" -model = "gemini-3.1-flash-lite" -api_key = "${PROVIDER_API_KEY}" +**"Provider not available"**: check the key, the network and the order of the fallback chain. -[[llm.providers]] -name = "ollama" -model = "llama3.2" -timeout = 60 +**No audio**: run `sumvox say "test"`. On macOS check System Settings, Sound, Output. On Linux +install one of `paplay`, `pw-play`, `ffplay`, `mpv` or `aplay`. SumVox lists the players it looked +for when it finds none. -[[tts.providers]] -name = "macos" +**Ollama not responding**: run `ollama serve`, then `ollama pull llama3.2`. -[[tts.providers]] -name = "google" -model = "gemini-2.5-flash-preview-tts" -voice = "Aoede" -api_key = "${PROVIDER_API_KEY}" -``` +## Menu Bar App (macOS) -**Highest quality** โ€” expressive cloud voices for everything: - -```toml -[[llm.providers]] -name = "anthropic" -model = "claude-haiku-4-5-20251001" -api_key = "${PROVIDER_API_KEY}" - -[[tts.providers]] -name = "elevenlabs" -model = "eleven_multilingual_v2" -voice = "21m00Tcm4TlvDq8ikWAM" -api_key = "${PROVIDER_API_KEY}" -``` - -### Customization - -```toml -# Voice and language (macOS: run `say -v ?` to list voices) -[[tts.providers]] -name = "macos" -voice = "Meijia" # Traditional Chinese; Tingting = Simplified; omit for the system default -rate = 200 - -[summarization] -# "transcript" reads the JSONL transcript; "last_message" uses Claude Code's -# last_assistant_message field directly, skipping the file read -content_source = "transcript" -turns = 1 -system_message = "Summarize in a friendly, casual tone." -prompt_template = "Based on the following context, generate a concise summary.\n\nContext:\n{context}\n\nSummary:" -fallback_message = "Task completed" - -[hooks.claude_code] -# Which notifications get spoken. ["*"] speaks all of them; [] disables them. -# Entries are matched literally against Claude Code's own `notification_type` -# field, so the valid values are whatever it sends. -notification_filter = ["permission_prompt", "idle_prompt", "elicitation_dialog"] -``` - -## ๐ŸŽฏ CLI Commands - -```bash -sumvox init # create the config file -sumvox init --force # overwrite an existing one - -# Direct TTS, no LLM -sumvox say "Hello world" -sumvox say "Hello" --tts macos --voice Daniel -sumvox say "Hello" --tts espeak --voice cmn+f3 -sumvox say "Hello" --tts piper --voice ~/voices/zh_CN-huayan-medium.onnx -sumvox say "Hello" --tts google --voice Aoede -sumvox say "Hello" --volume 80 - -# Speech rate: local engines only, ignored by the cloud providers. -# macos - words per minute, 90-300 -# espeak - words per minute, 80-450 (espeak-ng default 175) -# piper - inverse length-scale; 200 is piper's own default, higher is faster -sumvox say "Hello" --rate 250 - -# LLM summarization, then speech -sumvox sum "Long text to summarize..." -echo "Long text..." | sumvox sum - -sumvox sum "Text" --provider anthropic -sumvox sum "Text" --no-speak # print the summary, don't speak it - -# Hook mode: JSON on stdin, format auto-detected -echo '{"hook_event_name":"Notification","message":"Test"}' | sumvox - -# Logs: trace, debug, info, warn, error -RUST_LOG=debug sumvox say "test" -``` - -## ๐Ÿ”ง Provider Reference - -### LLM providers - -| Provider | Example model | API key | Speed | Cost | -|----------|---------------|---------|-------|------| -| **Google Gemini** | `gemini-3.1-flash-lite` | โœ… | Fast | Low | -| **Anthropic** | `claude-haiku-4-5-20251001` | โœ… | Fast | Medium | -| **OpenAI** | `gpt-5-nano` | โœ… | Medium | Medium | -| **xAI Grok** | `grok-build-0.1` | โœ… | Fast | Low | -| **Ollama** | `llama3.2` | โŒ | Slow | Free | - -Keys: [Gemini](https://ai.google.dev) ยท [Anthropic](https://console.anthropic.com) ยท -[OpenAI](https://platform.openai.com) ยท [xAI](https://console.x.ai). -xAI uses the OpenAI-compatible endpoint at `https://api.x.ai/v1`; the provider name is `xai` -(alias `grok`). Any provider accepts an optional `base_url` for proxies and compatible APIs. - -### TTS providers - -Every engine supports the 0โ€“100 `volume` knob. The single exception is a Linux host where `aplay` -is the only audio player installed โ€” it cannot change volume, so SumVox skips it when -`volume = 0`. - -| Provider | Voices | API key | Quality | Cost | -|----------|--------|---------|---------|------| -| **macOS say** | System voices | โŒ | Good | Free | -| **espeak-ng** | 100+ languages/variants | โŒ | Robotic | Free | -| **piper** | One model per voice | โŒ | Good (neural) | Free | -| **Google TTS** | 6 Gemini voices | โœ… | Excellent | ~$0.016/1K chars | -| **Google Cloud TTS** | 100+ (Standard/WaveNet/Chirp3-HD) | โœ… Service account | Professional | $4โ€“16/1M chars | -| **xAI TTS** | 5 voices | โœ… | Excellent | $4.20/1M chars | -| **OpenAI TTS** | 10 voices | โœ… | Excellent | ~$0.015/min | -| **ElevenLabs** | Library + Voice Design | โœ… | Premium | $0.06โ€“0.12/1K chars | - -- **macOS say** โ€” `say -v ?` lists voices. Omit `voice` for the system default. English: `Alex`, - `Samantha`, `Daniel`. Chinese: `Meijia` (็น้ซ”), `Tingting` (็ฎ€ไฝ“). -- **Google TTS (Gemini)** โ€” `Aoede`, `Charon`, `Fenrir`, `Kore`, `Puck`, `Orus`. Both `model` - (e.g. `gemini-2.5-flash-preview-tts`) and `voice` are required; there is no default for either. -- **Google Cloud TTS** โ€” `cloud_tts`; needs `service_account_key` pointing at a JSON key file - ([setup](https://cloud.google.com/text-to-speech/docs/before-you-begin)). Set a Gemini-TTS - `model` (e.g. `gemini-2.5-flash-tts`) to use bare voice names plus a `style_prompt`; the - `gemini_tts` alias binds to its own config entry. -- **xAI TTS** โ€” `eve` (default), `ara`, `rex`, `sal`, `leo`. Language is auto-detected or pinned - with `language_code`. -- **OpenAI TTS** โ€” `alloy`, `ash`, `ballad`, `coral`, `echo`, `fable`, `nova`, `onyx`, `sage`, - `shimmer`, plus `speed` (0.25โ€“4.0) and a `style_prompt` for tone and accent. -- **ElevenLabs** โ€” `voice` is a Voice ID (default `21m00Tcm4TlvDq8ikWAM`, Rachel) from the - [voice library](https://elevenlabs.io/app/voice-library). Models: `eleven_flash_v2_5`, - `eleven_turbo_v2_5`, `eleven_multilingual_v2`, `eleven_v3`. Tuning: `speed` (0.7โ€“1.2), - `stability` and `style` (0.0โ€“1.0). - -### Local TTS on Linux - -macOS `say` is unavailable off macOS; `espeak` and `piper` are its offline replacements. Both need -their binary on `PATH`, and both are skipped by the fallback chain when it is missing. - -- **espeak-ng** โ€” instant and tiny, but robotic. Install with `pacman -S espeak-ng` or - `apt install espeak-ng`. `voice` takes an espeak-ng voice name; append a variant with `+` - (`cmn` Mandarin, `cmn+f3` female, `en-us`, `yue` Cantonese). Run `espeak-ng --voices` to list - them. Traditional and Simplified Chinese produce identical phonemes, so no conversion is needed. -- **piper** โ€” neural, noticeably better Chinese prosody, still fast enough for notifications. - Install with `uv tool install piper-tts`. Voice models are downloaded by hand from - [rhasspy/piper-voices](https://huggingface.co/rhasspy/piper-voices); each voice is a `.onnx` file - that must sit next to its `.onnx.json` sidecar. Point `voice`, `model` or `path` at the `.onnx` - (first non-blank wins, `~` is expanded) โ€” piper is treated as unavailable until both the binary - and the model exist. The model is reloaded on every invocation (~640 ms), so espeak suits - per-notification alerts and piper suits summaries. - -Playback needs a command-line player: SumVox probes `paplay` โ†’ `pw-play` โ†’ `ffplay` โ†’ `mpv` โ†’ -`aplay` and uses the first one installed. `paplay` ships with both `pulseaudio-utils` and -`pipewire-pulse`, so most desktops already have it. - -```toml -[[tts.providers]] -name = "espeak" -voice = "cmn+f3" # optional; omit for the espeak-ng default voice -rate = 175 # words per minute (80-450) -volume = 80 - -[[tts.providers]] -name = "piper" -voice = "~/.local/share/piper/zh_CN-huayan-medium.onnx" -rate = 200 # inverse length-scale; higher = faster -volume = 80 -``` - -## โš™๏ธ Reference - -### Configuration file structure - -```toml -[[llm.providers]] # ordered fallback chain -# name, model, api_key, base_url, timeout, disable_thinking - -[llm.parameters] # shared by every LLM provider -# max_tokens, temperature, disable_thinking - -[[tts.providers]] # ordered fallback chain -# name, model, voice, api_key, rate, volume, path, service_account_key, -# language_code, speed, stability, style, style_prompt - -[summarization] -# content_source ("transcript" | "last_message"), turns, system_message, -# prompt_template ({context} placeholder), fallback_message - -[hooks.claude_code] -# notification_filter, notification_tts_provider, stop_tts_provider, -# notification_volume, stop_volume, queue_timeout -``` - -### Environment Variables - -API keys belong in the config file; the variables below are a fallback for when the config value -is missing or still holds the `${...}` placeholder. - -| Variable | Description | -|----------|-------------| -| `SUMVOX_DISABLE` | Any value skips all SumVox processing | -| `GEMINI_API_KEY` | Google Gemini LLM and Google TTS | -| `GOOGLE_API_KEY` | Google TTS only (fallback after `GEMINI_API_KEY`) | -| `ANTHROPIC_API_KEY` | Anthropic LLM | -| `OPENAI_API_KEY` | OpenAI LLM and OpenAI TTS | -| `XAI_API_KEY` | xAI Grok LLM and xAI TTS | -| `ELEVENLABS_API_KEY` | ElevenLabs TTS | -| `RUST_LOG` | Log level: `trace`, `debug`, `info`, `warn`, `error` | - -For a quiet Claude Code session: - -```bash -SUMVOX_DISABLE=1 claude # Bash / Zsh -env SUMVOX_DISABLE=1 claude # Fish - -alias claude-quiet='SUMVOX_DISABLE=1 claude' # Bash / Zsh -alias claude-quiet 'env SUMVOX_DISABLE=1 claude' # Fish -``` - -### Troubleshooting - -**"No API key found"** โ€” check `~/.config/sumvox/config.toml`: an `api_key` still reading -`${PROVIDER_API_KEY}` counts as unset. Either paste the real key or export the matching -environment variable. - -**"Provider not available"** โ€” verify the key, the network, and the order of the fallback chain. - -**No audio** โ€” test with `sumvox say "test"`. -On macOS check System Settings โ†’ Sound โ†’ Output. -On Linux install a player (`paplay`, `pw-play`, `ffplay`, `mpv` or `aplay`); SumVox lists the ones -it looked for when none is found. `aplay` has no volume control and is skipped when `volume = 0`. -Over SSH `XDG_RUNTIME_DIR` is often unset โ€” SumVox defaults it to `/run/user/{uid}` for the -player, so export the real value if yours differs. - -**Ollama not responding** โ€” `ollama serve`, then `ollama pull llama3.2`. - -## ๐Ÿ—๏ธ How It Works - -``` -Claude Code session - โ”‚ hook event (JSON on stdin) - โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ sumvox โ”‚ -โ”‚ 1. parse the event โ”‚ -โ”‚ 2. read the transcript ~/.claude/projects โ”‚ -โ”‚ 3. summarize LLM chain โ”‚ -โ”‚ 4. speak TTS chain โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ audio - โ–ผ - ๐Ÿ”Š system audio -``` - -Both chains behave the same way: try each provider in order, first success wins. An LLM chain that -runs out of providers speaks `summarization.fallback_message`; a TTS chain that runs out stays -silent. SumVox never fails the hook. - -## ๐Ÿ–ฅ๏ธ Menu Bar App (macOS) - -An optional companion app lives in [`menubar/SumVoxMenu.swift`](menubar/SumVoxMenu.swift) โ€” one -Swift file, system frameworks only: +An optional companion app lives in [`menubar/SumVoxMenu.swift`](menubar/SumVoxMenu.swift), one +Swift file using system frameworks only: ```bash just menubar # builds target/release/sumvox-menubar target/release/sumvox-menubar & ``` -It launches with no Dock icon and puts up two things: a ๐Ÿ”Š/๐Ÿ”‡ status item, and a small floating -**orb** that stays on screen (borderless, always-on-top, draggable to wherever you want it). -Clicking either one opens the same menu: - -- **ๆ’ญๆ”พ่ชž้Ÿณ** โ€” toggles `~/.config/sumvox/muted`. While that flag exists, SumVox skips playback on - *every* path โ€” the hooks, `sumvox say` and `sumvox sum` โ€” but still records the text. -- **ๆœ€่ฟ‘้€š็Ÿฅ** โ€” the last 50 spoken texts from `~/.config/sumvox/history.log`; click one to copy it. -- **้–‹ๅ•Ÿ่จญๅฎšๆช”** โ€” opens `~/.config/sumvox/config.toml`. -- **็ตๆŸ** โ€” quits. - -When a new notification is recorded, a speech bubble appears next to the orb โ€” on whichever side -has room, clamped to the visible screen โ€” types the text out, and fades away after 4โ€“12 s (scaled -to the text length). Because the text is recorded before -the mute check, the bubble still appears while muted โ€” you see the notification, you just don't -hear it. The orb itself deforms in time with the audio: it reads the path in -`~/.config/sumvox/now_playing`, decodes an RMS envelope off the main thread, and animates from -that; with no real audio file (macOS `say`, for example) the typewriter synthesizes a level -instead. It renders at 20 fps idle and 60 fps while speaking. - -State is never polled: the three files are watched with `DispatchSource`, and the menu is rebuilt -only when it opens. Those files โ€” `muted`, `history.log`, `now_playing` โ€” are the entire contract -between the two binaries (`src/notify_log.rs`), so not running the app just leaves them unread. - -## ๐Ÿ› ๏ธ Development - -```bash -cargo build --release -cargo test -cargo fmt && cargo clippy -- -D warnings -RUST_LOG=debug cargo run -``` - -See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development and release guide. - -## ๐Ÿค Contributing +It shows a status item and a floating orb. Either one opens a menu with: -Contributions welcome. Areas that need help: +- **ๆ’ญๆ”พ่ชž้Ÿณ**: toggles `~/.config/sumvox/muted`. While that file exists SumVox skips playback + but still records the text. +- **ๆœ€่ฟ‘้€š็Ÿฅ**: the last 50 notification texts from `~/.config/sumvox/history.log`; click one to + copy it. +- **้–‹ๅ•Ÿ่จญๅฎšๆช”**: opens `~/.config/sumvox/config.toml`. +- **็ตๆŸ**: quits. -- Test coverage for the non-Gemini providers -- Additional TTS engines -- Windows support -- Documentation +When a notification is recorded, a speech bubble shows its text next to the orb, even while muted. +The app watches `history.log` and `now_playing` for changes and reads `muted` when the menu opens. +These three files are the whole contract between the two programs (`src/notify_log.rs`), so not +running the app leaves them unread. -## ๐Ÿ“„ License +## Contributing -MIT โ€” see [LICENSE](LICENSE). +See [CONTRIBUTING.md](CONTRIBUTING.md). -## ๐Ÿ”— Links +## License -- **GitHub**: https://github.com/musingfox/sumvox -- **Issues**: https://github.com/musingfox/sumvox/issues -- **crates.io**: https://crates.io/crates/sumvox -- **Changelog**: [CHANGELOG.md](CHANGELOG.md) +MIT, see [LICENSE](LICENSE). From e40a8f33101a4f387863dc375aa29828e793125c Mon Sep 17 00:00:00 2001 From: musingfox Date: Thu, 1 Oct 2026 18:00:43 +0800 Subject: [PATCH 4/9] docs: delete QUICKSTART, README covers the same setup Co-Authored-By: Claude Opus 5.5 (1M context) --- QUICKSTART.md | 220 -------------------------------------------------- 1 file changed, 220 deletions(-) delete mode 100644 QUICKSTART.md diff --git a/QUICKSTART.md b/QUICKSTART.md deleted file mode 100644 index 37f7f05..0000000 --- a/QUICKSTART.md +++ /dev/null @@ -1,220 +0,0 @@ -# SumVox Quick Start Guide - -## ๐Ÿš€ Installation (1 minute) - -```bash -# Homebrew (macOS and Linux) -brew tap musingfox/sumvox -brew install sumvox - -# Cargo -cargo install sumvox - -# Binary download: https://github.com/musingfox/sumvox/releases -``` - -## โš™๏ธ Setup (3 minutes) - -### 1. Initialize Config - -```bash -sumvox init -``` - -### 2. Set API Key - -Edit `~/.config/sumvox/config.toml` and replace the `${PROVIDER_API_KEY}` placeholder with a real -key โ€” a placeholder counts as unset, so the provider is skipped: - -```toml -[[llm.providers]] -name = "google" -model = "gemini-3.1-flash-lite" -api_key = "AIza..." # Get one from https://ai.google.dev -``` - -Or leave the placeholder and export `GEMINI_API_KEY` instead. - -### 3. Test Voice - -```bash -sumvox say "Hello, SumVox is working!" -``` - -### 4. Configure Claude Code Hook - -Edit `~/.claude/settings.json` (find your path with `which sumvox`): - -```json -{ - "hooks": { - "Notification": [{ - "matcher": "", - "hooks": [{"type": "command", "command": "/opt/homebrew/bin/sumvox"}] - }], - "Stop": [{ - "matcher": "", - "hooks": [{"type": "command", "command": "/opt/homebrew/bin/sumvox"}] - }] - } -} -``` - -## ๐ŸŽฏ Common Configurations - -### Default (Recommended) - -```toml -[[llm.providers]] -name = "google" -model = "gemini-3.1-flash-lite" -api_key = "${PROVIDER_API_KEY}" - -[[llm.providers]] -name = "ollama" -model = "llama3.2" -timeout = 60 - -[[tts.providers]] -name = "macos" - -[[tts.providers]] -name = "google" -model = "gemini-2.5-flash-preview-tts" # required -voice = "Aoede" # required -api_key = "${PROVIDER_API_KEY}" -``` - -**Pros:** Fast cloud LLM, free local TTS, reliable fallback - -### Free & Offline - -```toml -[[llm.providers]] -name = "ollama" -model = "llama3.2" -timeout = 60 - -[[tts.providers]] -name = "macos" # Linux: "espeak", or "piper" with a downloaded .onnx voice -``` - -**Pros:** Zero cost, works offline -**Cons:** Slower (30-60s for summaries) - -On Linux, `macos` is unavailable โ€” install `espeak-ng` (`pacman -S espeak-ng` / `apt install -espeak-ng`) or piper (`uv tool install piper-tts` plus a voice model from -[rhasspy/piper-voices](https://huggingface.co/rhasspy/piper-voices)) and name it instead. See -[README](README.md#local-tts-on-linux) for the details. - -### High Quality - -```toml -[[llm.providers]] -name = "anthropic" -model = "claude-haiku-4-5-20251001" -api_key = "${PROVIDER_API_KEY}" - -[[tts.providers]] -name = "google" -model = "gemini-2.5-flash-preview-tts" -voice = "Aoede" -api_key = "${PROVIDER_API_KEY}" -``` - -## ๐ŸŽจ Customization Cheat Sheet - -### Change Voice Language - -```toml -[[tts.providers]] -name = "macos" -voice = "Meijia" # Chinese; "Daniel" for English; omit for the system default -``` - -List voices: `say -v ?` - -### Change Summary Style - -```toml -[summarization] -# "last_message" skips the transcript file read and uses Claude Code's -# last_assistant_message directly (LLM summarization still runs) -content_source = "transcript" # or "last_message" -system_message = "Be concise and technical" -``` - -### Filter Notifications - -```toml -[hooks.claude_code] -notification_filter = ["*"] # all; or ["permission_prompt", "idle_prompt"] -``` - -### TTS Provider per Hook - -```toml -[hooks.claude_code] -notification_tts_provider = "macos" # fast, local, free -stop_tts_provider = "auto" # best quality: try the whole fallback chain -notification_volume = 80 # 0-100 -stop_volume = 100 -``` - -**Note:** Volume works with every provider. The one exception is a Linux host where `aplay` is the -only player installed โ€” it has no volume control, so SumVox skips it when `volume = 0`. - -## ๐Ÿ”‡ Temporarily Disable SumVox - -```bash -SUMVOX_DISABLE=1 claude # Bash / Zsh -env SUMVOX_DISABLE=1 claude # Fish -``` - -**Tip:** Create an alias for quick access: - -```bash -alias claude-quiet='SUMVOX_DISABLE=1 claude' # Bash / Zsh -alias claude-quiet 'env SUMVOX_DISABLE=1 claude' # Fish -``` - -## ๐Ÿ”ง Troubleshooting - -### "No API key found" - -```bash -cat ~/.config/sumvox/config.toml -# An api_key still reading ${PROVIDER_API_KEY} counts as unset -``` - -### "No audio" - -```bash -sumvox say "test" -# macOS: System Settings โ†’ Sound โ†’ Output -# Linux: install paplay / pw-play / ffplay / mpv / aplay -``` - -### "Ollama not responding" - -```bash -ollama serve -ollama pull llama3.2 -``` - -### Debug mode - -```bash -RUST_LOG=debug sumvox say "test" -``` - -## ๐Ÿ“– Full Documentation - -- [README.md](README.md) - Complete guide -- [config/recommended.toml](config/recommended.toml) - Annotated config example -- [CHANGELOG.md](CHANGELOG.md) - Version history - -## ๐Ÿ’ฌ Support - -- Issues: https://github.com/musingfox/sumvox/issues -- Discussions: https://github.com/musingfox/sumvox/discussions From 3f28460f3b9b8324f5c3ec87666334efd51ed375 Mon Sep 17 00:00:00 2001 From: musingfox Date: Thu, 1 Oct 2026 18:01:00 +0800 Subject: [PATCH 5/9] docs: rewrite CONTRIBUTING around checks, tests, new providers and release Co-Authored-By: Claude Opus 5.5 (1M context) --- CONTRIBUTING.md | 501 +++++------------------------------------------- 1 file changed, 46 insertions(+), 455 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b6c83dd..42dccb3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,481 +1,72 @@ # Contributing to SumVox -Thank you for your interest in contributing to SumVox! This guide will help you get started. +## Setup -## Code of Conduct - -Be respectful, constructive, and collaborative. We're all here to build better tools. - -## Development Setup - -### Prerequisites - -- A stable Rust toolchain (install via [rustup](https://rustup.rs/)) โ€” CI builds on `stable` -- macOS or Linux. On Linux, install `espeak-ng` and an audio player (`ffplay`, `paplay`, ...) - so the local TTS path is exercisable -- API keys for testing (Gemini recommended) - -### Getting Started - -1. **Fork and clone** the repository: - ```bash - gh repo fork musingfox/sumvox --clone - cd sumvox - ``` - -2. **Build the project**: - ```bash - cargo build - ``` - -3. **Run tests**: - ```bash - cargo test - ``` - -4. **Set up development config**: - ```bash - cargo run -- init - # then edit ~/.config/sumvox/config.toml and set an API key - ``` - -### Project Structure - -``` -sumvox/ -โ”œโ”€โ”€ src/ -โ”‚ โ”œโ”€โ”€ main.rs # Entry point and hook orchestration -โ”‚ โ”œโ”€โ”€ cli.rs # CLI argument parsing -โ”‚ โ”œโ”€โ”€ config.rs # Configuration loading, migration and saving -โ”‚ โ”œโ”€โ”€ pipeline.rs # Shared summarize -> speak flow (say / sum / json / hooks) -โ”‚ โ”œโ”€โ”€ transcript.rs # Claude Code transcript parsing -โ”‚ โ”œโ”€โ”€ queue.rs # Serializes concurrent hook invocations -โ”‚ โ”œโ”€โ”€ notify_log.rs # History log + mute flag for the menu bar app -โ”‚ โ”œโ”€โ”€ error.rs # Error types -โ”‚ โ”œโ”€โ”€ hooks/ # Hook handlers (claude_code.rs) -โ”‚ โ”œโ”€โ”€ llm/ # LLM providers (gemini, anthropic, openai, ollama, cost_tracker) -โ”‚ โ”œโ”€โ”€ tts/ # TTS engines (macos, espeak, piper, google, cloud_tts, -โ”‚ โ”‚ # openai, xai, elevenlabs) -โ”‚ โ”œโ”€โ”€ audio/ # Playback: player.rs (choke point), file.rs, normalize.rs, -โ”‚ โ”‚ # wav_header.rs -โ”‚ โ””โ”€โ”€ provider_factory.rs # LLM provider creation -โ”œโ”€โ”€ config/ -โ”‚ โ”œโ”€โ”€ recommended.toml # Annotated reference configuration -โ”‚ โ””โ”€โ”€ e2e_test.toml.example # Template for the e2e suite's config -โ”œโ”€โ”€ menubar/ # macOS menu bar companion app (Swift) -โ”œโ”€โ”€ tests/ # Integration tests (e2e.rs, recommended_config.rs) -โ””โ”€โ”€ Cargo.toml # Dependencies and metadata -``` - -## Testing - -### Running Tests - -```bash -# All tests -cargo test - -# Specific module -cargo test llm:: -cargo test tts:: - -# With output -cargo test -- --nocapture -``` - -The `e2e` suite drives the built binary end to end and needs a real config: +You need a stable Rust toolchain (edition 2021) and [`just`](https://github.com/casey/just) +(`cargo install just`). `just` lists the available recipes. ```bash -cp config/e2e_test.toml.example config/e2e_test.toml # then fill in API keys -cargo test --test e2e -``` - -It runs on macOS and Linux from the same config โ€” the harness rewrites the `macos` engine to -`espeak` on non-macOS hosts, so `espeak-ng` must be installed there. `SUMVOX_DISABLE` must not be -set in the shell, or the binary exits before doing anything. - -### Writing Tests - -- Unit tests: Add `#[cfg(test)]` modules in source files -- Integration tests: Add files to `tests/` directory -- Mock external APIs using `mockito` crate -- Test both success and error paths - -Example: -```rust -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_provider_fallback() { - // Test implementation - } -} +just check # cargo fmt --check, cargo clippy -- -D warnings, cargo test ``` -## Code Style +CI runs the same three checks. Commit subjects follow Conventional Commits +(`feat`, `fix`, `docs`, `refactor`, `test`, `chore`, ...), and user-visible changes get a line +under `[Unreleased]` in [CHANGELOG.md](CHANGELOG.md). -### Formatting +## Tests -- Use `rustfmt` for consistent formatting: - ```bash - cargo fmt - ``` - -### Linting +- `cargo test` is hermetic: no network, no audio, no API keys. Tests that need real audio are + `#[ignore]`d. +- `just test-e2e` (`cargo test --test e2e -- --ignored`) drives the built binary against real + APIs and plays audio. It reads `config/e2e_test.toml`: -- Run `clippy` before committing: ```bash - cargo clippy -- -D warnings + cp config/e2e_test.toml.example config/e2e_test.toml # then fill in API keys ``` -### Conventions - -- Use descriptive variable names -- Add comments for complex logic only -- Write self-documenting code -- Follow Rust naming conventions: - - `snake_case` for functions and variables - - `PascalCase` for types and structs - - `SCREAMING_SNAKE_CASE` for constants - -## Commit Messages - -We follow [Conventional Commits](https://www.conventionalcommits.org/): - -``` -(): - - - -