diff --git a/.env.example b/.env.example index e8f993d..eeb965f 100644 --- a/.env.example +++ b/.env.example @@ -11,7 +11,7 @@ REDIS_HOST=127.0.0.1 REDIS_PORT=6789 REDIS_PASSWORD= -# STT Provider: "gladia" (default) or "openai" +# STT Provider: "gladia" (default), "openai" or "voxtral-realtime" # STT_PROVIDER=gladia # ============================================================================= @@ -81,3 +81,44 @@ GLADIA_TRANSLATION_LANG_MAP="de:de-DE,en:en-US,es:es-ES,fr:fr-FR,hi:hi-IN,it:it- # Base URL override — set this to use a compatible provider (e.g. a local Whisper server) #OPENAI_BASE_URL= + +# ============================================================================= +# --- Voxtral Realtime STT (STT_PROVIDER=voxtral-realtime) --- +# ============================================================================= + +#VOXTRAL_API_KEY= +#VOXTRAL_MODEL=mistralai/Voxtral-Mini-4B-Realtime-2602 +#VOXTRAL_BASE_URL= + +# Silero VAD: minimum silence duration (s) before end-of-speech fires (default: 0.6) +#VOXTRAL_VAD_MIN_SILENCE_S=0.6 + +# Silero VAD: speech probability activation threshold 0–1 (default: 0.5) +#VOXTRAL_VAD_ACTIVATION_THRESHOLD=0.5 + +# Rolling audio pre-roll kept while idle and replayed when speech starts, so the +# word onset preceding Silero's detection is not lost (default: 0.5) +#VOXTRAL_VAD_PREROLL_S=0.5 + +# Overlap replayed when a max-buffer split reopens mid-speech (default: 1.5). +# The reopened request starts mid-utterance with no context; ~1.28 s of lead-in +# is what the Voxtral paper recommends. The overlap is transcribed twice, so +# some duplicated words at split boundaries are the trade-off for not losing them. +#VOXTRAL_SPLIT_OVERLAP_S=1.5 + +# Safety cap on a single streaming request before a forced split (default: 30.0). +# Only reached during uninterrupted monologue. Splits replay VOXTRAL_SPLIT_OVERLAP_S +# of audio, so lowering this mainly trades duplicated boundary words for faster +# final transcripts during continuous speech (live interim captions are unaffected). +#VOXTRAL_MAX_BUFFER_DURATION_S=30.0 + +# How long to defer a segment's opening commit while the server is still +# generating the previous segment's transcript (default: 10.0). vLLM silently +# ignores commits sent during an in-flight generation, so the opener waits for +# the previous transcription.done; audio is buffered locally and replayed, so +# nothing is lost. On timeout (a done that never arrives) the pipeline resyncs +# and opens anyway. +#VOXTRAL_OPEN_GATE_TIMEOUT_S=10.0 + +# Emit incremental transcription.delta events as interim captions (default: true) +#VOXTRAL_INTERIM_RESULTS=true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 04f84d8..15c63fa 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -28,7 +28,7 @@ jobs: - uses: actions/setup-python@v6 with: - python-version: "3.10" + python-version: "3.11" - name: Install the project run: uv sync --all-extras --dev diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml index 5cefd7e..8e87d60 100644 --- a/.github/workflows/publish-docker-image.yml +++ b/.github/workflows/publish-docker-image.yml @@ -5,6 +5,7 @@ on: push: branches: - development + - voxtral-* tags: - "v*.*.*" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ce9b35b..5af0c4c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,7 +24,7 @@ jobs: - uses: actions/setup-python@v6 with: - python-version: "3.10" + python-version: "3.11" - name: Install dependencies run: uv sync --group dev @@ -54,7 +54,7 @@ jobs: - uses: actions/setup-python@v6 with: - python-version: "3.10" + python-version: "3.11" - name: Install dependencies if: ${{ env.GLADIA_API_KEY != '' }} diff --git a/.python-version b/.python-version index c8cfe39..2c07333 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.10 +3.11 diff --git a/CHANGELOG.md b/CHANGELOG.md index b06030e..aa8f93a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Final releases will consolidate all intermediate changes in chronological order. * build: declare aiohttp and numpy as direct dependencies * docs: correct how the OpenAI provider reaches the API and list its caveats * docs: expand and correct AGENTS.md +* feat(voxtral): Voxtral Realtime STT provider with concurrent streaming and Silero neural VAD on Python 3.11 ## v0.3.0 diff --git a/Dockerfile b/Dockerfile index 50d6e82..31104b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10-slim +FROM python:3.11-slim # Use uv instead of pip, see https://github.com/astral-sh/uv RUN pip install --no-cache-dir uv==0.10.4 diff --git a/README.md b/README.md index e7ca0b8..98464f2 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,15 @@ Supported STT engines: - **Gladia** — via the official [LiveKit Gladia plugin](https://docs.livekit.io/agents/integrations/stt/gladia/) (default) - **OpenAI** — via a direct REST client against `/v1/audio/transcriptions`; supports the official OpenAI API and any OpenAI-compatible endpoint +- **Voxtral Realtime** — [Mistral Voxtral Mini Realtime](https://huggingface.co/mistralai/Voxtral-Mini-4B-Realtime-2602) served by a self-hosted [vLLM](https://docs.vllm.ai/) instance via its realtime WebSocket API ## Getting Started ### Environment prerequisites -- Python 3.10+ +- Python 3.11+ - A LiveKit instance -- A Gladia API key **or** an OpenAI API key (depending on your chosen STT provider) +- A Gladia API key, an OpenAI API key, **or** a vLLM server hosting Voxtral (depending on your chosen STT provider) - uv: - See installation instructions: https://docs.astral.sh/uv/getting-started/installation/ @@ -145,6 +146,29 @@ Some caveats apply to this provider: there is no detected language to map back to a BBB locale and the transcript is discarded with a warning. Set an explicit locale in BBB when using OpenAI STT. +### Voxtral Realtime STT provider + +Set `STT_PROVIDER=voxtral-realtime` to use a self-hosted +[Voxtral Mini Realtime](https://huggingface.co/mistralai/Voxtral-Mini-4B-Realtime-2602) +model served by [vLLM](https://docs.vllm.ai/). The agent streams each +participant's audio over vLLM's realtime WebSocket API, gated by a local +Silero VAD, and emits live interim captions from the model's incremental +deltas. + +```bash +STT_PROVIDER=voxtral-realtime +VOXTRAL_BASE_URL=https://your-vllm-server:8000/v1 # required +VOXTRAL_API_KEY=your-key # if your server enforces one +# VOXTRAL_MODEL=mistralai/Voxtral-Mini-4B-Realtime-2602 # default +``` + +VAD and segmentation tuning options (silence duration, pre-roll, split +overlap, max segment length) are documented in `.env.example`. + +> **Note**: Voxtral Realtime does not support real-time translation. Only the +> original transcript language is returned, matching the user's BBB speech +> locale. + ### Development #### Testing diff --git a/providers/__init__.py b/providers/__init__.py index 03d0b9f..11b467b 100644 --- a/providers/__init__.py +++ b/providers/__init__.py @@ -10,4 +10,11 @@ def create_agent(provider: str) -> BaseSttAgent: from providers.openai import OpenAiSttAgent, openai_config return OpenAiSttAgent(openai_config) + if provider == "voxtral-realtime": + from providers.voxtral_realtime import ( + VoxtralRealtimeSttAgent, + voxtral_realtime_config, + ) + + return VoxtralRealtimeSttAgent(voxtral_realtime_config) raise ValueError(f"Unknown STT provider: {provider}") diff --git a/providers/voxtral_realtime.py b/providers/voxtral_realtime.py new file mode 100644 index 0000000..d01a155 --- /dev/null +++ b/providers/voxtral_realtime.py @@ -0,0 +1,807 @@ +"""STT provider for vLLM's Voxtral Realtime WebSocket API. + +vLLM's protocol differs from the OpenAI Realtime Transcription API in three ways: +- session.update: model is at the top level, not nested inside session.audio +- No server-side VAD: the client segments speech itself — a bare + input_audio_buffer.commit opens a streaming request, commit(final: true) + closes it (verified in notes/progressive-transcription-investigation.md) +- Response events: transcription.delta / transcription.done (not conversation.item.*) + +Audio must be PCM16, 16 kHz, mono, base64-encoded. +""" + +import asyncio +import base64 +import json +import logging +import os +import time +from collections import deque +from dataclasses import dataclass, field + +import aiohttp +import numpy as np +from livekit import rtc +from livekit.agents import stt +from livekit.agents import vad as agents_vad +from livekit.plugins import silero + +from providers.base import BaseSttAgent, BaseSttConfig + +# Safety cap on a single streaming request's length. Only hit during pure +# monologue (no VAD pause). It serves two purposes: BBB only commits a caption +# on transcription.done, so the cap bounds FINAL latency and caption size when +# a speaker never pauses; and vLLM resets its position counter per commit +# cycle (not per connection), so the cap also bounds per-request context — +# exceeding --max-model-len crashes the whole engine. At 80 ms/token, 30 s is +# ~375 tokens, far from any realistic limit; splits replay _SPLIT_OVERLAP_S so +# lowering the cap mainly trades boundary-word duplication for faster FINALs. +_MAX_BUFFER_DURATION_S = float(os.getenv("VOXTRAL_MAX_BUFFER_DURATION_S", "30.0")) +# The model requires 16 kHz mono PCM16; deliberately not configurable. +_TARGET_SAMPLE_RATE = 16000 +# Silero VAD parameters — replace the old RMS threshold and silence duration +_VAD_MIN_SILENCE_S = float(os.getenv("VOXTRAL_VAD_MIN_SILENCE_S", "0.6")) +_VAD_ACTIVATION_THRESHOLD = float(os.getenv("VOXTRAL_VAD_ACTIVATION_THRESHOLD", "0.5")) +# Rolling pre-roll kept while idle so the word onset preceding Silero's +# START_OF_SPEECH (its prefix padding) is sent once a streaming request opens. +_VAD_PREROLL_S = float(os.getenv("VOXTRAL_VAD_PREROLL_S", "0.5")) +# Audio replayed when a max-buffer split reopens mid-speech. The reopened +# request starts mid-utterance with no context; the Voxtral paper recommends +# ~1.28 s of lead-in (16 frames, "similar to attention sinks"), so short +# overlap loses or garbles the words right after a split. The overlap is +# transcribed twice — some duplication at split boundaries is the trade-off. +_SPLIT_OVERLAP_S = float(os.getenv("VOXTRAL_SPLIT_OVERLAP_S", "1.5")) +# Reconnect backoff bounds for a dropped WebSocket connection. +_RETRY_DELAY_INITIAL_S = 1.0 +_RETRY_DELAY_MAX_S = 30.0 +# After the audio stream ends, wait up to this long for the server's +# transcription.done of the final segment before tearing the reader down; +# cancelling it immediately would drop the tail utterance's FINAL. +_FINAL_DRAIN_TIMEOUT_S = 3.0 +# vLLM's realtime handler runs one generation per connection and SILENTLY +# drops any commit that arrives while the previous segment's generation is +# still running ("Generation already in progress, ignoring commit"). A dropped +# opener means the segment never streams (no interims, late batched FINAL); a +# dropped closer loses the segment's transcription.done entirely and desyncs +# the FIFO pairing. Openers are therefore gated until the previous segment's +# done has been read, buffering audio locally meanwhile. The timeout bounds +# the wait when a done never arrives (already-desynced session): give up, +# resync the counter, and open ungated as before. +_OPEN_GATE_TIMEOUT_S = float(os.getenv("VOXTRAL_OPEN_GATE_TIMEOUT_S", "10.0")) + + +@dataclass +class VoxtralRealtimeConfig(BaseSttConfig): + api_key: str | None = field(default_factory=lambda: os.getenv("VOXTRAL_API_KEY")) + model: str = field( + default_factory=lambda: os.getenv( + "VOXTRAL_MODEL", "mistralai/Voxtral-Mini-4B-Realtime-2602" + ) + ) + base_url: str | None = field( + default_factory=lambda: os.getenv("VOXTRAL_BASE_URL", None) + ) + interim_results: bool = field( + default_factory=lambda: ( + os.getenv("VOXTRAL_INTERIM_RESULTS", "true").lower() != "false" + ) + ) + + +voxtral_realtime_config = VoxtralRealtimeConfig() + + +class VoxtralRealtimeSttAgent(BaseSttAgent): + def __init__( + self, + config: VoxtralRealtimeConfig, + vad: agents_vad.VAD | None = None, + ): + super().__init__(config) + if not config.base_url: + # Fail at startup rather than with confusing auth/protocol errors + # mid-meeting: Voxtral Realtime is served by a self-hosted vLLM + # instance, never by api.openai.com. + raise ValueError( + "VOXTRAL_BASE_URL is required for the voxtral-realtime provider " + "(the URL of your vLLM server, e.g. https://your-server:8000/v1)." + ) + self._http_session: aiohttp.ClientSession | None = None + self._vad: agents_vad.VAD = vad or silero.VAD.load( + min_silence_duration=_VAD_MIN_SILENCE_S, + activation_threshold=_VAD_ACTIVATION_THRESHOLD, + ) + + def _get_http_session(self) -> aiohttp.ClientSession: + if self._http_session is None: + self._http_session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=None, connect=15) + ) + return self._http_session + + def _build_ws_url(self) -> str: + base = self.config.base_url.rstrip("/") + base = base.replace("https://", "wss://", 1).replace("http://", "ws://", 1) + return f"{base}/realtime?intent=transcription" + + def _create_stt_stream(self, locale: str) -> stt.SpeechStream: + raise NotImplementedError("VoxtralRealtime uses a custom pipeline") + + def _update_stream_locale(self, user_id: str, locale: str): + provider = self.participant_settings.get(user_id, {}).get( + "provider", "voxtral-realtime" + ) + self.stop_transcription_for_user(user_id) + self.start_transcription_for_user(user_id, locale, provider) + + def start_transcription_for_user(self, user_id: str, locale: str, provider: str): + settings = self.participant_settings.setdefault(user_id, {}) + settings["locale"] = locale + settings["provider"] = provider + + participant = self._find_participant(user_id) + if not participant: + logging.error( + f"Cannot start transcription, participant {user_id} not found." + ) + return + + track = self._find_audio_track(participant) + if not track: + logging.warning( + f"Won't start transcription yet, no audio track found for {user_id}." + ) + return + + if participant.identity in self.processing_info: + logging.debug( + f"Transcription already running for {participant.identity}, ignoring." + ) + return + + language = self._sanitize_locale(locale) + task = asyncio.create_task( + self._run_transcription_pipeline(participant, track, language) + ) + self.processing_info[participant.identity] = {"task": task} + logging.info( + f"Started Voxtral Realtime transcription for {participant.identity} ({locale})." + ) + + async def _cleanup(self): + await super()._cleanup() + if self._http_session: + await self._http_session.close() + self._http_session = None + + async def _run_transcription_pipeline( + self, + participant: rtc.RemoteParticipant, + track: rtc.Track, + language: str, + ): + ws_url = self._build_ws_url() + headers = {"Authorization": f"Bearer {self.config.api_key}"} + open_time = time.time() + retry_delay = _RETRY_DELAY_INITIAL_S + + try: + while True: + audio_stream = rtc.AudioStream(track) + try: + async with self._get_http_session().ws_connect( + ws_url, headers=headers + ) as ws: + msg = await asyncio.wait_for(ws.receive(), timeout=10.0) + if msg.type != aiohttp.WSMsgType.TEXT: + logging.error( + "Voxtral WS: expected text for session.created" + ) + return + data = json.loads(msg.data) + if data.get("type") != "session.created": + logging.error( + f"Voxtral WS: unexpected first message: {data}" + ) + return + logging.info( + f"Voxtral WS session created for {participant.identity}" + ) + # Connection is healthy again; reset reconnect backoff. + retry_delay = _RETRY_DELAY_INITIAL_S + + # vLLM expects a FLAT session.update — model and + # temperature at the top level; nesting under + # "session" is rejected (probe test 6). The model + # card mandates temperature 0.0: greedy decoding is + # required for stable transcription. + await ws.send_json( + { + "type": "session.update", + "model": self.config.model, + "temperature": 0.0, + } + ) + + await self._vad_loop( + ws, audio_stream, participant, language, open_time + ) + return # clean exit — audio stream finished normally + + except asyncio.CancelledError: + raise + except (TimeoutError, aiohttp.ClientError, ConnectionResetError) as e: + # TimeoutError covers a slow session.created handshake — + # vLLM takes 2–5 min of CUDA-graph warmup after startup, + # during which giving up permanently would cost the + # participant the whole meeting. Retry with backoff. + logging.warning( + f"Voxtral WS connection lost for {participant.identity} " + f"({type(e).__name__}: {e}), reconnecting in {retry_delay:.0f}s" + ) + await asyncio.sleep(retry_delay) + retry_delay = min(retry_delay * 2, _RETRY_DELAY_MAX_S) + except Exception as e: + logging.error( + f"Voxtral Realtime error for {participant.identity}: {e}", + exc_info=True, + ) + return + finally: + await audio_stream.aclose() + + except asyncio.CancelledError: + logging.info( + f"Voxtral Realtime transcription for {participant.identity} cancelled." + ) + finally: + # Deregister only if this task still owns the entry: a locale + # change (stop→start) registers the replacement task before this + # cancelled task runs its finally block, so an unconditional pop + # would deregister the replacement — leaving it running but + # untracked (unstoppable, and a later start would spawn a + # duplicate pipeline on the same track). + info = self.processing_info.get(participant.identity) + if info and info.get("task") is asyncio.current_task(): + self.processing_info.pop(participant.identity, None) + + async def _vad_loop( + self, + ws: aiohttp.ClientWebSocketResponse, + audio_stream: rtc.AudioStream, + participant: rtc.RemoteParticipant, + language: str, + open_time: float, + ): + chunk_size = ( + _TARGET_SAMPLE_RATE // 20 * 2 + ) # 50 ms of int16 (matches official plugin) + + # Per-segment start times, pushed by _writer._open() and popped by + # _reader at each segment's first delta. The server processes segments + # strictly in order on one connection, so FIFO pairing is exact. This + # gives every segment a distinct start_time — and therefore a distinct + # BBB transcriptId — including max-buffer splits of one long utterance, + # where Silero fires no new START_OF_SPEECH (a shared "speech start" + # variable would collide and make segment 2 overwrite segment 1). + segment_starts: deque[float] = deque() + + # In-flight segment state. Owned by _reader, but hoisted to _vad_loop + # scope so teardown can emit a best-effort FINAL from whatever delta + # text was received when the segment's transcription.done never comes + # (cancellation, reconnect, reader death). Without that, BBB keeps + # showing the already-emitted interim as pending forever. + seg_text = "" + seg_start: float | None = None + + # Segments opened whose transcription.done has not been read yet. + # Incremented by _writer._open(), decremented by _reader on done. + # _writer defers opening commits while this is non-zero — vLLM ignores + # commits sent during an in-flight generation (see _OPEN_GATE_TIMEOUT_S). + outstanding = 0 + + def _pop_segment_start(context: str) -> float: + if segment_starts: + return segment_starts.popleft() + # One transcription.done per opener commit is the pairing + # invariant. An empty queue here means the server emitted more + # transcription events than segments were opened, so this and + # every later segment gets a mis-stamped start_time (and thus a + # wrong BBB transcriptId that can overwrite a neighbor's caption). + fallback = time.time() - open_time + logging.warning( + f"Voxtral: {context} with no queued segment start for " + f"{participant.identity} — FIFO pairing desync; using " + f"wall-clock fallback t={fallback:.3f}s" + ) + return fallback + + def _emit_transcript(final: bool, text: str, start_time: float) -> None: + self.emit( + "final_transcript" if final else "interim_transcript", + participant=participant, + event=stt.SpeechEvent( + type=( + stt.SpeechEventType.FINAL_TRANSCRIPT + if final + else stt.SpeechEventType.INTERIM_TRANSCRIPT + ), + alternatives=[ + stt.SpeechData( + text=text, + language=language, + start_time=start_time, + end_time=time.time() - open_time, + ) + ], + ), + open_time=open_time, + ) + + # ── Reader ──────────────────────────────────────────────────────────── + + async def _reader() -> None: + """Read transcription events for all utterances on this session. + + Runs concurrently with _writer so that transcription.delta events + emitted by the server while audio is still streaming are consumed + in real time rather than buffered and replayed after each commit. + """ + nonlocal seg_text, seg_start, outstanding + + while True: + try: + msg = await ws.receive() + except asyncio.CancelledError: + raise + except Exception as e: + logging.error( + f"Voxtral: reader error for {participant.identity}: {e}" + ) + break + + if msg.type in ( + aiohttp.WSMsgType.CLOSED, + aiohttp.WSMsgType.CLOSE, + aiohttp.WSMsgType.CLOSING, + ): + logging.debug(f"Voxtral: WS closed for {participant.identity}") + break + + if msg.type == aiohttp.WSMsgType.ERROR: + logging.warning( + f"Voxtral: WS error frame for {participant.identity}: " + f"{ws.exception()}" + ) + break + + if msg.type != aiohttp.WSMsgType.TEXT: + continue + + try: + data = json.loads(msg.data) + except (ValueError, TypeError) as e: + logging.warning( + f"Voxtral: ignoring malformed WS message for " + f"{participant.identity}: {e}" + ) + continue + msg_type = data.get("type") + + if msg_type == "transcription.delta": + if seg_start is None: + # Pair this segment with the start time its opener pushed. + seg_start = _pop_segment_start("transcription.delta") + logging.debug( + f"Voxtral: first delta for {participant.identity} " + f"at t={time.time() - open_time:.3f}s " + f"(seg_start={seg_start:.3f}s)" + ) + seg_text += data.get("delta", "") + if seg_text and self.config.interim_results: + _emit_transcript(False, seg_text, seg_start) + + elif msg_type == "transcription.done": + # The server finished this segment's generation; the writer + # may open the next segment's request now. + outstanding = max(0, outstanding - 1) + # Prefer accumulated delta text over done.text: the realtime + # API sends content via deltas; done.text may be empty or absent. + server_text = data.get("text", "").strip() + logging.debug( + f"Voxtral: transcription.done for {participant.identity} " + f"at t={time.time() - open_time:.3f}s — " + f"delta_text='{seg_text[:60]}', server_text='{server_text[:60]}'" + ) + if server_text: + seg_text = server_text + + if seg_start is None: + # Zero-delta segment: consume its queued start anyway so + # later segments stay paired with their own openers. + seg_start = _pop_segment_start("transcription.done") + + if seg_text: + _emit_transcript(True, seg_text, seg_start) + + # Reset for next utterance + seg_text = "" + seg_start = None + + elif msg_type == "error": + logging.error(f"Voxtral WS error event: {data}") + break + + # Reaching here means the reader stopped while the writer may still + # be streaming (error event, receive failure, server close). Close + # the socket so the writer's next send fails and the pipeline's + # reconnect logic takes over — otherwise audio keeps flowing into a + # session nobody reads and transcription dies silently. + await ws.close() + + # ── VAD task + Writer (Silero VAD, server-side streaming) ──────────── + # + # Protocol (verified empirically against vLLM Voxtral, see + # notes/progressive-transcription-investigation.md): + # 1. On speech start (Silero START_OF_SPEECH): send an OPENING commit + # to start a streaming request. Without this the server only buffers + # and batch-transcribes on the closing commit (no live deltas). + # 2. While a request is open, audio is appended in real time. The + # server streams transcription.delta events back as audio arrives + # (first delta ~0.6 s after speech start), which _reader emits as + # INTERIM. While idle, only a bounded pre-roll is kept locally. + # 3. On speech end (Silero END_OF_SPEECH or max-buffer): send + # commit(final). The server emits transcription.done, which + # _reader emits as FINAL. + # Each segment is its own streaming request with its own entry in + # segment_starts, so consecutive segments — including max-buffer splits + # of one long utterance — never collide on BBB's second-granularity + # transcriptId. + # + # Silero VAD only controls WHEN commits are sent; audio filtering is not + # needed and was the root cause of our earlier failed attempt. + + # Shared between _vad_task and _writer (asyncio single-threaded, no locks) + is_in_speech = False + commit_event = asyncio.Event() # set by _vad_task on END_OF_SPEECH + + vad_stream = self._vad.stream() + + async def _vad_task() -> None: + nonlocal is_in_speech + try: + async for ev in vad_stream: + if ev.type == agents_vad.VADEventType.START_OF_SPEECH: + is_in_speech = True + logging.debug( + f"Voxtral: speech start for {participant.identity}" + ) + elif ev.type == agents_vad.VADEventType.END_OF_SPEECH: + is_in_speech = False + commit_event.set() + logging.debug(f"Voxtral: speech end for {participant.identity}") + except asyncio.CancelledError: + raise + except Exception as e: + # A dead VAD task would silently freeze is_in_speech and stop all + # commits; surface it rather than degrade to max-buffer-only. + logging.error( + f"Voxtral: VAD task failed for {participant.identity}: {e}", + exc_info=True, + ) + + async def _writer() -> None: + # `stream_open` is owned exclusively by _writer; `is_in_speech` is + # owned exclusively by _vad_task. Keeping them separate is what lets a + # max-buffer split mid-utterance reopen immediately on the next frame + # (we never clobber the VAD's view of whether speech is ongoing). + # + # One rolling buffer of recent audio, two replay lengths at _open(): + # - Fresh onset: at most the VAD pre-roll, capped by min-silence — + # otherwise a normal onset could replay the previous utterance's + # tail (it would not have rolled out of the buffer during the + # gap) and duplicate it. + # - Max-buffer split reopen: the full split overlap. Mid-speech the + # buffer holds only the current utterance, so the longer replay + # is safe and gives the reopened request the left-context the + # model needs (see _SPLIT_OVERLAP_S). + nonlocal outstanding + onset_max = ( + int(min(_VAD_PREROLL_S, _VAD_MIN_SILENCE_S) * _TARGET_SAMPLE_RATE) * 2 + ) + split_max = int(_SPLIT_OVERLAP_S * _TARGET_SAMPLE_RATE) * 2 # int16 bytes + preroll_max = max(onset_max, split_max) + preroll = bytearray() # rolling window of the most recent audio + split_reopen = False # next _open() continues a cap-split utterance + pending = b"" # audio buffered for chunked append while open + open_secs = 0.0 # duration of the current open segment + stream_open = False + normalizer = _AudioNormalizer() # stateful: one per audio stream + # Commit gate (see _OPEN_GATE_TIMEOUT_S): while the previous + # segment's transcription.done is outstanding, an opening commit + # would be silently ignored by vLLM, so it is deferred. gate_buf + # captures the lead-in plus all audio arriving during the wait and + # becomes the eventual _open() replay — the segment starts a + # little later but loses nothing. + gated = False + gate_buf: bytearray | None = None + gate_deadline = 0.0 + pending_end = False # END_OF_SPEECH arrived while gated + loop = asyncio.get_running_loop() + + async def _append(data: bytes) -> None: + await ws.send_json( + { + "type": "input_audio_buffer.append", + "audio": base64.b64encode(data).decode(), + } + ) + + async def _flush_pending() -> None: + nonlocal pending + while len(pending) >= chunk_size: + await _append(pending[:chunk_size]) + pending = pending[chunk_size:] + + def _take_lead_in() -> bytes: + """Consume the rolling buffer's tail as a new segment's lead-in.""" + nonlocal split_reopen + limit = split_max if split_reopen else onset_max + split_reopen = False + lead = ( + bytes(preroll[max(0, len(preroll) - limit) :]) if limit > 0 else b"" + ) + preroll.clear() + return lead + + async def _open(replay: bytes) -> None: + """Open a streaming request and replay the captured lead-in.""" + nonlocal stream_open, pending, open_secs, outstanding + commit_event.clear() # discard any stale END from a prior segment + # Record this segment's start (backdated by the replayed lead-in) + # for the reader to pair with the segment's transcription events. + replay_secs = len(replay) / (_TARGET_SAMPLE_RATE * 2) + segment_starts.append(time.time() - open_time - replay_secs) + await ws.send_json({"type": "input_audio_buffer.commit"}) + outstanding += 1 + stream_open = True + # The replay does not count toward the max-buffer cap: it is + # bounded on its own (lead-in caps, gate timeout), and counting + # it would make a long gated replay close the segment the + # moment it opens. + open_secs = 0.0 + if replay: + pending += replay + await _flush_pending() + + async def _close() -> None: + """Flush remaining audio and close the utterance's request. + + Only commit(final) is sent, matching the vLLM reference client. + Probe test 5 confirmed a preceding bare commit is redundant — + same text, one done either way — and it delays the done by + ~0.35 s (the server processes an extra commit boundary first). + """ + nonlocal stream_open, pending + if pending: + await _append(pending) + pending = b"" + await ws.send_json({"type": "input_audio_buffer.commit", "final": True}) + stream_open = False + # The rolling buffer is intentionally NOT cleared: if this was a + # max-buffer split, the next frame reopens immediately and + # replays the split overlap so the boundary words carry fully + # into the next segment. _open() clears it after replaying. + + try: + async for audio_event in audio_stream: + frame = audio_event.frame + frame_duration = frame.samples_per_channel / frame.sample_rate + + # Feed the VAD, then yield once so _vad_task is scheduled. In + # production the network awaits below also yield; this keeps the + # interleave deterministic and lets tests drive a sync frame + # iterator without starving the VAD task. + vad_stream.push_frame(frame) + await asyncio.sleep(0) + + resampled = normalizer.process(frame) + + # An END arriving while no request is open: if a gated + # segment is waiting, the utterance finished before the + # server freed up — remember to close it right after it + # opens. Otherwise it is stale (cap-split utterance ended + # before the reopen): whatever opens next is a fresh + # onset, not a split continuation. + if not stream_open and commit_event.is_set(): + commit_event.clear() + if gated: + pending_end = True + else: + split_reopen = False + + # Open a request as soon as the VAD reports speech — but + # only once the server has finished the previous segment's + # generation; a commit sent earlier would be silently + # ignored. While deferred, audio accumulates in gate_buf. + if not stream_open and (is_in_speech or gated): + if outstanding == 0: + replay = ( + bytes(gate_buf) + if gate_buf is not None + else _take_lead_in() + ) + gate_buf = None + gated = False + await _open(replay) + if pending_end: + pending_end = False + # The gated utterance already ended; close it + # now — unless the speaker has resumed, in + # which case keep streaming and let the next + # END close the merged segment. + if not is_in_speech: + await _close() + elif not gated: + gated = True + gate_deadline = loop.time() + _OPEN_GATE_TIMEOUT_S + gate_buf = bytearray(_take_lead_in()) + elif loop.time() >= gate_deadline: + # The done never arrived (lost closing commit or + # event desync); waiting longer only buffers more + # audio. Resync and open ungated on the next frame. + logging.warning( + f"Voxtral: open gate timed out for " + f"{participant.identity} with {outstanding} " + f"transcription(s) outstanding — resyncing" + ) + outstanding = 0 + + if stream_open: + pending += resampled + open_secs += frame_duration + await _flush_pending() + elif gated: + gate_buf += resampled + + # Keep a bounded rolling buffer of the most recent audio, + # whether idle or open. _open() replays its tail as the + # segment's lead-in: on a fresh onset the onset-capped + # pre-roll (the audio before the VAD fired); on a + # max-buffer split the full overlap that carries the + # boundary words into the next segment instead of cutting + # them in half. + preroll += resampled + if len(preroll) > preroll_max: + del preroll[: len(preroll) - preroll_max] + + # Close on end-of-speech (Silero) or the max-buffer safety cap. + # is_in_speech is deliberately left untouched: if the speaker is + # still talking past the cap, the next frame reopens immediately. + if stream_open and ( + commit_event.is_set() or open_secs >= _MAX_BUFFER_DURATION_S + ): + # A cap-triggered close (no END) splits mid-speech; the + # reopen replays the full overlap instead of the onset + # pre-roll. + split_reopen = not commit_event.is_set() + commit_event.clear() + await _close() + except asyncio.CancelledError: + # Frames stop before Silero can fire END_OF_SPEECH when a + # speaker mutes or unpublishes right after talking, so an open + # request would be dropped without its closing commit — losing + # the utterance's FINAL and leaving the server request dangling + # (abrupt drops are a known vLLM realtime crash trigger). + # Best-effort close before propagating the cancellation. A + # gated segment never sent its opener, so open-and-close it — + # the server may still be mid-generation and drop the opener, + # but that is no worse than losing the utterance outright. + if stream_open or (gated and gate_buf): + try: + + async def _cancel_flush() -> None: + if not stream_open: + await _open(bytes(gate_buf)) + await _close() + + await asyncio.wait_for(_cancel_flush(), timeout=1.0) + except Exception: + logging.debug( + f"Voxtral: cancel-time flush failed for " + f"{participant.identity}" + ) + raise + + # End of stream: drain the resampler tail and flush any open request. + # A gated segment's opener was deferred the whole time; send it now, + # best-effort, so the buffered utterance still gets transcribed. + if stream_open: + pending += normalizer.flush() + await _close() + elif gated and gate_buf: + gate_buf += normalizer.flush() + await _open(bytes(gate_buf)) + await _close() + vad_stream.end_input() + + # ── Run all three tasks concurrently ────────────────────────────────── + + reader_task = asyncio.create_task(_reader()) + vad_task = asyncio.create_task(_vad_task()) + + async def _drain_reader() -> None: + """Wait (bounded) for the reader to consume the final segment's + transcription.done after the writer finishes cleanly.""" + loop = asyncio.get_running_loop() + deadline = loop.time() + _FINAL_DRAIN_TIMEOUT_S + while ( + (segment_starts or seg_text) + and not reader_task.done() + and loop.time() < deadline + ): + await asyncio.sleep(0.05) + + try: + await _writer() + await _drain_reader() + finally: + reader_task.cancel() + vad_task.cancel() + await asyncio.gather(reader_task, vad_task, return_exceptions=True) + await vad_stream.aclose() + if segment_starts: + # Expected when teardown interrupts an open segment; anything + # beyond that means dones went missing (see _pop_segment_start). + logging.info( + f"Voxtral: {len(segment_starts)} opened segment(s) without a " + f"transcription.done at teardown for {participant.identity}" + ) + if seg_text: + # Teardown caught a segment mid-flight: its transcription.done + # will never be read, and BBB would show the already-emitted + # interim as pending forever. Commit the best text we have. + logging.info( + f"Voxtral: emitting best-effort FINAL for " + f"{participant.identity} on teardown: '{seg_text[:60]}'" + ) + _emit_transcript( + True, + seg_text, + seg_start if seg_start is not None else time.time() - open_time, + ) + + +class _AudioNormalizer: + """Downmix to mono and resample to _TARGET_SAMPLE_RATE PCM16. + + Wraps rtc.AudioResampler (SoX), which low-pass filters before decimating — + naive linear interpolation aliases everything above the target Nyquist + (8 kHz) into the speech band and measurably hurts transcription accuracy. + The resampler is streaming: its filter state must persist across frames, + so use one instance per audio stream, and call flush() at end of stream + to drain the tail samples held back by the filter. + """ + + def __init__(self): + self._resampler: rtc.AudioResampler | None = None + self._input_rate: int | None = None + + def process(self, frame: rtc.AudioFrame) -> bytes: + samples = np.frombuffer(frame.data, dtype=np.int16) + if frame.num_channels > 1: + samples = ( + samples.reshape(-1, frame.num_channels).mean(axis=1).astype(np.int16) + ) + if frame.sample_rate == _TARGET_SAMPLE_RATE: + return samples.tobytes() + if self._input_rate != frame.sample_rate: + self._resampler = rtc.AudioResampler(frame.sample_rate, _TARGET_SAMPLE_RATE) + self._input_rate = frame.sample_rate + frames = self._resampler.push(bytearray(samples.tobytes())) + return b"".join(bytes(f.data) for f in frames) + + def flush(self) -> bytes: + if self._resampler is None: + return b"" + return b"".join(bytes(f.data) for f in self._resampler.flush()) diff --git a/pyproject.toml b/pyproject.toml index 659e0b1..08a652e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,9 @@ name = "bbb-livekit-stt" version = "0.3.0" description = "Provides STT for BigBlueButton meetings using LiveKit as their audio bridge." readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" dependencies = [ - "livekit-agents[gladia]~=1.4", + "livekit-agents[gladia,silero]~=1.4", "python-dotenv~=1.1.1", "redis~=6.4.0", "nest-asyncio~=1.6.0", diff --git a/tests/test_voxtral_agent.py b/tests/test_voxtral_agent.py new file mode 100644 index 0000000..0e12a3f --- /dev/null +++ b/tests/test_voxtral_agent.py @@ -0,0 +1,1646 @@ +import asyncio +import base64 +import json +from unittest.mock import AsyncMock, MagicMock, patch + +import aiohttp +import numpy as np +import pytest +from livekit import rtc +from livekit.agents import stt + +from livekit.agents import vad as agents_vad + +from providers.voxtral_realtime import ( + VoxtralRealtimeConfig, + VoxtralRealtimeSttAgent, + _AudioNormalizer, +) + + +# ── Helpers ──────────────────────────────────────────────────────────────────── + + +def _make_mock_vad(vad_events=None): + """Return a mock VAD that emits the given VADEvent sequence.""" + mock_vad = MagicMock() + # AsyncMock with __aiter__.return_value is the correct pattern for async-for + # (same as AudioStream mocking in other test files). + mock_vad_stream = AsyncMock() + mock_vad_stream.push_frame = MagicMock() + mock_vad_stream.end_input = MagicMock() + mock_vad_stream.aclose = AsyncMock() + events = vad_events or [] + mock_vad_stream.__aiter__.return_value = iter(events) + mock_vad.stream.return_value = mock_vad_stream + return mock_vad + + +def _make_config(**kwargs): + kwargs.setdefault("base_url", "https://test-server.example.com/v1") + return VoxtralRealtimeConfig(api_key="test-key", **kwargs) + + +def _make_agent(vad_events=None, **kwargs): + return VoxtralRealtimeSttAgent( + _make_config(**kwargs), vad=_make_mock_vad(vad_events) + ) + + +def _make_participant(identity, has_audio_track=True): + participant = MagicMock(spec=rtc.RemoteParticipant) + participant.identity = identity + pubs = {} + if has_audio_track: + mock_track = MagicMock() + mock_track.kind = rtc.TrackKind.KIND_AUDIO + pub = MagicMock() + pub.track = mock_track + pubs["audio"] = pub + participant.track_publications = pubs + return participant + + +def _make_agent_with_room(participants=None, **kwargs): + agent = _make_agent(**kwargs) + mock_room = MagicMock() + mock_room.remote_participants = participants or {} + agent.room = mock_room + return agent + + +def _text_ws_msg(data: dict) -> MagicMock: + msg = MagicMock() + msg.type = aiohttp.WSMsgType.TEXT + msg.data = json.dumps(data) + return msg + + +def _make_audio_frame( + amplitude: int = 0, + sample_rate: int = 16000, + num_channels: int = 1, + samples_per_channel: int = 160, +) -> MagicMock: + total = samples_per_channel * num_channels + samples = np.full(total, amplitude, dtype=np.int16) + frame = MagicMock() + frame.data = samples.tobytes() + frame.sample_rate = sample_rate + frame.samples_per_channel = samples_per_channel + frame.num_channels = num_channels + return frame + + +def _make_loud_frame(): + # Amplitude value is irrelevant; speech detection is now Silero-based (mocked in tests) + return _make_audio_frame(amplitude=1000) + + +# ── Config ───────────────────────────────────────────────────────────────────── + + +class TestVoxtralRealtimeConfig: + @pytest.fixture(autouse=True) + def _clean_env(self, monkeypatch): + for key in [ + "VOXTRAL_API_KEY", + "VOXTRAL_MODEL", + "VOXTRAL_BASE_URL", + "VOXTRAL_INTERIM_RESULTS", + ]: + monkeypatch.delenv(key, raising=False) + + def test_default_model(self): + assert ( + VoxtralRealtimeConfig().model == "mistralai/Voxtral-Mini-4B-Realtime-2602" + ) + + def test_default_api_key_is_none(self): + assert VoxtralRealtimeConfig().api_key is None + + def test_default_base_url_is_none(self): + assert VoxtralRealtimeConfig().base_url is None + + def test_default_interim_results_is_true(self): + assert VoxtralRealtimeConfig().interim_results is True + + def test_interim_results_false_via_env(self, monkeypatch): + monkeypatch.setenv("VOXTRAL_INTERIM_RESULTS", "false") + assert VoxtralRealtimeConfig().interim_results is False + + def test_interim_results_true_via_env(self, monkeypatch): + monkeypatch.setenv("VOXTRAL_INTERIM_RESULTS", "true") + assert VoxtralRealtimeConfig().interim_results is True + + def test_custom_model_via_env(self, monkeypatch): + monkeypatch.setenv("VOXTRAL_MODEL", "my-custom-model") + assert VoxtralRealtimeConfig().model == "my-custom-model" + + def test_custom_api_key_via_env(self, monkeypatch): + monkeypatch.setenv("VOXTRAL_API_KEY", "sk-test-key") + assert VoxtralRealtimeConfig().api_key == "sk-test-key" + + def test_custom_base_url_via_env(self, monkeypatch): + monkeypatch.setenv("VOXTRAL_BASE_URL", "http://localhost:8000") + assert VoxtralRealtimeConfig().base_url == "http://localhost:8000" + + +# ── URL builder ──────────────────────────────────────────────────────────────── + + +class TestBuildWsUrl: + def test_missing_base_url_raises_at_agent_creation(self): + """Voxtral is self-hosted; defaulting to api.openai.com can only + produce confusing auth/protocol errors mid-meeting. Fail at startup.""" + with pytest.raises(ValueError, match="VOXTRAL_BASE_URL"): + VoxtralRealtimeSttAgent( + VoxtralRealtimeConfig(api_key="test-key", base_url=None), + vad=_make_mock_vad(), + ) + + def test_custom_https_url_becomes_wss(self): + agent = _make_agent(base_url="https://my-server.example.com/v1") + assert agent._build_ws_url().startswith("wss://") + + def test_custom_http_url_becomes_ws(self): + agent = _make_agent(base_url="http://localhost:8000/v1") + assert agent._build_ws_url().startswith("ws://") + + def test_trailing_slash_in_base_url_is_stripped(self): + agent = _make_agent(base_url="https://my-server.example.com/v1/") + url = agent._build_ws_url() + assert "//realtime" not in url + + def test_custom_host_is_preserved(self): + agent = _make_agent(base_url="https://my-server.example.com/v1") + assert "my-server.example.com" in agent._build_ws_url() + + +# ── PCM conversion ───────────────────────────────────────────────────────────── + + +class TestAudioNormalizer: + def test_returns_bytes(self): + frame = _make_audio_frame() + assert isinstance(_AudioNormalizer().process(frame), bytes) + + def test_mono_16k_passthrough_preserves_values(self): + frame = _make_audio_frame(amplitude=1000, sample_rate=16000, num_channels=1) + result = _AudioNormalizer().process(frame) + samples = np.frombuffer(result, dtype=np.int16) + assert len(samples) == frame.samples_per_channel + assert all(s == 1000 for s in samples) + + def test_stereo_downmix_to_mono(self): + """Stereo frame with equal channels averages to same amplitude.""" + frame = _make_audio_frame(amplitude=1000, sample_rate=16000, num_channels=2) + result = _AudioNormalizer().process(frame) + samples = np.frombuffer(result, dtype=np.int16) + assert len(samples) == frame.samples_per_channel + assert all(s == 1000 for s in samples) + + def test_resampling_from_48k_produces_correct_total_length(self): + """process() over many frames + flush() yields ~1/3 the samples. + + The resampler is streaming, so individual frames may return fewer + samples (filter latency); only the drained total is deterministic. + """ + normalizer = _AudioNormalizer() + n_frames = 20 # 200 ms at 48 kHz + out = b"".join( + normalizer.process(_make_audio_frame(amplitude=500, sample_rate=48000)) + for _ in range(n_frames) + ) + out += normalizer.flush() + total_in = n_frames * 160 + expected = total_in * 16000 // 48000 + got = len(out) // 2 + assert abs(got - expected) <= 32, f"expected ~{expected} samples, got {got}" + + def test_downsampling_attenuates_above_target_nyquist(self): + """Anti-aliasing regression: a 10 kHz tone at 48 kHz lies above the + 16 kHz target's Nyquist (8 kHz) and must be strongly attenuated. + Naive linear interpolation instead folds it into the speech band at + nearly full energy — the defect this normalizer replaces. + """ + normalizer = _AudioNormalizer() + rate_in, tone_hz, duration_s = 48000, 10000, 0.2 + t = np.arange(int(rate_in * duration_s)) / rate_in + tone = (0.5 * 32767 * np.sin(2 * np.pi * tone_hz * t)).astype(np.int16) + + out = bytearray() + frame_samples = 480 # 10 ms frames, as LiveKit delivers + for i in range(0, len(tone), frame_samples): + chunk = tone[i : i + frame_samples] + frame = _make_audio_frame(sample_rate=rate_in) + frame.data = chunk.tobytes() + frame.samples_per_channel = len(chunk) + out += normalizer.process(frame) + out += normalizer.flush() + + in_rms = np.sqrt(np.mean(tone.astype(np.float64) ** 2)) + out_samples = np.frombuffer(bytes(out), dtype=np.int16).astype(np.float64) + out_rms = np.sqrt(np.mean(out_samples**2)) if len(out_samples) else 0.0 + assert out_rms < 0.1 * in_rms, ( + f"10 kHz tone must be attenuated by the anti-aliasing filter " + f"(in_rms={in_rms:.0f}, out_rms={out_rms:.0f})" + ) + + +# ── start_transcription_for_user ─────────────────────────────────────────────── + + +class TestStartTranscriptionForUser: + def test_participant_not_found_logs_error(self, caplog): + agent = _make_agent_with_room(participants={}) + with caplog.at_level("ERROR"): + agent.start_transcription_for_user( + "ghost_user", "en-US", "voxtral-realtime" + ) + assert "ghost_user" in caplog.text + assert "ghost_user" not in agent.processing_info + + def test_no_audio_track_logs_warning(self, caplog): + participant = _make_participant("user_1", has_audio_track=False) + agent = _make_agent_with_room(participants={"p1": participant}) + with caplog.at_level("WARNING"): + agent.start_transcription_for_user("user_1", "en-US", "voxtral-realtime") + assert "user_1" in caplog.text + assert "user_1" not in agent.processing_info + + def test_already_running_is_ignored(self): + participant = _make_participant("user_1") + agent = _make_agent_with_room(participants={"p1": participant}) + existing_task = MagicMock() + agent.processing_info["user_1"] = {"task": existing_task} + + agent.start_transcription_for_user("user_1", "en-US", "voxtral-realtime") + + assert agent.processing_info["user_1"]["task"] is existing_task + + async def test_success_adds_task_to_processing_info(self): + participant = _make_participant("user_1") + agent = _make_agent_with_room(participants={"p1": participant}) + + with patch.object(agent, "_run_transcription_pipeline", new_callable=AsyncMock): + agent.start_transcription_for_user("user_1", "en-US", "voxtral-realtime") + + assert "user_1" in agent.processing_info + assert "task" in agent.processing_info["user_1"] + agent.processing_info.pop("user_1", None) + + async def test_locale_is_sanitized_to_language_code(self): + participant = _make_participant("user_1") + agent = _make_agent_with_room(participants={"p1": participant}) + + with patch.object( + agent, "_run_transcription_pipeline", new_callable=AsyncMock + ) as mock_pipeline: + agent.start_transcription_for_user("user_1", "pt-BR", "voxtral-realtime") + await asyncio.sleep(0) + + # _run_transcription_pipeline(participant, track, language) — language must be "pt" + assert mock_pipeline.call_args[0][2] == "pt" + agent.processing_info.pop("user_1", None) + + async def test_settings_stored_on_start(self): + participant = _make_participant("user_1") + agent = _make_agent_with_room(participants={"p1": participant}) + + with patch.object(agent, "_run_transcription_pipeline", new_callable=AsyncMock): + agent.start_transcription_for_user("user_1", "de-DE", "voxtral-realtime") + + settings = agent.participant_settings.get("user_1", {}) + assert settings["locale"] == "de-DE" + assert settings["provider"] == "voxtral-realtime" + agent.processing_info.pop("user_1", None) + + +# ── _cleanup ─────────────────────────────────────────────────────────────────── + + +class TestCleanup: + async def test_closes_http_session_and_sets_none(self): + agent = _make_agent() + mock_session = AsyncMock() + agent._http_session = mock_session + + await agent._cleanup() + + mock_session.close.assert_called_once() + assert agent._http_session is None + + async def test_no_op_when_no_session(self): + agent = _make_agent() + assert agent._http_session is None + await agent._cleanup() # must not raise + + +# ── _run_transcription_pipeline — early exit paths ──────────────────────────── + + +class TestRunTranscriptionPipeline: + def _ws_context(self, first_message): + """Build an async context manager that yields a mock WS with one receive.""" + mock_ws = AsyncMock() + mock_ws.receive = AsyncMock(return_value=first_message) + mock_ws.send_json = AsyncMock() + cm = AsyncMock() + cm.__aenter__ = AsyncMock(return_value=mock_ws) + cm.__aexit__ = AsyncMock(return_value=False) + return cm + + def _mock_session(self, first_message): + session = MagicMock() + session.ws_connect = MagicMock(return_value=self._ws_context(first_message)) + return session + + async def test_exits_cleanly_on_non_text_first_message(self, caplog): + agent = _make_agent() + participant = MagicMock(spec=rtc.RemoteParticipant) + participant.identity = "user_1" + + binary_msg = MagicMock() + binary_msg.type = aiohttp.WSMsgType.BINARY + + agent._http_session = self._mock_session(binary_msg) + + mock_stream = AsyncMock() + mock_stream.__aiter__.return_value = iter([]) + mock_stream.aclose = AsyncMock() + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", return_value=mock_stream + ): + await agent._run_transcription_pipeline(participant, MagicMock(), "en") + + assert "user_1" not in agent.processing_info + + async def test_exits_cleanly_on_wrong_first_message_type(self, caplog): + agent = _make_agent() + participant = MagicMock(spec=rtc.RemoteParticipant) + participant.identity = "user_1" + + agent._http_session = self._mock_session( + _text_ws_msg({"type": "session.error"}) # not "session.created" + ) + + mock_stream = AsyncMock() + mock_stream.__aiter__.return_value = iter([]) + mock_stream.aclose = AsyncMock() + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", return_value=mock_stream + ): + await agent._run_transcription_pipeline(participant, MagicMock(), "en") + + assert "user_1" not in agent.processing_info + + +# ── _vad_loop — speech detection and final flush ────────────────────────────── + + +def _make_vad_event(event_type: agents_vad.VADEventType) -> MagicMock: + ev = MagicMock() + ev.type = event_type + return ev + + +class TestVadLoop: + def _full_pipeline_setup(self, audio_frames, ws_messages, vad_events=None): + """ + Return (agent, participant, mock_stream) wired up so that + _run_transcription_pipeline can run end-to-end through the VAD loop. + ws_messages is a list appended after the mandatory session.created message. + vad_events: Silero VAD events emitted by the mock; defaults to + [START_OF_SPEECH, END_OF_SPEECH] so a commit fires. + """ + if vad_events is None: + vad_events = [ + _make_vad_event(agents_vad.VADEventType.START_OF_SPEECH), + _make_vad_event(agents_vad.VADEventType.END_OF_SPEECH), + ] + agent = _make_agent(interim_results=True, vad_events=vad_events) + participant = MagicMock(spec=rtc.RemoteParticipant) + participant.identity = "user_vad" + + all_ws = [_text_ws_msg({"type": "session.created"})] + ws_messages + + mock_ws = AsyncMock() + mock_ws.receive = AsyncMock(side_effect=all_ws) + + # Use a real async function so that awaiting send_json actually yields + # to the event loop — this gives the concurrent _reader() task a chance + # to process incoming WS messages while the writer is still sending. + async def _send_json(_data): + await asyncio.sleep(0) + + mock_ws.send_json = _send_json + cm = AsyncMock() + cm.__aenter__ = AsyncMock(return_value=mock_ws) + cm.__aexit__ = AsyncMock(return_value=False) + + mock_session = MagicMock() + mock_session.ws_connect = MagicMock(return_value=cm) + agent._http_session = mock_session + + audio_events = [MagicMock(frame=f) for f in audio_frames] + mock_stream = AsyncMock() + mock_stream.__aiter__.return_value = iter(audio_events) + mock_stream.aclose = AsyncMock() + + return agent, participant, mock_stream + + async def test_silent_frames_do_not_trigger_flush(self): + """No VAD events — no commit fires, no transcript event.""" + silence = _make_audio_frame(amplitude=0) + agent, participant, mock_stream = self._full_pipeline_setup( + audio_frames=[silence], + ws_messages=[], + vad_events=[], # Silero never fires → no commit → no transcript + ) + + emitted = [] + agent.on("final_transcript", lambda **kw: emitted.append(kw)) + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", return_value=mock_stream + ): + await agent._run_transcription_pipeline(participant, MagicMock(), "en") + + assert emitted == [] + + async def test_loud_frame_followed_by_end_of_stream_emits_final(self): + """ + One loud frame followed by end-of-stream triggers the end-of-stream flush, + which should produce a final_transcript event. + """ + loud = _make_loud_frame() + agent, participant, mock_stream = self._full_pipeline_setup( + audio_frames=[loud], + ws_messages=[ + _text_ws_msg({"type": "transcription.delta", "delta": "hello"}), + _text_ws_msg({"type": "transcription.done", "text": "hello"}), + ], + ) + + emitted = [] + agent.on("final_transcript", lambda **kw: emitted.append(kw)) + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", return_value=mock_stream + ): + await agent._run_transcription_pipeline(participant, MagicMock(), "en") + await asyncio.sleep(0) + + assert len(emitted) == 1 + event = emitted[0]["event"] + assert event.type == stt.SpeechEventType.FINAL_TRANSCRIPT + assert event.alternatives[0].text == "hello" + + async def test_interim_deltas_emitted_during_flush(self): + """Delta messages during flush are emitted as interim_transcript events.""" + loud = _make_loud_frame() + agent, participant, mock_stream = self._full_pipeline_setup( + audio_frames=[loud], + ws_messages=[ + _text_ws_msg({"type": "transcription.delta", "delta": "hi"}), + _text_ws_msg({"type": "transcription.done", "text": "hi"}), + ], + ) + + interim = [] + final = [] + agent.on("interim_transcript", lambda **kw: interim.append(kw)) + agent.on("final_transcript", lambda **kw: final.append(kw)) + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", return_value=mock_stream + ): + await agent._run_transcription_pipeline(participant, MagicMock(), "en") + await asyncio.sleep(0) + + assert len(interim) >= 1 + assert interim[0]["event"].type == stt.SpeechEventType.INTERIM_TRANSCRIPT + assert len(final) == 1 + + async def test_two_utterances_emit_two_finals_with_independent_text(self): + """ + Two speech→silence cycles produce two independent FINAL transcripts. + + Guards the per-utterance reset: the delta accumulator and utterance_start + are cleared on transcription.done so the second utterance does not inherit + the first utterance's text (no "helloworld" bleed). + """ + loud = _make_loud_frame() + # A single silence frame long enough to cross _SILENCE_DURATION_S (0.6 s), + # flushing the utterance: 9600 samples / 16000 Hz = 0.6 s. + silence = _make_audio_frame(amplitude=0, samples_per_channel=9600) + agent, participant, mock_stream = self._full_pipeline_setup( + audio_frames=[loud, silence, loud, silence], + ws_messages=[ + _text_ws_msg({"type": "transcription.delta", "delta": "hello"}), + _text_ws_msg({"type": "transcription.done", "text": "hello"}), + _text_ws_msg({"type": "transcription.delta", "delta": "world"}), + _text_ws_msg({"type": "transcription.done", "text": "world"}), + ], + ) + + interim = [] + final = [] + agent.on("interim_transcript", lambda **kw: interim.append(kw)) + agent.on("final_transcript", lambda **kw: final.append(kw)) + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", return_value=mock_stream + ): + await agent._run_transcription_pipeline(participant, MagicMock(), "en") + await asyncio.sleep(0) + + final_texts = [kw["event"].alternatives[0].text for kw in final] + assert final_texts == ["hello", "world"] + + # The second utterance's interim must not carry the first's text. + second_interim = [ + kw["event"].alternatives[0].text + for kw in interim + if kw["event"].alternatives[0].text.startswith("world") + ] + assert second_interim, "expected an interim for the second utterance" + assert not any(t.startswith("hello") for t in second_interim) + + async def test_all_events_of_an_utterance_share_start_time(self): + """ + The interim and final transcripts for one utterance carry the same + start_time — the snapshot taken at the first delta, so every event lands + on the same BBB transcriptId rather than drifting with later deltas. + """ + loud = _make_loud_frame() + agent, participant, mock_stream = self._full_pipeline_setup( + audio_frames=[loud], + ws_messages=[ + _text_ws_msg({"type": "transcription.delta", "delta": "one "}), + _text_ws_msg({"type": "transcription.delta", "delta": "two"}), + _text_ws_msg({"type": "transcription.done", "text": "one two"}), + ], + ) + + interim = [] + final = [] + agent.on("interim_transcript", lambda **kw: interim.append(kw)) + agent.on("final_transcript", lambda **kw: final.append(kw)) + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", return_value=mock_stream + ): + await agent._run_transcription_pipeline(participant, MagicMock(), "en") + await asyncio.sleep(0) + + assert len(final) == 1 + start_times = {kw["event"].alternatives[0].start_time for kw in interim} + start_times.add(final[0]["event"].alternatives[0].start_time) + assert len(start_times) == 1, ( + f"all events for one utterance must share start_time, got {start_times}" + ) + + async def test_max_buffer_split_reopens_stream(self, monkeypatch): + """ + Regression for the is_in_speech / stream_open dual-ownership bug: a + continuous utterance that exceeds the max-buffer cap (speaker never + pauses, so Silero emits START but no END) must REOPEN a fresh streaming + request after each forced close — otherwise the rest of the utterance is + appended with no opening commit and the server silently stops streaming. + """ + import providers.voxtral_realtime as vr + + # Trip the safety cap after ~2 frames (each _make_audio_frame is 0.01 s). + monkeypatch.setattr(vr, "_MAX_BUFFER_DURATION_S", 0.015) + + agent = _make_agent( + interim_results=True, + # START only, no END → speaker stays "in speech" the whole time. + vad_events=[_make_vad_event(agents_vad.VADEventType.START_OF_SPEECH)], + ) + participant = MagicMock(spec=rtc.RemoteParticipant) + participant.identity = "user_split" + + sent: list[dict] = [] + + async def _send_json(data): + sent.append(data) + await asyncio.sleep(0) + + mock_ws = AsyncMock() + mock_ws.receive = AsyncMock( + side_effect=[_text_ws_msg({"type": "session.created"})] + ) + mock_ws.send_json = _send_json + cm = AsyncMock() + cm.__aenter__ = AsyncMock(return_value=mock_ws) + cm.__aexit__ = AsyncMock(return_value=False) + mock_session = MagicMock() + mock_session.ws_connect = MagicMock(return_value=cm) + agent._http_session = mock_session + + frames = [_make_loud_frame() for _ in range(6)] + audio_events = [MagicMock(frame=f) for f in frames] + mock_stream = AsyncMock() + mock_stream.__aiter__.return_value = iter(audio_events) + mock_stream.aclose = AsyncMock() + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", return_value=mock_stream + ): + await agent._run_transcription_pipeline(participant, MagicMock(), "en") + + commits = [m for m in sent if m.get("type") == "input_audio_buffer.commit"] + # Each open sends one bare commit; each close sends one final commit. + openers = [m for m in commits if "final" not in m] + assert len(openers) >= 2, ( + f"expected the stream to reopen after a max-buffer split " + f"(>=2 opener commits), got {len(openers)}" + ) + + async def test_max_buffer_split_segments_get_distinct_start_times( + self, monkeypatch + ): + """ + Regression for the transcript-overwrite bug: when one long utterance is + split by the max-buffer cap, Silero fires no new START_OF_SPEECH, so a + shared "speech start" would give both segments the same start_time — + the same BBB transcriptId — and segment 2's text would REPLACE segment + 1's in the transcript. Each opener must record its own start time and + the reader must pair segments with them in FIFO order. + """ + import providers.voxtral_realtime as vr + + monkeypatch.setattr(vr, "_MAX_BUFFER_DURATION_S", 0.015) + # Deterministic, strictly-increasing clock so the two openers cannot + # land on the same wall-clock value (real splits are ~8 s apart). + fake_now = [1000.0] + + def _fake_time(): + fake_now[0] += 1.0 + return fake_now[0] + + monkeypatch.setattr(vr.time, "time", _fake_time) + + agent = _make_agent( + interim_results=True, + # START only, no END → continuous speech across the split. + vad_events=[_make_vad_event(agents_vad.VADEventType.START_OF_SPEECH)], + ) + participant = MagicMock(spec=rtc.RemoteParticipant) + participant.identity = "user_split_ts" + + sent: list[dict] = [] + + async def _send_json(data): + sent.append(data) + await asyncio.sleep(0) + + def _bare_commits(): + return [ + m + for m in sent + if m.get("type") == "input_audio_buffer.commit" and "final" not in m + ] + + def _closers(): + return [ + m + for m in sent + if m.get("type") == "input_audio_buffer.commit" + and m.get("final") is True + ] + + # Deliver each segment's transcription only after the writer has sent + # the corresponding commits — mirroring the real server's causality. + # Segment 1 events after its close; segment 2 events after reopen + # (bare commits are openers only: open1 + open2 = 2 bare commits). + closed_msg = MagicMock() + closed_msg.type = aiohttp.WSMsgType.CLOSED + script = [ + (lambda: True, _text_ws_msg({"type": "session.created"})), + ( + lambda: len(_closers()) >= 1, + _text_ws_msg({"type": "transcription.delta", "delta": "hello"}), + ), + ( + lambda: len(_closers()) >= 1, + _text_ws_msg({"type": "transcription.done", "text": "hello"}), + ), + ( + lambda: len(_bare_commits()) >= 2, + _text_ws_msg({"type": "transcription.delta", "delta": "world"}), + ), + ( + lambda: len(_bare_commits()) >= 2, + _text_ws_msg({"type": "transcription.done", "text": "world"}), + ), + (lambda: True, closed_msg), + ] + script_iter = iter(script) + + async def _receive(): + try: + cond, msg = next(script_iter) + except StopIteration: + return closed_msg + while not cond(): + await asyncio.sleep(0) + return msg + + mock_ws = AsyncMock() + mock_ws.receive = _receive + mock_ws.send_json = _send_json + cm = AsyncMock() + cm.__aenter__ = AsyncMock(return_value=mock_ws) + cm.__aexit__ = AsyncMock(return_value=False) + mock_session = MagicMock() + mock_session.ws_connect = MagicMock(return_value=cm) + agent._http_session = mock_session + + frames = [_make_loud_frame() for _ in range(6)] + audio_events = [MagicMock(frame=f) for f in frames] + mock_stream = AsyncMock() + mock_stream.__aiter__.return_value = iter(audio_events) + mock_stream.aclose = AsyncMock() + + final = [] + agent.on("final_transcript", lambda **kw: final.append(kw)) + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", return_value=mock_stream + ): + await agent._run_transcription_pipeline(participant, MagicMock(), "en") + await asyncio.sleep(0) + + texts = [kw["event"].alternatives[0].text for kw in final] + assert texts == ["hello", "world"] + + starts = [kw["event"].alternatives[0].start_time for kw in final] + assert starts[0] < starts[1], ( + f"split segments must have distinct, increasing start_times " + f"(distinct BBB transcriptIds) — got {starts}; equal values mean " + f"segment 2 overwrites segment 1 in the transcript" + ) + + +# ── Split overlap and onset pre-roll ─────────────────────────────────────────── + + +def _segments_from_sent(sent: list[dict]) -> list[bytes]: + """Group appended audio bytes into segments delimited by opener commits.""" + segments: list[bytearray] = [] + for m in sent: + t = m.get("type") + if t == "input_audio_buffer.commit" and "final" not in m: + segments.append(bytearray()) + elif t == "input_audio_buffer.append" and segments: + segments[-1] += base64.b64decode(m["audio"]) + return [bytes(s) for s in segments] + + +class _DeferredStartVadStream: + """VAD stream double that fires START_OF_SPEECH after N pushed frames.""" + + def __init__(self, after_frames: int): + self._after = after_frames + self.pushed = 0 + self._fired = False + + def push_frame(self, _frame): + self.pushed += 1 + + def end_input(self): + pass + + async def aclose(self): + pass + + def __aiter__(self): + return self + + async def __anext__(self): + if self._fired: + await asyncio.Event().wait() # block until cancelled + while self.pushed < self._after: + await asyncio.sleep(0) + self._fired = True + return _make_vad_event(agents_vad.VADEventType.START_OF_SPEECH) + + +class TestSplitOverlap: + def _run_pipeline(self, agent, frames): + participant = MagicMock(spec=rtc.RemoteParticipant) + participant.identity = "user_overlap" + + sent: list[dict] = [] + + async def _send_json(data): + sent.append(data) + await asyncio.sleep(0) + + mock_ws = AsyncMock() + mock_ws.receive = AsyncMock( + side_effect=[_text_ws_msg({"type": "session.created"})] + ) + mock_ws.send_json = _send_json + cm = AsyncMock() + cm.__aenter__ = AsyncMock(return_value=mock_ws) + cm.__aexit__ = AsyncMock(return_value=False) + mock_session = MagicMock() + mock_session.ws_connect = MagicMock(return_value=cm) + agent._http_session = mock_session + + audio_events = [MagicMock(frame=f) for f in frames] + mock_stream = AsyncMock() + mock_stream.__aiter__.return_value = iter(audio_events) + mock_stream.aclose = AsyncMock() + + return participant, mock_stream, sent + + async def test_split_reopen_replays_overlap(self, monkeypatch): + """ + Regression for word loss at max-buffer splits: the reopened segment + must begin with the overlap — the tail of the audio already sent in + the previous segment — so boundary words carry fully into the new + request instead of being cut at the commit boundary. + """ + import providers.voxtral_realtime as vr + + monkeypatch.setattr(vr, "_MAX_BUFFER_DURATION_S", 0.015) + # Shrink the onset pre-roll far below one frame so a full-frame overlap + # can ONLY come from the split path: if the reopen were mistaken for a + # fresh onset, it would replay a sliver and the assertion would fail. + monkeypatch.setattr(vr, "_VAD_PREROLL_S", 0.005) + + agent = _make_agent( + # START only, no END → continuous speech across the split. + vad_events=[_make_vad_event(agents_vad.VADEventType.START_OF_SPEECH)], + ) + # Distinct per-frame amplitudes so overlapping byte ranges are + # distinguishable (constant amplitude would make any comparison pass). + frames = [_make_audio_frame(amplitude=100 + i) for i in range(6)] + participant, mock_stream, sent = self._run_pipeline(agent, frames) + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", return_value=mock_stream + ): + await agent._run_transcription_pipeline(participant, MagicMock(), "en") + + segments = _segments_from_sent(sent) + assert len(segments) >= 2, f"expected a split, got {len(segments)} segment(s)" + seg1, seg2 = segments[0], segments[1] + + frame_bytes = 320 # 160 samples of int16 + overlap = min(len(seg1), len(seg2)) + assert overlap >= frame_bytes and seg1.endswith(seg2[:overlap]), ( + "the reopened segment must start with the tail of the previous " + "segment's audio (the split overlap)" + ) + + async def test_fresh_onset_replay_is_capped(self, monkeypatch): + """ + The rolling buffer is now sized for the split overlap (long), but a + FRESH onset must still replay only the onset pre-roll — otherwise it + would replay the previous utterance's tail from before the silence + gap and duplicate it. + """ + import providers.voxtral_realtime as vr + + onset_secs = 0.02 # 2 frames + monkeypatch.setattr(vr, "_VAD_PREROLL_S", onset_secs) + monkeypatch.setattr(vr, "_SPLIT_OVERLAP_S", 10.0) # buffer far larger + + idle_frames = 100 # 1 s of audio buffered before speech starts + vad_stream = _DeferredStartVadStream(after_frames=idle_frames) + mock_vad = MagicMock() + mock_vad.stream.return_value = vad_stream + agent = VoxtralRealtimeSttAgent(_make_config(), vad=mock_vad) + + frames = [_make_audio_frame(amplitude=100 + i) for i in range(idle_frames + 4)] + participant, mock_stream, sent = self._run_pipeline(agent, frames) + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", return_value=mock_stream + ): + await asyncio.wait_for( + agent._run_transcription_pipeline(participant, MagicMock(), "en"), + timeout=5.0, + ) + + segments = _segments_from_sent(sent) + assert len(segments) == 1, f"expected one segment, got {len(segments)}" + seg = segments[0] + + frame_bytes = 320 + onset_bytes = int(onset_secs * 16000) * 2 + full_audio = b"".join(f.data for f in frames) + + # The segment must be a contiguous suffix of the input audio (replay + # directly precedes the live frames, no gap) … + assert full_audio.endswith(seg), "segment audio must be a contiguous suffix" + # … and bounded: onset replay + the few frames after the VAD fired — + # NOT the ~32 kB of idle audio sitting in the oversized buffer. + max_live_frames = 6 + assert len(seg) <= onset_bytes + max_live_frames * frame_bytes, ( + f"fresh onset replayed {len(seg)} bytes; the onset cap is " + f"{onset_bytes} bytes — the long split-overlap buffer must not be " + f"replayed on a fresh onset" + ) + + +# ── Commit gate ──────────────────────────────────────────────────────────────── + + +class _ScheduledVadStream: + """VAD double firing scripted events after N pushed frames.""" + + def __init__(self, schedule): + # schedule: list of (after_pushed_frames, VADEventType) + self._schedule = list(schedule) + self.pushed = 0 + + def push_frame(self, _frame): + self.pushed += 1 + + def end_input(self): + pass + + async def aclose(self): + pass + + def __aiter__(self): + return self + + async def __anext__(self): + while True: + if not self._schedule: + await asyncio.Event().wait() # block until cancelled + after, ev_type = self._schedule[0] + if self.pushed >= after: + self._schedule.pop(0) + return _make_vad_event(ev_type) + await asyncio.sleep(0) + + +class TestCommitGate: + """vLLM silently drops a commit sent while the previous segment's + generation is still running ("Generation already in progress, ignoring + commit") — the segment then never streams, or loses its transcription.done + entirely. Opening commits must wait for the previous segment's done.""" + + _RELEASE_MARKER = {"type": "_test_done_released"} + + def _wire(self, agent, frames, script): + """Wire agent to a scripted WS; returns (participant, stream, sent). + + script: list of (condition, message, mark) — receive() serves each + message once its condition (over `sent`) holds; mark=True appends + _RELEASE_MARKER to `sent` first, recording the release moment in the + send/receive order. + """ + participant = MagicMock(spec=rtc.RemoteParticipant) + participant.identity = "user_gate" + + sent: list[dict] = [] + + async def _send_json(data): + sent.append(data) + await asyncio.sleep(0) + + closed_msg = MagicMock() + closed_msg.type = aiohttp.WSMsgType.CLOSED + script_iter = iter(script) + + async def _receive(*args, **kwargs): + try: + cond, msg, mark = next(script_iter) + except StopIteration: + return closed_msg + while not cond(): + await asyncio.sleep(0) + if mark: + sent.append(dict(self._RELEASE_MARKER)) + return msg + + mock_ws = AsyncMock() + mock_ws.receive = _receive + mock_ws.send_json = _send_json + cm = AsyncMock() + cm.__aenter__ = AsyncMock(return_value=mock_ws) + cm.__aexit__ = AsyncMock(return_value=False) + mock_session = MagicMock() + mock_session.ws_connect = MagicMock(return_value=cm) + agent._http_session = mock_session + + audio_events = [MagicMock(frame=f) for f in frames] + mock_stream = AsyncMock() + mock_stream.__aiter__.return_value = iter(audio_events) + mock_stream.aclose = AsyncMock() + + return participant, mock_stream, sent + + @staticmethod + def _openers(sent): + return [ + i + for i, m in enumerate(sent) + if m.get("type") == "input_audio_buffer.commit" and "final" not in m + ] + + @staticmethod + def _closers(sent): + return [ + i + for i, m in enumerate(sent) + if m.get("type") == "input_audio_buffer.commit" and m.get("final") is True + ] + + def _marker_index(self, sent): + return next(i for i, m in enumerate(sent) if m == self._RELEASE_MARKER) + + async def test_opener_waits_for_previous_done_and_loses_no_audio(self, monkeypatch): + """ + Regression for the dropped-commit degradation: after a max-buffer + split, the reopen's opening commit must NOT be sent until the previous + segment's transcription.done has been read — vLLM ignores commits + during an in-flight generation, which cost the reopened segment its + streaming (and, on a swallowed closer, its FINAL). The audio arriving + during the wait must be buffered and replayed, not dropped. + """ + import providers.voxtral_realtime as vr + + monkeypatch.setattr(vr, "_MAX_BUFFER_DURATION_S", 0.015) + + vad_stream = _ScheduledVadStream([(1, agents_vad.VADEventType.START_OF_SPEECH)]) + mock_vad = MagicMock() + mock_vad.stream.return_value = vad_stream + agent = VoxtralRealtimeSttAgent(_make_config(), vad=mock_vad) + + frames = [_make_audio_frame(amplitude=100 + i) for i in range(8)] + + # Hold segment 1's transcription until well after the split close + # (6 frames pushed), so an ungated reopen would fire first. + sent_holder: dict = {} + + def _cond_done1(): + sent = sent_holder["sent"] + return vad_stream.pushed >= 6 and self._closers(sent) + + def _cond_seg2(): + return len(self._openers(sent_holder["sent"])) >= 2 + + script = [ + (lambda: True, _text_ws_msg({"type": "session.created"}), False), + ( + _cond_done1, + _text_ws_msg({"type": "transcription.delta", "delta": "hello"}), + True, + ), + ( + lambda: True, + _text_ws_msg({"type": "transcription.done", "text": "hello"}), + False, + ), + ( + _cond_seg2, + _text_ws_msg({"type": "transcription.delta", "delta": "world"}), + False, + ), + ( + lambda: True, + _text_ws_msg({"type": "transcription.done", "text": "world"}), + False, + ), + ] + + participant, mock_stream, sent2 = self._wire(agent, frames, script) + sent_holder["sent"] = sent2 + + final = [] + agent.on("final_transcript", lambda **kw: final.append(kw)) + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", return_value=mock_stream + ): + await asyncio.wait_for( + agent._run_transcription_pipeline(participant, MagicMock(), "en"), + timeout=5.0, + ) + await asyncio.sleep(0) + + openers = self._openers(sent2) + assert len(openers) >= 2, f"expected a reopen, got {len(openers)} opener(s)" + marker = self._marker_index(sent2) + assert openers[1] > marker, ( + "the reopen's opening commit must be deferred until the previous " + "segment's transcription.done has been received — an earlier " + "commit is silently dropped by vLLM" + ) + + # No audio lost while gated: the frames that arrived while the opener + # was deferred (the gate window right after the split close) must be + # replayed contiguously into the reopened segment. + segments = _segments_from_sent(sent2) + assert len(segments) >= 2 + gated_audio = b"".join(f.data for f in frames[2:5]) + assert gated_audio in segments[1], ( + "audio arriving while the opener was gated must be buffered and " + "replayed into the reopened segment, not dropped" + ) + + texts = [kw["event"].alternatives[0].text for kw in final] + assert texts == ["hello", "world"] + + async def test_utterance_ending_while_gated_is_still_sent(self): + """ + A short utterance that starts AND ends while the previous segment's + done is outstanding must still be sent (open + close) once the server + frees up — not stay buffered forever or be dropped. + """ + vad_stream = _ScheduledVadStream( + [ + (1, agents_vad.VADEventType.START_OF_SPEECH), + (3, agents_vad.VADEventType.END_OF_SPEECH), + (6, agents_vad.VADEventType.START_OF_SPEECH), + (9, agents_vad.VADEventType.END_OF_SPEECH), + ] + ) + mock_vad = MagicMock() + mock_vad.stream.return_value = vad_stream + agent = VoxtralRealtimeSttAgent(_make_config(), vad=mock_vad) + + frames = [_make_audio_frame(amplitude=100 + i) for i in range(16)] + + sent_holder: dict = {} + + def _cond_done1(): + sent = sent_holder["sent"] + return vad_stream.pushed >= 12 and self._closers(sent) + + def _cond_seg2(): + return len(self._openers(sent_holder["sent"])) >= 2 + + script = [ + (lambda: True, _text_ws_msg({"type": "session.created"}), False), + ( + _cond_done1, + _text_ws_msg({"type": "transcription.delta", "delta": "hello"}), + True, + ), + ( + lambda: True, + _text_ws_msg({"type": "transcription.done", "text": "hello"}), + False, + ), + ( + _cond_seg2, + _text_ws_msg({"type": "transcription.delta", "delta": "world"}), + False, + ), + ( + lambda: True, + _text_ws_msg({"type": "transcription.done", "text": "world"}), + False, + ), + ] + + participant, mock_stream, sent = self._wire(agent, frames, script) + sent_holder["sent"] = sent + + final = [] + agent.on("final_transcript", lambda **kw: final.append(kw)) + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", return_value=mock_stream + ): + await asyncio.wait_for( + agent._run_transcription_pipeline(participant, MagicMock(), "en"), + timeout=5.0, + ) + await asyncio.sleep(0) + + openers = self._openers(sent) + closers = self._closers(sent) + assert len(openers) == 2 and len(closers) == 2, ( + f"gated utterance must be opened and closed once the server frees " + f"up — got {len(openers)} openers / {len(closers)} closers" + ) + marker = self._marker_index(sent) + assert openers[1] > marker, ( + "utterance 2's opener must wait for utterance 1's done" + ) + texts = [kw["event"].alternatives[0].text for kw in final] + assert texts == ["hello", "world"] + + async def test_gate_timeout_resyncs_and_opens(self, monkeypatch, caplog): + """ + When a transcription.done never arrives (swallowed closing commit, + already-desynced session), the gate must not wedge the pipeline: after + the timeout it resyncs the outstanding counter and opens ungated — + degrading to the pre-gate behavior instead of buffering forever. + """ + import providers.voxtral_realtime as vr + + monkeypatch.setattr(vr, "_MAX_BUFFER_DURATION_S", 0.015) + monkeypatch.setattr(vr, "_OPEN_GATE_TIMEOUT_S", 0.0) + + vad_stream = _ScheduledVadStream([(1, agents_vad.VADEventType.START_OF_SPEECH)]) + mock_vad = MagicMock() + mock_vad.stream.return_value = vad_stream + agent = VoxtralRealtimeSttAgent(_make_config(), vad=mock_vad) + + frames = [_make_audio_frame(amplitude=100 + i) for i in range(8)] + script = [ + (lambda: True, _text_ws_msg({"type": "session.created"}), False), + # No transcription events ever — the done is lost. + ] + participant, mock_stream, sent = self._wire(agent, frames, script) + + with ( + patch( + "providers.voxtral_realtime.rtc.AudioStream", + return_value=mock_stream, + ), + caplog.at_level("WARNING"), + ): + await asyncio.wait_for( + agent._run_transcription_pipeline(participant, MagicMock(), "en"), + timeout=5.0, + ) + + assert "open gate timed out" in caplog.text + assert len(self._openers(sent)) >= 2, ( + "after the gate timeout the next segment must still open" + ) + + +# ── Failure recovery and teardown flush ──────────────────────────────────────── + + +class _ScriptedWs: + """Minimal WS double: serves a fixed message list, then blocks until closed. + + send_json raises ClientError once the socket is closed — mirroring aiohttp, + so the writer's send failure is what surfaces a reader-initiated close. + """ + + def __init__(self, messages): + self._messages = list(messages) + self.closed = False + self.sent: list[dict] = [] + + async def receive(self, *args, **kwargs): + await asyncio.sleep(0) + if self._messages: + return self._messages.pop(0) + while not self.closed: + await asyncio.sleep(0) + msg = MagicMock() + msg.type = aiohttp.WSMsgType.CLOSED + return msg + + async def send_json(self, data): + if self.closed: + raise aiohttp.ClientError("socket closed") + self.sent.append(data) + await asyncio.sleep(0) + + async def close(self): + self.closed = True + + +class _EndlessAudioStream: + """Async audio stream that yields loud frames until abandoned.""" + + def __aiter__(self): + return self + + async def __anext__(self): + await asyncio.sleep(0) + return MagicMock(frame=_make_loud_frame()) + + async def aclose(self): + pass + + +def _ws_context(ws): + cm = AsyncMock() + cm.__aenter__ = AsyncMock(return_value=ws) + cm.__aexit__ = AsyncMock(return_value=False) + return cm + + +class TestReaderFailureRecovery: + async def test_server_error_event_closes_ws_and_reconnects(self, monkeypatch): + """ + Regression for silent transcription death: a server `error` event makes + the reader exit while the connection stays alive. The reader must close + the socket so the writer's sends fail and the pipeline reconnects — + otherwise audio keeps streaming into a session nobody reads and every + subsequent utterance is lost without a trace. + """ + import providers.voxtral_realtime as vr + + monkeypatch.setattr(vr, "_RETRY_DELAY_INITIAL_S", 0.01) + + agent = _make_agent( + vad_events=[_make_vad_event(agents_vad.VADEventType.START_OF_SPEECH)], + ) + participant = MagicMock(spec=rtc.RemoteParticipant) + participant.identity = "user_err" + + ws1 = _ScriptedWs( + [ + _text_ws_msg({"type": "session.created"}), + _text_ws_msg({"type": "error", "error": "boom"}), + ] + ) + ws2 = _ScriptedWs([_text_ws_msg({"type": "session.created"})]) + + mock_session = MagicMock() + mock_session.ws_connect = MagicMock( + side_effect=[_ws_context(ws1), _ws_context(ws2)] + ) + agent._http_session = mock_session + + # First connection: endless frames so the writer keeps sending until + # the reader-initiated close makes a send fail. Second connection: + # empty stream so the pipeline exits cleanly. + empty_stream = AsyncMock() + empty_stream.__aiter__.return_value = iter([]) + empty_stream.aclose = AsyncMock() + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", + side_effect=[_EndlessAudioStream(), empty_stream], + ): + await asyncio.wait_for( + agent._run_transcription_pipeline(participant, MagicMock(), "en"), + timeout=5.0, + ) + + assert ws1.closed, "reader must close the WS after a server error event" + assert mock_session.ws_connect.call_count == 2, ( + "pipeline must reconnect after the reader-initiated close" + ) + + +class TestSessionUpdate: + async def test_sends_flat_model_and_greedy_temperature(self): + """vLLM requires a FLAT session.update (nesting under "session" is + rejected with "Missing required field: model" — probe test 6), and + the model card mandates temperature 0.0 for stable transcription.""" + agent = _make_agent(vad_events=[]) + participant = MagicMock(spec=rtc.RemoteParticipant) + participant.identity = "user_cfg" + + ws = _ScriptedWs([_text_ws_msg({"type": "session.created"})]) + mock_session = MagicMock() + mock_session.ws_connect = MagicMock(return_value=_ws_context(ws)) + agent._http_session = mock_session + + empty = AsyncMock() + empty.__aiter__.return_value = iter([]) + empty.aclose = AsyncMock() + + with patch("providers.voxtral_realtime.rtc.AudioStream", return_value=empty): + await asyncio.wait_for( + agent._run_transcription_pipeline(participant, MagicMock(), "en"), + timeout=5.0, + ) + + updates = [m for m in ws.sent if m.get("type") == "session.update"] + assert updates == [ + { + "type": "session.update", + "model": agent.config.model, + "temperature": 0.0, + } + ] + + +class TestHandshakeTimeout: + async def test_slow_session_created_retries_instead_of_giving_up(self, monkeypatch): + """ + Regression for permanent give-up during server warmup: vLLM takes + minutes of CUDA-graph warmup after startup, during which the WS may + connect but session.created arrives late. A handshake timeout must + retry with backoff like a connection error — not end transcription + for the participant's whole meeting. + """ + import providers.voxtral_realtime as vr + + monkeypatch.setattr(vr, "_RETRY_DELAY_INITIAL_S", 0.01) + + agent = _make_agent(vad_events=[]) + participant = MagicMock(spec=rtc.RemoteParticipant) + participant.identity = "user_warmup" + + # First connection: session.created never arrives (handshake timeout). + slow_ws = AsyncMock() + slow_ws.receive = AsyncMock(side_effect=asyncio.TimeoutError) + ws2 = _ScriptedWs([_text_ws_msg({"type": "session.created"})]) + + mock_session = MagicMock() + mock_session.ws_connect = MagicMock( + side_effect=[_ws_context(slow_ws), _ws_context(ws2)] + ) + agent._http_session = mock_session + + empty = AsyncMock() + empty.__aiter__.return_value = iter([]) + empty.aclose = AsyncMock() + empty2 = AsyncMock() + empty2.__aiter__.return_value = iter([]) + empty2.aclose = AsyncMock() + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", side_effect=[empty, empty2] + ): + await asyncio.wait_for( + agent._run_transcription_pipeline(participant, MagicMock(), "en"), + timeout=5.0, + ) + + assert mock_session.ws_connect.call_count == 2, ( + "a handshake timeout must reconnect with backoff, not give up" + ) + + +class TestLocaleUpdateRace: + async def test_locale_change_does_not_orphan_replacement_pipeline(self): + """ + Regression for the stop→start restart race: _update_stream_locale + cancels the old pipeline task and synchronously registers a new one + under the same identity. task.cancel() only schedules the + cancellation, so the old task's cleanup runs AFTER the new entry + exists — an unconditional pop there deregisters the replacement, + leaving it running but untracked (unstoppable, and a later start + would spawn a duplicate pipeline on the same track). + """ + participant = _make_participant("user_1") + agent = _make_agent_with_room(participants={"p1": participant}) + + ws1 = _ScriptedWs([_text_ws_msg({"type": "session.created"})]) + ws2 = _ScriptedWs([_text_ws_msg({"type": "session.created"})]) + mock_session = MagicMock() + mock_session.ws_connect = MagicMock( + side_effect=[_ws_context(ws1), _ws_context(ws2)] + ) + agent._http_session = mock_session + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", + side_effect=[_EndlessAudioStream(), _EndlessAudioStream()], + ): + agent.start_transcription_for_user("user_1", "en-US", "voxtral-realtime") + task1 = agent.processing_info["user_1"]["task"] + await asyncio.sleep(0.05) # let pipeline 1 get going + + agent._update_stream_locale("user_1", "de-DE") + task2 = agent.processing_info["user_1"]["task"] + assert task2 is not task1 + + # Let the cancelled task run its cleanup to completion. + await asyncio.wait_for(task1, timeout=5.0) + await asyncio.sleep(0) + + assert agent.processing_info.get("user_1", {}).get("task") is task2, ( + "the cancelled pipeline's cleanup must not deregister its replacement" + ) + + agent.stop_transcription_for_user("user_1") + await asyncio.wait_for(task2, timeout=5.0) + assert "user_1" not in agent.processing_info + + +class TestTeardownFlush: + async def test_cancel_mid_utterance_emits_synthetic_final_and_closing_commit( + self, + ): + """ + Regression for the speak-then-mute loss: when frames stop before Silero + fires END_OF_SPEECH and the task is cancelled (mute / track + unsubscribed), the open segment must still (a) send its closing + commit(final) so the server request is not left dangling, and (b) emit + a FINAL from the delta text already received — otherwise BBB keeps the + interim caption pending forever and the utterance is lost. + """ + agent = _make_agent( + interim_results=True, + vad_events=[_make_vad_event(agents_vad.VADEventType.START_OF_SPEECH)], + ) + participant = MagicMock(spec=rtc.RemoteParticipant) + participant.identity = "user_mute" + + # session.created + one delta; transcription.done never arrives. + ws = _ScriptedWs( + [ + _text_ws_msg({"type": "session.created"}), + _text_ws_msg({"type": "transcription.delta", "delta": "hello"}), + ] + ) + mock_session = MagicMock() + mock_session.ws_connect = MagicMock(return_value=_ws_context(ws)) + agent._http_session = mock_session + + interim = [] + final = [] + agent.on("interim_transcript", lambda **kw: interim.append(kw)) + agent.on("final_transcript", lambda **kw: final.append(kw)) + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", + return_value=_EndlessAudioStream(), + ): + task = asyncio.create_task( + agent._run_transcription_pipeline(participant, MagicMock(), "en") + ) + # Wait until the delta has been received and emitted as interim. + for _ in range(500): + if interim: + break + await asyncio.sleep(0.01) + assert interim, "expected an interim before cancelling" + + task.cancel() + await asyncio.wait_for(task, timeout=5.0) + + # Let the emit task scheduled during teardown run. + for _ in range(5): + await asyncio.sleep(0) + + texts = [kw["event"].alternatives[0].text for kw in final] + assert texts == ["hello"], ( + f"cancellation mid-utterance must emit a best-effort FINAL from the " + f"accumulated delta text, got {texts}" + ) + closers = [m for m in ws.sent if m.get("final") is True] + assert closers, "cancellation with an open segment must send commit(final=True)" + + async def test_end_of_stream_waits_for_late_transcription_done(self): + """ + Regression for the tail-utterance drop at clean stream end: the server's + transcription.done for the flushed segment arrives after the writer has + finished. The reader must be drained, not cancelled immediately, so the + tail utterance still gets its real FINAL. + """ + agent = _make_agent( + interim_results=True, + vad_events=[_make_vad_event(agents_vad.VADEventType.START_OF_SPEECH)], + ) + participant = MagicMock(spec=rtc.RemoteParticipant) + participant.identity = "user_tail" + + ws = _ScriptedWs([_text_ws_msg({"type": "session.created"})]) + + # Release the transcription only after the writer has sent the closing + # commit(final) — mirroring real server causality at end of stream. + real_receive = ws.receive + released = False + + async def _receive(*args, **kwargs): + nonlocal released + if not ws._messages and not ws.closed and not released: + while not any(m.get("final") is True for m in ws.sent): + await asyncio.sleep(0) + released = True + ws._messages = [ + _text_ws_msg({"type": "transcription.delta", "delta": "tail"}), + _text_ws_msg({"type": "transcription.done", "text": "tail words"}), + ] + return await real_receive() + + ws.receive = _receive + + mock_session = MagicMock() + mock_session.ws_connect = MagicMock(return_value=_ws_context(ws)) + agent._http_session = mock_session + + audio_events = [MagicMock(frame=_make_loud_frame()) for _ in range(3)] + mock_stream = AsyncMock() + mock_stream.__aiter__.return_value = iter(audio_events) + mock_stream.aclose = AsyncMock() + + final = [] + agent.on("final_transcript", lambda **kw: final.append(kw)) + + with patch( + "providers.voxtral_realtime.rtc.AudioStream", return_value=mock_stream + ): + await asyncio.wait_for( + agent._run_transcription_pipeline(participant, MagicMock(), "en"), + timeout=5.0, + ) + await asyncio.sleep(0) + + texts = [kw["event"].alternatives[0].text for kw in final] + assert texts == ["tail words"], ( + f"the tail segment's late transcription.done must still be read " + f"before teardown, got {texts}" + ) diff --git a/uv.lock b/uv.lock index 49bbc1c..a7342cb 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,9 @@ version = 1 revision = 3 -requires-python = ">=3.10" +requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.13'", - "python_full_version >= '3.11' and python_full_version < '3.13'", - "python_full_version < '3.11'", + "python_full_version < '3.13'", ] [[package]] @@ -32,7 +31,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, { name = "aiosignal" }, - { name = "async-timeout", marker = "python_full_version < '3.11'" }, { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, @@ -41,23 +39,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/dc/ef9394bde9080128ad401ac7ede185267ed637df03b51f05d14d1c99ad67/aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc", size = 703921, upload-time = "2025-07-29T05:49:43.584Z" }, - { url = "https://files.pythonhosted.org/packages/8f/42/63fccfc3a7ed97eb6e1a71722396f409c46b60a0552d8a56d7aad74e0df5/aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af", size = 480288, upload-time = "2025-07-29T05:49:47.851Z" }, - { url = "https://files.pythonhosted.org/packages/9c/a2/7b8a020549f66ea2a68129db6960a762d2393248f1994499f8ba9728bbed/aiohttp-3.12.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421", size = 468063, upload-time = "2025-07-29T05:49:49.789Z" }, - { url = "https://files.pythonhosted.org/packages/8f/f5/d11e088da9176e2ad8220338ae0000ed5429a15f3c9dfd983f39105399cd/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79", size = 1650122, upload-time = "2025-07-29T05:49:51.874Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6b/b60ce2757e2faed3d70ed45dafee48cee7bfb878785a9423f7e883f0639c/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77", size = 1624176, upload-time = "2025-07-29T05:49:53.805Z" }, - { url = "https://files.pythonhosted.org/packages/dd/de/8c9fde2072a1b72c4fadecf4f7d4be7a85b1d9a4ab333d8245694057b4c6/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c", size = 1696583, upload-time = "2025-07-29T05:49:55.338Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ad/07f863ca3d895a1ad958a54006c6dafb4f9310f8c2fdb5f961b8529029d3/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4", size = 1738896, upload-time = "2025-07-29T05:49:57.045Z" }, - { url = "https://files.pythonhosted.org/packages/20/43/2bd482ebe2b126533e8755a49b128ec4e58f1a3af56879a3abdb7b42c54f/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6", size = 1643561, upload-time = "2025-07-29T05:49:58.762Z" }, - { url = "https://files.pythonhosted.org/packages/23/40/2fa9f514c4cf4cbae8d7911927f81a1901838baf5e09a8b2c299de1acfe5/aiohttp-3.12.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2", size = 1583685, upload-time = "2025-07-29T05:50:00.375Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c3/94dc7357bc421f4fb978ca72a201a6c604ee90148f1181790c129396ceeb/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d", size = 1627533, upload-time = "2025-07-29T05:50:02.306Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3f/1f8911fe1844a07001e26593b5c255a685318943864b27b4e0267e840f95/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb", size = 1638319, upload-time = "2025-07-29T05:50:04.282Z" }, - { url = "https://files.pythonhosted.org/packages/4e/46/27bf57a99168c4e145ffee6b63d0458b9c66e58bb70687c23ad3d2f0bd17/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5", size = 1613776, upload-time = "2025-07-29T05:50:05.863Z" }, - { url = "https://files.pythonhosted.org/packages/0f/7e/1d2d9061a574584bb4ad3dbdba0da90a27fdc795bc227def3a46186a8bc1/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b", size = 1693359, upload-time = "2025-07-29T05:50:07.563Z" }, - { url = "https://files.pythonhosted.org/packages/08/98/bee429b52233c4a391980a5b3b196b060872a13eadd41c3a34be9b1469ed/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065", size = 1716598, upload-time = "2025-07-29T05:50:09.33Z" }, - { url = "https://files.pythonhosted.org/packages/57/39/b0314c1ea774df3392751b686104a3938c63ece2b7ce0ba1ed7c0b4a934f/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1", size = 1644940, upload-time = "2025-07-29T05:50:11.334Z" }, - { url = "https://files.pythonhosted.org/packages/1b/83/3dacb8d3f8f512c8ca43e3fa8a68b20583bd25636ffa4e56ee841ffd79ae/aiohttp-3.12.15-cp310-cp310-win32.whl", hash = "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a", size = 429239, upload-time = "2025-07-29T05:50:12.803Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f9/470b5daba04d558c9673ca2034f28d067f3202a40e17804425f0c331c89f/aiohttp-3.12.15-cp310-cp310-win_amd64.whl", hash = "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830", size = 452297, upload-time = "2025-07-29T05:50:14.266Z" }, { url = "https://files.pythonhosted.org/packages/20/19/9e86722ec8e835959bd97ce8c1efa78cf361fa4531fca372551abcc9cdd6/aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117", size = 711246, upload-time = "2025-07-29T05:50:15.937Z" }, { url = "https://files.pythonhosted.org/packages/71/f9/0a31fcb1a7d4629ac9d8f01f1cb9242e2f9943f47f5d03215af91c3c1a26/aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe", size = 483515, upload-time = "2025-07-29T05:50:17.442Z" }, { url = "https://files.pythonhosted.org/packages/62/6c/94846f576f1d11df0c2e41d3001000527c0fdf63fce7e69b3927a731325d/aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9", size = 471776, upload-time = "2025-07-29T05:50:19.568Z" }, @@ -147,7 +128,6 @@ name = "anyio" version = "4.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, @@ -181,15 +161,6 @@ version = "15.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/17/89/940a509ee7e9449f0c877fa984b37b7cc485546035cc67bbc353f2ac20f3/av-15.0.0.tar.gz", hash = "sha256:871c1a9becddf00b60b1294dc0bff9ff193ac31286aeec1a34039bd27e650183", size = 3833128, upload-time = "2025-07-03T16:23:48.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/c6/4646aeffca77fb9e557509528341fdef409f7e5de44c70858fb639bb9a3e/av-15.0.0-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:f20c7565ad9aed8a5e3ca7ed30b151d4d8a937e072b6a4901c3200134fe7c68b", size = 21808084, upload-time = "2025-07-03T16:21:17.028Z" }, - { url = "https://files.pythonhosted.org/packages/89/a2/8a0349e2ebf998f2305b61365240a748bc137f94f431e769c2ac83c5a321/av-15.0.0-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:0d8b78a88f0fdaf6591bca32b41301e40ba60be294b0698318948c4d1fa6f206", size = 26989279, upload-time = "2025-07-03T16:21:20.63Z" }, - { url = "https://files.pythonhosted.org/packages/de/f7/2e3b9cc831a1891914ca09aaeac88195f36f24a22f8c18e57637604a8ef1/av-15.0.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:5d14f6cea6c4966d5478a50555fa92af4948f83e7843b63b747d4a451c53d4f1", size = 33955236, upload-time = "2025-07-03T16:21:23.752Z" }, - { url = "https://files.pythonhosted.org/packages/23/fa/cc4e32d85d6e765f9e9c2680ce9bee6a4d66c8a069f136322be04a66e70d/av-15.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:09f516947890dcf27482af2f0f7b31a579dbd11d5566dd74ce5f1f6396c452b7", size = 37681552, upload-time = "2025-07-03T16:21:27.265Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e3/438e1095c064fd21f1325ddae9383b4bcdc8f8493247144ed15bc1b931a2/av-15.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:908e2fb4358210a463f81e2dbfac77d5977cc53a400fea3f6decef6f9f9267e4", size = 39179769, upload-time = "2025-07-03T16:21:31.941Z" }, - { url = "https://files.pythonhosted.org/packages/b7/b7/e6c27a8bd75e3eede07c1ce888fc1aa6293ba35393d2f4adc1d2e41d563b/av-15.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:911fe092b1c75c35d3e9d836b750ff725599a343b6126449cb2c1b4aa8ac2792", size = 39725200, upload-time = "2025-07-03T16:21:35.73Z" }, - { url = "https://files.pythonhosted.org/packages/d1/42/06e91b07c77465af1b845ac5cf83be1b4cbe042fd940509ae3c5ad70e386/av-15.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:25743a08b674596f3b993392259a4953a445b4211796d168c992174c983b76f0", size = 36639563, upload-time = "2025-07-03T16:21:39.395Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d6/92aafdd8ef420100aca3503b7772ca2484d3688b83b09ca6f96bfb47b7c1/av-15.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2ecd5df62b9697a9304a084fbfed13fa890ec9ba2f647aaed35dca291991c7b1", size = 40482430, upload-time = "2025-07-03T16:21:42.671Z" }, - { url = "https://files.pythonhosted.org/packages/41/1a/22d7b2a151d4aeff6a1fb530e25c8d677dd59580418cab4a95c4628d5009/av-15.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:7156d1b326e328aaba7f10a0d89bec53d087aba16f4c5c7ae13890b9eefde972", size = 31363168, upload-time = "2025-07-03T16:21:45.809Z" }, { url = "https://files.pythonhosted.org/packages/e5/2a/40e0ec34e8235e4a1f9fe60288cd1eebe6413765931b5b74aeb3ce79c422/av-15.0.0-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:eb19386466aafbac4ede549ed7dc6198714e8d35ecc238d5b5c0d91e770d53d4", size = 21793541, upload-time = "2025-07-03T16:21:48.819Z" }, { url = "https://files.pythonhosted.org/packages/7d/21/74acec5492a901699a94715e94cb83772679b92183592a3d8b3e58cf0202/av-15.0.0-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:e3c841befff26823524f3260d29fb3162540535c43238587b24226d345c82af3", size = 26973175, upload-time = "2025-07-03T16:21:51.63Z" }, { url = "https://files.pythonhosted.org/packages/7b/d9/04e7fc09c6246aaf8e695620cc026779e366c49dcab561f8f434fbed3256/av-15.0.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:fe50ddab68af27bb9f7123dac5b1ff43ee8c7d941499c625018f3cac7da01ff3", size = 34423925, upload-time = "2025-07-03T16:21:54.628Z" }, @@ -219,22 +190,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/f0/fe14adaa670ab7a3f709805a8494fd0a2eeb6a5b18b8c59dc6014639a5b1/av-15.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:5758231163b5486dfbf664036be010b7f5ebb24564aaeb62577464be5ea996e0", size = 31332650, upload-time = "2025-07-03T16:23:16.558Z" }, ] -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, -] - [[package]] name = "bbb-livekit-stt" version = "0.3.0" source = { virtual = "." } dependencies = [ { name = "aiohttp" }, - { name = "livekit-agents", extra = ["gladia"] }, + { name = "livekit-agents", extra = ["gladia", "silero"] }, { name = "nest-asyncio" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -253,7 +215,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "aiohttp", specifier = "~=3.12" }, - { name = "livekit-agents", extras = ["gladia"], specifier = "~=1.4" }, + { name = "livekit-agents", extras = ["gladia", "silero"], specifier = "~=1.4" }, { name = "nest-asyncio", specifier = "~=1.6.0" }, { name = "numpy", specifier = "~=2.2" }, { name = "python-dotenv", specifier = "~=1.1.1" }, @@ -286,18 +248,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, - { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, - { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, - { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, - { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, - { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, - { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, - { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, - { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, @@ -340,17 +290,6 @@ version = "3.4.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, - { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, - { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, - { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, - { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, - { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, - { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, - { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, - { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, @@ -425,20 +364,6 @@ version = "7.13.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, - { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, - { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, - { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, - { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, - { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, - { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, - { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, - { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, - { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, - { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, - { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, @@ -565,15 +490,11 @@ wheels = [ ] [[package]] -name = "exceptiongroup" -version = "1.3.0" +name = "flatbuffers" +version = "25.12.19" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, ] [[package]] @@ -582,23 +503,6 @@ version = "1.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/36/0da0a49409f6b47cc2d060dc8c9040b897b5902a8a4e37d9bc1deb11f680/frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a", size = 81304, upload-time = "2025-06-09T22:59:46.226Z" }, - { url = "https://files.pythonhosted.org/packages/77/f0/77c11d13d39513b298e267b22eb6cb559c103d56f155aa9a49097221f0b6/frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61", size = 47735, upload-time = "2025-06-09T22:59:48.133Z" }, - { url = "https://files.pythonhosted.org/packages/37/12/9d07fa18971a44150593de56b2f2947c46604819976784bcf6ea0d5db43b/frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d", size = 46775, upload-time = "2025-06-09T22:59:49.564Z" }, - { url = "https://files.pythonhosted.org/packages/70/34/f73539227e06288fcd1f8a76853e755b2b48bca6747e99e283111c18bcd4/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e", size = 224644, upload-time = "2025-06-09T22:59:51.35Z" }, - { url = "https://files.pythonhosted.org/packages/fb/68/c1d9c2f4a6e438e14613bad0f2973567586610cc22dcb1e1241da71de9d3/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9", size = 222125, upload-time = "2025-06-09T22:59:52.884Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d0/98e8f9a515228d708344d7c6986752be3e3192d1795f748c24bcf154ad99/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c", size = 233455, upload-time = "2025-06-09T22:59:54.74Z" }, - { url = "https://files.pythonhosted.org/packages/79/df/8a11bcec5600557f40338407d3e5bea80376ed1c01a6c0910fcfdc4b8993/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981", size = 227339, upload-time = "2025-06-09T22:59:56.187Z" }, - { url = "https://files.pythonhosted.org/packages/50/82/41cb97d9c9a5ff94438c63cc343eb7980dac4187eb625a51bdfdb7707314/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615", size = 212969, upload-time = "2025-06-09T22:59:57.604Z" }, - { url = "https://files.pythonhosted.org/packages/13/47/f9179ee5ee4f55629e4f28c660b3fdf2775c8bfde8f9c53f2de2d93f52a9/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50", size = 222862, upload-time = "2025-06-09T22:59:59.498Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/df81e41ec6b953902c8b7e3a83bee48b195cb0e5ec2eabae5d8330c78038/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa", size = 222492, upload-time = "2025-06-09T23:00:01.026Z" }, - { url = "https://files.pythonhosted.org/packages/84/17/30d6ea87fa95a9408245a948604b82c1a4b8b3e153cea596421a2aef2754/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577", size = 238250, upload-time = "2025-06-09T23:00:03.401Z" }, - { url = "https://files.pythonhosted.org/packages/8f/00/ecbeb51669e3c3df76cf2ddd66ae3e48345ec213a55e3887d216eb4fbab3/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59", size = 218720, upload-time = "2025-06-09T23:00:05.282Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c0/c224ce0e0eb31cc57f67742071bb470ba8246623c1823a7530be0e76164c/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e", size = 232585, upload-time = "2025-06-09T23:00:07.962Z" }, - { url = "https://files.pythonhosted.org/packages/55/3c/34cb694abf532f31f365106deebdeac9e45c19304d83cf7d51ebbb4ca4d1/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd", size = 234248, upload-time = "2025-06-09T23:00:09.428Z" }, - { url = "https://files.pythonhosted.org/packages/98/c0/2052d8b6cecda2e70bd81299e3512fa332abb6dcd2969b9c80dfcdddbf75/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718", size = 221621, upload-time = "2025-06-09T23:00:11.32Z" }, - { url = "https://files.pythonhosted.org/packages/c5/bf/7dcebae315436903b1d98ffb791a09d674c88480c158aa171958a3ac07f0/frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e", size = 39578, upload-time = "2025-06-09T23:00:13.526Z" }, - { url = "https://files.pythonhosted.org/packages/8f/5f/f69818f017fa9a3d24d1ae39763e29b7f60a59e46d5f91b9c6b21622f4cd/frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464", size = 43830, upload-time = "2025-06-09T23:00:14.98Z" }, { url = "https://files.pythonhosted.org/packages/34/7e/803dde33760128acd393a27eb002f2020ddb8d99d30a44bfbaab31c5f08a/frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a", size = 82251, upload-time = "2025-06-09T23:00:16.279Z" }, { url = "https://files.pythonhosted.org/packages/75/a9/9c2c5760b6ba45eae11334db454c189d43d34a4c0b489feb2175e5e64277/frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750", size = 48183, upload-time = "2025-06-09T23:00:17.698Z" }, { url = "https://files.pythonhosted.org/packages/47/be/4038e2d869f8a2da165f35a6befb9158c259819be22eeaf9c9a8f6a87771/frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd", size = 47107, upload-time = "2025-06-09T23:00:18.952Z" }, @@ -688,16 +592,6 @@ version = "1.74.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/38/b4/35feb8f7cab7239c5b94bd2db71abb3d6adb5f335ad8f131abb6060840b6/grpcio-1.74.0.tar.gz", hash = "sha256:80d1f4fbb35b0742d3e3d3bb654b7381cd5f015f8497279a1e9c21ba623e01b1", size = 12756048, upload-time = "2025-07-24T18:54:23.039Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/54/68e51a90797ad7afc5b0a7881426c337f6a9168ebab73c3210b76aa7c90d/grpcio-1.74.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:85bd5cdf4ed7b2d6438871adf6afff9af7096486fcf51818a81b77ef4dd30907", size = 5481935, upload-time = "2025-07-24T18:52:43.756Z" }, - { url = "https://files.pythonhosted.org/packages/32/2a/af817c7e9843929e93e54d09c9aee2555c2e8d81b93102a9426b36e91833/grpcio-1.74.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:68c8ebcca945efff9d86d8d6d7bfb0841cf0071024417e2d7f45c5e46b5b08eb", size = 10986796, upload-time = "2025-07-24T18:52:47.219Z" }, - { url = "https://files.pythonhosted.org/packages/d5/94/d67756638d7bb07750b07d0826c68e414124574b53840ba1ff777abcd388/grpcio-1.74.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:e154d230dc1bbbd78ad2fdc3039fa50ad7ffcf438e4eb2fa30bce223a70c7486", size = 5983663, upload-time = "2025-07-24T18:52:49.463Z" }, - { url = "https://files.pythonhosted.org/packages/35/f5/c5e4853bf42148fea8532d49e919426585b73eafcf379a712934652a8de9/grpcio-1.74.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8978003816c7b9eabe217f88c78bc26adc8f9304bf6a594b02e5a49b2ef9c11", size = 6653765, upload-time = "2025-07-24T18:52:51.094Z" }, - { url = "https://files.pythonhosted.org/packages/fd/75/a1991dd64b331d199935e096cc9daa3415ee5ccbe9f909aa48eded7bba34/grpcio-1.74.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3d7bd6e3929fd2ea7fbc3f562e4987229ead70c9ae5f01501a46701e08f1ad9", size = 6215172, upload-time = "2025-07-24T18:52:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/01/a4/7cef3dbb3b073d0ce34fd507efc44ac4c9442a0ef9fba4fb3f5c551efef5/grpcio-1.74.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:136b53c91ac1d02c8c24201bfdeb56f8b3ac3278668cbb8e0ba49c88069e1bdc", size = 6329142, upload-time = "2025-07-24T18:52:54.927Z" }, - { url = "https://files.pythonhosted.org/packages/bf/d3/587920f882b46e835ad96014087054655312400e2f1f1446419e5179a383/grpcio-1.74.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fe0f540750a13fd8e5da4b3eaba91a785eea8dca5ccd2bc2ffe978caa403090e", size = 7018632, upload-time = "2025-07-24T18:52:56.523Z" }, - { url = "https://files.pythonhosted.org/packages/1f/95/c70a3b15a0bc83334b507e3d2ae20ee8fa38d419b8758a4d838f5c2a7d32/grpcio-1.74.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4e4181bfc24413d1e3a37a0b7889bea68d973d4b45dd2bc68bb766c140718f82", size = 6509641, upload-time = "2025-07-24T18:52:58.495Z" }, - { url = "https://files.pythonhosted.org/packages/4b/06/2e7042d06247d668ae69ea6998eca33f475fd4e2855f94dcb2aa5daef334/grpcio-1.74.0-cp310-cp310-win32.whl", hash = "sha256:1733969040989f7acc3d94c22f55b4a9501a30f6aaacdbccfaba0a3ffb255ab7", size = 3817478, upload-time = "2025-07-24T18:53:00.128Z" }, - { url = "https://files.pythonhosted.org/packages/93/20/e02b9dcca3ee91124060b65bbf5b8e1af80b3b76a30f694b44b964ab4d71/grpcio-1.74.0-cp310-cp310-win_amd64.whl", hash = "sha256:9e912d3c993a29df6c627459af58975b2e5c897d93287939b9d5065f000249b5", size = 4493971, upload-time = "2025-07-24T18:53:02.068Z" }, { url = "https://files.pythonhosted.org/packages/e7/77/b2f06db9f240a5abeddd23a0e49eae2b6ac54d85f0e5267784ce02269c3b/grpcio-1.74.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:69e1a8180868a2576f02356565f16635b99088da7df3d45aaa7e24e73a054e31", size = 5487368, upload-time = "2025-07-24T18:53:03.548Z" }, { url = "https://files.pythonhosted.org/packages/48/99/0ac8678a819c28d9a370a663007581744a9f2a844e32f0fa95e1ddda5b9e/grpcio-1.74.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8efe72fde5500f47aca1ef59495cb59c885afe04ac89dd11d810f2de87d935d4", size = 10999804, upload-time = "2025-07-24T18:53:05.095Z" }, { url = "https://files.pythonhosted.org/packages/45/c6/a2d586300d9e14ad72e8dc211c7aecb45fe9846a51e558c5bca0c9102c7f/grpcio-1.74.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:a8f0302f9ac4e9923f98d8e243939a6fb627cd048f5cd38595c97e38020dffce", size = 5987667, upload-time = "2025-07-24T18:53:07.157Z" }, @@ -803,18 +697,6 @@ version = "0.13.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, - { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, - { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, - { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, - { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, - { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, - { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, - { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, - { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, - { url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" }, - { url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" }, { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, @@ -900,8 +782,7 @@ version = "1.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "protobuf" }, { name = "types-protobuf" }, ] @@ -932,8 +813,7 @@ dependencies = [ { name = "livekit-blingfire" }, { name = "livekit-protocol" }, { name = "nest-asyncio" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "openai" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp" }, @@ -956,12 +836,14 @@ wheels = [ [package.optional-dependencies] codecs = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, ] gladia = [ { name = "livekit-plugins-gladia" }, ] +silero = [ + { name = "livekit-plugins-silero" }, +] [[package]] name = "livekit-api" @@ -984,11 +866,6 @@ name = "livekit-blingfire" version = "1.1.0" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/0e/e1d79fb428ad43396da2ee4217ae043e42d75b4270e97e76d20c9d17438d/livekit_blingfire-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb8f6a9e69b0e58abd913e0b3b5f27bd79ae498887a9e6708c2255a6841a3f1b", size = 152217, upload-time = "2025-12-16T00:47:59.429Z" }, - { url = "https://files.pythonhosted.org/packages/d3/e6/d881bc1bf61f4bd71df7b52e89a523b4046913977794dac2d2f0453151c2/livekit_blingfire-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:610a7ef7b1c81be587c41241cbdac474f8461345ee066330c69f7c460f81e7e0", size = 147320, upload-time = "2025-12-16T00:48:00.553Z" }, - { url = "https://files.pythonhosted.org/packages/db/81/714a5a4cc742856cf2077ac3851d943c2a4accb4ec76d291c9d8f96fe9d5/livekit_blingfire-1.1.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bf808159597d402415ae06cbb87e8cc8c2a58d2448e0fcd0ae3cf14b114f395", size = 165503, upload-time = "2025-12-16T00:48:01.818Z" }, - { url = "https://files.pythonhosted.org/packages/35/c9/fb8ca3881dcbea2d04cc8995e501a67a450fc93cda3ec4638608030b22f1/livekit_blingfire-1.1.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9fd97f49831c34065f8db3b1407e95c6c3353f0c35b6fff78547582d3d5278", size = 173081, upload-time = "2025-12-16T00:48:03.522Z" }, - { url = "https://files.pythonhosted.org/packages/40/2b/98ba07aae81eb87d426d2bf57426a0861f3f39c41c4d15158612c1d41fc5/livekit_blingfire-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:e747443f3b21999ec1d6d96c2f128dc8375937795dc7bedd8fa7b2a7e54d341c", size = 129305, upload-time = "2025-12-16T00:48:04.74Z" }, { url = "https://files.pythonhosted.org/packages/fc/09/1095ace608a41810d5c0f343eff36154505487c415acd9c653a882ff2cf1/livekit_blingfire-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0358058ba6cba59379d22a01acef6ff8a729b0facf880c0f75d13c26f1315c9d", size = 153650, upload-time = "2025-12-16T00:48:05.976Z" }, { url = "https://files.pythonhosted.org/packages/80/a5/f4eb0e5d97334581440d37ced2a1db4fdfc8454c641c7c144e858012f1ce/livekit_blingfire-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a0741a8abcfaa1f3af2313271f15ac0f79777681a8e3ab9a782a68d8eb121c89", size = 148628, upload-time = "2025-12-16T00:48:06.998Z" }, { url = "https://files.pythonhosted.org/packages/89/f9/dc5ad008cb8b9c2a300bb7f7d44f022cd4970a32707eb90358290a07f0e1/livekit_blingfire-1.1.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d99d7a34c9350da3a6ea738bc282a5f5b4ac4ffb7f8aa5251dfa96070ad845f6", size = 166832, upload-time = "2025-12-16T00:48:07.919Z" }, @@ -1023,14 +900,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, { name = "livekit-agents", extra = ["codecs"] }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/90/da/390bbfedb537ef379ed08c255077a7e7a47fa7b08683b3a27b721fe92b96/livekit_plugins_gladia-1.4.2.tar.gz", hash = "sha256:ddda267fd8b9668a7789ee980036d858213da9f05a7d340dd132f71d524b122a", size = 14607, upload-time = "2026-02-17T01:27:05.505Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fd/81/83f1a11e3b1a063559b871b8eeedca139c156cf0eba6e0d91a4083eaf8e5/livekit_plugins_gladia-1.4.2-py3-none-any.whl", hash = "sha256:8c6564bac7eaef75a834d635493c82ca76db7e736b2bd0091700ed2254ef5835", size = 15281, upload-time = "2026-02-17T01:27:04.668Z" }, ] +[[package]] +name = "livekit-plugins-silero" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "livekit-agents" }, + { name = "numpy" }, + { name = "onnxruntime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/b2/22ee934f798ecbf4e13cdd406a5ab204634f143794aae2e2d9962e9b49fd/livekit_plugins_silero-1.4.2.tar.gz", hash = "sha256:6bbaf05b046e09ce9a64b2cfdecb7799fdb02d83edc0b9e5f4a07a4b19fb0fe0", size = 1955575, upload-time = "2026-02-17T01:27:38.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/7c/294b1123e499fbbfc2f8b49a5fdcdc9691069aab0cdf4c707e29458927ef/livekit_plugins_silero-1.4.2-py3-none-any.whl", hash = "sha256:a10e420870fadc6a4052446e73d5b65a92447937eeef6a9c0912e77e7c844ec6", size = 3903689, upload-time = "2026-02-17T01:27:37.427Z" }, +] + [[package]] name = "livekit-protocol" version = "1.1.2" @@ -1069,29 +959,8 @@ wheels = [ name = "multidict" version = "6.6.4" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload-time = "2025-08-11T12:08:48.217Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/6b/86f353088c1358e76fd30b0146947fddecee812703b604ee901e85cd2a80/multidict-6.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f", size = 77054, upload-time = "2025-08-11T12:06:02.99Z" }, - { url = "https://files.pythonhosted.org/packages/19/5d/c01dc3d3788bb877bd7f5753ea6eb23c1beeca8044902a8f5bfb54430f63/multidict-6.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb", size = 44914, upload-time = "2025-08-11T12:06:05.264Z" }, - { url = "https://files.pythonhosted.org/packages/46/44/964dae19ea42f7d3e166474d8205f14bb811020e28bc423d46123ddda763/multidict-6.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495", size = 44601, upload-time = "2025-08-11T12:06:06.627Z" }, - { url = "https://files.pythonhosted.org/packages/31/20/0616348a1dfb36cb2ab33fc9521de1f27235a397bf3f59338e583afadd17/multidict-6.6.4-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8", size = 224821, upload-time = "2025-08-11T12:06:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/14/26/5d8923c69c110ff51861af05bd27ca6783011b96725d59ccae6d9daeb627/multidict-6.6.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7", size = 242608, upload-time = "2025-08-11T12:06:09.697Z" }, - { url = "https://files.pythonhosted.org/packages/5c/cc/e2ad3ba9459aa34fa65cf1f82a5c4a820a2ce615aacfb5143b8817f76504/multidict-6.6.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796", size = 222324, upload-time = "2025-08-11T12:06:10.905Z" }, - { url = "https://files.pythonhosted.org/packages/19/db/4ed0f65701afbc2cb0c140d2d02928bb0fe38dd044af76e58ad7c54fd21f/multidict-6.6.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db", size = 253234, upload-time = "2025-08-11T12:06:12.658Z" }, - { url = "https://files.pythonhosted.org/packages/94/c1/5160c9813269e39ae14b73debb907bfaaa1beee1762da8c4fb95df4764ed/multidict-6.6.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0", size = 251613, upload-time = "2025-08-11T12:06:13.97Z" }, - { url = "https://files.pythonhosted.org/packages/05/a9/48d1bd111fc2f8fb98b2ed7f9a115c55a9355358432a19f53c0b74d8425d/multidict-6.6.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877", size = 241649, upload-time = "2025-08-11T12:06:15.204Z" }, - { url = "https://files.pythonhosted.org/packages/85/2a/f7d743df0019408768af8a70d2037546a2be7b81fbb65f040d76caafd4c5/multidict-6.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace", size = 239238, upload-time = "2025-08-11T12:06:16.467Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b8/4f4bb13323c2d647323f7919201493cf48ebe7ded971717bfb0f1a79b6bf/multidict-6.6.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6", size = 233517, upload-time = "2025-08-11T12:06:18.107Z" }, - { url = "https://files.pythonhosted.org/packages/33/29/4293c26029ebfbba4f574febd2ed01b6f619cfa0d2e344217d53eef34192/multidict-6.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb", size = 243122, upload-time = "2025-08-11T12:06:19.361Z" }, - { url = "https://files.pythonhosted.org/packages/20/60/a1c53628168aa22447bfde3a8730096ac28086704a0d8c590f3b63388d0c/multidict-6.6.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb", size = 248992, upload-time = "2025-08-11T12:06:20.661Z" }, - { url = "https://files.pythonhosted.org/packages/a3/3b/55443a0c372f33cae5d9ec37a6a973802884fa0ab3586659b197cf8cc5e9/multidict-6.6.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987", size = 243708, upload-time = "2025-08-11T12:06:21.891Z" }, - { url = "https://files.pythonhosted.org/packages/7c/60/a18c6900086769312560b2626b18e8cca22d9e85b1186ba77f4755b11266/multidict-6.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f", size = 237498, upload-time = "2025-08-11T12:06:23.206Z" }, - { url = "https://files.pythonhosted.org/packages/11/3d/8bdd8bcaff2951ce2affccca107a404925a2beafedd5aef0b5e4a71120a6/multidict-6.6.4-cp310-cp310-win32.whl", hash = "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f", size = 41415, upload-time = "2025-08-11T12:06:24.77Z" }, - { url = "https://files.pythonhosted.org/packages/c0/53/cab1ad80356a4cd1b685a254b680167059b433b573e53872fab245e9fc95/multidict-6.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0", size = 46046, upload-time = "2025-08-11T12:06:25.893Z" }, - { url = "https://files.pythonhosted.org/packages/cf/9a/874212b6f5c1c2d870d0a7adc5bb4cfe9b0624fa15cdf5cf757c0f5087ae/multidict-6.6.4-cp310-cp310-win_arm64.whl", hash = "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729", size = 43147, upload-time = "2025-08-11T12:06:27.534Z" }, { url = "https://files.pythonhosted.org/packages/6b/7f/90a7f01e2d005d6653c689039977f6856718c75c5579445effb7e60923d1/multidict-6.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c", size = 76472, upload-time = "2025-08-11T12:06:29.006Z" }, { url = "https://files.pythonhosted.org/packages/54/a3/bed07bc9e2bb302ce752f1dabc69e884cd6a676da44fb0e501b246031fdd/multidict-6.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb", size = 44634, upload-time = "2025-08-11T12:06:30.374Z" }, { url = "https://files.pythonhosted.org/packages/a7/4b/ceeb4f8f33cf81277da464307afeaf164fb0297947642585884f5cad4f28/multidict-6.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e", size = 44282, upload-time = "2025-08-11T12:06:31.958Z" }, @@ -1176,79 +1045,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] -[[package]] -name = "numpy" -version = "2.2.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, - { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, - { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, - { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, - { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, - { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, - { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, - { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, - { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, - { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, - { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, - { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, - { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, - { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, - { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, - { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, - { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, - { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, - { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, - { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, - { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, - { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, - { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, - { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, - { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, - { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, - { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, - { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, - { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, - { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, - { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, - { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, - { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, -] - [[package]] name = "numpy" version = "2.3.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version >= '3.11' and python_full_version < '3.13'", -] sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/96/26/1320083986108998bd487e2931eed2aeedf914b6e8905431487543ec911d/numpy-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:852ae5bed3478b92f093e30f785c98e0cb62fa0a939ed057c31716e18a7a22b9", size = 21259016, upload-time = "2025-07-24T20:24:35.214Z" }, @@ -1326,6 +1126,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/e3/6690b3f85a05506733c7e90b577e4762517404ea78bab2ca3a5cb1aeb78d/numpy-2.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6936aff90dda378c09bea075af0d9c675fe3a977a9d2402f95a87f440f59f619", size = 12977811, upload-time = "2025-07-24T21:29:18.234Z" }, ] +[[package]] +name = "onnxruntime" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/81/29a9eb470994a75eb7b3ccf32be314d7c66675a00ac7b50294816cc2db27/onnxruntime-1.26.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ee1109ef4ef27cad90e823399e61e03b3c6c7bfe0fb820b4baf3678c15be8b3c", size = 18005108, upload-time = "2026-05-08T19:08:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/66/c7/73efa6c8a4000c38fcc14947d84f234a17e5d66f203b37b7f1ad4a7b46eb/onnxruntime-1.26.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35c7c7b0ac2e02001d28fab6c9fc24e9abc5e6faa35e6e19c63cecf1406ba89f", size = 16043752, upload-time = "2026-05-08T19:07:10.707Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3f/8de630f595daf6ce884d4dd95afd2a60e70ec6572e52bfee3aa2229befab/onnxruntime-1.26.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11a8df4dcfe9ad5ff0bd71a7571dbed019fabc7594676c89fe8b86ea029c246f", size = 18176043, upload-time = "2026-05-08T19:07:33.735Z" }, + { url = "https://files.pythonhosted.org/packages/9c/21/9f041de20787cd85498bd48e0ec4d098bf2a6c486e25b24b8dae1bf492b2/onnxruntime-1.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:e6456718125fd777c673f3b78d4a9ab58d6adea641e9afae85ee6444f0e0e9a9", size = 13023165, upload-time = "2026-05-08T19:08:00.633Z" }, + { url = "https://files.pythonhosted.org/packages/0e/82/3b9fe0ead2557cc3adf74c74c141bd1c7c4c6a9548c610af37df199f4512/onnxruntime-1.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:cd920e45b730e4a87833e2910d8ca375aaca9da6ccc09e24bce463b3356d637f", size = 12789514, upload-time = "2026-05-08T19:07:49.433Z" }, + { url = "https://files.pythonhosted.org/packages/81/b1/d111b1df656761f980d9e298a60039a9cb66036b1d039e777537743d0ac3/onnxruntime-1.26.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05b028781b322ad74b57ce5b50aa5280bb1fe96ceec334628ade681e0b24c1ac", size = 18016624, upload-time = "2026-05-12T00:41:01.735Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a0/3f9d896a0385a36bd04345d6d0b802821a5782adde562e7e135f6bb71c73/onnxruntime-1.26.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91f2bb870a4b9224eba0a6728c1fa7a9e552b8e59e1083c51fbbc3d013f2b5c0", size = 16052692, upload-time = "2026-05-08T19:07:13.829Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/2a4e04f8dbeffad19bbcced4bcd4289bf478921518437404d6b92bdf213b/onnxruntime-1.26.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b6dd70599005bd1bf29779f04a91978b92b5e719c11a20068a8f8e535f725b6", size = 18185439, upload-time = "2026-05-08T19:07:36.299Z" }, + { url = "https://files.pythonhosted.org/packages/44/fc/026d0a7162b9c2153dac292baea9e027c42304dc1d9dc6f8ff5b4cfbaedd/onnxruntime-1.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:a26374dc7fbcaae593601086b242120e13f2310558df0991da6dd8b8fac00414", size = 13026427, upload-time = "2026-05-08T19:08:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/3e/27/1dcf88e45e4c69db5f7b106f2dacc3801ba98994e082ca03e1dfdf7bfe57/onnxruntime-1.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:54a8053410fd31fd66469bd754fcfe8a4df9f7eb44756b4b5479bf50c842d948", size = 12796647, upload-time = "2026-05-08T19:07:52.108Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a2/c801242685e0ce48a4ca51dfafbb588765e0446397e123be53ba5598f3f5/onnxruntime-1.26.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccce19c5f771b8268902f77d9fed9e88f9499465d6780808faa6611a789d33f0", size = 18016563, upload-time = "2026-05-08T19:07:28.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/64/0492c0b1db04e29b2630c87cfa36f9d6872b1ca8614b90c5cad58fac7d76/onnxruntime-1.26.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdbed8cf3b672b66acb032f33a253bc27f42bce6ece48ae3fab4fa483a5e96e0", size = 16052634, upload-time = "2026-05-08T19:07:16.885Z" }, + { url = "https://files.pythonhosted.org/packages/3d/26/4d09ddc755a84fc8d5e192991626b0e0680e8f6c5d58f4f1d05c42bc48cf/onnxruntime-1.26.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c07af6fc6d5557835f2b6ee7a96d8b3235d0c57a8e230efdedaee106a8a3cbc6", size = 18185632, upload-time = "2026-05-08T19:07:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/77/89/3e52249aa08fa301e217ecba07b5246a8338fa2b401e109326e3fc5be0f9/onnxruntime-1.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:61bec80655efa460591c2bc655392d57d2650ce85533a6b9b3b7a790d7ea7916", size = 13026751, upload-time = "2026-05-08T19:08:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/06/b3/c1c8782b14af6797c303de132d6eef26a9fb80dfacd3750ce57911d11c6b/onnxruntime-1.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a6677545ff451e3539a02746d2f207d8c5baa4a0a818886bb9d6a6eb9511ee89", size = 12796807, upload-time = "2026-05-08T19:07:54.879Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f5/47b0676408abec652c14b84d7173e389837832d850c24f87184277313e8d/onnxruntime-1.26.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e016edc15d3c19f36807e1c6b10be5b27807688c32720f91b5ae480a95215d0", size = 16057265, upload-time = "2026-05-08T19:07:19.603Z" }, + { url = "https://files.pythonhosted.org/packages/3b/45/33ab6deeef010ca844c877dd618cebc079590bbe52d2a3678e7223b1b908/onnxruntime-1.26.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5fc48a91a046a6a5c9b147f83fb41d65d24d24923373b222cdd248f0f4f4aac", size = 18197590, upload-time = "2026-05-08T19:07:41.422Z" }, + { url = "https://files.pythonhosted.org/packages/40/89/17546c1c20f6bfc3ae41c22152378a26edfea918af3129e2139dcd7c99f3/onnxruntime-1.26.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:33a791f31432a3af1a96db5e54818b37aba5e5eefc2e6af5794c10a9118a9993", size = 18019724, upload-time = "2026-05-08T19:07:30.723Z" }, + { url = "https://files.pythonhosted.org/packages/bb/24/89457a35f6af29538a76647f2c18c3a28277e6c19234c847e7b4b7c19860/onnxruntime-1.26.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e90c00732c4553618103149d93f688e8c3063017938f8983e21a71d9f3b6d22e", size = 16054821, upload-time = "2026-05-08T19:07:22.348Z" }, + { url = "https://files.pythonhosted.org/packages/12/f9/15b2e1815cf570d238e0135529f80d2dce64e8e8818a1489cae83823c5c6/onnxruntime-1.26.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01498e80ba8988428d08c2d51b1338f89e3de2a93e6ffe555f79c68f26a5c06b", size = 18185815, upload-time = "2026-05-08T19:07:44.179Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/2e11055faf015e4b07f45b513fa49b391baf2e19d92d77d73ebee13c1004/onnxruntime-1.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:7ead61450d8405167c87dd3a31d8da1d576b490a57dab1aa8b82a7da6825f5aa", size = 13349887, upload-time = "2026-05-08T19:08:08.671Z" }, + { url = "https://files.pythonhosted.org/packages/19/e4/0f9d1a5718b1781c610c1e354765a3820597081754277a6a9a2b50705702/onnxruntime-1.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:31d71a53490e46910877d0902b5ad99c69a5955e5c7ea6c82863519410e1ba7c", size = 13140121, upload-time = "2026-05-08T19:07:57.804Z" }, + { url = "https://files.pythonhosted.org/packages/1c/42/3b8e635f067d06d9f45bede470b8d539d101a4166c272213158dfd08b6ce/onnxruntime-1.26.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b6d258fb78fdfcf049795bcfaa74dcb90ae7baa277afd21e6fd28b83f2c496", size = 16057240, upload-time = "2026-05-08T19:07:25.163Z" }, + { url = "https://files.pythonhosted.org/packages/93/99/f2be40a31b908d96b861ae0ce98582fa376c18a7f816b9d5eb4cd6aa0a4c/onnxruntime-1.26.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4eefd386a45202aefb7a5132b94f32df9d506c9edcc7faf2fc60d65183f4b183", size = 18197382, upload-time = "2026-05-08T19:07:46.965Z" }, +] + [[package]] name = "openai" version = "2.21.0" @@ -1491,22 +1328,6 @@ version = "0.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/14/510deed325e262afeb8b360043c5d7c960da7d3ecd6d6f9496c9c56dc7f4/propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770", size = 73178, upload-time = "2025-06-09T22:53:40.126Z" }, - { url = "https://files.pythonhosted.org/packages/cd/4e/ad52a7925ff01c1325653a730c7ec3175a23f948f08626a534133427dcff/propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3", size = 43133, upload-time = "2025-06-09T22:53:41.965Z" }, - { url = "https://files.pythonhosted.org/packages/63/7c/e9399ba5da7780871db4eac178e9c2e204c23dd3e7d32df202092a1ed400/propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3", size = 43039, upload-time = "2025-06-09T22:53:43.268Z" }, - { url = "https://files.pythonhosted.org/packages/22/e1/58da211eb8fdc6fc854002387d38f415a6ca5f5c67c1315b204a5d3e9d7a/propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e", size = 201903, upload-time = "2025-06-09T22:53:44.872Z" }, - { url = "https://files.pythonhosted.org/packages/c4/0a/550ea0f52aac455cb90111c8bab995208443e46d925e51e2f6ebdf869525/propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220", size = 213362, upload-time = "2025-06-09T22:53:46.707Z" }, - { url = "https://files.pythonhosted.org/packages/5a/af/9893b7d878deda9bb69fcf54600b247fba7317761b7db11fede6e0f28bd0/propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb", size = 210525, upload-time = "2025-06-09T22:53:48.547Z" }, - { url = "https://files.pythonhosted.org/packages/7c/bb/38fd08b278ca85cde36d848091ad2b45954bc5f15cce494bb300b9285831/propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614", size = 198283, upload-time = "2025-06-09T22:53:50.067Z" }, - { url = "https://files.pythonhosted.org/packages/78/8c/9fe55bd01d362bafb413dfe508c48753111a1e269737fa143ba85693592c/propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50", size = 191872, upload-time = "2025-06-09T22:53:51.438Z" }, - { url = "https://files.pythonhosted.org/packages/54/14/4701c33852937a22584e08abb531d654c8bcf7948a8f87ad0a4822394147/propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339", size = 199452, upload-time = "2025-06-09T22:53:53.229Z" }, - { url = "https://files.pythonhosted.org/packages/16/44/447f2253d859602095356007657ee535e0093215ea0b3d1d6a41d16e5201/propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0", size = 191567, upload-time = "2025-06-09T22:53:54.541Z" }, - { url = "https://files.pythonhosted.org/packages/f2/b3/e4756258749bb2d3b46defcff606a2f47410bab82be5824a67e84015b267/propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2", size = 193015, upload-time = "2025-06-09T22:53:56.44Z" }, - { url = "https://files.pythonhosted.org/packages/1e/df/e6d3c7574233164b6330b9fd697beeac402afd367280e6dc377bb99b43d9/propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7", size = 204660, upload-time = "2025-06-09T22:53:57.839Z" }, - { url = "https://files.pythonhosted.org/packages/b2/53/e4d31dd5170b4a0e2e6b730f2385a96410633b4833dc25fe5dffd1f73294/propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b", size = 206105, upload-time = "2025-06-09T22:53:59.638Z" }, - { url = "https://files.pythonhosted.org/packages/7f/fe/74d54cf9fbe2a20ff786e5f7afcfde446588f0cf15fb2daacfbc267b866c/propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c", size = 196980, upload-time = "2025-06-09T22:54:01.071Z" }, - { url = "https://files.pythonhosted.org/packages/22/ec/c469c9d59dada8a7679625e0440b544fe72e99311a4679c279562051f6fc/propcache-0.3.2-cp310-cp310-win32.whl", hash = "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70", size = 37679, upload-time = "2025-06-09T22:54:03.003Z" }, - { url = "https://files.pythonhosted.org/packages/38/35/07a471371ac89d418f8d0b699c75ea6dca2041fbda360823de21f6a9ce0a/propcache-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9", size = 41459, upload-time = "2025-06-09T22:54:04.134Z" }, { url = "https://files.pythonhosted.org/packages/80/8d/e8b436717ab9c2cfc23b116d2c297305aa4cd8339172a456d61ebf5669b8/propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", size = 74207, upload-time = "2025-06-09T22:54:05.399Z" }, { url = "https://files.pythonhosted.org/packages/d6/29/1e34000e9766d112171764b9fa3226fa0153ab565d0c242c70e9945318a7/propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", size = 43648, upload-time = "2025-06-09T22:54:08.023Z" }, { url = "https://files.pythonhosted.org/packages/46/92/1ad5af0df781e76988897da39b5f086c2bf0f028b7f9bd1f409bb05b6874/propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", size = 43496, upload-time = "2025-06-09T22:54:09.228Z" }, @@ -1636,19 +1457,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, - { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, - { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, - { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, - { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, - { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, - { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, - { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, - { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, @@ -1694,15 +1502,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, - { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, - { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, - { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, - { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, - { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, @@ -1738,12 +1537,10 @@ version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ @@ -1755,7 +1552,6 @@ name = "pytest-asyncio" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] @@ -2014,18 +1810,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406, upload-time = "2025-06-15T19:06:59.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/dd/579d1dc57f0f895426a1211c4ef3b0cb37eb9e642bb04bdcd962b5df206a/watchfiles-1.1.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc", size = 405757, upload-time = "2025-06-15T19:04:51.058Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a0/7a0318cd874393344d48c34d53b3dd419466adf59a29ba5b51c88dd18b86/watchfiles-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df", size = 397511, upload-time = "2025-06-15T19:04:52.79Z" }, - { url = "https://files.pythonhosted.org/packages/06/be/503514656d0555ec2195f60d810eca29b938772e9bfb112d5cd5ad6f6a9e/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68", size = 450739, upload-time = "2025-06-15T19:04:54.203Z" }, - { url = "https://files.pythonhosted.org/packages/4e/0d/a05dd9e5f136cdc29751816d0890d084ab99f8c17b86f25697288ca09bc7/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc", size = 458106, upload-time = "2025-06-15T19:04:55.607Z" }, - { url = "https://files.pythonhosted.org/packages/f1/fa/9cd16e4dfdb831072b7ac39e7bea986e52128526251038eb481effe9f48e/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97", size = 484264, upload-time = "2025-06-15T19:04:57.009Z" }, - { url = "https://files.pythonhosted.org/packages/32/04/1da8a637c7e2b70e750a0308e9c8e662ada0cca46211fa9ef24a23937e0b/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c", size = 597612, upload-time = "2025-06-15T19:04:58.409Z" }, - { url = "https://files.pythonhosted.org/packages/30/01/109f2762e968d3e58c95731a206e5d7d2a7abaed4299dd8a94597250153c/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5", size = 477242, upload-time = "2025-06-15T19:04:59.786Z" }, - { url = "https://files.pythonhosted.org/packages/b5/b8/46f58cf4969d3b7bc3ca35a98e739fa4085b0657a1540ccc29a1a0bc016f/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9", size = 453148, upload-time = "2025-06-15T19:05:01.103Z" }, - { url = "https://files.pythonhosted.org/packages/a5/cd/8267594263b1770f1eb76914940d7b2d03ee55eca212302329608208e061/watchfiles-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72", size = 626574, upload-time = "2025-06-15T19:05:02.582Z" }, - { url = "https://files.pythonhosted.org/packages/a1/2f/7f2722e85899bed337cba715723e19185e288ef361360718973f891805be/watchfiles-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc", size = 624378, upload-time = "2025-06-15T19:05:03.719Z" }, - { url = "https://files.pythonhosted.org/packages/bf/20/64c88ec43d90a568234d021ab4b2a6f42a5230d772b987c3f9c00cc27b8b/watchfiles-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587", size = 279829, upload-time = "2025-06-15T19:05:04.822Z" }, - { url = "https://files.pythonhosted.org/packages/39/5c/a9c1ed33de7af80935e4eac09570de679c6e21c07070aa99f74b4431f4d6/watchfiles-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82", size = 292192, upload-time = "2025-06-15T19:05:06.348Z" }, { url = "https://files.pythonhosted.org/packages/8b/78/7401154b78ab484ccaaeef970dc2af0cb88b5ba8a1b415383da444cdd8d3/watchfiles-1.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2", size = 405751, upload-time = "2025-06-15T19:05:07.679Z" }, { url = "https://files.pythonhosted.org/packages/76/63/e6c3dbc1f78d001589b75e56a288c47723de28c580ad715eb116639152b5/watchfiles-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c", size = 397313, upload-time = "2025-06-15T19:05:08.764Z" }, { url = "https://files.pythonhosted.org/packages/6c/a2/8afa359ff52e99af1632f90cbf359da46184207e893a5f179301b0c8d6df/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d", size = 450792, upload-time = "2025-06-15T19:05:09.869Z" }, @@ -2095,10 +1879,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/c4/088825b75489cb5b6a761a4542645718893d395d8c530b38734f19da44d2/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147", size = 452240, upload-time = "2025-06-15T19:06:26.552Z" }, { url = "https://files.pythonhosted.org/packages/10/8c/22b074814970eeef43b7c44df98c3e9667c1f7bf5b83e0ff0201b0bd43f9/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8", size = 625607, upload-time = "2025-06-15T19:06:27.606Z" }, { url = "https://files.pythonhosted.org/packages/32/fa/a4f5c2046385492b2273213ef815bf71a0d4c1943b784fb904e184e30201/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db", size = 623315, upload-time = "2025-06-15T19:06:29.076Z" }, - { url = "https://files.pythonhosted.org/packages/be/7c/a3d7c55cfa377c2f62c4ae3c6502b997186bc5e38156bafcb9b653de9a6d/watchfiles-1.1.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5", size = 406748, upload-time = "2025-06-15T19:06:44.2Z" }, - { url = "https://files.pythonhosted.org/packages/38/d0/c46f1b2c0ca47f3667b144de6f0515f6d1c670d72f2ca29861cac78abaa1/watchfiles-1.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d", size = 398801, upload-time = "2025-06-15T19:06:45.774Z" }, - { url = "https://files.pythonhosted.org/packages/70/9c/9a6a42e97f92eeed77c3485a43ea96723900aefa3ac739a8c73f4bff2cd7/watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea", size = 451528, upload-time = "2025-06-15T19:06:46.791Z" }, - { url = "https://files.pythonhosted.org/packages/51/7b/98c7f4f7ce7ff03023cf971cd84a3ee3b790021ae7584ffffa0eb2554b96/watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6", size = 454095, upload-time = "2025-06-15T19:06:48.211Z" }, { url = "https://files.pythonhosted.org/packages/8c/6b/686dcf5d3525ad17b384fd94708e95193529b460a1b7bf40851f1328ec6e/watchfiles-1.1.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3", size = 406910, upload-time = "2025-06-15T19:06:49.335Z" }, { url = "https://files.pythonhosted.org/packages/f3/d3/71c2dcf81dc1edcf8af9f4d8d63b1316fb0a2dd90cbfd427e8d9dd584a90/watchfiles-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c", size = 398816, upload-time = "2025-06-15T19:06:50.433Z" }, { url = "https://files.pythonhosted.org/packages/b8/fa/12269467b2fc006f8fce4cd6c3acfa77491dd0777d2a747415f28ccc8c60/watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432", size = 451584, upload-time = "2025-06-15T19:06:51.834Z" }, @@ -2116,23 +1896,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/65/7fed0d774abf47487c64be14e9223749468922817b5e8792b8a64792a1bb/yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4", size = 132910, upload-time = "2025-06-10T00:42:31.108Z" }, - { url = "https://files.pythonhosted.org/packages/8a/7b/988f55a52da99df9e56dc733b8e4e5a6ae2090081dc2754fc8fd34e60aa0/yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a", size = 90644, upload-time = "2025-06-10T00:42:33.851Z" }, - { url = "https://files.pythonhosted.org/packages/f7/de/30d98f03e95d30c7e3cc093759982d038c8833ec2451001d45ef4854edc1/yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed", size = 89322, upload-time = "2025-06-10T00:42:35.688Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7a/f2f314f5ebfe9200724b0b748de2186b927acb334cf964fd312eb86fc286/yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e", size = 323786, upload-time = "2025-06-10T00:42:37.817Z" }, - { url = "https://files.pythonhosted.org/packages/15/3f/718d26f189db96d993d14b984ce91de52e76309d0fd1d4296f34039856aa/yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73", size = 319627, upload-time = "2025-06-10T00:42:39.937Z" }, - { url = "https://files.pythonhosted.org/packages/a5/76/8fcfbf5fa2369157b9898962a4a7d96764b287b085b5b3d9ffae69cdefd1/yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e", size = 339149, upload-time = "2025-06-10T00:42:42.627Z" }, - { url = "https://files.pythonhosted.org/packages/3c/95/d7fc301cc4661785967acc04f54a4a42d5124905e27db27bb578aac49b5c/yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8", size = 333327, upload-time = "2025-06-10T00:42:44.842Z" }, - { url = "https://files.pythonhosted.org/packages/65/94/e21269718349582eee81efc5c1c08ee71c816bfc1585b77d0ec3f58089eb/yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23", size = 326054, upload-time = "2025-06-10T00:42:47.149Z" }, - { url = "https://files.pythonhosted.org/packages/32/ae/8616d1f07853704523519f6131d21f092e567c5af93de7e3e94b38d7f065/yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70", size = 315035, upload-time = "2025-06-10T00:42:48.852Z" }, - { url = "https://files.pythonhosted.org/packages/48/aa/0ace06280861ef055855333707db5e49c6e3a08840a7ce62682259d0a6c0/yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb", size = 338962, upload-time = "2025-06-10T00:42:51.024Z" }, - { url = "https://files.pythonhosted.org/packages/20/52/1e9d0e6916f45a8fb50e6844f01cb34692455f1acd548606cbda8134cd1e/yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2", size = 335399, upload-time = "2025-06-10T00:42:53.007Z" }, - { url = "https://files.pythonhosted.org/packages/f2/65/60452df742952c630e82f394cd409de10610481d9043aa14c61bf846b7b1/yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30", size = 338649, upload-time = "2025-06-10T00:42:54.964Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f5/6cd4ff38dcde57a70f23719a838665ee17079640c77087404c3d34da6727/yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309", size = 358563, upload-time = "2025-06-10T00:42:57.28Z" }, - { url = "https://files.pythonhosted.org/packages/d1/90/c42eefd79d0d8222cb3227bdd51b640c0c1d0aa33fe4cc86c36eccba77d3/yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24", size = 357609, upload-time = "2025-06-10T00:42:59.055Z" }, - { url = "https://files.pythonhosted.org/packages/03/c8/cea6b232cb4617514232e0f8a718153a95b5d82b5290711b201545825532/yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13", size = 350224, upload-time = "2025-06-10T00:43:01.248Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a3/eaa0ab9712f1f3d01faf43cf6f1f7210ce4ea4a7e9b28b489a2261ca8db9/yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8", size = 81753, upload-time = "2025-06-10T00:43:03.486Z" }, - { url = "https://files.pythonhosted.org/packages/8f/34/e4abde70a9256465fe31c88ed02c3f8502b7b5dead693a4f350a06413f28/yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16", size = 86817, upload-time = "2025-06-10T00:43:05.231Z" }, { url = "https://files.pythonhosted.org/packages/b1/18/893b50efc2350e47a874c5c2d67e55a0ea5df91186b2a6f5ac52eff887cd/yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", size = 133833, upload-time = "2025-06-10T00:43:07.393Z" }, { url = "https://files.pythonhosted.org/packages/89/ed/b8773448030e6fc47fa797f099ab9eab151a43a25717f9ac043844ad5ea3/yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", size = 91070, upload-time = "2025-06-10T00:43:09.538Z" }, { url = "https://files.pythonhosted.org/packages/e3/e3/409bd17b1e42619bf69f60e4f031ce1ccb29bd7380117a55529e76933464/yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", size = 89818, upload-time = "2025-06-10T00:43:11.575Z" },