livekit-plugins-duplexjev 0.2.0__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.2.0 → livekit_plugins_duplexjev-0.3.0}/CHANGELOG.md +7 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.0}/PKG-INFO +5 -1
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.0}/README.md +4 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.0}/livekit/plugins/duplexjev/turn_detector.py +25 -6
- livekit_plugins_duplexjev-0.3.0/livekit/plugins/duplexjev/version.py +1 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.0}/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.0}/.gitignore +0 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.0}/LICENSE +0 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.0}/examples/agent.py +0 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.0}/livekit/plugins/duplexjev/__init__.py +0 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.0}/livekit/plugins/duplexjev/log.py +0 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.0}/livekit/plugins/duplexjev/py.typed +0 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.0}/pyproject.toml +0 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.0}/tests/conftest.py +0 -0
- {livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.0}/tests/test_live.py +0 -0
|
@@ -1,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## 0.2.0 (2026-10-13)
|
|
4
11
|
|
|
5
12
|
- 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.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
|
|
@@ -117,6 +117,10 @@ Then pass `api_url="http://localhost:8420"` (or set `DUPLEXJEV_API_URL`).
|
|
|
117
117
|
| `trim_silence` | `True` | drop the silence before the user started talking (keeps 0.3 s) |
|
|
118
118
|
| `prediction_timeout` | `4.0` hosted, `1.0` own gateway | how long LiveKit waits for a prediction |
|
|
119
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.
|
|
120
124
|
|
|
121
125
|
## License
|
|
122
126
|
|
|
@@ -91,6 +91,10 @@ Then pass `api_url="http://localhost:8420"` (or set `DUPLEXJEV_API_URL`).
|
|
|
91
91
|
| `trim_silence` | `True` | drop the silence before the user started talking (keeps 0.3 s) |
|
|
92
92
|
| `prediction_timeout` | `4.0` hosted, `1.0` own gateway | how long LiveKit waits for a prediction |
|
|
93
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.
|
|
94
98
|
|
|
95
99
|
## License
|
|
96
100
|
|
|
@@ -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,9 @@ 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).
|
|
120
126
|
window_s: seconds of the current user turn sent with each request (the most recent ones).
|
|
121
127
|
trim_silence: drop the quiet audio before the user started talking, keeping 0.3 s of pre-roll.
|
|
122
128
|
prediction_timeout: how long LiveKit waits for a prediction before committing without one. Defaults to
|
|
@@ -142,6 +148,7 @@ class TurnDetector:
|
|
|
142
148
|
prediction_timeout: float | None = None,
|
|
143
149
|
fallback_probability: float = 1.0,
|
|
144
150
|
http_session: aiohttp.ClientSession | None = None,
|
|
151
|
+
readout: str = "turn",
|
|
145
152
|
) -> None:
|
|
146
153
|
if lang not in TURN_QUESTION:
|
|
147
154
|
raise ValueError("lang must be 'en' or 'zh'")
|
|
@@ -164,8 +171,12 @@ class TurnDetector:
|
|
|
164
171
|
self._fallback_probability = fallback_probability
|
|
165
172
|
self._http_session = http_session
|
|
166
173
|
|
|
167
|
-
|
|
168
|
-
|
|
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":
|
|
169
180
|
self._questions.append(REPLY_QUESTION[lang])
|
|
170
181
|
self._questions += [self._as_question(q) for q in (extra_questions or [])]
|
|
171
182
|
ids = [q["id"] for q in self._questions]
|
|
@@ -177,6 +188,9 @@ class TurnDetector:
|
|
|
177
188
|
"""Answers of the latest successful request: ``{id: {"answer", "confidence", "probs"}}``."""
|
|
178
189
|
self.last_server_ms: float | None = None
|
|
179
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)."""
|
|
180
194
|
self.assistant_said: str = ""
|
|
181
195
|
"""The agent's previous message, sent with every request (last 300 characters)."""
|
|
182
196
|
|
|
@@ -404,9 +418,12 @@ class TurnDetectorStream:
|
|
|
404
418
|
data = await resp.json()
|
|
405
419
|
answers: dict[str, Any] = data["answers"]
|
|
406
420
|
probs = answers["turn"]["probs"]
|
|
407
|
-
o = TURN_QUESTION[det._lang]["options"]
|
|
408
|
-
# 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
|
|
409
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)))]
|
|
410
427
|
except asyncio.CancelledError:
|
|
411
428
|
raise
|
|
412
429
|
except Exception as e: # network or server error: never block the conversation
|
|
@@ -425,8 +442,10 @@ class TurnDetectorStream:
|
|
|
425
442
|
if data.get("model"):
|
|
426
443
|
det._model_name = str(data["model"])
|
|
427
444
|
backchannel = None
|
|
428
|
-
if
|
|
429
|
-
|
|
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))
|
|
430
449
|
now = time.time()
|
|
431
450
|
self.last_answers = det.last_answers = answers
|
|
432
451
|
self.last_server_ms = det.last_server_ms = server_ms
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
{livekit_plugins_duplexjev-0.2.0 → livekit_plugins_duplexjev-0.3.0}/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.0}/livekit/plugins/duplexjev/log.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|