Godstrument/workers/godspeak_worker.py
monsterrobotparty e56e3bc28f 🗣 pantheon: GODSPEAK worker — the voice of god (Stage 2γ)
The user's wersperr, wired into the sky: hold Right ⌥, speak, release —
the transcript flows to the hub as OSC and reaches every client as a
voice event (Stage 3 gives it the grammar).

- workers/godspeak_worker.py: wersperr-derived (MLX Whisper, 16kHz mono,
  Right ⌥ via pynput). Sends /godspeak/rec 1|0 on key edges and
  /godspeak <text> after transcription instead of pbcopy/paste. House
  worker pattern: argparse --host/--port, lazy imports with graceful
  "not available — skipping (optional)" exit, --check self-test, warm-up.
  MLX is Apple-Silicon-only so it's local-rig-only + opt-in; deps stay
  OUT of requirements.txt (documented pip install in the docstring).
  Transcription serialized (one MLX graph at a time); a busy mic can't
  kill the hotkey listener.
- hub.py: _osc_handler special-cases /godspeak (text/state, not a float
  signal), queues it, and control_loop relays {"godspeak":{...}} to ws
  clients — dropped entirely in read-only mode (it's a command channel).
  Bridged OSC-thread → asyncio via a queue, like every other ingest.
- run.py: --godspeak opt-in flag mirroring --sports; never a default.

Python-only — zero viz/index.html, zero requirements.txt/config.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 16:57:37 +10:00

144 lines
5.5 KiB
Python

"""GODSPEAK — the voice of god. The user's own wersperr (local MLX Whisper),
wired into the sky: hold Right ⌥, speak a command, release. The transcript is
sent to the hub as OSC and reaches every client as a voice event — Stage 3 (ε)
gives it a grammar ("tempo ninety", "storm mood", "take it back").
LOCAL-RIG ONLY. MLX Whisper is Apple-Silicon-only, so this worker runs only
where the user's Mac runs the hub — the VPS simply never has it, exactly like
the ToF / vision workers. It is opt-in (`run.py --godspeak`) and is never in
the default worker set.
Its dependencies are deliberately NOT in requirements.txt — the VPS install must
stay clean. On a Mac, into the hub's venv:
pip install mlx-whisper sounddevice pynput # numpy rides along
Model: mlx-community/whisper-large-v3-turbo (~1.6 GB, downloaded on first run).
python3 workers/godspeak_worker.py # hold Right ⌥ to speak
python3 workers/godspeak_worker.py --check # self-test: model loads + silence transcribes, then exit
OSC it sends (to the hub, udp/9000):
/godspeak/rec 1 on key-down (a 🗣 pulse can rise while recording)
/godspeak/rec 0 on release
/godspeak <text> after transcription
"""
import argparse
import sys
import threading
MODEL = "mlx-community/whisper-large-v3-turbo"
SAMPLE_RATE = 16000 # Whisper's native rate; mono float32
def main():
ap = argparse.ArgumentParser(
description="GODSPEAK worker — local voice commands into the sky (Apple-Silicon only, opt-in).")
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=9000)
ap.add_argument("--check", action="store_true",
help="self-test: load the model and transcribe silence, then exit")
args = ap.parse_args()
# Lazy import of the optional Apple-Silicon voice stack. Anywhere without MLX
# (the VPS, a bare venv) this skips cleanly so the rest of the rig runs — the
# house pattern shared with the vision / MIDI workers.
try:
import numpy as np
import sounddevice as sd
import mlx_whisper
from pynput import keyboard
except Exception:
print("godspeak worker: mlx-whisper/sounddevice/pynput not available — skipping (optional)")
sys.exit(0)
from pythonosc.udp_client import SimpleUDPClient
def transcribe(audio):
return mlx_whisper.transcribe(audio, path_or_hf_repo=MODEL)["text"].strip()
if args.check:
# Self-check: the model loads and transcribes silence without exploding.
result = transcribe(np.zeros(SAMPLE_RATE, dtype=np.float32))
assert isinstance(result, str)
print("ok: godspeak model loaded, pipeline works")
sys.exit(0)
client = SimpleUDPClient(args.host, args.port)
HOTKEY = keyboard.Key.alt_r # Right Option — hold to dictate
def emit(addr, value):
# Never let a transient socket hiccup kill the hotkey listener.
try:
client.send_message(addr, value)
except Exception as exc:
print(f"godspeak worker: OSC send failed ({exc}); continuing")
frames = []
stream = {"s": None} # boxed so the key callbacks can rebind it
tx_lock = threading.Lock() # MLX isn't safe under concurrent eval — one utterance transcribes at a time
def start_recording():
if stream["s"]:
return
frames.clear()
s = None
try: # a busy / absent input device must not tear down the hotkey listener
s = sd.InputStream(
samplerate=SAMPLE_RATE, channels=1, dtype="float32",
callback=lambda data, *_: frames.append(data.copy()),
)
s.start()
except Exception as exc:
print(f"godspeak worker: mic open failed ({exc}); ignoring this press", flush=True)
if s is not None:
try: s.close()
except Exception: pass
return
stream["s"] = s
emit("/godspeak/rec", 1)
print("● recording...", flush=True)
def stop_recording():
s = stream["s"]
if not s:
return
s.stop(); s.close(); stream["s"] = None
emit("/godspeak/rec", 0)
if not frames:
return
audio = np.concatenate(frames).flatten()
if len(audio) < SAMPLE_RATE // 3: # ignore accidental taps (< 1/3 s)
return
# transcribe off the hotkey thread so the keyboard stays responsive
threading.Thread(target=lambda: finish(audio), daemon=True).start()
def finish(audio):
with tx_lock: # serialize: back-to-back commands can't run two MLX graphs at once
text = transcribe(audio)
if text:
print(f"{text}", flush=True)
emit("/godspeak", text)
def on_press(key):
if key == HOTKEY:
start_recording()
def on_release(key):
if key == HOTKEY:
stop_recording()
print("loading model (first run downloads ~1.6GB)...", flush=True)
transcribe(np.zeros(SAMPLE_RATE, dtype=np.float32)) # warm up so the first real utterance is fast
print(f"godspeak ready — hold Right ⌥ and speak (→ hub {args.host}:{args.port}); Ctrl+C to quit", flush=True)
with keyboard.Listener(on_press=on_press, on_release=on_release) as listener:
try:
listener.join()
except KeyboardInterrupt:
print("godspeak worker: shutting down")
if __name__ == "__main__":
main()