/
/
/
Ansible role that deployes services on my runner machine
1# ==============================================================================
2# THE THAUMATURGEON - live session agent
3# ==============================================================================
4
5LIVEKIT_URL={{ livekit_public_ws_url }}
6LIVEKIT_API_KEY={{ vault_runner.livekit_api_key }}
7LIVEKIT_API_SECRET={{ vault_runner.livekit_api_secret }}
8
9# LIVEKIT_ROOM is deliberately absent. It is per-session -- <campaign>-<nnn> --
10# and the Chamberlain injects it when it starts this container.
11#
12# A value here would be a fixed room shared by every session of every campaign.
13# It must also NOT be added back: the compose file declares LIVEKIT_ROOM as an
14# environment passthrough, and a declared variable takes precedence over
15# env_file -- so the two would fight, with the outcome depending on whether the
16# Chamberlain or a hand-run compose started the container.
17
18# Transcription over the container network; dnd-stt is on runner-network.
19DND_STT_URL=http://dnd-stt:8000/v1
20
21# The judge's endpoint. **By the host address, not the container name.**
22#
23# `LLMConfig` defaults to `http://litellm:4000/v1`, which resolves only from
24# inside `llm-stack-net`. The agent is on `runner-network`, so that default
25# fails with `[Errno -3] Temporary failure in name resolution` on every judge
26# call -- and the judge degrades quietly by design, so the session runs on
27# with transcription and effect cues working and **no scene judgement at
28# all**. Measured 2026-08-30: exactly that, unnoticed until the log was read.
29#
30# `dnd-post.env.j2` already points at the host for the same reason; this uses
31# the same two facts so the two cannot drift.
32DND_LLM_BASE_URL=http://{{ ansible_default_ipv4.address }}:{{ llm_stack_litellm_port }}/v1
33DND_LLM_API_KEY={{ vault_runner.lite_llm_master_key }}
34
35# Mix the session's own audio into what the table hears.
36#
37# **For a REPLAYED run, never a live table.** In a real evening the room
38# already hears itself, so this would be feedback rather than monitoring. It
39# exists so a test-feed run can be listened to, which is the only way to judge
40# whether a cue lands on the beat -- the timeline records that a cue fired 3 s
41# after an utterance began, and utterances run 10-30 s.
42DND_MONITOR_SESSION={{ dnd_monitor_session | lower }}
43DND_MONITOR_GAIN={{ dnd_monitor_gain }}
44
45# Publish the atmosphere mix into the LiveKit room, so listeners who are NOT
46# in the room -- the DM on his own client, a player dialling in -- hear the
47# ambience under the voices instead of voices alone.
48#
49# The room itself already hears it through the receiver at the table, so any
50# client that IS in the room must not play it twice; the Google TV app
51# unsubscribes from this track on sight.
52#
53# Only published when acting: observe mode plays nothing, so a scored run has
54# nothing to publish. `_on_track` also refuses to transcribe the agent's own
55# identity, so a published track can never reach the transcript or the judge.
56DND_PUBLISH_ATMOSPHERE={{ dnd_publish_atmosphere | lower }}
57
58# Shown in place of a video tile - the agent publishes no camera.
59DND_AVATAR_URL={{ dnd_avatar_url }}
60
61# Paths inside the container; /media/dnd is the NFS mount from storage.
62DND_TRANSCRIPT_DIR=/media/dnd/transcripts
63DND_RECORDING_DIR=/media/dnd/recordings
64
65# Holds cues.yml (the ambience and effect library) and atmosphere.yml (its
66# tuning). Without them the atmosphere layer stays off and the agent only
67# transcribes, which is what a node with no library should do.
68DND_MEDIA_ROOT=/media/dnd
69
70# The node at the table. It runs `dnd-atmosphere`, which takes its sound card
71# for the length of a session and hands it back to snapcast afterwards.
72#
73# Set here rather than as a passthrough on dnd-agent: this is fixed
74# infrastructure, not per-session state. Declaring it in both places would be
75# actively harmful -- compose treats a named-but-unset variable as an empty
76# override of the env file, so the agent would end up with no audio host and
77# no obvious reason why.
78DND_AUDIO_HOST={{ dnd_audio_host }}
79DND_AUDIO_PORT={{ dnd_audio_port }}
80
81# observe: detectors run and the timeline is written, nothing plays.
82# live: cues actually fire.
83# Overrides atmosphere.yml, so the mode can be changed without editing the
84# share.
85#
86# This governs the LIGHTS as well as the sound. A room that quietly starts
87# flickering during a session nobody meant to be live is exactly the surprise
88# observe mode exists to prevent, so the driver checks it before touching a
89# device and records what it would have done either way.
90DND_ATMOSPHERE_MODE={{ dnd_atmosphere_mode }}
91
92# The broker, for mirroring in what the DM does: the projector waking or
93# sleeping, and a battlemap going up. Both are the strongest scene signals the
94# system gets, because a human did them deliberately.
95#
96# Subscribe only -- the agent never writes a state topic. It is a client of the
97# contract in docs/mqtt-interface.md exactly as the DM page is.
98#
99# The projector one is load-bearing for the lights: the main light is capped
100# while a map is up. Without a broker the agent assumes the projector is ON,
101# which is the safe direction -- capping a dark room costs a little brightness,
102# while failing to cap a lit one washes out the map everyone is looking at.
103DND_MQTT_HOST={{ mqtt_host }}
104DND_MQTT_PORT={{ mqtt_port }}
105DND_MQTT_USERNAME={{ mqtt_username }}
106DND_MQTT_PASSWORD={{ mqtt_password }}
107
108# Home Assistant. Was for lighting scenes; the lights are now driven directly
109# over each Shelly's local HTTP API instead, from lights.yml on the share.
110# Kept because the token is still the way to reach HA for anything else.
111HA_URL={{ vault_runner.homeassistant_url }}
112HA_TOKEN={{ vault_runner.homeassistant_token }}
113