livekit-plugins-duplexjev 0.1.1__tar.gz → 0.3.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.
- {livekit_plugins_duplexjev-0.1.1 → livekit_plugins_duplexjev-0.3.0}/CHANGELOG.md +13 -0
- {livekit_plugins_duplexjev-0.1.1 → livekit_plugins_duplexjev-0.3.0}/PKG-INFO +19 -3
- {livekit_plugins_duplexjev-0.1.1 → livekit_plugins_duplexjev-0.3.0}/README.md +18 -2
- {livekit_plugins_duplexjev-0.1.1 → livekit_plugins_duplexjev-0.3.0}/livekit/plugins/duplexjev/turn_detector.py +52 -6
- livekit_plugins_duplexjev-0.3.0/livekit/plugins/duplexjev/version.py +1 -0
- {livekit_plugins_duplexjev-0.1.1 → livekit_plugins_duplexjev-0.3.0}/tests/test_turn_detector.py +56 -0
- livekit_plugins_duplexjev-0.1.1/livekit/plugins/duplexjev/version.py +0 -1
- {livekit_plugins_duplexjev-0.1.1 → livekit_plugins_duplexjev-0.3.0}/.gitignore +0 -0
- {livekit_plugins_duplexjev-0.1.1 → livekit_plugins_duplexjev-0.3.0}/LICENSE +0 -0
- {livekit_plugins_duplexjev-0.1.1 → livekit_plugins_duplexjev-0.3.0}/examples/agent.py +0 -0
- {livekit_plugins_duplexjev-0.1.1 → livekit_plugins_duplexjev-0.3.0}/livekit/plugins/duplexjev/__init__.py +0 -0
- {livekit_plugins_duplexjev-0.1.1 → livekit_plugins_duplexjev-0.3.0}/livekit/plugins/duplexjev/log.py +0 -0
- {livekit_plugins_duplexjev-0.1.1 → livekit_plugins_duplexjev-0.3.0}/livekit/plugins/duplexjev/py.typed +0 -0
- {livekit_plugins_duplexjev-0.1.1 → livekit_plugins_duplexjev-0.3.0}/pyproject.toml +0 -0
- {livekit_plugins_duplexjev-0.1.1 → livekit_plugins_duplexjev-0.3.0}/tests/conftest.py +0 -0
- {livekit_plugins_duplexjev-0.1.1 → livekit_plugins_duplexjev-0.3.0}/tests/test_live.py +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 (2026-10-13)
|
|
4
|
+
|
|
5
|
+
- `readout="act"`: ask "What should the assistant do now?" and report P(reply now) + P(short acknowledgement) as the
|
|
6
|
+
end-of-turn probability (also gives the backchannel probability from the same answer). On LiveKit eot-bench
|
|
7
|
+
(English, 32B-Para v1.2, with the agent's previous message) false cut-offs at 300 ms: 18.4% vs 19.7%.
|
|
8
|
+
- `last_turn_class` (finished / not_finished / backchannel / hesitating, or reply / keep_listening / acknowledge).
|
|
9
|
+
|
|
10
|
+
## 0.2.0 (2026-10-13)
|
|
11
|
+
|
|
12
|
+
- Turn-taking context: `detector.attach(session)` (or `detector.set_assistant_said(text)`) sends the agent's previous
|
|
13
|
+
message with every request. Needs a gateway from `duplexjev>=0.6`. On LiveKit eot-bench (English,
|
|
14
|
+
DuplexJev-32B-Para v1.2) false cut-offs at a 300 ms budget drop from 23.5% to 19.7%.
|
|
15
|
+
|
|
3
16
|
## 0.1.1 (2026-10-12)
|
|
4
17
|
|
|
5
18
|
- The turn probability is now P(finished) + P(backchannel): a short "ok" / "yeah" with nobody else talking also
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: livekit-plugins-duplexjev
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Audio end-of-turn detection for LiveKit Agents with DuplexJev: turn state, emotion and non-verbal sounds from one request, no transcript needed.
|
|
5
5
|
Project-URL: Homepage, https://github.com/adventists-ai/duplexjev/tree/main/integrations/livekit
|
|
6
6
|
Project-URL: Models, https://huggingface.co/adventists-ai/DuplexJev-4B-Para
|
|
@@ -66,6 +66,18 @@ session = AgentSession(
|
|
|
66
66
|
)
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
**Give it the agent's last message** (new in 0.2.0). Turn-taking is clearly better when the model knows what the
|
|
70
|
+
agent had just said. Keep a reference to the detector and attach it to the session once:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
detector = duplexjev.TurnDetector(api_url="http://localhost:8420")
|
|
74
|
+
session = AgentSession(..., turn_handling={"turn_detection": detector})
|
|
75
|
+
detector.attach(session) # follows conversation_item_added; or detector.set_assistant_said(text)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
It needs a gateway from `duplexjev>=0.6`. On LiveKit eot-bench (English, DuplexJev-32B-Para v1.2) this cuts false
|
|
79
|
+
cut-offs at a 300 ms budget from 23.5% to 19.7%.
|
|
80
|
+
|
|
69
81
|
`AgentSession(turn_detection=duplexjev.TurnDetector(), ...)` works too (LiveKit marks that argument as deprecated).
|
|
70
82
|
A complete agent is in [`examples/agent.py`](examples/agent.py).
|
|
71
83
|
|
|
@@ -80,10 +92,10 @@ commits the turn, as with any turn detector, because the LLM needs the text.
|
|
|
80
92
|
|
|
81
93
|
## Run the model yourself (recommended for real-time)
|
|
82
94
|
|
|
83
|
-
The default URL is our free trial API (rate limited to 30 requests per minute; the round trip from outside mainland China adds network latency). For a live agent, serve the model next to it (DuplexJev-4B-Para needs about 10 GB of GPU memory):
|
|
95
|
+
The default URL is our free trial API (rate limited to 30 requests per minute; the round trip from outside mainland China adds network latency). For a live agent, serve the model next to it (DuplexJev-4B-Para needs about 10 GB of GPU memory; for the best turn-taking use DuplexJev-32B-Para v1.2 on one 80 GB GPU):
|
|
84
96
|
|
|
85
97
|
```bash
|
|
86
|
-
pip install "vllm[audio]>=0.29" duplexjev-vllm "duplexjev[server]>=0.
|
|
98
|
+
pip install "vllm[audio]>=0.29" duplexjev-vllm "duplexjev[server]>=0.6"
|
|
87
99
|
vllm serve adventists-ai/DuplexJev-4B-Para --max-model-len 4096 --port 8000 &
|
|
88
100
|
duplexjev gateway --vllm http://localhost:8000/v1 --port 8420
|
|
89
101
|
```
|
|
@@ -105,6 +117,10 @@ Then pass `api_url="http://localhost:8420"` (or set `DUPLEXJEV_API_URL`).
|
|
|
105
117
|
| `trim_silence` | `True` | drop the silence before the user started talking (keeps 0.3 s) |
|
|
106
118
|
| `prediction_timeout` | `4.0` hosted, `1.0` own gateway | how long LiveKit waits for a prediction |
|
|
107
119
|
| `fallback_probability` | `1.0` | P(finished) reported if the gateway fails, so the conversation never blocks |
|
|
120
|
+
| `readout` | `"turn"` | `"act"` asks "What should the assistant do now?" (P(reply) + P(short acknowledgement)); slightly better on eot-bench with context |
|
|
121
|
+
|
|
122
|
+
After each prediction `detector.last_turn_class` is `finished`, `not_finished`, `backchannel` or `hesitating`
|
|
123
|
+
(`reply`, `keep_listening`, `acknowledge` with `readout="act"`), for your own routing.
|
|
108
124
|
|
|
109
125
|
## License
|
|
110
126
|
|
|
@@ -40,6 +40,18 @@ session = AgentSession(
|
|
|
40
40
|
)
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
**Give it the agent's last message** (new in 0.2.0). Turn-taking is clearly better when the model knows what the
|
|
44
|
+
agent had just said. Keep a reference to the detector and attach it to the session once:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
detector = duplexjev.TurnDetector(api_url="http://localhost:8420")
|
|
48
|
+
session = AgentSession(..., turn_handling={"turn_detection": detector})
|
|
49
|
+
detector.attach(session) # follows conversation_item_added; or detector.set_assistant_said(text)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
It needs a gateway from `duplexjev>=0.6`. On LiveKit eot-bench (English, DuplexJev-32B-Para v1.2) this cuts false
|
|
53
|
+
cut-offs at a 300 ms budget from 23.5% to 19.7%.
|
|
54
|
+
|
|
43
55
|
`AgentSession(turn_detection=duplexjev.TurnDetector(), ...)` works too (LiveKit marks that argument as deprecated).
|
|
44
56
|
A complete agent is in [`examples/agent.py`](examples/agent.py).
|
|
45
57
|
|
|
@@ -54,10 +66,10 @@ commits the turn, as with any turn detector, because the LLM needs the text.
|
|
|
54
66
|
|
|
55
67
|
## Run the model yourself (recommended for real-time)
|
|
56
68
|
|
|
57
|
-
The default URL is our free trial API (rate limited to 30 requests per minute; the round trip from outside mainland China adds network latency). For a live agent, serve the model next to it (DuplexJev-4B-Para needs about 10 GB of GPU memory):
|
|
69
|
+
The default URL is our free trial API (rate limited to 30 requests per minute; the round trip from outside mainland China adds network latency). For a live agent, serve the model next to it (DuplexJev-4B-Para needs about 10 GB of GPU memory; for the best turn-taking use DuplexJev-32B-Para v1.2 on one 80 GB GPU):
|
|
58
70
|
|
|
59
71
|
```bash
|
|
60
|
-
pip install "vllm[audio]>=0.29" duplexjev-vllm "duplexjev[server]>=0.
|
|
72
|
+
pip install "vllm[audio]>=0.29" duplexjev-vllm "duplexjev[server]>=0.6"
|
|
61
73
|
vllm serve adventists-ai/DuplexJev-4B-Para --max-model-len 4096 --port 8000 &
|
|
62
74
|
duplexjev gateway --vllm http://localhost:8000/v1 --port 8420
|
|
63
75
|
```
|
|
@@ -79,6 +91,10 @@ Then pass `api_url="http://localhost:8420"` (or set `DUPLEXJEV_API_URL`).
|
|
|
79
91
|
| `trim_silence` | `True` | drop the silence before the user started talking (keeps 0.3 s) |
|
|
80
92
|
| `prediction_timeout` | `4.0` hosted, `1.0` own gateway | how long LiveKit waits for a prediction |
|
|
81
93
|
| `fallback_probability` | `1.0` | P(finished) reported if the gateway fails, so the conversation never blocks |
|
|
94
|
+
| `readout` | `"turn"` | `"act"` asks "What should the assistant do now?" (P(reply) + P(short acknowledgement)); slightly better on eot-bench with context |
|
|
95
|
+
|
|
96
|
+
After each prediction `detector.last_turn_class` is `finished`, `not_finished`, `backchannel` or `hesitating`
|
|
97
|
+
(`reply`, `keep_listening`, `acknowledge` with `readout="act"`), for your own routing.
|
|
82
98
|
|
|
83
99
|
## License
|
|
84
100
|
|
|
@@ -8,6 +8,10 @@ exposed on ``last_answers`` / ``on_answers``.
|
|
|
8
8
|
This module implements LiveKit's *streaming* (audio) turn-detector protocol: ``AgentSession`` pushes every input
|
|
9
9
|
audio frame into the stream, calls ``predict()`` once the VAD has seen ~200 ms of silence, and waits up to
|
|
10
10
|
``prediction_timeout`` for the resulting ``TurnDetectionEvent``.
|
|
11
|
+
|
|
12
|
+
Turn-taking is markedly better when the model also knows what the agent had just said. Call
|
|
13
|
+
``detector.attach(session)`` once after creating the ``AgentSession`` (it follows ``conversation_item_added``), or
|
|
14
|
+
set it yourself with ``detector.set_assistant_said(text)``.
|
|
11
15
|
"""
|
|
12
16
|
|
|
13
17
|
from __future__ import annotations
|
|
@@ -52,6 +56,9 @@ REPLY_QUESTION = {
|
|
|
52
56
|
"options": ["马上回复", "继续听", "简短回应一下"], "lang": "zh"},
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
TURN_CLASSES = ["finished", "not_finished", "backchannel", "hesitating"]
|
|
60
|
+
ACT_CLASSES = ["reply", "keep_listening", "acknowledge"]
|
|
61
|
+
|
|
55
62
|
PRESETS = {
|
|
56
63
|
"emotion": {"en": ("What is the speaker's emotional state?", ["neutral", "happy", "angry", "sad"]),
|
|
57
64
|
"zh": ("说话人当时的情绪状态是?", ["中性", "高兴", "生气", "伤心"])},
|
|
@@ -113,6 +120,9 @@ class TurnDetector:
|
|
|
113
120
|
extra_questions: names from ``PRESETS`` (``"emotion"``, ``"sound"``, ``"barge_in"``) or question dicts
|
|
114
121
|
``{"id", "text", "options"}``, answered in the same request; see ``last_answers``.
|
|
115
122
|
on_answers: optional callback ``f(answers: dict)`` called after every successful prediction.
|
|
123
|
+
readout: ``"turn"`` (turn-state question, default) or ``"act"``: ask "What should the assistant do now?" and
|
|
124
|
+
report P(reply now) + P(short acknowledgement); slightly better on eot-bench with the agent's previous
|
|
125
|
+
message (18.4% vs 19.7% false cut-offs at 300 ms, English, 32B-Para v1.2).
|
|
116
126
|
window_s: seconds of the current user turn sent with each request (the most recent ones).
|
|
117
127
|
trim_silence: drop the quiet audio before the user started talking, keeping 0.3 s of pre-roll.
|
|
118
128
|
prediction_timeout: how long LiveKit waits for a prediction before committing without one. Defaults to
|
|
@@ -138,6 +148,7 @@ class TurnDetector:
|
|
|
138
148
|
prediction_timeout: float | None = None,
|
|
139
149
|
fallback_probability: float = 1.0,
|
|
140
150
|
http_session: aiohttp.ClientSession | None = None,
|
|
151
|
+
readout: str = "turn",
|
|
141
152
|
) -> None:
|
|
142
153
|
if lang not in TURN_QUESTION:
|
|
143
154
|
raise ValueError("lang must be 'en' or 'zh'")
|
|
@@ -160,8 +171,12 @@ class TurnDetector:
|
|
|
160
171
|
self._fallback_probability = fallback_probability
|
|
161
172
|
self._http_session = http_session
|
|
162
173
|
|
|
163
|
-
|
|
164
|
-
|
|
174
|
+
if readout not in ("turn", "act"):
|
|
175
|
+
raise ValueError("readout must be 'turn' or 'act'")
|
|
176
|
+
self._readout = readout
|
|
177
|
+
main = dict(REPLY_QUESTION[lang], id="turn") if readout == "act" else TURN_QUESTION[lang]
|
|
178
|
+
self._questions: list[dict[str, Any]] = [main]
|
|
179
|
+
if backchannel_threshold is not None and readout == "turn":
|
|
165
180
|
self._questions.append(REPLY_QUESTION[lang])
|
|
166
181
|
self._questions += [self._as_question(q) for q in (extra_questions or [])]
|
|
167
182
|
ids = [q["id"] for q in self._questions]
|
|
@@ -173,6 +188,30 @@ class TurnDetector:
|
|
|
173
188
|
"""Answers of the latest successful request: ``{id: {"answer", "confidence", "probs"}}``."""
|
|
174
189
|
self.last_server_ms: float | None = None
|
|
175
190
|
"""Server-side model time of the latest request, in milliseconds."""
|
|
191
|
+
self.last_turn_class: str | None = None
|
|
192
|
+
"""Most likely class of the latest request: finished / not_finished / backchannel / hesitating
|
|
193
|
+
(``readout="act"``: reply / keep_listening / acknowledge)."""
|
|
194
|
+
self.assistant_said: str = ""
|
|
195
|
+
"""The agent's previous message, sent with every request (last 300 characters)."""
|
|
196
|
+
|
|
197
|
+
def set_assistant_said(self, text: str | None) -> None:
|
|
198
|
+
"""What the agent said just before the user's current turn."""
|
|
199
|
+
self.assistant_said = " ".join((text or "").split())[-300:]
|
|
200
|
+
|
|
201
|
+
def attach(self, session: Any) -> None:
|
|
202
|
+
"""Keep ``assistant_said`` up to date from an ``AgentSession``'s ``conversation_item_added`` events."""
|
|
203
|
+
|
|
204
|
+
def _on_item(ev: Any) -> None:
|
|
205
|
+
item = getattr(ev, "item", None)
|
|
206
|
+
if getattr(item, "role", None) != "assistant":
|
|
207
|
+
return
|
|
208
|
+
text = getattr(item, "text_content", None)
|
|
209
|
+
if callable(text):
|
|
210
|
+
text = text()
|
|
211
|
+
if text:
|
|
212
|
+
self.set_assistant_said(text)
|
|
213
|
+
|
|
214
|
+
session.on("conversation_item_added", _on_item)
|
|
176
215
|
|
|
177
216
|
def _as_question(self, q: str | dict[str, Any]) -> dict[str, Any]:
|
|
178
217
|
if isinstance(q, str):
|
|
@@ -366,6 +405,8 @@ class TurnDetectorStream:
|
|
|
366
405
|
det = self._detector
|
|
367
406
|
payload = {"audio_b64": base64.b64encode(wav).decode(), "format": "wav", "lang": det._lang,
|
|
368
407
|
"questions": det._questions}
|
|
408
|
+
if det.assistant_said:
|
|
409
|
+
payload["assistant_said"] = det.assistant_said
|
|
369
410
|
try:
|
|
370
411
|
async with self._http().post(
|
|
371
412
|
det._decide_url, json=payload, headers=det._headers,
|
|
@@ -377,9 +418,12 @@ class TurnDetectorStream:
|
|
|
377
418
|
data = await resp.json()
|
|
378
419
|
answers: dict[str, Any] = data["answers"]
|
|
379
420
|
probs = answers["turn"]["probs"]
|
|
380
|
-
o = TURN_QUESTION[det._lang]["options"]
|
|
381
|
-
# finished + backchannel
|
|
421
|
+
o = (REPLY_QUESTION if det._readout == "act" else TURN_QUESTION)[det._lang]["options"]
|
|
422
|
+
# turn: finished + backchannel (a short "ok" with nobody else talking also ends the turn);
|
|
423
|
+
# act: reply now + short acknowledgement
|
|
382
424
|
p_finished = float(probs.get(o[0], 0.0)) + float(probs.get(o[2], 0.0))
|
|
425
|
+
names = ACT_CLASSES if det._readout == "act" else TURN_CLASSES
|
|
426
|
+
det.last_turn_class = names[max(range(len(o)), key=lambda i: float(probs.get(o[i], 0.0)))]
|
|
383
427
|
except asyncio.CancelledError:
|
|
384
428
|
raise
|
|
385
429
|
except Exception as e: # network or server error: never block the conversation
|
|
@@ -398,8 +442,10 @@ class TurnDetectorStream:
|
|
|
398
442
|
if data.get("model"):
|
|
399
443
|
det._model_name = str(data["model"])
|
|
400
444
|
backchannel = None
|
|
401
|
-
if
|
|
402
|
-
|
|
445
|
+
if det._backchannel_threshold is not None:
|
|
446
|
+
src = answers["turn"] if det._readout == "act" else answers.get("reply")
|
|
447
|
+
if src is not None:
|
|
448
|
+
backchannel = float(src["probs"].get(REPLY_QUESTION[det._lang]["options"][2], 0.0))
|
|
403
449
|
now = time.time()
|
|
404
450
|
self.last_answers = det.last_answers = answers
|
|
405
451
|
self.last_server_ms = det.last_server_ms = server_ms
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
{livekit_plugins_duplexjev-0.1.1 → livekit_plugins_duplexjev-0.3.0}/tests/test_turn_detector.py
RENAMED
|
@@ -172,3 +172,59 @@ async def test_agent_session_accepts_it(gateway):
|
|
|
172
172
|
det = duplexjev.TurnDetector(api_url=gateway.url)
|
|
173
173
|
session = AgentSession(turn_handling={"turn_detection": det})
|
|
174
174
|
assert session.turn_detection is det
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def test_assistant_said_is_sent_and_attach_follows_session(gateway):
|
|
178
|
+
det = duplexjev.TurnDetector(api_url=gateway.url)
|
|
179
|
+
stream = det.stream()
|
|
180
|
+
push(stream, 1.0)
|
|
181
|
+
await asyncio.wait_for(stream.predict(), 2)
|
|
182
|
+
assert "assistant_said" not in gateway.requests[-1]
|
|
183
|
+
|
|
184
|
+
handlers = {}
|
|
185
|
+
|
|
186
|
+
class FakeSession:
|
|
187
|
+
def on(self, name, fn):
|
|
188
|
+
handlers[name] = fn
|
|
189
|
+
|
|
190
|
+
class Item:
|
|
191
|
+
def __init__(self, role, text):
|
|
192
|
+
self.role, self.text_content = role, text
|
|
193
|
+
|
|
194
|
+
class Ev:
|
|
195
|
+
def __init__(self, item):
|
|
196
|
+
self.item = item
|
|
197
|
+
|
|
198
|
+
det.attach(FakeSession())
|
|
199
|
+
handlers["conversation_item_added"](Ev(Item("user", "hello")))
|
|
200
|
+
assert det.assistant_said == ""
|
|
201
|
+
handlers["conversation_item_added"](Ev(Item("assistant", "Sure, what time\nworks for you?")))
|
|
202
|
+
assert det.assistant_said == "Sure, what time works for you?"
|
|
203
|
+
await asyncio.wait_for(stream.predict(), 2)
|
|
204
|
+
assert gateway.requests[-1]["assistant_said"] == "Sure, what time works for you?"
|
|
205
|
+
det.set_assistant_said("y" * 500)
|
|
206
|
+
assert len(det.assistant_said) == 300
|
|
207
|
+
await stream.aclose()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def test_act_readout_and_turn_class(gateway):
|
|
211
|
+
det = duplexjev.TurnDetector(api_url=gateway.url, readout="act", backchannel_threshold=0.5)
|
|
212
|
+
stream = det.stream()
|
|
213
|
+
push(stream, 1.0)
|
|
214
|
+
gateway.p_finished = 0.8
|
|
215
|
+
ev = await asyncio.wait_for(stream.predict(), 2)
|
|
216
|
+
qs = gateway.requests[-1]["questions"]
|
|
217
|
+
assert [q["id"] for q in qs] == ["turn"] and qs[0]["text"] == "What should the assistant do now?"
|
|
218
|
+
assert ev.end_of_turn_probability == pytest.approx(0.8) and det.last_turn_class == "reply"
|
|
219
|
+
assert ev.backchannel_probability == pytest.approx(0.0)
|
|
220
|
+
gateway.p_finished = 0.2
|
|
221
|
+
ev = await asyncio.wait_for(stream.predict(), 2)
|
|
222
|
+
assert det.last_turn_class == "keep_listening"
|
|
223
|
+
await stream.aclose()
|
|
224
|
+
t = duplexjev.TurnDetector(api_url=gateway.url)
|
|
225
|
+
s = t.stream(); push(s, 1.0); gateway.p_finished = 0.9
|
|
226
|
+
await asyncio.wait_for(s.predict(), 2)
|
|
227
|
+
assert t.last_turn_class == "finished"
|
|
228
|
+
await s.aclose()
|
|
229
|
+
with pytest.raises(ValueError):
|
|
230
|
+
duplexjev.TurnDetector(readout="x")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{livekit_plugins_duplexjev-0.1.1 → livekit_plugins_duplexjev-0.3.0}/livekit/plugins/duplexjev/log.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|