livekit-plugins-duplexjev 0.2.0__tar.gz → 0.3.1__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.2.0 → livekit_plugins_duplexjev-0.3.1}/CHANGELOG.md +15 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.1}/PKG-INFO +20 -6
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.1}/README.md +19 -5
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.1}/livekit/plugins/duplexjev/turn_detector.py +30 -6
- livekit_plugins_duplexjev-0.3.1/livekit/plugins/duplexjev/version.py +1 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.1}/tests/test_turn_detector.py +23 -0
- livekit_plugins_duplexjev-0.2.0/livekit/plugins/duplexjev/version.py +0 -1
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.1}/.gitignore +0 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.1}/LICENSE +0 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.1}/examples/agent.py +0 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.1}/livekit/plugins/duplexjev/__init__.py +0 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.1}/livekit/plugins/duplexjev/log.py +0 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.1}/livekit/plugins/duplexjev/py.typed +0 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.1}/pyproject.toml +0 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.1}/tests/conftest.py +0 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.1}/tests/test_live.py +0 -0
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 (2026-10-13)
|
|
4
|
+
|
|
5
|
+
- Logs a one-time hint when the free trial API is used: it runs DuplexJev-4B-Para, so
|
|
6
|
+
its turn-taking does not match the published eot-bench numbers (DuplexJev-32B-Para v1.2 behind your own
|
|
7
|
+
`duplexjev gateway` >= 0.6).
|
|
8
|
+
- README: how to reproduce the eot-bench numbers; the self-hosting example now serves DuplexJev-32B-Para v1.2 with
|
|
9
|
+
`--rate 1000000`; eot-bench figure for the default setting updated to the full run (20.0%).
|
|
10
|
+
|
|
11
|
+
## 0.3.0 (2026-10-13)
|
|
12
|
+
|
|
13
|
+
- `readout="act"`: ask "What should the assistant do now?" and report P(reply now) + P(short acknowledgement) as the
|
|
14
|
+
end-of-turn probability (also gives the backchannel probability from the same answer). On LiveKit eot-bench
|
|
15
|
+
(English, 32B-Para v1.2, with the agent's previous message) false cut-offs at 300 ms: 18.4% vs 19.7%.
|
|
16
|
+
- `last_turn_class` (finished / not_finished / backchannel / hesitating, or reply / keep_listening / acknowledge).
|
|
17
|
+
|
|
3
18
|
## 0.2.0 (2026-10-13)
|
|
4
19
|
|
|
5
20
|
- Turn-taking context: `detector.attach(session)` (or `detector.set_assistant_said(text)`) sends the agent's previous
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: livekit-plugins-duplexjev
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -43,6 +43,12 @@ say, and tells your agent whether the user is **finished**, **still talking**, *
|
|
|
43
43
|
|
|
44
44
|
Tested with `livekit-agents` 1.8.6. Maintained by Adventists.ai (AI降临派).
|
|
45
45
|
|
|
46
|
+
> **Getting the published turn-taking numbers.** LiveKit eot-bench (English, false cut-offs at 300 ms): **20.0%** for
|
|
47
|
+
> DuplexJev-32B-Para v1.2 with the agent's previous message, 23.5% audio only, vs 35.2% for Smart Turn v3.2. That needs
|
|
48
|
+
> DuplexJev-32B-Para **v1.2 on your own GPU** behind `duplexjev gateway` >= 0.6 (see *Run the model yourself*), the
|
|
49
|
+
> default `readout="turn"`, and the agent's previous message (below). The free trial API runs the smaller DuplexJev-4B-Para
|
|
50
|
+
> (no v1.2 turn-taking update): use it to try the plugin, not to judge turn-taking quality.
|
|
51
|
+
|
|
46
52
|
## Install
|
|
47
53
|
|
|
48
54
|
```bash
|
|
@@ -75,8 +81,8 @@ session = AgentSession(..., turn_handling={"turn_detection": detector})
|
|
|
75
81
|
detector.attach(session) # follows conversation_item_added; or detector.set_assistant_said(text)
|
|
76
82
|
```
|
|
77
83
|
|
|
78
|
-
It needs a gateway from `duplexjev>=0.6`. On LiveKit eot-bench (English, DuplexJev-32B-Para v1.2) this cuts
|
|
79
|
-
cut-offs at a 300 ms budget from 23.5% to
|
|
84
|
+
It needs a gateway from `duplexjev>=0.6`. On LiveKit eot-bench (English, DuplexJev-32B-Para v1.2, full run) this cuts
|
|
85
|
+
false cut-offs at a 300 ms budget from 23.5% to 20.0%.
|
|
80
86
|
|
|
81
87
|
`AgentSession(turn_detection=duplexjev.TurnDetector(), ...)` works too (LiveKit marks that argument as deprecated).
|
|
82
88
|
A complete agent is in [`examples/agent.py`](examples/agent.py).
|
|
@@ -92,14 +98,18 @@ commits the turn, as with any turn detector, because the LLM needs the text.
|
|
|
92
98
|
|
|
93
99
|
## Run the model yourself (recommended for real-time)
|
|
94
100
|
|
|
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
|
|
101
|
+
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; it runs the smaller DuplexJev-4B-Para without the v1.2 turn-taking update). For a live agent, serve the model next to it. For turn-taking, use DuplexJev-32B-Para v1.2 on one 80 GB GPU (the eot-bench numbers above):
|
|
96
102
|
|
|
97
103
|
```bash
|
|
98
104
|
pip install "vllm[audio]>=0.29" duplexjev-vllm "duplexjev[server]>=0.6"
|
|
99
|
-
vllm serve adventists-ai/DuplexJev-
|
|
100
|
-
duplexjev gateway --vllm http://localhost:8000/v1 --port 8420
|
|
105
|
+
vllm serve adventists-ai/DuplexJev-32B-Para --revision v1.2 --max-model-len 4096 --port 8000 &
|
|
106
|
+
duplexjev gateway --vllm http://localhost:8000/v1 --port 8420 --rate 1000000
|
|
101
107
|
```
|
|
102
108
|
|
|
109
|
+
On a smaller GPU, `adventists-ai/DuplexJev-4B-Para` needs about 10 GB; it is fine for emotion and the other questions,
|
|
110
|
+
but its turn-taking has not had the v1.2 update. `--rate 1000000` lifts the gateway's default limit of 30 requests per
|
|
111
|
+
minute, which a live agent exceeds quickly.
|
|
112
|
+
|
|
103
113
|
Then pass `api_url="http://localhost:8420"` (or set `DUPLEXJEV_API_URL`).
|
|
104
114
|
|
|
105
115
|
## Options
|
|
@@ -117,6 +127,10 @@ Then pass `api_url="http://localhost:8420"` (or set `DUPLEXJEV_API_URL`).
|
|
|
117
127
|
| `trim_silence` | `True` | drop the silence before the user started talking (keeps 0.3 s) |
|
|
118
128
|
| `prediction_timeout` | `4.0` hosted, `1.0` own gateway | how long LiveKit waits for a prediction |
|
|
119
129
|
| `fallback_probability` | `1.0` | P(finished) reported if the gateway fails, so the conversation never blocks |
|
|
130
|
+
| `readout` | `"turn"` | `"act"` asks "What should the assistant do now?" (P(reply) + P(short acknowledgement)); slightly better on eot-bench with context |
|
|
131
|
+
|
|
132
|
+
After each prediction `detector.last_turn_class` is `finished`, `not_finished`, `backchannel` or `hesitating`
|
|
133
|
+
(`reply`, `keep_listening`, `acknowledge` with `readout="act"`), for your own routing.
|
|
120
134
|
|
|
121
135
|
## License
|
|
122
136
|
|
|
@@ -17,6 +17,12 @@ say, and tells your agent whether the user is **finished**, **still talking**, *
|
|
|
17
17
|
|
|
18
18
|
Tested with `livekit-agents` 1.8.6. Maintained by Adventists.ai (AI降临派).
|
|
19
19
|
|
|
20
|
+
> **Getting the published turn-taking numbers.** LiveKit eot-bench (English, false cut-offs at 300 ms): **20.0%** for
|
|
21
|
+
> DuplexJev-32B-Para v1.2 with the agent's previous message, 23.5% audio only, vs 35.2% for Smart Turn v3.2. That needs
|
|
22
|
+
> DuplexJev-32B-Para **v1.2 on your own GPU** behind `duplexjev gateway` >= 0.6 (see *Run the model yourself*), the
|
|
23
|
+
> default `readout="turn"`, and the agent's previous message (below). The free trial API runs the smaller DuplexJev-4B-Para
|
|
24
|
+
> (no v1.2 turn-taking update): use it to try the plugin, not to judge turn-taking quality.
|
|
25
|
+
|
|
20
26
|
## Install
|
|
21
27
|
|
|
22
28
|
```bash
|
|
@@ -49,8 +55,8 @@ session = AgentSession(..., turn_handling={"turn_detection": detector})
|
|
|
49
55
|
detector.attach(session) # follows conversation_item_added; or detector.set_assistant_said(text)
|
|
50
56
|
```
|
|
51
57
|
|
|
52
|
-
It needs a gateway from `duplexjev>=0.6`. On LiveKit eot-bench (English, DuplexJev-32B-Para v1.2) this cuts
|
|
53
|
-
cut-offs at a 300 ms budget from 23.5% to
|
|
58
|
+
It needs a gateway from `duplexjev>=0.6`. On LiveKit eot-bench (English, DuplexJev-32B-Para v1.2, full run) this cuts
|
|
59
|
+
false cut-offs at a 300 ms budget from 23.5% to 20.0%.
|
|
54
60
|
|
|
55
61
|
`AgentSession(turn_detection=duplexjev.TurnDetector(), ...)` works too (LiveKit marks that argument as deprecated).
|
|
56
62
|
A complete agent is in [`examples/agent.py`](examples/agent.py).
|
|
@@ -66,14 +72,18 @@ commits the turn, as with any turn detector, because the LLM needs the text.
|
|
|
66
72
|
|
|
67
73
|
## Run the model yourself (recommended for real-time)
|
|
68
74
|
|
|
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
|
|
75
|
+
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; it runs the smaller DuplexJev-4B-Para without the v1.2 turn-taking update). For a live agent, serve the model next to it. For turn-taking, use DuplexJev-32B-Para v1.2 on one 80 GB GPU (the eot-bench numbers above):
|
|
70
76
|
|
|
71
77
|
```bash
|
|
72
78
|
pip install "vllm[audio]>=0.29" duplexjev-vllm "duplexjev[server]>=0.6"
|
|
73
|
-
vllm serve adventists-ai/DuplexJev-
|
|
74
|
-
duplexjev gateway --vllm http://localhost:8000/v1 --port 8420
|
|
79
|
+
vllm serve adventists-ai/DuplexJev-32B-Para --revision v1.2 --max-model-len 4096 --port 8000 &
|
|
80
|
+
duplexjev gateway --vllm http://localhost:8000/v1 --port 8420 --rate 1000000
|
|
75
81
|
```
|
|
76
82
|
|
|
83
|
+
On a smaller GPU, `adventists-ai/DuplexJev-4B-Para` needs about 10 GB; it is fine for emotion and the other questions,
|
|
84
|
+
but its turn-taking has not had the v1.2 update. `--rate 1000000` lifts the gateway's default limit of 30 requests per
|
|
85
|
+
minute, which a live agent exceeds quickly.
|
|
86
|
+
|
|
77
87
|
Then pass `api_url="http://localhost:8420"` (or set `DUPLEXJEV_API_URL`).
|
|
78
88
|
|
|
79
89
|
## Options
|
|
@@ -91,6 +101,10 @@ Then pass `api_url="http://localhost:8420"` (or set `DUPLEXJEV_API_URL`).
|
|
|
91
101
|
| `trim_silence` | `True` | drop the silence before the user started talking (keeps 0.3 s) |
|
|
92
102
|
| `prediction_timeout` | `4.0` hosted, `1.0` own gateway | how long LiveKit waits for a prediction |
|
|
93
103
|
| `fallback_probability` | `1.0` | P(finished) reported if the gateway fails, so the conversation never blocks |
|
|
104
|
+
| `readout` | `"turn"` | `"act"` asks "What should the assistant do now?" (P(reply) + P(short acknowledgement)); slightly better on eot-bench with context |
|
|
105
|
+
|
|
106
|
+
After each prediction `detector.last_turn_class` is `finished`, `not_finished`, `backchannel` or `hesitating`
|
|
107
|
+
(`reply`, `keep_listening`, `acknowledge` with `readout="act"`), for your own routing.
|
|
94
108
|
|
|
95
109
|
## License
|
|
96
110
|
|
|
@@ -56,6 +56,9 @@ REPLY_QUESTION = {
|
|
|
56
56
|
"options": ["马上回复", "继续听", "简短回应一下"], "lang": "zh"},
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
TURN_CLASSES = ["finished", "not_finished", "backchannel", "hesitating"]
|
|
60
|
+
ACT_CLASSES = ["reply", "keep_listening", "acknowledge"]
|
|
61
|
+
|
|
59
62
|
PRESETS = {
|
|
60
63
|
"emotion": {"en": ("What is the speaker's emotional state?", ["neutral", "happy", "angry", "sad"]),
|
|
61
64
|
"zh": ("说话人当时的情绪状态是?", ["中性", "高兴", "生气", "伤心"])},
|
|
@@ -117,6 +120,10 @@ class TurnDetector:
|
|
|
117
120
|
extra_questions: names from ``PRESETS`` (``"emotion"``, ``"sound"``, ``"barge_in"``) or question dicts
|
|
118
121
|
``{"id", "text", "options"}``, answered in the same request; see ``last_answers``.
|
|
119
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, fast mode; the default
|
|
126
|
+
``"turn"`` scores 20.0% in a full eot-bench run).
|
|
120
127
|
window_s: seconds of the current user turn sent with each request (the most recent ones).
|
|
121
128
|
trim_silence: drop the quiet audio before the user started talking, keeping 0.3 s of pre-roll.
|
|
122
129
|
prediction_timeout: how long LiveKit waits for a prediction before committing without one. Defaults to
|
|
@@ -142,6 +149,7 @@ class TurnDetector:
|
|
|
142
149
|
prediction_timeout: float | None = None,
|
|
143
150
|
fallback_probability: float = 1.0,
|
|
144
151
|
http_session: aiohttp.ClientSession | None = None,
|
|
152
|
+
readout: str = "turn",
|
|
145
153
|
) -> None:
|
|
146
154
|
if lang not in TURN_QUESTION:
|
|
147
155
|
raise ValueError("lang must be 'en' or 'zh'")
|
|
@@ -161,11 +169,19 @@ class TurnDetector:
|
|
|
161
169
|
HOSTED_PREDICTION_TIMEOUT if self._base_url == DEFAULT_URL else SELF_HOSTED_PREDICTION_TIMEOUT
|
|
162
170
|
)
|
|
163
171
|
self._prediction_timeout = prediction_timeout
|
|
172
|
+
if self._base_url == DEFAULT_URL:
|
|
173
|
+
logger.info("Using the free DuplexJev trial API (DuplexJev-4B-Para, rate limited). "
|
|
174
|
+
"For the turn-taking quality reported on eot-bench, serve DuplexJev-32B-Para v1.2 behind "
|
|
175
|
+
"`duplexjev gateway` >= 0.6 and pass its URL; see the README.")
|
|
164
176
|
self._fallback_probability = fallback_probability
|
|
165
177
|
self._http_session = http_session
|
|
166
178
|
|
|
167
|
-
|
|
168
|
-
|
|
179
|
+
if readout not in ("turn", "act"):
|
|
180
|
+
raise ValueError("readout must be 'turn' or 'act'")
|
|
181
|
+
self._readout = readout
|
|
182
|
+
main = dict(REPLY_QUESTION[lang], id="turn") if readout == "act" else TURN_QUESTION[lang]
|
|
183
|
+
self._questions: list[dict[str, Any]] = [main]
|
|
184
|
+
if backchannel_threshold is not None and readout == "turn":
|
|
169
185
|
self._questions.append(REPLY_QUESTION[lang])
|
|
170
186
|
self._questions += [self._as_question(q) for q in (extra_questions or [])]
|
|
171
187
|
ids = [q["id"] for q in self._questions]
|
|
@@ -177,6 +193,9 @@ class TurnDetector:
|
|
|
177
193
|
"""Answers of the latest successful request: ``{id: {"answer", "confidence", "probs"}}``."""
|
|
178
194
|
self.last_server_ms: float | None = None
|
|
179
195
|
"""Server-side model time of the latest request, in milliseconds."""
|
|
196
|
+
self.last_turn_class: str | None = None
|
|
197
|
+
"""Most likely class of the latest request: finished / not_finished / backchannel / hesitating
|
|
198
|
+
(``readout="act"``: reply / keep_listening / acknowledge)."""
|
|
180
199
|
self.assistant_said: str = ""
|
|
181
200
|
"""The agent's previous message, sent with every request (last 300 characters)."""
|
|
182
201
|
|
|
@@ -404,9 +423,12 @@ class TurnDetectorStream:
|
|
|
404
423
|
data = await resp.json()
|
|
405
424
|
answers: dict[str, Any] = data["answers"]
|
|
406
425
|
probs = answers["turn"]["probs"]
|
|
407
|
-
o = TURN_QUESTION[det._lang]["options"]
|
|
408
|
-
# finished + backchannel
|
|
426
|
+
o = (REPLY_QUESTION if det._readout == "act" else TURN_QUESTION)[det._lang]["options"]
|
|
427
|
+
# turn: finished + backchannel (a short "ok" with nobody else talking also ends the turn);
|
|
428
|
+
# act: reply now + short acknowledgement
|
|
409
429
|
p_finished = float(probs.get(o[0], 0.0)) + float(probs.get(o[2], 0.0))
|
|
430
|
+
names = ACT_CLASSES if det._readout == "act" else TURN_CLASSES
|
|
431
|
+
det.last_turn_class = names[max(range(len(o)), key=lambda i: float(probs.get(o[i], 0.0)))]
|
|
410
432
|
except asyncio.CancelledError:
|
|
411
433
|
raise
|
|
412
434
|
except Exception as e: # network or server error: never block the conversation
|
|
@@ -425,8 +447,10 @@ class TurnDetectorStream:
|
|
|
425
447
|
if data.get("model"):
|
|
426
448
|
det._model_name = str(data["model"])
|
|
427
449
|
backchannel = None
|
|
428
|
-
if
|
|
429
|
-
|
|
450
|
+
if det._backchannel_threshold is not None:
|
|
451
|
+
src = answers["turn"] if det._readout == "act" else answers.get("reply")
|
|
452
|
+
if src is not None:
|
|
453
|
+
backchannel = float(src["probs"].get(REPLY_QUESTION[det._lang]["options"][2], 0.0))
|
|
430
454
|
now = time.time()
|
|
431
455
|
self.last_answers = det.last_answers = answers
|
|
432
456
|
self.last_server_ms = det.last_server_ms = server_ms
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.1"
|
{livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.1}/tests/test_turn_detector.py
RENAMED
|
@@ -205,3 +205,26 @@ async def test_assistant_said_is_sent_and_attach_follows_session(gateway):
|
|
|
205
205
|
det.set_assistant_said("y" * 500)
|
|
206
206
|
assert len(det.assistant_said) == 300
|
|
207
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.2.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.1}/livekit/plugins/duplexjev/log.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|