The GODSPEAK grammar: tempo/bpm with spoken numbers (one twenty → 120), squash N percent, AMPLER capture/chop, open/close every panel, play/stop the beat, mood, skin, zen — and take it back, the voice of god un-doing. First match wins; unmatched words arrive in the ticker, never an error. A 🗣 chip pulses while the mic is held. Polish: arp.up pruned, the arp breathes with groove.velo, tempo edits mark undo, serializePatch carries godtime.locked so undoing a mood restores the lock. Grimoire chapter for all four gods + regenerated manual + worker permission notes. Reviewed by fable: two fixes folded in — matchSkin normalizes candidate names (so god-s-eye matches post-convergence) and a virgin session saying play-the-beat gets the seed pattern first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
150 lines
5.9 KiB
Python
150 lines
5.9 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).
|
|
|
|
macOS one-time permissions (System Settings → Privacy & Security) for the
|
|
terminal/app that launches this worker — same as wersperr:
|
|
- Microphone — to hear you (sounddevice input)
|
|
- Input Monitoring — for pynput to see the Right ⌥ hotkey held globally
|
|
(older macOS may list this under Accessibility instead)
|
|
|
|
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()
|