Files
Bolt-Assistant/bolt_assistant/config.py
T

115 lines
5.4 KiB
Python

"""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()]
# These count anywhere in what was said, not just at the start ("what's the
# weather, assistant?"), and the whole sentence is sent as the request.
ANYWHERE_WAKE_WORDS = [w.strip().lower() for w in os.environ.get(
"ANYWHERE_WAKE_WORDS", "assistant").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]