Repository navigation
Releases: wiringai/mod_earshot
Release list
v0.4.1
Fixed
pausenow pauses. It stops caller audio and DTMF going to the agent, holds queued
agent audio and suspends barge-in untilresume(mod_audio_stream semantics, which
controllers rely on for "pause until pickCall"). Before, it only closed the ready-gate: the
agent kept hearing the caller while "paused", and in the defaultready=firstframemode the
next agent frame reopened the gate.statusreportspaused.- Agent audio frames of any size are played whole. The native/Deepgram/Vapi binary path
(clamped at 8 KB: 256 ms of L16 at 16 kHz), the pipecat protobuf path and the base64 JSON
paths (16 KB) decoded into one fixed buffer and silently dropped the rest of a larger frame.
Large frames are now decoded in pieces. Regression test:test/test_proto_audio.c
(native L16/PCMU, pipecat protobuf, OpenAI base64 delta). mask on|offcovers every stream on the channel, including fan-out transcription or
supervisor streams, from the operator API and from the agent'smaskcommand, and a stream
started inside an open window starts masked before its media bug goes live. Before, it
masked only the stream it was addressed to, so a side stream kept receiving the card number.- Twilio marks are echoed at their position in the audio, after the audio queued before
each mark has played, in order. Before, every pending mark was echoed only once the whole
play buffer drained. Marks pinned to audio discarded by a flush/clear are still echoed at
once (Twilio reports cleared marks too). - The caller hears FreeSWITCH's own audio while the agent is silent. WRITE_REPLACE used to
replace every outbound frame with silence when no agent audio was queued, which muted the
controller's ringback (uuid_broadcast) and hold music until the agent's first word. The
frame is now left untouched unless agent audio is being played; for 300 ms after the last
agent frame the line stays silent, so gaps between chunks of one utterance do not let the
underlying audio blip through. A barge-in hands the line back at once. - Cross-thread control flags (
ready,paused,masking) are read and written with
atomics instead of plain loads and stores.
Changed
es_ws.hno longer calls itself a reviewed draft; the header describes the shared
service-thread pool that has been in place since 0.1.0.- Docs: new
docs/MODULE-VS-AGENT.mdexplaining what module-side VAD is (energy endpointing, not a
turn model), when to leave it off (agents with their own VAD), and why edge latency metrics differ
from agent-side metrics; README/FEATURES/WHY say "VAD endpointing" instead of "turn detection".
Binaries
Built by CI from this tag on Debian 12 (bookworm), amd64, against the SignalWire FreeSWITCH packages:
| File | FreeSWITCH |
|---|---|
mod-earshot_0.4.1_fs1.11_debian12_amd64.deb / .so |
1.11.x |
mod-earshot_0.4.1_fs1.10_debian12_amd64.deb / .so |
1.10.x |
Needs libwebsockets (apt install libwebsockets16 on bookworm). Checksums in SHA256SUMS. Other distributions: build from source, see the README quickstart.
v0.4.0
[0.4.0] — 2026-10-03
Added
- The
uuid_audio_streamcompat API now speaks the mod_audio_stream agent dialect, so
agents and controllers written for that module run unchanged behind Earshot:
{"type":"streamAudio",...}audio is played to the caller (L16 at the stated rate,
resampled to the channel rate when they differ);{"control":{"command":...}}
messages are surfaced as themod_audio_stream::jsonevent, withclearalso
acting as a barge-in;clear_playbackdrops queued agent audio; the metadata after
<rate>is sent to the agent as the first text frame; connect failures and closes
are mirrored asmod_audio_stream::error/mod_audio_stream::disconnect.
Changed
- README: removed the commercial support section.
mod_earshot 0.3.0
Security hardening of the agent control channel, a concurrency fix that keeps audio smooth under load, and a batch of reliability + robustness fixes.
⚠️ Security — control channel. If you use the opt-in control channel (commands=), upgrade: this release closes a command-injection → RCE class. Default deployments (nocommands=) were never exposed.
Security
- Control-channel argument validation (
es_cmdguard): rejects FreeSWITCH'sapp::argsexec form, theinlinedialplan,..path traversal, whitespace positional-injection, and exec-triggering variables; blocked attempts are recorded in theearshot::commandaudit. - Per-action
commands=allowlist (commands=play,hangup) — enabling one action no longer grantstransfer/setvar/record. setvaris fail-closed behind asetvars=<name,...>name allowlist (nothing settable by default).
Changed
- Control-channel commands run on a worker pool, not the WebSocket service thread — a blocking
uuid_transfer/bridge/record/broadcastno longer stalls audio for other calls sharing that thread. A call's commands keep their submission order.
Fixed
- Use-after-free racing the command result-echo against media-bug teardown (per-stream lock + fresh session relocation).
- WebSocket receive path now gates on
suppress_events, closing an orphan-teardown window where a late frame could touch a freed play buffer. - Negative reconnect jitter (unsigned modulo).
- Mark-queue leak when the 64-deep queue is full.
Added
- Start-option robustness —
ES_MAX_ARGV16 → 48 so the full documented option set parses (a list past the 13th option was silently mis-parsed), plus a token-limit warning. - Warnings on unrecognized start options (key only, never the value) and on unsupported
stereo/mixedaudio_streammix-types. - Memory & capacity guidance in
docs/ARCHITECTURE.md(per-stream play buffer ≤ 2 MB, per-session greeting PCM ≤ ~1.4 MB).
Build / CI
- Module now compiles against real FreeSWITCH on same-repo PRs; the
.deb/module build job fixed; malformed-input fuzzing fores_proto_on_text.
Artifacts: mod_earshot.so + mod-earshot_0.3.0_amd64.deb for FreeSWITCH 1.10 and 1.11.
Full details in CHANGELOG.md.
v0.2.0 — the adapter release
Every protocol adapter is now live-validated against its real vendor — plus a barge-in
policy engine, caller-context + welcome-greeting, and box-level mTLS.
Adapters
- New:
vapi(WebSocket transport),assemblyai+cartesia(streaming STT → new
earshot::transcriptevent). - Fixed to work against current APIs:
elevenlabs(requiredconvaisubprotocol +
respectscodec/rate),gemini(JSON-over-binary frames +realtimeInput.audio+
current model). - All ten adapters validated: native · twilio · openai · deepgram · elevenlabs · gemini ·
pipecat · vapi · assemblyai · cartesia.
Features
- Barge-in policy engine —
interruptible=none|dtmf|speech|any,sensitivity,
ignore_backchannel,barge_min_ms,barge_fade_ms. - Caller context + welcome greeting —
EARSHOT_META→X-Earshot-Metaheader;
greeting=/EARSHOT_GREETING. - Box-level mTLS + custom CA —
EARSHOT_TLS_CLIENT_CERT/_KEY/EARSHOT_TLS_CA. - New event
earshot::transcript(STT).
Full details in CHANGELOG.md.
Full changelog: v0.1.0...v0.2.0
v0.1.0 — Within Earshot (first public release)
mod_earshot 0.1.0 — first public release
A FreeSWITCH module that bridges live call audio to your AI voice agent over one WebSocket — full-duplex, with the agent's voice played back reliably on the same leg. A drop-in mod_audio_stream alternative whose playback actually works, with seven protocol adapters, module-side turn-taking, agent-driven call control, PCI masking, multi-stream fan-out, and typed ESL events.
The transport, full-duplex audio path, and playout are validated end-to-end on real SIP calls with real audio — live OpenAI Realtime and Deepgram Voice Agent sessions (Deepgram from a hardware SIP phone), plus a shared-pool concurrency + soak load test.
Highlights
- Playback that works — the
WRITE_REPLACEagent→caller path validated with real audio, full duplex, on one leg. This is the wedge the incumbent module never delivered. - 7 protocol adapters —
native,twilio,openai(Realtime),deepgram(Voice Agent),elevenlabs,gemini(Live),pipecat. Swap vendors with one word (proto=). - Turn-taking for any agent — module-side VAD →
speech_started/speech_stopped, a ready-gate (no "answered into silence"), and speech-triggered barge-in. - Agent drives the call — opt-in, whitelisted control channel (transfer / hangup / DTMF / play / record / hold / bridge / setvar), every action audited via
earshot::command. - PCI/PII masking — mute caller audio + redact DTMF to the agent during card entry, while digits still reach a secure collector.
- Multi-stream fan-out — agent + live transcription + supervisor on one call.
- Scales with sessions, not threads — all streams share a small pool of libwebsockets service contexts (~1 MB/session vs ~17 MB thread-per-stream), so a small box holds thousands of sessions where thread-per-stream hit a wall near ~420. Validated to 150 concurrent + a 12-minute flat-memory soak, mass-teardown/RST-storm without a crash, and a ThreadSanitizer-clean fuzz run.
- Observable — typed
earshot::custom events over the FreeSWITCH event socket (ESL) + latency KPIs (time-to-first-audio, per-turn response, WebSocket RTT), keyed to a two-key trace (SIP Call-ID + channel UUID). - Drop-in migration —
uuid_audio_stream/audio_streamcompat runs existingmod_audio_streamdialplans unchanged.
Adapter validation (stated per vendor, not blanket)
native · openai · deepgram — live-validated on a real call · pipecat — against Pipecat's official protobuf schema · twilio — WebSocket echo round-trip · elevenlabs · gemini — implemented, not yet live-validated (see the adapter status table in the README).
Install
Needs FreeSWITCH dev headers + libwebsockets-dev:
cmake -S . -B build && cmake --build build
sudo cmake --install build # -> /usr/lib/freeswitch/mod/mod_earshot.so
fs_cli -x "load mod_earshot"
Quickstart recipes in QUICKSTART.md, full option/verb/event reference in docs/API.md.
License: MIT · Full changelog: CHANGELOG.md