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.
Files changed (64) hide show
  1. nitpick_voice-0.2.0/LICENSE +25 -0
  2. nitpick_voice-0.2.0/PKG-INFO +434 -0
  3. nitpick_voice-0.2.0/README.md +387 -0
  4. nitpick_voice-0.2.0/nitpick/__init__.py +136 -0
  5. nitpick_voice-0.2.0/nitpick/__main__.py +6 -0
  6. nitpick_voice-0.2.0/nitpick/agent.py +466 -0
  7. nitpick_voice-0.2.0/nitpick/billing/__init__.py +22 -0
  8. nitpick_voice-0.2.0/nitpick/billing/collector.py +228 -0
  9. nitpick_voice-0.2.0/nitpick/billing/events.py +104 -0
  10. nitpick_voice-0.2.0/nitpick/cli.py +79 -0
  11. nitpick_voice-0.2.0/nitpick/context.py +153 -0
  12. nitpick_voice-0.2.0/nitpick/frames/__init__.py +92 -0
  13. nitpick_voice-0.2.0/nitpick/frames/frames.py +268 -0
  14. nitpick_voice-0.2.0/nitpick/frames/kinds.py +164 -0
  15. nitpick_voice-0.2.0/nitpick/frames/processor.py +361 -0
  16. nitpick_voice-0.2.0/nitpick/pipeline/__init__.py +7 -0
  17. nitpick_voice-0.2.0/nitpick/pipeline/pipeline.py +53 -0
  18. nitpick_voice-0.2.0/nitpick/pipeline/task.py +294 -0
  19. nitpick_voice-0.2.0/nitpick/processors/assistant_aggregator.py +84 -0
  20. nitpick_voice-0.2.0/nitpick/processors/interruption.py +86 -0
  21. nitpick_voice-0.2.0/nitpick/processors/user_aggregator.py +102 -0
  22. nitpick_voice-0.2.0/nitpick/rooms/__init__.py +49 -0
  23. nitpick_voice-0.2.0/nitpick/rooms/control.py +207 -0
  24. nitpick_voice-0.2.0/nitpick/rooms/manager.py +419 -0
  25. nitpick_voice-0.2.0/nitpick/rooms/model.py +142 -0
  26. nitpick_voice-0.2.0/nitpick/sdk/__init__.py +6 -0
  27. nitpick_voice-0.2.0/nitpick/sdk/client.py +285 -0
  28. nitpick_voice-0.2.0/nitpick/services/__init__.py +6 -0
  29. nitpick_voice-0.2.0/nitpick/services/llm/__init__.py +6 -0
  30. nitpick_voice-0.2.0/nitpick/services/llm/openai.py +332 -0
  31. nitpick_voice-0.2.0/nitpick/services/stt/__init__.py +16 -0
  32. nitpick_voice-0.2.0/nitpick/services/stt/base.py +252 -0
  33. nitpick_voice-0.2.0/nitpick/services/stt/deepgram.py +120 -0
  34. nitpick_voice-0.2.0/nitpick/services/stt/openai.py +102 -0
  35. nitpick_voice-0.2.0/nitpick/services/stt/sarvam.py +202 -0
  36. nitpick_voice-0.2.0/nitpick/services/tts/__init__.py +6 -0
  37. nitpick_voice-0.2.0/nitpick/services/tts/openai.py +233 -0
  38. nitpick_voice-0.2.0/nitpick/signaling/server.py +560 -0
  39. nitpick_voice-0.2.0/nitpick/transport/__init__.py +436 -0
  40. nitpick_voice-0.2.0/nitpick/transport/webrtc.py +311 -0
  41. nitpick_voice-0.2.0/nitpick/vad/__init__.py +27 -0
  42. nitpick_voice-0.2.0/nitpick/vad/backends.py +337 -0
  43. nitpick_voice-0.2.0/nitpick/vad/core.py +225 -0
  44. nitpick_voice-0.2.0/nitpick/vad/processor.py +144 -0
  45. nitpick_voice-0.2.0/nitpick/web/index.html +482 -0
  46. nitpick_voice-0.2.0/nitpick_voice.egg-info/PKG-INFO +434 -0
  47. nitpick_voice-0.2.0/nitpick_voice.egg-info/SOURCES.txt +62 -0
  48. nitpick_voice-0.2.0/nitpick_voice.egg-info/dependency_links.txt +1 -0
  49. nitpick_voice-0.2.0/nitpick_voice.egg-info/entry_points.txt +3 -0
  50. nitpick_voice-0.2.0/nitpick_voice.egg-info/requires.txt +31 -0
  51. nitpick_voice-0.2.0/nitpick_voice.egg-info/top_level.txt +1 -0
  52. nitpick_voice-0.2.0/pyproject.toml +78 -0
  53. nitpick_voice-0.2.0/setup.cfg +4 -0
  54. nitpick_voice-0.2.0/tests/test_billing_and_interruption.py +213 -0
  55. nitpick_voice-0.2.0/tests/test_docs.py +189 -0
  56. nitpick_voice-0.2.0/tests/test_end_to_end.py +158 -0
  57. nitpick_voice-0.2.0/tests/test_pipeline.py +140 -0
  58. nitpick_voice-0.2.0/tests/test_regressions.py +307 -0
  59. nitpick_voice-0.2.0/tests/test_rooms.py +264 -0
  60. nitpick_voice-0.2.0/tests/test_server_vad.py +180 -0
  61. nitpick_voice-0.2.0/tests/test_stt_turn_gate.py +167 -0
  62. nitpick_voice-0.2.0/tests/test_tts_sentences.py +37 -0
  63. nitpick_voice-0.2.0/tests/test_vad.py +237 -0
  64. 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.