Files
livekit--agents/tests/utils/__init__.py
T
2026-07-13 13:39:38 +08:00

130 lines
3.7 KiB
Python

from __future__ import annotations
import asyncio
import io
import os
import pathlib
import wave
from collections.abc import AsyncGenerator
import jiwer as tr
import tiktoken
from livekit import rtc
from livekit.agents import utils
_TESTS_DIR = os.path.dirname(os.path.dirname(__file__))
TEST_AUDIO_FILEPATH = os.path.join(_TESTS_DIR, "long.mp3")
TEST_AUDIO_TRANSCRIPT = pathlib.Path(_TESTS_DIR, "long_transcript.txt").read_text()
def wer(hypothesis: str, reference: str) -> float:
wer_standardize_contiguous = tr.Compose(
[
tr.ToLowerCase(),
tr.ExpandCommonEnglishContractions(),
tr.RemoveKaldiNonWords(),
tr.RemoveWhiteSpace(replace_by_space=True),
tr.RemoveMultipleSpaces(),
tr.Strip(),
tr.ReduceToSingleSentence(),
tr.ReduceToListOfListOfWords(),
]
)
return tr.wer(
reference,
hypothesis,
reference_transform=wer_standardize_contiguous,
hypothesis_transform=wer_standardize_contiguous,
)
class EventCollector:
def __init__(self, emitter: rtc.EventEmitter, event: str) -> None:
emitter.on(event, self._on_event)
self._events = []
def _on_event(self, *args, **kwargs) -> None:
self._events.append((args, kwargs))
@property
def events(self) -> list[tuple[tuple, dict]]:
return self._events
@property
def count(self) -> int:
return len(self._events)
def clear(self) -> None:
self._events.clear()
async def read_audio_file(path) -> rtc.AudioFrame:
frames = []
async for f in utils.audio.audio_frames_from_file(path, sample_rate=48000, num_channels=1):
frames.append(f)
return rtc.combine_audio_frames(frames)
def make_wav_file(frames: list[rtc.AudioFrame]) -> bytes:
if not frames:
return b""
combined = rtc.combine_audio_frames(frames)
buf = io.BytesIO()
with wave.open(buf, "wb") as wf:
wf.setnchannels(combined.num_channels)
wf.setsampwidth(2) # int16
wf.setframerate(combined.sample_rate)
wf.writeframes(combined.data.tobytes())
return buf.getvalue()
async def make_test_speech(
*,
chunk_duration_ms: int | None = None,
sample_rate: int | None = None, # resample if not None
) -> tuple[list[rtc.AudioFrame], str, float]:
input_audio = await read_audio_file(TEST_AUDIO_FILEPATH)
if sample_rate is not None and input_audio.sample_rate != sample_rate:
resampler = rtc.AudioResampler(
input_rate=input_audio.sample_rate,
output_rate=sample_rate,
num_channels=input_audio.num_channels,
)
frames = []
if resampler:
frames = resampler.push(input_audio)
frames.extend(resampler.flush())
input_audio = rtc.combine_audio_frames(frames)
if not chunk_duration_ms:
return [input_audio], TEST_AUDIO_TRANSCRIPT, input_audio.duration
chunk_size = int(input_audio.sample_rate / (1000 / chunk_duration_ms))
bstream = utils.audio.AudioByteStream(
sample_rate=input_audio.sample_rate,
num_channels=input_audio.num_channels,
samples_per_channel=chunk_size,
)
frames = bstream.write(input_audio.data.tobytes())
frames.extend(bstream.flush())
return frames, TEST_AUDIO_TRANSCRIPT, input_audio.duration
async def fake_llm_stream(
text: str, *, model: str = "gpt-4o-mini", tokens_per_second: float = 3.0
) -> AsyncGenerator[str, None]:
enc = tiktoken.encoding_for_model(model)
token_ids = enc.encode(text)
sleep_time = 1.0 / max(tokens_per_second, 1e-6)
for tok_id in token_ids:
yield enc.decode([tok_id])
await asyncio.sleep(sleep_time)