Added chatterbox connections
This commit is contained in:
1 parent
41ef12f954
commit
e58ceeb192
58 files changed
+8931
-8
No files matched your search
@@ -0,0 +1,235 @@
|
||||
"""Local-only Glitch lab orchestration; never contacts a Pi or Odin."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
BASE = "http://127.0.0.1:18766"
|
||||
TOOLS = ("cargo", "flutter", "nats-server")
|
||||
PORTS = (14222, 18766, 18767)
|
||||
|
||||
def preflight():
|
||||
missing = []
|
||||
for name in TOOLS:
|
||||
path = shutil.which(name)
|
||||
print(f"{name}: {path or 'MISSING - install and add to PATH'}")
|
||||
if not path:
|
||||
missing.append(name)
|
||||
if missing:
|
||||
print("See README.md prerequisites. Nothing was started.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def run_checked(args, cwd=ROOT):
|
||||
# Flutter ships as a batch file on Windows; cmd is used solely for its fixed CLI.
|
||||
if os.name == "nt" and str(args[0]).lower().endswith((".bat", ".cmd")):
|
||||
args = ["cmd.exe", "/d", "/c", *args]
|
||||
subprocess.run(args, cwd=cwd, check=True)
|
||||
|
||||
def api(path, body=None, base=BASE):
|
||||
data = None if body is None else json.dumps(body).encode()
|
||||
request = urllib.request.Request(base + path, data=data,
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(request, timeout=5) as response:
|
||||
return json.load(response)
|
||||
|
||||
def command(action, session, command_id=None):
|
||||
return {"id": command_id or str(uuid.uuid4()), "session": session,
|
||||
"expires_ms": int(time.time() * 1000) + 10000, "action": action}
|
||||
|
||||
def smoke():
|
||||
state = api("/state")["state"]
|
||||
assert state["simulated"] is True, "Refusing to test a real cabinet"
|
||||
session = state["session"]
|
||||
def send(action):
|
||||
return api("/command", command(action, session))
|
||||
assert send("reset")["accepted"]
|
||||
assert not send("start")["accepted"], "Start must require power and credit"
|
||||
power = command("power_on", session)
|
||||
assert api("/command", power)["state"]["phase"] == "booting"
|
||||
assert api("/command", power)["duplicate"], "Duplicate command must be acknowledged"
|
||||
assert not send("scan")["accepted"], "Boot-period scans must not grant credit"
|
||||
deadline = time.monotonic() + 6
|
||||
while time.monotonic() < deadline:
|
||||
state = api("/state")["state"]
|
||||
if state["phase"] == "ready":
|
||||
break
|
||||
time.sleep(0.1)
|
||||
assert state["phase"] == "ready" and state["credits"] == 0
|
||||
assert not send("start")["accepted"]
|
||||
scan = command("scan", session)
|
||||
assert api("/command", scan)["state"]["credits"] == 1
|
||||
replay = api("/command", scan)
|
||||
assert replay["duplicate"] and replay["state"]["scans"] == 1
|
||||
assert send("start")["state"]["phase"] == "playing"
|
||||
assert send("win")["state"]["phase"] == "won"
|
||||
assert not api("/command", command("reset", "stale-session"))["accepted"]
|
||||
expired = command("reset", session)
|
||||
expired["expires_ms"] = 1
|
||||
assert not api("/command", expired)["accepted"]
|
||||
assert send("reset")["state"]["phase"] == "powered_off"
|
||||
print("PASS: real HTTP -> Rust -> NATS -> MessagePack -> simulated cabinet flow")
|
||||
|
||||
def build():
|
||||
run_checked([shutil.which("cargo"), "test"])
|
||||
run_checked([shutil.which("cargo"), "build"])
|
||||
flutter = shutil.which("flutter")
|
||||
run_checked([flutter, "pub", "get"], ROOT / "gm")
|
||||
run_checked([flutter, "analyze"], ROOT / "gm")
|
||||
run_checked([flutter, "test"], ROOT / "gm")
|
||||
run_checked([flutter, "build", "web", "--release"], ROOT / "gm")
|
||||
|
||||
def start(godot=False, room=False, speech=False, lan=False, speech_output="browser", speech_backend="chatterbox"):
|
||||
if speech:
|
||||
import teddy_speech
|
||||
if not room:
|
||||
raise RuntimeError("--speech requires start-room")
|
||||
if speech_backend == "chatterbox":
|
||||
from chatterbox_client import REFERENCE
|
||||
if not REFERENCE.exists():
|
||||
raise RuntimeError("Missing Teddy Chatterbox reference; see TEDDY_TRAINING.md")
|
||||
if speech_backend == "piper" and (not teddy_speech.python_path().exists() or not teddy_speech.MODEL.exists()):
|
||||
raise RuntimeError("Install the lab voice first: py -3 lab.py setup-speech")
|
||||
# Fail before launching anything if another process owns a lab port.
|
||||
ports = (14232, 18776, 18777) if room else PORTS
|
||||
check_ports = (*ports, 18768) if godot else ports
|
||||
if speech:
|
||||
check_ports = (*check_ports, 18778)
|
||||
for port in check_ports:
|
||||
with socket.socket() as sock:
|
||||
sock.bind(("127.0.0.1", port))
|
||||
binary = ROOT / "target" / "debug" / ("glitch-odin-lab.exe" if os.name == "nt" else "glitch-odin-lab")
|
||||
if not binary.exists() or not (ROOT / "gm/build/web/index.html").exists():
|
||||
raise RuntimeError("Build the lab first: python lab.py build")
|
||||
processes = []
|
||||
def spawn(args):
|
||||
flags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
|
||||
process = subprocess.Popen(args, cwd=ROOT, creationflags=flags)
|
||||
processes.append(process)
|
||||
return process
|
||||
try:
|
||||
broker = spawn([shutil.which("nats-server"), "-c", str(ROOT / ("room-nats.conf" if room else "nats.conf"))])
|
||||
deadline = time.monotonic() + 5
|
||||
while True:
|
||||
if broker.poll() is not None:
|
||||
raise RuntimeError("NATS exited; see its output above")
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", ports[0]), timeout=0.2):
|
||||
break
|
||||
except OSError:
|
||||
if time.monotonic() >= deadline:
|
||||
raise RuntimeError("NATS did not become ready")
|
||||
time.sleep(0.1)
|
||||
if room:
|
||||
spawn([str(binary), "room"])
|
||||
elif godot:
|
||||
executable = ROOT.parents[1] / "Godot_v4.7.2-stable_win64_console.exe"
|
||||
godot_path = os.environ.get("GLITCH_LAB_GODOT") or str(executable)
|
||||
if not Path(godot_path).is_file():
|
||||
raise RuntimeError("Set GLITCH_LAB_GODOT to the Godot executable path")
|
||||
spawn([godot_path, "--path", str(ROOT.parents[1]), "--windowed",
|
||||
"res://tests/odin_lab/cabinet.tscn", "--", "--glitch-lab", "--lab-inject-rfid"])
|
||||
spawn([str(binary), "godot"])
|
||||
else:
|
||||
spawn([str(binary), "cabinet"])
|
||||
if not room:
|
||||
spawn([str(binary), "controller"])
|
||||
if room:
|
||||
spawn([sys.executable, str(ROOT / "room_web.py"), "--bind", "0.0.0.0" if lan else "127.0.0.1"])
|
||||
else:
|
||||
spawn([sys.executable, "-m", "http.server", str(ports[2]), "--bind", "127.0.0.1",
|
||||
"--directory", str(ROOT / "gm/build/web")])
|
||||
deadline = time.monotonic() + 10
|
||||
while True:
|
||||
if any(p.poll() is not None for p in processes):
|
||||
raise RuntimeError("A lab process exited during startup")
|
||||
try:
|
||||
api("/room/state" if room else "/state", base=f"http://127.0.0.1:{ports[1]}")
|
||||
break
|
||||
except (OSError, ValueError):
|
||||
if time.monotonic() >= deadline:
|
||||
raise RuntimeError("Controller/cabinet did not become ready")
|
||||
time.sleep(0.2)
|
||||
if speech:
|
||||
spawn([sys.executable, str(ROOT / "teddy_speech.py"), "follow", "--backend", speech_backend, "--output-device", speech_output])
|
||||
deadline = time.monotonic() + 180
|
||||
while True:
|
||||
if any(p.poll() is not None for p in processes):
|
||||
raise RuntimeError("Teddy speech failed to start")
|
||||
try:
|
||||
health = api("/health", base="http://127.0.0.1:18778")
|
||||
if health.get("status") == "error":
|
||||
raise RuntimeError(health.get("message", "Teddy failed to load"))
|
||||
if health.get("status") != "starting":
|
||||
break
|
||||
if time.monotonic() >= deadline:
|
||||
raise RuntimeError("Teddy model loading timed out; see .speech/chatterbox.log")
|
||||
time.sleep(.2)
|
||||
except (OSError, ValueError):
|
||||
if time.monotonic() >= deadline:
|
||||
raise RuntimeError("Teddy speech did not become ready")
|
||||
time.sleep(0.2)
|
||||
url = f"http://127.0.0.1:{ports[2]}/" + ("?room=1" if room else "")
|
||||
print(f"READY: {url} - press Ctrl+C to stop all lab processes", flush=True)
|
||||
if lan:
|
||||
print("LAN DEMO: open http://<this-PC-Wi-Fi-IPv4>:18777/?room=1 on your phone. Shared simulation controls; enable browser audio on each listening device.", flush=True)
|
||||
while True:
|
||||
if any(p.poll() is not None for p in processes):
|
||||
raise RuntimeError("A lab process exited; stopping this lab")
|
||||
time.sleep(0.5)
|
||||
finally:
|
||||
for process in reversed(processes):
|
||||
if process.poll() is None:
|
||||
process.terminate()
|
||||
for process in processes:
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait()
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("mode", choices=("preflight", "build", "start", "start-godot", "start-room", "setup-speech", "smoke"))
|
||||
parser.add_argument("--speech", action="store_true", help="Play Teddy cues using Chatterbox (default) or Piper")
|
||||
parser.add_argument("--lan", action="store_true", help="Expose only the room demo webpage/gateway to the LAN")
|
||||
parser.add_argument("--speech-backend", choices=("chatterbox", "piper"), default="chatterbox")
|
||||
parser.add_argument("--speech-output", choices=("browser","local"), default="browser")
|
||||
args = parser.parse_args()
|
||||
mode = args.mode
|
||||
if args.lan and mode != "start-room":
|
||||
parser.error("--lan is only supported with start-room")
|
||||
if args.speech and mode != "start-room":
|
||||
parser.error("--speech is only supported with start-room")
|
||||
if mode == "setup-speech":
|
||||
import teddy_speech
|
||||
teddy_speech.setup()
|
||||
return 0
|
||||
if mode == "smoke":
|
||||
smoke()
|
||||
return 0
|
||||
if not preflight():
|
||||
return 1
|
||||
if mode == "build":
|
||||
build()
|
||||
elif mode in ("start", "start-godot", "start-room"):
|
||||
start(godot=mode == "start-godot", room=mode == "start-room", speech=args.speech, lan=args.lan, speech_output=args.speech_output, speech_backend=args.speech_backend)
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\nLab stopped.")
|
||||
except Exception as error:
|
||||
print(f"ERROR: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
Reference in new issue
Block a user