Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
1a6fbef
feat: add voice mode with push-to-talk and TTS output for CLI
0xbyt4 Mar 3, 2026
ec32e9a
feat: add Groq STT support and fix voice mode keybinding
0xbyt4 Mar 3, 2026
ea5b898
fix: voice mode TTS playback and keybinding issues
0xbyt4 Mar 3, 2026
37b01ab
test: add transcription_tools tests for multi-provider STT
0xbyt4 Mar 3, 2026
c23928d
fix: improve voice mode robustness and add integration tests
0xbyt4 Mar 3, 2026
a69bd55
fix: isolate GROQ_API_KEY in test_missing_stt_key test
0xbyt4 Mar 3, 2026
bfd9c97
feat: add Phase 4 low-latency features for voice mode
0xbyt4 Mar 3, 2026
32b033c
feat: add silence filter, hallucination guard, and continuous mode co…
0xbyt4 Mar 3, 2026
dad865e
fix: fix silence detection bugs and add Phase 4 voice mode features
0xbyt4 Mar 3, 2026
d742534
fix: fix voice recording stuck in continuous mode
0xbyt4 Mar 3, 2026
179d9e1
feat: add streaming sentence-by-sentence TTS via ElevenLabs
0xbyt4 Mar 3, 2026
fd4f229
fix: catch OSError on sounddevice import for CI without PortAudio
0xbyt4 Mar 5, 2026
a15fa85
fix: catch OSError on sounddevice import in voice_mode.py
0xbyt4 Mar 5, 2026
7d4b4e9
feat: sync text display with TTS audio playback
0xbyt4 Mar 5, 2026
3a1b35e
fix: voice mode race conditions, temp file leak, think tag parsing
0xbyt4 Mar 5, 2026
b00c594
fix: suppress verbose logs during streaming TTS, improve hallucinatio…
0xbyt4 Mar 5, 2026
404123a
feat: add persistent voice mode status bar below input area
0xbyt4 Mar 5, 2026
46db7ae
fix: streaming tool call parsing, error handling, and fake HA state m…
0xbyt4 Mar 6, 2026
143cc68
fix(test): add /voice to EXPECTED_COMMANDS set in test_commands.py
0xbyt4 Mar 8, 2026
b859dfa
fix: address voice mode review feedback
0xbyt4 Mar 9, 2026
a8838a7
fix: replace all hardcoded Ctrl+R references with Ctrl+B
0xbyt4 Mar 9, 2026
fc893f9
fix: wrap sd.InputStream in try-except and fix config key name
0xbyt4 Mar 9, 2026
a782492
fix: address voice mode PR review (streaming TTS, prompt cache, _vprint)
0xbyt4 Mar 10, 2026
ddfd6e0
fix: resolve 6 voice mode bugs found during audit
0xbyt4 Mar 10, 2026
6e51729
fix: remove browser_tool signal handlers that cause voice mode deadlock
0xbyt4 Mar 10, 2026
ecc3dd7
test: add comprehensive voice mode test coverage (86 tests)
0xbyt4 Mar 10, 2026
d0e3b39
fix: prevent Ctrl+B key handler from blocking prompt_toolkit event loop
0xbyt4 Mar 10, 2026
9d58caf
fix: move process_loop voice restart to daemon thread, use _cprint co…
0xbyt4 Mar 10, 2026
bcf4513
fix: add timeout to play_beep sd.wait and wrap silence callback in tr…
0xbyt4 Mar 10, 2026
0a89933
fix: add STT timeout, move finally restart to thread, guard exit on r…
0xbyt4 Mar 10, 2026
c3dc444
fix: disable STT retries and stop continuous mode after 3 silent cycles
0xbyt4 Mar 10, 2026
8b57a3c
fix: add max recording timeout to prevent infinite wait in quiet envi…
0xbyt4 Mar 10, 2026
eec04d1
fix(test): update play_beep test to match polling-based implementation
0xbyt4 Mar 10, 2026
eb79dda
fix: persistent audio stream and silence detection improvements
0xbyt4 Mar 10, 2026
39a7743
fix: use shutdown() instead of cancel() on CLI exit to release persis…
0xbyt4 Mar 10, 2026
8aab13d
refactor: remove dead _generation counter from AudioRecorder
0xbyt4 Mar 10, 2026
d80da5d
feat: add /voice command for auto voice reply in Telegram gateway
0xbyt4 Mar 10, 2026
f6cf4ca
feat: add /voice slash command to Discord + fix cross-platform send_v…
0xbyt4 Mar 10, 2026
cbe4c23
fix: Discord voice bubble + edge-tts mp3/ogg format mismatch
0xbyt4 Mar 10, 2026
cc97490
feat: Discord voice channel support — bot joins VC and speaks replies
0xbyt4 Mar 10, 2026
c0c358d
feat: add Discord voice channel listening — STT transcription and age…
0xbyt4 Mar 11, 2026
2bb2312
docs: add comprehensive voice mode documentation
0xbyt4 Mar 11, 2026
75bd5a5
docs: improve voice mode docs with prerequisites, startup commands, a…
0xbyt4 Mar 11, 2026
e50323f
fix(test): add missing _voice_mode attr to GatewayRunner test stubs
0xbyt4 Mar 11, 2026
a3905ef
feat: add web gateway — browser-based chat UI over WebSocket
0xbyt4 Mar 11, 2026
4e3b14d
docs: add Web UI config to .env.example
0xbyt4 Mar 11, 2026
ddfbc22
feat: add /remote-control command to start web UI on demand
0xbyt4 Mar 11, 2026
536be3e
fix: show correct LAN IP when VPN is active
0xbyt4 Mar 11, 2026
db51cfa
docs: add Web UI setup guide and update gateway docs
0xbyt4 Mar 11, 2026
d3e09df
feat: add voice conversation support and futuristic UI redesign
0xbyt4 Mar 11, 2026
c477f66
feat: add continuous voice mode with VAD silence detection
0xbyt4 Mar 11, 2026
6205f06
test: add comprehensive tests for web gateway adapter
0xbyt4 Mar 11, 2026
f078cb4
fix(test): isolate WEB_TOKEN env var in test_web_defaults
0xbyt4 Mar 11, 2026
1b10c37
fix: accept **kwargs in send_voice for Discord and Slack adapters
0xbyt4 Mar 11, 2026
e21a134
docs: add Discord DM usage and mention requirement to voice mode guide
0xbyt4 Mar 11, 2026
815e839
fix: prevent double TTS on Web UI voice messages
0xbyt4 Mar 11, 2026
62e75cd
fix: skip duplicate TTS file attachment when bot is in Discord voice …
0xbyt4 Mar 11, 2026
095815d
fix: skip gateway voice reply for all platforms on voice input
0xbyt4 Mar 11, 2026
dcb84a8
test: add double TTS prevention tests for voice reply logic
0xbyt4 Mar 11, 2026
fbf47e9
fix: allow voice reply in Discord VC despite skip_double guard
0xbyt4 Mar 11, 2026
3431f73
fix: show mic button on mobile Web UI with HTTPS warning
0xbyt4 Mar 11, 2026
0d56b79
docs: add firewall and mobile HTTPS troubleshooting for Web UI
0xbyt4 Mar 11, 2026
86ddaae
fix: extract voice reply logic and add comprehensive tests
0xbyt4 Mar 11, 2026
34c324f
fix(test): use real _strip_markdown_for_tts instead of duplicated copy
0xbyt4 Mar 11, 2026
c925d2e
fix: voice pipeline thread safety and error handling bugs
0xbyt4 Mar 11, 2026
9722bd8
fix: 8 voice pipeline bugs with tests proving each fix
0xbyt4 Mar 11, 2026
79ed0ef
docs: fix 3 inaccuracies found during code-vs-docs audit
0xbyt4 Mar 11, 2026
238a431
fix: make STT config env-overridable and fix doc issues
0xbyt4 Mar 11, 2026
3260413
docs: add STT override env vars to .env.example
0xbyt4 Mar 11, 2026
2c84979
refactor: extract get_stt_model_from_config helper to eliminate DRY v…
0xbyt4 Mar 11, 2026
0a8985a
fix: add missing load_config import in _show_voice_status
0xbyt4 Mar 12, 2026
d646442
fix: restore Anthropic interrupt handler in _interruptible_api_call
0xbyt4 Mar 13, 2026
0ff1b4a
fix: harden web gateway security and fix error swallowing
0xbyt4 Mar 13, 2026
c797314
test: add security and hardening tests for voice mode fixes
0xbyt4 Mar 13, 2026
44abe85
fix: add macOS Homebrew Opus fallback and fix shutdown dict iteration
0xbyt4 Mar 13, 2026
a21f518
fix: hide configured token value in Web UI startup log
0xbyt4 Mar 13, 2026
5b47b87
fix: show only reachable URLs in Web UI startup message
0xbyt4 Mar 13, 2026
fa2c825
fix: isolate WEB_UI_HOST env var in test and handle empty string
0xbyt4 Mar 13, 2026
c433c89
fix: demote RTP debug logs to DEBUG and isolate web sessions
0xbyt4 Mar 13, 2026
b8f8d3e
feat: integrate faster-whisper local STT with three-provider fallback
0xbyt4 Mar 13, 2026
eb052b1
fix: add explicit metadata param to Discord send_voice signature
0xbyt4 Mar 13, 2026
69cb373
fix: update /voice status to show correct STT provider
0xbyt4 Mar 13, 2026
41162e0
fix: prevent shutdown deadlock and unblockable Ctrl+C on exit
0xbyt4 Mar 13, 2026
e3126ae
fix: STT consistency — web.py model param, error matching, local prov…
0xbyt4 Mar 13, 2026
49f3f0f
fix: add choices/Choice to discord mock for /voice slash command test
0xbyt4 Mar 13, 2026
1ad5e0e
feat: add voice channel awareness — inject participant and speaking s…
0xbyt4 Mar 13, 2026
35748a2
fix: address PR review round 4 — remove web UI, fix audio/import/inte…
0xbyt4 Mar 14, 2026
cc0a453
fix: address PR review round 5 — streaming guard, VC auth, history pr…
0xbyt4 Mar 14, 2026
7a24168
fix: add missing choices/Choice to discord mock in test_discord_free_…
0xbyt4 Mar 14, 2026
eb34c0b
fix: voice pipeline hardening — 7 bug fixes with tests
0xbyt4 Mar 14, 2026
92c14ec
fix(test): add missing voice state attrs to CLI stub in skin tests
0xbyt4 Mar 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -275,3 +275,27 @@ WANDB_API_KEY=
# GITHUB_APP_ID=
# GITHUB_APP_PRIVATE_KEY_PATH=
# GITHUB_APP_INSTALLATION_ID=

# Groq API key (free tier — used for Whisper STT in voice mode)
# GROQ_API_KEY=

# =============================================================================
# STT PROVIDER SELECTION
# =============================================================================
# Default STT provider is "local" (faster-whisper) — runs on your machine, no API key needed.
# Install with: pip install faster-whisper
# Model downloads automatically on first use (~150 MB for "base").
# To use cloud providers instead, set GROQ_API_KEY or VOICE_TOOLS_OPENAI_KEY above.
# Provider priority: local > groq > openai
# Configure in config.yaml: stt.provider: local | groq | openai

# =============================================================================
# STT ADVANCED OVERRIDES (optional)
# =============================================================================
# Override default STT models per provider (normally set via stt.model in config.yaml)
# STT_GROQ_MODEL=whisper-large-v3-turbo
# STT_OPENAI_MODEL=whisper-1

# Override STT provider endpoints (for proxies or self-hosted instances)
# GROQ_BASE_URL=https://api.groq.com/openai/v1
# STT_OPENAI_BASE_URL=https://api.openai.com/v1
760 changes: 735 additions & 25 deletions cli.py

Large diffs are not rendered by default.

62 changes: 57 additions & 5 deletions gateway/platforms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ def __init__(self, config: PlatformConfig, platform: Platform):
# Key: session_key (e.g., chat_id), Value: (event, asyncio.Event for interrupt)
self._active_sessions: Dict[str, asyncio.Event] = {}
self._pending_messages: Dict[str, MessageEvent] = {}
# Chats where auto-TTS on voice input is disabled (set by /voice off)
self._auto_tts_disabled_chats: set = set()

@property
def name(self) -> str:
Expand Down Expand Up @@ -537,6 +539,20 @@ async def send_voice(
text = f"{caption}\n{text}"
return await self.send(chat_id=chat_id, content=text, reply_to=reply_to)

async def play_tts(
self,
chat_id: str,
audio_path: str,
**kwargs,
) -> SendResult:
"""
Play auto-TTS audio for voice replies.

Override in subclasses for invisible playback (e.g. Web UI).
Default falls back to send_voice (shows audio player).
"""
return await self.send_voice(chat_id=chat_id, audio_path=audio_path, **kwargs)

async def send_video(
self,
chat_id: str,
Expand Down Expand Up @@ -718,7 +734,43 @@ async def _process_message_background(self, event: MessageEvent, session_key: st
if images:
logger.info("[%s] extract_images found %d image(s) in response (%d chars)", self.name, len(images), len(response))

# Send the text portion first (if any remains after extractions)
# Auto-TTS: if voice message, generate audio FIRST (before sending text)
# Skipped when the chat has voice mode disabled (/voice off)
_tts_path = None
if (event.message_type == MessageType.VOICE
and text_content
and not media_files
and event.source.chat_id not in self._auto_tts_disabled_chats):
try:
from tools.tts_tool import text_to_speech_tool, check_tts_requirements
if check_tts_requirements():
import json as _json
speech_text = re.sub(r'[*_`#\[\]()]', '', text_content)[:4000].strip()
if not speech_text:
raise ValueError("Empty text after markdown cleanup")
tts_result_str = await asyncio.to_thread(
text_to_speech_tool, text=speech_text
)
tts_data = _json.loads(tts_result_str)
_tts_path = tts_data.get("file_path")
except Exception as tts_err:
logger.warning("[%s] Auto-TTS failed: %s", self.name, tts_err)

# Play TTS audio before text (voice-first experience)
if _tts_path and Path(_tts_path).exists():
try:
await self.play_tts(
chat_id=event.source.chat_id,
audio_path=_tts_path,
metadata=_thread_metadata,
)
finally:
try:
os.remove(_tts_path)
except OSError:
pass

# Send the text portion
if text_content:
logger.info("[%s] Sending response (%d chars) to %s", self.name, len(text_content), event.source.chat_id)
result = await self.send(
Expand All @@ -727,7 +779,7 @@ async def _process_message_background(self, event: MessageEvent, session_key: st
reply_to=event.message_id,
metadata=_thread_metadata,
)

# Log send failures (don't raise - user already saw tool progress)
if not result.success:
print(f"[{self.name}] Failed to send response: {result.error}")
Expand All @@ -740,10 +792,10 @@ async def _process_message_background(self, event: MessageEvent, session_key: st
)
if not fallback_result.success:
print(f"[{self.name}] Fallback send also failed: {fallback_result.error}")

# Human-like pacing delay between text and media
human_delay = self._get_human_delay()

# Send extracted images as native attachments
if images:
logger.info("[%s] Extracted %d image(s) to send as attachments", self.name, len(images))
Expand Down Expand Up @@ -771,7 +823,7 @@ async def _process_message_background(self, event: MessageEvent, session_key: st
logger.error("[%s] Failed to send image: %s", self.name, img_result.error)
except Exception as img_err:
logger.error("[%s] Error sending image: %s", self.name, img_err, exc_info=True)

# Send extracted media files — route by file type
_AUDIO_EXTS = {'.ogg', '.opus', '.mp3', '.wav', '.m4a'}
_VIDEO_EXTS = {'.mp4', '.mov', '.avi', '.mkv', '.3gp'}
Expand Down
Loading
Loading