Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -676,8 +676,10 @@ class _LangStats(NamedTuple):
class _TokenAccumulator:
"""Accumulates token metadata (text, language, speaker, timing, confidence).

Tokens are assumed to arrive in chronological order, so start_time is taken
from the first token and end_time is continuously overwritten by the latest.
Tokens are assumed to arrive in chronological order, but individual tokens
may omit timing keys. start_time is the earliest ``start_ms`` seen and
end_time the latest ``end_ms`` seen, so a token that carries timing late
(or regresses) cannot collapse the span onto itself.
"""

def __init__(self) -> None:
Expand Down Expand Up @@ -714,11 +716,13 @@ def update(self, token: dict[str, Any]) -> None:
self.language = self._get_language()
if "speaker" in token and self.speaker_id is None:
self.speaker_id = str(token["speaker"])
if "start_ms" in token and not self._has_start_time:
self._has_start_time = True
self.start_time = float(token["start_ms"])
if "start_ms" in token:
start_ms = float(token["start_ms"])
if not self._has_start_time or start_ms < self.start_time:
self._has_start_time = True
self.start_time = start_ms
if "end_ms" in token:
self.end_time = float(token["end_ms"])
self.end_time = max(self.end_time, float(token["end_ms"]))
if "confidence" in token:
self._confidence_sum += token["confidence"]
self._confidence_count += 1
Expand Down
61 changes: 61 additions & 0 deletions tests/test_plugin_soniox_stt.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,67 @@ def test_token_accumulator_lang_segments_empty_initially():
assert accumulator._lang_segments == []


def test_token_accumulator_span_covers_all_timed_tokens():
"""A token that carries timing late must not collapse the span onto itself.

Regression for #6885: when the leading tokens of an utterance arrive
without ``start_ms``/``end_ms`` and only a trailing token has them, the
emitted span used to collapse onto that trailing token (e.g. two words in
180ms).
"""
from livekit.plugins.soniox.stt import _TokenAccumulator

accumulator = _TokenAccumulator()
accumulator.update({"text": " He's", "language": "en", "is_final": True})
accumulator.update({"text": " trying", "language": "en", "is_final": True})
accumulator.update(
{"text": ".", "language": "en", "is_final": True, "start_ms": 43663, "end_ms": 43843}
)

assert accumulator.text == " He's trying."
assert accumulator.start_time == 43663
assert accumulator.end_time == 43843


def test_token_accumulator_end_time_never_regresses():
"""A trailing token whose ``end_ms`` regresses must not pull the end back.

Regression for #6885: ``end_time`` was overwritten by every token that
carried ``end_ms``, so a non-monotonic trailing token shortened the span.
"""
from livekit.plugins.soniox.stt import _TokenAccumulator

accumulator = _TokenAccumulator()
accumulator.update(
{"text": " He's", "language": "en", "is_final": True, "start_ms": 41000, "end_ms": 41400}
)
accumulator.update(
{"text": " trying", "language": "en", "is_final": True, "start_ms": 41400, "end_ms": 43843}
)
accumulator.update(
{"text": ".", "language": "en", "is_final": True, "start_ms": 43663, "end_ms": 43700}
)

assert accumulator.start_time == 41000
assert accumulator.end_time == 43843


def test_token_accumulator_start_time_takes_earliest():
"""The earliest ``start_ms`` wins even if a later token carries an earlier one."""
from livekit.plugins.soniox.stt import _TokenAccumulator

accumulator = _TokenAccumulator()
accumulator.update(
{"text": " He's", "language": "en", "is_final": True, "start_ms": 41000, "end_ms": 41400}
)
accumulator.update(
{"text": " trying", "language": "en", "is_final": True, "start_ms": 40000, "end_ms": 43843}
)

assert accumulator.start_time == 40000
assert accumulator.end_time == 43843


# ---------------------------------------------------------------------------
# _lang_segments_to_fields helper
# ---------------------------------------------------------------------------
Expand Down