nitpick-voice 0.2.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.
- nitpick_voice-0.2.0/LICENSE +25 -0
- nitpick_voice-0.2.0/PKG-INFO +434 -0
- nitpick_voice-0.2.0/README.md +387 -0
- nitpick_voice-0.2.0/nitpick/__init__.py +136 -0
- nitpick_voice-0.2.0/nitpick/__main__.py +6 -0
- nitpick_voice-0.2.0/nitpick/agent.py +466 -0
- nitpick_voice-0.2.0/nitpick/billing/__init__.py +22 -0
- nitpick_voice-0.2.0/nitpick/billing/collector.py +228 -0
- nitpick_voice-0.2.0/nitpick/billing/events.py +104 -0
- nitpick_voice-0.2.0/nitpick/cli.py +79 -0
- nitpick_voice-0.2.0/nitpick/context.py +153 -0
- nitpick_voice-0.2.0/nitpick/frames/__init__.py +92 -0
- nitpick_voice-0.2.0/nitpick/frames/frames.py +268 -0
- nitpick_voice-0.2.0/nitpick/frames/kinds.py +164 -0
- nitpick_voice-0.2.0/nitpick/frames/processor.py +361 -0
- nitpick_voice-0.2.0/nitpick/pipeline/__init__.py +7 -0
- nitpick_voice-0.2.0/nitpick/pipeline/pipeline.py +53 -0
- nitpick_voice-0.2.0/nitpick/pipeline/task.py +294 -0
- nitpick_voice-0.2.0/nitpick/processors/assistant_aggregator.py +84 -0
- nitpick_voice-0.2.0/nitpick/processors/interruption.py +86 -0
- nitpick_voice-0.2.0/nitpick/processors/user_aggregator.py +102 -0
- nitpick_voice-0.2.0/nitpick/rooms/__init__.py +49 -0
- nitpick_voice-0.2.0/nitpick/rooms/control.py +207 -0
- nitpick_voice-0.2.0/nitpick/rooms/manager.py +419 -0
- nitpick_voice-0.2.0/nitpick/rooms/model.py +142 -0
- nitpick_voice-0.2.0/nitpick/sdk/__init__.py +6 -0
- nitpick_voice-0.2.0/nitpick/sdk/client.py +285 -0
- nitpick_voice-0.2.0/nitpick/services/__init__.py +6 -0
- nitpick_voice-0.2.0/nitpick/services/llm/__init__.py +6 -0
- nitpick_voice-0.2.0/nitpick/services/llm/openai.py +332 -0
- nitpick_voice-0.2.0/nitpick/services/stt/__init__.py +16 -0
- nitpick_voice-0.2.0/nitpick/services/stt/base.py +252 -0
- nitpick_voice-0.2.0/nitpick/services/stt/deepgram.py +120 -0
- nitpick_voice-0.2.0/nitpick/services/stt/openai.py +102 -0
- nitpick_voice-0.2.0/nitpick/services/stt/sarvam.py +202 -0
- nitpick_voice-0.2.0/nitpick/services/tts/__init__.py +6 -0
- nitpick_voice-0.2.0/nitpick/services/tts/openai.py +233 -0
- nitpick_voice-0.2.0/nitpick/signaling/server.py +560 -0
- nitpick_voice-0.2.0/nitpick/transport/__init__.py +436 -0
- nitpick_voice-0.2.0/nitpick/transport/webrtc.py +311 -0
- nitpick_voice-0.2.0/nitpick/vad/__init__.py +27 -0
- nitpick_voice-0.2.0/nitpick/vad/backends.py +337 -0
- nitpick_voice-0.2.0/nitpick/vad/core.py +225 -0
- nitpick_voice-0.2.0/nitpick/vad/processor.py +144 -0
- nitpick_voice-0.2.0/nitpick/web/index.html +482 -0
- nitpick_voice-0.2.0/nitpick_voice.egg-info/PKG-INFO +434 -0
- nitpick_voice-0.2.0/nitpick_voice.egg-info/SOURCES.txt +62 -0
- nitpick_voice-0.2.0/nitpick_voice.egg-info/dependency_links.txt +1 -0
- nitpick_voice-0.2.0/nitpick_voice.egg-info/entry_points.txt +3 -0
- nitpick_voice-0.2.0/nitpick_voice.egg-info/requires.txt +31 -0
- nitpick_voice-0.2.0/nitpick_voice.egg-info/top_level.txt +1 -0
- nitpick_voice-0.2.0/pyproject.toml +78 -0
- nitpick_voice-0.2.0/setup.cfg +4 -0
- nitpick_voice-0.2.0/tests/test_billing_and_interruption.py +213 -0
- nitpick_voice-0.2.0/tests/test_docs.py +189 -0
- nitpick_voice-0.2.0/tests/test_end_to_end.py +158 -0
- nitpick_voice-0.2.0/tests/test_pipeline.py +140 -0
- nitpick_voice-0.2.0/tests/test_regressions.py +307 -0
- nitpick_voice-0.2.0/tests/test_rooms.py +264 -0
- nitpick_voice-0.2.0/tests/test_server_vad.py +180 -0
- nitpick_voice-0.2.0/tests/test_stt_turn_gate.py +167 -0
- nitpick_voice-0.2.0/tests/test_tts_sentences.py +37 -0
- nitpick_voice-0.2.0/tests/test_vad.py +237 -0
- nitpick_voice-0.2.0/tests/test_webrtc_transport.py +164 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, the nitpick authors
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
17
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
18
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
20
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
21
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
23
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
24
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
25
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nitpick-voice
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A Python framework for building voice agents — frame pipelines, STT/LLM/TTS services, and WebRTC rooms.
|
|
5
|
+
Author: Arsh0922
|
|
6
|
+
License-Expression: BSD-2-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/V1PASH/nitpick
|
|
8
|
+
Project-URL: Repository, https://github.com/V1PASH/nitpick
|
|
9
|
+
Project-URL: Issues, https://github.com/V1PASH/nitpick/issues
|
|
10
|
+
Keywords: voice,ai,agent,webrtc,stt,tts,llm,pipeline
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: fastapi>=0.110
|
|
22
|
+
Requires-Dist: uvicorn>=0.27
|
|
23
|
+
Requires-Dist: websockets>=12
|
|
24
|
+
Requires-Dist: httpx>=0.26
|
|
25
|
+
Requires-Dist: numpy>=1.24
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-timeout>=2.3; extra == "dev"
|
|
30
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
31
|
+
Requires-Dist: twine>=6.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pyflakes>=3.0; extra == "dev"
|
|
33
|
+
Provides-Extra: webrtcvad
|
|
34
|
+
Requires-Dist: webrtcvad>=2.0.10; extra == "webrtcvad"
|
|
35
|
+
Provides-Extra: onnx
|
|
36
|
+
Requires-Dist: onnxruntime>=1.17; extra == "onnx"
|
|
37
|
+
Provides-Extra: webrtc
|
|
38
|
+
Requires-Dist: aiortc>=1.9; extra == "webrtc"
|
|
39
|
+
Provides-Extra: whisper
|
|
40
|
+
Requires-Dist: faster-whisper>=1.0; extra == "whisper"
|
|
41
|
+
Provides-Extra: all
|
|
42
|
+
Requires-Dist: webrtcvad>=2.0.10; extra == "all"
|
|
43
|
+
Requires-Dist: onnxruntime>=1.17; extra == "all"
|
|
44
|
+
Requires-Dist: numpy>=1.24; extra == "all"
|
|
45
|
+
Requires-Dist: aiortc>=1.9; extra == "all"
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
# nitpick
|
|
49
|
+
|
|
50
|
+
**A Python framework for building voice agents.** Compose an agent from STT,
|
|
51
|
+
LLM and TTS services, run it locally, and talk to it in the browser over
|
|
52
|
+
WebRTC.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from nitpick import Agent, tool
|
|
56
|
+
|
|
57
|
+
@tool("get_weather", "Look up the weather for a city.")
|
|
58
|
+
def get_weather(city: str) -> str:
|
|
59
|
+
return f"It is 28°C in {city}."
|
|
60
|
+
|
|
61
|
+
agent = Agent(
|
|
62
|
+
name="Nitpick",
|
|
63
|
+
prompt="You are a friendly voice assistant.",
|
|
64
|
+
stt="openai",
|
|
65
|
+
llm="openai:gpt-4o-mini",
|
|
66
|
+
tts="openai:alloy",
|
|
67
|
+
tools=[get_weather],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
agent.run(room_id="demo", port=8000)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Open <http://localhost:8000> and talk to it.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Why
|
|
78
|
+
|
|
79
|
+
Most voice-agent frameworks make you choose between a rich pipeline you have
|
|
80
|
+
to wire up by hand and a black box you cannot extend. nitpick gives you the
|
|
81
|
+
pipeline, but makes the common case one expression.
|
|
82
|
+
|
|
83
|
+
Three ideas shape the framework:
|
|
84
|
+
|
|
85
|
+
**1. Frames move through a linked list of processors.**
|
|
86
|
+
Every component — VAD, STT, LLM, TTS, transport, the pipeline itself — is the
|
|
87
|
+
same type: a `FrameProcessor` with a handler. Adding a service means writing
|
|
88
|
+
one handler, not learning a new abstraction.
|
|
89
|
+
|
|
90
|
+
**2. System frames overtake data frames.**
|
|
91
|
+
nitpick keeps two lanes per processor. Lifecycle frames (interruption,
|
|
92
|
+
cancel, "the user stopped speaking") are processed on a separate lane and
|
|
93
|
+
always jump ahead of queued data. An in-flight LLM call can never delay a
|
|
94
|
+
barge-in.
|
|
95
|
+
|
|
96
|
+
**3. The transcript and the turn-close are one frame.**
|
|
97
|
+
This is the detail that separates a framework that works from one that
|
|
98
|
+
mostly works. Because system frames overtake data frames, emitting a
|
|
99
|
+
transcript and a "user stopped speaking" signal as two separate frames means
|
|
100
|
+
the stop can arrive first — and an agent that sees a turn close with no text
|
|
101
|
+
**silently never answers**. nitpick makes the turn-close frame *carry* the
|
|
102
|
+
transcript, so the ordering is structural rather than probabilistic.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
@dataclass
|
|
106
|
+
class VadStopped:
|
|
107
|
+
stop_secs: float
|
|
108
|
+
timestamp: float
|
|
109
|
+
transcript: Transcription | None = None # bundled, not raced
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Install
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pip install nitpick-voice
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The distribution is called **nitpick-voice** because `nitpick` is taken on
|
|
121
|
+
PyPI by an unrelated package. **The import name is still `nitpick`**:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from nitpick import Agent, RoomClient, tool
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
To work on the framework itself:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
git clone https://github.com/V1PASH/nitpick.git
|
|
131
|
+
cd nitpick
|
|
132
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
133
|
+
pip install -e ".[dev]"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Python 3.10+. The core install pulls in `fastapi`, `uvicorn`, `websockets`,
|
|
137
|
+
`httpx` and `numpy` — nothing else, and no model downloads.
|
|
138
|
+
|
|
139
|
+
Set the keys for the services you use:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
export OPENAI_API_KEY=sk-... # STT, LLM and TTS
|
|
143
|
+
export DEEPGRAM_API_KEY=... # optional: streaming STT
|
|
144
|
+
export SARVAM_API_KEY=... # optional: Sarvam STT (Indian languages)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Optional extras
|
|
148
|
+
|
|
149
|
+
| Extra | Adds | Why |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `pip install nitpick-voice[onnx]` | Silero VAD | A neural VAD that handles noisy rooms |
|
|
152
|
+
| `pip install nitpick-voice[webrtcvad]` | WebRTC VAD | Google's model; needs a C compiler to build |
|
|
153
|
+
| `pip install nitpick-voice[webrtc]` | `aiortc` | The agent as a native WebRTC peer |
|
|
154
|
+
| `pip install nitpick-voice[all]` | all of the above | |
|
|
155
|
+
|
|
156
|
+
The default VAD backend is `energy` — pure Python, no dependencies, no model
|
|
157
|
+
file. It works straight after `pip install`.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Running an agent
|
|
162
|
+
|
|
163
|
+
### From Python
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from nitpick import Agent
|
|
167
|
+
|
|
168
|
+
Agent(name="Assistant", prompt="You are helpful.").run(port=8000)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### From the CLI
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
nitpick serve --room demo --port 8000 --name Assistant
|
|
175
|
+
nitpick version
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The agent serves a browser client at `/`, a health check at `/health`, and the
|
|
179
|
+
signaling socket at `/ws`.
|
|
180
|
+
|
|
181
|
+
### From your own code — the SDK
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from nitpick import RoomClient
|
|
185
|
+
|
|
186
|
+
async with RoomClient("ws://localhost:8000/ws", name="My App") as client:
|
|
187
|
+
await client.join_room("demo")
|
|
188
|
+
await client.publish_pcm(pcm, sample_rate=16000)
|
|
189
|
+
async for message in client.messages():
|
|
190
|
+
...
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Documentation
|
|
196
|
+
|
|
197
|
+
| Document | What it covers |
|
|
198
|
+
|---|---|
|
|
199
|
+
| [docs/architecture.md](docs/architecture.md) | How the framework works — frames, the two-lane processor, the pipeline, turn detection, interruption, and the design trade-offs |
|
|
200
|
+
| [docs/sdk.md](docs/sdk.md) | The client SDK and the signaling protocol — every message type, every error code, and how to write your own client |
|
|
201
|
+
| [docs/README.md](docs/README.md) | Reading guide and the three things worth knowing |
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Publishing
|
|
206
|
+
|
|
207
|
+
The package is built and published with [go-task](https://taskfile.dev):
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
task # list the tasks
|
|
211
|
+
task test # run the tests
|
|
212
|
+
task build # build the wheel and the sdist
|
|
213
|
+
task check # the pre-publish checks on their own
|
|
214
|
+
task publish # build, check and upload to PyPI
|
|
215
|
+
task publish-test # upload to TestPyPI instead
|
|
216
|
+
task release # tag the current version and push the tag
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`task publish` refuses to run unless the working tree is clean, the version
|
|
220
|
+
in `pyproject.toml` matches the package's `__version__`, the version is not
|
|
221
|
+
already on PyPI, and the artefacts pass `twine check --strict`. Publishing is
|
|
222
|
+
the one thing you cannot undo, so the checks are strict.
|
|
223
|
+
|
|
224
|
+
The token comes from `~/.pypirc`:
|
|
225
|
+
|
|
226
|
+
```ini
|
|
227
|
+
[distutils]
|
|
228
|
+
index-servers =
|
|
229
|
+
pypi
|
|
230
|
+
|
|
231
|
+
[pypi]
|
|
232
|
+
username = V1PASH
|
|
233
|
+
password = pypi-...
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
There is also a `publish` GitHub workflow that uses PyPI's **trusted
|
|
237
|
+
publishing** — the workflow is authorised by GitHub, so a release can be
|
|
238
|
+
published from CI with no API token stored anywhere. Add the workflow as a
|
|
239
|
+
pending publisher on the project's PyPI page, and create a `pypi` environment
|
|
240
|
+
in the repository.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Architecture
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
┌──────────── PipelineTask ────────────┐
|
|
248
|
+
browser ──ws──► │ RoomTransport.input │
|
|
249
|
+
│ ↓ │
|
|
250
|
+
│ VadProcessor ← turn boundaries │
|
|
251
|
+
│ ↓ │
|
|
252
|
+
│ SttService ← turn gate │
|
|
253
|
+
│ ↓ │
|
|
254
|
+
│ UserAggregator ← triggers the LLM │
|
|
255
|
+
│ ↓ │
|
|
256
|
+
│ LLMService ← streaming + tools │
|
|
257
|
+
│ ↓ │
|
|
258
|
+
│ AssistantAggregator │
|
|
259
|
+
│ ↓ │
|
|
260
|
+
│ TTSService ← sentence buffering │
|
|
261
|
+
│ ↓ │
|
|
262
|
+
│ InterruptionHandler ← barge-in │
|
|
263
|
+
│ ↓ │
|
|
264
|
+
│ RoomTransport.output │
|
|
265
|
+
└───────────────────────────────────────┘
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Packages
|
|
269
|
+
|
|
270
|
+
| Package | What lives there |
|
|
271
|
+
|---|---|
|
|
272
|
+
| `nitpick.frames` | `Frame`, `FrameKind`, `FrameProcessor`, the two-lane queues |
|
|
273
|
+
| `nitpick.pipeline` | `Pipeline`, `PipelineTask`, `PipelineParams` |
|
|
274
|
+
| `nitpick.vad` | `StateMachine`, the `Energy`/`WebRtc`/`Silero` backends, `VadProcessor` |
|
|
275
|
+
| `nitpick.rooms` | `Room`, `Participant`, `RoomManager`, `RoomControlModule` |
|
|
276
|
+
| `nitpick.signaling` | The FastAPI signaling server and the WebSocket protocol |
|
|
277
|
+
| `nitpick.transport` | `RoomTransport`, `ChannelTransport`, `WebRtcTransport` |
|
|
278
|
+
| `nitpick.services.stt` | `SttProvider` protocol, `SttService`, OpenAI + Deepgram + Sarvam |
|
|
279
|
+
| `nitpick.services.llm` | `LLMService` — streaming, tool calls, tool loop |
|
|
280
|
+
| `nitpick.services.tts` | `TTSService` — sentence buffering, synthesis |
|
|
281
|
+
| `nitpick.processors` | The user, assistant and interruption processors |
|
|
282
|
+
| `nitpick.billing` | Per-session usage and transcript events |
|
|
283
|
+
| `nitpick.sdk` | `RoomClient` — drive a room from your own code |
|
|
284
|
+
| `nitpick.agent` | The `Agent` facade — the API you actually use |
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Turn detection
|
|
289
|
+
|
|
290
|
+
The VAD is a four-state machine — `QUIET → STARTING → SPEAKING → STOPPING` —
|
|
291
|
+
that debounces both edges, so a single cough does not open a turn and a
|
|
292
|
+
pause between words does not close one.
|
|
293
|
+
|
|
294
|
+
Three backends behind one API:
|
|
295
|
+
|
|
296
|
+
| Backend | Install | Notes |
|
|
297
|
+
|---|---|---|
|
|
298
|
+
| `energy` *(default)* | — | Pure Python, adaptive noise floor. No dependencies. |
|
|
299
|
+
| `webrtc` | `nitpick[webrtcvad]` | Google's model. Needs a C compiler. |
|
|
300
|
+
| `silero` | `nitpick[onnx]` | Neural. Best in noisy rooms; the model is downloaded once. |
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
agent = Agent(vad="silero") # or "energy", "webrtc", or None
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Client and server VAD coordination.** The browser reports its own speaking
|
|
307
|
+
edges, and the server has its own VAD. A shared `SpeakingGate` ensures
|
|
308
|
+
exactly one turn boundary per utterance — whichever side detects speech
|
|
309
|
+
first wins, and the other becomes a no-op.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Barge-in
|
|
314
|
+
|
|
315
|
+
When the user starts speaking while the bot is still talking, the bot stops.
|
|
316
|
+
The `InterruptionHandler` watches for that, broadcasts an interruption, and:
|
|
317
|
+
|
|
318
|
+
* the LLM service aborts its in-flight request,
|
|
319
|
+
* the TTS service drops its buffer,
|
|
320
|
+
* the room gets a `room_interrupt` message so the browser stops playback.
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
agent = Agent(allow_interruptions=True) # the default
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Usage tracking
|
|
329
|
+
|
|
330
|
+
`SessionBilling` records what a session consumed — LLM tokens, TTS
|
|
331
|
+
characters, STT duration, and the full transcript.
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
from nitpick import LogBillingStorage, SessionBilling
|
|
335
|
+
|
|
336
|
+
billing = SessionBilling("session-42")
|
|
337
|
+
billing.attach(LogBillingStorage("usage.jsonl"))
|
|
338
|
+
billing.start()
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Recording is a single `put_nowait` onto an unbounded queue drained by a
|
|
342
|
+
background task, so it never blocks and never drops. Cost tracking costs
|
|
343
|
+
the audio path nothing.
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Adding a service
|
|
348
|
+
|
|
349
|
+
An STT provider answers four questions and nothing else. Everything else —
|
|
350
|
+
turn detection, buffering, the frame plumbing, the ordering guarantee — is
|
|
351
|
+
handled by `SttService`:
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
from nitpick.services.stt import SttProvider, SttService
|
|
355
|
+
|
|
356
|
+
class MyProvider(SttProvider):
|
|
357
|
+
name = "myprovider"
|
|
358
|
+
sample_rate = 16_000
|
|
359
|
+
|
|
360
|
+
async def start(self, on_event): ...
|
|
361
|
+
async def push_audio(self, pcm: bytes): ...
|
|
362
|
+
async def flush(self): ...
|
|
363
|
+
async def stop(self): ...
|
|
364
|
+
|
|
365
|
+
stt = SttService(MyProvider())
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
`on_event` receives `SttEvent(kind="partial"|"final"|"error", text=...)`.
|
|
369
|
+
Report a `final` and the turn closes with the transcript attached.
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## Testing a pipeline
|
|
374
|
+
|
|
375
|
+
`ChannelTransport` drives a pipeline through plain queues — no server, no
|
|
376
|
+
network:
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
from nitpick import ChannelTransport, PipelineParams, PipelineTask
|
|
380
|
+
|
|
381
|
+
transport = ChannelTransport()
|
|
382
|
+
task = PipelineTask([transport.input(), stt, transport.output()], PipelineParams())
|
|
383
|
+
task.start()
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## The signaling protocol
|
|
389
|
+
|
|
390
|
+
One WebSocket, flat JSON messages.
|
|
391
|
+
|
|
392
|
+
**Client → server**
|
|
393
|
+
|
|
394
|
+
| Type | Purpose |
|
|
395
|
+
|---|---|
|
|
396
|
+
| `join_room` / `create_room` / `leave_room` | Room membership |
|
|
397
|
+
| `participant_activity` | `speaking_started` / `speaking_stopped` — turn boundaries |
|
|
398
|
+
| `audio_pcm` | **Raw PCM frames** — the streaming path to the agent |
|
|
399
|
+
| `stt_transcript` | A transcript from the browser's speech recogniser |
|
|
400
|
+
| `publish_audio` | Base64 audio fanned out to the room |
|
|
401
|
+
| `webrtc_offer` / `webrtc_answer` / `ice_candidate` | SDP and ICE relay |
|
|
402
|
+
|
|
403
|
+
**Server → client**
|
|
404
|
+
|
|
405
|
+
`connected`, `joined_room`, `left_room`, `room_event`, `room_audio`,
|
|
406
|
+
`room_interrupt`, `audio_published`, `webrtc_offer`, `webrtc_answer`,
|
|
407
|
+
`ice_candidate`, `error`.
|
|
408
|
+
|
|
409
|
+
WebRTC is native in the browser. The server never touches media for a
|
|
410
|
+
browser-to-browser call — it relays SDP and ICE to a single peer and checks
|
|
411
|
+
that both peers are in the same room.
|
|
412
|
+
|
|
413
|
+
### The agent as a native WebRTC peer
|
|
414
|
+
|
|
415
|
+
With `pip install nitpick-voice[webrtc]`, `WebRtcTransport` makes the agent a real
|
|
416
|
+
WebRTC endpoint: the browser negotiates with it directly and audio travels
|
|
417
|
+
as Opus over SRTP, with nothing relayed through the signaling socket.
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## Tests
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
pytest
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
56 tests covering the two-lane frame ordering, the transcript/turn-close
|
|
428
|
+
guarantee, the turn gate's release timeout, the VAD state machine and its
|
|
429
|
+
drain loop, client/server VAD coordination, barge-in, billing, the room
|
|
430
|
+
event bus, the signaling protocol, the SDK, and a full browser-to-agent turn.
|
|
431
|
+
|
|
432
|
+
## License
|
|
433
|
+
|
|
434
|
+
BSD-2-Clause.
|