Add voice-bridge.py prototype + systemd service, LibreOffice in package list
SamiOS CI / lint-and-test (push) Successful in 6s

- 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
This commit is contained in:
Sami Ahmed
2026-08-11 03:55:31 -07:00
parent 08758617b5
commit 2b969b2754
2 changed files with 225 additions and 0 deletions
+16
View File
@@ -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
+209
View File
@@ -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()