OBS Module
Overview#
The OBS module connects to OBS Studio via WebSocket and controls two types of sources in real time:
- Avatar source — swaps the image/video file to reflect Bea's current mood and speaking state.
- Text source — animates text with a typewriter effect for the speech bubble overlay.
src/modules/obs/
└── obs_websocket.py OBSController implementing OBSInterface
Connection#
obs = OBSController(host="localhost", port=4455, password="...", source_name="BeaPNG")
obs.connect()
If OBS is not running, the connection fails gracefully with a warning. The rest of the engine continues normally without OBS output.
Source Types#
Set obs_source_type in config:
| Value | OBS Source Type | Suitable for |
|---|---|---|
"image" | Image Source | Static PNGs |
"media" | Media Source (ffmpeg) | MP4, GIF, WebM |
Image switch:
obs.set_image("data/pngs/angry/talking.png")
Media switch:
obs.set_media("data/pngs/angry/talking.mp4")
Typing Animation#
type_text() writes a message character-by-character into the OBS text source, paginating if the message exceeds the visible area.
Parameters:
| Parameter | Description |
|---|---|
text | The full message to type |
source_name | OBS text source name |
line_width | Characters per line before wrapping |
max_lines | Max visible lines |
base_font_size | Starting font size |
min_font_size | Minimum font size (shrinks for long text) |
font_step | Font size decrement step |
typing_delay | Seconds between each character |
min_page_duration | Minimum seconds a page stays visible |
speaking_rate | Characters per second used to estimate reading time per page (default: 12.0). Controls the post-typing wait so that longer pages stay visible longer. |
Returns the final font size used (stored by the brain to correctly clear the source afterward).
The typing task and the playback task run in parallel — both are asyncio tasks,
and Expression.interrupt() cancels them together on barge-in, keeping what was
left unsaid in the resume buffer.
Font Management#
OBS text sources have their font settings stored in OBS. The controller reads the current font settings the first time it types to a given source, caches them, and then applies font-size changes per page. This avoids resetting user-configured font family/style.
Avatar Swap Logic#
Expression (src/core/expression/voice.py) owns this — it is the single
output sink, so nothing else touches OBS:
_resolve_paths(mood)looks up(idle_path, talking_path)inpng_map.- Before speaking:
set_media(talking_path)(orset_image). - After speaking:
set_media(idle_path).
If the mood is unknown it falls back to "normal". set_mood_avatar() is also
used directly for states rather than speech — sleeping when she goes to
sleep, normal when she wakes.
clear_text()#
A convenience method on OBSController that sets the text source to an empty string, preserving the given font size:
obs.clear_text(source_name="AIText", font_size=75)
It is equivalent to set_text("", source_name, font_size=font_size).
Expression calls set_text("", ...) directly in most places; clear_text()
is a helper and the two are interchangeable.
Hot Reload#
reload_config() updates host/port/password. If any of those changed and a client was already connected, it disconnects and reconnects automatically.