Merge pull request #124 from iqiu2023/main

添加 audio live 功能
This commit is contained in:
Chenyme authored and GitHub committed 2026-02-06 10:03:50 +08:00
commit 270a1350a4
9 files changed
+801 -13

No files matched your search

+19 -12
View File
@@ -3,9 +3,11 @@ FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
TZ=Asia/Shanghai \
VIRTUAL_ENV=/opt/venv
# 修改:让 uv 直接把包安装到系统 Python 环境,或者指定明确的虚拟环境
UV_PROJECT_ENVIRONMENT=/opt/venv
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# 确保虚拟环境的 bin 目录在最前面
ENV PATH="$UV_PROJECT_ENVIRONMENT/bin:$PATH"
RUN apt-get update \
&& apt-get install -y --no-install-recommends tzdata ca-certificates \
@@ -13,21 +15,26 @@ RUN apt-get update \
WORKDIR /app
RUN python -m venv "$VIRTUAL_ENV" \
&& pip install --no-cache-dir uv
# 安装 uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# 或者保留你原有的 pip install uv
COPY pyproject.toml uv.lock /app/
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project --active
# 修改:使用 --frozen 且确保同步到指定的 /opt/venv
RUN uv sync --frozen --no-dev --no-install-project
COPY config.defaults.toml /app/config.defaults.toml
COPY app /app/app
COPY main.py /app/main.py
COPY scripts /app/scripts
COPY config.defaults.toml ./
COPY app ./app
COPY main.py ./
COPY scripts ./scripts
RUN mkdir -p /app/data /app/data/tmp /app/logs
RUN mkdir -p /app/data /app/data/tmp /app/logs \
&& chmod +x /app/scripts/entrypoint.sh
EXPOSE 8000
ENTRYPOINT ["/app/scripts/entrypoint.sh"]
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# 建议:直接调用 uvicorn,因为 /opt/venv/bin 已经在 PATH 中
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
+69
View File
@@ -0,0 +1,69 @@
"""
Voice Mode API
"""
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from app.core.auth import verify_api_key
from app.services.grok.voice import VoiceService
from app.services.token import get_token_manager
from app.core.exceptions import AppException
router = APIRouter(tags=["Voice"])
class VoiceTokenResponse(BaseModel):
token: str
url: str
participant_name: str = ""
room_name: str = ""
@router.get("/voice/token", response_model=VoiceTokenResponse)
async def get_voice_token(
voice: str = "ara",
personality: str = "assistant",
speed: float = 1.0,
api_key: str = Depends(verify_api_key)
):
"""
Get Grok Voice Mode (LiveKit) Token
Returns:
token: Access Token
url: LiveKit Server URL (wss://livekit.grok.com)
"""
token_mgr = await get_token_manager()
# Use 'grok-3' or similar model to get a valid pool token
sso_token = token_mgr.get_token("ssoBasic")
if not sso_token:
raise AppException("No available tokens for voice mode", code="no_token", status_code=503)
service = VoiceService()
try:
data = await service.get_token(
token=sso_token,
voice=voice,
personality=personality,
speed=speed
)
# Check for errors in data structure
token = data.get("token")
if not token:
raise AppException("Upstream returned no voice token", code="upstream_error", status_code=502)
# Parse payload if needed, but for now return raw
return VoiceTokenResponse(
token=token,
url="wss://livekit.grok.com",
participant_name="",
room_name=""
)
except Exception as e:
if isinstance(e, AppException):
raise
raise AppException(f"Voice token error: {str(e)}", code="voice_error", status_code=500)
+115
View File
@@ -0,0 +1,115 @@
"""
Grok Voice Mode Service
"""
import uuid
import orjson
from typing import Dict, Any, Optional
from curl_cffi.requests import AsyncSession
from app.core.logger import logger
from app.core.config import get_config
from app.core.exceptions import UpstreamException
from app.services.grok.statsig import StatsigService
LIVEKIT_TOKEN_API = "https://grok.com/rest/livekit/tokens"
TIMEOUT = 30
BROWSER = "chrome136"
class VoiceService:
"""Voice Mode Service (LiveKit)"""
def __init__(self, proxy: str = None):
self.proxy = proxy or get_config("grok.base_proxy_url", "")
async def get_token(
self,
token: str,
model: str = "grok-4.1",
voice: str = "ara",
personality: str = "assistant",
speed: float = 1.0,
) -> Dict[str, Any]:
"""
Get LiveKit token
Args:
token: Auth token
model: Model name (default:grok-4.1, though voice uses specific settings)
Returns:
Dict containing token and livekitUrl
"""
headers = self._build_headers(token)
payload = self._build_payload(model, voice, personality, speed)
proxies = {"http": self.proxy, "https": self.proxy} if self.proxy else None
try:
async with AsyncSession(impersonate=BROWSER) as session:
response = await session.post(
LIVEKIT_TOKEN_API,
headers=headers,
data=orjson.dumps(payload),
timeout=TIMEOUT,
proxies=proxies,
)
if response.status_code != 200:
logger.error(
f"Voice token failed: {response.status_code}",
extra={"response": response.text[:200]}
)
raise UpstreamException(
message=f"Failed to get voice token: {response.status_code}",
details={"status": response.status_code, "body": response.text}
)
return response.json()
except Exception as e:
logger.error(f"Voice service error: {e}")
if isinstance(e, UpstreamException):
raise
raise UpstreamException(f"Voice service error: {str(e)}")
def _build_headers(self, token: str) -> Dict[str, str]:
headers = {
"Accept": "*/*",
"Content-Type": "application/json",
"Origin": "https://grok.com",
"Referer": "https://grok.com/",
# Statsig ID is crucial
"x-statsig-id": StatsigService.gen_id(),
"x-xai-request-id": str(uuid.uuid4()),
}
token = token[4:] if token.startswith("sso=") else token
cf = get_config("grok.cf_clearance", "")
headers["Cookie"] = f"sso={token};cf_clearance={cf}" if cf else f"sso={token}"
return headers
def _build_payload(
self,
model: str,
voice: str = "ara",
personality: str = "assistant",
speed: float = 1.0,
) -> Dict[str, Any]:
"""Construct payload with voice settings"""
return {
"sessionPayload": orjson.dumps({
"voice": voice,
"personality": personality,
"playback_speed": speed,
"enable_vision": False,
"turn_detection": {"type": "server_vad"}
}).decode(),
"requestAgentDispatch": False,
"livekitUrl": "wss://livekit.grok.com",
"params": {"enable_markdown_transcript": "true"}
}
+391
View File
@@ -0,0 +1,391 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Grok Voice Live 测试</title>
<script src="https://cdn.jsdelivr.net/npm/livekit-client@2.7.3/dist/livekit-client.umd.min.js"
onerror="log('LiveKit SDK Script failed to load')" onload="log('LiveKit SDK Script loaded')"></script>
<style>
:root {
--primary-color: #007AFF;
--bg-color: #1c1c1e;
--card-bg: #2c2c2e;
--text-color: #ffffff;
--success-color: #34c759;
--error-color: #ff3b30;
--input-bg: #1c1c1e;
--input-border: #444;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
.container {
background-color: var(--card-bg);
padding: 2rem;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
text-align: center;
width: 90%;
max-width: 450px;
}
h1 {
margin-bottom: 1.5rem;
font-weight: 600;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
background-color: #666;
}
.status-connected {
background-color: var(--success-color);
box-shadow: 0 0 10px var(--success-color);
}
.status-connecting {
background-color: #ffcc00;
}
.status-error {
background-color: var(--error-color);
}
.controls {
margin-top: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 5px;
text-align: left;
}
.form-group label {
font-size: 0.85rem;
color: #aaa;
margin-left: 2px;
}
select,
input {
padding: 10px;
border-radius: 8px;
border: 1px solid var(--input-border);
background: var(--input-bg);
color: white;
font-size: 0.95rem;
outline: none;
}
button {
padding: 12px 24px;
border-radius: 12px;
border: none;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-top: 0.5rem;
}
#startBtn {
background-color: var(--primary-color);
color: white;
}
#startBtn:disabled {
background-color: #3a3a3c;
color: #8e8e93;
cursor: not-allowed;
}
#stopBtn {
background-color: transparent;
color: var(--error-color);
border: 1px solid var(--error-color);
}
.hidden {
display: none;
}
#log {
margin-top: 1.5rem;
text-align: left;
font-size: 0.85rem;
color: #8e8e93;
max-height: 150px;
overflow-y: auto;
background: rgba(0, 0, 0, 0.2);
padding: 10px;
border-radius: 8px;
}
.visualizer {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
height: 40px;
margin-top: 1rem;
}
.bar {
width: 4px;
height: 4px;
background-color: var(--primary-color);
border-radius: 2px;
transition: height 0.1s;
}
</style>
</head>
<body>
<div class="container">
<h1>Grok Voice Live</h1>
<div>
<span id="statusIcon" class="status-indicator"></span>
<span id="statusText">未连接</span>
</div>
<div class="visualizer" id="visualizer">
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
</div>
<div class="controls">
<div class="form-group">
<label for="apiKey">API Key (可选)</label>
<input id="apiKey" placeholder="输入 API Key">
</div>
<div class="form-group">
<label for="voiceSelect">声音 (Voice)</label>
<select id="voiceSelect">
<option value="ara" selected>Ara (Upbeat Female)</option>
<option value="eve">Eve (Soothing Female)</option>
<option value="leo">Leo (British Male)</option>
<option value="rex">Rex (Calm Male)</option>
<option value="sal">Sal (Smooth Male)</option>
<option value="gork">Gork (Lazy Male)</option>
</select>
</div>
<div class="form-group">
<label for="personalitySelect">个性 (Personality)</label>
<select id="personalitySelect">
<option value="assistant" selected>Assistant (Default)</option>
<option value="custom">Custom</option>
<option value="therapist">Therapist</option>
<option value="storyteller">Storyteller</option>
<option value="kids_story_time">Kids Story Time</option>
<option value="kids_trivia_game">Kids Trivia Game</option>
<option value="meditation">Meditation</option>
<option value="doc">Grok "Doc"</option>
<option value="unhinged">Unhinged 18+</option>
<option value="sexy">Sexy 18+</option>
<option value="motivation">Motivation 18+</option>
<option value="conspiracy">Conspiracy</option>
<option value="romantic">Romantic 18+</option>
<option value="argumentative">Argumentative 18+</option>
</select>
</div>
<div class="form-group">
<label for="speedRange">语速: <span id="speedValue">1.0</span>x</label>
<input type="range" id="speedRange" min="0.5" max="2.0" step="0.1" value="1.0">
</div>
<button id="startBtn">开始语音对话</button>
<button id="stopBtn" class="hidden">停止对话</button>
</div>
<div id="log"></div>
</div>
<script>
// Use a more reliable way to wait for the SDK to load
let Room, createLocalTracks, RoomEvent, Track;
function initLiveKit() {
// Check both possible global variable names
const lk = window.LiveKitClient || window.LivekitClient;
if (!lk) {
const keys = Object.keys(window).filter(k => k.toLowerCase().includes('live'));
console.error('LiveKit SDK not found. Available window keys:', keys);
log('调试信息: 无法找到 LiveKitClient. Window 中包含 "live" 的属性: ' + JSON.stringify(keys));
return false;
}
Room = lk.Room;
createLocalTracks = lk.createLocalTracks;
RoomEvent = lk.RoomEvent;
Track = lk.Track;
return true;
}
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const statusIcon = document.getElementById('statusIcon');
const statusText = document.getElementById('statusText');
const logContainer = document.getElementById('log');
const apiKeyInput = document.getElementById('apiKey');
const voiceSelect = document.getElementById('voiceSelect');
const personalitySelect = document.getElementById('personalitySelect');
const speedRange = document.getElementById('speedRange');
const speedValue = document.getElementById('speedValue');
// Update speed label
speedRange.addEventListener('input', (e) => {
speedValue.textContent = e.target.value;
});
let room;
function log(msg) {
const p = document.createElement('p');
p.style.margin = '4px 0';
p.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
logContainer.prepend(p);
console.log(msg);
}
async function startSession() {
if (!Room && !initLiveKit()) {
log('错误: LiveKit SDK 未能正确加载,请刷新页面重试');
return;
}
try {
startBtn.disabled = true;
log('正在获取 Token...');
statusText.textContent = '正在连接...';
statusIcon.className = 'status-indicator status-connecting';
const apiKey = apiKeyInput.value;
const voice = voiceSelect.value;
const personality = personalitySelect.value;
const speed = speedRange.value;
// Build query params
const params = new URLSearchParams({
voice: voice,
personality: personality,
speed: speed
});
const response = await fetch(`/v1/voice/token?${params.toString()}`, {
headers: {
'Authorization': `Bearer ${apiKey}`
}
});
if (!response.ok) {
throw new Error(`获取 Token 失败: ${response.status}`);
}
const { token, url } = await response.json();
log(`获取 Token 成功 (${voice}, ${personality}, ${speed}x)`);
room = new Room({
adaptiveStream: true,
dynacast: true,
});
room.on(RoomEvent.ParticipantConnected, (p) => log(`参与者已连接: ${p.identity}`));
room.on(RoomEvent.ParticipantDisconnected, (p) => log(`参与者已断开: ${p.identity}`));
room.on(RoomEvent.TrackSubscribed, (track) => {
log(`订阅音轨: ${track.kind}`);
if (track.kind === 'audio') {
const element = track.attach();
document.body.appendChild(element);
}
});
room.on(RoomEvent.Disconnected, () => {
log('已断开连接');
resetUI();
});
await room.connect(url, token);
log('已连接到 LiveKit 服务器');
statusText.textContent = '通话中';
statusIcon.className = 'status-indicator status-connected';
startBtn.classList.add('hidden');
stopBtn.classList.remove('hidden');
log('正在开启麦克风...');
const tracks = await createLocalTracks({ audio: true, video: false });
for (const track of tracks) {
await room.localParticipant.publishTrack(track);
}
log('语音已开启');
} catch (err) {
log(`错误: ${err.message}`);
console.error(err);
statusText.textContent = '连接错误';
statusIcon.className = 'status-indicator status-error';
startBtn.disabled = false;
}
}
async function stopSession() {
if (room) {
await room.disconnect();
}
resetUI();
}
function resetUI() {
statusText.textContent = '未连接';
statusIcon.className = 'status-indicator';
startBtn.classList.remove('hidden');
stopBtn.classList.add('hidden');
startBtn.disabled = false;
}
startBtn.addEventListener('click', startSession);
stopBtn.addEventListener('click', stopSession);
// 简易动画效果
setInterval(() => {
if (statusIcon.classList.contains('status-connected')) {
document.querySelectorAll('.bar').forEach(bar => {
bar.style.height = `${Math.random() * 30 + 5}px`;
});
} else {
document.querySelectorAll('.bar').forEach(bar => {
bar.style.height = `4px`;
});
}
}, 150);
</script>
</body>
</html>
-1
View File
@@ -1 +0,0 @@
{}
+8
View File
@@ -29,6 +29,7 @@ from app.api.v1.chat import router as chat_router # noqa: E402
from app.api.v1.image import router as image_router # noqa: E402
from app.api.v1.files import router as files_router # noqa: E402
from app.api.v1.models import router as models_router # noqa: E402
from app.api.v1.voice import router as voice_router # noqa: E402
from app.services.token import get_scheduler # noqa: E402
@@ -108,6 +109,9 @@ def create_app() -> FastAPI:
app.include_router(
models_router, prefix="/v1", dependencies=[Depends(verify_api_key)]
)
app.include_router(
voice_router, prefix="/v1", dependencies=[Depends(verify_api_key)]
)
app.include_router(files_router, prefix="/v1/files")
# 静态文件服务
@@ -131,6 +135,10 @@ app = create_app()
if __name__ == "__main__":
import uvicorn
# Set proxy
os.environ["HTTP_PROXY"] = "http://127.0.0.1:19347"
os.environ["HTTPS_PROXY"] = "http://127.0.0.1:19347"
host = os.getenv("SERVER_HOST", "0.0.0.0")
port = int(os.getenv("SERVER_PORT", "8000"))
workers = int(os.getenv("SERVER_WORKERS", "1"))
+1
View File
@@ -12,6 +12,7 @@ dependencies = [
"fastapi>=0.119.0",
"greenlet>=3.3.1",
"httpx>=0.28.1",
"livekit>=1.0.25",
"loguru>=0.7.3",
"orjson>=3.11.4",
"pydantic-settings>=2.12.0",
+103
View File
@@ -0,0 +1,103 @@
import asyncio
import os
import sys
from pathlib import Path
# Add project root to path
sys.path.append(str(Path(__file__).parent.parent))
from dotenv import load_dotenv
# Load env vars
load_dotenv(Path(__file__).parent.parent / ".env")
from app.core.config import config
from app.services.token import get_token_manager
from app.services.grok.voice import VoiceService
try:
from livekit import rtc
except ImportError:
print("Please install livekit: pip install livekit")
sys.exit(1)
async def main():
print("Testing Grok Voice Connection...")
# Load config
await config.load()
# Get token
token = None
# 1. Try TokenManager
try:
token_mgr = await get_token_manager()
token = token_mgr.get_token('ssoBasic')
if token:
print(f"Using Token from Manager: {token[:10]}...")
except Exception as e:
print(f"TokenManager failed: {e}")
# 2. Try Env Vars
if not token:
token = os.getenv("SSO_TOKEN") or os.getenv("GROK_TOKEN")
if token:
print(f"Using Token from Env: {token[:10]}...")
if not token:
print("Error: No SSO token available. Please set SSO_TOKEN env var or check token pool.")
return
sso_token = token
# Get Voice Token
service = VoiceService()
try:
data = await service.get_token(sso_token)
token = data.get("token")
url = "wss://livekit.grok.com"
print(f"Got LiveKit Token: {token[:20]}...")
except Exception as e:
print(f"Failed to get voice token: {e}")
return
# Connect to LiveKit
print(f"Connecting to {url}...")
room = rtc.Room()
@room.on("participant_connected")
def on_participant_connected(participant: rtc.RemoteParticipant):
print(f"Participant connected: {participant.identity}")
@room.on("participant_disconnected")
def on_participant_disconnected(participant: rtc.RemoteParticipant):
print(f"Participant disconnected: {participant.identity}")
@room.on("track_subscribed")
def on_track_subscribed(track: rtc.Track, publication: rtc.RemoteTrackPublication, participant: rtc.RemoteParticipant):
print(f"Track subscribed: {publication.kind} from {participant.identity}")
if publication.kind == rtc.TrackKind.KIND_AUDIO:
print("Audio track received! Voice mode is working.")
try:
await room.connect(url, token)
print("Connected to Room!")
# Keep connection open for a few seconds
await asyncio.sleep(10)
except Exception as e:
print(f"Failed to connect to LiveKit: {e}")
finally:
await room.disconnect()
print("Disconnected")
if __name__ == "__main__":
asyncio.run(main())
Generated
+95
View File
@@ -278,6 +278,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "livekit"
version = "1.0.25"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
{ name = "numpy" },
{ name = "protobuf" },
{ name = "types-protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f6/80/b6dbac61e7c10edee295dba3d0431b8a8504cd6db5286709b03d66a66d0a/livekit-1.0.25.tar.gz", hash = "sha256:a28ef3a1f420616facfb36a92cd590e5510b9cfc5b8b9bd425587a159edfd57b", size = 316847, upload-time = "2026-01-30T17:07:25.236Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/bc/ad8d6e340c02db57cc624c025cc6d838703fa6a8a211b643beaf5c5f789d/livekit-1.0.25-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:1ec9e16e30cc9831c447cdf05fcffc74453ae40a33ed7e2ef2178f98a3bb2de4", size = 11059941, upload-time = "2026-01-30T17:07:13.644Z" },
{ url = "https://files.pythonhosted.org/packages/c8/fc/dfbbab4f3168f2e0ed4c255b9f387f37ff70a29d8026ca7b411e368a565a/livekit-1.0.25-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6237866efbbf033a3d174f15edc891cb793e9fbafe9432d1fd5bb62080704fbb", size = 9807946, upload-time = "2026-01-30T17:07:15.986Z" },
{ url = "https://files.pythonhosted.org/packages/cf/b6/1ec8e007631ba8da62d963547f2d7237e1696106e9ddb69b3cd1bc20318b/livekit-1.0.25-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:5a93e854744074fa37eb02f77b4dcdbafcefddc3763f28ec5b267059ddb0c65e", size = 15175694, upload-time = "2026-01-30T17:07:18.101Z" },
{ url = "https://files.pythonhosted.org/packages/a2/e7/1963f93804697de51c34677405e26960ca5934fe865632422eb712bca2e7/livekit-1.0.25-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:abe498700566e90c89dbbf73731f850f4b5f7d2185bac88be71a34657b51fd3a", size = 12636407, upload-time = "2026-01-30T17:07:21.013Z" },
{ url = "https://files.pythonhosted.org/packages/f7/3a/d5427fe78f2c5395d3da4b895978c693a2652f9480e75abf93d8ab315a94/livekit-1.0.25-py3-none-win_amd64.whl", hash = "sha256:6ef59cca92db6f207d40b4499b8d2338f8c7278e8cae9e24d8208d66a7d2b8b0", size = 11727716, upload-time = "2026-01-30T17:07:23.007Z" },
]
[[package]]
name = "loguru"
version = "0.7.3"
@@ -303,6 +322,7 @@ dependencies = [
{ name = "fastapi" },
{ name = "greenlet" },
{ name = "httpx" },
{ name = "livekit" },
{ name = "loguru" },
{ name = "orjson" },
{ name = "pydantic-settings" },
@@ -324,6 +344,7 @@ requires-dist = [
{ name = "fastapi", specifier = ">=0.119.0" },
{ name = "greenlet", specifier = ">=3.3.1" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "livekit", specifier = ">=1.0.25" },
{ name = "loguru", specifier = ">=0.7.3" },
{ name = "orjson", specifier = ">=3.11.4" },
{ name = "pydantic-settings", specifier = ">=2.12.0" },
@@ -336,6 +357,56 @@ requires-dist = [
{ name = "uvicorn", specifier = ">=0.37.0" },
]
[[package]]
name = "numpy"
version = "2.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" },
{ url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" },
{ url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" },
{ url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" },
{ url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" },
{ url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" },
{ url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" },
{ url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" },
{ url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" },
{ url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" },
{ url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" },
{ url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" },
{ url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" },
{ url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" },
{ url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" },
{ url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" },
{ url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" },
{ url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" },
{ url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" },
{ url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" },
{ url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" },
{ url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" },
{ url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" },
{ url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" },
{ url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" },
{ url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" },
{ url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" },
{ url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" },
{ url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" },
{ url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" },
{ url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" },
{ url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" },
{ url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" },
{ url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" },
{ url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" },
{ url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" },
{ url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" },
{ url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" },
{ url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" },
{ url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" },
{ url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" },
]
[[package]]
name = "orjson"
version = "3.11.6"
@@ -374,6 +445,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/55/74/f473a3ec7a0a7ebc825ca8e3c86763f7d039f379860c81ba12dcdd456547/orjson-3.11.6-cp314-cp314-win_arm64.whl", hash = "sha256:fe71f6b283f4f1832204ab8235ce07adad145052614f77c876fcf0dac97bc06f", size = 135168, upload-time = "2026-01-29T15:13:05.932Z" },
]
[[package]]
name = "protobuf"
version = "6.33.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" },
{ url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" },
{ url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" },
{ url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" },
{ url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" },
{ url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" },
{ url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" },
]
[[package]]
name = "pycparser"
version = "3.0"
@@ -620,6 +706,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
]
[[package]]
name = "types-protobuf"
version = "6.32.1.20251210"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/59/c743a842911887cd96d56aa8936522b0cd5f7a7f228c96e81b59fced45be/types_protobuf-6.32.1.20251210.tar.gz", hash = "sha256:c698bb3f020274b1a2798ae09dc773728ce3f75209a35187bd11916ebfde6763", size = 63900, upload-time = "2025-12-10T03:14:25.451Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/43/58e75bac4219cbafee83179505ff44cae3153ec279be0e30583a73b8f108/types_protobuf-6.32.1.20251210-py3-none-any.whl", hash = "sha256:2641f78f3696822a048cfb8d0ff42ccd85c25f12f871fbebe86da63793692140", size = 77921, upload-time = "2025-12-10T03:14:24.477Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"