/
/
/
Ansible role that deployes services on my runner machine
1---
2# Runner Services - D&D Session Agent (The Thaumaturgeon)
3#
4# One compose project covering speech-to-text, the live agent and the
5# post-session processor, so `dnd_enabled: false` removes the whole system.
6#
7# The three services have different lifecycles and are handled accordingly:
8# dnd-stt runs continuously, dnd-agent is started per session under the
9# "session" profile, and dnd-post runs to completion under "post".
10
11- name: Create D&D configuration directory structure
12 file:
13 path: "{{ item }}"
14 state: directory
15 owner: "{{ runner_user }}"
16 group: "{{ runner_group }}"
17 mode: '2775'
18 loop:
19 - "{{ dnd_config_dir }}"
20 - "{{ dnd_stt_model_dir }}"
21 # huggingface_hub.scan_cache_dir() raises CacheNotFound if hub/ is
22 # missing, which makes Speaches' /v1/models return 500 on a fresh deploy.
23 - "{{ dnd_stt_model_dir }}/hub"
24 - "{{ dnd_model_dir }}"
25
26- name: Create D&D environment files
27 template:
28 src: "{{ item.src }}"
29 dest: "{{ dnd_config_dir }}/{{ item.dest }}"
30 owner: "{{ runner_user }}"
31 group: "{{ runner_group }}"
32 mode: '0660'
33 loop:
34 - { src: "dnd-stt-env.j2", dest: "stt.env" }
35 - { src: "dnd-agent.env.j2", dest: "agent.env" }
36 - { src: "dnd-post.env.j2", dest: "post.env" }
37 loop_control:
38 label: "{{ item.dest }}"
39 notify: restart dnd
40
41- name: Create D&D Docker Compose file
42 template:
43 src: dnd-compose.yml.j2
44 dest: "{{ dnd_config_dir }}/docker-compose.yml"
45 owner: "{{ runner_user }}"
46 group: "{{ runner_group }}"
47 mode: '0664'
48 notify: restart dnd
49
50# --- Images ------------------------------------------------------------------
51# Everything is pulled from Harbor. Built and pushed from the dnd-agent repo
52# with tools/build-push.sh; the table server image is also deployed to the Pi,
53# so one build serves both hosts.
54
55- name: Create the table media directory
56 file:
57 path: "{{ dnd_table_media_dir }}"
58 state: directory
59 owner: "{{ runner_user }}"
60 group: "{{ runner_group }}"
61 mode: '2775'
62 when: dnd_table_enabled
63
64- name: Create the table media subdirectories
65 file:
66 path: "{{ dnd_table_media_dir }}/{{ item }}"
67 state: directory
68 owner: "{{ runner_user }}"
69 group: "{{ runner_group }}"
70 mode: '2775'
71 loop: [maps, handouts]
72 when: dnd_table_enabled
73
74# The runner already trusts the Harbor CA -- it hosts Harbor -- but Ansible
75# runs as root, which has its own docker credential store.
76- name: Log in to Harbor
77 community.docker.docker_login:
78 registry_url: "{{ dnd_registry }}"
79 username: "{{ dnd_registry_username }}"
80 password: "{{ dnd_registry_password }}"
81 reauthorize: true
82 no_log: true
83
84- name: Pull the agent and post-processing images
85 community.docker.docker_image:
86 name: "{{ item }}"
87 source: pull
88 force_source: true
89 loop:
90 - "{{ dnd_agent_image }}"
91 - "{{ dnd_post_image }}"
92
93- name: Pull the table server image
94 community.docker.docker_image:
95 name: "{{ dnd_table_runner_image }}"
96 source: pull
97 force_source: true
98 when: dnd_table_enabled
99
100# Only dnd-stt comes up here. The agent belongs to a session and the post
101# processor to a finished one, so both sit behind compose profiles.
102- name: Start D&D speech-to-text service
103 community.docker.docker_compose_v2:
104 project_src: "{{ dnd_config_dir }}"
105 services: [dnd-stt]
106 state: present
107 register: dnd_start_result
108 check_mode: no
109
110- name: Start the D&D table server
111 community.docker.docker_compose_v2:
112 project_src: "{{ dnd_config_dir }}"
113 services: [dnd-table-runner]
114 state: present
115 when: dnd_table_enabled
116 check_mode: no
117
118- name: Wait for dnd-stt to be healthy
119 uri:
120 url: "http://localhost:{{ dnd_stt_host_port }}/health"
121 method: GET
122 status_code: 200
123 register: dnd_stt_health
124 until: dnd_stt_health.status == 200
125 retries: 60
126 delay: 10
127 when: dnd_start_result is changed
128 check_mode: no
129
130# PRELOAD_MODELS is accepted by Speaches but does not fetch the weights in
131# 0.8.3, so the first transcription would otherwise pay a multi-minute
132# download mid-session.
133- name: Check whether the Whisper model is cached
134 ansible.builtin.stat:
135 path: "{{ dnd_stt_model_dir }}/hub/models--{{ dnd_stt_model | replace('/', '--') }}"
136 register: dnd_stt_model_cached
137
138- name: Download the Whisper model
139 ansible.builtin.uri:
140 url: "http://localhost:{{ dnd_stt_host_port }}/v1/models/{{ dnd_stt_model }}"
141 method: POST
142 status_code: [200, 201]
143 timeout: "{{ dnd_stt_model_download_timeout }}"
144 register: dnd_stt_model_pull
145 when: not dnd_stt_model_cached.stat.exists
146 # The endpoint streams progress and can outlive the HTTP timeout while the
147 # download continues server-side; the wait below decides success.
148 failed_when: false
149 check_mode: no
150
151- name: Wait for the Whisper model to finish downloading
152 ansible.builtin.stat:
153 path: "{{ dnd_stt_model_dir }}/hub/models--{{ dnd_stt_model | replace('/', '--') }}"
154 register: dnd_stt_model_ready
155 until: dnd_stt_model_ready.stat.exists
156 retries: 60
157 delay: 15
158 when: not dnd_stt_model_cached.stat.exists
159
160- name: Display D&D deployment summary
161 debug:
162 msg: |
163 D&D Session Agent (The Thaumaturgeon):
164 - Status: {{ 'Started' if dnd_start_result is changed else 'Already running' }}
165 - Config: {{ dnd_config_dir }}
166 - Media (NFS): {{ dnd_media_dir }}
167 - Images: {{ dnd_registry }}/{{ dnd_registry_project }} @ {{ dnd_image_tag }}
168
169 Speech to text (always on):
170 - Endpoint: http://{{ ansible_default_ipv4.address }}:{{ dnd_stt_host_port }}/v1
171 - Model: {{ dnd_stt_model }} ({{ dnd_stt_compute_type }})
172
173 Start a session:
174 cd {{ dnd_config_dir }} && docker compose --profile session up -d dnd-agent
175 cd {{ dnd_config_dir }} && docker compose logs -f dnd-agent
176
177 Stop the session (flushes transcript and compresses recordings):
178 cd {{ dnd_config_dir }} && docker compose stop dnd-agent
179
180 Process a finished session:
181 cd {{ dnd_config_dir }} && docker compose run --rm dnd-post \
182 dnd-refine /media/dnd/transcripts/<session>.jsonl \
183 --roster /media/dnd/campaign/roster.yml \
184 --table-audio /media/dnd/recordings/<session>/TV.flac
185 cd {{ dnd_config_dir }} && docker compose run --rm dnd-post \
186 dnd-chronicle /media/dnd/transcripts/<session>-refined.jsonl \
187 --campaign /media/dnd/campaign
188