Files
piperust/.env.example
T
2026-10-17 22:01:31 +01:00

342 lines
18 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
# 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