livekit-plugins-denoise 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+
9
+ # Virtual environments
10
+ .venv/
11
+ venv/
12
+ env/
13
+
14
+ # Test / tooling caches
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+
19
+ # Generated denoiser output
20
+ *.denoised.wav
21
+
22
+ # Editor / OS
23
+ .vscode/
24
+ .idea/
25
+ .DS_Store
26
+ Thumbs.db
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BotifyNow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,11 @@
1
+ livekit-plugins-denoise
2
+ Copyright 2026 BotifyNow
3
+
4
+ This package depends on deepfilter-stream, which bundles DeepFilterNet3 model
5
+ weights. deepfilter-stream and DeepFilterNet are dual-licensed under MIT or
6
+ Apache-2.0 at the recipient's option.
7
+
8
+ DeepFilterNet attribution:
9
+ H. Schröter, T. Rosenkranz, A. N. Escalante-B., and A. Maier,
10
+ "DeepFilterNet: A Low Complexity Speech Enhancement Framework for Full-Band
11
+ Audio based on Deep Filtering", IWAENC 2022.
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.5
2
+ Name: livekit-plugins-denoise
3
+ Version: 0.1.0
4
+ Summary: Self-hosted noise and echo suppression for LiveKit SIP telephony
5
+ Project-URL: Source, https://github.com/jaffarjawed/livekit-plugins-denoise
6
+ Project-URL: Issues, https://github.com/jaffarjawed/livekit-plugins-denoise/issues
7
+ Author: BotifyNow
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ License-File: NOTICE
11
+ Keywords: audio,echo-cancellation,livekit,noise-cancellation,sip,telephony,webrtc
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Communications :: Telephony
21
+ Classifier: Topic :: Multimedia :: Sound/Audio
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: deepfilter-stream<0.2,>=0.1.0
24
+ Requires-Dist: livekit-agents<2.0,>=1.6.0
25
+ Requires-Dist: livekit<2.0,>=1.1.0
26
+ Requires-Dist: numpy>=1.26
27
+ Provides-Extra: dev
28
+ Requires-Dist: build>=1.2; extra == 'dev'
29
+ Requires-Dist: pytest-asyncio<2,>=1.0; extra == 'dev'
30
+ Requires-Dist: pytest>=8; extra == 'dev'
31
+ Requires-Dist: twine>=6; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # livekit-plugins-denoise
35
+
36
+ Self-hosted noise suppression and acoustic echo cancellation for LiveKit SIP
37
+ agents.
38
+
39
+ - Removes background noise with DeepFilterNet3 or low-latency WebRTC noise
40
+ suppression.
41
+ - Removes the agent's voice when it returns through a caller's handset or
42
+ carrier path.
43
+ - Runs in your agent process: no per-minute denoising fee and no language
44
+ dependency.
45
+
46
+ This is an independent, community-maintained package and is not affiliated
47
+ with or endorsed by LiveKit, Inc.
48
+
49
+ ## Why this exists
50
+
51
+ LiveKit's managed `BVCTelephony` noise-cancellation option is billed per minute.
52
+ This package is a self-hosted, MIT-licensed alternative for teams that want
53
+ noise suppression and echo cancellation in their own agent process without a
54
+ per-minute denoising charge. You still pay for your own compute, telephony, and
55
+ other services.
56
+
57
+ ## Install
58
+
59
+ ```bash
60
+ python -m pip install livekit-plugins-denoise
61
+ ```
62
+
63
+ For local development:
64
+
65
+ ```bash
66
+ python -m pip install -e ".[dev]"
67
+ ```
68
+
69
+ ## Enable noise suppression and echo cancellation
70
+
71
+ Create one `TelephonyDenoiser` for each call. `EchoReferenceTap` is required
72
+ for echo cancellation because it supplies the agent's outgoing audio as the
73
+ far-end reference.
74
+
75
+ ```python
76
+ from livekit.agents import room_io
77
+ from livekit.plugins import telephony_denoise
78
+
79
+ denoiser = telephony_denoise.TelephonyDenoiser(
80
+ telephony_denoise.DenoiseOptions(
81
+ echo_cancellation=True,
82
+ noise_suppression=True,
83
+ high_pass_filter=True,
84
+ auto_gain_control=True,
85
+ enhancer="deepfilter", # use "webrtc" for lower latency
86
+ stream_delay_ms=120,
87
+ )
88
+ )
89
+
90
+ await session.start(
91
+ agent=agent,
92
+ room=room,
93
+ room_options=room_io.RoomOptions(
94
+ audio_input=room_io.AudioInputOptions(
95
+ noise_cancellation=denoiser,
96
+ auto_gain_control=False, # do not stack AGC
97
+ ),
98
+ ),
99
+ )
100
+
101
+ # Add this after session.start(); RoomIO replaces the output during startup.
102
+ session.output.audio = telephony_denoise.EchoReferenceTap(
103
+ denoiser, next_in_chain=session.output.audio
104
+ )
105
+ ```
106
+
107
+ To load the neural model before the first call:
108
+
109
+ ```python
110
+ def setup(proc):
111
+ telephony_denoise.prewarm()
112
+ ```
113
+
114
+ See [examples/sip_agent.py](examples/sip_agent.py) for a complete SIP agent.
115
+
116
+ ## Configuration
117
+
118
+ `DenoiseOptions` lets you control these features independently:
119
+
120
+ | Option | Default | Purpose |
121
+ | --- | --- | --- |
122
+ | `echo_cancellation` | `True` | Cancels the agent's returned audio; requires `EchoReferenceTap`. |
123
+ | `noise_suppression` | `True` | Removes background noise. |
124
+ | `enhancer` | `"deepfilter"` | Use `"webrtc"` when minimizing latency is more important. |
125
+ | `high_pass_filter` | `True` | Reduces low-frequency rumble and hum. |
126
+ | `auto_gain_control` | `True` | Helps keep quiet callers audible. |
127
+ | `stream_delay_ms` | `120` | Starting delay estimate for SIP echo paths. |
128
+
129
+ For tuning, supported formats, model download behavior, and operating notes, see
130
+ [Technical notes](docs/technical-notes.md).
131
+
132
+ ## Community
133
+
134
+ Issues, documentation improvements, and pull requests are welcome. See
135
+ [CONTRIBUTING.md](CONTRIBUTING.md). This project is MIT licensed; dependency
136
+ attribution is in [NOTICE](NOTICE).
@@ -0,0 +1,103 @@
1
+ # livekit-plugins-denoise
2
+
3
+ Self-hosted noise suppression and acoustic echo cancellation for LiveKit SIP
4
+ agents.
5
+
6
+ - Removes background noise with DeepFilterNet3 or low-latency WebRTC noise
7
+ suppression.
8
+ - Removes the agent's voice when it returns through a caller's handset or
9
+ carrier path.
10
+ - Runs in your agent process: no per-minute denoising fee and no language
11
+ dependency.
12
+
13
+ This is an independent, community-maintained package and is not affiliated
14
+ with or endorsed by LiveKit, Inc.
15
+
16
+ ## Why this exists
17
+
18
+ LiveKit's managed `BVCTelephony` noise-cancellation option is billed per minute.
19
+ This package is a self-hosted, MIT-licensed alternative for teams that want
20
+ noise suppression and echo cancellation in their own agent process without a
21
+ per-minute denoising charge. You still pay for your own compute, telephony, and
22
+ other services.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ python -m pip install livekit-plugins-denoise
28
+ ```
29
+
30
+ For local development:
31
+
32
+ ```bash
33
+ python -m pip install -e ".[dev]"
34
+ ```
35
+
36
+ ## Enable noise suppression and echo cancellation
37
+
38
+ Create one `TelephonyDenoiser` for each call. `EchoReferenceTap` is required
39
+ for echo cancellation because it supplies the agent's outgoing audio as the
40
+ far-end reference.
41
+
42
+ ```python
43
+ from livekit.agents import room_io
44
+ from livekit.plugins import telephony_denoise
45
+
46
+ denoiser = telephony_denoise.TelephonyDenoiser(
47
+ telephony_denoise.DenoiseOptions(
48
+ echo_cancellation=True,
49
+ noise_suppression=True,
50
+ high_pass_filter=True,
51
+ auto_gain_control=True,
52
+ enhancer="deepfilter", # use "webrtc" for lower latency
53
+ stream_delay_ms=120,
54
+ )
55
+ )
56
+
57
+ await session.start(
58
+ agent=agent,
59
+ room=room,
60
+ room_options=room_io.RoomOptions(
61
+ audio_input=room_io.AudioInputOptions(
62
+ noise_cancellation=denoiser,
63
+ auto_gain_control=False, # do not stack AGC
64
+ ),
65
+ ),
66
+ )
67
+
68
+ # Add this after session.start(); RoomIO replaces the output during startup.
69
+ session.output.audio = telephony_denoise.EchoReferenceTap(
70
+ denoiser, next_in_chain=session.output.audio
71
+ )
72
+ ```
73
+
74
+ To load the neural model before the first call:
75
+
76
+ ```python
77
+ def setup(proc):
78
+ telephony_denoise.prewarm()
79
+ ```
80
+
81
+ See [examples/sip_agent.py](examples/sip_agent.py) for a complete SIP agent.
82
+
83
+ ## Configuration
84
+
85
+ `DenoiseOptions` lets you control these features independently:
86
+
87
+ | Option | Default | Purpose |
88
+ | --- | --- | --- |
89
+ | `echo_cancellation` | `True` | Cancels the agent's returned audio; requires `EchoReferenceTap`. |
90
+ | `noise_suppression` | `True` | Removes background noise. |
91
+ | `enhancer` | `"deepfilter"` | Use `"webrtc"` when minimizing latency is more important. |
92
+ | `high_pass_filter` | `True` | Reduces low-frequency rumble and hum. |
93
+ | `auto_gain_control` | `True` | Helps keep quiet callers audible. |
94
+ | `stream_delay_ms` | `120` | Starting delay estimate for SIP echo paths. |
95
+
96
+ For tuning, supported formats, model download behavior, and operating notes, see
97
+ [Technical notes](docs/technical-notes.md).
98
+
99
+ ## Community
100
+
101
+ Issues, documentation improvements, and pull requests are welcome. See
102
+ [CONTRIBUTING.md](CONTRIBUTING.md). This project is MIT licensed; dependency
103
+ attribution is in [NOTICE](NOTICE).
@@ -0,0 +1,39 @@
1
+ # Technical notes
2
+
3
+ ## Choosing an enhancer
4
+
5
+ `enhancer="deepfilter"` is the default and generally removes more complex
6
+ background noise. It downloads DeepFilterNet3 model weights (about 13 MB) on
7
+ first use, so call `telephony_denoise.prewarm()` during worker startup.
8
+
9
+ Use `enhancer="webrtc"` when lower latency and call density matter more than
10
+ neural suppression quality.
11
+
12
+ ## Echo cancellation
13
+
14
+ Noise suppression works without additional wiring. Echo cancellation does not:
15
+ attach `EchoReferenceTap` after `session.start()` so the denoiser receives the
16
+ audio the caller actually hears. Create one denoiser per call; its echo and
17
+ noise state must not be shared between calls.
18
+
19
+ `stream_delay_ms=120` is a useful SIP starting point. Tune it using recordings
20
+ from your carrier route if returned agent audio remains audible.
21
+
22
+ ## Audio and pipeline constraints
23
+
24
+ - Inbound telephony audio must be mono.
25
+ - Supported input rates are multiples of 100 Hz, such as 8, 16, 24, 32, and
26
+ 48 kHz. Unsupported audio passes through unfiltered rather than failing a
27
+ call.
28
+ - Do not stack this denoiser with another echo or noise canceller; competing
29
+ filters can degrade speech quality.
30
+ - If this plugin owns AGC, set LiveKit's `AudioInputOptions.auto_gain_control`
31
+ to `False`.
32
+
33
+ ## Dependencies and licensing
34
+
35
+ This package depends on `livekit`, `livekit-agents`, `numpy`, and
36
+ `deepfilter-stream`. It is MIT licensed. `deepfilter-stream` and DeepFilterNet
37
+ are dual-licensed under MIT or Apache-2.0; the required attribution is in the
38
+ project [NOTICE](../NOTICE). The DeepFilterNet3 model is downloaded by the
39
+ dependency and is not redistributed in this package.
@@ -0,0 +1,120 @@
1
+ """LiveKit SIP telephony agent with self-hosted noise and echo suppression.
2
+
3
+ The audio filtering is the point of this file; the speech models are ordinary
4
+ and meant to be swapped for whatever you already use.
5
+
6
+ Three wires do the work:
7
+
8
+ 1. The model is loaded in `setup`, once per worker process, so no call pays
9
+ for the download and ONNX session build inside its first audio frame.
10
+
11
+ 2. The denoiser is handed to `AudioInputOptions.noise_cancellation`, so
12
+ LiveKit runs it on every inbound frame from the caller before speech
13
+ recognition sees it.
14
+
15
+ 3. The agent's own outgoing speech is tapped into the same denoiser, which is
16
+ what lets it cancel the echo of its own voice coming back down the line.
17
+
18
+ Run it with:
19
+
20
+ python examples/sip_agent.py dev
21
+
22
+ Inbound calls reach the agent through a SIP trunk and dispatch rule, which are
23
+ configured on the LiveKit side rather than here.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import logging
29
+
30
+ from livekit.agents import (
31
+ Agent,
32
+ AgentServer,
33
+ AgentSession,
34
+ JobContext,
35
+ JobProcess,
36
+ cli,
37
+ room_io,
38
+ )
39
+ from livekit.plugins import telephony_denoise
40
+
41
+ logger = logging.getLogger("sip-agent")
42
+
43
+ # Narrowband phone audio, so there is no point paying for a wider pipeline.
44
+ INPUT_SAMPLE_RATE = 16000
45
+
46
+ INSTRUCTIONS = """You are a helpful voice assistant speaking with a caller on
47
+ the phone. Keep replies short and conversational. Reply in the language the
48
+ caller uses."""
49
+
50
+
51
+ def setup(proc: JobProcess) -> None:
52
+ # Runs once per worker process, before any call is accepted.
53
+ telephony_denoise.prewarm()
54
+
55
+
56
+ server = AgentServer(setup_fnc=setup)
57
+
58
+
59
+ @server.rtc_session()
60
+ async def entrypoint(ctx: JobContext) -> None:
61
+ await ctx.connect()
62
+
63
+ denoiser = telephony_denoise.TelephonyDenoiser(
64
+ telephony_denoise.DenoiseOptions(
65
+ # Cancels the agent's own voice echoing back off the caller's
66
+ # handset or the carrier's hybrid.
67
+ echo_cancellation=True,
68
+ # Suppresses background noise via DeepFilterNet3 by default
69
+ # (gym, café, babble, hiss — unknown caller environments).
70
+ noise_suppression=True,
71
+ # Removes rumble and mains hum below the voice band.
72
+ high_pass_filter=True,
73
+ # Evens out callers who are too close to or far from the handset.
74
+ auto_gain_control=True,
75
+ # Tune this to your typical SIP round-trip delay.
76
+ stream_delay_ms=120,
77
+ )
78
+ )
79
+ # Build the filter now rather than on the first frame from the caller, so
80
+ # the greeting below can be used as an echo reference from its first word.
81
+ denoiser.prepare(INPUT_SAMPLE_RATE)
82
+
83
+ session = AgentSession(
84
+ # Multilingual models keep the whole pipeline language-agnostic, to
85
+ # match the filter. Swap these for your own plugins or providers.
86
+ stt="deepgram/nova-3:multi",
87
+ llm="openai/gpt-4o-mini",
88
+ tts="cartesia/sonic-2",
89
+ )
90
+
91
+ await session.start(
92
+ agent=Agent(instructions=INSTRUCTIONS),
93
+ room=ctx.room,
94
+ room_options=room_io.RoomOptions(
95
+ audio_input=room_io.AudioInputOptions(
96
+ noise_cancellation=denoiser,
97
+ sample_rate=INPUT_SAMPLE_RATE,
98
+ # The denoiser already applies gain control; running LiveKit's
99
+ # as well would compress the signal twice.
100
+ auto_gain_control=False,
101
+ ),
102
+ ),
103
+ )
104
+
105
+ # Must come after start(): RoomIO installs its own audio output during
106
+ # start and would overwrite anything set beforehand.
107
+ session.output.audio = telephony_denoise.EchoReferenceTap(
108
+ denoiser, next_in_chain=session.output.audio
109
+ )
110
+
111
+ logger.info("call ready, audio filter active: %s", denoiser.enhancer)
112
+
113
+ await session.generate_reply(
114
+ instructions="Greet the caller briefly and ask how you can help."
115
+ )
116
+
117
+
118
+ if __name__ == "__main__":
119
+ logging.basicConfig(level=logging.INFO)
120
+ cli.run_app(server)
@@ -0,0 +1,32 @@
1
+ """Self-hosted noise and echo suppression for LiveKit SIP telephony."""
2
+
3
+ from livekit.agents import Plugin
4
+
5
+ from .echo_reference import EchoReferenceTap
6
+ from .log import logger
7
+ from .neural import prewarm
8
+ from .processor import DenoiseOptions, Enhancer, TelephonyDenoiser
9
+ from .version import __version__
10
+
11
+ __all__ = [
12
+ "DenoiseOptions",
13
+ "EchoReferenceTap",
14
+ "Enhancer",
15
+ "TelephonyDenoiser",
16
+ "__version__",
17
+ "prewarm",
18
+ ]
19
+
20
+
21
+ class TelephonyDenoisePlugin(Plugin):
22
+ def __init__(self) -> None:
23
+ super().__init__(__name__, __version__, __package__, logger)
24
+
25
+ def download_files(self) -> None:
26
+ # Fetches the DeepFilterNet3 weights so `lk agent build` bakes them into
27
+ # the image; otherwise the first call of a fresh worker pays for the
28
+ # download on the event loop, inside its first 10 ms frame.
29
+ prewarm()
30
+
31
+
32
+ Plugin.register_plugin(TelephonyDenoisePlugin())
@@ -0,0 +1,53 @@
1
+ """Small int16 PCM helpers used by the processing pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+
7
+
8
+ class Int16Buffer:
9
+ """A FIFO of interleaved int16 samples."""
10
+
11
+ __slots__ = ("_buf",)
12
+
13
+ def __init__(self) -> None:
14
+ self._buf = np.zeros(0, dtype=np.int16)
15
+
16
+ @property
17
+ def size(self) -> int:
18
+ return int(self._buf.size)
19
+
20
+ def append(self, data: np.ndarray) -> None:
21
+ # `np.concatenate(..., dtype=np.int16)` casts same_kind, so an int32
22
+ # caller would be truncated into garbage PCM without a word. Only int16
23
+ # is ever correct here, so insist on it.
24
+ if data.dtype != np.int16:
25
+ raise TypeError(f"expected int16 samples, got {data.dtype}")
26
+ self._buf = np.concatenate((self._buf, data))
27
+
28
+ def take(self, count: int) -> np.ndarray:
29
+ out = self._buf[:count].copy()
30
+ self._buf = self._buf[count:]
31
+ return out
32
+
33
+ def clear(self) -> None:
34
+ self._buf = np.zeros(0, dtype=np.int16)
35
+
36
+
37
+ def to_mono(pcm: np.ndarray, src_channels: int) -> np.ndarray:
38
+ """Downmix interleaved int16 audio when needed."""
39
+
40
+ if src_channels < 1:
41
+ raise ValueError(f"src_channels must be positive, got {src_channels}")
42
+ if src_channels == 1:
43
+ return pcm
44
+ if pcm.size % src_channels:
45
+ raise ValueError(
46
+ f"{pcm.size} samples is not a whole number of {src_channels}ch frames"
47
+ )
48
+
49
+ frames = pcm.reshape(-1, src_channels).astype(np.int32)
50
+ # Round rather than truncate: `astype` rounds toward zero, which biases
51
+ # every downmixed sample toward silence and adds a DC-free crackle.
52
+ mean = frames.sum(axis=1) / src_channels
53
+ return np.rint(mean).astype(np.int16)