346 lines
18 KiB
Bash
346 lines
18 KiB
Bash
# Audio Agent Engine — Environment Variables
|
||
# Copy to `.env` and adjust as needed.
|
||
|
||
# --- Server ---
|
||
SERVER_PORT=8787
|
||
|
||
# --- TTS Engine Selection ---
|
||
# Choose the TTS engine plugin. Supported values:
|
||
# fish — Fish Audio / SGLang Drama 3 (OpenAI-compatible HTTP endpoint)
|
||
# kokoro — Kokoro TTS (stub; see src/tts/kokoro.rs)
|
||
TTS_ENGINE=fish
|
||
|
||
# --- Fish Audio / SGLang (TTS_ENGINE=fish) ---
|
||
SGLANG_URL=http://10.8.0.3:8000
|
||
TTS_MODEL=fishaudio/drama-3
|
||
TTS_VOICE=default
|
||
# Optional voice-cloning reference (path on the Fish Audio server filesystem).
|
||
TTS_REFERENCE_AUDIO=/root/pipecat-voice/voice_profiles/solis.wav
|
||
# Optional transcript. If it looks like a .txt path, its contents are loaded;
|
||
# otherwise the raw string is used. Leave empty to auto-load <audio>.txt.
|
||
TTS_REFERENCE_TEXT=/root/pipecat-voice/voice_profiles/solis.txt
|
||
|
||
# --- Drama 3 de-emphasis (TTS_ENGINE=fish) ---
|
||
# Drama 3 can sound overly theatrical/recitative. Two knobs trim that register
|
||
# without changing the voice reference. Leave a sampling variable EMPTY to omit
|
||
# the field and fall back to the server default.
|
||
# Sampling temperature sent to SGLang. Lower values reduce over-articulation;
|
||
# the recitative "radio drama" register lives in the distribution tails, so a
|
||
# lower value makes the delivery flatter. Tunable range: 0.3–0.6.
|
||
TTS_TEMPERATURE=0.45
|
||
# Nucleus sampling top-p: trims the tail of the acoustic token distribution
|
||
# (exaggerated intonations). 0.7–0.8 is the useful range.
|
||
TTS_TOP_P=0.75
|
||
# Optional top-k. Leave empty to omit the field (server default).
|
||
TTS_TOP_K=
|
||
# Neutral style tag prepended to every synthesis request that does not already
|
||
# start with a "[...]" tag. LLM replies carry no tag (the system prompt forbids
|
||
# stage directions), so they get this register; clip banks with their own tag
|
||
# are left untouched. Empty disables the prefix.
|
||
TTS_STYLE_TAG="[conversational, casual]"
|
||
|
||
# --- Custom voice post-processing (/customvoice page) ---
|
||
# A freshly recorded reference is cleaned and level-matched before it is
|
||
# installed. Disable the whole chain to store the plain transcode.
|
||
CUSTOMVOICE_POSTPROCESS=true
|
||
# Noise reduction (FFT denoise) + de-essing. Turn off for a clean studio take.
|
||
CUSTOMVOICE_DENOISE=true
|
||
# Integrated loudness target (LUFS) and true-peak ceiling (dBTP). Every take
|
||
# lands on the same level, so recordings never jump in volume. Kept below the
|
||
# shipped reference (solis.wav measures ≈-23.4 LUFS): a user take is far more
|
||
# dynamic, so at equal LUFS its hotter peaks make the cloned voice sound louder.
|
||
CUSTOMVOICE_TARGET_LUFS=-26
|
||
CUSTOMVOICE_TRUE_PEAK=-5
|
||
|
||
# --- Kokoro (TTS_ENGINE=kokoro) ---
|
||
KOKORO_URL=http://localhost:8880
|
||
KOKORO_VOICE=af_heart
|
||
|
||
# --- STT (Parakeet TDT, pure-Rust ONNX pipeline) ---
|
||
# Directory containing nemo128.onnx, encoder-model*.onnx,
|
||
# decoder_joint-model.onnx, vocab.txt and config.json.
|
||
STT_MODEL_PATH=models/parakeet
|
||
# Optional external tokenizer; leave empty — Parakeet decodes text internally.
|
||
STT_TOKENIZER_PATH=
|
||
|
||
# --- VAD (Silero VAD v5, ONNX) ---
|
||
# Master switch. When disabled, the STT worker falls back to its internal
|
||
# RMS-based segmentation (legacy behaviour).
|
||
VAD_ENABLED=true
|
||
# Path to the Silero VAD ONNX model (16 kHz, 512-sample windows).
|
||
VAD_MODEL_PATH=models/silero_vad.onnx
|
||
# Speech probability above which a window counts as voice.
|
||
VAD_THRESHOLD=0.5
|
||
# Probability below which a window counts as silence. Must be <= VAD_THRESHOLD;
|
||
# the gap provides hysteresis and prevents onset/offset flapping.
|
||
VAD_NEG_THRESHOLD=0.35
|
||
# Speech required (ms) to confirm an utterance start.
|
||
VAD_MIN_SPEECH_MS=250
|
||
# Silence required (ms) to close an utterance (end-of-utterance window).
|
||
VAD_MIN_SILENCE_MS=700
|
||
# Audio kept (ms) before the detected onset, so the first phoneme is not clipped.
|
||
VAD_SPEECH_PAD_MS=30
|
||
# Speech required (ms) to fire the fast barge-in onset (must be <= VAD_MIN_SPEECH_MS).
|
||
VAD_BARGE_IN_MIN_SPEECH_MS=150
|
||
# Utterance gate applied before transcription. A VAD segment is dropped (no STT,
|
||
# no LLM turn) when it is shorter than VAD_MIN_UTTERANCE_MS ms (0 disables) or
|
||
# its average speech probability is below VAD_MIN_AVG_PROB (0.0 disables). The
|
||
# average is taken over the voiced windows only (prob >= VAD_NEG_THRESHOLD), so
|
||
# the min_silence hangover does not dilute short words ("uno", "due"). This
|
||
# filters the VAD false-triggers (breaths, clicks, acoustic echo, noise) that the
|
||
# STT turns into hallucinated text ("Bye.", "She", <transcript 0 tokens>) and
|
||
# that would each otherwise cost a full LLM turn. Raise the probability to be
|
||
# stricter; lower it if genuine soft speech is being dropped.
|
||
VAD_MIN_UTTERANCE_MS=300
|
||
VAD_MIN_AVG_PROB=0.25
|
||
# Feed the STT engine with VAD-segmented utterances (true) instead of letting
|
||
# the worker run its own RMS segmentation (false).
|
||
VAD_SEGMENT_STT=true
|
||
# Interrupt the agent when the user starts speaking over it.
|
||
VAD_BARGE_IN=true
|
||
# Log the speech probability of every analysis window (debug).
|
||
VAD_DEBUG=false
|
||
|
||
# --- Fillers / Mixer ---
|
||
# Directory of preloaded "*.raw" PCM clips (16-bit LE mono, 44.1 kHz), e.g.
|
||
# uhm / eh / mmm. Played round-robin at most once per turn.
|
||
FILLERS_DIR=assets/fillers
|
||
# Master switch. When disabled, no end-of-turn filler is ever queued.
|
||
FILLER_ENABLED=true
|
||
# Latency guard (ms): the filler is a latency mask, so it is only played if the
|
||
# response audio has not started within this window. If the reply starts
|
||
# speaking earlier the filler is dropped, instead of talking over the first
|
||
# words of the answer. Increase for slower LLM/TTS, decrease for faster ones.
|
||
FILLER_DELAY_MS=2500
|
||
FILLER_GAIN_IDLE=1.0
|
||
FILLER_GAIN_DUCK=0.15
|
||
# --- Start filler (turn-start acknowledgment when the reply is late) ---
|
||
# A second latency mask, on its own mixer channel: if the first audio chunk of
|
||
# the reply has not reached the mixer within START_FILLER_GO_DELAY ms, one of the
|
||
# phrases below (picked at random) is played so the agent acknowledges the turn
|
||
# while the LLM/TTS are still working. If audio arrives in time the cue is
|
||
# dropped. It shares the filler gain and is faded out (then cut) as soon as the
|
||
# reply starts playing, so it does not overlap the answer.
|
||
START_FILLER_ENABLED=true
|
||
# How long (ms) to wait for the first audio chunk of the reply before playing the
|
||
# start filler (default 1200). Measured from chunk ARRIVAL (not from playback
|
||
# start, which would also include the TTS_PREBUFFER_MS jitter buffer). Same idea
|
||
# as FILLER_DELAY_MS, but shorter. Raised above the default so the cue only fires
|
||
# when the first audio is genuinely late.
|
||
START_FILLER_GO_DELAY=1800
|
||
# Phrases spoken by the start filler, ONE PER LINE (a single line with "|"
|
||
# separators also works). One clip is synthesized per phrase and a RANDOM one is
|
||
# played when the reply is late, so the cue varies turn to turn. The bank follows
|
||
# this list: editing it triggers a full re-synthesis at the next restart (no
|
||
# manual deletion needed), phrases dropped here are deleted on disk, and a
|
||
# failed synthesis keeps the previous clip. An unchanged list is skipped, so
|
||
# startup stays fast. Drama-3 style tags ([calm], [sigh], ...) are passed
|
||
# through to the TTS.
|
||
START_FILLER_TEXTS="[calm] Un momento...
|
||
[thoughtful] A ver, veamos...
|
||
[hesitant] Entonces... echemos un vistazo...
|
||
[sigh] Vale, ya voy..."
|
||
# Directory holding the start-filler clips (kept separate from FILLERS_DIR so
|
||
# they are not added to the round-robin filler bank).
|
||
START_FILLER_DIR=assets/start_filler
|
||
# TTS jitter buffer (ms): how much synthesized audio the mixer buffers before it
|
||
# starts playing the reply. Lower values start speaking sooner; higher values
|
||
# absorb uneven TTS chunk arrival at the cost of added latency. Drama-3's vocoder
|
||
# emits audio in windows of codec frames (2048 samples = 46.4 ms each); with the
|
||
# recommended server setting (stride 4, followup 8, overlap 16, crossfade 512)
|
||
# the first window is 4 frames = 185.8 ms and the steady windows are 8 frames =
|
||
# 371.6 ms. The first arrives at the server TTFB (~0.38 s) and the second ~350 ms
|
||
# later, so ANY value in (186, 557] ms primes on the same second window (TTFA
|
||
# ~0.73 s); 550 ms takes the whole ~557 ms cushion for free. That cushion absorbs
|
||
# the drain when several TTS requests share one GPU and produce slower than real
|
||
# time (measured ~1.5x concurrent vs ~0.94x alone); without it every chunk seam
|
||
# starves the queue and pauses playback (a micro-gap at each sentence start).
|
||
# Below ~186 ms playback starts on the first window (TTFA ~= server TTFB) but
|
||
# leaves almost no cushion. Mid-reply underruns resume as soon as any audio is
|
||
# queued again (no refill).
|
||
TTS_PREBUFFER_MS=550
|
||
# Number of TTS requests kept in flight at once. Reply chunks are synthesized
|
||
# ahead of playback (and replayed in order) so each request's start-up latency
|
||
# hides behind the previous chunk's audio instead of leaving an audible gap.
|
||
# 1 restores strictly serial synthesis.
|
||
TTS_PIPELINE_DEPTH=3
|
||
|
||
# --- Backchannel (short acknowledgments while the user speaks) ---
|
||
# Master switch. When disabled, no during-utterance acknowledgment is played.
|
||
BACKCHANNEL_ENABLED=true
|
||
# Directory of the generated "*.raw" PCM clips (16-bit LE mono, 44.1 kHz). One
|
||
# clip is synthesized per phrase of BACKCHANNEL_TEXTS at startup.
|
||
BACKCHANNEL_DIR=assets/backchannel
|
||
# Phrases spoken by the backchannel, ONE PER LINE (a single line with "|"
|
||
# separators also works). One clip is synthesized per phrase and a RANDOM one is
|
||
# played, so the cue varies from turn to turn. The bank follows this list:
|
||
# editing it triggers a full re-synthesis at the next restart (no manual
|
||
# deletion needed), phrases dropped here are deleted on disk, and a failed
|
||
# synthesis keeps the previous clip. An unchanged list is skipped, so startup
|
||
# stays fast. Drama-3 style tags ([calm], ...) are passed through to the TTS.
|
||
#
|
||
# A line may start with an optional "category::" prefix (e.g. "agree::..."); the
|
||
# scheduler uses it to progress through the turn (an acknowledgment first,
|
||
# agreement later) and to avoid chaining two cues of the same kind. Lines
|
||
# without a prefix belong to the default "ack" category.
|
||
BACKCHANNEL_TEXTS="ack::[calm] Mm-hmm...
|
||
ack::[attentive] Ajá...
|
||
agree::[agreeing] Claro...
|
||
ack::[listening] Ya veo..."
|
||
# Mix gain applied to the backchannel channel (0..1). Kept low so the cue stays
|
||
# in the background.
|
||
BACKCHANNEL_GAIN=0.55
|
||
# Confirmed speech required (ms) before the first cue may be considered.
|
||
BACKCHANNEL_MIN_SPEECH_MS=1500
|
||
# Base spacing (ms) between two cues of the same turn; randomized by ±JITTER/2.
|
||
BACKCHANNEL_INTERVAL_MS=3000
|
||
# Timing jitter (ms): delays the first cue by 0..JITTER after MIN_SPEECH_MS and
|
||
# spreads the spacing, so cues don't land on a fixed cadence.
|
||
BACKCHANNEL_JITTER_MS=1200
|
||
# Micro-pause window (ms): the cue lands only while the user is in a short
|
||
# pause. The reaction DEPTH is drawn at random inside [MIN, MAX] per pause, so
|
||
# cues don't all land at exactly MIN. Below MIN the user is likely still
|
||
# mid-word; above MAX (capped below VAD_MIN_SILENCE_MS) the utterance is about
|
||
# to close.
|
||
BACKCHANNEL_PAUSE_MIN_MS=96
|
||
BACKCHANNEL_PAUSE_MAX_MS=260
|
||
# Probability (0..1) of cueing when a qualifying micro-pause occurs.
|
||
BACKCHANNEL_PROB=0.65
|
||
# Random per-cue volume scaling (± fraction) so cues don't sit at a fixed level.
|
||
BACKCHANNEL_VOLUME_JITTER=0.2
|
||
# Maximum cues emitted per user turn.
|
||
BACKCHANNEL_MAX_PER_TURN=3
|
||
|
||
# --- Multilingual clip banks (backchannel + filler + start filler) ---
|
||
# Every clip bank lives in a per-language subdirectory `<dir>/<lang>` named after
|
||
# the ISO 639-1 code (<dir>/<lang>/, e.g. assets/backchannel/es/), including the
|
||
# default language LANG_DEFAULT. When the user is detected speaking a different
|
||
# language, that language's banks are generated on demand into their own
|
||
# subdirectory and the session switches to them. Each language has its own
|
||
# directory, so regenerating one never removes the clips of another.
|
||
LANG_DEFAULT=es
|
||
# Detect the user's language with a dedicated LLM call (a short auxiliary
|
||
# request that returns only the ISO code) and switch the banks when it differs
|
||
# from the active one. Requires the LLM (LLM_ENABLED=true).
|
||
LANG_DETECT_ENABLED=true
|
||
# Minimum transcript length (chars) before a sample is sent to the LLM.
|
||
LANG_MIN_CHARS=12
|
||
# The first user turn is always checked; afterwards the language is re-checked
|
||
# once every N turns, so a mid-conversation switch is picked up without paying
|
||
# for a detection call on every turn.
|
||
LANG_REDETECT_EVERY=5
|
||
# Built-in phrase tables cover es/en/it/fr/de/pt. For any other language the
|
||
# phrase list is generated by the LLM; set false to keep only the tables.
|
||
LANG_LLM_FALLBACK_ENABLED=true
|
||
|
||
# --- Per-session audit log ---
|
||
# Writes one structured JSONL file per WebSocket session: a configuration
|
||
# snapshot at the start, every inbound control message and outbound event, the
|
||
# cue emissions, the language switches, the barge-ins / interrupts and an
|
||
# event-count summary at the end. Useful for post-mortem analysis and for
|
||
# tuning latency knobs (FILLER_DELAY_MS, START_FILLER_GO_DELAY, ...). One
|
||
# <id>.jsonl file per session, written under SESSION_AUDIT_DIR.
|
||
SESSION_AUDIT_ENABLED=true
|
||
SESSION_AUDIT_DIR=logs/sessions
|
||
|
||
# --- External audio source (URL / radio / YouTube) ---
|
||
# Injects an audio stream — a direct media URL, an Icecast/Shoutcast radio or a
|
||
# YouTube link — into the mixer's own "external" channel, mixed under the
|
||
# agent's voice. Demonstrated in the /mivocero client (URL box + Play/Stop +
|
||
# volume slider). Decoding is delegated to external tools:
|
||
# * ffmpeg — decodes direct URLs, radio and HLS streams (required);
|
||
# * yt-dlp — resolves YouTube URLs to a decodable stream (YouTube only).
|
||
# A barge-in (interrupt) stops the agent but leaves this background source
|
||
# playing; the external channel does not count as the agent speaking.
|
||
# Gain applied to the external channel (kept below 1 so it sits under the voice).
|
||
EXTERNAL_AUDIO_GAIN=0.6
|
||
# Override the decoder binaries if they are not on PATH.
|
||
MEDIA_FFMPEG_BIN=ffmpeg
|
||
MEDIA_YTDLP_BIN=yt-dlp
|
||
# Extra yt-dlp arguments (space-separated), e.g. to pass cookies or a JS
|
||
# runtime: "--cookies /path/cookies.txt". Leave empty normally.
|
||
MEDIA_YTDLP_ARGS=
|
||
|
||
# --- LLM reasoning (OpenAI-compatible chat-completions endpoint) ---
|
||
# When enabled, the user transcript is sent to the model and the reply is
|
||
# spoken by the TTS (sentence by sentence while streaming). When disabled the
|
||
# pipeline is a plain STT → TTS echo. The `/mivocero` client shows both the
|
||
# user transcript and the assistant reply in its chat box.
|
||
# Enabled automatically when both LLM_BASE_URL and LLM_MODEL are set.
|
||
LLM_ENABLED=true
|
||
# Base URL including the API version, e.g. a local vLLM / SGLang server.
|
||
LLM_BASE_URL=http://10.8.0.4:8000/v1
|
||
# Sent as `Authorization: Bearer <key>`. Local servers accept any value.
|
||
LLM_API_KEY=EMPTY
|
||
LLM_MODEL=Qwen/Qwen3.6-35B-A3B
|
||
LLM_MAX_TOKENS=1024
|
||
LLM_TEMPERATURE=0.7
|
||
# Stream tokens as they are generated (true → lower time-to-first-audio) or
|
||
# wait for the complete reply before speaking (false).
|
||
LLM_STREAM=true
|
||
# Rolling conversation window kept in memory (messages, system prompt excluded).
|
||
LLM_HISTORY_MESSAGES=16
|
||
# --- Chain-of-thought control (three independent mechanisms) ---
|
||
# Some reasoning models narrate their thinking instead of answering directly:
|
||
# Qwen3 on vLLM/SGLang writes it to a separate `reasoning_content` field (or
|
||
# inline think blocks) when a reasoning parser is enabled, but with no parser
|
||
# at all it leaks straight into `content` and would be read aloud by the TTS.
|
||
# 1) Server-side thinking mode. `false` sends
|
||
# `chat_template_kwargs: {"enable_thinking": false}` (vLLM/SGLang, Qwen3).
|
||
# Leave unset/omitted to not send the vendor-specific field at all.
|
||
LLM_ENABLE_THINKING=false
|
||
# 2) Drop the reasoning from the reply before it reaches the chat box / TTS:
|
||
# ignores `reasoning_content` deltas and removes inline think blocks (also
|
||
# stable when a marker is split across streamed deltas).
|
||
LLM_STRIP_REASONING=true
|
||
# 3) Append an explicit "answer directly, never narrate your reasoning"
|
||
# directive to the system prompt (see `DIRECT_ANSWER_DIRECTIVE`).
|
||
LLM_DIRECT_ANSWERS=true
|
||
# Streaming chunking: characters the reply must hold before the FIRST chunk is
|
||
# handed to the TTS. Small by default so the agent starts speaking on the first
|
||
# few words instead of waiting for a whole sentence. `0` disables the early
|
||
# word-boundary flush (chunks then end on sentence punctuation only).
|
||
LLM_STREAM_FIRST_CHUNK_CHARS=20
|
||
# Same threshold for every chunk after the first. Larger chunks give better
|
||
# prosody and fewer TTS requests: each chunk is a separate request with its own
|
||
# start-up and its own audio seam, so a larger value cuts the number of seams
|
||
# (where the TTS queue can starve) per reply.
|
||
LLM_STREAM_CHUNK_CHARS=240
|
||
# Optional system prompt override. Leave unset for the built-in voice-assistant
|
||
# prompt (see `DEFAULT_LLM_SYSTEM_PROMPT` in src/config.rs).
|
||
#LLM_SYSTEM_PROMPT=
|
||
|
||
# --- Tool calling (configurable HTTP tools) ---
|
||
# Lets the model call external HTTP services (weather, home automation, a
|
||
# webhook, …) before answering. The deployment's vLLM server runs without a
|
||
# tool parser, so it returns the call as text in the Qwen native
|
||
# `<tool_call>` format: the engine parses it client-side, executes the HTTP
|
||
# request and hands the result back to the model (see src/tools.rs). The
|
||
# tool-call block is never spoken nor shown in the chat box.
|
||
# Runtime override without a restart: POST /api/tools with the same JSON array
|
||
# (GET /api/tools reports the active set and its `source`). The override lives
|
||
# in memory only — if the endpoint is never called, the LLM_TOOLS_FILE set below
|
||
# is used, and a restart restores it. See TOOL_CALLING.md.
|
||
# Enabled automatically when LLM_TOOLS_FILE points to a valid tool file.
|
||
LLM_TOOLS_ENABLED=false
|
||
# JSON file with the tool definitions (name, description, JSON schema, url,
|
||
# method, headers). See tools.example.json.
|
||
LLM_TOOLS_FILE=
|
||
# Maximum tool-call rounds per turn (call → execute → re-prompt). Bounds
|
||
# runaway loops; the first round is the model's initial reply.
|
||
LLM_TOOL_MAX_ITERATIONS=4
|
||
# Timeout of a single tool HTTP request, in seconds.
|
||
LLM_TOOL_TIMEOUT_SECS=15
|
||
# Tool responses are truncated to this many characters before being handed
|
||
# back to the model (keeps the context small).
|
||
LLM_TOOL_RESULT_MAX_CHARS=2000
|
||
|
||
# --- HTTPS (optional) ---
|
||
# Set BOTH to serve HTTPS/WSS instead of plain HTTP. A secure context is
|
||
# required by browsers to grant microphone access (getUserMedia) when the page
|
||
# is not opened on localhost. Generate a self-signed pair with:
|
||
# ./server.sh cert
|
||
# Then open https://localhost:8787/ and accept the certificate warning.
|
||
TLS_CERT=certs/cert.pem
|
||
TLS_KEY=certs/key.pem
|