Configuration Reference
Where a value comes from#
Three sources, merged at startup:
| Field type | Priority, highest first |
|---|---|
secrets (*_key, orpheus_endpoint, skill tokens) | CLI arg → environment variable → config.json → None |
| everything else | CLI arg → config.json → dataclass default |
Secrets are the important row. The environment always wins: if the variable
is set and non-empty, the config.json value is skipped entirely. A value in
config.json is only ever a fallback for a variable that is not set.
Secrets never reach config.json: save_to_file() strips every key in
SECRET_KEYS and every nested skill token before writing, and GET /config
returns them dropped or masked as ********. Posting the mask back is ignored,
so editing an unrelated field in the dashboard cannot overwrite a real token
with asterisks.
A secret typed into the dashboard is written to .env instead, under the
variable named in SECRET_ENV_VARS — which is the same file and the same
variable the wizard writes, and the one the engine reads back at startup. The
response names what it wrote in secrets_written_to_env. Emptying the field
clears the variable; an unwritable .env fails the whole save rather than
leaving a key that works until the next restart.
config.json is gitignored. Copy config.example.json and edit that.
config.json — full reference#
This is config.example.json verbatim; it matches the dataclass defaults in
src/core/config.py field for field.
{
"config_version": 1,
"language": "auto",
"soul_path": "data/prompts/soul.md",
"system_prompt_path": "data/prompts/chat.md",
"operating_prompt_path": "data/prompts/operating.md",
"llm_provider": "openrouter",
"openrouter_model": "deepseek/deepseek-v4-flash",
"openai_model": "gpt-5",
"groq_model": "openai/gpt-oss-20b",
"google_model": "gemini-3.8-flash",
"claude_model": "claude-sonnet-5",
"openai_compat_base_url": "",
"openai_compat_model": "",
"openai_compat_api": "chat",
"anthropic_compat_base_url": "",
"anthropic_compat_model": "",
"local_base_url": "http://localhost:11434/v1",
"local_model": "qwen3:8b",
"obs_text_source": "AIText",
"obs_avatar_source": "BeaPNG",
"obs_source_type": "image",
"obs_host": "localhost",
"obs_port": 4455,
"obs_password": "",
"audio_device": "",
"audio_buffer_ms": 100,
"audio_idle_close_s": 30.0,
"audio_latency_s": 0.0,
"tts_provider": "edge",
"tts_voice": "it-IT-IsabellaNeural",
"tts_pitch": "+13Hz",
"tts_rate": "+12%",
"tts_volume": "+33%",
"orpheus_endpoint": "",
"orpheus_voice": "zoe",
"tts_compat_base_url": "",
"tts_compat_model": "tts-1",
"tts_compat_voice": "alloy",
"tts_compat_speed": 1.0,
"kokoro_model": "kokoro-v0_19.onnx",
"kokoro_voices_file": "voices.json",
"kokoro_voice": "af_bella",
"kokoro_speed": 1,
"kokoro_lang": "en-us",
"avatar_map": {
"neutral": {
"idle": "",
"talking": ""
},
"happy": {
"idle": "",
"talking": ""
},
"sad": {
"idle": "",
"talking": ""
},
"angry": {
"idle": "",
"talking": ""
},
"surprised": {
"idle": "",
"talking": ""
},
"disgusted": {
"idle": "",
"talking": ""
},
"bored": {
"idle": "",
"talking": ""
}
},
"png_dir": "data/pngs",
"text_line_width": 40,
"text_lines": 4,
"text_font_size": 75,
"text_min_font_size": 55,
"text_font_step": 2,
"typing_delay": 0.03,
"text_min_duration": 2,
"updates": {
"check": true,
"allow_web_apply": true
},
"skills": {
"monologue": {
"enabled": false,
"prompt_path": "data/prompts/monologue.md"
},
"memory": {
"enabled": true,
"db_path": "data/bea.db",
"embedding_model": "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
"embedding_cache_dir": "data/embeddings_cache",
"min_similarity": 0.35
},
"social_memory": {
"enabled": true
},
"dream": {
"enabled": true,
"hour": 4
},
"minecraft": {
"enabled": false,
"server_url": "ws://127.0.0.1:8080",
"idle_nudge_seconds": 30,
"commentary_seconds": 20,
"build_scripts": true,
"system_prompt_path": "data/prompts/minecraft.md"
},
"discord": {
"enabled": false,
"api_port": 3030,
"brain_api_url": "http://127.0.0.1:8000",
"admin_id": "",
"duck_threshold_ms": 400,
"interrupt_threshold_ms": 4000,
"fill_silences": true,
"silence_seconds": 6.0,
"silence_jitter_seconds": 2.0,
"silence_min_gap_seconds": 25.0,
"unprompted_per_minute": 1
},
"telegram": {
"enabled": false,
"owner_id": "",
"allowed_chats": []
},
"twitch": {
"enabled": false,
"channel": "",
"nick": ""
},
"donations": {
"enabled": false
},
"web": {
"enabled": false,
"search_provider": "auto",
"searxng_url": "",
"safesearch": "moderate",
"max_results": 5,
"max_chars": 6000,
"long_pages": "passages",
"wait_seconds": 3.0
}
},
"stt_provider": "groq",
"stt_model": "whisper-large-v3-turbo",
"stt_language": "auto",
"stt_compat_base_url": "",
"faster_whisper_device": "auto",
"faster_whisper_compute_type": "auto",
"faster_whisper_download_root": "data/models/whisper",
"faster_whisper_vad": true,
"consciousness": {
"idle_after": 240.0,
"window": 0.3,
"text_window": 1.2,
"max_window": 8.0,
"burst_steps": 6,
"correlation_timeout": 90.0,
"context_max_tokens": 150000,
"handoff_trigger_tokens": 0,
"hot_tokens": 0,
"context_handoff": true,
"window_persist_after_turn": true,
"dynamic_context_timeout": 5.0
},
"models": {
"mind": [
"openrouter:deepseek/deepseek-v4-flash",
"groq:openai/gpt-oss-120b"
],
"background": [
"openrouter:google/gemma-4-31b-it:free",
"groq:openai/gpt-oss-20b"
]
},
"attention": {
"enabled": true,
"cooldown_seconds": 20,
"voice_cooldown_seconds": 5,
"quiet_hours": [
3,
9
],
"trigger_words": [],
"hot_names": [],
"self_ids": [],
"followup_enabled": true,
"followup_window_seconds": 180,
"followup_max_turns": 3,
"followup_max_interposed": 3,
"followup_active_bonus": 5,
"followup_lookback": 30
},
"rhythm": {
"enabled": true,
"tick_seconds": 900,
"spontaneous_enabled": true,
"spontaneous_probability": 0.15,
"spontaneous_min_silence": 3600,
"spontaneous_min_activity": 3
},
"affect": {
"enabled": true,
"half_life_minutes": 25,
"person_half_life_hours": 60,
"memory_ttl_hours": 6
}
}
Core#
| Key | Default | Description |
|---|---|---|
config_version | 1 | Config schema version (src/core/config.py). Bump only on meaning change. See Languages |
language | "auto" | The language she uses when she speaks first. What she answers in is the language of the message she is answering — see Languages |
soul_path | data/prompts/soul.md | Who she is. Prepended to every context, never edited by the engine |
operating_prompt_path | data/prompts/operating.md | How she exists: the speak tool, the moods, what she notices |
system_prompt_path | data/prompts/chat.md | Deprecated. Only used if the operating manual is missing |
llm_provider | "openrouter" | Only used when models has no pool for a role. One of openrouter, openai, groq, google, claude, openai_compat, anthropic_compat, local |
openai_compat_base_url / anthropic_compat_base_url | "" | Your endpoint, ending in /v1. Empty means the provider cannot build and the pool skips it with a warning |
openai_compat_model / anthropic_compat_model | "" | The model id your endpoint serves — only you know it |
openai_compat_api | "chat" | chat or responses. Flip it when your endpoint speaks the Responses protocol |
local_base_url | "http://localhost:11434/v1" | Ollama's address. LM Studio answers at http://localhost:1234/v1 |
local_model | "qwen3:8b" | Any model the runner serves. Every model in mind must support tool calling |
models — the role pools#
"models": {
"mind": ["openrouter:deepseek/deepseek-v4-flash", "groq:openai/gpt-oss-120b"],
"background": ["openrouter:google/gemma-4-31b-it:free", "groq:openai/gpt-oss-20b"],
"minecraft": []
}
A spec is "provider:model", split on the first : so OpenRouter ids keep
their / and their :free suffix. Within a pool, calls round-robin to spread
rate limits and fall back down the list on failure.
| Role | Used by | Requirement |
|---|---|---|
mind | the consciousness | must support tool calling |
background | diary, dreamer, profiler | anything |
An empty pool falls back to llm_provider + <provider>_model, so a
pre-pool config keeps working. If a role ends up with no usable client the
engine refuses to start and says which key is missing.
consciousness#
The mind is always on: there is no off switch, because there is no other path that could answer while it is off.
| Key | Default | Description |
|---|---|---|
idle_after | 240.0 | Seconds of silence before an IDLE perception. Only applies while the monologue skill is on |
window | 0.3 | The quiet gap a live sense waits for one more of its own: a voice line, a game event. The batch closes when nothing new has arrived for this long |
text_window | 1.2 | The same for written messages. Longer because the pause between two typed lines is somebody still typing — at 0 she answers each line of a paragraph separately. It also holds between two steps of a turn, before a line that arrived mid-turn goes in |
max_window | 8.0 | Ceiling on one batch however busy it gets, so a chat that never stops cannot hold a turn open forever |
burst_steps | 6 | Max reasoning steps in one turn |
correlation_timeout | 90.0 | How long an HTTP caller waits for her reply before giving up |
context_max_tokens | 150000 | How much conversation she remembers, in tokens. Slider, 150k–500k, applies without a restart. The recap point and word-for-word present follow it; a bigger memory means a proportionally larger prompt on every turn |
handoff_trigger_tokens | 0 | Manual override: memory size that starts the recap. 0 follows the slider. Must stay above hot_tokens |
hot_tokens | 0 | Manual override: the newest tokens that survive a recap exactly as written, never summarized, however old they are. 0 follows the slider |
context_handoff | true | Off means the window only grows until the ceiling trims it |
window_persist_after_turn | true | Off means the window only reaches disk on shutdown |
dynamic_context_timeout | 5.0 | Seconds a turn waits for recall before answering without it |
attention#
What wakes the mind, and what she merely notices. How it works →
| Key | Default | Description |
|---|---|---|
enabled | true | Off means every perception costs a full reasoning cycle |
cooldown_seconds | 20 | She just spoke: let the room breathe. Being addressed bypasses it |
voice_cooldown_seconds | 5 | The same, in a live call — where twenty seconds reads as absence, not restraint |
quiet_hours | [3, 9] | She never interjects in this window. Being addressed still gets through |
trigger_words | [] | Her names; empty means derived from the persona name. Whole-word, one typo tolerated. Shared by every platform |
hot_names | [] | Other names that pull her into a conversation |
self_ids | [] | Her own platform ids, so a reply to her is recognised as addressed |
followup_enabled | true | Off, a reply to her is scored like everything else |
followup_window_seconds | 180 | How long after she spoke an answer still counts as an answer |
followup_max_turns | 3 | How long she keeps it up before waiting to be called again |
followup_max_interposed | 3 | Unrelated lines allowed between her line and the reply |
followup_active_bonus | 5 | Extra weight for a reply in an already lively conversation |
followup_lookback | 30 | Recent window turns the reply check may read |
rhythm#
A day rather than an event loop.
| Key | Default | Description |
|---|---|---|
enabled | true | Runs the slow clock at all |
tick_seconds | 900 | How often the spontaneous check runs |
spontaneous_enabled | true | Whether she may open a conversation herself |
spontaneous_probability | 0.15 | Even when eligible, usually she does not |
spontaneous_min_silence | 3600 | She spoke there recently: more is noise, not presence |
spontaneous_min_activity | 3 | Below this the room is dead and she would be talking to nobody |
affect#
Whether what happens to her sticks, and for how long. How it works →
| Key | Default | Description |
|---|---|---|
enabled | true | Off, every line starts from neutral and nothing carries over |
half_life_minutes | 25 | Minutes until a mood is half of what it was |
person_half_life_hours | 60 | Hours until how she stands with someone is half of what it was |
memory_ttl_hours | 6 | How long she can still say what put her in that mood |
Nothing here tells her how to behave when she feels something — that is the soul's job, and the soul is a file you write.
OBS#
| Key | Default | Description |
|---|---|---|
obs_host / obs_port | localhost / 4455 | obs-websocket 5.x |
obs_password | "" | Empty if authentication is disabled |
obs_timeout | 2.0 | Seconds one request may take. Past it the socket is closed and reopened, so an OBS that hangs is never waited on for longer. |
obs_avatar_source | "BeaPNG" | Source name for the avatar |
obs_source_type | "image" | image for PNG, media for MP4/GIF/WebM |
obs_text_source | "AIText" | Source name for the speech bubble |
If OBS is not running the connection fails with a warning and everything else continues. OBS module →
Audio and TTS#
| Key | Default | Description |
|---|---|---|
audio_device | "" | The output her voice plays on, by name (Settings → Voice lists them). Empty follows the system default. A name that is not plugged in falls back to the default until it is; a Windows name cut short by the MME api still matches |
audio_device_id | null | The old way: a position in the device list, used only when audio_device is empty. Positions move whenever a monitor or a headset is plugged in, so the doctor warns about it |
audio_buffer_ms | 100 | Settings → Voice → Advanced, like the two below. The most of her voice written ahead of the speaker. Less stops sooner when she is talked over (a barge-in is heard within this plus the device's latency). It is a ceiling: the stream's own buffer can be smaller (about 85 ms on MacBook speakers, about 300 ms on a bluetooth headset). Floor 20 |
audio_idle_close_s | 30 | Seconds of silence before the sound card is let go. 0 keeps it open |
audio_latency_s | 0 | The latency asked of PortAudio. 0 is its default, the quickest to the speaker. A larger value gives the stream a bigger buffer, for a machine busy enough that her voice breaks up, at the price of a later first sound: on MacBook speakers 0.1 measured 40 ms to the speaker instead of 5 |
tts_provider | "edge" | edge, kokoro, orpheus or openai_compat. Changing it needs a restart |
tts_voice / tts_pitch / tts_rate / tts_volume | en-US-AvaNeural, +5Hz, +10%, +33% | EdgeTTS |
orpheus_voice | "zoe" | Orpheus. Key and endpoint come from the environment |
tts_compat_base_url | "" | Any server speaking OpenAI's /audio/speech, up to and including /v1. Its key is TTS_COMPAT_API_KEY, its own and not the mind's |
tts_compat_model / tts_compat_voice / tts_compat_speed | tts-1, alloy, 1.0 | Whatever that server calls them. Every language is allowed: the voices are the server's |
kokoro_model / kokoro_voices_file | kokoro-v0_19.onnx, voices.json | Downloaded on first run if missing, each from the release asset of the same basename. The voice pack must be the .json form; a path ending in voices.bin is read as voices.json |
kokoro_voice / kokoro_speed | af_bella, 1.0 | Kokoro |
kokoro_lang | en-us | Derived from the chosen voice, then from language. Only read as a last fallback — see Languages |
STT#
| Key | Default | Description |
|---|---|---|
stt_provider | "groq" | faster_whisper, groq, openrouter or openai_compat. Anything else disables speech input |
stt_model | "whisper-large-v3-turbo" | Rewritten to openai/whisper-large-v3-turbo on OpenRouter, and to large-v3-turbo on faster_whisper |
stt_language | "auto" | The language the transcriber is pinned to, on every provider. auto detects per turn and misplaces short ones — see Languages |
stt_compat_base_url | "" | openai_compat: any server speaking OpenAI's /audio/transcriptions, up to and including /v1. The model is stt_model; the key is STT_COMPAT_API_KEY |
faster_whisper runs Whisper on this machine and needs no key. Its four extra
knobs are only read when it is the chosen provider:
| Key | Default | Description |
|---|---|---|
faster_whisper_device | "auto" | auto, cpu or cuda |
faster_whisper_compute_type | "auto" | Auto is int8 on a CPU, float16 on a GPU |
faster_whisper_download_root | "data/models/whisper" | Where the weights are cached. Gitignored |
faster_whisper_vad | true | Drops silence, which Whisper otherwise invents words for |
stage — how she appears#
Two independent choices, plus the settings each one needs.
| Key | Default | What it does |
|---|---|---|
avatar_backend | "png" | png, model or vtube_studio. An unknown name falls back to png with a warning. |
png_render | "obs" | Where the png avatar is drawn: obs swaps a file in an OBS source, stage draws it in the browser source with a mouth that follows her voice. |
caption_backend | "obs" | obs, stage (the browser source) or off. |
lipsync_fps | 30 | How many times a second her mouth is told what to do. |
The model backend
| Key | Default | What it does |
|---|---|---|
model_path | "" | The .vrm on this machine. Nothing ships with the repo; make model fetches a free one. |
models_dir | "data/models" | Where the dashboard's library lists, downloads and uploads models. A model_path outside it still works and shows as external. |
clips_dir | "data/clips" | Where .vrma and Mixamo .fbx clips live. They appear by name in the dashboard. |
shot | "bust" | bust, half or full. Framed off the head bone, so any model is framed alike. |
mood_clips | {} | mood → clip name. Optional; a mood without one just changes expression. |
idle_clip | "idle_loop" | The clip always playing under her. Empty, or a clip that fails to load, means a still pose with the arms down. Never offered to <do:…>. |
expression_intensity | 0.7 | How strongly a mood shows on her face, 0–1. Scales every emotion weight the engine sends; the proportions between them stay. |
face_blend_blink | true | A blink blends under an emotion instead of adding to it, so a smile whose eyes are already shut does not close them twice. Only emotions the model declares with overrideBlink: none are changed. |
mouth_under_emotion | 0.5 | How much of the lip sync survives under a full emotion, 0–1. VRoid emotion shapes already move the mouth, and the visemes add on top of them. |
state_clips | {} | state (idle, listening, thinking, talking, sleeping) → clip carrying her in that state instead of idle_clip. Changing state crossfades over 0.5 s. |
background | "" | A colour behind her, or empty for transparent. |
light_preset | "flat" | flat (one key light and an even ambient), soft (a warm key, a cool fill and sky light) or studio (key, fill, a light from behind and a rim on MToon materials). Applies live. |
The vtube_studio backend — nothing is bundled; it drives the VTube Studio you already run.
| Key | Default | What it does |
|---|---|---|
vts_host | "127.0.0.1" | Where VTube Studio is. |
vts_port | 8001 | Its plugin API port. |
vts_expressions | {} | mood → expression file in your model. The dashboard reads the list from the connected model. |
vts_clips | {} | mood → hotkey id or name, triggered when she starts talking. |
vts_mouth_param | "MouthOpen" | The parameter the lip sync writes to. |
vts_mouth_form_param | "" | A second mouth parameter for its shape, if the model has one. |
vts_life | false | Inject head, eyes and blinks every frame, the same life as the 3D body, for a model with no webcam tracking. Sends faceFound: true. |
vts_life_params | {} | Replaces the input parameters one signal writes to, e.g. {"pitch": ["FaceAngleY"]}. Signals: yaw, pitch, roll, eye_x, eye_y, blink; a leading - inverts. |
The token VTube Studio issues is not kept here. It lives in
data/vtube_studio_token.json, gitignored, because GET /config is
unauthenticated.
Full reference: Avatar module →
Avatar and the text bubble#
avatar_map holds one {idle, talking} pair per mood: neutral, happy,
sad, angry, surprised, disgusted, bored. Leave a path empty to fall
back to neutral. png_dir (data/pngs) is where they live.
| Key | Default | Description |
|---|---|---|
text_line_width | 40 | Characters per line before wrapping |
text_lines | 4 | Visible lines before paginating; in the browser source, the newest lines kept on screen |
text_font_size | 75 | Starting font size; in the browser source, pixels on a 1080 px tall frame, scaled with the frame |
text_min_font_size | 55 | Floor when long text is shrunk to fit |
text_font_step | 2 | Shrink step |
typing_delay | 0.03 | Seconds per character |
text_min_duration | 2.0 | Minimum seconds a page stays up |
Updates#
"updates": { "check": true, "allow_web_apply": true }
| Key | Default | Description |
|---|---|---|
check | true | Ask origin whether there is a newer commit. The only outbound call, and only to the remote this copy was cloned from. Off means the dashboard never asks and never shows the pill |
allow_web_apply | true | Whether POST /update/apply is open. Off leaves make update working and closes the button — the endpoint runs git pull, uv sync and npm install, so it is separately revocable |
Neither affects the CLI. Docker installs are refused regardless: the source tree is the image.
Skills#
Every block lives under skills.<key> and carries enabled. The dashboard is
the single source of truth — Bea can never arm a capability herself.
| Skill | Keys | Page |
|---|---|---|
memory | db_path, embedding_model, embedding_cache_dir, min_similarity | memory |
social_memory | enabled only | social |
dream | hour | dream |
monologue | prompt_path — the timer is consciousness.idle_after | monologue |
minecraft | server_url, idle_nudge_seconds, commentary_seconds, build_scripts, system_prompt_path | minecraft |
discord | api_port, brain_api_url, admin_id, duck_threshold_ms, interrupt_threshold_ms, fill_silences, silence_seconds, silence_jitter_seconds, silence_min_gap_seconds, unprompted_per_minute, token | discord |
telegram | owner_id, allowed_chats, group_salience, token | telegram |
twitch | channel, nick, oauth_token | twitch |
donations | secret | donations |
web | search_provider, searxng_url, safesearch, max_results, max_chars, long_pages, wait_seconds — the keys live in .env, never here | web |
Trigger words are not per-platform: they come from attention.trigger_words
everywhere.
Environment variables#
| Variable | Used for |
|---|---|
OPENROUTER_API_KEY / OPENAI_API_KEY / GROQ_API_KEY | LLM providers, and STT for Groq / OpenRouter |
GOOGLE_API_KEY | Google AI Studio (Gemini) |
ANTHROPIC_API_KEY | Claude, called directly |
OPENAI_COMPAT_API_KEY / OPENAI_COMPAT_BASE_URL | A custom OpenAI-protocol endpoint. The key is optional — many local servers want none |
ANTHROPIC_COMPAT_API_KEY / ANTHROPIC_COMPAT_BASE_URL | A custom Messages-protocol endpoint, same deal |
LOCAL_API_KEY / LOCAL_BASE_URL | Local models. No key and the Ollama address by default; point the URL at LM Studio to switch runners |
ORPHEUS_API_KEY / ORPHEUS_ENDPOINT | Orpheus TTS |
DISCORD_TOKEN | The Discord bot |
DISCORD_ADMIN_ID | Fallback for skills.discord.admin_id |
TELEGRAM_TOKEN | The Telegram bot |
TWITCH_OAUTH_TOKEN | Writing in Twitch chat. Reading needs nothing |
DONATION_SECRET | Shared secret on the donation webhook |
BRAVE_API_KEY | Optional. Brave Search, tried before the free search |
TAVILY_API_KEY | Optional. Tavily, tried before the free search |
HF_TOKEN | Optional, and no account is needed to run anything here — the whisper and embedding models are public. It buys a higher Hugging Face rate limit, which is what makes the first download crawl, or get refused, on a shared IP |
BEA_ALLOWED_ORIGINS | Extra CORS origins, comma-separated |
LOG_LEVEL | DEBUG for verbose output |
CLI arguments#
Every one is an override for this launch only; nothing is written to disk.
uv run bea --web --llm-provider openrouter --tts-provider kokoro --device-id 22
| Argument | Notes |
|---|---|
--web | Serve the dashboard instead of the terminal loop |
--host / --port | Default 127.0.0.1:8000. See below before changing the host |
--llm-provider | openrouter, openai, groq, google, claude, openai_compat, anthropic_compat, local. Only affects the legacy single-model path |
--openrouter-key / --openrouter-model | and the same pair for --openai-*, --groq-*, --google-* and --claude-* |
--openai-compat-key / --openai-compat-base-url / --openai-compat-model / --openai-compat-api | the custom OpenAI-protocol endpoint |
--anthropic-compat-key / --anthropic-compat-base-url / --anthropic-compat-model | the custom Messages-protocol endpoint |
--local-key / --local-base-url / --local-model | the models on this machine |
--stt-provider / --stt-model | faster_whisper, groq, openrouter or openai_compat |
--stt-compat-base-url / --stt-compat-key | the custom transcription endpoint |
--tts-provider / --tts-voice | edge, kokoro, orpheus, openai_compat |
--orpheus-key / --orpheus-endpoint / --orpheus-voice | |
--tts-compat-base-url / --tts-compat-key / --tts-compat-model / --tts-compat-voice | the custom speech endpoint |
--kokoro-file / --kokoro-voices | |
--obs-host / --obs-port / --obs-password | |
--obs-avatar-source / --obs-source-type / --obs-text-source | |
--device-id | Audio output device index |
--typing-delay | Seconds per character in the OBS bubble |
--system-file / --png-dir | Prompt file and avatar directory |
--tts-provider coquiis still accepted by the parser but has no implementation; it falls through to EdgeTTS.
Hot reload#
POST /config writes the file and then calls reload_configuration(), which
re-reads the soul and the operating manual, drops the model-registry cache, and
reloads the TTS, STT and OBS clients in place.
Two things still need a restart, and the dashboard says so when you save:
tts_provider and stt_provider — the object type changes.
Startup behaviour#
Skills start according to their saved enabled value, so a skill left on
connects again on the next launch.
load_from_file() renames the legacy key obs_image_source to
obs_avatar_source silently. Delete the old key from your config.json to
avoid ambiguity.
Network exposure. The API has no authentication, so the server binds to
127.0.0.1 unless --host says otherwise. Binding to 0.0.0.0 hands anyone on
the network the ability to read the config, drive the brain and post donations.
Put it behind something that authenticates, and set DONATION_SECRET.