115 lines
5.4 KiB
Python
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]
|