Bolt Assistant: wake-phrase voice assistant for Bolt
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"""Settings for Bolt Assistant, from a `.env` next to the project (or the
|
||||
real environment, which wins). Everything else reads config from here."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent.parent # project root
|
||||
|
||||
|
||||
def _load_env() -> None:
|
||||
env_path = HERE / ".env"
|
||||
if not env_path.exists():
|
||||
return
|
||||
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, _, value = line.partition("=")
|
||||
os.environ.setdefault(key.strip(), value.strip())
|
||||
|
||||
|
||||
_load_env()
|
||||
|
||||
|
||||
def _flag(name: str, default: str) -> bool:
|
||||
return os.environ.get(name, default).strip().lower() in ("1", "true", "yes", "on")
|
||||
|
||||
|
||||
def _device(raw: str):
|
||||
"""An input/output device: blank = system default, digits = an index,
|
||||
anything else = a name substring (sounddevice's rules)."""
|
||||
raw = (raw or "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
return int(raw) if raw.lstrip("-").isdigit() else raw
|
||||
|
||||
|
||||
# ── server ──────────────────────────────────────────────────────────────────
|
||||
|
||||
SERVER_URL = os.environ.get("BOLT_SERVER_URL", "").rstrip("/")
|
||||
API_KEY = os.environ.get("DESK_API_KEY", "")
|
||||
# Its own conversation, separate from the pet's and the console's. The "pet-"
|
||||
# prefix gets the desk API's voice-client behaviour (short, speakable replies).
|
||||
SESSION_ID = os.environ.get("DESK_SESSION_ID") or "pet-assistant-" + (platform.node() or "desktop")
|
||||
# The voice the server's /desk/tts speaks in (an ElevenLabs voice id).
|
||||
VOICE_ID = os.environ.get("ELEVENLABS_VOICE_ID", "")
|
||||
TTS_SAMPLE_RATE = int(os.environ.get("TTS_SAMPLE_RATE", "16000"))
|
||||
STREAMING_REPLIES = _flag("STREAMING_REPLIES", "true")
|
||||
|
||||
# ── audio devices ───────────────────────────────────────────────────────────
|
||||
|
||||
MIC_DEVICE = _device(os.environ.get("MIC_DEVICE", ""))
|
||||
SPEAKER_DEVICE = _device(os.environ.get("SPEAKER_DEVICE", ""))
|
||||
|
||||
# ── wake phrases ────────────────────────────────────────────────────────────
|
||||
# Heard through the server's speech-to-text, so these are matched against
|
||||
# *text*. A phrase counts only near the start of what was said ("hey bolt,
|
||||
# what's the time"), not buried in a sentence ("tighten that bolt"). Speech
|
||||
# recognisers often write "Bolt" as "bolts"/"volt"/"bold"; those spellings
|
||||
# only count after "hey"/"ok", where they can't be ordinary words.
|
||||
|
||||
WAKE_WORDS = [w.strip().lower() for w in os.environ.get(
|
||||
"WAKE_WORDS",
|
||||
"hey bolt,bolt,thunderbolt,hey thunderbolt,ok bolt,okay bolt,yo bolt,hi bolt,"
|
||||
"hey assistant,ok assistant,okay assistant,"
|
||||
"hey bolts,hey volt,hey bold,hey boat,okay volt,hey thunder bolt,thunder bolt",
|
||||
).split(",") if w.strip()]
|
||||
|
||||
# ── listening (local voice-activity detection, before anything is sent) ────
|
||||
# Nothing leaves the machine until this says someone is talking: silence and
|
||||
# background hum are never sent (or billed). Raise VAD_RMS_THRESHOLD if
|
||||
# background noise keeps getting transcribed; lower it if you have to shout.
|
||||
|
||||
VAD_RMS_THRESHOLD = int(os.environ.get("VAD_RMS_THRESHOLD", "500"))
|
||||
VAD_START_FRAMES = int(os.environ.get("VAD_START_FRAMES", "3")) # 80 ms each
|
||||
VAD_SILENCE_END_SEC = float(os.environ.get("VAD_SILENCE_END_SEC", "0.8"))
|
||||
MIN_UTTERANCE_SEC = float(os.environ.get("MIN_UTTERANCE_SEC", "0.35")) # shorter: never sent
|
||||
MAX_UTTERANCE_SEC = float(os.environ.get("MAX_UTTERANCE_SEC", "12"))
|
||||
# After a bare "Hey Bolt", how long to wait for the actual request.
|
||||
COMMAND_WAIT_SEC = float(os.environ.get("COMMAND_WAIT_SEC", "8"))
|
||||
# When a reply ends on a question, keep listening for the answer without the
|
||||
# wake word, for this long, at most this many times in a row.
|
||||
FOLLOW_UP_SEC = float(os.environ.get("FOLLOW_UP_SEC", "8"))
|
||||
FOLLOW_UP_MAX_TURNS = int(os.environ.get("FOLLOW_UP_MAX_TURNS", "5"))
|
||||
CHIMES = _flag("CHIMES", "true")
|
||||
|
||||
# ── what Bolt may do on this machine ────────────────────────────────────────
|
||||
# The server can relay commands back. With no screen to approve them on:
|
||||
# off nothing runs; Bolt is told it can't here
|
||||
# read (default) listing and reading files only, via filectl
|
||||
# all everything, including shell commands and file writes — like
|
||||
# Bolt-Pet. Only if you trust every request that reaches your mic.
|
||||
|
||||
COMMANDS = os.environ.get("ASSISTANT_COMMANDS", "read").strip().lower()
|
||||
COMMAND_TIMEOUT_SECONDS = int(os.environ.get("COMMAND_TIMEOUT_SECONDS", "60"))
|
||||
|
||||
SAMPLE_RATE = 16000
|
||||
FRAME_SAMPLES = 1280 # 80 ms
|
||||
|
||||
|
||||
def is_configured() -> bool:
|
||||
return bool(SERVER_URL and API_KEY)
|
||||
|
||||
|
||||
def missing_config() -> list:
|
||||
return [name for name, value in (("BOLT_SERVER_URL", SERVER_URL), ("DESK_API_KEY", API_KEY),
|
||||
("ELEVENLABS_VOICE_ID", VOICE_ID)) if not value]
|
||||
Reference in New Issue
Block a user