From 2b969b2754c59875d39ad043c504e57c3a937026 Mon Sep 17 00:00:00 2001 From: Sami Ahmed Date: Tue, 11 Aug 2026 03:55:31 -0700 Subject: [PATCH] Add voice-bridge.py prototype + systemd service, LibreOffice in package list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - voice-bridge.py: SamiType → Hermes router with dictation mode - Handles command mode (→Hermes) and dictation mode (→xdotool typing) - Dictation commands: punctuation, formatting, scratch/undo, stop - Fallback to hermes CLI if API unavailable - systemd service unit for always-on voice --- packaging/scripts/samios-voice.service | 16 ++ packaging/scripts/voice-bridge.py | 209 +++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 packaging/scripts/samios-voice.service create mode 100644 packaging/scripts/voice-bridge.py diff --git a/packaging/scripts/samios-voice.service b/packaging/scripts/samios-voice.service new file mode 100644 index 0000000..f944af0 --- /dev/null +++ b/packaging/scripts/samios-voice.service @@ -0,0 +1,16 @@ +[Unit] +Description=SamiOS Voice Bridge (SamiType → Hermes) +After=pipewire.service graphical-session.target +Wants=pipewire.service + +[Service] +Type=simple +ExecStart=/usr/bin/python3 /usr/local/bin/voice-bridge.py +Restart=on-failure +RestartSec=3 +Environment=DISPLAY=:0 +Environment=XDG_RUNTIME_DIR=/mnt/wslg/runtime-dir +User=sami + +[Install] +WantedBy=default.target diff --git a/packaging/scripts/voice-bridge.py b/packaging/scripts/voice-bridge.py new file mode 100644 index 0000000..f36591a --- /dev/null +++ b/packaging/scripts/voice-bridge.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +""" +SamiOS Voice Command Bridge +Listens for voice commands from SamiType, routes them to Hermes or dictation mode. + +Usage: + python3 voice-bridge.py + +Requires: + - SamiType STT running in streaming mode (Unix socket or WebSocket) + - xdotool (for dictation mode typing) + - Hermes API access (for command mode) +""" + +import json +import subprocess +import socket +import os +import sys +import time +import threading +from enum import Enum + +class VoiceMode(Enum): + IDLE = "idle" + COMMAND = "command" + DICTATION = "dictation" + +class VoiceBridge: + def __init__(self): + self.mode = VoiceMode.IDLE + self.running = False + self.last_transcript = "" + self.context = { + "hostname": os.uname().nodename, + "user": os.environ.get("USER", "sami"), + } + + # Dictation commands (spoken → action) + self.dictation_commands = { + "new paragraph": "Return", + "new line": "Shift+Return", + "comma": ",", + "period": ".", + "question mark": "?", + "exclamation mark": "!", + "colon": ":", + "semicolon": ";", + "open quote": '"', + "close quote": '"', + "open paren": "(", + "close paren": ")", + "tab": "Tab", + "scratch that": "Ctrl+Z", + "undo that": "Ctrl+Z", + "stop dictation": "EXIT_DICTATION", + } + + def start(self): + """Start the voice bridge""" + self.running = True + print("[VoiceBridge] Ready. Say 'Computer' + command, or use hotkey.") + + # Connect to SamiType STT + # For now, read from stdin (will be SamiType socket later) + while self.running: + try: + line = input() + if not line.strip(): + continue + data = json.loads(line) if line.startswith("{") else {"text": line} + self.handle_transcript(data) + except EOFError: + break + except KeyboardInterrupt: + break + + def handle_transcript(self, data: dict): + """Handle incoming transcript from SamiType""" + text = data.get("text", "").strip() + confidence = data.get("confidence", 1.0) + + if not text: + return + + # Check mode + if self.mode == VoiceMode.DICTATION: + self._handle_dictation(text, confidence) + elif text.lower().startswith("computer"): + command = text[8:].strip() # Remove "computer" + self._handle_command(command, confidence) + elif text.lower() == "start dictation": + self._enter_dictation() + else: + # Not a command and not in dictation mode — ignore + pass + + def _handle_command(self, command: str, confidence: float): + """Route command to Hermes API""" + print(f"[COMMAND] → Hermes: {command}") + + # Show low-confidence warning + if confidence < 0.8: + print(f"[WARNING] Low confidence ({confidence:.0%}) — verify: {command}") + + # Send to Hermes + result = self._send_to_hermes(command) + + # Display result + if result: + print(f"[HERMES] {result.get('summary', 'Done.')}") + # TODO: TTS via SamiType + else: + print("[HERMES] No response") + + def _handle_dictation(self, text: str, confidence: float): + """Type text into focused application""" + lower = text.lower().strip() + + # Check for dictation commands + if lower in self.dictation_commands: + action = self.dictation_commands[lower] + if action == "EXIT_DICTATION": + self._exit_dictation() + return + self._send_key(action) + return + + # Type the text + self._type_text(text) + + # Show correction prompt if low confidence + if confidence < 0.85: + print(f"[CORRECT?] {text} (confidence: {confidence:.0%})") + + def _enter_dictation(self): + """Enter dictation mode""" + self.mode = VoiceMode.DICTATION + print("[MODE] Dictation active — speaking will type into focused app") + print("[MODE] Say 'stop dictation' to exit") + + def _exit_dictation(self): + """Exit dictation mode""" + self.mode = VoiceMode.IDLE + print("[MODE] Dictation stopped") + + def _type_text(self, text: str): + """Type text into the focused application""" + try: + subprocess.run( + ["xdotool", "type", "--clearmodifiers", "--delay", "0", text + " "], + check=True + ) + except FileNotFoundError: + # Fallback: xsel/xclip + try: + proc = subprocess.Popen(["xsel", "-b", "-i"], stdin=subprocess.PIPE) + proc.communicate(text.encode()) + subprocess.run(["xdotool", "key", "ctrl+v"], check=True) + except Exception as e: + print(f"[ERROR] Cannot type text: {e}") + + def _send_key(self, keysym: str): + """Send a key combination""" + try: + subprocess.run(["xdotool", "key", keysym], check=True) + except Exception as e: + print(f"[ERROR] Cannot send key {keysym}: {e}") + + def _send_to_hermes(self, command: str) -> dict: + """Send command to Hermes API""" + import urllib.request + + payload = json.dumps({ + "text": command, + "source": "voice", + "context": self.context, + }).encode() + + try: + req = urllib.request.Request( + "http://localhost:8765/command", + data=payload, + headers={"Content-Type": "application/json"}, + method="POST" + ) + with urllib.request.urlopen(req, timeout=30) as resp: + return json.loads(resp.read()) + except Exception as e: + print(f"[ERROR] Hermes API: {e}") + # Fallback: CLI + try: + result = subprocess.run( + ["hermes", "ask", command], + capture_output=True, text=True, timeout=60 + ) + return {"summary": result.stdout.strip()} + except Exception: + return None + +def main(): + bridge = VoiceBridge() + try: + bridge.start() + except KeyboardInterrupt: + print("\n[VoiceBridge] Stopped.") + +if __name__ == "__main__": + main()