commit dde3e12476f98743f4a6b5ba91ec0ece46dff5ef Author: Bifang <915779419@qq.com> Date: Mon Sep 28 17:00:53 2026 +0800 初始化 Qwen-Asr 本地仓库 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..45e18e6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,70 @@ +# Git文件 +.git +.gitignore +.gitmodules + +# Python缓存 +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +*.so + +# 虚拟环境 +venv +env +ENV +.venv + +# IDE文件 +.vscode +.idea +*.swp +*.swo + +# 系统文件 +.DS_Store +Thumbs.db + +# 日志文件 +*.log +logs/ + +# 临时文件 +temp/ +tmp/ +*.tmp +build-file/ + +# 模型文件(这些将通过volume挂载) +tts/third_party/CosyVoice/pretrained_models/ +models/ +*.pt +*.pth +*.bin +*.safetensors +*.tar.gz + +# 测试文件 +tests/ +*.test +coverage.* + +# 文档 +README.md +*.md +docs/ + +# 配置文件(可能包含敏感信息) +.env +.env.* + +# 本地音视频样本 +*.m4a +*.mp4 + +# 其他 +.pytest_cache +.coverage +node_modules diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a83b43f --- /dev/null +++ b/.env.example @@ -0,0 +1,150 @@ +# Qwen3-ASR 环境变量覆盖示例 +# 复制为 .env 后,只取消你需要修改的项的注释即可。 + +# ----------------------------------------------------------------------------- +# 仅在需要 API Key 鉴权时开启。 +# ----------------------------------------------------------------------------- +# API_KEY=your_api_key_here + +# ----------------------------------------------------------------------------- +# Docker / 部署入口配置。 +# ----------------------------------------------------------------------------- +# NGINX_PORT:宿主机暴露的 Nginx 端口。 +# NGINX_PORT=17003 +# ASR_IMAGE:离线包或自定义部署时使用的镜像标签。 +# ASR_IMAGE=unis/qwen3-asr:gpu-latest +# ASR_VISIBLE_DEVICES:统一可见卡编号配置,多个用逗号分隔;程序会按当前 accelerator 自动映射到底层变量。 +# ASR_VISIBLE_DEVICES=0 +# METAX_DRI_DEVICE:沐曦 / MuXi 需要映射的 DRI 设备路径。 +# METAX_DRI_DEVICE=/dev/dri +# METAX_MXSMI_PATH:沐曦 / MuXi 的 mx-smi 工具路径。 +# METAX_MXSMI_PATH=/opt/mxdriver/bin/mx-smi +# METAX_PRIVILEGED:沐曦 / MuXi 容器是否以 privileged 方式运行。 +# METAX_PRIVILEGED=false +# ILUVATAR_USR_SRC:天数 / Iluvatar 容器内 usr/src 挂载路径。 +# ILUVATAR_USR_SRC=/usr/src +# ILUVATAR_LIB_MODULES:天数 / Iluvatar 容器内模块目录挂载路径。 +# ILUVATAR_LIB_MODULES=/lib/modules +# ILUVATAR_DEV:天数 / Iluvatar 设备目录挂载路径。 +# ILUVATAR_DEV=/dev +# ILUVATAR_HOME:天数 / Iluvatar 主目录挂载路径。 +# ILUVATAR_HOME=/home +# ILUVATAR_DATA:天数 / Iluvatar 数据目录挂载路径。 +# ILUVATAR_DATA=/data +# MTHREADS_DEV:摩尔线程 / Moore Threads 设备目录挂载路径。 +# MTHREADS_DEV=/dev +# MTHREADS_USR_SRC:摩尔线程 / Moore Threads 容器内 usr/src 挂载路径。 +# MTHREADS_USR_SRC=/usr/src +# MTHREADS_LIB_MODULES:摩尔线程 / Moore Threads 容器内模块目录挂载路径。 +# MTHREADS_LIB_MODULES=/lib/modules +# MTHREADS_HOME:摩尔线程 / Moore Threads 主目录挂载路径。 +# MTHREADS_HOME=/home +# MTHREADS_DATA:摩尔线程 / Moore Threads 数据目录挂载路径。 +# MTHREADS_DATA=/data +# MODEL_STORAGE_DIR:模型宿主机挂载目录。 +# MODEL_STORAGE_DIR=/opt/dep/asr/models +# DATA_STORAGE_DIR:业务数据宿主机挂载目录。 +# DATA_STORAGE_DIR=/opt/dep/asr/data +# NGINX_RATE_LIMIT_RPS:Nginx 全局限流,单位为每秒请求数。 +# NGINX_RATE_LIMIT_RPS=0 +# NGINX_RATE_LIMIT_BURST:Nginx 全局突发限流额度,0 表示自动按 RPS 处理。 +# NGINX_RATE_LIMIT_BURST=0 +# ASR_DEPLOY_TOPOLOGY:部署拓扑,isolated=每卡一个实例,sharded=单实例多卡分片,auto=优先 sharded 失败回退 isolated。 +# ASR_DEPLOY_TOPOLOGY=isolated + +# ----------------------------------------------------------------------------- +# 运行时模型选择。 +# 留空 QWEN3_ASR_MODEL 时会自动选择。 +# 支持值:qwen3-asr-0.6b、qwen3-asr-1.7b +# ----------------------------------------------------------------------------- +# ACCELERATOR:加速后端类型,常见值有 auto、cpu、nvidia、metax、iluvatar、mthreads。 +# ACCELERATOR=auto +# DEVICE:具体运行设备,常见值有 auto、cpu、cuda:0。 +# DEVICE=auto +# QWEN3_ASR_MODEL:手动指定离线识别模型,留空则自动挑选。 +# QWEN3_ASR_MODEL= + +# ----------------------------------------------------------------------------- +# 模型下载 / 缓存行为。 +# 在 Docker Compose 中,默认宿主机挂载目录是 /opt/dep/asr/models。 +# 实际模型目录通常位于: +# /opt/dep/asr/models/Qwen +# /opt/dep/asr/models/iic +# /opt/dep/asr/models/damo +# ----------------------------------------------------------------------------- +# MODELS_DIR:项目主模型目录。 +# MODELS_DIR=/opt/dep/asr/models +# MODELSCOPE_CACHE:ModelScope 缓存根目录,下面会生成 models/{publisher}/{model}。 +# MODELSCOPE_CACHE=/opt/dep/asr +# MODELSCOPE_PATH:ModelScope 实际模型目录,建议保持在 models 目录下。 +# MODELSCOPE_PATH=/opt/dep/asr/models + +# ----------------------------------------------------------------------------- +# 说话人注册 / pgvector 数据库配置。 +# 表结构与 Model-Test-New 保持一致:speakers(id, name, user_id, embedding)。 +# ----------------------------------------------------------------------------- +# SPEAKER_DB_ENABLED:是否启用说话人库与 pgvector 检索。 +# SPEAKER_DB_ENABLED=true +# DB_HOST:PostgreSQL 主机地址。 +# DB_HOST=127.0.0.1 +# DB_PORT:PostgreSQL 端口。 +# DB_PORT=5432 +# DB_USER:PostgreSQL 用户名。 +# DB_USER=postgres +# DB_PASSWORD:PostgreSQL 密码。 +# DB_PASSWORD=postgres +# DB_NAME:PostgreSQL 数据库名。 +# DB_NAME=asr_db +# SV_MODEL:说话人识别模型。 +# SV_MODEL=iic/speech_campplus_sv_zh-cn_16k-common +# SV_THRESHOLD:说话人相似度阈值,越大越严格。 +# SV_THRESHOLD=0.6 +# TEMP_DIR:临时文件目录。 +# TEMP_DIR=/opt/dep/asr/data/temp +# LOG_FILE:日志文件路径。 +# LOG_FILE=/opt/dep/asr/data/logs/qwen3-asr.log +# TASK_STATE_DIR:任务状态持久化目录。 +# TASK_STATE_DIR=/opt/dep/asr/data/tasks +# TASK_RETENTION_HOURS:任务结果保留时间,单位小时。 +# TASK_RETENTION_HOURS=24 + +# ----------------------------------------------------------------------------- +# CPU Rust 后端覆盖配置。 +# 一般不需要改;自动检测会检查 vendor/qwenasr/target/{release,debug}。 +# ----------------------------------------------------------------------------- +# QWENASR_LIBRARY_PATH:手动指定 libqwen_asr.so 的绝对路径。 +# QWENASR_LIBRARY_PATH=/absolute/path/to/libqwen_asr.so + +# ----------------------------------------------------------------------------- +# 调优参数。除非你在做特定瓶颈测试,否则建议保持默认。 +# ----------------------------------------------------------------------------- +# ASR_BATCH_SIZE:批处理大小,表示一次并行推理的片段数。 +# ASR_BATCH_SIZE=4 +# ASR_ENABLE_WORD_TIMESTAMPS:是否启用字词级时间戳。 +# ASR_ENABLE_WORD_TIMESTAMPS=false +# MAX_SEGMENT_SEC:离线 ASR 单段最大时长,单位秒。 +# MAX_SEGMENT_SEC=60 +# QWEN_RUST_CPU_WORKERS:Qwen Rust CPU 后端 worker 数。 +# QWEN_RUST_CPU_WORKERS=4 +# QWEN_RUST_ASR_CONCURRENCY:Qwen Rust ASR 并发数,0 通常表示自动。 +# QWEN_RUST_ASR_CONCURRENCY=0 +# QWEN_RUST_ALIGN_CONCURRENCY:Qwen Rust 对齐并发数,0 通常表示自动。 +# QWEN_RUST_ALIGN_CONCURRENCY=0 +# QWEN_GPU_MEMORY_UTILIZATION:GPU 显存使用比例。 +# QWEN_GPU_MEMORY_UTILIZATION=0.9 +# QWEN_VLLM_ENFORCE_EAGER:是否强制 vLLM eager 执行;国产卡/稳定优先建议 true,NVIDIA 性能测试可设 false。 +# QWEN_VLLM_ENFORCE_EAGER=true +# QWEN_FORCE_ALIGNER_GPU_MEMORY_UTILIZATION:forced aligner 预留的显存比例。 +# QWEN_FORCE_ALIGNER_GPU_MEMORY_UTILIZATION=0.15 +# ASR_ENABLE_NEARFIELD_FILTER:是否启用近场/远场过滤。 +# ASR_ENABLE_NEARFIELD_FILTER=true +# ASR_NEARFIELD_RMS_THRESHOLD:近场判断的 RMS 能量阈值。 +# ASR_NEARFIELD_RMS_THRESHOLD=0.01 + +# ----------------------------------------------------------------------------- +# 仅用于本地开发调试。 +# ----------------------------------------------------------------------------- +# LOG_LEVEL:日志级别。 +# LOG_LEVEL=INFO +# FUNASR_STARTUP_UI:FunASR 启动界面模式。 +# FUNASR_STARTUP_UI=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..992df58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# Environment files (contain sensitive information) +.env +*.env.local +*.env.*.local + +# Build and deployment files +build-file/ + +# Downloaded models directory +/models/ + +# Large binary packages +*.tar.gz +*.tar +*.zip + +# Python cache +__pycache__/ +*.py[cod] +*$py.class +*.so + +# Model files (downloaded) +*.pt +*.pth +*.onnx +*.safetensors +*.bin + +# Log files +*.log +logs/ + +# OS files +.DS_Store +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Temporary files +*.tmp +*.temp +.cache/ +.code-review-graph/ \ No newline at end of file diff --git a/.pi-crg-mcp/README.md b/.pi-crg-mcp/README.md new file mode 100644 index 0000000..b29041b --- /dev/null +++ b/.pi-crg-mcp/README.md @@ -0,0 +1,36 @@ +# Code Review Graph MCP + +Generated from the `agent-tool-basic` template. + +## Contributions + +- Agent tool `echo_text` + +## Develop + +1. Open the Plugins page and use **Load development plugin**, pointing at this + directory. PI-Desktop reloads the plugin whenever you save a file here. +2. Verify the contributions from the command palette. +3. Validate and package: + +```bash +pnpm pi-plugin check . +pnpm pi-plugin pack . +# writes dist/crg-mcp-0.1.0.piplug +``` + +Install the resulting `.piplug` from the Plugins page to test it the way a +user would. + +### Panel top drag band + +PI-Desktop reserves exactly a transparent 46px frameless drag band above panel +content and renders a minimal fixed three-button window-control capsule in its +top-right corner. Normal-flow content is offset automatically. The panel title, +toolbar, and every other visible surface belong to the plugin. Development +panels show a reminder that the top 46px is not clickable outside the capsule. +For `position: fixed` or `position: sticky` content, use +`top: var(--pi-plugin-titlebar-height, 46px)` and account for the same value +in viewport-height calculations. Add `-webkit-app-region: drag` to a +plugin-owned toolbar when it should move the window, and +`-webkit-app-region: no-drag` to controls inside it. diff --git a/.pi-crg-mcp/crg.cmd b/.pi-crg-mcp/crg.cmd new file mode 100644 index 0000000..4b695f5 --- /dev/null +++ b/.pi-crg-mcp/crg.cmd @@ -0,0 +1,57 @@ +@echo off +chcp 65001 >nul +setlocal + +rem --------------------------------------------------------------------------- +rem code-review-graph MCP launcher for PI-Desktop. +rem +rem The MCP host spawns this with a minimal environment (PATH, SystemRoot, +rem windir, TEMP, TMP, LANG plus the manifest's env block) and cwd = plugin +rem directory, with stdin/stdout used for JSON-RPC. Never write to stdout. +rem --------------------------------------------------------------------------- + +if defined CRG_HOME goto have_home +set "CRG_HOME=D:\github-project\code-review-graph" +:have_home + +rem code_review_graph/constants.py calls Path.home() at import time; without a +rem user profile the server dies with "Could not determine home directory.". +if defined USERPROFILE goto have_profile +set "USERPROFILE=C:\Users\%USERNAME%" +:have_profile +if defined HOMEDRIVE goto have_hd +set "HOMEDRIVE=C:" +:have_hd +if defined HOMEPATH goto have_hp +set "HOMEPATH=\Users\%USERNAME%" +:have_hp +if defined APPDATA set "APPDATA=%USERPROFILE%\AppData\Roaming" +if defined LOCALAPPDATA set "LOCALAPPDATA=%USERPROFILE%\AppData\Local" + +set "PYTHONUTF8=1" +set "PYTHONIOENCODING=utf-8" + +rem The editable install's .pth points at D:\github_project\... (underscore) +rem while the checkout lives at D:\github-project\... (hyphen), so the package +rem is only importable with the checkout explicitly on sys.path. +set "PYTHONPATH=%CRG_HOME%" + +rem The venv's code-review-graph.exe is a broken uv trampoline +rem ("failed to canonicalize script path"), so prefer a working interpreter. +set "CRG_PY=%CRG_HOME%\.venv\Scripts\python.exe" +if exist "%CRG_PY%" goto py_ready +set "CRG_PY=python" +:py_ready + +rem Upstream ships 30 tools; keep the surface small unless overridden. +if defined CRG_TOOLS goto tools_ready +set "CRG_TOOLS=build_or_update_graph_tool,run_postprocess_tool,get_minimal_context_tool,get_review_context_tool,get_impact_radius_tool,query_graph_tool,semantic_search_nodes_tool,detect_changes_tool,list_graph_stats_tool,get_affected_flows_tool" +:tools_ready + +if defined CRG_REPO goto repo_ready +set "CRG_REPO=%CRG_HOME%" +:repo_ready + +"%CRG_PY%" -m code_review_graph serve --repo "%CRG_REPO%" + +endlocal diff --git a/.pi-crg-mcp/main.js b/.pi-crg-mcp/main.js new file mode 100644 index 0000000..3930880 --- /dev/null +++ b/.pi-crg-mcp/main.js @@ -0,0 +1,16 @@ +/** + * crg-mcp — thin loader for the code-review-graph MCP bridge. + * + * All wiring lives in manifest.json under `contributes.mcpServers`: the host + * spawns `crg.cmd`, speaks MCP over its stdio, and publishes each upstream tool + * as `plugin_crg_mcp_crg_`. The host owns the client, the retry on the + * next call, and the tool lifecycle, so there is nothing to register here. + */ + +async function onLoad() { + // Intentionally empty: the MCP client is owned by the host. +} + +async function onUnload() {} + +module.exports = { onLoad, onUnload }; diff --git a/.pi-crg-mcp/manifest.json b/.pi-crg-mcp/manifest.json new file mode 100644 index 0000000..44bfe1f --- /dev/null +++ b/.pi-crg-mcp/manifest.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 1, + "id": "crg-mcp", + "name": "Code Review Graph MCP", + "version": "1.0.0", + "description": "Bridges the locally deployed code-review-graph MCP server into the agent as native tools.", + "main": "main.js", + "contributes": { + "mcpServers": [ + { + "id": "crg", + "label": "Code Review Graph", + "transport": "stdio", + "command": "crg.cmd", + "env": { + "PYTHONPATH": "D:\\github-project\\code-review-graph", + "USERPROFILE": "C:\\Users\\admin", + "HOMEDRIVE": "C:", + "HOMEPATH": "\\Users\\admin" + } + } + ] + }, + "permissions": [ + "mcp.server.local" + ], + "engines": { + "piDesktop": ">=0.1.0" + }, + "activationEvents": [ + "onStartup" + ] +} diff --git a/.pi-crg-mcp/selfcheck.ps1 b/.pi-crg-mcp/selfcheck.ps1 new file mode 100644 index 0000000..5270319 --- /dev/null +++ b/.pi-crg-mcp/selfcheck.ps1 @@ -0,0 +1,96 @@ +<# + Self-check for the crg-mcp plugin. + + Drives crg.cmd exactly the way the PI-Desktop MCP host does — `cmd /c crg.cmd` + with piped stdio and cwd = this folder — then reports the MCP handshake, the + tool list, and one real tool call. Run it from PowerShell: + + powershell -NoProfile -ExecutionPolicy Bypass -File .\selfcheck.ps1 +#> +$ErrorActionPreference = 'Stop' + +$dir = Split-Path -Parent $MyInvocation.MyCommand.Path + +# Mirror the host's minimal environment plus the manifest's env block. +foreach ($k in 'PATH', 'SystemRoot', 'TEMP', 'TMP') { + if (-not (Test-Path "Env:$k")) { Write-Warning "missing $k in ambient env" } +} + +$psi = New-Object System.Diagnostics.ProcessStartInfo +$psi.FileName = 'cmd.exe' +$psi.Arguments = '/c crg.cmd' +$psi.WorkingDirectory = $dir +$psi.RedirectStandardInput = $true +$psi.RedirectStandardOutput = $true +$psi.RedirectStandardError = $true +$psi.UseShellExecute = $false +$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8 + +$proc = [System.Diagnostics.Process]::Start($psi) + +function Send($obj) { + $proc.StandardInput.WriteLine(($obj | ConvertTo-Json -Compress -Depth 8)) + $proc.StandardInput.Flush() +} + +function ReadLine([int]$waitSeconds = 25) { + $task = $proc.StandardOutput.ReadLineAsync() + if ($task.Wait([TimeSpan]::FromSeconds($waitSeconds))) { return $task.Result } + return $null +} + +Send @{ + jsonrpc = '2.0'; id = 1; method = 'initialize' + params = @{ + protocolVersion = '2025-06-18' + capabilities = @{} + clientInfo = @{ name = 'crg-selfcheck'; version = '1' } + } +} + +$initLine = ReadLine 40 +if (-not $initLine) { + Write-Host 'HANDSHAKE FAILED: no stdout from crg.cmd' -ForegroundColor Red + Write-Host '--- stderr ---' + Write-Host $proc.StandardError.ReadToEnd() + try { $proc.Kill() } catch { } + exit 1 +} + +$init = $initLine | ConvertFrom-Json +Write-Host ("HANDSHAKE OK server={0} {1}" -f $init.result.serverInfo.name, $init.result.serverInfo.version) -ForegroundColor Green + +Send @{ jsonrpc = '2.0'; method = 'notifications/initialized'; params = @{} } +Send @{ jsonrpc = '2.0'; id = 2; method = 'tools/list'; params = @{} } + +$toolsLine = ReadLine +if (-not $toolsLine) { + Write-Host 'tools/list returned nothing' -ForegroundColor Red + try { $proc.Kill() } catch { } + exit 1 +} +$tools = ($toolsLine | ConvertFrom-Json).result.tools +Write-Host ("TOOLS: {0}" -f $tools.Count) -ForegroundColor Green +foreach ($t in $tools) { Write-Host (" plugin_crg_mcp_crg_{0}" -f $t.name) } + +Send @{ + jsonrpc = '2.0'; id = 3; method = 'tools/call' + params = @{ name = 'list_graph_stats_tool'; arguments = @{} } +} +$callLine = ReadLine 40 +if ($callLine) { + $call = $callLine | ConvertFrom-Json + if ($call.result) { + $text = $call.result.content[0].text + Write-Host 'TOOL CALL OK' -ForegroundColor Green + Write-Host (' ' + ($text -split "`n")[0..3] -join ' | ') + } + else { + Write-Host ("TOOL CALL ERROR: {0}" -f ($call | ConvertTo-Json -Compress -Depth 6)) -ForegroundColor Red + } +} +else { + Write-Host 'TOOL CALL: no response' -ForegroundColor Red +} + +try { $proc.Kill() } catch { } diff --git a/Dockerfile.cpu b/Dockerfile.cpu new file mode 100644 index 0000000..ceda127 --- /dev/null +++ b/Dockerfile.cpu @@ -0,0 +1,73 @@ +FROM python:3.10-slim AS runtime + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + HF_HUB_DISABLE_SYMLINKS_WARNING=1 \ + HF_HUB_DISABLE_PROGRESS_BARS=1 \ + OPENBLAS_NUM_THREADS=1 \ + OMP_NUM_THREADS=1 \ + GOTO_NUM_THREADS=1 \ + QWENASR_LIBRARY_PATH=/opt/qwenasr/lib/libqwen_asr.so + +# Install system packages required for audio processing +RUN apt-get update && apt-get install -y --no-install-recommends \ + ffmpeg \ + sox \ + libsox-dev \ + libsndfile1 \ + libopenblas-dev \ + nginx \ + build-essential \ + curl + +WORKDIR /app + +ARG TARGETARCH +ARG QWENASR_RUST_TARGET_CPU=x86-64-v2 + +COPY vendor/qwenasr /tmp/qwenasr + +# Install Rust compiler (required for sudachipy on ARM64) +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +# Build QwenASR Rust shared library for CPU Qwen3-ASR inference. The default +# amd64 target stays portable across common AVX2-era servers; set +# QWENASR_RUST_TARGET_CPU=native only for self-built, host-specific images. +RUN if [ "$TARGETARCH" = "amd64" ]; then export RUSTFLAGS="-C target-cpu=${QWENASR_RUST_TARGET_CPU}"; fi \ + && cargo build --release -p qwen-asr --features ffi --manifest-path /tmp/qwenasr/Cargo.toml \ + && mkdir -p /opt/qwenasr/lib \ + && cp /tmp/qwenasr/target/release/libqwen_asr.so /opt/qwenasr/lib/libqwen_asr.so + +# Install Python dependencies (CPU mode) +COPY environments/cpu/pyproject.toml /app/environments/cpu/pyproject.toml +RUN python - <<'PY' > /tmp/qwen3-asr-cpu-reqs.txt +import tomllib +from pathlib import Path + +data = tomllib.loads(Path("/app/environments/cpu/pyproject.toml").read_text()) +for dep in data["project"]["dependencies"]: + print(dep) +print("asyncpg==0.31.0") +PY +RUN pip install --no-cache-dir -r /tmp/qwen3-asr-cpu-reqs.txt && \ + rm -f /tmp/qwen3-asr-cpu-reqs.txt + +# Clean build tools and cache to reduce image size +RUN apt remove -y build-essential curl && apt autoremove -y \ + && rm -rf /tmp/qwenasr \ + && rm -rf /root/.cargo /root/.rustup \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Copy application code +COPY . . + +# Create runtime directories +RUN mkdir -p /app/data/temp /app/data/logs /app/data/tasks \ + && chmod +x start.py /app/scripts/docker/entrypoint.sh + +EXPOSE 8000 + +ENTRYPOINT ["/app/scripts/docker/entrypoint.sh"] +CMD ["python", "start.py"] diff --git a/Dockerfile.gpu b/Dockerfile.gpu new file mode 100644 index 0000000..da75b2f --- /dev/null +++ b/Dockerfile.gpu @@ -0,0 +1,101 @@ +ARG PYTORCH_BASE_IMAGE=pytorch/pytorch:2.11.0-cuda13.0-cudnn9-runtime +FROM ${PYTORCH_BASE_IMAGE} + +ARG CUDA_NVCC_PACKAGE=cuda-nvcc-13-0 +ARG PYTORCH_CUDA_INDEX=https://download.pytorch.org/whl/cu130 +ARG TORCH_VERSION=2.11.0 +ARG TORCHAUDIO_VERSION=2.11.0 +ARG TORCHVISION_VERSION=0.26.0 +ARG VLLM_VERSION=0.20.0 +ARG TORCH_CUDA_ARCH_LIST=12.0+PTX + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + HF_HUB_DISABLE_SYMLINKS_WARNING=1 \ + HF_HUB_DISABLE_PROGRESS_BARS=1 \ + PIP_BREAK_SYSTEM_PACKAGES=1 \ + TORCH_CUDA_ARCH_LIST=${TORCH_CUDA_ARCH_LIST} + +# Add NVIDIA apt repository for CUDA packages +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + gnupg \ + wget \ + && wget -qO - https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/3bf863cc.pub | apt-key add - \ + && echo "deb https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64 /" > /etc/apt/sources.list.d/cuda.list \ + && apt-get update + +# Install system packages required for audio processing +# Note: nvcc and build-essential are required for FlashInfer JIT compilation. +RUN apt-get install -y --no-install-recommends \ + ffmpeg \ + sox \ + libsox-dev \ + libsndfile1 \ + nginx \ + build-essential \ + ${CUDA_NVCC_PACKAGE} + +WORKDIR /app + +# Install Python dependencies directly into the image runtime Python. +# The image defaults to CUDA 13.0/cu130 for Blackwell-capable GPUs. Developers +# can rebuild with a different PyTorch CUDA backend by overriding: +# PYTORCH_BASE_IMAGE, PYTORCH_CUDA_INDEX, CUDA_NVCC_PACKAGE, TORCH_CUDA_ARCH_LIST. +COPY pyproject.toml /app/ +RUN python - <<'PY' > /tmp/qwen3-asr-gpu-reqs.txt +import tomllib +from pathlib import Path + +data = tomllib.loads(Path("/app/pyproject.toml").read_text()) +for dep in data["project"]["dependencies"]: + if dep.startswith("torch=="): + continue + if dep.startswith("torchaudio=="): + continue + if dep.startswith("torchvision=="): + continue + if dep.startswith("vllm=="): + continue + print(dep) +PY +RUN pip install --no-cache-dir -r /tmp/qwen3-asr-gpu-reqs.txt && \ + pip install --no-cache-dir \ + --index-url "${PYTORCH_CUDA_INDEX}" \ + --extra-index-url https://pypi.org/simple \ + "torch==${TORCH_VERSION}" \ + "torchaudio==${TORCHAUDIO_VERSION}" \ + "torchvision==${TORCHVISION_VERSION}" && \ + pip install --no-cache-dir "vllm==${VLLM_VERSION}" && \ + rm -f /tmp/qwen3-asr-gpu-reqs.txt + +# Fail the image build early if the runtime dependency chain is inconsistent. +RUN python - <<'PY' +import torch +import torchaudio +import transformers +import vllm +from transformers import PreTrainedModel + +print("torch", torch.__version__) +print("torchaudio", torchaudio.__version__) +print("transformers", transformers.__version__) +print("vllm", vllm.__version__) +print("PreTrainedModel", PreTrainedModel) +PY + +# Clean apt cache but keep build-essential and nvcc for FlashInfer JIT compilation. +RUN apt-get clean && rm -rf /var/lib/apt/lists/* + +# Copy application code +COPY . . + +# Create runtime directories +RUN mkdir -p /app/data/temp /app/data/logs /app/data/tasks \ + && chmod +x start.py /app/scripts/docker/entrypoint.sh + +EXPOSE 8000 + +ENTRYPOINT ["/app/scripts/docker/entrypoint.sh"] +CMD ["python", "start.py"] diff --git a/Dockerfile.iluvatar b/Dockerfile.iluvatar new file mode 100644 index 0000000..7bf1713 --- /dev/null +++ b/Dockerfile.iluvatar @@ -0,0 +1,51 @@ +ARG ILUVATAR_BASE_IMAGE=registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5 +FROM ${ILUVATAR_BASE_IMAGE} + +ARG PYTHON_BIN=python3 +ARG INSTALL_SYSTEM_PACKAGES=false + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + HF_HUB_DISABLE_SYMLINKS_WARNING=1 \ + HF_HUB_DISABLE_PROGRESS_BARS=1 \ + PIP_BREAK_SYSTEM_PACKAGES=1 \ + ACCELERATOR=iluvatar \ + DEVICE=auto \ + APP_PYTHON_BIN=${PYTHON_BIN} + +# Iluvatar production images should be built on top of the official Iluvatar +# vLLM image. The base image owns the IX runtime, PyTorch, vLLM, and kernels; +# this layer only adds generic project/runtime dependencies and app code. +RUN if [ "${INSTALL_SYSTEM_PACKAGES}" = "true" ]; then \ + apt-get update && apt-get install -y --no-install-recommends \ + python3 python3-pip python3-venv ffmpeg sox libsox-dev libsndfile1 nginx \ + && apt-get clean && rm -rf /var/lib/apt/lists/*; \ + fi + +WORKDIR /app + +COPY environments/iluvatar/requirements.txt /tmp/qwen3-asr-iluvatar-requirements.txt +RUN ${APP_PYTHON_BIN} -m pip install --no-cache-dir -r /tmp/qwen3-asr-iluvatar-requirements.txt && \ + rm -f /tmp/qwen3-asr-iluvatar-requirements.txt + +RUN ${APP_PYTHON_BIN} - <<'PY' +import importlib +import torch + +print("torch", torch.__version__) +print("torch.cuda.is_available", torch.cuda.is_available()) +for name in ("vllm",): + module = importlib.import_module(name) + print(name, getattr(module, "__version__", "unknown")) +PY + +COPY . . + +RUN mkdir -p /app/data/temp /app/data/logs /app/data/tasks \ + && chmod +x start.py /app/scripts/docker/entrypoint.sh + +EXPOSE 8000 + +ENTRYPOINT ["/app/scripts/docker/entrypoint.sh"] +CMD ["python3", "start.py"] diff --git a/Dockerfile.metax b/Dockerfile.metax new file mode 100644 index 0000000..81f304f --- /dev/null +++ b/Dockerfile.metax @@ -0,0 +1,55 @@ +ARG METAX_BASE_IMAGE +FROM ${METAX_BASE_IMAGE} + +ARG PYTHON_BIN=/opt/conda/bin/python +ARG INSTALL_SYSTEM_PACKAGES=true + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + HF_HUB_DISABLE_SYMLINKS_WARNING=1 \ + HF_HUB_DISABLE_PROGRESS_BARS=1 \ + PIP_BREAK_SYSTEM_PACKAGES=1 \ + ACCELERATOR=metax \ + DEVICE=auto \ + APP_PYTHON_BIN=${PYTHON_BIN} + +# MetaX production images should be built on top of an official MetaX vLLM +# image that already contains MACA, PyTorch, vLLM, and their compiled kernels. +# We only add generic project/runtime dependencies and the application code. +RUN if [ "${INSTALL_SYSTEM_PACKAGES}" = "true" ]; then \ + apt-get update && apt-get install -y --no-install-recommends \ + python3 python3-pip python3-venv ffmpeg sox libsox-dev libsndfile1 nginx \ + && apt-get clean && rm -rf /var/lib/apt/lists/*; \ + fi + +WORKDIR /app + +COPY environments/metax/requirements.txt /tmp/qwen3-asr-metax-requirements.txt +RUN ${APP_PYTHON_BIN} -m pip install \ + --no-cache-dir \ + --no-warn-conflicts \ + --root-user-action=ignore \ + -r /tmp/qwen3-asr-metax-requirements.txt && \ + rm -f /tmp/qwen3-asr-metax-requirements.txt + +RUN ${APP_PYTHON_BIN} - <<'PY' +import importlib +import torch + +print("torch", torch.__version__) +print("torch.cuda.is_available", torch.cuda.is_available()) +for name in ("vllm",): + module = importlib.import_module(name) + print(name, getattr(module, "__version__", "unknown")) +PY + +COPY . . + +RUN mkdir -p /app/data/temp /app/data/logs /app/data/tasks \ + && chmod +x start.py /app/scripts/docker/entrypoint.sh + +EXPOSE 8000 + +ENTRYPOINT ["/app/scripts/docker/entrypoint.sh"] +CMD ["/opt/conda/bin/python", "start.py"] diff --git a/Dockerfile.mthreads b/Dockerfile.mthreads new file mode 100644 index 0000000..112195b --- /dev/null +++ b/Dockerfile.mthreads @@ -0,0 +1,40 @@ +ARG MTHREADS_BASE_IMAGE=registry.mthreads.com/presale/devtech/vllm_musa:s4000_4.3.5_d0519 +FROM ${MTHREADS_BASE_IMAGE} + +ARG PYTHON_BIN=python3 +ARG INSTALL_SYSTEM_PACKAGES=true + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + HF_HUB_DISABLE_SYMLINKS_WARNING=1 \ + HF_HUB_DISABLE_PROGRESS_BARS=1 \ + PIP_BREAK_SYSTEM_PACKAGES=1 \ + ACCELERATOR=mthreads \ + DEVICE=auto \ + APP_PYTHON_BIN=${PYTHON_BIN} + +# Moore Threads production images should be built on top of the official MUSA +# vLLM image. Keep the vendor Python/runtime stack intact and only add the +# extra system tools needed by the application. +RUN if [ "${INSTALL_SYSTEM_PACKAGES}" = "true" ]; then \ + apt-get update && apt-get install -y --no-install-recommends \ + ffmpeg sox libsox-dev libsndfile1 nginx \ + && apt-get clean && rm -rf /var/lib/apt/lists/*; \ + fi + +WORKDIR /app + +COPY environments/mthreads/requirements.txt /tmp/qwen3-asr-mthreads-requirements.txt +RUN ${APP_PYTHON_BIN} -m pip install --no-cache-dir -r /tmp/qwen3-asr-mthreads-requirements.txt && \ + rm -f /tmp/qwen3-asr-mthreads-requirements.txt + +COPY . . + +RUN mkdir -p /app/data/temp /app/data/logs /app/data/tasks \ + && chmod +x start.py /app/scripts/docker/entrypoint.sh + +EXPOSE 8000 + +ENTRYPOINT ["/app/scripts/docker/entrypoint.sh"] +CMD ["python3", "start.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..20dbc01 --- /dev/null +++ b/README.md @@ -0,0 +1,610 @@ +
+ +

Qwen3-ASR

+

Ready-to-use Local Speech Recognition API Service

+ +Speech recognition API service centered on [Qwen3-ASR](https://github.com/QwenLM/Qwen3-ASR), with NVIDIA CUDA vLLM, MetaX/MuXi MACA vLLM, and CPU Rust backends, OpenAI API compatibility, Alibaba Cloud Speech API compatibility, and a Paraformer realtime websocket capability. + +[简体中文](./docs/README_zh.md) + +--- + +![Static Badge](https://img.shields.io/badge/Python-3.10+-blue?logo=python) +![Static Badge](https://img.shields.io/badge/Torch-2.11.0-%23EE4C2C?logo=pytorch&logoColor=white) +![Static Badge](https://img.shields.io/badge/CUDA-13.0_default-%2376B900?logo=nvidia&logoColor=white) + +
+ +## Live Demo Site + +- **Web Demo**: https://asr.vect.one + +## Demo + +[![Demo](./demo/demo.png)](https://media.cdn.vect.one/qwenasr_client_demo.mp4) + +## Release 1.0.1 + +> `v1.0.1` is the current patch release. `v1.0.0` introduced a large breaking refactor relative to the earlier `main` branch. +> If you are upgrading from `main`, read the release notes before reusing old deployment assumptions. +> +> Key breaking changes: +> - Python dependency management is now `uv`-based (`pyproject.toml` + `uv.lock`); `requirements*.txt` are gone +> - Runtime stack changed to `NVIDIA/MetaX GPU -> vLLM`, `CPU/macOS -> vendored QwenASR Rust` +> - `MLX` / Apple Silicon GPU path has been removed; `mps` is normalized to `cpu` +> - macOS / Apple Silicon now defaults to `qwen3-asr-0.6b`; set `QWEN3_ASR_MODEL` to override it +> - `ENABLED_MODELS` has been removed + +## Features + +- **Hybrid Runtime Stack** - Uses auto-selected Qwen3-ASR for offline inference and Paraformer realtime for websocket streaming +- **Speaker Diarization** - Automatic multi-speaker identification using CAM++ model +- **OpenAI API Compatible** - Supports `/v1/audio/transcriptions` endpoint, works with OpenAI SDK +- **Alibaba Cloud API Compatible** - Supports Alibaba Cloud Speech RESTful API and WebSocket streaming protocol +- **WebSocket Streaming** - Real-time streaming speech recognition with low latency +- **Smart Far-Field Filtering** - Automatically filters far-field sounds and ambient noise in streaming ASR +- **Intelligent Audio Segmentation** - VAD-based greedy merge algorithm for automatic long audio splitting +- **GPU Batch Processing** - Batch inference support, 2-3x faster than sequential processing +- **Resource-Aware Runtime** - Auto-selects the appropriate Qwen3-ASR model for the current machine + +## Acknowledgements + +- [Qwen3-ASR](https://github.com/QwenLM/Qwen3-ASR) provides the official model family and multimodal/vLLM usage guidance +- [QwenASR](https://github.com/huanglizhuo/QwenASR) provides the CPU Rust backend vendored by this project + +## Quick Deployment + +### 1. Docker Deployment (Recommended) + +```bash +# Copy and edit configuration +cp .env.example .env +# Edit .env to set API_KEY (optional) + +# Compose defaults: +# /opt/dep/asr/models -> /app/models +# /opt/dep/asr/data -> /app/data +# /opt/dep/asr/data/logs, temp, tasks live under this data mount +# Optional: override any host mount root in .env +# export MODEL_STORAGE_DIR=/data/qwen3-asr-models +# export DATA_STORAGE_DIR=/data/qwen3-asr-data + +# Start service (NVIDIA GPU version) +docker-compose up -d + +# Or MetaX/MuXi GPU version +docker-compose -f docker-compose-metax.yml up -d + +# Or Iluvatar/Tianshu GPU version +docker-compose -f docker-compose-iluvatar.yml up -d + +# Or Moore Threads / MUSA GPU version +docker-compose -f docker-compose-mthreads.yml up -d + +# Or CPU version +docker-compose -f docker-compose-cpu.yml up -d + +# NVIDIA multi-GPU auto mode (one instance per visible GPU) +CUDA_VISIBLE_DEVICES=0,1,2,3 docker-compose up -d + +# MetaX/MuXi multi-GPU auto mode +METAX_VISIBLE_DEVICES=0,1 docker-compose -f docker-compose-metax.yml up -d + +# Iluvatar/Tianshu multi-GPU auto mode +ILUVATAR_VISIBLE_DEVICES=0,1 docker-compose -f docker-compose-iluvatar.yml up -d + +# Moore Threads / MUSA multi-GPU auto mode +MTHREADS_VISIBLE_DEVICES=0,1 docker-compose -f docker-compose-mthreads.yml up -d +``` + +Service URLs: +- **API Endpoint**: `http://localhost:17003` +- **API Docs**: `http://localhost:17003/docs` + +Optional built-in rate limit settings: +- `NGINX_RATE_LIMIT_RPS` (global requests/sec, `0` = disabled) +- `NGINX_RATE_LIMIT_BURST` (global burst, `0` = auto use RPS) + +**docker run (alternative):** + +```bash +# NVIDIA GPU version +docker run -d --name qwen3-asr \ + --gpus all \ + -p 17003:8000 \ + -e ACCELERATOR=nvidia \ + -e CUDA_VISIBLE_DEVICES=0,1,2,3 \ + -e API_KEY=your_api_key \ + -v /opt/dep/asr/models:/app/models \ + -v /opt/dep/asr/data:/app/data \ + unis/qwen3-asr:gpu-latest + +# MetaX/MuXi GPU version +docker run -d --name qwen3-asr-metax \ + --privileged \ + --network=host \ + --pid=host \ + --ipc=host \ + -v /dev:/dev \ + -v /opt/mxdriver:/opt/mxdriver:ro \ + -e ACCELERATOR=metax \ + -e PORT=17003 \ + -e METAX_VISIBLE_DEVICES=0 \ + -v /opt/dep/asr/models:/app/models \ + -v /opt/dep/asr/data:/app/data \ + unis/qwen3-asr:metax-latest + +# CPU version +docker run -d --name qwen3-asr \ + -p 17003:8000 \ + -v /opt/dep/asr/models:/app/models \ + -v /opt/dep/asr/data:/app/data \ + unis/qwen3-asr:cpu-latest +``` + +> **Note**: NVIDIA GPU images default to CUDA 13.0/cu130 with `torch 2.11.0` + `vllm 0.20.0`. +> Developers can rebuild `Dockerfile.gpu` for CUDA 12.6, CUDA 13.0, or another backend by overriding Docker build args. +> MetaX/MuXi images use `Dockerfile.metax` on top of an official MetaX vLLM image. In field deployments, use host networking plus privileged `/dev` and `/opt/mxdriver` mounts so both `mx-smi` and the MetaX PyTorch runtime can initialize devices. +> CPU images now support `qwen3-asr-0.6b` via the bundled QwenASR Rust backend. The default CPU image uses a portable Rust target; set `QWENASR_RUST_TARGET_CPU=native` only for self-built, host-specific images. +> On CUDA vLLM and CPU Rust, `word_timestamps=true` now triggers the forced aligner automatically. +> On macOS / Apple Silicon, Qwen3-ASR now runs through the Rust CPU backend. +> `start.py` now forces the vLLM multiprocessing method to `spawn` so startup does not hit CUDA re-initialization failures in forked subprocesses. + +**Custom GPU backend builds:** + +```bash +# Default GPU build: CUDA 13.0 / PyTorch cu130 +docker build -t qwen3-asr:gpu-cu130 -f Dockerfile.gpu . + +# CUDA 12.6 build for older deployments +docker build -t qwen3-asr:gpu-cu126 -f Dockerfile.gpu \ + --build-arg PYTORCH_BASE_IMAGE=pytorch/pytorch:2.11.0-cuda12.6-cudnn9-runtime \ + --build-arg PYTORCH_CUDA_INDEX=https://download.pytorch.org/whl/cu126 \ + --build-arg CUDA_NVCC_PACKAGE=cuda-nvcc-12-6 \ + --build-arg TORCH_CUDA_ARCH_LIST="8.0;8.6;8.9" \ + . + +# CUDA 13.0 build when your driver/toolchain requires it +docker build -t qwen3-asr:gpu-cu130 -f Dockerfile.gpu \ + --build-arg PYTORCH_BASE_IMAGE=pytorch/pytorch:2.11.0-cuda13.0-cudnn9-runtime \ + --build-arg PYTORCH_CUDA_INDEX=https://download.pytorch.org/whl/cu130 \ + --build-arg CUDA_NVCC_PACKAGE=cuda-nvcc-13-0 \ + --build-arg TORCH_CUDA_ARCH_LIST="12.0+PTX" \ + . + +# MetaX/MuXi build: fuse this project into an official MetaX vLLM image +./scripts/package_vendor_gpu_image.sh \ + --vendor metax \ + --base-image \ + -v n260-3.7.0.38 + +# Iluvatar/Tianshu build: fuse this project into the official Iluvatar vLLM image +docker pull registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5 +./scripts/package_vendor_gpu_image.sh \ + --vendor iluvatar \ + --base-image registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5 \ + -v vllm0.17.0-4.4.0-v5 + +# Moore Threads / MUSA build: fuse this project into the official MUSA vLLM image +docker pull registry.mthreads.com/presale/devtech/vllm_musa:s4000_4.3.5_d0519 +./scripts/package_vendor_gpu_image.sh \ + --vendor mthreads \ + --base-image registry.mthreads.com/presale/devtech/vllm_musa:s4000_4.3.5_d0519 \ + -v s4000_4.3.5_d0519 +``` + +For MetaX/MuXi offline delivery, see [docs/metax_offline_deployment.md](docs/metax_offline_deployment.md). +For Iluvatar/Tianshu offline delivery, see [docs/iluvatar_offline_deployment.md](docs/iluvatar_offline_deployment.md). +For Moore Threads / MUSA offline delivery, see [docs/mthreads_offline_deployment.md](docs/mthreads_offline_deployment.md). + +**Offline Deployment**: You can now build a timestamped offline delivery folder that includes the image archive, compose file, env template, host-dir init script, and usage docs. The export script uses plain `docker build` + `docker save`, so it does not depend on `buildx`: + +```bash +# 1. Build an offline delivery folder +./export_offline_bundle.sh --type gpu +# or +./export_offline_bundle.sh --type cpu +# or MetaX/MuXi GPU +./export_offline_bundle.sh \ + --type metax \ + --metax-base cr.metax-tech.com/public-ai-release/maca/vllm-metax:0.17.0-maca.ai3.5.3.307-torch2.8-py312-ubuntu22.04-amd64 \ + --skip-models +# or Iluvatar/Tianshu GPU +./export_offline_bundle.sh --type iluvatar --iluvatar-base registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5 +# or Moore Threads / MUSA GPU +./export_offline_bundle.sh --type mthreads --mthreads-base registry.mthreads.com/presale/devtech/vllm_musa:s4000_4.3.5_d0519 +# or build both in one bundle +./export_offline_bundle.sh --type all + +# 2. Prepare models separately, without deleting existing model files +./scripts/download-models.sh --models-dir /opt/dep/asr/models + +# 3. Copy the generated folder to the offline server +scp -r build-file/-all user@server:/opt/dep/asr/ + +# 4. On the offline server +cd /opt/dep/asr/-all +./init_host_dirs.sh +gunzip -c qwen3-asr-gpu--amd64.tar.gz | docker load +gunzip -c qwen3-asr-cpu--amd64.tar.gz | docker load +# NVIDIA GPU +docker compose up -d +# or MetaX/MuXi GPU +# docker compose -f docker-compose-metax.yml up -d +# or Iluvatar/Tianshu GPU +# docker compose -f docker-compose-iluvatar.yml up -d +# or Moore Threads / MUSA GPU +# docker compose -f docker-compose-mthreads.yml up -d +# or CPU +# docker compose -f docker-compose-cpu.yml up -d +``` + +> Detailed deployment instructions: [Deployment Guide](./docs/deployment.md) + +### Local Development + +**System Requirements:** + +- Python 3.10+ +- CUDA 13.0+ for the default GPU image; CUDA 12.6 / 13.0 can be built with Docker args +- FFmpeg (audio format conversion) + +**Installation:** + +Runtime dependency locks now default to the GPU stack at the repo root, with CPU kept as a specialized environment: + +| Mode | Command | Notes | +|------|---------|-------| +| NVIDIA GPU (default) | `uv sync` or `./scripts/sync_gpu_env.sh` | Syncs the root [pyproject.toml](/opt/qwen3-asr/pyproject.toml) and [uv.lock](/opt/qwen3-asr/uv.lock) into `.venv`, including CUDA 13.0/cu130 `torch 2.11.0` / `torchaudio 2.11.0` / `torchvision 0.26.0` / `vllm 0.20.0` | +| MetaX/MuXi GPU | `./scripts/sync_metax_env.sh` | Syncs common dependencies from [environments/metax/pyproject.toml](/opt/qwen3-asr/environments/metax/pyproject.toml); optional GPU-stack install uses the MetaX MACA PyPI index with `--no-deps` by default | +| Iluvatar/Tianshu GPU | `./scripts/sync_iluvatar_env.sh` | Syncs common dependencies from [environments/iluvatar/pyproject.toml](/opt/qwen3-asr/environments/iluvatar/pyproject.toml); GPU stack should come from the official Iluvatar vLLM image | +| Moore Threads / MUSA GPU | `./scripts/sync_mthreads_env.sh` | Syncs common dependencies from [environments/mthreads/pyproject.toml](/opt/qwen3-asr/environments/mthreads/pyproject.toml); GPU stack should come from the official Moore Threads MUSA vLLM image | +| CPU (specialized) | `./scripts/sync_cpu_env.sh` | Syncs the dedicated CPU lock in [environments/cpu/pyproject.toml](/opt/qwen3-asr/environments/cpu/pyproject.toml) into `.venv` | +| Auto | `./scripts/sync_accel_env.sh` | Chooses MetaX when `mx-smi` is present, Iluvatar when `ixsmi` is present, Moore Threads when `mthreads-gmi` is present, otherwise NVIDIA when `nvidia-smi` is present, otherwise CPU | + +```bash +# Clone project +cd qwen3-asr + +# Install dependencies (Linux/NVIDIA CUDA) +uv sync + +# Start service +source .venv/bin/activate +python start.py +``` + +MetaX/MuXi local development: + +```bash +./scripts/sync_metax_env.sh +source .venv/bin/activate +ACCELERATOR=metax python start.py +``` + +Local model storage defaults to `./models` under the project root: + +```text +./models/ + Qwen/ + iic/ + damo/ +``` + +Override it when needed: + +```bash +export MODELS_DIR=/data/qwen3-asr-models +export MODELSCOPE_CACHE=/data +export MODELSCOPE_PATH=$MODELS_DIR +``` + +macOS / Apple Silicon local development: + +```bash +./scripts/sync_cpu_env.sh +source .venv/bin/activate +python start.py +``` + +## Runtime Defaults + +Current runtime behavior on the mainline codebase: + +- `ACCELERATOR=auto` resolves to `metax` when `mx-smi` reports devices, then `iluvatar` when `ixsmi` reports devices, then `mthreads` when `mthreads-gmi` reports devices, otherwise `nvidia` when NVIDIA CUDA is available, otherwise `cpu` +- `DEVICE=auto` resolves to the active accelerator device (`cuda:0` for NVIDIA/MetaX/Iluvatar GPU, otherwise `cpu`) +- `DEVICE=mps` is normalized to `cpu` +- `Linux + NVIDIA CUDA` uses official `vLLM` +- `Linux + MetaX/MuXi MACA` uses the MetaX-compatible PyTorch/vLLM stack +- `Linux + Iluvatar/Tianshu` uses the Iluvatar official vLLM image stack +- `Linux + CPU` uses vendored `QwenASR` Rust +- `macOS / Apple Silicon` also uses vendored `QwenASR` Rust +- macOS / Apple Silicon defaults to `qwen3-asr-0.6b` +- `qwen3-asr-1.7b` on macOS is only used when `QWEN3_ASR_MODEL=qwen3-asr-1.7b` +- `word_timestamps=true` works on the current offline CUDA and CPU Rust paths +- WebSocket streaming does not currently return word-level timestamps +- CAM++ speaker diarization remains required and still follows `DEVICE`; on CPU its main hotspot is speaker verification embedding + +## API Endpoints + +### OpenAI Compatible API + +| Endpoint | Method | Function | +|----------|--------|----------| +| `/v1/audio/transcriptions` | POST | Audio transcription (OpenAI compatible) | +| `/v1/models` | GET | Offline model list | + +**Request Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file` | file | Preferred when provided | Audio/video file | +| `audio_address` | string | Optional | Audio/video URL (HTTP/HTTPS), `file://`, or server-local path. Ignored when `file` is also provided | +| `language` | string | Auto-detect | Language code (zh/en/ja) | +| `enable_speaker_diarization` | bool | `true` | Enable speaker diarization | +| `enable_speaker_identification` | bool | `true` | Match registered speaker database when diarization is enabled | +| `enable_text_cleanup` | bool | `true` | Enable text deduplication, boundary-overlap trimming, and filler cleanup | +| `word_timestamps` | bool | `false` | Return word-level timestamps when the backend supports them. Qwen CUDA vLLM and CPU Rust automatically use the forced aligner when enabled. | +| `hotwords` | string | - | Hotwords, format: `word1 weight1 word2 weight2` | +| `response_format` | string | `verbose_json` | Output format | +| `prompt` | string | - | Prompt text (reserved) | +| `temperature` | float | `0` | Sampling temperature (reserved) | + +**Audio / Video Input Methods:** +- **File Upload**: Use `file` parameter to upload an audio file or a video container with an audio track +- **URL / Local Path**: Use `audio_address` parameter to provide an audio/video URL or server-local path, service will read it automatically +- **Precedence**: If both `file` and `audio_address` are provided, the service uses `file` and ignores `audio_address` + +**Usage Examples:** + +```python +# Using OpenAI SDK +from openai import OpenAI + +client = OpenAI(base_url="http://localhost:8000/v1", api_key="your_api_key") + +with open("audio.wav", "rb") as f: + transcript = client.audio.transcriptions.create( + file=f, + response_format="verbose_json" # Get segments and speaker info + ) +print(transcript.text) +``` + +```bash +# Using curl +curl -X POST "http://localhost:8000/v1/audio/transcriptions" \ + -H "Authorization: Bearer your_api_key" \ + -F "file=@audio.wav" \ + -F "model=qwen3-asr-0.6b" \ + -F "response_format=verbose_json" \ + -F "enable_speaker_diarization=true" \ + -F "enable_speaker_identification=true" \ + -F "enable_text_cleanup=true" \ + -F "hotwords=Qwen 2.0 ModelScope 1.5" +``` + +**Supported Response Formats:** `json`, `text`, `srt`, `vtt`, `verbose_json` + +### Alibaba Cloud Compatible API + +| Endpoint | Method | Function | +|----------|--------|----------| +| `/stream/v1/asr` | POST | Speech recognition (long audio support) | +| `/stream/v1/asr/models` | GET | Declared model/capability entries | +| `/stream/v1/asr/health` | GET | Health check | +| `/ws/v1/asr` | WebSocket | Qwen3-ASR streaming | +| `/ws/v1/asr/qwen` | WebSocket | Qwen3-ASR streaming (explicit path) | +| `/ws/v1/asr/funasr` | WebSocket | Removed; returns a deprecation error and asks clients to switch to `/ws/v1/asr/qwen` | + +**Request Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `audio_address` | string | `https://media.cdn.vect.one/podcast_demo.mp4` (docs example) | Audio/video URL, `file://`, or server-local path (optional; ignored when body content is uploaded) | +| `sample_rate` | int | `16000` | Sample rate | +| `enable_speaker_diarization` | bool | `true` | Enable speaker diarization | +| `enable_speaker_identification` | bool | `true` | Match registered speaker database when diarization is enabled | +| `enable_text_cleanup` | bool | `true` | Enable text deduplication, boundary-overlap trimming, and filler cleanup | +| `word_timestamps` | bool | `false` | Return word-level timestamps when the backend supports them. Qwen CUDA vLLM and CPU Rust automatically use the forced aligner when enabled. | +| `vocabulary_id` | string | - | Hotwords (format: `word1 weight1 word2 weight2`) | + +**Usage Examples:** + +```bash +# Basic usage +curl -X POST "http://localhost:8000/stream/v1/asr" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @audio.wav + +# With parameters +curl -X POST "http://localhost:8000/stream/v1/asr?enable_speaker_diarization=true&enable_speaker_identification=true&enable_text_cleanup=true&vocabulary_id=Qwen%202.0%20ModelScope%201.5" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @audio.wav +``` + +### Meeting Offline API + +| Endpoint | Method | Function | +|----------|--------|----------| +| `/api/v1/asr/transcriptions` | POST | Create an offline meeting transcription task | +| `/api/v1/asr/transcriptions/{task_id}` | GET | Query task status and result | + +`audio_address` is the required production input field for this endpoint. + +```json +{ + "audio_address": "https://example.com/media/meeting.mp4", + "config": { + "enable_speaker": true, + "match_speaker_registry": true, + "enable_text_cleanup": true, + "speaker_threshold": 0.6, + "word_timestamps": false, + "hotwords": [ + { "hotword": "Qwen", "weight": 2.0 }, + { "hotword": "ModelScope", "weight": 1.5 } + ] + } +} +``` + +**Response Example:** + +```json +{ + "task_id": "xxx", + "status": 200, + "message": "SUCCESS", + "result": "Speaker1 content...\nSpeaker2 content...", + "duration": 60.5, + "processing_time": 1.234, + "segments": [ + { + "text": "Today is a nice day.", + "start_time": 0.0, + "end_time": 2.5, + "speaker_id": "Speaker1", + "word_tokens": [ + {"text": "Today", "start_time": 0.0, "end_time": 0.5}, + {"text": "is", "start_time": 0.5, "end_time": 0.7}, + {"text": "a nice day", "start_time": 0.7, "end_time": 1.5} + ] + } + ] +} +``` + +## Speaker Diarization + +Multi-speaker automatic identification based on CAM++ model: + +- **Enabled by Default** - `enable_speaker_diarization=true` +- **Automatic Detection** - No preset speaker count needed, model auto-detects +- **Speaker Labels** - Response includes `speaker_id` field (e.g., "Speaker1", "Speaker2") +- **Smart Merging** - Two-layer merge strategy to avoid isolated short segments: + - Layer 1: Accumulate merge same-speaker segments < 10 seconds + - Layer 2: Accumulate merge continuous segments up to 60 seconds +- **Subtitle Support** - SRT/VTT output includes speaker labels `[Speaker1] text content` + +Disable speaker diarization: + +```bash +# OpenAI API +-F "enable_speaker_diarization=false" + +# Alibaba Cloud API +?enable_speaker_diarization=false +``` + +## Audio Processing + +### Intelligent Segmentation Strategy + +Automatic long audio segmentation: + +1. **VAD Voice Detection** - Detect voice boundaries, filter silence +2. **Greedy Merge** - Accumulate voice segments, ensure each segment does not exceed `MAX_SEGMENT_SEC` (default 60s) +3. **Silence Split** - Force split when silence between voice segments exceeds 3 seconds +4. **Batch Inference** - Multi-segment parallel processing, 2-3x performance improvement in GPU mode + +### WebSocket Streaming Limitations + +**Qwen3-ASR Streaming** (using `/ws/v1/asr` or `/ws/v1/asr/qwen`): +- ✅ Multi-language real-time recognition +- ✅ CUDA vLLM and CPU Rust both support the current streaming path +- ❌ Word-level timestamps are not available in the current streaming path + +### Qwen3 Runtime Matrix + +| Runtime | Backend | Offline | WebSocket Streaming | Word Timestamps Offline | Word Timestamps Streaming | Maturity | +|---------|---------|---------|---------------------|-------------------------|---------------------------|----------| +| Linux + NVIDIA GPU | Official vLLM 0.20.0 | ✅ | ✅ | ✅ | ❌ | Production-oriented | +| CPU / macOS | QwenASR Rust | ✅ | ✅ | ✅ (forced aligner) | ❌ | Recommended local fallback | + +## Offline-Capable Models + +| Model ID | Name | Description | Features | +|----------|------|-------------|----------| +| `qwen3-asr-1.7b` | Qwen3-ASR 1.7B | High-performance multilingual ASR, 52 languages + dialects; CUDA uses vLLM | Offline/Realtime | +| `qwen3-asr-0.6b` | Qwen3-ASR 0.6B | Lightweight multilingual ASR; CUDA uses vLLM, CPU/macOS uses Rust backend | Offline/Realtime | + +**Runtime selection:** +- **VRAM >= 32GB**: Select `qwen3-asr-1.7b` +- **VRAM < 32GB**: Select `qwen3-asr-0.6b` +- **No CUDA**: Select the vendored Rust-backed `qwen3-asr-0.6b` +- **macOS / Apple Silicon**: Always default to `qwen3-asr-0.6b`, regardless of memory size +- **Environment override**: Set `QWEN3_ASR_MODEL=qwen3-asr-1.7b` or `QWEN3_ASR_MODEL=qwen3-asr-0.6b` to bypass automatic selection + +At startup the service checks the current runtime model plan and downloads missing models from ModelScope by default. + +## Environment Variables + +Recommended public settings: + +| Variable | Default | Description | +|----------|---------|-------------| +| `API_KEY` | - | API authentication key (optional, unauthenticated if not set) | +| `LOG_LEVEL` | `INFO` | Log level (DEBUG/INFO/WARNING/ERROR) | +| `MAX_AUDIO_SIZE` | `2048` | Max audio file size (MB, supports units like 2GB) | +| `ASR_BATCH_SIZE` | `4` | ASR batch size for long-audio segment processing | +| `MAX_SEGMENT_SEC` | `60` | Max audio segment duration (seconds) | +| `ASR_ENABLE_NEARFIELD_FILTER` | `true` | Enable far-field sound filtering | +| `QWEN3_ASR_MODEL` | auto | Force `qwen3-asr-1.7b` or `qwen3-asr-0.6b` instead of VRAM-based selection | +| `QWEN_GPU_MEMORY_UTILIZATION` | `0.9` | Upper bound for vLLM GPU memory reservation; lower it on shared GPUs, raise it when KV cache is too small | +| `QWEN_VLLM_ENFORCE_EAGER` | `true` | Force vLLM eager execution for compatibility; set `false` to allow CUDA Graph optimization on supported NVIDIA deployments | + +Far-field filter notes: + +- `ASR_NEARFIELD_RMS_THRESHOLD=0.01` is the current default and recommended starting point +- raise it in noisy rooms to filter more background speech +- lower it in quiet rooms if soft speech is being dropped +- use `LOG_LEVEL=DEBUG` temporarily when you need to inspect filter behavior + +Advanced backend-specific settings: + +| Variable | Default | Description | +|----------|---------|-------------| +| `QWEN_RUST_CPU_WORKERS` | `4` | CPU Rust backend worker count (Rust ASR / forced align default to 4 runtimes) | +| `QWENASR_LIBRARY_PATH` | auto-detect | Override vendored Rust dylib/so path | + +## Resource Requirements + +**Minimum (CPU):** + +- CPU: 4 cores +- Memory: 16GB +- Disk: 20GB + +**Recommended (GPU):** + +- CPU: 4 cores +- Memory: 16GB +- GPU: NVIDIA GPU (16GB+ VRAM) +- Disk: 20GB + +## API Documentation + +After starting the service: + +- Swagger UI: `http://localhost:8000/docs` +- ReDoc: `http://localhost:8000/redoc` + +## Links + +- **Deployment Guide**: [Detailed Docs](./docs/deployment.md) +- **Qwen3-ASR**: [Qwen3-ASR GitHub](https://github.com/QwenLM/Qwen3-ASR) +- **FunASR**: [FunASR GitHub](https://github.com/alibaba-damo-academy/FunASR) +- **Chinese README**: [中文文档](./docs/README_zh.md) + +## License + +This project uses the MIT License - see [LICENSE](LICENSE) file for details. + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=Quantatirsk/qwen3-asr&type=Date)](https://star-history.com/#Quantatirsk/qwen3-asr&Date) + +## Contributing + +Issues and Pull Requests are welcome to improve the project! diff --git a/REALTIME_ASR_TROUBLESHOOTING.md b/REALTIME_ASR_TROUBLESHOOTING.md new file mode 100644 index 0000000..7bb3391 --- /dev/null +++ b/REALTIME_ASR_TROUBLESHOOTING.md @@ -0,0 +1,310 @@ +# 实时 ASR 当前问题排查记录 + +更新时间:2026-09-07 +排查范围:实时 ASR、原生流式 partial、说话人识别、声纹姓名匹配、WebSocket 输出和前端展示。 + +## 1. 当前结论 + +目前发现的问题分布在三个层次: + +```text +识别层 原生 partial 默认关闭,非原生路径会反复重识别窗口 +说话人层 短片段无条件继承上一位实名,并可能把复制的 embedding 写回记录池 +输出层 内部物理切段直接作为前端展示单元,导致同一说话人被拆成多行 +``` + +其中,“不同的人进入已经确定的说话人气泡”最明确的根因是说话人层的短段快路径:小于 1.6 秒的片段在提取新特征之前直接继承上一位已命名说话人。WebSocket 本身负责传递这些结果,但错误身份是在上游状态机中产生的。 + +“同一说话人被切成几行”主要是输出层问题。silence 和 max duration 可以结束一次内部识别片段,但不应直接决定前端展示换行。 + +## 2. 当前实时 WebSocket 链路 + +入口位于 [app/api/v1/websocket_asr.py](app/api/v1/websocket_asr.py)。`/ws/v1/asr` 和 `/ws/v1/asr/qwen` 最终都进入 [Qwen3ASRService.handle_connection](app/services/qwen3_websocket_asr.py#L2538)。 + +主流程如下: + +```text +客户端 start + ↓ +初始化 ConnectionContext 和 streaming state + ↓ +接收二进制音频 + ↓ +RMS/peak 判断是否有声音,维护 pre-roll 和当前 turn + ↓ +partial 解码并向前端发送 sentence_type=0 + ↓ +silence / max_duration / sentence_limit / stop 触发内部提交 + ↓ +最终重识别,创建 confirmed segment + ↓ +异步 speaker worker 处理说话人 + ↓ +通过相同 sentence_id 发送 speaker update + ↓ +stop 时发送最终 sentences 和 end +``` + +图谱分析显示 `handle_connection` 是当前实时链路的关键汇聚点;说话人解析又连接到 `RealtimeSpeakerClusterer.resolve_segment_speaker`、声纹注册服务和历史记录重聚类。因此直接修改主服务的影响范围较大,本次验证优先放在独立 demo 中。 + +## 3. 问题一:内部切段直接变成前端展示行 + +### 3.1 当前切段触发器 + +| 触发器 | 当前条件 | 当前作用 | 对展示的实际影响 | +|---|---|---|---| +| `silence` | 连续静音默认 `800ms` | 主力结束当前 turn | 直接产生一个 confirmed segment | +| `max_duration` | 缓冲默认最多 `12s` | 防止单段无限增长 | 长讲话被硬切成多个 segment | +| `sentence_limit` | 段内达到默认 `8` 句 | 兜底限制 | 可能提前提交 | +| `complete_sentence` | 标点结尾、时长达到阈值且字数足够 | 长独讲兜底 | 正常短句通常到不了该条件 | +| `final` | 客户端发送 `stop` | 提交最后一段 | 输出最终结果 | + +相关逻辑位于 [qwen3_websocket_asr.py](app/services/qwen3_websocket_asr.py#L2538)、[_should_commit_complete_sentence](app/services/qwen3_websocket_asr.py#L1100) 和 [_commit_retranscribe_turn](app/services/qwen3_websocket_asr.py#L2333)。 + +### 3.2 当前输出方式 + +每次内部提交都会: + +1. 创建一条 `confirmed_segments` 记录; +2. 使用该记录的 `index` 作为 `sentence_id`; +3. 立即发送 `sentence_type=1` 的 `sentences` 事件; +4. 把所有记录用换行连接成 `full_text`。 + +代码中 `full_text` 使用 `"\\n".join(...)`,位置在 [_commit_retranscribe_turn](app/services/qwen3_websocket_asr.py#L2464) 和 [_stop](app/services/qwen3_websocket_asr.py#L2916)。因此这里的 `sentence_id` 实际上是“物理切段编号”,不一定是语义完整句子编号。 + +项目协议文档明确区分了 `sentence_type=0` 的 partial 和 `sentence_type=1` 的 final,但没有把“内部 segment”和“前端 display block”分开,见 [realtime_meeting_websocket.md](docs/realtime_meeting_websocket.md#L162)。 + +前端可以根据相同 `sentence_id` 更新原记录,但不会把不同 `sentence_id` 且说话人相同的记录合并。因此停顿、12 秒硬切和前端换行目前形成了直接关系。 + +另外,图谱显示 [_should_force_stable_segment](app/services/qwen3_websocket_asr.py#L2051) 当前没有调用方,它不能实际改变实时切段;`complete_sentence` 主要是长段兜底。 + +## 4. 问题二:原生流式 partial 与输出合并不是同一层 + +当前配置中 `REALTIME_STREAM_CHUNK_SEC` 默认约为 `1.2s`,`REALTIME_PARTIAL_WINDOW_SEC` 默认约为 `8s`,原生 partial 开关默认关闭,相关默认值在 [app/core/config.py](app/core/config.py#L103)。 + +非原生路径会维护窗口并调用 `_transcribe_audio_text` 重识别;原生路径则通过: + +```text +create_stream → push_stream/feed_stream → finish_stream +``` + +对应 [qwen3_engine.py](app/services/asr/qwen3_engine.py#L570) 和 [qwen3_websocket_asr.py](app/services/qwen3_websocket_asr.py#L1162)。 + +原生 partial 影响的是: + +- partial 首次出现的延迟; +- 每次增量识别的计算量; +- 流式状态维护方式; +- 文本回滚和未固定 token 的处理。 + +它不应决定: + +- 是否因为停顿产生前端新行; +- 哪些物理 segment 合并成一个说话人气泡; +- 是否把某个 speaker 姓名写入展示结果。 + +所以两个优化需要同时做,但必须保持两个独立状态:ASR streaming state 和 display aggregation state。 + +## 5. 问题三:短音频无条件继承实名 + +### 5.1 已确认的代码路径 + +在 [realtime_speaker_clusterer.py](app/services/realtime_speaker_clusterer.py#L27) 中,`_FAST_ATTACH_MAX_SEC = 1.6`。 + +`resolve_segment_speaker` 的入口逻辑是: + +```text +duration_sec < 1.6s +且 speaker_records 非空 +且上一条记录存在实名身份 + ↓ +直接复制上一条记录的 speaker_id / speaker_name / user_id / registry_speaker_id +speaker_confidence = 0.0 +speaker_strategy = "short_attach" +``` + +该分支在 [realtime_speaker_clusterer.py](app/services/realtime_speaker_clusterer.py#L505),早于 `extract_chunk_embeddings`。因此短段没有经过新的声纹特征验证。 + +### 5.2 为什么实名会直接通过稳定性判断 + +[_is_stable_speaker_info](app/services/qwen3_websocket_asr.py#L613) 首先检查 `registry_speaker_id` 或 `user_id`。短段继承时这两个字段被完整复制,所以即使 `speaker_confidence=0.0`,仍会被判断为 stable。 + +随后 [_resolve_segment_speaker](app/services/qwen3_websocket_asr.py#L2156) 会把该身份写入最终 segment,并由 [_emit_speaker_update](app/services/qwen3_websocket_asr.py#L498) 通过相同 `sentence_id` 推送给客户端。 + +这解释了为什么匿名 `SpeakerNN` 的继承可能被拦截,而带 `user_id` 或注册声纹 ID 的实名继承可以直接进入前端。 + +### 5.3 为什么短段是高风险场景 + +“嗯”“对”“好的”“可以”等短插话通常不到 1.6 秒,恰好是多人会议中最容易发生换人的场景。当前快路径把上一位实名当作新片段身份,产生的结果就是:新说话人的短句进入上一位实名气泡。 + +## 6. 问题四:特征提取失败分支与实际路径 + +代码中确实存在 `embedding_attach` 分支:特征提取后如果 `current_chunks` 为空,则尝试继承上一位实名,位置在 [realtime_speaker_clusterer.py](app/services/realtime_speaker_clusterer.py#L521)。 + +但当前 `extract_chunk_embeddings` 在没有 chunk 时会回退为整段单 chunk,因此正常情况下很难返回空列表;如果模型真正抛异常,异常会向上传递,最终由 [_resolve_and_emit_segment_speaker](app/services/qwen3_websocket_asr.py#L2220) 捕获,当前片段保持 pending。 + +准确结论是: + +- 模型抛异常:当前片段通常保持 `speaker_id=-1`,不会走继承; +- chunk 为空:代码意图是继承实名,但该分支近乎不可达; +- 模型不抛异常但产生垃圾 embedding:仍需单独检查零向量、NaN 和相似度边界; +- 当前最确定、最直接的错误来源是 `<1.6s` 的 `short_attach`。 + +## 7. 问题五:继承 embedding 造成污染链 + +短段继承返回时还会复制上一条记录的 `_embedding`、`_chunk_embeddings` 和 `_chunks`。下游 [_record_segment_speaker](app/services/qwen3_websocket_asr.py#L644) 只要发现 `_embedding` 非空,就会把它作为正常 speaker record 保存。 + +污染链如下: + +```text +上一位实名离场 + ↓ +新人的短段直接复制实名和 embedding + ↓ +复制结果成为新的 last_record + ↓ +后续短段继续继承这条记录 + ↓ +历史匹配池重复出现同一个 embedding + ↓ +相似度、聚类中心和重聚类结果被污染 +``` + +影响包括: + +1. 继承链持续延长,错误实名被不断续写; +2. 历史 embedding 被重复计数,匹配置信度可能虚高; +3. `cluster_records_with_ranges` 会把复制的 chunk 作为真实样本参与聚类; +4. 后续重聚类可能把错误身份回写到更多 segment。 + +## 8. 问题六:speaker update 是异步的 + +实时最终段先写入 `confirmed_segments`,speaker worker 再异步处理,处理完成后通过相同 `sentence_id` 发送更新,相关位置是 [_speaker_worker_loop](app/services/qwen3_websocket_asr.py#L442) 和 [_emit_speaker_update](app/services/qwen3_websocket_asr.py#L498)。 + +当前协议允许首次 final 没有姓名,后续再补姓名,这一点在 [realtime_meeting_websocket.md](docs/realtime_meeting_websocket.md#L240) 有说明。 + +这里有两个风险: + +- 前端如果把每次事件当成追加消息,会出现重复行; +- 上游异步继承结果如果带着错误实名,前端的幂等更新会把错误身份稳定显示出来。 + +因此上层 WebSocket 需要维护 segment 状态表,按 `sentence_id` 覆盖更新,然后根据完整状态重新生成 display block 快照。 + +## 9. 需要同时实施的两层优化 + +### 9.1 识别层:原生流式 partial + +目标是让同一轮语音持续使用一个 streaming state,通过增量音频推进识别,减少窗口重复重识别。 + +demo 应透传并记录: + +- `enable_native_partial_stream`; +- partial 产生时间; +- 每次 partial 的文本长度和修订次数; +- 首次 partial 延迟; +- final 延迟; +- 原服务返回的 chunk 或 segment 时间范围。 + +### 9.2 说话人与输出层 + +目标是把“身份确认”和“展示合并”分开: + +1. `<1.6s` 片段不继承上一位实名; +2. 没有新鲜声纹证据时保持 pending; +3. 特征提取异常时保持 pending; +4. 不复制上一条记录的 embedding; +5. 只有达到确认阈值的独立特征才能更新身份缓存; +6. 同一 `sentence_id` 的更新覆盖原片段; +7. 相邻且身份可信度一致的物理 segment 才合并为 display block; +8. A→B→A 保留时间顺序,不把非相邻发言重新拼接到一起; +9. 前端展示使用 display block,入库和诊断仍保留 raw segment。 + +推荐的状态关系是: + +```text +raw segment + ├─ ASR text state:partial / final + ├─ speaker evidence:pending / fresh / confirmed + ├─ speaker identity:cluster / registry / user + └─ display block:按时间和可信身份重新生成 +``` + +## 10. 服务器接口边界 + +已验证服务器 `10.100.53.199:8000` 可访问,原实时 WebSocket 地址为: + +```text +ws://10.100.53.199:8000/ws/v1/asr/qwen +``` + +当前公开接口包括: + +- `/ws/v1/asr/qwen`:原实时 WebSocket,已经包含主服务内部的 ASR、切段和 speaker 逻辑; +- `/v1/audio/transcriptions`:整段音频转写; +- `/api/v1/speakers/identify`:通过文件来源识别注册说话人; +- `/api/v1/speakers`:声纹注册和人员管理。 + +当前公开接口没有返回实时聚类所需的原始 embedding,也没有把 [Qwen3ASREngine](app/services/asr/qwen3_engine.py#L570) 的 `create_stream`、`feed_stream`、`finish_stream` 暴露为独立远程模型 RPC。 + +所以存在一个边界: + +- 上层 demo WebSocket 可以独立重写事件顺序、切段提交策略、pending 保护和展示合并; +- 如果要在 demo 中完整重建原项目的声纹聚类,服务器还需要提供 embedding 接口或模型 RPC; +- 只依赖当前 `/ws/v1/asr/qwen` 返回字段,无法重新计算已被原服务错误归类的长段身份。 + +## 11. 当前独立 demo 状态 + +独立验证项目位于 [realtime_asr_optimization_demo](realtime_asr_optimization_demo)。 + +当前结构: + +```text +浏览器 + ↓ ws://127.0.0.1:8082/ws +demo 上层 WebSocket + ↓ model_service.py 远程模型服务适配层 + ↓ ws://10.100.53.199:8000/ws/v1/asr/qwen +已部署模型服务 +``` + +当前 demo 已具备: + +- 原生 partial 参数透传; +- 同一 `sentence_id` 覆盖更新; +- raw event 与 display block 分离; +- 同一说话人的相邻片段合并; +- `<1.6s` 实名结果降级为 pending; +- 已命名身份不接受只有弱 cluster id 的片段加入; +- embedding 字段不进入 demo 状态池。 +- 模型服务与 WebSocket 解耦;demo 不启动或加载模型; +- 模型服务地址可通过页面或 `--model-service-url` 配置。 + +对应实现见 [server.py](realtime_asr_optimization_demo/server.py)、[model_service.py](realtime_asr_optimization_demo/model_service.py)、[speaker_assembler.py](realtime_asr_optimization_demo/speaker_assembler.py) 和 [static/app.js](realtime_asr_optimization_demo/static/app.js)。 + +这个 demo 当前主要验证上层协议、状态覆盖和展示保护。后续把模型服务部署到新服务器时,只需替换模型服务地址;若新模型服务协议不同,则替换 `model_service.py`,不改变 WebSocket 编排层。若要验证“demo 自己完成声纹特征提取、聚类和按需姓名匹配”,仍需服务器提供远程 embedding/model RPC。 + +## 12. 建议验证用例 + +| 用例 | 关注结果 | +|---|---| +| 单人连续讲话,中间停顿 800ms 以上 | 前端仍属于同一个 display block | +| A 讲话后,B 说“嗯/好的” | B 的短段保持 pending,不进入 A 的实名块 | +| A→B→A | 保持三个时间顺序块 | +| 同一 `sentence_id` 先 partial 后 final | 文本覆盖,不重复追加 | +| final 先返回,speaker update 后返回 | 原 block 更新并重新归并 | +| 声纹模型异常 | 片段保持 pending,不继承上一位实名 | +| 连续多个短插话 | 不复制上一条 embedding,不形成继承链 | +| 关闭 native partial | 只影响识别延迟和资源,不改变展示合并规则 | +| 关闭 display merge | 可以看到原始物理 segment,用于对照 | + +## 13. 当前未确认项 + +以下问题不能仅靠现有公开 WebSocket 字段确认: + +- 原服务是否存在未写入 OpenAPI 的内部 embedding/model RPC; +- 实际使用的 realtime speaker 模型是否会输出零向量或 NaN; +- 不同真实说话人被分配相同稳定 cluster id 的比例; +- speaker worker 完成时间与 `end` 事件之间是否存在竞态; +- 远程 Docker 是否还映射了独立的模型后端端口。 + +这些项目需要通过服务器日志、embedding 接口或带原始音频的端到端录音继续验证。 diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..07b4988 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +"""Qwen3-ASR application package.""" + +__version__ = "1.0.1" +__author__ = "Nexa Team" +__description__ = "Qwen3-ASR speech recognition API service" diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..64bce62 --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +""" +API路由模块 +包含所有API端点的路由定义 +""" diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py new file mode 100644 index 0000000..6adc06e --- /dev/null +++ b/app/api/v1/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +"""API v1版本路由""" + +from fastapi import APIRouter +from .asr import router as asr_router +from .websocket_asr import router as websocket_asr_router +from .openai_compatible import router as openai_router +from .meeting import router as meeting_router + +api_router = APIRouter() + +# 原有 API (阿里云兼容) +api_router.include_router(asr_router) + +# WebSocket ASR 端点(包含阿里云协议和 Qwen3 流式协议) +api_router.include_router(websocket_asr_router) + +# OpenAI 兼容 API +api_router.include_router(openai_router) + +# 独立会议离线/声纹管理 API(Model-Test-New 兼容形状) +api_router.include_router(meeting_router) diff --git a/app/api/v1/asr.py b/app/api/v1/asr.py new file mode 100644 index 0000000..ee67a69 --- /dev/null +++ b/app/api/v1/asr.py @@ -0,0 +1,481 @@ +# -*- coding: utf-8 -*- +""" +ASR API路由 +""" + +from fastapi import ( + APIRouter, + Request, + HTTPException, + Depends +) +from fastapi.responses import JSONResponse +from typing import Annotated, Optional +import time +import logging + +from ...core.config import settings +from ...core.exceptions import ( + AuthenticationException, + InvalidParameterException, + InvalidMessageException, + UnsupportedSampleRateException, + DefaultServerErrorException, + get_http_status_code, +) +from ...core.security import validate_token +from ...models.common import SampleRate +from ...models.asr import ( + ASRResponse, + ASRHealthCheckResponse, + ASRModelsResponse, + ASRSuccessResponse, + ASRErrorResponse, + ASRQueryParams, +) +from ...utils.common import generate_task_id +from ...services.asr.manager import get_model_manager +from ...services.asr.model_selection import validate_offline_model_id +from ...services.asr.runtime import get_runtime_router +from ...services.asr.audio_validation import validate_sample_rate +from ...services.asr.offline_transcription_service import ( + OfflineTranscriptionOptions, + PreparedAudio, + get_offline_transcription_service, +) + +# 配置日志 +logger = logging.getLogger(__name__) + +# 创建路由器 +router = APIRouter(prefix="/stream/v1", tags=["ASR"]) + + +def _build_asr_openapi_parameters() -> list[dict]: + parameters: list[dict] = [ + { + "name": "model", + "in": "query", + "required": False, + "schema": { + "type": "string", + "maxLength": 128, + "example": "qwen3-asr-0.6b", + }, + "description": "可选。离线 ASR 模型 ID;不传则使用服务当前默认模型", + }, + { + "name": "audio_address", + "in": "query", + "required": False, + "schema": { + "type": "string", + "maxLength": 512, + "example": "https://media.cdn.vect.one/podcast_demo.mp4", + }, + "description": "音频/视频文件地址,支持 HTTP/HTTPS URL、file:// 或服务端本地路径。仅当请求体为空时使用;若同时上传请求体,服务会忽略此参数", + }, + { + "name": "sample_rate", + "in": "query", + "required": False, + "schema": { + "type": "integer", + "enum": [8000, 16000, 22050, 24000, 32000, 44100, 48000], + "default": 16000, + "example": 16000, + }, + "description": "音频采样率(Hz)。音频会在服务端自动转换,通常保持默认值即可", + }, + { + "name": "enable_speaker_diarization", + "in": "query", + "required": False, + "schema": { + "type": "boolean", + "default": True, + "example": True, + }, + "description": "是否启用说话人分离。启用后响应会包含 speaker_id 字段", + }, + { + "name": "enable_speaker_identification", + "in": "query", + "required": False, + "schema": { + "type": "boolean", + "default": True, + "example": True, + }, + "description": "是否匹配已注册声纹库。仅在 enable_speaker_diarization=true 时生效,命中后响应会包含 speaker_name/user_id", + }, + { + "name": "enable_text_cleanup", + "in": "query", + "required": False, + "schema": { + "type": "boolean", + "default": True, + "example": True, + }, + "description": "是否启用文本去重、跨段重叠裁剪和口头语清理", + }, + { + "name": "vocabulary_id", + "in": "query", + "required": False, + "schema": { + "type": "string", + "maxLength": 512, + "example": "阿里巴巴 20 腾讯 15", + }, + "description": "热词字符串,格式:`热词1 权重1 热词2 权重2`。权重范围 1-100,建议 10-30。可提升特定词汇的识别准确率", + }, + { + "name": "X-NLS-Token", + "in": "header", + "required": False, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "example": "", + }, + "description": "访问令牌,用于身份认证。未配置 API_KEY 环境变量时可忽略", + }, + ] + if settings.ASR_ENABLE_WORD_TIMESTAMPS: + parameters.insert( + 5, + { + "name": "word_timestamps", + "in": "query", + "required": False, + "schema": { + "type": "boolean", + "default": False, + "example": False, + }, + "description": "是否返回字词级时间戳(默认关闭;启用时会自动调用 forced aligner)", + }, + ) + return parameters + + +async def get_asr_params(request: Request) -> ASRQueryParams: + """从请求中提取并验证ASR参数""" + # 从URL查询参数中获取 + query_params = dict(request.query_params) + + # 使用统一的验证器验证参数 + try: + # 验证采样率(转换为整数) + if "sample_rate" in query_params and query_params["sample_rate"]: + try: + sample_rate = int(query_params["sample_rate"]) # type: ignore + validated_rate = validate_sample_rate(sample_rate) + query_params["sample_rate"] = str(validated_rate) # type: ignore + except ValueError: + raise InvalidParameterException( + f"采样率必须是整数,收到: {query_params['sample_rate']}" + ) + + # 创建ASRQueryParams实例,Pydantic会自动验证和设置默认值 + return ASRQueryParams.model_validate(query_params) + except InvalidParameterException: + raise + except Exception as e: + raise InvalidParameterException(f"请求参数错误: {str(e)}") + + +@router.post( + "/asr", + response_model=ASRResponse, + responses={ + 200: { + "description": "识别成功", + "model": ASRSuccessResponse, + }, + 400: { + "description": "请求参数错误", + "model": ASRErrorResponse, + }, + 401: {"description": "认证失败", "model": ASRErrorResponse}, + 500: {"description": "服务器内部错误", "model": ASRErrorResponse}, + }, + summary="语音识别(支持长音频)", + description=""" +将音频文件转写为文本,兼容阿里云语音识别 RESTful API。 + +## 功能特性 +- 支持多种音频格式与常见含音轨视频容器:WAV, MP3, M4A, FLAC, OGG, AAC, AMR, PCM, WEBM, MP4, MOV, MKV, AVI 等 +- 自动音频格式检测和转换 +- 支持长音频自动分段识别(返回带时间戳的分段结果) +- 最大文件大小:{settings.MAX_AUDIO_SIZE // (1024 * 1024)}MB(可通过环境变量 MAX_AUDIO_SIZE 配置) + +## 音频输入方式 +1. **请求体上传**:将音频/视频二进制数据作为请求体发送 +2. **URL/本地路径读取**:通过 `audio_address` 参数指定音频/视频文件 URL(HTTP/HTTPS)或服务端本地路径 + +如果请求体和 `audio_address` 同时存在,服务会优先使用请求体,并忽略 `audio_address`。 + +## 注意事项 +- 默认使用服务当前启用的 Qwen3-ASR 模型;也可通过可选 `model` 参数指定当前可用离线模型 +- `vocabulary_id` 参数用于传递热词,格式:`热词1 权重1 热词2 权重2`(如:`阿里巴巴 20 腾讯 15`) +- `enable_speaker_identification` 仅在 `enable_speaker_diarization=true` 时生效,用于匹配已注册声纹库 +- `enable_text_cleanup` 控制识别后的文本去重、跨段重叠裁剪和口头语清理 +- 音频会自动转换为 16kHz 采样率进行识别 +""", + openapi_extra={ + "parameters": _build_asr_openapi_parameters(), + "requestBody": { + "description": "音频/视频文件二进制数据。支持格式:WAV, MP3, M4A, FLAC, OGG, AAC, AMR, PCM, WEBM, MP4, MOV, MKV, AVI 等。若同时提供 audio_address,服务会优先使用这里上传的内容", + "content": { + "application/octet-stream": { + "schema": {"type": "string", "format": "binary"} + } + }, + "required": False, + }, + }, +) +async def asr_transcribe( + request: Request, params: Annotated[ASRQueryParams, Depends(get_asr_params)] +) -> JSONResponse: + """语音识别API端点""" + task_id = generate_task_id() + prepared_audio: Optional[PreparedAudio] = None + + # 性能计时 + request_start_time = time.time() + + # 记录请求开始(此时文件已上传完成) + content_length = request.headers.get("content-length", "unknown") + logger.info(f"[{task_id}] 收到ASR请求, content_length={content_length}") + + transcription_service = get_offline_transcription_service() + + try: + # 验证请求头部(鉴权) + result, content = validate_token(request, task_id) + if not result: + raise AuthenticationException(content, task_id) + + model_id = validate_offline_model_id(params.model) + + # 使用音频服务处理音频 + target_sample_rate = int(params.sample_rate) if params.sample_rate else 16000 + prepared_audio = await transcription_service.prepare_from_request( + request=request, + audio_address=params.audio_address, + task_id=task_id, + sample_rate=target_sample_rate, + ) + + logger.info(f"[{task_id}] 开始调用 transcribe_long_audio (enable_speaker_diarization={params.enable_speaker_diarization})...") + asr_result = await transcription_service.transcribe( + prepared_audio, + OfflineTranscriptionOptions( + model_id=model_id, + sample_rate=int(params.sample_rate or SampleRate.RATE_16000), + hotwords=params.vocabulary_id or "", + enable_speaker_diarization=params.enable_speaker_diarization is not False, + enable_speaker_identification=( + params.enable_speaker_diarization is not False + and params.enable_speaker_identification is not False + ), + enable_text_cleanup=params.enable_text_cleanup is not False, + word_timestamps=( + settings.ASR_ENABLE_WORD_TIMESTAMPS + and params.word_timestamps is True + ), + task_id=task_id, + ), + ) + + logger.info(f"[{task_id}] 识别完成,共 {len(asr_result.segments)} 个分段,总字符: {len(asr_result.text)}") + + # 构建分段结果(始终返回 segments,短音频也是 1 个 segment) + segments_data = [] + for seg in asr_result.segments: + seg_dict = { + "text": seg.text, + "start_time": round(seg.start_time, 2), + "end_time": round(seg.end_time, 2), + } + if seg.speaker_id: + seg_dict["speaker_id"] = seg.speaker_id + if seg.speaker_name: + seg_dict["speaker_name"] = seg.speaker_name + if seg.user_id: + seg_dict["user_id"] = seg.user_id + # 添加字词级时间戳(如果存在) + if seg.word_tokens: + seg_dict["word_tokens"] = [ + { + "text": wt.text, + "start_time": round(wt.start_time, 3), + "end_time": round(wt.end_time, 3), + } + for wt in seg.word_tokens + ] + segments_data.append(seg_dict) + + # 计算请求处理时间 + request_duration = time.time() - request_start_time + + # 返回成功响应(统一数据结构) + response_data = { + "task_id": task_id, + "result": asr_result.text, + "status": 200, + "message": "SUCCESS", + "segments": segments_data, + "duration": round(asr_result.duration, 2), + "processing_time": round(request_duration, 3), + } + + return JSONResponse(content=response_data, headers={"task_id": task_id}) + + except ( + AuthenticationException, + InvalidParameterException, + InvalidMessageException, + UnsupportedSampleRateException, + DefaultServerErrorException, + ) as e: + e.task_id = task_id + logger.error(f"[{task_id}] ASR异常: {e.message}") + + # 使用标准错误格式 + response_data = e.to_dict() + return JSONResponse( + content=response_data, + headers={"task_id": task_id}, + status_code=get_http_status_code(e.status_code), + ) + + except Exception as e: + logger.error(f"[{task_id}] 未知异常: {str(e)}") + + # 使用标准错误格式 + from ...core.exceptions import create_error_response + response_data = create_error_response( + error_code="DEFAULT_SERVER_ERROR", + message=f"内部服务错误: {str(e)}", + task_id=task_id, + ) + return JSONResponse(content=response_data, headers={"task_id": task_id}) + + finally: + transcription_service.cleanup(prepared_audio) + + +@router.get( + "/asr/health", + response_model=ASRHealthCheckResponse, + summary="ASR 服务健康检查", + description=""" +检查语音识别服务的运行状态和资源使用情况。 + +## 返回信息 +- **status**: 服务状态(healthy/unhealthy/error) +- **model_loaded**: 默认模型是否已加载 +- **device**: 当前推理设备(cuda:0/cpu) +- **loaded_models**: 已加载的模型列表 +- **memory_usage**: GPU 显存使用情况(仅 GPU 模式) +""", +) +async def health_check(request: Request): + """ASR服务健康检查端点""" + # 鉴权 + result, content = validate_token(request) + if not result: + raise AuthenticationException(content, "health_check") + + try: + # 尝试获取默认模型的引擎 + try: + runtime_router = get_runtime_router() + default_model = runtime_router.resolve_model_id(None) + async with await runtime_router.acquire_engine(default_model) as engine: + model_loaded = True + device = engine.device + except Exception: + model_loaded = False + device = "unknown" + + runtime_router = get_runtime_router() + memory_info = runtime_router.get_memory_usage() + loaded_models = runtime_router.get_loaded_model_ids() + accelerator_info = memory_info.get("accelerator") + + return { + "status": "healthy" if model_loaded else "unhealthy", + "model_loaded": model_loaded, + "device": device, + "version": settings.APP_VERSION, + "message": ( + "ASR service is running normally" + if model_loaded + else "ASR model not loaded" + ), + "loaded_models": loaded_models, + "memory_usage": memory_info.get("gpu_memory"), + "accelerator": accelerator_info, + } + except Exception as e: + return { + "status": "error", + "model_loaded": False, + "device": "unknown", + "version": settings.APP_VERSION, + "message": str(e), + "accelerator": None, + } + +@router.get( + "/asr/models", + response_model=ASRModelsResponse, + summary="获取声明条目列表", + description=""" +返回系统声明的离线模型与 realtime capability 信息。 + +## 条目说明 + +| ID | 类型 | 说明 | +|----|------|------| +| qwen3-asr-1.7b | model | 离线/实时共用的 Qwen3-ASR 模型条目 | +| qwen3-asr-0.6b | model | 轻量版 Qwen3-ASR 模型条目 | +## 返回信息 +- **declared_entries**: 声明的模型与 capability 列表 +- **declared_count**: 声明项总数 +- **runtime**: 运行时加载状态 +""", +) +async def list_models(request: Request): + """获取声明条目列表端点""" + # 鉴权 + result, content = validate_token(request) + if not result: + raise AuthenticationException(content, "list_models") + + try: + + model_manager = get_model_manager() + runtime_router = get_runtime_router() + loaded_model_ids = runtime_router.get_loaded_model_ids() + entries = model_manager.list_declared_entries() + + return { + "declared_entries": entries, + "declared_count": len(entries), + "runtime": { + "loaded_model_ids": loaded_model_ids, + "loaded_count": len(loaded_model_ids), + "default_offline_model_id": runtime_router.resolve_model_id(None), + }, + } + except Exception as e: + logger.error(f"获取模型列表时发生错误: {str(e)}") + raise HTTPException(status_code=500, detail=f"获取模型列表失败: {str(e)}") diff --git a/app/api/v1/meeting.py b/app/api/v1/meeting.py new file mode 100644 index 0000000..1ec1326 --- /dev/null +++ b/app/api/v1/meeting.py @@ -0,0 +1,855 @@ +# -*- coding: utf-8 -*- +"""Independent meeting-style offline API compatible with Model-Test-New shapes.""" + +from __future__ import annotations + +import asyncio +import math +import time +import uuid +from pathlib import Path +from typing import Any, Optional +from urllib.parse import unquote, urlparse + +import requests +from fastapi import APIRouter, BackgroundTasks, Body, File, HTTPException, UploadFile +from pydantic import BaseModel, ConfigDict, Field + +from app.core.config import settings +from app.core.database import pg_speaker_db +from app.core.exceptions import InvalidParameterException +from app.core.task_store import create_task_record, get_task_record, update_task_record +from app.models.common import SampleRate +from app.services.asr.model_selection import validate_offline_model_id +from app.services.asr.offline_transcription_service import ( + OfflineTranscriptionOptions, + PreparedAudio, + get_offline_transcription_service, +) +from app.services.speaker_registry import get_speaker_registry_service + +router = APIRouter(prefix=settings.API_PREFIX) + +_UPLOAD_DIR = Path(settings.TEMP_DIR) / "api_uploads" +_UPLOAD_DIR.mkdir(parents=True, exist_ok=True) +_uploaded_files: dict[str, dict[str, str]] = {} + + +def _recognition_request_config_example() -> dict[str, Any]: + example: dict[str, Any] = { + "enable_speaker": True, + "match_speaker_registry": True, + "enable_text_cleanup": True, + "speaker_threshold": 0.6, + "hotwords": [ + {"hotword": "阿里巴巴", "weight": 2.0}, + {"hotword": "腾讯", "weight": 1.5}, + ], + } + if settings.ASR_ENABLE_WORD_TIMESTAMPS: + example["word_timestamps"] = False + return example + + +def _meeting_request_examples() -> dict[str, Any]: + return { + "url_with_hotwords": { + "summary": "URL + 结构化热词", + "description": "推荐的生产调用格式。", + "value": { + "model": "qwen3-asr-0.6b", + "audio_address": "https://example.com/media/meeting.mp4", + "config": _recognition_request_config_example(), + }, + }, + "local_path": { + "summary": "服务端本地路径", + "value": { + "audio_address": "/data/audio/meeting.m4a", + "config": { + "enable_speaker": True, + "match_speaker_registry": False, + "enable_text_cleanup": True, + }, + }, + }, + } + + +def _recognition_request_config_schema_extra(schema: dict[str, Any], _model: Any) -> None: + schema["example"] = _recognition_request_config_example() + if settings.ASR_ENABLE_WORD_TIMESTAMPS: + return + properties = schema.get("properties") + if isinstance(properties, dict): + properties.pop("word_timestamps", None) + required = schema.get("required") + if isinstance(required, list): + schema["required"] = [item for item in required if item != "word_timestamps"] + + +class HotwordItem(BaseModel): + model_config = ConfigDict(json_schema_extra={"example": {"hotword": "汇智", "weight": 2.0}}) + + hotword: str = Field( + ..., + title="热词内容", + description="需要优先匹配或纠正的业务词、专有名词、人名、产品名等。Qwen3-ASR 无原生热词能力,服务会在识别后做规则化热词纠正。", + examples=["通义千问"], + ) + weight: float = Field( + 1.0, + title="热词权重", + description="热词权重,兼容老项目 Model-Test-New 的传参格式;当前规则纠正主要使用热词文本,权重保留用于接口兼容和后续扩展。", + examples=[2.0], + ) + + +class RecognitionRequestConfig(BaseModel): + model_config = ConfigDict( + json_schema_extra=_recognition_request_config_schema_extra + ) + enable_speaker: bool = Field( + True, + title="是否启用说话人分离", + description="是否执行说话人分离。开启后返回的分段会带 speaker 字段;关闭后不做说话人分离和声纹库匹配。", + examples=[True], + ) + match_speaker_registry: bool = Field( + True, + title="是否匹配已注册声纹库", + description="是否在说话人分离后匹配已注册声纹库。只有 enable_speaker=true 时生效。", + examples=[True], + ) + enable_text_cleanup: bool = Field( + True, + title="是否启用文字去重", + description="是否启用识别结果去重/边界重叠裁剪,适合长音频分段识别后的重复文本清理。", + examples=[True], + ) + speaker_threshold: Optional[float] = Field( + None, + title="声纹匹配阈值", + description="本次声纹库匹配阈值,范围通常为 0~1;不传则使用服务默认配置。", + examples=[0.6], + ) + word_timestamps: bool = Field( + False, + title="是否返回字词级时间戳", + description="是否返回字词级时间戳。长会议场景建议默认关闭以降低耗时和结果体积。", + examples=[False], + ) + hotwords: str | list[HotwordItem] = Field( + default_factory=list, + title="热词", + description=( + "热词参数。推荐传老项目兼容的结构化数组:" + "[{\"hotword\":\"通义千问\",\"weight\":2.0}];也兼容字符串:\"通义千问 2.0 ModelScope 1.5\"。" + "Qwen3-ASR 本身没有原生热词参数,服务会在识别后使用热词规则做文本纠正。" + ), + examples=[[{"hotword": "通义千问", "weight": 2.0}, {"hotword": "ModelScope", "weight": 1.5}]], + ) + sample_rate: int = Field( + 16000, + title="采样率", + description="音频处理采样率,默认 16000。", + examples=[16000], + ) + + +class TranscriptionCreateRequest(BaseModel): + model_config = ConfigDict( + json_schema_extra={ + "example": { + "model": "qwen3-asr-0.6b", + "audio_address": "https://example.com/meeting.m4a", + "config": { + "enable_speaker": True, + "match_speaker_registry": True, + "enable_text_cleanup": True, + "speaker_threshold": 0.6, + "hotwords": [ + {"hotword": "汇智", "weight": 2.0}, + {"hotword": "通义千问", "weight": 2.5}, + ], + }, + } + } + ) + model: Optional[str] = Field( + None, + title="离线 ASR 模型 ID", + description="可选。不传则使用服务当前默认模型;可传当前可用的本地模型 ID,如 qwen3-asr-0.6b 或 qwen3-asr-1.7b。", + examples=["qwen3-asr-0.6b"], + max_length=128, + ) + audio_address: str = Field( + ..., + title="音视频地址", + description="必填。音频/视频文件地址,支持 HTTP/HTTPS URL、file:// 地址或服务端本地音视频路径。该接口生产调用不使用 file_id/file_url。", + examples=["https://example.com/meeting.m4a"], + ) + config: RecognitionRequestConfig = Field( + default_factory=RecognitionRequestConfig, + title="识别配置", + description=( + "离线会议识别配置,包括说话人分离、声纹库匹配、文字去重和热词规则。" + if not settings.ASR_ENABLE_WORD_TIMESTAMPS + else "离线会议识别配置,包括说话人分离、声纹库匹配、文字去重、字词时间戳和热词规则。" + ) + ) + + +class SpeakerRegisterRequest(BaseModel): + model_config = ConfigDict( + json_schema_extra={"example": {"name": "张三", "user_id": "u001", "audio_address": "/data/speakers/zhangsan.wav"}} + ) + name: str + audio_address: Optional[str] = None + file_url: Optional[str] = None + file_id: Optional[str] = None + user_id: Optional[str] = None + + +class SpeakerIdentifyRequest(BaseModel): + model_config = ConfigDict(json_schema_extra={"example": {"audio_address": "/data/audio/sample.wav", "threshold": 0.6}}) + audio_address: Optional[str] = None + file_url: Optional[str] = None + file_id: Optional[str] = None + threshold: Optional[float] = None + + +def _get_meeting_transcription_description() -> str: + return """创建离线会议转写任务。该接口采用 **JSON 请求体**,适合业务系统直接提交会议音频/视频地址并异步轮询结果。 + +**音频输入方式:** +1. **HTTP/HTTPS URL**:`audio_address="https://example.com/meeting.mp4"` +2. **file:// 地址**:`audio_address="file:///data/audio/meeting.m4a"` +3. **服务端本地路径**:`audio_address="/data/audio/meeting.m4a"` + +> 生产调用只使用 `audio_address`。`file_id` / `file_url` 不属于该接口参数,上传接口仅用于测试页面调试。 + +**请求参数:** +| 字段 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `model` | string/null | 否 | 服务默认模型 | 离线 ASR 模型 ID;不传则使用 `QWEN3_ASR_MODEL` 或自动选择结果 | +| `audio_address` | string | 是 | - | 音频/视频文件地址,支持 HTTP/HTTPS、`file://`、服务端本地路径 | +| `config` | object | 否 | 默认配置 | 识别配置对象 | +| `config.enable_speaker` | boolean | 否 | `true` | 是否启用说话人分离 | +| `config.match_speaker_registry` | boolean | 否 | `true` | 是否匹配已注册声纹库,仅在 `enable_speaker=true` 时生效 | +| `config.enable_text_cleanup` | boolean | 否 | `true` | 是否启用文字去重、边界重叠裁剪 | +| `config.speaker_threshold` | number/null | 否 | 服务默认值 | 本次声纹库匹配阈值,通常为 `0~1` | +""" + ( + "| `config.word_timestamps` | boolean | 否 | `false` | 是否返回字词级时间戳;开启后会调用 Qwen3 ForcedAligner |\n" + if settings.ASR_ENABLE_WORD_TIMESTAMPS + else "" + ) + """ +| `config.hotwords` | array/string | 否 | `[]` | 热词。推荐老项目兼容数组格式:`[{\"hotword\":\"通义千问\",\"weight\":2.0}]` | +| `config.sample_rate` | integer | 否 | `16000` | 音频处理采样率 | + +**热词说明:** +- Qwen3-ASR 本身没有原生热词参数。 +- 本服务兼容 Model-Test-New 的热词格式,识别完成后通过规则做热词纠正。 +- 推荐格式:`[{\"hotword\":\"通义千问\",\"weight\":2.0},{\"hotword\":\"ModelScope\",\"weight\":1.5}]` +- 兼容字符串格式:`\"通义千问 2.0 ModelScope 1.5\"` + +**创建任务返回:** +| 字段 | 类型 | 说明 | +|------|------|------| +| `code` | integer | 业务状态码,`0` 表示成功 | +| `message` | string | 业务消息 | +| `data.task_id` | string | 任务 ID,用于查询识别进度与结果 | +| `data.status` | string | 初始状态,通常为 `queued` | +| `data.stage` | string | 当前阶段 | +| `data.percentage` | number | 当前进度百分比 | + +**查询任务返回:** +调用 `GET /api/v1/asr/transcriptions/{task_id}` 查询任务。完成后 `data.result` 包含完整结果。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `data.status` | string | `queued` / `processing` / `completed` / `failed` | +| `data.result.text` | string | 完整转写文本 | +| `data.result.segments` | array | 分段结果 | +| `data.result.segments[].start` | number | 分段开始时间,秒 | +| `data.result.segments[].end` | number | 分段结束时间,秒 | +| `data.result.segments[].text` | string | 分段文本 | +| `data.result.segments[].speaker_id` | string | 说话人 ID,启用说话人分离时返回 | +| `data.result.segments[].speaker_name` | string | 匹配到声纹库时返回姓名,否则通常等于 speaker_id | +| `data.result.segments[].user_id` | string | 匹配到声纹库用户时返回 | +| `data.result.segments[].word_tokens` | array | 字词级时间戳,仅 `word_timestamps=true` 且模型支持时返回 | +| `data.result.audio_duration_seconds` | number | 音频总时长,秒 | +| `data.result.speaker_count` | integer | 说话人数量 | +| `data.result.speakers` | array | 说话人列表 | +""" + + +def _ok(data: Any) -> dict[str, Any]: + return {"code": 0, "message": "ok", "data": data or {}} + + +def _public_task_payload(task_id: str, task: dict[str, Any]) -> dict[str, Any]: + payload: dict[str, Any] = {"task_id": task_id, "status": task["status"]} + for key in ( + "stage", + "message", + "percentage", + "detail", + "created_at", + "updated_at", + ): + if task.get(key) is not None: + payload[key] = task[key] + if ( + task["status"] == "processing" + and task.get("created_at") is not None + and task.get("percentage") not in (None, 0) + ): + elapsed_seconds = max(1.0, time.time() - float(task["created_at"])) + percentage = max(1.0, float(task.get("percentage") or 0)) + payload["eta_seconds"] = max( + 0, + int(math.ceil(elapsed_seconds * (100.0 - percentage) / percentage)), + ) + elif task["status"] == "completed": + payload["eta_seconds"] = 0 + if task.get("result") is not None: + payload["result"] = task["result"] + if task.get("error"): + payload["error"] = task["error"] + return payload + + +def _require_db() -> None: + if not pg_speaker_db.is_connected: + raise HTTPException( + status_code=503, + detail={"code": 1001, "message": "speaker database is not connected"}, + ) + + +def _normalize_hotwords_for_asr(hotwords: str | list[HotwordItem] | None) -> str: + if isinstance(hotwords, str): + return hotwords.strip() + parts: list[str] = [] + for item in hotwords or []: + text = item.hotword.strip() + if not text: + continue + parts.extend([text, f"{float(item.weight):g}"]) + return " ".join(parts) + + +async def _download_url(url: str) -> bytes: + loop = asyncio.get_running_loop() + + def _download() -> bytes: + response = requests.get(url, timeout=60) + response.raise_for_status() + return response.content + + return await loop.run_in_executor(None, _download) + + +def _is_http_url(value: str) -> bool: + return value.lower().startswith(("http://", "https://")) + + +def _source_filename(source: str) -> str: + parsed = urlparse(source) + path = unquote(parsed.path or source) + return Path(path).name or "audio" + + +async def _read_local_file(path_value: str) -> bytes: + parsed = urlparse(path_value) + local_path = Path(unquote(parsed.path)) if parsed.scheme == "file" else Path(path_value).expanduser() + if not local_path.is_file(): + raise HTTPException(status_code=404, detail={"code": 1001, "message": "local media file not found"}) + loop = asyncio.get_running_loop() + return await loop.run_in_executor(None, local_path.read_bytes) + + +async def _resolve_source_bytes(audio_address: Optional[str], file_id: Optional[str]) -> tuple[bytes, Optional[str]]: + if audio_address: + source = audio_address.strip() + if _is_http_url(source): + return await _download_url(source), _source_filename(source) + return await _read_local_file(source), _source_filename(source) + if file_id: + item = _uploaded_files.get(file_id) + if not item: + matched_files = sorted(_UPLOAD_DIR.glob(f"{file_id}.*")) + if not matched_files: + raise HTTPException(status_code=404, detail={"code": 1001, "message": "file_id not found"}) + matched_path = matched_files[0] + item = {"path": str(matched_path), "filename": matched_path.name} + _uploaded_files[file_id] = item + path = item["path"] + with open(path, "rb") as file_obj: + return file_obj.read(), item.get("filename") + raise HTTPException(status_code=400, detail={"code": 1001, "message": "audio_address or file_id is required"}) + + +def _cleanup_uploaded_file(file_id: Optional[str]) -> None: + if not file_id: + return + item = _uploaded_files.pop(file_id, None) + candidate_paths: list[Path] = [] + if item and item.get("path"): + candidate_paths.append(Path(item["path"])) + candidate_paths.extend(sorted(_UPLOAD_DIR.glob(f"{file_id}.*"))) + for path in candidate_paths: + try: + path.unlink(missing_ok=True) + except Exception: + pass + + +def _format_segments(asr_result: Any) -> list[dict[str, Any]]: + segments: list[dict[str, Any]] = [] + for segment in asr_result.segments: + item: dict[str, Any] = { + "start": round(float(segment.start_time), 2), + "end": round(float(segment.end_time), 2), + "duration": round(float(segment.end_time - segment.start_time), 2), + "text": segment.text, + } + if segment.speaker_id: + item["speaker_id"] = segment.speaker_id + if segment.speaker_name: + item["speaker_name"] = segment.speaker_name + elif segment.speaker_id: + item["speaker_name"] = segment.speaker_id + if segment.user_id: + item["user_id"] = segment.user_id + if segment.word_tokens: + item["word_tokens"] = [ + { + "text": token.text, + "start": round(float(token.start_time), 3), + "end": round(float(token.end_time), 3), + } + for token in segment.word_tokens + ] + segments.append(item) + return segments + + +def _build_transcription_result(asr_result: Any) -> dict[str, Any]: + segments = _format_segments(asr_result) + speakers: list[str] = [] + for segment in segments: + name = str(segment.get("speaker_name") or segment.get("speaker_id") or "").strip() + if name and name not in speakers: + speakers.append(name) + return { + "text": asr_result.text, + "segments": segments, + "audio_duration_seconds": round(float(asr_result.duration), 2), + "speaker_count": len(speakers), + "speakers": speakers, + } + + +def _update_task_progress( + task_id: str, + *, + status: str, + stage: str, + message: str, + percentage: int, + detail: Optional[dict[str, Any]] = None, +) -> None: + current_task = get_task_record(task_id) or {} + current_status = str(current_task.get("status") or "") + current_percentage = int(current_task.get("percentage") or 0) + incoming_percentage = max(0, min(100, int(percentage))) + if ( + current_status in {"queued", "processing"} + and status in {"queued", "processing"} + and incoming_percentage < current_percentage + ): + return + payload: dict[str, Any] = { + "status": status, + "stage": stage, + "message": message, + "percentage": incoming_percentage, + } + if detail is not None: + payload["detail"] = detail + update_task_record(task_id, payload) + + +async def _run_transcription_task( + *, + task_id: str, + model: Optional[str], + audio_address: Optional[str], + config: RecognitionRequestConfig, +) -> None: + service = get_offline_transcription_service() + prepared_audio: Optional[PreparedAudio] = None + _update_task_progress( + task_id, + status="processing", + stage="preparing", + message="任务已开始,正在准备音频文件。", + percentage=5, + detail={"segment_total": 0, "segment_completed": 0}, + ) + try: + audio_data, filename = await _resolve_source_bytes(audio_address, None) + _update_task_progress( + task_id, + status="processing", + stage="normalizing", + message="音频来源已读取,正在转换为识别格式。", + percentage=8, + detail={"segment_total": 0, "segment_completed": 0}, + ) + prepared_audio = await service.prepare_upload( + audio_data=audio_data, + filename=filename, + task_id=task_id, + sample_rate=config.sample_rate or int(SampleRate.RATE_16000), + ) + _update_task_progress( + task_id, + status="processing", + stage="transcribing", + message="音频已准备完成,正在执行离线识别。", + percentage=12, + detail={ + "segment_total": 0, + "segment_completed": 0, + "audio_duration_seconds": round(float(prepared_audio.duration), 2), + }, + ) + + def progress_callback( + stage: str, + message: str, + percentage: int, + detail: Optional[dict[str, Any]] = None, + ) -> None: + _update_task_progress( + task_id, + status="processing", + stage=stage, + message=message, + percentage=percentage, + detail=detail, + ) + + asr_result = await service.transcribe( + prepared_audio, + OfflineTranscriptionOptions( + model_id=model, + sample_rate=config.sample_rate or int(SampleRate.RATE_16000), + hotwords=_normalize_hotwords_for_asr(config.hotwords), + enable_speaker_diarization=config.enable_speaker, + enable_speaker_identification=( + config.enable_speaker and config.match_speaker_registry + ), + enable_text_cleanup=config.enable_text_cleanup, + word_timestamps=( + settings.ASR_ENABLE_WORD_TIMESTAMPS and config.word_timestamps + ), + task_id=task_id, + progress_callback=progress_callback, + ), + ) + if ( + config.enable_speaker + and config.match_speaker_registry + and config.speaker_threshold is not None + ): + _update_task_progress( + task_id, + status="processing", + stage="speaker_matching", + message="正在使用本次阈值匹配已注册声纹库。", + percentage=95, + detail={ + "segment_total": len(asr_result.segments), + "segment_completed": len(asr_result.segments), + "audio_duration_seconds": round(float(asr_result.duration), 2), + }, + ) + await get_speaker_registry_service().apply_registered_speakers( + asr_result, + threshold=config.speaker_threshold, + ) + result = _build_transcription_result(asr_result) + _update_task_progress( + task_id, + status="completed", + stage="completed", + message="离线识别已经完成。", + percentage=100, + detail={ + "segment_total": len(result.get("segments") or []), + "segment_completed": len(result.get("segments") or []), + "audio_duration_seconds": result.get("audio_duration_seconds"), + "speaker_count": result.get("speaker_count"), + "speakers": result.get("speakers"), + }, + ) + update_task_record(task_id, {"result": result}) + except Exception as exc: + _update_task_progress( + task_id, + status="failed", + stage="failed", + message="离线识别执行失败。", + percentage=100, + ) + update_task_record(task_id, {"error": str(exc)}) + finally: + service.cleanup(prepared_audio) + + +@router.post("/files", tags=["会议离线接口"], summary="上传音频文件") +async def upload_file(file: UploadFile = File(...)) -> dict[str, Any]: + file_id = uuid.uuid4().hex + suffix = Path(file.filename or "").suffix or ".audio" + path = _UPLOAD_DIR / f"{file_id}{suffix}" + content = await file.read() + path.write_bytes(content) + _uploaded_files[file_id] = {"path": str(path), "filename": file.filename or path.name} + return _ok({"file_id": file_id, "file_path": str(path)}) + + +@router.post( + "/asr/transcriptions", + tags=["会议离线接口"], + summary="创建离线会议识别任务", + description=_get_meeting_transcription_description(), + response_description="任务创建成功,返回 task_id;后续调用 GET /api/v1/asr/transcriptions/{task_id} 查询进度和结果。", + responses={ + 200: { + "description": "任务创建成功", + "content": { + "application/json": { + "example": { + "code": 0, + "message": "ok", + "data": { + "task_id": "task_6fd90daffd2f446f944b64fdf6c17572", + "status": "queued", + "stage": "queued", + "message": "任务已创建,等待进入处理流程。", + "percentage": 0, + }, + } + } + }, + }, + 400: { + "description": "请求参数错误", + "content": { + "application/json": { + "example": { + "detail": { + "code": 1001, + "message": "audio_address is required", + } + } + } + }, + }, + }, +) +async def create_transcription_task( + background_tasks: BackgroundTasks, + item: TranscriptionCreateRequest = Body( + ..., + description="离线会议识别任务 JSON 请求体。必须提供 audio_address,可在 config 中配置说话人分离、声纹库匹配、文字去重、热词等。", + examples=_meeting_request_examples(), + ), +) -> dict[str, Any]: + if not item.audio_address: + raise HTTPException(status_code=400, detail={"code": 1001, "message": "audio_address is required"}) + try: + model_id = validate_offline_model_id(item.model) + except InvalidParameterException as exc: + raise HTTPException(status_code=400, detail={"code": 1001, "message": exc.message}) from exc + task_id = f"task_{uuid.uuid4().hex}" + create_task_record( + task_id, + { + "status": "queued", + "stage": "queued", + "message": "任务已创建,等待进入处理流程。", + "percentage": 0, + "result": None, + "detail": {"segment_total": 0, "segment_completed": 0}, + }, + ) + background_tasks.add_task( + _run_transcription_task, + task_id=task_id, + model=model_id, + audio_address=item.audio_address, + config=item.config, + ) + return _ok( + { + "task_id": task_id, + "status": "queued", + "stage": "queued", + "message": "任务已创建,等待进入处理流程。", + "percentage": 0, + } + ) + + +@router.get( + "/asr/transcriptions/{task_id}", + tags=["会议离线接口"], + summary="查询离线会议识别任务", + description=( + "根据创建任务时返回的 `task_id` 查询离线会议识别进度和结果。" + "`status=completed` 时,`data.result` 会包含完整文本、分段、说话人和可选字词级时间戳。" + ), + responses={ + 200: { + "description": "查询成功", + "content": { + "application/json": { + "examples": { + "processing": { + "summary": "处理中", + "value": { + "code": 0, + "message": "ok", + "data": { + "task_id": "task_6fd90daffd2f446f944b64fdf6c17572", + "status": "processing", + "stage": "asr", + "message": "正在识别分段 80/127", + "percentage": 62, + "detail": {"segment_total": 127, "segment_completed": 80}, + "eta_seconds": 38, + }, + }, + }, + "completed": { + "summary": "已完成", + "value": { + "code": 0, + "message": "ok", + "data": { + "task_id": "task_6fd90daffd2f446f944b64fdf6c17572", + "status": "completed", + "stage": "completed", + "message": "识别完成。", + "percentage": 100, + "eta_seconds": 0, + "result": { + "text": "大家好,今天我们讨论通义千问和 ModelScope 的接入方案。", + "segments": [ + { + "start": 0.0, + "end": 4.28, + "duration": 4.28, + "text": "大家好,今天我们讨论通义千问和 ModelScope 的接入方案。", + "speaker_id": "speaker_1", + "speaker_name": "张三", + "user_id": "u001", + "word_tokens": [ + {"text": "大家好", "start": 0.12, "end": 0.68} + ], + } + ], + "audio_duration_seconds": 4669.05, + "speaker_count": 1, + "speakers": ["张三"], + }, + }, + }, + }, + } + } + }, + }, + 404: { + "description": "任务不存在", + "content": { + "application/json": { + "example": { + "detail": { + "code": 1004, + "message": "task not found", + } + } + } + }, + }, + }, +) +async def get_transcription_task(task_id: str) -> dict[str, Any]: + task = get_task_record(task_id, include_result=True) + if not task: + raise HTTPException(status_code=404, detail={"code": 1001, "message": "Task not found"}) + return _ok(_public_task_payload(task_id, task)) + + +@router.post("/speakers", tags=["声纹管理"], summary="注册声纹") +async def register_speaker(item: SpeakerRegisterRequest) -> dict[str, Any]: + _require_db() + temp_path: Optional[str] = None + try: + audio_data, filename = await _resolve_source_bytes(item.audio_address or item.file_url, item.file_id) + suffix = Path(filename or "").suffix or ".wav" + temp_path = await get_speaker_registry_service().save_upload_to_temp(audio_data, suffix=suffix) + result = await get_speaker_registry_service().register_file( + name=item.name, + file_path=temp_path, + user_id=item.user_id, + ) + return _ok(result) + finally: + get_speaker_registry_service().cleanup_file(temp_path) + _cleanup_uploaded_file(item.file_id) + + +@router.get("/speakers", tags=["声纹管理"], summary="获取声纹人员列表") +async def list_speakers() -> dict[str, Any]: + _require_db() + return _ok( + { + "items": await get_speaker_registry_service().list_speakers(), + "speaker_model": "CampPlus", + } + ) + + +@router.delete("/speakers/{speaker_id}", tags=["声纹管理"], summary="删除声纹人员") +async def delete_speaker(speaker_id: str) -> dict[str, Any]: + _require_db() + deleted = await get_speaker_registry_service().delete_speaker(speaker_id) + if not deleted: + raise HTTPException(status_code=404, detail={"code": 1001, "message": "Speaker not found"}) + return _ok({"id": speaker_id}) + + +@router.post("/speakers/identify", tags=["声纹管理"], summary="识别音频中的说话人") +async def identify_speaker(item: SpeakerIdentifyRequest) -> dict[str, Any]: + _require_db() + temp_path: Optional[str] = None + try: + audio_data, filename = await _resolve_source_bytes(item.audio_address or item.file_url, item.file_id) + suffix = Path(filename or "").suffix or ".wav" + temp_path = await get_speaker_registry_service().save_upload_to_temp(audio_data, suffix=suffix) + return _ok( + await get_speaker_registry_service().identify_file( + file_path=temp_path, + threshold=item.threshold, + ) + ) + finally: + get_speaker_registry_service().cleanup_file(temp_path) + _cleanup_uploaded_file(item.file_id) diff --git a/app/api/v1/openai_compatible.py b/app/api/v1/openai_compatible.py new file mode 100644 index 0000000..6071b9c --- /dev/null +++ b/app/api/v1/openai_compatible.py @@ -0,0 +1,692 @@ +# -*- coding: utf-8 -*- +""" +OpenAI 兼容 API +实现 OpenAI Audio API 规范,兼容 OpenAI SDK 和第三方客户端 +""" + +import asyncio +import json +import time +import logging +from typing import Any, Optional, List +from enum import Enum + +from fastapi import APIRouter, File, Form, UploadFile, Request, HTTPException +from fastapi.responses import JSONResponse, PlainTextResponse, StreamingResponse +from pydantic import BaseModel, Field + +from ...core.config import settings +from ...core.security import validate_openai_token +from ...core.exceptions import ( + create_error_response, + InvalidParameterException, +) +from ...services.asr.model_selection import ( + get_default_offline_model_id, + get_offline_model_ids, + validate_offline_model_id, +) +from ...services.asr.offline_transcription_service import ( + OfflineTranscriptionOptions, + PreparedAudio, + get_offline_transcription_service, +) + +logger = logging.getLogger(__name__) + +def _parse_hidden_bool(raw_value: Any) -> bool: + if isinstance(raw_value, bool): + return raw_value + if raw_value is None: + return False + return str(raw_value).strip().lower() in {"1", "true", "yes", "on"} + + +router = APIRouter(prefix="/v1", tags=["OpenAI Compatible"]) +HEARTBEAT_INTERVAL_SECONDS = 15.0 + + +# ============= 枚举类型 ============= + +class ResponseFormat(str, Enum): + JSON = "json" + TEXT = "text" + SRT = "srt" + VERBOSE_JSON = "verbose_json" + VTT = "vtt" + + +# ============= 响应模型 ============= + +class TranscriptionSegment(BaseModel): + """转写分段""" + id: int + seek: int = 0 + start: float + end: float + text: str + tokens: List[int] = Field(default_factory=list) + temperature: float = 0.0 + avg_logprob: float = 0.0 + compression_ratio: float = 0.0 + no_speech_prob: float = 0.0 + speaker: Optional[str] = Field(default=None, description="说话人ID") + + +class TranscriptionWord(BaseModel): + """转写词级别信息""" + word: str + start: float + end: float + + +class TranscriptionResponse(BaseModel): + """简单转写响应 (json 格式)""" + text: str + + +class VerboseTranscriptionResponse(BaseModel): + """详细转写响应 (verbose_json 格式)""" + task: str = "transcribe" + language: str + duration: float + text: str + segments: List[TranscriptionSegment] = Field(default_factory=list) + words: Optional[List[TranscriptionWord]] = None + + +class ModelObject(BaseModel): + """模型对象""" + id: str + object: str = "model" + created: int = Field(default_factory=lambda: int(time.time())) + owned_by: str = "qwen3-asr" + + +class ModelsResponse(BaseModel): + """模型列表响应""" + object: str = "list" + data: List[ModelObject] + + +# ============= 辅助函数 ============= + +def format_timestamp_srt(seconds: float) -> str: + """格式化时间戳为 SRT 格式 (HH:MM:SS,mmm)""" + hours = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + secs = int(seconds % 60) + millis = int((seconds % 1) * 1000) + return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}" + + +def format_timestamp_vtt(seconds: float) -> str: + """格式化时间戳为 VTT 格式 (HH:MM:SS.mmm)""" + hours = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + secs = int(seconds % 60) + millis = int((seconds % 1) * 1000) + return f"{hours:02d}:{minutes:02d}:{secs:02d}.{millis:03d}" + + +def generate_srt(segments: List[TranscriptionSegment]) -> str: + """生成 SRT 字幕格式""" + lines = [] + for i, seg in enumerate(segments, 1): + start = format_timestamp_srt(seg.start) + end = format_timestamp_srt(seg.end) + lines.append(f"{i}") + lines.append(f"{start} --> {end}") + text = seg.text.strip() + if seg.speaker: + text = f"[{seg.speaker}] {text}" + lines.append(text) + lines.append("") + return "\n".join(lines) + + +def generate_vtt(segments: List[TranscriptionSegment]) -> str: + """生成 WebVTT 字幕格式""" + lines = ["WEBVTT", ""] + for seg in segments: + start = format_timestamp_vtt(seg.start) + end = format_timestamp_vtt(seg.end) + lines.append(f"{start} --> {end}") + text = seg.text.strip() + if seg.speaker: + text = f"[{seg.speaker}] {text}" + lines.append(text) + lines.append("") + return "\n".join(lines) + + +def detect_language(text: str, language: Optional[str]) -> str: + """检测识别语言。""" + if language: + return language + + import re + + if re.search(r"[\u4e00-\u9fff]", text): + return "zh" + return "en" + + +def build_transcription_payload( + *, + response_format: ResponseFormat, + asr_result, + audio_duration: float, + language: Optional[str], +) -> tuple[object, int, int]: + """构建 OpenAI 转写响应载荷,并返回 segments / words 计数。""" + segments: List[TranscriptionSegment] = [] + words: List[TranscriptionWord] = [] + + for i, seg in enumerate(asr_result.segments): + segments.append( + TranscriptionSegment( + id=i, + seek=int(seg.start_time * 100), + start=seg.start_time, + end=seg.end_time, + text=seg.text, + speaker=seg.speaker_id, + ) + ) + if seg.word_tokens: + for wt in seg.word_tokens: + words.append( + TranscriptionWord( + word=wt.text, + start=round(seg.start_time + wt.start_time, 3), + end=round(seg.start_time + wt.end_time, 3), + ) + ) + + detected_language = detect_language(asr_result.text, language) + + if response_format == ResponseFormat.VERBOSE_JSON: + payload = VerboseTranscriptionResponse( + task="transcribe", + language=detected_language, + duration=audio_duration, + text=asr_result.text, + segments=segments, + words=words if words else None, + ).model_dump() + elif response_format == ResponseFormat.JSON: + payload = {"text": asr_result.text} + elif response_format == ResponseFormat.TEXT: + payload = asr_result.text + elif response_format == ResponseFormat.SRT: + if not segments: + segments = [ + TranscriptionSegment( + id=0, + start=0, + end=audio_duration, + text=asr_result.text, + ) + ] + payload = generate_srt(segments) + elif response_format == ResponseFormat.VTT: + if not segments: + segments = [ + TranscriptionSegment( + id=0, + start=0, + end=audio_duration, + text=asr_result.text, + ) + ] + payload = generate_vtt(segments) + else: + payload = {"text": asr_result.text} + + return payload, len(segments), len(words) + + +def create_heartbeat_streaming_response( + *, + response_format: ResponseFormat, + inference_coro, + audio_duration: float, + language: Optional[str], + cleanup_callback, +) -> StreamingResponse: + """为长耗时 JSON 响应生成带心跳的流式输出。""" + + async def response_stream(): + inference_task = asyncio.create_task(inference_coro) + heartbeat_count = 0 + + try: + while True: + done, _pending = await asyncio.wait( + {inference_task}, + timeout=HEARTBEAT_INTERVAL_SECONDS, + return_when=asyncio.FIRST_COMPLETED, + ) + if inference_task in done: + break + + heartbeat_count += 1 + logger.info( + "[OpenAI API] 发送响应心跳: " + f"format={response_format}, heartbeat_count={heartbeat_count}" + ) + yield b" \n" + + asr_result = await inference_task + logger.info(f"[OpenAI API] 识别完成: {len(asr_result.text)} 字符") + + payload, segments_count, words_count = build_transcription_payload( + response_format=response_format, + asr_result=asr_result, + audio_duration=audio_duration, + language=language, + ) + response_bytes = json.dumps( + payload, + ensure_ascii=False, + separators=(",", ":"), + ).encode("utf-8") + + logger.info( + "[OpenAI API] 准备发送 JSON 响应: " + f"format={response_format}, " + f"segments={segments_count}, " + f"words={words_count}, " + f"payload_bytes={len(response_bytes)}, " + f"heartbeat_count={heartbeat_count}" + ) + yield response_bytes + finally: + cleanup_callback() + + return StreamingResponse( + response_stream(), + media_type="application/json", + headers={ + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no", + }, + ) + + +# ============= API 端点 ============= + +def _get_openai_model_description() -> str: + """获取动态的模型描述""" + available_models = get_offline_model_ids() + default_model = get_default_offline_model_id() + + model_descriptions = { + "qwen3-asr-1.7b": "Qwen3-ASR 1.7B,52 种语言,vLLM 高性能", + "qwen3-asr-0.6b": "Qwen3-ASR 0.6B,轻量版,适合小显存环境", + "qwen3-asr": "自动路由到当前已启动的 Qwen3-ASR 版本", + } + + # 构建表格行 + table_rows = [] + for m in available_models: + desc = model_descriptions.get(m, "") + if m == default_model: + desc += "(默认)" + table_rows.append(f"| `{m}` | {desc} |") + + return f"""返回当前可用的离线 Qwen3-ASR 模型列表(OpenAI `/v1/models` 兼容)。 + +**可用离线模型:** + +| 模型 ID | 说明 | +|---------|------| +{chr(10).join(table_rows)} + +**兼容性说明:** +- 支持 OpenAI SDK 和第三方客户端调用 +- 当前默认模型根据显存自动选择;也可通过 `QWEN3_ASR_MODEL` 覆盖 +""" + + +@router.get( + "/models", + response_model=ModelsResponse, + summary="列出可用离线模型", + description=_get_openai_model_description(), +) +async def list_models(request: Request): + """列出可用离线模型 (OpenAI 兼容)""" + result, _ = validate_openai_token(request) + if not result: + response_data = create_error_response( + error_code="AUTHENTICATION_FAILED", + message="Invalid authentication", + ) + return JSONResponse(content=response_data, status_code=401) + + try: + # 使用动态模型列表 + model_ids = get_offline_model_ids() + + model_objects = [] + for model_id in model_ids: + model_objects.append(ModelObject( + id=model_id, + owned_by="qwen3-asr", + )) + + return ModelsResponse(data=model_objects) + except Exception as e: + logger.error(f"获取模型列表失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +def _get_transcription_description() -> str: + """获取动态的转写端点描述""" + return f"""将音频文件转写为文本(完全兼容 OpenAI Audio API)。 + +**支持的音频格式与常见含音轨视频容器:** +`mp3`, `mp4`, `mpeg`, `mpga`, `m4a`, `wav`, `webm`, `flac`, `ogg`, `amr`, `pcm`, `mov`, `mkv`, `avi` + +**音频输入方式:** +1. **文件上传**:通过 `file` 参数上传音频/视频文件(标准 OpenAI 方式) +2. **URL/本地路径读取**:通过 `audio_address` 参数提供音频/视频文件 URL(HTTP/HTTPS)或服务端本地路径 + +如果同时提供 `file` 和 `audio_address`,服务会优先使用 `file`,并忽略 `audio_address`。 + +**文件大小限制:** +- 最大支持 {settings.MAX_AUDIO_SIZE // (1024 * 1024)}MB(可通过 `MAX_AUDIO_SIZE` 环境变量配置) +- OpenAI 原生限制为 25MB + +**说话人分离:** +- 默认开启 (`enable_speaker_diarization=true`) +- 启用后 `verbose_json` 格式的 segments 会包含 `speaker` 字段(如 "说话人1") +- 可设置 `enable_speaker_diarization=false` 关闭 + +**增强选项:** +- `enable_speaker_identification=true` 时会尝试匹配已注册声纹库,仅在说话人分离开启时生效 +- `enable_text_cleanup=true` 时会执行文本去重、跨段重叠裁剪和口头语清理 +- `hotwords` 可传热词字符串,格式:`热词1 权重1 热词2 权重2` + +**输出格式:** +| 格式 | Content-Type | 说明 | +|------|-------------|------| +| `json` | application/json | 简单 JSON,仅含 text 字段(默认) | +| `text` | text/plain | 纯文本 | +| `verbose_json` | application/json | 详细 JSON,含时间戳、分段和说话人 | +| `srt` | text/plain | SRT 字幕格式 | +| `vtt` | text/vtt | WebVTT 字幕格式 | + +**模型选择:** +- 默认使用当前服务启用的 Qwen3-ASR 模型 +- 可通过可选 `model` 表单字段指定当前可用离线模型 +- 通过 `QWEN3_ASR_MODEL` 控制服务端默认模型型号 +- `/v1/models` 仍可用于查看当前服务端实际在线模型 + +**兼容参数:** +`prompt`、`temperature`、`timestamp_granularities` 参数已保留;其中 `prompt` 可作为热词提示的兼容入口 +""" + + +@router.post( + "/audio/transcriptions", + summary="音频转写", + description=_get_transcription_description(), + responses={ + 200: { + "description": "转写成功", + "content": { + "application/json": { + "example": {"text": "今天天气不错,明天可能会下雨。"} + }, + "text/plain": { + "example": "今天天气不错,明天可能会下雨。" + }, + }, + }, + 400: { + "description": "请求错误", + "content": { + "application/json": { + "example": { + "error_code": "INVALID_PARAMETER", + "message": f"File too large. Maximum size is {settings.MAX_AUDIO_SIZE // (1024 * 1024)}MB", + "task_id": "", + "timestamp": "2025-01-31T12:00:00Z", + "details": {} + } + } + }, + }, + 401: { + "description": "认证失败", + "content": { + "application/json": { + "example": { + "error_code": "AUTHENTICATION_FAILED", + "message": "Invalid API key", + "task_id": "", + "timestamp": "2025-01-31T12:00:00Z", + "details": {} + } + } + }, + }, + }, +) +async def create_transcription( + request: Request, + # 1. 音频输入(二选一) + file: Optional[UploadFile] = File( + default=None, + description="要转写的音频/视频文件。若同时提供 audio_address,服务会优先使用这里上传的文件" + ), + audio_address: Optional[str] = Form( + default=None, + description="音频/视频文件地址,支持 HTTP/HTTPS URL、file:// 或服务端本地路径。仅当 file 为空时使用;若同时上传 file,服务会忽略此参数", + json_schema_extra={"example": "https://media.cdn.vect.one/podcast_demo.mp4"}, + ), + model: Optional[str] = Form( + default=None, + description="可选。离线 ASR 模型 ID;不传则使用服务当前默认模型", + examples=["qwen3-asr-0.6b"], + ), + language: Optional[str] = Form( + None, + description="音频语言代码(ISO-639-1),如 zh/en/ja,不填则自动检测", + examples=["zh", "en", "ja"], + ), + # 4. 功能开关 + enable_speaker_diarization: bool = Form( + True, + description="是否启用说话人分离(默认开启)。启用后响应 segments 会包含 speaker 字段" + ), + enable_speaker_identification: bool = Form( + True, + description="是否匹配已注册声纹库。仅在 enable_speaker_diarization=true 时生效" + ), + enable_text_cleanup: bool = Form( + True, + description="是否启用文本去重、跨段重叠裁剪和口头语清理" + ), + hotwords: Optional[str] = Form( + None, + description="热词字符串,格式:热词1 权重1 热词2 权重2" + ), + # 5. 输出选项 + response_format: ResponseFormat = Form( + ResponseFormat.VERBOSE_JSON, + description="输出格式", + examples=["verbose_json", "json", "text", "srt", "vtt"], + ), + # 6. 兼容性参数(暂不支持) + prompt: Optional[str] = Form(None, description="提示文本(暂不支持,保留兼容)"), # noqa: ARG001 + temperature: Optional[float] = Form(0, description="采样温度(暂不支持,保留兼容)"), # noqa: ARG001 + timestamp_granularities: Optional[List[str]] = Form( # noqa: ARG001 + None, + alias="timestamp_granularities[]", + description="时间戳粒度(暂不支持,保留兼容)" + ), +): + """音频转写 API (OpenAI Audio API 兼容)""" + # 标记暂不支持的参数(保留以兼容 OpenAI API) + _ = (temperature, timestamp_granularities) + hotword_text = (hotwords or prompt or "").strip() + form_data = await request.form() + requested_word_timestamps = _parse_hidden_bool(form_data.get("word_timestamps")) + word_timestamps = settings.ASR_ENABLE_WORD_TIMESTAMPS and requested_word_timestamps + + prepared_audio: Optional[PreparedAudio] = None + response_cleanup_managed = False + + logger.info(f"[OpenAI API] 收到转写请求: model={model or 'default'}, format={response_format}, " + f"speaker_diarization={enable_speaker_diarization}, word_level={word_timestamps}, " + f"audio_address={'有' if audio_address else '无'}") + + # 验证输入:至少提供一种输入源;若二者同时存在,优先 file + if not file and not audio_address: + response_data = create_error_response( + error_code="INVALID_PARAMETER", + message="必须提供 file(上传文件)或 audio_address(音频 URL)其中之一", + ) + return JSONResponse(content=response_data, status_code=400) + + transcription_service = get_offline_transcription_service() + + try: + result, _ = validate_openai_token(request) + if not result: + response_data = create_error_response( + error_code="AUTHENTICATION_FAILED", + message="Invalid authentication", + ) + return JSONResponse(content=response_data, status_code=401) + + model_id = validate_offline_model_id(model) + + # 处理音频输入:优先 file,其次 audio_address + if file is not None: + if audio_address: + logger.info("[OpenAI API] 检测到同时提供 file 和 audio_address,已忽略 audio_address") + + logger.info(f"[OpenAI API] 从上传文件读取音频: {file.filename}") + audio_data = await file.read() + + prepared_audio = await transcription_service.prepare_upload( + audio_data=audio_data, + filename=file.filename if file else None, + task_id=f"openai-{int(time.time() * 1000)}", + sample_rate=16000, + ) + else: + logger.info(f"[OpenAI API] 从 URL 下载音频: {audio_address}") + prepared_audio = await transcription_service.prepare_from_request( + request=request, + audio_address=audio_address, + task_id=f"openai-{int(time.time() * 1000)}", + sample_rate=16000, + ) + + inference_coro = transcription_service.transcribe( + prepared_audio, + OfflineTranscriptionOptions( + model_id=model_id, + sample_rate=16000, + hotwords=hotword_text, + enable_speaker_diarization=enable_speaker_diarization, + enable_speaker_identification=( + enable_speaker_diarization and enable_speaker_identification + ), + enable_text_cleanup=enable_text_cleanup, + word_timestamps=word_timestamps, + ), + ) + audio_duration = prepared_audio.duration + + # 根据 response_format 返回不同格式 + if response_format == ResponseFormat.TEXT: + asr_result = await inference_coro + logger.info(f"[OpenAI API] 识别完成: {len(asr_result.text)} 字符") + payload, _, _ = build_transcription_payload( + response_format=response_format, + asr_result=asr_result, + audio_duration=audio_duration, + language=language, + ) + return PlainTextResponse(content=payload) + + elif response_format == ResponseFormat.SRT: + asr_result = await inference_coro + logger.info(f"[OpenAI API] 识别完成: {len(asr_result.text)} 字符") + payload, _, _ = build_transcription_payload( + response_format=response_format, + asr_result=asr_result, + audio_duration=audio_duration, + language=language, + ) + return PlainTextResponse(content=payload, media_type="text/plain") + + elif response_format == ResponseFormat.VTT: + asr_result = await inference_coro + logger.info(f"[OpenAI API] 识别完成: {len(asr_result.text)} 字符") + payload, _, _ = build_transcription_payload( + response_format=response_format, + asr_result=asr_result, + audio_duration=audio_duration, + language=language, + ) + return PlainTextResponse(content=payload, media_type="text/vtt") + + elif response_format in {ResponseFormat.VERBOSE_JSON, ResponseFormat.JSON}: + response_cleanup_managed = True + return create_heartbeat_streaming_response( + response_format=response_format, + inference_coro=inference_coro, + audio_duration=audio_duration, + language=language, + cleanup_callback=lambda: transcription_service.cleanup(prepared_audio), + ) + + else: + asr_result = await inference_coro + logger.info(f"[OpenAI API] 识别完成: {len(asr_result.text)} 字符") + payload, _, _ = build_transcription_payload( + response_format=ResponseFormat.JSON, + asr_result=asr_result, + audio_duration=audio_duration, + language=language, + ) + return JSONResponse(content=payload) + + except HTTPException as http_exc: + # 将 HTTPException 转换为标准错误格式 + logger.error(f"[OpenAI API] HTTP异常: {http_exc.detail}") + + response_data = create_error_response( + error_code="DEFAULT_CLIENT_ERROR" if http_exc.status_code < 500 else "DEFAULT_SERVER_ERROR", + message=http_exc.detail, + ) + return JSONResponse(content=response_data, status_code=http_exc.status_code) + except InvalidParameterException as e: + logger.error(f"[OpenAI API] 参数异常: {e.message}") + response_data = create_error_response( + error_code=e.error_code, + message=e.message, + details=e.details, + ) + return JSONResponse(content=response_data, status_code=400) + except Exception as e: + logger.error(f"[OpenAI API] 转写失败: {e}") + + # 使用标准错误格式 + response_data = create_error_response( + error_code="DEFAULT_SERVER_ERROR", + message=str(e), + ) + return JSONResponse(content=response_data, status_code=500) + + finally: + if not response_cleanup_managed: + transcription_service.cleanup(prepared_audio) diff --git a/app/api/v1/websocket_asr.py b/app/api/v1/websocket_asr.py new file mode 100644 index 0000000..ce82187 --- /dev/null +++ b/app/api/v1/websocket_asr.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +"""WebSocket ASR API routes.""" + +import logging +import time +import uuid +from typing import Optional + +from fastapi import APIRouter, WebSocket + +from ...services.qwen3_websocket_asr import Qwen3ASRService + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/ws/v1/asr", tags=["WebSocket ASR"]) + + +@router.websocket("/funasr") +async def funasr_websocket(websocket: WebSocket) -> None: + await websocket.accept() + task_id = f"deprecated_funasr_ws_{int(time.time())}_{id(websocket)}" + try: + await websocket.send_json( + { + "type": "error", + "task_id": task_id, + "code": "FUNASR_REALTIME_REMOVED", + "message": "FunASR/Paraformer realtime websocket has been removed. Use /ws/v1/asr/qwen instead.", + } + ) + except Exception: + pass + finally: + await websocket.close(code=1008, reason="Use /ws/v1/asr/qwen") + + +_qwen3_service = Qwen3ASRService() + + +@router.websocket("") +@router.websocket("/qwen") +async def qwen_asr_websocket( + websocket: WebSocket, + task_id: Optional[str] = None, +) -> None: + if task_id is None: + task_id = str(uuid.uuid4())[:8] + await _qwen3_service.handle_connection(websocket, task_id) diff --git a/app/bootstrap.py b/app/bootstrap.py new file mode 100644 index 0000000..b7c20a0 --- /dev/null +++ b/app/bootstrap.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +"""Shared bootstrap helpers for process startup.""" + +from __future__ import annotations + +import sys + + +def ensure_models_downloaded(interactive: bool) -> bool: + """Ensure declared deployment models exist locally, downloading if needed.""" + try: + from app.utils.download_models import check_all_models, download_models + + missing = check_all_models() + if not missing: + return True + + print(f"\n⚠️ 检测到 {len(missing)} 个模型未下载") + for model_id, *_ in missing: + print(f" - {model_id}") + + print("\n将自动下载缺失模型后继续启动。") + if download_models(auto_mode=True): + return True + + print("\n模型自动下载失败。") + if interactive: + print("可手动运行以下命令排查:") + print(" uv run python -m app.utils.download_models") + print(" ./scripts/prepare-models.sh") + else: + print("非交互式终端下请确认网络可用,或预先准备模型缓存。") + return False + except Exception as exc: + print(f"⚠️ 模型检查失败: {exc}") + return False + + +def run_cli_preflight() -> bool: + """Preflight checks for the CLI entrypoint.""" + try: + from app.core.accelerator import get_accelerator_info, validate_accelerator_runtime + + ok, message = validate_accelerator_runtime() + info = get_accelerator_info() + print( + "Accelerator | " + f"vendor={info.vendor} runtime={info.runtime} device={info.device} " + f"count={info.device_count}" + ) + if not ok: + print(f"加速器运行栈检查失败: {message}") + return False + except Exception as exc: + print(f"加速器检查失败: {exc}") + return False + + return ensure_models_downloaded(interactive=sys.stdin.isatty()) diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..97b189c --- /dev/null +++ b/app/core/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +""" +核心模块 +包含配置、异常、安全等基础组件 +""" diff --git a/app/core/accelerator.py b/app/core/accelerator.py new file mode 100644 index 0000000..9685e85 --- /dev/null +++ b/app/core/accelerator.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +"""Unified accelerator detection and runtime validation.""" + +from __future__ import annotations + +import os +from functools import lru_cache +from typing import Optional + +from .accelerators import ( + AcceleratorInfo, + IluvatarAcceleratorAdapter, + MetaxAcceleratorAdapter, + MThreadsAcceleratorAdapter, + NvidiaAcceleratorAdapter, +) + +SUPPORTED_ACCELERATORS = {"auto", "cpu", "nvidia", "metax", "iluvatar", "mthreads"} + + +def _cpu_info(reason: str = "") -> AcceleratorInfo: + return AcceleratorInfo( + vendor="cpu", + runtime="cpu", + device="cpu", + available=True, + reason=reason, + ) + + +def _normalize_configured(configured: Optional[str]) -> str: + value = (configured or os.getenv("ACCELERATOR") or "auto").strip().lower() + if value == "cuda": + return "nvidia" + if value in {"maca", "muxi", "mx"}: + return "metax" + if value in {"ix", "tianshu", "天数"}: + return "iluvatar" + if value in {"mthreads", "musa", "moorethreads", "摩尔线程"}: + return "mthreads" + if value not in SUPPORTED_ACCELERATORS: + raise ValueError( + f"Unsupported ACCELERATOR={value!r}; expected one of: " + f"{', '.join(sorted(SUPPORTED_ACCELERATORS))}" + ) + return value + + +def _detect_uncached(configured: Optional[str]) -> AcceleratorInfo: + accelerator = _normalize_configured(configured) + if accelerator == "cpu": + return _cpu_info("forced by ACCELERATOR=cpu") + + if accelerator == "nvidia": + return NvidiaAcceleratorAdapter().detect() + + if accelerator == "metax": + return MetaxAcceleratorAdapter().detect() + + if accelerator == "iluvatar": + return IluvatarAcceleratorAdapter().detect() + + if accelerator == "mthreads": + return MThreadsAcceleratorAdapter().detect() + + # Auto mode prefers vendor SMI commands before NVIDIA. This avoids a vendor + # PyTorch build exposing torch.cuda and being mistaken for NVIDIA. + metax = MetaxAcceleratorAdapter().detect() + if metax.available: + return metax + + iluvatar = IluvatarAcceleratorAdapter().detect() + if iluvatar.available: + return iluvatar + + mthreads = MThreadsAcceleratorAdapter().detect() + if mthreads.available: + return mthreads + + nvidia = NvidiaAcceleratorAdapter().detect() + if nvidia.available: + return nvidia + + return _cpu_info("no supported accelerator detected") + + +@lru_cache(maxsize=8) +def _detect_cached(configured: str) -> AcceleratorInfo: + return _detect_uncached(configured) + + +def detect_accelerator(configured: Optional[str] = None, *, refresh: bool = False) -> AcceleratorInfo: + """Detect the active accelerator. + + Args: + configured: Optional override matching ACCELERATOR values. + refresh: Clear cached detection before probing. + """ + normalized = _normalize_configured(configured) + if refresh: + _detect_cached.cache_clear() + return _detect_cached(normalized) + + +def get_accelerator_info(*, refresh: bool = False) -> AcceleratorInfo: + try: + from app.core.config import settings + + configured = settings.ACCELERATOR + except Exception: + configured = os.getenv("ACCELERATOR", "auto") + return detect_accelerator(configured, refresh=refresh) + + +def validate_accelerator_runtime() -> tuple[bool, str]: + """Validate explicit accelerator selections before model loading.""" + try: + from app.core.config import settings + + configured = _normalize_configured(settings.ACCELERATOR) + except Exception: + configured = _normalize_configured(os.getenv("ACCELERATOR", "auto")) + + info = detect_accelerator(configured, refresh=True) + if configured in {"auto", "cpu", ""}: + return True, "" + + if not info.available: + return False, f"ACCELERATOR={configured} requested but {info.reason}" + + if configured == "metax" and not info.metadata.get("torch_cuda_available"): + return ( + False, + "ACCELERATOR=metax detected mx-smi devices, but the active Python " + "environment does not expose torch.cuda. Run ./scripts/sync_metax_env.sh " + "or install the MetaX MACA PyTorch stack.", + ) + + if configured == "iluvatar" and not info.metadata.get("torch_cuda_available"): + return ( + False, + "ACCELERATOR=iluvatar detected ixsmi devices, but the active Python " + "environment does not expose torch.cuda. Use the official Iluvatar " + "vLLM image as the base image or install the Iluvatar PyTorch stack.", + ) + + if configured == "mthreads" and not info.metadata.get("torch_cuda_available"): + return ( + False, + "ACCELERATOR=mthreads detected mthreads-gmi devices, but the active Python " + "environment does not expose torch.cuda. Use the official Moore Threads " + "MUSA vLLM image as the base image or install the matching MUSA PyTorch stack.", + ) + + if configured == "nvidia" and not info.metadata.get("torch_cuda_available"): + return ( + False, + "ACCELERATOR=nvidia detected NVIDIA devices, but torch.cuda is not " + "available in the active Python environment. Run ./scripts/sync_gpu_env.sh.", + ) + + return True, "" diff --git a/app/core/accelerators/__init__.py b/app/core/accelerators/__init__.py new file mode 100644 index 0000000..38644ac --- /dev/null +++ b/app/core/accelerators/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +"""Accelerator adapter exports.""" + +from .base import AcceleratorInfo +from .iluvatar import IluvatarAcceleratorAdapter +from .metax import MetaxAcceleratorAdapter +from .mthreads import MThreadsAcceleratorAdapter +from .nvidia import NvidiaAcceleratorAdapter + +__all__ = [ + "AcceleratorInfo", + "IluvatarAcceleratorAdapter", + "MetaxAcceleratorAdapter", + "MThreadsAcceleratorAdapter", + "NvidiaAcceleratorAdapter", +] diff --git a/app/core/accelerators/base.py b/app/core/accelerators/base.py new file mode 100644 index 0000000..464724f --- /dev/null +++ b/app/core/accelerators/base.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +"""Shared accelerator adapter primitives.""" + +from __future__ import annotations + +import os +import re +import shutil +import subprocess +from dataclasses import dataclass, field +from typing import Optional, Protocol + + +@dataclass(frozen=True) +class AcceleratorInfo: + """Normalized hardware/runtime information used by the application.""" + + vendor: str + runtime: str + device: str + device_count: int = 0 + visible_devices: tuple[str, ...] = () + total_memory_gb: float = 0.0 + smi_command: Optional[str] = None + available: bool = False + reason: str = "" + metadata: dict[str, object] = field(default_factory=dict) + + @property + def is_gpu(self) -> bool: + return self.vendor not in {"cpu", "unknown"} and self.available + + def as_dict(self) -> dict[str, object]: + return { + "vendor": self.vendor, + "runtime": self.runtime, + "device": self.device, + "device_count": self.device_count, + "visible_devices": list(self.visible_devices), + "total_memory_gb": self.total_memory_gb, + "smi_command": self.smi_command, + "available": self.available, + "reason": self.reason, + "metadata": self.metadata, + } + + @property + def supports_sharded(self) -> bool: + value = self.metadata.get("supports_sharded") + return bool(value) + + +class AcceleratorAdapter(Protocol): + vendor: str + runtime: str + + def detect(self) -> AcceleratorInfo: + """Return normalized accelerator info.""" + + +def command_exists(command: str) -> bool: + return shutil.which(command) is not None + + +def run_command(command: list[str], timeout: float = 3.0) -> str: + try: + completed = subprocess.run( + command, + check=False, + capture_output=True, + text=True, + timeout=timeout, + ) + except (OSError, subprocess.TimeoutExpired): + return "" + + if completed.returncode != 0: + return "" + return completed.stdout.strip() + + +def parse_visible_devices(raw: str | None) -> tuple[str, ...]: + value = (raw or "").strip() + if not value or value.lower() in {"all", "none", "void"}: + return () + return tuple(part.strip() for part in value.split(",") if part.strip()) + + +def first_env_devices(names: tuple[str, ...]) -> tuple[str, ...]: + shared = parse_visible_devices(os.getenv("ASR_VISIBLE_DEVICES")) + if shared: + return shared + for name in names: + devices = parse_visible_devices(os.getenv(name)) + if devices: + return devices + return () + + +def parse_memory_gb(text: str) -> float: + """Parse the smallest memory value from common smi outputs.""" + values: list[float] = [] + for number, unit in re.findall(r"([0-9]+(?:\.[0-9]+)?)\s*(GiB|GB|MiB|MB)", text, re.I): + value = float(number) + normalized_unit = unit.lower() + if normalized_unit in {"mib", "mb"}: + value = value / 1024 + values.append(value) + return min(values) if values else 0.0 diff --git a/app/core/accelerators/iluvatar.py b/app/core/accelerators/iluvatar.py new file mode 100644 index 0000000..958315c --- /dev/null +++ b/app/core/accelerators/iluvatar.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +"""Iluvatar/Tianshu accelerator adapter.""" + +from __future__ import annotations + +import re + +from .base import ( + AcceleratorInfo, + command_exists, + first_env_devices, + parse_memory_gb, + run_command, +) + + +class IluvatarAcceleratorAdapter: + vendor = "iluvatar" + runtime = "ix" + smi_command = "ixsmi" + visible_env_names = ( + "ILUVATAR_VISIBLE_DEVICES", + "IX_VISIBLE_DEVICES", + "CUDA_VISIBLE_DEVICES", + ) + + def _query_device_count(self) -> tuple[int, str]: + list_output = run_command([self.smi_command, "-L"]) + if list_output: + lines = [line for line in list_output.splitlines() if line.strip()] + gpu_lines = [ + line + for line in lines + if re.search(r"\b(gpu|device|card)\b", line, re.I) + ] + return len(gpu_lines or lines), list_output + + table_output = run_command([self.smi_command]) + if table_output: + indexes = set(re.findall(r"(?:GPU|Device|Card)\s*[:#]?\s*([0-9]+)", table_output, re.I)) + if not indexes: + indexes = set(re.findall(r"^\s*\|\s*([0-9]+)\s+", table_output, re.M)) + if indexes: + return len(indexes), table_output + if re.search(r"\bIluvatar\b|\b天数\b|\bIX\b", table_output, re.I): + return 1, table_output + + return 0, "" + + def detect(self) -> AcceleratorInfo: + visible_devices = first_env_devices(self.visible_env_names) + smi_available = command_exists(self.smi_command) + if not smi_available: + return AcceleratorInfo( + vendor=self.vendor, + runtime=self.runtime, + device="cpu", + visible_devices=visible_devices, + smi_command=None, + available=False, + reason="ixsmi not found", + ) + + device_count, raw_output = self._query_device_count() + if visible_devices: + device_count = min(device_count or len(visible_devices), len(visible_devices)) + + torch_cuda_available = False + torch_count = 0 + torch_memory_gb = 0.0 + torch_version = "" + try: + import torch + + torch_version = getattr(torch, "__version__", "") + torch_cuda_available = bool(torch.cuda.is_available()) + if torch_cuda_available: + torch_count = int(torch.cuda.device_count()) + torch_memory_gb = min( + torch.cuda.get_device_properties(i).total_memory / (1024**3) + for i in range(torch_count) + ) + except Exception: + pass + + if torch_count: + device_count = min(torch_count, len(visible_devices)) if visible_devices else torch_count + total_memory_gb = torch_memory_gb or parse_memory_gb(raw_output) + available = device_count > 0 + + return AcceleratorInfo( + vendor=self.vendor, + runtime=self.runtime, + device="cuda:0" if available else "cpu", + device_count=device_count, + visible_devices=visible_devices, + total_memory_gb=total_memory_gb, + smi_command=self.smi_command, + available=available, + reason="" if available else "ixsmi did not report any device", + metadata={ + "torch_cuda_available": torch_cuda_available, + "torch_version": torch_version, + "supports_sharded": available, + }, + ) diff --git a/app/core/accelerators/metax.py b/app/core/accelerators/metax.py new file mode 100644 index 0000000..4d73a4b --- /dev/null +++ b/app/core/accelerators/metax.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +"""MetaX/MuXi MACA accelerator adapter.""" + +from __future__ import annotations + +import json +import re +from typing import Any + +from .base import ( + AcceleratorInfo, + command_exists, + first_env_devices, + parse_memory_gb, + run_command, +) + + +class MetaxAcceleratorAdapter: + vendor = "metax" + runtime = "maca" + smi_command = "mx-smi" + visible_env_names = ( + "METAX_VISIBLE_DEVICES", + "MACA_VISIBLE_DEVICES", + "MX_VISIBLE_DEVICES", + ) + + def _json_output(self, command: list[str]) -> Any: + output = run_command(command) + if not output: + return None + try: + return json.loads(output) + except json.JSONDecodeError: + return None + + def _walk_json(self, value: Any): + if isinstance(value, dict): + yield value + for child in value.values(): + yield from self._walk_json(child) + elif isinstance(value, list): + for item in value: + yield from self._walk_json(item) + + def _count_from_json(self, value: Any) -> int: + max_index = -1 + device_like = 0 + for item in self._walk_json(value): + keys = {str(key).lower(): key for key in item.keys()} + if any(key in keys for key in ("gpu id", "gpu_id", "gpu", "device id", "device_id", "index")): + device_like += 1 + for key_name in ("gpu id", "gpu_id", "device id", "device_id", "index", "id"): + original = keys.get(key_name) + if original is None: + continue + try: + max_index = max(max_index, int(item[original])) + except (TypeError, ValueError): + continue + if max_index >= 0: + return max_index + 1 + return device_like + + def _memory_from_json(self, value: Any) -> float: + memory_values: list[float] = [] + for item in self._walk_json(value): + for raw_key, raw_value in item.items(): + key = str(raw_key).lower() + if not any(token in key for token in ("memory", "mem", "hbm", "vram", "容量")): + continue + if isinstance(raw_value, (int, float)): + numeric = float(raw_value) + # mx-smi reports are commonly MiB for memory counters. + if numeric > 1024: + numeric = numeric / 1024 + memory_values.append(numeric) + continue + if isinstance(raw_value, str): + parsed = parse_memory_gb(raw_value) + if parsed > 0: + memory_values.append(parsed) + return min(memory_values) if memory_values else 0.0 + + def _query_device_count(self) -> tuple[int, str]: + json_info = self._json_output([self.smi_command, "-j"]) + json_count = self._count_from_json(json_info) + if json_count > 0: + return json_count, json.dumps(json_info, ensure_ascii=False) + + list_output = run_command([self.smi_command, "-L"]) + if list_output: + lines = [line for line in list_output.splitlines() if line.strip()] + gpu_lines = [ + line + for line in lines + if re.search(r"\b(gpu|device|card)\b", line, re.I) + ] + return len(gpu_lines or lines), list_output + + table_output = run_command([self.smi_command]) + if table_output: + indexes = set(re.findall(r"(?:GPU|Device|Card)\s*[:#]?\s*([0-9]+)", table_output, re.I)) + if not indexes: + indexes = set(re.findall(r"^\s*\|\s*([0-9]+)\s+", table_output, re.M)) + return len(indexes), table_output + + return 0, "" + + def _query_memory_gb(self, fallback_output: str) -> float: + for command in ( + [self.smi_command, "--show-memory", "-j"], + [self.smi_command, "--show-hwinfo", "-j"], + [self.smi_command, "-j"], + ): + parsed = self._memory_from_json(self._json_output(command)) + if parsed > 0: + return parsed + + memory_output = run_command([self.smi_command, "--show-memory"]) + hwinfo_output = run_command([self.smi_command, "--show-hwinfo"]) + return ( + parse_memory_gb(memory_output) + or parse_memory_gb(hwinfo_output) + or parse_memory_gb(fallback_output) + ) + + def detect(self) -> AcceleratorInfo: + visible_devices = first_env_devices(self.visible_env_names) + smi_available = command_exists(self.smi_command) + if not smi_available: + return AcceleratorInfo( + vendor=self.vendor, + runtime=self.runtime, + device="cpu", + visible_devices=visible_devices, + smi_command=None, + available=False, + reason="mx-smi not found", + ) + + device_count, raw_output = self._query_device_count() + if visible_devices: + device_count = min(device_count or len(visible_devices), len(visible_devices)) + total_memory_gb = self._query_memory_gb(raw_output) + + torch_cuda_available = False + torch_version = "" + try: + import torch + + torch_version = getattr(torch, "__version__", "") + torch_cuda_available = bool(torch.cuda.is_available()) + except Exception: + pass + + available = device_count > 0 + return AcceleratorInfo( + vendor=self.vendor, + runtime=self.runtime, + device="cuda:0" if available else "cpu", + device_count=device_count, + visible_devices=visible_devices, + total_memory_gb=total_memory_gb, + smi_command=self.smi_command, + available=available, + reason="" if available else "mx-smi did not report any device", + metadata={ + "torch_cuda_available": torch_cuda_available, + "torch_version": torch_version, + "supports_sharded": available, + }, + ) diff --git a/app/core/accelerators/mthreads.py b/app/core/accelerators/mthreads.py new file mode 100644 index 0000000..34e4b6e --- /dev/null +++ b/app/core/accelerators/mthreads.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +"""Moore Threads / MUSA accelerator adapter.""" + +from __future__ import annotations + +import re + +from .base import ( + AcceleratorInfo, + command_exists, + first_env_devices, + parse_memory_gb, + run_command, +) + + +class MThreadsAcceleratorAdapter: + vendor = "mthreads" + runtime = "musa" + smi_command = "mthreads-gmi" + visible_env_names = ( + "MTHREADS_VISIBLE_DEVICES", + "MUSA_VISIBLE_DEVICES", + "CUDA_VISIBLE_DEVICES", + ) + + def _query_device_count(self) -> tuple[int, str]: + for command in ( + [self.smi_command, "-L"], + [self.smi_command, "list"], + [self.smi_command], + ): + output = run_command(command) + if not output: + continue + lines = [line for line in output.splitlines() if line.strip()] + gpu_lines = [ + line + for line in lines + if re.search(r"\b(gpu|device|card|musa|mthreads|moore)\b", line, re.I) + ] + if gpu_lines: + return len(gpu_lines), output + + indexes = set( + re.findall(r"(?:GPU|Device|Card)\s*[:#]?\s*([0-9]+)", output, re.I) + ) + if not indexes: + indexes = set(re.findall(r"^\s*\|\s*([0-9]+)\s+", output, re.M)) + if indexes: + return len(indexes), output + + if re.search(r"\bMUSA\b|\bMoore\s+Threads\b|\bMThreads\b", output, re.I): + return max(len(lines), 1), output + + return 0, "" + + def detect(self) -> AcceleratorInfo: + visible_devices = first_env_devices(self.visible_env_names) + smi_available = command_exists(self.smi_command) + if not smi_available: + return AcceleratorInfo( + vendor=self.vendor, + runtime=self.runtime, + device="cpu", + visible_devices=visible_devices, + smi_command=None, + available=False, + reason="mthreads-gmi not found", + ) + + device_count, raw_output = self._query_device_count() + if visible_devices: + device_count = min(device_count or len(visible_devices), len(visible_devices)) + + torch_cuda_available = False + torch_count = 0 + torch_memory_gb = 0.0 + torch_version = "" + try: + import torch + + torch_version = getattr(torch, "__version__", "") + torch_cuda_available = bool(torch.cuda.is_available()) + if torch_cuda_available: + torch_count = int(torch.cuda.device_count()) + torch_memory_gb = min( + torch.cuda.get_device_properties(i).total_memory / (1024**3) + for i in range(torch_count) + ) + except Exception: + pass + + if torch_count: + device_count = min(torch_count, len(visible_devices)) if visible_devices else torch_count + total_memory_gb = torch_memory_gb or parse_memory_gb(raw_output) + available = device_count > 0 + + return AcceleratorInfo( + vendor=self.vendor, + runtime=self.runtime, + device="cuda:0" if available else "cpu", + device_count=device_count, + visible_devices=visible_devices, + total_memory_gb=total_memory_gb, + smi_command=self.smi_command, + available=available, + reason="" if available else "mthreads-gmi did not report any device", + metadata={ + "torch_cuda_available": torch_cuda_available, + "torch_version": torch_version, + "supports_sharded": available, + }, + ) diff --git a/app/core/accelerators/nvidia.py b/app/core/accelerators/nvidia.py new file mode 100644 index 0000000..19fe621 --- /dev/null +++ b/app/core/accelerators/nvidia.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +"""NVIDIA CUDA accelerator adapter.""" + +from __future__ import annotations + +from .base import ( + AcceleratorInfo, + command_exists, + first_env_devices, + parse_memory_gb, + run_command, +) + + +class NvidiaAcceleratorAdapter: + vendor = "nvidia" + runtime = "cuda" + smi_command = "nvidia-smi" + + def detect(self) -> AcceleratorInfo: + visible_devices = first_env_devices(("CUDA_VISIBLE_DEVICES",)) + smi_available = command_exists(self.smi_command) + smi_count = 0 + smi_memory_gb = 0.0 + + if smi_available: + indexes_output = run_command( + [ + self.smi_command, + "--query-gpu=index", + "--format=csv,noheader,nounits", + ] + ) + if indexes_output: + smi_count = len([line for line in indexes_output.splitlines() if line.strip()]) + memory_output = run_command( + [ + self.smi_command, + "--query-gpu=memory.total", + "--format=csv,noheader", + ] + ) + smi_memory_gb = parse_memory_gb(memory_output) + + torch_available = False + torch_count = 0 + torch_memory_gb = 0.0 + torch_version = "" + try: + import torch + + torch_version = getattr(torch, "__version__", "") + torch_available = bool(torch.cuda.is_available()) + if torch_available: + torch_count = int(torch.cuda.device_count()) + torch_memory_gb = min( + torch.cuda.get_device_properties(i).total_memory / (1024**3) + for i in range(torch_count) + ) + except Exception: + pass + + device_count = torch_count or smi_count + if visible_devices: + device_count = min(device_count or len(visible_devices), len(visible_devices)) + total_memory_gb = torch_memory_gb or smi_memory_gb + available = bool(torch_available or smi_count > 0) + reason = "" if available else "nvidia runtime not detected" + + return AcceleratorInfo( + vendor=self.vendor, + runtime=self.runtime, + device="cuda:0" if available else "cpu", + device_count=device_count, + visible_devices=visible_devices, + total_memory_gb=total_memory_gb, + smi_command=self.smi_command if smi_available else None, + available=available, + reason=reason, + metadata={ + "torch_cuda_available": torch_available, + "torch_version": torch_version, + "supports_sharded": available, + }, + ) diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..f6feb65 --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,536 @@ +# -*- coding: utf-8 -*- +""" +统一配置管理 +ASR语音识别配置选项 +""" + +import os +from typing import Optional +from pathlib import Path + + +class Settings: + """统一应用配置类""" + + # 应用信息 + APP_NAME: str = "Qwen3-ASR Server" + APP_VERSION: str = "1.0.1" + APP_DESCRIPTION: str = "Qwen3-ASR speech recognition API service" + + # 服务器配置 + HOST: str = "0.0.0.0" + PORT: int = 8000 + DEBUG: bool = False + + # 鉴权配置 + API_KEY: Optional[str] = None # 从环境变量API_KEY读取,如果为None则鉴权可选 + + # 设备配置 + ACCELERATOR: str = "auto" # auto, cpu, nvidia, metax, iluvatar, mthreads + DEVICE: str = "auto" # auto, cpu, cuda:0 + ASR_DEPLOY_TOPOLOGY: str = "isolated" # isolated, sharded, auto + + # 路径配置 + BASE_DIR: Path = Path(__file__).parent.parent.parent + DATA_DIR: str = str(BASE_DIR / "data") + TEMP_DIR: str = str(BASE_DIR / "data" / "temp") + # 项目总模型目录。实际模型目录直接扁平化到: + # /models/{Qwen,iic,damo,...} + MODELS_DIR: str = str(BASE_DIR / "models") + # ModelScope 会在 MODELSCOPE_CACHE 下创建 models/{publisher}/{model_name}。 + # 因此 cache 根目录应指向 models 的上一级,实际运行模型根目录仍由 MODELSCOPE_PATH 指定。 + MODELSCOPE_CACHE: str = str(BASE_DIR) + MODELSCOPE_PATH: str = str(BASE_DIR / "models") + + # 日志配置 + LOG_LEVEL: str = "INFO" + LOG_FILE: Optional[str] = str(BASE_DIR / "data" / "logs" / "qwen3-asr.log") + LOG_MAX_BYTES: int = 20 * 1024 * 1024 # 20MB + LOG_BACKUP_COUNT: int = 50 # 保留50个备份文件 + + # ASR模型配置 + WS_MAX_BUFFER_SIZE: int = 10 * 16000 # WebSocket音频缓冲区最大大小(10秒@16kHz) + + FUNASR_AUTOMODEL_KWARGS = { + "trust_remote_code": False, + "disable_update": True, + "disable_pbar": True, + "disable_log": True, # 禁用FunASR的tables输出 + "local_files_only": True, # 强制使用本地模型,禁止联网下载 + } + ASR_MODELS_CONFIG: str = str(BASE_DIR / "app/services/asr/models.json") + ASR_ENABLE_REALTIME_PUNC: bool = True # 是否启用实时标点模型(用于中间结果展示) + VAD_MODEL: str = "damo/speech_fsmn_vad_zh-cn-16k-common-pytorch" + PUNC_MODEL: str = "iic/punc_ct-transformer_zh-cn-common-vocab272727-pytorch" + PUNC_REALTIME_MODEL: str = ( + "iic/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727" + ) + + # 流式ASR远场过滤配置 + ASR_ENABLE_NEARFIELD_FILTER: bool = True # 是否启用远场声音过滤 + ASR_NEARFIELD_RMS_THRESHOLD: float = 0.01 # RMS能量阈值(宽松模式,适合大多数场景) + # 音频处理配置 + MAX_AUDIO_SIZE: int = 2048 * 1024 * 1024 # 2GB + + # 批处理推理配置(GPU 真并行) + ASR_BATCH_SIZE: int = 4 # ASR 批处理大小(同时推理的片段数),建议 2-8 + ASR_ENABLE_WORD_TIMESTAMPS: bool = False # 全局字词级时间戳开关;关闭时不预热 forced aligner,接口参数也默认隐藏 + + # 音频分段配置 + MAX_SEGMENT_SEC: float = 60.0 # Max offline ASR segment duration in seconds. + + # Runtime 并发配置(按 backend 独立控制) + QWEN_VLLM_SHARED_CONCURRENCY: int = 8 + QWEN_VLLM_ENFORCE_EAGER: bool = True + QWEN_RUST_CPU_WORKERS: int = 4 + QWEN_RUST_ASR_CONCURRENCY: int = 0 + QWEN_RUST_ALIGN_CONCURRENCY: int = 0 + FUNASR_WORKERS: int = 1 + + # 声纹数据库配置(与 Model-Test-New 使用同一套 PostgreSQL/pgvector 表结构) + SPEAKER_DB_ENABLED: bool = True + DB_USER: str = "postgres" + DB_PASSWORD: str = "postgres" + DB_NAME: str = "asr_db" + DB_HOST: str = "127.0.0.1" + DB_PORT: int = 5432 + DB_POOL_MAX_SIZE: int = 5 + SV_MODEL: str = "iic/speech_campplus_sv_zh-cn_16k-common" + SV_MODEL_REVISION: str = "v2.0.2" + REALTIME_SV_MODEL: str = "iic/speech_eres2netv2_sv_zh-cn_16k-common" + REALTIME_SV_MODEL_REVISION: str = "" + SV_THRESHOLD: float = 0.6 + REALTIME_MAX_SEGMENT_SEC: float = 12.0 + REALTIME_MAX_SEGMENT_TAIL_SEC: float = 1.6 + REALTIME_FORCE_STABLE_SEGMENT_SEC: float = 8.0 + REALTIME_FORCE_STABLE_MIN_CHARS: int = 24 + REALTIME_MIN_PARTIAL_SEC: float = 0.45 + REALTIME_PARTIAL_EMIT_INTERVAL_SEC: float = 0.25 + REALTIME_PARTIAL_WINDOW_SEC: float = 8.0 + REALTIME_STREAM_CHUNK_SEC: float = 1.2 + REALTIME_STREAM_MAX_PENDING_CHUNKS: int = 3 + REALTIME_STREAM_WINDOW_SEC: float = 8.0 + REALTIME_STREAM_STABLE_TAIL_CHARS: int = 8 + REALTIME_STREAM_STABLE_MIN_GROW_CHARS: int = 2 + REALTIME_STREAM_DIVERGENCE_TOLERANCE_CHARS: int = 2 + REALTIME_PARTIAL_HOLDBACK_CHARS: int = 6 + REALTIME_LONGFORM_MIN_SEC: float = 8.0 + REALTIME_LONGFORM_CHUNK_SEC: float = 6.0 + REALTIME_LONGFORM_OVERLAP_SEC: float = 1.2 + REALTIME_VAD_CHECK_INTERVAL_SEC: float = 0.8 + REALTIME_VAD_FINALIZE_SILENCE_SEC: float = 0.6 + REALTIME_FINAL_SEGMENT_SOFT_LIMIT_SEC: float = 8.0 + REALTIME_FINAL_SEGMENT_HARD_LIMIT_SEC: float = 12.0 + REALTIME_ENABLE_DIARIZATION: bool = True + REALTIME_ENABLE_SEGMENT_REFINE: bool = False + REALTIME_DIARIZATION_MIN_SEC: float = 3.0 + REALTIME_DIARIZATION_LOOKBACK_SEC: float = 3.0 + REALTIME_DIARIZATION_WINDOW_SEC: float = 15.0 + REALTIME_SPEAKER_MIN_SEC: float = 1.2 + REALTIME_SPEAKER_CLUSTER_THRESHOLD: float = 0.75 + REALTIME_UNKNOWN_SPK_CLUSTER_THRESHOLD: float = 0.58 + REALTIME_RECENT_UNKNOWN_SPK_THRESHOLD: float = 0.50 + REALTIME_SPEAKER_CONFIRM_THRESHOLD: float = 0.62 + REALTIME_REGISTRY_MIN_CLUSTER_CONFIDENCE: float = 0.72 + REALTIME_SPEAKER_MAX_SLOTS: int = 8 + REALTIME_SESSION_RESUME_TTL_SEC: int = 120 # WebSocket 断线后保留会话上下文的秒数;TTL 内同 session_id 可恢复 + API_PREFIX: str = "/api/v1" + TASK_STATE_DIR: str = str(BASE_DIR / "data" / "tasks") + TASK_RETENTION_HOURS: int = 24 + + def __init__(self): + """从环境变量读取配置""" + self._load_from_env() + self._ensure_directories() + + def _load_from_env(self): + """从环境变量加载配置""" + # 服务器配置 + self.HOST = os.getenv("HOST", self.HOST) + self.PORT = int(os.getenv("PORT", str(self.PORT))) + self.DEBUG = os.getenv("DEBUG", "false").lower() == "true" + + self.DATA_DIR = os.getenv("DATA_DIR", self.DATA_DIR) + self.TEMP_DIR = os.getenv("TEMP_DIR", self.TEMP_DIR) + + # 日志配置 + self.LOG_LEVEL = os.getenv("LOG_LEVEL", self.LOG_LEVEL) + self.LOG_FILE = os.getenv("LOG_FILE", self.LOG_FILE) + self.LOG_MAX_BYTES = int(os.getenv("LOG_MAX_BYTES", str(self.LOG_MAX_BYTES))) + self.LOG_BACKUP_COUNT = int( + os.getenv("LOG_BACKUP_COUNT", str(self.LOG_BACKUP_COUNT)) + ) + + # 鉴权配置:空值/空白统一视为未配置 + self.API_KEY = (os.getenv("API_KEY") or "").strip() or None + + # 设备配置 + self.ACCELERATOR = os.getenv("ACCELERATOR", self.ACCELERATOR) + self.DEVICE = os.getenv("DEVICE", self.DEVICE) + self.ASR_DEPLOY_TOPOLOGY = os.getenv( + "ASR_DEPLOY_TOPOLOGY", + self.ASR_DEPLOY_TOPOLOGY, + ).strip().lower() + + # 模型缓存路径 + self.MODELS_DIR = os.getenv("MODELS_DIR", self.MODELS_DIR) + self.MODELSCOPE_CACHE = os.getenv("MODELSCOPE_CACHE", self.MODELSCOPE_CACHE) + self.MODELSCOPE_PATH = os.getenv("MODELSCOPE_PATH", self.MODELSCOPE_PATH) + + # 给第三方库补齐默认缓存环境变量,允许用户自行覆盖 + os.environ.setdefault("MODELS_DIR", self.MODELS_DIR) + os.environ.setdefault("MODELSCOPE_CACHE", self.MODELSCOPE_CACHE) + os.environ.setdefault("MODELSCOPE_PATH", self.MODELSCOPE_PATH) + + # ASR模型配置 + self.ASR_ENABLE_REALTIME_PUNC = ( + os.getenv("ASR_ENABLE_REALTIME_PUNC", "true").lower() == "true" + ) + + # WebSocket缓冲区配置 + self.WS_MAX_BUFFER_SIZE = int( + os.getenv("WS_MAX_BUFFER_SIZE", str(self.WS_MAX_BUFFER_SIZE)) + ) + + # 远场过滤配置 + self.ASR_ENABLE_NEARFIELD_FILTER = ( + os.getenv("ASR_ENABLE_NEARFIELD_FILTER", "true").lower() == "true" + ) + self.ASR_NEARFIELD_RMS_THRESHOLD = float( + os.getenv( + "ASR_NEARFIELD_RMS_THRESHOLD", str(self.ASR_NEARFIELD_RMS_THRESHOLD) + ) + ) + + # 音频处理配置 + # 支持简化格式:纯数字表示MB,或带单位(如 2048MB, 2GB) + max_audio_size_str = os.getenv("MAX_AUDIO_SIZE") + if max_audio_size_str: + self.MAX_AUDIO_SIZE = self._parse_size(max_audio_size_str) + + self.ASR_BATCH_SIZE = int( + os.getenv("ASR_BATCH_SIZE", str(self.ASR_BATCH_SIZE)) + ) + self.ASR_ENABLE_WORD_TIMESTAMPS = ( + os.getenv( + "ASR_ENABLE_WORD_TIMESTAMPS", + str(self.ASR_ENABLE_WORD_TIMESTAMPS), + ).lower() + == "true" + ) + + self.MAX_SEGMENT_SEC = float( + os.getenv("MAX_SEGMENT_SEC", str(self.MAX_SEGMENT_SEC)) + ) + + self.QWEN_VLLM_SHARED_CONCURRENCY = int( + os.getenv( + "QWEN_VLLM_SHARED_CONCURRENCY", + str(self.QWEN_VLLM_SHARED_CONCURRENCY), + ) + ) + self.QWEN_VLLM_ENFORCE_EAGER = ( + os.getenv( + "QWEN_VLLM_ENFORCE_EAGER", + str(self.QWEN_VLLM_ENFORCE_EAGER), + ).lower() + == "true" + ) + self.QWEN_RUST_CPU_WORKERS = int( + os.getenv("QWEN_RUST_CPU_WORKERS", str(self.QWEN_RUST_CPU_WORKERS)) + ) + self.QWEN_RUST_ASR_CONCURRENCY = int( + os.getenv("QWEN_RUST_ASR_CONCURRENCY", str(self.QWEN_RUST_ASR_CONCURRENCY)) + ) + self.QWEN_RUST_ALIGN_CONCURRENCY = int( + os.getenv("QWEN_RUST_ALIGN_CONCURRENCY", str(self.QWEN_RUST_ALIGN_CONCURRENCY)) + ) + self.FUNASR_WORKERS = int( + os.getenv("FUNASR_WORKERS", str(self.FUNASR_WORKERS)) + ) + + self.SPEAKER_DB_ENABLED = ( + os.getenv("SPEAKER_DB_ENABLED", str(self.SPEAKER_DB_ENABLED)).lower() + == "true" + ) + self.DB_USER = os.getenv("DB_USER", self.DB_USER) + self.DB_PASSWORD = os.getenv("DB_PASSWORD", self.DB_PASSWORD) + self.DB_NAME = os.getenv("DB_NAME", self.DB_NAME) + self.DB_HOST = os.getenv("DB_HOST", self.DB_HOST) + self.DB_PORT = int(os.getenv("DB_PORT", str(self.DB_PORT))) + self.DB_POOL_MAX_SIZE = int( + os.getenv("DB_POOL_MAX_SIZE", str(self.DB_POOL_MAX_SIZE)) + ) + self.SV_MODEL = os.getenv("SV_MODEL", self.SV_MODEL) + self.SV_MODEL_REVISION = os.getenv("SV_MODEL_REVISION", self.SV_MODEL_REVISION) + self.REALTIME_SV_MODEL = os.getenv("REALTIME_SV_MODEL", self.REALTIME_SV_MODEL) + self.REALTIME_SV_MODEL_REVISION = os.getenv( + "REALTIME_SV_MODEL_REVISION", + self.REALTIME_SV_MODEL_REVISION, + ) + self.SV_THRESHOLD = float(os.getenv("SV_THRESHOLD", str(self.SV_THRESHOLD))) + self.REALTIME_MAX_SEGMENT_SEC = float( + os.getenv( + "REALTIME_MAX_SEGMENT_SEC", + str(self.REALTIME_MAX_SEGMENT_SEC), + ) + ) + self.REALTIME_MAX_SEGMENT_TAIL_SEC = float( + os.getenv( + "REALTIME_MAX_SEGMENT_TAIL_SEC", + str(self.REALTIME_MAX_SEGMENT_TAIL_SEC), + ) + ) + self.REALTIME_FORCE_STABLE_SEGMENT_SEC = float( + os.getenv( + "REALTIME_FORCE_STABLE_SEGMENT_SEC", + str(self.REALTIME_FORCE_STABLE_SEGMENT_SEC), + ) + ) + self.REALTIME_FORCE_STABLE_MIN_CHARS = int( + os.getenv( + "REALTIME_FORCE_STABLE_MIN_CHARS", + str(self.REALTIME_FORCE_STABLE_MIN_CHARS), + ) + ) + self.REALTIME_MIN_PARTIAL_SEC = float( + os.getenv( + "REALTIME_MIN_PARTIAL_SEC", + str(self.REALTIME_MIN_PARTIAL_SEC), + ) + ) + self.REALTIME_PARTIAL_EMIT_INTERVAL_SEC = float( + os.getenv( + "REALTIME_PARTIAL_EMIT_INTERVAL_SEC", + str(self.REALTIME_PARTIAL_EMIT_INTERVAL_SEC), + ) + ) + self.REALTIME_PARTIAL_WINDOW_SEC = float( + os.getenv( + "REALTIME_PARTIAL_WINDOW_SEC", + str(self.REALTIME_PARTIAL_WINDOW_SEC), + ) + ) + self.REALTIME_STREAM_CHUNK_SEC = float( + os.getenv( + "REALTIME_STREAM_CHUNK_SEC", + str(self.REALTIME_STREAM_CHUNK_SEC), + ) + ) + self.REALTIME_STREAM_MAX_PENDING_CHUNKS = int( + os.getenv( + "REALTIME_STREAM_MAX_PENDING_CHUNKS", + str(self.REALTIME_STREAM_MAX_PENDING_CHUNKS), + ) + ) + self.REALTIME_STREAM_WINDOW_SEC = float( + os.getenv( + "REALTIME_STREAM_WINDOW_SEC", + str(self.REALTIME_STREAM_WINDOW_SEC), + ) + ) + self.REALTIME_STREAM_STABLE_TAIL_CHARS = int( + os.getenv( + "REALTIME_STREAM_STABLE_TAIL_CHARS", + str(self.REALTIME_STREAM_STABLE_TAIL_CHARS), + ) + ) + self.REALTIME_STREAM_STABLE_MIN_GROW_CHARS = int( + os.getenv( + "REALTIME_STREAM_STABLE_MIN_GROW_CHARS", + str(self.REALTIME_STREAM_STABLE_MIN_GROW_CHARS), + ) + ) + self.REALTIME_STREAM_DIVERGENCE_TOLERANCE_CHARS = int( + os.getenv( + "REALTIME_STREAM_DIVERGENCE_TOLERANCE_CHARS", + str(self.REALTIME_STREAM_DIVERGENCE_TOLERANCE_CHARS), + ) + ) + self.REALTIME_PARTIAL_HOLDBACK_CHARS = int( + os.getenv( + "REALTIME_PARTIAL_HOLDBACK_CHARS", + str(self.REALTIME_PARTIAL_HOLDBACK_CHARS), + ) + ) + self.REALTIME_LONGFORM_MIN_SEC = float( + os.getenv( + "REALTIME_LONGFORM_MIN_SEC", + str(self.REALTIME_LONGFORM_MIN_SEC), + ) + ) + self.REALTIME_LONGFORM_CHUNK_SEC = float( + os.getenv( + "REALTIME_LONGFORM_CHUNK_SEC", + str(self.REALTIME_LONGFORM_CHUNK_SEC), + ) + ) + self.REALTIME_LONGFORM_OVERLAP_SEC = float( + os.getenv( + "REALTIME_LONGFORM_OVERLAP_SEC", + str(self.REALTIME_LONGFORM_OVERLAP_SEC), + ) + ) + self.REALTIME_VAD_CHECK_INTERVAL_SEC = float( + os.getenv( + "REALTIME_VAD_CHECK_INTERVAL_SEC", + str(self.REALTIME_VAD_CHECK_INTERVAL_SEC), + ) + ) + self.REALTIME_VAD_FINALIZE_SILENCE_SEC = float( + os.getenv( + "REALTIME_VAD_FINALIZE_SILENCE_SEC", + str(self.REALTIME_VAD_FINALIZE_SILENCE_SEC), + ) + ) + self.REALTIME_FINAL_SEGMENT_SOFT_LIMIT_SEC = float( + os.getenv( + "REALTIME_FINAL_SEGMENT_SOFT_LIMIT_SEC", + str(self.REALTIME_FINAL_SEGMENT_SOFT_LIMIT_SEC), + ) + ) + self.REALTIME_FINAL_SEGMENT_HARD_LIMIT_SEC = float( + os.getenv( + "REALTIME_FINAL_SEGMENT_HARD_LIMIT_SEC", + str(self.REALTIME_FINAL_SEGMENT_HARD_LIMIT_SEC), + ) + ) + self.REALTIME_ENABLE_DIARIZATION = ( + os.getenv( + "REALTIME_ENABLE_DIARIZATION", + str(self.REALTIME_ENABLE_DIARIZATION), + ).lower() + == "true" + ) + self.REALTIME_ENABLE_SEGMENT_REFINE = ( + os.getenv( + "REALTIME_ENABLE_SEGMENT_REFINE", + str(self.REALTIME_ENABLE_SEGMENT_REFINE), + ).lower() + == "true" + ) + self.REALTIME_DIARIZATION_MIN_SEC = float( + os.getenv( + "REALTIME_DIARIZATION_MIN_SEC", + str(self.REALTIME_DIARIZATION_MIN_SEC), + ) + ) + self.REALTIME_DIARIZATION_LOOKBACK_SEC = float( + os.getenv( + "REALTIME_DIARIZATION_LOOKBACK_SEC", + str(self.REALTIME_DIARIZATION_LOOKBACK_SEC), + ) + ) + self.REALTIME_DIARIZATION_WINDOW_SEC = float( + os.getenv( + "REALTIME_DIARIZATION_WINDOW_SEC", + str(self.REALTIME_DIARIZATION_WINDOW_SEC), + ) + ) + self.REALTIME_SPEAKER_MIN_SEC = float( + os.getenv( + "REALTIME_SPEAKER_MIN_SEC", + str(self.REALTIME_SPEAKER_MIN_SEC), + ) + ) + self.REALTIME_SPEAKER_CLUSTER_THRESHOLD = float( + os.getenv( + "REALTIME_SPEAKER_CLUSTER_THRESHOLD", + str(self.REALTIME_SPEAKER_CLUSTER_THRESHOLD), + ) + ) + self.REALTIME_UNKNOWN_SPK_CLUSTER_THRESHOLD = float( + os.getenv( + "REALTIME_UNKNOWN_SPK_CLUSTER_THRESHOLD", + str(self.REALTIME_UNKNOWN_SPK_CLUSTER_THRESHOLD), + ) + ) + self.REALTIME_RECENT_UNKNOWN_SPK_THRESHOLD = float( + os.getenv( + "REALTIME_RECENT_UNKNOWN_SPK_THRESHOLD", + str(self.REALTIME_RECENT_UNKNOWN_SPK_THRESHOLD), + ) + ) + self.REALTIME_SPEAKER_CONFIRM_THRESHOLD = float( + os.getenv( + "REALTIME_SPEAKER_CONFIRM_THRESHOLD", + str(self.REALTIME_SPEAKER_CONFIRM_THRESHOLD), + ) + ) + self.REALTIME_REGISTRY_MIN_CLUSTER_CONFIDENCE = float( + os.getenv( + "REALTIME_REGISTRY_MIN_CLUSTER_CONFIDENCE", + str(self.REALTIME_REGISTRY_MIN_CLUSTER_CONFIDENCE), + ) + ) + self.REALTIME_SPEAKER_MAX_SLOTS = int( + os.getenv( + "REALTIME_SPEAKER_MAX_SLOTS", + str(self.REALTIME_SPEAKER_MAX_SLOTS), + ) + ) + self.REALTIME_SESSION_RESUME_TTL_SEC = int( + os.getenv( + "REALTIME_SESSION_RESUME_TTL_SEC", + str(self.REALTIME_SESSION_RESUME_TTL_SEC), + ) + ) + self.API_PREFIX = os.getenv("API_PREFIX", self.API_PREFIX) + self.TASK_STATE_DIR = os.getenv("TASK_STATE_DIR", self.TASK_STATE_DIR) + self.TASK_RETENTION_HOURS = int( + os.getenv("TASK_RETENTION_HOURS", str(self.TASK_RETENTION_HOURS)) + ) + + def _parse_size(self, size_str: str) -> int: + """解析带单位的大小字符串 + + 支持格式: + - 纯数字:视为 MB(如 2048 = 2048MB = 2147483648 bytes) + - 带单位:如 2GB, 2048MB, 1.5GB + """ + size_str = size_str.strip().upper() + + # 如果纯数字,视为 MB + if size_str.isdigit(): + return int(size_str) * 1024 * 1024 + + # 带单位的处理 + if size_str.endswith('GB'): + return int(float(size_str[:-2]) * 1024 * 1024 * 1024) + elif size_str.endswith('MB'): + return int(float(size_str[:-2]) * 1024 * 1024) + elif size_str.endswith('KB'): + return int(float(size_str[:-2]) * 1024) + else: + # 默认视为字节 + return int(size_str) + + def _ensure_directories(self): + """确保必需的目录存在""" + os.makedirs(self.TEMP_DIR, exist_ok=True) + if self.LOG_FILE: + os.makedirs(os.path.dirname(self.LOG_FILE), exist_ok=True) + os.makedirs(self.MODELS_DIR, exist_ok=True) + os.makedirs(self.MODELSCOPE_CACHE, exist_ok=True) + os.makedirs(self.MODELSCOPE_PATH, exist_ok=True) + os.makedirs(self.DATA_DIR, exist_ok=True) + os.makedirs(self.TASK_STATE_DIR, exist_ok=True) + + @property + def models_config_path(self) -> str: + """获取模型配置文件的完整路径""" + return str(self.BASE_DIR / self.ASR_MODELS_CONFIG) + + @property + def docs_url(self) -> Optional[str]: + """获取文档URL""" + return "/docs" + + @property + def redoc_url(self) -> Optional[str]: + """获取ReDoc URL""" + return "/redoc" + + +# 全局配置实例 +settings = Settings() diff --git a/app/core/database.py b/app/core/database.py new file mode 100644 index 0000000..593e5b8 --- /dev/null +++ b/app/core/database.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +"""PostgreSQL/pgvector storage for registered speaker embeddings.""" + +from __future__ import annotations + +import logging +from typing import Optional + +import numpy as np + +from app.core.config import settings + +logger = logging.getLogger(__name__) + +try: + import asyncpg +except ImportError: # pragma: no cover - runtime dependency is installed in Docker images. + asyncpg = None # type: ignore[assignment] + + +class PgSpeakerStorage: + """Small asyncpg wrapper shared by speaker registration and ASR matching.""" + + def __init__(self) -> None: + self.pool: Optional[asyncpg.Pool] = None + + @property + def is_enabled(self) -> bool: + return bool(settings.SPEAKER_DB_ENABLED) + + @property + def is_connected(self) -> bool: + return self.pool is not None + + async def _create_database_if_not_exists(self) -> None: + system_config = { + "user": settings.DB_USER, + "password": settings.DB_PASSWORD, + "database": "postgres", + "host": settings.DB_HOST, + "port": settings.DB_PORT, + } + connection: Optional[asyncpg.Connection] = None + try: + if asyncpg is None: + raise RuntimeError("asyncpg is not installed") + connection = await asyncpg.connect(**system_config) + exists = await connection.fetchval( + "SELECT 1 FROM pg_database WHERE datname = $1", + settings.DB_NAME, + ) + if not exists: + await connection.execute(f'CREATE DATABASE "{settings.DB_NAME}"') + except Exception as exc: + logger.warning("尝试自动创建数据库失败,将继续连接业务库: %s", exc) + finally: + if connection is not None: + await connection.close() + + async def connect(self) -> None: + if not self.is_enabled: + logger.info("声纹数据库未启用,跳过 PostgreSQL 连接") + return + if asyncpg is None: + raise RuntimeError("asyncpg is not installed") + if self.pool is not None: + return + + await self._create_database_if_not_exists() + self.pool = await asyncpg.create_pool( + user=settings.DB_USER, + password=settings.DB_PASSWORD, + database=settings.DB_NAME, + host=settings.DB_HOST, + port=settings.DB_PORT, + min_size=1, + max_size=settings.DB_POOL_MAX_SIZE, + ) + async with self.pool.acquire() as connection: + await connection.execute("CREATE EXTENSION IF NOT EXISTS vector;") + await connection.execute( + """ + CREATE TABLE IF NOT EXISTS speakers ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + user_id TEXT, + embedding vector(192), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + """ + ) + await connection.execute( + """ + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name='speakers' AND column_name='user_id' + ) THEN + ALTER TABLE speakers ADD COLUMN user_id TEXT; + END IF; + END $$; + """ + ) + await connection.execute( + """ + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = 'speakers_embedding_idx' + ) THEN + CREATE INDEX speakers_embedding_idx + ON speakers USING hnsw (embedding vector_cosine_ops) + WITH (m = 16, ef_construction = 64); + END IF; + END $$; + """ + ) + logger.info( + "声纹数据库已连接: %s:%s/%s", + settings.DB_HOST, + settings.DB_PORT, + settings.DB_NAME, + ) + + async def close(self) -> None: + if self.pool is not None: + await self.pool.close() + self.pool = None + + def _require_pool(self) -> asyncpg.Pool: + if self.pool is None: + raise RuntimeError("Speaker database pool is not initialized") + return self.pool + + @staticmethod + def _embedding_text(embedding: np.ndarray) -> str: + return str(np.asarray(embedding, dtype=np.float32).flatten().tolist()) + + async def save_speaker( + self, + name: str, + embedding: np.ndarray, + user_id: Optional[str] = None, + ) -> dict[str, Optional[str]]: + pool = self._require_pool() + async with pool.acquire() as connection: + row = await connection.fetchrow( + """ + INSERT INTO speakers (name, embedding, user_id) + VALUES ($1, $2, $3) + ON CONFLICT (name) DO UPDATE + SET embedding = EXCLUDED.embedding, user_id = EXCLUDED.user_id + RETURNING id, name, user_id; + """, + name, + self._embedding_text(embedding), + user_id, + ) + return { + "id": str(row["id"]) if row is not None else None, + "name": row["name"] if row is not None else name, + "user_id": row["user_id"] if row is not None else user_id, + } + + async def identify_speaker( + self, + embedding: np.ndarray, + threshold: float, + ) -> dict[str, Optional[str]]: + pool = self._require_pool() + distance_threshold = 1.0 - float(threshold) + async with pool.acquire() as connection: + row = await connection.fetchrow( + """ + SELECT id, name, user_id, (embedding <=> $1::vector) AS distance + FROM speakers + ORDER BY embedding <=> $1::vector + LIMIT 1; + """, + self._embedding_text(embedding), + ) + if row is not None and float(row["distance"]) < distance_threshold: + return { + "id": str(row["id"]), + "name": row["name"], + "user_id": row["user_id"], + } + return {"id": None, "name": None, "user_id": None} + + async def list_speakers(self) -> list[dict[str, Optional[str]]]: + pool = self._require_pool() + async with pool.acquire() as connection: + rows = await connection.fetch( + "SELECT id, name, user_id, created_at FROM speakers ORDER BY id ASC" + ) + return [ + { + "id": str(row["id"]), + "name": row["name"], + "user_id": row["user_id"], + "created_at": row["created_at"].isoformat() + if row["created_at"] + else None, + } + for row in rows + ] + + async def delete_speaker(self, speaker_id: int) -> bool: + pool = self._require_pool() + async with pool.acquire() as connection: + result = await connection.execute( + "DELETE FROM speakers WHERE id = $1", + speaker_id, + ) + return result != "DELETE 0" + + +pg_speaker_db = PgSpeakerStorage() diff --git a/app/core/device.py b/app/core/device.py new file mode 100644 index 0000000..11edf2a --- /dev/null +++ b/app/core/device.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +"""Centralized device detection utility. + +This module keeps the historic device helpers while delegating hardware +probing to ``app.core.accelerator``. +""" + +from app.core.accelerator import get_accelerator_info + + +def detect_device(configured: str = "auto") -> str: + """Resolve a device configuration string to a concrete PyTorch device. + + Priority for ``"auto"``: configured accelerator > detected GPU > CPU. + + Args: + configured: Value from ``settings.DEVICE`` or caller override. + Accepted: ``"auto"``, ``"cpu"``, ``"cuda:0"``, ``"npu:0"``, etc. + + Returns: + A device string ready for ``torch.device()`` / FunASR / ModelScope. + """ + device = configured.strip().lower() + + if device == "auto": + return get_accelerator_info().device + + # Normalize bare "cuda" to "cuda:0" + if device == "cuda": + return "cuda:0" + + if device == "mps": + return "cpu" + + return device + + +def is_cuda() -> bool: + """True when the active runtime exposes a CUDA-compatible device.""" + info = get_accelerator_info() + return info.available and info.device.startswith("cuda") + + +def has_gpu() -> bool: + """True when a supported accelerator is available.""" + return get_accelerator_info().is_gpu + + +def get_vram_gb() -> float: + """Return usable accelerator memory in GB.""" + return get_accelerator_info().total_memory_gb diff --git a/app/core/exceptions.py b/app/core/exceptions.py new file mode 100644 index 0000000..f7aa3b1 --- /dev/null +++ b/app/core/exceptions.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +""" +统一异常处理模块 +定义所有自定义异常类和错误处理函数 +""" + +from datetime import datetime, timezone +from fastapi import HTTPException, Request +from fastapi.exception_handlers import ( + http_exception_handler as fastapi_http_exception_handler, + request_validation_exception_handler as fastapi_validation_exception_handler, +) +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse +import logging +from typing import Any, Dict, Optional + +logger = logging.getLogger(__name__) + + +def _uses_meeting_legacy_response(request: Request) -> bool: + """Return True for Model-Test-New compatible offline meeting/speaker APIs.""" + path = request.url.path.rstrip("/") + method = request.method.upper() + if path.endswith("/asr/transcriptions"): + return method == "POST" + if "/asr/transcriptions/" in path: + return method == "GET" + if path.endswith("/speakers"): + return method == "POST" + return False + + +def get_iso_timestamp() -> str: + """获取ISO 8601格式的UTC时间戳""" + return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + +def create_error_response( + error_code: str, + message: str, + task_id: str = "", + details: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + """ + 创建标准错误响应格式 + + Args: + error_code: 错误代码(如 INVALID_PARAMETER) + message: 人类可读的错误信息 + task_id: 任务ID(可选) + details: 额外详细信息(可选) + + Returns: + 标准错误响应字典 + """ + return { + "error_code": error_code, + "message": message, + "task_id": task_id or "", + "timestamp": get_iso_timestamp(), + "details": details or {}, + } + + +def get_http_status_code(status_code: int) -> int: + """Map internal API status code to HTTP status code.""" + return 500 if status_code >= 50000000 else 400 + + +class APIException(Exception): + """API基础异常类""" + + def __init__( + self, + status_code: int, + message: str, + task_id: str = "", + error_code: str = "", + details: Optional[Dict[str, Any]] = None, + ): + self.status_code = status_code + self.message = message + self.task_id = task_id + self.error_code = error_code or self._get_error_code(status_code) + self.details = details or {} + super().__init__(self.message) + + def _get_error_code(self, status_code: int) -> str: + """根据状态码获取错误代码""" + code_mapping = { + 20000000: "SUCCESS", + 40000000: "DEFAULT_CLIENT_ERROR", + 40000001: "AUTHENTICATION_FAILED", + 40000002: "INVALID_MESSAGE", + 40000003: "INVALID_PARAMETER", + 40000004: "IDLE_TIMEOUT", + 40000005: "TOO_MANY_REQUESTS", + 40000010: "TRIAL_EXPIRED", + 41010101: "UNSUPPORTED_SAMPLE_RATE", + 50000000: "DEFAULT_SERVER_ERROR", + 50000001: "INTERNAL_GRPC_ERROR", + } + return code_mapping.get(status_code, "UNKNOWN_ERROR") + + def to_dict(self) -> Dict[str, Any]: + """ + 将异常转换为标准错误响应字典 + + Returns: + 标准错误响应字典,包含 error_code, message, task_id, timestamp, details + """ + return create_error_response( + error_code=self.error_code, + message=self.message, + task_id=self.task_id, + details=self.details, + ) + + +# 标准异常类 +class AuthenticationException(APIException): + """身份认证异常""" + + def __init__(self, message: str, task_id: str = "", details: Optional[Dict[str, Any]] = None): + super().__init__(40000001, message, task_id, details=details) + + +class InvalidMessageException(APIException): + """无效消息异常""" + + def __init__(self, message: str, task_id: str = "", details: Optional[Dict[str, Any]] = None): + super().__init__(40000002, message, task_id, details=details) + + +class InvalidParameterException(APIException): + """无效参数异常""" + + def __init__(self, message: str, task_id: str = "", details: Optional[Dict[str, Any]] = None): + super().__init__(40000003, message, task_id, details=details) + + +class UnsupportedSampleRateException(APIException): + """不支持的采样率异常""" + + def __init__(self, message: str, task_id: str = "", details: Optional[Dict[str, Any]] = None): + super().__init__(41010101, message, task_id, details=details) + + +class DefaultServerErrorException(APIException): + """默认服务端错误异常""" + + def __init__(self, message: str, task_id: str = "", details: Optional[Dict[str, Any]] = None): + super().__init__(50000000, message, task_id, details=details) + + +# 异常处理器 +async def api_exception_handler(request: Request, exc: Exception) -> JSONResponse: + """API异常处理器""" + # FastAPI 只会在抛出 APIException 时调用此处理器 + api_exc = exc if isinstance(exc, APIException) else APIException(50000000, str(exc)) + logger.error(f"[{api_exc.task_id}] API异常: {api_exc.message}") + + # 使用标准错误格式 + response_data = api_exc.to_dict() + + # 确定HTTP状态码 + http_status_code = get_http_status_code(api_exc.status_code) + + return JSONResponse( + content=response_data, + headers={"task_id": api_exc.task_id} if api_exc.task_id else {}, + status_code=http_status_code, + ) + + +async def http_exception_handler(request: Request, exc: Exception) -> JSONResponse: + """FastAPI HTTPException handler for legacy meeting/speaker endpoints.""" + http_exc = exc if isinstance(exc, HTTPException) else HTTPException(500, str(exc)) + if not _uses_meeting_legacy_response(request): + return await fastapi_http_exception_handler(request, http_exc) + + detail = http_exc.detail + if isinstance(detail, dict): + return JSONResponse( + status_code=http_exc.status_code, + content={ + "code": detail.get("code", 4001), + "message": detail.get("message", "Request failed"), + }, + ) + return JSONResponse( + status_code=http_exc.status_code, + content={"code": 4001, "message": str(detail)}, + ) + + +async def validation_exception_handler(request: Request, exc: Exception) -> JSONResponse: + """FastAPI/Pydantic validation handler for legacy meeting/speaker endpoints.""" + validation_exc = exc if isinstance(exc, RequestValidationError) else None + if validation_exc is None: + return JSONResponse( + status_code=422, + content={"code": 1001, "message": "Invalid request"}, + ) + if not _uses_meeting_legacy_response(request): + return await fastapi_validation_exception_handler(request, validation_exc) + + errors = validation_exc.errors() + message = str(errors[0]["msg"]) if errors else "Invalid request" + return JSONResponse(status_code=422, content={"code": 1001, "message": message}) + + +async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse: + """通用异常处理器""" + logger.error(f"未处理的异常: {str(exc)}", exc_info=True) + + # 使用标准错误格式 + response_data = create_error_response( + error_code="DEFAULT_SERVER_ERROR", + message=f"内部服务错误: {str(exc)}", + ) + + return JSONResponse(content=response_data, status_code=500) diff --git a/app/core/executor.py b/app/core/executor.py new file mode 100644 index 0000000..8bcb1d4 --- /dev/null +++ b/app/core/executor.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +""" +异步执行器模块 + +用于将同步的模型推理调用放入线程池执行,避免阻塞事件循环, +实现真正的多路并发处理。 + +设计要点: +1. 使用 ThreadPoolExecutor 而非 ProcessPoolExecutor + - 模型已加载在内存中,进程间无法共享 + - GPU操作会自动释放GIL,线程池足以实现并发 + +2. 对于流式生成器,使用 asyncio.Queue 实现异步迭代 + +3. 线程池大小根据使用场景配置: + - CPU推理:受GIL限制,多线程并发收益有限,但可以让I/O不阻塞 + - GPU推理:CUDA操作释放GIL,可以实现真正并发 +""" + +import os +import asyncio +import logging +from concurrent.futures import ThreadPoolExecutor +from typing import Callable, TypeVar, Generator, AsyncGenerator, Optional +from functools import partial + +logger = logging.getLogger(__name__) + +# 类型变量 +T = TypeVar("T") + +# 全局线程池执行器 +# 默认线程数:max(4, CPU核心数),可通过环境变量覆盖 +_DEFAULT_WORKERS = max(4, os.cpu_count() or 4) +_MAX_WORKERS = int(os.getenv("INFERENCE_THREAD_POOL_SIZE", str(_DEFAULT_WORKERS))) + +_executor: Optional[ThreadPoolExecutor] = None + + +def get_executor() -> ThreadPoolExecutor: + """获取全局线程池执行器(懒加载)""" + global _executor + if _executor is None: + _executor = ThreadPoolExecutor( + max_workers=_MAX_WORKERS, + thread_name_prefix="inference_worker" + ) + logger.info(f"推理线程池已创建,最大工作线程数: {_MAX_WORKERS}") + return _executor + + +def shutdown_executor(): + """关闭线程池执行器""" + global _executor + if _executor is not None: + _executor.shutdown(wait=True) + _executor = None + logger.info("推理线程池已关闭") + + +async def run_sync(func: Callable[..., T], *args, **kwargs) -> T: + """ + 在线程池中执行同步函数,不阻塞事件循环 + + Args: + func: 同步函数 + *args: 位置参数 + **kwargs: 关键字参数 + + Returns: + 函数返回值 + + Example: + result = await run_sync(model.generate, input=audio_array, cache=cache) + """ + loop = asyncio.get_running_loop() + executor = get_executor() + + # 使用 partial 绑定参数 + if kwargs: + func_with_args = partial(func, *args, **kwargs) + else: + func_with_args = partial(func, *args) if args else func + + return await loop.run_in_executor(executor, func_with_args) + + +async def run_sync_generator( + generator_func: Callable[..., Generator[T, None, None]], + *args, + **kwargs +) -> AsyncGenerator[T, None]: + """ + 将同步生成器转换为异步生成器,在线程池中执行 + + 用于流式处理等需要逐步产出结果的场景。 + + Args: + generator_func: 返回生成器的同步函数 + *args: 位置参数 + **kwargs: 关键字参数 + + Yields: + 生成器的每个产出值 + + Example: + async for chunk in run_sync_generator(model.inference_sft, text, voice, stream=True): + await websocket.send_bytes(chunk) + """ + loop = asyncio.get_running_loop() + executor = get_executor() + queue: asyncio.Queue = asyncio.Queue() + + # 标记生成器结束的哨兵值 + _SENTINEL = object() + + def producer(): + """在线程中运行生成器,将结果放入队列""" + try: + gen = generator_func(*args, **kwargs) + for item in gen: + # 使用 call_soon_threadsafe 安全地将结果放入队列 + loop.call_soon_threadsafe(queue.put_nowait, item) + except BaseException as e: + # 发生异常时,记录日志并将异常放入队列 + logger.error(f"生成器执行异常: {type(e).__name__}: {e}") + loop.call_soon_threadsafe(queue.put_nowait, e) + finally: + # 发送结束标记 + loop.call_soon_threadsafe(queue.put_nowait, _SENTINEL) + + # 在线程池中启动生产者 + future = executor.submit(producer) + + try: + while True: + item = await queue.get() + + if item is _SENTINEL: + break + + # 使用 BaseException 捕获所有异常类型(包括 KeyboardInterrupt 等) + if isinstance(item, BaseException): + raise item + + yield item + finally: + # 确保检查线程是否有未捕获的异常 + if future.done(): + try: + # 如果线程已完成,检查是否有异常 + future.result() + except Exception as e: + logger.error(f"生成器线程异常: {type(e).__name__}: {e}") + elif not future.cancelled(): + future.cancel() + + +class AsyncInferenceWrapper: + """ + 异步推理包装器 + + 将同步的模型推理方法包装为异步方法,方便复用。 + + Example: + wrapper = AsyncInferenceWrapper(asr_engine.realtime_model) + result = await wrapper.generate(input=audio_array, cache=cache) + """ + + def __init__(self, model): + self._model = model + + async def generate(self, *args, **kwargs): + """异步调用模型的 generate 方法""" + return await run_sync(self._model.generate, *args, **kwargs) + + async def inference_sft(self, *args, **kwargs): + """异步流式调用模型的 inference_sft 方法""" + async for item in run_sync_generator(self._model.inference_sft, *args, **kwargs): + yield item + + async def inference_zero_shot(self, *args, **kwargs): + """异步流式调用模型的 inference_zero_shot 方法""" + async for item in run_sync_generator(self._model.inference_zero_shot, *args, **kwargs): + yield item diff --git a/app/core/hotword_resolver.py b/app/core/hotword_resolver.py new file mode 100644 index 0000000..4b40fae --- /dev/null +++ b/app/core/hotword_resolver.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +"""Rule-based hotword correction for ASR post-processing.""" + +from __future__ import annotations + +import re +from difflib import SequenceMatcher +from typing import Any + +try: + from pypinyin import lazy_pinyin +except Exception: # pragma: no cover - optional runtime enhancement. + lazy_pinyin = None # type: ignore[assignment] + + +_HOTWORD_PROMPT_LEAK_PATTERNS = ( + re.compile(r"^\s*Use this context when resolving named entities:\s*", re.IGNORECASE), + re.compile(r"^\s*(?:上下文信息[::]?\s*)?热词列表[::]\s*[[\[][^\]]]{0,500}[]\]]\s*[,。,::;;\s]*"), +) + + +def parse_hotwords(hotwords: str | list[dict[str, object]] | None) -> list[dict[str, Any]]: + if isinstance(hotwords, list): + return _extract_hotword_entries(hotwords) + + raw_text = str(hotwords or "").strip() + if not raw_text: + return [] + + tokens = [part for part in re.split(r"[\s,,;;]+", raw_text) if part] + entries: list[dict[str, Any]] = [] + index = 0 + order = 0 + while index < len(tokens): + word = tokens[index].strip() + if not word: + index += 1 + continue + weight = 1.0 + if index + 1 < len(tokens): + try: + weight = float(tokens[index + 1]) + index += 2 + except ValueError: + index += 1 + else: + index += 1 + entries.append({"text": word, "weight": weight, "order": order}) + order += 1 + return entries + + +def format_hotword_prompt_context(hotwords: str | list[dict[str, object]] | None) -> str: + entries = parse_hotwords(hotwords) + if not entries: + return "" + + unique_words: list[str] = [] + for entry in entries: + word = str(entry["text"]).strip() + if word and word not in unique_words: + unique_words.append(word) + if not unique_words: + return "" + return f"热词列表:[{', '.join(unique_words)}]" + + +def strip_hotword_prompt_leakage(text: str) -> str: + cleaned = str(text or "") + changed = True + while changed and cleaned: + changed = False + for pattern in _HOTWORD_PROMPT_LEAK_PATTERNS: + updated, count = pattern.subn("", cleaned, count=1) + if count: + cleaned = updated + changed = True + return cleaned.strip() + + +def _extract_hotword_entries(hotwords: list[dict[str, object]] | None) -> list[dict[str, Any]]: + entries: list[dict[str, Any]] = [] + for index, item in enumerate(hotwords or []): + hotword_text = str(item.get("hotword", "")).strip() + if not hotword_text: + continue + try: + weight = float(item.get("weight", 1.0)) + except Exception: + weight = 1.0 + entries.append({"text": hotword_text, "weight": weight, "order": index}) + return entries + + +def _normalize_ascii_token(text: str) -> str: + return re.sub(r"[^a-z0-9]+", "", str(text or "").lower()) + + +def _common_prefix_length(left: str, right: str) -> int: + matched = 0 + for left_char, right_char in zip(left, right): + if left_char != right_char: + break + matched += 1 + return matched + + +def _common_suffix_length(left: str, right: str) -> int: + return _common_prefix_length(left[::-1], right[::-1]) + + +def _normalize_cjk_pinyin(text: str) -> tuple[str, ...]: + if lazy_pinyin is None: + return () + return tuple(part.strip().lower() for part in lazy_pinyin(str(text or ""), errors="ignore") if str(part).strip()) + + +def _replace_ascii_hotwords(text: str, hotword_entries: list[dict[str, Any]]) -> tuple[str, bool, list[str]]: + updated_text = str(text or "") + changed = False + matched_hotwords: list[str] = [] + ascii_pattern = re.compile(r"[A-Za-z][A-Za-z0-9\s._-]{0,40}") + for entry in hotword_entries: + hotword = str(entry["text"]) + if not re.search(r"[A-Za-z]", hotword): + continue + normalized_hotword = _normalize_ascii_token(hotword) + if not normalized_hotword: + continue + + def _replace_match(match: re.Match[str]) -> str: + nonlocal changed + candidate = match.group(0) + if _normalize_ascii_token(candidate) == normalized_hotword and candidate != hotword: + changed = True + if hotword not in matched_hotwords: + matched_hotwords.append(hotword) + return hotword + return candidate + + updated_text = ascii_pattern.sub(_replace_match, updated_text) + return updated_text, changed, matched_hotwords + + +def _score_cjk_candidate(candidate: str, hotword_entry: dict[str, Any]) -> tuple[int, float, int, float, int] | None: + hotword = str(hotword_entry["text"]) + if candidate == hotword or len(candidate) != len(hotword): + return None + candidate_pinyin = _normalize_cjk_pinyin(candidate) + hotword_pinyin = _normalize_cjk_pinyin(hotword) + if candidate_pinyin and candidate_pinyin == hotword_pinyin: + return (3, float(hotword_entry["weight"]), len(hotword), 1.0, -int(hotword_entry["order"])) + if len(hotword) <= 2: + return None + prefix_length = _common_prefix_length(candidate, hotword) + suffix_length = _common_suffix_length(candidate, hotword) + similarity = SequenceMatcher(None, candidate, hotword).ratio() + if prefix_length >= len(hotword) - 1 or suffix_length >= len(hotword) - 1: + return (2, float(hotword_entry["weight"]), prefix_length + suffix_length, similarity, -int(hotword_entry["order"])) + if similarity >= 0.67 and prefix_length >= 1 and suffix_length >= 1: + return (2, float(hotword_entry["weight"]), prefix_length + suffix_length, similarity, -int(hotword_entry["order"])) + return None + + +def _replace_cjk_hotwords(text: str, hotword_entries: list[dict[str, Any]]) -> tuple[str, bool, list[str]]: + updated_text = str(text or "") + changed = False + matched_hotwords: list[str] = [] + chinese_entries = [entry for entry in hotword_entries if re.fullmatch(r"[\u4e00-\u9fff]{2,12}", str(entry["text"]))] + if not chinese_entries: + return updated_text, False, matched_hotwords + candidate_lengths = sorted({len(str(entry["text"])) for entry in chinese_entries}) + token_pattern = re.compile(r"[\u4e00-\u9fff]{2,24}") + + def _replace_match(match: re.Match[str]) -> str: + nonlocal changed + token = match.group(0) + best_choice: tuple[tuple[int, float, int, float, int], int, int, str] | None = None + for hotword_length in candidate_lengths: + if hotword_length > len(token): + continue + scoped_entries = [entry for entry in chinese_entries if len(str(entry["text"])) == hotword_length] + for index in range(0, len(token) - hotword_length + 1): + candidate = token[index:index + hotword_length] + for entry in scoped_entries: + score = _score_cjk_candidate(candidate, entry) + if score is None: + continue + current_choice = (score, index, hotword_length, str(entry["text"])) + if best_choice is None or current_choice > best_choice: + best_choice = current_choice + if best_choice is None: + return token + _, begin_index, hotword_length, hotword = best_choice + token_chars = list(token) + token_chars[begin_index:begin_index + hotword_length] = list(hotword) + changed = True + if hotword not in matched_hotwords: + matched_hotwords.append(hotword) + return "".join(token_chars) + + updated_text = token_pattern.sub(_replace_match, updated_text) + return updated_text, changed, matched_hotwords + + +def apply_hotword_rules(text: str, hotwords: str | list[dict[str, object]] | None = None) -> tuple[str, bool, list[str]]: + hotword_entries = parse_hotwords(hotwords) + if not hotword_entries: + return str(text or ""), False, [] + updated_text, ascii_changed, ascii_matches = _replace_ascii_hotwords(str(text or ""), hotword_entries) + updated_text, cjk_changed, cjk_matches = _replace_cjk_hotwords(updated_text, hotword_entries) + matched_hotwords: list[str] = [] + for hotword_text in ascii_matches + cjk_matches: + if hotword_text not in matched_hotwords: + matched_hotwords.append(hotword_text) + return updated_text, ascii_changed or cjk_changed, matched_hotwords diff --git a/app/core/logging.py b/app/core/logging.py new file mode 100644 index 0000000..661d377 --- /dev/null +++ b/app/core/logging.py @@ -0,0 +1,342 @@ +# -*- coding: utf-8 -*- +""" +日志配置模块 +统一的日志配置和管理,支持多 Worker 模式 +""" + +import logging +import logging.handlers +import sys +import os +import json +from datetime import datetime, timezone +from typing import Optional, Dict, Any +from pathlib import Path +from .config import settings + + +class StructuredLogFormatter(logging.Formatter): + """结构化JSON日志格式化器 + + 将日志记录格式化为JSON格式,支持extra字段传递结构化数据。 + + 输出示例: + { + "timestamp": "2025-01-31T12:00:00Z", + "level": "INFO", + "logger": "app.services.asr", + "message": "推理完成", + "task_id": "xxx", + "duration_ms": 1234, + "audio_duration_sec": 60, + "rtf": 0.02, + "model_id": "qwen3-asr-1.7b" + } + """ + + def __init__(self, include_extra: bool = True): + """ + Args: + include_extra: 是否包含extra字段中的结构化数据 + """ + super().__init__() + self.include_extra = include_extra + self._reserved_attrs = { + 'name', 'msg', 'args', 'levelname', 'levelno', 'pathname', + 'filename', 'module', 'exc_info', 'exc_text', 'stack_info', + 'lineno', 'funcName', 'created', 'msecs', 'relativeCreated', + 'thread', 'threadName', 'processName', 'process', 'getMessage', + 'message', 'asctime' + } + + def format(self, record: logging.LogRecord) -> str: + """将日志记录格式化为JSON""" + log_data: Dict[str, Any] = { + "timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", + "level": record.levelname, + "logger": record.name, + "message": record.getMessage(), + } + + # 添加worker_id(多worker模式下) + workers = int(os.getenv("WORKERS", "1")) + if workers > 1: + log_data["worker_id"] = f"worker-{os.getpid()}" + + # 添加异常信息(如果有) + if record.exc_info: + exc_type = record.exc_info[0] + exc_value = record.exc_info[1] + if exc_type and exc_value: + log_data["exception"] = { + "type": exc_type.__name__, + "message": str(exc_value) + } + + # 添加extra字段中的结构化数据 + if self.include_extra: + extra_data = self._extract_extra_data(record) + if extra_data: + log_data.update(extra_data) + + return json.dumps(log_data, ensure_ascii=False, default=str) + + def _extract_extra_data(self, record: logging.LogRecord) -> Dict[str, Any]: + """从日志记录中提取extra数据""" + extra_data = {} + for key, value in record.__dict__.items(): + if key not in self._reserved_attrs and not key.startswith('_'): + extra_data[key] = value + return extra_data + + +class HybridLogFormatter(logging.Formatter): + """混合日志格式化器 + + 根据日志内容自动选择格式: + - 包含结构化数据(extra字段)的日志使用JSON格式 + - 普通日志使用文本格式 + + 这允许在代码中逐步迁移到结构化日志,同时保持可读性。 + """ + + def __init__( + self, + text_format: Optional[str] = None, + json_formatter: Optional[StructuredLogFormatter] = None, + ): + super().__init__() + self.text_format = text_format or "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + self.json_formatter = json_formatter or StructuredLogFormatter() + self._reserved_attrs = { + 'name', 'msg', 'args', 'levelname', 'levelno', 'pathname', + 'filename', 'module', 'exc_info', 'exc_text', 'stack_info', + 'lineno', 'funcName', 'created', 'msecs', 'relativeCreated', + 'thread', 'threadName', 'processName', 'process', 'getMessage', + 'message', 'asctime' + } + + def format(self, record: logging.LogRecord) -> str: + """根据内容选择格式""" + # 检查是否有extra数据 + has_extra = self._has_extra_data(record) + + if has_extra: + return self.json_formatter.format(record) + else: + # 使用文本格式 + text_formatter = logging.Formatter(self.text_format) + return text_formatter.format(record) + + def _has_extra_data(self, record: logging.LogRecord) -> bool: + """检查日志记录是否包含extra数据""" + for key in record.__dict__.keys(): + if key not in self._reserved_attrs and not key.startswith('_'): + return True + return False + + +def get_structured_logger(name: str) -> logging.Logger: + """获取支持结构化日志的记录器 + + 这是一个便捷函数,返回一个配置好的日志记录器, + 可以直接使用 extra 参数记录结构化数据。 + + 示例: + logger = get_structured_logger(__name__) + logger.info( + "推理完成", + extra={ + "duration_ms": 1234, + "audio_duration_sec": 60, + "rtf": 0.02, + "model_id": "qwen3-asr-1.7b" + } + ) + + Args: + name: 记录器名称 + + Returns: + 配置好的日志记录器 + """ + return logging.getLogger(name) + + +def log_inference_metrics( + logger: logging.Logger, + message: str, + task_id: Optional[str] = None, + duration_ms: Optional[float] = None, + audio_duration_sec: Optional[float] = None, + model_id: Optional[str] = None, + status: str = "success", + **kwargs +) -> None: + """记录推理性能指标 + + 这是一个辅助函数,用于统一记录推理性能指标。 + + Args: + logger: 日志记录器 + message: 日志消息 + task_id: 任务ID + duration_ms: 推理耗时(毫秒) + audio_duration_sec: 音频时长(秒) + model_id: 模型ID + status: 状态(success/error) + **kwargs: 其他结构化数据 + """ + extra: Dict[str, Any] = { + "status": status, + } + + if task_id: + extra["task_id"] = task_id + if duration_ms is not None: + extra["duration_ms"] = round(duration_ms, 2) + if audio_duration_sec is not None: + extra["audio_duration_sec"] = round(audio_duration_sec, 2) + if model_id: + extra["model_id"] = model_id + + # 计算RTF(实时率) + if duration_ms is not None and audio_duration_sec is not None and audio_duration_sec > 0: + rtf = (duration_ms / 1000) / audio_duration_sec + extra["rtf"] = round(rtf, 4) + + # 添加其他数据 + extra.update(kwargs) + + logger.info(message, extra=extra) + + +def get_worker_id() -> str: + """获取当前 Worker ID + + Returns: + Worker 标识符,格式为 'worker-{pid}' 或 'main' + """ + # 检查是否在多 worker 模式下 + workers = int(os.getenv("WORKERS", "1")) + if workers > 1: + return f"worker-{os.getpid()}" + return "main" + + +def setup_logging( + level: Optional[str] = None, + log_file: Optional[str] = None, + format_string: Optional[str] = None, + max_bytes: Optional[int] = None, + backup_count: Optional[int] = None, + worker_id: Optional[str] = None, + use_structured: bool = False, +) -> None: + """设置应用日志配置 + + Args: + level: 日志级别 + log_file: 日志文件路径 + format_string: 日志格式字符串 + max_bytes: 单个日志文件最大大小(字节) + backup_count: 保留的备份文件数量 + worker_id: Worker 标识符(多 Worker 模式下使用) + use_structured: 是否使用结构化JSON日志格式 + """ + # 使用传入的参数或配置文件中的设置 + log_level = level or settings.LOG_LEVEL + log_file_path = log_file or settings.LOG_FILE + max_file_size = max_bytes or settings.LOG_MAX_BYTES + backup_files = backup_count or settings.LOG_BACKUP_COUNT + + # 获取 Worker ID + current_worker_id = worker_id or get_worker_id() + workers = int(os.getenv("WORKERS", "1")) + worker_log_path: Optional[Path] = None + + # 确定日志格式 + if use_structured: + # 使用结构化日志格式 + formatter: logging.Formatter = StructuredLogFormatter() + else: + # 使用混合格式(普通日志文本,带extra的JSON) + if workers > 1: + text_format = format_string or f"%(asctime)s - [{current_worker_id}] - %(name)s - %(levelname)s - %(message)s" + else: + text_format = format_string or "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + formatter = HybridLogFormatter(text_format=text_format) + + # 创建处理器列表 + stream_handler = logging.StreamHandler(sys.stdout) + stream_handler.setFormatter(formatter) + handlers: list[logging.Handler] = [stream_handler] + + # 确定日志文件路径 + if log_file_path: + log_path = Path(log_file_path) + else: + log_path = Path("logs/qwen3-asr.log") + + # 确保日志目录存在 + log_dir = log_path.parent + log_dir.mkdir(parents=True, exist_ok=True) + + # 多 Worker 模式下,每个 Worker 使用独立的日志文件 + if workers > 1: + # Example: qwen3-asr.log -> qwen3-asr.worker-12345.log + worker_log_path = log_dir / f"{log_path.stem}.{current_worker_id}{log_path.suffix}" + + # Worker 专属日志文件 + worker_file_handler = logging.handlers.RotatingFileHandler( + worker_log_path, + maxBytes=max_file_size, + backupCount=backup_files, + encoding="utf-8", + ) + worker_file_handler.setFormatter(formatter) + handlers.append(worker_file_handler) + + # 同时也写入主日志文件(汇总所有 Worker 的日志) + main_file_handler = logging.handlers.RotatingFileHandler( + log_path, + maxBytes=max_file_size, + backupCount=backup_files, + encoding="utf-8", + ) + main_file_handler.setFormatter(formatter) + handlers.append(main_file_handler) + else: + # 单 Worker 模式,只写入主日志文件 + file_handler = logging.handlers.RotatingFileHandler( + log_path, + maxBytes=max_file_size, + backupCount=backup_files, + encoding="utf-8", + ) + file_handler.setFormatter(formatter) + handlers.append(file_handler) + + # 配置根日志记录器 + logging.basicConfig( + level=getattr(logging, log_level.upper()), + handlers=handlers, + force=True, # 强制重新配置 + ) + + # 设置第三方库的日志级别(由LOG_LEVEL控制) + third_party_level = getattr(logging, log_level.upper()) + logging.getLogger("urllib3").setLevel(third_party_level) + logging.getLogger("requests").setLevel(third_party_level) + logging.getLogger("httpx").setLevel(third_party_level) + logging.getLogger("httpcore").setLevel(third_party_level) + + # 始终禁用噪音特别大的库 + logging.getLogger("numba").setLevel(logging.WARNING) + logging.getLogger("numba.core").setLevel(logging.WARNING) + logging.getLogger("numba.core.ssa").setLevel(logging.WARNING) + + # 多 Worker 模式下记录启动日志 + if workers > 1 and worker_log_path: + logger = logging.getLogger(__name__) + logger.info(f"Worker {current_worker_id} 日志系统已初始化,日志文件: {worker_log_path}") diff --git a/app/core/security.py b/app/core/security.py new file mode 100644 index 0000000..f94cb67 --- /dev/null +++ b/app/core/security.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- +""" +安全相关功能 +包含鉴权、token验证等安全功能 +""" + +from typing import Optional +from fastapi import Request +from .config import settings + +TOKEN_HEADER_NAME = "X-NLS-Token" +AUTH_OPTIONAL_PLACEHOLDER = "optional" +WEBSOCKET_QUERY_TOKEN_KEYS = ("token", "x_nls_token", "X-NLS-Token") + + +def normalize_token(token: Optional[str]) -> Optional[str]: + """将 token 归一化为非空字符串或 None。""" + if token is None: + return None + + normalized = token.strip() + return normalized or None + + +def get_expected_api_key(expected_token: Optional[str] = None) -> Optional[str]: + """获取归一化后的期望 API_KEY。""" + if expected_token is not None: + return normalize_token(expected_token) + return normalize_token(settings.API_KEY) + + +def mask_sensitive_data( + data: str, mask_char: str = "*", keep_prefix: int = 4, keep_suffix: int = 4 +) -> str: + """遮盖敏感数据 + + Args: + data: 需要遮盖的数据 + mask_char: 遮盖字符 + keep_prefix: 保留前缀字符数 + keep_suffix: 保留后缀字符数 + + Returns: + 遮盖后的数据 + """ + if not data or len(data) <= keep_prefix + keep_suffix: + return data + + prefix = data[:keep_prefix] + suffix = data[-keep_suffix:] if keep_suffix > 0 else "" + mask_length = len(data) - keep_prefix - keep_suffix + mask = mask_char * mask_length + + return f"{prefix}{mask}{suffix}" + + +def validate_token_value(token: Optional[str], expected_token: Optional[str] = None) -> bool: + """验证访问令牌 + + Args: + token: 客户端提供的token + expected_token: 期望的token值(从环境变量读取),如果为None则鉴权可选 + + Returns: + bool: 验证结果 + """ + normalized_expected_token = get_expected_api_key(expected_token) + if not normalized_expected_token: + return True + + normalized_token = normalize_token(token) + if not normalized_token: + return False + + # 简单的token格式验证(长度检查) + if len(normalized_token) < 10: + return False + + # 验证token是否匹配 + if normalized_token != normalized_expected_token: + return False + + return True + + +def extract_header_token(request: Request) -> Optional[str]: + """从标准头部提取 token。""" + return normalize_token(request.headers.get(TOKEN_HEADER_NAME)) + + +def extract_bearer_token(request: Request) -> Optional[str]: + """从 Authorization: Bearer 提取 token。""" + auth_header = request.headers.get("Authorization") + if not auth_header: + return None + + scheme, _, value = auth_header.partition(" ") + if scheme.lower() != "bearer": + return None + return normalize_token(value) + + +def extract_openai_token(request: Request) -> Optional[str]: + """OpenAI 兼容接口鉴权:优先 Bearer,其次 X-NLS-Token。""" + return extract_bearer_token(request) or extract_header_token(request) + + +def extract_websocket_token(websocket) -> Optional[str]: + """从 WebSocket 连接中提取 token。""" + if hasattr(websocket, "headers"): + token = normalize_token(websocket.headers.get(TOKEN_HEADER_NAME)) + if token: + return token + + if hasattr(websocket, "query_params"): + for key in WEBSOCKET_QUERY_TOKEN_KEYS: + token = normalize_token(websocket.query_params.get(key)) + if token: + return token + + return None + + +def _validate_resolved_token( + token: Optional[str], + missing_message: str, + expected_token: Optional[str] = None, +) -> tuple[bool, str]: + """统一 token 校验逻辑。""" + expected = get_expected_api_key(expected_token) + normalized_token = normalize_token(token) + + if not expected: + return True, normalized_token or AUTH_OPTIONAL_PLACEHOLDER + + if not normalized_token: + return False, missing_message + + if not validate_token_value(normalized_token, expected): + masked_token = mask_sensitive_data(normalized_token) + return False, f"Gateway:ACCESS_DENIED:The token '{masked_token}' is invalid!" + + return True, normalized_token + + +def validate_token(request: Request, task_id: str = "") -> tuple[bool, str]: + """验证X-NLS-Token头部""" + _ = task_id + token = extract_header_token(request) + return _validate_resolved_token(token, "缺少X-NLS-Token头部") + + +def validate_openai_token(request: Request, task_id: str = "") -> tuple[bool, str]: + """验证 OpenAI 兼容接口 token(Bearer/X-NLS-Token)。""" + _ = task_id + token = extract_openai_token(request) + return _validate_resolved_token(token, "缺少Authorization Bearer或X-NLS-Token头部") + + +def validate_token_websocket(token: str, task_id: str = "") -> tuple[bool, str]: + """验证WebSocket连接中的token""" + _ = task_id + return _validate_resolved_token(token, "缺少token参数") + + +def validate_websocket_token(websocket, task_id: str = "") -> tuple[bool, str]: + """验证 WebSocket 连接 token(header/query 参数)。""" + _ = task_id + token = extract_websocket_token(websocket) + return _validate_resolved_token( + token, + "缺少鉴权信息,请通过 X-NLS-Token header 或 token/x_nls_token 查询参数传入", + ) diff --git a/app/core/task_store.py b/app/core/task_store.py new file mode 100644 index 0000000..ba359b8 --- /dev/null +++ b/app/core/task_store.py @@ -0,0 +1,280 @@ +# -*- coding: utf-8 -*- +"""Persistent task state store for meeting-style offline jobs.""" + +from __future__ import annotations + +import json +import time +from pathlib import Path +from threading import RLock +from typing import Any, Optional + +from app.core.config import settings + +tasks_db: dict[str, dict[str, Any]] = {} +_tasks_lock = RLock() +_last_cleanup_at = 0 + + +def _task_state_dir() -> Path: + return Path(settings.TASK_STATE_DIR) + + +def _task_file_path(task_id: str) -> Path: + return _task_state_dir() / f"{task_id}.json" + + +def _task_result_file_path(task_id: str) -> Path: + return _task_state_dir() / f"{task_id}.result.json" + + +def _retention_seconds() -> int: + return max(0, int(settings.TASK_RETENTION_HOURS)) * 3600 + + +def _is_task_expired(task_payload: dict[str, Any], now_ts: Optional[int] = None) -> bool: + retention_seconds = _retention_seconds() + if retention_seconds <= 0: + return False + current_time = int(now_ts or time.time()) + base_timestamp = int(task_payload.get("updated_at") or task_payload.get("created_at") or 0) + if base_timestamp <= 0: + return False + return current_time - base_timestamp >= retention_seconds + + +def _write_json_file(file_path: Path, payload: dict[str, Any]) -> None: + file_path.parent.mkdir(parents=True, exist_ok=True) + temporary_file_path = file_path.with_suffix(f"{file_path.suffix}.tmp") + temporary_file_path.write_text( + json.dumps(payload, ensure_ascii=False), + encoding="utf-8", + ) + temporary_file_path.replace(file_path) + + +def _write_task_file(task_id: str, payload: dict[str, Any]) -> None: + _write_json_file(_task_file_path(task_id), payload) + + +def _remove_task_file(task_id: str) -> None: + task_file_path = _task_file_path(task_id) + if task_file_path.exists(): + task_file_path.unlink() + + +def _write_task_result_file(task_id: str, payload: dict[str, Any]) -> None: + _write_json_file(_task_result_file_path(task_id), payload) + + +def _remove_task_result_file(task_id: str) -> None: + task_result_file_path = _task_result_file_path(task_id) + if task_result_file_path.exists(): + task_result_file_path.unlink() + + +def _load_task_payload_from_file(task_file_path: Path) -> Optional[tuple[str, dict[str, Any]]]: + try: + payload = json.loads(task_file_path.read_text(encoding="utf-8")) + except Exception: + return None + if not isinstance(payload, dict): + return None + task_id = str(payload.get("task_id") or task_file_path.stem).strip() + if not task_id: + return None + payload["task_id"] = task_id + return task_id, payload + + +def _load_task_result_payload(task_id: str) -> Optional[dict[str, Any]]: + task_result_file_path = _task_result_file_path(task_id) + if not task_result_file_path.exists(): + return None + try: + payload = json.loads(task_result_file_path.read_text(encoding="utf-8")) + except Exception: + return None + if not isinstance(payload, dict): + return None + return payload + + +def _split_task_payload(task_id: str, payload: dict[str, Any], *, persist_legacy_result: bool = False) -> tuple[dict[str, Any], bool]: + status_payload = dict(payload) + has_result = "result" in status_payload + result_payload = status_payload.pop("result", None) + if has_result: + if isinstance(result_payload, dict): + _write_task_result_file(task_id, result_payload) + elif result_payload is None: + _remove_task_result_file(task_id) + if persist_legacy_result: + _write_task_file(task_id, status_payload) + return status_payload, has_result + + +def _status_file_paths() -> list[Path]: + return [ + task_file_path + for task_file_path in _task_state_dir().glob("*.json") + if not task_file_path.name.endswith(".result.json") + ] + + +def load_tasks_from_disk() -> None: + with _tasks_lock: + tasks_db.clear() + task_directory = _task_state_dir() + task_directory.mkdir(parents=True, exist_ok=True) + current_time = int(time.time()) + for task_file_path in _status_file_paths(): + loaded_item = _load_task_payload_from_file(task_file_path) + if loaded_item is None: + try: + task_file_path.unlink() + except Exception: + pass + continue + task_id, payload = loaded_item + payload, _ = _split_task_payload(task_id, payload, persist_legacy_result=True) + if _is_task_expired(payload, now_ts=current_time): + try: + task_file_path.unlink() + except Exception: + pass + try: + _remove_task_result_file(task_id) + except Exception: + pass + continue + tasks_db[task_id] = payload + + +def cleanup_expired_tasks(force: bool = False) -> None: + global _last_cleanup_at + current_time = int(time.time()) + if not force and current_time - _last_cleanup_at < 60: + return + with _tasks_lock: + expired_task_ids = [ + task_id + for task_id, task_payload in tasks_db.items() + if _is_task_expired(task_payload, now_ts=current_time) + ] + for task_id in expired_task_ids: + tasks_db.pop(task_id, None) + try: + _remove_task_file(task_id) + except Exception: + pass + try: + _remove_task_result_file(task_id) + except Exception: + pass + for task_file_path in _status_file_paths(): + loaded_item = _load_task_payload_from_file(task_file_path) + if loaded_item is None: + try: + task_file_path.unlink() + except Exception: + pass + continue + task_id, payload = loaded_item + payload, _ = _split_task_payload(task_id, payload, persist_legacy_result=True) + if _is_task_expired(payload, now_ts=current_time): + try: + task_file_path.unlink() + except Exception: + pass + try: + _remove_task_result_file(task_id) + except Exception: + pass + _last_cleanup_at = current_time + + +def recover_tasks_after_restart() -> None: + load_tasks_from_disk() + cleanup_expired_tasks(force=True) + with _tasks_lock: + for task_id, task_payload in list(tasks_db.items()): + if str(task_payload.get("status") or "").strip() not in {"queued", "processing"}: + continue + interrupted_message = "服务已重启,原离线任务已中断,请重新提交。" + task_payload.update( + { + "status": "failed", + "stage": "failed", + "message": interrupted_message, + "error": interrupted_message, + "percentage": 100, + "updated_at": int(time.time()), + } + ) + tasks_db[task_id] = task_payload + _write_task_file(task_id, task_payload) + + +def create_task_record(task_id: str, payload: dict[str, Any]) -> dict[str, Any]: + current_time = int(time.time()) + task_payload = dict(payload) + task_payload["task_id"] = task_id + task_payload.setdefault("created_at", current_time) + task_payload["updated_at"] = current_time + status_payload, has_result = _split_task_payload(task_id, task_payload) + with _tasks_lock: + tasks_db[task_id] = status_payload + _write_task_file(task_id, status_payload) + response_payload = dict(status_payload) + if has_result: + response_payload["result"] = _load_task_result_payload(task_id) + return response_payload + + +def get_task_record(task_id: str, *, include_result: bool = False) -> Optional[dict[str, Any]]: + with _tasks_lock: + task_file_path = _task_file_path(task_id) + if task_file_path.exists(): + loaded_item = _load_task_payload_from_file(task_file_path) + if loaded_item is None: + return None + loaded_task_id, task_payload = loaded_item + task_payload, _ = _split_task_payload( + loaded_task_id, + task_payload, + persist_legacy_result=True, + ) + if loaded_task_id != task_id or _is_task_expired(task_payload): + return None + tasks_db[task_id] = task_payload + else: + task_payload = tasks_db.get(task_id) + if task_payload is None: + return None + response_payload = dict(task_payload) + if include_result: + result_payload = _load_task_result_payload(task_id) + if result_payload is not None: + response_payload["result"] = result_payload + return response_payload + + +def update_task_record(task_id: str, payload: dict[str, Any]) -> dict[str, Any]: + current_time = int(time.time()) + with _tasks_lock: + task_payload = dict(tasks_db.get(task_id) or {}) + task_payload.update(payload) + task_payload["task_id"] = task_id + task_payload.setdefault("created_at", current_time) + task_payload["updated_at"] = current_time + status_payload, has_result = _split_task_payload(task_id, task_payload) + tasks_db[task_id] = status_payload + _write_task_file(task_id, status_payload) + response_payload = dict(status_payload) + if has_result: + response_payload["result"] = _load_task_result_payload(task_id) + return response_payload + + +load_tasks_from_disk() diff --git a/app/core/text_cleanup.py b/app/core/text_cleanup.py new file mode 100644 index 0000000..ced47cc --- /dev/null +++ b/app/core/text_cleanup.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +"""Conservative ASR text deduplication and filler cleanup.""" + +from __future__ import annotations + +import re + +from app.core.text_cleanup_lexicon import DISCOURSE_FILLER_TOKENS +from app.core.text_cleanup_lexicon import FILLER_TOKENS +from app.core.text_cleanup_lexicon import NUMERIC_STUTTER_CHARS +from app.core.text_cleanup_lexicon import REPEAT_COLLAPSIBLE_DISCOURSE_TOKENS +from app.core.text_cleanup_lexicon import SAFE_DOUBLE_WORDS +from app.core.text_cleanup_lexicon import TERMINAL_FILLER_TOKENS + +_PREFIX_STUTTER_CHARS = "这那离超和跟在对把将又还上先后了的" +_PREFIX_STUTTER_PATTERN = re.compile(rf"([{re.escape(_PREFIX_STUTTER_CHARS)}])\1(?!\1)([\u4e00-\u9fffA-Za-z]{{1,3}})") +_REPEATED_PHRASE_PATTERN = re.compile(r"([\u4e00-\u9fffA-Za-z]{2,4})\1") +_WORD_STUTTER_PREFIX_PATTERN = re.compile(r"([\u4e00-\u9fff])\1{1,5}(?=\1[\u4e00-\u9fff])") +_LONG_CHAR_REPEAT_PATTERN = re.compile(r"([\u4e00-\u9fff])\1{2,}") +_INTERNAL_DOUBLE_CHAR_REPEAT_PATTERN = re.compile(r"(?<=[\u4e00-\u9fff])([\u4e00-\u9fff])\1(?=[\u4e00-\u9fff])") +_SHORT_LEADING_STUTTER_TOKEN_PATTERN = re.compile(r"(^|[,。!?;:、,\s])([\u4e00-\u9fff])\2([\u4e00-\u9fff])(?=($|[,。!?;:、,\s]))") +_SHORT_CONFIRMATION_STUTTER_PATTERN = re.compile(r"(^|[,。!?;:、,\s])([\u4e00-\u9fff])\2([\u4e00-\u9fff])(?=(是吧|对吧|对吗|对不对))") +_REPEATED_SEPARATED_PHRASE_PATTERN = re.compile(r"([\u4e00-\u9fffA-Za-z]{1,6})([,、,\s]+)\1(?:\2\1)*") +_REPEATED_SENTENCE_CLAUSE_PATTERN = re.compile(r"([\u4e00-\u9fffA-Za-z0-9]{2,12})([。!?;:]+)(?:\s*\1\2)+") +_REPEATED_SHORT_SENTENCE_CLAUSE_PATTERN = re.compile( + r"([\u4e00-\u9fff])([。!?;:]+)(?:\s*\1\2){2,}(?:\s*\1)?(?=$|[,。!?;:、,\s])" +) +_BOUNDARY_OVERLAP_MIN_CHARS = 2 +_BOUNDARY_OVERLAP_MAX_CHARS = 12 +_STANDALONE_FILLER_PATTERN = re.compile(rf"(^|[,。!?;:、,\s])({'|'.join(map(re.escape, FILLER_TOKENS))})(?=($|[,。!?;:、,\s]))") +_FILLER_ONLY_PATTERN = re.compile(rf"^[\s,。!?;:、,]*(?:{'|'.join(map(re.escape, FILLER_TOKENS))}[\s,。!?;:、,]*)+$") +_LEADING_FILLER_PREFIX_PATTERN = re.compile(rf"^(?:{'|'.join(map(re.escape, FILLER_TOKENS))})[,、,\s]*") +_INLINE_FILLER_PATTERN = re.compile(r"(?<=[\u4e00-\u9fffA-Za-z0-9])(嗯|呃|啊|(? bool: + return bool(text) and all(character.isdigit() or character in "零〇○O一幺二两三四五六七八九十百千万亿点" for character in text) + + +def _has_meaningful_overlap(text: str) -> bool: + return bool(text and any(not character.isspace() and character not in ",。!?;:、,.!?;:" for character in text)) + + +def _collapse_internal_double_char(match: re.Match[str]) -> str: + start_index = match.start() + source_text = match.string + pair_text = source_text[start_index:start_index + 2] + if match.group(1) in NUMERIC_STUTTER_CHARS: + return pair_text + if pair_text in SAFE_DOUBLE_WORDS: + return pair_text + return match.group(1) + + +def _remove_filler_words(text: str) -> str: + normalized_text = str(text or "").strip() + if not normalized_text: + return normalized_text + if _FILLER_ONLY_PATTERN.fullmatch(normalized_text): + return "" + compact_text = _LEADING_FILLER_PREFIX_PATTERN.sub("", normalized_text) + compact_text = _INLINE_FILLER_PATTERN.sub("", compact_text) + compact_text = _BRIDGE_FILLER_PATTERN.sub(lambda match: match.group(1) if match.group(1) == match.group(2) else match.group(2), compact_text) + compact_text = _REPEATED_TOPIC_WITH_DEICTIC_PATTERN.sub(lambda match: match.group(1), compact_text) + compact_text = _REPEATED_DISCOURSE_FILLER_PATTERN.sub(lambda match: match.group(1), compact_text) + compact_text = _STANDALONE_FILLER_PATTERN.sub(lambda match: match.group(1), compact_text) + compact_text = _STANDALONE_DISCOURSE_FILLER_PATTERN.sub(lambda match: match.group(1), compact_text) + compact_text = _TERMINAL_FILLER_PATTERN.sub("", compact_text) + compact_text = _POST_PUNCT_TERMINAL_FILLER_PATTERN.sub("", compact_text) + compact_text = re.sub(r"([。!?;:])[。!?;:]+", r"\1", compact_text) + compact_text = re.sub(r"([。!?;:])[,、,]+", r"\1", compact_text) + compact_text = re.sub(r"[,、,\s]{2,}", ",", compact_text) + compact_text = re.sub(r"^[,、,\s]+", "", compact_text) + compact_text = re.sub(r"[,、,\s]+([。!?;:])", r"\1", compact_text) + compact_text = re.sub(r"[,、,\s]+$", "", compact_text) + if not compact_text or re.fullmatch(r"[\s,。!?;:、,]*", compact_text): + return "" + return compact_text + + +def _repair_household_collection_count(match: re.Match[str]) -> str: + prefix, digits, suffix = match.groups() + following_context = match.string[match.end():match.end() + 100] + + # Meeting reports often say "collected N households, estimated M households, + # close to 50%". If the glued count has a plausible suffix matching that + # ratio, keep the suffix and drop the noisy leading recognition artifact. + if "50" in following_context: + reference_counts = [ + int(value) + for value in re.findall(r"(?:大概有|约|预计|测算[^,。!?;:]{0,10}?有)(\d{1,4})户", following_context) + ] + for suffix_len in range(min(4, len(digits) - 1), 1, -1): + candidate = int(digits[-suffix_len:]) + if candidate <= 0: + continue + if any(0.35 <= reference_count / candidate <= 0.65 for reference_count in reference_counts): + return f"{prefix}{candidate}{suffix}" + + return match.group(0) + + +def _repair_numeric_asr_artifacts(text: str) -> str: + repaired_text = _CURRENCY_STUTTER_PATTERN.sub(lambda match: f"{match.group(1)}元", text) + repaired_text = _CURRENCY_SYMBOL_PATTERN.sub(lambda match: f"{match.group(1)}元", repaired_text) + repaired_text = _HOUSEHOLD_COLLECTION_GLUE_PATTERN.sub(_repair_household_collection_count, repaired_text) + return repaired_text + + +def _collapse_repeated_numeric_clauses(text: str) -> str: + return _REPEATED_NUMERIC_CLAUSE_PATTERN.sub(lambda match: f"{match.group(1)}{match.group(2)}", text) + + +def _collapse_repeated_short_sentence_clauses(text: str) -> str: + return _REPEATED_SHORT_SENTENCE_CLAUSE_PATTERN.sub(lambda match: f"{match.group(1)}{match.group(2)}", text) + + +def deduplicate_asr_text(text: str) -> str: + normalized_text = str(text or "").strip() + if not normalized_text: + return normalized_text + previous_text = None + while previous_text != normalized_text: + previous_text = normalized_text + normalized_text = _PREFIX_STUTTER_PATTERN.sub(lambda match: f"{match.group(1)}{match.group(2)}", normalized_text) + + def _collapse_repeated_phrase(match: re.Match[str]) -> str: + phrase = match.group(1) + if _is_numeric_like(phrase): + return match.group(0) + return phrase + + normalized_text = _REPEATED_PHRASE_PATTERN.sub(_collapse_repeated_phrase, normalized_text) + normalized_text = _WORD_STUTTER_PREFIX_PATTERN.sub(lambda match: match.group(0) if match.group(1) in NUMERIC_STUTTER_CHARS else "", normalized_text) + normalized_text = _LONG_CHAR_REPEAT_PATTERN.sub(lambda match: match.group(0) if match.group(1) in NUMERIC_STUTTER_CHARS else match.group(1), normalized_text) + normalized_text = _INTERNAL_DOUBLE_CHAR_REPEAT_PATTERN.sub(_collapse_internal_double_char, normalized_text) + normalized_text = _SHORT_LEADING_STUTTER_TOKEN_PATTERN.sub(lambda match: f"{match.group(1)}{match.group(2)}{match.group(3)}", normalized_text) + + def _collapse_confirmation(match: re.Match[str]) -> str: + repeated_pair = f"{match.group(2)}{match.group(2)}" + if match.group(2) in NUMERIC_STUTTER_CHARS or repeated_pair in SAFE_DOUBLE_WORDS: + return match.group(0) + return f"{match.group(1)}{match.group(2)}{match.group(3)}" + + normalized_text = _SHORT_CONFIRMATION_STUTTER_PATTERN.sub(_collapse_confirmation, normalized_text) + + def _collapse_separated(match: re.Match[str]) -> str: + phrase = match.group(1) + if phrase.isascii() and len(phrase) == 1: + return match.group(0) + if _is_numeric_like(phrase): + return match.group(0) + return phrase + + normalized_text = _REPEATED_SEPARATED_PHRASE_PATTERN.sub(_collapse_separated, normalized_text) + normalized_text = _REPEATED_SENTENCE_CLAUSE_PATTERN.sub(lambda match: match.group(0) if _is_numeric_like(match.group(1)) else f"{match.group(1)}{match.group(2)}", normalized_text) + normalized_text = _collapse_repeated_short_sentence_clauses(normalized_text) + normalized_text = _collapse_repeated_numeric_clauses(normalized_text) + normalized_text = _remove_filler_words(normalized_text) + normalized_text = _repair_numeric_asr_artifacts(normalized_text) + return normalized_text + + +def trim_segment_boundary_overlap(previous_text: str, current_text: str) -> tuple[str, bool]: + normalized_previous = str(previous_text or "").strip() + normalized_current = str(current_text or "").strip() + if not normalized_previous or not normalized_current: + return normalized_current, False + max_overlap_chars = min(len(normalized_previous), len(normalized_current), _BOUNDARY_OVERLAP_MAX_CHARS) + for overlap_chars in range(max_overlap_chars, _BOUNDARY_OVERLAP_MIN_CHARS - 1, -1): + overlap_suffix = normalized_previous[-overlap_chars:] + overlap_prefix = normalized_current[:overlap_chars] + if overlap_suffix != overlap_prefix: + continue + if not _has_meaningful_overlap(overlap_prefix): + continue + return normalized_current[overlap_chars:].lstrip(), True + return normalized_current, False diff --git a/app/core/text_cleanup_lexicon.py b/app/core/text_cleanup_lexicon.py new file mode 100644 index 0000000..2640886 --- /dev/null +++ b/app/core/text_cleanup_lexicon.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +"""Conservative ASR text cleanup lexicons.""" + +from __future__ import annotations + +SAFE_DOUBLE_WORDS = { + "哥哥", + "叔叔", + "爸爸", + "妈妈", + "奶奶", + "爷爷", + "姐姐", + "弟弟", + "妹妹", + "伯伯", + "姑姑", + "舅舅", + "星星", + "猩猩", +} + +NUMERIC_STUTTER_CHARS = set("零〇○O一幺二两三四五六七八九十百千万亿点") + +FILLER_TOKENS = ( + "啊", + "呢", + "吧", + "哦", + "嗯", + "呃", + "哈", + "哇", + "呀", + "哎", + "诶", + "欸", + "额", +) + +DISCOURSE_FILLER_TOKENS = ( + "那个", + "这个", + "然后", + "所以说", + "其实", + "那么", +) + +REPEAT_COLLAPSIBLE_DISCOURSE_TOKENS = ( + "那个", + "这个", + "然后", + "就是", + "所以", + "所以说", + "其实", + "那么", +) + +TERMINAL_FILLER_TOKENS = ( + "哈", + "嗯", +) diff --git a/app/infrastructure/__init__.py b/app/infrastructure/__init__.py new file mode 100644 index 0000000..7763654 --- /dev/null +++ b/app/infrastructure/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" +基础设施层 - 提供底层通用功能 +""" + +from .model_utils import resolve_model_path + +__all__ = ["resolve_model_path"] diff --git a/app/infrastructure/model_utils.py b/app/infrastructure/model_utils.py new file mode 100644 index 0000000..c724f43 --- /dev/null +++ b/app/infrastructure/model_utils.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +""" +模型工具模块 - 提供模型路径解析等通用功能 +""" + +import logging +from pathlib import Path +from typing import Optional + +from app.core.config import settings + +logger = logging.getLogger(__name__) + + +def resolve_model_path(model_id: Optional[str]) -> str: + """将模型 ID 解析为本地模型路径(如果存在) + + 本项目默认模型目录结构: + ./models/{publisher}/{model_name}/ + + 如果本地模型存在,返回本地路径;否则返回原始 model_id + """ + if not model_id: + raise ValueError("model_id 不能为空") + + # 项目内扁平化模型根目录 + local_path = Path(settings.MODELSCOPE_PATH) / model_id + + if local_path.exists() and local_path.is_dir(): + resolved = str(local_path) + logger.info(f"模型 {model_id} 使用本地缓存: {resolved}") + return resolved + + # 兼容历史错误配置:MODELSCOPE_CACHE 指向了 models 目录时, + # ModelScope 会生成 /models/models/{publisher}/{model_name}。 + legacy_nested_path = Path(settings.MODELSCOPE_PATH) / "models" / model_id + if legacy_nested_path.exists() and legacy_nested_path.is_dir(): + resolved = str(legacy_nested_path) + logger.warning( + "模型 %s 命中历史嵌套缓存: %s。建议迁移到 %s", + model_id, + resolved, + local_path, + ) + return resolved + + logger.warning(f"模型 {model_id} 本地缓存不存在,将在运行时下载") + return model_id diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..8d24ab7 --- /dev/null +++ b/app/main.py @@ -0,0 +1,283 @@ +# -*- coding: utf-8 -*- +""" +FastAPI应用创建和配置 +""" + +import warnings +import asyncio +import os +import logging +from contextlib import asynccontextmanager +from fastapi import FastAPI, HTTPException +from fastapi.exceptions import RequestValidationError +from fastapi_offline import FastAPIOffline +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles + +from .core.config import settings +from .core.exceptions import ( + APIException, + api_exception_handler, + general_exception_handler, + http_exception_handler, + validation_exception_handler, +) +from .core.logging import setup_logging, get_worker_id +from .core.executor import shutdown_executor +from .api.v1 import api_router +from .utils.boot_events import emit_boot_event + +# 忽略 Pydantic V2 兼容性警告 +warnings.filterwarnings("ignore", message="Valid config keys have changed in V2") +warnings.filterwarnings("ignore", message=".*has conflict with protected namespace.*") +warnings.filterwarnings("ignore", category=UserWarning, module="pydantic") + +logger = logging.getLogger(__name__) + + +async def cleanup_task_state_loop(): + """定期清理过期离线任务状态文件。""" + while True: + await asyncio.sleep(600) + try: + from .core.task_store import cleanup_expired_tasks + + cleanup_expired_tasks(force=True) + except asyncio.CancelledError: + raise + except Exception as exc: + logger.warning("定期清理离线任务状态失败: %s", exc) + + +def cleanup_temp_directory(): + """清理临时目录中的旧文件""" + import time + temp_dir = settings.TEMP_DIR + if not os.path.exists(temp_dir): + return + + # 清理超过 1 小时的临时文件 + max_age_seconds = 3600 + current_time = time.time() + cleaned_count = 0 + + try: + for filename in os.listdir(temp_dir): + filepath = os.path.join(temp_dir, filename) + if os.path.isfile(filepath): + file_age = current_time - os.path.getmtime(filepath) + if file_age > max_age_seconds: + try: + os.remove(filepath) + cleaned_count += 1 + except Exception: + pass + + if cleaned_count > 0: + logger.info(f"已清理 {cleaned_count} 个过期临时文件") + except Exception as e: + logger.warning(f"清理临时目录时出错: {e}") + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """应用生命周期管理""" + workers = int(os.getenv("WORKERS", "1")) + worker_id = get_worker_id() + task_cleanup_task = None + + # 启动时 + logger.info(f"Worker [{worker_id}] 启动中...") + emit_boot_event("phase_start", phase="worker", total=1, message=f"Worker [{worker_id}] 启动中") + + # 清理旧的临时文件(仅主 Worker 执行) + if worker_id == 0: + cleanup_temp_directory() + try: + from .core.task_store import recover_tasks_after_restart + + recover_tasks_after_restart() + except Exception as exc: + logger.warning("恢复离线任务状态失败: %s", exc) + task_cleanup_task = asyncio.create_task(cleanup_task_state_loop()) + + if settings.SPEAKER_DB_ENABLED: + try: + from .core.database import pg_speaker_db + + await pg_speaker_db.connect() + except Exception as exc: + logger.warning( + "声纹数据库连接失败,将保留原说话人编号并禁用声纹注册接口: %s", + exc, + ) + + from .utils.model_loader import ( + preload_models, + verify_required_models_integrity, + ) + + integrity_result = verify_required_models_integrity() + if integrity_result["invalid_models"]: + emit_boot_event("error", phase="integrity", message="required model integrity check failed") + raise RuntimeError("required model integrity check failed") + + logger.info(f"Worker [{worker_id}] 正在加载模型...") + preload_result = preload_models() + + asr_results = preload_result.get("asr_models", {}) + loaded_count = sum(1 for r in asr_results.values() if r.get("loaded")) + total_count = len(asr_results) + logger.info(f"Worker [{worker_id}] 模型加载完成: {loaded_count}/{total_count}") + failed_asr_models = { + model_id: status.get("error") + for model_id, status in asr_results.items() + if not status.get("loaded") and status.get("error") + } + try: + from .services.asr.model_plan import get_active_qwen_model, load_supported_model_ids + + active_qwen_model = get_active_qwen_model(load_supported_model_ids()) + except Exception as exc: + logger.error(f"Worker [{worker_id}] 无法解析当前应启用的 Qwen 模型: {exc}") + emit_boot_event("error", phase="preload", message=f"无法解析当前应启用的 Qwen 模型: {exc}") + raise + + active_qwen_status = asr_results.get(active_qwen_model, {}) + if not active_qwen_status.get("loaded"): + qwen_error = active_qwen_status.get("error") or "unknown error" + logger.error( + f"Worker [{worker_id}] Qwen 主模型预加载失败,拒绝启动: {active_qwen_model}, error={qwen_error}" + ) + emit_boot_event( + "error", + phase="preload", + message=f"Qwen 主模型预加载失败,拒绝启动: {active_qwen_model}, error={qwen_error}", + ) + raise RuntimeError( + f"required qwen model preload failed: {active_qwen_model}: {qwen_error}" + ) + + if failed_asr_models: + if loaded_count == 0: + logger.error(f"Worker [{worker_id}] ASR模型预加载失败详情: {failed_asr_models}") + emit_boot_event("error", phase="preload", message=f"ASR模型预加载失败详情: {failed_asr_models}") + raise RuntimeError(f"ASR model preload failed: {failed_asr_models}") + logger.warning( + f"Worker [{worker_id}] 部分ASR模型预加载失败,将以可用模型继续启动: {failed_asr_models}" + ) + emit_boot_event( + "warning", + phase="preload", + message=f"部分ASR模型预加载失败,将以可用模型继续启动: {failed_asr_models}", + ) + + if settings.SPEAKER_DB_ENABLED: + try: + from .core.database import pg_speaker_db + from .services.speaker_registry import get_speaker_registry_service + + if pg_speaker_db.is_connected: + logger.info(f"Worker [{worker_id}] 正在预加载声纹识别模型...") + get_speaker_registry_service().ensure_loaded() + except Exception as exc: + logger.warning("声纹识别模型预加载失败,将在首次请求时重试: %s", exc) + + logger.info(f"Worker [{worker_id}] 已就绪") + emit_boot_event("ready", phase="worker", message=f"Worker [{worker_id}] 已就绪") + + yield + + # 关闭时 + if task_cleanup_task is not None: + task_cleanup_task.cancel() + try: + await task_cleanup_task + except asyncio.CancelledError: + pass + + if settings.SPEAKER_DB_ENABLED: + try: + from .core.database import pg_speaker_db + + await pg_speaker_db.close() + except Exception as exc: + logger.warning("关闭声纹数据库连接时出错: %s", exc) + logger.info(f"Worker [{worker_id}] 正在关闭推理线程池...") + shutdown_executor() + logger.info(f"Worker [{worker_id}] 已关闭") + + +def create_app() -> FastAPI: + """创建FastAPI应用""" + + # 设置日志 + setup_logging() + + app = FastAPIOffline( + title=settings.APP_NAME, + description=settings.APP_DESCRIPTION, + version=settings.APP_VERSION, + docs_url=settings.docs_url, + redoc_url=settings.redoc_url, + lifespan=lifespan, # 添加生命周期管理 + ) + + # 添加CORS中间件 + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + # 注册异常处理器 + app.add_exception_handler(APIException, api_exception_handler) + app.add_exception_handler(HTTPException, http_exception_handler) + app.add_exception_handler(RequestValidationError, validation_exception_handler) + app.add_exception_handler(Exception, general_exception_handler) + + # 注册静态文件服务(用于临时文件) + app.mount("/tmp", StaticFiles(directory=settings.TEMP_DIR), name="temp_files") + app.mount( + "/test-web", + StaticFiles(directory=str(settings.BASE_DIR / "test_web"), html=True), + name="test_web", + ) + + # 注册API路由 + app.include_router(api_router) + + # 根路径 + @app.get("/", summary="根路径", description="API服务根路径") + async def root(): + return { + "message": settings.APP_NAME, + "version": settings.APP_VERSION, + "description": settings.APP_DESCRIPTION, + "endpoints": { + # 阿里云兼容 API + "asr": "/stream/v1/asr", + "asr_models": "/stream/v1/asr/models", + "asr_health": "/stream/v1/asr/health", + "ws_asr": "/ws/v1/asr/qwen", + "ws_qwen3_asr": "/ws/v1/asr/qwen", + # OpenAI 兼容 API + "openai_models": "/v1/models", + "openai_transcriptions": "/v1/audio/transcriptions", + # 独立会议离线 / 声纹管理 API + "meeting_files": "/api/v1/files", + "meeting_transcriptions": "/api/v1/asr/transcriptions", + "speakers": "/api/v1/speakers", + "test_web": "/test-web/", + # 文档 + "docs": settings.docs_url or "禁用", + }, + } + + return app + + +# 创建全局应用实例 +app = create_app() diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..6204332 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +""" +数据模型模块 +包含API的请求和响应模型定义 +""" diff --git a/app/models/asr.py b/app/models/asr.py new file mode 100644 index 0000000..3b938ed --- /dev/null +++ b/app/models/asr.py @@ -0,0 +1,321 @@ +# -*- coding: utf-8 -*- +""" +ASR数据模型 +定义语音识别相关的请求和响应模型 +""" + +from typing import Optional, List, Union +from pydantic import BaseModel, Field + +from .common import ( + SampleRate, + BaseResponse, + HealthCheckResponse, + ErrorResponse, +) + + +# ============= 请求模型 ============= + + +class ASRQueryParams(BaseModel): + """ASR接口查询参数模型""" + + model: Optional[str] = Field( + default=None, + description="可选。离线 ASR 模型 ID;不传则使用服务当前默认模型,如 qwen3-asr-0.6b 或 qwen3-asr-1.7b", + max_length=128, + ) + + audio_address: Optional[str] = Field( + default=None, + description="音频/视频文件地址,支持 HTTP/HTTPS URL、file:// 或服务端本地路径,格式自动识别", + max_length=512, + ) + + sample_rate: Optional[SampleRate] = Field( + default=SampleRate.RATE_16000, + description=f"音频采样率(Hz)。支持: {', '.join(map(str, SampleRate.get_enums()))}", + ) + + enable_speaker_diarization: Optional[bool] = Field( + default=True, + description="是否启用说话人分离。启用后响应会包含 speaker_id", + ) + + enable_speaker_identification: Optional[bool] = Field( + default=True, + description="是否匹配已注册声纹库。仅在 enable_speaker_diarization=true 时生效", + ) + + enable_text_cleanup: Optional[bool] = Field( + default=True, + description="是否启用文本去重和口头语清理", + ) + + word_timestamps: Optional[bool] = Field( + default=False, + description="是否返回字词级时间戳(默认关闭;Qwen CUDA vLLM / CPU Rust 会在启用时自动调用 forced aligner)", + ) + + vocabulary_id: Optional[str] = Field( + default=None, + description="热词字符串,格式:热词1 权重1 热词2 权重2(如:阿里巴巴 20 腾讯 15)", + max_length=512, + ) + + +# ============= 响应模型 ============= + + +class WordToken(BaseModel): + """字词级时间戳信息""" + + text: str = Field( + ..., + description="字词文本", + ) + start_time: float = Field( + ..., + description="开始时间(秒)", + ) + end_time: float = Field( + ..., + description="结束时间(秒)", + ) + + model_config = { + "json_schema_extra": { + "example": { + "text": "今", + "start_time": 0.0, + "end_time": 0.15, + } + } + } + + +class ASRSegment(BaseModel): + """ASR 识别分段结果""" + + text: str = Field( + ..., + description="该段识别文本", + ) + start_time: float = Field( + ..., + description="段落开始时间(秒)", + ) + end_time: float = Field( + ..., + description="段落结束时间(秒)", + ) + speaker_id: Optional[str] = Field( + default=None, + description="说话人ID(如 说话人1),仅启用说话人分离时返回", + ) + word_tokens: Optional[List[WordToken]] = Field( + default=None, + description="字词级时间戳(仅启用 word_timestamps 且模型支持时返回)", + ) + + model_config = { + "json_schema_extra": { + "example": { + "text": "今天天气不错。", + "start_time": 0.0, + "end_time": 2.5, + "speaker_id": "说话人1", + "word_tokens": [ + {"text": "今", "start_time": 0.0, "end_time": 0.15}, + {"text": "天", "start_time": 0.15, "end_time": 0.35}, + ], + } + } + } + + +class ASRSuccessResponse(BaseResponse): + """ASR成功响应模型""" + + result: str = Field( + ..., + description="识别结果文本(完整)", + max_length=100000, + ) + + segments: Optional[List[ASRSegment]] = Field( + default=None, + description="分段识别结果(含时间戳),仅长音频分段识别时返回", + ) + + duration: Optional[float] = Field( + default=None, + description="音频总时长(秒)", + ) + + model_config = { + "json_schema_extra": { + "example": { + "task_id": "cf7b0c5339244ee29cd4e43fb97f1234", + "result": "今天天气不错。明天可能会下雨。", + "segments": [ + {"text": "今天天气不错。", "start_time": 0.0, "end_time": 2.5, "speaker_id": "说话人1"}, + {"text": "明天可能会下雨。", "start_time": 3.2, "end_time": 5.8, "speaker_id": "说话人2"}, + ], + "duration": 5.8, + "status": 200, + "message": "SUCCESS", + } + } + } + + +class ASRErrorResponse(ErrorResponse): + """ASR错误响应模型""" + + result: str = Field(default="", description="识别结果(错误时为空)") + + model_config = { + "json_schema_extra": { + "example": { + "task_id": "8bae3613dfc54ebfa811a17d8a7a1234", + "result": "", + "status": 40000001, + "message": "Gateway:ACCESS_DENIED:The token 'invalid_token' is invalid!", + } + } + } + + +class ASRHealthCheckResponse(HealthCheckResponse): + """ASR健康检查响应模型""" + + model_config = { + "protected_namespaces": (), + "json_schema_extra": { + "example": { + "status": "healthy", + "model_loaded": True, + "device": "cuda:0", + "version": "1.0.0", + "message": "ASR service is running normally", + "loaded_models": ["qwen3-asr-1.7b"], + "memory_usage": { + "gpu_memory_used": "2.1GB", + "gpu_memory_total": "8.0GB", + }, + "accelerator": { + "vendor": "nvidia", + "runtime": "cuda", + "device": "cuda:0", + "device_count": 1, + }, + }, + }, + } + + model_loaded: bool = Field(..., description="模型是否已加载") + device: str = Field(..., description="推理设备") + loaded_models: Optional[List[str]] = Field(default=[], description="已加载的模型列表") + memory_usage: Optional[dict] = Field(default=None, description="内存使用情况") + accelerator: Optional[dict] = Field(default=None, description="加速器信息") + + +# ============= 模型相关 ============= + + +class ASRDeclaredEntryInfo(BaseModel): + """声明式 ASR 条目信息,可表示离线模型或 realtime capability。""" + + id: str = Field(..., description="模型id") + kind: str = Field(..., description="条目类型:model 或 capability") + name: str = Field(..., description="模型名称") + engine: str = Field(..., description="引擎类型") + description: str = Field(..., description="模型描述") + languages: List[str] = Field(..., description="支持的语言列表") + default: bool = Field(default=False, description="是否为默认模型") + supports_realtime: bool = Field(default=False, description="是否支持实时识别") + offline_model: Optional[dict] = Field(default=None, description="离线模型信息") + realtime_model: Optional[dict] = Field(default=None, description="实时模型信息") + + model_config = { + "json_schema_extra": { + "example": { + "id": "qwen3-asr-1.7b", + "kind": "model", + "name": "Qwen3-ASR-1.7B", + "engine": "qwen3", + "description": "多语言离线语音识别模型", + "languages": ["zh", "en"], + "default": True, + "supports_realtime": True, + "offline_model": { + "path": "Qwen/Qwen3-ASR-1.7B", + "exists": True, + }, + "realtime_model": None, + } + } + } + + +class ASRRuntimeInfo(BaseModel): + """运行时视角的模型加载状态。""" + + loaded_model_ids: List[str] = Field(default_factory=list, description="当前已加载模型 ID 列表") + loaded_count: int = Field(..., description="已加载模型数量") + default_offline_model_id: Optional[str] = Field(default=None, description="当前默认离线模型 ID") + + model_config = { + "json_schema_extra": { + "example": { + "loaded_model_ids": ["qwen3-asr-1.7b"], + "loaded_count": 1, + "default_offline_model_id": "qwen3-asr-1.7b", + } + } + } + + +class ASRModelsResponse(BaseModel): + """ASR 模型列表响应,分离声明视角与运行时视角。""" + + declared_entries: List[ASRDeclaredEntryInfo] = Field(..., description="声明的模型与 capability 列表") + declared_count: int = Field(..., description="声明条目总数") + runtime: ASRRuntimeInfo = Field(..., description="运行时加载状态") + + model_config = { + "json_schema_extra": { + "example": { + "declared_entries": [ + { + "id": "qwen3-asr-1.7b", + "kind": "model", + "name": "Qwen3-ASR-1.7B", + "engine": "qwen3", + "description": "多语言离线语音识别模型", + "languages": ["zh", "en"], + "default": True, + "supports_realtime": True, + "offline_model": { + "path": "Qwen/Qwen3-ASR-1.7B", + "exists": True, + }, + "realtime_model": None, + } + ], + "declared_count": 2, + "runtime": { + "loaded_model_ids": ["qwen3-asr-1.7b"], + "loaded_count": 1, + "default_offline_model_id": "qwen3-asr-1.7b", + }, + } + } + } + + +# ============= 联合响应类型 ============= + +ASRResponse = Union[ASRSuccessResponse, ASRErrorResponse] diff --git a/app/models/common.py b/app/models/common.py new file mode 100644 index 0000000..32978c1 --- /dev/null +++ b/app/models/common.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +""" +通用数据模型 +定义通用的枚举、基础模型等 +""" + +from pydantic import BaseModel, Field +from enum import Enum + + +class AudioFormat(str, Enum): + """支持的音频格式""" + + PCM = "pcm" + WAV = "wav" + OPUS = "opus" + SPEEX = "speex" + AMR = "amr" + MP3 = "mp3" + AAC = "aac" + M4A = "m4a" + FLAC = "flac" + OGG = "ogg" + + @classmethod + def get_enums(cls): + return [e.value for e in cls] + + +class SampleRate(int, Enum): + """支持的采样率""" + + RATE_8000 = 8000 + RATE_16000 = 16000 + RATE_22050 = 22050 + RATE_24000 = 24000 + + @classmethod + def get_enums(cls): + return [e.value for e in cls] + + +class BaseResponse(BaseModel): + """基础响应模型""" + + task_id: str = Field(..., description="任务ID") + status: int = Field(..., description="状态码") + message: str = Field(..., description="响应消息") + + +class HealthCheckResponse(BaseModel): + """健康检查响应模型""" + + status: str = Field(description="服务状态", examples=["healthy"]) + version: str = Field(description="服务版本", examples=["1.0.0"]) + message: str = Field( + description="状态消息", examples=["Service is running normally"] + ) + + +class ErrorResponse(BaseResponse): + """错误响应模型""" + + result: str = Field("", description="结果内容") diff --git a/app/models/websocket_asr.py b/app/models/websocket_asr.py new file mode 100644 index 0000000..5a9a03b --- /dev/null +++ b/app/models/websocket_asr.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +""" +WebSocket ASR 数据模型 - 阿里云协议 +""" + +from typing import Optional, Dict, Any, Union, List +from pydantic import BaseModel, Field, field_validator +import uuid + + +class AliyunASRWSHeader(BaseModel): + """阿里云WebSocket ASR消息头部""" + + message_id: str = Field(..., description="消息ID,32位唯一ID") + task_id: str = Field(..., description="任务ID,32位唯一ID") + namespace: str = Field(..., description="命名空间,固定为SpeechTranscriber") + name: str = Field(..., description="消息名称") + appkey: Optional[str] = Field(None, description="应用密钥") + status: Optional[int] = Field(None, description="状态码") + status_text: Optional[str] = Field(None, description="状态文本") + status_message: Optional[str] = Field(None, description="状态消息") + + @staticmethod + def generate_message_id() -> str: + """生成32位消息ID""" + return str(uuid.uuid4()).replace("-", "")[:32] + + +class AliyunStartTranscriptionPayload(BaseModel): + """StartTranscription 消息负载""" + + format: str = Field(default="pcm", description="音频格式: pcm, wav, opus, speex, amr, mp3, aac") + sample_rate: int = Field(default=16000, description="音频采样率: 8000/16000") + enable_intermediate_result: bool = Field(default=True, description="是否返回中间识别结果") + enable_punctuation_prediction: bool = Field(default=True, description="是否在后处理中添加标点") + enable_inverse_text_normalization: bool = Field(default=True, description="是否将中文数字转为阿拉伯数字") + customization_id: Optional[str] = Field(None, description="自学习模型ID") + vocabulary_id: Optional[str] = Field(None, description="定制泛热词ID") + max_sentence_silence: int = Field(default=800, ge=200, le=2000, description="语音断句检测阈值(ms)") + enable_words: bool = Field(default=False, description="是否开启返回词信息") + disfluency: bool = Field(default=False, description="过滤语气词") + speech_noise_threshold: Optional[float] = Field(None, ge=-1.0, le=1.0, description="噪音参数阈值") + enable_semantic_sentence_detection: bool = Field(default=False, description="是否开启语义断句") + + @field_validator("format") + @classmethod + def validate_format(cls, v): + supported_formats = ["pcm", "wav", "opus", "speex", "amr", "mp3", "aac"] + if v.lower() not in supported_formats: + raise ValueError(f"不支持的音频格式: {v}") + return v.lower() + + @field_validator("sample_rate") + @classmethod + def validate_sample_rate(cls, v): + supported_rates = [8000, 16000] + if v not in supported_rates: + raise ValueError(f"不支持的采样率: {v}") + return v + + +class AliyunWordInfo(BaseModel): + """词信息""" + + text: str = Field("", description="文本") + startTime: int = Field(0, description="词开始时间(ms)") + endTime: int = Field(0, description="词结束时间(ms)") + + +class AliyunTranscriptionResultPayload(BaseModel): + """识别结果负载""" + + session_id: Optional[str] = Field(None, description="会话ID") + index: Optional[int] = Field(None, description="句子编号,从1开始递增") + time: Optional[int] = Field(None, description="已处理的音频时长(ms)") + begin_time: Optional[int] = Field(None, description="句子开始时间(ms)") + result: Optional[str] = Field(None, description="识别结果文本") + confidence: Optional[float] = Field(None, description="置信度[0.0,1.0]") + words: Optional[List[AliyunWordInfo]] = Field(None, description="词信息列表") + status: Optional[int] = Field(None, description="状态码") + + +class AliyunStashResult(BaseModel): + """暂存结果(语义断句)""" + + sentenceId: int = Field(0, description="句子编号") + beginTime: int = Field(0, description="句子开始时间(ms)") + text: str = Field("", description="转写内容") + currentTime: int = Field(0, description="当前处理时间(ms)") + + +class AliyunASRWSMessage(BaseModel): + """阿里云WebSocket ASR消息""" + + header: AliyunASRWSHeader = Field(..., description="消息头部") + payload: Optional[ + Union[ + AliyunStartTranscriptionPayload, + AliyunTranscriptionResultPayload, + Dict[str, Any], + ] + ] = Field(None, description="消息负载") + + +class AliyunASRNamespace: + """阿里云ASR命名空间""" + + SPEECH_TRANSCRIBER = "SpeechTranscriber" + + +class AliyunASRMessageName: + """阿里云ASR消息名称""" + + START_TRANSCRIPTION = "StartTranscription" + STOP_TRANSCRIPTION = "StopTranscription" + + TRANSCRIPTION_STARTED = "TranscriptionStarted" + SENTENCE_BEGIN = "SentenceBegin" + TRANSCRIPTION_RESULT_CHANGED = "TranscriptionResultChanged" + SENTENCE_END = "SentenceEnd" + TRANSCRIPTION_COMPLETED = "TranscriptionCompleted" + TASK_FAILED = "TaskFailed" + + +class AliyunASRStatus: + """阿里云ASR状态码""" + + SUCCESS = 20000000 + TASK_FAILED = 40000000 + INVALID_PARAMETER = 40000001 + MESSAGE_INVALID = 40000002 + AUTHENTICATION_FAILED = 40100005 + QUOTA_EXCEEDED = 40300016 + INTERNAL_ERROR = 50000000 + SERVICE_UNAVAILABLE = 50300018 \ No newline at end of file diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e6f76e9 --- /dev/null +++ b/app/services/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +""" +服务层模块 +包含业务逻辑和模型管理 +""" diff --git a/app/services/asr/__init__.py b/app/services/asr/__init__.py new file mode 100644 index 0000000..0054e4d --- /dev/null +++ b/app/services/asr/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +""" +ASR服务模块 +包含语音识别相关的服务和引擎 +""" diff --git a/app/services/asr/audio_validation.py b/app/services/asr/audio_validation.py new file mode 100644 index 0000000..16d37e9 --- /dev/null +++ b/app/services/asr/audio_validation.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +"""Audio-related request validation helpers.""" + +from __future__ import annotations + +from typing import Optional + +from ...core.exceptions import InvalidParameterException +from ...models.common import SampleRate + + +SUPPORTED_SAMPLE_RATES = SampleRate.get_enums() + + +def validate_sample_rate(rate: Optional[int]) -> int: + """Validate input sample rate.""" + if not rate: + return 16000 + + if rate not in SUPPORTED_SAMPLE_RATES: + raise InvalidParameterException( + f"不支持的采样率: {rate}。支持的采样率: {', '.join(map(str, SUPPORTED_SAMPLE_RATES))}" + ) + return rate diff --git a/app/services/asr/engines/__init__.py b/app/services/asr/engines/__init__.py new file mode 100644 index 0000000..c00a73b --- /dev/null +++ b/app/services/asr/engines/__init__.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" +ASR引擎模块 +支持多种ASR引擎实现 +""" + +# 基础类和数据类 +from .base import ( + BaseASREngine, + RealTimeASREngine, + WordToken, + ASRSegmentResult, + ASRFullResult, + ASRRawResult, +) + +# 全局模型管理 +from .global_models import ( + get_global_vad_model, + get_global_punc_model, + get_global_punc_realtime_model, + get_punc_inference_lock, + get_punc_realtime_inference_lock, +) + +__all__ = [ + # 基础类 + "BaseASREngine", + "RealTimeASREngine", + # 数据类 + "WordToken", + "ASRSegmentResult", + "ASRFullResult", + "ASRRawResult", + # 全局模型管理 + "get_global_vad_model", + "get_global_punc_model", + "get_global_punc_realtime_model", + "get_punc_inference_lock", + "get_punc_realtime_inference_lock", +] diff --git a/app/services/asr/engines/base.py b/app/services/asr/engines/base.py new file mode 100644 index 0000000..f868ecf --- /dev/null +++ b/app/services/asr/engines/base.py @@ -0,0 +1,785 @@ +# -*- coding: utf-8 -*- +""" +ASR引擎基础模块 +包含抽象基类和数据类定义 +""" + +import time +import logging +from typing import Optional, Dict, List, Any, Callable +from abc import ABC, abstractmethod +from dataclasses import dataclass + +from app.core.config import settings +from app.core.exceptions import DefaultServerErrorException +from app.core.text_cleanup import deduplicate_asr_text +from app.core.text_cleanup import trim_segment_boundary_overlap +from app.core.hotword_resolver import apply_hotword_rules +from app.core.logging import log_inference_metrics +from app.utils.audio import get_audio_duration + + +logger = logging.getLogger(__name__) + + +def _log_asr_stage_timing( + stage: str, + duration_ms: float, + *, + task_id: Optional[str] = None, + model_id: Optional[str] = None, + audio_duration_sec: Optional[float] = None, + **extra: Any, +) -> None: + """Record structured timing for one ASR processing stage.""" + payload: Dict[str, Any] = { + "event": "asr_stage_timing", + "stage": stage, + "duration_ms": round(duration_ms, 2), + } + if task_id: + payload["task_id"] = task_id + if model_id: + payload["model_id"] = model_id + if audio_duration_sec is not None: + payload["audio_duration_sec"] = round(audio_duration_sec, 2) + if audio_duration_sec > 0: + payload["rtf"] = round((duration_ms / 1000) / audio_duration_sec, 4) + payload.update(extra) + logger.info("ASR阶段耗时", extra=payload) + + +@dataclass +class WordToken: + """字词级时间戳信息""" + + text: str # 字词文本 + start_time: float # 开始时间(秒) + end_time: float # 结束时间(秒) + + +@dataclass +class ASRSegmentResult: + """ASR 分段识别结果""" + + text: str # 该段识别文本 + start_time: float # 开始时间(秒) + end_time: float # 结束时间(秒) + speaker_id: Optional[str] = None # 说话人ID(多说话人模式) + speaker_name: Optional[str] = None # 已注册声纹命中后的显示名称 + user_id: Optional[str] = None # 已注册声纹命中后的业务用户编号 + speaker_embedding: Optional[Any] = None # 片段声纹向量,仅供服务端匹配使用 + word_tokens: Optional[List[WordToken]] = None # 字词级时间戳(可选) + + +@dataclass +class ASRFullResult: + """ASR 完整识别结果(支持长音频)""" + + text: str # 完整识别文本 + segments: List[ASRSegmentResult] # 分段结果 + duration: float # 音频总时长(秒) + + +@dataclass +class ASRRawResult: + """ASR 原始识别结果(包含时间戳)""" + + text: str # 完整识别文本 + segments: List[ASRSegmentResult] # 分段结果(从 VAD 时间戳解析) + + +class BaseASREngine(ABC): + """基础ASR引擎抽象基类""" + + @abstractmethod + def transcribe_file( + self, + audio_path: str, + hotwords: str = "", + enable_punctuation: bool = False, + enable_itn: bool = False, + enable_vad: bool = False, + sample_rate: int = 16000, + ) -> str: + """转录音频文件""" + pass + + @abstractmethod + def transcribe_file_with_vad( + self, + audio_path: str, + hotwords: str = "", + enable_punctuation: bool = True, + enable_itn: bool = True, + sample_rate: int = 16000, + **kwargs, + ) -> ASRRawResult: + """使用 VAD 转录音频文件,返回带时间戳分段的结果 + + Args: + audio_path: 音频文件路径 + hotwords: 热词/上下文提示 + enable_punctuation: 是否启用标点 + enable_itn: 是否启用 ITN + sample_rate: 采样率 + **kwargs: 额外参数(如 word_timestamps 字词级时间戳) + + Returns: + ASRRawResult 包含文本和分段信息 + """ + pass + + def transcribe_long_audio( + self, + audio_path: str, + hotwords: str = "", + enable_punctuation: bool = False, + enable_itn: bool = False, + sample_rate: int = 16000, + enable_speaker_diarization: bool = True, + enable_speaker_identification: bool = True, + enable_text_cleanup: bool = True, + word_timestamps: bool = False, + timestamp_scale: float = 1.0, + task_id: Optional[str] = None, + progress_callback: Optional[Callable[[str, str, int, Optional[dict[str, Any]]], None]] = None, + ) -> ASRFullResult: + """转录长音频文件(自动分段) + + Args: + audio_path: 音频文件路径 + hotwords: 热词 + enable_punctuation: 是否启用标点 + enable_itn: 是否启用 ITN + sample_rate: 采样率 + enable_speaker_diarization: 是否启用说话人分离 + enable_speaker_identification: 是否提取声纹向量用于匹配已注册声纹库 + enable_text_cleanup: 是否启用文本去重和口头语清理 + word_timestamps: 是否返回字词级时间戳(仅部分模型支持) + timestamp_scale: Timestamp correction factor from audio normalization. + task_id: 任务ID(用于日志追踪) + + Returns: + ASRFullResult: 包含完整文本、分段结果和时长的结果 + """ + from app.utils.audio_splitter import AudioSplitter + + # 开始性能计时 + start_time = time.time() + start_perf = time.perf_counter() + model_id = getattr(self, 'model_id', 'unknown') + stage_timings_ms: Dict[str, float] = { + "duration_probe_ms": 0.0, + "speaker_diarization_ms": 0.0, + "vad_audio_split_ms": 0.0, + "asr_inference_ms": 0.0, + "speaker_embedding_ms": 0.0, + "cleanup_ms": 0.0, + "text_cleanup_ms": 0.0, + "hotword_resolver_ms": 0.0, + } + + task_prefix = f"[{task_id}] " if task_id else "" + + def emit_progress( + stage: str, + message: str, + percentage: int, + detail: Optional[dict[str, Any]] = None, + ) -> None: + if progress_callback is None: + return + try: + progress_callback(stage, message, percentage, detail) + except Exception as exc: + logger.warning("%s进度回调失败: %s", task_prefix, exc) + + logger.info( + f"{task_prefix}[transcribe_long_audio] 音频: {audio_path}, " + f"speaker_diarization={enable_speaker_diarization}, " + f"speaker_identification={enable_speaker_identification}, " + f"text_cleanup={enable_text_cleanup}, " + f"word_level={word_timestamps}" + ) + + try: + # 获取音频时长 + duration_started = time.perf_counter() + duration = get_audio_duration(audio_path) + stage_timings_ms["duration_probe_ms"] = ( + time.perf_counter() - duration_started + ) * 1000 + _log_asr_stage_timing( + "duration_probe", + stage_timings_ms["duration_probe_ms"], + task_id=task_id, + model_id=model_id, + audio_duration_sec=duration, + audio_path=audio_path, + ) + logger.info(f"{task_prefix}[transcribe_long_audio] 音频时长: {duration:.2f}秒") + emit_progress( + "analyzing", + f"音频时长 {duration:.1f} 秒,正在进行分割。", + 15, + {"audio_duration_seconds": round(duration, 2)}, + ) + + # 统一使用分段处理 + speaker_segments = None + audio_segments = None + + if enable_speaker_diarization: + # 多说话人:使用说话人分离 + from app.utils.speaker_diarizer import SpeakerDiarizer + + logger.info(f"{task_prefix}使用说话人分离模式") + emit_progress( + "diarizing", + "正在执行说话人分离。", + 18, + {"audio_duration_seconds": round(duration, 2)}, + ) + diarizer = SpeakerDiarizer() + diarization_started = time.perf_counter() + speaker_segments = diarizer.split_audio_by_speakers(audio_path) + stage_timings_ms["speaker_diarization_ms"] = ( + time.perf_counter() - diarization_started + ) * 1000 + speaker_audio_sec = sum( + float(getattr(seg, "duration_sec", 0.0)) + for seg in (speaker_segments or []) + ) + _log_asr_stage_timing( + "speaker_diarization", + stage_timings_ms["speaker_diarization_ms"], + task_id=task_id, + model_id=model_id, + audio_duration_sec=duration, + segment_count=len(speaker_segments or []), + segmented_audio_sec=round(speaker_audio_sec, 2), + ) + + if not speaker_segments: + logger.warning(f"{task_prefix}说话人分离未检测到片段,fallback 到 VAD 分割") + + if not speaker_segments: + # 单说话人:使用 VAD 分割 + logger.info(f"{task_prefix}使用 VAD 分割模式") + emit_progress( + "splitting", + "正在执行 VAD 音频分割。", + 20, + {"audio_duration_seconds": round(duration, 2)}, + ) + splitter = AudioSplitter(device=self.device) + split_started = time.perf_counter() + audio_segments = splitter.split_audio_file(audio_path) + stage_timings_ms["vad_audio_split_ms"] = ( + time.perf_counter() - split_started + ) * 1000 + split_audio_sec = sum( + float(getattr(seg, "duration_sec", 0.0)) + for seg in (audio_segments or []) + ) + _log_asr_stage_timing( + "vad_audio_split", + stage_timings_ms["vad_audio_split_ms"], + task_id=task_id, + model_id=model_id, + audio_duration_sec=duration, + segment_count=len(audio_segments or []), + segmented_audio_sec=round(split_audio_sec, 2), + ) + + # 选择要处理的片段 + segments_to_process = speaker_segments if speaker_segments else audio_segments + if not segments_to_process: + raise DefaultServerErrorException("音频分割失败:未生成任何片段") + + logger.info(f"{task_prefix}音频已分割为 {len(segments_to_process)} 段") + total_segments = len(segments_to_process) + emit_progress( + "transcribing", + "开始批量识别。", + 30, + { + "segment_total": total_segments, + "segment_completed": 0, + "audio_duration_seconds": round(duration, 2), + }, + ) + + results: List[ASRSegmentResult] = [] + + # 使用批处理推理 + batch_size = settings.ASR_BATCH_SIZE + total_batches = (len(segments_to_process) + batch_size - 1) // batch_size + logger.info( + f"{task_prefix}使用批处理推理,batch_size={batch_size}, " + f"word_timestamps={word_timestamps}" + ) + + for batch_start in range(0, len(segments_to_process), batch_size): + batch_end = min(batch_start + batch_size, len(segments_to_process)) + batch_segments = segments_to_process[batch_start:batch_end] + batch_index = batch_start // batch_size + 1 + + logger.info( + f"{task_prefix}推理批次 " + f"{batch_index}/{total_batches}: " + f"片段 {batch_start+1}-{batch_end}/{len(segments_to_process)}" + ) + emit_progress( + "transcribing", + f"识别中:{batch_index}/{total_batches} 批,{batch_start}/{total_segments} 段", + min(88, 30 + int((batch_start / max(total_segments, 1)) * 58)), + { + "segment_total": total_segments, + "segment_completed": batch_start, + "batch_index": batch_index, + "batch_total": total_batches, + "audio_duration_seconds": round(duration, 2), + }, + ) + + try: + # 批量推理,支持时间戳 + batch_started = time.perf_counter() + batch_results = self._transcribe_batch( + segments=batch_segments, + hotwords=hotwords, + enable_punctuation=enable_punctuation, + enable_itn=enable_itn, + sample_rate=sample_rate, + word_timestamps=word_timestamps, + ) + batch_inference_ms = (time.perf_counter() - batch_started) * 1000 + stage_timings_ms["asr_inference_ms"] += batch_inference_ms + batch_audio_sec = sum( + float(getattr(seg, "duration_sec", 0.0)) + for seg in batch_segments + ) + + valid_batch_results = 0 + batch_embedding_ms = 0.0 + for seg, result in zip(batch_segments, batch_results): + if result and result.text: + valid_batch_results += 1 + start_sec = float(getattr(seg, "start_sec", 0.0)) + end_sec = float(getattr(seg, "end_sec", start_sec)) + speaker_embedding = None + if ( + speaker_segments + and settings.SPEAKER_DB_ENABLED + and enable_speaker_identification + ): + try: + from app.core.database import pg_speaker_db + from app.services.speaker_registry import ( + get_speaker_registry_service, + ) + + audio_data = getattr(seg, "audio_data", None) + if pg_speaker_db.is_connected and audio_data is not None: + embedding_started = time.perf_counter() + speaker_embedding = ( + get_speaker_registry_service() + .extract_embedding_from_audio(audio_data) + ) + batch_embedding_ms += ( + time.perf_counter() - embedding_started + ) * 1000 + except Exception as exc: + logger.warning( + "%s提取片段声纹向量失败,保留原说话人编号: %s", + task_prefix, + exc, + ) + results.append( + ASRSegmentResult( + text=result.text, + start_time=start_sec, + end_time=end_sec, + speaker_id=getattr(seg, "speaker_id", None), + speaker_embedding=speaker_embedding, + word_tokens=result.word_tokens if word_timestamps else None, + ) + ) + stage_timings_ms["speaker_embedding_ms"] += batch_embedding_ms + _log_asr_stage_timing( + "asr_batch", + batch_inference_ms, + task_id=task_id, + model_id=model_id, + audio_duration_sec=batch_audio_sec, + batch_index=batch_index, + batch_total=total_batches, + segment_start=batch_start + 1, + segment_end=batch_end, + segment_total=total_segments, + batch_segment_count=len(batch_segments), + valid_segment_count=valid_batch_results, + speaker_embedding_ms=round(batch_embedding_ms, 2), + ) + + logger.info( + f"{task_prefix}批次推理完成,有效片段: " + f"{len([r for r in batch_results if r and r.text])}" + ) + emit_progress( + "transcribing", + f"识别中:{batch_index}/{total_batches} 批,{batch_end}/{total_segments} 段", + min(90, 30 + int((batch_end / max(total_segments, 1)) * 60)), + { + "segment_total": total_segments, + "segment_completed": batch_end, + "batch_index": batch_index, + "batch_total": total_batches, + "audio_duration_seconds": round(duration, 2), + }, + ) + + except Exception as e: + logger.error(f"{task_prefix}批次推理失败: {e}, 跳过该批次") + emit_progress( + "transcribing", + f"第 {batch_index}/{total_batches} 批识别失败,已跳过。", + min(90, 30 + int((batch_end / max(total_segments, 1)) * 60)), + { + "segment_total": total_segments, + "segment_completed": batch_end, + "batch_index": batch_index, + "batch_total": total_batches, + "error": str(e), + }, + ) + + # 清理临时文件(独立清理,避免条件遗漏) + try: + cleanup_started = time.perf_counter() + if speaker_segments: + from app.utils.speaker_diarizer import SpeakerDiarizer + SpeakerDiarizer.cleanup_segments(speaker_segments) + if audio_segments: + AudioSplitter.cleanup_segments(audio_segments) + stage_timings_ms["cleanup_ms"] = ( + time.perf_counter() - cleanup_started + ) * 1000 + _log_asr_stage_timing( + "temp_cleanup", + stage_timings_ms["cleanup_ms"], + task_id=task_id, + model_id=model_id, + segment_count=len(segments_to_process), + ) + except Exception as e: + logger.warning(f"清理临时文件时出错: {e}") + + overlap_trimmed_count = 0 + if enable_text_cleanup: + text_cleanup_started = time.perf_counter() + cleaned_results: List[ASRSegmentResult] = [] + previous_text = "" + for seg in results: + cleaned_text = deduplicate_asr_text(seg.text) + cleaned_text, was_trimmed = trim_segment_boundary_overlap( + previous_text, + cleaned_text, + ) + if was_trimmed: + overlap_trimmed_count += 1 + if not cleaned_text: + continue + seg.text = cleaned_text + cleaned_results.append(seg) + previous_text = cleaned_text + + if len(cleaned_results) != len(results) or overlap_trimmed_count: + logger.info( + "%s文字去重完成:%s -> %s 段,边界重叠裁剪 %s 次", + task_prefix, + len(results), + len(cleaned_results), + overlap_trimmed_count, + ) + results = cleaned_results + stage_timings_ms["text_cleanup_ms"] = ( + time.perf_counter() - text_cleanup_started + ) * 1000 + _log_asr_stage_timing( + "text_cleanup", + stage_timings_ms["text_cleanup_ms"], + task_id=task_id, + model_id=model_id, + segment_count=len(results), + overlap_trimmed_count=overlap_trimmed_count, + cleanup_enabled=enable_text_cleanup, + ) + + hotword_resolver_started = time.perf_counter() + hotword_resolver_applied_count = 0 + matched_hotwords: list[str] = [] + if hotwords: + for seg in results: + resolved_text, was_applied, matched = apply_hotword_rules( + seg.text, + hotwords, + ) + if resolved_text: + seg.text = resolved_text + if was_applied: + hotword_resolver_applied_count += 1 + for hotword_text in matched: + if hotword_text not in matched_hotwords: + matched_hotwords.append(hotword_text) + if hotwords: + stage_timings_ms["hotword_resolver_ms"] = ( + time.perf_counter() - hotword_resolver_started + ) * 1000 + _log_asr_stage_timing( + "hotword_resolver", + stage_timings_ms["hotword_resolver_ms"], + task_id=task_id, + model_id=model_id, + segment_count=len(results), + applied_segment_count=hotword_resolver_applied_count, + matched_hotwords=matched_hotwords, + ) + if hotword_resolver_applied_count: + logger.info( + "%s热词纠偏完成:命中 %s 段,热词=%s", + task_prefix, + hotword_resolver_applied_count, + matched_hotwords, + ) + all_texts = [seg.text for seg in results] + full_text = "\n".join(all_texts) + emit_progress( + "finalizing", + "分段识别完成,正在整理文本。", + 92, + { + "segment_total": len(segments_to_process), + "segment_completed": len(segments_to_process), + "valid_segment_count": len(results), + "text_cleanup_enabled": enable_text_cleanup, + "overlap_trimmed_count": overlap_trimmed_count, + "audio_duration_seconds": round(duration, 2), + }, + ) + + logger.info( + f"长音频识别完成,共 {len(results)} 个有效分段," + f"总字符数: {len(full_text)}" + ) + + # 计算性能指标 + total_duration_ms = (time.time() - start_time) * 1000 + total_perf_ms = (time.perf_counter() - start_perf) * 1000 + + if timestamp_scale != 1.0: + for seg in results: + seg.start_time *= timestamp_scale + seg.end_time *= timestamp_scale + if seg.word_tokens: + for word_token in seg.word_tokens: + word_token.start_time *= timestamp_scale + word_token.end_time *= timestamp_scale + duration *= timestamp_scale + logger.info( + f"{task_prefix}Timestamp scaling applied: scale={timestamp_scale:.6f}" + ) + + _log_asr_stage_timing( + "asr_total", + total_perf_ms, + task_id=task_id, + model_id=model_id, + audio_duration_sec=duration, + segment_count=len(results), + total_segment_count=len(segments_to_process), + **{key: round(value, 2) for key, value in stage_timings_ms.items()}, + ) + log_inference_metrics( + logger=logger, + message="长音频识别完成", + task_id=task_id, + duration_ms=total_duration_ms, + audio_duration_sec=duration, + model_id=model_id, + status="success", + segments_count=len(results), + batch_size=settings.ASR_BATCH_SIZE, + event="asr_inference_metrics", + **{key: round(value, 2) for key, value in stage_timings_ms.items()}, + enable_speaker_diarization=enable_speaker_diarization, + enable_speaker_identification=enable_speaker_identification, + enable_text_cleanup=enable_text_cleanup, + word_timestamps=word_timestamps, + ) + + return ASRFullResult( + text=full_text, + segments=results, + duration=duration, + ) + + except Exception as e: + # 计算失败时的性能指标 + total_duration_ms = (time.time() - start_time) * 1000 + try: + duration = get_audio_duration(audio_path) + except Exception: + duration = 0 + + log_inference_metrics( + logger=logger, + message="长音频识别失败", + task_id=task_id, + duration_ms=total_duration_ms, + audio_duration_sec=duration, + model_id=model_id, + status="error", + error=str(e), + ) + + logger.error(f"长音频识别失败: {e}") + raise DefaultServerErrorException(f"长音频识别失败: {str(e)}") + + @abstractmethod + def is_model_loaded(self) -> bool: + """检查模型是否已加载""" + pass + + @property + @abstractmethod + def device(self) -> str: + """获取设备信息""" + pass + + @property + @abstractmethod + def supports_realtime(self) -> bool: + """是否支持实时识别""" + pass + + def _transcribe_batch( + self, + segments: List[Any], + hotwords: str = "", + enable_punctuation: bool = False, + enable_itn: bool = False, + sample_rate: int = 16000, + word_timestamps: bool = False, + ) -> List[ASRSegmentResult]: + """批量推理多个音频片段 + + Args: + segments: 音频片段列表(每个片段需要有 temp_file 属性) + hotwords: 热词 + enable_punctuation: 是否启用标点 + enable_itn: 是否启用 ITN + sample_rate: 采样率 + word_timestamps: 是否返回字词级时间戳 + + Returns: + ASRSegmentResult 列表,与输入片段一一对应 + """ + # 默认实现:逐个推理(子类可以重写实现真正的批处理) + results = [] + for idx, seg in enumerate(segments): + try: + if not seg.temp_file: + logger.warning(f"批处理片段 {idx + 1} 临时文件不存在,跳过") + results.append(ASRSegmentResult(text="", start_time=0.0, end_time=0.0)) + continue + + if word_timestamps: + # 需要时间戳:使用 transcribe_file_with_vad + raw_result = self.transcribe_file_with_vad( + audio_path=seg.temp_file, + hotwords=hotwords, + enable_punctuation=enable_punctuation, + enable_itn=enable_itn, + sample_rate=sample_rate, + word_timestamps=True, + ) + if raw_result.segments: + result_seg = raw_result.segments[0] + results.append( + ASRSegmentResult( + text=result_seg.text, + start_time=seg.start_sec, + end_time=seg.end_sec, + speaker_id=getattr(seg, 'speaker_id', None), + word_tokens=result_seg.word_tokens, + ) + ) + else: + results.append( + ASRSegmentResult( + text=raw_result.text, + start_time=seg.start_sec, + end_time=seg.end_sec, + speaker_id=getattr(seg, 'speaker_id', None), + ) + ) + else: + # 不需要时间戳:使用 transcribe_file + text = self.transcribe_file( + audio_path=seg.temp_file, + hotwords=hotwords, + enable_punctuation=enable_punctuation, + enable_itn=enable_itn, + enable_vad=False, + sample_rate=sample_rate, + ) + results.append( + ASRSegmentResult( + text=text or "", + start_time=seg.start_sec, + end_time=seg.end_sec, + speaker_id=getattr(seg, 'speaker_id', None), + ) + ) + except Exception as e: + logger.error(f"批处理片段 {idx + 1} 推理失败: {e}") + results.append( + ASRSegmentResult( + text="", + start_time=getattr(seg, 'start_sec', 0.0), + end_time=getattr(seg, 'end_sec', 0.0), + speaker_id=getattr(seg, 'speaker_id', None), + ) + ) + + return results + + @staticmethod + def _detect_device(device: str = "auto") -> str: + """检测可用设备""" + from app.core.device import detect_device + + return detect_device(device) + + +class RealTimeASREngine(BaseASREngine): + """实时ASR引擎抽象基类""" + + @property + def supports_realtime(self) -> bool: + """支持实时识别""" + return True + + @abstractmethod + def transcribe_websocket( + self, + audio_chunk: bytes, + cache: Optional[Dict] = None, + is_final: bool = False, + **kwargs, + ) -> str: + """WebSocket流式语音识别""" + pass diff --git a/app/services/asr/engines/global_models.py b/app/services/asr/engines/global_models.py new file mode 100644 index 0000000..586db03 --- /dev/null +++ b/app/services/asr/engines/global_models.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +""" +全局VAD/PUNC模型管理模块 +提供线程安全的全局模型实例管理 +""" + +import logging +import threading +from funasr import AutoModel + +from app.core.config import settings +from app.infrastructure import resolve_model_path + + +logger = logging.getLogger(__name__) + + +# 全局语音活动检测(VAD)模型缓存(避免重复加载) +_global_vad_model = None +_vad_model_lock = threading.Lock() +_vad_inference_lock = threading.Lock() # 推理互斥锁,防止并发状态混乱 + +# 全局标点符号模型缓存(避免重复加载) +_global_punc_model = None +_punc_model_lock = threading.Lock() +_punc_inference_lock = threading.Lock() # 推理互斥锁,防止并发状态混乱 + +# 全局实时标点符号模型缓存(避免重复加载) +_global_punc_realtime_model = None +_punc_realtime_model_lock = threading.Lock() +_punc_realtime_inference_lock = threading.Lock() # 推理互斥锁,防止并发状态混乱 + + +def _resolve_device(device: str) -> str: + """解析设备字符串,将 auto 转换为实际的设备""" + from app.core.device import detect_device + + return detect_device(device) + + +def get_global_vad_model(device: str): + """获取全局语音活动检测(VAD)模型实例(线程安全,双重检查锁定)""" + global _global_vad_model + + if _global_vad_model is None: + with _vad_model_lock: + if _global_vad_model is None: + try: + # 解析模型路径:优先使用本地缓存 + resolved_vad_path = resolve_model_path(settings.VAD_MODEL) + logger.info(f"正在加载全局语音活动检测(VAD)模型: {resolved_vad_path}") + + # 解析 auto 设备 + resolved_device = _resolve_device(device) + + _global_vad_model = AutoModel( + model=resolved_vad_path, + device=resolved_device, + speech_noise_thres=0.6, # VAD 语音噪声阈值(FunASR默认0.6,设为0.7稍微严格一些,分段更碎) + **settings.FUNASR_AUTOMODEL_KWARGS, + ) + logger.info("全局语音活动检测(VAD)模型加载成功 (speech_noise_thres=0.6)") + except Exception as e: + logger.error(f"全局语音活动检测(VAD)模型加载失败: {str(e)}") + _global_vad_model = None + raise + + return _global_vad_model + + +def get_vad_inference_lock(): + """获取VAD模型推理锁(线程安全)""" + return _vad_inference_lock + + +def get_global_punc_model(device: str): + """获取全局标点符号模型实例(离线版,线程安全,双重检查锁定)""" + global _global_punc_model + + if _global_punc_model is None: + with _punc_model_lock: + if _global_punc_model is None: + try: + # 解析模型路径:优先使用本地缓存 + resolved_punc_path = resolve_model_path(settings.PUNC_MODEL) + logger.info(f"正在加载全局标点符号模型(离线): {resolved_punc_path}") + + # 解析 auto 设备 + resolved_device = _resolve_device(device) + + _global_punc_model = AutoModel( + model=resolved_punc_path, + device=resolved_device, + **settings.FUNASR_AUTOMODEL_KWARGS, + ) + logger.info("全局标点符号模型(离线)加载成功") + except Exception as e: + logger.error(f"全局标点符号模型(离线)加载失败: {str(e)}") + _global_punc_model = None + raise + + return _global_punc_model + + +def get_punc_inference_lock(): + """获取PUNC模型推理锁(线程安全)""" + return _punc_inference_lock + + +def get_global_punc_realtime_model(device: str): + """获取全局实时标点符号模型实例(线程安全,双重检查锁定)""" + global _global_punc_realtime_model + + if _global_punc_realtime_model is None: + with _punc_realtime_model_lock: + if _global_punc_realtime_model is None: + try: + # 解析模型路径:优先使用本地缓存 + resolved_punc_realtime_path = resolve_model_path(settings.PUNC_REALTIME_MODEL) + logger.info(f"正在加载全局标点符号模型(实时): {resolved_punc_realtime_path}") + + # 解析 auto 设备 + resolved_device = _resolve_device(device) + + _global_punc_realtime_model = AutoModel( + model=resolved_punc_realtime_path, + device=resolved_device, + **settings.FUNASR_AUTOMODEL_KWARGS, + ) + logger.info("全局标点符号模型(实时)加载成功") + except Exception as e: + logger.error(f"全局标点符号模型(实时)加载失败: {str(e)}") + _global_punc_realtime_model = None + raise + + return _global_punc_realtime_model + + +def get_punc_realtime_inference_lock(): + """获取实时PUNC模型推理锁(线程安全)""" + return _punc_realtime_inference_lock diff --git a/app/services/asr/implementations/__init__.py b/app/services/asr/implementations/__init__.py new file mode 100644 index 0000000..1c727a6 --- /dev/null +++ b/app/services/asr/implementations/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +""" +模型实现模块 +包含需要本地代码的自定义模型实现 +""" diff --git a/app/services/asr/manager.py b/app/services/asr/manager.py new file mode 100644 index 0000000..e75825f --- /dev/null +++ b/app/services/asr/manager.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +"""ASR model metadata and engine factory.""" + +import json +import threading +import logging +from typing import Dict, Any, Optional, List +from pathlib import Path + +from typing import Callable +from ...core.config import settings +from ...core.exceptions import DefaultServerErrorException, InvalidParameterException +from .engines import BaseASREngine +from .model_plan import get_default_model_id + +logger = logging.getLogger(__name__) + +# 引擎注册表(使用Any避免循环导入问题) +_ENGINE_REGISTRY: Dict[str, Callable[[Any], BaseASREngine]] = {} + + +def _supports_qwen_realtime_on_device(configured_device: str) -> bool: + """Resolve whether Qwen realtime mode is available on the active device.""" + from app.core.device import detect_device + from app.core.accelerator import get_accelerator_info + from .qwenasr_rust import is_qwenasr_rust_available + + device = detect_device(configured_device) + accelerator = get_accelerator_info() + if accelerator.is_gpu and device.startswith("cuda"): + return True + if device == "cpu": + return is_qwenasr_rust_available() + return False + + +def register_engine(engine_type: str, factory: Callable[[Any], BaseASREngine]): + """注册ASR引擎工厂函数""" + _ENGINE_REGISTRY[engine_type] = factory + logger.info(f"注册引擎类型: {engine_type}") + + +class DeclaredEntryConfig: + """声明条目配置,可表示模型或 capability。""" + + def __init__(self, model_id: str, config: Dict[str, Any]): + self.model_id = model_id + self.name = config["name"] + self.kind = config.get("kind", "model") + self.engine = config["engine"] + self.description = config.get("description", "") + self.languages = config.get("languages", []) + self.supports_realtime = config.get("supports_realtime", False) + + # 模型路径结构 + self.models = config.get("models", {}) + self.offline_model_path = self.models.get("offline") + self.realtime_model_path = self.models.get("realtime") + + # 额外参数(如 trust_remote_code 等) + self.extra_kwargs = config.get("extra_kwargs", {}) + + @property + def has_offline_model(self) -> bool: + """是否有离线模型""" + return bool(self.offline_model_path) + + @property + def has_realtime_model(self) -> bool: + """是否有实时模型""" + return bool(self.realtime_model_path) + +class ModelManager: + """Static model metadata plus engine construction.""" + + def __init__(self): + self._declared_entry_configs: Dict[str, DeclaredEntryConfig] = {} + self._default_model_id: Optional[str] = None + self._load_models_config() + + def _load_models_config(self) -> None: + """加载模型配置文件""" + models_file = Path(settings.models_config_path) + if not models_file.exists(): + raise DefaultServerErrorException("models.json 配置文件不存在") + + try: + with open(models_file, "r", encoding="utf-8") as f: + config = json.load(f) + + for model_id, model_config in config["models"].items(): + self._declared_entry_configs[model_id] = DeclaredEntryConfig(model_id, model_config) + self._default_model_id = get_default_model_id( + all_model_ids=list(self._declared_entry_configs.keys()), + ) + + if not self._default_model_id and self._declared_entry_configs: + self._default_model_id = list(self._declared_entry_configs.keys())[0] + + except (json.JSONDecodeError, KeyError) as e: + raise DefaultServerErrorException(f"模型配置文件格式错误: {str(e)}") + + def get_declared_entry_config(self, model_id: Optional[str] = None) -> DeclaredEntryConfig: + """获取声明条目配置。""" + if model_id is None: + model_id = self._default_model_id + + if not model_id: + raise InvalidParameterException("未指定模型且没有默认模型") + + if model_id not in self._declared_entry_configs: + available_models = ", ".join(self._declared_entry_configs.keys()) + raise InvalidParameterException( + f"未知的模型: {model_id},可用模型: {available_models}" + ) + + return self._declared_entry_configs[model_id] + + def list_declared_entries(self) -> List[Dict[str, Any]]: + """列出声明的模型与 capability 元数据。""" + entries = [] + for model_id, config in self._declared_entry_configs.items(): + offline_path_exists = False + realtime_path_exists = False + + if config.offline_model_path: + offline_model_path = ( + Path(settings.MODELSCOPE_PATH) / config.offline_model_path + ) + offline_path_exists = offline_model_path.exists() + + if config.realtime_model_path: + realtime_model_path = ( + Path(settings.MODELSCOPE_PATH) / config.realtime_model_path + ) + realtime_path_exists = realtime_model_path.exists() + + supports_realtime = config.supports_realtime + if config.engine == "qwen3": + supports_realtime = _supports_qwen_realtime_on_device(settings.DEVICE) + + entries.append( + { + "id": model_id, + "kind": config.kind, + "name": config.name, + "engine": config.engine, + "description": config.description, + "languages": config.languages, + "default": model_id == self._default_model_id, + "supports_realtime": supports_realtime, + "offline_model": ( + { + "path": config.offline_model_path, + "exists": offline_path_exists, + } + if config.offline_model_path + else None + ), + "realtime_model": ( + { + "path": config.realtime_model_path, + "exists": realtime_path_exists, + } + if config.realtime_model_path + else None + ), + } + ) + + return entries + + def _create_engine(self, config: DeclaredEntryConfig) -> BaseASREngine: + """创建ASR引擎实例""" + engine_type = config.engine.lower() + factory = _ENGINE_REGISTRY.get(engine_type) + if not factory: + raise InvalidParameterException( + f"不支持的引擎类型: {config.engine}" + ) + return factory(config) + + def create_engine(self, model_id: Optional[str] = None) -> BaseASREngine: + """Create a fresh engine instance.""" + config = self.get_declared_entry_config(model_id) + return self._create_engine(config) + +# 全局模型管理器实例 +_model_manager: Optional[ModelManager] = None +_model_manager_lock = threading.Lock() + + +def get_model_manager() -> ModelManager: + """获取全局模型管理器实例(线程安全)""" + global _model_manager + if _model_manager is None: + with _model_manager_lock: + if _model_manager is None: + _model_manager = ModelManager() + return _model_manager + + +# 注册内置引擎 +def _register_builtin_engines(): + """注册内置的ASR引擎""" + try: + from .qwen3_engine import Qwen3ASREngine # noqa: F401 + from .qwen3_engine import _register_qwen3_engine + _register_qwen3_engine(register_engine, DeclaredEntryConfig) + except ImportError as e: + logger.warning(f"Qwen3引擎不可用: {e}") + + +# 模块加载时自动注册内置引擎 +_register_builtin_engines() diff --git a/app/services/asr/model_capabilities.py b/app/services/asr/model_capabilities.py new file mode 100644 index 0000000..8800128 --- /dev/null +++ b/app/services/asr/model_capabilities.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- +"""Shared capability-to-model asset definitions.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal, Optional + +from app.core.config import settings +from app.services.asr.manager import get_model_manager +from app.services.asr.model_plan import ( + get_active_qwen_model, + load_supported_model_ids, +) + + +ModelSource = Literal["modelscope"] + + +@dataclass(frozen=True) +class ModelAsset: + source: ModelSource + model_id: str + description: str + revision: Optional[str] = None + required_patterns: tuple[str, ...] = () + alternative_required_patterns: tuple[tuple[str, ...], ...] = () + min_total_size_bytes: int = 0 + + +_VAD_ASSETS = ( + ModelAsset( + source="modelscope", + model_id=settings.VAD_MODEL, + description="VAD", + revision="v2.0.2", + required_patterns=("configuration.json", "config.yaml", "model.pb"), + min_total_size_bytes=1_000_000, + ), +) + +_DIARIZATION_ASSETS = ( + ModelAsset( + source="modelscope", + model_id="iic/speech_campplus_speaker-diarization_common", + description="CAM++ Diarization", + required_patterns=( + "configuration.json", + "config.yaml", + "onnx/asd.onnx", + "onnx/face_recog_ir101.onnx", + "onnx/fqa.onnx", + "onnx/version-RFB-320.onnx", + ), + min_total_size_bytes=50_000_000, + ), + ModelAsset( + source="modelscope", + model_id=settings.SV_MODEL, + description="Configured Speaker Verification", + required_patterns=("configuration.json", "config.yaml", "campplus_cn_common.bin"), + min_total_size_bytes=10_000_000, + ), + ModelAsset( + source="modelscope", + model_id=settings.REALTIME_SV_MODEL, + description="Realtime Speaker Verification", + required_patterns=("configuration.json",), + min_total_size_bytes=10_000_000, + ), + ModelAsset( + source="modelscope", + model_id="damo/speech_campplus_sv_zh-cn_16k-common", + description="CAM++ Speaker Verification", + required_patterns=("configuration.json", "config.yaml", "campplus_cn_common.bin"), + min_total_size_bytes=10_000_000, + ), + ModelAsset( + source="modelscope", + model_id="damo/speech_campplus-transformer_scl_zh-cn_16k-common", + description="CAM++ Transformer", + required_patterns=("configuration.json", "campplus_cn_encoder.pt", "transformer_backend.pt"), + min_total_size_bytes=10_000_000, + ), +) + +def get_download_modelscope_assets() -> list[ModelAsset]: + """Return the full static ModelScope export set used by predownload/export.""" + return _dedupe_assets([ + *_VAD_ASSETS, + *_DIARIZATION_ASSETS, + ]) + + +def get_runtime_required_modelscope_assets( + *, + include_realtime_punc: bool, +) -> list[ModelAsset]: + """Return ModelScope assets required by the current runtime plan.""" + _ = include_realtime_punc + return _dedupe_assets([*_VAD_ASSETS, *_DIARIZATION_ASSETS]) + + +def _dedupe_assets(assets: list[ModelAsset]) -> list[ModelAsset]: + deduped: list[ModelAsset] = [] + seen: set[tuple[str, str]] = set() + for asset in assets: + key = (asset.source, asset.model_id) + if key in seen: + continue + seen.add(key) + deduped.append(asset) + return deduped + + +def get_camplusplus_replacement_paths(cache_dir: str) -> dict[str, str]: + """Return the CAM++ offline replacement map for local cache paths.""" + return { + "damo/speech_campplus_sv_zh-cn_16k-common": f"{cache_dir}/damo/speech_campplus_sv_zh-cn_16k-common", + "iic/speech_campplus_sv_zh-cn_16k-common": f"{cache_dir}/iic/speech_campplus_sv_zh-cn_16k-common", + "damo/speech_campplus-transformer_scl_zh-cn_16k-common": f"{cache_dir}/damo/speech_campplus-transformer_scl_zh-cn_16k-common", + "damo/speech_campplus-transformer_scl_zh-cn-16k-common": f"{cache_dir}/damo/speech_campplus-transformer_scl_zh-cn-16k-common", + "damo/speech_fsmn_vad_zh-cn-16k-common-pytorch": f"{cache_dir}/damo/speech_fsmn_vad_zh-cn-16k-common-pytorch", + } + + +_QWEN_MODELSCOPE_MODEL_IDS = { + "Qwen/Qwen3-ASR-0.6B": "Qwen/Qwen3-ASR-0.6B", + "Qwen/Qwen3-ASR-1.7B": "Qwen/Qwen3-ASR-1.7B", + "Qwen/Qwen3-ForcedAligner-0.6B": "Qwen/Qwen3-ForcedAligner-0.6B", +} + + +def get_qwen_modelscope_model_id(model_id: str) -> Optional[str]: + """Map runtime Qwen model ID to the corresponding ModelScope model ID. + + Returns: + ModelScope model ID if available, None otherwise. + """ + return _QWEN_MODELSCOPE_MODEL_IDS.get(model_id) + + +def get_enabled_qwen_modelscope_assets( + *, + include_forced_aligner: bool = True, +) -> list[ModelAsset]: + """Return ModelScope assets required by the runtime Qwen plan.""" + manager = get_model_manager() + assets: list[ModelAsset] = [] + model_id = get_active_qwen_model() + model_config = manager.get_declared_entry_config(model_id) + offline_model = model_config.offline_model_path + if offline_model: + ms_model_id = get_qwen_modelscope_model_id(offline_model) + if ms_model_id: + assets.append( + ModelAsset( + source="modelscope", + model_id=ms_model_id, + description=f"{model_config.name} Offline (ModelScope)", + required_patterns=("config.json",), + alternative_required_patterns=( + ("model.safetensors",), + ("model-*.safetensors",), + ), + min_total_size_bytes=500_000_000, + ) + ) + forced_aligner = str(model_config.extra_kwargs.get("forced_aligner_path") or "").strip() + if forced_aligner and include_forced_aligner: + ms_aligner_id = get_qwen_modelscope_model_id(forced_aligner) + if ms_aligner_id: + assets.append( + ModelAsset( + source="modelscope", + model_id=ms_aligner_id, + description=f"{model_config.name} Forced Aligner (ModelScope)", + required_patterns=("config.json", "model.safetensors"), + min_total_size_bytes=500_000_000, + ) + ) + return assets + + +def get_all_qwen_modelscope_assets( + *, + include_forced_aligner: bool = True, +) -> list[ModelAsset]: + """Return all declared Qwen ModelScope assets for offline bundles.""" + manager = get_model_manager() + assets: list[ModelAsset] = [] + seen_model_ids: set[str] = set() + + for model_id in sorted(load_supported_model_ids()): + if not model_id.startswith("qwen3-asr-"): + continue + model_config = manager.get_declared_entry_config(model_id) + offline_model = model_config.offline_model_path + if offline_model: + ms_model_id = get_qwen_modelscope_model_id(offline_model) + if ms_model_id and ms_model_id not in seen_model_ids: + seen_model_ids.add(ms_model_id) + assets.append( + ModelAsset( + source="modelscope", + model_id=ms_model_id, + description=f"{model_config.name} Offline (ModelScope)", + required_patterns=("config.json",), + alternative_required_patterns=( + ("model.safetensors",), + ("model-*.safetensors",), + ), + min_total_size_bytes=500_000_000, + ) + ) + forced_aligner = str(model_config.extra_kwargs.get("forced_aligner_path") or "").strip() + if forced_aligner and include_forced_aligner: + ms_aligner_id = get_qwen_modelscope_model_id(forced_aligner) + if ms_aligner_id and ms_aligner_id not in seen_model_ids: + seen_model_ids.add(ms_aligner_id) + assets.append( + ModelAsset( + source="modelscope", + model_id=ms_aligner_id, + description="Qwen3 Forced Aligner (ModelScope)", + required_patterns=("config.json", "model.safetensors"), + min_total_size_bytes=500_000_000, + ) + ) + return assets diff --git a/app/services/asr/model_plan.py b/app/services/asr/model_plan.py new file mode 100644 index 0000000..2a13565 --- /dev/null +++ b/app/services/asr/model_plan.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +"""Single-source deployment model planning.""" + +from __future__ import annotations + +import json +import os +import platform +from pathlib import Path +from typing import Optional + +from app.core.config import settings + +QWEN_MODEL_OVERRIDE_ENV = "QWEN3_ASR_MODEL" +_QWEN_MODEL_ALIASES = { + "qwen3-asr-0.6b": "qwen3-asr-0.6b", + "0.6b": "qwen3-asr-0.6b", + "0.6": "qwen3-asr-0.6b", + "qwen/qwen3-asr-0.6b": "qwen3-asr-0.6b", + "qwen3-asr-1.7b": "qwen3-asr-1.7b", + "1.7b": "qwen3-asr-1.7b", + "1.7": "qwen3-asr-1.7b", + "qwen/qwen3-asr-1.7b": "qwen3-asr-1.7b", +} + + +def load_supported_model_ids() -> list[str]: + """Load declared model ids from models.json.""" + models_file = Path(settings.models_config_path) + if not models_file.exists(): + return [] + + with open(models_file, "r", encoding="utf-8") as f: + config = json.load(f) + + return list(config.get("models", {}).keys()) + + +def get_qwen_model_override() -> Optional[str]: + """Return the explicit Qwen model override from the environment.""" + raw_value = (os.getenv(QWEN_MODEL_OVERRIDE_ENV) or "").strip() + if not raw_value: + return None + + normalized = _QWEN_MODEL_ALIASES.get(raw_value.lower(), raw_value) + return normalized + + +def detect_qwen_model_by_vram(all_model_ids: Optional[list[str]] = None) -> Optional[str]: + """Pick the active Qwen model for the current machine.""" + from app.core.accelerator import get_accelerator_info + from app.core.device import detect_device, get_vram_gb + from app.services.asr.qwenasr_rust import is_qwenasr_rust_available + + model_ids = all_model_ids or load_supported_model_ids() + override_model = get_qwen_model_override() + if override_model: + return override_model if override_model in model_ids else None + + resolved_device = detect_device(settings.DEVICE) + accelerator = get_accelerator_info() + + # macOS defaults to the lighter Rust CPU path unless QWEN3_ASR_MODEL is set. + if platform.system() == "Darwin": + return "qwen3-asr-0.6b" if is_qwenasr_rust_available() and "qwen3-asr-0.6b" in model_ids else None + + if resolved_device == "cpu" or not accelerator.is_gpu: + return "qwen3-asr-0.6b" if is_qwenasr_rust_available() and "qwen3-asr-0.6b" in model_ids else None + + vram = get_vram_gb() + preferred = "qwen3-asr-1.7b" if vram >= 32 else "qwen3-asr-0.6b" + if preferred in model_ids: + return preferred + + fallback = "qwen3-asr-0.6b" if preferred == "qwen3-asr-1.7b" else "qwen3-asr-1.7b" + return fallback if fallback in model_ids else None + + +def get_active_qwen_model(all_model_ids: Optional[list[str]] = None) -> str: + """Return the required Qwen model for the current machine.""" + model_ids = all_model_ids or load_supported_model_ids() + qwen_model = detect_qwen_model_by_vram(model_ids) + if not qwen_model: + override_model = get_qwen_model_override() + if override_model: + available_qwen_models = ", ".join( + model_id for model_id in model_ids if model_id.startswith("qwen") + ) + raise RuntimeError( + f"{QWEN_MODEL_OVERRIDE_ENV}={override_model} 不在可用 Qwen3-ASR 模型中: " + f"{available_qwen_models}" + ) + raise RuntimeError("当前环境未找到可运行的 Qwen3-ASR 模型") + return qwen_model + + +def get_runtime_model_ids(all_model_ids: Optional[list[str]] = None) -> list[str]: + """Return the runtime model/capability plan for the current machine.""" + model_ids = all_model_ids or load_supported_model_ids() + return [get_active_qwen_model(model_ids)] + + +def get_default_model_id(all_model_ids: Optional[list[str]] = None) -> str: + """Return the single default offline model for API/UI selection.""" + return get_active_qwen_model(all_model_ids) diff --git a/app/services/asr/model_selection.py b/app/services/asr/model_selection.py new file mode 100644 index 0000000..336bac1 --- /dev/null +++ b/app/services/asr/model_selection.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +"""Offline/realtime model selection helpers.""" + +from __future__ import annotations + +from typing import List, Optional + +from ...core.exceptions import InvalidParameterException +from .manager import get_model_manager +from .model_plan import ( + get_active_qwen_model, + get_default_model_id, + get_runtime_model_ids, +) + + +def get_active_qwen_model_id() -> str: + """Return the currently active Qwen model id.""" + active_qwen_model = get_active_qwen_model() + return active_qwen_model or "qwen3-asr-0.6b" + + +def get_offline_model_ids() -> List[str]: + """Return enabled offline-capable models for docs and APIs.""" + manager = get_model_manager() + runtime_models = get_runtime_model_ids() + + def sort_key(model_id: str) -> tuple[int, str]: + if model_id.startswith("qwen"): + return (0, model_id) + return (1, model_id) + + offline_models = [ + model_id + for model_id in runtime_models + if manager.get_declared_entry_config(model_id).has_offline_model + ] + return sorted(offline_models, key=sort_key) + + +def get_default_offline_model_id() -> str: + """Return the default offline-capable model.""" + default_model = get_default_model_id() + if default_model: + try: + if get_model_manager().get_declared_entry_config(default_model).has_offline_model: + return default_model + except InvalidParameterException: + pass + return get_active_qwen_model_id() + + +def validate_offline_model_id(model_id: Optional[str]) -> str: + """Validate offline-capable model ids for REST transcription requests.""" + available_models = get_offline_model_ids() + + if not model_id or not model_id.strip(): + return get_default_offline_model_id() + + requested_model = model_id.strip() + if requested_model.lower() == "qwen3-asr": + active_qwen_model = get_active_qwen_model_id() + if active_qwen_model.startswith("qwen") and active_qwen_model in available_models: + return active_qwen_model + raise InvalidParameterException("当前环境未启用 Qwen3-ASR 模型") + + if requested_model not in available_models: + raise InvalidParameterException( + f"不支持的离线模型ID: {requested_model}。可用模型: {', '.join(available_models)}" + ) + + return requested_model + + +def validate_realtime_model_id(model_id: Optional[str]) -> str: + """Validate realtime-capable model ids for websocket protocols.""" + available_models = get_offline_model_ids() + + if not model_id: + return get_default_offline_model_id() + + if model_id.lower() == "qwen3-asr": + active_qwen_model = get_active_qwen_model_id() + if active_qwen_model.startswith("qwen") and active_qwen_model in available_models: + return active_qwen_model + raise InvalidParameterException("当前环境未启用 Qwen3-ASR 模型") + + if model_id not in available_models: + raise InvalidParameterException( + f"不支持的模型ID: {model_id}。可用模型: {', '.join(available_models)}" + ) + + return model_id diff --git a/app/services/asr/models.json b/app/services/asr/models.json new file mode 100644 index 0000000..05e6cb3 --- /dev/null +++ b/app/services/asr/models.json @@ -0,0 +1,70 @@ +{ + "models": { + "qwen3-asr-1.7b": { + "name": "Qwen3-ASR-1.7B", + "kind": "model", + "engine": "qwen3", + "description": "Qwen3-ASR 1.7B,支持52种语言和方言;CUDA 使用 vLLM,CPU/macOS 使用 QwenASR Rust backend", + "languages": [ + "zh", + "en", + "yue", + "ja", + "ko", + "ar", + "de", + "es", + "fr", + "pt", + "id", + "it", + "ru", + "th", + "vi" + ], + "default": true, + "supports_realtime": true, + "models": { + "offline": "Qwen/Qwen3-ASR-1.7B" + }, + "extra_kwargs": { + "max_model_len": 16384, + "forced_aligner_path": "Qwen/Qwen3-ForcedAligner-0.6B", + "max_inference_batch_size": 16 + } + }, + "qwen3-asr-0.6b": { + "name": "Qwen3-ASR-0.6B", + "kind": "model", + "engine": "qwen3", + "description": "Qwen3-ASR 0.6B,轻量版支持52种语言和方言;CUDA 使用 vLLM,CPU/macOS 使用 QwenASR Rust backend", + "languages": [ + "zh", + "en", + "yue", + "ja", + "ko", + "ar", + "de", + "es", + "fr", + "pt", + "id", + "it", + "ru", + "th", + "vi" + ], + "default": false, + "supports_realtime": true, + "models": { + "offline": "Qwen/Qwen3-ASR-0.6B" + }, + "extra_kwargs": { + "max_model_len": 16384, + "forced_aligner_path": "Qwen/Qwen3-ForcedAligner-0.6B", + "max_inference_batch_size": 16 + } + } + } +} diff --git a/app/services/asr/offline_transcription_service.py b/app/services/asr/offline_transcription_service.py new file mode 100644 index 0000000..f91419c --- /dev/null +++ b/app/services/asr/offline_transcription_service.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +"""Shared offline transcription workflow.""" + +from __future__ import annotations + +from dataclasses import dataclass +import logging +from typing import Any, Callable, Optional + +from fastapi import Request + +from app.models.common import SampleRate +from app.services.asr.engines import ASRFullResult +from app.services.asr.model_selection import validate_offline_model_id +from app.services.asr.runtime import OfflineASRRequest, get_runtime_router +from app.services.audio import get_audio_service + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class PreparedAudio: + normalized_path: str + duration: float + original_path: str + timestamp_scale: float = 1.0 + + +@dataclass(frozen=True) +class OfflineTranscriptionOptions: + model_id: Optional[str] = None + sample_rate: int = 16000 + hotwords: str = "" + enable_speaker_diarization: bool = True + enable_speaker_identification: bool = True + enable_text_cleanup: bool = True + word_timestamps: bool = False + task_id: Optional[str] = None + progress_callback: Optional[ + Callable[[str, str, int, Optional[dict[str, Any]]], None] + ] = None + + +class OfflineTranscriptionService: + """Prepare audio and run the active offline ASR model.""" + + def __init__(self) -> None: + self._audio_service = get_audio_service() + + async def prepare_from_request( + self, + *, + request: Request, + audio_address: Optional[str], + task_id: str, + sample_rate: int, + ) -> PreparedAudio: + audio = await self._audio_service.process_from_request( + request=request, + audio_address=audio_address, + task_id=task_id, + sample_rate=sample_rate, + ) + return PreparedAudio( + normalized_path=audio.normalized_path, + duration=audio.duration, + original_path=audio.original_path, + timestamp_scale=audio.timestamp_scale, + ) + + async def prepare_upload( + self, + *, + audio_data: bytes, + filename: Optional[str], + task_id: str, + sample_rate: int, + ) -> PreparedAudio: + audio = await self._audio_service.process_upload_file( + audio_data=audio_data, + filename=filename, + task_id=task_id, + sample_rate=sample_rate, + ) + return PreparedAudio( + normalized_path=audio.normalized_path, + duration=audio.duration, + original_path=audio.original_path, + timestamp_scale=audio.timestamp_scale, + ) + + async def transcribe( + self, + prepared_audio: PreparedAudio, + options: OfflineTranscriptionOptions, + ) -> ASRFullResult: + model_id = validate_offline_model_id(options.model_id) + logger.info( + "ASR model resolved: requested=%s, resolved=%s", + options.model_id, + model_id, + ) + return await get_runtime_router().run_offline( + OfflineASRRequest( + model_id=model_id, + audio_path=prepared_audio.normalized_path, + hotwords=options.hotwords, + enable_punctuation=True, + enable_itn=True, + sample_rate=options.sample_rate or int(SampleRate.RATE_16000), + enable_speaker_diarization=options.enable_speaker_diarization, + enable_speaker_identification=options.enable_speaker_identification, + enable_text_cleanup=options.enable_text_cleanup, + word_timestamps=options.word_timestamps, + timestamp_scale=prepared_audio.timestamp_scale, + task_id=options.task_id, + progress_callback=options.progress_callback, + ) + ) + + def cleanup(self, prepared_audio: Optional[PreparedAudio]) -> None: + if prepared_audio is None: + return + self._audio_service.cleanup( + prepared_audio.original_path, + prepared_audio.normalized_path, + ) + + +_offline_transcription_service: Optional[OfflineTranscriptionService] = None + + +def get_offline_transcription_service() -> OfflineTranscriptionService: + global _offline_transcription_service + if _offline_transcription_service is None: + _offline_transcription_service = OfflineTranscriptionService() + return _offline_transcription_service diff --git a/app/services/asr/qwen3_engine.py b/app/services/asr/qwen3_engine.py new file mode 100644 index 0000000..2ab2f3b --- /dev/null +++ b/app/services/asr/qwen3_engine.py @@ -0,0 +1,693 @@ +# -*- coding: utf-8 -*- +"""Qwen3-ASR engine with official vLLM and vendored Rust backends.""" + +import logging +import os +from concurrent.futures import ThreadPoolExecutor +from typing import Optional, List, Any +from dataclasses import dataclass + +import torch +import numpy as np + +from app.core.accelerator import get_accelerator_info +from app.core.device import get_vram_gb +from app.core.hotword_resolver import format_hotword_prompt_context +from .engines import BaseASREngine, ASRRawResult, ASRSegmentResult, WordToken +from .qwenasr_rust import ( + QwenASRRustRuntime, + is_qwenasr_rust_available, + resolve_qwenasr_model_path, +) +from .qwen3_vllm import Qwen3VLLMBackend, is_vllm_available +from ...core.exceptions import DefaultServerErrorException +from ...core.config import settings +from ...utils.text_processing import normalize_asr_text + +logger = logging.getLogger(__name__) + + +def _resolve_tensor_parallel_size() -> int: + topology = (os.getenv("ASR_ACTIVE_TOPOLOGY") or os.getenv("ASR_DEPLOY_TOPOLOGY") or "isolated").strip().lower() + if topology != "sharded": + return 1 + + visible = ( + os.getenv("ASR_VISIBLE_DEVICES") + or os.getenv("CUDA_VISIBLE_DEVICES") + or os.getenv("METAX_VISIBLE_DEVICES") + or os.getenv("MACA_VISIBLE_DEVICES") + or os.getenv("MX_VISIBLE_DEVICES") + or os.getenv("ILUVATAR_VISIBLE_DEVICES") + or os.getenv("IX_VISIBLE_DEVICES") + or os.getenv("MTHREADS_VISIBLE_DEVICES") + or os.getenv("MUSA_VISIBLE_DEVICES") + or "" + ).strip() + if not visible or visible.lower() in {"all", "none", "void"}: + return 1 + + count = len([part for part in visible.split(",") if part.strip()]) + return count if count > 1 else 1 + + +def calculate_gpu_memory_utilization(model_path: str) -> float: + """Calculate vLLM GPU memory utilization for the active model. + + vLLM uses this ratio as an allocation budget, not just model weights. + Keep the observed requirement slightly above the bare minimum so KV cache + and profiling have enough room. + """ + # Check environment variable override first + env_override = os.getenv("QWEN_GPU_MEMORY_UTILIZATION") + if env_override: + try: + value = float(env_override) + if 0.0 < value <= 1.0: + logger.info(f"Using environment override: gpu_memory_utilization={value}") + return value + else: + logger.warning(f"Invalid QWEN_GPU_MEMORY_UTILIZATION={env_override}, must be 0.0-1.0") + except ValueError: + logger.warning(f"Invalid QWEN_GPU_MEMORY_UTILIZATION={env_override}, not a float") + + model_memory_profiles = { + "0.6B": 8, + "1.7B": 12.0, + } + + if "0.6B" in model_path: + model_size = "0.6B" + else: + model_size = "1.7B" + required_memory_gb = model_memory_profiles[model_size] + + try: + accelerator = get_accelerator_info() + total_vram_gb = get_vram_gb() + if not accelerator.is_gpu or total_vram_gb <= 0: + logger.warning("Accelerator memory unavailable, using fallback gpu_memory_utilization=0.5") + return 0.5 + + utilization = max(required_memory_gb / total_vram_gb, 0.25) + utilization = min(utilization, 0.95) + + logger.info( + "GPU memory calculation: vendor=%s, model=%s, requires=%.1fGB, total_vram=%.1fGB, utilization=%.2f", + accelerator.vendor, + model_size, + required_memory_gb, + total_vram_gb, + utilization, + ) + + if utilization >= 0.90: + logger.warning( + "VRAM may be insufficient: %.1fGB available, %.1fGB required. Consider using smaller model.", + total_vram_gb, + required_memory_gb, + ) + + return round(utilization, 2) + + except Exception as e: + logger.error(f"Failed to detect VRAM: {e}, using fallback gpu_memory_utilization=0.5") + return 0.5 + + +def _handle_asr_error(operation: str): + """统一错误处理装饰器""" + def decorator(func): + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + logger.error(f"{operation} 失败: {e}") + raise DefaultServerErrorException(f"{operation} 失败: {e}") + return wrapper + return decorator + + +@dataclass +class Qwen3StreamingState: + internal_state: Any + chunk_size_sec: float = 1.2 + unfixed_chunk_num: int = 2 + unfixed_token_num: int = 5 + max_new_tokens: int = 32 + language: Optional[str] = None + chunk_count: int = 0 + last_text: str = "" + last_language: str = "" + + +class Qwen3ASREngine(BaseASREngine): + model: Any + + @property + def supports_realtime(self) -> bool: + return self._backend in {"vllm", "rust"} + + def __init__( + self, + model_path: str = "Qwen/Qwen3-ASR-1.7B", + device: str = "auto", + forced_aligner_path: Optional[str] = None, + max_inference_batch_size: int = 32, + max_new_tokens: int = 1024, + max_model_len: Optional[int] = None, + **_kwargs, + ): + """Initialize Qwen3-ASR engine + + CUDA -> official vLLM backend + CPU/macOS -> QwenASR Rust backend + """ + from app.core.device import detect_device + + model_id = _kwargs.pop("model_id", None) + if model_id: + model_path = model_id + + self._device = detect_device(device) + self._accelerator = get_accelerator_info() + self.model_id = model_path + self.model_path = model_path + self._backend = self._select_backend() + self._forced_aligner_path = forced_aligner_path + self._rust_num_threads = 0 + self._rust_verbosity = 0 + self._rust_batch_runtimes: list[QwenASRRustRuntime] = [] + + try: + if self._backend == "vllm": + self.model = self._load_vllm( + model_path, forced_aligner_path, + max_inference_batch_size, max_new_tokens, max_model_len, + ) + elif self._backend == "rust": + self.model = self._load_rust_backend(model_path, forced_aligner_path) + self._warmup_forced_aligner() + logger.info("Qwen3-ASR model loaded successfully with backend=%s", self._backend) + except Exception as e: + logger.error(f"Failed to load Qwen3-ASR model: {e}") + raise DefaultServerErrorException(f"Failed to load Qwen3-ASR model: {e}") + + def _select_backend(self) -> str: + if self._accelerator.is_gpu and self._device.startswith("cuda"): + if not is_vllm_available(): + raise DefaultServerErrorException( + "Current Python environment is missing vLLM with Qwen3 forced aligner support. " + f"accelerator={self._accelerator.vendor}. " + "For NVIDIA run ./scripts/sync_gpu_env.sh; for MetaX run ./scripts/sync_metax_env.sh; " + "for Iluvatar or Moore Threads prefer the official vendor vLLM Docker image fused with this project." + ) + return "vllm" + if self._device == "cpu" and is_qwenasr_rust_available(): + return "rust" + raise DefaultServerErrorException( + f"Qwen3-ASR is not available on accelerator '{self._accelerator.vendor}' " + f"device '{self._device}'. Supported backends are vendor-compatible vLLM " + "GPU runtimes and CPU QwenASR Rust." + ) + + def _load_rust_backend( + self, + model_path: str, + forced_aligner_path: Optional[str], + ) -> QwenASRRustRuntime: + logger.info("Loading Qwen3-ASR (QwenASR Rust): %s, device=%s", model_path, self._device) + + num_threads = 0 if settings.QWEN_RUST_CPU_WORKERS <= 1 else 1 + if settings.QWEN_RUST_CPU_WORKERS > 1: + logger.info( + "Using fixed QwenASR CPU thread count for multi-runtime mode: num_threads=%s workers=%s", + num_threads, + settings.QWEN_RUST_CPU_WORKERS, + ) + self._rust_num_threads = num_threads + self._rust_verbosity = 0 + return QwenASRRustRuntime( + model_path=model_path, + forced_aligner_path=forced_aligner_path, + num_threads=num_threads, + verbosity=0, + ) + + def _get_rust_batch_runtimes(self, worker_count: int) -> list[QwenASRRustRuntime]: + if worker_count <= 1: + return [self.model] + + if not self._rust_batch_runtimes: + self._rust_batch_runtimes = [self.model] + + while len(self._rust_batch_runtimes) < worker_count: + self._rust_batch_runtimes.append( + QwenASRRustRuntime( + model_path=self.model_path, + forced_aligner_path=self._forced_aligner_path, + num_threads=self._rust_num_threads, + verbosity=self._rust_verbosity, + ) + ) + + return self._rust_batch_runtimes[:worker_count] + + def _get_rust_stage_concurrency(self, configured: int, segment_count: int) -> int: + target = configured if configured > 0 else settings.QWEN_RUST_CPU_WORKERS + return max(1, min(target, segment_count)) + + def _get_rust_asr_concurrency(self, segment_count: int) -> int: + return self._get_rust_stage_concurrency( + settings.QWEN_RUST_ASR_CONCURRENCY, + segment_count, + ) + + def _get_rust_align_concurrency(self, segment_count: int) -> int: + return self._get_rust_stage_concurrency( + settings.QWEN_RUST_ALIGN_CONCURRENCY, + segment_count, + ) + + @staticmethod + def _build_hotword_prompt_context(hotwords: str) -> str: + return format_hotword_prompt_context(hotwords) + + def _rust_transcribe_text_segment( + self, + runtime: QwenASRRustRuntime, + seg: Any, + hotwords: str, + enable_punctuation: bool, + enable_itn: bool, + sample_rate: int, + ) -> str: + _ = (hotwords, enable_punctuation, sample_rate) + text = runtime.transcribe_file(seg.temp_file) or "" + return normalize_asr_text(text, enable_itn=enable_itn) + + def _rust_align_word_tokens( + self, + runtime: QwenASRRustRuntime, + seg: Any, + text: str, + language: Optional[str] = None, + ) -> list[WordToken]: + return [ + WordToken( + text=str(item["text"]), + start_time=round(float(item["start_ms"]) / 1000.0, 3), + end_time=round(float(item["end_ms"]) / 1000.0, 3), + ) + for item in runtime.align_transcript( + audio_path=seg.temp_file, + text=text, + language=language, + ) + ] + + def _run_rust_asr_stage( + self, + valid_segments: List[tuple[int, Any]], + hotwords: str, + enable_punctuation: bool, + enable_itn: bool, + sample_rate: int, + ) -> dict[int, str]: + if not valid_segments: + return {} + + worker_count = self._get_rust_asr_concurrency(len(valid_segments)) + runtimes = self._get_rust_batch_runtimes(worker_count) + output: dict[int, str] = {} + for batch_start in range(0, len(valid_segments), worker_count): + chunk = valid_segments[batch_start:batch_start + worker_count] + chunk_runtimes = runtimes[:len(chunk)] + with ThreadPoolExecutor(max_workers=len(chunk)) as executor: + futures = [ + executor.submit( + self._rust_transcribe_text_segment, + runtime, + seg, + hotwords, + enable_punctuation, + enable_itn, + sample_rate, + ) + for runtime, (_idx, seg) in zip(chunk_runtimes, chunk) + ] + for (idx, _seg), future in zip(chunk, futures): + output[idx] = future.result() + + return output + + def _run_rust_align_stage( + self, + valid_segments: List[tuple[int, Any]], + texts: dict[int, str], + language: Optional[str] = None, + ) -> dict[int, list[WordToken]]: + if not valid_segments: + return {} + + align_inputs = [(idx, seg, texts.get(idx, "")) for idx, seg in valid_segments if texts.get(idx, "").strip()] + worker_count = self._get_rust_align_concurrency(len(valid_segments)) + runtimes = self._get_rust_batch_runtimes(worker_count) + output: dict[int, list[WordToken]] = {} + + if not align_inputs: + return output + + for batch_start in range(0, len(align_inputs), worker_count): + chunk = align_inputs[batch_start:batch_start + worker_count] + chunk_runtimes = runtimes[:len(chunk)] + with ThreadPoolExecutor(max_workers=len(chunk)) as executor: + futures = [ + executor.submit( + self._rust_align_word_tokens, + runtime, + seg, + text, + language, + ) + for runtime, (_idx, seg, text) in zip(chunk_runtimes, chunk) + ] + for (idx, _seg, _text), future in zip(chunk, futures): + output[idx] = future.result() + + return output + + def _warmup_forced_aligner(self) -> None: + if not self._forced_aligner_path: + return + if not settings.ASR_ENABLE_WORD_TIMESTAMPS: + return + if self._backend == "vllm": + self.model.ensure_forced_aligner_loaded() + + def _load_vllm( + self, model_path: str, forced_aligner_path: Optional[str], + max_inference_batch_size: int, max_new_tokens: int, + max_model_len: Optional[int], + ) -> Qwen3VLLMBackend: + """Load model via official vLLM backend (CUDA only).""" + resolved_model_path = str(resolve_qwenasr_model_path(model_path)) + resolved_forced_aligner_path = None + if forced_aligner_path: + resolved_forced_aligner_path = str(resolve_qwenasr_model_path(forced_aligner_path)) + gpu_memory_utilization = calculate_gpu_memory_utilization(model_path) + tensor_parallel_size = _resolve_tensor_parallel_size() + logger.info( + f"Loading Qwen3-ASR (official vLLM): {resolved_model_path}, " + f"device={self._device}, gpu_memory_utilization={gpu_memory_utilization}, " + f"enforce_eager={settings.QWEN_VLLM_ENFORCE_EAGER}, " + f"tensor_parallel_size={tensor_parallel_size}" + ) + return Qwen3VLLMBackend( + model_path=resolved_model_path, + forced_aligner_path=resolved_forced_aligner_path, + gpu_memory_utilization=gpu_memory_utilization, + max_inference_batch_size=max_inference_batch_size, + max_new_tokens=max_new_tokens, + enforce_eager=settings.QWEN_VLLM_ENFORCE_EAGER, + max_model_len=max_model_len, + tensor_parallel_size=tensor_parallel_size, + ) + + @_handle_asr_error("转写") + def transcribe_file( + self, + audio_path: str, + hotwords: str = "", + enable_punctuation: bool = True, + enable_itn: bool = True, + enable_vad: bool = False, + sample_rate: int = 16000, + ) -> str: + if self._backend == "rust": + text = self.model.transcribe_file(audio_path) + return normalize_asr_text(text, enable_itn=enable_itn) + if self._backend == "vllm": + return self.model.transcribe_text( + audio_path, + context=self._build_hotword_prompt_context(hotwords), + enable_itn=enable_itn, + ) + raise DefaultServerErrorException(f"Qwen3 backend={self._backend} does not support offline transcription") + + @_handle_asr_error("VAD 转写") + def transcribe_file_with_vad( + self, + audio_path: str, + hotwords: str = "", + enable_punctuation: bool = True, + enable_itn: bool = True, + sample_rate: int = 16000, + **kwargs, + ) -> ASRRawResult: + if self._backend == "rust": + text = self.transcribe_file( + audio_path=audio_path, + hotwords=hotwords, + enable_punctuation=enable_punctuation, + enable_itn=enable_itn, + sample_rate=sample_rate, + ) + if kwargs.get("word_timestamps", False): + word_tokens = [ + WordToken( + text=str(item["text"]), + start_time=round(float(item["start_ms"]) / 1000.0, 3), + end_time=round(float(item["end_ms"]) / 1000.0, 3), + ) + for item in self.model.align_transcript( + audio_path=audio_path, + text=text, + language=kwargs.get("language"), + ) + ] + if word_tokens: + return ASRRawResult( + text=text, + segments=[ + ASRSegmentResult( + text=text, + start_time=word_tokens[0].start_time, + end_time=word_tokens[-1].end_time, + word_tokens=word_tokens, + ) + ], + ) + return ASRRawResult( + text=text, + segments=[ASRSegmentResult(text=text, start_time=0.0, end_time=0.0)] if text else [], + ) + if self._backend == "vllm": + return self.model.transcribe_raw( + audio_path=audio_path, + context=self._build_hotword_prompt_context(hotwords), + language=kwargs.get("language"), + word_timestamps=kwargs.get("word_timestamps", False), + enable_itn=enable_itn, + ) + + raise DefaultServerErrorException( + f"Qwen3 backend={self._backend} does not support VAD transcription" + ) + + @_handle_asr_error("批量推理") + def _transcribe_batch( + self, + segments: List[Any], + hotwords: str = "", + enable_punctuation: bool = False, + enable_itn: bool = False, + sample_rate: int = 16000, + word_timestamps: bool = False, + ) -> List[ASRSegmentResult]: + output = [ASRSegmentResult(text="", start_time=0.0, end_time=0.0) for _ in segments] + + valid: List[tuple[int, Any]] = [] + for idx, seg in enumerate(segments): + temp_file = getattr(seg, "temp_file", None) + if temp_file and os.path.exists(temp_file): + valid.append((idx, seg)) + else: + logger.warning(f"Qwen3 批处理片段无效或文件不存在: segment={idx + 1}, file={temp_file}") + + if not valid: + return output + + if self._backend == "rust": + texts = self._run_rust_asr_stage( + valid_segments=valid, + hotwords=hotwords, + enable_punctuation=enable_punctuation, + enable_itn=enable_itn, + sample_rate=sample_rate, + ) + + word_tokens_by_idx: dict[int, list[WordToken]] = {} + if word_timestamps: + word_tokens_by_idx = self._run_rust_align_stage( + valid_segments=valid, + texts=texts, + ) + + for idx, seg in valid: + text = texts.get(idx, "") + output[idx] = ASRSegmentResult( + text=text, + start_time=seg.start_sec, + end_time=seg.end_sec, + speaker_id=getattr(seg, "speaker_id", None), + word_tokens=word_tokens_by_idx.get(idx) or None, + ) + return output + + if self._backend == "vllm": + vllm_results = self.model.transcribe_batch( + [seg.temp_file for _, seg in valid], + context=self._build_hotword_prompt_context(hotwords), + word_timestamps=word_timestamps, + enable_itn=enable_itn, + ) + for (idx, seg), result in zip(valid, vllm_results): + output[idx] = ASRSegmentResult( + text=result.text, + start_time=round(seg.start_sec, 2), + end_time=round(seg.end_sec, 2), + speaker_id=getattr(seg, "speaker_id", None), + word_tokens=result.word_tokens if word_timestamps else None, + ) + return output + + raise DefaultServerErrorException( + f"Qwen3 backend={self._backend} does not support batch transcription" + ) + + @_handle_asr_error("初始化流式状态") + def init_streaming_state(self, context: str = "", language: Optional[str] = None, **kwargs) -> Qwen3StreamingState: + if self._backend not in {"vllm", "rust"}: + raise DefaultServerErrorException( + f"Qwen3 backend={self._backend} does not support realtime streaming" + ) + if self._backend == "rust": + if context: + logger.debug("QwenASR Rust backend ignores streaming context hints") + chunk_size_sec = float(kwargs.get("chunk_size_sec", 1.2)) + unfixed_chunk_num = int(kwargs.get("unfixed_chunk_num", 2)) + unfixed_token_num = int(kwargs.get("unfixed_token_num", 5)) + max_new_tokens = int(kwargs.get("max_new_tokens", 32)) + stream_handle = self.model.create_stream( + chunk_size_sec=chunk_size_sec, + unfixed_chunk_num=unfixed_chunk_num, + rollback_tokens=unfixed_token_num, + max_new_tokens=max_new_tokens, + language=language, + ) + return Qwen3StreamingState( + internal_state=stream_handle, + chunk_size_sec=chunk_size_sec, + unfixed_chunk_num=unfixed_chunk_num, + unfixed_token_num=unfixed_token_num, + max_new_tokens=max_new_tokens, + language=language, + chunk_count=0, + last_text="", + last_language=language or "", + ) + if self._backend == "vllm": + streaming_state = self.model.init_streaming_state(context=context, language=language, **kwargs) + return Qwen3StreamingState( + internal_state=streaming_state, + chunk_size_sec=float(kwargs.get("chunk_size_sec", 1.2)), + unfixed_chunk_num=int(kwargs.get("unfixed_chunk_num", 2)), + unfixed_token_num=int(kwargs.get("unfixed_token_num", 5)), + max_new_tokens=int(kwargs.get("max_new_tokens", 32)), + language=language, + chunk_count=int(getattr(streaming_state, "chunk_id", 0)), + last_text=str(getattr(streaming_state, "text", "") or ""), + last_language=str(getattr(streaming_state, "language", "") or ""), + ) + + raise DefaultServerErrorException( + f"Qwen3 backend={self._backend} does not support realtime streaming" + ) + + @_handle_asr_error("流式识别") + def streaming_transcribe(self, pcm16k: np.ndarray, state: Qwen3StreamingState) -> Qwen3StreamingState: + if self._backend not in {"vllm", "rust"}: + raise DefaultServerErrorException( + f"Qwen3 backend={self._backend} does not support realtime streaming" + ) + pcm = pcm16k.astype(np.float32) / (32768.0 if pcm16k.dtype == np.int16 else 1.0) + if self._backend == "rust": + text = self.model.push_stream( + stream=state.internal_state, + samples=pcm, + chunk_size_sec=state.chunk_size_sec, + unfixed_chunk_num=state.unfixed_chunk_num, + rollback_tokens=state.unfixed_token_num, + max_new_tokens=state.max_new_tokens, + language=state.language, + ) + state.chunk_count += 1 + state.last_text = text + state.last_language = state.language or "" + return state + + streaming_state = self.model.feed_stream(pcm, state.internal_state) + state.internal_state = streaming_state + state.chunk_count = int(getattr(streaming_state, "chunk_id", state.chunk_count)) + state.last_text = str(getattr(streaming_state, "text", "") or "") + state.last_language = str(getattr(streaming_state, "language", "") or "") + return state + + @_handle_asr_error("结束流式识别") + def finish_streaming_transcribe(self, state: Qwen3StreamingState) -> Qwen3StreamingState: + if self._backend not in {"vllm", "rust"}: + raise DefaultServerErrorException( + f"Qwen3 backend={self._backend} does not support realtime streaming" + ) + if self._backend == "rust": + text = self.model.finish_stream( + stream=state.internal_state, + chunk_size_sec=state.chunk_size_sec, + unfixed_chunk_num=state.unfixed_chunk_num, + rollback_tokens=state.unfixed_token_num, + max_new_tokens=state.max_new_tokens, + language=state.language, + ) + state.last_text = text + state.last_language = state.language or "" + return state + + streaming_state = self.model.finish_stream(state.internal_state) + state.internal_state = streaming_state + state.chunk_count = int(getattr(streaming_state, "chunk_id", state.chunk_count)) + state.last_text = str(getattr(streaming_state, "text", "") or "") + state.last_language = str(getattr(streaming_state, "language", "") or "") + return state + + def is_model_loaded(self) -> bool: + return self.model is not None + + @property + def backend(self) -> str: + return self._backend + + @property + def device(self) -> str: + return self._device + + +def _register_qwen3_engine(register_func, _declared_entry_cls): + from app.core.config import settings + + def _create(config): + extra = {k: v for k, v in config.extra_kwargs.items() if v is not None} + model_id = config.models.get("offline") + return Qwen3ASREngine(model_path=model_id, device=settings.DEVICE, **extra) + + register_func("qwen3", _create) diff --git a/app/services/asr/qwen3_vllm.py b/app/services/asr/qwen3_vllm.py new file mode 100644 index 0000000..b07d553 --- /dev/null +++ b/app/services/asr/qwen3_vllm.py @@ -0,0 +1,527 @@ +# -*- coding: utf-8 -*- +"""Official vLLM adapter for CUDA Qwen3-ASR.""" + +from __future__ import annotations + +import importlib +import importlib.util +import logging +import os +import re +import threading +from dataclasses import dataclass, field +from typing import Any, Optional + +import librosa +import numpy as np + +from app.core.hotword_resolver import strip_hotword_prompt_leakage +from app.utils.text_processing import normalize_asr_text + +from .engines import ASRRawResult, ASRSegmentResult, WordToken + +logger = logging.getLogger(__name__) + +_DEFAULT_SAMPLE_RATE = 16000 +_LANGUAGE_ALIASES = { + "zh": "Chinese", + "zh-cn": "Chinese", + "zh-hans": "Chinese", + "zh-hant": "Chinese", + "cn": "Chinese", + "en": "English", + "en-us": "English", + "en-gb": "English", + "ja": "Japanese", + "jp": "Japanese", + "ko": "Korean", + "yue": "Cantonese", + "fr": "French", + "de": "German", + "es": "Spanish", + "ru": "Russian", +} +_PROMPT_LEAK_PATTERNS = ( + re.compile(r"^\s*Transcribe the speech accurately\.\s*", re.IGNORECASE), + re.compile(r"^\s*Transcribe the speech in [A-Za-z\s-]+\.\s*", re.IGNORECASE), +) + + +def is_vllm_available() -> bool: + """Return True when the official vLLM runtime is installed.""" + return importlib.util.find_spec("vllm") is not None + + +def _normalize_language_name(language: Optional[str]) -> Optional[str]: + if not language: + return None + normalized = language.strip() + if not normalized: + return None + alias = _LANGUAGE_ALIASES.get(normalized.lower()) + if alias: + return alias + if " " in normalized: + return " ".join(part.capitalize() for part in normalized.split()) + return normalized.capitalize() + + +def _load_audio(audio_path: str) -> np.ndarray: + audio, _sample_rate = librosa.load(audio_path, sr=_DEFAULT_SAMPLE_RATE, mono=True) + return audio.astype(np.float32) + + +def _build_chat_prompt(context: str = "", language: Optional[str] = None) -> str: + instructions: list[str] = [] + if language: + instructions.append(f"Transcribe the speech in {language}.") + else: + instructions.append("Transcribe the speech accurately.") + if context.strip(): + instructions.append(context.strip()) + system_text = " ".join(instructions).strip() + return ( + f"<|im_start|>system\n{system_text}<|im_end|>\n" + "<|im_start|>user\n<|audio_start|><|audio_pad|><|audio_end|><|im_end|>\n" + "<|im_start|>assistant\n" + ) + + +def _build_alignment_prompt(tokens: list[str]) -> str: + body = "".join(tokens) + "" + return f"<|audio_start|><|audio_pad|><|audio_end|>{body}" + + +def _strip_prompt_leakage(text: str) -> str: + cleaned = text or "" + changed = True + while changed and cleaned: + changed = False + for pattern in _PROMPT_LEAK_PATTERNS: + updated, count = pattern.subn("", cleaned, count=1) + if count: + cleaned = updated + changed = True + return strip_hotword_prompt_leakage(cleaned) + + +def _sanitize_detected_language(detected: str, fallback: Optional[str]) -> str: + candidate = (detected or "").strip() + if not candidate: + return fallback or "" + + candidate = re.sub(r"^[^\w]+", "", candidate) + match = re.search(r"language\s+([A-Za-z][A-Za-z\s-]*)$", candidate, re.IGNORECASE) + if match: + candidate = match.group(1).strip() + + normalized = _normalize_language_name(candidate) + return normalized or (fallback or "") + + +def _parse_asr_output(raw_text: str, language: Optional[str]) -> tuple[str, str]: + text = (raw_text or "").strip() + if "" in text: + left, right = text.split("", 1) + detected = _sanitize_detected_language(left.strip(), language) + return detected, _strip_prompt_leakage(right.strip()) + return (language or ""), _strip_prompt_leakage(text) + + +def _split_alignment_units(text: str) -> list[str]: + if not text: + return [] + + # Mixed Chinese/English transcripts should not fall back to whitespace-only + # tokenization, otherwise a long CJK sentence with a single embedded English + # word can collapse into one giant alignment unit. + token_pattern = re.compile( + r"[\u4e00-\u9fff]" # CJK ideographs, align per character + r"|[A-Za-z0-9]+(?:['._+-][A-Za-z0-9]+)*" # Latin / alnum words + r"|[^\w\s]", # punctuation and symbols + re.UNICODE, + ) + return token_pattern.findall(text) + + +def _resolve_forced_aligner_gpu_memory_utilization(primary_utilization: float) -> float: + override = (os.getenv("QWEN_FORCE_ALIGNER_GPU_MEMORY_UTILIZATION") or "").strip() + if override: + try: + value = float(override) + if 0.0 < value <= 1.0: + return value + except ValueError: + logger.warning( + "Invalid QWEN_FORCE_ALIGNER_GPU_MEMORY_UTILIZATION=%s, ignoring override", + override, + ) + + return primary_utilization + + +@dataclass +class _GeneratedTranscript: + text: str + language: str + + +@dataclass +class VLLMRealtimeState: + prompt_raw: str + language: str + chunk_size_sec: float + unfixed_chunk_num: int + unfixed_token_num: int + max_new_tokens: int + chunk_id: int = 0 + text: str = "" + raw_decoded: str = "" + audio_buffer: np.ndarray = field(default_factory=lambda: np.array([], dtype=np.float32)) + audio_accum: np.ndarray = field(default_factory=lambda: np.array([], dtype=np.float32)) + + +class Qwen3VLLMBackend: + """Thin adapter over official vLLM APIs for Qwen3-ASR.""" + + def __init__( + self, + model_path: str, + forced_aligner_path: Optional[str], + gpu_memory_utilization: float, + max_inference_batch_size: int, + max_new_tokens: int, + enforce_eager: bool = True, + max_model_len: Optional[int] = None, + tensor_parallel_size: int = 1, + ) -> None: + try: + vllm_module = importlib.import_module("vllm") + transformers_module = importlib.import_module("transformers") + except ImportError as exc: + raise RuntimeError( + "CUDA Qwen3-ASR now requires official vLLM with Qwen3 forced aligner support. " + "Install it with: pip install 'vllm[audio]==0.19.0'" + ) from exc + + self._llm_cls = getattr(vllm_module, "LLM") + self._sampling_params_cls = getattr(vllm_module, "SamplingParams") + self._tokenizer = getattr(transformers_module, "AutoTokenizer").from_pretrained( + model_path, + trust_remote_code=True, + ) + + llm_kwargs: dict[str, Any] = { + "model": model_path, + "gpu_memory_utilization": gpu_memory_utilization, + "enforce_eager": enforce_eager, + "trust_remote_code": True, + } + if max_model_len is not None: + llm_kwargs["max_model_len"] = max_model_len + if tensor_parallel_size > 1: + llm_kwargs["tensor_parallel_size"] = tensor_parallel_size + + self._llm = self._llm_cls(**llm_kwargs) + self._sampling_params = self._sampling_params_cls( + temperature=0.01, + max_tokens=max_new_tokens, + ) + self._max_inference_batch_size = max_inference_batch_size + self._gpu_memory_utilization = gpu_memory_utilization + self._enforce_eager = enforce_eager + self._tensor_parallel_size = tensor_parallel_size + self._forced_aligner_path = forced_aligner_path + self._forced_aligner: Any | None = None + self._timestamp_token_id: int | None = None + self._timestamp_segment_time: float | None = None + # Share one backend instance across multiple offline tasks, but serialize + # direct vLLM engine calls to avoid cross-request state corruption/hangs. + self._engine_lock = threading.RLock() + + def _get_forced_aligner_gpu_memory_utilization(self) -> float: + configured = _resolve_forced_aligner_gpu_memory_utilization(self._gpu_memory_utilization) + logger.info( + "Resolved forced aligner gpu_memory_utilization=%s (primary=%s)", + configured, + self._gpu_memory_utilization, + ) + return configured + + def _get_forced_aligner(self) -> Any: + if not self._forced_aligner_path: + raise RuntimeError("word_timestamps requires a configured forced aligner model") + + if self._forced_aligner is None: + forced_aligner_gpu_memory_utilization = self._get_forced_aligner_gpu_memory_utilization() + logger.info( + "Loading Qwen3 forced aligner via official vLLM: %s (gpu_memory_utilization=%s)", + self._forced_aligner_path, + forced_aligner_gpu_memory_utilization, + ) + self._forced_aligner = self._llm_cls( + model=self._forced_aligner_path, + runner="pooling", + enforce_eager=self._enforce_eager, + gpu_memory_utilization=forced_aligner_gpu_memory_utilization, + tensor_parallel_size=self._tensor_parallel_size, + trust_remote_code=True, + hf_overrides={ + "architectures": ["Qwen3ASRForcedAlignerForTokenClassification"], + }, + ) + llm_engine = getattr(self._forced_aligner, "llm_engine", None) + if llm_engine is None: + raise RuntimeError("Forced aligner did not expose a vLLM engine instance") + config = llm_engine.vllm_config.model_config.hf_config + self._timestamp_token_id = int(config.timestamp_token_id) + self._timestamp_segment_time = float(config.timestamp_segment_time) + + return self._forced_aligner + + def ensure_forced_aligner_loaded(self) -> None: + if self._forced_aligner_path: + self._get_forced_aligner() + + def _run_generate( + self, + audio_items: list[tuple[np.ndarray, str, Optional[str]]], + ) -> list[_GeneratedTranscript]: + prompts: list[dict[str, Any]] = [] + for audio, context, language in audio_items: + prompts.append( + { + "prompt": _build_chat_prompt(context=context, language=_normalize_language_name(language)), + "multi_modal_data": {"audio": [audio]}, + } + ) + + with self._engine_lock: + outputs = self._llm.generate( + prompts, + sampling_params=self._sampling_params, + use_tqdm=False, + ) + + transcripts: list[_GeneratedTranscript] = [] + for output, (_audio, _context, language) in zip(outputs, audio_items): + raw_text = str(output.outputs[0].text if output.outputs else "") + parsed_language, parsed_text = _parse_asr_output(raw_text, _normalize_language_name(language)) + transcripts.append(_GeneratedTranscript(text=parsed_text, language=parsed_language)) + return transcripts + + def transcribe_text( + self, + audio_path: str, + context: str = "", + language: Optional[str] = None, + enable_itn: bool = False, + ) -> str: + transcript = self._run_generate([(_load_audio(audio_path), context, language)])[0] + return normalize_asr_text(transcript.text, enable_itn=enable_itn) + + def transcribe_raw( + self, + audio_path: str, + context: str = "", + language: Optional[str] = None, + word_timestamps: bool = False, + enable_itn: bool = False, + ) -> ASRRawResult: + audio = _load_audio(audio_path) + transcript = self._run_generate([(audio, context, language)])[0] + text = normalize_asr_text(transcript.text, enable_itn=enable_itn) + if not word_timestamps: + return ASRRawResult( + text=text, + segments=[ASRSegmentResult(text=text, start_time=0.0, end_time=0.0)] if text else [], + ) + + aligned = self.align_transcript(audio_path=audio_path, text=text, language=language, audio=audio) + word_tokens = [ + WordToken( + text=str(item["text"]), + start_time=round(float(item["start_ms"]) / 1000.0, 3), + end_time=round(float(item["end_ms"]) / 1000.0, 3), + ) + for item in aligned + ] + if not word_tokens: + return ASRRawResult( + text=text, + segments=[ASRSegmentResult(text=text, start_time=0.0, end_time=0.0)] if text else [], + ) + return ASRRawResult( + text=text, + segments=[ + ASRSegmentResult( + text=text, + start_time=word_tokens[0].start_time, + end_time=word_tokens[-1].end_time, + word_tokens=word_tokens, + ) + ], + ) + + def transcribe_batch( + self, + audio_paths: list[str], + context: str = "", + language: Optional[str] = None, + word_timestamps: bool = False, + enable_itn: bool = False, + ) -> list[ASRSegmentResult]: + audios = [_load_audio(path) for path in audio_paths] + results: list[ASRSegmentResult] = [] + for start in range(0, len(audios), self._max_inference_batch_size): + chunk = audios[start:start + self._max_inference_batch_size] + transcripts = self._run_generate([(audio, context, language) for audio in chunk]) + for audio_path, audio, transcript in zip(audio_paths[start:start + len(chunk)], chunk, transcripts): + text = normalize_asr_text(transcript.text, enable_itn=enable_itn) + if not word_timestamps: + results.append(ASRSegmentResult(text=text, start_time=0.0, end_time=0.0)) + continue + aligned = self.align_transcript( + audio_path=audio_path, + text=text, + language=language, + audio=audio, + ) + word_tokens = [ + WordToken( + text=str(item["text"]), + start_time=round(float(item["start_ms"]) / 1000.0, 3), + end_time=round(float(item["end_ms"]) / 1000.0, 3), + ) + for item in aligned + ] + results.append( + ASRSegmentResult( + text=text, + start_time=word_tokens[0].start_time if word_tokens else 0.0, + end_time=word_tokens[-1].end_time if word_tokens else 0.0, + word_tokens=word_tokens or None, + ) + ) + return results + + def align_transcript( + self, + audio_path: str, + text: str, + language: Optional[str] = None, + audio: Optional[np.ndarray] = None, + ) -> list[dict[str, float | str]]: + tokens = _split_alignment_units(text) + if not tokens: + return [] + + aligner = self._get_forced_aligner() + prompt = _build_alignment_prompt(tokens) + audio_array = audio if audio is not None else _load_audio(audio_path) + with self._engine_lock: + outputs = aligner.encode( + [{"prompt": prompt, "multi_modal_data": {"audio": audio_array}}], + pooling_task="token_classify", + ) + output = outputs[0] + logits = output.outputs.data + predictions = logits.argmax(dim=-1) if hasattr(logits, "argmax") else np.argmax(logits, axis=-1) + ts_predictions = [ + float(pred.item() if hasattr(pred, "item") else pred) * float(self._timestamp_segment_time or 0.0) + for tid, pred in zip(output.prompt_token_ids, predictions) + if int(tid) == int(self._timestamp_token_id or -1) + ] + + expected_timestamps = len(tokens) * 2 + if len(ts_predictions) < expected_timestamps: + raise RuntimeError( + "Forced aligner returned fewer timestamp predictions than expected: " + f"expected={expected_timestamps}, got={len(ts_predictions)}, tokens={len(tokens)}" + ) + + aligned: list[dict[str, float | str]] = [] + for index, token in enumerate(tokens): + start_ms = ts_predictions[index * 2] + end_ms = ts_predictions[index * 2 + 1] + if end_ms < start_ms: + logger.warning( + "Forced aligner produced reversed timestamps for token=%r: start_ms=%s end_ms=%s", + token, + start_ms, + end_ms, + ) + start_ms, end_ms = end_ms, start_ms + aligned.append({"text": token, "start_ms": start_ms, "end_ms": end_ms}) + return aligned + + def init_streaming_state( + self, + *, + context: str = "", + language: Optional[str] = None, + chunk_size_sec: float = 1.2, + unfixed_chunk_num: int = 2, + unfixed_token_num: int = 5, + max_new_tokens: int = 32, + ) -> VLLMRealtimeState: + normalized_language = _normalize_language_name(language) or "" + return VLLMRealtimeState( + prompt_raw=_build_chat_prompt(context=context, language=normalized_language or None), + language=normalized_language, + chunk_size_sec=chunk_size_sec, + unfixed_chunk_num=unfixed_chunk_num, + unfixed_token_num=unfixed_token_num, + max_new_tokens=max_new_tokens, + audio_buffer=np.array([], dtype=np.float32), + audio_accum=np.array([], dtype=np.float32), + ) + + def _decode_stream(self, state: VLLMRealtimeState) -> VLLMRealtimeState: + prefix = "" + if state.chunk_id >= state.unfixed_chunk_num and state.raw_decoded: + token_ids = self._tokenizer.encode(state.raw_decoded, add_special_tokens=False) + rollback = token_ids[-state.unfixed_token_num:] if state.unfixed_token_num > 0 else [] + if rollback: + prefix = self._tokenizer.decode(rollback, skip_special_tokens=False).replace("\ufffd", "") + + with self._engine_lock: + output = self._llm.generate( + [ + { + "prompt": state.prompt_raw + prefix, + "multi_modal_data": {"audio": [state.audio_accum]}, + } + ], + sampling_params=self._sampling_params_cls( + temperature=0.01, + max_tokens=state.max_new_tokens, + ), + use_tqdm=False, + )[0] + generated = str(output.outputs[0].text if output.outputs else "") + parsed_language, parsed_text = _parse_asr_output(prefix + generated, state.language or None) + state.raw_decoded = prefix + generated + state.text = parsed_text + state.language = parsed_language or state.language + state.chunk_id += 1 + return state + + def feed_stream(self, pcm: np.ndarray, state: VLLMRealtimeState) -> VLLMRealtimeState: + state.audio_buffer = np.concatenate([state.audio_buffer, pcm.astype(np.float32)]) + segment_size = int(max(state.chunk_size_sec, 0.1) * _DEFAULT_SAMPLE_RATE) + while len(state.audio_buffer) >= segment_size: + segment = state.audio_buffer[:segment_size].copy() + state.audio_buffer = state.audio_buffer[segment_size:] + state.audio_accum = np.concatenate([state.audio_accum, segment]) + state = self._decode_stream(state) + return state + + def finish_stream(self, state: VLLMRealtimeState) -> VLLMRealtimeState: + if len(state.audio_buffer) > 0: + state.audio_accum = np.concatenate([state.audio_accum, state.audio_buffer]) + state.audio_buffer = np.array([], dtype=np.float32) + state = self._decode_stream(state) + elif state.chunk_id == 0 and len(state.audio_accum) > 0: + state = self._decode_stream(state) + return state diff --git a/app/services/asr/qwenasr_rust.py b/app/services/asr/qwenasr_rust.py new file mode 100644 index 0000000..0079ac9 --- /dev/null +++ b/app/services/asr/qwenasr_rust.py @@ -0,0 +1,564 @@ +# -*- coding: utf-8 -*- +"""QwenASR Rust FFI wrapper for CPU inference.""" + +from __future__ import annotations + +import ctypes +import json +import logging +import os +import platform +import re +import sys +from pathlib import Path +from typing import Optional + +import numpy as np + +from app.core.config import settings + +logger = logging.getLogger(__name__) + +_SHARED_LIBRARY: Optional[ctypes.CDLL] = None + +_LANGUAGE_MAP = { + "": "", + "auto": "", + "zh": "Chinese", + "zh-cn": "Chinese", + "yue": "Chinese", + "en": "English", + "ja": "Japanese", + "ko": "Korean", + "de": "German", + "es": "Spanish", + "fr": "French", + "it": "Italian", + "pt": "Portuguese", + "ru": "Russian", + "ar": "Arabic", + "th": "Thai", + "vi": "Vietnamese", + "id": "Indonesian", +} + + +def _shared_library_filename() -> str: + if sys.platform == "darwin": + return "libqwen_asr.dylib" + if sys.platform == "win32": + return "qwen_asr.dll" + return "libqwen_asr.so" + + +def _repo_root() -> Path: + return Path(__file__).resolve().parents[3] + + +def _candidate_library_paths() -> list[Path]: + filename = _shared_library_filename() + candidates: list[Path] = [] + + env_path = (os.getenv("QWENASR_LIBRARY_PATH") or "").strip() + if env_path: + candidate = Path(env_path).expanduser() + if candidate.is_dir(): + candidates.append(candidate / filename) + else: + candidates.append(candidate) + + repo_root = _repo_root() + candidates.extend( + [ + repo_root / "vendor" / "qwenasr" / "target" / "release" / filename, + repo_root / "vendor" / "qwenasr" / "target" / "debug" / filename, + Path("/opt/qwenasr/lib") / filename, + Path("/usr/local/lib") / filename, + ] + ) + + return candidates + + +def resolve_qwenasr_library_path() -> Optional[Path]: + for candidate in _candidate_library_paths(): + if candidate.exists(): + return candidate.resolve() + return None + + +def is_qwenasr_rust_available() -> bool: + return resolve_qwenasr_library_path() is not None + + +def validate_qwenasr_cpu_features() -> None: + if platform.machine().lower() not in {"amd64", "x86_64"}: + return + + flags = _read_linux_cpu_flags() + if not flags: + return + + missing = [flag for flag in ("avx2", "fma") if flag not in flags] + if missing: + raise RuntimeError( + "QwenASR Rust backend requires x86_64 CPU features: avx2, fma. " + f"Missing: {', '.join(missing)}. Use a newer CPU host or rebuild the " + "Rust backend with scalar x86 kernels." + ) + + +def pick_cpu_qwen_model(all_available_models: list[str]) -> Optional[str]: + for model_id in ["qwen3-asr-0.6b", "qwen3-asr-1.7b"]: + if model_id in all_available_models: + return model_id + return None + + +def _read_linux_cpu_flags() -> set[str]: + cpuinfo = Path("/proc/cpuinfo") + if not cpuinfo.exists(): + return set() + + flags: set[str] = set() + for line in cpuinfo.read_text(encoding="utf-8", errors="ignore").splitlines(): + key, _, value = line.partition(":") + if key.strip().lower() in {"flags", "features"}: + flags.update(value.strip().lower().split()) + if flags: + break + return flags + + +def _resolve_modelscope_dir(model_ref: str, cache_root: Path) -> Optional[Path]: + if "/" not in model_ref: + return None + base_dir = cache_root / model_ref + if base_dir.exists() and base_dir.is_dir(): + return base_dir.resolve() + return None + + +def _append_unique_path(paths: list[Path], path: Path) -> None: + if path not in paths: + paths.append(path) + + +def resolve_qwenasr_model_path(model_ref_or_path: str) -> Path: + raw_path = Path(model_ref_or_path).expanduser() + if raw_path.exists(): + return raw_path.resolve() + + modelscope_cache_roots: list[Path] = [] + ms_cache = (os.getenv("MODELSCOPE_CACHE") or "").strip() + if ms_cache: + cache_root = Path(ms_cache).expanduser() + _append_unique_path(modelscope_cache_roots, cache_root) + + # 兼容旧目录结构,允许外部仍传入 models/modelscope + legacy_cache_root = cache_root / "hub" / "models" + if legacy_cache_root.exists(): + _append_unique_path(modelscope_cache_roots, legacy_cache_root) + + default_ms_cache_root = Path(settings.MODELSCOPE_PATH).expanduser() + _append_unique_path(modelscope_cache_roots, default_ms_cache_root) + + for cache_root in modelscope_cache_roots: + ms_dir = _resolve_modelscope_dir(model_ref_or_path, cache_root) + if ms_dir is not None: + return ms_dir + + raise FileNotFoundError( + f"QwenASR model path not found for '{model_ref_or_path}'. " + f"Checked direct path and ModelScope caches at: " + f"{', '.join(str(path) for path in modelscope_cache_roots)}." + ) + + +def _bind_ffi_signatures(lib: ctypes.CDLL) -> None: + lib.qwen_asr_load_model.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_int] + lib.qwen_asr_load_model.restype = ctypes.c_void_p + + lib.qwen_asr_transcribe_file.argtypes = [ctypes.c_void_p, ctypes.c_char_p] + lib.qwen_asr_transcribe_file.restype = ctypes.c_void_p + + lib.qwen_asr_force_align_file.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_char_p, + ] + lib.qwen_asr_force_align_file.restype = ctypes.c_void_p + + lib.qwen_asr_set_language.argtypes = [ctypes.c_void_p, ctypes.c_char_p] + lib.qwen_asr_set_language.restype = ctypes.c_int + + lib.qwen_asr_free_string.argtypes = [ctypes.c_void_p] + lib.qwen_asr_free_string.restype = None + + lib.qwen_asr_free.argtypes = [ctypes.c_void_p] + lib.qwen_asr_free.restype = None + + lib.qwen_asr_stream_new.argtypes = [] + lib.qwen_asr_stream_new.restype = ctypes.c_void_p + + lib.qwen_asr_stream_free.argtypes = [ctypes.c_void_p] + lib.qwen_asr_stream_free.restype = None + + lib.qwen_asr_stream_push.argtypes = [ + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.POINTER(ctypes.c_float), + ctypes.c_int, + ctypes.c_int, + ] + lib.qwen_asr_stream_push.restype = ctypes.c_void_p + + lib.qwen_asr_stream_get_result.argtypes = [ctypes.c_void_p] + lib.qwen_asr_stream_get_result.restype = ctypes.c_void_p + + lib.qwen_asr_stream_set_chunk_sec.argtypes = [ctypes.c_void_p, ctypes.c_float] + lib.qwen_asr_stream_set_chunk_sec.restype = None + + lib.qwen_asr_stream_set_rollback.argtypes = [ctypes.c_void_p, ctypes.c_int] + lib.qwen_asr_stream_set_rollback.restype = None + + lib.qwen_asr_stream_set_unfixed_chunks.argtypes = [ctypes.c_void_p, ctypes.c_int] + lib.qwen_asr_stream_set_unfixed_chunks.restype = None + + lib.qwen_asr_stream_set_max_new_tokens.argtypes = [ctypes.c_void_p, ctypes.c_int] + lib.qwen_asr_stream_set_max_new_tokens.restype = None + + lib.qwen_asr_stream_set_past_text.argtypes = [ctypes.c_void_p, ctypes.c_int] + lib.qwen_asr_stream_set_past_text.restype = None + + +def load_qwenasr_library() -> ctypes.CDLL: + global _SHARED_LIBRARY + + if _SHARED_LIBRARY is not None: + return _SHARED_LIBRARY + + library_path = resolve_qwenasr_library_path() + if library_path is None: + searched = ", ".join(str(path) for path in _candidate_library_paths()) + raise FileNotFoundError( + "QwenASR shared library not found. " + f"Checked: {searched}" + ) + + logger.info("Loading QwenASR Rust library from %s", library_path) + library = ctypes.CDLL(str(library_path)) + _bind_ffi_signatures(library) + _SHARED_LIBRARY = library + return library + + +def normalize_qwen_language(language: Optional[str]) -> str: + if language is None: + return "" + return _LANGUAGE_MAP.get(language.strip().lower(), language.strip()) + + +def guess_alignment_language(text: str, language: Optional[str] = None) -> str: + normalized = normalize_qwen_language(language) + if normalized: + return normalized + if re.search(r"[\u4e00-\u9fff]", text): + return "Chinese" + if re.search(r"[\u3040-\u30ff]", text): + return "Japanese" + if re.search(r"[\uac00-\ud7af]", text): + return "Korean" + return "English" + + +def _decode_and_free_string(lib: ctypes.CDLL, raw_ptr: ctypes.c_void_p) -> Optional[str]: + if not raw_ptr: + return None + + try: + value = ctypes.cast(raw_ptr, ctypes.c_char_p).value + if value is None: + return None + return value.decode("utf-8") + finally: + lib.qwen_asr_free_string(raw_ptr) + + +class QwenASRRustStreamHandle: + """Owns a Rust streaming state pointer.""" + + def __init__(self, lib: ctypes.CDLL, handle: ctypes.c_void_p): + self._lib = lib + self.handle = handle + self.accumulated_text = "" + + def close(self) -> None: + if self.handle: + self._lib.qwen_asr_stream_free(self.handle) + self.handle = ctypes.c_void_p() + + def __del__(self) -> None: + try: + self.close() + except Exception: + pass + + +class QwenASRRustBackend: + """Thin Python wrapper around the QwenASR C API.""" + + def __init__(self, model_path: str, num_threads: int = 0, verbosity: int = 0): + validate_qwenasr_cpu_features() + self._lib = load_qwenasr_library() + self.model_dir = resolve_qwenasr_model_path(model_path) + self._engine = self._lib.qwen_asr_load_model( + str(self.model_dir).encode("utf-8"), + num_threads, + verbosity, + ) + if not self._engine: + raise RuntimeError(f"Failed to load QwenASR model from '{self.model_dir}'") + + def close(self) -> None: + if self._engine: + self._lib.qwen_asr_free(self._engine) + self._engine = ctypes.c_void_p() + + def __del__(self) -> None: + try: + self.close() + except Exception: + pass + + def _set_language(self, language: Optional[str]) -> None: + normalized = normalize_qwen_language(language) + status = self._lib.qwen_asr_set_language( + self._engine, + normalized.encode("utf-8"), + ) + if status != 0 and normalized: + logger.warning("QwenASR rejected language hint: %s", normalized) + + def _configure_stream( + self, + *, + chunk_size_sec: float, + unfixed_chunk_num: int, + rollback_tokens: int, + max_new_tokens: int, + past_text: bool, + ) -> None: + self._lib.qwen_asr_stream_set_chunk_sec(self._engine, ctypes.c_float(chunk_size_sec)) + self._lib.qwen_asr_stream_set_unfixed_chunks(self._engine, int(unfixed_chunk_num)) + self._lib.qwen_asr_stream_set_rollback(self._engine, int(rollback_tokens)) + self._lib.qwen_asr_stream_set_max_new_tokens(self._engine, int(max_new_tokens)) + self._lib.qwen_asr_stream_set_past_text(self._engine, 1 if past_text else 0) + + def transcribe_file(self, audio_path: str, language: Optional[str] = None) -> str: + self._set_language(language) + raw_ptr = self._lib.qwen_asr_transcribe_file( + self._engine, + audio_path.encode("utf-8"), + ) + text = _decode_and_free_string(self._lib, raw_ptr) + if text is None: + raise RuntimeError(f"QwenASR failed to transcribe '{audio_path}'") + return text + + def force_align_file( + self, + audio_path: str, + text: str, + language: Optional[str] = None, + ) -> list[dict[str, float | str]]: + normalized_language = normalize_qwen_language(language) or "English" + raw_ptr = self._lib.qwen_asr_force_align_file( + self._engine, + audio_path.encode("utf-8"), + text.encode("utf-8"), + normalized_language.encode("utf-8"), + ) + payload = _decode_and_free_string(self._lib, raw_ptr) + if payload is None: + raise RuntimeError(f"QwenASR failed to force align '{audio_path}'") + + items = json.loads(payload) + if not isinstance(items, list): + raise RuntimeError("QwenASR force alignment returned invalid payload") + return [ + { + "text": str(item.get("text", "")), + "start_ms": float(item.get("start_ms", 0.0)), + "end_ms": float(item.get("end_ms", 0.0)), + } + for item in items + if isinstance(item, dict) and str(item.get("text", "")).strip() + ] + + def create_stream( + self, + *, + chunk_size_sec: float = 1.2, + unfixed_chunk_num: int = 2, + rollback_tokens: int = 5, + max_new_tokens: int = 32, + language: Optional[str] = None, + ) -> QwenASRRustStreamHandle: + handle = self._lib.qwen_asr_stream_new() + if not handle: + raise RuntimeError("QwenASR failed to create stream state") + + self._configure_stream( + chunk_size_sec=chunk_size_sec, + unfixed_chunk_num=unfixed_chunk_num, + rollback_tokens=rollback_tokens, + max_new_tokens=max_new_tokens, + past_text=True, + ) + self._set_language(language) + return QwenASRRustStreamHandle(self._lib, handle) + + def push_stream( + self, + stream: QwenASRRustStreamHandle, + samples: np.ndarray, + *, + chunk_size_sec: float, + unfixed_chunk_num: int, + rollback_tokens: int, + max_new_tokens: int, + language: Optional[str], + finalize: bool = False, + ) -> str: + self._configure_stream( + chunk_size_sec=chunk_size_sec, + unfixed_chunk_num=unfixed_chunk_num, + rollback_tokens=rollback_tokens, + max_new_tokens=max_new_tokens, + past_text=True, + ) + self._set_language(language) + + pcm = np.ascontiguousarray(samples, dtype=np.float32) + pointer = ( + pcm.ctypes.data_as(ctypes.POINTER(ctypes.c_float)) + if len(pcm) > 0 + else None + ) + delta_ptr = self._lib.qwen_asr_stream_push( + self._engine, + stream.handle, + pointer, + len(pcm), + 1 if finalize else 0, + ) + delta_text = _decode_and_free_string(self._lib, delta_ptr) or "" + stream.accumulated_text += delta_text + return stream.accumulated_text + + +class QwenASRRustRuntime: + """Higher-level Rust runtime bundle for ASR + aligner + streaming.""" + + def __init__( + self, + model_path: str, + *, + forced_aligner_path: Optional[str] = None, + num_threads: int = 0, + verbosity: int = 0, + ) -> None: + self._asr = QwenASRRustBackend( + model_path=model_path, + num_threads=num_threads, + verbosity=verbosity, + ) + self._aligner: Optional[QwenASRRustBackend] = None + if forced_aligner_path: + self._aligner = QwenASRRustBackend( + model_path=forced_aligner_path, + num_threads=num_threads, + verbosity=verbosity, + ) + + def transcribe_file(self, audio_path: str, language: Optional[str] = None) -> str: + return self._asr.transcribe_file(audio_path=audio_path, language=language) + + def align_transcript( + self, + audio_path: str, + text: str, + language: Optional[str] = None, + ) -> list[dict[str, float | str]]: + transcript = text.strip() + if not transcript: + return [] + if self._aligner is None: + raise RuntimeError("Forced alignment requires a configured aligner model") + return self._aligner.force_align_file( + audio_path=audio_path, + text=transcript, + language=guess_alignment_language(transcript, language), + ) + + def create_stream( + self, + *, + chunk_size_sec: float = 1.2, + unfixed_chunk_num: int = 2, + rollback_tokens: int = 5, + max_new_tokens: int = 32, + language: Optional[str] = None, + ) -> QwenASRRustStreamHandle: + return self._asr.create_stream( + chunk_size_sec=chunk_size_sec, + unfixed_chunk_num=unfixed_chunk_num, + rollback_tokens=rollback_tokens, + max_new_tokens=max_new_tokens, + language=language, + ) + + def push_stream( + self, + stream: QwenASRRustStreamHandle, + samples: np.ndarray, + *, + chunk_size_sec: float, + unfixed_chunk_num: int, + rollback_tokens: int, + max_new_tokens: int, + language: Optional[str], + ) -> str: + return self._asr.push_stream( + stream=stream, + samples=samples, + chunk_size_sec=chunk_size_sec, + unfixed_chunk_num=unfixed_chunk_num, + rollback_tokens=rollback_tokens, + max_new_tokens=max_new_tokens, + language=language, + finalize=False, + ) + + def finish_stream( + self, + stream: QwenASRRustStreamHandle, + *, + chunk_size_sec: float, + unfixed_chunk_num: int, + rollback_tokens: int, + max_new_tokens: int, + language: Optional[str], + ) -> str: + return self._asr.push_stream( + stream=stream, + samples=np.array([], dtype=np.float32), + chunk_size_sec=chunk_size_sec, + unfixed_chunk_num=unfixed_chunk_num, + rollback_tokens=rollback_tokens, + max_new_tokens=max_new_tokens, + language=language, + finalize=True, + ) diff --git a/app/services/asr/runtime/__init__.py b/app/services/asr/runtime/__init__.py new file mode 100644 index 0000000..f4cc7a0 --- /dev/null +++ b/app/services/asr/runtime/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +"""ASR runtime routing and pooling layer.""" + +from .router import ( + OfflineASRRequest, + RuntimeEngineLease, + RuntimeRouter, + get_runtime_router, +) + +__all__ = [ + "OfflineASRRequest", + "RuntimeEngineLease", + "RuntimeRouter", + "get_runtime_router", +] diff --git a/app/services/asr/runtime/local_pool.py b/app/services/asr/runtime/local_pool.py new file mode 100644 index 0000000..6f8b9bf --- /dev/null +++ b/app/services/asr/runtime/local_pool.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +"""Small async pool for per-request ASR engines.""" + +from __future__ import annotations + +import asyncio +import threading +from dataclasses import dataclass +from typing import Callable, Generic, Optional, TypeVar + +T = TypeVar("T") + + +@dataclass +class _PoolState(Generic[T]): + queue: asyncio.Queue[T] + + +class LocalEnginePool(Generic[T]): + """Fixed-size lazy engine pool backed by ``asyncio.Queue``.""" + + def __init__(self, size: int, factory: Callable[[], T]): + self._size = max(1, size) + self._factory = factory + self._state: Optional[_PoolState[T]] = None + self._init_lock = threading.Lock() + + def _ensure_state(self) -> _PoolState[T]: + if self._state is not None: + return self._state + + with self._init_lock: + if self._state is None: + self._state = _PoolState(queue=asyncio.Queue(maxsize=self._size)) + for _ in range(self._size): + self._state.queue.put_nowait(self._factory()) + return self._state + + def warmup(self) -> None: + self._ensure_state() + + async def acquire(self) -> T: + state = self._ensure_state() + return await state.queue.get() + + async def release(self, engine: T) -> None: + state = self._ensure_state() + await state.queue.put(engine) diff --git a/app/services/asr/runtime/router.py b/app/services/asr/runtime/router.py new file mode 100644 index 0000000..eaf9125 --- /dev/null +++ b/app/services/asr/runtime/router.py @@ -0,0 +1,241 @@ +# -*- coding: utf-8 -*- +"""Runtime router for pooled ASR execution.""" + +from __future__ import annotations +import asyncio +import threading +from dataclasses import dataclass +from enum import Enum +from typing import Awaitable, Callable, Optional + +import torch + +from app.core.accelerator import get_accelerator_info +from app.core.config import settings +from app.core.device import detect_device +from app.core.executor import run_sync +from app.services.asr.engines import ASRFullResult, BaseASREngine +from app.services.asr.manager import get_model_manager +from app.services.asr.qwenasr_rust import is_qwenasr_rust_available +from .local_pool import LocalEnginePool + + +class RuntimeFamily(str, Enum): + QWEN_VLLM = "qwen_vllm" + QWEN_RUST_CPU = "qwen_rust_cpu" + + +@dataclass +class OfflineASRRequest: + model_id: str + audio_path: str + hotwords: str = "" + enable_punctuation: bool = True + enable_itn: bool = True + sample_rate: int = 16000 + enable_speaker_diarization: bool = True + enable_speaker_identification: bool = True + enable_text_cleanup: bool = True + word_timestamps: bool = False + timestamp_scale: float = 1.0 + task_id: Optional[str] = None + progress_callback: Optional[Callable[[str, str, int, Optional[dict[str, object]]], None]] = None + + +class RuntimeEngineLease: + """Lifecycle wrapper around a pooled engine instance.""" + + def __init__(self, engine: BaseASREngine, release_callback: Callable[[], None | Awaitable[None]]): + self.engine = engine + self._release_callback = release_callback + self._closed = False + + async def close(self) -> None: + if self._closed: + return + self._closed = True + result = self._release_callback() + if asyncio.iscoroutine(result): + await result + + async def __aenter__(self) -> BaseASREngine: + return self.engine + + async def __aexit__(self, exc_type, exc, tb) -> None: + await self.close() + + +class RuntimeRouter: + """Central backend router for all ASR entrypoints.""" + + def __init__(self): + self._manager = get_model_manager() + self._pools: dict[tuple[RuntimeFamily, str], LocalEnginePool[BaseASREngine]] = {} + self._shared_engines: dict[tuple[RuntimeFamily, str], BaseASREngine] = {} + self._shared_limits: dict[tuple[RuntimeFamily, str], asyncio.Semaphore] = {} + self._pool_lock = threading.Lock() + self._loaded_model_ids: set[str] = set() + + def resolve_model_id(self, model_id: Optional[str]) -> str: + if model_id: + return model_id + config = self._manager.get_declared_entry_config() + return config.model_id + + def _resolve_family(self, model_id: str) -> RuntimeFamily: + device = detect_device(settings.DEVICE) + accelerator = get_accelerator_info() + if model_id.startswith("qwen3-asr-"): + if accelerator.is_gpu and device.startswith("cuda"): + return RuntimeFamily.QWEN_VLLM + if device == "cpu" and is_qwenasr_rust_available(): + return RuntimeFamily.QWEN_RUST_CPU + raise RuntimeError( + "Qwen3-ASR is not available on " + f"accelerator='{accelerator.vendor}' device='{device}'" + ) + raise RuntimeError(f"Unsupported runtime model: {model_id}") + + def _pool_size_for_family(self, family: RuntimeFamily) -> int: + if family == RuntimeFamily.QWEN_VLLM: + return 1 + return settings.QWEN_RUST_CPU_WORKERS + + def _create_pool(self, family: RuntimeFamily, model_id: str) -> LocalEnginePool[BaseASREngine]: + pool_key = (family, model_id) + existing = self._pools.get(pool_key) + if existing is not None: + return existing + + with self._pool_lock: + existing = self._pools.get(pool_key) + if existing is not None: + return existing + pool = LocalEnginePool( + size=self._pool_size_for_family(family), + factory=lambda: self._manager.create_engine(model_id), + ) + self._pools[pool_key] = pool + self._loaded_model_ids.add(model_id) + return pool + + def _get_shared_engine(self, family: RuntimeFamily, model_id: str) -> tuple[BaseASREngine, asyncio.Semaphore]: + runtime_key = (family, model_id) + engine = self._shared_engines.get(runtime_key) + semaphore = self._shared_limits.get(runtime_key) + if engine is not None and semaphore is not None: + return engine, semaphore + + with self._pool_lock: + engine = self._shared_engines.get(runtime_key) + semaphore = self._shared_limits.get(runtime_key) + if engine is None: + engine = self._manager.create_engine(model_id) + self._shared_engines[runtime_key] = engine + self._loaded_model_ids.add(model_id) + if semaphore is None: + shared_concurrency = max(1, int(settings.QWEN_VLLM_SHARED_CONCURRENCY)) + semaphore = asyncio.Semaphore(shared_concurrency) + self._shared_limits[runtime_key] = semaphore + return engine, semaphore + + def warmup_model(self, model_id: Optional[str] = None) -> None: + resolved_model_id = self.resolve_model_id(model_id) + family = self._resolve_family(resolved_model_id) + if family == RuntimeFamily.QWEN_VLLM: + self._get_shared_engine(family, resolved_model_id) + return + pool = self._create_pool(family, resolved_model_id) + pool.warmup() + + def get_loaded_model_ids(self) -> list[str]: + return sorted(self._loaded_model_ids) + + def get_memory_usage(self) -> dict[str, object]: + memory_info: dict[str, object] = { + "model_list": self.get_loaded_model_ids(), + "loaded_count": len(self._loaded_model_ids), + } + + accelerator = get_accelerator_info() + memory_info["accelerator"] = accelerator.as_dict() + + if accelerator.is_gpu and torch.cuda.is_available(): + memory_info["gpu_memory"] = { + "allocated": f"{torch.cuda.memory_allocated() / 1024**3:.2f}GB", + "cached": f"{torch.cuda.memory_reserved() / 1024**3:.2f}GB", + "max_allocated": f"{torch.cuda.max_memory_allocated() / 1024**3:.2f}GB", + } + + return memory_info + + async def acquire_engine(self, model_id: Optional[str] = None) -> RuntimeEngineLease: + resolved_model_id = self.resolve_model_id(model_id) + family = self._resolve_family(resolved_model_id) + if family == RuntimeFamily.QWEN_VLLM: + engine, semaphore = self._get_shared_engine(family, resolved_model_id) + await semaphore.acquire() + return RuntimeEngineLease( + engine=engine, + release_callback=semaphore.release, + ) + pool = self._create_pool(family, resolved_model_id) + engine = await pool.acquire() + return RuntimeEngineLease( + engine=engine, + release_callback=lambda: pool.release(engine), + ) + + async def run_offline(self, request: OfflineASRRequest) -> ASRFullResult: + async with await self.acquire_engine(request.model_id) as engine: + result = await run_sync( + engine.transcribe_long_audio, + audio_path=request.audio_path, + hotwords=request.hotwords, + enable_punctuation=request.enable_punctuation, + enable_itn=request.enable_itn, + sample_rate=request.sample_rate, + enable_speaker_diarization=request.enable_speaker_diarization, + enable_speaker_identification=request.enable_speaker_identification, + enable_text_cleanup=request.enable_text_cleanup, + word_timestamps=request.word_timestamps, + timestamp_scale=request.timestamp_scale, + task_id=request.task_id, + progress_callback=request.progress_callback, + ) + if ( + request.enable_speaker_diarization + and request.enable_speaker_identification + and settings.SPEAKER_DB_ENABLED + ): + try: + from app.services.speaker_registry import get_speaker_registry_service + + if request.progress_callback is not None: + request.progress_callback( + "speaker_matching", + "正在匹配已注册声纹库。", + 94, + None, + ) + result = await get_speaker_registry_service().apply_registered_speakers( + result, + threshold=settings.SV_THRESHOLD, + ) + except Exception: + # 数据库/声纹匹配失败不影响原有 ASR 输出,仍保留 CAM++ 的局部说话人编号。 + pass + return result + + +_runtime_router: Optional[RuntimeRouter] = None +_runtime_router_lock = threading.Lock() + + +def get_runtime_router() -> RuntimeRouter: + global _runtime_router + if _runtime_router is None: + with _runtime_router_lock: + if _runtime_router is None: + _runtime_router = RuntimeRouter() + return _runtime_router diff --git a/app/services/audio/__init__.py b/app/services/audio/__init__.py new file mode 100644 index 0000000..009879a --- /dev/null +++ b/app/services/audio/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +""" +音频处理服务模块 + +提供统一的音频处理服务层,封装音频下载、格式转换、归一化等功能。 +""" + +from .audio_service import AudioProcessingService, get_audio_service + +__all__ = ["AudioProcessingService", "get_audio_service"] diff --git a/app/services/audio/audio_service.py b/app/services/audio/audio_service.py new file mode 100644 index 0000000..a2572ad --- /dev/null +++ b/app/services/audio/audio_service.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- +""" +音频处理服务 + +封装音频处理逻辑,提供统一的音频下载、格式转换、归一化等服务。 +API层应该通过此服务层处理音频,而不是直接调用 utils/audio.py 中的函数。 +""" + +import logging +import threading +from dataclasses import dataclass +from typing import Optional +from fastapi import Request + +from ...core.config import settings +from ...core.exceptions import InvalidMessageException +from ...utils.audio import ( + download_audio_from_url, + save_audio_to_temp_file, + normalize_audio_for_asr, + get_audio_duration, + cleanup_temp_file, + get_audio_file_suffix, +) + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class AudioProcessingResult: + normalized_path: str + duration: float + original_path: str + timestamp_scale: float = 1.0 + + +class AudioProcessingService: + """音频处理服务 + + 提供统一的音频处理接口,包括: + 1. 从URL下载音频 + 2. 处理上传的音频文件 + 3. 音频格式转换和归一化 + 4. 临时文件管理 + """ + + async def process_from_request( + self, + request: Request, + audio_address: Optional[str] = None, + task_id: Optional[str] = None, + sample_rate: Optional[int] = None, + ) -> AudioProcessingResult: + """从请求中处理音频 + + 支持两种方式: + 1. 请求体上传:从请求体读取二进制音频/视频数据 + 2. URL下载:通过 audio_address 参数指定音频/视频 URL + + 当请求体和 audio_address 同时存在时,优先使用请求体, + 并忽略 audio_address。 + + Args: + request: FastAPI请求对象 + audio_address: 音频文件URL(可选) + task_id: 任务ID,用于日志记录(可选) + sample_rate: 目标采样率(可选,默认16000) + + Returns: + Processed audio path, duration, original path, and timestamp metadata. + + Raises: + InvalidMessageException: 音频数据为空或文件太大 + InvalidParameterException: URL无效或下载失败 + """ + task_id = task_id or "unknown" + target_sr = sample_rate or 16000 + + # 优先读取请求体;若请求体为空,再回退到 audio_address。 + # 注意:对于 FastAPI 已经解析过 form/multipart 的请求, + # 再次读取 body 可能抛出 "Stream consumed"。 + try: + uploaded_data = await request.body() + except RuntimeError as exc: + if "Stream consumed" in str(exc): + logger.info(f"[{task_id}] 请求体已被上游读取,跳过 request.body() 回退逻辑") + uploaded_data = b"" + else: + raise + + if uploaded_data: + if audio_address: + logger.info(f"[{task_id}] 检测到同时提供上传内容和 audio_address,已忽略 audio_address") + return self._process_audio_bytes( + audio_data=uploaded_data, + filename=None, + task_id=task_id, + target_sr=target_sr, + ) + + if audio_address: + logger.info(f"[{task_id}] 开始从URL下载音频: {audio_address}") + audio_data = download_audio_from_url(audio_address) + logger.info( + f"[{task_id}] 音频下载完成,大小: {len(audio_data) / 1024 / 1024:.2f}MB" + ) + return self._process_audio_bytes( + audio_data=audio_data, + filename=audio_address, + task_id=task_id, + target_sr=target_sr, + ) + + raise InvalidMessageException("音频数据为空", task_id) + + async def process_upload_file( + self, + audio_data: bytes, + filename: Optional[str] = None, + task_id: Optional[str] = None, + sample_rate: Optional[int] = None, + ) -> AudioProcessingResult: + """处理上传的音频文件 + + Args: + audio_data: 音频二进制数据 + filename: 原始文件名(用于检测格式,可选) + task_id: 任务ID,用于日志记录(可选) + sample_rate: 目标采样率(可选,默认16000) + + Returns: + Processed audio path, duration, original path, and timestamp metadata. + + Raises: + InvalidMessageException: 音频数据为空或文件太大 + """ + task_id = task_id or "unknown" + target_sr = sample_rate or 16000 + return self._process_audio_bytes( + audio_data=audio_data, + filename=filename, + task_id=task_id, + target_sr=target_sr, + ) + + def _process_audio_bytes( + self, + *, + audio_data: bytes, + filename: Optional[str], + task_id: str, + target_sr: int, + ) -> AudioProcessingResult: + """Persist, normalize, and measure audio bytes.""" + audio_path = None + normalized_audio_path = None + + try: + if not audio_data: + raise InvalidMessageException("音频数据为空", task_id) + + file_size = len(audio_data) + logger.info(f"[{task_id}] 音频文件大小: {file_size / 1024 / 1024:.2f}MB") + + # 检查文件大小 + if file_size > settings.MAX_AUDIO_SIZE: + max_mb = settings.MAX_AUDIO_SIZE // 1024 // 1024 + raise InvalidMessageException( + f"音频文件太大,最大支持{max_mb}MB", task_id + ) + + file_suffix = get_audio_file_suffix( + audio_address=filename, + audio_data=audio_data, + ) + logger.info(f"[{task_id}] 识别文件格式: {file_suffix}") + audio_path = save_audio_to_temp_file(audio_data, file_suffix) + logger.info(f"[{task_id}] 临时文件: {audio_path}") + + logger.info(f"[{task_id}] 开始音频格式转换...") + normalized_audio = normalize_audio_for_asr(audio_path, target_sr) + normalized_audio_path = normalized_audio.path + logger.info(f"[{task_id}] 音频格式转换完成: {normalized_audio_path}") + + decoded_duration = get_audio_duration(normalized_audio_path) + audio_duration = decoded_duration * normalized_audio.timestamp_scale + logger.info(f"[{task_id}] 音频时长: {audio_duration:.1f}s") + + return AudioProcessingResult( + normalized_path=normalized_audio_path, + duration=audio_duration, + original_path=audio_path, + timestamp_scale=normalized_audio.timestamp_scale, + ) + + except Exception: + if audio_path: + cleanup_temp_file(audio_path) + if normalized_audio_path and normalized_audio_path != audio_path: + cleanup_temp_file(normalized_audio_path) + raise + + def cleanup( + self, audio_path: Optional[str], normalized_path: Optional[str] = None + ) -> None: + """清理临时文件 + + Args: + audio_path: 原始音频文件路径 + normalized_path: 归一化后的音频文件路径(可选) + """ + if audio_path: + cleanup_temp_file(audio_path) + if normalized_path and normalized_path != audio_path: + cleanup_temp_file(normalized_path) + + +# 全局服务实例(单例模式) +_audio_service: Optional[AudioProcessingService] = None +_audio_service_lock = threading.Lock() + + +def get_audio_service() -> AudioProcessingService: + """获取音频处理服务实例(线程安全的单例) + + Returns: + AudioProcessingService: 音频处理服务实例 + """ + global _audio_service + if _audio_service is None: + with _audio_service_lock: + if _audio_service is None: + _audio_service = AudioProcessingService() + return _audio_service diff --git a/app/services/qwen3_websocket_asr.py b/app/services/qwen3_websocket_asr.py new file mode 100644 index 0000000..d71f3ed --- /dev/null +++ b/app/services/qwen3_websocket_asr.py @@ -0,0 +1,2949 @@ +# -*- coding: utf-8 -*- +"""Qwen3-ASR websocket streaming service.""" + +from __future__ import annotations + +import asyncio +import io +import json +import logging +import re +import time +import unicodedata +from difflib import SequenceMatcher +from dataclasses import dataclass, field +from enum import IntEnum +from typing import Any, Dict, List, Optional + +import numpy as np +import soundfile as sf +from fastapi import WebSocket, WebSocketDisconnect + +from app.core.config import settings +from app.core.exceptions import create_error_response +from app.core.executor import run_sync +from app.core.text_cleanup import deduplicate_asr_text +from app.services.asr.model_selection import validate_realtime_model_id +from app.services.asr.qwen3_engine import Qwen3ASREngine, Qwen3StreamingState +from app.services.asr.engines.global_models import get_global_vad_model, get_vad_inference_lock +from app.services.realtime_speaker_clusterer import get_realtime_speaker_clusterer +from app.services.asr.runtime import RuntimeEngineLease, get_runtime_router +from app.services.speaker_registry import get_speaker_registry_service +from app.utils.text_processing import normalize_asr_text + +logger = logging.getLogger(__name__) + + +def _remove_repeated_chars(text: str, max_run: int) -> str: + out: List[str] = [] + last: Optional[str] = None + run = 0 + for ch in text: + if ch == last: + run += 1 + else: + last = ch + run = 1 + if run <= max_run: + out.append(ch) + return "".join(out) + + +def _normalize_transcript_chunk_text(text: str) -> str: + collapsed = " ".join(str(text or "").split()).strip() + if not collapsed: + return "" + return _remove_repeated_chars(collapsed, 8) + + +def _is_opening_punctuation(ch: str) -> bool: + return ch in '([{"\'“‘' + + +def _is_closing_punctuation(ch: str) -> bool: + return ch in '.,!?:;)]}"\'”’。,!?:;、' + + +def _append_with_spacing(left: str, right: str) -> str: + if not left: + return right + if not right: + return left + prev = left[-1] + nxt = right[0] + needs_space = ( + not prev.isspace() + and not nxt.isspace() + and not _is_closing_punctuation(nxt) + and not _is_opening_punctuation(prev) + ) + return f"{left} {right}" if needs_space else f"{left}{right}" + + +def _normalize_token(token: str) -> str: + return "".join( + ch.lower() + for ch in token + if ch.isalnum() or ch in {"'", "-"} + ) + + +def _token_views(text: str) -> List[tuple[str, int]]: + views: List[tuple[str, int]] = [] + start: Optional[int] = None + for idx, ch in enumerate(text): + if ch.isspace(): + if start is not None: + token = text[start:idx] + normalized = _normalize_token(token) + if normalized: + views.append((normalized, start)) + start = None + continue + if start is None: + start = idx + if start is not None: + token = text[start:] + normalized = _normalize_token(token) + if normalized: + views.append((normalized, start)) + return views + + +def _dedupe_overlap_word_boundary( + previous: str, + current: str, + min_overlap: int = 3, + max_overlap: int = 24, +) -> int: + prev_tokens = _token_views(previous) + curr_tokens = _token_views(current) + if not prev_tokens or not curr_tokens: + return 0 + upper = min(max_overlap, len(prev_tokens), len(curr_tokens)) + lower = max(min_overlap, 1) + if upper < lower: + return 0 + for size in range(upper, lower - 1, -1): + left = prev_tokens[-size:] + right = curr_tokens[:size] + if all(a == b for (a, _), (b, _) in zip(left, right)): + if size == len(curr_tokens): + return len(current) + return curr_tokens[size][1] + return 0 + + +def _char_count_to_index(text: str, char_count: int) -> int: + if char_count <= 0: + return 0 + count = 0 + for idx, ch in enumerate(text): + count += 1 + if count == char_count: + return idx + 1 + return len(text) + + +def _dedupe_overlap_char_boundary( + previous: str, + current: str, + min_chars: int = 6, + max_chars: int = 80, +) -> int: + prev_chars = list(previous) + curr_chars = list(current) + if not prev_chars or not curr_chars: + return 0 + upper = min(max_chars, len(prev_chars), len(curr_chars)) + lower = max(min_chars, 1) + if upper < lower: + return 0 + for size in range(upper, lower - 1, -1): + left = "".join(ch.lower() for ch in prev_chars[-size:]) + right = "".join(ch.lower() for ch in curr_chars[:size]) + if left == right: + return _char_count_to_index(current, size) + return 0 + + +class RealtimeTranscriptAssembler: + def __init__(self) -> None: + self._merged = "" + + def push(self, text: str) -> None: + cleaned = _normalize_transcript_chunk_text(text) + if not cleaned: + return + if not self._merged: + self._merged = cleaned + return + delta_start = _dedupe_overlap_word_boundary(self._merged, cleaned) + if delta_start == 0: + delta_start = _dedupe_overlap_char_boundary(self._merged, cleaned) + if delta_start >= len(cleaned): + return + delta = cleaned[delta_start:].lstrip() + if not delta: + return + self._merged = _append_with_spacing(self._merged, delta) + + def text(self) -> str: + return self._merged.strip() + + +def _convert_audio( + audio_bytes: bytes, + fmt: str, + sample_rate: int, +) -> Optional[np.ndarray]: + try: + if fmt == "pcm": + audio = np.frombuffer(audio_bytes, dtype=np.int16).astype(np.float32) / 32768.0 + elif fmt == "wav": + try: + audio, sr = sf.read(io.BytesIO(audio_bytes)) + audio = np.asarray(audio, dtype=np.float32) + if sr != sample_rate: + logger.warning("WAV sample rate mismatch: payload=%s actual=%s", sample_rate, sr) + sample_rate = int(sr) + except Exception: + if len(audio_bytes) > 44: + audio_bytes = audio_bytes[44:] + audio = np.frombuffer(audio_bytes, dtype=np.int16).astype(np.float32) / 32768.0 + else: + raise ValueError(f"Unsupported audio format: {fmt}") + + if getattr(audio, "ndim", 1) > 1: + audio = np.mean(audio, axis=1) + + if sample_rate != 16000: + import scipy.signal + + num = int(len(audio) * 16000 / sample_rate) + audio = scipy.signal.resample(audio, num) + if isinstance(audio, tuple): + audio = audio[0] + + return np.asarray(audio, dtype=np.float32) + except Exception as exc: + logger.error("Audio conversion failed: %s", exc) + return None + + +class ConnectionState(IntEnum): + READY = 1 + STARTED = 2 + STREAMING = 3 + + +@dataclass +class ConnectionContext: + state: ConnectionState = ConnectionState.READY + params: Dict[str, Any] = field(default_factory=dict) + engine_lease: Optional[RuntimeEngineLease] = None + engine: Optional[Qwen3ASREngine] = None + pre_roll_audio: np.ndarray = field(default_factory=lambda: np.array([], dtype=np.float32)) + # segment_audio_buffer 保留“当前断句以来”的完整音频,用于断句时提取声纹。 + segment_audio_buffer: np.ndarray = field(default_factory=lambda: np.array([], dtype=np.float32)) + # stream_window_buffer 保留最近窗口,供 realtime partial 在 native stream 失效时兜底重转写。 + stream_window_buffer: np.ndarray = field(default_factory=lambda: np.array([], dtype=np.float32)) + # realtime_stream_state 只负责低延迟 partial,不参与最终 segment 定稿。 + realtime_stream_state: Optional[Qwen3StreamingState] = None + sentence_active: bool = False + silence_samples: int = 0 + total_samples: int = 0 + confirmed_segments: List[Dict[str, Any]] = field(default_factory=list) + speaker_records: List[Dict[str, Any]] = field(default_factory=list) + speaker_display_map: Dict[int, str] = field(default_factory=dict) + speaker_display_profiles: Dict[str, np.ndarray] = field(default_factory=dict) + speaker_display_counts: Dict[str, int] = field(default_factory=dict) + next_speaker_display_id: int = 1 + timeline_cursor_ms: int = 0 + segment_index: int = 0 + last_partial_text: str = "" + last_partial_language: str = "" + last_partial_chunk_id: int = 0 + last_partial_raw_text: str = "" + last_partial_display_text: str = "" + stable_partial_prefix: str = "" + segment_observed_text: str = "" + segment_observed_language: str = "" + best_partial_text: str = "" + best_partial_language: str = "" + last_partial_decode_samples: int = 0 + partial_stable_rounds: int = 0 + send_lock: asyncio.Lock = field(default_factory=asyncio.Lock) + background_tasks: set[asyncio.Task[Any]] = field(default_factory=set) + speaker_job_queue: asyncio.Queue[Any] = field(default_factory=asyncio.Queue) + speaker_worker_task: Optional[asyncio.Task[Any]] = None + speaker_worker_socket_id: Optional[int] = None + MAX_BUFFER = 960000 + DEFAULT_MAX_PARTIAL_TEXT_CHARS = 1200 + + +@dataclass +class SessionEntry: + session_id: str + ctx: ConnectionContext + attached: bool = False + detached_at: Optional[float] = None + closed: bool = False + + +class Qwen3ASRService: + _MAX_SPEAKER_HISTORY_RECORDS = 64 + _MAX_SPEAKER_MATCH_CONTEXT = 24 + _MAX_SPEAKER_FINAL_RECLUSTER = 48 + _MAX_SPEAKER_REALTIME_RECLUSTER = 12 + _MIN_REALTIME_RECLUSTER_SEGMENTS = 5 + _MIN_HARD_SEGMENT_SEC = 12.0 + _SEGMENT_EVENT_TEXT_TAIL_CHARS = 2000 + _SEGMENT_EVENT_TAIL_COUNT = 8 + _LIGHTWEIGHT_RECENT_SENTENCE_COUNT = 3 + + def __init__(self) -> None: + self._sessions: Dict[str, SessionEntry] = {} + self._session_lock = asyncio.Lock() + + async def _cleanup_expired_sessions(self) -> None: + ttl_sec = max(int(getattr(settings, "REALTIME_SESSION_RESUME_TTL_SEC", 120) or 0), 0) + if ttl_sec <= 0: + expired_ids = [session_id for session_id, entry in self._sessions.items() if not entry.attached] + else: + now = time.monotonic() + expired_ids = [ + session_id + for session_id, entry in self._sessions.items() + if not entry.attached + and ( + entry.closed + or entry.detached_at is None + or now - entry.detached_at > ttl_sec + ) + ] + + for session_id in expired_ids: + entry = self._sessions.pop(session_id, None) + if entry is not None: + await self._release_session_resources(entry.ctx) + + async def _release_session_resources(self, ctx: ConnectionContext) -> None: + if ctx.speaker_worker_task is not None and not ctx.speaker_worker_task.done(): + await ctx.speaker_job_queue.put(None) + if ctx.background_tasks: + await asyncio.gather(*list(ctx.background_tasks), return_exceptions=True) + if ctx.engine_lease is not None: + await ctx.engine_lease.close() + ctx.engine_lease = None + ctx.engine = None + ctx.realtime_stream_state = None + + async def _detach_session( + self, + session_id: str, + *, + keep_for_resume: bool, + ) -> None: + async with self._session_lock: + entry = self._sessions.get(session_id) + if entry is None: + return + entry.attached = False + entry.detached_at = time.monotonic() + entry.closed = not keep_for_resume + self._stop_speaker_worker(entry.ctx) + await self._cleanup_expired_sessions() + + async def _acquire_session( + self, + session_id: str, + ) -> tuple[ConnectionContext, bool]: + async with self._session_lock: + await self._cleanup_expired_sessions() + entry = self._sessions.get(session_id) + if entry is not None and not entry.attached and not entry.closed: + entry.attached = True + entry.detached_at = None + return entry.ctx, True + + ctx = ConnectionContext() + self._sessions[session_id] = SessionEntry( + session_id=session_id, + ctx=ctx, + attached=True, + detached_at=None, + closed=False, + ) + return ctx, False + + @staticmethod + def _tencent_speaker_id(value: Any) -> int: + if value is None: + return -1 + if isinstance(value, bool): + return int(value) + if isinstance(value, int): + return value + text = str(value).strip() + if not text: + return -1 + if re.fullmatch(r"-?\d+", text): + return int(text) + match = re.fullmatch(r"Speaker(\d+)", text) + if match: + return max(int(match.group(1)) - 1, 0) + return -1 + + def _build_tencent_sentence( + self, + payload: Dict[str, Any], + *, + sentence_type: int, + sentence_text: Optional[str] = None, + ) -> Dict[str, Any]: + speaker_id = self._tencent_speaker_id(payload.get("speaker_id")) + if payload.get("speaker_pending"): + speaker_id = -1 + sentence = { + "sentence_id": int(payload.get("index", 0)), + "sentence_type": int(sentence_type), + "speaker_id": speaker_id, + "start_time": int(payload.get("start_ms", 0)), + "end_time": int(payload.get("end_ms", 0)), + "sentence": str(sentence_text if sentence_text is not None else payload.get("text", "")), + } + # Tencent-compatible core fields first; project-specific fields remain minimal. + for key in ( + "speaker_name", + "user_id", + ): + if key in payload: + sentence[key] = payload.get(key) + return sentence + + async def _send_json_safe( + self, + websocket: WebSocket, + ctx: ConnectionContext, + payload: Dict[str, Any], + ) -> None: + async with ctx.send_lock: + await websocket.send_json(payload) + + def _track_background_task( + self, + ctx: ConnectionContext, + task: asyncio.Task[Any], + ) -> None: + ctx.background_tasks.add(task) + task.add_done_callback(ctx.background_tasks.discard) + + def _ensure_speaker_worker( + self, + websocket: WebSocket, + ctx: ConnectionContext, + task_id: str, + ) -> None: + task = ctx.speaker_worker_task + socket_id = id(websocket) + if ( + task is not None + and not task.done() + and ctx.speaker_worker_socket_id == socket_id + ): + return + worker = asyncio.create_task( + self._speaker_worker_loop( + websocket, + ctx, + task_id, + ) + ) + ctx.speaker_worker_task = worker + ctx.speaker_worker_socket_id = socket_id + self._track_background_task(ctx, worker) + + def _stop_speaker_worker(self, ctx: ConnectionContext) -> None: + task = ctx.speaker_worker_task + if task is not None and not task.done(): + task.cancel() + ctx.speaker_worker_task = None + ctx.speaker_worker_socket_id = None + + async def _speaker_worker_loop( + self, + websocket: WebSocket, + ctx: ConnectionContext, + task_id: str, + ) -> None: + while True: + job = await ctx.speaker_job_queue.get() + try: + if job is None: + return + await self._resolve_and_emit_segment_speaker( + websocket, + ctx, + task_id, + segment_index=int(job["segment_index"]), + audio=np.asarray(job["audio"], dtype=np.float32), + reason=str(job["reason"]), + segment_start_ms=int(job["segment_start_ms"]), + segment_end_ms=int(job["segment_end_ms"]), + ) + finally: + ctx.speaker_job_queue.task_done() + + async def _emit_speaker_update( + self, + websocket: WebSocket, + ctx: ConnectionContext, + task_id: str, + segment_index: int, + ) -> None: + segment = next( + (item for item in ctx.confirmed_segments if int(item.get("index", -1)) == int(segment_index)), + None, + ) + if segment is None: + return + sentence_payload = self._build_tencent_sentence(segment, sentence_type=1) + await self._send_json_safe( + websocket, + ctx, + { + "type": "sentences", + "code": 0, + "voice_id": task_id, + "final": 0, + "result": { + "slice_type": 2, + "index": int(segment_index), + "voice_text_str": str(segment.get("text") or ""), + }, + "sentences": [sentence_payload], + }, + ) + + @staticmethod + def _public_speaker_view(payload: Dict[str, Any]) -> tuple[Any, Any, Any]: + return ( + payload.get("speaker_id"), + payload.get("speaker_name"), + payload.get("user_id"), + ) + + def _create_display_speaker_id( + self, + ctx: ConnectionContext, + ) -> str: + display_id = f"Speaker{ctx.next_speaker_display_id:02d}" + ctx.next_speaker_display_id += 1 + return display_id + + def _match_display_speaker_id( + self, + ctx: ConnectionContext, + *, + cluster_index: Optional[int], + embedding: Optional[np.ndarray], + ) -> Optional[str]: + if embedding is None: + return None + + normalized = np.asarray(embedding, dtype=np.float32) + norm = float(np.linalg.norm(normalized)) + if norm <= 0: + return None + normalized = normalized / norm + + preferred_display_id: Optional[str] = None + if cluster_index is not None: + mapped = ctx.speaker_display_map.get(int(cluster_index)) + if mapped: + profile = ctx.speaker_display_profiles.get(mapped) + if profile is not None: + preferred_score = float(np.dot(normalized, profile)) + if preferred_score >= settings.REALTIME_SPEAKER_CONFIRM_THRESHOLD: + preferred_display_id = mapped + # Do not aggressively reuse a generic display id across unrelated + # segments when there is no stable cluster mapping yet; that easily + # collapses multiple real speakers into Speaker01. + + if preferred_display_id is None: + preferred_display_id = self._create_display_speaker_id(ctx) + + count = int(ctx.speaker_display_counts.get(preferred_display_id, 0)) + previous = ctx.speaker_display_profiles.get(preferred_display_id) + if previous is None or count <= 0: + updated = normalized + count = 1 + else: + updated = previous * float(count) + normalized + updated_norm = float(np.linalg.norm(updated)) + if updated_norm > 0: + updated = updated / updated_norm + count += 1 + + ctx.speaker_display_profiles[preferred_display_id] = np.asarray(updated, dtype=np.float32) + ctx.speaker_display_counts[preferred_display_id] = count + if cluster_index is not None: + ctx.speaker_display_map[int(cluster_index)] = preferred_display_id + return preferred_display_id + + @staticmethod + def _safe_int(value: Any, default: Optional[int] = None) -> Optional[int]: + try: + if value is None: + return default + return int(value) + except (TypeError, ValueError): + return default + + def _public_payload(self, payload: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: + if payload is None: + return None + return { + key: value + for key, value in payload.items() + if not str(key).startswith("_") + } + + def _is_stable_speaker_info( + self, + speaker_info: Optional[Dict[str, Any]], + ) -> bool: + if not speaker_info: + return False + if speaker_info.get("registry_speaker_id") or speaker_info.get("user_id"): + return True + strategy = str(speaker_info.get("speaker_strategy") or "") + if strategy == "registry_match": + return True + if strategy in {"recent_named_attach", "recent_named_inherit"}: + confidence = float(speaker_info.get("speaker_confidence") or 0.0) + return confidence >= 0.7 + if strategy == "embedding_match" and speaker_info.get("_matched_existing"): + confidence = float(speaker_info.get("speaker_confidence") or 0.0) + return confidence >= 0.62 + if strategy == "embedding_match": + confidence = float(speaker_info.get("speaker_confidence") or 0.0) + duration_ms = float(speaker_info.get("_segment_duration_ms") or 0.0) + if duration_ms >= 8000: + return confidence >= 0.62 + if strategy != "embedding_match": + return False + confidence = float(speaker_info.get("speaker_confidence") or 0.0) + threshold = max( + float(getattr(settings, "REALTIME_SPEAKER_CONFIRM_THRESHOLD", 0.62) or 0.62), + 0.68, + ) + return confidence >= threshold + + def _record_segment_speaker( + self, + ctx: ConnectionContext, + *, + segment_index: int, + segment_start_ms: int, + segment_end_ms: int, + speaker_info: Optional[Dict[str, Any]], + ) -> None: + if not speaker_info: + return + embedding = speaker_info.get("_embedding") + if embedding is None: + return + ctx.speaker_records.append( + { + "index": segment_index, + "start_ms": int(segment_start_ms), + "end_ms": int(segment_end_ms), + "embedding": np.asarray(embedding, dtype=np.float32), + "embeddings": [ + np.asarray(item, dtype=np.float32) + for item in (speaker_info.get("_chunk_embeddings") or [embedding]) + ], + "chunks": [ + { + "start_ms": int(item.get("start_ms", segment_start_ms)), + "end_ms": int(item.get("end_ms", segment_end_ms)), + "embedding": np.asarray(item["embedding"], dtype=np.float32), + } + for item in (speaker_info.get("_chunks") or []) + if item.get("embedding") is not None + ], + "speaker_id": speaker_info.get("speaker_id"), + "cluster_index": speaker_info.get("_cluster_index"), + "speaker_name": speaker_info.get("speaker_name"), + "registry_speaker_id": speaker_info.get("registry_speaker_id"), + "user_id": speaker_info.get("user_id"), + } + ) + if len(ctx.speaker_records) > self._MAX_SPEAKER_HISTORY_RECORDS: + ctx.speaker_records = ctx.speaker_records[-self._MAX_SPEAKER_HISTORY_RECORDS:] + + def _inherit_recent_anonymous_speaker( + self, + ctx: ConnectionContext, + speaker_info: Dict[str, Any], + ) -> Optional[Dict[str, Any]]: + if speaker_info.get("registry_speaker_id") or speaker_info.get("user_id"): + return None + if speaker_info.get("_matched_existing"): + return None + duration_ms = int(speaker_info.get("_segment_duration_ms") or 0) + if duration_ms < 8000: + return None + if not ctx.speaker_records: + return None + last_record = ctx.speaker_records[-1] + last_speaker_id = str(last_record.get("speaker_id") or "").strip() + if not re.fullmatch(r"Speaker\d+", last_speaker_id): + return None + inherited = dict(speaker_info) + inherited["speaker_id"] = last_speaker_id + inherited["speaker_name"] = str(last_record.get("speaker_name") or last_speaker_id) + inherited["_cluster_index"] = last_record.get("cluster_index") + inherited["_matched_existing"] = True + inherited["speaker_confidence"] = max(float(inherited.get("speaker_confidence") or 0.0), 0.66) + inherited["speaker_strategy"] = "recent_inherit" + return inherited + + def _inherit_recent_named_speaker( + self, + ctx: ConnectionContext, + speaker_info: Dict[str, Any], + ) -> Optional[Dict[str, Any]]: + if speaker_info.get("registry_speaker_id") or speaker_info.get("user_id"): + return speaker_info + if speaker_info.get("_matched_existing"): + return None + duration_ms = int(speaker_info.get("_segment_duration_ms") or 0) + if duration_ms < 4500: + return None + if not ctx.speaker_records: + return None + last_record = ctx.speaker_records[-1] + if not last_record.get("registry_speaker_id") and not last_record.get("user_id"): + return None + inherited = dict(speaker_info) + inherited["speaker_id"] = last_record.get("registry_speaker_id") or last_record.get("speaker_id") + inherited["speaker_name"] = last_record.get("speaker_name") or inherited["speaker_id"] + inherited["user_id"] = last_record.get("user_id") + inherited["registry_speaker_id"] = last_record.get("registry_speaker_id") + inherited["_cluster_index"] = last_record.get("cluster_index") + inherited["_matched_existing"] = True + inherited["speaker_confidence"] = max(float(inherited.get("speaker_confidence") or 0.0), 0.72) + inherited["speaker_strategy"] = "recent_named_inherit" + return inherited + + def _speaker_match_records( + self, + ctx: ConnectionContext, + ) -> List[Dict[str, Any]]: + if len(ctx.speaker_records) <= self._MAX_SPEAKER_MATCH_CONTEXT: + return ctx.speaker_records + return ctx.speaker_records[-self._MAX_SPEAKER_MATCH_CONTEXT:] + + def _recluster_confirmed_segments( + self, + ctx: ConnectionContext, + *, + max_records: Optional[int] = None, + only_stable_updates: bool = False, + only_pending_updates: bool = False, + ) -> List[Dict[str, Any]]: + if len(ctx.speaker_records) < 2: + return [] + + record_limit = max_records or self._MAX_SPEAKER_FINAL_RECLUSTER + records = ( + ctx.speaker_records + if len(ctx.speaker_records) <= record_limit + else ctx.speaker_records[-record_limit:] + ) + clusters, assignments = self._cluster_speaker_records(records) + if not clusters: + return [] + + cluster_labels: dict[int, Dict[str, Any]] = {} + unknown_display_index = 1 + for idx, cluster in enumerate(clusters): + named_records = [ + item for item in cluster["record_refs"] + if item.get("registry_speaker_id") + or item.get("user_id") + or ( + item.get("speaker_name") + and not re.fullmatch(r"Speaker\d+", str(item.get("speaker_name")).strip()) + ) + ] + if named_records: + preferred = max( + named_records, + key=lambda item: ( + 1 if item.get("registry_speaker_id") else 0, + 1 if item.get("user_id") else 0, + 1 if item.get("speaker_name") else 0, + ), + ) + speaker_id = preferred.get("registry_speaker_id") or preferred.get("speaker_id") + speaker_name = preferred.get("speaker_name") or speaker_id or f"Speaker{unknown_display_index:02d}" + cluster_labels[idx] = { + "speaker_id": speaker_id or f"Speaker{unknown_display_index:02d}", + "speaker_name": speaker_name, + "user_id": preferred.get("user_id"), + "registry_speaker_id": preferred.get("registry_speaker_id"), + "speaker_strategy": "final_recluster", + "speaker_confidence": 1.0, + } + else: + speaker_id = f"Speaker{unknown_display_index:02d}" + cluster_labels[idx] = { + "speaker_id": speaker_id, + "speaker_name": speaker_id, + "user_id": None, + "speaker_strategy": "final_recluster", + "speaker_confidence": 1.0, + } + unknown_display_index += 1 + + updates: List[Dict[str, Any]] = [] + for record, cluster_idx in zip(records, assignments): + segment_index = int(record["index"]) + segment = next( + (item for item in ctx.confirmed_segments if int(item.get("index", -1)) == segment_index), + None, + ) + if segment is None: + continue + if only_pending_updates and not segment.get("speaker_pending"): + continue + new_info = cluster_labels[cluster_idx] + if only_stable_updates: + has_named_identity = bool(new_info.get("registry_speaker_id") or new_info.get("user_id")) + generic_name = str(new_info.get("speaker_name") or "").strip() + if not has_named_identity and re.fullmatch(r"Speaker\d+", generic_name): + if not segment.get("speaker_pending"): + continue + changed = any(segment.get(key) != value for key, value in new_info.items()) + if not changed and not segment.get("speaker_pending"): + continue + segment.update(new_info) + segment.pop("speaker_pending", None) + updates.append( + { + "segment_index": segment_index, + "segment": self._public_payload(segment), + } + ) + + return updates + + def _maybe_recluster_recent_segments( + self, + ctx: ConnectionContext, + ) -> List[Dict[str, Any]]: + if len(ctx.speaker_records) < self._MIN_REALTIME_RECLUSTER_SEGMENTS: + return [] + return self._recluster_confirmed_segments( + ctx, + max_records=self._MAX_SPEAKER_REALTIME_RECLUSTER, + only_stable_updates=True, + only_pending_updates=True, + ) + + def _build_sv_chunks(self, audio: np.ndarray) -> List[np.ndarray]: + return get_realtime_speaker_clusterer().build_sv_chunks(audio) + + async def _extract_chunk_embeddings( + self, + audio: np.ndarray, + ) -> List[np.ndarray]: + return await get_realtime_speaker_clusterer().extract_chunk_embeddings(audio) + + def _cluster_speaker_records( + self, + records: List[Dict[str, Any]], + ) -> tuple[List[Dict[str, Any]], List[int]]: + return get_realtime_speaker_clusterer().cluster_records(records) + + async def _ensure_engine(self, ctx: ConnectionContext) -> Qwen3ASREngine: + if ctx.engine is not None: + return ctx.engine + + runtime_router = get_runtime_router() + model = validate_realtime_model_id("qwen3-asr") + logger.info("Using Qwen3-ASR model: %s", model) + + ctx.engine_lease = await runtime_router.acquire_engine(model) + engine = ctx.engine_lease.engine + if not isinstance(engine, Qwen3ASREngine): + raise RuntimeError("Current model is not Qwen3-ASR") + if not engine.supports_realtime: + raise RuntimeError( + f"Current device {engine.device} does not support Qwen3-ASR realtime streaming; " + "only CUDA vLLM and CPU Rust paths are supported" + ) + + ctx.engine = engine + return engine + + def _has_voice(self, audio: np.ndarray) -> bool: + if audio.size == 0: + return False + rms = float(np.sqrt(np.mean(audio**2))) + peak = float(np.max(np.abs(audio))) + return rms >= 0.0045 or (rms >= 0.0025 and peak >= 0.08) + + def _get_dynamic_silence_threshold_samples(self, ctx: ConnectionContext) -> int: + silence_ms = max(int(ctx.params.get("silence_duration_ms", 800) or 800), 100) + return int(silence_ms * 16) + + def _normalize_output_text( + self, + text: str, + ctx: ConnectionContext, + *, + enable_itn: Optional[bool] = None, + ) -> str: + raw = str(text or "").strip() + if "" in raw: + _prefix, raw = raw.split("", 1) + raw = re.sub(r"^\s*language\s+[A-Za-z][A-Za-z\s-]*\s*", "", raw, flags=re.IGNORECASE) + if enable_itn is None: + enable_itn = bool(ctx.params.get("enable_inverse_text_normalization", True)) + return normalize_asr_text( + raw, + enable_itn=enable_itn, + ) + + def _normalize_output_language(self, language: Optional[str], ctx: ConnectionContext) -> str: + raw = str(language or "").strip() + if not raw or raw.lower() == "none": + return "" + + raw = re.sub(r"^[^\w]+", "", raw) + match = re.search(r"language\s+([A-Za-z][A-Za-z\s-]*)$", raw, re.IGNORECASE) + if match: + return match.group(1).strip() + return raw + + def _infer_text_language(self, text: str) -> str: + has_cjk = any("\u4e00" <= ch <= "\u9fff" for ch in text) + has_thai = any("\u0e00" <= ch <= "\u0e7f" for ch in text) + has_latin = any("LATIN" in unicodedata.name(ch, "") for ch in text if ch.isalpha()) + if has_thai: + return "Thai" + if has_cjk: + return "Chinese" + if has_latin: + return "English" + return "" + + def _get_session_dominant_language(self, ctx: ConnectionContext) -> str: + counts: Dict[str, int] = {} + for segment in ctx.confirmed_segments: + language = str(segment.get("language") or "").strip() + if not language: + continue + counts[language] = counts.get(language, 0) + 1 + if not counts: + return "" + language, count = max(counts.items(), key=lambda item: item[1]) + return language if count >= 2 else "" + + def _pre_roll_samples(self, ctx: ConnectionContext) -> int: + pre_roll_ms = max(int(ctx.params.get("pre_roll_ms", 240) or 0), 0) + return int(pre_roll_ms * 16) + + def _min_partial_samples(self, ctx: ConnectionContext) -> int: + return int( + max( + float( + ctx.params.get( + "min_partial_sec", + settings.REALTIME_MIN_PARTIAL_SEC, + ) + or settings.REALTIME_MIN_PARTIAL_SEC + ), + 0.25, + ) * 16000 + ) + + def _partial_emit_interval_samples(self, ctx: ConnectionContext) -> int: + """ + Control how often we surface partial text to the client. + + When native Qwen streaming is healthy we can emit much more frequently, + because decoding is already happening incrementally in memory. + If native streaming falls back to window retranscription, keep the + cadence a little slower to avoid excessive temp-file retranscribes. + """ + interval_sec = max( + float( + getattr( + settings, + "REALTIME_PARTIAL_EMIT_INTERVAL_SEC", + 0.25, + ) or 0.25 + ), + 0.12, + ) + if ctx.params.get("enable_native_partial_stream") is False: + interval_sec = max(interval_sec, max(self._stream_chunk_size_sec(ctx), 0.9)) + elif ctx.realtime_stream_state is None: + interval_sec = max(interval_sec, max(min(self._stream_chunk_size_sec(ctx), 0.8), 0.55)) + return int(interval_sec * 16000) + + def _stream_window_samples(self) -> int: + window_sec = max( + float(getattr(settings, "REALTIME_STREAM_WINDOW_SEC", 8.0) or 8.0), + 1.0, + ) + return int(window_sec * 16000) + + def _partial_window_samples(self, ctx: ConnectionContext) -> int: + window_sec = max( + float( + ctx.params.get( + "partial_window_sec", + getattr(settings, "REALTIME_PARTIAL_WINDOW_SEC", 8.0), + ) + or getattr(settings, "REALTIME_PARTIAL_WINDOW_SEC", 8.0) + ), + 1.0, + ) + return int(window_sec * 16000) + + def _max_sentence_count(self, ctx: ConnectionContext) -> int: + return max(int(ctx.params.get("max_sentence_count", 8) or 8), 1) + + def _max_partial_text_chars(self, ctx: ConnectionContext) -> int: + return max( + int( + ctx.params.get( + "max_partial_text_chars", + ctx.DEFAULT_MAX_PARTIAL_TEXT_CHARS, + ) + or ctx.DEFAULT_MAX_PARTIAL_TEXT_CHARS + ), + 200, + ) + + def _max_segment_samples(self, ctx: ConnectionContext) -> int: + configured_sec = float( + ctx.params.get( + "hard_limit_sec", + ctx.params.get("max_segment_sec", settings.REALTIME_FINAL_SEGMENT_HARD_LIMIT_SEC), + ) + or 0.0 + ) + if configured_sec <= 0: + return 0 + return int(max(configured_sec, self._MIN_HARD_SEGMENT_SEC, 1.0) * 16000) + + def _soft_segment_limit_samples(self, ctx: ConnectionContext) -> int: + limit_sec = float( + ctx.params.get( + "soft_limit_sec", + getattr(settings, "REALTIME_FINAL_SEGMENT_SOFT_LIMIT_SEC", 8.0), + ) + or 0.0 + ) + if limit_sec <= 0: + return 0 + return int(max(limit_sec, 1.0) * 16000) + + def _clip_partial_text( + self, + text: str, + ctx: ConnectionContext, + ) -> str: + limit = self._max_partial_text_chars(ctx) + if len(text) <= limit: + return text + return text[-limit:] + + def _enable_realtime_vad_split(self, ctx: ConnectionContext) -> bool: + return bool(ctx.params.get("enable_realtime_vad_split", False)) + + def _enable_realtime_longform(self, ctx: ConnectionContext) -> bool: + return bool(ctx.params.get("enable_realtime_longform", False)) + + def _enable_realtime_refine(self, ctx: ConnectionContext) -> bool: + return False + + @staticmethod + def _sentence_count(text: str) -> int: + if not text: + return 0 + return len(re.findall(r"[。!??!]", text)) + + @staticmethod + def _ends_with_sentence_punctuation(text: str) -> bool: + stripped = str(text or "").rstrip() + return bool(stripped) and stripped[-1] in "。!??!" + + def _last_sentence_body_len(self, text: str) -> int: + stripped = str(text or "").rstrip() + if not stripped: + return 0 + if self._ends_with_sentence_punctuation(stripped): + stripped = stripped[:-1].rstrip() + last_boundary = max(stripped.rfind(ch) for ch in "。!??!") + body = stripped[last_boundary + 1:] if last_boundary >= 0 else stripped + return sum(1 for ch in body if ch.isalnum() or "\u4e00" <= ch <= "\u9fff") + + def _should_commit_complete_sentence(self, ctx: ConnectionContext, text: str) -> bool: + candidate = self._sanitize_candidate_text(text) + if not candidate or not self._ends_with_sentence_punctuation(candidate): + return False + + duration_sec = float(ctx.segment_audio_buffer.size) / 16000.0 + min_sec = max( + float( + ctx.params.get( + "complete_sentence_commit_sec", + max(float(ctx.params.get("force_stable_segment_sec", 8.0) or 8.0), 12.0), + ) + or 12.0 + ), + 6.0, + ) + if duration_sec < min_sec: + return False + + min_chars = max( + int( + ctx.params.get( + "complete_sentence_commit_min_chars", + max(int(ctx.params.get("force_stable_min_chars", 24) or 24), 48), + ) + or 48 + ), + 12, + ) + if len(candidate) < min_chars: + return False + + # Avoid committing on tiny unstable tails such as "可。" or "啊。". + return self._last_sentence_body_len(candidate) >= 6 + + def _stream_chunk_size_sec(self, ctx: ConnectionContext) -> float: + return max( + float( + ctx.params.get( + "chunk_size_sec", + settings.REALTIME_STREAM_CHUNK_SEC, + ) + or settings.REALTIME_STREAM_CHUNK_SEC + ), + 0.4, + ) + + def _stream_unfixed_chunk_num(self, ctx: ConnectionContext) -> int: + return max( + int( + ctx.params.get( + "unfixed_chunk_num", + settings.REALTIME_STREAM_MAX_PENDING_CHUNKS, + ) + or settings.REALTIME_STREAM_MAX_PENDING_CHUNKS + ), + 1, + ) + + def _stream_unfixed_token_num(self, ctx: ConnectionContext) -> int: + return max(int(ctx.params.get("unfixed_token_num", 5) or 5), 1) + + def _should_use_native_partial_stream( + self, + ctx: ConnectionContext, + engine: Qwen3ASREngine, + ) -> bool: + _ = engine + configured = ctx.params.get("enable_native_partial_stream") + if configured is not None: + return bool(configured) + # 默认使用整段周期重转写;仅在客户端明确要求时启用底层 native partial stream。 + return False + + async def _init_realtime_stream_state( + self, + ctx: ConnectionContext, + engine: Qwen3ASREngine, + ) -> None: + if not self._should_use_native_partial_stream(ctx, engine): + return + if ctx.realtime_stream_state is not None: + return + + try: + # Native stream 只用来拿低延迟 partial,最终句子仍走整段重转写定稿。 + ctx.realtime_stream_state = await run_sync( + engine.init_streaming_state, + ctx.params.get("context", ""), + ctx.params.get("language"), + chunk_size_sec=self._stream_chunk_size_sec(ctx), + unfixed_chunk_num=self._stream_unfixed_chunk_num(ctx), + unfixed_token_num=self._stream_unfixed_token_num(ctx), + max_new_tokens=48, + ) + except Exception as exc: + logger.debug("Init realtime partial stream failed: %s", exc) + ctx.realtime_stream_state = None + + async def _push_realtime_stream_audio( + self, + ctx: ConnectionContext, + engine: Qwen3ASREngine, + audio: np.ndarray, + ) -> None: + if audio.size == 0: + return + if not self._should_use_native_partial_stream(ctx, engine): + return + + if ctx.realtime_stream_state is None: + await self._init_realtime_stream_state(ctx, engine) + if ctx.realtime_stream_state is None: + return + + try: + ctx.realtime_stream_state = await run_sync( + engine.streaming_transcribe, + audio, + ctx.realtime_stream_state, + ) + except Exception as exc: + logger.debug("Realtime partial stream push failed: %s", exc) + ctx.realtime_stream_state = None + + async def _finish_realtime_stream_text( + self, + ctx: ConnectionContext, + engine: Qwen3ASREngine, + ) -> tuple[str, str]: + if not self._should_use_native_partial_stream(ctx, engine): + return "", "" + if ctx.realtime_stream_state is None: + return "", "" + + try: + ctx.realtime_stream_state = await run_sync( + engine.finish_streaming_transcribe, + ctx.realtime_stream_state, + ) + text = self._normalize_output_text( + str(ctx.realtime_stream_state.last_text or ""), + ctx, + enable_itn=False, + ) + language = self._normalize_output_language( + getattr(ctx.realtime_stream_state, "last_language", ""), + ctx, + ) + return text, language + except Exception as exc: + logger.debug("Finalize realtime partial stream failed: %s", exc) + return "", "" + finally: + ctx.realtime_stream_state = None + + async def _decode_turn_partial_text( + self, + engine: Qwen3ASREngine, + ctx: ConnectionContext, + ) -> tuple[str, str]: + if self._should_use_native_partial_stream(ctx, engine) and ctx.realtime_stream_state is not None: + stream_text = self._normalize_output_text( + str(ctx.realtime_stream_state.last_text or ""), + ctx, + enable_itn=False, + ) + stream_language = self._normalize_output_language( + getattr(ctx.realtime_stream_state, "last_language", ""), + ctx, + ) + if stream_text.strip(): + return stream_text, stream_language + + # 流式文本为空时回退到当前音频窗口重转写。 + partial_audio = np.asarray( + ctx.stream_window_buffer if ctx.stream_window_buffer.size > 0 else ctx.segment_audio_buffer, + dtype=np.float32, + ) + fallback = await self._transcribe_audio_text( + engine, + ctx, + partial_audio, + ) + return fallback, "" + + def _append_pre_roll(self, ctx: ConnectionContext, audio: np.ndarray) -> None: + if audio.size == 0: + return + ctx.pre_roll_audio = np.concatenate([ctx.pre_roll_audio, audio]) + max_samples = self._pre_roll_samples(ctx) + if max_samples <= 0: + ctx.pre_roll_audio = np.array([], dtype=np.float32) + elif ctx.pre_roll_audio.size > max_samples: + ctx.pre_roll_audio = ctx.pre_roll_audio[-max_samples:] + + def _start_turn(self, ctx: ConnectionContext, audio: np.ndarray) -> None: + parts: List[np.ndarray] = [] + if ctx.pre_roll_audio.size > 0: + parts.append(np.asarray(ctx.pre_roll_audio, dtype=np.float32)) + parts.append(np.asarray(audio, dtype=np.float32)) + ctx.segment_audio_buffer = ( + np.concatenate(parts) if len(parts) > 1 else np.asarray(parts[0], dtype=np.float32) + ) + ctx.pre_roll_audio = np.array([], dtype=np.float32) + window_samples = self._stream_window_samples() + ctx.stream_window_buffer = np.asarray( + ctx.segment_audio_buffer[-window_samples:], + dtype=np.float32, + ) + partial_window_samples = self._partial_window_samples(ctx) + if ctx.stream_window_buffer.size > partial_window_samples: + ctx.stream_window_buffer = ctx.stream_window_buffer[-partial_window_samples:] + ctx.sentence_active = True + ctx.silence_samples = 0 + ctx.total_samples = int(ctx.segment_audio_buffer.size) + ctx.last_partial_decode_samples = 0 + self._reset_partial_state(ctx) + + def _append_turn_audio( + self, + ctx: ConnectionContext, + audio: np.ndarray, + *, + has_voice: bool, + ) -> None: + ctx.segment_audio_buffer = np.concatenate([ctx.segment_audio_buffer, audio]) + ctx.stream_window_buffer = np.concatenate([ctx.stream_window_buffer, audio]) + window_samples = self._partial_window_samples(ctx) + if ctx.stream_window_buffer.size > window_samples: + ctx.stream_window_buffer = ctx.stream_window_buffer[-window_samples:] + ctx.total_samples = int(ctx.segment_audio_buffer.size) + if has_voice: + ctx.silence_samples = 0 + else: + ctx.silence_samples += int(audio.size) + + def _should_decode_turn_partial(self, ctx: ConnectionContext) -> bool: + if not ctx.sentence_active: + return False + current_samples = int(ctx.segment_audio_buffer.size) + if current_samples < self._min_partial_samples(ctx): + return False + return (current_samples - ctx.last_partial_decode_samples) >= self._partial_emit_interval_samples(ctx) + + def _reset_partial_state(self, ctx: ConnectionContext) -> None: + ctx.last_partial_text = "" + ctx.last_partial_language = "" + ctx.last_partial_chunk_id = 0 + ctx.last_partial_raw_text = "" + ctx.last_partial_display_text = "" + ctx.stable_partial_prefix = "" + ctx.segment_observed_text = "" + ctx.segment_observed_language = "" + ctx.best_partial_text = "" + ctx.best_partial_language = "" + ctx.last_partial_decode_samples = 0 + ctx.partial_stable_rounds = 0 + ctx.realtime_stream_state = None + + def _update_partial_stability( + self, + ctx: ConnectionContext, + text: str, + ) -> int: + current = text.strip() + previous = ctx.last_partial_text.strip() + if current and previous and current == previous: + ctx.partial_stable_rounds += 1 + else: + ctx.partial_stable_rounds = 0 + return ctx.partial_stable_rounds + + def _prefer_segment_text( + self, + primary: str, + fallback: str, + ) -> str: + primary_text = primary.strip() + fallback_text = fallback.strip() + if not primary_text: + return fallback_text + if not fallback_text: + return primary_text + if primary_text == fallback_text: + return primary_text + if len(fallback_text) > len(primary_text) and ( + primary_text in fallback_text + or self._common_prefix_len(primary_text, fallback_text) >= min(len(primary_text), 8) + ): + return fallback_text + if len(primary_text) > len(fallback_text) and ( + fallback_text in primary_text + or self._common_prefix_len(primary_text, fallback_text) >= min(len(fallback_text), 8) + ): + return primary_text + return fallback_text if len(fallback_text) > len(primary_text) else primary_text + + def _merge_segment_text( + self, + stable: str, + candidate: str, + ) -> str: + stable_text = stable.strip() + candidate_text = candidate.strip() + if not stable_text: + return candidate_text + if not candidate_text: + return stable_text + if stable_text == candidate_text: + return stable_text + if stable_text in candidate_text: + return candidate_text + if candidate_text in stable_text: + return stable_text + + match = SequenceMatcher( + None, + stable_text, + candidate_text, + autojunk=False, + ).find_longest_match(0, len(stable_text), 0, len(candidate_text)) + if match.size >= min(6, len(stable_text), len(candidate_text)): + return stable_text[:match.a] + candidate_text[match.b:] + + return self._prefer_segment_text(stable_text, candidate_text) + + def _accumulate_segment_text( + self, + previous: str, + current: str, + ) -> str: + previous_text = previous.strip() + current_text = current.strip() + if not previous_text: + return current_text + if not current_text: + return previous_text + if previous_text == current_text: + return previous_text + if current_text.startswith(previous_text) or previous_text in current_text: + return current_text + if previous_text.startswith(current_text): + return previous_text + + common_len = self._common_prefix_len(previous_text, current_text) + if common_len >= min(len(previous_text), len(current_text), 8): + return current_text if len(current_text) >= len(previous_text) else previous_text + + return current_text if len(current_text) >= len(previous_text) else previous_text + + def _sequence_similarity(self, left: str, right: str) -> float: + left_text = left.strip() + right_text = right.strip() + if not left_text or not right_text: + return 0.0 + if left_text == right_text: + return 1.0 + return float( + SequenceMatcher( + None, + left_text, + right_text, + autojunk=False, + ).ratio() + ) + + @staticmethod + def _overlap_char_views(text: str) -> List[tuple[str, int]]: + views: List[tuple[str, int]] = [] + for idx, ch in enumerate(str(text or "")): + if ch.isalnum() or "\u4e00" <= ch <= "\u9fff": + views.append((ch.lower(), idx)) + return views + + def _previous_segment_overlap_cut_index( + self, + previous: str, + current: str, + ) -> int: + previous_text = str(previous or "").strip() + current_text = str(current or "").strip() + if not previous_text or not current_text: + return 0 + + exact_cut = _dedupe_overlap_word_boundary(previous_text, current_text) + if exact_cut == 0: + exact_cut = _dedupe_overlap_char_boundary(previous_text, current_text) + if exact_cut > 0: + return exact_cut + + prev_views = self._overlap_char_views(previous_text) + curr_views = self._overlap_char_views(current_text) + if len(prev_views) < 12 or len(curr_views) < 12: + return 0 + + lookback = min(140, len(prev_views)) + lookahead = min(180, len(curr_views)) + prev_tail = prev_views[-lookback:] + curr_prefix = curr_views[:lookahead] + prev_key = "".join(ch for ch, _ in prev_tail) + curr_key = "".join(ch for ch, _ in curr_prefix) + match = SequenceMatcher(None, prev_key, curr_key, autojunk=False).find_longest_match( + 0, + len(prev_key), + 0, + len(curr_key), + ) + if match.size < 12: + return 0 + + prev_suffix_gap = len(prev_key) - (match.a + match.size) + current_prefix_gap = match.b + if prev_suffix_gap > 4 or current_prefix_gap > 8: + return 0 + + matched_current_chars = match.b + match.size + if matched_current_chars >= len(curr_views): + return len(current_text) + return curr_views[matched_current_chars][1] + + def _trim_previous_segment_overlap( + self, + ctx: ConnectionContext, + text: str, + ) -> str: + current = str(text or "").strip() + if not current or not ctx.confirmed_segments: + return current + + previous = str(ctx.confirmed_segments[-1].get("text") or "").strip() + cut_index = self._previous_segment_overlap_cut_index(previous, current) + if cut_index <= 0: + return current + + trimmed = current[cut_index:].lstrip(" \t\r\n,,。.!!??;;::、") + if trimmed != current: + logger.debug( + "Trimmed realtime segment overlap: removed=%s remaining=%s", + cut_index, + len(trimmed), + ) + return trimmed + + def _is_unstable_expansion( + self, + stable: str, + candidate: str, + ) -> bool: + stable_text = stable.strip() + candidate_text = candidate.strip() + if len(stable_text) < 6 or len(candidate_text) <= len(stable_text): + return False + + common_prefix = self._common_prefix_len(stable_text, candidate_text) + similarity = self._sequence_similarity(stable_text, candidate_text) + grows_too_fast = len(candidate_text) >= len(stable_text) + max(20, len(stable_text) // 2) + loses_context = common_prefix < min(6, len(stable_text) // 2) + return grows_too_fast and loses_context and similarity < 0.45 + + def _is_degenerate_repetition(self, text: str) -> bool: + stripped = text.strip() + if len(stripped) < 16: + return False + + units = re.findall(r"[\u4e00-\u9fff]+|[A-Za-z0-9]+|[^\w\s]", stripped) + lexical_units = [unit for unit in units if re.search(r"[\u4e00-\u9fffA-Za-z0-9]", unit)] + if len(lexical_units) < 6: + return False + + counts: Dict[str, int] = {} + for unit in lexical_units: + counts[unit] = counts.get(unit, 0) + 1 + + dominant = max(counts.values()) if counts else 0 + unique = len(counts) + if dominant >= 6 and unique <= 2: + return True + if dominant / max(len(lexical_units), 1) >= 0.7 and len(lexical_units) >= 10: + return True + return False + + def _trim_repetitive_suffix(self, text: str) -> str: + trimmed = text.strip() + if len(trimmed) < 12: + return trimmed + + repeated_char = re.search(r"(.)\1{7,}$", trimmed) + if repeated_char: + start = repeated_char.start() + trimmed = (trimmed[:start] + repeated_char.group(1) * 2).strip() + + for unit_len in range(1, 7): + pattern = re.compile(rf"(.{{{unit_len}}})\1{{4,}}$") + match = pattern.search(trimmed) + if match: + start = match.start() + unit = match.group(1) + trimmed = (trimmed[:start] + unit * 2).strip() + break + + return trimmed + + def _sanitize_candidate_text(self, text: str) -> str: + candidate = text.strip() + if not candidate: + return "" + candidate = self._trim_repetitive_suffix(candidate) + candidate = deduplicate_asr_text(candidate) + if self._is_degenerate_repetition(candidate): + return "" + return candidate + + def _update_segment_observed_text( + self, + ctx: ConnectionContext, + text: str, + language: str, + ) -> str: + candidate = self._sanitize_candidate_text(text) + if not candidate: + return ctx.segment_observed_text + if self._is_unstable_expansion(ctx.segment_observed_text, candidate): + return ctx.segment_observed_text + + observed = self._accumulate_segment_text(ctx.segment_observed_text, candidate) + ctx.segment_observed_text = observed + if language: + ctx.segment_observed_language = language + return observed + + def _update_best_partial( + self, + ctx: ConnectionContext, + text: str, + language: str, + ) -> None: + candidate = self._sanitize_candidate_text(text) + if not candidate: + return + if self._is_unstable_expansion(ctx.best_partial_text, candidate): + return + chosen = self._prefer_segment_text(ctx.best_partial_text, candidate) + if chosen != ctx.best_partial_text: + ctx.best_partial_text = chosen + ctx.best_partial_language = language or ctx.best_partial_language + + def _common_prefix_len(self, left: str, right: str) -> int: + limit = min(len(left), len(right)) + idx = 0 + while idx < limit and left[idx] == right[idx]: + idx += 1 + return idx + + def _stable_prefix_cutoff(self, text: str, max_commit_len: int) -> int: + if max_commit_len <= 0: + return 0 + + candidate = text[:max_commit_len] + if not candidate: + return 0 + + for idx in range(len(candidate) - 1, -1, -1): + ch = candidate[idx] + if ch.isspace() or unicodedata.category(ch).startswith("P"): + return idx + 1 + + return len(candidate) + + def _stabilize_partial_text(self, ctx: ConnectionContext, raw_text: str) -> str: + text = self._sanitize_candidate_text(raw_text) + if not text: + ctx.last_partial_raw_text = "" + ctx.stable_partial_prefix = "" + return "" + + previous_raw = ctx.last_partial_raw_text + stable_prefix = ctx.stable_partial_prefix + previous_emitted = ctx.last_partial_text.strip() + if previous_raw: + common_len = self._common_prefix_len(previous_raw, text) + keep_tail_chars = max(int(settings.REALTIME_STREAM_STABLE_TAIL_CHARS), 2) + max_commit_len = max(0, common_len - keep_tail_chars) + stable_cutoff = self._stable_prefix_cutoff(text, max_commit_len) + if stable_cutoff - len(stable_prefix) >= max( + int(settings.REALTIME_STREAM_STABLE_MIN_GROW_CHARS), + 1, + ): + stable_prefix = text[:stable_cutoff] + + if stable_prefix and not text.startswith(stable_prefix): + stable_common = self._common_prefix_len(stable_prefix, text) + tolerated_divergence = max( + int(settings.REALTIME_STREAM_DIVERGENCE_TOLERANCE_CHARS), + 1, + ) + if len(stable_prefix) - stable_common >= tolerated_divergence and previous_emitted: + return previous_emitted + stable_prefix = text[:stable_common] + stable_prefix = text[: self._stable_prefix_cutoff(text, len(stable_prefix))] + + ctx.stable_partial_prefix = stable_prefix + ctx.last_partial_raw_text = text + unstable_suffix = text[len(stable_prefix):] + return f"{stable_prefix}{unstable_suffix}".strip() + + def _should_emit_partial( + self, + ctx: ConnectionContext, + text: str, + language: str, + ) -> bool: + stripped = self._sanitize_candidate_text(text) + if not stripped: + return False + if len(stripped) <= 1: + return False + if self._is_unstable_expansion(ctx.last_partial_display_text, stripped): + return False + + normalized_language = self._normalize_output_language(language, ctx) + if not normalized_language: + normalized_language = self._infer_text_language(stripped) + + if ( + stripped == ctx.last_partial_display_text + and normalized_language == ctx.last_partial_language + ): + return False + + dominant_language = self._get_session_dominant_language(ctx) + duration_sec = float(ctx.segment_audio_buffer.size) / 16000.0 + if len(stripped) <= 6 and self._is_suspicious_segment_text( + stripped, + language=normalized_language, + dominant_language=dominant_language, + duration_sec=max(duration_sec, 0.1), + explicit_language=ctx.params.get("language"), + ): + return False + + return True + + def _partial_display_text(self, ctx: ConnectionContext, text: str) -> str: + stripped = self._sanitize_candidate_text(text) + if not stripped: + return "" + + holdback_chars = max( + int( + getattr( + settings, + "REALTIME_PARTIAL_HOLDBACK_CHARS", + 0, + ) + ), + 0, + ) + holdback_chars = int( + max( + int(ctx.params.get("partial_holdback_chars", holdback_chars) or holdback_chars), + 0, + ) + ) + if holdback_chars <= 0: + return stripped + + if ctx.partial_stable_rounds >= 1: + return stripped + + if self._sentence_count(stripped) >= 1 and stripped[-1:] in "。!?.!?": + return stripped + + if len(stripped) <= max(holdback_chars + 2, 8): + return stripped + + stable_prefix = stripped[:-holdback_chars].rstrip() + return stable_prefix or stripped + + def _is_suspicious_segment_text( + self, + text: str, + *, + language: str, + dominant_language: str, + duration_sec: float, + explicit_language: Optional[str], + ) -> bool: + stripped = text.strip() + if not stripped: + return True + + chars = [ch for ch in stripped if not ch.isspace()] + if not chars: + return True + + digit_count = sum(ch.isdigit() for ch in chars) + alpha_count = sum(ch.isalpha() for ch in chars) + cjk_count = sum("\u4e00" <= ch <= "\u9fff" for ch in chars) + thai_count = sum("\u0e00" <= ch <= "\u0e7f" for ch in chars) + punct_count = sum(unicodedata.category(ch).startswith("P") for ch in chars) + total = len(chars) + + digit_ratio = digit_count / total + punct_ratio = punct_count / total + latin_alpha_count = max(alpha_count - thai_count, 0) + script_families = sum( + 1 + for present in ( + cjk_count > 0, + thai_count > 0, + latin_alpha_count > 0, + digit_count > 0, + ) + if present + ) + + if digit_count >= 3 and digit_ratio >= 0.3: + return True + if total <= 20 and script_families >= 3: + return True + if total <= 8 and punct_ratio >= 0.5: + return True + # Allow short pure-Latin utterances to pass during bilingual switching. + if ( + not explicit_language + and language == "English" + and latin_alpha_count >= max(total - punct_count - digit_count, 1) + and total >= 3 + ): + return False + if ( + not explicit_language + and dominant_language + and language + and language != dominant_language + and duration_sec <= 6.0 + ): + return True + return False + + def _is_valid_committed_segment_text(self, text: str, duration_sec: float) -> bool: + stripped = self._sanitize_candidate_text(text) + if len(stripped) >= 3: + return True + + lexical_count = sum( + 1 for ch in stripped + if ch.isalnum() or "\u4e00" <= ch <= "\u9fff" + ) + if lexical_count <= 0: + return False + + # Short acknowledgements such as "嗯。", "好。", "啊?" should still + # close the visible segment after silence; speaker attribution may + # attach to nearby speech, but the ASR turn itself is valid. + return duration_sec >= 0.25 + + async def _estimate_voiced_duration_ms(self, audio: np.ndarray) -> int: + if audio.size == 0: + return 0 + + vad_segments = await self._run_vad_segments(audio) + if vad_segments is None: + return int(len(audio) / 16) + return sum( + max(0, int(seg[1]) - int(seg[0])) + for seg in vad_segments + if len(seg) >= 2 + ) + + async def _run_vad_segments(self, audio: np.ndarray) -> Optional[list[list[int]]]: + if audio.size == 0: + return [] + + temp_path: Optional[str] = None + try: + temp_path = get_speaker_registry_service().save_audio_array_to_temp( + audio, + sample_rate=16000, + ) + + def _run_vad() -> list[list[int]]: + vad_model = get_global_vad_model(settings.DEVICE) + with get_vad_inference_lock(): + result = vad_model.generate(input=temp_path, cache={}) + return result[0].get("value", []) if result else [] + + return await run_sync(_run_vad) + except Exception as exc: + logger.debug("Realtime voiced-duration VAD failed: %s", exc) + return None + finally: + get_speaker_registry_service().cleanup_file(temp_path) + + async def _split_silence_audio_by_vad( + self, + audio: np.ndarray, + ) -> tuple[np.ndarray, np.ndarray]: + vad_segments = await self._run_vad_segments(audio) + if not vad_segments: + return audio, np.array([], dtype=np.float32) + + valid_segments = [ + (int(seg[0]), int(seg[1])) + for seg in vad_segments + if len(seg) >= 2 and int(seg[1]) > int(seg[0]) + ] + if not valid_segments: + return audio, np.array([], dtype=np.float32) + + finalized_end_ms = valid_segments[-1][1] + finalized_end_sample = min(audio.size, int(finalized_end_ms * 16)) + finalized_audio = np.asarray(audio[:finalized_end_sample], dtype=np.float32) + return finalized_audio, np.array([], dtype=np.float32) + + async def _find_completed_segment_split_sample( + self, + audio: np.ndarray, + ) -> Optional[int]: + if audio.size < int(3.0 * 16000): + return None + + vad_segments = await self._run_vad_segments(audio) + if not vad_segments: + return None + + valid_segments = [ + (int(seg[0]), int(seg[1])) + for seg in vad_segments + if len(seg) >= 2 and int(seg[1]) > int(seg[0]) + ] + if len(valid_segments) < 2: + return None + + total_ms = int(audio.size / 16) + finalize_silence_ms = int(max(settings.REALTIME_VAD_FINALIZE_SILENCE_SEC, 0.2) * 1000) + + last_start_ms, last_end_ms = valid_segments[-1] + if total_ms - last_end_ms >= finalize_silence_ms: + return min(audio.size, last_end_ms * 16) + + # 接近 max_duration 时,优先把倒数第二个已完成语音段刷出去,保留最后一段继续等上下文。 + hard_limit_sec = max( + float(getattr(settings, "REALTIME_FINAL_SEGMENT_HARD_LIMIT_SEC", 12.0) or 12.0), + 1.0, + ) + near_limit_ms = int(max(hard_limit_sec * 0.85, 4.0) * 1000) + if total_ms >= near_limit_ms: + _, prev_end_ms = valid_segments[-2] + if prev_end_ms > 0: + return min(audio.size, prev_end_ms * 16) + + return None + + async def _transcribe_audio_text( + self, + engine: Qwen3ASREngine, + ctx: ConnectionContext, + audio: np.ndarray, + *, + final_pass: bool = False, + ) -> str: + temp_path: Optional[str] = None + try: + temp_path = get_speaker_registry_service().save_audio_array_to_temp( + audio, + sample_rate=16000, + ) + text = await run_sync( + engine.transcribe_file, + temp_path, + ctx.params.get("context", ""), + False, + False, + False, + 16000, + ) + normalized = self._normalize_output_text( + text or "", + ctx, + enable_itn=False, + ) + if final_pass and normalized.strip(): + return self._normalize_output_text( + normalized, + ctx, + enable_itn=bool(ctx.params.get("enable_inverse_text_normalization", True)), + ) + return normalized + except Exception as exc: + logger.warning("Realtime segment audio transcription failed: %s", exc) + return "" + finally: + get_speaker_registry_service().cleanup_file(temp_path) + + async def _transcribe_audio_longform_text( + self, + engine: Qwen3ASREngine, + ctx: ConnectionContext, + audio: np.ndarray, + *, + final_pass: bool = False, + ) -> str: + duration_sec = float(len(audio)) / 16000.0 + if duration_sec < settings.REALTIME_LONGFORM_MIN_SEC: + return await self._transcribe_audio_text( + engine, + ctx, + audio, + final_pass=final_pass, + ) + + chunk_samples = int(max(settings.REALTIME_LONGFORM_CHUNK_SEC, 1.0) * 16000) + overlap_samples = int(max(settings.REALTIME_LONGFORM_OVERLAP_SEC, 0.0) * 16000) + step_samples = max(int(16000), chunk_samples - overlap_samples) + + assembler = RealtimeTranscriptAssembler() + start = 0 + while start < audio.size: + end = min(audio.size, start + chunk_samples) + chunk_audio = np.asarray(audio[start:end], dtype=np.float32) + if chunk_audio.size == 0: + break + chunk_text = await self._transcribe_audio_text( + engine, + ctx, + chunk_audio, + final_pass=final_pass, + ) + assembler.push(chunk_text) + if end >= audio.size: + break + start += step_samples + + return assembler.text() + + def _split_max_duration_audio( + self, + audio: np.ndarray, + ) -> tuple[np.ndarray, np.ndarray]: + tail_samples = int(max(settings.REALTIME_MAX_SEGMENT_TAIL_SEC, 0.0) * 16000) + min_flush_samples = int(max(settings.REALTIME_SPEAKER_MIN_SEC, 1.0) * 16000) + if tail_samples <= 0 or audio.size <= tail_samples + min_flush_samples: + return audio, np.array([], dtype=np.float32) + flush_audio = np.asarray(audio[:-tail_samples], dtype=np.float32) + carry_audio = np.asarray(audio[-tail_samples:], dtype=np.float32) + return flush_audio, carry_audio + + def _should_force_stable_segment(self, ctx: ConnectionContext, text: str) -> bool: + force_sec = float( + max( + float( + ctx.params.get( + "force_stable_segment_sec", + getattr(settings, "REALTIME_FORCE_STABLE_SEGMENT_SEC", 0.0), + ) + or 0.0 + ), + 0.0, + ) + ) + if force_sec <= 0: + return False + + duration_sec = float(ctx.segment_audio_buffer.size) / 16000.0 + if duration_sec < force_sec: + return False + + partial_text = text.strip() + min_chars = max( + int( + ctx.params.get( + "force_stable_min_chars", + getattr(settings, "REALTIME_FORCE_STABLE_MIN_CHARS", 24), + ) + or 0 + ), + 8, + ) + if len(partial_text) < min_chars: + return False + + # 只因为前文某处已经出现过句号,就立刻截断整段,容易把后半句切坏。 + # 这里收紧条件:优先要求“当前尾部已经自然收句”。 + if self._ends_with_sentence_punctuation(partial_text): + return True + + # 如果句尾还没闭合,只在明显超时并且 partial 连续稳定时才软提交, + # 避免把“我们需要…”这类正在继续的从句过早切成两段。 + force_overtime_sec = max(force_sec + 2.0, force_sec * 1.35) + soft_limit_samples = self._soft_segment_limit_samples(ctx) + if soft_limit_samples > 0 and ctx.segment_audio_buffer.size >= soft_limit_samples: + force_overtime_sec = min( + force_overtime_sec, + float(ctx.segment_audio_buffer.size) / 16000.0, + ) + if duration_sec >= force_overtime_sec and ctx.partial_stable_rounds >= 1: + return True + + return ctx.partial_stable_rounds >= 2 + + def _choose_committed_segment_text( + self, + *, + final_text: str, + stable_text: str, + ctx: ConnectionContext, + duration_sec: float, + language: str, + ) -> str: + """Prefer the more plausible candidate between final retranscription and stable partial text.""" + _ = (ctx, language) + final_candidate = self._sanitize_candidate_text(final_text) + stable_candidate = self._sanitize_candidate_text(stable_text) + if not final_candidate: + return stable_candidate + if not stable_candidate: + return final_candidate + if final_candidate == stable_candidate: + return final_candidate + + suspicious_final = self._is_suspicious_segment_text( + final_candidate, + language=self._infer_text_language(final_candidate), + dominant_language=self._get_session_dominant_language(ctx), + duration_sec=max(duration_sec, 0.1), + explicit_language=ctx.params.get("language"), + ) + suspicious_stable = self._is_suspicious_segment_text( + stable_candidate, + language=self._infer_text_language(stable_candidate), + dominant_language=self._get_session_dominant_language(ctx), + duration_sec=max(duration_sec, 0.1), + explicit_language=ctx.params.get("language"), + ) + + if suspicious_final and not suspicious_stable: + return stable_candidate + if ( + len(stable_candidate) >= max(len(final_candidate) + 10, 24) + and final_candidate in stable_candidate + ): + return stable_candidate + if ( + len(final_candidate) >= max(len(stable_candidate) + 12, 28) + and stable_candidate in final_candidate + and not suspicious_final + ): + return final_candidate + if len(stable_candidate) > len(final_candidate) and not suspicious_stable: + return stable_candidate + return final_candidate + + async def _resolve_segment_speaker( + self, + ctx: ConnectionContext, + audio: np.ndarray, + *, + reason: str, + segment_start_ms: int, + segment_end_ms: int, + ) -> Optional[Dict[str, Any]]: + if not bool(ctx.params.get("enable_speaker", True)): + return None + speaker_info = await get_realtime_speaker_clusterer().resolve_segment_speaker( + self._speaker_match_records(ctx), + np.asarray(audio, dtype=np.float32), + segment_start_ms=segment_start_ms, + segment_end_ms=segment_end_ms, + enable_registry_match=bool(ctx.params.get("enable_speaker_identification", True)), + speaker_threshold=ctx.params.get("speaker_threshold"), + ) + if not speaker_info: + return None + speaker_info["_segment_duration_ms"] = int(max(segment_end_ms - segment_start_ms, 0)) + inherited_named_speaker = self._inherit_recent_named_speaker(ctx, speaker_info) + if inherited_named_speaker is not None: + speaker_info = inherited_named_speaker + inherited_speaker = self._inherit_recent_anonymous_speaker(ctx, speaker_info) + if inherited_speaker is not None: + speaker_info = inherited_speaker + + stable = self._is_stable_speaker_info(speaker_info) + speaker_info["speaker_pending"] = not stable + cluster_index = speaker_info.get("_cluster_index") + if ( + stable + and + not speaker_info.get("registry_speaker_id") + and not speaker_info.get("user_id") + ): + safe_cluster_index = self._safe_int(cluster_index) + display_id = self._match_display_speaker_id( + ctx, + cluster_index=safe_cluster_index, + embedding=speaker_info.get("_embedding"), + ) + if display_id: + speaker_info["speaker_id"] = display_id + speaker_info["speaker_name"] = display_id + + safe_cluster_index = self._safe_int(cluster_index) + if ( + stable + and safe_cluster_index is not None + and not speaker_info.get("registry_speaker_id") + and not speaker_info.get("user_id") + and speaker_info.get("speaker_id") + ): + ctx.speaker_display_map[safe_cluster_index] = str(speaker_info["speaker_id"]) + + if not stable: + speaker_info["speaker_id"] = -1 + speaker_info["speaker_name"] = "" + + return speaker_info + + async def _resolve_and_emit_segment_speaker( + self, + websocket: WebSocket, + ctx: ConnectionContext, + task_id: str, + *, + segment_index: int, + audio: np.ndarray, + reason: str, + segment_start_ms: int, + segment_end_ms: int, + ) -> None: + try: + speaker_info = await self._resolve_segment_speaker( + ctx, + audio, + reason=reason, + segment_start_ms=segment_start_ms, + segment_end_ms=segment_end_ms, + ) + if not speaker_info: + return + segment = next( + (item for item in ctx.confirmed_segments if int(item.get("index", -1)) == int(segment_index)), + None, + ) + if segment is None: + return + before_view = self._public_speaker_view(segment) + segment.update(speaker_info) + after_view = self._public_speaker_view(segment) + self._record_segment_speaker( + ctx, + segment_index=segment_index, + segment_start_ms=segment_start_ms, + segment_end_ms=segment_end_ms, + speaker_info=speaker_info, + ) + if before_view == after_view: + return + queue_backlog = ctx.speaker_job_queue.qsize() + updates: List[Dict[str, Any]] = [] + if queue_backlog <= 1: + updates = self._maybe_recluster_recent_segments(ctx) + if updates: + for updated in updates: + await self._emit_speaker_update( + websocket, + ctx, + task_id, + int(updated["segment_index"]), + ) + return + await self._emit_speaker_update(websocket, ctx, task_id, segment_index) + except Exception as exc: + logger.warning("[%s] Async speaker resolution failed for segment %s: %s", task_id, segment_index, exc) + + async def _refine_segment_text( + self, + engine: Qwen3ASREngine, + ctx: ConnectionContext, + fallback_text: str, + *, + reason: str, + force: bool = False, + audio: Optional[np.ndarray] = None, + ) -> str: + target_audio = np.asarray( + ctx.segment_audio_buffer if audio is None else audio, + dtype=np.float32, + ) + duration_sec = float(len(target_audio)) / 16000.0 + if ( + not force + and not bool( + ctx.params.get("enable_segment_refine", settings.REALTIME_ENABLE_SEGMENT_REFINE) + ) + ): + return fallback_text + if not force and duration_sec < 6.0 and reason != "max_duration": + return fallback_text + + temp_path: Optional[str] = None + try: + temp_path = get_speaker_registry_service().save_audio_array_to_temp( + target_audio, + sample_rate=16000, + ) + refined_text = await run_sync( + engine.transcribe_file, + temp_path, + ctx.params.get("context", ""), + False, + False, + False, + 16000, + ) + refined_text = self._sanitize_candidate_text( + self._normalize_output_text( + refined_text or "", + ctx, + enable_itn=bool(ctx.params.get("enable_inverse_text_normalization", True)), + ) + ) + if refined_text.strip(): + return refined_text + except Exception as exc: + logger.warning("Realtime segment refinement failed: %s", exc) + finally: + get_speaker_registry_service().cleanup_file(temp_path) + + return fallback_text + + async def _commit_retranscribe_turn( + self, + websocket: WebSocket, + ctx: ConnectionContext, + task_id: str, + reason: str, + *, + finalized_audio_override: Optional[np.ndarray] = None, + carry_audio_override: Optional[np.ndarray] = None, + emit_segment_start: bool = True, + ) -> None: + engine = await self._ensure_engine(ctx) + + current_audio = np.asarray(ctx.segment_audio_buffer, dtype=np.float32) + finalized_audio = current_audio + carry_audio = np.array([], dtype=np.float32) + + if finalized_audio_override is not None: + finalized_audio = np.asarray(finalized_audio_override, dtype=np.float32) + carry_audio = np.asarray( + np.array([], dtype=np.float32) if carry_audio_override is None else carry_audio_override, + dtype=np.float32, + ) + elif reason == "silence" and self._enable_realtime_vad_split(ctx): + finalized_audio, carry_audio = await self._split_silence_audio_by_vad(current_audio) + elif reason in {"max_duration", "long_speech"} and self._enable_realtime_vad_split(ctx): + finalized_audio, carry_audio = self._split_max_duration_audio(current_audio) + + if carry_audio.size == 0: + stream_text, stream_language = await self._finish_realtime_stream_text(ctx, engine) + if stream_text.strip(): + observed_text = self._update_segment_observed_text( + ctx, + stream_text, + stream_language or self._infer_text_language(stream_text), + ) + self._update_best_partial( + ctx, + observed_text, + stream_language or self._infer_text_language(observed_text), + ) + else: + ctx.realtime_stream_state = None + + if self._enable_realtime_longform(ctx): + segment_text = await self._transcribe_audio_longform_text( + engine, + ctx, + finalized_audio, + final_pass=True, + ) + else: + segment_text = await self._transcribe_audio_text( + engine, + ctx, + finalized_audio, + final_pass=True, + ) + segment_text = self._sanitize_candidate_text(segment_text) + if not segment_text.strip(): + segment_text = self._prefer_segment_text( + ctx.best_partial_text, + ctx.segment_observed_text, + ) + + segment_language = self._infer_text_language(segment_text) + if not segment_language and ctx.segment_observed_language: + segment_language = ctx.segment_observed_language + if not segment_language and ctx.best_partial_language: + segment_language = ctx.best_partial_language + + duration_sec = float(finalized_audio.size) / 16000.0 + dominant_language = self._get_session_dominant_language(ctx) + suspicious = self._is_suspicious_segment_text( + segment_text, + language=segment_language, + dominant_language=dominant_language, + duration_sec=max(duration_sec, 0.1), + explicit_language=ctx.params.get("language"), + ) + if suspicious and finalized_audio.size > 0 and self._enable_realtime_refine(ctx): + refined_text = await self._refine_segment_text( + engine, + ctx, + segment_text, + reason=reason, + force=True, + audio=finalized_audio, + ) + if refined_text.strip(): + segment_text = refined_text + inferred = self._infer_text_language(segment_text) + if inferred: + segment_language = inferred + + stable_text = self._prefer_segment_text( + ctx.best_partial_text, + ctx.segment_observed_text, + ) + + segment_text = self._choose_committed_segment_text( + final_text=segment_text, + stable_text=stable_text, + ctx=ctx, + duration_sec=duration_sec, + language=segment_language, + ) + segment_text = self._trim_previous_segment_overlap(ctx, segment_text) + + is_valid = self._is_valid_committed_segment_text(segment_text, duration_sec) + segment_duration_ms = int(finalized_audio.size / 16) + segment_start_ms = int(ctx.timeline_cursor_ms) + segment_end_ms = int(segment_start_ms + segment_duration_ms) + + if is_valid: + segment_payload = { + "index": ctx.segment_index, + "text": segment_text, + "language": segment_language, + "reason": reason, + "duration_ms": segment_duration_ms, + "start_ms": segment_start_ms, + "end_ms": segment_end_ms, + "sentence_type": 1, + "speaker_id": -1, + "speaker_name": "", + "user_id": None, + } + segment_payload["speaker_pending"] = True + ctx.confirmed_segments.append(segment_payload) + + full_text = "\n".join(segment["text"] for segment in ctx.confirmed_segments) + full_text_tail = ( + full_text[-self._SEGMENT_EVENT_TEXT_TAIL_CHARS:] + if len(full_text) > self._SEGMENT_EVENT_TEXT_TAIL_CHARS + else full_text + ) + sentence_payload = self._build_tencent_sentence( + segment_payload, + sentence_type=1, + ) + + await self._send_json_safe( + websocket, + ctx, + { + "type": "sentences", + "code": 0, + "voice_id": task_id, + "final": 0, + "result": { + "slice_type": 2, + "index": int(ctx.segment_index), + "voice_text_str": full_text_tail, + }, + "sentences": [sentence_payload], + }, + ) + self._ensure_speaker_worker(websocket, ctx, task_id) + ctx.speaker_job_queue.put_nowait( + { + "segment_index": int(ctx.segment_index), + "audio": np.asarray(finalized_audio, dtype=np.float32), + "reason": reason, + "segment_start_ms": segment_start_ms, + "segment_end_ms": segment_end_ms, + } + ) + ctx.segment_index += 1 + + ctx.timeline_cursor_ms = segment_end_ms + ctx.segment_audio_buffer = np.asarray(carry_audio, dtype=np.float32) + ctx.stream_window_buffer = np.asarray(carry_audio, dtype=np.float32) + ctx.sentence_active = bool(carry_audio.size > 0) + ctx.silence_samples = 0 + ctx.total_samples = int(carry_audio.size) + self._reset_partial_state(ctx) + + if ctx.sentence_active and carry_audio.size > 0: + await self._push_realtime_stream_audio( + ctx, + engine, + np.asarray(carry_audio, dtype=np.float32), + ) + + async def _send_error( + self, + websocket: WebSocket, + message: str, + task_id: str, + code: str = "DEFAULT_SERVER_ERROR", + ) -> None: + try: + error = create_error_response(error_code=code, message=message, task_id=task_id) + await websocket.send_json( + { + "type": "error", + "code": error.get("code", -1), + "message": error.get("message", message), + "voice_id": task_id, + } + ) + except Exception: + pass + + async def handle_connection(self, websocket: WebSocket, task_id: str) -> None: + await websocket.accept() + logger.info("[%s] Qwen3 websocket connected", task_id) + + ctx = ConnectionContext() + session_id = task_id + keep_for_resume = False + + try: + while True: + message = await websocket.receive() + + if "text" in message: + data = json.loads(message["text"]) + msg_type = data.get("type", "") + + if msg_type == "start": + if ctx.state != ConnectionState.READY: + await self._send_error(websocket, "识别已在进行中", task_id, "INVALID_STATE") + continue + + payload = data.get("payload", {}) + requested_session_id = str(payload.get("session_id") or task_id).strip() or task_id + resumed_ctx, resumed = await self._acquire_session(requested_session_id) + if resumed_ctx is not ctx: + ctx = resumed_ctx + session_id = requested_session_id + task_id = session_id + keep_for_resume = True + + if resumed and ctx.state != ConnectionState.READY: + await self._send_json_safe( + websocket, + ctx, + { + "type": "voice_id", + "voice_id": task_id, + "session_id": session_id, + } + ) + await self._send_json_safe( + websocket, + ctx, + { + "type": "start", + "session_id": session_id, + } + ) + logger.info("[%s] Recognition resumed", task_id) + continue + + match_speaker_registry = bool( + payload.get( + "match_speaker_registry", + payload.get("enable_speaker_identification", False), + ) + ) + ctx.params = { + "format": payload.get("format", "pcm"), + "sample_rate": payload.get("sample_rate", 16000), + "language": payload.get("language"), + "context": payload.get("context", ""), + "enable_inverse_text_normalization": payload.get( + "enable_inverse_text_normalization", + True, + ), + "chunk_size_sec": payload.get( + "chunk_size_sec", + settings.REALTIME_STREAM_CHUNK_SEC, + ), + "unfixed_chunk_num": payload.get( + "unfixed_chunk_num", + settings.REALTIME_STREAM_MAX_PENDING_CHUNKS, + ), + "unfixed_token_num": payload.get("unfixed_token_num", 5), + "silence_duration_ms": payload.get("silence_duration_ms", 800), + "min_partial_sec": payload.get( + "min_partial_sec", + 0.9, + ), + "partial_window_sec": payload.get( + "partial_window_sec", + settings.REALTIME_PARTIAL_WINDOW_SEC, + ), + "pre_roll_ms": payload.get("pre_roll_ms", 240), + "max_sentence_count": payload.get("max_sentence_count", 8), + "max_partial_text_chars": payload.get( + "max_partial_text_chars", + ctx.DEFAULT_MAX_PARTIAL_TEXT_CHARS, + ), + "partial_holdback_chars": payload.get( + "partial_holdback_chars", + settings.REALTIME_PARTIAL_HOLDBACK_CHARS, + ), + "enable_native_partial_stream": payload.get( + "enable_native_partial_stream", + False, + ), + # 与离线会议接口保持一致:前端优先传 enable_speaker / match_speaker_registry。 + "enable_speaker": payload.get("enable_speaker", True), + "match_speaker_registry": match_speaker_registry, + "enable_speaker_identification": match_speaker_registry, + "speaker_threshold": payload.get("speaker_threshold"), + "enable_segment_refine": payload.get( + "enable_segment_refine", + settings.REALTIME_ENABLE_SEGMENT_REFINE, + ), + "enable_realtime_longform": payload.get("enable_realtime_longform", False), + "enable_realtime_vad_split": payload.get("enable_realtime_vad_split", False), + "force_stable_segment_sec": payload.get( + "force_stable_segment_sec", + settings.REALTIME_FORCE_STABLE_SEGMENT_SEC, + ), + "force_stable_min_chars": payload.get( + "force_stable_min_chars", + settings.REALTIME_FORCE_STABLE_MIN_CHARS, + ), + "soft_limit_sec": payload.get( + "soft_limit_sec", + settings.REALTIME_FINAL_SEGMENT_SOFT_LIMIT_SEC, + ), + "hard_limit_sec": payload.get( + "hard_limit_sec", + payload.get( + "max_segment_sec", + settings.REALTIME_FINAL_SEGMENT_HARD_LIMIT_SEC, + ), + ), + } + + await self._ensure_engine(ctx) + ctx.stream_window_buffer = np.array([], dtype=np.float32) + self._reset_partial_state(ctx) + + await self._send_json_safe( + websocket, + ctx, + { + "type": "voice_id", + "voice_id": task_id, + "session_id": session_id, + } + ) + await self._send_json_safe( + websocket, + ctx, + { + "type": "start", + "session_id": session_id, + }, + ) + ctx.state = ConnectionState.STARTED + logger.info("[%s] Recognition started: %s", task_id, ctx.params) + + elif msg_type == "stop": + if ctx.state in (ConnectionState.STARTED, ConnectionState.STREAMING): + await self._stop(websocket, ctx, task_id) + keep_for_resume = False + break + + else: + await self._send_error( + websocket, + f"未知消息类型: {msg_type}", + task_id, + "INVALID_MESSAGE", + ) + + elif "bytes" in message: + if ctx.state not in (ConnectionState.STARTED, ConnectionState.STREAMING): + await self._send_error(websocket, "请先发送 start", task_id, "INVALID_STATE") + continue + + audio = _convert_audio( + message["bytes"], + ctx.params["format"], + ctx.params["sample_rate"], + ) + if audio is None: + continue + + has_voice = self._has_voice(audio) + + if not ctx.sentence_active: + if not has_voice: + self._append_pre_roll(ctx, audio) + continue + self._start_turn(ctx, audio) + engine = await self._ensure_engine(ctx) + await self._push_realtime_stream_audio( + ctx, + engine, + np.asarray(ctx.segment_audio_buffer, dtype=np.float32), + ) + else: + self._append_turn_audio(ctx, audio, has_voice=has_voice) + engine = await self._ensure_engine(ctx) + await self._push_realtime_stream_audio(ctx, engine, audio) + + if not ctx.sentence_active: + continue + + if self._should_decode_turn_partial(ctx): + current, current_language = await self._decode_turn_partial_text( + engine, + ctx, + ) + ctx.last_partial_decode_samples = int(ctx.segment_audio_buffer.size) + current = self._sanitize_candidate_text(current) + if current and not self._is_degenerate_repetition(current): + current_language = current_language or self._infer_text_language(current) + observed = self._update_segment_observed_text( + ctx, + current, + current_language, + ) + visible_observed = self._trim_previous_segment_overlap(ctx, observed) + partial_display = self._clip_partial_text(visible_observed, ctx) + if ( + partial_display + and ( + partial_display != ctx.last_partial_display_text + or current_language != ctx.last_partial_language + ) + ): + ctx.last_partial_chunk_id += 1 + ctx.last_partial_text = visible_observed.strip() + ctx.last_partial_display_text = partial_display.strip() + ctx.last_partial_language = current_language + self._update_best_partial(ctx, visible_observed, current_language) + + sentence_payload = self._build_tencent_sentence( + { + "index": ctx.segment_index, + "start_ms": int(ctx.timeline_cursor_ms), + "end_ms": int(ctx.timeline_cursor_ms + (ctx.segment_audio_buffer.size / 16)), + "text": partial_display, + "speaker_id": -1, + "sentence_type": 0, + }, + sentence_type=0, + sentence_text=partial_display, + ) + await self._send_json_safe( + websocket, + ctx, + { + "type": "sentences", + "code": 0, + "voice_id": task_id, + "final": 0, + "result": { + "slice_type": 1, + "index": int(ctx.segment_index), + "voice_text_str": partial_display, + }, + "sentences": [sentence_payload], + }, + ) + ctx.state = ConnectionState.STREAMING + + if self._sentence_count(visible_observed) >= self._max_sentence_count(ctx): + await self._commit_retranscribe_turn( + websocket, + ctx, + task_id, + "sentence_limit", + ) + continue + + if self._should_commit_complete_sentence(ctx, visible_observed): + await self._commit_retranscribe_turn( + websocket, + ctx, + task_id, + "complete_sentence", + ) + continue + + silence_threshold = self._get_dynamic_silence_threshold_samples(ctx) + if ctx.silence_samples >= silence_threshold: + await self._commit_retranscribe_turn( + websocket, + ctx, + task_id, + "silence", + ) + ctx.state = ConnectionState.STREAMING + continue + + max_segment_samples = self._max_segment_samples(ctx) + if max_segment_samples > 0 and ctx.segment_audio_buffer.size >= max_segment_samples: + if not self._enable_realtime_vad_split(ctx): + await self._commit_retranscribe_turn( + websocket, + ctx, + task_id, + "max_duration", + ) + ctx.state = ConnectionState.STREAMING + continue + + split_sample = await self._find_completed_segment_split_sample( + ctx.segment_audio_buffer, + ) + if split_sample is not None and 0 < split_sample < ctx.segment_audio_buffer.size: + finalized_audio = np.asarray( + ctx.segment_audio_buffer[:split_sample], + dtype=np.float32, + ) + carry_audio = np.asarray( + ctx.segment_audio_buffer[split_sample:], + dtype=np.float32, + ) + await self._commit_retranscribe_turn( + websocket, + ctx, + task_id, + "long_speech", + finalized_audio_override=finalized_audio, + carry_audio_override=carry_audio, + ) + else: + await self._commit_retranscribe_turn( + websocket, + ctx, + task_id, + "max_duration", + ) + ctx.state = ConnectionState.STREAMING + continue + + except WebSocketDisconnect: + logger.info("[%s] WebSocket disconnected", task_id) + except Exception as exc: + logger.error("[%s] Connection error: %s", task_id, exc) + await self._send_error(websocket, str(exc), task_id) + finally: + await self._detach_session(session_id, keep_for_resume=keep_for_resume) + logger.info("[%s] Connection closed", task_id) + + async def _stop( + self, + websocket: WebSocket, + ctx: ConnectionContext, + task_id: str, + ) -> None: + try: + if ctx.sentence_active and ctx.segment_audio_buffer.size > 0: + await self._commit_retranscribe_turn( + websocket, + ctx, + task_id, + "final", + emit_segment_start=False, + ) + + final_updates = self._recluster_confirmed_segments(ctx) + for updated in final_updates: + sentence_payload = self._build_tencent_sentence( + updated["segment"], + sentence_type=1, + ) + await websocket.send_json( + { + "type": "sentences", + "code": 0, + "voice_id": task_id, + "final": 0, + "result": { + "slice_type": 2, + "index": int(updated["segment_index"]), + "voice_text_str": str(updated["segment"].get("text") or ""), + }, + "sentences": [sentence_payload], + } + ) + + all_texts = [ + segment["text"] + for segment in ctx.confirmed_segments + if segment["text"].strip() + ] + full_text = "\n".join(all_texts) + public_segments = [ + self._public_payload(segment) or {} + for segment in ctx.confirmed_segments + ] + + await websocket.send_json( + { + "type": "end", + "code": 0, + "message": "", + "voice_id": task_id, + "session_id": task_id, + "final": 1, + "result": { + "slice_type": 2, + "index": max(len(ctx.confirmed_segments) - 1, 0), + "voice_text_str": full_text, + }, + "sentences": [ + self._build_tencent_sentence(segment, sentence_type=1) + for segment in public_segments + ], + } + ) + + logger.info("[%s] Recognition completed, segments=%s", task_id, len(ctx.confirmed_segments)) + + except Exception as exc: + logger.error("[%s] Stop failed: %s", task_id, exc) + await self._send_error(websocket, f"结束识别失败: {exc}", task_id) diff --git a/app/services/realtime_speaker_clusterer.py b/app/services/realtime_speaker_clusterer.py new file mode 100644 index 0000000..25bbae6 --- /dev/null +++ b/app/services/realtime_speaker_clusterer.py @@ -0,0 +1,640 @@ +# -*- coding: utf-8 -*- +"""FunASR-style realtime speaker chunking and clustering.""" + +from __future__ import annotations + +import logging +import re +from collections import defaultdict +from typing import Any, Dict, List, Optional, Tuple + +import numpy as np +import scipy.linalg +import sklearn.metrics.pairwise +from sklearn.cluster._kmeans import k_means + +from app.core.config import settings +from app.core.executor import run_sync +from app.services.speaker_registry import get_speaker_registry_service + +logger = logging.getLogger(__name__) + + +class RealtimeSpeakerClusterer: + """Sentence-level speaker attribution using chunk embeddings and session clustering.""" + + FUNASR_SMOOTH_MINDUR_SEC = 0.7 + _FAST_ATTACH_MAX_SEC = 1.6 + _REGISTRY_MATCH_MIN_SEC = 2.4 + + def _is_generic_speaker_name(self, value: Any) -> bool: + text = str(value or "").strip() + return bool(re.fullmatch(r"Speaker\d+", text)) + + def _has_named_identity(self, record: Optional[Dict[str, Any]]) -> bool: + if not record: + return False + return bool( + record.get("registry_speaker_id") + or record.get("user_id") + or ( + record.get("speaker_name") + and not self._is_generic_speaker_name(record.get("speaker_name")) + ) + ) + + def _normalize_embedding(self, embedding: np.ndarray) -> np.ndarray: + emb = np.asarray(embedding, dtype=np.float32).reshape(-1) + norm = max(float(np.linalg.norm(emb)), 1e-12) + return (emb / norm).astype(np.float32) + + @staticmethod + def _coerce_cluster_index(value: Any, fallback: int) -> int: + try: + if value is None: + return int(fallback) + return int(value) + except (TypeError, ValueError): + return int(fallback) + + def _match_existing_speaker( + self, + speaker_records: List[Dict[str, Any]], + mean_embedding: np.ndarray, + ) -> Optional[Dict[str, Any]]: + if not speaker_records: + return None + + best_record: Optional[Dict[str, Any]] = None + best_score = -1.0 + for record in speaker_records: + record_embedding = record.get("embedding") + if record_embedding is None: + continue + score = float(np.dot(self._normalize_embedding(record_embedding), mean_embedding)) + if score > best_score: + best_score = score + best_record = record + + if best_record is None: + return None + + base_threshold = max( + float(getattr(settings, "REALTIME_UNKNOWN_SPK_CLUSTER_THRESHOLD", 0.58) or 0.58), + 0.3, + ) + has_named_identity = bool( + best_record.get("registry_speaker_id") + or best_record.get("user_id") + or ( + best_record.get("speaker_name") + and not self._is_generic_speaker_name(best_record.get("speaker_name")) + ) + ) + threshold = max(base_threshold, 0.62 if has_named_identity else 0.72) + if best_score < threshold: + return None + + matched = dict(best_record) + matched["_match_score"] = best_score + return matched + + def _next_generic_speaker_id( + self, + speaker_records: List[Dict[str, Any]], + ) -> str: + seen: set[int] = set() + for record in speaker_records: + for value in ( + record.get("speaker_id"), + record.get("speaker_name"), + ): + text = str(value or "").strip() + match = re.fullmatch(r"Speaker(\d+)", text) + if match: + seen.add(int(match.group(1))) + next_index = max(seen, default=0) + 1 + return f"Speaker{next_index:02d}" + + def _is_mixed_speaker_segment( + self, + speaker_records: List[Dict[str, Any]], + current_chunks: List[Dict[str, Any]], + ) -> bool: + if len(current_chunks) < 2 or len(speaker_records) < 2: + return False + assigned_labels: List[str] = [] + for chunk in current_chunks: + embedding = chunk.get("embedding") + if embedding is None: + continue + matched = self._match_existing_speaker( + speaker_records, + self._normalize_embedding(np.asarray(embedding, dtype=np.float32)), + ) + if not matched: + continue + label = str( + matched.get("registry_speaker_id") + or matched.get("speaker_name") + or matched.get("speaker_id") + or "" + ).strip() + if label: + assigned_labels.append(label) + if len(assigned_labels) < 2: + return False + return len(set(assigned_labels)) >= 2 + + def _correct_labels(self, labels: np.ndarray) -> np.ndarray: + labels_id = 0 + id2id: dict[int, int] = {} + new_labels: list[int] = [] + for label in labels.tolist(): + label = int(label) + if label not in id2id: + id2id[label] = labels_id + labels_id += 1 + new_labels.append(id2id[label]) + return np.asarray(new_labels, dtype=np.int32) + + def _spectral_cluster(self, X: np.ndarray, oracle_num: Optional[int] = None) -> np.ndarray: + sim_mat = sklearn.metrics.pairwise.cosine_similarity(X, X) + A = sim_mat.copy() + pval = 0.022 + if A.shape[0] * pval < 6: + pval = 6.0 / A.shape[0] + n_elems = int((1 - pval) * A.shape[0]) + for i in range(A.shape[0]): + low_indexes = np.argsort(A[i, :])[:n_elems] + A[i, low_indexes] = 0 + A = 0.5 * (A + A.T) + A[np.diag_indices(A.shape[0])] = 0 + D = np.diag(np.sum(np.abs(A), axis=1)) + L = D - A + lambdas, eig_vecs = scipy.linalg.eigh(L) + if oracle_num is not None: + num_spk = max(int(oracle_num), 1) + else: + min_num_spks = 1 + max_num_spks = min(15, max(1, X.shape[0] - 1)) + eig_slice = lambdas[min_num_spks - 1 : max_num_spks + 1] + gap_list = [float(eig_slice[i + 1]) - float(eig_slice[i]) for i in range(len(eig_slice) - 1)] + num_spk = int(np.argmax(gap_list)) + min_num_spks if gap_list else 1 + emb = eig_vecs[:, : max(num_spk, 1)] + _, labels, _ = k_means(emb, max(num_spk, 1)) + return np.asarray(labels, dtype=np.int32) + + def _merge_by_cos(self, labels: np.ndarray, embs: np.ndarray, cos_thr: float) -> np.ndarray: + labels = np.asarray(labels, dtype=np.int32).copy() + while True: + spk_num = int(labels.max()) + 1 + if spk_num <= 1: + break + centers = [] + for i in range(spk_num): + spk_emb = embs[labels == i].mean(0) + centers.append(spk_emb) + centers = np.stack(centers, axis=0) + norm_centers = centers / np.linalg.norm(centers, axis=1, keepdims=True) + affinity = np.matmul(norm_centers, norm_centers.T) + affinity = np.triu(affinity, 1) + spks = np.unravel_index(np.argmax(affinity), affinity.shape) + if float(affinity[spks]) < cos_thr: + break + for i in range(len(labels)): + if labels[i] == spks[1]: + labels[i] = spks[0] + elif labels[i] > spks[1]: + labels[i] -= 1 + return self._correct_labels(labels) + + def _cluster_embeddings(self, X: np.ndarray, oracle_num: Optional[int] = None) -> np.ndarray: + if X.shape[0] < 20: + labels = np.zeros(X.shape[0], dtype=np.int32) + else: + labels = self._spectral_cluster(X, oracle_num=oracle_num) + return self._merge_by_cos(labels, X, cos_thr=0.78 if oracle_num is None else 1.0) + + def build_sv_chunks( + self, + audio: np.ndarray, + *, + segment_start_ms: int = 0, + ) -> List[Dict[str, Any]]: + audio = np.asarray(audio, dtype=np.float32).flatten() + if audio.size == 0: + return [] + sample_rate = 16000 + chunk_len = int(1.5 * sample_rate) + chunk_shift = int(0.75 * sample_rate) + chunks: List[Dict[str, Any]] = [] + last_chunk_end = 0 + for chunk_start in range(0, audio.shape[0], chunk_shift): + chunk_end = min(chunk_start + chunk_len, audio.shape[0]) + if chunk_end <= last_chunk_end: + break + actual_start = max(0, chunk_end - chunk_len) + actual_end = chunk_end + last_chunk_end = actual_end + chunk_audio = np.asarray(audio[actual_start:actual_end], dtype=np.float32) + if chunk_audio.shape[0] < chunk_len: + chunk_audio = np.pad(chunk_audio, (0, chunk_len - chunk_audio.shape[0]), "constant") + chunks.append( + { + "start_ms": int(segment_start_ms + actual_start / 16.0), + "end_ms": int(segment_start_ms + actual_end / 16.0), + "audio": chunk_audio, + } + ) + return chunks + + async def extract_chunk_embeddings( + self, + audio: np.ndarray, + *, + segment_start_ms: int = 0, + ) -> List[Dict[str, Any]]: + chunk_items = self.build_sv_chunks(audio, segment_start_ms=segment_start_ms) + if not chunk_items: + chunk_items = [ + { + "start_ms": int(segment_start_ms), + "end_ms": int(segment_start_ms + np.asarray(audio, dtype=np.float32).size / 16.0), + "audio": np.asarray(audio, dtype=np.float32), + } + ] + + registry = get_speaker_registry_service() + results: List[Dict[str, Any]] = [] + for item in chunk_items: + embedding = await run_sync( + registry.extract_embedding_from_audio, + item["audio"], + 16000, + model_id=settings.REALTIME_SV_MODEL, + model_revision=settings.REALTIME_SV_MODEL_REVISION or None, + ) + results.append( + { + "start_ms": int(item["start_ms"]), + "end_ms": int(item["end_ms"]), + "embedding": self._normalize_embedding(np.asarray(embedding, dtype=np.float32)), + } + ) + return results + + async def extract_registry_embedding(self, audio: np.ndarray) -> Optional[np.ndarray]: + audio_array = np.asarray(audio, dtype=np.float32).flatten() + if audio_array.size == 0: + return None + try: + registry = get_speaker_registry_service() + embedding = await run_sync( + registry.extract_embedding_from_audio, + audio_array, + 16000, + ) + return self._normalize_embedding(np.asarray(embedding, dtype=np.float32)) + except Exception as exc: + logger.warning("Realtime registry speaker embedding failed: %s", exc) + return None + + def _flatten_chunk_records( + self, + records: List[Dict[str, Any]], + ) -> Tuple[List[Dict[str, Any]], np.ndarray]: + flat_chunks: List[Dict[str, Any]] = [] + flat_embeddings: List[np.ndarray] = [] + for record_idx, record in enumerate(records): + chunk_entries = record.get("chunks") or [] + if not chunk_entries: + embeddings = record.get("embeddings") or [] + if not embeddings and record.get("embedding") is not None: + embeddings = [record["embedding"]] + start_ms = int(record.get("start_ms", 0)) + end_ms = int(record.get("end_ms", start_ms)) + for embedding in embeddings: + chunk_entries.append( + { + "start_ms": start_ms, + "end_ms": end_ms, + "embedding": embedding, + } + ) + for chunk in chunk_entries: + emb = self._normalize_embedding(np.asarray(chunk["embedding"], dtype=np.float32)) + flat_chunks.append( + { + "record_index": record_idx, + "start_ms": int(chunk.get("start_ms", record.get("start_ms", 0))), + "end_ms": int(chunk.get("end_ms", record.get("end_ms", 0))), + } + ) + flat_embeddings.append(emb) + if not flat_embeddings: + return [], np.zeros((0, 0), dtype=np.float32) + return flat_chunks, np.stack(flat_embeddings, axis=0).astype(np.float32) + + def _merge_seque(self, distribute_res: List[List[float]]) -> List[List[float]]: + if not distribute_res: + return [] + res = [distribute_res[0][:]] + for item in distribute_res[1:]: + if item[2] != res[-1][2] or item[0] > res[-1][1]: + res.append(item[:]) + else: + res[-1][1] = item[1] + return res + + def _smooth_timeline( + self, + res: List[List[float]], + mindur: float = FUNASR_SMOOTH_MINDUR_SEC, + ) -> List[List[float]]: + if len(res) < 2: + return res + for item in res: + item[0] = round(float(item[0]), 2) + item[1] = round(float(item[1]), 2) + for idx in range(len(res)): + if res[idx][1] - res[idx][0] < mindur: + if idx == 0: + res[idx][2] = res[idx + 1][2] + elif idx == len(res) - 1: + res[idx][2] = res[idx - 1][2] + elif res[idx][0] - res[idx - 1][1] <= res[idx + 1][0] - res[idx][1]: + res[idx][2] = res[idx - 1][2] + else: + res[idx][2] = res[idx + 1][2] + return self._merge_seque(res) + + def _postprocess_timeline( + self, + flat_chunks: List[Dict[str, Any]], + labels: np.ndarray, + embeddings: np.ndarray, + ) -> List[Dict[str, Any]]: + assert len(flat_chunks) == len(labels) + labels = self._correct_labels(labels) + distribute_res = [ + [ + float(chunk["start_ms"]) / 1000.0, + float(chunk["end_ms"]) / 1000.0, + int(labels[idx]), + ] + for idx, chunk in enumerate(flat_chunks) + ] + distribute_res = self._merge_seque(distribute_res) + + def is_overlapped(t1: float, t2: float) -> bool: + return t1 > t2 + 1e-4 + + for idx in range(1, len(distribute_res)): + if is_overlapped(distribute_res[idx - 1][1], distribute_res[idx][0]): + pivot = (distribute_res[idx][0] + distribute_res[idx - 1][1]) / 2.0 + distribute_res[idx][0] = pivot + distribute_res[idx - 1][1] = pivot + + distribute_res = self._smooth_timeline(distribute_res) + return [ + { + "start_ms": int(round(item[0] * 1000.0)), + "end_ms": int(round(item[1] * 1000.0)), + "cluster_index": int(item[2]), + } + for item in distribute_res + ] + + def _pick_segment_cluster( + self, + cluster_ranges: List[Dict[str, Any]], + *, + segment_start_ms: int, + segment_end_ms: int, + ) -> int: + overlaps: Dict[int, int] = defaultdict(int) + for item in cluster_ranges: + overlap = min(segment_end_ms, int(item["end_ms"])) - max(segment_start_ms, int(item["start_ms"])) + if overlap > 0: + overlaps[int(item["cluster_index"])] += int(overlap) + if overlaps: + return max(overlaps.items(), key=lambda kv: (kv[1], -kv[0]))[0] + + centers = [ + item for item in cluster_ranges + if int(item["start_ms"]) <= segment_end_ms and int(item["end_ms"]) >= segment_start_ms + ] + if centers: + return int(centers[0]["cluster_index"]) + return 0 + + def cluster_records_with_ranges( + self, + records: List[Dict[str, Any]], + ) -> Tuple[List[Dict[str, Any]], List[int], List[Dict[str, Any]]]: + flat_chunks, X = self._flatten_chunk_records(records) + if X.size == 0: + return [], [], [] + + labels = self._cluster_embeddings(X, oracle_num=None) + cluster_ranges = self._postprocess_timeline(flat_chunks, labels, X) + + cluster_ids = sorted({int(item["cluster_index"]) for item in cluster_ranges}) + if not cluster_ids: + cluster_ids = sorted({int(label) for label in labels.tolist()}) + + clusters: List[Dict[str, Any]] = [] + for cluster_idx in cluster_ids: + member_mask = labels == int(cluster_idx) + member_embeddings = X[member_mask] + centroid = self._normalize_embedding(member_embeddings.mean(0)) + record_refs = [] + seen_record_indices = set() + for emb_idx, chunk in enumerate(flat_chunks): + record_idx = int(chunk["record_index"]) + if not member_mask[emb_idx] or record_idx in seen_record_indices: + continue + seen_record_indices.add(record_idx) + record_refs.append(records[record_idx]) + clusters.append( + { + "centroid": centroid, + "count": int(member_mask.sum()), + "record_refs": record_refs, + } + ) + + record_to_labels: List[List[int]] = [[] for _ in records] + for emb_idx, chunk in enumerate(flat_chunks): + record_to_labels[int(chunk["record_index"])].append(int(labels[emb_idx])) + + record_assignments: List[int] = [] + for record, local_assignments in zip(records, record_to_labels): + if local_assignments: + cluster_idx = self._pick_segment_cluster( + cluster_ranges, + segment_start_ms=int(record.get("start_ms", 0)), + segment_end_ms=int(record.get("end_ms", record.get("start_ms", 0))), + ) + record_assignments.append(cluster_idx) + else: + record_assignments.append(-1) + + return clusters, record_assignments, cluster_ranges + + def cluster_records( + self, + records: List[Dict[str, Any]], + ) -> Tuple[List[Dict[str, Any]], List[int]]: + clusters, record_assignments, _ = self.cluster_records_with_ranges(records) + return clusters, record_assignments + + async def resolve_segment_speaker( + self, + speaker_records: List[Dict[str, Any]], + audio: np.ndarray, + *, + segment_start_ms: int, + segment_end_ms: int, + enable_registry_match: bool, + speaker_threshold: Optional[float], + ) -> Optional[Dict[str, Any]]: + duration_sec = float(len(audio)) / 16000.0 + last_record = speaker_records[-1] if speaker_records else None + if duration_sec < self._FAST_ATTACH_MAX_SEC and speaker_records: + if self._has_named_identity(last_record): + speaker_id = last_record.get("registry_speaker_id") or last_record.get("speaker_id") or "Speaker01" + speaker_name = last_record.get("speaker_name") or speaker_id + return { + "speaker_id": speaker_id, + "speaker_name": speaker_name, + "user_id": last_record.get("user_id"), + "registry_speaker_id": last_record.get("registry_speaker_id"), + "speaker_confidence": 0.0, + "speaker_strategy": "short_attach", + "_cluster_index": last_record.get("cluster_index"), + "_embedding": last_record.get("embedding"), + "_chunk_embeddings": last_record.get("embeddings") or [], + "_chunks": last_record.get("chunks") or [], + } + + current_chunks = await self.extract_chunk_embeddings( + np.asarray(audio, dtype=np.float32), + segment_start_ms=segment_start_ms, + ) + if not current_chunks: + if speaker_records: + if self._has_named_identity(last_record): + speaker_id = last_record.get("registry_speaker_id") or last_record.get("speaker_id") or "Speaker01" + speaker_name = last_record.get("speaker_name") or speaker_id + return { + "speaker_id": speaker_id, + "speaker_name": speaker_name, + "user_id": last_record.get("user_id"), + "registry_speaker_id": last_record.get("registry_speaker_id"), + "speaker_confidence": 0.0, + "speaker_strategy": "embedding_attach", + "_cluster_index": last_record.get("cluster_index"), + "_embedding": last_record.get("embedding"), + "_chunk_embeddings": last_record.get("embeddings") or [], + "_chunks": last_record.get("chunks") or [], + } + return None + + mean_embedding = self._normalize_embedding( + np.mean(np.stack([chunk["embedding"] for chunk in current_chunks], axis=0), axis=0) + ) + if duration_sec >= 4.0 and self._is_mixed_speaker_segment(speaker_records, current_chunks): + return { + "speaker_id": -1, + "speaker_name": "", + "user_id": None, + "registry_speaker_id": None, + "speaker_confidence": 0.0, + "speaker_strategy": "mixed_segment", + "_cluster_index": None, + "_embedding": mean_embedding, + "_chunk_embeddings": [np.asarray(chunk["embedding"], dtype=np.float32) for chunk in current_chunks], + "_chunks": current_chunks, + } + matched_record = self._match_existing_speaker(speaker_records, mean_embedding) + speaker_id = self._next_generic_speaker_id(speaker_records) + speaker_name = speaker_id + user_id = None + registry_speaker_id = None + strategy = "new_speaker" + cluster_index = max( + [ + int(record.get("cluster_index", -1)) + for record in speaker_records + if record.get("cluster_index") is not None + ] or [-1] + ) + 1 + confidence = 0.0 + + if matched_record is not None: + speaker_id = ( + matched_record.get("registry_speaker_id") + or matched_record.get("speaker_id") + or speaker_id + ) + speaker_name = matched_record.get("speaker_name") or speaker_id + user_id = matched_record.get("user_id") + registry_speaker_id = matched_record.get("registry_speaker_id") + cluster_index = self._coerce_cluster_index( + matched_record.get("cluster_index"), + cluster_index, + ) + confidence = float(matched_record.get("_match_score", 0.0)) + strategy = "embedding_match" + + if ( + not registry_speaker_id + and enable_registry_match + and duration_sec >= self._REGISTRY_MATCH_MIN_SEC + and ( + matched_record is None + or confidence >= max(float(getattr(settings, "REALTIME_SPEAKER_CONFIRM_THRESHOLD", 0.62) or 0.62), 0.7) + ) + ): + # Realtime clustering can use a dedicated fast model, while the + # registry stores embeddings from the registration model. Match the + # registry in its own embedding space instead of comparing vectors + # extracted by a different model. + registry_embedding = await self.extract_registry_embedding(audio) + if registry_embedding is not None: + matched = await get_speaker_registry_service().identify_embedding( + registry_embedding, + threshold=speaker_threshold, + ) + if matched.get("name"): + speaker_id = matched.get("speaker_id") or speaker_id + speaker_name = matched.get("name") or speaker_name + user_id = matched.get("user_id") + registry_speaker_id = matched.get("speaker_id") + strategy = "registry_match" + + return { + "speaker_id": speaker_id, + "speaker_name": speaker_name, + "user_id": user_id, + "registry_speaker_id": registry_speaker_id, + "speaker_confidence": round(confidence, 4), + "speaker_strategy": strategy, + "_cluster_index": self._coerce_cluster_index(cluster_index, 0), + "_embedding": mean_embedding, + "_chunk_embeddings": [np.asarray(chunk["embedding"], dtype=np.float32) for chunk in current_chunks], + "_chunks": current_chunks, + "_matched_existing": matched_record is not None, + } + + +_realtime_speaker_clusterer: Optional[RealtimeSpeakerClusterer] = None + + +def get_realtime_speaker_clusterer() -> RealtimeSpeakerClusterer: + global _realtime_speaker_clusterer + if _realtime_speaker_clusterer is None: + _realtime_speaker_clusterer = RealtimeSpeakerClusterer() + return _realtime_speaker_clusterer diff --git a/app/services/speaker_registry.py b/app/services/speaker_registry.py new file mode 100644 index 0000000..f171bc9 --- /dev/null +++ b/app/services/speaker_registry.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- +"""Speaker embedding extraction, registration, and database identification.""" + +from __future__ import annotations + +import asyncio +import logging +import os +import tempfile +import threading +from typing import Any, Optional + +import librosa +import numpy as np +import soundfile as sf +import torch + +from app.core.config import settings +from app.core.database import pg_speaker_db + +logger = logging.getLogger(__name__) + + +def _normalize_embedding(embedding: np.ndarray) -> np.ndarray: + array = np.asarray(embedding, dtype=np.float32).flatten() + norm = float(np.linalg.norm(array)) + if norm < 1e-12: + return array + return array / norm + + +class SpeakerRegistryService: + """Owns speaker embedding models and pgvector lookups.""" + + def __init__(self) -> None: + self._pipelines: dict[tuple[str, str], Any] = {} + self._lock = threading.Lock() + self._inference_lock = threading.BoundedSemaphore(1) + + def _device(self) -> str: + from app.core.device import detect_device + + return detect_device(settings.DEVICE) + + def _get_pipeline( + self, + model_id: Optional[str] = None, + model_revision: Optional[str] = None, + ) -> Any: + effective_model_id = model_id or settings.SV_MODEL + effective_revision = model_revision if model_revision is not None else settings.SV_MODEL_REVISION + cache_key = (effective_model_id, effective_revision or "") + cached = self._pipelines.get(cache_key) + if cached is not None: + return cached + with self._lock: + cached = self._pipelines.get(cache_key) + if cached is not None: + return cached + from modelscope.pipelines import pipeline + from modelscope.utils.constant import Tasks + + from app.infrastructure.model_utils import resolve_model_path + + model_path = resolve_model_path(effective_model_id) + device = self._device() + kwargs: dict[str, Any] = { + "task": Tasks.speaker_verification, + "model": model_path, + "device": device, + } + if effective_revision: + kwargs["model_revision"] = effective_revision + logger.info("正在加载声纹识别模型: %s, device=%s", model_path, device) + pipeline_instance = pipeline(**kwargs) + if hasattr(pipeline_instance, "device_name"): + pipeline_instance.device_name = device + model = getattr(pipeline_instance, "model", None) + if model is not None and hasattr(model, "to"): + pipeline_instance.model = model.to(device) + logger.info("声纹识别模型加载成功") + self._pipelines[cache_key] = pipeline_instance + return pipeline_instance + + def ensure_loaded(self) -> None: + """Eagerly initialize the speaker verification pipeline at startup.""" + self._get_pipeline() + + def extract_embedding_from_audio( + self, + audio_data: np.ndarray, + sample_rate: int = 16000, + *, + model_id: Optional[str] = None, + model_revision: Optional[str] = None, + ) -> np.ndarray: + audio = np.asarray(audio_data, dtype=np.float32).flatten() + if audio.size == 0: + raise ValueError("empty audio") + if sample_rate != 16000: + audio = librosa.resample(audio, orig_sr=sample_rate, target_sr=16000) + pipeline_instance = self._get_pipeline(model_id=model_id, model_revision=model_revision) + model = getattr(pipeline_instance, "model", None) + if model is None: + raise RuntimeError("speaker verification model is not initialized") + device = getattr(pipeline_instance, "device_name", self._device()) + with self._inference_lock: + with torch.no_grad(): + embeddings = model(torch.as_tensor(audio[None, :]).to(device)) + if isinstance(embeddings, torch.Tensor): + embedding = embeddings.detach().cpu().numpy()[0] + else: + embedding = np.asarray(embeddings, dtype=np.float32)[0] + return _normalize_embedding(embedding) + + def extract_embedding_from_file(self, file_path: str) -> np.ndarray: + audio_data, sample_rate = librosa.load(file_path, sr=16000, mono=True) + return self.extract_embedding_from_audio(audio_data, int(sample_rate)) + + async def register_file( + self, + *, + name: str, + file_path: str, + user_id: Optional[str] = None, + ) -> dict[str, Optional[str]]: + if not pg_speaker_db.is_connected: + raise RuntimeError("Speaker database is not connected") + loop = asyncio.get_running_loop() + embedding = await loop.run_in_executor( + None, + self.extract_embedding_from_file, + file_path, + ) + speaker = await pg_speaker_db.save_speaker(name, embedding, user_id=user_id) + return { + "speaker_id": speaker.get("id"), + "name": speaker.get("name") or name, + "user_id": speaker.get("user_id"), + "speaker_model": "CampPlus", + } + + async def identify_embedding( + self, + embedding: np.ndarray, + threshold: Optional[float] = None, + ) -> dict[str, Optional[str]]: + if not pg_speaker_db.is_connected: + return {"speaker_id": None, "name": None, "user_id": None} + speaker = await pg_speaker_db.identify_speaker( + _normalize_embedding(embedding), + threshold if threshold is not None else settings.SV_THRESHOLD, + ) + return { + "speaker_id": speaker.get("id"), + "name": speaker.get("name"), + "user_id": speaker.get("user_id"), + "speaker_model": "CampPlus", + } + + async def identify_file( + self, + *, + file_path: str, + threshold: Optional[float] = None, + ) -> dict[str, Optional[str]]: + loop = asyncio.get_running_loop() + embedding = await loop.run_in_executor( + None, + self.extract_embedding_from_file, + file_path, + ) + return await self.identify_embedding(embedding, threshold=threshold) + + async def apply_registered_speakers( + self, + result: Any, + threshold: Optional[float] = None, + ) -> Any: + if not pg_speaker_db.is_connected: + return result + + cache: dict[str, dict[str, Optional[str]]] = {} + for segment in getattr(result, "segments", []) or []: + embedding = getattr(segment, "speaker_embedding", None) + local_speaker_id = getattr(segment, "speaker_id", None) + if embedding is None or not local_speaker_id: + continue + if local_speaker_id not in cache: + matched_candidate = await self.identify_embedding( + np.asarray(embedding, dtype=np.float32), + threshold=threshold, + ) + if matched_candidate.get("name"): + cache[local_speaker_id] = matched_candidate + else: + continue + matched = cache[local_speaker_id] + if matched.get("name"): + segment.speaker_id = matched.get("speaker_id") or segment.speaker_id + segment.speaker_name = matched.get("name") + segment.user_id = matched.get("user_id") + return result + + async def list_speakers(self) -> list[dict[str, Optional[str]]]: + return await pg_speaker_db.list_speakers() + + async def delete_speaker(self, speaker_id: str) -> bool: + if not speaker_id.isdigit(): + return False + return await pg_speaker_db.delete_speaker(int(speaker_id)) + + @staticmethod + async def save_upload_to_temp(content: bytes, suffix: str = ".wav") -> str: + fd, path = tempfile.mkstemp(prefix="speaker_", suffix=suffix, dir=settings.TEMP_DIR) + try: + with os.fdopen(fd, "wb") as file_obj: + file_obj.write(content) + except Exception: + os.close(fd) + raise + return path + + @staticmethod + def cleanup_file(path: Optional[str]) -> None: + if path and os.path.exists(path): + try: + os.remove(path) + except Exception as exc: + logger.warning("清理临时声纹文件失败 %s: %s", path, exc) + + @staticmethod + def save_audio_array_to_temp(audio_data: np.ndarray, sample_rate: int = 16000) -> str: + fd, path = tempfile.mkstemp(prefix="speaker_array_", suffix=".wav", dir=settings.TEMP_DIR) + os.close(fd) + sf.write(path, np.asarray(audio_data, dtype=np.float32), sample_rate) + return path + + +_speaker_registry_service: Optional[SpeakerRegistryService] = None +_speaker_registry_lock = threading.Lock() + + +def get_speaker_registry_service() -> SpeakerRegistryService: + global _speaker_registry_service + if _speaker_registry_service is None: + with _speaker_registry_lock: + if _speaker_registry_service is None: + _speaker_registry_service = SpeakerRegistryService() + return _speaker_registry_service diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..5f5e9a7 --- /dev/null +++ b/app/utils/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +""" +工具模块 +包含通用工具函数和辅助功能 +""" + +from .common import generate_task_id, validate_text_input, parse_language_code +from .audio import save_audio_array, load_audio_file, generate_temp_audio_path, cleanup_temp_file +from .text_processing import apply_itn_to_text, normalize_asr_text + +__all__ = [ + # 通用工具函数 + "generate_task_id", + "validate_text_input", + "parse_language_code", + # 音频工具函数 + "save_audio_array", + "load_audio_file", + "generate_temp_audio_path", + "cleanup_temp_file", + # ITN(逆文本标准化)功能 - 基于itntext + "apply_itn_to_text", + "normalize_asr_text", +] diff --git a/app/utils/audio.py b/app/utils/audio.py new file mode 100644 index 0000000..71f2ab1 --- /dev/null +++ b/app/utils/audio.py @@ -0,0 +1,528 @@ +# -*- coding: utf-8 -*- +""" +统一音频处理工具 +ASR音频处理功能 +""" + +import os +import tempfile +import requests +import librosa +import soundfile as sf +import numpy as np +import subprocess +import logging +from dataclasses import dataclass +from typing import Tuple, Optional +from io import BytesIO +from urllib.parse import unquote, urlparse + +from ..core.config import settings +from ..core.exceptions import ( + InvalidParameterException, + InvalidMessageException, + DefaultServerErrorException, +) +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class NormalizedAudio: + path: str + timestamp_scale: float = 1.0 + + +def download_audio_from_url(url: str, max_size: Optional[int] = None) -> bytes: + """从 URL 或服务端本地路径读取音频文件 + + Args: + url: 音频文件 URL、本地路径或 file:// 路径 + max_size: 最大文件大小限制 + + Returns: + 音频文件的二进制数据 + + Raises: + InvalidParameterException: URL无效或下载失败 + InvalidMessageException: 文件太大 + """ + if not url: + raise InvalidParameterException("URL不能为空") + + max_file_size = max_size or settings.MAX_AUDIO_SIZE + parsed = urlparse(url) + + if parsed.scheme in {"", "file"}: + local_path = os.path.expanduser(unquote(parsed.path) if parsed.scheme == "file" else url) + if not os.path.isfile(local_path): + raise InvalidParameterException(f"本地音频文件不存在: {local_path}") + file_size = os.path.getsize(local_path) + if file_size > max_file_size: + max_size_mb = max_file_size // 1024 // 1024 + raise InvalidMessageException(f"音频文件太大,最大支持{max_size_mb}MB") + with open(local_path, "rb") as file_obj: + return file_obj.read() + + try: + response = requests.get(url, timeout=30, stream=True) + response.raise_for_status() + + # 检查Content-Length头 + content_length = response.headers.get("content-length") + if content_length and int(content_length) > max_file_size: + max_size_mb = max_file_size // 1024 // 1024 + raise InvalidMessageException(f"音频文件太大,最大支持{max_size_mb}MB") + + # 分块下载并检查大小 + audio_data = BytesIO() + downloaded_size = 0 + + for chunk in response.iter_content(chunk_size=8192): + downloaded_size += len(chunk) + if downloaded_size > max_file_size: + max_size_mb = max_file_size // 1024 // 1024 + raise InvalidMessageException(f"音频文件太大,最大支持{max_size_mb}MB") + audio_data.write(chunk) + + return audio_data.getvalue() + + except requests.RequestException as e: + raise InvalidParameterException(f"下载音频文件失败: {str(e)}") + + +def save_audio_to_temp_file(audio_data: bytes, suffix: str = ".wav") -> str: + """保存音频数据到临时文件 + + Args: + audio_data: 音频二进制数据 + suffix: 文件后缀 + + Returns: + 临时文件路径 + + Raises: + AudioProcessingException: 保存失败 + """ + try: + with tempfile.NamedTemporaryFile( + delete=False, suffix=suffix, dir=settings.TEMP_DIR + ) as temp_file: + temp_file.write(audio_data) + return temp_file.name + except Exception as e: + raise DefaultServerErrorException(f"保存音频文件失败: {str(e)}") + + +def cleanup_temp_file(file_path: str) -> None: + """清理临时文件 + + Args: + file_path: 文件路径 + """ + try: + if file_path and os.path.exists(file_path): + os.remove(file_path) + except Exception: + # 静默忽略清理错误 + pass + + +def load_audio_file(audio_path: str, target_sr: int = 16000) -> Tuple[np.ndarray, int]: + """加载音频文件并转换为指定采样率 + + Args: + audio_path: 音频文件路径 + target_sr: 目标采样率 + + Returns: + (audio_data, sample_rate): 音频数据和采样率 + + Raises: + AudioProcessingException: 加载失败 + """ + try: + # 使用librosa加载音频 + audio_data, sr = librosa.load(audio_path, sr=target_sr) + return audio_data, int(sr) + except Exception as e: + raise DefaultServerErrorException(f"加载音频文件失败: {str(e)}") + + +def get_audio_duration(audio_path: str) -> float: + """获取音频文件时长 + + Args: + audio_path: 音频文件路径 + + Returns: + 音频时长(秒) + + Raises: + AudioProcessingException: 获取时长失败 + """ + try: + # Load audio and get duration + y, sr = librosa.load(audio_path, sr=None) + duration = librosa.get_duration(y=y, sr=sr) + return duration + except Exception as e: + raise DefaultServerErrorException(f"获取音频时长失败: {str(e)}") + + +def get_container_duration(audio_path: str) -> Optional[float]: + """通过 ffprobe 获取音频容器的 metadata 时长 + + 对于 m4a/AAC 等压缩格式,容器记录的时长可能与实际解码样本数不一致 + (常见于 m3u8/ts 分片合并的音频)。返回 None 表示获取失败。 + """ + try: + result = subprocess.run( + ["ffprobe", "-v", "error", "-show_entries", "format=duration", + "-of", "default=noprint_wrappers=1:nokey=1", audio_path], + capture_output=True, text=True, timeout=10, + ) + if result.returncode == 0 and result.stdout.strip(): + return float(result.stdout.strip()) + except Exception as e: + logger.debug(f"ffprobe 获取容器时长失败: {e}") + return None + + +def get_timestamp_scale(original_audio_path: str, decoded_duration: float) -> float: + """计算时间戳缩放系数 + + 对比容器 metadata 时长与解码后实际时长,返回缩放系数。 + 用于修正 m4a/AAC 等格式中容器时长与解码时长不一致的问题。 + + Args: + original_audio_path: 原始音频文件路径(转换前) + decoded_duration: 解码后的实际音频时长(秒) + + Returns: + 缩放系数(容器时长 / 解码时长),无差异时返回 1.0 + """ + container_duration = get_container_duration(original_audio_path) + if container_duration is None or decoded_duration <= 0: + return 1.0 + + scale = container_duration / decoded_duration + if abs(scale - 1.0) < 0.001: + # 差异 < 0.1%,忽略 + return 1.0 + + logger.info( + f"检测到容器/解码时长不一致: container={container_duration:.3f}s, " + f"decoded={decoded_duration:.3f}s, scale={scale:.6f}" + ) + return scale + + +def resample_audio_array( + audio_array: np.ndarray, + original_sr: int, + target_sr: int, +) -> np.ndarray: + """重采样音频数组 + + Args: + audio_array: 原始音频数据 + original_sr: 原始采样率 + target_sr: 目标采样率 + + Returns: + 重采样后的音频数据 + """ + if original_sr == target_sr: + return audio_array + + try: + # 确保是1D数组用于librosa重采样 + if audio_array.ndim > 1: + # 如果是多声道,取第一个声道 + if audio_array.shape[0] > audio_array.shape[1]: + audio_1d = audio_array[0, :] + else: + audio_1d = ( + audio_array[:, 0] + if audio_array.shape[1] > 1 + else audio_array.flatten() + ) + else: + audio_1d = audio_array + + # 使用librosa进行重采样 + resampled = librosa.resample(audio_1d, orig_sr=original_sr, target_sr=target_sr) + + logger.info(f"音频重采样: {original_sr}Hz -> {target_sr}Hz") + return resampled + + except Exception as e: + logger.warning(f"音频重采样失败: {str(e)},使用原始音频") + return audio_array + + +def adjust_audio_volume(audio_array: np.ndarray, volume: int) -> np.ndarray: + """调节音频音量 + + Args: + audio_array: 音频数据数组 + volume: 音量值,范围0~100,50为原始音量 + + Returns: + 调节后的音频数据 + """ + if int(volume) == 50: + return audio_array + + if volume < 0 or volume > 100: + logger.warning(f"音量值{volume}超出范围[0,100],使用默认值50") + volume = 50 + + # 将音量值转换为倍数 (0-100 -> 0-2.0) + volume_factor = volume / 50.0 + + # 应用音量调节 + adjusted_audio = audio_array * volume_factor + + # 防止削波,如果音量过大导致超过范围,进行归一化 + max_val = np.max(np.abs(adjusted_audio)) + if max_val > 1.0: + adjusted_audio = adjusted_audio / max_val + logger.info(f"音量调节后进行归一化,最大值: {max_val:.3f}") + + logger.info(f"音频音量已调节: {volume}/100 (倍数: {volume_factor:.2f})") + return adjusted_audio + + +def save_audio_array( + audio_array: np.ndarray, + output_path: str, + sample_rate: int = 22050, + format: str = "wav", + original_sr: Optional[int] = None, + volume: int = 50, +) -> str: + """保存音频数组到文件 + + Args: + audio_array: 音频数据数组 + output_path: 输出文件路径 + sample_rate: 目标采样率 + format: 音频格式 + original_sr: 原始采样率(用于重采样) + volume: 音量值,范围0~100,默认50 + + Returns: + 保存的文件路径 + + Raises: + AudioProcessingException: 保存失败 + """ + try: + # 如果指定了原始采样率且与目标采样率不同,进行重采样 + if original_sr and original_sr != sample_rate: + audio_array = resample_audio_array(audio_array, original_sr, sample_rate) + + # 调节音频音量 + audio_array = adjust_audio_volume(audio_array, volume) + + # 确保音频数据是float32格式 + if audio_array.dtype != np.float32: + audio_array = audio_array.astype(np.float32) + + # 确保音频数据在正确的范围内 + if np.max(np.abs(audio_array)) > 1.0: + audio_array = audio_array / np.max(np.abs(audio_array)) + + # 确保是2D张量 (channels, samples) + if audio_array.ndim == 1: + audio_array = audio_array[np.newaxis, :] # 添加通道维度 + elif audio_array.ndim > 2: + audio_array = audio_array.squeeze() + if audio_array.ndim == 1: + audio_array = audio_array[np.newaxis, :] + + # 根据格式选择保存方法 + if format.lower() == "wav": + sf.write(output_path, audio_array.T, sample_rate, format="WAV") + else: + # 使用soundfile保存其他格式 + # 确保音频数据是单声道 + if audio_array.shape[0] > 1: + audio_array = np.mean(audio_array, axis=0) + + sf.write(output_path, audio_array.T, sample_rate, format=format.upper()) + + return output_path + + except Exception as e: + raise DefaultServerErrorException(f"保存音频文件失败: {str(e)}") + + +def convert_audio_to_wav( + input_path: str, output_path: Optional[str] = None, target_sr: int = 16000 +) -> str: + """转换音频文件为WAV格式 + + Args: + input_path: 输入文件路径 + output_path: 输出文件路径(可选) + target_sr: 目标采样率,默认16000Hz + + Returns: + 转换后的文件路径 + + Raises: + AudioProcessingException: 转换失败 + """ + if not output_path: + output_path = input_path.rsplit(".", 1)[0] + ".wav" + + try: + # 使用librosa加载并重采样 + audio_data, _ = librosa.load(input_path, sr=target_sr) + sf.write(output_path, audio_data, target_sr, format="WAV") + return output_path + + except Exception as e: + # 尝试使用ffmpeg转换 + try: + subprocess.run( + [ + "ffmpeg", + "-f", "s16le", + "-ar", str(target_sr), + "-ac", "1", + "-i", input_path, + "-acodec", "pcm_s16le", + output_path, + "-y", + ], + check=True, + capture_output=True, + ) + return output_path + except (subprocess.CalledProcessError, FileNotFoundError): + raise DefaultServerErrorException(f"音频格式转换失败: {str(e)}") + + +def normalize_audio_for_asr(audio_path: str, target_sr: int = 16000) -> NormalizedAudio: + """Normalize audio and return explicit timestamp metadata. + + Args: + audio_path: 输入音频文件路径 + target_sr: 目标采样率,默认16000Hz + + Returns: + Normalized audio path and timestamp scale metadata. + """ + try: + # 检查文件扩展名 + file_ext = os.path.splitext(audio_path)[1].lower() + + # 如果已经是WAV格式且采样率正确,直接返回 + if file_ext == ".wav": + # 检查采样率 + _, sr = librosa.load(audio_path, sr=None) + if sr == target_sr: + return NormalizedAudio(path=audio_path) + + # 转换为标准WAV格式 + normalized_path = convert_audio_to_wav(audio_path, target_sr=target_sr) + logger.debug(f"音频文件已标准化: {audio_path} -> {normalized_path}") + + timestamp_scale = 1.0 + if normalized_path != audio_path: + decoded_duration = get_audio_duration(normalized_path) + timestamp_scale = get_timestamp_scale(audio_path, decoded_duration) + + return NormalizedAudio(path=normalized_path, timestamp_scale=timestamp_scale) + + except Exception as e: + raise DefaultServerErrorException(f"音频标准化失败: {str(e)}") + + +def generate_temp_audio_path(prefix: str = "audio", suffix: str = ".wav") -> str: + """生成临时音频文件路径 + + Args: + prefix: 文件名前缀 + suffix: 文件后缀 + + Returns: + 临时文件路径 + """ + import time + + timestamp = int(time.time()) + filename = f"{prefix}_{timestamp}_{os.getpid()}{suffix}" + return os.path.join(settings.TEMP_DIR, filename) + + +def detect_audio_format_from_bytes(data: bytes) -> str: + """通过文件头(magic bytes)检测音频格式 + + Args: + data: 音频文件的前几个字节 + + Returns: + 文件后缀(包含点号) + """ + if len(data) < 12: + return ".wav" + + # 检查常见音频格式的文件头 + if data[:4] == b"RIFF" and data[8:12] == b"WAVE": + return ".wav" + elif data[:3] == b"ID3" or (data[0:2] == b"\xff\xfb") or (data[0:2] == b"\xff\xfa"): + return ".mp3" + elif data[:4] == b"fLaC": + return ".flac" + elif data[:4] == b"OggS": + return ".ogg" + elif data[4:8] == b"ftyp": + # M4A/AAC/MP4/MOV 容器 + return ".mp4" + elif data[:4] == b"\x1aE\xdf\xa3": + # WebM/MKV + return ".webm" + + # 默认为 wav,librosa 会自动处理 + return ".wav" + + +def get_audio_file_suffix( + audio_address: Optional[str] = None, audio_data: Optional[bytes] = None +) -> str: + """自动识别音频文件后缀 + + Args: + audio_address: 音频文件URL(可选) + audio_data: 音频二进制数据(可选,用于检测文件头) + + Returns: + 文件后缀(包含点号) + """ + if audio_address: + # 从URL中提取扩展名 + parsed = urlparse(audio_address) + path = unquote(parsed.path) + + # 获取扩展名 + ext = os.path.splitext(path)[1].lower() + if ext and ext in [ + ".wav", ".mp3", ".flac", ".ogg", ".m4a", ".aac", ".pcm", ".webm", + ".mp4", ".mpeg", ".mpga", ".mov", ".mkv", ".avi", + ]: + return ext + + # 无法识别扩展名,默认为 .wav + return ".wav" + + elif audio_data: + # 通过文件头检测格式 + return detect_audio_format_from_bytes(audio_data[:12]) + + else: + # 默认为 .wav + return ".wav" diff --git a/app/utils/audio_filter.py b/app/utils/audio_filter.py new file mode 100644 index 0000000..3cce0f0 --- /dev/null +++ b/app/utils/audio_filter.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +""" +音频过滤工具 - 用于流式ASR的近场/远场声音检测 +""" + +import numpy as np +import logging +from typing import Tuple, Dict + +logger = logging.getLogger(__name__) + + +def calculate_rms_energy(audio_array: np.ndarray) -> float: + """计算音频RMS能量 + + Args: + audio_array: float32音频数组,范围-1.0到1.0 + + Returns: + RMS能量值 + """ + if len(audio_array) == 0: + return 0.0 + return float(np.sqrt(np.mean(audio_array ** 2))) + + +def is_nearfield_voice( + audio_array: np.ndarray, + sample_rate: int = 16000, # noqa: ARG001 + rms_threshold: float = 0.01, + enable_filter: bool = True, +) -> Tuple[bool, Dict]: + """判断是否为近场有效声音(仅基于RMS能量) + + Args: + audio_array: float32音频数组,范围-1.0到1.0 + sample_rate: 采样率(保留用于兼容性) + rms_threshold: RMS能量阈值 + enable_filter: 是否启用过滤(开关) + + Returns: + (is_nearfield, metrics): 是否近场声音 + 检测指标详情 + """ + if not enable_filter: + return True, {'enabled': False} + + if len(audio_array) == 0: + return False, {'error': 'empty_array'} + + # 计算RMS能量 + rms_energy = calculate_rms_energy(audio_array) + + # 仅使用RMS能量判断 + is_nearfield = rms_energy >= rms_threshold + + metrics = { + 'rms_energy': round(rms_energy, 6), + 'is_nearfield': is_nearfield, + 'thresholds': { + 'rms': rms_threshold, + } + } + + return is_nearfield, metrics diff --git a/app/utils/audio_splitter.py b/app/utils/audio_splitter.py new file mode 100644 index 0000000..138d2ee --- /dev/null +++ b/app/utils/audio_splitter.py @@ -0,0 +1,439 @@ +# -*- coding: utf-8 -*- +""" +音频分割模块 +基于 VAD 的智能音频分割,支持长音频分段识别 +""" + +import logging +import numpy as np +import librosa +import soundfile as sf +import tempfile +import os +import time +from typing import List, Tuple, Optional +from dataclasses import dataclass + +from ..core.config import settings +from ..core.exceptions import DefaultServerErrorException + +logger = logging.getLogger(__name__) + + +def _log_audio_split_timing(stage: str, duration_ms: float, **extra) -> None: + payload = { + "event": "audio_split_timing", + "stage": stage, + "duration_ms": round(duration_ms, 2), + } + payload.update(extra) + logger.info("音频分割阶段耗时", extra=payload) + + +@dataclass +class AudioSegment: + """音频片段信息""" + + start_ms: int # 开始时间(毫秒) + end_ms: int # 结束时间(毫秒) + audio_data: Optional[np.ndarray] = None # 音频数据 + temp_file: Optional[str] = None # 临时文件路径 + speaker_id: Optional[str] = None # 说话人ID(多说话人模式) + + @property + def start_sec(self) -> float: + """开始时间(秒)""" + return self.start_ms / 1000.0 + + @property + def end_sec(self) -> float: + """结束时间(秒)""" + return self.end_ms / 1000.0 + + @property + def duration_ms(self) -> int: + """时长(毫秒)""" + return self.end_ms - self.start_ms + + @property + def duration_sec(self) -> float: + """时长(秒)""" + return self.duration_ms / 1000.0 + + +class AudioSplitter: + """音频分割器 + + 使用 VAD 模型检测语音边界,智能分割长音频 + """ + + # 默认配置 + DEFAULT_MIN_SEGMENT_SEC = 1.0 # 每段最小时长(秒) + DEFAULT_SAMPLE_RATE = 16000 # 默认采样率 + + def __init__( + self, + min_segment_sec: float = DEFAULT_MIN_SEGMENT_SEC, + device: str = "auto", + ): + """初始化音频分割器 + + Args: + min_segment_sec: 每段最小时长(秒) + device: 计算设备("cuda", "cpu", "auto") + """ + split_trigger_sec = settings.MAX_SEGMENT_SEC + + self.split_trigger_sec = split_trigger_sec + self.min_segment_sec = min_segment_sec + self.split_trigger_ms = int(split_trigger_sec * 1000) + self.min_segment_ms = int(min_segment_sec * 1000) + self.device = device + + def get_vad_segments( + self, audio_path: str + ) -> List[Tuple[int, int]]: + """使用 VAD 模型获取语音段 + + Args: + audio_path: 音频文件路径 + + Returns: + 语音段列表,每个元素为 (start_ms, end_ms) + """ + try: + from ..services.asr.engines import get_global_vad_model + + logger.info("开始 VAD 语音段检测...") + vad_model = get_global_vad_model(self.device) + if vad_model is None: + raise DefaultServerErrorException("VAD 模型未加载") + + # 调用 VAD 模型 + vad_started = time.perf_counter() + result = vad_model.generate(input=audio_path, cache={}) + vad_duration_ms = (time.perf_counter() - vad_started) * 1000 + + if not result or len(result) == 0: + _log_audio_split_timing( + "vad_generate", + vad_duration_ms, + audio_path=audio_path, + vad_segment_count=0, + ) + logger.warning("VAD 未检测到语音段") + return [] + + # 解析 VAD 结果 + # FunASR VAD 返回格式: [[start_ms, end_ms], [start_ms, end_ms], ...] + vad_segments = result[0].get("value", []) + + if not vad_segments: + _log_audio_split_timing( + "vad_generate", + vad_duration_ms, + audio_path=audio_path, + vad_segment_count=0, + ) + logger.warning("VAD 结果为空") + return [] + + _log_audio_split_timing( + "vad_generate", + vad_duration_ms, + audio_path=audio_path, + vad_segment_count=len(vad_segments), + ) + logger.info(f"VAD 检测到 {len(vad_segments)} 个语音段") + logger.info( + "开始按 VAD 边界重分段 " + f"(split_trigger={self.split_trigger_sec}s, min_segment={self.min_segment_sec}s)..." + ) + return [(int(seg[0]), int(seg[1])) for seg in vad_segments] + + except Exception as e: + logger.error(f"VAD 检测失败: {e}") + raise DefaultServerErrorException(f"VAD 检测失败: {str(e)}") + + def merge_segments_greedy( + self, vad_segments: List[Tuple[int, int]], total_duration_ms: int + ) -> List[Tuple[int, int]]: + """按 VAD 结果重分段 + + 策略: + 1. 默认保留 VAD 原始边界,避免将整段连续语音合并成超长片段 + 2. 仅对短片段(< min_segment_ms)做邻段合并 + 3. 对重叠片段进行边界修正,避免重复音频 + + Args: + vad_segments: VAD 检测到的语音段列表 [(start_ms, end_ms), ...] + total_duration_ms: 音频总时长(毫秒) + + Returns: + 合并后的段列表 [(start_ms, end_ms), ...] + """ + if not vad_segments: + # 没有 VAD 段,返回整个音频(按最大时长切分) + return self._split_by_fixed_duration(total_duration_ms) + + # 按时间排序并修正边界(防止越界、重叠) + sorted_vad = sorted(vad_segments, key=lambda x: x[0]) + normalized: List[Tuple[int, int]] = [] + for raw_start, raw_end in sorted_vad: + start_ms = max(0, int(raw_start)) + end_ms = min(total_duration_ms, int(raw_end)) + if end_ms <= start_ms: + continue + + if not normalized: + normalized.append((start_ms, end_ms)) + continue + + last_end = normalized[-1][1] + # 有重叠时,优先保持边界,避免与上一段重复采样 + if start_ms < last_end: + start_ms = last_end + + if end_ms > start_ms: + normalized.append((start_ms, end_ms)) + + if not normalized: + return self._split_by_fixed_duration(total_duration_ms) + + merged = list(normalized) + + # 只处理短片段:与相邻片段合并(不基于静音间隙) + idx = 0 + while idx < len(merged): + start_ms, end_ms = merged[idx] + duration = end_ms - start_ms + + if duration >= self.min_segment_ms or len(merged) == 1: + idx += 1 + continue + + if idx == 0: + # 首段过短:并入后段 + next_end = merged[idx + 1][1] + merged[idx + 1] = (start_ms, next_end) + del merged[idx] + continue + + if idx == len(merged) - 1: + # 尾段过短:并入前段 + prev_start, _ = merged[idx - 1] + merged[idx - 1] = (prev_start, end_ms) + del merged[idx] + idx = max(0, idx - 1) + continue + + # 中间短段:优先并入时长更短的一侧,避免单段过长 + prev_start = merged[idx - 1][0] + next_end = merged[idx + 1][1] + merged_with_prev_duration = end_ms - prev_start + merged_with_next_duration = next_end - start_ms + + if merged_with_prev_duration <= merged_with_next_duration: + merged[idx - 1] = (prev_start, end_ms) + del merged[idx] + idx = max(0, idx - 1) + else: + merged[idx + 1] = (start_ms, next_end) + del merged[idx] + + return merged + + def _split_by_fixed_duration(self, total_duration_ms: int) -> List[Tuple[int, int]]: + """按固定时长切分(无 VAD 时的 fallback) + + Args: + total_duration_ms: 音频总时长(毫秒) + + Returns: + 切分后的段列表 + """ + segments = [] + current = 0 + while current < total_duration_ms: + end = min(current + self.split_trigger_ms, total_duration_ms) + if end - current >= self.min_segment_ms: + segments.append((current, end)) + current = end + return segments + + def split_audio_file( + self, + audio_path: str, + output_dir: Optional[str] = None, + ) -> List[AudioSegment]: + """分割音频文件 + + Args: + audio_path: 音频文件路径 + output_dir: 输出目录(可选,默认使用临时目录) + + Returns: + 音频片段列表 + """ + try: + total_started = time.perf_counter() + # 加载音频 + load_started = time.perf_counter() + audio_data, sr = librosa.load(audio_path, sr=self.DEFAULT_SAMPLE_RATE) + load_audio_ms = (time.perf_counter() - load_started) * 1000 + total_duration_ms = int(len(audio_data) / sr * 1000) + audio_duration_sec = total_duration_ms / 1000 + + logger.info(f"音频总时长: {audio_duration_sec:.2f}秒") + _log_audio_split_timing( + "load_audio", + load_audio_ms, + audio_path=audio_path, + audio_duration_sec=round(audio_duration_sec, 2), + sample_rate=sr, + ) + + # 检查是否需要分割 + if total_duration_ms <= self.split_trigger_ms: + logger.info("音频时长在限制内,无需分割") + total_duration_ms_for_log = (time.perf_counter() - total_started) * 1000 + _log_audio_split_timing( + "split_total", + total_duration_ms_for_log, + audio_path=audio_path, + audio_duration_sec=round(audio_duration_sec, 2), + output_segment_count=1, + need_split=False, + load_audio_ms=round(load_audio_ms, 2), + vad_ms=0, + merge_ms=0, + write_segments_ms=0, + ) + return [ + AudioSegment( + start_ms=0, + end_ms=total_duration_ms, + audio_data=audio_data, + temp_file=audio_path, + ) + ] + + # 获取 VAD 段 + vad_started = time.perf_counter() + vad_segments = self.get_vad_segments(audio_path) + vad_ms = (time.perf_counter() - vad_started) * 1000 + + # 贪婪合并 + merge_started = time.perf_counter() + merged_segments = self.merge_segments_greedy(vad_segments, total_duration_ms) + merge_ms = (time.perf_counter() - merge_started) * 1000 + logger.info(f"重分段完成: 原始VAD={len(vad_segments)}, 输出={len(merged_segments)}") + _log_audio_split_timing( + "merge_segments", + merge_ms, + audio_path=audio_path, + vad_segment_count=len(vad_segments), + output_segment_count=len(merged_segments), + audio_duration_sec=round(audio_duration_sec, 2), + ) + + # 切分音频并保存到临时文件 + logger.info("开始切分音频并保存临时文件...") + output_dir = output_dir or settings.TEMP_DIR + os.makedirs(output_dir, exist_ok=True) + + audio_segments = [] + write_started = time.perf_counter() + for idx, (start_ms, end_ms) in enumerate(merged_segments): + # 计算采样点范围 + start_sample = int(start_ms / 1000 * sr) + end_sample = int(end_ms / 1000 * sr) + + # 提取音频片段 + segment_data = audio_data[start_sample:end_sample] + + # 保存到临时文件 + temp_file = tempfile.NamedTemporaryFile( + delete=False, + suffix=".wav", + dir=output_dir, + prefix=f"segment_{idx:03d}_", + ) + temp_path = temp_file.name + temp_file.close() + + sf.write(temp_path, segment_data, sr) + + segment = AudioSegment( + start_ms=start_ms, + end_ms=end_ms, + audio_data=segment_data, + temp_file=temp_path, + ) + audio_segments.append(segment) + + logger.debug( + f"分段 {idx + 1}/{len(merged_segments)}: " + f"{start_ms / 1000:.2f}s - {end_ms / 1000:.2f}s " + f"(时长: {segment.duration_sec:.2f}s)" + ) + + write_segments_ms = (time.perf_counter() - write_started) * 1000 + logger.info(f"音频切分完成,共 {len(audio_segments)} 个分段") + _log_audio_split_timing( + "write_segments", + write_segments_ms, + audio_path=audio_path, + output_dir=output_dir, + output_segment_count=len(audio_segments), + audio_duration_sec=round(audio_duration_sec, 2), + ) + total_ms = (time.perf_counter() - total_started) * 1000 + _log_audio_split_timing( + "split_total", + total_ms, + audio_path=audio_path, + audio_duration_sec=round(audio_duration_sec, 2), + output_segment_count=len(audio_segments), + need_split=True, + load_audio_ms=round(load_audio_ms, 2), + vad_ms=round(vad_ms, 2), + merge_ms=round(merge_ms, 2), + write_segments_ms=round(write_segments_ms, 2), + ) + return audio_segments + + except Exception as e: + logger.error(f"音频分割失败: {e}") + raise DefaultServerErrorException(f"音频分割失败: {str(e)}") + + @staticmethod + def cleanup_segments(segments: List[AudioSegment]) -> None: + """清理临时文件 + + Args: + segments: 音频片段列表 + """ + for segment in segments: + if segment.temp_file and os.path.exists(segment.temp_file): + try: + os.remove(segment.temp_file) + except Exception as e: + logger.warning(f"清理临时文件失败: {segment.temp_file}, {e}") + + +def split_long_audio( + audio_path: str, + device: str = "auto", +) -> List[AudioSegment]: + """分割长音频的便捷函数 + + Args: + audio_path: 音频文件路径 + device: 计算设备 + + Returns: + 音频片段列表 + """ + splitter = AudioSplitter(device=device) + return splitter.split_audio_file(audio_path) diff --git a/app/utils/boot_events.py b/app/utils/boot_events.py new file mode 100644 index 0000000..65b3355 --- /dev/null +++ b/app/utils/boot_events.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +"""Structured startup events for the optional terminal dashboard.""" + +from __future__ import annotations + +import json +import os +import sys +from typing import Any + +_BOOT_EVENT_PREFIX = "__FUNASR_BOOT__" + + +def boot_events_enabled() -> bool: + return (os.getenv("FUNASR_BOOT_EVENTS") or "").strip() == "1" + + +def emit_boot_event(event: str, **payload: Any) -> None: + if not boot_events_enabled(): + return + + data = {"event": event, **payload} + sys.stdout.write(_BOOT_EVENT_PREFIX + json.dumps(data, ensure_ascii=False) + "\n") + sys.stdout.flush() + + +def get_boot_event_prefix() -> str: + return _BOOT_EVENT_PREFIX diff --git a/app/utils/common.py b/app/utils/common.py new file mode 100644 index 0000000..5a26fda --- /dev/null +++ b/app/utils/common.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +""" +通用工具函数 +包含任务ID生成、参数验证等通用功能 +""" + +import uuid +import hashlib +import time +import re +from typing import Optional + + +def generate_task_id(prefix: str = "") -> str: + """生成唯一的任务ID + + Args: + prefix: 任务ID前缀 + + Returns: + 生成的任务ID + """ + timestamp = str(int(time.time() * 1000)) + random_id = str(uuid.uuid4()).replace("-", "") + combined = timestamp + random_id + + # 使用MD5哈希生成32位字符串 + task_id = hashlib.md5(combined.encode()).hexdigest() + + if prefix: + return f"{prefix}_{task_id}" + return task_id + + +def validate_text_input(text: str, max_length: int = 10000) -> tuple[bool, str]: + """验证输入文本 + + Args: + text: 待验证的文本 + max_length: 最大长度限制 + + Returns: + (is_valid, message): 验证结果和消息 + """ + if not text or not text.strip(): + return False, "文本内容不能为空" + + text = text.strip() + + if len(text) > max_length: + return False, f"文本长度超过限制,最大支持{max_length}个字符" + + # 检查是否包含有效字符 + if not re.search(r"[\u4e00-\u9fff\w\s]", text): + return False, "文本内容无效,请输入有效的中文、英文或数字" + + return True, "验证通过" + + +def parse_language_code(lang_code: Optional[str]) -> str: + """解析语言代码 + + Args: + lang_code: 语言代码(如 zh, zh-cn, en, ja等) + + Returns: + 标准化的语言代码 + """ + if not lang_code: + return "zh" # 默认中文 + + lang_code = lang_code.lower().strip() + + # 语言代码映射 + lang_mapping = { + "zh": "zh", + "zh-cn": "zh", + "zh-tw": "zh", + "zh-hk": "zh", + "en": "en", + "en-us": "en", + "en-gb": "en", + "ja": "jp", + "jp": "jp", + "ko": "kr", + "kr": "kr", + "yue": "yue", # 粤语 + } + + return lang_mapping.get(lang_code, "zh") diff --git a/app/utils/download_models.py b/app/utils/download_models.py new file mode 100644 index 0000000..0d7b2d1 --- /dev/null +++ b/app/utils/download_models.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +模型预下载脚本 +统一从 ModelScope 预下载所有运行所需模型 +""" + +import argparse +import json +from pathlib import Path +from typing import Optional + +from modelscope.hub.snapshot_download import snapshot_download as ms_snapshot_download +from app.core.config import settings +from app.services.asr.model_capabilities import ( + get_all_qwen_modelscope_assets, + get_camplusplus_replacement_paths, + get_download_modelscope_assets, +) + + +def _get_qwen_modelscope_assets(): + """Return all declared ModelScope Qwen assets for offline deployment.""" + assets = get_all_qwen_modelscope_assets() + if not assets: + print("当前部署计划未启用 Qwen3-ASR,跳过 Qwen 模型下载") + return [] + + print("离线部署模式:下载全部已声明 Qwen 模型(含 forced aligner)") + return assets + +def _get_cache_path(model_id: str, source: str = "modelscope") -> Path: + """获取模型缓存路径""" + _ = source + return Path(settings.MODELSCOPE_PATH) / model_id + + +def check_model_exists(model_id: str, source: str = "modelscope") -> tuple[bool, str]: + """检查模型是否已存在于本地缓存""" + try: + model_path = _get_cache_path(model_id, source) + + if model_path.exists() and model_path.is_dir(): + if any(model_path.iterdir()): + return True, str(model_path) + except Exception: + pass + + return False, "" + + +def check_all_models() -> list[tuple[str, str, str, Optional[str]]]: + """检查所有模型是否存在 + + Returns: + 缺失的模型列表,每个元素为 (model_id, description, source, revision) + """ + missing = [] + ms_assets = get_download_modelscope_assets() + qwen_assets = _get_qwen_modelscope_assets() + + for asset in ms_assets: + exists, _ = check_model_exists(asset.model_id, source="modelscope") + if not exists: + missing.append((asset.model_id, asset.description, "modelscope", asset.revision)) + + for asset in qwen_assets: + exists, _ = check_model_exists(asset.model_id, source="modelscope") + if not exists: + missing.append((asset.model_id, asset.description, "modelscope", asset.revision)) + + return missing + + +def fix_camplusplus_config() -> bool: + """修复 CAM++ 配置文件,将模型ID替换为本地路径(用于离线环境) + + 修复 issue #15: 离线环境下 CAM++ 模型会尝试从 modelscope.cn 获取依赖模型配置 + + Returns: + 是否修复成功 + """ + try: + cache_dir = Path(settings.MODELSCOPE_PATH) + config_file = cache_dir / "iic/speech_campplus_speaker-diarization_common/configuration.json" + + if not config_file.exists(): + return False + + # 读取配置文件 + with open(config_file, 'r', encoding='utf-8') as f: + config = json.load(f) + + # 需要替换的模型ID -> 本地路径映射 + replacements = get_camplusplus_replacement_paths(str(cache_dir)) + + # 检查是否需要修改 + modified = False + if "model" in config: + for key in ["speaker_model", "change_locator", "vad_model"]: + if key in config["model"]: + old_value = config["model"][key] + if old_value in replacements: + new_value = replacements[old_value] + # 检查本地路径是否存在 + if Path(new_value).exists(): + config["model"][key] = new_value + modified = True + + # 写回配置文件 + if modified: + with open(config_file, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=4, ensure_ascii=False) + return True + + return False + + except Exception as e: + print(f"⚠️ 修复 CAM++ 配置文件失败: {e}") + return False + + +def download_models( + auto_mode: bool = False, + export_dir: Optional[str] = None, +) -> bool: + """下载所有需要的模型 + + Args: + auto_mode: 如果为True,表示自动模式(从start.py调用),会简化输出 + export_dir: 如果指定,将下载的模型导出到该目录(用于离线部署) + + Returns: + 是否全部下载成功 + """ + import shutil + + # 检查缺失的模型 + missing = check_all_models() + ms_assets = get_download_modelscope_assets() + qwen_assets = _get_qwen_modelscope_assets() + + export_path = Path(export_dir) if export_dir else None + + if not missing: + if not auto_mode: + print("✅ 所有模型已存在,无需下载") + if not export_path: + return True + + ms_cache_dir = Path(settings.MODELSCOPE_CACHE) + if auto_mode: + print(f"📦 检测到 {len(missing)} 个模型需要下载...") + else: + print("=" * 60) + print("Qwen3-ASR 模型预下载") + print("=" * 60) + print(f"ModelScope 缓存: {ms_cache_dir}") + print(f"待下载模型: {len(missing)} 个") + print("=" * 60) + + failed = [] + downloaded = [] + + # 下载 ModelScope 模型 (Paraformer) + ms_missing = [(mid, desc, rev) for mid, desc, src, rev in missing if src == "modelscope"] + if ms_missing: + if not auto_mode: + print("\n📦 开始下载 ModelScope 模型 (Paraformer)...") + print("-" * 60) + + for i, (model_id, desc, revision) in enumerate(ms_missing, 1): + if not auto_mode: + print(f"\n[{i}/{len(ms_missing)}] {desc}") + print(f" 模型ID: {model_id}") + if revision: + print(f" 版本: {revision}") + print(f" 📥 开始下载...", end="") + + try: + local_dir = _get_cache_path(model_id, "modelscope") + local_dir.parent.mkdir(parents=True, exist_ok=True) + # 传递版本参数,如果指定了版本 + if revision: + path = ms_snapshot_download( + model_id, + revision=revision, + cache_dir=str(ms_cache_dir), + local_dir=str(local_dir), + ) + else: + path = ms_snapshot_download( + model_id, + cache_dir=str(ms_cache_dir), + local_dir=str(local_dir), + ) + if not auto_mode: + print(f" ✅ 完成: {path}") + downloaded.append((model_id, "modelscope", path)) + except Exception as e: + if not auto_mode: + print(f" ❌ 失败: {e}") + failed.append((model_id, str(e))) + + # 修复 CAM++ 配置文件(用于离线环境) + if not auto_mode: + print("\n🔧 修复 CAM++ 配置文件...") + if fix_camplusplus_config(): + if not auto_mode: + print(" ✅ CAM++ 配置已修复(离线环境可用)") + else: + if not auto_mode: + print(" ℹ️ 无需修复或配置文件不存在") + + # 导出模式:复制模型到扁平化的 models/ 根目录 + if export_path and not failed: + if not auto_mode: + print(f"\n📦 导出模型到: {export_path}") + + # 收集所有需要导出的模型 + all_models = [] + for asset in ms_assets: + all_models.append((asset.model_id, "modelscope")) + for asset in qwen_assets: + all_models.append((asset.model_id, "modelscope")) + + exported = 0 + for model_entry in all_models: + if len(model_entry) == 3: + model_id, source, actual_model_id = model_entry + else: + model_id, source = model_entry + actual_model_id = model_id + cache_path = _get_cache_path(actual_model_id, source) + if cache_path.exists(): + rel_path = cache_path.relative_to(Path(settings.MODELSCOPE_PATH)) + target_dir = export_path / rel_path + + target_dir.parent.mkdir(parents=True, exist_ok=True) + if not auto_mode: + print(f" 📂 {model_id}", end="") + try: + shutil.copytree(cache_path, target_dir, dirs_exist_ok=True) + exported += 1 + if not auto_mode: + print(" ✅") + except Exception as e: + if not auto_mode: + print(f" ❌ {e}") + + if not auto_mode: + print(f"\n✅ 已导出 {exported} 个模型到 models/") + + if not auto_mode: + print("\n" + "=" * 60) + print("📊 下载统计:") + print(f" ✅ 已下载: {len(downloaded)} 个") + print(f" ❌ 失败: {len(failed)} 个") + print("=" * 60) + + if failed: + print(f"\n失败的模型:") + for model_id, err in failed: + print(f" - {model_id}: {err}") + return False + else: + print("\n✅ 所有模型准备就绪!") + print("=" * 60) + + return len(failed) == 0 + + +def main() -> int: + """CLI entrypoint for model download and export.""" + parser = argparse.ArgumentParser(description="Download or export Qwen3-ASR models") + parser.add_argument( + "--export-dir", + default=None, + help="Optional export directory for offline deployment packaging", + ) + parser.add_argument( + "--auto-mode", + action="store_true", + help="Reduce output for startup/bootstrap usage", + ) + args = parser.parse_args() + + success = download_models( + auto_mode=args.auto_mode, + export_dir=args.export_dir, + ) + return 0 if success else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/app/utils/model_loader.py b/app/utils/model_loader.py new file mode 100644 index 0000000..7b0a7ea --- /dev/null +++ b/app/utils/model_loader.py @@ -0,0 +1,520 @@ +# -*- coding: utf-8 -*- +""" +模型预加载工具 +在应用启动时预加载所有需要的模型,避免首次请求时的延迟 +""" + +import logging +import os +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +try: + from rich.console import Console +except ImportError: + Console = None + +from .boot_events import emit_boot_event + +logger = logging.getLogger(__name__) + +_PRELOAD_QUIET_LOGGERS = ( + "root", + "vllm", + "app.infrastructure.model_utils", + "app.services.asr.engines.global_models", + "app.services.asr.qwen3_engine", + "app.utils.speaker_diarizer", +) + + +class _ProgressNoiseFilter(logging.Filter): + def filter(self, record: logging.LogRecord) -> bool: + if record.levelno >= logging.WARNING: + return True + return not any( + record.name == prefix or record.name.startswith(f"{prefix}.") + for prefix in _PRELOAD_QUIET_LOGGERS + ) + + +class _StartupProgress: + def __init__(self, title: str, total: int): + self._title = title + self._total = max(total, 1) + self._enabled = bool( + Console is not None + and sys.stderr.isatty() + ) + self._console: Any = None + self._filter = _ProgressNoiseFilter() + self._handlers: list[logging.Handler] = [] + self._current_step = 1 + self._last_description: str | None = None + + def __enter__(self) -> "_StartupProgress": + emit_boot_event( + "phase_start", + phase=self._title, + total=self._total, + message=self._title, + ) + if not self._enabled or Console is None: + return self + self._console = Console(stderr=True) + root_logger = logging.getLogger() + self._handlers = list(root_logger.handlers) + for handler in self._handlers: + handler.addFilter(self._filter) + return self + + def __exit__(self, exc_type, exc, tb) -> None: + for handler in self._handlers: + handler.removeFilter(self._filter) + self._handlers.clear() + + def update(self, description: str) -> None: + emit_boot_event( + "step_start", + phase=self._title, + step=self._current_step, + total=self._total, + message=description, + ) + if self._console is None: + return + if description == self._last_description: + return + self._last_description = description + self._console.print( + f"[bold cyan][startup {self._current_step}/{self._total}][/bold cyan] {description}", + highlight=False, + ) + + def advance(self, description: str) -> None: + emit_boot_event( + "step_done", + phase=self._title, + step=self._current_step, + total=self._total, + message=description, + ) + self._last_description = description + self._current_step = min(self._current_step + 1, self._total) + + +@dataclass(frozen=True) +class ModelIntegritySpec: + description: str + path: Path + required_patterns: tuple[str, ...] + alternative_required_patterns: tuple[tuple[str, ...], ...] = () + min_total_size_bytes: int = 0 + + +def _format_bytes(num_bytes: int) -> str: + value = float(num_bytes) + units = ["B", "KB", "MB", "GB", "TB"] + for unit in units: + if value < 1024.0 or unit == units[-1]: + return f"{value:.1f}{unit}" + value /= 1024.0 + return f"{num_bytes}B" + + +def _find_pattern_matches(root: Path, pattern: str) -> list[Path]: + return [path for path in root.glob(pattern) if path.is_file()] + + +def _find_missing_patterns(root: Path, patterns: tuple[str, ...]) -> list[str]: + return [pattern for pattern in patterns if not _find_pattern_matches(root, pattern)] + + +def _format_alternative_patterns(pattern_groups: tuple[tuple[str, ...], ...]) -> str: + return " OR ".join(" + ".join(group) for group in pattern_groups) + + +def _check_model_integrity_spec(spec: ModelIntegritySpec) -> dict[str, Any]: + if not spec.path.exists() or not spec.path.is_dir(): + return { + "description": spec.description, + "path": str(spec.path), + "ok": False, + "missing_patterns": [ + *spec.required_patterns, + *( + [_format_alternative_patterns(spec.alternative_required_patterns)] + if spec.alternative_required_patterns + else [] + ), + ], + "total_size_bytes": 0, + "reason": "directory_missing", + } + + files = [path for path in spec.path.rglob("*") if path.is_file()] + total_size_bytes = sum(path.stat().st_size for path in files) + + missing_patterns = _find_missing_patterns(spec.path, spec.required_patterns) + if not missing_patterns and spec.alternative_required_patterns: + alternative_missing_patterns = [ + _find_missing_patterns(spec.path, group) + for group in spec.alternative_required_patterns + ] + if all(alternative_missing_patterns): + missing_patterns = [ + _format_alternative_patterns(spec.alternative_required_patterns) + ] + + if missing_patterns: + return { + "description": spec.description, + "path": str(spec.path), + "ok": False, + "missing_patterns": missing_patterns, + "total_size_bytes": total_size_bytes, + "reason": "required_files_missing", + } + + if total_size_bytes < spec.min_total_size_bytes: + return { + "description": spec.description, + "path": str(spec.path), + "ok": False, + "missing_patterns": [], + "total_size_bytes": total_size_bytes, + "reason": "directory_too_small", + } + + return { + "description": spec.description, + "path": str(spec.path), + "ok": True, + "missing_patterns": [], + "total_size_bytes": total_size_bytes, + "reason": "ok", + } + + +def _build_modelscope_spec( + model_id: str, + description: str, + required_patterns: tuple[str, ...], + *, + min_total_size_bytes: int, + alternative_required_patterns: tuple[tuple[str, ...], ...] = (), +) -> ModelIntegritySpec: + from ..core.config import settings + + return ModelIntegritySpec( + description=description, + path=Path(settings.MODELSCOPE_PATH) / model_id, + required_patterns=required_patterns, + alternative_required_patterns=alternative_required_patterns, + min_total_size_bytes=min_total_size_bytes, + ) + + +def _convert_ms_patterns( + patterns: tuple[str, ...], +) -> tuple[str, ...]: + return tuple(p.replace("snapshots/*/", "") for p in patterns) + + +def _build_qwen_spec( + model_id: str, + description: str, + required_patterns: tuple[str, ...], + *, + min_total_size_bytes: int, + alternative_required_patterns: tuple[tuple[str, ...], ...] = (), +) -> ModelIntegritySpec: + from ..core.config import settings + ms_path = Path(settings.MODELSCOPE_PATH) / model_id + ms_required = _convert_ms_patterns(required_patterns) + ms_alternative = tuple( + _convert_ms_patterns(group) + for group in alternative_required_patterns + ) + return ModelIntegritySpec( + description=description, + path=ms_path, + required_patterns=ms_required, + alternative_required_patterns=ms_alternative, + min_total_size_bytes=min_total_size_bytes, + ) + + +def _should_check_qwen_forced_aligner( + resolved_device: str, + using_cpu_qwen_rust: bool, +) -> bool: + """Return True when startup integrity should require Qwen forced aligner files.""" + from ..core.config import settings + + _ = (resolved_device, using_cpu_qwen_rust) + return settings.ASR_ENABLE_WORD_TIMESTAMPS + + +def _build_required_model_integrity_specs() -> list[ModelIntegritySpec]: + from ..core.config import settings + from ..core.device import detect_device + from ..services.asr.manager import get_model_manager + from ..services.asr.model_capabilities import ( + get_enabled_qwen_modelscope_assets, + get_runtime_required_modelscope_assets, + ) + from ..services.asr.model_plan import get_runtime_model_ids + from ..services.asr.qwenasr_rust import is_qwenasr_rust_available + manager = get_model_manager() + model_ids = [item["id"] for item in manager.list_declared_entries()] + runtime_models = get_runtime_model_ids(model_ids) + resolved_device = detect_device(settings.DEVICE) + using_cpu_qwen_rust = ( + resolved_device == "cpu" and is_qwenasr_rust_available() + ) + specs: list[ModelIntegritySpec] = [] + + for asset in get_runtime_required_modelscope_assets( + include_realtime_punc=settings.ASR_ENABLE_REALTIME_PUNC, + ): + specs.append( + _build_modelscope_spec( + asset.model_id, + asset.description, + asset.required_patterns, + alternative_required_patterns=asset.alternative_required_patterns, + min_total_size_bytes=asset.min_total_size_bytes, + ) + ) + + for asset in get_enabled_qwen_modelscope_assets( + include_forced_aligner=_should_check_qwen_forced_aligner( + resolved_device=resolved_device, + using_cpu_qwen_rust=using_cpu_qwen_rust, + ), + ): + specs.append( + _build_qwen_spec( + asset.model_id, + asset.description, + asset.required_patterns, + alternative_required_patterns=asset.alternative_required_patterns, + min_total_size_bytes=asset.min_total_size_bytes, + ) + ) + + return specs + + +def verify_required_models_integrity(use_logger: bool = True) -> dict[str, Any]: + output = logger.info if use_logger else print + specs = _build_required_model_integrity_specs() + total = len(specs) + results: list[dict[str, Any]] = [] + invalid: list[dict[str, Any]] = [] + + if not use_logger: + output("=" * 60) + output(f"🔍 开始检查运行时模型完整性,共 {total} 个") + output("=" * 60) + for index, spec in enumerate(specs, start=1): + output(f"[{index}/{total}] 检查 {spec.description}") + result = _check_model_integrity_spec(spec) + results.append(result) + if result["ok"]: + output( + f" ✅ OK size={_format_bytes(result['total_size_bytes'])} " + f"path={result['path']}" + ) + continue + invalid.append(result) + if result["reason"] == "directory_missing": + output(f" ❌ FAIL directory_missing path={result['path']}") + elif result["reason"] == "required_files_missing": + output( + f" ❌ FAIL missing={', '.join(result['missing_patterns'])} " + f"size={_format_bytes(result['total_size_bytes'])} path={result['path']}" + ) + else: + output( + f" ❌ FAIL size_too_small size={_format_bytes(result['total_size_bytes'])} " + f"path={result['path']}" + ) + output("=" * 60) + output(f"模型完整性检查完成: total={total} ok={total - len(invalid)} failed={len(invalid)}") + output("=" * 60) + return { + "total": total, + "results": results, + "invalid_models": invalid, + } + + logger.info("开始检查运行时模型完整性: total=%s", total) + with _StartupProgress("检查运行时模型完整性", total) as progress: + for spec in specs: + progress.update(f"检查 {spec.description}") + result = _check_model_integrity_spec(spec) + results.append(result) + if not result["ok"]: + invalid.append(result) + if result["reason"] == "directory_missing": + logger.error("模型完整性检查失败: %s, reason=directory_missing, path=%s", spec.description, result["path"]) + elif result["reason"] == "required_files_missing": + logger.error( + "模型完整性检查失败: %s, reason=required_files_missing, missing=%s, size=%s, path=%s", + spec.description, + ", ".join(result["missing_patterns"]), + _format_bytes(result["total_size_bytes"]), + result["path"], + ) + else: + logger.error( + "模型完整性检查失败: %s, reason=directory_too_small, size=%s, path=%s", + spec.description, + _format_bytes(result["total_size_bytes"]), + result["path"], + ) + progress.advance(f"检查完成 {spec.description}") + + logger.info( + "模型完整性检查完成: total=%s ok=%s failed=%s", + total, + total - len(invalid), + len(invalid), + ) + + return { + "total": total, + "results": results, + "invalid_models": invalid, + } +def preload_models() -> dict[str, Any]: + """ + 预加载所有需要的模型(根据 ENABLE_* 配置过滤) + + Returns: + dict: 包含加载状态的字典 + """ + # 修复 CAM++ 配置文件(用于离线环境) + try: + from .download_models import fix_camplusplus_config + fix_camplusplus_config() + except Exception: + pass # 修复失败不影响启动 + + result: dict[str, Any] = { + "asr_models": {}, # 所有ASR模型加载状态 + "vad_model": {"loaded": False, "error": None}, + "speaker_diarization_model": {"loaded": False, "error": None}, + } + + from ..core.config import settings + from ..core.device import detect_device + + # 初始化变量,避免未绑定错误 + asr_device = detect_device(settings.DEVICE) + model_manager = None + + # 1. 预加载所有配置的ASR模型(根据 ENABLE_* 配置过滤) + model_ids: list[str] = [] + model_manager = None + + try: + from ..services.asr.manager import get_model_manager + from ..services.asr.model_plan import get_runtime_model_ids + from ..services.asr.runtime import get_runtime_router + + model_manager = get_model_manager() + runtime_router = get_runtime_router() + + # 获取所有模型配置 + all_models = model_manager.list_declared_entries() + model_ids = [m["id"] for m in all_models] + + models_to_load = get_runtime_model_ids(model_ids) + + if not models_to_load: + logger.warning("⚠️ 当前环境未解析出可运行的 ASR 模型") + + except Exception as e: + logger.error(f"❌ 获取模型管理器失败: {e}") + models_to_load = [] + runtime_router = None + + total_steps = len(models_to_load) + 2 + + logger.info( + "开始预加载模型: declared=%s runtime=%s models=%s", + len(model_ids) if model_manager else 0, + len(models_to_load), + ", ".join(models_to_load) if models_to_load else "(无)", + ) + + with _StartupProgress("预加载模型", total_steps) as progress: + for model_id in models_to_load: + result["asr_models"][model_id] = {"loaded": False, "error": None} + progress.update(f"加载 ASR 模型 {model_id}") + try: + if runtime_router is None: + raise RuntimeError("runtime router unavailable") + runtime_router.warmup_model(model_id) + result["asr_models"][model_id]["loaded"] = True + except Exception as e: + result["asr_models"][model_id]["error"] = str(e) + logger.error("ASR模型预加载失败: %s, error=%s", model_id, e) + progress.advance(f"已完成 ASR 模型 {model_id}") + + # 2. 预加载语音活动检测模型(VAD) + progress.update("加载语音活动检测模型(VAD)") + try: + from ..services.asr.engines import get_global_vad_model + + vad_model = get_global_vad_model(asr_device) + if vad_model: + result["vad_model"]["loaded"] = True + else: + result["vad_model"]["error"] = "语音活动检测模型(VAD)加载后返回None" + except Exception as e: + result["vad_model"]["error"] = str(e) + logger.error("语音活动检测模型(VAD)加载失败: %s", e) + progress.advance("已完成语音活动检测模型(VAD)") + + # 5. 预加载说话人分离模型 (CAM++) - 必需模型,始终加载 + progress.update("加载说话人分离模型(CAM++)") + try: + from ..utils.speaker_diarizer import get_global_diarization_pipeline + + diarization_pipeline = get_global_diarization_pipeline() + if diarization_pipeline: + result["speaker_diarization_model"]["loaded"] = True + else: + result["speaker_diarization_model"]["error"] = "说话人分离模型加载后返回None" + except Exception as e: + result["speaker_diarization_model"]["error"] = str(e) + logger.error("说话人分离模型(CAM++)加载失败: %s", e) + progress.advance("已完成说话人分离模型(CAM++)") + + loaded_asr_count = sum(1 for status in result["asr_models"].values() if status["loaded"]) + total_asr_count = len(result["asr_models"]) + extra_loaded = sum( + 1 + for key in ("vad_model", "speaker_diarization_model") + if result[key]["loaded"] + ) + extra_failed = sum( + 1 + for key in ("vad_model", "speaker_diarization_model") + if result[key]["error"] + ) + logger.info( + "模型预加载完成: asr=%s/%s extra_loaded=%s extra_failed=%s", + loaded_asr_count, + total_asr_count, + extra_loaded, + extra_failed, + ) + + return result diff --git a/app/utils/speaker_diarizer.py b/app/utils/speaker_diarizer.py new file mode 100644 index 0000000..ec314f5 --- /dev/null +++ b/app/utils/speaker_diarizer.py @@ -0,0 +1,697 @@ +# -*- coding: utf-8 -*- +""" +说话人分离模块 +基于 CAM++ 的说话人分离,用于多说话人音频分割 +""" + +from loguru import logger +import numpy as np +import librosa +import soundfile as sf +import tempfile +import os +import threading +from typing import Any, List, Mapping, Optional, Sequence, cast +from dataclasses import dataclass + +import torch + +from ..core.config import settings +from ..core.exceptions import DefaultServerErrorException + +# 全局 CAM++ pipeline 缓存(单例) +_global_diarization_pipeline: Any | None = None +_diarization_pipeline_lock = threading.Lock() +_diarization_inference_semaphore = threading.BoundedSemaphore(1) + + +@dataclass +class SpeakerSegment: + """说话人分段信息""" + + start_ms: int + end_ms: int + speaker_id: str + audio_data: Optional[np.ndarray] = None + temp_file: Optional[str] = None + + @property + def start_sec(self) -> float: + return self.start_ms / 1000.0 + + @property + def end_sec(self) -> float: + return self.end_ms / 1000.0 + + @property + def duration_ms(self) -> int: + return self.end_ms - self.start_ms + + @property + def duration_sec(self) -> float: + return self.duration_ms / 1000.0 + + +def _resolve_modelscope_device() -> str: + """根据配置和硬件自动选择 modelscope pipeline 设备 + """ + from ..core.device import detect_device + + return detect_device(settings.DEVICE) + + +def _move_pipeline_model_to_device(pipeline_instance: Any, modelscope_device: str) -> None: + """将 pipeline 的底层模型迁移到目标设备。""" + if hasattr(pipeline_instance, "device_name"): + pipeline_instance.device_name = modelscope_device + model = getattr(pipeline_instance, "model", None) + if model is not None and hasattr(model, "to"): + pipeline_instance.model = model.to(modelscope_device) + + +def _create_modelscope_pipeline( + *, + task: Any, + model: str, + modelscope_device: str, + model_revision: Optional[str] = None, +) -> Any: + """创建 modelscope pipeline,并在需要时把底层模型迁移到目标设备。""" + from modelscope.pipelines import pipeline + + pipeline_kwargs: dict[str, Any] = { + "task": task, + "model": model, + "device": modelscope_device, + } + if model_revision is not None: + pipeline_kwargs["model_revision"] = model_revision + + pipeline_instance = pipeline(**pipeline_kwargs) + _move_pipeline_model_to_device(pipeline_instance, modelscope_device) + return pipeline_instance + + +def _enable_batched_sv( + pipeline_instance: Any, + modelscope_device: str, + max_batch_size: int = 32, +) -> Any: + """ + 对说话人分离 pipeline 启用 batched SV 推理。 + + 原始 pipeline 的 forward 方法逐个 segment 调用 sv_pipeline 提取 embedding, + 这里改为将所有 segment 拼成一个 batch 一次性推理,大幅减少 GPU 调用次数。 + 同时将子 pipeline(sv / vad / change_locator)绑定到指定 device。 + + Args: + pipeline_instance: CAM++ diarization pipeline 实例 + modelscope_device: 设备名称 + max_batch_size: 最大批处理大小,防止 OOM + """ + if getattr(pipeline_instance, "_batched_sv_enabled", False): + return pipeline_instance + + from modelscope.utils.constant import Tasks + + config = getattr(pipeline_instance, "config", None) + if not isinstance(config, Mapping): + logger.warning("CAM++ pipeline 缺少可读取的 config,跳过 batched SV 优化") + return pipeline_instance + + sv_model = config.get("speaker_model") + vad_model = config.get("vad_model") + change_locator = config.get("change_locator") + + if isinstance(sv_model, str) and sv_model: + pipeline_instance.sv_pipeline = _create_modelscope_pipeline( + task=Tasks.speaker_verification, + model=sv_model, + modelscope_device=modelscope_device, + ) + + if isinstance(vad_model, str) and vad_model: + pipeline_instance.vad_pipeline = _create_modelscope_pipeline( + task=Tasks.voice_activity_detection, + model=vad_model, + modelscope_device=modelscope_device, + model_revision="v2.0.2", + ) + + if isinstance(change_locator, str) and change_locator: + pipeline_instance.change_locator_pipeline = _create_modelscope_pipeline( + task=Tasks.speaker_diarization, + model=change_locator, + modelscope_device=modelscope_device, + ) + + def batched_forward(self: Any, segments: Sequence[Sequence[Any]]) -> np.ndarray: + """批量提取说话人 embedding,替代逐段串行推理""" + sv_model_instance = getattr(getattr(self, "sv_pipeline", None), "model", None) + emb_size = int(getattr(sv_model_instance, "emb_size", 192)) + + if not segments: + return np.empty((0, emb_size), dtype=np.float32) + + if sv_model_instance is None: + raise RuntimeError("CAM++ sv_pipeline.model 未初始化") + + all_embeddings: list[np.ndarray] = [] + total_segments = len(segments) + start_idx = 0 + + while start_idx < total_segments: + end_idx = min(start_idx + max_batch_size, total_segments) + batch_segments = segments[start_idx:end_idx] + + batch_items: list[np.ndarray] = [] + for segment in batch_segments: + if len(segment) < 3: + continue + batch_items.append(np.asarray(segment[2], dtype=np.float32)) + + if not batch_items: + start_idx = end_idx + continue + + batch = np.stack(batch_items, axis=0) + + with torch.no_grad(): + embeddings = sv_model_instance( + cast(Any, torch).as_tensor(batch).to(modelscope_device) + ) + + if isinstance(embeddings, torch.Tensor): + all_embeddings.append(embeddings.detach().cpu().numpy()) + else: + all_embeddings.append(np.asarray(embeddings, dtype=np.float32)) + + start_idx = end_idx + + if not all_embeddings: + return np.empty((0, emb_size), dtype=np.float32) + + return ( + np.concatenate(all_embeddings, axis=0) + if len(all_embeddings) > 1 + else all_embeddings[0] + ) + + import types + + pipeline_instance.forward = types.MethodType(batched_forward, pipeline_instance) + pipeline_instance._batched_sv_enabled = True + + logger.info( + "CAM++ 说话人分离启用 batched SV: device={}, sv_device={}, vad_device={}", + modelscope_device, + getattr(getattr(pipeline_instance, "sv_pipeline", None), "device_name", "unknown"), + getattr(getattr(pipeline_instance, "vad_pipeline", None), "device_name", "unknown"), + ) + return pipeline_instance + + +def get_global_diarization_pipeline() -> Any: + """获取全局说话人分离 pipeline(懒加载单例)""" + global _global_diarization_pipeline + + with _diarization_pipeline_lock: + if _global_diarization_pipeline is None: + try: + from modelscope.utils.constant import Tasks + from ..infrastructure.model_utils import resolve_model_path + + model_id = 'iic/speech_campplus_speaker-diarization_common' + model_path = resolve_model_path(model_id) + modelscope_device = _resolve_modelscope_device() + + logger.info( + "正在加载 CAM++ 说话人分离模型: {}, device={}", + model_path, + modelscope_device, + ) + _global_diarization_pipeline = _create_modelscope_pipeline( + task=Tasks.speaker_diarization, + model=model_path, + modelscope_device=modelscope_device, + ) + _global_diarization_pipeline = _enable_batched_sv( + _global_diarization_pipeline, modelscope_device + ) + logger.info("CAM++ 模型加载成功(已启用 batched SV)") + except Exception as e: + logger.error(f"CAM++ 模型加载失败: {e}") + raise DefaultServerErrorException(f"说话人分离模型加载失败: {str(e)}") + + return _global_diarization_pipeline + + +class SpeakerDiarizer: + """基于 CAM++ 的说话人分离器""" + + DEFAULT_MIN_SEGMENT_SEC = 1.0 + DEFAULT_SAMPLE_RATE = 16000 + LOW_ENERGY_SEARCH_WINDOW_MS = 10000 + LOW_ENERGY_CONTEXT_MS = 160 + LOW_ENERGY_STEP_MS = 20 + + def __init__( + self, + min_segment_sec: float = DEFAULT_MIN_SEGMENT_SEC, + ): + self.min_segment_sec = min_segment_sec + self.min_segment_ms = int(min_segment_sec * 1000) + + def diarize( + self, audio_path: str + ) -> List[SpeakerSegment]: + """执行说话人分离 + + Args: + audio_path: 音频文件路径 + + Returns: + 原始分段列表(未合并) + """ + audio_duration_ms: Optional[int] = None + try: + try: + audio_duration_ms = int(librosa.get_duration(path=audio_path) * 1000) + except Exception: + audio_duration_ms = None + + # CAM++ 对极短片段收益很低,且容易直接报 "too short"。 + # 这里提前降级成单说话人,避免无意义 warning 刷屏。 + if audio_duration_ms is not None and audio_duration_ms < self.min_segment_ms: + logger.debug( + "音频时长过短,跳过 CAM++ 说话人分离: duration_ms=%s < min_segment_ms=%s", + audio_duration_ms, + self.min_segment_ms, + ) + return [ + SpeakerSegment( + start_ms=0, + end_ms=max(audio_duration_ms, 1), + speaker_id="说话人1", + ) + ] + + pipeline = get_global_diarization_pipeline() + + logger.info(f"开始说话人分离: {audio_path}") + with _diarization_inference_semaphore: + result = pipeline(audio_path) + + # 解析结果: {'text': [[start, end, speaker_id], ...]} + # pipeline 返回类型不确定,需要安全地获取 'text' 字段 + if isinstance(result, dict): + raw_output = result.get('text', []) + else: + raw_output = getattr(result, 'text', []) or [] + + segments = [] + for seg in raw_output: + if isinstance(seg, list) and len(seg) == 3: + try: + start_ms = int(float(seg[0]) * 1000) + end_ms = int(float(seg[1]) * 1000) + speaker_id = f"说话人{int(seg[2]) + 1}" + segments.append(SpeakerSegment( + start_ms=start_ms, + end_ms=end_ms, + speaker_id=speaker_id, + )) + except (ValueError, TypeError) as e: + logger.warning(f"跳过格式错误的片段: {seg}, 错误: {e}") + + logger.info(f"说话人分离完成,原始片段数: {len(segments)}") + # 诊断日志:打印前20个原始片段 + for i, seg in enumerate(segments[:20]): + logger.debug( + f"[CAM++原始] #{i}: {seg.start_sec:.2f}-{seg.end_sec:.2f}s " + f"({seg.duration_sec:.2f}s) {seg.speaker_id}" + ) + return segments + + except Exception as e: + error_msg = str(e).lower() + + # 音频太短时,返回默认的单说话人片段 + if "too short" in error_msg: + logger.debug("CAM++ 跳过过短音频,回退单说话人片段: %s", e) + if audio_duration_ms is None: + try: + audio_duration_ms = int(librosa.get_duration(path=audio_path) * 1000) + except Exception: + audio_duration_ms = 5000 + + return [ + SpeakerSegment( + start_ms=0, + end_ms=audio_duration_ms, + speaker_id="说话人1", + ) + ] + + # 其他异常正常抛出 + logger.error(f"说话人分离失败: {e}") + raise DefaultServerErrorException(f"说话人分离失败: {str(e)}") + + def merge_consecutive_segments( + self, segments: List[SpeakerSegment] + ) -> List[SpeakerSegment]: + """合并同一说话人的连续片段""" + if not segments: + return [] + + # 按开始时间排序 + sorted_segments = sorted(segments, key=lambda x: x.start_ms) + + merged = [] + current = SpeakerSegment( + start_ms=sorted_segments[0].start_ms, + end_ms=sorted_segments[0].end_ms, + speaker_id=sorted_segments[0].speaker_id, + ) + + for seg in sorted_segments[1:]: + if seg.speaker_id == current.speaker_id: + # 同一说话人,扩展结束时间 + current.end_ms = max(current.end_ms, seg.end_ms) + else: + # 不同说话人,保存当前段,开始新段 + logger.debug( + f"[合并中断] 说话人切换: {current.speaker_id} → {seg.speaker_id} " + f"在 {seg.start_sec:.2f}s,保存片段 {current.start_sec:.2f}-{current.end_sec:.2f}s" + ) + merged.append(current) + current = SpeakerSegment( + start_ms=seg.start_ms, + end_ms=seg.end_ms, + speaker_id=seg.speaker_id, + ) + + # 保存最后一段 + merged.append(current) + + logger.info(f"合并同一说话人连续片段: {len(segments)} → {len(merged)}") + # 诊断日志:打印合并后的前20个片段 + for i, seg in enumerate(merged[:20]): + logger.debug( + f"[合并后] #{i}: {seg.start_sec:.2f}-{seg.end_sec:.2f}s " + f"({seg.duration_sec:.2f}s) {seg.speaker_id}" + ) + return merged + + def merge_short_segments( + self, segments: List[SpeakerSegment] + ) -> List[SpeakerSegment]: + """智能合并短片段 + + 策略: + 1. 第一层:<10s的片段向后合并(避免孤立短片段) + 2. 第二层:60s累积合并(合并连续片段) + """ + if not segments: + return [] + + max_segment_sec = settings.MAX_SEGMENT_SEC + + # 按开始时间排序 + sorted_segments = sorted(segments, key=lambda x: x.start_ms) + + # 第一层:<10s累积向后合并(循环计算直到>=10s或超过60s) + merged = [] + i = 0 + while i < len(sorted_segments): + seg = sorted_segments[i] + + # 如果>=10s,直接添加 + if seg.duration_sec >= 10.0: + merged.append(seg) + i += 1 + continue + + # <10s,开始累积合并 + current_start_ms = seg.start_ms + current_end_ms = seg.end_ms + current_duration_sec = seg.duration_sec + j = i + 1 + + # 累积合并,只要<10s且同说话人且不超过60s + while j < len(sorted_segments) and current_duration_sec < 10.0: + next_seg = sorted_segments[j] + if next_seg.speaker_id != seg.speaker_id: + break + new_duration = (next_seg.end_ms - current_start_ms) / 1000.0 + if new_duration > max_segment_sec: + break + current_end_ms = next_seg.end_ms + current_duration_sec = new_duration + j += 1 + + # 创建合并后的片段 + merged_seg = SpeakerSegment( + start_ms=current_start_ms, + end_ms=current_end_ms, + speaker_id=seg.speaker_id, + ) + merged.append(merged_seg) + + if j > i + 1: + logger.debug( + f"[第一层] {seg.speaker_id}: " + f"累积合并了 {j - i} 个片段,结果 {merged_seg.duration_sec:.1f}s" + ) + i = j + + # 第二层:60s累积合并 + final_merged = [] + i = 0 + while i < len(merged): + seg = merged[i] + current_start_ms = seg.start_ms + current_end_ms = seg.end_ms + j = i + 1 + + # 累积合并,只要 <= 60s 且同说话人 + while j < len(merged): + next_seg = merged[j] + if next_seg.speaker_id != seg.speaker_id: + break + new_duration = (next_seg.end_ms - current_start_ms) / 1000.0 + if new_duration > max_segment_sec: + break + current_end_ms = next_seg.end_ms + j += 1 + + merged_seg = SpeakerSegment( + start_ms=current_start_ms, + end_ms=current_end_ms, + speaker_id=seg.speaker_id, + ) + final_merged.append(merged_seg) + + if j > i + 1: + logger.debug( + f"[第二层] {seg.speaker_id}: " + f"合并了 {j - i} 个片段" + ) + i = j + + return final_merged + + def _find_low_energy_boundary_ms( + self, + audio_data: np.ndarray, + sample_rate: int, + lower_ms: int, + upper_ms: int, + ) -> int: + lower_ms = max(0, lower_ms) + upper_ms = max(lower_ms, upper_ms) + if sample_rate <= 0 or audio_data.size == 0: + return upper_ms + + context_samples = max( + 1, int(sample_rate * self.LOW_ENERGY_CONTEXT_MS / 1000) + ) + candidate_points = list( + range(lower_ms, upper_ms + 1, self.LOW_ENERGY_STEP_MS) + ) + if not candidate_points or candidate_points[-1] != upper_ms: + candidate_points.append(upper_ms) + + best_ms = upper_ms + best_energy = float("inf") + audio_length = int(audio_data.shape[0]) + + for candidate_ms in candidate_points: + center_sample = int(candidate_ms * sample_rate / 1000) + start_sample = max(0, center_sample - context_samples // 2) + end_sample = min(audio_length, center_sample + context_samples // 2) + if start_sample >= end_sample: + continue + + window = audio_data[start_sample:end_sample] + energy = float(np.mean(np.square(window))) + if energy <= best_energy: + best_energy = energy + best_ms = candidate_ms + + return best_ms + + def split_long_segments( + self, + segments: List[SpeakerSegment], + audio_data: np.ndarray, + sample_rate: int, + ) -> List[SpeakerSegment]: + max_segment_ms = int(settings.MAX_SEGMENT_SEC * 1000) + if max_segment_ms <= 0: + return segments + + split_segments: List[SpeakerSegment] = [] + for seg in segments: + if seg.duration_ms <= max_segment_ms: + split_segments.append(seg) + continue + + current_start_ms = seg.start_ms + while seg.end_ms - current_start_ms > max_segment_ms: + hard_boundary_ms = current_start_ms + max_segment_ms + lower_boundary_ms = max( + current_start_ms + self.min_segment_ms, + hard_boundary_ms - self.LOW_ENERGY_SEARCH_WINDOW_MS, + ) + boundary_ms = self._find_low_energy_boundary_ms( + audio_data=audio_data, + sample_rate=sample_rate, + lower_ms=lower_boundary_ms, + upper_ms=hard_boundary_ms, + ) + if boundary_ms <= current_start_ms: + boundary_ms = hard_boundary_ms + + split_segments.append( + SpeakerSegment( + start_ms=current_start_ms, + end_ms=boundary_ms, + speaker_id=seg.speaker_id, + ) + ) + current_start_ms = boundary_ms + + remaining_ms = seg.end_ms - current_start_ms + if remaining_ms >= self.min_segment_ms: + split_segments.append( + SpeakerSegment( + start_ms=current_start_ms, + end_ms=seg.end_ms, + speaker_id=seg.speaker_id, + ) + ) + elif split_segments: + split_segments[-1].end_ms = seg.end_ms + + if len(split_segments) != len(segments): + logger.info( + "Split long speaker segments by low energy: {} -> {}, max={}s", + len(segments), + len(split_segments), + settings.MAX_SEGMENT_SEC, + ) + return split_segments + + def split_audio_by_speakers( + self, + audio_path: str, + output_dir: Optional[str] = None, + ) -> List[SpeakerSegment]: + """完整的说话人分离流程 + + 流程: + 1. 执行CAM++说话人分离 + 2. 智能合并短片段(两层合并策略) + - 第一层:<10s片段累积合并 + - 第二层:60s累积合并 + 3. 提取音频数据,保存临时文件 + + Args: + audio_path: 音频文件路径 + output_dir: 输出目录 + + Returns: + SpeakerSegment 列表 + """ + try: + # 1. 执行说话人分离 + raw_segments = self.diarize(audio_path) + + if not raw_segments: + logger.warning("说话人分离未检测到任何片段") + return [] + + # 2. 智能合并短片段(第一个<10s的同说话人片段向后合并) + final_segments = self.merge_short_segments(raw_segments) + + # 3. Load audio before low-energy splitting and segment extraction. + logger.info("加载音频并提取片段...") + audio_data, sr = librosa.load(audio_path, sr=self.DEFAULT_SAMPLE_RATE) + sample_rate = int(sr) + + final_segments = self.split_long_segments( + final_segments, + audio_data, + sample_rate, + ) + logger.info(f"智能合并完成: {len(raw_segments)} → {len(final_segments)} 个片段") + + output_dir = output_dir or settings.TEMP_DIR + os.makedirs(output_dir, exist_ok=True) + + for idx, seg in enumerate(final_segments): + start_sample = int(seg.start_ms / 1000 * sample_rate) + end_sample = int(seg.end_ms / 1000 * sample_rate) + + seg.audio_data = audio_data[start_sample:end_sample] + + # 保存临时文件 + temp_file = tempfile.NamedTemporaryFile( + delete=False, + suffix=".wav", + dir=output_dir, + prefix=f"{seg.speaker_id}_{idx:03d}_", + ) + temp_path = temp_file.name + temp_file.close() + + sf.write(temp_path, seg.audio_data, sample_rate) + seg.temp_file = temp_path + + # 统计 + unique_speakers = sorted(set(seg.speaker_id for seg in final_segments)) + logger.info( + f"音频分割完成: {len(final_segments)} 个片段, " + f"{len(unique_speakers)} 个说话人" + ) + for spk in unique_speakers: + spk_segs = [s for s in final_segments if s.speaker_id == spk] + total_time = sum(s.duration_sec for s in spk_segs) + logger.info(f" {spk}: {len(spk_segs)} 片段, {total_time:.2f}s") + + return final_segments + + except Exception as e: + logger.error(f"说话人分离流程失败: {e}") + raise DefaultServerErrorException(f"说话人分离失败: {str(e)}") + + @staticmethod + def cleanup_segments(segments: List[SpeakerSegment]) -> None: + """清理临时文件""" + for seg in segments: + if seg.temp_file and os.path.exists(seg.temp_file): + try: + os.remove(seg.temp_file) + except Exception as e: + logger.warning(f"清理临时文件失败: {seg.temp_file}, {e}") diff --git a/app/utils/text_processing.py b/app/utils/text_processing.py new file mode 100644 index 0000000..68b6ab3 --- /dev/null +++ b/app/utils/text_processing.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +""" +基于itntext的ITN(逆文本标准化)工具模块 +使用itntext库提供高质量的中文ITN处理 +""" + +import logging + +logger = logging.getLogger(__name__) + +# itntext导入 - 延迟导入以避免初始化问题 +_itntext_normalizer = None + + +def _get_normalizer(): + """获取itntext标准化器实例(单例模式)""" + global _itntext_normalizer + if _itntext_normalizer is None: + try: + from itntext import Normalizer + _itntext_normalizer = Normalizer(lang="zh", operator="itn") + logger.info("itntext ITN模块初始化成功") + except ImportError as e: + logger.error(f"导入itntext失败: {e}") + raise ImportError("请安装itntext库: pip install itntext") + except Exception as e: + logger.error(f"初始化itntext失败: {e}") + raise + return _itntext_normalizer + + +def apply_itn_to_text(text: str) -> str: + """ + 对文本应用逆文本标准化(ITN) + 使用itntext库进行高质量的中文ITN处理 + + Args: + text: 语音识别结果文本 + + Returns: + 应用ITN后的文本 + """ + if not text or not text.strip(): + return text + + try: + normalizer = _get_normalizer() + result = normalizer.normalize(text) + logger.debug(f"ITN处理: '{text}' -> '{result}'") + return result + except Exception as e: + logger.warning(f"ITN处理失败: {text}, 错误: {str(e)}") + return text + + +def normalize_asr_text(text: str, enable_itn: bool) -> str: + if not enable_itn: + return text + return apply_itn_to_text(text) diff --git a/crg-mcp-plugin/README.md b/crg-mcp-plugin/README.md new file mode 100644 index 0000000..ff2626e --- /dev/null +++ b/crg-mcp-plugin/README.md @@ -0,0 +1,68 @@ +# crg-mcp — code-review-graph MCP 接入插件 + +把已部署在本机的 `D:\github-project\code-review-graph` MCP 服务接入 PI-Desktop, +让它的工具以原生 agent 工具的形式出现。**未修改该项目任何文件。** + +## 接入原理 + +PI-Desktop 的 MCP 客户端由插件宿主承载:宿主读取插件 `manifest.json` 里的 +`contributes.mcpServers`,自行拉起进程、完成 MCP 握手,并把上游每个工具发布为 +`plugin_<插件id>__<工具名>`。因此这里只需声明,无需自己写 JSON-RPC。 + +## 文件 + +| 文件 | 作用 | +| --- | --- | +| `manifest.json` | 声明 stdio MCP 服务 + `mcp.server.local` 权限 | +| `crg.cmd` | 启动包装脚本(修环境后 exec `python -m code_review_graph serve`) | +| `main.js` | 空加载器,客户端生命周期归宿主管理 | +| `dist/crg-mcp-1.0.0.piplug` | 可安装包 | + +## 两个必须保留的环境修正 + +宿主的 `mcpProcessEnv()` 只向子进程传递 +`PATH / SystemRoot / windir / TEMP / TMP / LANG`(加插件声明的 env),所以: + +1. **`PYTHONPATH` 必须注入。** 项目的 editable 安装记录 + `.venv\Lib\site-packages\_editable_impl_code_review_graph.pth` 指向 + `D:\github_project\code-review-graph`(下划线),而项目实际位于 + `D:\github-project\code-review-graph`(连字符),因此直接 + `import code_review_graph` 会 `ModuleNotFoundError`。 + 同理 `.venv\Scripts\code-review-graph.exe` 也不能用 + (`error: uv trampoline failed to canonicalize script path`)——包装脚本绕过了 + 这两点,改用 `python -m code_review_graph`。 +2. **`USERPROFILE` / `HOMEDRIVE` / `HOMEPATH` 必须补齐。** + `code_review_graph/constants.py` 在 import 期调用 `Path.home()`, + 精简环境下会抛 `RuntimeError: Could not determine home directory.` + +## 暴露的工具(10 个) + +默认通过 `CRG_TOOLS` 只开放审查相关的 10 个工具(上游共 30 个,全开会显著占上下文): + +`build_or_update_graph_tool`、`run_postprocess_tool`、`get_minimal_context_tool`、 +`get_review_context_tool`、`get_impact_radius_tool`、`query_graph_tool`、 +`semantic_search_nodes_tool`、`detect_changes_tool`、`list_graph_stats_tool`、 +`get_affected_flows_tool` + +工具名前缀为 `plugin_crg_mcp_crg_`,例如 `plugin_crg_mcp_crg_query_graph_tool`。 + +### 调整暴露范围 / 目标仓库 + +编辑 `crg.cmd`: + +- 改 `CRG_TOOLS=...`:改工具白名单(置空并删除该行 = 暴露全部 30 个)。 +- 改 `CRG_REPO=...`:改被分析的仓库根目录(默认指向 code-review-graph 自身, + 即已建好图的那个库)。 + +改完重启 PI-Desktop 生效。 + +## 为何用 stdio 而不是 HTTP + +`serve --http`(127.0.0.1:5555/mcp)实测可用,但宿主只负责 spawn,不会托管一个 +常驻服务;HTTP 需要外部进程守护,进程一掉工具就全空。stdio 由宿主拉起并在每次 +调用时自动重连握手,更稳。 + +## 校验方式 + +装好后对 agent 说“用图谱统计一下仓库规模”,应命中 +`plugin_crg_mcp_crg_list_graph_stats_tool` 并返回节点/边数量。 diff --git a/crg-mcp-plugin/crg.cmd b/crg-mcp-plugin/crg.cmd new file mode 100644 index 0000000..4b695f5 --- /dev/null +++ b/crg-mcp-plugin/crg.cmd @@ -0,0 +1,57 @@ +@echo off +chcp 65001 >nul +setlocal + +rem --------------------------------------------------------------------------- +rem code-review-graph MCP launcher for PI-Desktop. +rem +rem The MCP host spawns this with a minimal environment (PATH, SystemRoot, +rem windir, TEMP, TMP, LANG plus the manifest's env block) and cwd = plugin +rem directory, with stdin/stdout used for JSON-RPC. Never write to stdout. +rem --------------------------------------------------------------------------- + +if defined CRG_HOME goto have_home +set "CRG_HOME=D:\github-project\code-review-graph" +:have_home + +rem code_review_graph/constants.py calls Path.home() at import time; without a +rem user profile the server dies with "Could not determine home directory.". +if defined USERPROFILE goto have_profile +set "USERPROFILE=C:\Users\%USERNAME%" +:have_profile +if defined HOMEDRIVE goto have_hd +set "HOMEDRIVE=C:" +:have_hd +if defined HOMEPATH goto have_hp +set "HOMEPATH=\Users\%USERNAME%" +:have_hp +if defined APPDATA set "APPDATA=%USERPROFILE%\AppData\Roaming" +if defined LOCALAPPDATA set "LOCALAPPDATA=%USERPROFILE%\AppData\Local" + +set "PYTHONUTF8=1" +set "PYTHONIOENCODING=utf-8" + +rem The editable install's .pth points at D:\github_project\... (underscore) +rem while the checkout lives at D:\github-project\... (hyphen), so the package +rem is only importable with the checkout explicitly on sys.path. +set "PYTHONPATH=%CRG_HOME%" + +rem The venv's code-review-graph.exe is a broken uv trampoline +rem ("failed to canonicalize script path"), so prefer a working interpreter. +set "CRG_PY=%CRG_HOME%\.venv\Scripts\python.exe" +if exist "%CRG_PY%" goto py_ready +set "CRG_PY=python" +:py_ready + +rem Upstream ships 30 tools; keep the surface small unless overridden. +if defined CRG_TOOLS goto tools_ready +set "CRG_TOOLS=build_or_update_graph_tool,run_postprocess_tool,get_minimal_context_tool,get_review_context_tool,get_impact_radius_tool,query_graph_tool,semantic_search_nodes_tool,detect_changes_tool,list_graph_stats_tool,get_affected_flows_tool" +:tools_ready + +if defined CRG_REPO goto repo_ready +set "CRG_REPO=%CRG_HOME%" +:repo_ready + +"%CRG_PY%" -m code_review_graph serve --repo "%CRG_REPO%" + +endlocal diff --git a/crg-mcp-plugin/dist/crg-mcp-1.0.0.piplug b/crg-mcp-plugin/dist/crg-mcp-1.0.0.piplug new file mode 100644 index 0000000..5a307a4 Binary files /dev/null and b/crg-mcp-plugin/dist/crg-mcp-1.0.0.piplug differ diff --git a/crg-mcp-plugin/main.js b/crg-mcp-plugin/main.js new file mode 100644 index 0000000..6167bcf --- /dev/null +++ b/crg-mcp-plugin/main.js @@ -0,0 +1,18 @@ +/** + * crg-mcp — thin loader for the code-review-graph MCP bridge. + * + * All of the wiring lives in manifest.json under `contributes.mcpServers`: + * PI-Desktop spawns `crg.cmd` (stdio MCP), performs the handshake, and + * publishes every upstream tool as `plugin_crg_mcp_crg_`. Nothing has + * to be registered from here — the host owns the client, the retries, and the + * tool lifecycle. This module only keeps the plugin loadable and offers a + * place for future local helpers. + */ + +async function onLoad() { + // The MCP client is owned by the host; no tool registration needed. +} + +async function onUnload() {} + +module.exports = { onLoad, onUnload }; diff --git a/crg-mcp-plugin/manifest.json b/crg-mcp-plugin/manifest.json new file mode 100644 index 0000000..f429523 --- /dev/null +++ b/crg-mcp-plugin/manifest.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 1, + "id": "crg-mcp", + "name": "Code Review Graph MCP", + "version": "1.0.0", + "description": "Bridges the locally deployed code-review-graph MCP server (D:\\github-project\\code-review-graph) into the agent as native tools.", + "main": "main.js", + "contributes": { + "mcpServers": [ + { + "id": "crg", + "label": "Code Review Graph", + "transport": "stdio", + "command": "crg.cmd", + "env": { + "PYTHONPATH": "D:\\github-project\\code-review-graph", + "USERPROFILE": "C:\\Users\\admin", + "HOMEDRIVE": "C:", + "HOMEPATH": "\\Users\\admin" + } + } + ] + }, + "permissions": [ + "mcp.server.local" + ], + "engines": { + "piDesktop": ">=0.1.0" + }, + "activationEvents": [ + "onStartup" + ] +} diff --git a/crg-mcp-plugin/selfcheck.ps1 b/crg-mcp-plugin/selfcheck.ps1 new file mode 100644 index 0000000..5270319 --- /dev/null +++ b/crg-mcp-plugin/selfcheck.ps1 @@ -0,0 +1,96 @@ +<# + Self-check for the crg-mcp plugin. + + Drives crg.cmd exactly the way the PI-Desktop MCP host does — `cmd /c crg.cmd` + with piped stdio and cwd = this folder — then reports the MCP handshake, the + tool list, and one real tool call. Run it from PowerShell: + + powershell -NoProfile -ExecutionPolicy Bypass -File .\selfcheck.ps1 +#> +$ErrorActionPreference = 'Stop' + +$dir = Split-Path -Parent $MyInvocation.MyCommand.Path + +# Mirror the host's minimal environment plus the manifest's env block. +foreach ($k in 'PATH', 'SystemRoot', 'TEMP', 'TMP') { + if (-not (Test-Path "Env:$k")) { Write-Warning "missing $k in ambient env" } +} + +$psi = New-Object System.Diagnostics.ProcessStartInfo +$psi.FileName = 'cmd.exe' +$psi.Arguments = '/c crg.cmd' +$psi.WorkingDirectory = $dir +$psi.RedirectStandardInput = $true +$psi.RedirectStandardOutput = $true +$psi.RedirectStandardError = $true +$psi.UseShellExecute = $false +$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8 + +$proc = [System.Diagnostics.Process]::Start($psi) + +function Send($obj) { + $proc.StandardInput.WriteLine(($obj | ConvertTo-Json -Compress -Depth 8)) + $proc.StandardInput.Flush() +} + +function ReadLine([int]$waitSeconds = 25) { + $task = $proc.StandardOutput.ReadLineAsync() + if ($task.Wait([TimeSpan]::FromSeconds($waitSeconds))) { return $task.Result } + return $null +} + +Send @{ + jsonrpc = '2.0'; id = 1; method = 'initialize' + params = @{ + protocolVersion = '2025-06-18' + capabilities = @{} + clientInfo = @{ name = 'crg-selfcheck'; version = '1' } + } +} + +$initLine = ReadLine 40 +if (-not $initLine) { + Write-Host 'HANDSHAKE FAILED: no stdout from crg.cmd' -ForegroundColor Red + Write-Host '--- stderr ---' + Write-Host $proc.StandardError.ReadToEnd() + try { $proc.Kill() } catch { } + exit 1 +} + +$init = $initLine | ConvertFrom-Json +Write-Host ("HANDSHAKE OK server={0} {1}" -f $init.result.serverInfo.name, $init.result.serverInfo.version) -ForegroundColor Green + +Send @{ jsonrpc = '2.0'; method = 'notifications/initialized'; params = @{} } +Send @{ jsonrpc = '2.0'; id = 2; method = 'tools/list'; params = @{} } + +$toolsLine = ReadLine +if (-not $toolsLine) { + Write-Host 'tools/list returned nothing' -ForegroundColor Red + try { $proc.Kill() } catch { } + exit 1 +} +$tools = ($toolsLine | ConvertFrom-Json).result.tools +Write-Host ("TOOLS: {0}" -f $tools.Count) -ForegroundColor Green +foreach ($t in $tools) { Write-Host (" plugin_crg_mcp_crg_{0}" -f $t.name) } + +Send @{ + jsonrpc = '2.0'; id = 3; method = 'tools/call' + params = @{ name = 'list_graph_stats_tool'; arguments = @{} } +} +$callLine = ReadLine 40 +if ($callLine) { + $call = $callLine | ConvertFrom-Json + if ($call.result) { + $text = $call.result.content[0].text + Write-Host 'TOOL CALL OK' -ForegroundColor Green + Write-Host (' ' + ($text -split "`n")[0..3] -join ' | ') + } + else { + Write-Host ("TOOL CALL ERROR: {0}" -f ($call | ConvertTo-Json -Compress -Depth 6)) -ForegroundColor Red + } +} +else { + Write-Host 'TOOL CALL: no response' -ForegroundColor Red +} + +try { $proc.Kill() } catch { } diff --git a/docker-compose-cpu.yml b/docker-compose-cpu.yml new file mode 100644 index 0000000..8cca49c --- /dev/null +++ b/docker-compose-cpu.yml @@ -0,0 +1,35 @@ +name: qwen3-asr + +services: + qwen3-asr: + image: ${ASR_IMAGE:-unis/qwen3-asr:cpu-latest} + container_name: qwen3-asr-cpu + ports: + - "${NGINX_PORT:-17003}:8000" + volumes: + - ${DATA_STORAGE_DIR:-/opt/dep/asr/data}:/app/data + - ${MODEL_STORAGE_DIR:-/opt/dep/asr/models}:/app/models + environment: + ACCELERATOR: cpu + API_KEY: ${API_KEY:-} + MODELS_DIR: /app/models + DATA_DIR: /app/data + TEMP_DIR: /app/data/temp + LOG_FILE: /app/data/logs/qwen3-asr.log + TASK_STATE_DIR: /app/data/tasks + TASK_RETENTION_HOURS: ${TASK_RETENTION_HOURS:-24} + MODELSCOPE_CACHE: /app + MODELSCOPE_PATH: /app/models + QWEN3_ASR_MODEL: ${QWEN3_ASR_MODEL:-} + SPEAKER_DB_ENABLED: ${SPEAKER_DB_ENABLED:-true} + DB_HOST: ${DB_HOST:-127.0.0.1} + DB_PORT: ${DB_PORT:-5432} + DB_USER: ${DB_USER:-postgres} + DB_PASSWORD: ${DB_PASSWORD:-postgres} + DB_NAME: ${DB_NAME:-asr_db} + SV_MODEL: ${SV_MODEL:-iic/speech_campplus_sv_zh-cn_16k-common} + SV_THRESHOLD: ${SV_THRESHOLD:-0.6} + REALTIME_SESSION_RESUME_TTL_SEC: ${REALTIME_SESSION_RESUME_TTL_SEC:-120} + NGINX_RATE_LIMIT_RPS: ${NGINX_RATE_LIMIT_RPS:-0} + NGINX_RATE_LIMIT_BURST: ${NGINX_RATE_LIMIT_BURST:-0} + restart: unless-stopped diff --git a/docker-compose-iluvatar.yml b/docker-compose-iluvatar.yml new file mode 100644 index 0000000..598f0e8 --- /dev/null +++ b/docker-compose-iluvatar.yml @@ -0,0 +1,50 @@ +name: qwen3-asr-iluvatar + +services: + qwen3-asr: + image: ${ASR_IMAGE:-unis/qwen3-asr:iluvatar-latest} + container_name: qwen3-asr-iluvatar + network_mode: host + pid: host + ipc: host + privileged: true + cap_add: + - ALL + volumes: + - ${ILUVATAR_USR_SRC:-/usr/src}:/usr/src + - ${ILUVATAR_LIB_MODULES:-/lib/modules}:/lib/modules + - ${ILUVATAR_DEV:-/dev}:/dev + - ${ILUVATAR_HOME:-/home}:/home + - ${ILUVATAR_DATA:-/data}:/data + - ${DATA_STORAGE_DIR:-/opt/dep/asr/data}:/app/data + - ${MODEL_STORAGE_DIR:-/opt/dep/asr/models}:/app/models + environment: + ACCELERATOR: iluvatar + DEVICE: ${DEVICE:-auto} + PORT: ${NGINX_PORT:-17003} + API_KEY: ${API_KEY:-} + MODELS_DIR: /app/models + DATA_DIR: /app/data + TEMP_DIR: /app/data/temp + LOG_FILE: /app/data/logs/qwen3-asr.log + TASK_STATE_DIR: /app/data/tasks + TASK_RETENTION_HOURS: ${TASK_RETENTION_HOURS:-24} + MODELSCOPE_CACHE: /app + MODELSCOPE_PATH: /app/models + QWEN3_ASR_MODEL: ${QWEN3_ASR_MODEL:-} + ASR_DEPLOY_TOPOLOGY: ${ASR_DEPLOY_TOPOLOGY:-isolated} + QWEN_GPU_MEMORY_UTILIZATION: ${QWEN_GPU_MEMORY_UTILIZATION:-} + QWEN_VLLM_ENFORCE_EAGER: ${QWEN_VLLM_ENFORCE_EAGER:-true} + SPEAKER_DB_ENABLED: ${SPEAKER_DB_ENABLED:-true} + DB_HOST: ${DB_HOST:-127.0.0.1} + DB_PORT: ${DB_PORT:-5432} + DB_USER: ${DB_USER:-postgres} + DB_PASSWORD: ${DB_PASSWORD:-postgres} + DB_NAME: ${DB_NAME:-asr_db} + SV_MODEL: ${SV_MODEL:-iic/speech_campplus_sv_zh-cn_16k-common} + SV_THRESHOLD: ${SV_THRESHOLD:-0.6} + REALTIME_SESSION_RESUME_TTL_SEC: ${REALTIME_SESSION_RESUME_TTL_SEC:-120} + ASR_VISIBLE_DEVICES: ${ASR_VISIBLE_DEVICES:-0} + NGINX_RATE_LIMIT_RPS: ${NGINX_RATE_LIMIT_RPS:-0} + NGINX_RATE_LIMIT_BURST: ${NGINX_RATE_LIMIT_BURST:-0} + restart: unless-stopped diff --git a/docker-compose-metax.yml b/docker-compose-metax.yml new file mode 100644 index 0000000..baa580c --- /dev/null +++ b/docker-compose-metax.yml @@ -0,0 +1,47 @@ +name: qwen3-asr-metax + +services: + qwen3-asr: + image: ${ASR_IMAGE:-unis/qwen3-asr:metax-latest} + container_name: qwen3-asr-metax + network_mode: host + pid: host + ipc: host + privileged: true + cap_add: + - ALL + volumes: + - ${METAX_DEV:-/dev}:/dev + - ${METAX_DRIVER_DIR:-/opt/mxdriver}:/opt/mxdriver:ro + - ${DATA_STORAGE_DIR:-/opt/dep/asr/data}:/app/data + - ${MODEL_STORAGE_DIR:-/opt/dep/asr/models}:/app/models + environment: + ACCELERATOR: metax + DEVICE: ${DEVICE:-auto} + PORT: ${NGINX_PORT:-17003} + API_KEY: ${API_KEY:-} + MODELS_DIR: /app/models + DATA_DIR: /app/data + TEMP_DIR: /app/data/temp + LOG_FILE: /app/data/logs/qwen3-asr.log + TASK_STATE_DIR: /app/data/tasks + TASK_RETENTION_HOURS: ${TASK_RETENTION_HOURS:-24} + MODELSCOPE_CACHE: /app + MODELSCOPE_PATH: /app/models + QWEN3_ASR_MODEL: ${QWEN3_ASR_MODEL:-} + ASR_DEPLOY_TOPOLOGY: ${ASR_DEPLOY_TOPOLOGY:-isolated} + QWEN_GPU_MEMORY_UTILIZATION: ${QWEN_GPU_MEMORY_UTILIZATION:-} + QWEN_VLLM_ENFORCE_EAGER: ${QWEN_VLLM_ENFORCE_EAGER:-true} + SPEAKER_DB_ENABLED: ${SPEAKER_DB_ENABLED:-true} + DB_HOST: ${DB_HOST:-127.0.0.1} + DB_PORT: ${DB_PORT:-5432} + DB_USER: ${DB_USER:-postgres} + DB_PASSWORD: ${DB_PASSWORD:-postgres} + DB_NAME: ${DB_NAME:-asr_db} + SV_MODEL: ${SV_MODEL:-iic/speech_campplus_sv_zh-cn_16k-common} + SV_THRESHOLD: ${SV_THRESHOLD:-0.6} + REALTIME_SESSION_RESUME_TTL_SEC: ${REALTIME_SESSION_RESUME_TTL_SEC:-120} + ASR_VISIBLE_DEVICES: ${ASR_VISIBLE_DEVICES:-0} + NGINX_RATE_LIMIT_RPS: ${NGINX_RATE_LIMIT_RPS:-0} + NGINX_RATE_LIMIT_BURST: ${NGINX_RATE_LIMIT_BURST:-0} + restart: unless-stopped diff --git a/docker-compose-mthreads.yml b/docker-compose-mthreads.yml new file mode 100644 index 0000000..7e344e5 --- /dev/null +++ b/docker-compose-mthreads.yml @@ -0,0 +1,50 @@ +name: qwen3-asr-mthreads + +services: + qwen3-asr: + image: ${ASR_IMAGE:-unis/qwen3-asr:mthreads-latest} + container_name: qwen3-asr-mthreads + network_mode: host + pid: host + ipc: host + privileged: true + cap_add: + - ALL + volumes: + - ${MTHREADS_DEV:-/dev}:/dev + - ${MTHREADS_USR_SRC:-/usr/src}:/usr/src + - ${MTHREADS_LIB_MODULES:-/lib/modules}:/lib/modules + - ${MTHREADS_HOME:-/home}:/home + - ${MTHREADS_DATA:-/data}:/data + - ${DATA_STORAGE_DIR:-/opt/dep/asr/data}:/app/data + - ${MODEL_STORAGE_DIR:-/opt/dep/asr/models}:/app/models + environment: + ACCELERATOR: mthreads + DEVICE: ${DEVICE:-auto} + PORT: ${NGINX_PORT:-17003} + API_KEY: ${API_KEY:-} + MODELS_DIR: /app/models + DATA_DIR: /app/data + TEMP_DIR: /app/data/temp + LOG_FILE: /app/data/logs/qwen3-asr.log + TASK_STATE_DIR: /app/data/tasks + TASK_RETENTION_HOURS: ${TASK_RETENTION_HOURS:-24} + MODELSCOPE_CACHE: /app + MODELSCOPE_PATH: /app/models + QWEN3_ASR_MODEL: ${QWEN3_ASR_MODEL:-} + ASR_DEPLOY_TOPOLOGY: ${ASR_DEPLOY_TOPOLOGY:-isolated} + QWEN_GPU_MEMORY_UTILIZATION: ${QWEN_GPU_MEMORY_UTILIZATION:-} + QWEN_VLLM_ENFORCE_EAGER: ${QWEN_VLLM_ENFORCE_EAGER:-true} + SPEAKER_DB_ENABLED: ${SPEAKER_DB_ENABLED:-true} + DB_HOST: ${DB_HOST:-127.0.0.1} + DB_PORT: ${DB_PORT:-5432} + DB_USER: ${DB_USER:-postgres} + DB_PASSWORD: ${DB_PASSWORD:-postgres} + DB_NAME: ${DB_NAME:-asr_db} + SV_MODEL: ${SV_MODEL:-iic/speech_campplus_sv_zh-cn_16k-common} + SV_THRESHOLD: ${SV_THRESHOLD:-0.6} + REALTIME_SESSION_RESUME_TTL_SEC: ${REALTIME_SESSION_RESUME_TTL_SEC:-120} + ASR_VISIBLE_DEVICES: ${ASR_VISIBLE_DEVICES:-0} + NGINX_RATE_LIMIT_RPS: ${NGINX_RATE_LIMIT_RPS:-0} + NGINX_RATE_LIMIT_BURST: ${NGINX_RATE_LIMIT_BURST:-0} + restart: unless-stopped diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..da9392e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ +name: qwen3-asr + +services: + qwen3-asr: + image: ${ASR_IMAGE:-unis/qwen3-asr:gpu-latest} + container_name: qwen3-asr + ports: + - "${NGINX_PORT:-17003}:8000" + volumes: + - ${DATA_STORAGE_DIR:-/opt/dep/asr/data}:/app/data + - ${MODEL_STORAGE_DIR:-/opt/dep/asr/models}:/app/models + runtime: nvidia + environment: + ACCELERATOR: nvidia + API_KEY: ${API_KEY:-} + MODELS_DIR: /app/models + DATA_DIR: /app/data + TEMP_DIR: /app/data/temp + LOG_FILE: /app/data/logs/qwen3-asr.log + TASK_STATE_DIR: /app/data/tasks + TASK_RETENTION_HOURS: ${TASK_RETENTION_HOURS:-24} + MODELSCOPE_CACHE: /app + MODELSCOPE_PATH: /app/models + QWEN3_ASR_MODEL: ${QWEN3_ASR_MODEL:-} + ASR_DEPLOY_TOPOLOGY: ${ASR_DEPLOY_TOPOLOGY:-isolated} + QWEN_VLLM_ENFORCE_EAGER: ${QWEN_VLLM_ENFORCE_EAGER:-true} + SPEAKER_DB_ENABLED: ${SPEAKER_DB_ENABLED:-true} + DB_HOST: ${DB_HOST:-127.0.0.1} + DB_PORT: ${DB_PORT:-5432} + DB_USER: ${DB_USER:-postgres} + DB_PASSWORD: ${DB_PASSWORD:-postgres} + DB_NAME: ${DB_NAME:-asr_db} + SV_MODEL: ${SV_MODEL:-iic/speech_campplus_sv_zh-cn_16k-common} + SV_THRESHOLD: ${SV_THRESHOLD:-0.6} + REALTIME_SESSION_RESUME_TTL_SEC: ${REALTIME_SESSION_RESUME_TTL_SEC:-120} + NVIDIA_VISIBLE_DEVICES: all + ASR_VISIBLE_DEVICES: ${ASR_VISIBLE_DEVICES:-0} + NGINX_RATE_LIMIT_RPS: ${NGINX_RATE_LIMIT_RPS:-0} + NGINX_RATE_LIMIT_BURST: ${NGINX_RATE_LIMIT_BURST:-0} + restart: unless-stopped diff --git a/docs/README_zh.md b/docs/README_zh.md new file mode 100644 index 0000000..c0d29ff --- /dev/null +++ b/docs/README_zh.md @@ -0,0 +1,606 @@ +
+ +

Qwen3-ASR

+

开箱即用的本地私有化部署语音识别服务

+ +以 [Qwen3-ASR](https://github.com/QwenLM/Qwen3-ASR) 为核心的语音识别 API 服务,提供 NVIDIA CUDA vLLM、沐曦 MACA vLLM 与 CPU Rust 后端,兼容阿里云语音 API 和 OpenAI Audio API,并保留 Paraformer realtime WebSocket 能力。 + +--- + +![Static Badge](https://img.shields.io/badge/Python-3.10+-blue?logo=python) +![Static Badge](https://img.shields.io/badge/Torch-2.11.0-%23EE4C2C?logo=pytorch&logoColor=white) +![Static Badge](https://img.shields.io/badge/CUDA-13.0_default-%2376B900?logo=nvidia&logoColor=white) + +
+ +## 在线演示站点 + +- **在线体验**: https://asr.vect.one + +## 演示 + +[![演示](../demo/demo.png)](https://media.cdn.vect.one/qwenasr_client_demo.mp4) + +## Release 1.0.1 + +> `v1.0.1` 是当前补丁版本。`v1.0.0` 相对于早期 `main` 分支引入了一轮大规模 breaking refactor。 +> 如果你是从 `main` 升级过来,请先阅读 release 说明,再决定是否沿用旧的部署与运行时假设。 +> +> 关键 breaking changes: +> - Python 依赖管理已经切到 `uv`(`pyproject.toml` + `uv.lock`),`requirements*.txt` 已移除 +> - 运行时栈改成 `NVIDIA/沐曦 GPU -> vLLM`、`CPU/macOS -> vendored QwenASR Rust` +> - `MLX` / Apple Silicon GPU 路径已移除,`mps` 会归一化到 `cpu` +> - macOS / Apple Silicon 现在默认总是 `qwen3-asr-0.6b`,可通过 `QWEN3_ASR_MODEL` 覆盖 +> - `ENABLED_MODELS` 已移除 + +## 主要特性 + +- **混合运行时栈** - 离线推理由自动选择的 Qwen3-ASR 提供,WebSocket 流式由 Paraformer realtime 能力提供 +- **说话人分离** - 基于 CAM++ 模型自动识别多说话人,返回说话人标记 +- **OpenAI API 兼容** - 支持 `/v1/audio/transcriptions` 端点,可直接使用 OpenAI SDK +- **阿里云 API 兼容** - 支持阿里云语音识别 RESTful API 和 WebSocket 流式协议 +- **WebSocket 流式识别** - 支持实时流式语音识别,低延迟 +- **智能远场过滤** - 流式 ASR 自动过滤远场声音和环境音,减少误触发 +- **智能音频分段** - 基于 VAD 的贪婪合并算法,自动切分长音频,避免包含过长静音 +- **GPU 批处理加速** - 支持批量推理,比逐个处理快 2-3 倍 +- **资源感知运行时** - 根据当前机器资源自动选择合适的 Qwen3-ASR 模型 + +## 致谢 + +- [Qwen3-ASR](https://github.com/QwenLM/Qwen3-ASR) 提供官方模型与多模态 / vLLM 使用方式 +- [QwenASR](https://github.com/huanglizhuo/QwenASR) 提供本项目 vendored 的 CPU Rust backend + +## 快速部署 + +### 1. Docker 部署(推荐) + +```bash +# 复制并编辑配置 +cp .env.example .env +# 编辑 .env 设置 API_KEY(可选) + +# Compose 默认挂载: +# /opt/dep/asr/models -> /app/models +# /opt/dep/asr/data -> /app/data +# /opt/dep/asr/data/logs、temp、tasks 都在 data 挂载内 + +# 启动服务(NVIDIA GPU 版本) +docker-compose up -d + +# 或沐曦 GPU 版本 +docker-compose -f docker-compose-metax.yml up -d + +# 或天数 GPU 版本 +docker-compose -f docker-compose-iluvatar.yml up -d + +# 或摩尔线程 / MUSA GPU 版本 +docker-compose -f docker-compose-mthreads.yml up -d + +# 或 CPU 版本 +docker-compose -f docker-compose-cpu.yml up -d + +# NVIDIA 多卡自动模式(每张可见卡自动拉起 1 个实例) +CUDA_VISIBLE_DEVICES=0,1,2,3 docker-compose up -d + +# 沐曦多卡自动模式 +METAX_VISIBLE_DEVICES=0,1 docker-compose -f docker-compose-metax.yml up -d + +# 天数多卡自动模式 +ILUVATAR_VISIBLE_DEVICES=0,1 docker-compose -f docker-compose-iluvatar.yml up -d + +# 摩尔线程 / MUSA 多卡自动模式 +MTHREADS_VISIBLE_DEVICES=0,1 docker-compose -f docker-compose-mthreads.yml up -d +``` + +服务访问地址: +- **API 端点**: `http://localhost:17003` +- **API 文档**: `http://localhost:17003/docs` + +可选的内置限流参数: +- `NGINX_RATE_LIMIT_RPS`(全局每秒请求上限,`0` 表示关闭) +- `NGINX_RATE_LIMIT_BURST`(全局突发请求数,`0` 时自动使用 RPS) + +**docker run 方式(替代):** + +```bash +# NVIDIA GPU 版本 +docker run -d --name qwen3-asr \ + --gpus all \ + -p 17003:8000 \ + -e ACCELERATOR=nvidia \ + -e CUDA_VISIBLE_DEVICES=0,1,2,3 \ + -e API_KEY=your_api_key \ + -v /opt/dep/asr/models:/app/models \ + -v /opt/dep/asr/data:/app/data \ + unis/qwen3-asr:gpu-latest + +# 沐曦 GPU 版本 +docker run -d --name qwen3-asr-metax \ + --privileged \ + --network=host \ + --pid=host \ + --ipc=host \ + -v /dev:/dev \ + -v /opt/mxdriver:/opt/mxdriver:ro \ + -e ACCELERATOR=metax \ + -e PORT=17003 \ + -e METAX_VISIBLE_DEVICES=0 \ + -v /opt/dep/asr/models:/app/models \ + -v /opt/dep/asr/data:/app/data \ + unis/qwen3-asr:metax-latest + +# CPU 版本 +docker run -d --name qwen3-asr \ + -p 17003:8000 \ + -v /opt/dep/asr/models:/app/models \ + -v /opt/dep/asr/data:/app/data \ + unis/qwen3-asr:cpu-latest +``` + +默认推荐将宿主机目录统一挂载到 `/opt/dep/asr` 下,模型目录结构如下: + +```text +/opt/dep/asr/models/ + Qwen/ + iic/ + damo/ +``` + +如果你希望改成自定义目录,也可以在启动前设置: + +```bash +export MODEL_STORAGE_DIR=/data/qwen3-asr-models +export DATA_STORAGE_DIR=/data/qwen3-asr-data +``` + +> **注意**: NVIDIA GPU 镜像默认使用 CUDA 13.0/cu130,并固定 `torch 2.11.0` + `vllm 0.20.0`。 +> 开发者可通过 Docker build args 自行构建 CUDA 12.6、CUDA 13.0 或其他后端组合。 +> 沐曦镜像使用 `Dockerfile.metax` 基于沐曦官方 vLLM 镜像融合本项目。现场部署使用 host network、privileged,并挂载 `/dev` 与 `/opt/mxdriver`,确保 `mx-smi` 查询和沐曦 PyTorch 运行时都能初始化设备。 +> 当前 CPU 镜像已通过内置 QwenASR Rust backend 支持 `qwen3-asr-0.6b`。默认 CPU 镜像使用可分发 Rust 构建目标;只有自建且构建机/部署机 CPU 同构时才建议设置 `QWENASR_RUST_TARGET_CPU=native`。 +> CUDA vLLM 与 CPU Rust 路径下,`word_timestamps=true` 都会自动调用 forced aligner;当前实际后端为 `CUDA -> vLLM`、`CPU/macOS -> vendored QwenASR Rust`。 +> Apple Silicon 上的 Qwen3-ASR 现已统一走 Rust CPU backend。 +> `start.py` 现在会强制把 vLLM 多进程方式设为 `spawn`,避免 CUDA 在 fork 子进程中重复初始化导致启动失败。 + +**自定义 GPU 后端构建:** + +```bash +# 默认 GPU 构建:CUDA 13.0 / PyTorch cu130 +docker build -t qwen3-asr:gpu-cu130 -f Dockerfile.gpu . + +# CUDA 12.6 构建,用于旧部署环境 +docker build -t qwen3-asr:gpu-cu126 -f Dockerfile.gpu \ + --build-arg PYTORCH_BASE_IMAGE=pytorch/pytorch:2.11.0-cuda12.6-cudnn9-runtime \ + --build-arg PYTORCH_CUDA_INDEX=https://download.pytorch.org/whl/cu126 \ + --build-arg CUDA_NVCC_PACKAGE=cuda-nvcc-12-6 \ + --build-arg TORCH_CUDA_ARCH_LIST="8.0;8.6;8.9" \ + . + +# CUDA 13.0 构建,用于需要 CUDA 13 工具链的环境 +docker build -t qwen3-asr:gpu-cu130 -f Dockerfile.gpu \ + --build-arg PYTORCH_BASE_IMAGE=pytorch/pytorch:2.11.0-cuda13.0-cudnn9-runtime \ + --build-arg PYTORCH_CUDA_INDEX=https://download.pytorch.org/whl/cu130 \ + --build-arg CUDA_NVCC_PACKAGE=cuda-nvcc-13-0 \ + --build-arg TORCH_CUDA_ARCH_LIST="12.0+PTX" \ + . + +# 沐曦构建:基于沐曦官方 vLLM 镜像融合本项目 +./scripts/package_vendor_gpu_image.sh \ + --vendor metax \ + --base-image <沐曦官方vLLM镜像名> \ + -v n260-3.7.0.38 + +# 天数构建:基于天数官方 vLLM 镜像融合本项目 +docker pull registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5 +./scripts/package_vendor_gpu_image.sh \ + --vendor iluvatar \ + --base-image registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5 \ + -v vllm0.17.0-4.4.0-v5 + +# 摩尔线程构建:基于摩尔线程官方 MUSA vLLM 镜像融合本项目 +docker pull registry.mthreads.com/presale/devtech/vllm_musa:s4000_4.3.5_d0519 +./scripts/package_vendor_gpu_image.sh \ + --vendor mthreads \ + --base-image registry.mthreads.com/presale/devtech/vllm_musa:s4000_4.3.5_d0519 \ + -v s4000_4.3.5_d0519 +``` + +沐曦 GPU 国产化离线交付请优先参考 [沐曦 GPU 国产化离线部署指南](./metax_offline_deployment.md)。 +天数 GPU 国产化离线交付请优先参考 [天数 GPU 国产化离线部署指南](./iluvatar_offline_deployment.md)。 +摩尔线程 GPU 国产化离线交付请优先参考 [摩尔线程 GPU 国产化离线部署指南](./mthreads_offline_deployment.md)。 + +**内网部署**:现在可以直接生成一个带时间戳和 CPU/GPU 标识的离线交付目录,里面包含镜像包、compose、`.env` 模板、目录初始化脚本和使用说明。离线导出脚本使用普通 `docker build` + `docker save`,不依赖 `buildx`: + +```bash +# 1. 生成离线交付目录 +./export_offline_bundle.sh --type gpu +# 或 +./export_offline_bundle.sh --type cpu +# 或沐曦 GPU +./export_offline_bundle.sh \ + --type metax \ + --metax-base cr.metax-tech.com/public-ai-release/maca/vllm-metax:0.17.0-maca.ai3.5.3.307-torch2.8-py312-ubuntu22.04-amd64 \ + --skip-models +# 或天数 GPU +./export_offline_bundle.sh --type iluvatar --iluvatar-base registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5 +# 或摩尔线程 GPU +./export_offline_bundle.sh --type mthreads --mthreads-base registry.mthreads.com/presale/devtech/vllm_musa:s4000_4.3.5_d0519 +# 或一次同时打包 GPU + CPU +./export_offline_bundle.sh --type all + +# 2. 单独准备模型,不删除已有模型文件 +./scripts/download-models.sh --models-dir /opt/dep/asr/models + +# 3. 把交付目录复制到内网服务器 +scp -r build-file/<时间戳>-all user@server:/opt/dep/asr/ + +# 4. 在内网服务器上导入并启动 +cd /opt/dep/asr/<时间戳>-all +./init_host_dirs.sh +gunzip -c qwen3-asr-gpu-<时间戳>-amd64.tar.gz | docker load +gunzip -c qwen3-asr-cpu-<时间戳>-amd64.tar.gz | docker load +# NVIDIA GPU +docker compose up -d +# 或沐曦 GPU +# docker compose -f docker-compose-metax.yml up -d +# 或天数 GPU +# docker compose -f docker-compose-iluvatar.yml up -d +# 或摩尔线程 GPU +# docker compose -f docker-compose-mthreads.yml up -d +# 或 CPU +# docker compose -f docker-compose-cpu.yml up -d +``` + +> 详细部署说明请查看 [部署指南](./deployment.md) + +### 本地开发 + +**系统要求:** + +- Python 3.10+ +- 默认 GPU 镜像要求 CUDA 13.0+;CUDA 12.6 / 13.0 可通过 Docker build args 自行构建 +- FFmpeg (音频格式转换) + +**安装步骤:** + +运行时依赖现在改成“根目录默认 GPU,CPU 单独特化环境”: + +| 模式 | 命令 | 说明 | +|------|------|------| +| NVIDIA GPU(默认) | `uv sync` 或 `./scripts/sync_gpu_env.sh` | 同步根目录 [pyproject.toml](/opt/qwen3-asr/pyproject.toml) 和 [uv.lock](/opt/qwen3-asr/uv.lock) 到 `.venv`,包含 CUDA 13.0/cu130 `torch 2.11.0` / `torchaudio 2.11.0` / `torchvision 0.26.0` / `vllm 0.20.0` | +| 沐曦 GPU | `./scripts/sync_metax_env.sh` | 同步 [environments/metax/pyproject.toml](/opt/qwen3-asr/environments/metax/pyproject.toml) 的公共依赖;可选 GPU 栈安装默认从沐曦 MACA PyPI 源按 `--no-deps` 安装 | +| 天数 GPU | `./scripts/sync_iluvatar_env.sh` | 同步 [environments/iluvatar/pyproject.toml](/opt/qwen3-asr/environments/iluvatar/pyproject.toml) 的公共依赖;GPU 栈建议来自天数官方 vLLM 镜像 | +| 摩尔线程 GPU | `./scripts/sync_mthreads_env.sh` | 同步 [environments/mthreads/pyproject.toml](/opt/qwen3-asr/environments/mthreads/pyproject.toml) 的公共依赖;GPU 栈建议来自摩尔线程官方 MUSA vLLM 镜像 | +| CPU(特化) | `./scripts/sync_cpu_env.sh` | 同步 [environments/cpu/pyproject.toml](/opt/qwen3-asr/environments/cpu/pyproject.toml) 对应的 CPU lock 到 `.venv` | +| 自动 | `./scripts/sync_accel_env.sh` | 有 `mx-smi` 时选择沐曦,有 `ixsmi` 时选择天数,有 `mthreads-gmi` 时选择摩尔线程,有 `nvidia-smi` 时选择 NVIDIA,否则选择 CPU | + +```bash +# 克隆项目 +cd qwen3-asr + +# 安装依赖(Linux/NVIDIA CUDA) +uv sync + +# 启动服务 +source .venv/bin/activate +python start.py +``` + +沐曦本地开发: + +```bash +./scripts/sync_metax_env.sh +source .venv/bin/activate +ACCELERATOR=metax python start.py +``` + +macOS / Apple Silicon 本地开发: + +```bash +./scripts/sync_cpu_env.sh +source .venv/bin/activate +python start.py +``` + +## 当前运行时默认值 + +当前主线代码的运行时行为如下: + +- `ACCELERATOR=auto` 会优先识别 `mx-smi` 上报的沐曦设备,其次识别 `ixsmi` 上报的天数设备,再识别 `mthreads-gmi` 上报的摩尔线程设备,再识别 NVIDIA CUDA,否则回落 CPU +- `DEVICE=auto` + - NVIDIA/沐曦/天数 GPU 时解析为 `cuda:0` + - 否则解析为 `cpu` +- `DEVICE=mps` 会直接归一化为 `cpu` +- `Linux + NVIDIA CUDA` 使用官方 `vLLM` +- `Linux + 沐曦 MACA` 使用沐曦兼容 PyTorch/vLLM 运行栈 +- `Linux + 天数` 使用天数官方 vLLM 镜像运行栈 +- `Linux + CPU` 使用 vendored `QwenASR` Rust +- `macOS / Apple Silicon` 也使用 vendored `QwenASR` Rust +- macOS / Apple Silicon 默认总是 `qwen3-asr-0.6b` +- 在 macOS 上,只有设置 `QWEN3_ASR_MODEL=qwen3-asr-1.7b` 时才会使用 `qwen3-asr-1.7b` +- `word_timestamps=true` 在当前离线 CUDA 与 CPU Rust 路径下可用 +- WebSocket 流式路径当前不返回词级时间戳 +- CAM++ 说话人分离仍然必须保留,并继续跟随 `DEVICE`;在 CPU 上的主要热点仍是 speaker verification embedding + +## API 接口 + +### OpenAI 兼容接口 + +| 端点 | 方法 | 功能 | +| ---------------------------- | ---- | ----------------------- | +| `/v1/audio/transcriptions` | POST | 音频转写(OpenAI 兼容) | +| `/v1/models` | GET | 离线模型列表 | + +**请求参数:** + +| 参数 | 类型 | 默认值 | 说明 | +| ------------------------------ | ------ | --------------------- | ------------------------------------- | +| `file` | file | 提供时优先使用 | 音频/视频文件 | +| `audio_address` | string | 可选 | 音频/视频文件 URL(HTTP/HTTPS)、`file://` 或服务端本地路径;若同时提供 `file`,则忽略 | +| `language` | string | 自动检测 | 语言代码 (zh/en/ja) | +| `enable_speaker_diarization` | bool | `true` | 启用说话人分离 | +| `enable_speaker_identification` | bool | `true` | 说话人分离开启时匹配已注册声纹库 | +| `enable_text_cleanup` | bool | `true` | 启用文本去重、跨段重叠裁剪和口头语清理 | +| `word_timestamps` | bool | `false` | 返回后端支持的字词级时间戳;Qwen CUDA vLLM 与 CPU Rust 在启用时会自动调用 forced aligner | +| `hotwords` | string | - | 热词,格式:`词1 权重1 词2 权重2` | +| `response_format` | string | `verbose_json` | 输出格式 | +| `prompt` | string | - | 提示文本(保留兼容) | +| `temperature` | float | `0` | 采样温度(保留兼容) | + +**音频/视频输入方式:** +- **文件上传**: 使用 `file` 参数上传音频文件或带音轨的视频容器 +- **URL / 本地路径读取**: 使用 `audio_address` 参数提供音频/视频 URL 或服务端本地路径,服务将自动读取 +- **优先级**: 如果同时提供 `file` 和 `audio_address`,服务会优先使用 `file`,并忽略 `audio_address` + +**使用示例:** + +```python +# 使用 OpenAI SDK +from openai import OpenAI + +client = OpenAI(base_url="http://localhost:8000/v1", api_key="your_api_key") + +with open("audio.wav", "rb") as f: + transcript = client.audio.transcriptions.create( + file=f, + response_format="verbose_json" # 获取分段和说话人信息 + ) +print(transcript.text) +``` + +```bash +# 使用 curl +curl -X POST "http://localhost:8000/v1/audio/transcriptions" \ + -H "Authorization: Bearer your_api_key" \ + -F "file=@audio.wav" \ + -F "model=qwen3-asr-0.6b" \ + -F "response_format=verbose_json" \ + -F "enable_speaker_diarization=true" \ + -F "enable_speaker_identification=true" \ + -F "enable_text_cleanup=true" \ + -F "hotwords=Qwen 2.0 ModelScope 1.5" +``` + +**支持的响应格式:** `json`, `text`, `srt`, `vtt`, `verbose_json` + +### 阿里云兼容接口 + +| 端点 | 方法 | 功能 | +| ------------------------- | --------- | ---------------------- | +| `/stream/v1/asr` | POST | 语音识别(支持长音频) | +| `/stream/v1/asr/models` | GET | 声明条目列表 | +| `/stream/v1/asr/health` | GET | 健康检查 | +| `/ws/v1/asr` | WebSocket | Qwen3-ASR 流式识别 | +| `/ws/v1/asr/qwen` | WebSocket | Qwen3-ASR 流式识别(显式路径) | +| `/ws/v1/asr/funasr` | WebSocket | 已移除;会返回废弃错误并提示切换到 `/ws/v1/asr/qwen` | + +**请求参数:** + +| 参数 | 类型 | 默认值 | 说明 | +| ------------------------------ | ------ | ------------------ | ------------------------------------- | +| `audio_address` | string | `https://media.cdn.vect.one/podcast_demo.mp4`(文档示例) | 音频/视频 URL、`file://` 或服务端本地路径(可选;若同时上传内容则忽略) | +| `sample_rate` | int | `16000` | 采样率 | +| `enable_speaker_diarization` | bool | `true` | 启用说话人分离 | +| `enable_speaker_identification` | bool | `true` | 说话人分离开启时匹配已注册声纹库 | +| `enable_text_cleanup` | bool | `true` | 启用文本去重、跨段重叠裁剪和口头语清理 | +| `word_timestamps` | bool | `false` | 返回后端支持的字词级时间戳;Qwen CUDA vLLM 与 CPU Rust 在启用时会自动调用 forced aligner | +| `vocabulary_id` | string | - | 热词(格式:`词1 权重1 词2 权重2`) | + +**使用示例:** + +```bash +# 基本用法 +curl -X POST "http://localhost:8000/stream/v1/asr" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @audio.wav + +# 带参数 +curl -X POST "http://localhost:8000/stream/v1/asr?enable_speaker_diarization=true&enable_speaker_identification=true&enable_text_cleanup=true&vocabulary_id=Qwen%202.0%20ModelScope%201.5" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @audio.wav +``` + +### 会议离线接口 + +| 端点 | 方法 | 功能 | +| ---- | ---- | ---- | +| `/api/v1/asr/transcriptions` | POST | 创建离线会议识别任务 | +| `/api/v1/asr/transcriptions/{task_id}` | GET | 查询任务状态和结果 | + +该接口生产调用只使用 `audio_address`。 + +```json +{ + "audio_address": "https://example.com/media/meeting.mp4", + "config": { + "enable_speaker": true, + "match_speaker_registry": true, + "enable_text_cleanup": true, + "speaker_threshold": 0.6, + "word_timestamps": false, + "hotwords": [ + { "hotword": "通义千问", "weight": 2.0 }, + { "hotword": "ModelScope", "weight": 1.5 } + ] + } +} +``` + +**响应示例:** + +```json +{ + "task_id": "xxx", + "status": 200, + "message": "SUCCESS", + "result": "说话人1的内容...\n说话人2的内容...", + "duration": 60.5, + "processing_time": 1.234, + "segments": [ + { + "text": "今天天气不错。", + "start_time": 0.0, + "end_time": 2.5, + "speaker_id": "说话人1", + "word_tokens": [ + {"text": "今天", "start_time": 0.0, "end_time": 0.5}, + {"text": "天气", "start_time": 0.5, "end_time": 0.9}, + {"text": "不错", "start_time": 0.9, "end_time": 1.3} + ] + } + ] +} +``` + +## 说话人分离 + +基于 CAM++ 模型实现多说话人自动识别: + +- **默认开启** - `enable_speaker_diarization=true` +- **自动识别** - 无需预设说话人数量,模型自动检测 +- **说话人标记** - 响应中包含 `speaker_id` 字段(如 "说话人1"、"说话人2") +- **智能合并** - 两层合并策略避免孤立短片段: + - 第一层:小于10秒的同说话人片段累积合并 + - 第二层:连续片段累积合并至60秒上限 +- **字幕支持** - SRT/VTT 格式输出包含说话人标记 `[说话人1] 文本内容` + +关闭说话人分离: + +```bash +# OpenAI API +-F "enable_speaker_diarization=false" + +# 阿里云 API +?enable_speaker_diarization=false +``` + +## 音频处理 + +### 智能分段策略 + +长音频自动分段处理: + +1. **VAD 语音检测** - 检测语音边界,过滤静音 +2. **贪婪合并** - 累积语音段,确保每段不超过 `MAX_SEGMENT_SEC`(默认60秒) +3. **静音切分** - 语音段间静音超过3秒时强制切分,避免包含过长静音 +4. **批处理推理** - 多片段并行处理,GPU 模式下性能提升 2-3 倍 + +### WebSocket 流式识别限制 + +**Qwen3-ASR 流式**(使用 `/ws/v1/asr` 或 `/ws/v1/asr/qwen`): +- ✅ 支持多语言实时识别 +- ✅ 当前支持 CUDA vLLM 与 CPU Rust 两条流式路径 +- ❌ 当前流式路径不返回词级时间戳 + +### Qwen3 运行时矩阵 + +| 运行环境 | 后端 | 离线转写 | WebSocket 流式 | 离线词级时间戳 | 流式词级时间戳 | 成熟度 | +|---------|------|---------|----------------|----------------|----------------|--------| +| Linux + NVIDIA GPU | 官方 vLLM 0.20.0 | ✅ | ✅ | ✅ | ❌ | 面向生产 | +| CPU / macOS | QwenASR Rust | ✅ | ✅ | ✅(forced aligner) | ❌ | 推荐本地后端 | + +## 支持离线的模型 + +| 模型 ID | 名称 | 说明 | 特性 | +| -------------------- | ----------------- | ---------------------------------------- | --------- | +| `qwen3-asr-1.7b` | Qwen3-ASR 1.7B | 高性能多语言 ASR;CUDA 使用 vLLM | 离线/实时 | +| `qwen3-asr-0.6b` | Qwen3-ASR 0.6B | 轻量版多语言 ASR;CUDA 使用 vLLM,CPU/macOS 使用 Rust backend | 离线/实时 | + +**运行时选择:** +- **显存 >= 32GB**: 选择 `qwen3-asr-1.7b` +- **显存 < 32GB**: 选择 `qwen3-asr-0.6b` +- **无 CUDA**: 选择基于 vendored Rust 的 `qwen3-asr-0.6b` +- **macOS / Apple Silicon**: 无论内存大小多少,默认都选择 `qwen3-asr-0.6b` +- **环境变量覆盖**: 设置 `QWEN3_ASR_MODEL=qwen3-asr-1.7b` 或 `QWEN3_ASR_MODEL=qwen3-asr-0.6b` 可跳过自动选择 + +启动时会先检测当前运行计划所需模型;如果本地缓存缺失,会自动从 ModelScope 下载。离线部署请提前准备模型缓存。 + +## 环境变量 + +推荐直接关心的公开配置: + +| 变量 | 默认值 | 说明 | +| ---------------------------------- | ------------ | ----------------------------------------------- | +| `API_KEY` | - | API 认证密钥(可选,未配置时无需认证) | +| `LOG_LEVEL` | `INFO` | 日志级别(DEBUG/INFO/WARNING/ERROR) | +| `MAX_AUDIO_SIZE` | `2048` | 最大音频文件大小(MB,支持单位如 2GB) | +| `ASR_BATCH_SIZE` | `4` | 长音频分段后的 ASR 批处理大小 | +| `MAX_SEGMENT_SEC` | `60` | 音频分段最大时长(秒) | +| `ASR_ENABLE_NEARFIELD_FILTER` | `true` | 启用远场声音过滤 | +| `QWEN3_ASR_MODEL` | 自动选择 | 强制选择 `qwen3-asr-1.7b` 或 `qwen3-asr-0.6b` | +| `QWEN_GPU_MEMORY_UTILIZATION` | `0.9` | vLLM 可保留的 GPU 显存上限;共享显卡时可调低,KV cache 不足时可适当调高 | +| `QWEN_VLLM_ENFORCE_EAGER` | `true` | 强制 vLLM eager 执行以提高兼容性;NVIDIA 性能测试可设为 `false` 允许 CUDA Graph 优化 | + +远场过滤调优建议: + +- `ASR_NEARFIELD_RMS_THRESHOLD=0.01` 是当前默认值,也是推荐起点 +- 嘈杂环境可以适当调高,增强背景语音过滤 +- 安静环境如果出现小声说话漏识别,可以适当调低 +- 需要观察过滤行为时,可临时设置 `LOG_LEVEL=DEBUG` + +后端专项高级配置: + +| 变量 | 默认值 | 说明 | +| --- | --- | --- | +| `QWEN_RUST_CPU_WORKERS` | `4` | CPU Rust backend worker 数(Rust ASR / forced align 默认 4 个 runtime) | +| `QWENASR_LIBRARY_PATH` | 自动探测 | 覆盖 vendored Rust 动态库路径 | + +## 资源需求 + +**最小配置(CPU):** + +- CPU: 4 核 +- 内存: 16GB +- 磁盘: 20GB + +**推荐配置(GPU):** + +- CPU: 4 核 +- 内存: 16GB +- GPU: NVIDIA GPU (16GB+ 显存) +- 磁盘: 20GB + +## API 文档 + +启动服务后访问: + +- Swagger UI: `http://localhost:8000/docs` +- ReDoc: `http://localhost:8000/redoc` + +## 相关链接 + +- **部署指南**: [详细文档](./deployment.md) +- **Qwen3-ASR**: [Qwen3-ASR GitHub](https://github.com/QwenLM/Qwen3-ASR) +- **FunASR**: [FunASR GitHub](https://github.com/alibaba-damo-academy/FunASR) +- **QwenASR**: [QwenASR GitHub](https://github.com/huanglizhuo/QwenASR) + +## 许可证 + +本项目采用 MIT 许可证 - 查看 [LICENSE](../LICENSE) 文件了解详情。 + +## Star 历史 + +[![Star History Chart](https://api.star-history.com/svg?repos=Quantatirsk/qwen3-asr&type=Date)](https://star-history.com/#Quantatirsk/qwen3-asr&Date) + +## 贡献 + +欢迎提交 Issue 和 Pull Request 来改进项目! diff --git a/docs/REALTIME_WEBSOCKET_PROCESSING_COMPARISON.md b/docs/REALTIME_WEBSOCKET_PROCESSING_COMPARISON.md new file mode 100644 index 0000000..8ea96ee --- /dev/null +++ b/docs/REALTIME_WEBSOCKET_PROCESSING_COMPARISON.md @@ -0,0 +1,382 @@ +# 实时 ASR WebSocket 处理细节对照 + +本文只整理当前代码,不修改 WebSocket 或说话人算法。对照对象是: + +- 当前独立 Demo:`demo/realtime_asr_optimization_demo` +- 原项目实时接口:`app/api/v1/websocket_asr.py`、`app/services/qwen3_websocket_asr.py`、`app/services/realtime_speaker_clusterer.py` + +代码是本文的依据;旧的排查记录或早期说明如果与当前实现冲突,以代码为准。 + +## 1. 先看整体差异 + +```mermaid +sequenceDiagram + participant B as 浏览器 + participant D as 当前 Demo /ws + participant V as 独立 vLLM HTTP + participant A as 独立辅助服务 + + B->>D: start(扁平字段) + B->>D: PCM/WAV 二进制帧 + D->>D: 20ms RMS 门控、turn 缓冲、静音切段 + D->>V: 当前 turn 的累积 WAV(partial/final) + V-->>D: 文本 + D-->>B: sentences + display_state + D->>A: final turn + session_id + A-->>D: CAM++ embedding / 在线聚类标签 + D-->>B: 同 sentence_id 的 speaker 更新 + 新 display_state + B->>D: eof 或 stop + D->>D: 等待音频队列和 speaker 队列清空 + D-->>B: end +``` + +```mermaid +sequenceDiagram + participant B as 浏览器 + participant R as 原项目 FastAPI 路由 + participant Q as Qwen3ASRService + participant E as Qwen3ASREngine + participant S as RealtimeSpeakerClusterer + + B->>R: /ws/v1/asr 或 /ws/v1/asr/qwen + R->>Q: handle_connection + B->>Q: start.payload(嵌套字段) + Q-->>B: voice_id、start + B->>Q: PCM/WAV 二进制帧 + Q->>Q: 转采样、VAD、pre-roll、partial + Q->>E: 原生流式或当前窗口重转写 + E-->>Q: partial 文本 + Q-->>B: sentence_type=0 + Q->>E: 当前 turn 全量 final 重转写 + E-->>Q: final 文本 + Q-->>B: sentence_type=1、speaker_id=-1 + Q->>S: speaker_job_queue(异步) + S-->>Q: 聚类/注册库匹配 + Q-->>B: 相同 sentence_id 的 speaker 回写 + B->>Q: stop + Q-->>B: end(stop 内部会再次 final/recluster) +``` + +核心设计差别是:当前 Demo 把 ASR 和声纹都放在独立 HTTP 服务后面,WebSocket 只做编排;原项目把 Qwen ASR 引擎、VAD、在线声纹聚类放在同一个服务进程里,但 speaker 归属仍是 final 之后异步补回。 + +## 2. 当前独立 Demo 的处理链路 + +### 2.1 进程和启动关系 + +`server.py` 启动一个 aiohttp HTTP/WebSocket 进程,并在生命周期中创建两个 HTTP 客户端: + +| 组件 | 默认地址 | 职责 | +| --- | --- | --- | +| `VLLMTranscriptionService` | `http://127.0.0.1:9950/v1` | 只调用 `/audio/transcriptions`,partial 和 final 都是累积窗口 HTTP 请求 | +| `AuxiliaryModelService` | `http://127.0.0.1:8010` | 调用 `/health`、`/v1/speaker/resolve`、`/v1/speaker/reset` | +| `RealtimeSession` | `server.py:/ws` | 接收音频、VAD 切 turn、调用两个服务、维护展示状态 | + +辅助服务在 `demo/scripts/auxiliary_server.py` 中预加载 VAD 与 CAM++ `speaker_verification`。每次 resolve 只上传一个已结束 turn,服务以 `session_id` 保存在线聚类中心;这不是把整段会议音频重新上传。 + +### 2.2 WebSocket 输入和输出 + +客户端首条消息是扁平结构: + +```json +{ + "type": "start", + "source": "mic", + "model_service_url": "http://127.0.0.1:9950/v1", + "model": "Qwen/Qwen3-ASR-0.6B", + "speaker_diarization": 1, + "sentence_strategy": 0, + "partial_interval_ms": 1200, + "max_segment_sec": 12, + "display_merge": true +} +``` + +`start` 成功后,客户端发送 16kHz、单声道、PCM16 二进制帧。文件模式只接收 `.pcm` 和 `.wav`;WAV 的 RIFF/fmt/data chunk 在 WebSocket 服务端增量剥离,并且要求 16kHz、单声道、PCM16。 + +控制消息: + +| 消息 | 行为 | +| --- | --- | +| `eof` | 输入生产者结束;服务端把 `EOF` 放入音频队列,完成尾部 turn 和 speaker 队列后发送 `end` | +| `stop` | 与 `eof` 走同一排空流程,同时记录 `input_stopped`,用于 WAV 不完整时的校验差异 | +| `abort` | 取消音频和 speaker worker,直接结束,不保证当前 turn 有 final | + +服务端消息的实际顺序通常是: + +```text +start + -> sentences(partial,可能多次) + -> display_state(每次状态改变一份快照) + -> sentences(final,speaker 尚未确认) + -> display_state(pending) + -> display_state(processing/confirmed 或失败原因) + -> draining + -> end +``` + +`sentences` 是兼容性事件;`display_state` 是当前页面的主要渲染数据。`display_state.revision` 单调递增,包含: + +- `raw_segments`:按 `start_time`、`sentence_id` 排序的原始片段 +- `display_blocks`:按相邻且可信的说话人合并后的展示块 +- `metrics`:音频字节数、输入帧数、partial 数量、partial 修订次数和耗时 + +### 2.3 音频、VAD 和 turn 边界 + +`RealtimeSession.process_audio()` 的边界是本 Demo 最重要的状态机: + +1. 二进制数据进入 `audio_queue`,再拆成 640 bytes 的 PCM 帧,即 20ms。 +2. 每帧用 RMS 阈值 `450` 判断有声/静音;这是 WebSocket 层的轻量门控,不是辅助服务的整段 VAD pipeline。 +3. 未进入说话状态时,保留最近 6400 bytes(约 200ms)`pre_roll`。 +4. 第一帧有声时,将 pre-roll 加到新 `segment_audio`,设置 `segment_start_ms`。 +5. 进入说话状态后,所有帧追加到当前 turn;有声帧累计 `voiced_ms`,静音帧累计 `silence_ms`。 +6. `sentence_strategy=0` 默认约 800ms 静音提交;`sentence_strategy=1` 使用约 1400ms 静音提交。 +7. 达到 `partial_interval_ms`(默认 1200ms)且尚未达到静音阈值时,调用一次 vLLM partial。 +8. 达到 `max_segment_sec`(默认 12s)时按 `max_duration` 提交。 + +当前 Demo 不把切段尾部静音送给 ASR/声纹:提交前按 `silence_ms` 从 `segment_audio` 尾部删除。下一个 turn 没有原项目那样的 `carry_audio`,而是从后续有声帧重新开始;因此两段之间的静音会形成时间间隔,不会自动带入下一段。 + +### 2.4 ASR 结果和同句覆盖 + +`_emit_transcription()` 始终使用当前 `segment_id`: + +- `sentence_type=0`:partial,写入/覆盖同一个 `sentence_id` +- `sentence_type=1`:final,仍写入同一个 `sentence_id`,并加入 speaker 队列 + +`SegmentAssembler.apply_sentence()` 先按 `sentence_id` 找旧记录再覆盖;如果已经是 final,后来的 partial 不会回滚 final。final 没有文本时,会移除该片段,避免遗留一个永久 pending 的 partial。 + +### 2.5 声纹异步链路 + +`_commit_segment()` 只负责把 `SpeakerJob` 放入 `speaker_queue`,不会等待 CAM++。`process_speakers()` 是单 worker,按 turn 入队顺序串行调用辅助服务: + +1. `voiced_ms < 800ms`:不提取声纹,写入 `insufficient_audio`,保持 `speaker_id=-1`。 +2. 辅助服务缺失或异常:写入 `service_unavailable`/`service_error`,ASR 继续输出。 +3. 辅助服务返回 embedding/聚类结果:回写同一 `sentence_id`。 +4. `SegmentAssembler.apply_speaker_update()` 只接受可信身份:`speaker_evidence` 必须是 `fresh` 或 `confirmed`,置信度至少 `0.6`,并拒绝 `short_attach`、`embedding_attach`。 +5. 不可信结果被归一化为 `speaker_id=-1`、`speaker_name=""`,但保留状态和原因供诊断。 + +辅助服务的在线聚类是简单的 session 级中心匹配:首次 embedding 新建 `speaker_id`,之后与已有中心的余弦相似度达到阈值就更新中心并复用 ID。`/v1/speaker/reset` 在 WebSocket 结束时清理该 session。 + +### 2.6 展示块如何合并 + +`SegmentAssembler.display_blocks(merge_adjacent=True)` 先按时间排序原始片段,然后遵循: + +- pending/unknown 片段始终以自己的 `sentence_id` 作为身份键,独立成块; +- 只有相邻且可信的片段,且 `user_id`/`registry_speaker_id`/`speaker_id` 身份键相同,才合并; +- 合并只拼接文本、扩大结束时间并追加 `segment_ids`,原始片段仍保留在 `raw_segments`。 + +当前页面收到 `display_state` 后会整块重建结果区,按 `block_id` 渲染;收到 `sentences` 时如果服务端声明支持 `display_state`,页面不会再次追加,避免同一片段重复显示。旧页面或绕过 `display_state` 的客户端不具备这个保护。 + +## 3. 原项目 WebSocket 的处理链路 + +### 3.1 路由和状态 + +`app/api/v1/websocket_asr.py` 当前实际路由: + +- `/ws/v1/asr` +- `/ws/v1/asr/qwen` +- `/ws/v1/asr/funasr` 已废弃,接受后发送 `FUNASR_REALTIME_REMOVED` 并以 1008 关闭 + +每个连接进入 `Qwen3ASRService.handle_connection()`。`ConnectionContext.state` 为 `READY -> STARTED -> STREAMING`;会话还可以通过 `session_id` 在 TTL 内断线恢复。恢复的是完整上下文,包括已确认片段、speaker history、时间线和待处理状态,不只是一个 WebSocket ID。 + +### 3.2 start 参数 + +原项目的 `start` 使用 `payload` 嵌套对象。常用字段包括: + +| 类别 | 字段 | +| --- | --- | +| 音频 | `format`、`sample_rate`、`language`、`context`、`enable_inverse_text_normalization` | +| partial | `min_partial_sec`、`partial_window_sec`、`partial_holdback_chars`、`unfixed_token_num`、`enable_native_partial_stream` | +| 切段 | `silence_duration_ms`(默认 800)、`pre_roll_ms`(默认 240)、`max_sentence_count`(默认 8)、`enable_realtime_vad_split`、`max_segment_sec` | +| speaker | `enable_speaker`(默认 true)、`match_speaker_registry`、`speaker_threshold` | +| 稳定性 | `force_stable_segment_sec`、`force_stable_min_chars`、`soft_limit_sec`、`hard_limit_sec` | + +服务端先发送 `voice_id`,再发送 `start`。`voice_id` 与会话 ID 通常相同;如果客户端传入固定 `payload.session_id`,断线重连时可以复用上下文。 + +### 3.3 音频转换、pre-roll 和 partial + +`_convert_audio()` 接收 PCM/WAV,转为 float32;多声道下混为单声道,非 16kHz 用 scipy 重采样。每个二进制消息都在服务端转换后立即参与 VAD。 + +原项目的 `ConnectionContext` 同时维护: + +- `pre_roll_audio`:未开始说话前的前滚音频,默认约 240ms; +- `segment_audio_buffer`:从当前 turn 开始到提交前的完整音频; +- `stream_window_buffer`:最近窗口,用于 partial 或 native stream 失败时回退; +- `realtime_stream_state`:只服务低延迟 partial,不决定 final; +- `silence_samples`、`sentence_active`、`total_samples`:VAD 状态和当前 turn 长度。 + +有声输入到达时,`_start_turn()` 把 pre-roll 与当前音频拼接;后续由 `_append_turn_audio()` 追加。达到最短窗口后,服务端可走原生 Qwen partial,或对当前窗口/当前 turn 重转写;partial 会经过清理、去重、与上一段重叠裁剪后发送。 + +提交触发条件不只有静音: + +- 识别到足够完整的标点句,且时长/字数达到稳定门槛; +- 句子数达到 `max_sentence_count`; +- 静音样本达到 `silence_duration_ms`; +- 达到硬时长限制;开启实时 VAD split 时会尝试找一个完成的分割点。 + +### 3.4 final、carry 和时间线 + +`_commit_retranscribe_turn()` 对当前完整 turn 做一次 final 重转写,生成 `confirmed_segments` 元素: + +```text +index / text / language / reason +duration_ms / start_ms / end_ms +sentence_type=1 / speaker_id=-1 / speaker_pending=true +``` + +final 事件先发送,speaker 之后再补。默认 final 的 `start_ms` 来自 `ctx.timeline_cursor_ms`;提交后时间线前移到 `segment_end_ms`。 + +开启实时 VAD split 时,提交可能得到 `finalized_audio + carry_audio`:前半段定稿,后半段留在下一个逻辑 turn 中,且会重新初始化 stream 状态。这个 carry 是原项目与当前 Demo 的一个实质差异,也是跨说话人边界时必须重点观察的音频来源。 + +### 3.5 原项目 speaker worker 和聚类 + +原项目 final 后把 job 放入 `ctx.speaker_job_queue`,由 `_speaker_worker_loop()` 串行消费。`_resolve_segment_speaker()` 调用 `RealtimeSpeakerClusterer.resolve_segment_speaker()`,再把结果写入指定 `segment_index`,通过相同索引发送一条新的 `sentences`。 + +`RealtimeSpeakerClusterer` 当前行为: + +- 1.6s 以下且上一条有命名身份:使用 `short_attach` 直接沿用上一条; +- 正常 turn:按 1.5s 窗口、0.75s 步长提取 CAM++ embedding,匹配已有记录或新建 generic speaker; +- 4s 以上若 chunk 明显混合:返回 `mixed_segment`,保持未知; +- 开启注册库匹配且时长至少 2.4s:在独立注册 embedding 空间匹配实名; +- timeline 平滑时,短于 0.7s 的范围会并给相邻说话人; +- 实时记录达到至少 5 条且队列积压不超过 1 条时,可能对最近 12 条 pending 片段重新聚类; +- `stop` 时还会对历史记录做一次最终 recluster。 + +此外,`Qwen3ASRService` 自身还有两类“最近说话人继承”:generic speaker 新 turn 时长至少 8s 才允许 `recent_inherit`,实名 speaker 至少 4.5s 才允许 `recent_named_inherit`。这些继承都发生在 embedding 结果之后,不能与 `short_attach` 混为一谈。 + +### 3.6 stop 和 end + +客户端只发送 `{"type":"stop"}`。服务端会: + +1. 对仍 active 的 `segment_audio_buffer` 做 `reason=final` 的 final 提交; +2. 立即对现有 `speaker_records` 做最终 recluster,并发送可能的 speaker 更新; +3. 汇总 `confirmed_segments`,发送 `end(final=1)`。 + +这里与当前 Demo 不同:原项目的 `_stop()` 没有显式等待 `speaker_job_queue.join()`。如果 stop 到达时 speaker worker 仍在处理,最终 recluster/end 可能先于某个异步 speaker 回写;断开清理还会停止 worker。客户端必须把同 `sentence_id` 的后续 speaker 事件当作可迟到更新,而不能认为 `end` 之后绝不会再有归属变化。 + +## 4. 两套消息契约对照 + +| 维度 | 当前独立 Demo | 原项目 | +| --- | --- | --- | +| WebSocket | aiohttp `/ws` | FastAPI `/ws/v1/asr`、`/qwen` | +| start | 扁平字段 | `payload` 嵌套字段 | +| ASR | 外部 vLLM HTTP 累积窗口 | 进程内 Qwen engine,原生 stream 或重转写回退 | +| VAD | 20ms PCM RMS 门控 | float32 音频门控,支持实时 VAD split 辅助切分 | +| pre-roll | 固定约 200ms | `pre_roll_ms` 默认约 240ms | +| final 音频 | 删除提交尾部静音,不保留 carry | 可有 `carry_audio` 并带入后续 turn | +| speaker | 外部 CAM++/在线中心服务,单 worker | 进程内 CAM++ chunk 聚类、注册库、重聚类,单 worker | +| 未确认 speaker | `speaker_evidence=pending`,展示独立未知块 | `speaker_id=-1` 或 `speaker_pending=true`,客户端需自行暂存 | +| 文本更新键 | `sentence_id` | `sentence_id` 对外,内部 `segment_index` | +| 展示快照 | `display_state.revision`,服务端生成 `display_blocks` | 没有同等的服务端展示块协议,客户端按 sentence upsert | +| end 屏障 | `EOF -> audio worker -> speaker EOF -> end` | `_stop()` final/recluster/end,不等待 speaker 队列清空 | + +## 5. “上一人的最后一句进入下一人气泡”的定位框架 + +当前先不修改,定位时要把“片段本身错了”和“片段正确但展示合错了”分开。 + +### 5.1 当前独立 Demo 的可能路径 + +1. **同一个 turn**:两人换话之间没有达到 800ms(或段落模式 1400ms)静音,RMS VAD 不切段。此时 vLLM 收到的是混合 turn,前端只有一个 `sentence_id`,不是气泡合并问题。 +2. **声纹误归属**:A 的 final 先是 pending,随后辅助服务把 A 误匹配到 B 的 cluster。下一次 `display_state` 中,A、B 两个相邻片段拥有同一可信身份,`display_blocks()` 会把两者拼成一个 block。 +3. **异步回写改变了合并条件**:A 的 speaker 结果可能在 B 的 final 之后才到达。服务端按时间排序重建快照,所以视觉上是 A 的文字“后来进入”B 的气泡;实际是 A 的旧片段身份被补齐后触发了相邻合并。 +4. **旧客户端渲染路径**:当前页面在 `display_state_supported=true` 时忽略 `sentences`,但旧页面若逐条 append `sentences`,可能把同一 `sentence_id` 的 final/speaker 更新当成新气泡,或把 pending 文本追加到上一气泡。必须确认浏览器加载的 `app.js` 版本和服务端返回的 `display_state`。 +5. **session 污染**:辅助服务按 `session_id` 保存聚类中心。若 reset 没有执行、多个连接错误复用同一个 session ID,上一场会话的 cluster 可能影响新会话;正常一次连接内 A/B 共用中心是设计行为,不是跨人合并的充分证据。 + +当前 Demo 的 worker 是串行的,`emit()` 有发送锁,因而“并发返回顺序打乱”不是首要嫌疑;首要证据应是 `raw_segments` 的 `sentence_id/start_time/speaker_id/speaker_strategy` 是否正确,以及 `display_blocks.segment_ids` 是否把两个片段合到一起。 + +### 5.2 原项目的可能路径 + +1. **静音边界不足**:默认 800ms 静音才提交;换话前的短停顿会让 A 尾部和 B 开头留在同一个 `segment_audio_buffer`。 +2. **carry 音频污染**:启用实时 VAD split 时,分割点之后的 `carry_audio` 会成为下一个 turn 的开头。若 split 点落在 A 尾音或 B 起音中间,下一段声纹和 ASR 都会携带前一人尾部。 +3. **短句沿用上一身份**:B 的新段小于 1.6s 且历史有命名 speaker 时,`short_attach` 会直接复用上一条;chunk 提取为空时的 `embedding_attach` 也可能复用上一条。这是代码中最直接的“上一人污染下一段”路径。 +4. **最近身份继承**:较长的新段在匹配失败时可能触发 `recent_named_inherit`(至少 4.5s)或 `recent_inherit`(至少 8s),因此不能只看最终 `speaker_id`,还要记录 `speaker_strategy`。 +5. **重聚类改写历史**:实时重聚类和 stop 最终重聚类都可能改写已有 segment 的 speaker。客户端如果按到达顺序追加,而不是按 `sentence_id` upsert,就会看到旧气泡和新气泡互相覆盖或合并。 +6. **stop 竞态**:原项目 stop 不等待 speaker job 队列清空;end 可能先发,随后连接清理还会取消 worker。最后一个人的 speaker 归属可能缺失、迟到或停留在旧标签,前端若把 end 当成不可变快照会放大问题。 + +### 5.3 需要同时保存的证据 + +对同一段测试音频,至少保存以下三层结果: + +```text +音频层:每个 turn 的 start/end、有效有声时长、是否包含 carry/pre-roll +识别层:sentence_id/index、sentence_type、文本、speaker_strategy、置信度 +展示层:display_state.revision、raw_segments、display_blocks.segment_ids +``` + +判定规则: + +- `raw_segments` 已经只有一个片段:先查 VAD/切段边界; +- `raw_segments` 有 A、B 两段且 speaker ID 相同:查声纹误匹配/继承/重聚类; +- `raw_segments` 的 ID 不同但 `display_blocks.segment_ids` 合并:查展示合并键; +- `display_blocks` 正确但页面仍显示一只气泡:查浏览器脚本版本、是否绕过 `display_state`、是否按 `sentence_id` upsert。 + +## 6. 后续细节优化的优先级(本轮不实施) + +### P0:先证明边界和身份是否正确 + +- 记录每个 final 的实际音频起止、有效有声毫秒、pre-roll/carry 长度。 +- 记录 speaker resolve 请求和返回的 `session_id`、策略、置信度、cluster ID。 +- 前端临时展示 `block.segment_ids`,确认“合并”到底是两个片段还是一个片段。 +- 用固定 A-静音-B 音频,比较 200ms、500ms、800ms、1400ms 停顿。 + +### P1:降低错误身份传播 + +- 对 `short_attach`、`embedding_attach`、recent inherit 单独统计,不要只统计 speaker_id。 +- 对跨边界的短 turn 保持 pending,等到有独立 embedding 或后续重聚类再确认。 +- 明确实时重聚类和最终重聚类的可修改范围,客户端统一按 ID 幂等更新。 +- 为 stop 增加“最后一个 speaker job 已完成”的可观察状态。 + +### P2:改善展示稳定性 + +- 展示层只把可信且相邻的片段合并,保留 segment_ids 和 revision。 +- 对 speaker 更新做局部重绘或整快照重绘,但不要把同一个 sentence 当成新消息追加。 +- unknown/pending 使用独立块,不把诊断文本放在说话人名称中。 + +## 7. 建议的验收用例 + +| 用例 | 观察点 | 通过标准 | +| --- | --- | --- | +| A 说 3s,停 1s,B 说 3s | 两套服务的 raw segment | 至少两个不同 sentence_id,时间不重叠 | +| A 说 3s,停 300ms,B 说 3s | VAD 边界 | 明确记录为同段或分段,不能只看气泡颜色判断 | +| A 说 3s,B 只说 0.8s | 短 turn speaker 策略 | 原项目应能观察 `short_attach`;Demo 应保持 pending 或独立结果 | +| A/B 各说多段,speaker 服务延迟 2s | 异步回写 | 文本不重复,更新按 sentence_id 定位,顺序按时间恢复 | +| speaker 服务不可用 | 降级 | ASR 仍有 final,speaker 为未知并有明确 reason | +| stop 紧跟最后一帧 | 收尾屏障 | Demo 的 end 在 speaker 队列完成后发送;原项目记录可能迟到的 speaker 更新 | +| 断线后同 session_id 重连 | 会话隔离 | 原项目按 TTL 恢复;Demo 新连接不会复用旧 speaker center | + +## 8. 源码索引 + +### 当前独立 Demo + +- `demo/realtime_asr_optimization_demo/server.py` + - `RealtimeSession.__init__`:会话参数、队列和 VAD 状态 + - `emit_state`:`raw_segments/display_blocks/revision` + - `_emit_transcription`:partial/final 写入同一 `sentence_id` + - `_commit_segment`、`process_audio`:VAD、切段和 speaker job 入队 + - `_resolve_speaker`、`process_speakers`:异步声纹回写 + - `websocket_handler`:start、二进制帧、eof/stop/abort、end +- `demo/realtime_asr_optimization_demo/speaker_assembler.py` + - `apply_sentence`、`apply_speaker_update`、`display_blocks` +- `demo/realtime_asr_optimization_demo/model_service.py` + - 独立 vLLM OpenAI-compatible HTTP 适配 +- `demo/realtime_asr_optimization_demo/auxiliary_service.py` + - `/health`、`/v1/speaker/resolve`、`/v1/speaker/reset` 客户端适配 +- `demo/scripts/auxiliary_server.py` + - VAD/CAM++ 预加载、embedding 提取、session 级在线聚类 + +### 原项目 + +- `app/api/v1/websocket_asr.py` + - `/ws/v1/asr`、`/ws/v1/asr/qwen`、废弃 `/funasr` +- `app/services/qwen3_websocket_asr.py` + - `ConnectionContext`:音频、partial、confirmed segments、speaker 队列 + - `handle_connection`:WebSocket 状态机和消息协议 + - `_commit_retranscribe_turn`:final、carry、timeline、speaker job + - `_speaker_worker_loop`、`_resolve_and_emit_segment_speaker`:异步 speaker 回写 + - `_inherit_recent_*`、`_maybe_recluster_recent_segments`:身份传播和重聚类 + - `_stop`:最终提交、recluster、end +- `app/services/realtime_speaker_clusterer.py` + - chunk embedding、短段沿用、已有 speaker 匹配、混合段、timeline 平滑 +- `docs/realtime_meeting_websocket.md` + - 对外协议示例;其中 partial/final/speaker 回写必须按 `sentence_id` 幂等处理 + +本轮只新增本文档,没有修改上述实现。后续修复应先用第 5 节的三层证据确定问题属于切段、声纹还是展示层,再决定改哪一层。 diff --git a/docs/TODO/ENTROPY_REDUCTION_PLAN.md b/docs/TODO/ENTROPY_REDUCTION_PLAN.md new file mode 100644 index 0000000..ec95b91 --- /dev/null +++ b/docs/TODO/ENTROPY_REDUCTION_PLAN.md @@ -0,0 +1,66 @@ +# 熵减执行清单 + +本文档记录本轮已执行的熵减工作。目标是移除不可达路径、兼容占位、隐藏 fallback、重复请求流水线和无用依赖。 + +## 范围 + +- 主范围:`app/`、根运行配置、公开运行文档。 +- 不处理:`vendor/qwenasr` 内部实现、仅 benchmark 使用且不阻塞主链路的代码。 +- 原则:开发中项目不保留废弃接口、旧字段、兼容层或 fallback 逻辑。 + +## P0 + +- [x] 移除 Qwen3 `transformers` 后端残留路径。 + - 删除后端选择里的 `"transformers"` fallback。 + - 删除仅服务该路径的 batch/segment 转换死代码。 + - 不支持的设备显式失败。 +- [x] 启动预加载改为 fail-fast。 + - 模型完整性检查或预加载失败时停止 worker。 + - 不再静默降级到首次请求加载。 +- [x] 移除被忽略的离线模型兼容参数。 + - 删除 REST `model_id` 兼容处理。 + - 删除 OpenAI transcription `model` 兼容处理。 + - 运行时模型选择统一由部署计划和 `QWEN3_ASR_MODEL` 控制。 + +## P1 + +- [x] 抽出共享离线转写服务。 + - API 层只处理协议输入和响应格式。 + - 音频准备、`OfflineASRRequest`、runtime 调用和清理边界集中到服务层。 +- [x] 合并音频字节处理逻辑。 + - `process_from_request` 和 `process_upload_file` 共享私有 byte 处理 helper。 + +## P2 + +- [x] 将 `.tsscale` sidecar 隐式耦合改为显式结构化元数据。 +- [x] 拆分 WebSocket 路由和 Qwen3 协议服务。 + - Qwen3 websocket 状态机移出 API route。 + - route 模块仅保留端点注册和 service delegation。 +- [x] 删除首轮发现的无用 helper。 +- [x] 审计并移除无用直接依赖。 + - 根环境和 CPU 环境移除直接依赖 `pydub`、`httpx`。 + - 保留 ModelScope/FunASR 动态 runtime 依赖。 +- [x] 继续压缩阿里协议 WebSocket service。 + - 删除大段注释、未使用状态、未使用参数和死函数。 + - 合并重复响应构造。 + - 删除重复音频转换。 + +## 验证 + +- [x] `uv run python -m py_compile $(find app -name '*.py' -not -path '*/__pycache__/*') start.py` +- [x] `uvx pyright` +- [x] 变更模块 import smoke check。 +- [x] 手工 API smoke plan 已记录: + - `/stream/v1/asr` + - `/v1/audio/transcriptions` + - `/ws/v1/asr/funasr` + - `/ws/v1/asr/qwen` + +## 手工 Smoke Plan + +启动服务并准备模型后执行: + +1. `POST /stream/v1/asr`,使用小 WAV request body,确认返回 `result`、`segments`、`duration`、`processing_time`。 +2. `POST /v1/audio/transcriptions`,使用 multipart `file` 和 `response_format=verbose_json`,确认返回 OpenAI 风格 `text` 和 `segments`。 +3. 连接 `/ws/v1/asr/funasr`,发送阿里兼容 start/audio/stop 消息,确认 sentence 事件仍正常返回。 +4. 连接 `/ws/v1/asr/qwen`,发送 start/audio/stop 消息,确认 partial/final 事件仍正常返回。 diff --git a/docs/TODO/X86_RUST_ALIGN_OPT_PLAN.md b/docs/TODO/X86_RUST_ALIGN_OPT_PLAN.md new file mode 100644 index 0000000..845d37e --- /dev/null +++ b/docs/TODO/X86_RUST_ALIGN_OPT_PLAN.md @@ -0,0 +1,457 @@ +# x86 Rust Align Optimization Plan + +## 当前结论 + +- 当前工作区的 vendored Rust backend 已经收敛到更接近 upstream `huanglizhuo/QwenASR` 的 Linux/x86_64 路径: + - `release` + - `RUSTFLAGS="-C target-cpu=native"` + - `BLAS/OpenBLAS` + - x86_64 默认 `BF16` decode +- 保留的有意偏离只有两类: + - `SharedQwenModel` / shared model cache + - 中性的 `ffi` feature(`macos-ffi` 仅作为兼容别名保留) + +## upstream 参考 + +- upstream repo: `https://github.com/huanglizhuo/QwenASR` +- inspected commit: `4e85a19b05f034e106a345d279c68f50df718ab8` + +## 本机环境 + +- CPU: `Intel Core i5-13600KF` +- visible CPUs: `14` +- memory: user reported `G.SKILL DDR5-6400` + +## 已验证 benchmark + +音频: + +- `/opt/qwen3-asr/temp/test_assets/podcast_demo_2min_16k.wav` +- duration: `120s` + +### decode 路径对照(runtime concurrency = 4) + +`INT8 decode` + +- total: `173.42s` +- asr: `54.69s` +- align: `118.74s` +- rtf: `1.4452` + +来源: + +- `/opt/qwen3-asr/temp/test_assets/qwen_rust_runtime_concurrency_2min_int8.json` + +`BF16 decode` + +- total: `125.87s` +- asr: `46.40s` +- align: `79.47s` +- rtf: `1.0489` + +来源: + +- `/opt/qwen3-asr/temp/test_assets/qwen_rust_runtime_concurrency_2min_bf16.json` + +结论: + +- 在当前这台 x86_64 机器上,`BF16 decode` 明显优于 `INT8 decode` +- 因此 x86_64 默认 decode 路径应保持 `BF16` + +### 收敛后的默认路径 benchmark + +来源: + +- `/opt/qwen3-asr/temp/test_assets/qwen_rust_runtime_concurrency_2min_default_after_converge.json` + +结果: + +- `runtime concurrency = 4` + - total: `131.77s` + - asr: `51.54s` + - align: `80.23s` + - rtf: `1.0981` +- `runtime concurrency = 14` + - total: `128.25s` + - asr: `57.12s` + - align: `71.13s` + - rtf: `1.0688` + +结论: + +- 当前主瓶颈仍然在 `align` +- `align_sec` 明显大于或接近 `asr_sec` +- `runtime concurrency` 的最优值并不稳定,说明问题不在单纯线程数,而在具体阶段的访问模式和 kernel 行为 + +## 为什么不继续默认走 INT8 + +- upstream README 对 Linux/x86_64 的主路径描述是 `BLAS + AVX2/FMA` +- 当前本机实测中,`INT8 decode` 明显慢于 `BF16 decode` +- 说明这台机器上的主瓶颈不只是权重带宽,更多是: + - x86_64 上 INT8 kernel 的有效带宽利用率 + - cache / 数据布局 + - 实现成熟度差异 + +## 当前仍需保留的偏离 + +### 1. Shared model cache + +文件: + +- `/opt/qwen3-asr/vendor/qwenasr/crates/qwen-asr/src/context.rs` + +目的: + +- 多 runtime / 多 worker 场景下复用只读模型权重 +- 避免每个 runtime 重复 mmap / 持有整套权重 + +### 2. ffi feature + +文件: + +- `/opt/qwen3-asr/vendor/qwenasr/crates/qwen-asr/Cargo.toml` +- `/opt/qwen3-asr/vendor/qwenasr/crates/qwen-asr/src/lib.rs` +- `/opt/qwen3-asr/Dockerfile.cpu` + +目的: + +- 让 Linux CPU 集成不再依赖命名不准确的 `macos-ffi` +- 同时保留兼容别名,避免已有脚本立即失效 + +## align 热点拆解计划 + +### Phase 1: 阶段级 profiling + +状态:已完成 + +目标: + +- 先确认 `align` 的主耗时究竟在哪一段 + +位置: + +- `/opt/qwen3-asr/vendor/qwenasr/crates/qwen-asr/src/align.rs` +- `/opt/qwen3-asr/vendor/qwenasr/crates/qwen-asr/src/decoder.rs` + +需要拆出的阶段: + +- `mel_spectrogram` +- `encoder.forward` +- `input_embeds build` +- `decoder_prefill_logits` +- `timestamp argmax extract` +- `fix_timestamps` + +验收: + +- 2 分钟样本上输出稳定的阶段级耗时表 + +实际结果: + +- 产物: + - `/opt/qwen3-asr/temp/test_assets/qwen_rust_runtime_concurrency_2min_align_profile.json` + - `/opt/qwen3-asr/temp/test_logs/qwen_rust_runtime_concurrency_2min_align_profile.stderr` +- 结论: + - `align` 的主热点明确落在 `decoder_prefill_logits` + - `final rms_norm` 和 `lm_head projection` 不是主矛盾 + +### Phase 2: decoder_prefill_logits 内部分解 + +状态:已完成 + +如果 `decoder_prefill_logits` 是主热点,则继续拆分: + +- `decoder_prefill` +- final `rms_norm` +- `lm_head projection` + +目的: + +- 判断到底是 decoder prefill 慢,还是最后分类头 projection 慢 + +实际结果: + +- 产物: + - `/opt/qwen3-asr/temp/test_assets/qwen_rust_runtime_concurrency_2min_align_breakdown.json` + - `/opt/qwen3-asr/temp/test_logs/qwen_rust_runtime_concurrency_2min_align_breakdown.stderr` +- 结论: + - 真正的大头是 `decoder_prefill` + - 长段(`seq_len=1801`)时,`attention_ms` 占 `decoder_prefill` 的绝大部分 + - 关键样本: + - `decoder_prefill total_ms=79280.78` + - `attention_ms=73769.91` + - `qkv_ms=1336.33` + - `gate_up_ms=1866.68` + - `down_proj_ms=955.94` + +### Phase 2.5: 失败尝试记录 + +状态:已完成并回退 + +尝试: + +- 针对 x86_64 预先物化 prefill 用 F32 权重,避免每次 `align` 反复做 `BF16 -> F32` + +结果: + +- 长段 `decoder_prefill` 没有稳定收益,反而出现回归 +- 这条路径已经回退,不保留在主线代码里 + +结论: + +- 当前瓶颈不是简单的 BF16 权重转换 +- 更直接的问题是 multi-token causal attention 的算法路径 + +### Phase 3: 对热点段做针对性优化 + +状态:第一轮已完成 + +根据 profiling 结果,按优先级选一个方向: + +1. 如果热点在 `input_embeds build` + - 复用固定 prefix/suffix embeddings + - 减少逐 token 小块 copy + - 降低每段 align 的重复构造开销 + +2. 如果热点在 `decoder_prefill` + - 检查 `BF16 matvec / attention / swiglu` 的实际热点 + - 优化并行粒度或数据布局 + +3. 如果热点在 `lm_head projection` + - 优先优化 `BF16` classify head 路径 + - 避免无价值的整块 materialize + - 但只有在 profiling 证明是主热点后才动 + +4. 如果热点在后处理 + - 精简 `fix_timestamps` + - 减少 `Vec/String` 分配 + +### Phase 3 实施结果 + +本轮实际落地的是: + +- 文件: + - `/opt/qwen3-asr/vendor/qwenasr/crates/qwen-asr/src/kernels/mod.rs` +- 改动: + - 对 BLAS multi-token causal attention 增加长序列专用 batched 路径 + - 仅在 `seq_q >= 256` 时启用 + - 从“每个 head、每一行 2 次小 GEMM”改为: + - 每个 head 1 次 `Q @ K^T` + - 行级 causal softmax + - 每个 head 1 次 `softmax @ V` + +### Phase 3 回归结果 + +来源: + +- 优化后 benchmark: + - `/opt/qwen3-asr/temp/test_assets/qwen_rust_runtime_concurrency_2min_after_attention_opt.json` +- 优化后 profiling: + - `/opt/qwen3-asr/temp/test_assets/qwen_rust_runtime_concurrency_2min_attention_opt_profile.json` + - `/opt/qwen3-asr/temp/test_logs/qwen_rust_runtime_concurrency_2min_attention_opt_profile.stderr` + +关键对比: + +- 之前默认路径(2 分钟样本,基线文件): + - `total=128.25s` + - `asr=57.12s` + - `align=71.13s` +- 优化后: + - `total=76.46s` + - `asr=45.32s` + - `align=31.14s` + - `rtf=0.6372` + +attention 热点变化: + +- 长段 `seq_len=1801` + - 优化前: + - `decoder_prefill total_ms=79280.78` + - `attention_ms=73769.91` + - 优化后: + - `decoder_prefill total_ms=16625.88` + - `attention_ms=9981.88` + +结论: + +- 当前这台 i5 上,`align` 的主矛盾已经从“attention 明显失控”收敛到了“attention 仍是第一热点,但已降到可接受量级” +- 这一轮优化是有效的,应该保留 + +## Phase 4: FFN 路径继续收敛 + +状态:已完成一轮,并保留有效部分 + +本轮动作: + +- 文件: + - `/opt/qwen3-asr/vendor/qwenasr/crates/qwen-asr/src/decoder.rs` +- 改动: + - 仅对 x86_64 `BF16 prefill` 的 FFN 路径物化共享 F32 权重 + - 范围只包括: + - `gate_up_fused` + - `down_weight` + - `QKV` 和 `O-proj` 暂不纳入 + +回归数据: + +- 不带 profiling: + - `/opt/qwen3-asr/temp/test_assets/qwen_rust_runtime_concurrency_2min_after_ffn_opt.json` + - `total=58.61s` + - `asr=29.50s` + - `align=29.11s` +- 带 profiling: + - `/opt/qwen3-asr/temp/test_assets/qwen_rust_runtime_concurrency_2min_ffn_opt_profile.json` + - `total=62.07s` + - `asr=32.15s` + - `align=29.92s` + +与上一轮 attention-only profiling 对比: + +- attention-only: + - `total=64.58s` + - `align=30.86s` +- FFN-opt: + - `total=62.07s` + - `align=29.92s` + +长段热点对比(`seq_len=1801`): + +- FFN-opt 之前: + - `attention_ms=9981.88` + - `gate_up_ms=2080.10` + - `down_proj_ms=1183.64` +- FFN-opt 之后: + - `attention_ms=9660.15` + - `gate_up_ms=2210.39` + - `down_proj_ms=1166.96` + +结论: + +- FFN 这轮不是“大收益”,但 `align_sec` 仍然有小幅下降 +- 收益不像 attention 优化那样压倒性,更像是小幅收敛 +- 当前可以保留,但不值得继续在同一方向上扩大复杂度 + +## Phase 5: QKV / O-proj 试验与回退 + +状态:已完成并回退 + +尝试: + +- 在 prefill 中进一步物化 `QKV` 和 `O-proj` 的共享 F32 权重 + +回归数据: + +- 试验版本: + - `/opt/qwen3-asr/temp/test_assets/qwen_rust_runtime_concurrency_2min_after_qkv_opt.json` + - `total=63.12s` + - `align=30.07s` + +结论: + +- 相比 FFN-opt 版本,没有形成净收益 +- 因此这条路径已回退,不保留在主线代码里 + +## Phase 6: attention 继续细化的两次试验 + +状态:已完成并回退 + +### 试验 A:query-block batched attention + +尝试: + +- 将长序列 batched causal attention 从“整段一次性 `QK^T` / `SV`”改成按 query block 分块执行 + +结果: + +- `/opt/qwen3-asr/temp/test_assets/qwen_rust_runtime_concurrency_2min_after_attention_block_opt.json` +- `total=79.18s` +- `align=33.40s` + +结论: + +- 这条路径在当前 i5 + OpenBLAS 组合下没有收益 +- 增加 GEMM 次数带来的额外调度开销,超过了小块缓存收益 +- 已回退 + +### 试验 B:提高 batched 切换阈值到 512 + +尝试: + +- 只改 `BATCHED_CAUSAL_ATTENTION_THRESHOLD` +- 让中等长度序列继续走 row-wise 路径,只把更长的序列交给 batched 路径 + +结果: + +- `/opt/qwen3-asr/temp/test_assets/qwen_rust_runtime_concurrency_2min_after_attention_threshold512.json` +- `total=74.49s` +- `align=30.42s` + +结论: + +- 相比当前主线最优版本也没有收益 +- 说明当前阈值 `256` 不是主要问题 +- 已恢复回 `256` + +## Phase 7: K/V block + online softmax 试验 + +状态:已完成并回退 + +尝试: + +- 仅替换长序列 batched attention 路径 +- 改为按 `K/V` block 流式累积的 online softmax +- 短序列和单 token 路径完全不动 + +目标: + +- 不再一次性物化整块 `scores` +- 降低大矩阵内存压力 +- 观察是否能进一步压低长段 `attention_ms` + +结果: + +- `/opt/qwen3-asr/temp/test_assets/qwen_rust_runtime_concurrency_2min_after_kvblock_online_softmax.json` +- `total=74.11s` +- `align=30.33s` + +结论: + +- 当前实现下没有优于主线最优版本 +- 在这台机器上,额外的 block 循环和 online softmax 合并开销,超过了减少大 `scores` 矩阵带来的收益 +- 已回退 + +## 当前判断 + +- 当前最值钱、且已验证有效的优化仍然是: + - 长序列 batched causal attention + - FFN selective F32 物化 +- 继续扩大到 `QKV/O-proj` 这一步暂时不划算 +- query-block 化和 batched 阈值调优目前也不划算 +- `K/V block + online softmax` 在当前实现形态下也不划算 +- 后续判断应优先看: + - `align_sec` + - `decoder_prefill` profiling + - 尤其是长段 `seq_len` 下的热点变化 + +补充: + +- `asr_sec` 在多次回归中波动明显大于 `align_sec` +- 因此后续评估优化效果时,不应只盯总耗时,应优先以 `align` profiling 为准 + +## 暂不做的事 + +- 不再把 x86_64 默认路径改回 `INT8 decode` +- 不继续做没有 profiling 支撑的 `align` 结构性改写 +- 不围绕 `runtime concurrency` 数量盲调 + +## 下一步执行顺序 + +1. 保留当前 batched causal attention 路径,继续观察不同长段下的稳定性 +2. 保留 FFN selective F32 物化,继续观察其稳定收益 +3. 如需继续优化,优先看: + - 更激进的 `attention` 算法级改动,例如按 `K/V` block 的 online softmax + - 再其次才是 `qkv_ms + gate_up_ms + down_proj_ms` +4. 如果后续继续深挖,再考虑: + - batched attention 的 block 化,降低大 score matrix 的瞬时内存 + - `decoder_prefill` 内的投影层进一步收敛 +5. 保持同一份 2 分钟样本持续回归,避免再次把回归误当成优化 diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..fddb2eb --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,699 @@ +# Qwen3-ASR 部署指南 + +快速部署 Qwen3-ASR 语音识别服务,支持 CPU/macOS、NVIDIA GPU、沐曦 GPU、天数 GPU 与摩尔线程 GPU 运行形态。 + +如果你正在继续验证本轮 CUDA 官方 vLLM 迁移,请同时参考: + +- [PENDING_CUDA_VLLM_HANDOFF.md](./TODO/PENDING_CUDA_VLLM_HANDOFF.md) + +依赖安装现在改成根目录默认 NVIDIA GPU,CPU、沐曦、天数与摩尔线程为单独特化环境: + +| 模式 | 命令 | 说明 | +|------|------|------| +| NVIDIA GPU | `uv sync` 或 `./scripts/sync_gpu_env.sh` | Linux/NVIDIA 运行时,默认锁定 CUDA 13.0/cu130 `torch 2.11.0` / `torchaudio 2.11.0` / `torchvision 0.26.0` + `vllm 0.20.0` | +| 沐曦 GPU | `./scripts/sync_metax_env.sh` | 同步公共依赖;可选 GPU 栈安装默认从沐曦 MACA PyPI 源按 `--no-deps` 安装 | +| 天数 GPU | `./scripts/sync_iluvatar_env.sh` | 同步公共依赖;GPU 栈使用天数官方 vLLM 镜像 | +| 摩尔线程 GPU | `./scripts/sync_mthreads_env.sh` | 同步公共依赖;GPU 栈使用摩尔线程官方 MUSA vLLM 镜像 | +| CPU | `./scripts/sync_cpu_env.sh` | Linux/CPU 运行时 | +| 自动 | `./scripts/sync_accel_env.sh` | 根据 `mx-smi` / `ixsmi` / `mthreads-gmi` / `nvidia-smi` 自动选择沐曦、天数、摩尔线程、NVIDIA 或 CPU 环境 | + +## 快速部署 + +### NVIDIA GPU 版本部署(推荐) + +适用于生产环境,提供更快的推理速度: + +**前置要求:** +- NVIDIA GPU(默认镜像面向 CUDA 13.0+;CUDA 12.6 / 13.0 可通过构建参数覆盖) +- 已安装 NVIDIA Container Toolkit +- 显存 12GB+(推荐 16GB+ 以支持 Qwen3-ASR 1.7B) + +```bash +# 使用 docker run(带模型挂载) +docker run -d --name qwen3-asr \ + --gpus all \ + -p 17003:8000 \ + -v /opt/dep/asr/models:/app/models \ + -v /opt/dep/asr/data:/app/data \ + -e ACCELERATOR=nvidia \ + -e DEVICE=auto \ + -e QWEN_GPU_MEMORY_UTILIZATION=0.3 \ + -e QWEN_VLLM_ENFORCE_EAGER=true \ + unis/qwen3-asr:gpu-latest + +# 或使用 docker-compose(推荐) +docker-compose up -d +``` + +### 沐曦 GPU 版本部署 + +适用于已安装沐曦驱动与容器运行栈的机器: + +```bash +docker run -d --name qwen3-asr-metax \ + --privileged \ + --network=host \ + --pid=host \ + --ipc=host \ + -v /dev:/dev \ + -v /opt/mxdriver:/opt/mxdriver:ro \ + -v /opt/dep/asr/models:/app/models \ + -v /opt/dep/asr/data:/app/data \ + -e ACCELERATOR=metax \ + -e PORT=17003 \ + -e METAX_VISIBLE_DEVICES=0 \ + unis/qwen3-asr:metax-latest + +# 或使用 docker-compose-metax.yml +docker compose -f docker-compose-metax.yml up -d +``` + +构建沐曦镜像时,`Dockerfile.metax` 会基于沐曦官方 vLLM 镜像融合本项目代码与通用依赖: + +```bash +./scripts/package_vendor_gpu_image.sh \ + --vendor metax \ + --base-image <沐曦官方vLLM镜像名> \ + -v n260-3.7.0.38 +``` + +沐曦等国产 GPU 的生产推荐路径是“厂商官方 vLLM 镜像 + 本项目代码/通用依赖”。不要在项目 Dockerfile 中重新 `pip install vllm`,避免解析到 PyPI/NVIDIA CUDA 依赖。 + +沐曦 GPU 的完整编译、模型准备和离线交付流程见 [沐曦 GPU 国产化离线部署指南](./metax_offline_deployment.md)。 + +### 天数 GPU 版本部署 + +适用于已安装天数驱动与容器运行栈的机器。按天数官方镜像运行建议,本 compose 使用 host network、host pid/ipc、privileged、`/dev`、`/usr/src`、`/lib/modules` 等挂载,并额外挂载本项目模型与数据目录: + +```bash +docker pull registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5 + +./scripts/package_vendor_gpu_image.sh \ + --vendor iluvatar \ + --base-image registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5 \ + -v vllm0.17.0-4.4.0-v5 + +ASR_IMAGE=unis/qwen3-asr:iluvatar-vllm0.17.0-4.4.0-v5 \ +docker compose -f docker-compose-iluvatar.yml up -d +``` + +天数 GPU 的完整编译、模型准备和离线交付流程见 [天数 GPU 国产化离线部署指南](./iluvatar_offline_deployment.md)。 + +### 摩尔线程 GPU 版本部署 + +适用于已安装摩尔线程驱动与 MUSA 容器运行栈的机器: + +```bash +docker pull registry.mthreads.com/presale/devtech/vllm_musa:s4000_4.3.5_d0519 + +./scripts/package_vendor_gpu_image.sh \ + --vendor mthreads \ + --base-image registry.mthreads.com/presale/devtech/vllm_musa:s4000_4.3.5_d0519 \ + -v s4000_4.3.5_d0519 + +ASR_IMAGE=unis/qwen3-asr:mthreads-s4000_4.3.5_d0519 \ +docker compose -f docker-compose-mthreads.yml up -d +``` + +摩尔线程 GPU 的完整编译、模型准备和离线交付流程见 [摩尔线程 GPU 国产化离线部署指南](./mthreads_offline_deployment.md)。 + +默认推荐将宿主机目录统一挂载到 `/opt/dep/asr` 下,目录结构如下: + +```text +/opt/dep/asr/models/ + Qwen/ + iic/ + damo/ +``` + +如果你希望挂载到自定义目录,可统一设置: + +```bash +export MODEL_STORAGE_DIR=/data/qwen3-asr-models +export DATA_STORAGE_DIR=/data/qwen3-asr-data +mkdir -p "$MODEL_STORAGE_DIR" "$DATA_STORAGE_DIR" +docker-compose up -d +``` + +### 多 GPU 拓扑模式 + +项目现在支持统一的多 GPU 拓扑开关: + +- `ASR_DEPLOY_TOPOLOGY=isolated` + - 默认模式 + - 每张卡启动 1 个 backend 实例 + - 容器内使用 Nginx 负载均衡到多个实例 +- `ASR_DEPLOY_TOPOLOGY=sharded` + - 单个 backend 进程占用多张卡 + - 由 vLLM 在进程内部做多卡分片 +- `ASR_DEPLOY_TOPOLOGY=auto` + - 优先尝试 `sharded` + - 如果当前平台、可见设备或 shard 数不满足条件,则自动回退到 `isolated` + +### NVIDIA 多 GPU 自动并行部署(推荐) + +适用于并发量较高场景。该方案通过容器 entrypoint 自动完成: +- 根据 `ASR_VISIBLE_DEVICES` 拉起多个 ASR 实例(每张卡 1 个实例) +- 容器内自动生成 Nginx upstream 并负载均衡到各实例 +- 对外仍只暴露一个服务端口(默认 `8000`) + +你不需要手工维护多个 `docker-compose` 服务块或手工维护 nginx upstream。 + +```bash +# 4 卡示例:GPU0,1,2,3 各启动 1 个实例 +ASR_VISIBLE_DEVICES=0,1,2,3 docker-compose up -d +``` + +常用组合: +- 单卡(保持默认):`ASR_VISIBLE_DEVICES=0` +- 双卡:`ASR_VISIBLE_DEVICES=0,1` +- 四卡:`ASR_VISIBLE_DEVICES=0,1,2,3` + +强制单实例多卡分片: + +```bash +ASR_DEPLOY_TOPOLOGY=sharded \ +ASR_VISIBLE_DEVICES=0,1 \ +docker-compose up -d +``` + +自动选择模式: + +```bash +ASR_DEPLOY_TOPOLOGY=auto \ +ASR_VISIBLE_DEVICES=0,1 \ +docker-compose up -d +``` + +**服务访问地址:** +- API 服务: `http://localhost:17003` +- API 文档: `http://localhost:17003/docs` + +### 沐曦多 GPU 自动并行部署 + +```bash +ASR_VISIBLE_DEVICES=0,1 docker compose -f docker-compose-metax.yml up -d +``` + +### 摩尔线程多 GPU 自动并行部署 + +```bash +ASR_VISIBLE_DEVICES=0,1 docker compose -f docker-compose-mthreads.yml up -d +``` + +### CPU 版本部署 + +适用于开发测试或无 GPU 环境: + +```bash +docker run -d --name qwen3-asr \ + -p 17003:8000 \ + -v /opt/dep/asr/models:/app/models \ + -v /opt/dep/asr/data:/app/data \ + -e DEVICE=cpu \ + unis/qwen3-asr:cpu-latest +``` + +**注意:** CPU 版本不使用 GPU/vLLM 路径。 +当前 CPU 镜像已集成 QwenASR Rust backend,会自动选择 `qwen3-asr-0.6b`。 +CPU 镜像默认使用可分发的 `x86-64-v2` Rust 构建目标,避免把构建机的 native CPU 指令带入通用镜像。 +如果你确认构建机与部署机 CPU 指令集一致,可在自建镜像时设置 `QWENASR_RUST_TARGET_CPU=native` 换取更激进优化。 +当前 Rust backend 的 x86 kernel 需要 `avx2` 与 `fma`,不满足时启动会给出明确错误。镜像默认限制 +`OPENBLAS_NUM_THREADS=1` / `OMP_NUM_THREADS=1` / `GOTO_NUM_THREADS=1`,以减少多 runtime 并发时的线程争抢。 +CUDA vLLM 与 CPU Rust 路径下,`word_timestamps=true` 会自动调用 forced aligner 返回字词级时间戳。 + +### 离线交付目录导出 + +如果你需要把镜像交付到不能联网的机器,推荐直接生成一个完整的离线交付目录。该脚本使用普通 `docker build` + `docker save`,不依赖 `buildx`: + +```bash +# 生成 GPU 离线交付目录 +./export_offline_bundle.sh --type gpu + +# 或生成 CPU 离线交付目录 +./export_offline_bundle.sh --type cpu + +# 或生成沐曦 GPU 离线交付目录 +./export_offline_bundle.sh \ + --type metax \ + --metax-base cr.metax-tech.com/public-ai-release/maca/vllm-metax:0.17.0-maca.ai3.5.3.307-torch2.8-py312-ubuntu22.04-amd64 \ + --skip-models +./export_offline_bundle.sh --type iluvatar --iluvatar-base registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5 +./export_offline_bundle.sh --type mthreads --mthreads-base registry.mthreads.com/presale/devtech/vllm_musa:s4000_4.3.5_d0519 + +# 或一次同时生成 GPU + CPU 离线交付目录 +./export_offline_bundle.sh --type all +``` + +生成后的目录形如: + +```text +build-file/ + 20260520_153000-all/ + qwen3-asr-cpu-20260520_153000-amd64.tar.gz + qwen3-asr-gpu-20260520_153000-amd64.tar.gz + docker-compose.yml + docker-compose-cpu.yml + docker-compose-metax.yml + .env.example + init_host_dirs.sh + README.md + DEPLOYMENT.md +``` + +其中会自动包含: + +- 对应类型的一份或两份镜像压缩包 +- 对应类型的 compose 文件;天数包只包含 `docker-compose-iluvatar.yml` +- 摩尔线程包只包含 `docker-compose-mthreads.yml` +- `.env` 模板 +- 宿主机目录初始化脚本 +- 离线部署说明文档 + +模型文件建议使用 `./scripts/download-models.sh --models-dir /opt/dep/asr/models` 单独准备;该脚本增量补齐缺失模型,不删除已有目录,也不依赖 uv。 + +当前运行时 / 设备默认值以主 README 为准: + +- `README.md` +- `docs/README_zh.md` +设计背景与实现思路可参考: + +- 当前 Qwen3 后端:`NVIDIA/沐曦 GPU -> vLLM`、`CPU/macOS -> vendored QwenASR Rust` +- 引用项目 [QwenASR](https://github.com/huanglizhuo/QwenASR) + +### macOS / Apple Silicon 本地部署 + +适用于 M1/M2/M3/M4 机器上的本地 Qwen3-ASR 推理。当前 macOS 已统一走 vendored QwenASR Rust CPU backend。 + +```bash +./scripts/sync_cpu_env.sh +source .venv/bin/activate +python start.py +``` + +### 验证部署 + +```bash +# 健康检查 +curl http://localhost:17003/stream/v1/asr/health + +# 查看可用模型 +curl http://localhost:17003/stream/v1/asr/models + +# 测试语音识别(阿里云协议) +curl -X POST "http://localhost:17003/stream/v1/asr" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @test.wav + +# 测试 OpenAI 兼容接口 +curl -X POST "http://localhost:17003/v1/audio/transcriptions" \ + -H "Authorization: Bearer any" \ + -F "file=@test.wav" \ + -F "model=qwen3-asr-1.7b" +``` + +## 从源码构建镜像 + +### 使用构建脚本 + +项目提供了一个更薄的 `scripts/build_docker.sh` 包装层,用于统一 `docker buildx` 参数: + +```bash +# 构建所有版本(CPU + GPU) +./scripts/build_docker.sh + +# 仅构建 GPU 版本 +./scripts/build_docker.sh -t gpu + +# 构建指定版本并推送 +./scripts/build_docker.sh -t all -v 1.0.1 -p + +# 查看帮助 +./scripts/build_docker.sh -h +``` + +**构建脚本参数:** + +| 参数 | 说明 | 默认值 | +|------|------|--------| +| `-a, --arch` | 目标架构: `amd64`, `arm64`, `multi` | `amd64` | +| `-t, --type` | 构建类型: `cpu`, `gpu`, `all` | `all` | +| `-v, --version` | 版本标签 | `latest` | +| `-p, --push` | 构建后推送到 Docker Hub | 否 | +| `-e, --export` | 导出单架构镜像为 tar.gz | 否 | +| `-o, --output` | 导出目录 | `.` | +| `-r, --registry` | 镜像仓库 | `unis` | +| `-n, --no-cache` | 禁用 Docker 构建缓存 | 否 | + +### 手动构建 + +```bash +# 构建 CPU 版本 +docker build -t qwen3-asr:cpu-latest -f Dockerfile.cpu . + +# 构建绑定当前机器指令集的 CPU 版本(仅适合同构部署) +docker build -t qwen3-asr:cpu-native -f Dockerfile.cpu \ + --build-arg QWENASR_RUST_TARGET_CPU=native \ + . + +# 构建默认 GPU 版本(CUDA 13.0 / PyTorch cu130) +docker build -t qwen3-asr:gpu-cu130 -f Dockerfile.gpu . + +# 构建 CUDA 12.6 版本 +docker build -t qwen3-asr:gpu-cu126 -f Dockerfile.gpu \ + --build-arg PYTORCH_BASE_IMAGE=pytorch/pytorch:2.11.0-cuda12.6-cudnn9-runtime \ + --build-arg PYTORCH_CUDA_INDEX=https://download.pytorch.org/whl/cu126 \ + --build-arg CUDA_NVCC_PACKAGE=cuda-nvcc-12-6 \ + --build-arg TORCH_CUDA_ARCH_LIST="8.0;8.6;8.9" \ + . + +# 构建 CUDA 13.0 版本 +docker build -t qwen3-asr:gpu-cu130 -f Dockerfile.gpu \ + --build-arg PYTORCH_BASE_IMAGE=pytorch/pytorch:2.11.0-cuda13.0-cudnn9-runtime \ + --build-arg PYTORCH_CUDA_INDEX=https://download.pytorch.org/whl/cu130 \ + --build-arg CUDA_NVCC_PACKAGE=cuda-nvcc-13-0 \ + --build-arg TORCH_CUDA_ARCH_LIST="12.0+PTX" \ + . +``` + +`Dockerfile.cpu` 可覆盖的 CPU 构建参数: + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `QWENASR_RUST_TARGET_CPU` | `x86-64-v2` | amd64 Rust backend 编译目标;可设为 `native` 构建绑定当前 CPU 的镜像 | + +`Dockerfile.gpu` 可覆盖的 GPU 构建参数: + +| 参数 | 默认值 | 用途 | +|------|--------|------| +| `PYTORCH_BASE_IMAGE` | `pytorch/pytorch:2.11.0-cuda13.0-cudnn9-runtime` | 选择 PyTorch/CUDA 基础镜像 | +| `PYTORCH_CUDA_INDEX` | `https://download.pytorch.org/whl/cu130` | 选择 PyTorch wheel CUDA 后端 | +| `CUDA_NVCC_PACKAGE` | `cuda-nvcc-13-0` | 安装匹配的 nvcc,用于 vLLM/FlashInfer JIT | +| `TORCH_CUDA_ARCH_LIST` | `12.0+PTX` | 指定 JIT 编译目标架构 | +| `VLLM_PACKAGE` | `vllm==0.20.0` | 覆盖 vLLM 包版本或来源 | + +### 模型说明 + +服务支持以下 ASR 模型: + +| 模型 | 说明 | 适用场景 | +|------|------|----------| +| Qwen3-ASR-1.7B ⭐ | 多语言 ASR(52种语言+方言,字级时间戳) | CUDA | +| Qwen3-ASR-0.6B | 轻量版多语言 ASR | CUDA / CPU Rust / macOS | + +**运行时模型选择:** + +系统根据机器资源自动选择合适的 Qwen3-ASR 模型: +- **显存 >= 32GB**: 自动加载 `qwen3-asr-1.7b` +- **显存 < 32GB**: 自动加载 `qwen3-asr-0.6b` +- **无 CUDA**: 自动加载基于 vendored Rust 的 `qwen3-asr-0.6b` +- **macOS / Apple Silicon**: 无论内存大小多少,默认都加载 `qwen3-asr-0.6b` +- **环境变量覆盖**: 设置 `QWEN3_ASR_MODEL=qwen3-asr-1.7b` 或 `QWEN3_ASR_MODEL=qwen3-asr-0.6b` 可硬覆盖自动选择 + +### 模型下载 + +启动时会先检测当前运行计划所需模型;如果本地缓存缺失,会自动从 ModelScope 下载。离线部署请提前准备模型缓存。 +手动准备方式: + +```bash +# 增量补齐离线部署所需模型,不删除已有文件 +./scripts/download-models.sh --models-dir /opt/dep/asr/models + +# 如果本机缺少 Python 依赖,也可以使用已构建镜像下载 +ASR_IMAGE=unis/qwen3-asr:iluvatar-vllm0.17.0-4.4.0-v5 \ +./scripts/download-models.sh --mode docker --models-dir /opt/dep/asr/models +``` + +离线部署时,推荐目录结构: + +```text +/opt/dep/asr/models/ + Qwen/ + iic/ + damo/ +``` + +然后保持与 compose 文件一致的挂载: + +```yaml +volumes: + - ${MODEL_STORAGE_DIR:-/opt/dep/asr/models}:/app/models + - ${DATA_STORAGE_DIR:-/opt/dep/asr/data}:/app/data +``` + +## 环境变量配置 + +### 基础配置 + +| 环境变量 | 默认值 | 说明 | +|----------|--------|------| +| `HOST` | `0.0.0.0` | 服务绑定地址 | +| `PORT` | `8000` | 服务端口 | +| `DEBUG` | `false` | 调试模式(启用后可访问 /docs) | +| `LOG_LEVEL` | `INFO` | 日志级别:DEBUG, INFO, WARNING, ERROR | +| `WORKERS` | `1` | 工作进程数(多进程会复制模型,显存成倍增加) | +| `MAX_AUDIO_SIZE` | `2048` | 最大音频文件大小(MB,支持单位如 2GB) | +| `API_KEY` | - | 服务端统一鉴权密钥 | + +### 设备配置 + +| 环境变量 | 默认值 | 说明 | +|----------|--------|------| +| `DEVICE` | `auto` | 设备选择:`auto`, `cpu`, `cuda:0` | +| `ASR_VISIBLE_DEVICES` | `0` | 统一可见 GPU 设备配置,程序会按当前 accelerator 自动映射到底层变量 | +| `ASR_DEPLOY_TOPOLOGY` | `isolated` | 部署拓扑:`isolated`, `sharded`, `auto` | + +### 内置 Nginx 与限流配置 + +| 环境变量 | 默认值 | 说明 | +|----------|--------|------| +| `NGINX_RATE_LIMIT_RPS` | `0` | 全局每秒请求上限,`0` 表示关闭 | +| `NGINX_RATE_LIMIT_BURST` | `0` | 全局突发请求数,`0` 时自动取 `NGINX_RATE_LIMIT_RPS` | + +### ASR 模型配置 + +| 环境变量 | 默认值 | 说明 | +|----------|--------|------| +| `ASR_ENABLE_REALTIME_PUNC` | `true` | 是否启用实时标点模型 | + +### 性能优化配置 + +| 环境变量 | 默认值 | 说明 | +|----------|--------|------| +| `ASR_BATCH_SIZE` | `4` | 长音频分段后的 ASR 批处理大小 | +| `INFERENCE_THREAD_POOL_SIZE` | 自动 | 推理线程池大小;默认按 CPU 核数自动设置 | +| `MAX_SEGMENT_SEC` | `60` | 音频分段最大时长(秒) | +| `QWEN_GPU_MEMORY_UTILIZATION` | `0.9` | vLLM 可保留的 GPU 显存上限,KV cache 不足时可适当调高 | +| `QWEN_VLLM_ENFORCE_EAGER` | `true` | 强制 vLLM eager 执行以提高兼容性;NVIDIA 性能测试可设为 `false` 允许 CUDA Graph 优化 | +| `WS_MAX_BUFFER_SIZE` | `160000` | WebSocket 音频缓冲区大小(样本数) | +| `QWEN_RUST_CPU_WORKERS` | `4` | CPU Rust backend worker 数;Rust ASR / forced align 默认按该数量并行 | +| `QWEN_RUST_ASR_CONCURRENCY` | `0` | Rust ASR 阶段批内并行度;`0` 表示跟随 `QWEN_RUST_CPU_WORKERS` | +| `QWEN_RUST_ALIGN_CONCURRENCY` | `0` | Rust forced align 阶段批内并行度;`0` 表示跟随 `QWEN_RUST_CPU_WORKERS` | +| `QWENASR_LIBRARY_PATH` | 自动探测 | 覆盖 vendored Rust 动态库路径 | + +### 远场过滤配置 + +流式 ASR 远场声音过滤功能,自动过滤远场声音和环境音: + +| 环境变量 | 默认值 | 说明 | +|----------|--------|------| +| `ASR_ENABLE_NEARFIELD_FILTER` | `true` | 启用远场声音过滤 | +| `ASR_NEARFIELD_RMS_THRESHOLD` | `0.01` | RMS 能量阈值 | +| `LOG_LEVEL=DEBUG` | - | 需要观察过滤细节时打开调试日志 | + +调优建议: + +- `ASR_NEARFIELD_RMS_THRESHOLD=0.01` 是当前默认值,也是推荐起点 +- 嘈杂环境可以适当调高,增强背景语音过滤 +- 安静环境如果出现小声说话漏识别,可以适当调低 +- 需要观察过滤行为时,可临时设置 `LOG_LEVEL=DEBUG` + +### 鉴权配置 + +| 环境变量 | 默认值 | 说明 | +|----------|--------|------| +| `API_KEY` | - | 服务端统一鉴权密钥;同时兼容 `Authorization: Bearer` 和 `X-NLS-Token` | + +**使用示例:** + +```bash +# 使用 Token +curl -H "X-NLS-Token: your_token" http://localhost:8000/stream/v1/asr/health + +# 使用 Bearer Token(OpenAI 兼容) +curl -H "Authorization: Bearer your_token" http://localhost:8000/v1/models +``` + +### 日志配置 + +| 环境变量 | 默认值 | 说明 | +|----------|--------|------| +| `LOG_LEVEL` | `INFO` | 日志级别:`DEBUG`, `INFO`, `WARNING` | +| `LOG_FILE` | `data/logs/qwen3-asr.log` | 日志文件路径 | +| `LOG_MAX_BYTES` | `20971520` | 单个日志文件最大大小(20MB) | +| `LOG_BACKUP_COUNT` | `50` | 日志备份文件数量 | + +## Docker Compose 配置 + +### 基础配置(GPU) + +```yaml +services: + qwen3-asr: + image: unis/qwen3-asr:gpu-latest + container_name: qwen3-asr + ports: + - "17003:8000" + volumes: + - /opt/dep/asr/models:/app/models + - /opt/dep/asr/data:/app/data + environment: + - DEBUG=false + - LOG_LEVEL=INFO + - DEVICE=auto + - QWEN_GPU_MEMORY_UTILIZATION=0.3 + - QWEN_VLLM_ENFORCE_EAGER=true + - ASR_BATCH_SIZE=4 + - WORKERS=1 + - INFERENCE_THREAD_POOL_SIZE=4 + restart: unless-stopped + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] +``` + +### CPU 版本配置 + +```yaml +services: + qwen3-asr: + image: unis/qwen3-asr:cpu-latest + container_name: qwen3-asr + ports: + - "17003:8000" + volumes: + - /opt/dep/asr/models:/app/models + - /opt/dep/asr/data:/app/data + environment: + - DEBUG=false + - LOG_LEVEL=INFO + - DEVICE=cpu + - WORKERS=1 + - INFERENCE_THREAD_POOL_SIZE=1 + restart: unless-stopped +``` + +### 生产环境配置(内置 Nginx,推荐) + +```yaml +services: + qwen3-asr: + image: unis/qwen3-asr:gpu-latest + container_name: qwen3-asr + ports: + - "17003:8000" + volumes: + - /opt/dep/asr/models:/app/models + - /opt/dep/asr/data:/app/data + environment: + - DEBUG=false + - LOG_LEVEL=INFO + - DEVICE=auto + - CUDA_VISIBLE_DEVICES=0,1 + - QWEN_GPU_MEMORY_UTILIZATION=0.3 + - QWEN_VLLM_ENFORCE_EAGER=true + - NGINX_RATE_LIMIT_RPS=20 + - NGINX_RATE_LIMIT_BURST=40 + - WORKERS=1 + - INFERENCE_THREAD_POOL_SIZE=4 + - ASR_BATCH_SIZE=4 + restart: unless-stopped + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] +``` + +## 服务监控 + +### 健康检查 + +```bash +curl http://localhost:17003/stream/v1/asr/health +``` + +### 日志监控 + +```bash +# 实时查看日志 +docker logs -f qwen3-asr + +# 查看错误日志 +docker logs qwen3-asr 2>&1 | grep -i error +``` + +### 资源监控 + +```bash +# 容器资源使用 +docker stats qwen3-asr + +# GPU 使用情况 +docker exec -it qwen3-asr nvidia-smi +``` + +## 资源需求 + +### 最小配置(CPU 版本) + +- CPU: 4 核 +- 内存: 8GB +- 磁盘: 10GB + +### 推荐配置(GPU 版本) + +- CPU: 8 核 +- 内存: 16GB +- GPU: NVIDIA GPU (12GB+ 显存,含说话人分离模型) +- 磁盘: 25GB + +## 故障排除 + +### 常见问题 + +| 问题 | 症状 | 解决方案 | +|------|------|----------| +| GPU 内存不足 | CUDA OOM 错误 | 设置 `DEVICE=cpu` 或使用更大显存的 GPU | +| 模型加载失败 / 缓慢 | 本地模型缓存缺失 | 先运行 `./scripts/download-models.sh --models-dir /opt/dep/asr/models` 预准备模型 | +| 端口被占用 | 端口冲突错误 | 修改端口映射:`"8080:8000"` | +| 说话人分离失败 | CAM++ 模型错误 | 检查模型是否完整下载,显存是否充足 | + +### 调试模式 + +```bash +# 启用调试模式 +docker run -e DEBUG=true -e LOG_LEVEL=DEBUG ... + +# 进入容器调试 +docker exec -it qwen3-asr /bin/bash +``` + +## 更新服务 + +```bash +# 拉取最新镜像(GPU 版本) +docker pull unis/qwen3-asr:gpu-latest + +# 拉取最新镜像(CPU 版本) +docker pull unis/qwen3-asr:cpu-latest + +# 重启服务 +docker-compose down && docker-compose up -d +``` diff --git a/docs/iluvatar_offline_deployment.md b/docs/iluvatar_offline_deployment.md new file mode 100644 index 0000000..4f9b4d6 --- /dev/null +++ b/docs/iluvatar_offline_deployment.md @@ -0,0 +1,242 @@ +# 天数 GPU 国产化离线部署指南 + +本文档用于天数/Iluvatar GPU 环境的编译、模型准备、离线包导出与目标机部署。 + +## 推荐基础镜像 + +天数默认推荐使用以下官方 vLLM 镜像作为基础镜像: + +```bash +registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5 +``` + +该镜像负责提供天数 IX 运行时、PyTorch、vLLM 与相关内核。本项目的 `Dockerfile.iluvatar` 只叠加通用 Python 依赖和业务代码,不在 Dockerfile 内重新安装 vLLM,也不使用 uv 虚拟环境。 + +## 目录约定 + +目标机推荐统一使用以下宿主机目录: + +```text +/opt/dep/asr/ + models/ + data/ + logs/ + temp/ + tasks/ +``` + +容器内默认挂载为: + +```text +/app/models +/app/data +``` + +## 在线编译融合镜像 + +在可访问天数镜像仓库和 Python 包源的构建机上执行: + +```bash +docker pull registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5 + +./scripts/package_vendor_gpu_image.sh \ + --vendor iluvatar \ + --base-image registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5 \ + -v vllm0.17.0-4.4.0-v5 +``` + +脚本会生成融合镜像: + +```text +unis/qwen3-asr:iluvatar-vllm0.17.0-4.4.0-v5 +``` + +并导出镜像归档到: + +```text +build-file/qwen3-asr-iluvatar-vllm0.17.0-4.4.0-v5-amd64.tar.gz +``` + +如果只需要本机镜像,不需要导出 tar 包,可增加 `--no-export`。 + +## 单独准备模型 + +模型下载建议独立于业务服务执行。`download-models.sh` 是增量下载脚本,不会删除已有模型目录,也不依赖 uv。 + +在项目根目录或离线包目录执行: + +```bash +./scripts/download-models.sh --models-dir /opt/dep/asr/models +``` + +如果本机没有 Python 依赖,但已经有融合镜像,可以用镜像内环境下载: + +```bash +ASR_IMAGE=unis/qwen3-asr:iluvatar-vllm0.17.0-4.4.0-v5 \ +./scripts/download-models.sh \ + --mode docker \ + --models-dir /opt/dep/asr/models +``` + +模型目录最终应至少包含: + +```text +/opt/dep/asr/models/ + Qwen/ + Qwen3-ASR-0.6B/ + Qwen3-ForcedAligner-0.6B/ + damo/ + iic/ +``` + +## 导出完整离线交付包 + +如果要交付给不能联网的目标机,推荐直接导出天数专用离线包: + +```bash +./export_offline_bundle.sh \ + --type iluvatar \ + --iluvatar-base registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5 \ + -v vllm0.17.0-4.4.0-v5 +``` + +如果模型要单独准备,不希望离线包包含模型压缩包: + +```bash +./export_offline_bundle.sh \ + --type iluvatar \ + --iluvatar-base registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5 \ + -v vllm0.17.0-4.4.0-v5 \ + --skip-models +``` + +天数离线包只会包含天数专用启动文件: + +```text +docker-compose-iluvatar.yml +.env +.env.example +init_host_dirs.sh +download-models.sh +download_models_standalone.py +README.md +DEPLOYMENT.md +BUNDLE_INFO.txt +qwen3-asr-iluvatar-*-amd64.tar.gz +``` + +不会再要求使用通用 `docker-compose.yml`。 + +## 目标机离线部署 + +将整个离线包复制到目标机后,进入离线包目录: + +```bash +chmod +x init_host_dirs.sh download-models.sh +./init_host_dirs.sh +``` + +导入镜像: + +```bash +gunzip -c qwen3-asr-iluvatar-vllm0.17.0-4.4.0-v5-amd64.tar.gz | docker load +``` + +确认 `.env` 中的镜像名与导入镜像一致: + +```env +ASR_IMAGE=unis/qwen3-asr:iluvatar-vllm0.17.0-4.4.0-v5 +``` + +按需设置显卡与 vLLM 显存比例: + +```env +ILUVATAR_VISIBLE_DEVICES=0 +IX_VISIBLE_DEVICES=0 +CUDA_VISIBLE_DEVICES=0 +QWEN_GPU_MEMORY_UTILIZATION=0.25 +QWEN_VLLM_ENFORCE_EAGER=true +``` + +启动服务: + +```bash +docker compose -f docker-compose-iluvatar.yml up -d +``` + +查看状态与日志: + +```bash +docker compose -f docker-compose-iluvatar.yml ps +docker compose -f docker-compose-iluvatar.yml logs -f +``` + +服务默认监听 host 网络端口: + +```text +http://<目标机IP>:17003 +``` + +## 已导出的旧离线包处理 + +如果旧离线包里的镜像已经能识别 `Qwen3ASRForConditionalGeneration`,但启动时报 KV cache 不足,例如: + +```text +Try increasing gpu_memory_utilization or decreasing max_model_len +``` + +不需要重新打镜像。只需要在旧离线包的 `docker-compose-iluvatar.yml` 的 `QWEN3_ASR_MODEL` 附近补充: + +```yaml + QWEN_GPU_MEMORY_UTILIZATION: ${QWEN_GPU_MEMORY_UTILIZATION:-0.25} + QWEN_VLLM_ENFORCE_EAGER: ${QWEN_VLLM_ENFORCE_EAGER:-true} +``` + +然后重新创建容器: + +```bash +docker compose -f docker-compose-iluvatar.yml down +docker compose -f docker-compose-iluvatar.yml up -d +``` + +只执行 `restart` 不会重新注入环境变量。 + +## 常见问题 + +### 为什么不用 uv? + +天数 Docker 镜像内推荐直接使用系统 Python 环境。`Dockerfile.iluvatar` 使用: + +```bash +python3 -m pip install --no-cache-dir -r environments/iluvatar/requirements.txt +``` + +不创建 uv 虚拟环境,也不在镜像内运行 `uv pip install`。 + +### 为什么不重新安装 vLLM? + +国产 GPU 的 vLLM、PyTorch、内核和运行时通常需要严格匹配厂商镜像。项目层重新 `pip install vllm` 容易解析到 PyPI/NVIDIA CUDA 依赖,破坏天数官方镜像里的匹配关系。 + +### `QWEN_GPU_MEMORY_UTILIZATION` 为什么没生效? + +变量必须进入容器才会生效。天数 compose 中需要有: + +```yaml + QWEN_GPU_MEMORY_UTILIZATION: ${QWEN_GPU_MEMORY_UTILIZATION:-} + QWEN_VLLM_ENFORCE_EAGER: ${QWEN_VLLM_ENFORCE_EAGER:-true} +``` + +然后在 `.env` 设置: + +```env +QWEN_GPU_MEMORY_UTILIZATION=0.25 +QWEN_VLLM_ENFORCE_EAGER=true +``` + +修改 `.env` 后必须 `down` 再 `up -d`。 + +### 日志中的本地模型 repo id warning 是否致命? + +vLLM 可能会先尝试按远端 repo 方式读取 safetensors,遇到本地路径时打印 warning。如果后续出现 `Loading safetensors checkpoint shards` 并继续加载权重,通常不是致命错误。 + +真正需要处理的是最后的异常,例如模型架构不识别、KV cache 不足、模型文件缺失等。 diff --git a/docs/metax_offline_deployment.md b/docs/metax_offline_deployment.md new file mode 100644 index 0000000..66b4b0a --- /dev/null +++ b/docs/metax_offline_deployment.md @@ -0,0 +1,234 @@ +# 沐曦 GPU 国产化离线部署指南 + +本文档用于沐曦/MetaX GPU 环境的编译、模型准备、离线包导出与目标机部署。 + +## 基础镜像原则 + +沐曦部署应使用沐曦官方或现场确认的 vLLM/MACA 基础镜像: + +```bash +cr.metax-tech.com/public-ai-release/maca/vllm-metax:0.17.0-maca.ai3.5.3.307-torch2.8-py312-ubuntu22.04-amd64 +``` + +该镜像负责提供 MACA 运行时、PyTorch、vLLM 与相关内核。本项目的 `Dockerfile.metax` 只叠加通用 Python 依赖和业务代码,不在 Dockerfile 内重新安装 vLLM,也不使用 uv 虚拟环境。 + +## 目录约定 + +目标机推荐统一使用以下宿主机目录: + +```text +/opt/dep/asr/ + models/ + data/ + logs/ + temp/ + tasks/ +``` + +容器内默认挂载为: + +```text +/app/models +/app/data +``` + +## 在线编译融合镜像 + +先按沐曦官网复制的命令登录并拉取基础镜像。账号、密码和 token 不要写入项目文件、`.env` 或离线包。 + +官网命令通常形如: + +```bash +docker login --username=<沐曦账号> --password=<沐曦token> cr.metax-tech.com && \ +docker pull cr.metax-tech.com/public-ai-release/maca/vllm-metax:0.17.0-maca.ai3.5.3.307-torch2.8-py312-ubuntu22.04-amd64 +``` + +如果沐曦基础镜像是 tar 包,则改为先导入: + +```bash +docker load -i metax-vllm-official.tar +docker images | grep -i -E 'metax|maca|vllm' +``` + +然后用完整官方镜像名构建融合镜像: + +```bash +./scripts/package_vendor_gpu_image.sh \ + --vendor metax \ + --base-image cr.metax-tech.com/public-ai-release/maca/vllm-metax:0.17.0-maca.ai3.5.3.307-torch2.8-py312-ubuntu22.04-amd64 \ + -v 0.17.0-maca.ai3.5.3.307 +``` + +脚本会生成融合镜像: + +```text +unis/qwen3-asr:metax-0.17.0-maca.ai3.5.3.307 +``` + +并导出镜像归档到: + +```text +build-file/qwen3-asr-metax-0.17.0-maca.ai3.5.3.307-amd64.tar.gz +``` + +## 单独准备模型 + +模型下载建议独立于业务服务执行。`download-models.sh` 是增量下载脚本,不会删除已有模型目录,也不依赖 uv。 + +在项目根目录或离线包目录执行: + +```bash +./scripts/download-models.sh --models-dir /opt/dep/asr/models +``` + +如果本机没有 Python 依赖,但已经有融合镜像,可以用镜像内环境下载: + +```bash +ASR_IMAGE=unis/qwen3-asr:metax-0.17.0-maca.ai3.5.3.307 \ +./scripts/download-models.sh \ + --mode docker \ + --models-dir /opt/dep/asr/models +``` + +## 导出完整离线交付包 + +```bash +./export_offline_bundle.sh \ + --type metax \ + --metax-base cr.metax-tech.com/public-ai-release/maca/vllm-metax:0.17.0-maca.ai3.5.3.307-torch2.8-py312-ubuntu22.04-amd64 \ + -v 0.17.0-maca.ai3.5.3.307 +``` + +如果模型要单独准备,不希望离线包包含模型压缩包: + +```bash +./export_offline_bundle.sh \ + --type metax \ + --metax-base cr.metax-tech.com/public-ai-release/maca/vllm-metax:0.17.0-maca.ai3.5.3.307-torch2.8-py312-ubuntu22.04-amd64 \ + -v 0.17.0-maca.ai3.5.3.307 \ + --skip-models +``` + +如果不指定 `-v`,脚本会使用当前时间戳作为版本号。 + +沐曦官方镜像内的 Python 默认使用 `/opt/conda/bin/python`。如果现场镜像路径不同,可额外指定: + +```bash +./export_offline_bundle.sh \ + --type metax \ + --metax-base cr.metax-tech.com/public-ai-release/maca/vllm-metax:0.17.0-maca.ai3.5.3.307-torch2.8-py312-ubuntu22.04-amd64 \ + --metax-python /path/to/python \ + --skip-models +``` + +也可以先给官方长镜像名打一个本地短标签,再把短标签传给 `--metax-base`;这只是为了少复制长镜像名,不是必需步骤。 + +沐曦离线包只会包含沐曦专用启动文件: + +```text +docker-compose-metax.yml +.env +.env.example +init_host_dirs.sh +download-models.sh +download_models_standalone.py +README.md +DEPLOYMENT.md +METAX_DEPLOYMENT.md +BUNDLE_INFO.txt +qwen3-asr-metax-*-amd64.tar.gz +``` + +不会要求使用通用 `docker-compose.yml`。 + +## 目标机离线部署 + +将整个离线包复制到目标机后,进入离线包目录: + +```bash +chmod +x init_host_dirs.sh download-models.sh +./init_host_dirs.sh +``` + +导入镜像: + +```bash +gunzip -c qwen3-asr-metax-0.17.0-maca.ai3.5.3.307-amd64.tar.gz | docker load +``` + +确认 `.env` 中的镜像名与导入镜像一致: + +```env +ASR_IMAGE=unis/qwen3-asr:metax-0.17.0-maca.ai3.5.3.307 +``` + +按需设置显卡与 vLLM 显存比例: + +```env +METAX_VISIBLE_DEVICES=0 +MACA_VISIBLE_DEVICES=0 +MX_VISIBLE_DEVICES=0 +QWEN_GPU_MEMORY_UTILIZATION=0.25 +QWEN_VLLM_ENFORCE_EAGER=true +``` + +启动服务: + +```bash +docker compose -f docker-compose-metax.yml up -d +``` + +查看状态与日志: + +```bash +docker compose -f docker-compose-metax.yml ps +docker compose -f docker-compose-metax.yml logs -f +``` + +服务默认端口: + +```text +http://<目标机IP>:17003 +``` + +## 沐曦与天数的差异 + +- 沐曦使用 `Dockerfile.metax` 和 `docker-compose-metax.yml` +- 天数使用 `Dockerfile.iluvatar` 和 `docker-compose-iluvatar.yml` +- 沐曦目标机需要可访问宿主 `/dev` 与 `/opt/mxdriver` +- 沐曦 compose 使用 host network、host pid/ipc、privileged,并挂载 `/dev` 与 `/opt/mxdriver` +- 天数 compose 同样使用 host network、host pid/ipc、privileged 和宿主机设备/驱动目录挂载 + +## 常见问题 + +### 为什么不用 uv? + +沐曦 Docker 镜像内推荐直接使用系统 Python 环境。`Dockerfile.metax` 使用: + +```bash +python3 -m pip install --no-cache-dir -r environments/metax/requirements.txt +``` + +不创建 uv 虚拟环境,也不在镜像内运行 `uv pip install`。 + +### 为什么不重新安装 vLLM? + +国产 GPU 的 vLLM、PyTorch、内核和运行时通常需要严格匹配厂商镜像。项目层重新 `pip install vllm` 容易解析到 PyPI/NVIDIA CUDA 依赖,破坏沐曦官方镜像里的匹配关系。 + +### `QWEN_GPU_MEMORY_UTILIZATION` 为什么没生效? + +变量必须进入容器才会生效。沐曦 compose 中需要有: + +```yaml + QWEN_GPU_MEMORY_UTILIZATION: ${QWEN_GPU_MEMORY_UTILIZATION:-} + QWEN_VLLM_ENFORCE_EAGER: ${QWEN_VLLM_ENFORCE_EAGER:-true} +``` + +然后在 `.env` 设置: + +```env +QWEN_GPU_MEMORY_UTILIZATION=0.25 +QWEN_VLLM_ENFORCE_EAGER=true +``` + +修改 `.env` 后必须 `down` 再 `up -d`。 diff --git a/docs/mthreads_offline_deployment.md b/docs/mthreads_offline_deployment.md new file mode 100644 index 0000000..538d819 --- /dev/null +++ b/docs/mthreads_offline_deployment.md @@ -0,0 +1,53 @@ +# 摩尔线程 GPU 国产化离线部署指南 + +推荐使用摩尔线程官方 MUSA vLLM 基础镜像: + +```bash +registry.mthreads.com/presale/devtech/vllm_musa:s4000_4.3.5_d0519 +``` + +该镜像负责提供 MUSA 运行时、PyTorch、vLLM 与相关内核。本项目的 `Dockerfile.mthreads` 只叠加通用 Python 依赖和业务代码,不在 Dockerfile 内重新安装 vLLM,也不使用 uv 虚拟环境。 + +## 1. 拉取官方镜像 + +```bash +docker pull registry.mthreads.com/presale/devtech/vllm_musa:s4000_4.3.5_d0519 +``` + +## 2. 生成融合镜像 + +```bash +./scripts/package_vendor_gpu_image.sh \ + --vendor mthreads \ + --base-image registry.mthreads.com/presale/devtech/vllm_musa:s4000_4.3.5_d0519 \ + -v s4000_4.3.5_d0519 +``` + +## 3. 启动服务 + +```bash +ASR_IMAGE=unis/qwen3-asr:mthreads-s4000_4.3.5_d0519 \ +docker compose -f docker-compose-mthreads.yml up -d +``` + +多卡示例: + +```bash +MTHREADS_VISIBLE_DEVICES=0,1 docker compose -f docker-compose-mthreads.yml up -d +``` + +项目默认同时兼容 `MTHREADS_VISIBLE_DEVICES`、`MUSA_VISIBLE_DEVICES` 和 `CUDA_VISIBLE_DEVICES`。 + +## 4. 离线交付目录 + +```bash +./export_offline_bundle.sh \ + --type mthreads \ + --mthreads-base registry.mthreads.com/presale/devtech/vllm_musa:s4000_4.3.5_d0519 +``` + +## 说明 + +- 容器内设备探测命令使用 `mthreads-gmi` +- `ACCELERATOR=mthreads` 会走 vLLM GPU 路径 +- 不建议在项目层重新 `pip install vllm`,避免破坏厂商镜像内的 MUSA 依赖匹配关系 diff --git a/docs/realtime_meeting_refactor_report.md b/docs/realtime_meeting_refactor_report.md new file mode 100644 index 0000000..63b83a1 --- /dev/null +++ b/docs/realtime_meeting_refactor_report.md @@ -0,0 +1,412 @@ +# 实时会议识别改造报告 + +## 1. 背景 + +当前 `Qwen-Asr` 的实时会议链路,已经暴露出 3 类系统性问题: + +1. 长句在 `max_duration` 或静音点附近容易被截坏,出现残句、半句、尾词漂移。 +2. `partial` 与 `final segment` 责任混杂,静音、串音、背景音、测试语种会直接污染最终结果。 +3. 说话人识别仍以“句级单 embedding + 会话聚类”为主,面对插话、背景音、儿童声音、英语/泰语测试时容易抖动。 + +这些问题并不是单个阈值导致的,而是实时链路的职责分层不清晰。 + +## 2. 外部方案调研结论 + +本次调研覆盖了: + +- vLLM / Qwen3-ASR 官方实时文档 +- `diart` +- `pyannote.audio` +- NVIDIA NeMo `Streaming Sortformer` +- 在线 speaker diarization / label matching 论文 +- streaming ASR partial stability / endpointing 论文 + +### 2.1 vLLM/Qwen3-ASR 的边界 + +vLLM 的 `Qwen3-ASR realtime` 更像“流式推理后端”,它负责: + +- 累积音频 +- 到固定块长后执行流式推理 +- 提供 `flush/finish` + +它不负责: + +- 会议场景 endpointing +- speaker diarization +- partial 稳定化 +- 说话人标签稳定跟踪 + +结论: + +`vLLM 只能做实时 ASR 的第一层,不是完整会议链路的解决方案。` + +### 2.2 开源实时 speaker diarization 的主流范式 + +#### A. `diart` 范式 + +特征: + +- rolling buffer +- overlap-aware segmentation +- incremental clustering +- cannot-link constraints +- 低延迟滚动更新 + +优点: + +- 工程上相对容易接入 +- 很适合作为现有系统的 speaker 旁路 + +#### B. `Streaming Sortformer` 范式 + +特征: + +- 真正 streaming diarization +- chunk-based processing +- speaker cache / AOSC +- 跨 chunk 标签稳定 + +优点: + +- 更像现代工业方案 +- 标签稳定性更强 + +缺点: + +- 接入成本高于 `diart` + +### 2.3 论文结论 + +#### 在线 diarization 的关键问题不是“分离”,而是“标签一致性” + +主流论文强调: + +- 在线 diarization 必须解决 label matching +- 否则 `Speaker01/02/03` 会在不同 chunk 间乱跳 + +#### streaming ASR 的关键问题不是“能不能出字”,而是“partial 稳定性” + +论文结论: + +- partial 会被不断修正 +- partial 不应直接当 final 使用 +- 需要单独设计稳定策略 + +#### endpointing 对长句质量至关重要 + +论文与开源实现都说明: + +- 只靠静音阈值不够 +- 最好使用 VAD/SAD 或模型辅助 endpointing +- `max_duration` 只能是兜底机制,不应成为主要切句方式 + +## 3. 当前项目存在的核心架构问题 + +### 3.1 `partial` 和 `final` 混线 + +当前链路中: + +- `partial` 的输出直接影响最终句子定稿 +- 静音前后、噪声和串音有机会直接污染最终句子 + +这会带来: + +- 静音幻觉 +- 末尾漂移 +- 错误残句 + +### 3.2 断句主要靠“能量阈值 + 最长时长” + +当前主逻辑仍然偏向: + +- `self._has_voice(audio)` 做粗能量判定 +- `silence_samples` 达阈值就截断 +- `12s max_duration` 强行保底 + +这会导致: + +- 长句被硬切 +- 断句点不自然 +- 下一段拿到的是残尾巴 + +### 3.3 speaker 仍然是“句级单 embedding” + +当前 speaker 的主要依据是: + +- 当前句或其裁剪后的音频 +- 提一个 embedding +- 与会话内 slot 聚类 + +这对干净单人句子有效,但对会议场景不够: + +- 一句里可能混多人 +- 背景音会污染 embedding +- 不同 chunk 之间缺少真正的 diarization cache + +### 3.4 registry match 介入过早 + +当前一旦句级聚类通过阈值,就可能直接映射实名。 + +这会带来: + +- 错误聚类被直接升级为错实名 +- 后续纠正空间变小 + +## 4. 推荐的目标架构 + +建议将实时会议链路拆成 4 层: + +### Layer A: Streaming ASR Partial + +职责: + +- 面向 UI 输出中间文本 +- 只作为“参考内容” +- 不入库 +- 不参与 speaker +- 不做实名映射 + +要求: + +- 可以允许回退、修正 +- 需要稳定策略,但不要求和 final 完全一致 + +### Layer B: Endpointing / Final Segment Flush + +职责: + +- 决定“什么时候一句真正结束” +- 产出 final segment + +推荐方式: + +- `pending_buffer` +- 周期性 VAD/SAD 检查 +- 只刷出“已完成”的语音片段 +- 最后一段继续留 buffer,等待更多上下文 + +说明: + +- `max_duration` 仍保留 +- 但只作为异常保底 + +### Layer C: Online Speaker Tracking + +职责: + +- 产出稳定 `Speaker01/02/03/...` +- 不直接输出实名 + +推荐路线: + +- 低成本版:`rolling window diarization + incremental clustering` +- 强化版:`streaming diarization + speaker cache` + +必要能力: + +- label matching +- speaker cache +- cluster centroid 更新 +- recent speaker continuity + +### Layer D: Speaker Registry Match + +职责: + +- 把稳定的 slot 映射成实名 + +原则: + +- 先有稳定 `Speaker01/02/03` +- 再做 registry match +- 不要在每个短句上直接实名匹配 + +## 5. 对当前代码库的具体落地建议 + +### 5.1 保留 `Qwen3ASREngine` 作为流式 ASR 后端 + +文件: + +- `app/services/asr/qwen3_engine.py` + +建议: + +- 继续让它只负责 `init_streaming_state / streaming_transcribe / finish_streaming_transcribe` +- 不再让它承担句边界和 speaker 逻辑 + +### 5.2 重构 `qwen3_websocket_asr.py` + +文件: + +- `app/services/qwen3_websocket_asr.py` + +建议把它拆成以下内部组件: + +1. `PartialStreamController` + - 负责把有声 chunk 喂给流式 ASR + - 维护 partial 状态 + +2. `RealtimeEndpointController` + - 维护 `pending_buffer` + - 周期性 VAD flush + - 产出 finalized audio span + +3. `RealtimeSpeakerController` + - 对 finalized span 做 speaker tracking + - 维护 `Speaker01/02/...` + +4. `RegistryMatchController` + - 在 slot 稳定后映射实名 + +### 5.3 speaker tracker 升级方向 + +当前文件: + +- `app/services/realtime_speaker_tracker.py` + +建议保留它,但升级为: + +- slot centroid +- slot history +- recent speaker cache +- explicit label matching step +- “未知 slot” 与 “实名 slot” 分层 + +不建议继续只用: + +- `assign(embedding)` 的一次性聚类结果 + +### 5.4 引入真正的 VAD completed-segment flush + +当前项目最应该借鉴老项目的部分就是: + +- `pending_buffer` +- `flush_completed_segments` +- 只把已完成语音刷成 final + +建议新建模块,例如: + +- `app/services/realtime_endpointing.py` + +它负责: + +- 累积 PCM +- 每隔固定步长跑 VAD +- 判断哪些片段已完成 +- 返回 `finalized_audio` 与 `remaining_audio` + +### 5.5 `max_duration` 的正确角色 + +建议保留,但只作为: + +- buffer 过长 +- 长时间不出句 +- VAD 失效 + +时的保底。 + +不建议让它继续承担: + +- 常规切句 +- 主要 final 机制 + +## 6. 推荐改造路线 + +### 阶段 1:先把文本链路拆干净 + +目标: + +- `partial` 只显示 +- `final segment` 只由 endpointing 决定 + +工作项: + +- 引入 `pending_buffer + VAD flush` +- 保留现有 websocket 接口字段不变 +- `max_duration` 降级为兜底机制 + +### 阶段 2:speaker 从句级 embedding 升级为在线 tracking + +目标: + +- 稳定 `Speaker01/02/03` + +工作项: + +- 引入 rolling window diarization 或 speaker turn 检测 +- 增加 label matching +- 增加 speaker cache + +### 阶段 3:实名映射后移 + +目标: + +- 先稳定 slot +- 再实名 + +工作项: + +- registry match 不再逐句触发 +- 改成基于 slot centroid 或稳定窗口触发 + +### 阶段 4:partial 稳定策略 + +目标: + +- UI 不再频繁抖动 + +工作项: + +- prefix commit +- suffix freeze +- partial stability score + +## 7. 不建议继续做的事 + +以下方向收益很低,且会继续增加系统复杂度: + +- 继续堆更多 `if suspicious` +- 继续调 `speaker_threshold` +- 继续靠 `max_duration` 修长句 +- 继续用句级单 embedding 解决多人会议 speaker +- 继续让 `partial` 直接影响 final + +## 8. 结论 + +当前项目最需要的不是继续补条件分支,而是把实时会议能力拆成明确的四层: + +1. `streaming partial` +2. `endpointing/final flush` +3. `online diarization / speaker tracking` +4. `registry match` + +其中: + +- `vLLM` 属于第 1 层 +- 不是第 2、3、4 层的替代品 + +如果后续继续在当前单文件链路上修修补补,复杂度会继续升高,稳定性仍然不可控。 +如果按本报告做分层重构,问题会从“靠猜修 bug”变成“按职责逐层验证”。 + +## 9. 参考资料 + +- vLLM realtime Qwen3-ASR model docs + +- vLLM Qwen3-ASR recipe + +- diart + +- pyannote.audio + +- NVIDIA NeMo speaker diarization docs + +- NVIDIA Streaming Sortformer blog + +- Low-Latency Online Speaker Diarization with Graph-Based Label Generation + +- TURN-TO-DIARIZE: Online Speaker Diarization Constrained by Transformer Transducer Speaker Turn Detection + +- Analyzing the Quality and Stability of a Streaming End-to-End On-Device Speech Recognizer + +- Improving endpoint detection in end-to-end streaming ASR for conversational speech + diff --git a/docs/realtime_meeting_websocket.md b/docs/realtime_meeting_websocket.md new file mode 100644 index 0000000..b44b666 --- /dev/null +++ b/docs/realtime_meeting_websocket.md @@ -0,0 +1,417 @@ +# 实时会议 WebSocket 调用说明 + +本文档说明当前项目实时会议识别接口的调用方式、入参与返回格式。 + +## 1. 接口地址 + +- 推荐地址:`/ws/v1/asr` +- 显式地址:`/ws/v1/asr/qwen` + +说明: +- `/ws/v1/asr/funasr` 已废弃,不再使用。 +- 当前返回结构按腾讯实时识别风格组织,核心消息类型为:`voice_id`、`start`、`sentences`、`end`、`error`。 + +## 2. 交互流程 + +客户端调用顺序: + +1. 建立 WebSocket 连接 +2. 发送 `start` 消息 +3. 持续发送音频二进制数据 +4. 接收服务端返回的 `voice_id`、`start`、`sentences` +5. 发送 `stop` +6. 接收最终 `end` + +## 3. start 消息 + +客户端先发送文本消息: + +```json +{ + "type": "start", + "payload": { + "format": "pcm", + "sample_rate": 16000, + "session_id": "meeting-abc-123", + "language": null, + "context": "", + "enable_inverse_text_normalization": true, + "silence_duration_ms": 800, + "min_partial_sec": 0.3, + "pre_roll_ms": 240, + "max_sentence_count": 8, + "partial_holdback_chars": 2, + "unfixed_token_num": 3, + "enable_native_partial_stream": true, + "enable_speaker": true, + "match_speaker_registry": true, + "speaker_threshold": 0.6, + "enable_realtime_vad_split": true, + "enable_realtime_longform": false, + "force_stable_segment_sec": 6, + "force_stable_min_chars": 24, + "max_segment_sec": 12 + } +} +``` + +## 4. start 参数说明 + +### 必填/常用参数 + +| 字段 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `format` | string | `pcm` | 当前建议传 `pcm` | +| `sample_rate` | number | `16000` | 采样率 | +| `session_id` | string | 自动生成/可不传 | 会话标识;断线重连时传同一个值可在保活期内恢复同一会议会话 | +| `language` | string/null | `null` | 识别语言,留空表示自动判断 | +| `context` | string | `""` | 上下文提示,如会议主题、术语 | +| `enable_inverse_text_normalization` | boolean | `true` | 是否启用数字归一化 | + +### partial / 切段相关 + +| 字段 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `silence_duration_ms` | number | `800` | 静音多久触发收段 | +| `min_partial_sec` | number | `0.9`(后端默认) | 最短 partial 窗口 | +| `pre_roll_ms` | number | `240` | 句首预读毫秒数 | +| `max_sentence_count` | number | `8` | 一段内最多允许的句子数 | +| `partial_holdback_chars` | number | 后端默认 | partial 尾部保留字数,减少尾字抖动 | +| `unfixed_token_num` | number | `5`(后端默认) | native partial 模式下回滚 token 数 | +| `enable_native_partial_stream` | boolean | `false` | 是否启用底层原生流式 partial | +| `enable_realtime_vad_split` | boolean | `false` | 是否启用实时 VAD 拆段 | +| `enable_realtime_longform` | boolean | `false` | 是否启用长段重转写 | +| `force_stable_segment_sec` | number | 后端默认 | 超时后软提交的时间阈值 | +| `force_stable_min_chars` | number | 后端默认 | 软提交最少字数 | +| `max_segment_sec` | number | 后端默认 | 单段最长秒数 | + +### speaker 相关 + +| 字段 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `enable_speaker` | boolean | `true` | 是否开启说话人分离 | +| `match_speaker_registry` | boolean | `false` | 是否匹配已注册声纹库 | +| `speaker_threshold` | number/null | `null` | 本次声纹匹配阈值 | + +说明: +- `enable_speaker=true` 时,服务端会先返回文本,再异步补充 speaker 归属。 +- `match_speaker_registry=true` 时,若匹配到声纹库,会返回实名 speaker 信息。 + +## 5. 音频数据发送 + +`start` 成功后,客户端持续发送音频二进制帧。 + +当前推荐: + +- 单声道 PCM +- 16k 采样率 +- 小块持续发送 + +示例: + +```js +ws.send(pcmChunkArrayBuffer); +``` + +## 6. stop 消息 + +识别结束后发送: + +```json +{ + "type": "stop" +} +``` + +## 7. 服务端返回消息 + +## 7.1 `voice_id` + +连接启动后首先返回: + +```json +{ + "type": "voice_id", + "voice_id": "41ff1926", + "session_id": "meeting-abc-123" +} +``` + +## 7.2 `start` + +服务端确认开始识别: + +```json +{ + "type": "start", + "session_id": "meeting-abc-123" +} +``` + +说明: +- 若客户端在 `start.payload.session_id` 中传入固定值,服务端会优先复用该值。 +- 若客户端未传 `session_id`,服务端仍按当前连接生成 `voice_id`,并同步作为 `session_id` 返回。 +- 在 `REALTIME_SESSION_RESUME_TTL_SEC` 保活时间内,客户端断线后使用相同 `session_id` 再次发送 `start`,服务端会恢复原会话上下文。 + +## 7.3 `sentences` + +识别过程中会持续返回 `sentences`。 + +重要说明: + +- `sentence_type=0` / `slice_type=1` 表示实时 partial,仅用于实时展示。 +- `sentence_type=1` / `slice_type=2` 表示该段最终定稿,建议作为最终业务入库依据。 +- 若开启 `enable_native_partial_stream=true`,partial 刷新会更快,但中间文本可能更活。 +- 若希望少存数据,建议不要存 partial,只对 `sentence_type=1` 做落库或 upsert。 + +### partial 返回 + +```json +{ + "type": "sentences", + "code": 0, + "voice_id": "41ff1926", + "final": 0, + "result": { + "slice_type": 1, + "index": 3, + "voice_text_str": "今天这个会议主要讨论预算。" + }, + "sentences": [ + { + "sentence_id": 3, + "sentence_type": 0, + "speaker_id": -1, + "start_time": 12000, + "end_time": 15600, + "sentence": "今天这个会议主要讨论预算。" + } + ] +} +``` + +字段说明: + +| 字段 | 说明 | +| --- | --- | +| `slice_type=1` | partial | +| `sentence_type=0` | partial 句子 | +| `speaker_id=-1` | 当前还未确认 speaker | + +### 定稿返回 + +```json +{ + "type": "sentences", + "code": 0, + "voice_id": "41ff1926", + "final": 0, + "result": { + "slice_type": 2, + "index": 3, + "voice_text_str": "今天这个会议主要讨论预算。" + }, + "sentences": [ + { + "sentence_id": 3, + "sentence_type": 1, + "speaker_id": -1, + "start_time": 12000, + "end_time": 15880, + "sentence": "今天这个会议主要讨论预算。", + "speaker_name": "", + "user_id": null + } + ] +} +``` + +字段说明: + +| 字段 | 说明 | +| --- | --- | +| `slice_type=2` | 该段文本已定稿 | +| `sentence_type=1` | final 句子 | +| `speaker_name` | 项目扩展字段 | +| `user_id` | 项目扩展字段 | + +### speaker 回写返回 + +如果 speaker 后续识别完成,服务端会再次返回相同 `sentence_id` 的 `sentences` 消息,只更新 speaker 归属: + +```json +{ + "type": "sentences", + "code": 0, + "voice_id": "41ff1926", + "final": 0, + "result": { + "slice_type": 2, + "index": 3, + "voice_text_str": "今天这个会议主要讨论预算。" + }, + "sentences": [ + { + "sentence_id": 3, + "sentence_type": 1, + "speaker_id": 19, + "start_time": 12000, + "end_time": 15880, + "sentence": "今天这个会议主要讨论预算。", + "speaker_name": "Alan Paine", + "user_id": "3" + } + ] +} +``` + +说明: +- speaker 是异步补归属,不保证和文本首个 final 同时返回。 +- 前端应使用 `sentence_id` 做幂等更新,而不是简单追加。 + +## 7.3.1 partial 与 final 的使用建议 + +推荐使用方式: + +- 前端实时显示:使用 `sentence_type=0` +- 最终文本入库:使用 `sentence_type=1` +- 说话人归属更新:继续按相同 `sentence_id` 更新已有 final 记录 + +推荐入库字段: + +- `sentence_id` +- `start_time` +- `end_time` +- `sentence` +- `speaker_id` +- `speaker_name` +- `user_id` + +推荐入库策略: + +1. 收到 `sentences` 后,仅处理 `sentence_type=1` +2. 使用 `sentence_id` 作为同一连接内的幂等更新键 +3. 若后续相同 `sentence_id` 再次返回,通常表示 speaker 归属补写,直接更新原记录 +4. 收到 `end` 后,可将本次会话的 final 结果视为最终完成结果 + +## 7.4 `end` + +客户端发送 `stop` 后,服务端返回最终结束消息: + +```json +{ + "type": "end", + "code": 0, + "message": "", + "voice_id": "41ff1926", + "final": 1, + "result": { + "slice_type": 2, + "index": 3, + "voice_text_str": "完整识别文本" + }, + "sentences": [ + { + "sentence_id": 0, + "sentence_type": 1, + "speaker_id": 19, + "start_time": 0, + "end_time": 3200, + "sentence": "第一句", + "speaker_name": "Alan Paine", + "user_id": "3" + } + ] +} +``` + +## 7.5 `error` + +异常时返回: + +```json +{ + "type": "error", + "code": -1, + "message": "错误信息", + "voice_id": "41ff1926" +} +``` + +## 8. 腾讯风格字段兼容说明 + +当前实时会议返回格式按腾讯实时 speaker demo 风格组织,核心字段保持一致: + +- `type` +- `code` +- `voice_id` +- `final` +- `result.slice_type` +- `result.index` +- `result.voice_text_str` +- `sentences[].sentence_id` +- `sentences[].sentence_type` +- `sentences[].speaker_id` +- `sentences[].start_time` +- `sentences[].end_time` +- `sentences[].sentence` + +在此基础上,项目额外补充: + +- `sentences[].speaker_name` +- `sentences[].user_id` + +## 9. JavaScript 调用示例 + +```js +const ws = new WebSocket("ws://127.0.0.1:8000/ws/v1/asr"); + +ws.onopen = () => { + ws.send(JSON.stringify({ + type: "start", + payload: { + format: "pcm", + sample_rate: 16000, + language: null, + context: "预算评审会", + enable_inverse_text_normalization: true, + silence_duration_ms: 800, + min_partial_sec: 0.3, + pre_roll_ms: 240, + max_sentence_count: 8, + partial_holdback_chars: 2, + unfixed_token_num: 3, + enable_native_partial_stream: true, + enable_speaker: true, + match_speaker_registry: true, + speaker_threshold: 0.6, + enable_realtime_vad_split: true, + enable_realtime_longform: false, + force_stable_segment_sec: 6, + force_stable_min_chars: 24, + max_segment_sec: 12 + } + })); +}; + +ws.onmessage = (event) => { + const payload = JSON.parse(event.data); + console.log("ws message:", payload); +}; + +function sendPcmChunk(arrayBuffer) { + ws.send(arrayBuffer); +} + +function stopRecognition() { + ws.send(JSON.stringify({ type: "stop" })); +} +``` + +## 10. 对接注意事项 + +- 前端要按 `sentence_id` 更新句子,不要把 speaker 回写当成新句子追加。 +- `speaker_id=-1` 代表 speaker 暂未确认,不代表识别失败。 +- 默认启用 `enable_native_partial_stream`;partial 刷新更快,但中间文本可能会修订。 +- 最终展示建议以 `sentence_type=1` 的句子为准。 +- 若需要排查前端是否真的传了某个参数,建议把 `start payload` 直接打印到页面日志。 diff --git a/docs/realtime_meeting_websocket_customer.md b/docs/realtime_meeting_websocket_customer.md new file mode 100644 index 0000000..5063cbc --- /dev/null +++ b/docs/realtime_meeting_websocket_customer.md @@ -0,0 +1,297 @@ +# 实时会议 WebSocket 对接文档 + +本文档用于客户侧接入实时会议识别服务,包含接口地址、消息格式、返回示例和重连约定。 + +## 1. 接口地址 + +- 推荐地址:`/ws/v1/asr` +- 显式地址:`/ws/v1/asr/qwen` + +说明: +- 当前协议为 JSON 控制消息 + 音频二进制流。 +- 返回结构按实时会议场景组织,核心消息类型为:`voice_id`、`start`、`sentences`、`end`、`error`。 + +## 2. 交互流程 + +客户端调用顺序: + +1. 建立 WebSocket 连接 +2. 发送 `start` 文本消息 +3. 持续发送音频二进制数据 +4. 持续接收服务端返回的 `voice_id`、`start`、`sentences` +5. 识别结束时发送 `stop` +6. 接收最终 `end` + +## 3. start 请求 + +### 3.1 请求示例 + +```json +{ + "type": "start", + "payload": { + "format": "pcm", + "sample_rate": 16000, + "session_id": "meeting-abc-123", + "language": null, + "context": "", + "enable_inverse_text_normalization": true, + "silence_duration_ms": 800, + "min_partial_sec": 0.3, + "pre_roll_ms": 240, + "max_sentence_count": 8, + "partial_holdback_chars": 2, + "unfixed_token_num": 3, + "enable_native_partial_stream": true, + "enable_speaker": true, + "match_speaker_registry": true, + "speaker_threshold": 0.6, + "enable_realtime_vad_split": true, + "enable_realtime_longform": false, + "force_stable_segment_sec": 6, + "force_stable_min_chars": 24, + "max_segment_sec": 12 + } +} +``` + +### 3.2 主要参数说明 + +| 字段 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `format` | string | `pcm` | 建议传 `pcm` | +| `sample_rate` | number | `16000` | 音频采样率 | +| `session_id` | string | 可不传 | 会话标识;断线重连时传同一个值可恢复同一会议会话 | +| `language` | string/null | `null` | 留空表示自动识别 | +| `context` | string | `""` | 业务上下文提示,如会议主题、术语 | +| `enable_inverse_text_normalization` | boolean | `true` | 是否启用数字归一化 | +| `silence_duration_ms` | number | `800` | 静音多久触发收段 | +| `min_partial_sec` | number | 后端默认 | 最短 partial 窗口 | +| `pre_roll_ms` | number | `240` | 句首预读时长 | +| `max_sentence_count` | number | `8` | 单段最多句数 | +| `partial_holdback_chars` | number | 后端默认 | partial 尾部保留字数,减少尾字抖动 | +| `unfixed_token_num` | number | 后端默认 | native partial 模式下回滚 token 数 | +| `enable_native_partial_stream` | boolean | `false` | 是否启用原生流式 partial | +| `enable_speaker` | boolean | `true` | 是否开启说话人分离 | +| `match_speaker_registry` | boolean | `false` | 是否匹配已注册声纹库 | +| `speaker_threshold` | number/null | `null` | 本次声纹匹配阈值 | +| `enable_realtime_vad_split` | boolean | `false` | 是否启用实时 VAD 拆段 | +| `enable_realtime_longform` | boolean | `false` | 是否启用长段重转写 | +| `force_stable_segment_sec` | number | 后端默认 | 超时后软提交阈值 | +| `force_stable_min_chars` | number | 后端默认 | 软提交最少字数 | +| `max_segment_sec` | number | 后端默认 | 单段最长秒数 | + +说明: +- `enable_speaker=true` 时,服务端会先返回文本,再异步补充 speaker 归属。 +- `match_speaker_registry=true` 时,若匹配到声纹库,会返回实名 speaker 信息。 + +## 4. 音频发送 + +`start` 成功后,客户端持续发送音频二进制帧。 + +推荐格式: + +- 单声道 PCM +- 16k 采样率 +- 小块连续发送 + +示例: + +```js +ws.send(pcmChunkArrayBuffer); +``` + +## 5. stop 请求 + +```json +{ + "type": "stop" +} +``` + +## 6. 服务端返回 + +## 6.1 `voice_id` + +连接建立并收到 `start` 后,服务端先返回: + +```json +{ + "type": "voice_id", + "voice_id": "41ff1926", + "session_id": "meeting-abc-123" +} +``` + +说明: +- `voice_id` 为本次实时识别标识。 +- `session_id` 为本次会话标识;若未传,服务端会返回自动生成值。 + +## 6.2 `start` + +服务端确认开始识别: + +```json +{ + "type": "start", + "session_id": "meeting-abc-123" +} +``` + +## 6.3 `sentences` + +识别过程中会持续返回 `sentences`。 + +重要说明: + +- `sentence_type=0` / `slice_type=1` 表示实时 partial,仅用于实时展示。 +- `sentence_type=1` / `slice_type=2` 表示该段最终定稿,建议作为最终入库依据。 +- 说话人可能异步补写,不保证和首个 final 同时返回。 + +### partial 返回示例 + +```json +{ + "type": "sentences", + "code": 0, + "voice_id": "41ff1926", + "final": 0, + "result": { + "slice_type": 1, + "index": 3, + "voice_text_str": "今天这个会议主要讨论预算。" + }, + "sentences": [ + { + "sentence_id": 3, + "sentence_type": 0, + "speaker_id": -1, + "start_time": 12000, + "end_time": 15600, + "sentence": "今天这个会议主要讨论预算。" + } + ] +} +``` + +### final 返回示例 + +```json +{ + "type": "sentences", + "code": 0, + "voice_id": "41ff1926", + "final": 0, + "result": { + "slice_type": 2, + "index": 3, + "voice_text_str": "今天这个会议主要讨论预算。" + }, + "sentences": [ + { + "sentence_id": 3, + "sentence_type": 1, + "speaker_id": -1, + "start_time": 12000, + "end_time": 15880, + "sentence": "今天这个会议主要讨论预算。", + "speaker_name": "", + "user_id": null + } + ] +} +``` + +### speaker 回写示例 + +如果 speaker 后续识别完成,服务端会再次返回相同 `sentence_id` 的消息,只更新 speaker 信息: + +```json +{ + "type": "sentences", + "code": 0, + "voice_id": "41ff1926", + "final": 0, + "result": { + "slice_type": 2, + "index": 3, + "voice_text_str": "今天这个会议主要讨论预算。" + }, + "sentences": [ + { + "sentence_id": 3, + "sentence_type": 1, + "speaker_id": 19, + "start_time": 12000, + "end_time": 15880, + "sentence": "今天这个会议主要讨论预算。", + "speaker_name": "Alan Paine", + "user_id": "3" + } + ] +} +``` + +接入建议: + +- 客户端按 `sentence_id` 更新句子,不要把 speaker 回写当成新句子追加。 +- `speaker_id=-1` 代表当前 speaker 暂未确认,不代表识别失败。 + +## 6.4 `end` + +识别结束后返回: + +```json +{ + "type": "end", + "code": 0, + "message": "", + "voice_id": "41ff1926", + "session_id": "meeting-abc-123", + "final": 1, + "result": { + "slice_type": 2, + "index": 5, + "voice_text_str": "完整会议文本" + }, + "sentences": [] +} +``` + +## 6.5 `error` + +异常时返回: + +```json +{ + "type": "error", + "code": "INVALID_STATE", + "message": "请先发送 start", + "voice_id": "41ff1926" +} +``` + +## 7. 断线重连 + +支持基于 `session_id` 的会话恢复。 + +约定如下: + +1. 首次连接时,客户端可以自行生成 `session_id` 并放入 `start.payload.session_id` +2. 若客户端未传,服务端会返回自动生成的 `session_id` +3. 断线重连时,客户端使用同一个 `session_id` 再次发送 `start` +4. 若服务端会话仍在保活期内,则恢复同一会议上下文 + +建议: + +- 客户端在会议生命周期内保持 `session_id` 稳定 +- 网络抖动或页面刷新后,优先使用上一次会话的 `session_id` 重连 + +## 8. 客户端接入建议 + +- 实时展示可消费 `sentence_type=0` 的 partial +- 业务落库建议只以 `sentence_type=1` 的 final 为准 +- 若后续收到相同 `sentence_id` 的 final 更新,应执行更新而不是新增 +- 若需要最佳实时体验,建议开启 `enable_native_partial_stream=true` +- 若希望 speaker 识别更完整,建议开启 `enable_speaker=true` + diff --git a/docs/国产化兼容适配汇报.md b/docs/国产化兼容适配汇报.md new file mode 100644 index 0000000..4572240 --- /dev/null +++ b/docs/国产化兼容适配汇报.md @@ -0,0 +1,95 @@ +# Qwen-Asr 国产化兼容适配汇报 + +## 一、适配目标 + +完成对 **沐曦(MetaX)** 和 **天数(Iluvatar)** 两款国产GPU的完整适配,实现语音识别服务在国产硬件平台的稳定运行。 + +--- + +## 二、适配历程与关键节点 + +| 时间 | 里程碑 | 关键动作 | +|------|--------|----------| +| 2026-06-02 | 架构设计 | 新增Accelerator抽象层,重构设备检测逻辑 | +| 2026-06-02 | 沐曦适配 | 新增沐曦GPU支持(MetaX MACA runtime) | +| 2026-06-02 | 天数适配 | 新增天数GPU支持(Iluvatar IX runtime) | +| 2026-06-03 | 部署完善 | 完善沐曦全流程部署支持,升级天数基础镜像 | +| 2026-06-04 | 问题修复 | 修复setuptools版本冲突,优化pip安装配置 | + +--- + +## 三、遇到的主要"坑"与解决方案 + +### **坑1:设备检测适配问题** +- **问题**:不同厂商使用不同的设备查询命令(`mx-smi` vs `ixsmi`),输出格式差异大 +- **解决方案**: + - 抽象统一的`AcceleratorAdapter`接口 + - 支持多种SMI命令格式解析(JSON/表格/列表) + - 自动检测优先级:国产厂商SMI → NVIDIA → CPU + +### **坑2:vLLM/PyTorch版本不兼容** +- **问题**:国产GPU的vLLM、PyTorch、内核和运行时需要严格匹配厂商官方版本 +- **解决方案**: + - 采用"厂商官方vLLM镜像 + 项目代码叠加"策略 + - 不在Dockerfile中重新`pip install vllm`,避免解析到NVIDIA CUDA依赖 + - 提供专用环境配置文件(`environments/metax/`、`environments/iluvatar/`) + +### **坑3:环境变量命名混乱** +- **问题**:各厂商使用不同的设备可见性变量名 +- **解决方案**: + - 沐曦:`METAX_VISIBLE_DEVICES` / `MACA_VISIBLE_DEVICES` / `MX_VISIBLE_DEVICES` + - 天数:`ILUVATAR_VISIBLE_DEVICES` / `IX_VISIBLE_DEVICES` / `CUDA_VISIBLE_DEVICES` + - 代码层统一处理,支持多种变量名自动识别 + +### **坑4:显存管理问题** +- **问题**:国产GPU显存分配策略与NVIDIA不同,默认配置易导致KV cache不足 +- **解决方案**: + - 引入`QWEN_GPU_MEMORY_UTILIZATION`配置项(默认0.25) + - 强制启用`QWEN_VLLM_ENFORCE_EAGER=true`提升兼容性 + - 提供详细的显存配置指导文档 + +### **坑5:离线部署打包复杂** +- **问题**:目标机通常无法联网,需要完整的离线交付包 +- **解决方案**: + - 开发`export_offline_bundle.sh`一键打包脚本 + - 支持跳过模型打包(模型单独准备) + - 生成厂商专用的docker-compose配置 + +### **坑6:Python环境依赖冲突** +- **问题**:沐曦官方镜像使用特定Python路径和setuptools版本 +- **解决方案**: + - 不使用uv虚拟环境,直接使用系统Python + - 固定setuptools版本为69.5.1 + - 支持自定义Python路径配置 + +--- + +## 四、兼容性矩阵 + +| 平台 | 支持状态 | 基础镜像 | 设备命令 | +|------|----------|----------|----------| +| NVIDIA CUDA | ✅ 支持 | 自定义CUDA 13.0 | nvidia-smi | +| 沐曦 MetaX | ✅ 支持 | vLLM 0.17.0 + MACA AI3.5.3 | mx-smi | +| 天数 Iluvatar | ✅ 支持 | vLLM 0.17.0 + IX 4.4.0 | ixsmi | +| CPU (Rust) | ✅ 支持 | - | - | + +--- + +## 五、交付成果 + +1. **代码层**:统一的加速器抽象层(`app/core/accelerator.py`) +2. **部署文档**: + - 《沐曦GPU国产化离线部署指南》 + - 《天数GPU国产化离线部署指南》 +3. **环境配置**:专用依赖配置文件(metax/iluvatar) +4. **打包脚本**:一键离线打包工具 +5. **Docker镜像**:专用Dockerfile和docker-compose配置 + +--- + +## 六、关键经验总结 + +1. **厂商官方镜像优先**:不自行编译GPU运行时,直接使用厂商验证过的vLLM镜像 +2. **统一抽象层**:通过Adapter模式屏蔽硬件差异,上层业务无感知 +3. **配置化驱动**:设备可见性、显存比例等参数化配置,适应不同现场环境 +4. **离线交付优先**:提前规划离线部署方案,避免现场网络限制问题 \ No newline at end of file diff --git a/environments/cpu/pyproject.toml b/environments/cpu/pyproject.toml new file mode 100644 index 0000000..f624140 --- /dev/null +++ b/environments/cpu/pyproject.toml @@ -0,0 +1,54 @@ +[project] +name = "qwen3-asr-cpu-env" +version = "1.0.0" +description = "CPU runtime environment for qwen3-asr" +requires-python = ">=3.10,<3.13" +dependencies = [ + "fastapi==0.128.0", + "fastapi-offline==1.7.6", + "uvicorn[standard]==0.40.0", + "pydantic==2.12.0", + "python-multipart==0.0.22", + "funasr==1.3.1", + "requests==2.32.5", + "modelscope[framework]==1.34.0", + "soundfile==0.13.1", + "librosa==0.11.0", + "websockets==16.0", + "addict==2.4.0", + "datasets==3.6.0", + "scipy==1.15.3", + "itntext==0.1.5", + "python-dotenv==1.2.1", + "huggingface_hub==0.34.0", + "hdbscan==0.8.41", + "loguru==0.7.2", + "rich>=13.9,<14", + "setuptools>=70.0.0,<81", + "torch==2.3.1 ; sys_platform == 'linux' or sys_platform == 'darwin'", + "torchaudio==2.3.1 ; sys_platform == 'linux' or sys_platform == 'darwin'", + "transformers>=4.45,<4.50", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = [] + +[tool.uv] +package = false + +[tool.uv.sources] +torch = [ + { index = "pytorch-cpu", marker = "platform_system == 'Linux'" }, +] +torchaudio = [ + { index = "pytorch-cpu", marker = "platform_system == 'Linux'" }, +] + +[[tool.uv.index]] +name = "pytorch-cpu" +url = "https://download.pytorch.org/whl/cpu" +explicit = true diff --git a/environments/cpu/uv.lock b/environments/cpu/uv.lock new file mode 100644 index 0000000..66a1fad --- /dev/null +++ b/environments/cpu/uv.lock @@ -0,0 +1,2832 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <3.13" +resolution-markers = [ + "(python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'linux')", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'linux')", + "python_full_version >= '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'", + "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.11' and sys_platform == 'win32'", + "python_full_version >= '3.11' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux'", +] + +[[package]] +name = "addict" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/ef/fd7649da8af11d93979831e8f1f8097e85e82d5bfeabc8c68b39175d8e75/addict-2.4.0.tar.gz", hash = "sha256:b3b2210e0e067a281f5646c8c5db92e99b7231ea8b0eb5f74dbdf9e259d4e494", size = 9186, upload-time = "2020-11-21T16:21:31.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/00/b08f23b7d7e1e14ce01419a467b583edbb93c6cdb8654e54a9cc579cd61f/addict-2.4.0-py3-none-any.whl", hash = "sha256:249bb56bbfd3cdc2a004ea0ff4c2b6ddc84d53bc2194761636eb314d5cfa5dfc", size = 3832, upload-time = "2020-11-21T16:21:29.588Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +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" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/85/cebc47ee74d8b408749073a1a46c6fcba13d170dc8af7e61996c6c9394ac/aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b", size = 750547, upload-time = "2026-03-31T21:56:30.024Z" }, + { url = "https://files.pythonhosted.org/packages/05/98/afd308e35b9d3d8c9ec54c0918f1d722c86dc17ddfec272fcdbcce5a3124/aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5", size = 503535, upload-time = "2026-03-31T21:56:31.935Z" }, + { url = "https://files.pythonhosted.org/packages/6f/4d/926c183e06b09d5270a309eb50fbde7b09782bfd305dec1e800f329834fb/aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670", size = 497830, upload-time = "2026-03-31T21:56:33.654Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d6/f47d1c690f115a5c2a5e8938cce4a232a5be9aac5c5fb2647efcbbbda333/aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274", size = 1682474, upload-time = "2026-03-31T21:56:35.513Z" }, + { url = "https://files.pythonhosted.org/packages/01/44/056fd37b1bb52eac760303e5196acc74d9d546631b035704ae5927f7b4ac/aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a", size = 1655259, upload-time = "2026-03-31T21:56:37.843Z" }, + { url = "https://files.pythonhosted.org/packages/91/9f/78eb1a20c1c28ae02f6a3c0f4d7b0dcc66abce5290cadd53d78ce3084175/aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d", size = 1736204, upload-time = "2026-03-31T21:56:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/de/6c/d20d7de23f0b52b8c1d9e2033b2db1ac4dacbb470bb74c56de0f5f86bb4f/aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796", size = 1826198, upload-time = "2026-03-31T21:56:41.378Z" }, + { url = "https://files.pythonhosted.org/packages/2f/86/a6f3ff1fd795f49545a7c74b2c92f62729135d73e7e4055bf74da5a26c82/aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95", size = 1681329, upload-time = "2026-03-31T21:56:43.374Z" }, + { url = "https://files.pythonhosted.org/packages/fb/68/84cd3dab6b7b4f3e6fe9459a961acb142aaab846417f6e8905110d7027e5/aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5", size = 1560023, upload-time = "2026-03-31T21:56:45.031Z" }, + { url = "https://files.pythonhosted.org/packages/41/2c/db61b64b0249e30f954a65ab4cb4970ced57544b1de2e3c98ee5dc24165f/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a", size = 1652372, upload-time = "2026-03-31T21:56:47.075Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/e96988a6c982d047810c772e28c43c64c300c943b0ed5c1c0c4ce1e1027c/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73", size = 1662031, upload-time = "2026-03-31T21:56:48.835Z" }, + { url = "https://files.pythonhosted.org/packages/b7/26/a56feace81f3d347b4052403a9d03754a0ab23f7940780dada0849a38c92/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297", size = 1708118, upload-time = "2026-03-31T21:56:50.833Z" }, + { url = "https://files.pythonhosted.org/packages/78/6e/b6173a8ff03d01d5e1a694bc06764b5dad1df2d4ed8f0ceec12bb3277936/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074", size = 1548667, upload-time = "2026-03-31T21:56:52.81Z" }, + { url = "https://files.pythonhosted.org/packages/16/13/13296ffe2c132d888b3fe2c195c8b9c0c24c89c3fa5cc2c44464dc23b22e/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e", size = 1724490, upload-time = "2026-03-31T21:56:54.541Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1f1c287f4a79782ef36e5a6e62954c85343bc30470d862d30bd5f26c9fa2/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7", size = 1667109, upload-time = "2026-03-31T21:56:56.21Z" }, + { url = "https://files.pythonhosted.org/packages/ef/42/8461a2aaf60a8f4ea4549a4056be36b904b0eb03d97ca9a8a2604681a500/aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9", size = 439478, upload-time = "2026-03-31T21:56:58.292Z" }, + { url = "https://files.pythonhosted.org/packages/e5/71/06956304cb5ee439dfe8d86e1b2e70088bd88ed1ced1f42fb29e5d855f0e/aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76", size = 462047, upload-time = "2026-03-31T21:57:00.257Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, + { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" }, + { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, + { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, + { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, + { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, + { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" }, + { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "aliyun-python-sdk-core" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jmespath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/09/da9f58eb38b4fdb97ba6523274fbf445ef6a06be64b433693da8307b4bec/aliyun-python-sdk-core-2.16.0.tar.gz", hash = "sha256:651caad597eb39d4fad6cf85133dffe92837d53bdf62db9d8f37dab6508bb8f9", size = 449555, upload-time = "2024-10-09T06:01:01.762Z" } + +[[package]] +name = "aliyun-python-sdk-kms" +version = "2.16.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aliyun-python-sdk-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/2c/9877d0e6b18ecf246df671ac65a5d1d9fecbf85bdcb5d43efbde0d4662eb/aliyun-python-sdk-kms-2.16.5.tar.gz", hash = "sha256:f328a8a19d83ecbb965ffce0ec1e9930755216d104638cd95ecd362753b813b3", size = 12018, upload-time = "2024-08-30T09:01:20.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/5c/0132193d7da2c735669a1ed103b142fd63c9455984d48c5a88a1a516efaa/aliyun_python_sdk_kms-2.16.5-py2.py3-none-any.whl", hash = "sha256:24b6cdc4fd161d2942619479c8d050c63ea9cd22b044fe33b60bbb60153786f0", size = 99495, upload-time = "2024-08-30T09:01:18.462Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "audioread" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/4a/874ecf9b472f998130c2b5e145dcdb9f6131e84786111489103b66772143/audioread-3.1.0.tar.gz", hash = "sha256:1c4ab2f2972764c896a8ac61ac53e261c8d29f0c6ccd652f84e18f08a4cab190", size = 20082, upload-time = "2025-10-26T19:44:13.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/16/fbe8e1e185a45042f7cd3a282def5bb8d95bb69ab9e9ef6a5368aa17e426/audioread-3.1.0-py3-none-any.whl", hash = "sha256:b30d1df6c5d3de5dcef0fb0e256f6ea17bdcf5f979408df0297d8a408e2971b4", size = 23143, upload-time = "2025-10-26T19:44:12.016Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "crcmod" +version = "1.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/b0/e595ce2a2527e169c3bcd6c33d2473c1918e0b7f6826a043ca1245dd4e5b/crcmod-1.7.tar.gz", hash = "sha256:dc7051a0db5f2bd48665a990d3ec1cc305a466a77358ca4492826f41f283601e", size = 89670, upload-time = "2010-06-27T14:35:29.538Z" } + +[[package]] +name = "cryptography" +version = "46.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, + { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, +] + +[[package]] +name = "datasets" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, + { name = "filelock" }, + { name = "fsspec", extra = ["http"] }, + { name = "huggingface-hub" }, + { name = "multiprocess" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pyarrow" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/89/d3d6fef58a488f8569c82fd293ab7cbd4250244d67f425dcae64c63800ea/datasets-3.6.0.tar.gz", hash = "sha256:1b2bf43b19776e2787e181cfd329cb0ca1a358ea014780c3581e0f276375e041", size = 569336, upload-time = "2025-05-07T15:15:02.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/34/a08b0ee99715eaba118cbe19a71f7b5e2425c2718ef96007c325944a1152/datasets-3.6.0-py3-none-any.whl", hash = "sha256:25000c4a2c0873a710df127d08a202a06eab7bf42441a6bc278b499c2f72cd1b", size = 491546, upload-time = "2025-05-07T15:14:59.742Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "dill" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/17/4d/ac7ffa80c69ea1df30a8aa11b3578692a5118e7cd1aa157e3ef73b092d15/dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca", size = 184847, upload-time = "2024-01-27T23:42:16.145Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/7a/cef76fd8438a42f96db64ddaa85280485a9c395e7df3db8158cfec1eee34/dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7", size = 116252, upload-time = "2024-01-27T23:42:14.239Z" }, +] + +[[package]] +name = "editdistance" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/18/9f4f975ca87a390832b1c22478f3702fcdf739f83211e24d054b7551270d/editdistance-0.8.1.tar.gz", hash = "sha256:d1cdf80a5d5014b0c9126a69a42ce55a457b457f6986ff69ca98e4fe4d2d8fed", size = 50006, upload-time = "2024-02-10T07:44:53.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/c9/302658ce7f4c537a4e85cf578d11bbf7af120a712e1d78fedc6cb8823c65/editdistance-0.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:adeb705f32b93accc74960d227875abff150ee42d676e428536361fe5f8f5388", size = 106150, upload-time = "2024-02-10T07:43:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/45/80/0b3c7d2c0e183725986fea5dd2df11f0b4b46320e9a64f6077a121ab1f64/editdistance-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3de77951b105d0972deec7684a0b3d1a9dee69c9b5d34f6e2acc0d76cd4a1c52", size = 80551, upload-time = "2024-02-10T07:43:17.64Z" }, + { url = "https://files.pythonhosted.org/packages/b5/14/681460965c6a4a48321b07f88de2273d097fdca0491ff55db891aacbd291/editdistance-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e88efb052d45e924606c305cb833a80579dca3e8e4ff01309d50ba2c1c0bbd5", size = 79142, upload-time = "2024-02-10T07:43:19.195Z" }, + { url = "https://files.pythonhosted.org/packages/ed/0d/abdbc8e394a9461cf2ae27c16564fadaa65f52bd242dd1582ae5e7736dc3/editdistance-0.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0247e7a1e9c66ea75211a97e725366bff19a52aac2c838ed5f90025630e976dd", size = 396768, upload-time = "2024-02-10T07:43:20.912Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fb/2940d26ebda12efd280ae939436f17ac482930d862df9e774cb8b771ab03/editdistance-0.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67d143429a49ab552411505f550a0fb4285a1d4336e096804d233ec495ac20fc", size = 401846, upload-time = "2024-02-10T07:43:23.169Z" }, + { url = "https://files.pythonhosted.org/packages/53/cc/c63d75c7f387d4df0645682c1ab8706c2dfe5c9c0c4999723ce9a3ba0853/editdistance-0.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca9d3be2b10e5d44a950a4bd1e84bca9ebbecd364bce0cf5693bf8224c78eaef", size = 397543, upload-time = "2024-02-10T07:43:24.621Z" }, + { url = "https://files.pythonhosted.org/packages/8e/38/bb0f734a7571e093184606b930734b12da5b6bff2635eba9312fe4536dd9/editdistance-0.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5c72aa1df8535f2e2b3d8773a1a7da091bc1a7e52bb396e7e48d375ba687e7b2", size = 898934, upload-time = "2024-02-10T07:43:26.926Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/624fc7a09918f850a057465f02e86f269e139a457f48ff8cabfb12701756/editdistance-0.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9a606c34a2a6cc190e4fffc856b36333cdcf1f1fab5b22bd3088e585c22d6ca0", size = 959637, upload-time = "2024-02-10T07:43:28.997Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/7fa6cc277f91c477ee370807d51c1826891dc6dfc307544223ce7f2687de/editdistance-0.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5af173d442ffac33b7c7990132f97f88818a3abf4b21c0c702a7022df37c0c5c", size = 911024, upload-time = "2024-02-10T07:43:30.449Z" }, + { url = "https://files.pythonhosted.org/packages/ad/97/556215f71184291155aee340a6d34f0676e7238fdfd10615b6b775ce25fe/editdistance-0.8.1-cp310-cp310-win32.whl", hash = "sha256:fd64b58f5a7b59afd9d75982aaeeacd2a98498bf472fa0360c122ffe6ea4c871", size = 80834, upload-time = "2024-02-10T07:43:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d1/7ec5f5cbb95838d0eff7f980a660c81acd1363d658f2f5d4ceba38877c5a/editdistance-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:6c7c62c3cae45ca1fa01bb2722b297b9de1e3a244ac44cfba88bdcb488fe6aee", size = 79614, upload-time = "2024-02-10T07:43:33.255Z" }, + { url = "https://files.pythonhosted.org/packages/e2/dc/d0c29fd52d8f9e795653ed2b838a2a48c739cdfff04ac5b79c6c0ecbdf79/editdistance-0.8.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:486105603a273d73d12a54f347dffa70ab281749d7c3879658b377bc49e4b98c", size = 106079, upload-time = "2024-02-10T07:43:34.34Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/75fa45d7b78fbea6fd894f4e48895a75bd3c83d4a9a6b57673881d74d3e0/editdistance-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fad081f5f86a175c1a09a4e9e45b95c9349e454c21e181e842e01c85f1f536fc", size = 80580, upload-time = "2024-02-10T07:43:35.947Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a3/058d823b6285c3511dc94ed80620c3fb0c18b4aaa708f70ba71f3af28436/editdistance-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8cb78e125f6759398885a775f5eed07c2bb72b2f86da43e674c6b6a3335b273b", size = 79087, upload-time = "2024-02-10T07:43:36.923Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3a/0b13c7864c93b1e9b9952bd2a33c5ef3c4fd1bf70a5fad6924789e70e5eb/editdistance-0.8.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3778ca60aa89def9144b70e330bcec5330c7da1d69cb28c612e90b84510a1d3d", size = 409296, upload-time = "2024-02-10T07:43:38.52Z" }, + { url = "https://files.pythonhosted.org/packages/96/8a/db0fd79e8ddb9b5f86f274107c5d0a27ec4f2af88877df1f26c2c6d150cc/editdistance-0.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fba945eaa0436cf40bc53d7e299dc537c7c71353379a095b7459ff4af910da33", size = 412913, upload-time = "2024-02-10T07:43:39.852Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d2/98be7112750ff17b436dd76f988f1e38570dcec0df8578ee19ef046f22fe/editdistance-0.8.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:877f2a0d801f32bc1a1878901ffb947b974361e849c66e314a7f1d786a446b58", size = 407430, upload-time = "2024-02-10T07:43:41.048Z" }, + { url = "https://files.pythonhosted.org/packages/03/62/1815e3bf164910c47ba1948c8b5e937a40c7f9763b64e98fb6666b01dd06/editdistance-0.8.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e79d351ca40a6ead5f3763253fd7521572ee0d3e5d42538630e56d10f48db481", size = 909217, upload-time = "2024-02-10T07:43:42.916Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d3/a832cea7b507a9be54e4ac3d1340fb66dca5f9c16c70bf38d5039e8fdede/editdistance-0.8.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:70ed382b3052a51161bad0149d4665003bf3b949fce0b01bf1253a4cc1a88239", size = 969407, upload-time = "2024-02-10T07:43:44.912Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b4/db291d2a3845cbf8047b4b5aad3b3e038a8a2994d87027b40e1a1b0f4b74/editdistance-0.8.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a529bfb384c4000775d76739c4e64f73337f0f5a3784933b1321b577a62bed4e", size = 922112, upload-time = "2024-02-10T07:43:47.047Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/7ddeacada4982d0b892a28897e21871d0f25bca165e3663e37c3a272808a/editdistance-0.8.1-cp311-cp311-win32.whl", hash = "sha256:b082232429e731f181af7f7d2bcf79da6ca8fadd04e9086c11e2973f7d330c81", size = 80799, upload-time = "2024-02-10T07:43:48.231Z" }, + { url = "https://files.pythonhosted.org/packages/52/a1/778af8590b8b12f03f62eacc3c8744407ade9e3d69be6dabe38d0afbf2dd/editdistance-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:cef1a4359252a49f2c4718e64e9d40027d9d951b289d045bdb278656e59f6af8", size = 79698, upload-time = "2024-02-10T07:43:49.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4c/7f195588949b4e72436dc7fc902632381f96e586af829685b56daebb38b8/editdistance-0.8.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04af61b3fcdd287a07c15b6ae3b02af01c5e3e9c3aca76b8c1d13bd266b6f57", size = 106723, upload-time = "2024-02-10T07:43:50.268Z" }, + { url = "https://files.pythonhosted.org/packages/8d/82/31dc1640d830cd7d36865098329f34e4dad3b77f31cfb9404b347e700196/editdistance-0.8.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:18fc8b6eaae01bfd9cf999af726c1e8dcf667d120e81aa7dbd515bea7427f62f", size = 80998, upload-time = "2024-02-10T07:43:51.259Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2a/6b823e71cef694d6f070a1d82be2842706fa193541aab8856a8f42044cd0/editdistance-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a87839450a5987028738d061ffa5ef6a68bac2ddc68c9147a8aae9806629c7f", size = 79248, upload-time = "2024-02-10T07:43:52.873Z" }, + { url = "https://files.pythonhosted.org/packages/e1/31/bfb8e590f922089dc3471ed7828a6da2fc9453eba38c332efa9ee8749fd7/editdistance-0.8.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24b5f9c9673c823d91b5973d0af8b39f883f414a55ade2b9d097138acd10f31e", size = 415262, upload-time = "2024-02-10T07:43:54.498Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/57423942b2f847cdbbb46494568d00cd8a45500904ea026f0aad6ca01bc7/editdistance-0.8.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c59248eabfad603f0fba47b0c263d5dc728fb01c2b6b50fb6ca187cec547fdb3", size = 418905, upload-time = "2024-02-10T07:43:55.779Z" }, + { url = "https://files.pythonhosted.org/packages/1b/05/dfa4cdcce063596cbf0d7a32c46cd0f4fa70980311b7da64d35f33ad02a0/editdistance-0.8.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e239d88ff52821cf64023fabd06a1d9a07654f364b64bf1284577fd3a79d0e", size = 412511, upload-time = "2024-02-10T07:43:57.567Z" }, + { url = "https://files.pythonhosted.org/packages/0e/14/39608ff724a9523f187c4e28926d78bc68f2798f74777ac6757981108345/editdistance-0.8.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2f7f71698f83e8c83839ac0d876a0f4ef996c86c5460aebd26d85568d4afd0db", size = 917293, upload-time = "2024-02-10T07:43:59.559Z" }, + { url = "https://files.pythonhosted.org/packages/df/92/4a1c61d72da40dedfd0ff950fdc71ae83f478330c58a8bccfd776518bd67/editdistance-0.8.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:04e229d6f4ce0c12abc9f4cd4023a5b5fa9620226e0207b119c3c2778b036250", size = 975580, upload-time = "2024-02-10T07:44:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/47/3d/9877566e724c8a37f2228a84ec5cbf66dbfd0673515baf68a0fe07caff40/editdistance-0.8.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e16721636da6d6b68a2c09eaced35a94f4a4a704ec09f45756d4fd5e128ed18d", size = 929121, upload-time = "2024-02-10T07:44:02.764Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/8c50757d198b8ca30ddb91e8b8f0247a8dca04ff2ec30755245f0ab1ff0c/editdistance-0.8.1-cp312-cp312-win32.whl", hash = "sha256:87533cf2ebc3777088d991947274cd7e1014b9c861a8aa65257bcdc0ee492526", size = 81039, upload-time = "2024-02-10T07:44:04.134Z" }, + { url = "https://files.pythonhosted.org/packages/28/f0/65101e51dc7c850e7b7581a5d8fa8721a1d7479a0dca6c08386328e19882/editdistance-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:09f01ed51746d90178af7dd7ea4ebb41497ef19f53c7f327e864421743dffb0a", size = 79853, upload-time = "2024-02-10T07:44:05.687Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4c/c9d02eeb47815d35f8d324b52f6704ea7beb032bcb209358cac44047d413/editdistance-0.8.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a4a90c6b03094c07358572027a8d0a13cca7450b1aa6caca98a5f1fa4f0b8961", size = 76455, upload-time = "2024-02-10T07:44:36.838Z" }, + { url = "https://files.pythonhosted.org/packages/af/b0/2818fa6a24595dac069b0bfb9d05658406779a1ded8fd2b0c9066396cf99/editdistance-0.8.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:510a4f9ced348a4fd89ae2e102357d4d801a771e29bb2bc2f130a1692193407f", size = 84104, upload-time = "2024-02-10T07:44:37.928Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/3d5e09bcf7fdb7aed705bf74047a8634bd2b8fd92177c25a2547e6dbadfb/editdistance-0.8.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4787fa7228ba6a34b430066d174320f011d605015baa7299c2c4911e6ea6bd46", size = 89058, upload-time = "2024-02-10T07:44:39.113Z" }, + { url = "https://files.pythonhosted.org/packages/cd/88/fca5d7b1a1edf66ce1e5b6b60bff75842e6814b4f5facbdf4585d88c912d/editdistance-0.8.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee02601375073afccd6b4d811129ce1cb696d47db734784d8dbd1fddcea75447", size = 84635, upload-time = "2024-02-10T07:44:40.714Z" }, + { url = "https://files.pythonhosted.org/packages/a9/91/0e6285bbe2358d81fd16313d30306b2d0036387348f7bc11d8c076ca3c72/editdistance-0.8.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bc7ad9f9a20e6f351523de77c59249f005242e3f317b5de45d02c378d24f6531", size = 77389, upload-time = "2024-02-10T07:44:41.725Z" }, +] + +[[package]] +name = "einops" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fastapi" +version = "0.128.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, +] + +[[package]] +name = "fastapi-offline" +version = "1.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastapi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/8e/6f1d86982f09ecf9d326c7313d2e6be0a96d979130a87f61022c807a7414/fastapi_offline-1.7.6.tar.gz", hash = "sha256:c84d08584faa646932951b493106992caa79b838e454f14dd410cef9a1a5e07d", size = 745483, upload-time = "2026-01-15T04:29:43.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/56/10bd6900c99d9ecb3d803583b3736e8d741f19f04dd39e36aa7ad776c638/fastapi_offline-1.7.6-py3-none-any.whl", hash = "sha256:24d6851b5a94c50f669594b7ab9d1bbe3a4c0a53c4f4e9ce47798ff4591790d2", size = 744654, upload-time = "2026-01-15T04:29:41.943Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/f4/5721faf47b8c499e776bc34c6a8fc17efdf7fdef0b00f398128bc5dcb4ac/fsspec-2025.3.0.tar.gz", hash = "sha256:a935fd1ea872591f2b5148907d103488fc523295e6c64b835cfad8c3eca44972", size = 298491, upload-time = "2025-03-07T21:47:56.461Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/53/eb690efa8513166adef3e0669afd31e95ffde69fb3c52ec2ac7223ed6018/fsspec-2025.3.0-py3-none-any.whl", hash = "sha256:efb87af3efa9103f94ca91a7f8cb7a4df91af9f74fc106c9c7ea0efd7277c1b3", size = 193615, upload-time = "2025-03-07T21:47:54.809Z" }, +] + +[package.optional-dependencies] +http = [ + { name = "aiohttp" }, +] + +[[package]] +name = "funasr" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "editdistance" }, + { name = "hydra-core" }, + { name = "jaconv" }, + { name = "jamo" }, + { name = "jieba" }, + { name = "kaldiio" }, + { name = "librosa" }, + { name = "modelscope" }, + { name = "oss2" }, + { name = "pytorch-wpe" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "scipy" }, + { name = "sentencepiece" }, + { name = "soundfile" }, + { name = "tensorboardx" }, + { name = "torch-complex" }, + { name = "tqdm" }, + { name = "umap-learn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/dc/d5586cd788bdf75286d73e646f5f354f8b3220b75a61c0325a177689065d/funasr-1.3.1.tar.gz", hash = "sha256:ed813c0ecade7d24393943a82a91f7cee822178ab4bfd303adb8ad318605e0a5", size = 664362, upload-time = "2026-01-26T13:07:43.579Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/89/61c09967f0f4f091402367215000c3c060bfeaadbbdc9aca42856d299c95/funasr-1.3.1-py3-none-any.whl", hash = "sha256:f63050d7d625f287ec741b84a0325365699c49550a4bfd3ace4acb43e41a87a8", size = 811975, upload-time = "2026-01-26T13:07:41.866Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hdbscan" +version = "0.8.41" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/22/32a66dd4ce72145ec1b792c794b98897be467bdf18c10aa8b48275530b11/hdbscan-0.8.41.tar.gz", hash = "sha256:e41e823e5bb21ff2173f252d226266b1dda82bdbba5d89106eafb251429dff3d", size = 7091384, upload-time = "2025-12-12T15:48:30.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/7a/8bc50300f7c1240284b8a69d6c69c59ba37e6349bc9ca097760d3efae077/hdbscan-0.8.41-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:0589ea22e225e4ed6fae8b0a6ac6d18c0aff15165b8ddea7561962e1006b7e63", size = 755274, upload-time = "2025-12-12T15:49:22.72Z" }, + { url = "https://files.pythonhosted.org/packages/10/d9/cf2dc6c14ff2a85f2f48a5c3e034df3690b655f5dc09e2e7db6bc140e0ce/hdbscan-0.8.41-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3970e33b7b370cdca5d0fc5d31171c4b5d588590ec6f04b83e08f743099ba950", size = 4162517, upload-time = "2025-12-12T15:48:27.787Z" }, + { url = "https://files.pythonhosted.org/packages/63/f1/37daced2420b5edaea6fb91875fa4c08e1ef72eaf6f84bc8d4442f45c1dc/hdbscan-0.8.41-cp310-cp310-win_amd64.whl", hash = "sha256:7a689386170d91d1bd9386665b521e4ae66b6a78e0b7e34265ea5b1aa1eb165f", size = 687021, upload-time = "2025-12-12T15:48:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/70/58/5c1cbfac6dd5fd4310da17b09950c68dba1a7c1bdd267eb31468b140bd3a/hdbscan-0.8.41-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be948fc76d0035d93d309920f14ab6a0185580a63c5a63b05739f08b45dc6c03", size = 1394443, upload-time = "2025-12-12T15:49:56.098Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f1/9a17849751488049003a6af08b270eac1e0135d1d29dfd006bcc4edcca00/hdbscan-0.8.41-cp311-cp311-win_amd64.whl", hash = "sha256:0af3e3bab1eb6b07ea497afc4d2db1b58122974fb052bd21f0ea4b42fcf8d535", size = 687100, upload-time = "2025-12-12T15:49:57.879Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6b/b589c0e903e00108c62f324e99840ab050f1da344fab9cf143ce8ebf1d38/hdbscan-0.8.41-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:07ae4c44098449bd9de12145ad17e92ef699754a43988cbea2dd1a95b89bf142", size = 1393282, upload-time = "2025-12-12T15:52:38.472Z" }, + { url = "https://files.pythonhosted.org/packages/59/ab/6314e52aee546cc14b74fbb575b8713eeec2255880ec99d6490838306d3a/hdbscan-0.8.41-cp312-cp312-win_amd64.whl", hash = "sha256:dce39272d2d4f1dde50dde9cc428cadb84ed16326de872b01761f7ec4f690419", size = 671752, upload-time = "2025-12-12T15:51:33.213Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477, upload-time = "2026-03-31T22:40:07.874Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/9f/9c23e4a447b8f83120798f9279d0297a4d1360bdbf59ef49ebec78fe2545/hf_xet-1.4.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d0da85329eaf196e03e90b84c2d0aca53bd4573d097a75f99609e80775f98025", size = 3805048, upload-time = "2026-03-31T22:39:53.105Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178, upload-time = "2026-03-31T22:39:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320, upload-time = "2026-03-31T22:39:42.922Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546, upload-time = "2026-03-31T22:39:41.335Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200, upload-time = "2026-03-31T22:40:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392, upload-time = "2026-03-31T22:40:03.492Z" }, + { url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359, upload-time = "2026-03-31T22:40:13.619Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664, upload-time = "2026-03-31T22:40:12.152Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" }, + { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" }, + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/77/a9e27cdb6a2f8387c7b8f7e6a107458b7b96f156a5a78fab922d0d563df8/huggingface_hub-0.34.0.tar.gz", hash = "sha256:2c6f373fac66b1afc2fe47efd8e603a876935dbd669f966231f265da5c353e25", size = 456749, upload-time = "2025-07-25T08:52:20.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/ac/2f910d2572360ff2f3c4faf41b6c3e170b8934704be8bab2c7f80ff801e9/huggingface_hub-0.34.0-py3-none-any.whl", hash = "sha256:29737225f21dcc5c6b41bc9ab1073e65f9a2f84ecf9ad0a0c2fb92ae8cee173f", size = 558651, upload-time = "2025-07-25T08:52:18.787Z" }, +] + +[[package]] +name = "hydra-core" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "omegaconf" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/8e/07e42bc434a847154083b315779b0a81d567154504624e181caf2c71cd98/hydra-core-1.3.2.tar.gz", hash = "sha256:8a878ed67216997c3e9d88a8e72e7b4767e81af37afb4ea3334b269a4390a824", size = 3263494, upload-time = "2023-02-23T18:33:43.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/50/e0edd38dcd63fb26a8547f13d28f7a008bc4a3fd4eb4ff030673f22ad41a/hydra_core-1.3.2-py3-none-any.whl", hash = "sha256:fa0238a9e31df3373b35b0bfb672c34cc92718d21f81311d8996a16de1141d8b", size = 154547, upload-time = "2023-02-23T18:33:40.801Z" }, +] + +[[package]] +name = "idna" +version = "3.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/12/2948fbe5513d062169bd91f7d7b1cd97bc8894f32946b71fa39f6e63ca0c/idna-3.12.tar.gz", hash = "sha256:724e9952cc9e2bd7550ea784adb098d837ab5267ef67a1ab9cf7846bdbdd8254", size = 194350, upload-time = "2026-04-21T13:32:48.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/b2/acc33950394b3becb2b664741a0c0889c7ef9f9ffbfa8d47eddb53a50abd/idna-3.12-py3-none-any.whl", hash = "sha256:60ffaa1858fac94c9c124728c24fcde8160f3fb4a7f79aa8cdd33a9d1af60a67", size = 68634, upload-time = "2026-04-21T13:32:47.403Z" }, +] + +[[package]] +name = "itntext" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "kaldifst" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/74/a993913031b21d5fcc9d22ef363ce1c589d9c89ea16cb4d6748be745008d/itntext-0.1.5.tar.gz", hash = "sha256:6cf0bf2deee51786f8523b35d61dc357d3ece0612a41021d9b9d2d17dfbcc31f", size = 484023, upload-time = "2026-06-12T04:52:18.238Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/04/0dd5e86a9e6f843cd286d7cec890ead69c90e398d7ccce85cc181786e3a8/itntext-0.1.5-py3-none-any.whl", hash = "sha256:1a89a8a2105f129ce07b90f44a9b8542ac3b3da4b369c97fea5b0fc52755c03c", size = 514874, upload-time = "2026-06-12T04:52:16.812Z" }, +] + +[[package]] +name = "jaconv" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/0e/9fffaacda59bdfa479372c71d18d72968d2af5a36a5a2086b02a60124b98/jaconv-0.5.0.tar.gz", hash = "sha256:53f6f968276846716f0f37100a6d5c7308cfa1e0c714eb41287d5bb09345c40f", size = 21816, upload-time = "2026-02-08T11:15:57.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/da/9657d637bcacdbaf6a914ce504000da5639f9d945f8d3552a940f021d6c0/jaconv-0.5.0-py3-none-any.whl", hash = "sha256:2914114fe761ca49fc7089e25e6ad4a400c26f262ffce84e13b176916b71610a", size = 16831, upload-time = "2026-02-08T11:15:55.322Z" }, +] + +[[package]] +name = "jamo" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/a2/bda770579809726e929ca6356743f9f50f64a2cbaee578fa9d4824afb00e/jamo-0.4.1.tar.gz", hash = "sha256:ea65cf9d35338d0e0af48d75ff426d8a369b0ebde6f07051c3ac37256f56d025", size = 7386, upload-time = "2017-11-06T19:28:51.729Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/cc/49812faae67f9a24be6ddaf58a2cf7e8c3cbfcf5b762d9414f7103d2ea2c/jamo-0.4.1-py3-none-any.whl", hash = "sha256:d4b94fd23324c606ed2fbc4037c603e2c3a7ae9390c05d3473aea1ccb6b1c3fb", size = 9543, upload-time = "2017-11-06T19:28:49.624Z" }, +] + +[[package]] +name = "jieba" +version = "0.42.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/cb/18eeb235f833b726522d7ebed54f2278ce28ba9438e3135ab0278d9792a2/jieba-0.42.1.tar.gz", hash = "sha256:055ca12f62674fafed09427f176506079bc135638a14e23e25be909131928db2", size = 19214172, upload-time = "2020-01-20T14:27:23.5Z" } + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jmespath" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/56/3f325b1eef9791759784aa5046a8f6a1aff8f7c898a2e34506771d3b99d8/jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", size = 21607, upload-time = "2020-05-12T22:03:47.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/cb/5f001272b6faeb23c1c9e0acc04d48eaaf5c862c17709d20e3469c6e0139/jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f", size = 24489, upload-time = "2020-05-12T22:03:45.643Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "kaldifst" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/ad/3d2bbf5e91f05d78c385510d04d4749948856a2db3a01034d36904e2a6e9/kaldifst-1.8.0.tar.gz", hash = "sha256:80c8b819cb762ab6251578444e8b80379049a85a122ac74d37b2534faa45b63a", size = 154383, upload-time = "2026-04-09T13:14:36.102Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/f0/b4d00999db899ec2480aba1e122c2287befc1880a234ff6ed6b600aba4e5/kaldifst-1.8.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:15bd392fc1fc9f6fe4a4ea6a9bfe6de6cb3370cc0a037e1b7bddd3ffaf56879b", size = 2137735, upload-time = "2026-04-09T13:20:52.087Z" }, + { url = "https://files.pythonhosted.org/packages/68/d5/916e288ecdf4b24b148ee64d4b0bc29ec85d234d6793e0a7d7ca4fc78014/kaldifst-1.8.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b0dd41eaf512d1fc04d6a501d2b21b3288addf20eb5776cdd4c5103b94e9ebe1", size = 1131799, upload-time = "2026-04-09T13:27:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/a1/aa/262dea7f3227292df0ec108a31ccd4c5c9d47cade6f8d4e75167906fbeb4/kaldifst-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a233d587f89de95858ce3b23b39c12c4cdbba4d1d9a75398814b1d146dee08", size = 1015393, upload-time = "2026-04-09T13:20:23.202Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6d/76c6e153886cfae42f17206b65c367bdbd46723e449beb283819b049bcab/kaldifst-1.8.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b131922d7eac9dc1aebd38b072d61d2cb8068d02d955247e4e980829056f6c73", size = 1353357, upload-time = "2026-04-09T13:13:51.639Z" }, + { url = "https://files.pythonhosted.org/packages/c9/39/97c230b34c7a79e38d7d395cb686a6998b28553df7df81f51db4706ed61e/kaldifst-1.8.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da9fb560466cd6abde0300099194dfcf13f6383809c49c08f3d81df166d52661", size = 1461491, upload-time = "2026-04-09T13:14:43.087Z" }, + { url = "https://files.pythonhosted.org/packages/22/7f/83a9fbcd8f4378636b8169ef12757e1a014ec811f2f45f8cd94b3a228925/kaldifst-1.8.0-cp310-cp310-win32.whl", hash = "sha256:be8c18f15a7a7987c66ded61afaa44cb1015900b6f198f00c09bc367e79ce74c", size = 1005277, upload-time = "2026-04-09T13:15:17.189Z" }, + { url = "https://files.pythonhosted.org/packages/73/d5/e228103272986e7e755d02a4255b40e801327dc811704cf6de63be3d481b/kaldifst-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:ec9b5967d6d4089d45354fb5a13947b6da5bce9e5ca67338684650e8b4a2caa6", size = 1086453, upload-time = "2026-04-09T13:13:53.105Z" }, + { url = "https://files.pythonhosted.org/packages/72/53/775b58ff033951aadf5ee1cbe56dd8ea2681d437a79611bbc55b4a7881a2/kaldifst-1.8.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:8deeadd5d391663d6c6fdbbd4f7a1daf8c9392613b67fb6815f8bf14ba209298", size = 2142137, upload-time = "2026-04-09T13:30:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/8c/54/ae8afef4e9e84a79dfe516a9d3ae671746f0a7c7484d01bc6471a1d5dc60/kaldifst-1.8.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:04555763c04e1938dcbcbc0c60bd2f56db0a5bf29b2e7efa16d11c19aae14b06", size = 1133774, upload-time = "2026-04-09T13:01:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/00/3c/6e27e1dec2baa389171e3680baab9414f0d8005374ce700192fdafd023e6/kaldifst-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d04b56b00233afeab05eb781157840a444b7a56992c30a08fac8096f86be8389", size = 1017817, upload-time = "2026-04-09T13:19:28.516Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d4/8ffab37d491e1047041f7c2c936498a57a95b3a13a08ba3ee32f4c1722b3/kaldifst-1.8.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77dcbecd257cb0d6eb4920bd566af7ba4a380d493cbab8844fb5a51558d8de8a", size = 1360677, upload-time = "2026-04-09T13:13:35.467Z" }, + { url = "https://files.pythonhosted.org/packages/96/d3/e891ff8dbc1beaee04c0a081423595d6e276ee369342eb508f36a7e25767/kaldifst-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9d585ee47c37ebca2ad401bacef747675486f38a21d380aec94242e3a3b0a13", size = 1463829, upload-time = "2026-04-09T13:14:34.887Z" }, + { url = "https://files.pythonhosted.org/packages/85/36/e7e72868544fd04b9c216006401c5091d0e4d7b0db8844ae337b4b44e378/kaldifst-1.8.0-cp311-cp311-win32.whl", hash = "sha256:d8a4cfbb5ee09c0f6e515ac619b265a0b19c0676b5b78b84c54440bbf1892628", size = 1012695, upload-time = "2026-04-09T13:14:56.462Z" }, + { url = "https://files.pythonhosted.org/packages/f0/99/2001d401ecd2e48013b947ce3c99083ffa60d8273142a8f78c5511c2d668/kaldifst-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:8932f8bb9cd06491edb8d2bea0f454e67c34c27b2fb4a0ceb1110160d26a70d8", size = 1093455, upload-time = "2026-04-09T13:14:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/ec/1e/c1fddc62632014f10d891feb457929119845213418ab51d5353af8cff7df/kaldifst-1.8.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:4e7cec0eea5e72f87ad915c7a0762404331cf5f6f04517292fe50f8de4e98f39", size = 2151447, upload-time = "2026-04-09T13:12:29.577Z" }, + { url = "https://files.pythonhosted.org/packages/73/b3/6f1ac9dfad226fd940eca8e61d89c760fc315eaf18a3db6633c95988d48e/kaldifst-1.8.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:f86c756e09dddee70a0609b7f87aa749d965e2ca1acddbc1220fa0807c3d3e16", size = 1142310, upload-time = "2026-04-09T13:02:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/30/7f8f3b350dc8b2961da4b5bc5af9fd9478f6084173d5f13a0fe25e06686f/kaldifst-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6ff7845e27c58a71ff4841050fadbbab6c75444645d9e2bc9834f8dbae6c10e", size = 1019098, upload-time = "2026-04-09T13:41:15.94Z" }, + { url = "https://files.pythonhosted.org/packages/29/63/09324462016f14dac99f975c4e94cbe9ea90aa2795a9142e98424df931b5/kaldifst-1.8.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f4cc1ab32cdc68bad732154fec2bd69032a94ed498705b5423cd17499214ab0", size = 1357280, upload-time = "2026-04-09T13:13:29.875Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bd/dcf9d079cdf158360bdce69762b22d4fc3eccde89d6b08e180af7f42274c/kaldifst-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70e28ca1b1ed68ad61988d684754ff43ba6386d177ec24f8445bab316e10c3d0", size = 1466422, upload-time = "2026-04-09T13:14:56.638Z" }, + { url = "https://files.pythonhosted.org/packages/2b/1a/56b249c7cfdc80bd4e84b2076e06a68a76418cb27e277eb36a0be8bb3075/kaldifst-1.8.0-cp312-cp312-win32.whl", hash = "sha256:8512e2a9a175f7d8521b2956aa4e4dbed01026123bd6947f7f37c3c750832e04", size = 1013100, upload-time = "2026-04-09T13:15:09.981Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b2/6445e81706075c6ccaf0eba59df45fdf966f7e48e4e0c5f4585aa7458f02/kaldifst-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1d28cd0e25f9250b720b3fca9aa8f06b35fd09a7944bb0a1306c137702f9dd0", size = 1094573, upload-time = "2026-04-09T13:13:59.448Z" }, +] + +[[package]] +name = "kaldiio" +version = "2.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/85/92435e8e62eb3d43eded9f24643fc2a6dbce031cebceed11528147c7873f/kaldiio-2.18.1.tar.gz", hash = "sha256:0283d197fac6ac683f7a9e6af8d18aad9dbd2c4a997f22e45294f2ac1ee3c432", size = 35570, upload-time = "2025-03-06T15:57:52.375Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/e3/6c3b42233225f398f7a72988b524f654ae818cca0d441db847a2761203e9/kaldiio-2.18.1-py3-none-any.whl", hash = "sha256:397a4cd18977acaae7acabfba6807ee0a6978c620064381a266eac15b3c1a0a0", size = 29330, upload-time = "2025-03-06T15:57:50.82Z" }, +] + +[[package]] +name = "lazy-loader" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294, upload-time = "2026-03-06T15:45:09.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl", hash = "sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005", size = 8044, upload-time = "2026-03-06T15:45:07.668Z" }, +] + +[[package]] +name = "librosa" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "audioread" }, + { name = "decorator" }, + { name = "joblib" }, + { name = "lazy-loader" }, + { name = "msgpack" }, + { name = "numba" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pooch" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy" }, + { name = "soundfile" }, + { name = "soxr" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/36/360b5aafa0238e29758729e9486c6ed92a6f37fa403b7875e06c115cdf4a/librosa-0.11.0.tar.gz", hash = "sha256:f5ed951ca189b375bbe2e33b2abd7e040ceeee302b9bbaeeffdfddb8d0ace908", size = 327001, upload-time = "2025-03-11T15:09:54.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/ba/c63c5786dfee4c3417094c4b00966e61e4a63efecee22cb7b4c0387dda83/librosa-0.11.0-py3-none-any.whl", hash = "sha256:0b6415c4fd68bff4c29288abe67c6d80b587e0e1e2cfb0aad23e4559504a7fa1", size = 260749, upload-time = "2025-03-11T15:09:52.982Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz", hash = "sha256:62031ce968ec74e95092184d4b0e857e444f8fdff0b8f9213707699570c33ccc", size = 193614, upload-time = "2026-03-31T18:29:53.497Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/f5/a1bde3aa8c43524b0acaf3f72fb3d80a32dd29dbb42d7dc434f84584cdcc/llvmlite-0.47.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41270b0b1310717f717cf6f2a9c68d3c43bd7905c33f003825aebc361d0d1b17", size = 37232772, upload-time = "2026-03-31T18:28:12.198Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fb/76d88fc05ee1f9c1a6efe39eb493c4a727e5d1690412469017cd23bcb776/llvmlite-0.47.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f9d118bc1dd7623e0e65ca9ac485ec6dd543c3b77bc9928ddc45ebd34e1e30a7", size = 56275179, upload-time = "2026-03-31T18:28:15.725Z" }, + { url = "https://files.pythonhosted.org/packages/4d/08/29da7f36217abd56a0c389ef9a18bea47960826e691ced1a36c92c6ce93c/llvmlite-0.47.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ea5cfb04a6ab5b18e46be72b41b015975ba5980c4ddb41f1975b83e19031063", size = 55128632, upload-time = "2026-03-31T18:28:19.946Z" }, + { url = "https://files.pythonhosted.org/packages/df/f8/5e12e9ed447d65f04acf6fcf2d79cded2355640b5131a46cee4c99a5949d/llvmlite-0.47.0-cp310-cp310-win_amd64.whl", hash = "sha256:166b896a2262a2039d5fc52df5ee1659bd1ccd081183df7a2fba1b74702dd5ea", size = 38138402, upload-time = "2026-03-31T18:28:23.327Z" }, + { url = "https://files.pythonhosted.org/packages/34/0b/b9d1911cfefa61399821dfb37f486d83e0f42630a8d12f7194270c417002/llvmlite-0.47.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74090f0dcfd6f24ebbef3f21f11e38111c4d7e6919b54c4416e1e357c3446b07", size = 37232770, upload-time = "2026-03-31T18:28:26.765Z" }, + { url = "https://files.pythonhosted.org/packages/46/27/5799b020e4cdfb25a7c951c06a96397c135efcdc21b78d853bbd9c814c7d/llvmlite-0.47.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca14f02e29134e837982497959a8e2193d6035235de1cb41a9cb2bd6da4eedbb", size = 56275177, upload-time = "2026-03-31T18:28:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/7e/51/48a53fedf01cb1f3f43ef200be17ebf83c8d9a04018d3783c1a226c342c2/llvmlite-0.47.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12a69d4bb05f402f30477e21eeabe81911e7c251cecb192bed82cd83c9db10d8", size = 55128631, upload-time = "2026-03-31T18:28:36.046Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/59227d06bdc96e23322713c381af4e77420949d8cd8a042c79e0043096cc/llvmlite-0.47.0-cp311-cp311-win_amd64.whl", hash = "sha256:c37d6eb7aaabfa83ab9c2ff5b5cdb95a5e6830403937b2c588b7490724e05327", size = 38138400, upload-time = "2026-03-31T18:28:40.076Z" }, + { url = "https://files.pythonhosted.org/packages/fa/48/4b7fe0e34c169fa2f12532916133e0b219d2823b540733651b34fdac509a/llvmlite-0.47.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:306a265f408c259067257a732c8e159284334018b4083a9e35f67d19792b164f", size = 37232769, upload-time = "2026-03-31T18:28:43.735Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4b/e3f2cd17822cf772a4a51a0a8080b0032e6d37b2dbe8cfb724eac4e31c52/llvmlite-0.47.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5853bf26160857c0c2573415ff4efe01c4c651e59e2c55c2a088740acfee51cd", size = 56275178, upload-time = "2026-03-31T18:28:48.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a3b4a543185305a9bdf3d9759d53646ed96e55e7dfd43f53e7a421b8fbae/llvmlite-0.47.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:003bcf7fa579e14db59c1a1e113f93ab8a06b56a4be31c7f08264d1d4072d077", size = 55128632, upload-time = "2026-03-31T18:28:52.901Z" }, + { url = "https://files.pythonhosted.org/packages/2f/f5/d281ae0f79378a5a91f308ea9fdb9f9cc068fddd09629edc0725a5a8fde1/llvmlite-0.47.0-cp312-cp312-win_amd64.whl", hash = "sha256:f3079f25bdc24cd9d27c4b2b5e68f5f60c4fdb7e8ad5ee2b9b006007558f9df7", size = 38138692, upload-time = "2026-03-31T18:28:57.147Z" }, +] + +[[package]] +name = "loguru" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/30/d87a423766b24db416a46e9335b9602b054a72b96a88a241f2b09b560fa8/loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac", size = 145103, upload-time = "2023-09-11T15:24:37.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/0a/4f6fed21aa246c6b49b561ca55facacc2a44b87d65b8b92362a8e99ba202/loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb", size = 62549, upload-time = "2023-09-11T15:24:35.016Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "modelscope" +version = "1.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "requests" }, + { name = "setuptools" }, + { name = "tqdm" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/89/7a13bf70090e631f81a943797e875272d19d47e0a6984eda0a5a3f04e7e3/modelscope-1.34.0.tar.gz", hash = "sha256:c3041af301334aa9ca3f66f5b23e11ca33a2bdf28cc415dcceb75f68e4732aac", size = 4560273, upload-time = "2026-01-19T02:50:23.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/df/2c112a7c4160aa5e74dad87060019be5eca197d910af3f5b12e68ec090a9/modelscope-1.34.0-py3-none-any.whl", hash = "sha256:4629ace145972520b71b0ad02e4604282426c0cfae6a4b0922509898f3b269c8", size = 6050825, upload-time = "2026-01-19T02:50:20.018Z" }, +] + +[package.optional-dependencies] +framework = [ + { name = "addict" }, + { name = "attrs" }, + { name = "datasets" }, + { name = "einops" }, + { name = "pillow" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "scipy" }, + { name = "setuptools" }, + { name = "simplejson" }, + { name = "sortedcontainers" }, + { name = "transformers" }, + { name = "urllib3" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "msgpack" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" }, + { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" }, + { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" }, + { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" }, + { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, + { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, + { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, + { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, + { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, + { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "multiprocess" +version = "0.70.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/ae/04f39c5d0d0def03247c2893d6f2b83c136bf3320a2154d7b8858f2ba72d/multiprocess-0.70.16.tar.gz", hash = "sha256:161af703d4652a0e1410be6abccecde4a7ddffd19341be0a7011b94aeb171ac1", size = 1772603, upload-time = "2024-01-28T18:52:34.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/76/6e712a2623d146d314f17598df5de7224c85c0060ef63fd95cc15a25b3fa/multiprocess-0.70.16-pp310-pypy310_pp73-macosx_10_13_x86_64.whl", hash = "sha256:476887be10e2f59ff183c006af746cb6f1fd0eadcfd4ef49e605cbe2659920ee", size = 134980, upload-time = "2024-01-28T18:52:15.731Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ab/1e6e8009e380e22254ff539ebe117861e5bdb3bff1fc977920972237c6c7/multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec", size = 134982, upload-time = "2024-01-28T18:52:17.783Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f7/7ec7fddc92e50714ea3745631f79bd9c96424cb2702632521028e57d3a36/multiprocess-0.70.16-py310-none-any.whl", hash = "sha256:c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02", size = 134824, upload-time = "2024-01-28T18:52:26.062Z" }, + { url = "https://files.pythonhosted.org/packages/50/15/b56e50e8debaf439f44befec5b2af11db85f6e0f344c3113ae0be0593a91/multiprocess-0.70.16-py311-none-any.whl", hash = "sha256:af4cabb0dac72abfb1e794fa7855c325fd2b55a10a44628a3c1ad3311c04127a", size = 143519, upload-time = "2024-01-28T18:52:28.115Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7d/a988f258104dcd2ccf1ed40fdc97e26c4ac351eeaf81d76e266c52d84e2f/multiprocess-0.70.16-py312-none-any.whl", hash = "sha256:fc0544c531920dde3b00c29863377f87e1632601092ea2daca74e4beb40faa2e", size = 146741, upload-time = "2024-01-28T18:52:29.395Z" }, + { url = "https://files.pythonhosted.org/packages/ea/89/38df130f2c799090c978b366cfdf5b96d08de5b29a4a293df7f7429fa50b/multiprocess-0.70.16-py38-none-any.whl", hash = "sha256:a71d82033454891091a226dfc319d0cfa8019a4e888ef9ca910372a446de4435", size = 132628, upload-time = "2024-01-28T18:52:30.853Z" }, + { url = "https://files.pythonhosted.org/packages/da/d9/f7f9379981e39b8c2511c9e0326d212accacb82f12fbfdc1aa2ce2a7b2b6/multiprocess-0.70.16-py39-none-any.whl", hash = "sha256:a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3", size = 133351, upload-time = "2024-01-28T18:52:31.981Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'linux')", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'linux')", + "python_full_version >= '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'", + "python_full_version >= '3.11' and sys_platform == 'darwin'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numba" +version = "0.65.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/61/7299643b9c18d669e04be7c5bcb64d985070d07553274817b45b049e7bfe/numba-0.65.0.tar.gz", hash = "sha256:edad0d9f6682e93624c00125a471ae4df186175d71fd604c983c377cdc03e68b", size = 2764131, upload-time = "2026-04-01T03:52:01.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/9b/e8453d93d5cb3f53cc956f135024be09d52f4f99643acaf8fdca090a8f3c/numba-0.65.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:dff9fd5fbc9a35c517359c5823ea705d9b65f01fb46e42e35a2eabe5a52c2e96", size = 2680537, upload-time = "2026-04-01T03:51:17.325Z" }, + { url = "https://files.pythonhosted.org/packages/07/95/d6a2f0625e1092624228301eea11cdaff21ddcaf917ef3d631846a38b2f4/numba-0.65.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4c894c94afa5ffd627c7e3b693df10cb0d905bd5eb06de3dfc31775140cf4f89", size = 3739444, upload-time = "2026-04-01T03:51:19.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/fe518c97af035e4ec670c2edc3f0ff7a518cbed2f0b5053124d7c979bd8a/numba-0.65.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7325b1aab88f0339057288ee32f39dc660e14f93872a6fda14fa6eb9f95b047", size = 3446390, upload-time = "2026-04-01T03:51:21.55Z" }, + { url = "https://files.pythonhosted.org/packages/d0/06/5010939854249c290c6217e3fb7404914f4ed953f9923e340c3e166bcaf0/numba-0.65.0-cp310-cp310-win_amd64.whl", hash = "sha256:71e72e9ca2f619df4768f9c3962bfec60191a5a26fe2b6a8c6a07532b6146169", size = 2747200, upload-time = "2026-04-01T03:51:23.674Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ce/d67c499703eb5479ce02420e8ccd65c5753d87d2e16d563f152d71405346/numba-0.65.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:28e547d0b18024f19cbaf9de02fc5c145790213d9be8a2c95b43f93ec162b9e4", size = 2680228, upload-time = "2026-04-01T03:51:25.401Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a7/11e2b24251d57cf41fc9ad83f378d890d61a890e3f8eb6338b39833f67a4/numba-0.65.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:032b0b8e879512cd424d79eed6d772a1399c6387ded184c2cf3cc22c08d750a6", size = 3744674, upload-time = "2026-04-01T03:51:27.311Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0b/7c63eb742859a6243f42288441f65ac9dac96ea59f409e43b713aafbe867/numba-0.65.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af143d823624033a128b5950c0aaf9ffc2386dfe954eb757119cf0432335534c", size = 3450620, upload-time = "2026-04-01T03:51:29.092Z" }, + { url = "https://files.pythonhosted.org/packages/53/ff/1371cbbe955be340a46093a10b61462437e0fadc7a63290473a0e584cb03/numba-0.65.0-cp311-cp311-win_amd64.whl", hash = "sha256:15d159578e59a39df246b83480f78d7794b0fca40153b5684d3849a99c48a0fb", size = 2747081, upload-time = "2026-04-01T03:51:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2f/8bd31a1ea43c01ac215283d83aa5f8d5acbe7a36c85b82f1757bfe9ccb31/numba-0.65.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:b27ee4847e1bfb17e9604d100417ee7c1d10f15a6711c6213404b3da13a0b2aa", size = 2680705, upload-time = "2026-04-01T03:51:32.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/36/88406bd58600cc696417b8e5dd6a056478da808f3eaf48d18e2421e0c2d9/numba-0.65.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a52d92ffd297c10364bce60cd1fcb88f99284ab5df085f2c6bcd1cb33b529a6f", size = 3801411, upload-time = "2026-04-01T03:51:34.321Z" }, + { url = "https://files.pythonhosted.org/packages/0c/61/ce753a1d7646dd477e16d15e89473703faebb8995d2f71d7ad69a540b565/numba-0.65.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da8e371e328c06d0010c3d8b44b21858652831b85bcfba78cb22c042e22dbd8e", size = 3501622, upload-time = "2026-04-01T03:51:36.348Z" }, + { url = "https://files.pythonhosted.org/packages/7d/86/db87a5393f1b1fabef53ac3ba4e6b938bb27e40a04ad7cc512098fcae032/numba-0.65.0-cp312-cp312-win_amd64.whl", hash = "sha256:59bb9f2bb9f1238dfd8e927ba50645c18ae769fef4f3d58ea0ea22a2683b91f5", size = 2749979, upload-time = "2026-04-01T03:51:37.88Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'linux')", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux'", +] +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/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.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'linux')", + "python_full_version >= '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'", + "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.11' and sys_platform == 'win32'", + "python_full_version >= '3.11' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, + { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, +] + +[[package]] +name = "omegaconf" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, +] + +[[package]] +name = "oss2" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aliyun-python-sdk-core" }, + { name = "aliyun-python-sdk-kms" }, + { name = "crcmod" }, + { name = "pycryptodome" }, + { name = "requests" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/b5/f2cb1950dda46ac2284d6c950489fdacd0e743c2d79a347924d3cc44b86f/oss2-2.19.1.tar.gz", hash = "sha256:a8ab9ee7eb99e88a7e1382edc6ea641d219d585a7e074e3776e9dec9473e59c1", size = 298845, upload-time = "2024-10-25T11:37:46.638Z" } + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'linux')", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'linux')", + "python_full_version >= '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'", + "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.11' and sys_platform == 'win32'", + "python_full_version >= '3.11' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, + { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/35/6411db530c618e0e0005187e35aa02ce60ae4c4c4d206964a2f978217c27/pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0", size = 10326926, upload-time = "2026-03-31T06:46:08.29Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c", size = 9926987, upload-time = "2026-03-31T06:46:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/52/77/9b1c2d6070b5dbe239a7bc889e21bfa58720793fb902d1e070695d87c6d0/pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb", size = 10757067, upload-time = "2026-03-31T06:46:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76", size = 11258787, upload-time = "2026-03-31T06:46:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/90/e3/3f1126d43d3702ca8773871a81c9f15122a1f412342cc56284ffda5b1f70/pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e", size = 11771616, upload-time = "2026-03-31T06:46:20.532Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cf/0f4e268e1f5062e44a6bda9f925806721cd4c95c2b808a4c82ebe914f96b/pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa", size = 12337623, upload-time = "2026-03-31T06:46:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df", size = 9897372, upload-time = "2026-03-31T06:46:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/781516b808a99ddf288143cec46b342b3016c3414d137da1fdc3290d8860/pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f", size = 9154922, upload-time = "2026-03-31T06:46:30.284Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, + { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, + { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, + { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, + { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pooch" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "platformdirs" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/43/85ef45e8b36c6a48546af7b266592dc32d7f67837a6514d111bced6d7d75/pooch-1.9.0.tar.gz", hash = "sha256:de46729579b9857ffd3e741987a2f6d5e0e03219892c167c6578c0091fb511ed", size = 61788, upload-time = "2026-01-30T19:15:09.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl", hash = "sha256:f265597baa9f760d25ceb29d0beb8186c243d6607b0f60b83ecf14078dbc703b", size = 67175, upload-time = "2026-01-30T19:15:08.36Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "protobuf" +version = "7.34.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753, upload-time = "2026-03-20T17:34:38.751Z" }, + { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198, upload-time = "2026-03-20T17:34:39.871Z" }, + { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267, upload-time = "2026-03-20T17:34:41.1Z" }, + { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628, upload-time = "2026-03-20T17:34:42.536Z" }, + { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901, upload-time = "2026-03-20T17:34:44.112Z" }, + { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" }, +] + +[[package]] +name = "pyarrow" +version = "24.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/bf/a34fee1d624152124fa8355c42f34195ad5fe5233ce5bb87946432047d52/pyarrow-24.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:7c2b98645d576a0b9616892ead22b64a83a5f043c5e2ca15ebcefcb5b70c80cb", size = 35076681, upload-time = "2026-04-21T08:51:46.845Z" }, + { url = "https://files.pythonhosted.org/packages/1d/41/64180033d7027afce12dc96d0fe1f504c6fa112190582b458acea2399530/pyarrow-24.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:644a246325b8c69c595ad1dd4b463eba4b0cdb731370e4a86137d433208d6147", size = 36684260, upload-time = "2026-04-21T08:51:53.642Z" }, + { url = "https://files.pythonhosted.org/packages/57/02/9b9320e673dd8a99411fac78690f3df92f6dd6f59754c750110bca66d64e/pyarrow-24.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3a577bd840ca83f646f0a625dbc571dba7044c43c2d1503afc378b570954345c", size = 45698566, upload-time = "2026-04-21T10:46:02.133Z" }, + { url = "https://files.pythonhosted.org/packages/67/33/f75e91b9a64c3f33c787e263c93b871ad91b8a4a68c1d5cebddd9840e835/pyarrow-24.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e3268e43984d0b1a185c89b4cfff282a7ead12fc93f56cfd7088bdbcbe727041", size = 48835562, upload-time = "2026-04-21T10:46:10.278Z" }, + { url = "https://files.pythonhosted.org/packages/a5/63/097510448e47e4091faa41c43ba92f97cecaab8f4535b56a3d149578f634/pyarrow-24.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2392d954fcb920f42d230284b677605e4e2fbb11f2821e823e642abd67fbb491", size = 49394997, upload-time = "2026-04-21T10:46:18.08Z" }, + { url = "https://files.pythonhosted.org/packages/60/6b/c047d6222ab279024a062742d1807e2fbaf27bba88a98637299ff47b9236/pyarrow-24.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bec9373df11544592b0ba7ec2af0e35059e5f0e7647c6183a854dedd193298f1", size = 51911424, upload-time = "2026-04-21T10:46:25.347Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ba/464cc70761c2a525d97ebd84e21c31ebd47f3ef4bdcee117009f51c46f24/pyarrow-24.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:c42ab9439498270139cc63e18847a02afe5c8b3ed9c931266533cfe378bd3591", size = 27251730, upload-time = "2026-04-21T10:46:30.913Z" }, + { url = "https://files.pythonhosted.org/packages/62/c9/a47ab7ece0d86cbe6678418a0fbd1ac4bb493b9184a3891dfa0e7f287ae0/pyarrow-24.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b0e131f880cda8d04e076cee175a46fc0e8bc8b65c99c6c09dff6669335fde74", size = 35068898, upload-time = "2026-04-21T10:46:36.599Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bc/8db86617a9a58008acf8913d6fed68ea2a46acb6de928db28d724c891a68/pyarrow-24.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:1b2fe7f9a5566401a0ef2571f197eb92358925c1f0c8dba305d6e43ea0871bb3", size = 36679915, upload-time = "2026-04-21T10:46:42.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8e/fb178720400ef69db251eb4a9c3ccf4af269bc1feb5055529b8fc87170d1/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0b3537c00fb8d384f15ac1e79b6eb6db04a16514c8c1d22e59a9b95c8ba42868", size = 45697931, upload-time = "2026-04-21T10:46:48.403Z" }, + { url = "https://files.pythonhosted.org/packages/f3/27/99c42abe8e21b44f4917f62631f3aa31404882a2c41d8a4cd5c110e13d52/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:14e31a3c9e35f1ab6356c6378f6f72830e6d2d5f1791df3774a7b097d18a6a1e", size = 48837449, upload-time = "2026-04-21T10:46:55.329Z" }, + { url = "https://files.pythonhosted.org/packages/36/b6/333749e2666e9032891125bf9c691146e92901bece62030ac1430e2e7c88/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7d9a514e73bc42711e6a35aaccf3587c520024fe0a25d830a1a8a27c15f4f57", size = 49395949, upload-time = "2026-04-21T10:47:01.869Z" }, + { url = "https://files.pythonhosted.org/packages/17/25/c5201706a2dd374e8ba6ee3fd7a8c89fb7ffc16eed5217a91fd2bd7f7626/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b196eb3f931862af3fa84c2a253514d859c08e0d8fe020e07be12e75a5a9780c", size = 51912986, upload-time = "2026-04-21T10:47:09.872Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d2/4d1bbba65320b21a49678d6fbdc6ff7c649251359fdcfc03568c4136231d/pyarrow-24.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:35405aecb474e683fb36af650618fd5340ee5471fc65a21b36076a18bbc6c981", size = 27255371, upload-time = "2026-04-21T10:47:15.943Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" }, + { url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" }, + { url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" }, + { url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/da/b8a7ee04378a53f6fefefc0c5e05570a3ebfdfa0523a878bcd3b475683ee/pydantic-2.12.0.tar.gz", hash = "sha256:c1a077e6270dbfb37bfd8b498b3981e2bb18f68103720e51fa6c306a5a9af563", size = 814760, upload-time = "2025-10-07T15:58:03.467Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/9d/d5c855424e2e5b6b626fbc6ec514d8e655a600377ce283008b115abb7445/pydantic-2.12.0-py3-none-any.whl", hash = "sha256:f6a1da352d42790537e95e83a8bdfb91c7efbae63ffd0b86fa823899e807116f", size = 459730, upload-time = "2025-10-07T15:58:01.576Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/14/12b4a0d2b0b10d8e1d9a24ad94e7bbb43335eaf29c0c4e57860e8a30734a/pydantic_core-2.41.1.tar.gz", hash = "sha256:1ad375859a6d8c356b7704ec0f547a58e82ee80bb41baa811ad710e124bc8f2f", size = 454870, upload-time = "2025-10-07T10:50:45.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2c/a5c4640dc7132540109f67fe83b566fbc7512ccf2a068cfa22a243df70c7/pydantic_core-2.41.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e63036298322e9aea1c8b7c0a6c1204d615dbf6ec0668ce5b83ff27f07404a61", size = 2113814, upload-time = "2025-10-06T21:09:50.892Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e7/a8694c3454a57842095d69c7a4ab3cf81c3c7b590f052738eabfdfc2e234/pydantic_core-2.41.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:241299ca91fc77ef64f11ed909d2d9220a01834e8e6f8de61275c4dd16b7c936", size = 1916660, upload-time = "2025-10-06T21:09:52.783Z" }, + { url = "https://files.pythonhosted.org/packages/9c/58/29f12e65b19c1877a0269eb4f23c5d2267eded6120a7d6762501ab843dc9/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab7e594a2a5c24ab8013a7dc8cfe5f2260e80e490685814122081705c2cf2b0", size = 1975071, upload-time = "2025-10-06T21:09:54.009Z" }, + { url = "https://files.pythonhosted.org/packages/98/26/4e677f2b7ec3fbdd10be6b586a82a814c8ebe3e474024c8df2d4260e564e/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b054ef1a78519cb934b58e9c90c09e93b837c935dcd907b891f2b265b129eb6e", size = 2067271, upload-time = "2025-10-06T21:09:55.175Z" }, + { url = "https://files.pythonhosted.org/packages/29/50/50614bd906089904d7ca1be3b9ecf08c00a327143d48f1decfdc21b3c302/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f2ab7d10d0ab2ed6da54c757233eb0f48ebfb4f86e9b88ccecb3f92bbd61a538", size = 2253207, upload-time = "2025-10-06T21:09:56.709Z" }, + { url = "https://files.pythonhosted.org/packages/ea/58/b1e640b4ca559273cca7c28e0fe8891d5d8e9a600f5ab4882670ec107549/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2757606b7948bb853a27e4040820306eaa0ccb9e8f9f8a0fa40cb674e170f350", size = 2375052, upload-time = "2025-10-06T21:09:57.97Z" }, + { url = "https://files.pythonhosted.org/packages/53/25/cd47df3bfb24350e03835f0950288d1054f1cc9a8023401dabe6d4ff2834/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cec0e75eb61f606bad0a32f2be87507087514e26e8c73db6cbdb8371ccd27917", size = 2076834, upload-time = "2025-10-06T21:09:59.58Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b4/71b2c77e5df527fbbc1a03e72c3fd96c44cd10d4241a81befef8c12b9fc4/pydantic_core-2.41.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0234236514f44a5bf552105cfe2543a12f48203397d9d0f866affa569345a5b5", size = 2195374, upload-time = "2025-10-06T21:10:01.18Z" }, + { url = "https://files.pythonhosted.org/packages/aa/08/4b8a50733005865efde284fec45da75fe16a258f706e16323c5ace4004eb/pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1b974e41adfbb4ebb0f65fc4ca951347b17463d60893ba7d5f7b9bb087c83897", size = 2156060, upload-time = "2025-10-06T21:10:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/83/c3/1037cb603ef2130c210150a51b1710d86825b5c28df54a55750099f91196/pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:248dafb3204136113c383e91a4d815269f51562b6659b756cf3df14eefc7d0bb", size = 2331640, upload-time = "2025-10-06T21:10:04.39Z" }, + { url = "https://files.pythonhosted.org/packages/56/4c/52d111869610e6b1a46e1f1035abcdc94d0655587e39104433a290e9f377/pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:678f9d76a91d6bcedd7568bbf6beb77ae8447f85d1aeebaab7e2f0829cfc3a13", size = 2329844, upload-time = "2025-10-06T21:10:05.68Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/4b435f0b52ab543967761aca66b84ad3f0026e491e57de47693d15d0a8db/pydantic_core-2.41.1-cp310-cp310-win32.whl", hash = "sha256:dff5bee1d21ee58277900692a641925d2dddfde65182c972569b1a276d2ac8fb", size = 1991289, upload-time = "2025-10-06T21:10:07.199Z" }, + { url = "https://files.pythonhosted.org/packages/88/52/31b4deafc1d3cb96d0e7c0af70f0dc05454982d135d07f5117e6336153e8/pydantic_core-2.41.1-cp310-cp310-win_amd64.whl", hash = "sha256:5042da12e5d97d215f91567110fdfa2e2595a25f17c19b9ff024f31c34f9b53e", size = 2027747, upload-time = "2025-10-06T21:10:08.503Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a9/ec440f02e57beabdfd804725ef1e38ac1ba00c49854d298447562e119513/pydantic_core-2.41.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4f276a6134fe1fc1daa692642a3eaa2b7b858599c49a7610816388f5e37566a1", size = 2111456, upload-time = "2025-10-06T21:10:09.824Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f9/6bc15bacfd8dcfc073a1820a564516d9c12a435a9a332d4cbbfd48828ddd/pydantic_core-2.41.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07588570a805296ece009c59d9a679dc08fab72fb337365afb4f3a14cfbfc176", size = 1915012, upload-time = "2025-10-06T21:10:11.599Z" }, + { url = "https://files.pythonhosted.org/packages/38/8a/d9edcdcdfe80bade17bed424284427c08bea892aaec11438fa52eaeaf79c/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28527e4b53400cd60ffbd9812ccb2b5135d042129716d71afd7e45bf42b855c0", size = 1973762, upload-time = "2025-10-06T21:10:13.154Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b3/ff225c6d49fba4279de04677c1c876fc3dc6562fd0c53e9bfd66f58c51a8/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46a1c935c9228bad738c8a41de06478770927baedf581d172494ab36a6b96575", size = 2065386, upload-time = "2025-10-06T21:10:14.436Z" }, + { url = "https://files.pythonhosted.org/packages/47/ba/183e8c0be4321314af3fd1ae6bfc7eafdd7a49bdea5da81c56044a207316/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:447ddf56e2b7d28d200d3e9eafa936fe40485744b5a824b67039937580b3cb20", size = 2252317, upload-time = "2025-10-06T21:10:15.719Z" }, + { url = "https://files.pythonhosted.org/packages/57/c5/aab61e94fd02f45c65f1f8c9ec38bb3b33fbf001a1837c74870e97462572/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63892ead40c1160ac860b5debcc95c95c5a0035e543a8b5a4eac70dd22e995f4", size = 2373405, upload-time = "2025-10-06T21:10:17.017Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4f/3aaa3bd1ea420a15acc42d7d3ccb3b0bbc5444ae2f9dbc1959f8173e16b8/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4a9543ca355e6df8fbe9c83e9faab707701e9103ae857ecb40f1c0cf8b0e94d", size = 2073794, upload-time = "2025-10-06T21:10:18.383Z" }, + { url = "https://files.pythonhosted.org/packages/58/bd/e3975cdebe03ec080ef881648de316c73f2a6be95c14fc4efb2f7bdd0d41/pydantic_core-2.41.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2611bdb694116c31e551ed82e20e39a90bea9b7ad9e54aaf2d045ad621aa7a1", size = 2194430, upload-time = "2025-10-06T21:10:19.638Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/6b7e7217f147d3b3105b57fb1caec3c4f667581affdfaab6d1d277e1f749/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fecc130893a9b5f7bfe230be1bb8c61fe66a19db8ab704f808cb25a82aad0bc9", size = 2154611, upload-time = "2025-10-06T21:10:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/239c2fe76bd8b7eef9ae2140d737368a3c6fea4fd27f8f6b4cde6baa3ce9/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:1e2df5f8344c99b6ea5219f00fdc8950b8e6f2c422fbc1cc122ec8641fac85a1", size = 2329809, upload-time = "2025-10-06T21:10:22.678Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/77a821a67ff0786f2f14856d6bd1348992f695ee90136a145d7a445c1ff6/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:35291331e9d8ed94c257bab6be1cb3a380b5eee570a2784bffc055e18040a2ea", size = 2327907, upload-time = "2025-10-06T21:10:24.447Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9a/b54512bb9df7f64c586b369328c30481229b70ca6a5fcbb90b715e15facf/pydantic_core-2.41.1-cp311-cp311-win32.whl", hash = "sha256:2876a095292668d753f1a868c4a57c4ac9f6acbd8edda8debe4218d5848cf42f", size = 1989964, upload-time = "2025-10-06T21:10:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/9d/72/63c9a4f1a5c950e65dd522d7dd67f167681f9d4f6ece3b80085a0329f08f/pydantic_core-2.41.1-cp311-cp311-win_amd64.whl", hash = "sha256:b92d6c628e9a338846a28dfe3fcdc1a3279388624597898b105e078cdfc59298", size = 2025158, upload-time = "2025-10-06T21:10:27.522Z" }, + { url = "https://files.pythonhosted.org/packages/d8/16/4e2706184209f61b50c231529257c12eb6bd9eb36e99ea1272e4815d2200/pydantic_core-2.41.1-cp311-cp311-win_arm64.whl", hash = "sha256:7d82ae99409eb69d507a89835488fb657faa03ff9968a9379567b0d2e2e56bc5", size = 1972297, upload-time = "2025-10-06T21:10:28.814Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bc/5f520319ee1c9e25010412fac4154a72e0a40d0a19eb00281b1f200c0947/pydantic_core-2.41.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:db2f82c0ccbce8f021ad304ce35cbe02aa2f95f215cac388eed542b03b4d5eb4", size = 2099300, upload-time = "2025-10-06T21:10:30.463Z" }, + { url = "https://files.pythonhosted.org/packages/31/14/010cd64c5c3814fb6064786837ec12604be0dd46df3327cf8474e38abbbd/pydantic_core-2.41.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47694a31c710ced9205d5f1e7e8af3ca57cbb8a503d98cb9e33e27c97a501601", size = 1910179, upload-time = "2025-10-06T21:10:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/8e/2e/23fc2a8a93efad52df302fdade0a60f471ecc0c7aac889801ac24b4c07d6/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e9decce94daf47baf9e9d392f5f2557e783085f7c5e522011545d9d6858e00", size = 1957225, upload-time = "2025-10-06T21:10:33.11Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b6/6db08b2725b2432b9390844852e11d320281e5cea8a859c52c68001975fa/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab0adafdf2b89c8b84f847780a119437a0931eca469f7b44d356f2b426dd9741", size = 2053315, upload-time = "2025-10-06T21:10:34.87Z" }, + { url = "https://files.pythonhosted.org/packages/61/d9/4de44600f2d4514b44f3f3aeeda2e14931214b6b5bf52479339e801ce748/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5da98cc81873f39fd56882e1569c4677940fbc12bce6213fad1ead784192d7c8", size = 2224298, upload-time = "2025-10-06T21:10:36.233Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ae/dbe51187a7f35fc21b283c5250571a94e36373eb557c1cba9f29a9806dcf/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:209910e88afb01fd0fd403947b809ba8dba0e08a095e1f703294fda0a8fdca51", size = 2351797, upload-time = "2025-10-06T21:10:37.601Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a7/975585147457c2e9fb951c7c8dab56deeb6aa313f3aa72c2fc0df3f74a49/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365109d1165d78d98e33c5bfd815a9b5d7d070f578caefaabcc5771825b4ecb5", size = 2074921, upload-time = "2025-10-06T21:10:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/62/37/ea94d1d0c01dec1b7d236c7cec9103baab0021f42500975de3d42522104b/pydantic_core-2.41.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:706abf21e60a2857acdb09502bc853ee5bce732955e7b723b10311114f033115", size = 2187767, upload-time = "2025-10-06T21:10:40.651Z" }, + { url = "https://files.pythonhosted.org/packages/d3/fe/694cf9fdd3a777a618c3afd210dba7b414cb8a72b1bd29b199c2e5765fee/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bf0bd5417acf7f6a7ec3b53f2109f587be176cb35f9cf016da87e6017437a72d", size = 2136062, upload-time = "2025-10-06T21:10:42.09Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/174aeabd89916fbd2988cc37b81a59e1186e952afd2a7ed92018c22f31ca/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2e71b1c6ceb9c78424ae9f63a07292fb769fb890a4e7efca5554c47f33a60ea5", size = 2317819, upload-time = "2025-10-06T21:10:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/65/e8/e9aecafaebf53fc456314f72886068725d6fba66f11b013532dc21259343/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:80745b9770b4a38c25015b517451c817799bfb9d6499b0d13d8227ec941cb513", size = 2312267, upload-time = "2025-10-06T21:10:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/35/2f/1c2e71d2a052f9bb2f2df5a6a05464a0eb800f9e8d9dd800202fe31219e1/pydantic_core-2.41.1-cp312-cp312-win32.whl", hash = "sha256:83b64d70520e7890453f1aa21d66fda44e7b35f1cfea95adf7b4289a51e2b479", size = 1990927, upload-time = "2025-10-06T21:10:46.738Z" }, + { url = "https://files.pythonhosted.org/packages/b1/78/562998301ff2588b9c6dcc5cb21f52fa919d6e1decc75a35055feb973594/pydantic_core-2.41.1-cp312-cp312-win_amd64.whl", hash = "sha256:377defd66ee2003748ee93c52bcef2d14fde48fe28a0b156f88c3dbf9bc49a50", size = 2034703, upload-time = "2025-10-06T21:10:48.524Z" }, + { url = "https://files.pythonhosted.org/packages/b2/53/d95699ce5a5cdb44bb470bd818b848b9beadf51459fd4ea06667e8ede862/pydantic_core-2.41.1-cp312-cp312-win_arm64.whl", hash = "sha256:c95caff279d49c1d6cdfe2996e6c2ad712571d3b9caaa209a404426c326c4bde", size = 1972719, upload-time = "2025-10-06T21:10:50.256Z" }, + { url = "https://files.pythonhosted.org/packages/16/89/d0afad37ba25f5801735af1472e650b86baad9fe807a42076508e4824a2a/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:68f2251559b8efa99041bb63571ec7cdd2d715ba74cc82b3bc9eff824ebc8bf0", size = 2124001, upload-time = "2025-10-07T10:49:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/8e/c4/08609134b34520568ddebb084d9ed0a2a3f5f52b45739e6e22cb3a7112eb/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:c7bc140c596097cb53b30546ca257dbe3f19282283190b1b5142928e5d5d3a20", size = 1941841, upload-time = "2025-10-07T10:49:56.248Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/94a4877094e5fe19a3f37e7e817772263e2c573c94f1e3fa2b1eee56ef3b/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2896510fce8f4725ec518f8b9d7f015a00db249d2fd40788f442af303480063d", size = 1961129, upload-time = "2025-10-07T10:49:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/a2/30/23a224d7e25260eb5f69783a63667453037e07eb91ff0e62dabaadd47128/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ced20e62cfa0f496ba68fa5d6c7ee71114ea67e2a5da3114d6450d7f4683572a", size = 2148770, upload-time = "2025-10-07T10:49:59.959Z" }, + { url = "https://files.pythonhosted.org/packages/2b/3e/a51c5f5d37b9288ba30683d6e96f10fa8f1defad1623ff09f1020973b577/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b04fa9ed049461a7398138c604b00550bc89e3e1151d84b81ad6dc93e39c4c06", size = 2115344, upload-time = "2025-10-07T10:50:02.466Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/389504c9e0600ef4502cd5238396b527afe6ef8981a6a15cd1814fc7b434/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b3b7d9cfbfdc43c80a16638c6dc2768e3956e73031fca64e8e1a3ae744d1faeb", size = 1927994, upload-time = "2025-10-07T10:50:04.379Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9c/5111c6b128861cb792a4c082677e90dac4f2e090bb2e2fe06aa5b2d39027/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eec83fc6abef04c7f9bec616e2d76ee9a6a4ae2a359b10c21d0f680e24a247ca", size = 1959394, upload-time = "2025-10-07T10:50:06.335Z" }, + { url = "https://files.pythonhosted.org/packages/14/3f/cfec8b9a0c48ce5d64409ec5e1903cb0b7363da38f14b41de2fcb3712700/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6771a2d9f83c4038dfad5970a3eef215940682b2175e32bcc817bdc639019b28", size = 2147365, upload-time = "2025-10-07T10:50:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/f403d7ca8352e3e4df352ccacd200f5f7f7fe81cef8e458515f015091625/pydantic_core-2.41.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fabcbdb12de6eada8d6e9a759097adb3c15440fafc675b3e94ae5c9cb8d678a0", size = 2114268, upload-time = "2025-10-07T10:50:10.257Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b5/334473b6d2810df84db67f03d4f666acacfc538512c2d2a254074fee0889/pydantic_core-2.41.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:80e97ccfaf0aaf67d55de5085b0ed0d994f57747d9d03f2de5cc9847ca737b08", size = 1935786, upload-time = "2025-10-07T10:50:12.333Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5e/45513e4dc621f47397cfa5fef12ba8fa5e8b1c4c07f2ff2a5fef8ff81b25/pydantic_core-2.41.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34df1fe8fea5d332484a763702e8b6a54048a9d4fe6ccf41e34a128238e01f52", size = 1971995, upload-time = "2025-10-07T10:50:14.071Z" }, + { url = "https://files.pythonhosted.org/packages/22/e3/f1797c168e5f52b973bed1c585e99827a22d5e579d1ed57d51bc15b14633/pydantic_core-2.41.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:421b5595f845842fc093f7250e24ee395f54ca62d494fdde96f43ecf9228ae01", size = 2191264, upload-time = "2025-10-07T10:50:15.788Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e1/24ef4c3b4ab91c21c3a09a966c7d2cffe101058a7bfe5cc8b2c7c7d574e2/pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dce8b22663c134583aaad24827863306a933f576c79da450be3984924e2031d1", size = 2152430, upload-time = "2025-10-07T10:50:18.018Z" }, + { url = "https://files.pythonhosted.org/packages/35/74/70c1e225d67f7ef3fdba02c506d9011efaf734020914920b2aa3d1a45e61/pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:300a9c162fea9906cc5c103893ca2602afd84f0ec90d3be36f4cc360125d22e1", size = 2324691, upload-time = "2025-10-07T10:50:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bf/dd4d21037c8bef0d8cce90a86a3f2dcb011c30086db2a10113c3eea23eba/pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e019167628f6e6161ae7ab9fb70f6d076a0bf0d55aa9b20833f86a320c70dd65", size = 2324493, upload-time = "2025-10-07T10:50:21.568Z" }, + { url = "https://files.pythonhosted.org/packages/7e/78/3093b334e9c9796c8236a4701cd2ddef1c56fb0928fe282a10c797644380/pydantic_core-2.41.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:13ab9cc2de6f9d4ab645a050ae5aee61a2424ac4d3a16ba23d4c2027705e0301", size = 2146156, upload-time = "2025-10-07T10:50:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6c/fa3e45c2b054a1e627a89a364917f12cbe3abc3e91b9004edaae16e7b3c5/pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:af2385d3f98243fb733862f806c5bb9122e5fba05b373e3af40e3c82d711cef1", size = 2112094, upload-time = "2025-10-07T10:50:25.513Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/7eebc38b4658cc8e6902d0befc26388e4c2a5f2e179c561eeb43e1922c7b/pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6550617a0c2115be56f90c31a5370261d8ce9dbf051c3ed53b51172dd34da696", size = 1935300, upload-time = "2025-10-07T10:50:27.715Z" }, + { url = "https://files.pythonhosted.org/packages/2b/00/9fe640194a1717a464ab861d43595c268830f98cb1e2705aa134b3544b70/pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc17b6ecf4983d298686014c92ebc955a9f9baf9f57dad4065e7906e7bee6222", size = 1970417, upload-time = "2025-10-07T10:50:29.573Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ad/f4cdfaf483b78ee65362363e73b6b40c48e067078d7b146e8816d5945ad6/pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42ae9352cf211f08b04ea110563d6b1e415878eea5b4c70f6bdb17dca3b932d2", size = 2190745, upload-time = "2025-10-07T10:50:31.48Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c1/18f416d40a10f44e9387497ba449f40fdb1478c61ba05c4b6bdb82300362/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e82947de92068b0a21681a13dd2102387197092fbe7defcfb8453e0913866506", size = 2150888, upload-time = "2025-10-07T10:50:33.477Z" }, + { url = "https://files.pythonhosted.org/packages/42/30/134c8a921630d8a88d6f905a562495a6421e959a23c19b0f49b660801d67/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e244c37d5471c9acdcd282890c6c4c83747b77238bfa19429b8473586c907656", size = 2324489, upload-time = "2025-10-07T10:50:36.48Z" }, + { url = "https://files.pythonhosted.org/packages/9c/48/a9263aeaebdec81e941198525b43edb3b44f27cfa4cb8005b8d3eb8dec72/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1e798b4b304a995110d41ec93653e57975620ccb2842ba9420037985e7d7284e", size = 2322763, upload-time = "2025-10-07T10:50:38.751Z" }, + { url = "https://files.pythonhosted.org/packages/1d/62/755d2bd2593f701c5839fc084e9c2c5e2418f460383ad04e3b5d0befc3ca/pydantic_core-2.41.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f1fc716c0eb1663c59699b024428ad5ec2bcc6b928527b8fe28de6cb89f47efb", size = 2144046, upload-time = "2025-10-07T10:50:40.686Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pynndescent" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "llvmlite" }, + { name = "numba" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/fb/7f58c397fb31666756457ee2ac4c0289ef2daad57f4ae4be8dec12f80b03/pynndescent-0.6.0.tar.gz", hash = "sha256:7ffde0fb5b400741e055a9f7d377e3702e02250616834231f6c209e39aac24f5", size = 2992987, upload-time = "2026-01-08T21:29:58.943Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/e6/94145d714402fd5ade00b5661f2d0ab981219e07f7db9bfa16786cdb9c04/pynndescent-0.6.0-py3-none-any.whl", hash = "sha256:dc8c74844e4c7f5cbd1e0cd6909da86fdc789e6ff4997336e344779c3d5538ef", size = 73511, upload-time = "2026-01-08T21:29:57.306Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[package]] +name = "pytorch-wpe" +version = "0.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/39/8d94737fd6fab4028687575099566a125100f3ba8c638f861506747d7b7c/pytorch_wpe-0.0.1.tar.gz", hash = "sha256:fc7e706b5411800c4483fe94db7dcd82ecf6c57bc013af529ab4fb675c9cc29c", size = 4457, upload-time = "2021-03-05T10:10:09.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/de/c47967a11bfe68cb28d2f19e55c7027993c3721eba79813db65d245e4ced/pytorch_wpe-0.0.1-py3-none-any.whl", hash = "sha256:fa0dc9f818fba81b36c1a51a53331cf6ed975f29b33f23e07b0deb4bee82eaad", size = 8080, upload-time = "2021-03-05T10:10:08.686Z" }, +] + +[[package]] +name = "pytz" +version = "2026.1.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, +] + +[[package]] +name = "qwen3-asr-cpu-env" +version = "1.0.0" +source = { virtual = "." } +dependencies = [ + { name = "addict" }, + { name = "datasets" }, + { name = "fastapi" }, + { name = "fastapi-offline" }, + { name = "funasr" }, + { name = "hdbscan" }, + { name = "huggingface-hub" }, + { name = "itntext" }, + { name = "librosa" }, + { name = "loguru" }, + { name = "modelscope", extra = ["framework"] }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "python-multipart" }, + { name = "requests" }, + { name = "rich" }, + { name = "scipy" }, + { name = "setuptools" }, + { name = "soundfile" }, + { name = "torch", version = "2.3.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'" }, + { name = "torch", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, + { name = "torch", version = "2.3.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_python_implementation != 'CPython' and sys_platform == 'linux')" }, + { name = "torchaudio", version = "2.3.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'" }, + { name = "torchaudio", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, + { name = "torchaudio", version = "2.3.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_python_implementation != 'CPython' and sys_platform == 'linux')" }, + { name = "transformers" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "websockets" }, +] + +[package.metadata] +requires-dist = [ + { name = "addict", specifier = "==2.4.0" }, + { name = "datasets", specifier = "==3.6.0" }, + { name = "fastapi", specifier = "==0.128.0" }, + { name = "fastapi-offline", specifier = "==1.7.6" }, + { name = "funasr", specifier = "==1.3.1" }, + { name = "hdbscan", specifier = "==0.8.41" }, + { name = "huggingface-hub", specifier = "==0.34.0" }, + { name = "itntext", specifier = "==0.1.5" }, + { name = "librosa", specifier = "==0.11.0" }, + { name = "loguru", specifier = "==0.7.2" }, + { name = "modelscope", extras = ["framework"], specifier = "==1.34.0" }, + { name = "pydantic", specifier = "==2.12.0" }, + { name = "python-dotenv", specifier = "==1.2.1" }, + { name = "python-multipart", specifier = "==0.0.22" }, + { name = "requests", specifier = "==2.32.5" }, + { name = "rich", specifier = ">=13.9,<14" }, + { name = "scipy", specifier = "==1.15.3" }, + { name = "setuptools", specifier = ">=70.0.0,<81" }, + { name = "soundfile", specifier = "==0.13.1" }, + { name = "torch", marker = "sys_platform == 'darwin'", specifier = "==2.3.1" }, + { name = "torch", marker = "sys_platform == 'linux'", specifier = "==2.3.1", index = "https://download.pytorch.org/whl/cpu" }, + { name = "torchaudio", marker = "sys_platform == 'darwin'", specifier = "==2.3.1" }, + { name = "torchaudio", marker = "sys_platform == 'linux'", specifier = "==2.3.1", index = "https://download.pytorch.org/whl/cpu" }, + { name = "transformers", specifier = ">=4.45,<4.50" }, + { name = "uvicorn", extras = ["standard"], specifier = "==0.40.0" }, + { name = "websockets", specifier = "==16.0" }, +] + +[[package]] +name = "regex" +version = "2026.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/3a246dbf05666918bd3664d9d787f84a9108f6f43cc953a077e4a7dfdb7e/regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423", size = 416000, upload-time = "2026-04-03T20:56:28.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/59/fd98f8fd54b3feaa76a855324c676c17668c5a1121ec91b7ec96b01bf865/regex-2026.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:74fa82dcc8143386c7c0392e18032009d1db715c25f4ba22d23dc2e04d02a20f", size = 489403, upload-time = "2026-04-03T20:52:39.742Z" }, + { url = "https://files.pythonhosted.org/packages/6c/64/d0f222f68e3579d50babf0e4fcc9c9639ef0587fecc00b15e1e46bfc32fa/regex-2026.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a85b620a388d6c9caa12189233109e236b3da3deffe4ff11b84ae84e218a274f", size = 291208, upload-time = "2026-04-03T20:52:42.943Z" }, + { url = "https://files.pythonhosted.org/packages/16/7f/3fab9709b0b0060ba81a04b8a107b34147cd14b9c5551b772154d6505504/regex-2026.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2895506ebe32cc63eeed8f80e6eae453171cfccccab35b70dc3129abec35a5b8", size = 289214, upload-time = "2026-04-03T20:52:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/14/bc/f5dcf04fd462139dcd75495c02eee22032ef741cfa151386a39c3f5fc9b5/regex-2026.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6780f008ee81381c737634e75c24e5a6569cc883c4f8e37a37917ee79efcafd9", size = 785505, upload-time = "2026-04-03T20:52:46.35Z" }, + { url = "https://files.pythonhosted.org/packages/37/36/8a906e216d5b4de7ec3788c1d589b45db40c1c9580cd7b326835cfc976d4/regex-2026.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:88e9b048345c613f253bea4645b2fe7e579782b82cac99b1daad81e29cc2ed8e", size = 852129, upload-time = "2026-04-03T20:52:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/a5/bb/bad2d79be0917a6ef31f5e0f161d9265cb56fd90a3ae1d2e8d991882a48b/regex-2026.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:be061028481186ba62a0f4c5f1cc1e3d5ab8bce70c89236ebe01023883bc903b", size = 899578, upload-time = "2026-04-03T20:52:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b9/7cd0ceb58cd99c70806241636640ae15b4a3fe62e22e9b99afa67a0d7965/regex-2026.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2228c02b368d69b724c36e96d3d1da721561fb9cc7faa373d7bf65e07d75cb5", size = 793634, upload-time = "2026-04-03T20:52:53Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fb/c58e3ea40ed183806ccbac05c29a3e8c2f88c1d3a66ed27860d5cad7c62d/regex-2026.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0540e5b733618a2f84e9cb3e812c8afa82e151ca8e19cf6c4e95c5a65198236f", size = 786210, upload-time = "2026-04-03T20:52:54.713Z" }, + { url = "https://files.pythonhosted.org/packages/54/a9/53790fc7a6c948a7be2bc7214fd9cabdd0d1ba561b0f401c91f4ff0357f0/regex-2026.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf9b1b2e692d4877880388934ac746c99552ce6bf40792a767fd42c8c99f136d", size = 769930, upload-time = "2026-04-03T20:52:56.825Z" }, + { url = "https://files.pythonhosted.org/packages/e3/3c/29ca44729191c79f5476538cd0fa04fa2553b3c45508519ecea4c7afa8f6/regex-2026.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:011bb48bffc1b46553ac704c975b3348717f4e4aa7a67522b51906f99da1820c", size = 774892, upload-time = "2026-04-03T20:52:58.934Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/6ae74ef8a4cfead341c367e4eed45f71fb1aaba35827a775eed4f1ba4f74/regex-2026.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8512fcdb43f1bf18582698a478b5ab73f9c1667a5b7548761329ef410cd0a760", size = 848816, upload-time = "2026-04-03T20:53:00.684Z" }, + { url = "https://files.pythonhosted.org/packages/53/9a/f7f2c1c6b610d7c6de1c3dc5951effd92c324b1fde761af2044b4721020f/regex-2026.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:867bddc63109a0276f5a31999e4c8e0eb7bbbad7d6166e28d969a2c1afeb97f9", size = 758363, upload-time = "2026-04-03T20:53:02.155Z" }, + { url = "https://files.pythonhosted.org/packages/dd/55/e5386d393bbf8b43c8b084703a46d635e7b2bdc6e0f5909a2619ea1125f1/regex-2026.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1b9a00b83f3a40e09859c78920571dcb83293c8004079653dd22ec14bbfa98c7", size = 837122, upload-time = "2026-04-03T20:53:03.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/da/cc78710ea2e60b10bacfcc9beb18c67514200ab03597b3b2b319995785c2/regex-2026.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e355be718caf838aa089870259cf1776dc2a4aa980514af9d02c59544d9a8b22", size = 782140, upload-time = "2026-04-03T20:53:05.608Z" }, + { url = "https://files.pythonhosted.org/packages/a2/5f/c7bcba41529105d6c2ca7080ecab7184cd00bee2e1ad1fdea80e618704ea/regex-2026.4.4-cp310-cp310-win32.whl", hash = "sha256:33bfda9684646d323414df7abe5692c61d297dbb0530b28ec66442e768813c59", size = 266225, upload-time = "2026-04-03T20:53:07.342Z" }, + { url = "https://files.pythonhosted.org/packages/eb/26/a745729c2c49354ec4f4bce168f29da932ca01b4758227686cc16c7dde1b/regex-2026.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:0709f22a56798457ae317bcce42aacee33c680068a8f14097430d9f9ba364bee", size = 278393, upload-time = "2026-04-03T20:53:08.65Z" }, + { url = "https://files.pythonhosted.org/packages/87/8b/4327eeb9dbb4b098ebecaf02e9f82b79b6077beeb54c43d9a0660cf7c44c/regex-2026.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:ee9627de8587c1a22201cb16d0296ab92b4df5cdcb5349f4e9744d61db7c7c98", size = 270470, upload-time = "2026-04-03T20:53:10.018Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7a/617356cbecdb452812a5d42f720d6d5096b360d4a4c1073af700ea140ad2/regex-2026.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4c36a85b00fadb85db9d9e90144af0a980e1a3d2ef9cd0f8a5bef88054657c6", size = 489415, upload-time = "2026-04-03T20:53:11.645Z" }, + { url = "https://files.pythonhosted.org/packages/20/e6/bf057227144d02e3ba758b66649e87531d744dda5f3254f48660f18ae9d8/regex-2026.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dcb5453ecf9cd58b562967badd1edbf092b0588a3af9e32ee3d05c985077ce87", size = 291205, upload-time = "2026-04-03T20:53:13.289Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3b/637181b787dd1a820ba1c712cee2b4144cd84a32dc776ca067b12b2d70c8/regex-2026.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6aa809ed4dc3706cc38594d67e641601bd2f36d5555b2780ff074edfcb136cf8", size = 289225, upload-time = "2026-04-03T20:53:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/05/21/bac05d806ed02cd4b39d9c8e5b5f9a2998c94c3a351b7792e80671fa5315/regex-2026.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33424f5188a7db12958246a54f59a435b6cb62c5cf9c8d71f7cc49475a5fdada", size = 792434, upload-time = "2026-04-03T20:53:17.414Z" }, + { url = "https://files.pythonhosted.org/packages/d9/17/c65d1d8ae90b772d5758eb4014e1e011bb2db353fc4455432e6cc9100df7/regex-2026.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d346fccdde28abba117cc9edc696b9518c3307fbfcb689e549d9b5979018c6d", size = 861730, upload-time = "2026-04-03T20:53:18.903Z" }, + { url = "https://files.pythonhosted.org/packages/ad/64/933321aa082a2c6ee2785f22776143ba89840189c20d3b6b1d12b6aae16b/regex-2026.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:415a994b536440f5011aa77e50a4274d15da3245e876e5c7f19da349caaedd87", size = 906495, upload-time = "2026-04-03T20:53:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/01/ea/4c8d306e9c36ac22417336b1e02e7b358152c34dc379673f2d331143725f/regex-2026.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21e5eb86179b4c67b5759d452ea7c48eb135cd93308e7a260aa489ed2eb423a4", size = 799810, upload-time = "2026-04-03T20:53:22.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/ce/7605048f00e1379eba89d610c7d644d8f695dc9b26d3b6ecfa3132b872ff/regex-2026.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:312ec9dd1ae7d96abd8c5a36a552b2139931914407d26fba723f9e53c8186f86", size = 774242, upload-time = "2026-04-03T20:53:25.015Z" }, + { url = "https://files.pythonhosted.org/packages/e9/77/283e0d5023fde22cd9e86190d6d9beb21590a452b195ffe00274de470691/regex-2026.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0d2b28aa1354c7cd7f71b7658c4326f7facac106edd7f40eda984424229fd59", size = 781257, upload-time = "2026-04-03T20:53:26.918Z" }, + { url = "https://files.pythonhosted.org/packages/8b/fb/7f3b772be101373c8626ed34c5d727dcbb8abd42a7b1219bc25fd9a3cc04/regex-2026.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:349d7310eddff40429a099c08d995c6d4a4bfaf3ff40bd3b5e5cb5a5a3c7d453", size = 854490, upload-time = "2026-04-03T20:53:29.065Z" }, + { url = "https://files.pythonhosted.org/packages/85/30/56547b80f34f4dd2986e1cdd63b1712932f63b6c4ce2f79c50a6cd79d1c2/regex-2026.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e7ab63e9fe45a9ec3417509e18116b367e89c9ceb6219222a3396fa30b147f80", size = 763544, upload-time = "2026-04-03T20:53:30.917Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2f/ce060fdfea8eff34a8997603532e44cdb7d1f35e3bc253612a8707a90538/regex-2026.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fe896e07a5a2462308297e515c0054e9ec2dd18dfdc9427b19900b37dfe6f40b", size = 844442, upload-time = "2026-04-03T20:53:32.463Z" }, + { url = "https://files.pythonhosted.org/packages/e5/44/810cb113096a1dacbe82789fbfab2823f79d19b7f1271acecb7009ba9b88/regex-2026.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb59c65069498dbae3c0ef07bbe224e1eaa079825a437fb47a479f0af11f774f", size = 789162, upload-time = "2026-04-03T20:53:34.039Z" }, + { url = "https://files.pythonhosted.org/packages/20/96/9647dd7f2ecf6d9ce1fb04dfdb66910d094e10d8fe53e9c15096d8aa0bd2/regex-2026.4.4-cp311-cp311-win32.whl", hash = "sha256:2a5d273181b560ef8397c8825f2b9d57013de744da9e8257b8467e5da8599351", size = 266227, upload-time = "2026-04-03T20:53:35.601Z" }, + { url = "https://files.pythonhosted.org/packages/33/80/74e13262460530c3097ff343a17de9a34d040a5dc4de9cf3a8241faab51c/regex-2026.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:9542ccc1e689e752594309444081582f7be2fdb2df75acafea8a075108566735", size = 278399, upload-time = "2026-04-03T20:53:37.021Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/39f19f47f19dcefa3403f09d13562ca1c0fd07ab54db2bc03148f3f6b46a/regex-2026.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:b5f9fb784824a042be3455b53d0b112655686fdb7a91f88f095f3fee1e2a2a54", size = 270473, upload-time = "2026-04-03T20:53:38.633Z" }, + { url = "https://files.pythonhosted.org/packages/e5/28/b972a4d3df61e1d7bcf1b59fdb3cddef22f88b6be43f161bb41ebc0e4081/regex-2026.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c07ab8794fa929e58d97a0e1796b8b76f70943fa39df225ac9964615cf1f9d52", size = 490434, upload-time = "2026-04-03T20:53:40.219Z" }, + { url = "https://files.pythonhosted.org/packages/84/20/30041446cf6dc3e0eab344fc62770e84c23b6b68a3b657821f9f80cb69b4/regex-2026.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c785939dc023a1ce4ec09599c032cc9933d258a998d16ca6f2b596c010940eb", size = 292061, upload-time = "2026-04-03T20:53:41.862Z" }, + { url = "https://files.pythonhosted.org/packages/62/c8/3baa06d75c98c46d4cc4262b71fd2edb9062b5665e868bca57859dadf93a/regex-2026.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b1ce5c81c9114f1ce2f9288a51a8fd3aeea33a0cc440c415bf02da323aa0a76", size = 289628, upload-time = "2026-04-03T20:53:43.701Z" }, + { url = "https://files.pythonhosted.org/packages/31/87/3accf55634caad8c0acab23f5135ef7d4a21c39f28c55c816ae012931408/regex-2026.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ef21c17d8e6a4fe8cf406a97cf2806a4df93416ccc82fc98d25b1c20425be", size = 796651, upload-time = "2026-04-03T20:53:45.379Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0c/aaa2c83f34efedbf06f61cb1942c25f6cf1ee3b200f832c4d05f28306c2e/regex-2026.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7088fcdcb604a4417c208e2169715800d28838fefd7455fbe40416231d1d47c1", size = 865916, upload-time = "2026-04-03T20:53:47.064Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f6/8c6924c865124643e8f37823eca845dc27ac509b2ee58123685e71cd0279/regex-2026.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07edca1ba687998968f7db5bc355288d0c6505caa7374f013d27356d93976d13", size = 912287, upload-time = "2026-04-03T20:53:49.422Z" }, + { url = "https://files.pythonhosted.org/packages/11/0e/a9f6f81013e0deaf559b25711623864970fe6a098314e374ccb1540a4152/regex-2026.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f657a7c1c6ec51b5e0ba97c9817d06b84ea5fa8d82e43b9405de0defdc2b9", size = 801126, upload-time = "2026-04-03T20:53:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/71/61/3a0cc8af2dc0c8deb48e644dd2521f173f7e6513c6e195aad9aa8dd77ac5/regex-2026.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b69102a743e7569ebee67e634a69c4cb7e59d6fa2e1aa7d3bdbf3f61435f62d", size = 776788, upload-time = "2026-04-03T20:53:52.889Z" }, + { url = "https://files.pythonhosted.org/packages/64/0b/8bb9cbf21ef7dee58e49b0fdb066a7aded146c823202e16494a36777594f/regex-2026.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dac006c8b6dda72d86ea3d1333d45147de79a3a3f26f10c1cf9287ca4ca0ac3", size = 785184, upload-time = "2026-04-03T20:53:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/99/c2/d3e80e8137b25ee06c92627de4e4d98b94830e02b3e6f81f3d2e3f504cf5/regex-2026.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:50a766ee2010d504554bfb5f578ed2e066898aa26411d57e6296230627cdefa0", size = 859913, upload-time = "2026-04-03T20:53:57.249Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/9d5d876157d969c804622456ef250017ac7a8f83e0e14f903b9e6df5ce95/regex-2026.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9e2f5217648f68e3028c823df58663587c1507a5ba8419f4fdfc8a461be76043", size = 765732, upload-time = "2026-04-03T20:53:59.428Z" }, + { url = "https://files.pythonhosted.org/packages/82/80/b568935b4421388561c8ed42aff77247285d3ae3bb2a6ca22af63bae805e/regex-2026.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39d8de85a08e32632974151ba59c6e9140646dcc36c80423962b1c5c0a92e244", size = 852152, upload-time = "2026-04-03T20:54:01.505Z" }, + { url = "https://files.pythonhosted.org/packages/39/29/f0f81217e21cd998245da047405366385d5c6072048038a3d33b37a79dc0/regex-2026.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55d9304e0e7178dfb1e106c33edf834097ddf4a890e2f676f6c5118f84390f73", size = 789076, upload-time = "2026-04-03T20:54:03.323Z" }, + { url = "https://files.pythonhosted.org/packages/49/1d/1d957a61976ab9d4e767dd4f9d04b66cc0c41c5e36cf40e2d43688b5ae6f/regex-2026.4.4-cp312-cp312-win32.whl", hash = "sha256:04bb679bc0bde8a7bfb71e991493d47314e7b98380b083df2447cda4b6edb60f", size = 266700, upload-time = "2026-04-03T20:54:05.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/bf575d396aeb58ea13b06ef2adf624f65b70fafef6950a80fc3da9cae3bc/regex-2026.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:db0ac18435a40a2543dbb3d21e161a6c78e33e8159bd2e009343d224bb03bb1b", size = 277768, upload-time = "2026-04-03T20:54:07.312Z" }, + { url = "https://files.pythonhosted.org/packages/c9/27/049df16ec6a6828ccd72add3c7f54b4df029669bea8e9817df6fff58be90/regex-2026.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:4ce255cc05c1947a12989c6db801c96461947adb7a59990f1360b5983fab4983", size = 270568, upload-time = "2026-04-03T20:54:09.484Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'linux')", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux'", +] +dependencies = [ + { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", marker = "python_full_version < '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" }, + { url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" }, + { url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" }, + { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" }, + { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" }, + { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" }, + { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" }, + { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'linux')", + "python_full_version >= '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'", + "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.11' and sys_platform == 'win32'", + "python_full_version >= '3.11' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", +] +dependencies = [ + { name = "joblib", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", marker = "python_full_version >= '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, +] + +[[package]] +name = "sentencepiece" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/15/2e7a025fc62d764b151ae6d0f2a92f8081755ebe8d4a64099accc6f77ba6/sentencepiece-0.2.1.tar.gz", hash = "sha256:8138cec27c2f2282f4a34d9a016e3374cd40e5c6e9cb335063db66a0a3b71fad", size = 3228515, upload-time = "2025-08-12T07:00:51.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/31/5b7cccb307b485db1a2372d6d2980b0a65d067f8be5ca943a103b4acd5b3/sentencepiece-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e10fa50bdbaa5e2445dbd387979980d391760faf0ec99a09bd7780ff37eaec44", size = 1942557, upload-time = "2025-08-12T06:59:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/1f/41/0ac923a8e685ad290c5afc8ae55c5844977b8d75076fcc04302b9a324274/sentencepiece-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f27ae6deea72efdb6f361750c92f6c21fd0ad087445082770cc34015213c526", size = 1325384, upload-time = "2025-08-12T06:59:14.334Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ef/3751555d67daf9003384978f169d31c775cb5c7baf28633caaf1eb2b2b4d/sentencepiece-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:60937c959e6f44159fdd9f56fbdd302501f96114a5ba436829496d5f32d8de3f", size = 1253317, upload-time = "2025-08-12T06:59:16.247Z" }, + { url = "https://files.pythonhosted.org/packages/46/a5/742c69b7bd144eb32b6e5fd50dbd8abbbc7a95fce2fe16e50156fa400e3b/sentencepiece-0.2.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8b1d91545578852f128650b8cce4ec20f93d39b378ff554ebe66290f2dabb92", size = 1316379, upload-time = "2025-08-12T06:59:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/8deeafbba2871e8fa10f20f17447786f4ac38085925335728d360eaf4cae/sentencepiece-0.2.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27e38eee653abc3d387862e67bc5c8b6f428cd604e688b85d29170b7e725c26c", size = 1387926, upload-time = "2025-08-12T06:59:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ca/67fe73005f0ab617c6a970b199754e28e524b6873aa7025224fad3cda252/sentencepiece-0.2.1-cp310-cp310-win32.whl", hash = "sha256:251874d720ac7f28024a168501f3c7bb15d1802245f6e66de565f18bbb9b5eaa", size = 999550, upload-time = "2025-08-12T06:59:20.844Z" }, + { url = "https://files.pythonhosted.org/packages/6d/33/dc5b54042050d2dda4229c3ce1f862541c99966390b6aa20f54d520d2dc2/sentencepiece-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:e52144670738b4b477fade6c2a9b6af71a8d0094514c9853ac9f6fc1fcfabae7", size = 1054613, upload-time = "2025-08-12T06:59:22.255Z" }, + { url = "https://files.pythonhosted.org/packages/fa/19/1ea47f46ff97fe04422b78997da1a37cd632f414aae042d27a9009c5b733/sentencepiece-0.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:9076430ac25dfa7147d9d05751dbc66a04bc1aaac371c07f84952979ea59f0d0", size = 1033884, upload-time = "2025-08-12T06:59:24.194Z" }, + { url = "https://files.pythonhosted.org/packages/d8/15/46afbab00733d81788b64be430ca1b93011bb9388527958e26cc31832de5/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6356d0986b8b8dc351b943150fcd81a1c6e6e4d439772e8584c64230e58ca987", size = 1942560, upload-time = "2025-08-12T06:59:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/fa/79/7c01b8ef98a0567e9d84a4e7a910f8e7074fcbf398a5cd76f93f4b9316f9/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f8ba89a3acb3dc1ae90f65ec1894b0b9596fdb98ab003ff38e058f898b39bc7", size = 1325385, upload-time = "2025-08-12T06:59:27.722Z" }, + { url = "https://files.pythonhosted.org/packages/bb/88/2b41e07bd24f33dcf2f18ec3b74247aa4af3526bad8907b8727ea3caba03/sentencepiece-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02593eca45440ef39247cee8c47322a34bdcc1d8ae83ad28ba5a899a2cf8d79a", size = 1253319, upload-time = "2025-08-12T06:59:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a0/54/38a1af0c6210a3c6f95aa46d23d6640636d020fba7135cd0d9a84ada05a7/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a0d15781a171d188b661ae4bde1d998c303f6bd8621498c50c671bd45a4798e", size = 1316162, upload-time = "2025-08-12T06:59:30.914Z" }, + { url = "https://files.pythonhosted.org/packages/ef/66/fb191403ade791ad2c3c1e72fe8413e63781b08cfa3aa4c9dfc536d6e795/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f5a3e0d9f445ed9d66c0fec47d4b23d12cfc858b407a03c194c1b26c2ac2a63", size = 1387785, upload-time = "2025-08-12T06:59:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2d/3bd9b08e70067b2124518b308db6a84a4f8901cc8a4317e2e4288cdd9b4d/sentencepiece-0.2.1-cp311-cp311-win32.whl", hash = "sha256:6d297a1748d429ba8534eebe5535448d78b8acc32d00a29b49acf28102eeb094", size = 999555, upload-time = "2025-08-12T06:59:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/32/b8/f709977f5fda195ae1ea24f24e7c581163b6f142b1005bc3d0bbfe4d7082/sentencepiece-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:82d9ead6591015f009cb1be1cb1c015d5e6f04046dbb8c9588b931e869a29728", size = 1054617, upload-time = "2025-08-12T06:59:36.461Z" }, + { url = "https://files.pythonhosted.org/packages/7a/40/a1fc23be23067da0f703709797b464e8a30a1c78cc8a687120cd58d4d509/sentencepiece-0.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:39f8651bd10974eafb9834ce30d9bcf5b73e1fc798a7f7d2528f9820ca86e119", size = 1033877, upload-time = "2025-08-12T06:59:38.391Z" }, + { url = "https://files.pythonhosted.org/packages/4a/be/32ce495aa1d0e0c323dcb1ba87096037358edee539cac5baf8755a6bd396/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57cae326c8727de58c85977b175af132a7138d84c764635d7e71bbee7e774133", size = 1943152, upload-time = "2025-08-12T06:59:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/88/7e/ff23008899a58678e98c6ff592bf4d368eee5a71af96d0df6b38a039dd4f/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56dd39a3c4d6493db3cdca7e8cc68c6b633f0d4195495cbadfcf5af8a22d05a6", size = 1325651, upload-time = "2025-08-12T06:59:41.536Z" }, + { url = "https://files.pythonhosted.org/packages/19/84/42eb3ce4796777a1b5d3699dfd4dca85113e68b637f194a6c8d786f16a04/sentencepiece-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9381351182ff9888cc80e41c632e7e274b106f450de33d67a9e8f6043da6f76", size = 1253645, upload-time = "2025-08-12T06:59:42.903Z" }, + { url = "https://files.pythonhosted.org/packages/89/fa/d3d5ebcba3cb9e6d3775a096251860c41a6bc53a1b9461151df83fe93255/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f955df238021bf11f0fc37cdb54fd5e5b5f7fd30ecc3d93fb48b6815437167", size = 1316273, upload-time = "2025-08-12T06:59:44.476Z" }, + { url = "https://files.pythonhosted.org/packages/04/88/14f2f4a2b922d8b39be45bf63d79e6cd3a9b2f248b2fcb98a69b12af12f5/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdfecef430d985f1c2bcbfff3defd1d95dae876fbd0173376012d2d7d24044b", size = 1387881, upload-time = "2025-08-12T06:59:46.09Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b8/903e5ccb77b4ef140605d5d71b4f9e0ad95d456d6184688073ed11712809/sentencepiece-0.2.1-cp312-cp312-win32.whl", hash = "sha256:a483fd29a34c3e34c39ac5556b0a90942bec253d260235729e50976f5dba1068", size = 999540, upload-time = "2025-08-12T06:59:48.023Z" }, + { url = "https://files.pythonhosted.org/packages/2d/81/92df5673c067148c2545b1bfe49adfd775bcc3a169a047f5a0e6575ddaca/sentencepiece-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4cdc7c36234fda305e85c32949c5211faaf8dd886096c7cea289ddc12a2d02de", size = 1054671, upload-time = "2025-08-12T06:59:49.895Z" }, + { url = "https://files.pythonhosted.org/packages/fe/02/c5e3bc518655d714622bec87d83db9cdba1cd0619a4a04e2109751c4f47f/sentencepiece-0.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:daeb5e9e9fcad012324807856113708614d534f596d5008638eb9b40112cd9e4", size = 1033923, upload-time = "2025-08-12T06:59:51.952Z" }, +] + +[[package]] +name = "setuptools" +version = "80.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, +] + +[[package]] +name = "simplejson" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/91/b0e7a38d63706dde006d1213f9c394ad7702df841c019fb4cf0e3295c58c/simplejson-4.0.1.tar.gz", hash = "sha256:bc13170567a5c856a0e6c16620c0b0388722f7d6382acd8007857624c3dedf3e", size = 115959, upload-time = "2026-04-18T22:46:17.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/65/461422d2f596f2de331c5fbc671135ff863313592ac59f2590bcb2f8dabf/simplejson-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1dcbdf164019b6cdaf043100b44b394a0e79287d3651b7553bd2fc456dc7557b", size = 110085, upload-time = "2026-04-18T22:44:10.161Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/c98bbe63ecac3effd5af83e77438c65aba8f1c6eabe5ada4d157577bb04b/simplejson-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5145b49d2f0fdb81c1229c84001176979662731c6e18a5255a6735941aa25cfd", size = 89650, upload-time = "2026-04-18T22:44:11.3Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c6/88108bb6b080058dcfe1e46eaee2586986fb063772bba3e820782b810957/simplejson-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:35a08c66326400e6b47f91c6bb1a6ef879131b0ca64a56141bea2359d44dad59", size = 90189, upload-time = "2026-04-18T22:44:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/40/23/d25e9ec5aa9f51b8363d2d643d81acd2648440d3d36451ce2fd4545f4d85/simplejson-4.0.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1e6374020ffcd9589d7b1b9c693df24dba3c2c2bcd6da590ac2a4995c7057df5", size = 165507, upload-time = "2026-04-18T22:44:13.998Z" }, + { url = "https://files.pythonhosted.org/packages/e7/64/b4273bfef76e405567fa49307e0490c8c1034783fd4270111ce1207a2236/simplejson-4.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6cf43a90797bca6af4306745f8b0bc9493377dda0f52e1f3ecd393b73078491e", size = 164871, upload-time = "2026-04-18T22:44:15.579Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5a/5cdad85bf0a58ec8d2ced5390bf48f01d3c9e703342658459dc918be2d71/simplejson-4.0.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35c3436e48ae4c983d1910996fed585b8aca68f20eea12d71f975067a905f061", size = 173120, upload-time = "2026-04-18T22:44:16.806Z" }, + { url = "https://files.pythonhosted.org/packages/55/65/86d8b277b14bd765d71e2b9ac6fccc926e65d756947c4ae7dbb20a215019/simplejson-4.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:82284dd4adbdba0b0147860b62bdeed8003aff1b700366e3097213394ee40664", size = 163497, upload-time = "2026-04-18T22:44:18.184Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ed/e30df26965e918ffdbd1cd8fce01f44e6897e601bf1e18085d97fead713a/simplejson-4.0.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5f44a93c117ea38feb2d773289d2900cbafadecc3c2db3c526d0bc9cbaf5f27b", size = 170619, upload-time = "2026-04-18T22:44:19.556Z" }, + { url = "https://files.pythonhosted.org/packages/45/3c/2e99ac18f017b6afec4026e4921afe9a10f2cece4f650673c08e3fd23c1b/simplejson-4.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:48274b6bdc34740f6e309ed6c684e4ac6b7992d00f63a5cad94b384072265f6e", size = 164182, upload-time = "2026-04-18T22:44:21.098Z" }, + { url = "https://files.pythonhosted.org/packages/ce/85/fb7f5d82f620905f442119b8717cf97b622df33cc1a2f9578533337b584e/simplejson-4.0.1-cp310-cp310-win32.whl", hash = "sha256:d450fae24e081703506cdecb7e22477b6c53b055f6a81ac8c13476530ad8d4f5", size = 88000, upload-time = "2026-04-18T22:44:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/00/8b/6fc1e1222164fd3c0606253745f3c34ae2730f97e1279f06e54f23e91c4c/simplejson-4.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:f70d78eba637f3aea597d6959489f5ec4c6f10a42749dc2bb228bcf56298c6fc", size = 90056, upload-time = "2026-04-18T22:44:24.064Z" }, + { url = "https://files.pythonhosted.org/packages/47/39/2957376ab297643d03d799677eb7e539174c340a498582762989664b97ec/simplejson-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aad360482f1c8a779b6d0610d12b66ef01e481ba8af8bfce2693549b102a9875", size = 108970, upload-time = "2026-04-18T22:44:25.184Z" }, + { url = "https://files.pythonhosted.org/packages/db/13/70375fe52b3a7716999c17eb53c3fa88ee6da9d883773f1f819ecb1602b1/simplejson-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:67128132d5d509177a0c9aabcb6f7eea7be9e5a846e1b79e2b5a6b1c7cd6fb49", size = 89159, upload-time = "2026-04-18T22:44:26.326Z" }, + { url = "https://files.pythonhosted.org/packages/12/60/a4772eca1efb4b8e14adcadf9d9b93af4ea8889c61160022066320b8784b/simplejson-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49990f02d0ee7681c01c095de3aa08d379e713f1b84c226cc70ac6701f2698a5", size = 89575, upload-time = "2026-04-18T22:44:27.549Z" }, + { url = "https://files.pythonhosted.org/packages/86/c6/1f5290baaaba5ea1b13ef371b42bd9638c87d562a61fb4e3e3963f6fb955/simplejson-4.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:af7e2afde556391281ca98917b72075a642c2988afb9be9a768ba4e40ff6b5d7", size = 174949, upload-time = "2026-04-18T22:44:28.645Z" }, + { url = "https://files.pythonhosted.org/packages/40/98/f12acc888a3bec439c54c6714d5734e44c2ab60c681f68bd2d0086c6dad5/simplejson-4.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:784476f9048049e0bbd490521994505c2f7e0778d94e1f91ce2ac4d90d09471f", size = 172432, upload-time = "2026-04-18T22:44:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/6f/61/9243858301787d396533d705d4ebdb404a2c75676c7450639cc6b13bb864/simplejson-4.0.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e2dfe5d6fb354e7148c15e1fa29dae87f468e7eb99a0996aab8516bd891f6e34", size = 182000, upload-time = "2026-04-18T22:44:31.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8f/fae6220cdcf7c376d731fc179d49edd2ad0855ad97c9979fb0710e84b894/simplejson-4.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dda7c9c89626ea6b5a23fe8077bee3c37d49626ce76f4e1b4c1c315c62d7127f", size = 170291, upload-time = "2026-04-18T22:44:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/66/9b/8371c0f58214b43746e97b9d91ebcaf3fb03bab1039a29e338f6aab65cad/simplejson-4.0.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3628583e322fb83ed1ba99d7f3fd2e3ee4879e17c1c8d9982f6867eecea5bca7", size = 178934, upload-time = "2026-04-18T22:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/7d/60/e83933422071b8712d1f7d3f720ce975d3988d489cf88c418478ddd7576a/simplejson-4.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71fe98c69b876dfde468e0a7e5fd045b5bc60567b30905c78e52a58c03a52946", size = 173585, upload-time = "2026-04-18T22:44:36.519Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8f/47998fcf7b909a8c8adbb494a88bbaa0e10bf4a48ba1c087e187beb07fe8/simplejson-4.0.1-cp311-cp311-win32.whl", hash = "sha256:2f322ac3cb80711e9fdd5c8e625a10db572e116b5e85e22dd302257c49f02953", size = 87861, upload-time = "2026-04-18T22:44:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d5/54e79dd81d3aac52779ddcf3df0205cedd6e2356f790f81c825fd943a61b/simplejson-4.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:e6593307239ded14f9a528d633ff054f00ebc874ab3c5dea3c49f1988c9b5f06", size = 89921, upload-time = "2026-04-18T22:44:39.604Z" }, + { url = "https://files.pythonhosted.org/packages/ca/21/03ec9b2cceff7966637bd7c96aa4f4df7d59edf4f481fcd76ada5d883f20/simplejson-4.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5be5c2b2ea9e81528b3622f8fd486da04e80e983829f2e69a7acb94302792ef2", size = 110113, upload-time = "2026-04-18T22:44:40.74Z" }, + { url = "https://files.pythonhosted.org/packages/a5/41/77b6f4301566206998ba1911ec334cbe3c574fa5095f846ea99e50e81242/simplejson-4.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aef5610bbbdffc24820daba4f4c7dd1e52bf0f4e016e2860484b562bb964dde5", size = 89804, upload-time = "2026-04-18T22:44:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/8f/48/859d3d12a5f2d6acd1ef1ce75bdd8d25758d63d47223e66eccf3b02461e6/simplejson-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82b34e268f1c7acb9d1bed942b376cd01c418cee9bf05a207ac962c6ce28d29a", size = 90103, upload-time = "2026-04-18T22:44:43.446Z" }, + { url = "https://files.pythonhosted.org/packages/12/17/5135ac3005d2db660182117053d63b97d4ae841efbf77c02ad3aa8b36e3d/simplejson-4.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:905d03e5336b20c6d8ac2a648cb178259807836fb5c8a1ee091ca4601962ec47", size = 183967, upload-time = "2026-04-18T22:44:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e4/3f61fc83ca56256b581c2c55331ba4a51fc50e28d0153f40baebfeb9b90c/simplejson-4.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95c6a6e1f66b6fda46fb9f44809911a90715f05f6df1c6d6db941a8ad2872c35", size = 181209, upload-time = "2026-04-18T22:44:45.899Z" }, + { url = "https://files.pythonhosted.org/packages/3d/26/809a1ed84241a96868fd3915aad173c7e983acb0f7896b7a895635ab1577/simplejson-4.0.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99f12a0d959d7103403e4774189b562f8b1e0529a41561248e2a3c25b6c80797", size = 189815, upload-time = "2026-04-18T22:44:47.175Z" }, + { url = "https://files.pythonhosted.org/packages/08/29/c157b85210df6e286f0f52f8683040d49e654ed51d30126f9eea0f669725/simplejson-4.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5deb4b6c77a02c2e2267135c5afd0d4a9acd3c83c3f73674816d2377b475630", size = 177993, upload-time = "2026-04-18T22:44:48.771Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f5/b8259ecfad5b717199a9ed1c01c54dff0d858934e226dbb6013bfe4a4995/simplejson-4.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c95f7c5c53c3e62bde8c452f41d6795f541e5dc951926275771ff7f2bbae0511", size = 186796, upload-time = "2026-04-18T22:44:50.181Z" }, + { url = "https://files.pythonhosted.org/packages/78/ee/d3dcfaf22d9eb620f8840390a655c22c858e9c2c75073883d7090e9ccf78/simplejson-4.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f057dd469501aff95bb438fc9593d0c2f353dba83f00c0a3db6447ad79f5728", size = 181521, upload-time = "2026-04-18T22:44:51.413Z" }, + { url = "https://files.pythonhosted.org/packages/24/ed/6f13edaff9106e657aab61320e1adceccbd30f50afd877e99d0de292bc48/simplejson-4.0.1-cp312-cp312-win32.whl", hash = "sha256:cc962d5029509f2170ef40e5e7db4df87f71208a931aca6e8bdc0c76e1ff2b3a", size = 88003, upload-time = "2026-04-18T22:44:53.045Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7d/1eebe8ef0682da987d637606aa2eaae0e211010a6fce21abd2c680cf65a6/simplejson-4.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:d2031d87911379c7a778e5f902eeffc161355c9137c695dd12dc219456e976e9", size = 90055, upload-time = "2026-04-18T22:44:54.491Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2d/93a5b862ac29f182a658eb3fc2c98fe28acbb5c05a9076402572c7eb6966/simplejson-4.0.1-py3-none-any.whl", hash = "sha256:dfa6e9923c0ec2880738d09e5ce045741eb6cd4551e261dcd6c3625d26666075", size = 69242, upload-time = "2026-04-18T22:46:16.183Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "soundfile" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751, upload-time = "2025-01-25T09:16:44.235Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250, upload-time = "2025-01-25T09:16:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406, upload-time = "2025-01-25T09:16:49.662Z" }, + { url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729, upload-time = "2025-01-25T09:16:53.018Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646, upload-time = "2025-01-25T09:16:54.872Z" }, + { url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881, upload-time = "2025-01-25T09:16:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" }, +] + +[[package]] +name = "soxr" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/7e/f4b461944662ad75036df65277d6130f9411002bfb79e9df7dff40a31db9/soxr-1.0.0.tar.gz", hash = "sha256:e07ee6c1d659bc6957034f4800c60cb8b98de798823e34d2a2bba1caa85a4509", size = 171415, upload-time = "2025-09-07T13:22:21.317Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/a7/11c36d71595b52fe84a220040ace679035953acf06b83bf2c7117c565d2c/soxr-1.0.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:b876a3156f67c76aef0cff1084eaf4088d9ca584bb569cb993f89a52ec5f399f", size = 206459, upload-time = "2025-09-07T13:21:46.904Z" }, + { url = "https://files.pythonhosted.org/packages/43/5e/8962f2aeea7777d2a6e65a24a2b83c6aea1a28badeda027fd328f7f03bb7/soxr-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d3b957a7b0cc19ae6aa45d40b2181474e53a8dd00efd7bce6bcf4e60e020892", size = 164808, upload-time = "2025-09-07T13:21:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/fc/91/00384166f110a3888ea8efd44523ba7168dd2dc39e3e43c931cc2d069fa9/soxr-1.0.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89685faedebc45af71f08f9957b61cc6143bc94ba43fe38e97067f81e272969", size = 208586, upload-time = "2025-09-07T13:21:50.341Z" }, + { url = "https://files.pythonhosted.org/packages/75/34/e18f1003e242aabed44ed8902534814d3e64209e4d1d874f5b9b67d73cde/soxr-1.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d255741b2f0084fd02d4a2ddd77cd495be9e7e7b6f9dba1c9494f86afefac65b", size = 242310, upload-time = "2025-09-07T13:21:51.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/9c/a1c5ed106b40cc1e2e12cd58831b7f1b61c5fbdb8eceeca4b3a0b0dbef6c/soxr-1.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:158a4a9055958c4b95ef91dbbe280cabb00946b5423b25a9b0ce31bd9e0a271e", size = 173561, upload-time = "2025-09-07T13:21:53.03Z" }, + { url = "https://files.pythonhosted.org/packages/65/ce/a3262bc8733d3a4ce5f660ed88c3d97f4b12658b0909e71334cba1721dcb/soxr-1.0.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:28e19d74a5ef45c0d7000f3c70ec1719e89077379df2a1215058914d9603d2d8", size = 206739, upload-time = "2025-09-07T13:21:54.572Z" }, + { url = "https://files.pythonhosted.org/packages/64/dc/e8cbd100b652697cc9865dbed08832e7e135ff533f453eb6db9e6168d153/soxr-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8dc69fc18884e53b72f6141fdf9d80997edbb4fec9dc2942edcb63abbe0d023", size = 165233, upload-time = "2025-09-07T13:21:55.887Z" }, + { url = "https://files.pythonhosted.org/packages/75/12/4b49611c9ba5e9fe6f807d0a83352516808e8e573f8b4e712fc0c17f3363/soxr-1.0.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f15450e6f65f22f02fcd4c5a9219c873b1e583a73e232805ff160c759a6b586", size = 208867, upload-time = "2025-09-07T13:21:57.076Z" }, + { url = "https://files.pythonhosted.org/packages/cc/70/92146ab970a3ef8c43ac160035b1e52fde5417f89adb10572f7e788d9596/soxr-1.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f73f57452f9df37b4de7a4052789fcbd474a5b28f38bba43278ae4b489d4384", size = 242633, upload-time = "2025-09-07T13:21:58.621Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a7/628479336206959463d08260bffed87905e7ba9e3bd83ca6b405a0736e94/soxr-1.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:9f417c3d69236051cf5a1a7bad7c4bff04eb3d8fcaa24ac1cb06e26c8d48d8dc", size = 173814, upload-time = "2025-09-07T13:21:59.798Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c7/f92b81f1a151c13afb114f57799b86da9330bec844ea5a0d3fe6a8732678/soxr-1.0.0-cp312-abi3-macosx_10_14_x86_64.whl", hash = "sha256:abecf4e39017f3fadb5e051637c272ae5778d838e5c3926a35db36a53e3a607f", size = 205508, upload-time = "2025-09-07T13:22:01.252Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1d/c945fea9d83ea1f2be9d116b3674dbaef26ed090374a77c394b31e3b083b/soxr-1.0.0-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:e973d487ee46aa8023ca00a139db6e09af053a37a032fe22f9ff0cc2e19c94b4", size = 163568, upload-time = "2025-09-07T13:22:03.558Z" }, + { url = "https://files.pythonhosted.org/packages/b5/80/10640970998a1d2199bef6c4d92205f36968cddaf3e4d0e9fe35ddd405bd/soxr-1.0.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8ce273cca101aff3d8c387db5a5a41001ba76ef1837883438d3c652507a9ccc", size = 204707, upload-time = "2025-09-07T13:22:05.125Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/2726603c13c2126cb8ded9e57381b7377f4f0df6ba4408e1af5ddbfdc3dd/soxr-1.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8f2a69686f2856d37823bbb7b78c3d44904f311fe70ba49b893af11d6b6047b", size = 238032, upload-time = "2025-09-07T13:22:06.428Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/530252227f4d0721a5524a936336485dfb429bb206a66baf8e470384f4a2/soxr-1.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:2a3b77b115ae7c478eecdbd060ed4f61beda542dfb70639177ac263aceda42a2", size = 172070, upload-time = "2025-09-07T13:22:07.62Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tensorboardx" +version = "2.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/a9/fc520ea91ab1f3ba51cbf3fe24f2b6364ed3b49046969e0868d46d6da372/tensorboardx-2.6.5.tar.gz", hash = "sha256:ca176db3997ee8c07d2eb77381225956a3fd1c10c91beafab1f17069adc47017", size = 4770195, upload-time = "2026-04-03T15:40:23.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/0f/69fbab4c30b2f3a76e6de67585ea72a8eccf381751f5c0046b966fde9658/tensorboardx-2.6.5-py3-none-any.whl", hash = "sha256:c10b891d00af306537cb8b58a039b2ba41571f0da06f433a41c4ca8d6abe1373", size = 87510, upload-time = "2026-04-03T15:40:22.111Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.21.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/2f/402986d0823f8d7ca139d969af2917fefaa9b947d1fb32f6168c509f2492/tokenizers-0.21.4.tar.gz", hash = "sha256:fa23f85fbc9a02ec5c6978da172cdcbac23498c3ca9f3645c5c68740ac007880", size = 351253, upload-time = "2025-07-28T15:48:54.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/c6/fdb6f72bf6454f52eb4a2510be7fb0f614e541a2554d6210e370d85efff4/tokenizers-0.21.4-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ccc10a7c3bcefe0f242867dc914fc1226ee44321eb618cfe3019b5df3400133", size = 2863987, upload-time = "2025-07-28T15:48:44.877Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a6/28975479e35ddc751dc1ddc97b9b69bf7fcf074db31548aab37f8116674c/tokenizers-0.21.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5e2f601a8e0cd5be5cc7506b20a79112370b9b3e9cb5f13f68ab11acd6ca7d60", size = 2732457, upload-time = "2025-07-28T15:48:43.265Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8f/24f39d7b5c726b7b0be95dca04f344df278a3fe3a4deb15a975d194cbb32/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b376f5a1aee67b4d29032ee85511bbd1b99007ec735f7f35c8a2eb104eade5", size = 3012624, upload-time = "2025-07-28T13:22:43.895Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/26358925717687a58cb74d7a508de96649544fad5778f0cd9827398dc499/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2107ad649e2cda4488d41dfd031469e9da3fcbfd6183e74e4958fa729ffbf9c6", size = 2939681, upload-time = "2025-07-28T13:22:47.499Z" }, + { url = "https://files.pythonhosted.org/packages/99/6f/cc300fea5db2ab5ddc2c8aea5757a27b89c84469899710c3aeddc1d39801/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c73012da95afafdf235ba80047699df4384fdc481527448a078ffd00e45a7d9", size = 3247445, upload-time = "2025-07-28T15:48:39.711Z" }, + { url = "https://files.pythonhosted.org/packages/be/bf/98cb4b9c3c4afd8be89cfa6423704337dc20b73eb4180397a6e0d456c334/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f23186c40395fc390d27f519679a58023f368a0aad234af145e0f39ad1212732", size = 3428014, upload-time = "2025-07-28T13:22:49.569Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/96c1cc780e6ca7f01a57c13235dd05b7bc1c0f3588512ebe9d1331b5f5ae/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc88bb34e23a54cc42713d6d98af5f1bf79c07653d24fe984d2d695ba2c922a2", size = 3193197, upload-time = "2025-07-28T13:22:51.471Z" }, + { url = "https://files.pythonhosted.org/packages/f2/90/273b6c7ec78af547694eddeea9e05de771278bd20476525ab930cecaf7d8/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51b7eabb104f46c1c50b486520555715457ae833d5aee9ff6ae853d1130506ff", size = 3115426, upload-time = "2025-07-28T15:48:41.439Z" }, + { url = "https://files.pythonhosted.org/packages/91/43/c640d5a07e95f1cf9d2c92501f20a25f179ac53a4f71e1489a3dcfcc67ee/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:714b05b2e1af1288bd1bc56ce496c4cebb64a20d158ee802887757791191e6e2", size = 9089127, upload-time = "2025-07-28T15:48:46.472Z" }, + { url = "https://files.pythonhosted.org/packages/44/a1/dd23edd6271d4dca788e5200a807b49ec3e6987815cd9d0a07ad9c96c7c2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1340ff877ceedfa937544b7d79f5b7becf33a4cfb58f89b3b49927004ef66f78", size = 9055243, upload-time = "2025-07-28T15:48:48.539Z" }, + { url = "https://files.pythonhosted.org/packages/21/2b/b410d6e9021c4b7ddb57248304dc817c4d4970b73b6ee343674914701197/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3c1f4317576e465ac9ef0d165b247825a2a4078bcd01cba6b54b867bdf9fdd8b", size = 9298237, upload-time = "2025-07-28T15:48:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/b7/0a/42348c995c67e2e6e5c89ffb9cfd68507cbaeb84ff39c49ee6e0a6dd0fd2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c212aa4e45ec0bb5274b16b6f31dd3f1c41944025c2358faaa5782c754e84c24", size = 9461980, upload-time = "2025-07-28T15:48:52.325Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d3/dacccd834404cd71b5c334882f3ba40331ad2120e69ded32cf5fda9a7436/tokenizers-0.21.4-cp39-abi3-win32.whl", hash = "sha256:6c42a930bc5f4c47f4ea775c91de47d27910881902b0f20e4990ebe045a415d0", size = 2329871, upload-time = "2025-07-28T15:48:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/41/f2/fd673d979185f5dcbac4be7d09461cbb99751554ffb6718d0013af8604cb/tokenizers-0.21.4-cp39-abi3-win_amd64.whl", hash = "sha256:475d807a5c3eb72c59ad9b5fcdb254f6e17f53dfcbb9903233b0dfa9c943b597", size = 2507568, upload-time = "2025-07-28T15:48:55.456Z" }, +] + +[[package]] +name = "torch" +version = "2.3.1" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "python_full_version >= '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'", +] +dependencies = [ + { name = "filelock", marker = "platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'" }, + { name = "fsspec", marker = "platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'" }, + { name = "jinja2", marker = "platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'" }, + { name = "sympy", marker = "platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6544fdf29018668c0a6d4a1bcc955982c1ada70806281b010cba93bdcfbdcf22", upload-time = "2024-06-05T21:12:29Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2aaf0e37734cbc5fe6bfcc81ada36ecbb899d4ddbe13498bd84aaca8a91c8628", upload-time = "2024-06-05T21:12:35Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77d2de1a495a1c07f592c338a0d592e55cc0b9d2f800309e46a0ea2c0e3a2919", upload-time = "2024-06-05T21:12:42Z" }, +] + +[[package]] +name = "torch" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "filelock", marker = "sys_platform == 'darwin'" }, + { name = "fsspec", marker = "sys_platform == 'darwin'" }, + { name = "jinja2", marker = "sys_platform == 'darwin'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform == 'darwin'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform == 'darwin'" }, + { name = "sympy", marker = "sys_platform == 'darwin'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/52/7ab0a00b54aa1651e79a9ebc721d45fba86d8c8ab65c4ec6e0a49f09527a/torch-2.3.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:7c09a94362778428484bcf995f6004b04952106aee0ef45ff0b4bab484f5498d", size = 61002907, upload-time = "2024-06-05T16:42:33.299Z" }, + { url = "https://files.pythonhosted.org/packages/d0/5f/f41b14a398d484bf218d5167ec9061c1e76f500d9e25166117818c8bacda/torch-2.3.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:a7dd4ed388ad1f3d502bf09453d5fe596c7b121de7e0cfaca1e2017782e9bbac", size = 61007595, upload-time = "2024-06-05T16:42:16.707Z" }, + { url = "https://files.pythonhosted.org/packages/49/b6/1a2e3d43d4bc4ad7a4575b3745d707a68d5ed00ba263b205b6281bdd0921/torch-2.3.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:3c333dc2ebc189561514eda06e81df22bf8fb64e2384746b2cb9f04f96d1d4c8", size = 60978559, upload-time = "2024-06-05T16:41:27.77Z" }, +] + +[[package]] +name = "torch" +version = "2.3.1+cpu" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "(python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'linux')", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'linux')", +] +dependencies = [ + { name = "filelock", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_python_implementation != 'CPython' and sys_platform == 'linux')" }, + { name = "fsspec", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_python_implementation != 'CPython' and sys_platform == 'linux')" }, + { name = "jinja2", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_python_implementation != 'CPython' and sys_platform == 'linux')" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'linux')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'linux')" }, + { name = "sympy", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_python_implementation != 'CPython' and sys_platform == 'linux')" }, + { name = "typing-extensions", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_python_implementation != 'CPython' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.3.1%2Bcpu-cp310-cp310-linux_x86_64.whl", hash = "sha256:d679e21d871982b9234444331a26350902cfd2d5ca44ce6f49896af8b3a3087d", upload-time = "2024-06-05T21:11:24Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.3.1%2Bcpu-cp311-cp311-linux_x86_64.whl", hash = "sha256:a272defe305dbd944aa28a91cc3db0f0149495b3ebec2e39723a7224fa05dc57", upload-time = "2024-06-05T21:11:38Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.3.1%2Bcpu-cp312-cp312-linux_x86_64.whl", hash = "sha256:2141a6cb7021adf2f92a0fd372cfeac524ba460bd39ce3a641d30a561e41f69a", upload-time = "2024-06-05T21:11:51Z" }, +] + +[[package]] +name = "torch-complex" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/2b/17cb15a383cf2135330371e034d13b9043dc6d8bd07c871b5aa3064fbed1/torch_complex-0.4.4.tar.gz", hash = "sha256:4153fd6b24a0bad689e6f193bfbd00f38283b1890d808bef684ddc6d1f63fd3f", size = 10025, upload-time = "2024-06-28T07:10:28.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/c5/9b4d756a7ada951e9b17dcc636f98ed1073c737ae809b150ef408afb6298/torch_complex-0.4.4-py3-none-any.whl", hash = "sha256:6ab4ecd4f3a16e3adb70a7f7cd2e769a9dfd07d7a8e27d04ff9c621ebbe34b13", size = 9125, upload-time = "2024-06-28T07:10:26.651Z" }, +] + +[[package]] +name = "torchaudio" +version = "2.3.1" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "python_full_version >= '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'", +] +dependencies = [ + { name = "torch", version = "2.3.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cpu/torchaudio-2.3.1-cp310-cp310-linux_aarch64.whl", hash = "sha256:6b57e773aad72743d50a64a7402a06cb8bdfcc709efc6d8c26429d940e6788e2", upload-time = "2024-06-05T21:13:03Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchaudio-2.3.1-cp311-cp311-linux_aarch64.whl", hash = "sha256:c8c727c8341825bd18d91017c4c00f36b53b08f2176cdb9bdcb0def1c450b21d", upload-time = "2024-06-05T21:13:03Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchaudio-2.3.1-cp312-cp312-linux_aarch64.whl", hash = "sha256:42af6c7a430e6268f2c028e06078d413912b5ec6efa28a097ebdd3c3c79659df", upload-time = "2024-06-05T21:13:04Z" }, +] + +[[package]] +name = "torchaudio" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "torch", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/b9/727f98f746d94b52ff490761abe8e8c595e79b284a4e4f157ba36bd74d44/torchaudio-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1f9134b27e5a7f0c1e33382fc0fe278e53695768cb0af02e8d22b5006c74a2ad", size = 1811090, upload-time = "2024-06-05T16:45:07.998Z" }, + { url = "https://files.pythonhosted.org/packages/41/c3/51482591d741c0c069f5825fe02d9ae387dc63eef2fe4cea1d1f3b07a623/torchaudio-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:01984f38398ca5e98ecfbfeafb72ae5b2131d0bb8aa464b5777addb3e4826877", size = 1816843, upload-time = "2024-06-05T16:45:04.237Z" }, + { url = "https://files.pythonhosted.org/packages/45/97/e584276755305d3a0af0ff280ce7eeafc45855355ae0a6de38b13ac195ac/torchaudio-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5e36685420a07a176146e9d6e0fa8225198f126e167a00785538f853807e2d43", size = 1808180, upload-time = "2024-06-05T16:44:58.503Z" }, +] + +[[package]] +name = "torchaudio" +version = "2.3.1+cpu" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "(python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'linux')", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'linux')", +] +dependencies = [ + { name = "torch", version = "2.3.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_python_implementation != 'CPython' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cpu/torchaudio-2.3.1%2Bcpu-cp310-cp310-linux_x86_64.whl", hash = "sha256:e156d43a1d48073f4eed943ddf0285b9079fc93ce41371172cd1e94ac766f803", upload-time = "2024-06-05T21:13:00Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchaudio-2.3.1%2Bcpu-cp311-cp311-linux_x86_64.whl", hash = "sha256:37cb3d8b003e8fc7bc7a6b0bd1c7a033960d77efb688f4d40464055a07ea6bbe", upload-time = "2024-06-05T21:13:01Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchaudio-2.3.1%2Bcpu-cp312-cp312-linux_x86_64.whl", hash = "sha256:f8a3ba91df57fd9229c96188cbb56f9f960e9adee26c4e57e8e5a3fdbe81b2a8", upload-time = "2024-06-05T21:13:01Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "transformers" +version = "4.49.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/50/46573150944f46df8ec968eda854023165a84470b42f69f67c7d475dabc5/transformers-4.49.0.tar.gz", hash = "sha256:7e40e640b5b8dc3f48743f5f5adbdce3660c82baafbd3afdfc04143cdbd2089e", size = 8610952, upload-time = "2025-02-17T15:19:03.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/37/1f29af63e9c30156a3ed6ebc2754077016577c094f31de7b2631e5d379eb/transformers-4.49.0-py3-none-any.whl", hash = "sha256:6b4fded1c5fee04d384b1014495b4235a2b53c87503d7d592423c06128cbbe03", size = 9970275, upload-time = "2025-02-17T15:18:58.814Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, +] + +[[package]] +name = "umap-learn" +version = "0.5.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numba" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pynndescent" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/ee/af4171241117f85c74b5ca6448ea1033cc28d599c13651d67289bacd4083/umap_learn-0.5.12.tar.gz", hash = "sha256:6aff02ecac5f2aad9f3c65ee518d7ae93e1a985ae38721fdcffceee4232c33c7", size = 96672, upload-time = "2026-04-08T20:03:54.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/98/f63318ccbe75c810011fe9233884c5d348d94d90005de1b79e5f93bef9c0/umap_learn-0.5.12-py3-none-any.whl", hash = "sha256:f2a85d2a2adcb52b541bed9b27a23ca169b56bb1b23283abeebfb8dfb8a42fe5", size = 91849, upload-time = "2026-04-08T20:03:52.561Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, +] + +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/ee/f9f1d656ad168681bb0f6b092372c1e533c4416b8069b1896a175c46e484/xxhash-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71", size = 32845, upload-time = "2025-10-02T14:33:51.573Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/93508d9460b292c74a09b83d16750c52a0ead89c51eea9951cb97a60d959/xxhash-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d", size = 30807, upload-time = "2025-10-02T14:33:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/28c93a3662f2d200c70704efe74aab9640e824f8ce330d8d3943bf7c9b3c/xxhash-3.6.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8", size = 193786, upload-time = "2025-10-02T14:33:54.272Z" }, + { url = "https://files.pythonhosted.org/packages/c1/96/fec0be9bb4b8f5d9c57d76380a366f31a1781fb802f76fc7cda6c84893c7/xxhash-3.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058", size = 212830, upload-time = "2025-10-02T14:33:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a0/c706845ba77b9611f81fd2e93fad9859346b026e8445e76f8c6fd057cc6d/xxhash-3.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2", size = 211606, upload-time = "2025-10-02T14:33:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/164126a2999e5045f04a69257eea946c0dc3e86541b400d4385d646b53d7/xxhash-3.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc", size = 444872, upload-time = "2025-10-02T14:33:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4b/55ab404c56cd70a2cf5ecfe484838865d0fea5627365c6c8ca156bd09c8f/xxhash-3.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc", size = 193217, upload-time = "2025-10-02T14:33:59.724Z" }, + { url = "https://files.pythonhosted.org/packages/45/e6/52abf06bac316db33aa269091ae7311bd53cfc6f4b120ae77bac1b348091/xxhash-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07", size = 210139, upload-time = "2025-10-02T14:34:02.041Z" }, + { url = "https://files.pythonhosted.org/packages/34/37/db94d490b8691236d356bc249c08819cbcef9273a1a30acf1254ff9ce157/xxhash-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4", size = 197669, upload-time = "2025-10-02T14:34:03.664Z" }, + { url = "https://files.pythonhosted.org/packages/b7/36/c4f219ef4a17a4f7a64ed3569bc2b5a9c8311abdb22249ac96093625b1a4/xxhash-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06", size = 210018, upload-time = "2025-10-02T14:34:05.325Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/bfac889a374fc2fc439a69223d1750eed2e18a7db8514737ab630534fa08/xxhash-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4", size = 413058, upload-time = "2025-10-02T14:34:06.925Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d1/555d8447e0dd32ad0930a249a522bb2e289f0d08b6b16204cfa42c1f5a0c/xxhash-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b", size = 190628, upload-time = "2025-10-02T14:34:08.669Z" }, + { url = "https://files.pythonhosted.org/packages/d1/15/8751330b5186cedc4ed4b597989882ea05e0408b53fa47bcb46a6125bfc6/xxhash-3.6.0-cp310-cp310-win32.whl", hash = "sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b", size = 30577, upload-time = "2025-10-02T14:34:10.234Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cc/53f87e8b5871a6eb2ff7e89c48c66093bda2be52315a8161ddc54ea550c4/xxhash-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb", size = 31487, upload-time = "2025-10-02T14:34:11.618Z" }, + { url = "https://files.pythonhosted.org/packages/9f/00/60f9ea3bb697667a14314d7269956f58bf56bb73864f8f8d52a3c2535e9a/xxhash-3.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d", size = 27863, upload-time = "2025-10-02T14:34:12.619Z" }, + { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, + { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, + { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, + { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, + { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, + { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, + { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, + { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, + { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, + { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, + { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0d/9cc638702f6fc3c7a3685bcc8cf2a9ed7d6206e932a49f5242658047ef51/yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", size = 123764, upload-time = "2026-03-01T22:04:09.7Z" }, + { url = "https://files.pythonhosted.org/packages/7a/35/5a553687c5793df5429cd1db45909d4f3af7eee90014888c208d086a44f0/yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", size = 86282, upload-time = "2026-03-01T22:04:11.892Z" }, + { url = "https://files.pythonhosted.org/packages/68/2e/c5a2234238f8ce37a8312b52801ee74117f576b1539eec8404a480434acc/yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", size = 86053, upload-time = "2026-03-01T22:04:13.292Z" }, + { url = "https://files.pythonhosted.org/packages/74/3f/bbd8ff36fb038622797ffbaf7db314918bb4d76f1cc8a4f9ca7a55fe5195/yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", size = 99395, upload-time = "2026-03-01T22:04:15.133Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/9516bc4e269d2a3ec9c6779fcdeac51ce5b3a9b0156f06ac7152e5bba864/yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", size = 92143, upload-time = "2026-03-01T22:04:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/c7/63/88802d1f6b1cb1fc67d67a58cd0cf8a1790de4ce7946e434240f1d60ab4a/yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", size = 107643, upload-time = "2026-03-01T22:04:18.519Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/4f9b838f4d8bdd6f0f385aed8bbf21c71ed11a0b9983305c302cbd557815/yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", size = 108700, upload-time = "2026-03-01T22:04:20.373Z" }, + { url = "https://files.pythonhosted.org/packages/50/12/95a1d33f04a79c402664070d43b8b9f72dc18914e135b345b611b0b1f8cc/yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", size = 102769, upload-time = "2026-03-01T22:04:23.055Z" }, + { url = "https://files.pythonhosted.org/packages/86/65/91a0285f51321369fd1a8308aa19207520c5f0587772cfc2e03fc2467e90/yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", size = 101114, upload-time = "2026-03-01T22:04:25.031Z" }, + { url = "https://files.pythonhosted.org/packages/58/80/c7c8244fc3e5bc483dc71a09560f43b619fab29301a0f0a8f936e42865c7/yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", size = 98883, upload-time = "2026-03-01T22:04:27.281Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/71ca9cc9ca79c0b7d491216177d1aed559d632947b8ffb0ee60f7d8b23e3/yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", size = 94172, upload-time = "2026-03-01T22:04:28.554Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3f/6c6c8a0fe29c26fb2db2e8d32195bb84ec1bfb8f1d32e7f73b787fcf349b/yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", size = 107010, upload-time = "2026-03-01T22:04:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/56/38/12730c05e5ad40a76374d440ed8b0899729a96c250516d91c620a6e38fc2/yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", size = 100285, upload-time = "2026-03-01T22:04:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/34/92/6a7be9239f2347234e027284e7a5f74b1140cc86575e7b469d13fba1ebfe/yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", size = 108230, upload-time = "2026-03-01T22:04:33.844Z" }, + { url = "https://files.pythonhosted.org/packages/5e/81/4aebccfa9376bd98b9d8bfad20621a57d3e8cfc5b8631c1fa5f62cdd03f4/yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", size = 103008, upload-time = "2026-03-01T22:04:35.856Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/0b4e3edcec794a86b853b0c6396c0a888d72dfce19b2d88c02ac289fb6c1/yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", size = 83073, upload-time = "2026-03-01T22:04:38.268Z" }, + { url = "https://files.pythonhosted.org/packages/a0/71/ad95c33da18897e4c636528bbc24a1dd23fe16797de8bc4ec667b8db0ba4/yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", size = 87328, upload-time = "2026-03-01T22:04:39.558Z" }, + { url = "https://files.pythonhosted.org/packages/e2/14/dfa369523c79bccf9c9c746b0a63eb31f65db9418ac01275f7950962e504/yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", size = 82463, upload-time = "2026-03-01T22:04:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, + { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, + { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, + { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] diff --git a/environments/iluvatar/pyproject.toml b/environments/iluvatar/pyproject.toml new file mode 100644 index 0000000..8af3caf --- /dev/null +++ b/environments/iluvatar/pyproject.toml @@ -0,0 +1,41 @@ +[project] +name = "qwen3-asr-iluvatar-env" +version = "1.0.0" +description = "Iluvatar IX runtime environment for qwen3-asr" +requires-python = ">=3.10,<3.13" +dependencies = [ + "fastapi==0.128.0", + "fastapi-offline==1.7.6", + "uvicorn[standard]==0.40.0", + "pydantic==2.12.0", + "python-multipart==0.0.22", + "funasr==1.3.1", + "requests==2.32.5", + "modelscope[framework]==1.34.0", + "soundfile==0.13.1", + "librosa==0.11.0", + "websockets==16.0", + "addict==2.4.0", + "datasets==3.6.0", + "scipy==1.15.3", + "itntext==0.1.5", + "python-dotenv==1.2.1", + "asyncpg==0.31.0", + "pypinyin==0.55.0", + "huggingface_hub==0.34.0", + "hdbscan==0.8.41", + "loguru==0.7.2", + "rich>=13.9,<14", + "setuptools>=70.0.0,<81", + "transformers>=4.56.0,<5", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = [] + +[tool.uv] +package = false diff --git a/environments/iluvatar/requirements.txt b/environments/iluvatar/requirements.txt new file mode 100644 index 0000000..995482c --- /dev/null +++ b/environments/iluvatar/requirements.txt @@ -0,0 +1,24 @@ +fastapi==0.128.0 +fastapi-offline==1.7.6 +uvicorn[standard]==0.40.0 +pydantic==2.12.0 +python-multipart==0.0.22 +funasr==1.3.1 +requests==2.32.5 +modelscope[framework]==1.34.0 +soundfile==0.13.1 +librosa==0.11.0 +websockets==16.0 +addict==2.4.0 +datasets==3.6.0 +scipy==1.15.3 +itntext==0.1.5 +python-dotenv==1.2.1 +asyncpg==0.31.0 +pypinyin==0.55.0 +huggingface_hub==0.34.0 +hdbscan==0.8.41 +loguru==0.7.2 +rich>=13.9,<14 +setuptools>=70.0.0,<81 +transformers>=4.56.0,<5 diff --git a/environments/metax/pyproject.toml b/environments/metax/pyproject.toml new file mode 100644 index 0000000..0cd1afe --- /dev/null +++ b/environments/metax/pyproject.toml @@ -0,0 +1,45 @@ +[project] +name = "qwen3-asr-metax-env" +version = "1.0.0" +description = "MetaX MACA runtime environment for qwen3-asr" +requires-python = ">=3.10,<3.13" +dependencies = [ + "fastapi==0.128.0", + "fastapi-offline==1.7.6", + "uvicorn[standard]==0.40.0", + "pydantic==2.12.0", + "python-multipart==0.0.22", + "funasr==1.3.1", + "requests==2.32.5", + "modelscope[framework]==1.34.0", + "soundfile==0.13.1", + "librosa==0.11.0", + "websockets==16.0", + "addict==2.4.0", + "datasets==3.6.0", + "scipy==1.15.3", + "itntext==0.1.5", + "python-dotenv==1.2.1", + "asyncpg==0.31.0", + "pypinyin==0.55.0", + "huggingface_hub==0.34.0", + "hdbscan==0.8.41", + "loguru==0.7.2", + "rich>=13.9,<14", + "transformers>=4.56.0,<5", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = [] + +[tool.uv] +package = false + +[[tool.uv.index]] +name = "metax-maca" +url = "https://repos.metax-tech.com/r/maca-pypi/simple" +explicit = true diff --git a/environments/metax/requirements.txt b/environments/metax/requirements.txt new file mode 100644 index 0000000..a165de4 --- /dev/null +++ b/environments/metax/requirements.txt @@ -0,0 +1,23 @@ +fastapi==0.128.0 +fastapi-offline==1.7.6 +uvicorn[standard]==0.40.0 +pydantic==2.12.0 +python-multipart==0.0.22 +funasr==1.3.1 +requests==2.32.5 +modelscope[framework]==1.34.0 +soundfile==0.13.1 +librosa==0.11.0 +websockets==16.0 +addict==2.4.0 +datasets==3.6.0 +scipy==1.15.3 +itntext==0.1.5 +python-dotenv==1.2.1 +asyncpg==0.31.0 +pypinyin==0.55.0 +huggingface_hub==0.34.0 +hdbscan==0.8.41 +loguru==0.7.2 +rich>=13.9,<14 +transformers>=4.56.0,<5 diff --git a/environments/mthreads/pyproject.toml b/environments/mthreads/pyproject.toml new file mode 100644 index 0000000..848c816 --- /dev/null +++ b/environments/mthreads/pyproject.toml @@ -0,0 +1,41 @@ +[project] +name = "qwen3-asr-mthreads-env" +version = "1.0.0" +description = "Moore Threads MUSA runtime environment for qwen3-asr" +requires-python = ">=3.10,<3.13" +dependencies = [ + "fastapi==0.128.0", + "fastapi-offline==1.7.6", + "uvicorn[standard]==0.40.0", + "pydantic==2.12.0", + "python-multipart==0.0.22", + "funasr==1.3.1", + "requests==2.32.5", + "modelscope[framework]==1.34.0", + "soundfile==0.13.1", + "librosa==0.11.0", + "websockets==16.0", + "addict==2.4.0", + "datasets==3.6.0", + "scipy==1.15.3", + "itntext==0.1.5", + "python-dotenv==1.2.1", + "asyncpg==0.31.0", + "pypinyin==0.55.0", + "huggingface_hub==0.34.0", + "hdbscan==0.8.41", + "loguru==0.7.2", + "rich>=13.9,<14", + "setuptools>=70.0.0,<81", + "transformers>=4.56.0,<5", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = [] + +[tool.uv] +package = false diff --git a/environments/mthreads/requirements.txt b/environments/mthreads/requirements.txt new file mode 100644 index 0000000..995482c --- /dev/null +++ b/environments/mthreads/requirements.txt @@ -0,0 +1,24 @@ +fastapi==0.128.0 +fastapi-offline==1.7.6 +uvicorn[standard]==0.40.0 +pydantic==2.12.0 +python-multipart==0.0.22 +funasr==1.3.1 +requests==2.32.5 +modelscope[framework]==1.34.0 +soundfile==0.13.1 +librosa==0.11.0 +websockets==16.0 +addict==2.4.0 +datasets==3.6.0 +scipy==1.15.3 +itntext==0.1.5 +python-dotenv==1.2.1 +asyncpg==0.31.0 +pypinyin==0.55.0 +huggingface_hub==0.34.0 +hdbscan==0.8.41 +loguru==0.7.2 +rich>=13.9,<14 +setuptools>=70.0.0,<81 +transformers>=4.56.0,<5 diff --git a/export_offline_bundle.sh b/export_offline_bundle.sh new file mode 100644 index 0000000..f91fb83 --- /dev/null +++ b/export_offline_bundle.sh @@ -0,0 +1,562 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="${SCRIPT_DIR}" + +TIMESTAMP="$(date +"%Y%m%d_%H%M%S")" +BUILD_TYPE="" +VERSION="" +OUTPUT_ROOT="${PROJECT_ROOT}/build-file" +REGISTRY="unis" +IMAGE_NAME="qwen3-asr" +INCLUDE_MODELS="true" +METAX_BASE_IMAGE="${METAX_BASE_IMAGE:-}" +ILUVATAR_BASE_IMAGE="${ILUVATAR_BASE_IMAGE:-}" +MTHREADS_BASE_IMAGE="${MTHREADS_BASE_IMAGE:-}" +METAX_PYTHON_BIN="${METAX_PYTHON_BIN:-/opt/conda/bin/python}" +ILUVATAR_PYTHON_BIN="${ILUVATAR_PYTHON_BIN:-python3}" +MTHREADS_PYTHON_BIN="${MTHREADS_PYTHON_BIN:-python3}" + +info() { echo "[INFO] $1"; } +die() { echo "[ERROR] $1" >&2; exit 1; } + +show_help() { + cat </dev/null 2>&1; then + uv run python -m app.utils.download_models --export-dir "$model_export_dir" + else + python -m app.utils.download_models --export-dir "$model_export_dir" + fi + ) + + info "压缩模型目录: $(basename "$model_archive")" + tar -C "$bundle_dir" -czf "$model_archive" models + rm -rf "$model_export_dir" +} + +prompt_build_type() { + local choice + echo "请选择离线交付类型:" + echo " 1) GPU" + echo " 2) CPU" + echo " 3) MetaX GPU" + echo " 4) Iluvatar GPU" + echo " 5) Moore Threads GPU" + echo " 6) ALL" + read -r -p "请输入选项 [6]: " choice + choice="${choice:-6}" + case "$choice" in + 1) BUILD_TYPE="gpu" ;; + 2) BUILD_TYPE="cpu" ;; + 3) BUILD_TYPE="metax" ;; + 4) BUILD_TYPE="iluvatar" ;; + 5) BUILD_TYPE="mthreads" ;; + 6) BUILD_TYPE="all" ;; + *) die "无效选项: $choice" ;; + esac +} + +validate() { + case "$BUILD_TYPE" in + cpu|gpu|metax|iluvatar|mthreads|all) ;; + "") if [[ -t 0 ]]; then prompt_build_type; else BUILD_TYPE="all"; fi ;; + *) die "不支持的构建类型: ${BUILD_TYPE}" ;; + esac + + if [[ "$BUILD_TYPE" == "metax" && -z "$METAX_BASE_IMAGE" ]]; then + die "--type metax 需要指定 --metax-base,值为已 docker load/pull 的沐曦官方 vLLM 镜像" + fi + if [[ "$BUILD_TYPE" == "iluvatar" && -z "$ILUVATAR_BASE_IMAGE" ]]; then + die "--type iluvatar 需要指定 --iluvatar-base,值为已 docker load/pull 的天数官方 vLLM 镜像" + fi + if [[ "$BUILD_TYPE" == "mthreads" && -z "$MTHREADS_BASE_IMAGE" ]]; then + die "--type mthreads 需要指定 --mthreads-base,值为已 docker load/pull 的摩尔线程官方 vLLM 镜像" + fi + + VERSION="${VERSION:-$TIMESTAMP}" +} + +export_compressor() { + command -v pigz >/dev/null 2>&1 && echo "pigz -f" || echo "gzip -f" +} + +build_and_export_image() { + local target="$1" + local dockerfile="$2" + local tag="$3" + local bundle_dir="$4" + local tar_path="${bundle_dir}/${IMAGE_NAME}-${target}-${VERSION}-amd64.tar" + + info "构建 ${target} 镜像: ${tag}" + ( + cd "$PROJECT_ROOT" + case "$target" in + metax) + docker build -f "$dockerfile" -t "$tag" \ + --build-arg "METAX_BASE_IMAGE=${METAX_BASE_IMAGE}" \ + --build-arg "PYTHON_BIN=${METAX_PYTHON_BIN}" . + ;; + iluvatar) + docker build -f "$dockerfile" -t "$tag" \ + --build-arg "ILUVATAR_BASE_IMAGE=${ILUVATAR_BASE_IMAGE}" \ + --build-arg "PYTHON_BIN=${ILUVATAR_PYTHON_BIN}" . + ;; + mthreads) + docker build -f "$dockerfile" -t "$tag" \ + --build-arg "MTHREADS_BASE_IMAGE=${MTHREADS_BASE_IMAGE}" \ + --build-arg "PYTHON_BIN=${MTHREADS_PYTHON_BIN}" . + ;; + *) + docker build -f "$dockerfile" -t "$tag" . + ;; + esac + ) + + info "导出 ${target} 镜像归档" + docker save -o "$tar_path" "$tag" + + info "压缩 ${target} 镜像归档" + $(export_compressor) "$tar_path" +} + +build_offline_images() { + local bundle_dir="$1" + local cpu_tag="${REGISTRY}/${IMAGE_NAME}:cpu-${VERSION}" + local gpu_tag="${REGISTRY}/${IMAGE_NAME}:gpu-${VERSION}" + local metax_tag="${REGISTRY}/${IMAGE_NAME}:metax-${VERSION}" + local iluvatar_tag="${REGISTRY}/${IMAGE_NAME}:iluvatar-${VERSION}" + local mthreads_tag="${REGISTRY}/${IMAGE_NAME}:mthreads-${VERSION}" + + case "$BUILD_TYPE" in + cpu) + build_and_export_image "cpu" "Dockerfile.cpu" "$cpu_tag" "$bundle_dir" + ;; + gpu) + build_and_export_image "gpu" "Dockerfile.gpu" "$gpu_tag" "$bundle_dir" + ;; + metax) + build_and_export_image "metax" "Dockerfile.metax" "$metax_tag" "$bundle_dir" + ;; + iluvatar) + build_and_export_image "iluvatar" "Dockerfile.iluvatar" "$iluvatar_tag" "$bundle_dir" + ;; + mthreads) + build_and_export_image "mthreads" "Dockerfile.mthreads" "$mthreads_tag" "$bundle_dir" + ;; + all) + build_and_export_image "cpu" "Dockerfile.cpu" "$cpu_tag" "$bundle_dir" + build_and_export_image "gpu" "Dockerfile.gpu" "$gpu_tag" "$bundle_dir" + ;; + esac +} + +append_bundle_image_env() { + local bundle_dir="$1" + local env_file="${bundle_dir}/.env.example" + local image_tag="" + local note="" + + case "$BUILD_TYPE" in + gpu) + image_tag="${REGISTRY}/${IMAGE_NAME}:gpu-${VERSION}" + note="GPU" + ;; + metax) + image_tag="${REGISTRY}/${IMAGE_NAME}:metax-${VERSION}" + note="MetaX GPU" + ;; + iluvatar) + image_tag="${REGISTRY}/${IMAGE_NAME}:iluvatar-${VERSION}" + note="Iluvatar GPU" + ;; + mthreads) + image_tag="${REGISTRY}/${IMAGE_NAME}:mthreads-${VERSION}" + note="Moore Threads GPU" + ;; + cpu) + image_tag="${REGISTRY}/${IMAGE_NAME}:cpu-${VERSION}" + note="CPU" + ;; + all) + image_tag="${REGISTRY}/${IMAGE_NAME}:gpu-${VERSION}" + note="GPU by default; switch to ${REGISTRY}/${IMAGE_NAME}:cpu-${VERSION} when using docker-compose-cpu.yml" + ;; + esac + + cat >> "$env_file" < "${bundle_dir}/BUNDLE_INFO.txt" <> "${bundle_dir}/BUNDLE_INFO.txt" + done + echo "Compose Files:" >> "${bundle_dir}/BUNDLE_INFO.txt" + while IFS= read -r compose_file; do + [[ -n "$compose_file" ]] || continue + echo " ${compose_file}" >> "${bundle_dir}/BUNDLE_INFO.txt" + done < <(bundle_compose_files) + cat >> "${bundle_dir}/BUNDLE_INFO.txt" <> "${bundle_dir}/BUNDLE_INFO.txt" < "${bundle_dir}/README.md" <=13.9,<14", + "setuptools>=70.0.0,<81", + "torch==2.11.0 ; sys_platform == 'linux'", + "torchaudio==2.11.0 ; sys_platform == 'linux'", + "torchvision==0.26.0 ; sys_platform == 'linux'", + "transformers>=4.56.0,<5", + "vllm==0.20.0 ; sys_platform == 'linux'", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = [] + +[tool.uv] +package = false + +[tool.uv.sources] +torch = [ + { index = "pytorch-cu130", marker = "platform_system == 'Linux'" }, +] +torchaudio = [ + { index = "pytorch-cu130", marker = "platform_system == 'Linux'" }, +] +torchvision = [ + { index = "pytorch-cu130", marker = "platform_system == 'Linux'" }, +] + +[[tool.uv.index]] +name = "pytorch-cu130" +url = "https://download.pytorch.org/whl/cu130" +explicit = true diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000..89e2d90 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,14 @@ +{ + "include": [ + "app", + "start.py" + ], + "exclude": [ + "**/__pycache__", + "tests", + "scripts", + "video" + ], + "venvPath": ".", + "venv": ".venv" +} diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..f6e1c68 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,354 @@ +# 脚本说明 + +## 环境同步脚本 + +## 厂商 GPU 镜像融合打包 + +NVIDIA 镜像由项目 `Dockerfile.gpu` 完整构建;沐曦、天数、摩尔线程等国产 GPU 推荐先导入厂商官方 vLLM 镜像,再把本项目注入进去生成融合镜像: + +```bash +docker load -i metax-vllm-official.tar +docker images | grep -i -E 'metax|maca|vllm' + +./scripts/package_vendor_gpu_image.sh \ + --vendor metax \ + --base-image <沐曦官方vLLM镜像名> \ + -v n260-3.7.0.38 + +docker pull registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5 +./scripts/package_vendor_gpu_image.sh \ + --vendor iluvatar \ + --base-image registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5 \ + -v vllm0.17.0-4.4.0-v5 + +docker pull registry.mthreads.com/presale/devtech/vllm_musa:s4000_4.3.5_d0519 +./scripts/package_vendor_gpu_image.sh \ + --vendor mthreads \ + --base-image registry.mthreads.com/presale/devtech/vllm_musa:s4000_4.3.5_d0519 \ + -v s4000_4.3.5_d0519 +``` + +沐曦 GPU 的离线交付流程见 `docs/metax_offline_deployment.md`。 +天数 GPU 的离线交付流程见 `docs/iluvatar_offline_deployment.md`。 +摩尔线程 GPU 的离线交付流程见 `docs/mthreads_offline_deployment.md`。 + +输出形如: + +```text +build-file/qwen3-asr-metax-n260-3.7.0.38-amd64.tar.gz +``` + +这个压缩包就是可交付给客户的融合镜像。 + +| 脚本 | 说明 | +|------|------| +| `./scripts/sync_gpu_env.sh` | 同步根目录 NVIDIA CUDA/vLLM 环境 | +| `./scripts/sync_metax_env.sh` | 同步沐曦 MACA 环境;公共依赖来自 `environments/metax`,可选 GPU 栈默认按 `--no-deps` 从沐曦 PyPI 源安装 | +| `./scripts/sync_iluvatar_env.sh` | 同步天数环境公共依赖;GPU 栈使用天数官方 vLLM 镜像或匹配安装包 | +| `./scripts/sync_mthreads_env.sh` | 同步摩尔线程环境公共依赖;GPU 栈使用摩尔线程官方 MUSA vLLM 镜像或匹配安装包 | +| `./scripts/sync_cpu_env.sh` | 同步 CPU Rust 环境 | +| `./scripts/sync_accel_env.sh` | 自动识别 `mx-smi` / `ixsmi` / `mthreads-gmi` / `nvidia-smi` 并调用对应同步脚本 | + +沐曦包版本可通过环境变量覆盖: + +```bash +METAX_TORCH_SPEC='torch==x.y.z' \ +METAX_TORCHAUDIO_SPEC='torchaudio==x.y.z' \ +METAX_TORCHVISION_SPEC='torchvision==x.y.z' \ +METAX_VLLM_SPEC='vllm==x.y.z' \ +./scripts/sync_metax_env.sh +``` + +沐曦 GPU 栈默认使用 `--no-deps` 且只访问沐曦 PyPI 源,避免回退到 PyPI 后拉取 NVIDIA CUDA 包。 +只有确认沐曦源中包含完整 GPU 栈依赖时,才设置: + +```bash +METAX_INSTALL_GPU_DEPS=true ./scripts/sync_metax_env.sh +``` + +查看沐曦源中的包版本: + +```bash +./scripts/sync_metax_env.sh --list-versions torch +``` + +## RMS 音频分析工具 + +用于分析音频文件的 RMS 能量时序,帮助确定远场声音过滤的最佳阈值。 + +## 功能特性 + +- ✅ 支持多种音频格式 (WAV, MP3, FLAC 等) +- ✅ 支持立体声、左声道、右声道选择 +- ✅ 生成 RMS 时序图和分布直方图 +- ✅ 详细的统计分析和阈值建议 +- ✅ 可自定义分块大小(默认 240ms,与流式 ASR 一致) + +## 安装依赖 + +```bash +./scripts/sync_cpu_env.sh +``` + +## 使用方法 + +### 基础用法 + +```bash +# 分析立体声音频(默认) +.venv/bin/python scripts/analyze_audio_rms.py audio.wav + +# 仅分析左声道 +.venv/bin/python scripts/analyze_audio_rms.py audio.wav --channel left + +# 仅分析右声道 +.venv/bin/python scripts/analyze_audio_rms.py audio.wav --channel right +``` + +### 高级用法 + +```bash +# 自定义阈值 +.venv/bin/python scripts/analyze_audio_rms.py audio.wav --threshold 0.015 + +# 自定义分块大小(例如 160ms) +.venv/bin/python scripts/analyze_audio_rms.py audio.wav --chunk-size 160 + +# 保存图表到文件 +.venv/bin/python scripts/analyze_audio_rms.py audio.wav --output analysis.png + +# 仅输出统计信息,不显示图表 +.venv/bin/python scripts/analyze_audio_rms.py audio.wav --no-plot +``` + +### 完整示例 + +```bash +# 分析右声道,使用 0.015 阈值,保存图表 +.venv/bin/python scripts/analyze_audio_rms.py recording.wav \ + --channel right \ + --threshold 0.015 \ + --output rms_analysis.png +``` + +## 参数说明 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `audio_file` | 必需 | - | 音频文件路径 | +| `--channel` | 可选 | `stereo` | 声道选择: `stereo`, `left`, `right` | +| `--threshold` | 可选 | `0.01` | RMS 能量阈值 | +| `--chunk-size` | 可选 | `240` | 分块大小(毫秒) | +| `--output` | 可选 | - | 保存图表的路径 | +| `--no-plot` | 标志 | - | 不显示图表,仅输出统计 | + +## 输出说明 + +### 统计信息 + +脚本会输出以下统计信息: + +1. **基础统计** + - 最小值、最大值、平均值、中位数、标准差 + +2. **百分位数** + - P10, P25, P50, P75, P90, P95, P99 + +3. **阈值分析** + - 超过/低于阈值的帧数和百分比 + +4. **建议的阈值** + - 保守模式(高灵敏度):P10 + - 宽松模式(推荐):P25 + - 严格模式(低误触):平均值的 50% + +### 可视化图表 + +生成两个图表: + +1. **RMS 时序图** + - 显示整个音频的 RMS 能量变化 + - 绿色区域:近场音频(>= 阈值) + - 红色区域:远场音频(< 阈值) + - 红色虚线:当前阈值 + +2. **RMS 分布直方图** + - 显示 RMS 值的分布情况 + - 红色虚线:当前阈值 + - 橙色虚线:平均值 + - 绿色虚线:中位数 + +## 使用场景 + +### 1. 确定初始阈值 + +录制一段包含近场说话和远场环境音的测试音频,然后: + +```bash +.venv/bin/python scripts/analyze_audio_rms.py test_audio.wav +``` + +查看**建议的阈值**,选择合适的模式。 + +### 2. 对比不同声道 + +如果使用立体声麦克风,可以对比左右声道: + +```bash +.venv/bin/python scripts/analyze_audio_rms.py audio.wav --channel left --output left.png +.venv/bin/python scripts/analyze_audio_rms.py audio.wav --channel right --output right.png +``` + +### 3. 验证阈值效果 + +使用自定义阈值查看过滤效果: + +```bash +# 测试宽松模式 +.venv/bin/python scripts/analyze_audio_rms.py audio.wav --threshold 0.01 + +# 测试严格模式 +.venv/bin/python scripts/analyze_audio_rms.py audio.wav --threshold 0.015 + +# 测试保守模式 +.venv/bin/python scripts/analyze_audio_rms.py audio.wav --threshold 0.005 +``` + +### 4. 批量分析 + +如需批量分析多个文件,可以使用简单的循环: + +```bash +#!/bin/bash +for file in recordings/*.wav; do + echo "Analyzing: $file" + .venv/bin/python scripts/analyze_audio_rms.py "$file" \ + --threshold 0.01 \ + --no-plot \ + --output "analysis/$(basename "$file" .wav).png" +done +``` + +## 输出示例 + +``` +============================================================== +音频 RMS 时序分析工具 +============================================================== + +📁 文件: test_audio.wav +🎚️ 声道: stereo +📊 分块大小: 240ms +🎯 阈值: 0.010000 + +正在加载音频... +✓ 使用立体声(双声道平均) +✓ 采样率: 16000 Hz +✓ 时长: 30.50 秒 +✓ 样本数: 488000 + +正在分析 RMS 时序 (分块大小: 240ms)... +✓ 分析了 127 个音频块 + +============================================================== +RMS 统计分析 +============================================================== + +📊 基础统计: + - 最小值: 0.000234 + - 最大值: 0.085432 + - 平均值: 0.012567 + - 中位数: 0.009234 + - 标准差: 0.015678 + +📈 百分位数: + - P10: 0.002345 + - P25: 0.005678 + - P50: 0.009234 + - P75: 0.015432 + - P90: 0.035678 + - P95: 0.045678 + - P99: 0.075432 + +🎯 阈值分析 (当前阈值: 0.010000): + - 超过阈值的帧数: 65 (51.2%) + - 低于阈值的帧数: 62 (48.8%) + +💡 建议的阈值范围: + - 保守模式 (高灵敏度): 0.002345 (P10) + - 宽松模式 (推荐): 0.005678 (P25) + - 严格模式 (低误触): 0.006284 (平均值的50%) +============================================================== +``` + +## 调优建议 + +1. **录制测试音频** + - 包含正常说话(近场) + - 包含远处说话或电视声音(远场) + - 包含安静时刻(环境音) + +2. **分析并选择阈值** + - 查看时序图,观察近场和远场的 RMS 差异 + - 查看分布直方图,找到明显的分界点 + - 参考建议的阈值范围 + +3. **验证效果** + - 在实际应用中测试选定的阈值 + - 如果误触发过多,提高阈值 + - 如果正常说话被过滤,降低阈值 + +4. **微调** + - 宽松模式(0.01)适合大多数场景 + - 嘈杂环境使用严格模式(0.015) + - 安静环境使用保守模式(0.005) + +## 故障排查 + +### 问题:无法加载音频文件 + +```bash +# 安装必要的库 +./scripts/sync_cpu_env.sh + +# 对于 MP3 文件,可能还需要 +./scripts/sync_cpu_env.sh +``` + +### 问题:中文显示乱码 + +脚本已配置中文字体,但如果仍然显示乱码: + +1. macOS: 会自动使用 'Arial Unicode MS' +2. Windows: 会自动使用 'SimHei' +3. Linux: 需要安装中文字体 + +### 问题:图表不显示 + +```bash +# 后台运行或 SSH 连接时使用 +python scripts/analyze_audio_rms.py audio.wav --no-plot --output analysis.png +``` + +## 与远场过滤功能的关系 + +此工具使用与 `app/utils/audio_filter.py` 相同的 RMS 计算方法,确保分析结果与实际运行时的行为一致。 + +确定阈值后,在配置文件中设置: + +```bash +# docker-compose.yml +environment: + - ASR_NEARFIELD_RMS_THRESHOLD=0.01 # 使用分析得出的阈值 +``` + +或在 `.env` 文件中: + +```bash +ASR_NEARFIELD_RMS_THRESHOLD=0.01 +``` + +## 相关文档 + +- [远场过滤功能文档](../docs/nearfield_filter.md) +- [部署配置指南](../docs/deployment.md) diff --git a/scripts/analyze_audio_rms.py b/scripts/analyze_audio_rms.py new file mode 100644 index 0000000..160d7a9 --- /dev/null +++ b/scripts/analyze_audio_rms.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +音频RMS时序分析工具 + +用于分析音频���件的RMS能量,帮助确定远场过滤的阈值。 +支持立体声、左声道、右声道选择。 +""" + +import argparse +import numpy as np +import matplotlib.pyplot as plt +from pathlib import Path +import sys +from typing import Optional + +# 设置中文显示 +plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'DejaVu Sans'] +plt.rcParams['axes.unicode_minus'] = False + + +def load_audio(file_path: str, channel: str = 'stereo') -> tuple: + """加载音频文件 + + Args: + file_path: 音频文件路径 + channel: 声道选择 ('stereo', 'left', 'right') + + Returns: + (audio_data, sample_rate): 音频数据和采样率 + """ + file_ext = Path(file_path).suffix.lower() + + if file_ext == '.wav': + import wave + with wave.open(file_path, 'rb') as wav_file: + sample_rate = wav_file.getframerate() + n_channels = wav_file.getnchannels() + sample_width = wav_file.getsampwidth() + n_frames = wav_file.getnframes() + + # 读取音频数据 + audio_bytes = wav_file.readframes(n_frames) + + # 转换为numpy数组 + if sample_width == 2: # 16-bit + audio_int = np.frombuffer(audio_bytes, dtype=np.int16) + elif sample_width == 4: # 32-bit + audio_int = np.frombuffer(audio_bytes, dtype=np.int32) + else: + raise ValueError(f"不支持的采样位深: {sample_width}") + + # 转换为float32 (-1.0 to 1.0) + audio_float = audio_int.astype(np.float32) / (2 ** (8 * sample_width - 1)) + + # 处理多声道 + if n_channels > 1: + audio_float = audio_float.reshape(-1, n_channels) + if channel == 'left': + audio_float = audio_float[:, 0] + print(f"✓ 使用左声道") + elif channel == 'right': + audio_float = audio_float[:, 1] + print(f"✓ 使用右声道") + else: # stereo - 平均 + audio_float = np.mean(audio_float, axis=1) + print(f"✓ 使用立体声(双声道平均)") + else: + print(f"✓ 使用单声道") + + return audio_float, sample_rate + + else: + # 尝试使用 soundfile 或 librosa + try: + import soundfile as sf + audio_float, sample_rate = sf.read(file_path) + + if len(audio_float.shape) > 1: # 多声道 + if channel == 'left': + audio_float = audio_float[:, 0] + print(f"✓ 使用左声道") + elif channel == 'right': + audio_float = audio_float[:, 1] + print(f"✓ 使用右声道") + else: + audio_float = np.mean(audio_float, axis=1) + print(f"✓ 使用立体声(双声道平均)") + else: + print(f"✓ 使用单声道") + + return audio_float, sample_rate + + except ImportError: + print("错误: 请先运行 ./scripts/sync_cpu_env.sh 安装 soundfile 依赖") + sys.exit(1) + + +def calculate_rms_energy(audio_array: np.ndarray) -> float: + """计算音频RMS能量 + + Args: + audio_array: float32音频数组,范围-1.0到1.0 + + Returns: + RMS能量值 + """ + if len(audio_array) == 0: + return 0.0 + return float(np.sqrt(np.mean(audio_array ** 2))) + + +def analyze_rms_timeline(audio_data: np.ndarray, sample_rate: int, + chunk_size_ms: int = 240) -> tuple: + """分析音频的RMS时序 + + Args: + audio_data: 音频数据 + sample_rate: 采样率 + chunk_size_ms: 分块大小(毫秒) + + Returns: + (time_points, rms_values): 时间点和对应的RMS值 + """ + chunk_samples = int(sample_rate * chunk_size_ms / 1000) + n_chunks = len(audio_data) // chunk_samples + + time_points = [] + rms_values = [] + + for i in range(n_chunks): + start_idx = i * chunk_samples + end_idx = start_idx + chunk_samples + chunk = audio_data[start_idx:end_idx] + + rms = calculate_rms_energy(chunk) + time_s = (start_idx + chunk_samples / 2) / sample_rate + + time_points.append(time_s) + rms_values.append(rms) + + return np.array(time_points), np.array(rms_values) + + +def print_statistics(rms_values: np.ndarray, threshold: float = 0.01): + """打印RMS统计信息 + + Args: + rms_values: RMS值数组 + threshold: 阈值 + """ + print("\n" + "="*60) + print("RMS 统计分析") + print("="*60) + + print(f"\n📊 基础统计:") + print(f" - 最小值: {np.min(rms_values):.6f}") + print(f" - 最大值: {np.max(rms_values):.6f}") + print(f" - 平均值: {np.mean(rms_values):.6f}") + print(f" - 中位数: {np.median(rms_values):.6f}") + print(f" - 标准差: {np.std(rms_values):.6f}") + + print(f"\n📈 百分位数:") + for p in [10, 25, 50, 75, 90, 95, 99]: + value = np.percentile(rms_values, p) + print(f" - P{p:2d}: {value:.6f}") + + print(f"\n🎯 阈值分析 (当前阈值: {threshold:.6f}):") + above_threshold = np.sum(rms_values >= threshold) + below_threshold = np.sum(rms_values < threshold) + total = len(rms_values) + + print(f" - 超过阈值的帧数: {above_threshold} ({above_threshold/total*100:.1f}%)") + print(f" - 低于阈值的帧数: {below_threshold} ({below_threshold/total*100:.1f}%)") + + print(f"\n💡 建议的阈值范围:") + # 基于非零RMS值的统计 + non_zero_rms = rms_values[rms_values > 0.001] + if len(non_zero_rms) > 0: + p10 = np.percentile(non_zero_rms, 10) + p25 = np.percentile(non_zero_rms, 25) + mean = np.mean(non_zero_rms) + + print(f" - 保守模式 (高灵敏度): {p10:.6f} (P10)") + print(f" - 宽松模式 (推荐): {p25:.6f} (P25)") + print(f" - 严格模式 (低误触): {mean*0.5:.6f} (平均值的50%)") + + print("="*60 + "\n") + + +def plot_rms_timeline(time_points: np.ndarray, rms_values: np.ndarray, + threshold: float = 0.01, save_path: Optional[str] = None): + """绘制RMS时序图 + + Args: + time_points: 时间点数组 + rms_values: RMS值数组 + threshold: 阈值线 + save_path: 保存路径 + """ + _, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10)) + + # 上图: RMS时序 + ax1.plot(time_points, rms_values, linewidth=1, label='RMS Energy', color='steelblue') + ax1.axhline(y=threshold, color='red', linestyle='--', linewidth=2, + label=f'阈值 = {threshold:.6f}') + + # 标记超过阈值的区域 + above_threshold = rms_values >= threshold + ax1.fill_between(time_points, 0, rms_values, where=above_threshold, + alpha=0.3, color='green', label='近场音频 (>= 阈值)') + ax1.fill_between(time_points, 0, rms_values, where=~above_threshold, + alpha=0.3, color='red', label='远场音频 (< 阈值)') + + ax1.set_xlabel('时间 (秒)', fontsize=12) + ax1.set_ylabel('RMS 能量', fontsize=12) + ax1.set_title('音频 RMS 能量时序分析', fontsize=14, fontweight='bold') + ax1.legend(loc='upper right', fontsize=10) + ax1.grid(True, alpha=0.3) + ax1.set_ylim(bottom=0) + + # 下图: RMS分布直方图 + ax2.hist(rms_values, bins=100, color='steelblue', alpha=0.7, edgecolor='black') + ax2.axvline(x=threshold, color='red', linestyle='--', linewidth=2, + label=f'阈值 = {threshold:.6f}') + ax2.axvline(x=np.mean(rms_values), color='orange', linestyle=':', linewidth=2, + label=f'平均值 = {np.mean(rms_values):.6f}') + ax2.axvline(x=np.median(rms_values), color='green', linestyle=':', linewidth=2, + label=f'中位数 = {np.median(rms_values):.6f}') + + ax2.set_xlabel('RMS 能量', fontsize=12) + ax2.set_ylabel('帧数', fontsize=12) + ax2.set_title('RMS 能量分布直方图', fontsize=14, fontweight='bold') + ax2.legend(loc='upper right', fontsize=10) + ax2.grid(True, alpha=0.3, axis='y') + + plt.tight_layout() + + if save_path: + plt.savefig(save_path, dpi=150, bbox_inches='tight') + print(f"✓ 图表已保存到: {save_path}") + + plt.show() + + +def main(): + parser = argparse.ArgumentParser( + description='音频RMS时序分析工具 - 帮助确定远场过滤阈值', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +示例用法: + # 分析立体声音频(默认) + python analyze_audio_rms.py audio.wav + + # 仅分析左声道 + python analyze_audio_rms.py audio.wav --channel left + + # 仅分析右声道 + python analyze_audio_rms.py audio.wav --channel right + + # 自定义阈值和分块大小 + python analyze_audio_rms.py audio.wav --threshold 0.015 --chunk-size 160 + + # 保存图表 + python analyze_audio_rms.py audio.wav --output rms_analysis.png + """ + ) + + parser.add_argument('audio_file', type=str, + help='音频文件路径 (支持 WAV, MP3, FLAC 等格式)') + parser.add_argument('--channel', type=str, choices=['stereo', 'left', 'right'], + default='stereo', + help='声道选择: stereo(立体声平均), left(左声道), right(右声道) [默认: stereo]') + parser.add_argument('--threshold', type=float, default=0.01, + help='RMS能量阈值 [默认: 0.01]') + parser.add_argument('--chunk-size', type=int, default=240, + help='分块大小(毫秒) [默认: 240ms,与流式ASR一致]') + parser.add_argument('--output', '-o', type=str, default=None, + help='保存图表的路径 (例如: output.png)') + parser.add_argument('--no-plot', action='store_true', + help='不显示图表,仅输出统计信息') + + args = parser.parse_args() + + # 检查文件是否存在 + if not Path(args.audio_file).exists(): + print(f"错误: 文件不存在: {args.audio_file}") + sys.exit(1) + + print("="*60) + print("音频 RMS 时序分析工具") + print("="*60) + print(f"\n📁 文件: {args.audio_file}") + print(f"🎚️ 声道: {args.channel}") + print(f"📊 分块大小: {args.chunk_size}ms") + print(f"🎯 阈值: {args.threshold:.6f}") + print() + + # 加载音频 + print("正在加载音频...") + audio_data, sample_rate = load_audio(args.audio_file, args.channel) + duration = len(audio_data) / sample_rate + + print(f"✓ 采样率: {sample_rate} Hz") + print(f"✓ 时长: {duration:.2f} 秒") + print(f"✓ 样本数: {len(audio_data)}") + + # 分析RMS时序 + print(f"\n正在分析 RMS 时序 (分块大小: {args.chunk_size}ms)...") + time_points, rms_values = analyze_rms_timeline(audio_data, sample_rate, args.chunk_size) + print(f"✓ 分析了 {len(rms_values)} 个音频块") + + # 打印统计信息 + print_statistics(rms_values, args.threshold) + + # 绘制图表 + if not args.no_plot: + print("正在生成图表...") + plot_rms_timeline(time_points, rms_values, args.threshold, args.output) + elif args.output: + print("正在保存图表...") + plot_rms_timeline(time_points, rms_values, args.threshold, args.output) + # 关闭显示窗口 + plt.close() + + +if __name__ == '__main__': + main() diff --git a/scripts/benchmark/README.md b/scripts/benchmark/README.md new file mode 100644 index 0000000..2a5548e --- /dev/null +++ b/scripts/benchmark/README.md @@ -0,0 +1,198 @@ +# Qwen3-ASR 并发性能测试脚本 + +测试 ASR/TTS WebSocket 服务在不同并发级别下的性能表现。 + +## 依赖 + +```bash +./scripts/sync_cpu_env.sh +``` + +## 快速开始 + +### 1. 启动服务 + +```bash +# 在项目根目录 +source .venv/bin/activate +python start.py +``` + +### 2. 运行测试 + +```bash +# 完整测试 (ASR + TTS) +.venv/bin/python -m scripts.benchmark.run --audio-file /path/to/audio.wav + +# 仅测试 TTS (无需音频文件) +.venv/bin/python -m scripts.benchmark.run --test-type tts + +# 仅测试 ASR +.venv/bin/python -m scripts.benchmark.run --audio-file /path/to/audio.wav --test-type asr + +# Qwen Rust CPU 固定配置跑测(固定 VAD 分段) +.venv/bin/python -m scripts.benchmark.qwen_rust_sensitivity \ + --audio-file /path/to/audio.wav +``` + +## 命令行参数 + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `--host` | localhost | 服务器主机名 | +| `--port` | 8000 | 服务器端口 | +| `--audio-file` | - | ASR 测试音频文件路径 (测试 ASR 时必需) | +| `--test-type` | both | 测试类型: `asr` / `tts` / `both` | +| `--concurrency` | 5 10 20 50 | 并发级别列表 | +| `--output` | ./benchmark_results | 报告输出目录 | +| `--timeout` | 120 | 请求超时时间 (秒) | +| `--voice` | 中文女 | TTS 测试音色 | + +## Qwen Rust CPU 固定配置跑测 + +用于对 vendored Rust backend 做固定 workload 的端到端对比: + +- 同一组 VAD 分段 +- Rust ASR +- Rust forced align +- 跑当前环境变量指定的 Rust 并发配置 + +示例: + +```bash +# 直接跑当前配置 +.venv/bin/python -m scripts.benchmark.qwen_rust_sensitivity \ + --audio-file temp/podcast_demo_10min_16k.wav \ + --json-out temp/qwen_rust_runtime_config.json + +# 通过环境变量切换不同并发配置后再跑 +QWEN_RUST_CPU_WORKERS=8 \ +QWEN_RUST_ASR_CONCURRENCY=8 \ +QWEN_RUST_ALIGN_CONCURRENCY=4 \ +.venv/bin/python -m scripts.benchmark.qwen_rust_sensitivity \ + --audio-file temp/podcast_demo_10min_16k.wav \ + --json-out temp/qwen_rust_runtime_config.json +``` + +说明: + +- 脚本只跑当前一组配置 +- 并发度由 `QWEN_RUST_CPU_WORKERS`、`QWEN_RUST_ASR_CONCURRENCY`、`QWEN_RUST_ALIGN_CONCURRENCY` 控制 +- 跑完后会输出一条进度日志,并写入 `--json-out` +- 如果传了 `--json-out`,脚本还会自动生成同名 `.md` 中文对比报告 +- 也可以显式指定 Markdown 路径: + +```bash +.venv/bin/python -m scripts.benchmark.qwen_rust_sensitivity \ + --audio-file temp/podcast_demo_10min_16k.wav \ + --json-out temp/qwen_rust_runtime_config.json \ + --markdown-out temp/qwen_rust_runtime_config_report.md +``` + +### TTS 流式模拟配置 (config.py) + +TTS 测试模拟 LLM 流式输出场景,按标点符号(逗号、句号、顿号等)分割文本逐步发送: + +| 配置项 | 默认值 | 说明 | +|--------|--------|------| +| `tts_chunk_interval` | 0.05 | 发送间隔秒数 (模拟 LLM 生成速度) | + +## 使用示例 + +```bash +# 自定义并发级别 +.venv/bin/python -m scripts.benchmark.run \ + --audio-file test.wav \ + --concurrency 5 10 20 50 100 + +# 连接远程服务器 +.venv/bin/python -m scripts.benchmark.run \ + --host 192.168.1.100 \ + --port 8000 \ + --test-type tts + +# 使用不同音色测试 TTS +.venv/bin/python -m scripts.benchmark.run \ + --test-type tts \ + --voice 中文男 +``` + +## 测试指标 + +### ASR 指标 +- **首次响应延迟**: 从开始到收到第一个识别结果的时间 +- **总处理时间**: 从开始到识别完成的总时间 +- **RTF**: 处理时间 / 音频时长 (小于 1.0 表示快于实时) + +### TTS 指标 +- **首包延迟**: 从发送文本到收到第一个音频块的时间 +- **总合成时间**: 从开始到合成完成的总时间 +- **RTF**: 合成时间 / 生成音频时长 + +### 统计维度 +每个指标计算: 平均值 (Avg)、P50、P95、P99、最大值 (Max) + +## 输出文件 + +测试完成后在输出目录生成: + +``` +benchmark_results/ +├── benchmark_report_20241202_143000.md # Markdown 报告 +├── first_latency_20241202_143000.png # 首次响应延迟图 +├── rtf_20241202_143000.png # RTF 对比图 +├── throughput_20241202_143000.png # 吞吐量图 +└── total_time_20241202_143000.png # 总时间图 +``` + +## 报告示例 + +```markdown +## ASR 性能测试结果 + +### 延迟指标 (毫秒) + +| 并发数 | 首次响应 (Avg) | 首次响应 (P95) | 总时间 (Avg) | 总时间 (P95) | +|--------|---------------|---------------|-------------|-------------| +| 5 | 245.3 | 312.5 | 62345.2 | 63521.4 | +| 10 | 289.7 | 425.3 | 63245.8 | 65123.6 | + +### RTF 和吞吐量 + +| 并发数 | RTF (Avg) | RTF (P95) | 吞吐量 (req/s) | 成功率 | +|--------|----------|----------|---------------|--------| +| 5 | 1.04 | 1.06 | 0.08 | 100% | +| 10 | 1.05 | 1.08 | 0.16 | 100% | +``` + +## 目录结构 + +``` +scripts/benchmark/ +├── run.py # 主入口脚本 +├── config.py # 测试配置 +├── clients/ +│ ├── base_client.py # WebSocket 客户端基类 +│ ├── asr_client.py # ASR 测试客户端 +│ └── tts_client.py # TTS 测试客户端 +├── metrics/ +│ ├── models.py # 指标数据类 +│ └── statistics.py # 统计计算 +├── reporters/ +│ ├── markdown_reporter.py # Markdown 报告生成 +│ └── chart_generator.py # 图表生成 +└── utils/ + ├── audio_utils.py # 音频文件处理 + └── text_generator.py # 测试文本生成 +``` + +## 注意事项 + +1. **ASR 测试需要音频文件**: 建议使用 1 分钟左右的音频,格式支持 wav/mp3 等常见格式 +2. **TTS 测试自动生成文本**: 使用内置的中文随机句子生成器,无需额外准备 +3. **TTS 模拟流式输入**: 测试会按标点符号(逗号、句号、顿号等)分割文本逐步发送,模拟 LLM 流式输出场景 +4. **并发测试会占用资源**: 高并发测试时请确保服务器有足够资源 +5. **RTF 解读**: + - RTF < 1.0: 处理速度快于实时,性能良好 + - RTF ≈ 1.0: 刚好实时处理 + - RTF > 1.0: 处理速度慢于实时,可能出现延迟累积 diff --git a/scripts/benchmark/__init__.py b/scripts/benchmark/__init__.py new file mode 100644 index 0000000..35afc3c --- /dev/null +++ b/scripts/benchmark/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" +Qwen3-ASR 并发性能测试脚本 + +用于测试 ASR/TTS WebSocket 服务在不同并发级别下的性能表现。 +""" + +__version__ = "1.0.0" diff --git a/scripts/benchmark/clients/__init__.py b/scripts/benchmark/clients/__init__.py new file mode 100644 index 0000000..46938a9 --- /dev/null +++ b/scripts/benchmark/clients/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from .base_client import BaseWebSocketClient +from .asr_client import ASRWebSocketClient +from .tts_client import TTSWebSocketClient + +__all__ = ["BaseWebSocketClient", "ASRWebSocketClient", "TTSWebSocketClient"] diff --git a/scripts/benchmark/clients/asr_client.py b/scripts/benchmark/clients/asr_client.py new file mode 100644 index 0000000..b053c73 --- /dev/null +++ b/scripts/benchmark/clients/asr_client.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +""" +ASR WebSocket 测试客户端 +""" + +import asyncio +import json +import time +import logging +from pathlib import Path +from typing import Optional + +from .base_client import BaseWebSocketClient +from ..metrics.models import ASRMetrics + +logger = logging.getLogger(__name__) + +# 协议常量 +ASR_NAMESPACE = "SpeechTranscriber" +MSG_START_TRANSCRIPTION = "StartTranscription" +MSG_STOP_TRANSCRIPTION = "StopTranscription" +MSG_TRANSCRIPTION_STARTED = "TranscriptionStarted" +MSG_TRANSCRIPTION_RESULT_CHANGED = "TranscriptionResultChanged" +MSG_SENTENCE_END = "SentenceEnd" +MSG_TRANSCRIPTION_COMPLETED = "TranscriptionCompleted" +MSG_TASK_FAILED = "TaskFailed" + + +class ASRWebSocketClient(BaseWebSocketClient): + """ASR WebSocket 测试客户端""" + + def __init__( + self, + ws_url: str, + audio_data: bytes, + audio_duration_ms: float, + sample_rate: int = 16000, + chunk_size: int = 9600, + timeout: float = 120.0, + save_result_dir: Optional[Path] = None, # 保存识别结果的目录 + ): + """ + 初始化 ASR 客户端 + + Args: + ws_url: WebSocket URL + audio_data: PCM 音频数据 + audio_duration_ms: 音频时长 (毫秒) + sample_rate: 采样率 + chunk_size: 每块采样数 + timeout: 超时时间 (秒) + save_result_dir: 保存识别结果的目录 + """ + super().__init__(ws_url, timeout) + self.audio_data = audio_data + self.audio_duration_ms = audio_duration_ms + self.sample_rate = sample_rate + self.chunk_size = chunk_size + self.chunk_bytes = chunk_size * 2 # 16-bit PCM + self.save_result_dir = save_result_dir + + async def run_test(self) -> ASRMetrics: + """ + 执行 ASR 测试 + + Returns: + ASR 测试指标 + """ + metrics = ASRMetrics( + request_id=self.task_id, + concurrency_level=0, # 由调用者设置 + start_time=time.perf_counter(), + audio_duration_ms=self.audio_duration_ms, + ) + + try: + await asyncio.wait_for( + self._run_asr_session(metrics), + timeout=self.timeout, + ) + metrics.success = True + except asyncio.TimeoutError: + metrics.error_message = "Timeout" + logger.warning(f"ASR 请求超时: {self.task_id}") + except Exception as e: + metrics.error_message = str(e) + logger.warning(f"ASR 请求失败: {self.task_id}, 错误: {e}") + finally: + await self.close() + + return metrics + + async def _run_asr_session(self, metrics: ASRMetrics) -> None: + """运行完整的 ASR 会话""" + await self.connect() + + # 1. 发送 StartTranscription + await self._send_start_transcription() + + # 2. 等待 TranscriptionStarted + await self.wait_for_message(MSG_TRANSCRIPTION_STARTED) + + # 3. 启动音频流式发送任务 + stream_task = asyncio.create_task(self._stream_audio()) + + # 4. 接收识别结果 + try: + await self._receive_results(metrics) + finally: + # 确保流式任务完成 + if not stream_task.done(): + stream_task.cancel() + try: + await stream_task + except asyncio.CancelledError: + pass + + async def _send_start_transcription(self) -> None: + """发送 StartTranscription 消息""" + message = { + "header": self._create_header(MSG_START_TRANSCRIPTION, ASR_NAMESPACE), + "payload": { + "format": "pcm", + "sample_rate": self.sample_rate, + "enable_intermediate_result": True, + "enable_punctuation_prediction": True, + "enable_inverse_text_normalization": True, + "max_sentence_silence": 800, + }, + } + await self.send_json(message) + + async def _stream_audio(self) -> None: + """流式发送音频数据""" + offset = 0 + chunk_duration = self.chunk_size / self.sample_rate # 秒 + + while offset < len(self.audio_data): + chunk = self.audio_data[offset : offset + self.chunk_bytes] + await self.send_bytes(chunk) + offset += self.chunk_bytes + + # 以接近实时的速度发送 (稍快一些) + await asyncio.sleep(chunk_duration * 0.5) + + # 发送 StopTranscription + await self._send_stop_transcription() + + async def _send_stop_transcription(self) -> None: + """发送 StopTranscription 消息""" + message = { + "header": self._create_header(MSG_STOP_TRANSCRIPTION, ASR_NAMESPACE), + } + await self.send_json(message) + + async def _receive_results(self, metrics: ASRMetrics) -> None: + """接收识别结果""" + while True: + response = await self.receive() + + if isinstance(response, str): + data = json.loads(response) + header = data.get("header", {}) + name = header.get("name", "") + payload = data.get("payload", {}) + + if name == MSG_TRANSCRIPTION_RESULT_CHANGED: + # 记录首次响应时间 + if metrics.first_result_time is None: + metrics.first_result_time = time.perf_counter() + + elif name == MSG_SENTENCE_END: + metrics.sentence_end_time = time.perf_counter() + # 更新识别结果 + result = payload.get("result", "") + if result: + metrics.result_text = result + + elif name == MSG_TRANSCRIPTION_COMPLETED: + metrics.complete_time = time.perf_counter() + # 保存识别结果 + if self.save_result_dir and metrics.result_text: + self._save_result(metrics.result_text) + break + + elif name == MSG_TASK_FAILED: + status_text = header.get("status_text", "Unknown error") + raise Exception(f"TaskFailed: {status_text}") + + def _save_result(self, result_text: str) -> None: + """保存识别结果到文本文件""" + if self.save_result_dir is None: + return + + try: + filename = f"{self.task_id[:8]}_{int(self.audio_duration_ms)}ms.txt" + filepath = self.save_result_dir / filename + + with open(filepath, 'w', encoding='utf-8') as f: + f.write(result_text) + + logger.debug(f"识别结果已保存: {filepath}") + except Exception as e: + logger.warning(f"保存识别结果失败: {e}") diff --git a/scripts/benchmark/clients/base_client.py b/scripts/benchmark/clients/base_client.py new file mode 100644 index 0000000..d95abfb --- /dev/null +++ b/scripts/benchmark/clients/base_client.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +""" +WebSocket 客户端基类 +""" + +import json +import uuid +import logging +from abc import ABC, abstractmethod +from typing import Optional, Any, Dict + +from websockets.legacy.client import connect, WebSocketClientProtocol # type: ignore + +logger = logging.getLogger(__name__) + + +class BaseWebSocketClient(ABC): + """WebSocket 测试客户端基类""" + + def __init__(self, ws_url: str, timeout: float = 120.0): + """ + 初始化客户端 + + Args: + ws_url: WebSocket URL + timeout: 超时时间 (秒) + """ + self.ws_url = ws_url + self.timeout = timeout + self.websocket: Optional[WebSocketClientProtocol] = None + self.task_id = self._generate_id() + + @staticmethod + def _generate_id() -> str: + """生成 32 位唯一 ID""" + return str(uuid.uuid4()).replace("-", "")[:32] + + async def connect(self) -> None: + """建立 WebSocket 连接""" + self.websocket = await connect( + self.ws_url, + ping_interval=None, + ping_timeout=None, + max_size=10 * 1024 * 1024, # 10MB + ) + + async def close(self) -> None: + """关闭 WebSocket 连接""" + if self.websocket: + try: + await self.websocket.close() + except Exception: + pass + self.websocket = None + + async def send_json(self, data: Dict[str, Any]) -> None: + """发送 JSON 消息""" + if self.websocket: + await self.websocket.send(json.dumps(data, ensure_ascii=False)) + + async def send_bytes(self, data: bytes) -> None: + """发送二进制数据""" + if self.websocket: + await self.websocket.send(data) + + async def receive(self) -> Any: + """接收消息 (JSON 或二进制)""" + if self.websocket: + return await self.websocket.recv() + return None + + async def receive_json(self) -> Optional[Dict[str, Any]]: + """接收 JSON 消息""" + data = await self.receive() + if isinstance(data, str): + return json.loads(data) + return None + + async def wait_for_message(self, expected_name: str) -> Dict[str, Any]: + """ + 等待指定名称的消息 + + Args: + expected_name: 期望的消息名称 + + Returns: + 消息数据 + + Raises: + Exception: 收到 TaskFailed 消息 + """ + while True: + response = await self.receive() + if isinstance(response, str): + data = json.loads(response) + header = data.get("header", {}) + name = header.get("name", "") + + if name == expected_name: + return data + elif name == "TaskFailed": + status_text = header.get("status_text", "Unknown error") + raise Exception(f"TaskFailed: {status_text}") + + def _create_header(self, name: str, namespace: str) -> Dict[str, Any]: + """ + 创建消息头部 + + Args: + name: 消息名称 + namespace: 命名空间 + + Returns: + 头部字典 + """ + return { + "message_id": self._generate_id(), + "task_id": self.task_id, + "namespace": namespace, + "name": name, + } + + @abstractmethod + async def run_test(self) -> Any: + """ + 执行测试 + + Returns: + 测试指标 + """ + pass diff --git a/scripts/benchmark/clients/tts_client.py b/scripts/benchmark/clients/tts_client.py new file mode 100644 index 0000000..0ef6075 --- /dev/null +++ b/scripts/benchmark/clients/tts_client.py @@ -0,0 +1,251 @@ +# -*- coding: utf-8 -*- +""" +TTS WebSocket 测试客户端 + +参考 realtime-llm/backend/cti_websocket_handler.py 中的调用方式。 +模拟 LLM 流式输出场景,按句子发送文本进行合成。 +""" + +import asyncio +import json +import time +import logging +import wave +from pathlib import Path +from typing import Optional + +from .base_client import BaseWebSocketClient +from ..metrics.models import TTSMetrics + +logger = logging.getLogger(__name__) + +# 协议常量 +TTS_NAMESPACE = "FlowingSpeechSynthesizer" +MSG_START_SYNTHESIS = "StartSynthesis" +MSG_RUN_SYNTHESIS = "RunSynthesis" +MSG_STOP_SYNTHESIS = "StopSynthesis" +MSG_SYNTHESIS_STARTED = "SynthesisStarted" +MSG_SENTENCE_BEGIN = "SentenceBegin" +MSG_SENTENCE_END = "SentenceEnd" +MSG_SYNTHESIS_COMPLETED = "SynthesisCompleted" +MSG_TASK_FAILED = "TaskFailed" + + +class TTSWebSocketClient(BaseWebSocketClient): + """TTS WebSocket 测试客户端 (模拟流式文本输入)""" + + def __init__( + self, + ws_url: str, + text: str, + voice: str = "中文女", + audio_format: str = "PCM", + sample_rate: int = 22050, + timeout: float = 120.0, + chunk_interval: float = 0.05, # 发送间隔 (秒),模拟 LLM 生成速度 + debug: bool = False, # 调试模式 + save_audio_dir: Optional[Path] = None, # 保存音频的目录 + ): + super().__init__(ws_url, timeout) + self.text = text + self.voice = voice + self.audio_format = audio_format + self.sample_rate = sample_rate + self.chunk_interval = chunk_interval + self.debug = debug + self.save_audio_dir = save_audio_dir + self._audio_chunks = [] # 存储接收到的音频数据 + + def _log(self, msg: str): + """调试日志""" + if self.debug: + logger.info(f"[{self.task_id[:8]}] {msg}") + + async def run_test(self) -> TTSMetrics: + """执行 TTS 测试""" + metrics = TTSMetrics( + request_id=self.task_id, + concurrency_level=0, + start_time=time.perf_counter(), + text_length=len(self.text), + sample_rate=self.sample_rate, + ) + + try: + await asyncio.wait_for( + self._run_tts_session(metrics), + timeout=self.timeout, + ) + metrics.success = True + except asyncio.TimeoutError: + metrics.error_message = "Timeout" + logger.warning(f"TTS 请求超时: {self.task_id}") + except Exception as e: + metrics.error_message = str(e) + logger.warning(f"TTS 请求失败: {self.task_id}, 错误: {e}") + finally: + await self.close() + + return metrics + + async def _run_tts_session(self, metrics: TTSMetrics) -> None: + """运行完整的 TTS 会话""" + self._log(f"连接 {self.ws_url}") + await self.connect() + + # 用于同步的事件 + started_event = asyncio.Event() + completed_event = asyncio.Event() + error_message = None + + # 1. 发送 StartSynthesis + await self._send_start_synthesis() + + # 2. 启动接收任务 + async def receive_loop(): + nonlocal error_message + while True: + try: + response = await self.receive() + except Exception as e: + self._log(f"接收异常: {e}") + break + + if isinstance(response, bytes): + if metrics.first_chunk_time is None: + metrics.first_chunk_time = time.perf_counter() + metrics.audio_bytes_received += len(response) + # 收集音频数据用于保存 + if self.save_audio_dir: + self._audio_chunks.append(response) + self._log(f"← 收到音频: {len(response)} bytes") + + elif isinstance(response, str): + try: + data = json.loads(response) + header = data.get("header", {}) + name = header.get("name", "") + status = header.get("status", 0) + + self._log(f"← 收到事件: {name} (status={status})") + + if name == MSG_SYNTHESIS_STARTED: + started_event.set() + + elif name == MSG_SENTENCE_END: + metrics.sentence_end_time = time.perf_counter() + + elif name == MSG_SYNTHESIS_COMPLETED: + completed_event.set() + break + + elif name == MSG_TASK_FAILED: + status_text = header.get("status_text", "Unknown error") + error_message = f"TaskFailed: {status_text}" + self._log(f"← 错误: {status_text}") + completed_event.set() + break + + except json.JSONDecodeError: + pass + + receive_task = asyncio.create_task(receive_loop()) + + try: + # 3. 等待 SynthesisStarted + self._log("等待 SynthesisStarted...") + await asyncio.wait_for(started_event.wait(), timeout=10.0) + self._log("收到 SynthesisStarted") + + # 4. 发送文本 - 直接发送完整文本,不分割 + # 参考 CTI 客户端:发送完整句子而不是切分片段 + await self._send_run_synthesis(self.text) + + # 5. 发送 StopSynthesis + await self._send_stop_synthesis() + + # 6. 等待 SynthesisCompleted + self._log("等待 SynthesisCompleted...") + await completed_event.wait() + + if error_message: + raise Exception(error_message) + + metrics.complete_time = time.perf_counter() + self._log(f"完成! 收到 {metrics.audio_bytes_received} bytes 音频") + + # 保存音频文件 + if self.save_audio_dir and self._audio_chunks: + self._save_audio() + + finally: + if not receive_task.done(): + receive_task.cancel() + try: + await receive_task + except asyncio.CancelledError: + pass + + async def _send_start_synthesis(self) -> None: + """发送 StartSynthesis 消息""" + message = { + "header": self._create_header(MSG_START_SYNTHESIS, TTS_NAMESPACE), + "payload": { + "voice": self.voice, + "format": self.audio_format, + "sample_rate": self.sample_rate, + "volume": 50, + "speech_rate": 0, + "pitch_rate": 0, + "platform": "python", + }, + } + self._log(f"→ 发送 StartSynthesis (voice={self.voice}, format={self.audio_format})") + await self.send_json(message) + + async def _send_run_synthesis(self, text: str) -> None: + """发送 RunSynthesis 消息""" + message = { + "header": self._create_header(MSG_RUN_SYNTHESIS, TTS_NAMESPACE), + "payload": { + "text": text, + }, + } + # 截断显示 + display_text = text[:50] + "..." if len(text) > 50 else text + self._log(f"→ 发送 RunSynthesis: \"{display_text}\" ({len(text)} chars)") + await self.send_json(message) + + async def _send_stop_synthesis(self) -> None: + """发送 StopSynthesis 消息""" + message = { + "header": self._create_header(MSG_STOP_SYNTHESIS, TTS_NAMESPACE), + } + self._log("→ 发送 StopSynthesis") + await self.send_json(message) + + def _save_audio(self) -> None: + """保存收到的音频数据为 WAV 文件""" + if self.save_audio_dir is None: + return + + try: + # 合并所有音频块 + audio_data = b"".join(self._audio_chunks) + if not audio_data: + return + + # 生成文件名 + filename = f"{self.task_id[:8]}_{len(self.text)}chars.wav" + filepath = self.save_audio_dir / filename + + # PCM 数据保存为 WAV + with wave.open(str(filepath), 'wb') as wav_file: + wav_file.setnchannels(1) # 单声道 + wav_file.setsampwidth(2) # 16位 = 2字节 + wav_file.setframerate(self.sample_rate) + wav_file.writeframes(audio_data) + + self._log(f"音频已保存: {filepath}") + except Exception as e: + logger.warning(f"保存音频失败: {e}") diff --git a/scripts/benchmark/config.py b/scripts/benchmark/config.py new file mode 100644 index 0000000..bb1341a --- /dev/null +++ b/scripts/benchmark/config.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +""" +测试配置模块 +""" + +from dataclasses import dataclass, field +from typing import List, Optional +from pathlib import Path + + +@dataclass +class TestConfig: + """测试配置""" + + # 服务器配置 + host: str = "localhost" + port: int = 8000 + timeout_seconds: float = 300.0 # 默认 5 分钟,并发 TTS 可能需要更长时间 + warmup_requests: int = 3 + + # 并发配置 + concurrency_levels: List[int] = field(default_factory=lambda: [5, 10, 20, 50]) + + # ASR 配置 + asr_audio_file: Optional[Path] = None + asr_sample_rate: int = 16000 + asr_chunk_size: int = 9600 # 600ms @ 16kHz + asr_format: str = "pcm" + + # TTS 配置 + tts_text_count: int = 50 # 预生成的测试文本数量 + tts_text_length_range: tuple = (50, 100) # 文本字符数范围 + tts_voice: str = "中文女" + tts_format: str = "PCM" + tts_sample_rate: int = 22050 + tts_chunk_interval: float = 0.05 # 发送间隔秒数 (模拟 LLM 生成速度) + + # 输出配置 + output_dir: Path = field(default_factory=lambda: Path("./benchmark_results")) + report_name: str = "benchmark_report" + + @property + def ws_base_url(self) -> str: + """WebSocket 基础 URL""" + return f"ws://{self.host}:{self.port}" + + @property + def asr_ws_url(self) -> str: + """ASR WebSocket URL""" + return f"{self.ws_base_url}/ws/v1/asr" + + @property + def tts_ws_url(self) -> str: + """TTS WebSocket URL""" + return f"{self.ws_base_url}/ws/v1/tts" + + def validate(self, test_type: str = "both") -> None: + """ + 验证配置 + + Args: + test_type: 测试类型 (asr/tts/both) + + Raises: + ValueError: 配置无效 + """ + if test_type in ("asr", "both"): + if self.asr_audio_file is None: + raise ValueError("ASR 测试需要提供音频文件路径 (--audio-file)") + if not self.asr_audio_file.exists(): + raise ValueError(f"音频文件不存在: {self.asr_audio_file}") + + if not self.concurrency_levels: + raise ValueError("至少需要一个并发级别") + + for level in self.concurrency_levels: + if level < 1: + raise ValueError(f"并发级别必须大于 0: {level}") + + if self.timeout_seconds <= 0: + raise ValueError("超时时间必须大于 0") diff --git a/scripts/benchmark/metrics/__init__.py b/scripts/benchmark/metrics/__init__.py new file mode 100644 index 0000000..c3a8551 --- /dev/null +++ b/scripts/benchmark/metrics/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +from .models import ASRMetrics, TTSMetrics, AggregatedMetrics +from .statistics import calculate_statistics, calculate_percentile + +__all__ = [ + "ASRMetrics", + "TTSMetrics", + "AggregatedMetrics", + "calculate_statistics", + "calculate_percentile", +] diff --git a/scripts/benchmark/metrics/models.py b/scripts/benchmark/metrics/models.py new file mode 100644 index 0000000..0aa7e62 --- /dev/null +++ b/scripts/benchmark/metrics/models.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +""" +性能指标数据类 +""" + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class ASRMetrics: + """ASR 单次请求指标""" + + request_id: str + concurrency_level: int + start_time: float # time.perf_counter() + audio_duration_ms: float = 0.0 + + # 时间戳 + first_result_time: Optional[float] = None # 第一个 TranscriptionResultChanged + sentence_end_time: Optional[float] = None # SentenceEnd + complete_time: Optional[float] = None # TranscriptionCompleted + + # 结果 + result_text: str = "" + success: bool = False + error_message: str = "" + + @property + def first_result_latency_ms(self) -> Optional[float]: + """首次响应延迟 (ms)""" + if self.first_result_time is not None: + return (self.first_result_time - self.start_time) * 1000 + return None + + @property + def total_processing_time_ms(self) -> Optional[float]: + """总处理时间 (ms)""" + if self.complete_time is not None: + return (self.complete_time - self.start_time) * 1000 + return None + + @property + def rtf(self) -> Optional[float]: + """RTF (Real-Time Factor) = 处理时间 / 音频时长""" + total_time = self.total_processing_time_ms + if total_time is not None and self.audio_duration_ms > 0: + return total_time / self.audio_duration_ms + return None + + +@dataclass +class TTSMetrics: + """TTS 单次请求指标""" + + request_id: str + concurrency_level: int + start_time: float # time.perf_counter() + text_length: int = 0 + sample_rate: int = 22050 + + # 时间戳 + first_chunk_time: Optional[float] = None # 第一个音频二进制块 + sentence_end_time: Optional[float] = None # SentenceEnd + complete_time: Optional[float] = None # SynthesisCompleted + + # 结果 + audio_bytes_received: int = 0 + success: bool = False + error_message: str = "" + + @property + def first_chunk_latency_ms(self) -> Optional[float]: + """首包延迟 (ms)""" + if self.first_chunk_time is not None: + return (self.first_chunk_time - self.start_time) * 1000 + return None + + @property + def total_synthesis_time_ms(self) -> Optional[float]: + """总合成时间 (ms)""" + if self.complete_time is not None: + return (self.complete_time - self.start_time) * 1000 + return None + + @property + def estimated_audio_duration_ms(self) -> float: + """估算的音频时长 (基于采样率和字节数)""" + if self.audio_bytes_received > 0: + # PCM 16-bit mono: 2 bytes per sample + samples = self.audio_bytes_received / 2 + return (samples / self.sample_rate) * 1000 + return 0.0 + + @property + def rtf(self) -> Optional[float]: + """RTF = 合成时间 / 生成音频时长""" + total_time = self.total_synthesis_time_ms + audio_duration = self.estimated_audio_duration_ms + if total_time is not None and audio_duration > 0: + return total_time / audio_duration + return None + + +@dataclass +class AggregatedMetrics: + """聚合后的指标 (针对一个并发级别)""" + + test_type: str # "asr" or "tts" + concurrency_level: int + total_requests: int + successful_requests: int + failed_requests: int + total_test_time_seconds: float + + # 首次延迟统计 (ms) + first_latency_avg: float = 0.0 + first_latency_p50: float = 0.0 + first_latency_p95: float = 0.0 + first_latency_p99: float = 0.0 + first_latency_max: float = 0.0 + + # 总时间统计 (ms) + total_time_avg: float = 0.0 + total_time_p50: float = 0.0 + total_time_p95: float = 0.0 + total_time_p99: float = 0.0 + total_time_max: float = 0.0 + + # RTF 统计 + rtf_avg: float = 0.0 + rtf_p50: float = 0.0 + rtf_p95: float = 0.0 + rtf_p99: float = 0.0 + rtf_max: float = 0.0 + + @property + def success_rate(self) -> float: + """成功率 (%)""" + if self.total_requests > 0: + return (self.successful_requests / self.total_requests) * 100 + return 0.0 + + @property + def throughput(self) -> float: + """吞吐量 (成功请求数/秒)""" + if self.total_test_time_seconds > 0: + return self.successful_requests / self.total_test_time_seconds + return 0.0 diff --git a/scripts/benchmark/metrics/statistics.py b/scripts/benchmark/metrics/statistics.py new file mode 100644 index 0000000..69e8da5 --- /dev/null +++ b/scripts/benchmark/metrics/statistics.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +""" +统计计算模块 +""" + +from typing import List, Union +import numpy as np + +from .models import ASRMetrics, TTSMetrics, AggregatedMetrics + + +def calculate_percentile(values: List[float], percentile: float) -> float: + """ + 计算百分位数 + + Args: + values: 数值列表 + percentile: 百分位 (0-100) + + Returns: + 百分位值 + """ + if not values: + return 0.0 + return float(np.percentile(values, percentile)) + + +def calculate_asr_statistics( + metrics_list: List[ASRMetrics], + concurrency_level: int, + total_test_time: float, +) -> AggregatedMetrics: + """ + 计算 ASR 指标统计 + + Args: + metrics_list: ASR 指标列表 + concurrency_level: 并发级别 + total_test_time: 总测试时间 (秒) + + Returns: + 聚合后的指标 + """ + successful = [m for m in metrics_list if m.success] + failed = [m for m in metrics_list if not m.success] + + # 提取各项指标值 + first_latencies = [ + m.first_result_latency_ms for m in successful if m.first_result_latency_ms is not None + ] + total_times = [ + m.total_processing_time_ms for m in successful if m.total_processing_time_ms is not None + ] + rtfs = [m.rtf for m in successful if m.rtf is not None] + + return AggregatedMetrics( + test_type="asr", + concurrency_level=concurrency_level, + total_requests=len(metrics_list), + successful_requests=len(successful), + failed_requests=len(failed), + total_test_time_seconds=total_test_time, + # 首次延迟 + first_latency_avg=float(np.mean(first_latencies)) if first_latencies else 0.0, + first_latency_p50=calculate_percentile(first_latencies, 50), + first_latency_p95=calculate_percentile(first_latencies, 95), + first_latency_p99=calculate_percentile(first_latencies, 99), + first_latency_max=max(first_latencies) if first_latencies else 0.0, + # 总时间 + total_time_avg=float(np.mean(total_times)) if total_times else 0.0, + total_time_p50=calculate_percentile(total_times, 50), + total_time_p95=calculate_percentile(total_times, 95), + total_time_p99=calculate_percentile(total_times, 99), + total_time_max=max(total_times) if total_times else 0.0, + # RTF + rtf_avg=float(np.mean(rtfs)) if rtfs else 0.0, + rtf_p50=calculate_percentile(rtfs, 50), + rtf_p95=calculate_percentile(rtfs, 95), + rtf_p99=calculate_percentile(rtfs, 99), + rtf_max=max(rtfs) if rtfs else 0.0, + ) + + +def calculate_tts_statistics( + metrics_list: List[TTSMetrics], + concurrency_level: int, + total_test_time: float, +) -> AggregatedMetrics: + """ + 计算 TTS 指标统计 + + Args: + metrics_list: TTS 指标列表 + concurrency_level: 并发级别 + total_test_time: 总测试时间 (秒) + + Returns: + 聚合后的指标 + """ + successful = [m for m in metrics_list if m.success] + failed = [m for m in metrics_list if not m.success] + + # 提取各项指标值 + first_latencies = [ + m.first_chunk_latency_ms for m in successful if m.first_chunk_latency_ms is not None + ] + total_times = [ + m.total_synthesis_time_ms for m in successful if m.total_synthesis_time_ms is not None + ] + rtfs = [m.rtf for m in successful if m.rtf is not None] + + return AggregatedMetrics( + test_type="tts", + concurrency_level=concurrency_level, + total_requests=len(metrics_list), + successful_requests=len(successful), + failed_requests=len(failed), + total_test_time_seconds=total_test_time, + # 首包延迟 + first_latency_avg=float(np.mean(first_latencies)) if first_latencies else 0.0, + first_latency_p50=calculate_percentile(first_latencies, 50), + first_latency_p95=calculate_percentile(first_latencies, 95), + first_latency_p99=calculate_percentile(first_latencies, 99), + first_latency_max=max(first_latencies) if first_latencies else 0.0, + # 总时间 + total_time_avg=float(np.mean(total_times)) if total_times else 0.0, + total_time_p50=calculate_percentile(total_times, 50), + total_time_p95=calculate_percentile(total_times, 95), + total_time_p99=calculate_percentile(total_times, 99), + total_time_max=max(total_times) if total_times else 0.0, + # RTF + rtf_avg=float(np.mean(rtfs)) if rtfs else 0.0, + rtf_p50=calculate_percentile(rtfs, 50), + rtf_p95=calculate_percentile(rtfs, 95), + rtf_p99=calculate_percentile(rtfs, 99), + rtf_max=max(rtfs) if rtfs else 0.0, + ) + + +def calculate_statistics( + metrics_list: Union[List[ASRMetrics], List[TTSMetrics]], + concurrency_level: int, + total_test_time: float, +) -> AggregatedMetrics: + """ + 通用统计计算函数 + + Args: + metrics_list: 指标列表 (ASR 或 TTS) + concurrency_level: 并发级别 + total_test_time: 总测试时间 (秒) + + Returns: + 聚合后的指标 + """ + if not metrics_list: + raise ValueError("指标列表不能为空") + + if isinstance(metrics_list[0], ASRMetrics): + # 类型缩窄:确保类型检查器知道这是 List[ASRMetrics] + asr_metrics_list: List[ASRMetrics] = [m for m in metrics_list if isinstance(m, ASRMetrics)] + return calculate_asr_statistics(asr_metrics_list, concurrency_level, total_test_time) + elif isinstance(metrics_list[0], TTSMetrics): + # 类型缩窄:确保类型检查器知道这是 List[TTSMetrics] + tts_metrics_list: List[TTSMetrics] = [m for m in metrics_list if isinstance(m, TTSMetrics)] + return calculate_tts_statistics(tts_metrics_list, concurrency_level, total_test_time) + else: + raise TypeError(f"不支持的指标类型: {type(metrics_list[0])}") diff --git a/scripts/benchmark/qwen_rust_sensitivity.py b/scripts/benchmark/qwen_rust_sensitivity.py new file mode 100644 index 0000000..b9e13c0 --- /dev/null +++ b/scripts/benchmark/qwen_rust_sensitivity.py @@ -0,0 +1,313 @@ +# -*- coding: utf-8 -*- +"""Qwen Rust CPU end-to-end benchmark for the current runtime configuration.""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +from dataclasses import asdict, dataclass +from pathlib import Path + +from app.core.config import settings +from app.services.asr.qwen3_engine import Qwen3ASREngine +from app.utils.audio import get_audio_duration +from app.utils.audio_splitter import AudioSplitter + + +@dataclass +class WorkerBenchRow: + cpu_count: int + rust_workers: int + asr_concurrency: int + align_concurrency: int + audio_file: str + audio_duration_sec: float + batch_size: int + engine_init_sec: float + vad_sec: float + vad_segments: int + asr_sec: float + asr_calls: int + align_sec: float + align_calls: int + total_sec: float + rtf: float + segments: int + word_tokens: int + text_len: int + + +def _persist_rows(rows: list[WorkerBenchRow], json_out: Path | None) -> None: + if json_out is None: + return + payload = [asdict(row) for row in rows] + tmp_path = json_out.with_suffix(f"{json_out.suffix}.tmp") + tmp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") + tmp_path.replace(json_out) + + +def _render_markdown(rows: list[WorkerBenchRow]) -> str: + if not rows: + return "# Qwen Rust CPU 对比结果\n\n暂无结果。\n" + + audio_file = rows[0].audio_file + audio_duration_sec = rows[0].audio_duration_sec + batch_size = rows[0].batch_size + cpu_count = rows[0].cpu_count + + lines = [ + "# Qwen Rust CPU 对比结果", + "", + f"- 音频文件:`{audio_file}`", + f"- 音频时长:`{audio_duration_sec:.2f}s`", + f"- batch size:`{batch_size}`", + f"- CPU 数量:`{cpu_count}`", + "", + f"- Rust workers:`{rows[0].rust_workers}`", + f"- ASR concurrency:`{rows[0].asr_concurrency}`", + f"- Align concurrency:`{rows[0].align_concurrency}`", + "", + "| total_sec | RTF | engine_init_sec | vad_sec | asr_sec | align_sec | segments | word_tokens | text_len |", + "|---:|---:|---:|---:|---:|---:|---:|---:|---:|", + ] + + for row in rows: + lines.append( + "| " + f"{row.total_sec:.2f} | " + f"{row.rtf:.4f} | " + f"{row.engine_init_sec:.3f} | " + f"{row.vad_sec:.3f} | " + f"{row.asr_sec:.2f} | " + f"{row.align_sec:.2f} | " + f"{row.segments} | " + f"{row.word_tokens} | " + f"{row.text_len} |" + ) + + row = rows[0] + + lines.extend( + [ + "", + "## 结论", + "", + f"- 当前配置:workers=`{row.rust_workers}` / asr=`{row.asr_concurrency}` / align=`{row.align_concurrency}`", + f"- 总耗时:`{row.total_sec:.2f}s`", + f"- RTF:`{row.rtf:.4f}`", + "", + ] + ) + return "\n".join(lines) + + +def _persist_markdown(rows: list[WorkerBenchRow], markdown_out: Path | None) -> None: + if markdown_out is None: + return + tmp_path = markdown_out.with_suffix(f"{markdown_out.suffix}.tmp") + tmp_path.write_text(_render_markdown(rows), encoding="utf-8") + tmp_path.replace(markdown_out) + + +def _log_progress(row: WorkerBenchRow) -> None: + print( + ( + f"[bench] workers={row.rust_workers} " + f"asr={row.asr_concurrency} " + f"align={row.align_concurrency} " + f"total={row.total_sec:.2f}s " + f"rtf={row.rtf:.4f} " + f"asr_sec={row.asr_sec:.2f}s " + f"align_sec={row.align_sec:.2f}s " + f"segments={row.segments} " + f"words={row.word_tokens}" + ), + file=sys.stderr, + flush=True, +) + + +def _clean_segments(segments: list) -> None: + AudioSplitter.cleanup_segments(segments) + + +def _prepare_segments(audio_file: Path) -> tuple[float, float, list]: + duration = get_audio_duration(str(audio_file)) + splitter = AudioSplitter(device="cpu") + t0 = time.perf_counter() + segments = splitter.split_audio_file(str(audio_file)) + vad_sec = time.perf_counter() - t0 + if not segments: + raise RuntimeError("VAD returned no segments") + return duration, vad_sec, segments + + +def _build_engine( + model_path: str, + forced_aligner_path: str, + *, + batch_size: int, +) -> tuple[Qwen3ASREngine, float]: + settings.DEVICE = "cpu" + settings.ASR_BATCH_SIZE = batch_size + + t0 = time.perf_counter() + engine = Qwen3ASREngine( + model_path=model_path, + forced_aligner_path=forced_aligner_path, + device="cpu", + ) + return engine, time.perf_counter() - t0 + + +def _run_asr_stage( + engine: Qwen3ASREngine, + segments: list, +) -> tuple[dict[int, str], float]: + valid_segments = [ + (idx, seg) for idx, seg in enumerate(segments) if getattr(seg, "temp_file", None) + ] + t0 = time.perf_counter() + texts = engine._run_rust_asr_stage( + valid_segments=valid_segments, + hotwords="", + enable_punctuation=True, + enable_itn=True, + sample_rate=16000, + ) + return texts, time.perf_counter() - t0 + + +def _run_align_stage( + engine: Qwen3ASREngine, + segments: list, + texts: dict[int, str], +) -> tuple[dict[int, list], float]: + valid_segments = [ + (idx, seg) for idx, seg in enumerate(segments) if getattr(seg, "temp_file", None) + ] + t0 = time.perf_counter() + aligned = engine._run_rust_align_stage( + valid_segments=valid_segments, + texts=texts, + ) + return aligned, time.perf_counter() - t0 + + +def _summarize( + *, + cpu_count: int, + audio_file: Path, + audio_duration_sec: float, + batch_size: int, + engine_init_sec: float, + vad_sec: float, + segments: list, + texts: dict[int, str], + aligned: dict[int, list], + asr_sec: float, + align_sec: float, + total_sec: float, +) -> WorkerBenchRow: + text_len = sum(len(text) for text in texts.values()) + word_tokens = sum(len(items) for items in aligned.values()) + return WorkerBenchRow( + cpu_count=cpu_count, + rust_workers=settings.QWEN_RUST_CPU_WORKERS, + asr_concurrency=settings.QWEN_RUST_ASR_CONCURRENCY or settings.QWEN_RUST_CPU_WORKERS, + align_concurrency=settings.QWEN_RUST_ALIGN_CONCURRENCY or settings.QWEN_RUST_CPU_WORKERS, + audio_file=str(audio_file), + audio_duration_sec=audio_duration_sec, + batch_size=batch_size, + engine_init_sec=engine_init_sec, + vad_sec=vad_sec, + vad_segments=len(segments), + asr_sec=asr_sec, + asr_calls=len(texts), + align_sec=align_sec, + align_calls=len(aligned), + total_sec=total_sec, + rtf=total_sec / audio_duration_sec if audio_duration_sec else 0.0, + segments=len(texts), + word_tokens=word_tokens, + text_len=text_len, + ) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Qwen Rust CPU end-to-end benchmark") + parser.add_argument("--audio-file", required=True, help="Input audio file path") + parser.add_argument("--model-path", default="Qwen/Qwen3-ASR-0.6B") + parser.add_argument("--forced-aligner-path", default="Qwen/Qwen3-ForcedAligner-0.6B") + parser.add_argument("--batch-size", type=int, default=4) + parser.add_argument("--json-out", help="Optional JSON output path") + parser.add_argument( + "--markdown-out", + help="Optional Markdown report output path. Defaults to a sibling .md next to --json-out.", + ) + return parser + + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + + audio_file = Path(args.audio_file).expanduser().resolve() + cpu_count = os.cpu_count() or 1 + out_path = Path(args.json_out).expanduser().resolve() if args.json_out else None + markdown_out = Path(args.markdown_out).expanduser().resolve() if args.markdown_out else None + if markdown_out is None and out_path is not None: + markdown_out = out_path.with_suffix(".md") + + duration, vad_sec, segments = _prepare_segments(audio_file) + rows: list[WorkerBenchRow] = [] + + try: + engine, init_sec = _build_engine( + args.model_path, + args.forced_aligner_path, + batch_size=args.batch_size, + ) + + t0 = time.perf_counter() + texts, asr_sec = _run_asr_stage(engine, segments) + aligned, align_sec = _run_align_stage(engine, segments, texts) + total_sec = time.perf_counter() - t0 + + row = _summarize( + cpu_count=cpu_count, + audio_file=audio_file, + audio_duration_sec=duration, + batch_size=args.batch_size, + engine_init_sec=init_sec, + vad_sec=vad_sec, + segments=segments, + texts=texts, + aligned=aligned, + asr_sec=asr_sec, + align_sec=align_sec, + total_sec=total_sec, + ) + rows.append(row) + _persist_rows(rows, out_path) + _persist_markdown(rows, markdown_out) + _log_progress(row) + finally: + _clean_segments(segments) + + payload = [asdict(row) for row in rows] + if out_path is not None: + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") + if markdown_out is not None: + markdown_out.parent.mkdir(parents=True, exist_ok=True) + markdown_out.write_text(_render_markdown(rows), encoding="utf-8") + + print(json.dumps(payload, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/scripts/benchmark/reporters/__init__.py b/scripts/benchmark/reporters/__init__.py new file mode 100644 index 0000000..caa1186 --- /dev/null +++ b/scripts/benchmark/reporters/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +from .markdown_reporter import MarkdownReporter +from .chart_generator import ChartGenerator + +__all__ = ["MarkdownReporter", "ChartGenerator"] diff --git a/scripts/benchmark/reporters/chart_generator.py b/scripts/benchmark/reporters/chart_generator.py new file mode 100644 index 0000000..57b367d --- /dev/null +++ b/scripts/benchmark/reporters/chart_generator.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- +""" +Matplotlib 图表生成器 +""" + +from pathlib import Path +from typing import List + +import matplotlib.pyplot as plt +import matplotlib +import numpy as np + +from ..metrics.models import AggregatedMetrics + +# 设置中文字体支持 +matplotlib.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'DejaVu Sans'] +matplotlib.rcParams['axes.unicode_minus'] = False + + +class ChartGenerator: + """图表生成器""" + + def __init__(self): + self.colors = { + "asr": "#4CAF50", # 绿色 + "tts": "#2196F3", # 蓝色 + } + + def generate_all_charts( + self, + asr_results: List[AggregatedMetrics], + tts_results: List[AggregatedMetrics], + output_dir: Path, + timestamp: str, + ) -> List[Path]: + """ + 生成所有图表 + + Args: + asr_results: ASR 测试结果 + tts_results: TTS 测试结果 + output_dir: 输出目录 + timestamp: 时间戳 + + Returns: + 生成的图表文件路径列表 + """ + output_dir.mkdir(parents=True, exist_ok=True) + generated_files = [] + + # 1. 首次延迟对比图 + if asr_results or tts_results: + path = output_dir / f"first_latency_{timestamp}.png" + self._generate_first_latency_chart(asr_results, tts_results, path) + generated_files.append(path) + + # 2. RTF 对比图 + if asr_results or tts_results: + path = output_dir / f"rtf_{timestamp}.png" + self._generate_rtf_chart(asr_results, tts_results, path) + generated_files.append(path) + + # 3. 吞吐量对比图 + if asr_results or tts_results: + path = output_dir / f"throughput_{timestamp}.png" + self._generate_throughput_chart(asr_results, tts_results, path) + generated_files.append(path) + + # 4. 总时间对比图 + if asr_results or tts_results: + path = output_dir / f"total_time_{timestamp}.png" + self._generate_total_time_chart(asr_results, tts_results, path) + generated_files.append(path) + + return generated_files + + def _generate_first_latency_chart( + self, + asr_results: List[AggregatedMetrics], + tts_results: List[AggregatedMetrics], + output_path: Path, + ) -> None: + """生成首次延迟对比图""" + _fig, ax = plt.subplots(figsize=(10, 6)) + + levels = [] + if asr_results: + levels = [r.concurrency_level for r in asr_results] + avg_values = [r.first_latency_avg for r in asr_results] + p95_values = [r.first_latency_p95 for r in asr_results] + + ax.plot(levels, avg_values, 'o-', color=self.colors["asr"], + label='ASR 首次响应 (Avg)', linewidth=2, markersize=8) + ax.plot(levels, p95_values, 's--', color=self.colors["asr"], + label='ASR 首次响应 (P95)', linewidth=1.5, markersize=6, alpha=0.7) + + if tts_results: + levels = [r.concurrency_level for r in tts_results] + avg_values = [r.first_latency_avg for r in tts_results] + p95_values = [r.first_latency_p95 for r in tts_results] + + ax.plot(levels, avg_values, 'o-', color=self.colors["tts"], + label='TTS 首包延迟 (Avg)', linewidth=2, markersize=8) + ax.plot(levels, p95_values, 's--', color=self.colors["tts"], + label='TTS 首包延迟 (P95)', linewidth=1.5, markersize=6, alpha=0.7) + + ax.set_xlabel('并发数', fontsize=12) + ax.set_ylabel('延迟 (ms)', fontsize=12) + ax.set_title('首次响应延迟 vs 并发数', fontsize=14, fontweight='bold') + ax.legend(loc='best') + ax.grid(True, alpha=0.3) + if levels: + ax.set_xticks(levels) + + plt.tight_layout() + plt.savefig(output_path, dpi=150) + plt.close() + + def _generate_rtf_chart( + self, + asr_results: List[AggregatedMetrics], + tts_results: List[AggregatedMetrics], + output_path: Path, + ) -> None: + """生成 RTF 对比图""" + _fig, ax = plt.subplots(figsize=(10, 6)) + + if asr_results: + levels = [r.concurrency_level for r in asr_results] + avg_values = [r.rtf_avg for r in asr_results] + p95_values = [r.rtf_p95 for r in asr_results] + + ax.plot(levels, avg_values, 'o-', color=self.colors["asr"], + label='ASR RTF (Avg)', linewidth=2, markersize=8) + ax.plot(levels, p95_values, 's--', color=self.colors["asr"], + label='ASR RTF (P95)', linewidth=1.5, markersize=6, alpha=0.7) + + if tts_results: + levels = [r.concurrency_level for r in tts_results] + avg_values = [r.rtf_avg for r in tts_results] + p95_values = [r.rtf_p95 for r in tts_results] + + ax.plot(levels, avg_values, 'o-', color=self.colors["tts"], + label='TTS RTF (Avg)', linewidth=2, markersize=8) + ax.plot(levels, p95_values, 's--', color=self.colors["tts"], + label='TTS RTF (P95)', linewidth=1.5, markersize=6, alpha=0.7) + + # 添加 RTF=1.0 参考线 + all_levels = set() + if asr_results: + all_levels.update(r.concurrency_level for r in asr_results) + if tts_results: + all_levels.update(r.concurrency_level for r in tts_results) + if all_levels: + ax.axhline(y=1.0, color='red', linestyle=':', linewidth=1.5, + label='RTF = 1.0 (实时)') + + ax.set_xlabel('并发数', fontsize=12) + ax.set_ylabel('RTF', fontsize=12) + ax.set_title('RTF vs 并发数', fontsize=14, fontweight='bold') + ax.legend(loc='best') + ax.grid(True, alpha=0.3) + + plt.tight_layout() + plt.savefig(output_path, dpi=150) + plt.close() + + def _generate_throughput_chart( + self, + asr_results: List[AggregatedMetrics], + tts_results: List[AggregatedMetrics], + output_path: Path, + ) -> None: + """生成吞吐量柱状图""" + _fig, ax = plt.subplots(figsize=(10, 6)) + + all_levels = sorted(set( + [r.concurrency_level for r in asr_results] + + [r.concurrency_level for r in tts_results] + )) + + x = np.arange(len(all_levels)) + width = 0.35 + + if asr_results: + asr_throughput = [] + for level in all_levels: + r = next((r for r in asr_results if r.concurrency_level == level), None) + asr_throughput.append(r.throughput if r else 0) + ax.bar(x - width/2, asr_throughput, width, label='ASR', + color=self.colors["asr"], alpha=0.8) + + if tts_results: + tts_throughput = [] + for level in all_levels: + r = next((r for r in tts_results if r.concurrency_level == level), None) + tts_throughput.append(r.throughput if r else 0) + ax.bar(x + width/2, tts_throughput, width, label='TTS', + color=self.colors["tts"], alpha=0.8) + + ax.set_xlabel('并发数', fontsize=12) + ax.set_ylabel('吞吐量 (req/s)', fontsize=12) + ax.set_title('吞吐量 vs 并发数', fontsize=14, fontweight='bold') + ax.set_xticks(x) + ax.set_xticklabels([str(level) for level in all_levels]) + ax.legend(loc='best') + ax.grid(True, alpha=0.3, axis='y') + + plt.tight_layout() + plt.savefig(output_path, dpi=150) + plt.close() + + def _generate_total_time_chart( + self, + asr_results: List[AggregatedMetrics], + tts_results: List[AggregatedMetrics], + output_path: Path, + ) -> None: + """生成总时间对比图""" + _fig, ax = plt.subplots(figsize=(10, 6)) + + if asr_results: + levels = [r.concurrency_level for r in asr_results] + avg_values = [r.total_time_avg for r in asr_results] + p95_values = [r.total_time_p95 for r in asr_results] + + ax.plot(levels, avg_values, 'o-', color=self.colors["asr"], + label='ASR 总时间 (Avg)', linewidth=2, markersize=8) + ax.plot(levels, p95_values, 's--', color=self.colors["asr"], + label='ASR 总时间 (P95)', linewidth=1.5, markersize=6, alpha=0.7) + + if tts_results: + levels = [r.concurrency_level for r in tts_results] + avg_values = [r.total_time_avg for r in tts_results] + p95_values = [r.total_time_p95 for r in tts_results] + + ax.plot(levels, avg_values, 'o-', color=self.colors["tts"], + label='TTS 总时间 (Avg)', linewidth=2, markersize=8) + ax.plot(levels, p95_values, 's--', color=self.colors["tts"], + label='TTS 总时间 (P95)', linewidth=1.5, markersize=6, alpha=0.7) + + ax.set_xlabel('并发数', fontsize=12) + ax.set_ylabel('时间 (ms)', fontsize=12) + ax.set_title('总处理时间 vs 并发数', fontsize=14, fontweight='bold') + ax.legend(loc='best') + ax.grid(True, alpha=0.3) + + plt.tight_layout() + plt.savefig(output_path, dpi=150) + plt.close() diff --git a/scripts/benchmark/reporters/markdown_reporter.py b/scripts/benchmark/reporters/markdown_reporter.py new file mode 100644 index 0000000..ab93b32 --- /dev/null +++ b/scripts/benchmark/reporters/markdown_reporter.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +""" +Markdown 报告生成器 +""" + +from datetime import datetime +from pathlib import Path +from typing import List, Optional + +from ..metrics.models import AggregatedMetrics + + +class MarkdownReporter: + """Markdown 报告生成器""" + + def generate( + self, + asr_results: List[AggregatedMetrics], + tts_results: List[AggregatedMetrics], + output_path: Path, + config_info: Optional[dict] = None, + ) -> None: + """ + 生成 Markdown 报告 + + Args: + asr_results: ASR 测试结果 + tts_results: TTS 测试结果 + output_path: 输出文件路径 + config_info: 配置信息 + """ + lines = [] + + # 标题 + lines.append("# Qwen3-ASR 并发性能测试报告") + lines.append("") + lines.append(f"**测试时间:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + if config_info: + lines.append(f"**服务器:** {config_info.get('host', 'localhost')}:{config_info.get('port', 8000)}") + lines.append(f"**并发级别:** {', '.join(map(str, config_info.get('concurrency_levels', [])))}") + + lines.append("") + lines.append("---") + lines.append("") + + # ASR 结果 + if asr_results: + lines.extend(self._generate_asr_section(asr_results)) + + # TTS 结果 + if tts_results: + lines.extend(self._generate_tts_section(tts_results)) + + # 结论 + lines.extend(self._generate_conclusions(asr_results, tts_results)) + + # 写入文件 + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text("\n".join(lines), encoding="utf-8") + + def _generate_asr_section(self, results: List[AggregatedMetrics]) -> List[str]: + """生成 ASR 结果部分""" + lines = [] + lines.append("## ASR 性能测试结果") + lines.append("") + + # 延迟指标表格 + lines.append("### 延迟指标 (毫秒)") + lines.append("") + lines.append("| 并发数 | 首次响应 (Avg) | 首次响应 (P95) | 总时间 (Avg) | 总时间 (P95) | 总时间 (Max) |") + lines.append("|--------|---------------|---------------|-------------|-------------|-------------|") + + for r in results: + lines.append( + f"| {r.concurrency_level} | " + f"{r.first_latency_avg:.1f} | " + f"{r.first_latency_p95:.1f} | " + f"{r.total_time_avg:.1f} | " + f"{r.total_time_p95:.1f} | " + f"{r.total_time_max:.1f} |" + ) + + lines.append("") + + # RTF 和吞吐量表格 + lines.append("### RTF 和吞吐量") + lines.append("") + lines.append("| 并发数 | RTF (Avg) | RTF (P95) | 吞吐量 (req/s) | 成功率 |") + lines.append("|--------|----------|----------|---------------|--------|") + + for r in results: + lines.append( + f"| {r.concurrency_level} | " + f"{r.rtf_avg:.3f} | " + f"{r.rtf_p95:.3f} | " + f"{r.throughput:.3f} | " + f"{r.success_rate:.1f}% |" + ) + + lines.append("") + return lines + + def _generate_tts_section(self, results: List[AggregatedMetrics]) -> List[str]: + """生成 TTS 结果部分""" + lines = [] + lines.append("## TTS 性能测试结果") + lines.append("") + + # 延迟指标表格 + lines.append("### 延迟指标 (毫秒)") + lines.append("") + lines.append("| 并发数 | 首包延迟 (Avg) | 首包延迟 (P95) | 总时间 (Avg) | 总时间 (P95) | 总时间 (Max) |") + lines.append("|--------|---------------|---------------|-------------|-------------|-------------|") + + for r in results: + lines.append( + f"| {r.concurrency_level} | " + f"{r.first_latency_avg:.1f} | " + f"{r.first_latency_p95:.1f} | " + f"{r.total_time_avg:.1f} | " + f"{r.total_time_p95:.1f} | " + f"{r.total_time_max:.1f} |" + ) + + lines.append("") + + # RTF 和吞吐量表格 + lines.append("### RTF 和吞吐量") + lines.append("") + lines.append("| 并发数 | RTF (Avg) | RTF (P95) | 吞吐量 (req/s) | 成功率 |") + lines.append("|--------|----------|----------|---------------|--------|") + + for r in results: + lines.append( + f"| {r.concurrency_level} | " + f"{r.rtf_avg:.3f} | " + f"{r.rtf_p95:.3f} | " + f"{r.throughput:.3f} | " + f"{r.success_rate:.1f}% |" + ) + + lines.append("") + return lines + + def _generate_conclusions( + self, + asr_results: List[AggregatedMetrics], + tts_results: List[AggregatedMetrics], + ) -> List[str]: + """生成结论部分""" + lines = [] + lines.append("## 结论") + lines.append("") + + if asr_results: + max_level = max(asr_results, key=lambda x: x.concurrency_level) + lines.append(f"- **ASR 最大并发 ({max_level.concurrency_level}) RTF:** {max_level.rtf_avg:.3f}") + lines.append(f"- **ASR 最大并发吞吐量:** {max_level.throughput:.3f} req/s") + + # 找到 RTF 超过 1.0 的并发级别 + stable_levels = [r for r in asr_results if r.rtf_avg <= 1.0] + if stable_levels: + max_stable = max(stable_levels, key=lambda x: x.concurrency_level) + lines.append(f"- **ASR 稳定并发上限 (RTF < 1.0):** {max_stable.concurrency_level}") + + if tts_results: + max_level = max(tts_results, key=lambda x: x.concurrency_level) + lines.append(f"- **TTS 最大并发 ({max_level.concurrency_level}) RTF:** {max_level.rtf_avg:.3f}") + lines.append(f"- **TTS 最大并发吞吐量:** {max_level.throughput:.3f} req/s") + + stable_levels = [r for r in tts_results if r.rtf_avg <= 1.0] + if stable_levels: + max_stable = max(stable_levels, key=lambda x: x.concurrency_level) + lines.append(f"- **TTS 稳定并发上限 (RTF < 1.0):** {max_stable.concurrency_level}") + + lines.append("") + lines.append("---") + lines.append("") + lines.append("*RTF (Real-Time Factor): 处理时间与音频时长的比值,小于 1.0 表示处理速度快于实时*") + lines.append("") + + return lines diff --git a/scripts/benchmark/run.py b/scripts/benchmark/run.py new file mode 100644 index 0000000..38d0065 --- /dev/null +++ b/scripts/benchmark/run.py @@ -0,0 +1,418 @@ +# -*- coding: utf-8 -*- +""" +Qwen3-ASR 并发性能测试主入口 + +使用方法: + # 完整测试 (ASR + TTS) + python -m scripts.benchmark.run --audio-file /path/to/audio.wav + + # 仅测试 TTS + python -m scripts.benchmark.run --test-type tts + + # 仅测试 ASR + python -m scripts.benchmark.run --audio-file /path/to/audio.wav --test-type asr + + # 自定义并发级别 + python -m scripts.benchmark.run --audio-file /path/to/audio.wav --concurrency 5 10 20 +""" + +import asyncio +import argparse +import logging +import time +from datetime import datetime +from pathlib import Path +from typing import List + +from .config import TestConfig +from .clients.asr_client import ASRWebSocketClient +from .clients.tts_client import TTSWebSocketClient +from .metrics.models import ASRMetrics, TTSMetrics, AggregatedMetrics +from .metrics.statistics import calculate_statistics +from .reporters.markdown_reporter import MarkdownReporter +from .reporters.chart_generator import ChartGenerator +from .utils.audio_utils import load_audio_file +from .utils.text_generator import generate_test_texts + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + + +class ConcurrentBenchmark: + """并发性能测试运行器""" + + def __init__(self, config: TestConfig): + self.config = config + # 创建保存目录 + self._setup_output_dirs() + + def _setup_output_dirs(self): + """创建输出目录结构""" + self.config.output_dir.mkdir(parents=True, exist_ok=True) + # ASR 结果目录 + self.asr_output_dir = self.config.output_dir / "asr" + self.asr_output_dir.mkdir(exist_ok=True) + # TTS 音频目录 + self.tts_output_dir = self.config.output_dir / "tts" + self.tts_output_dir.mkdir(exist_ok=True) + + async def run_asr_benchmark(self) -> List[AggregatedMetrics]: + """ + 运行 ASR 并发测试 + + Returns: + 各并发级别的聚合指标列表 + """ + logger.info("开始 ASR 并发性能测试...") + + # 检查音频文件 + if self.config.asr_audio_file is None: + raise ValueError("ASR 测试需要提供音频文件") + + # 加载音频文件 + audio_data, audio_duration = load_audio_file( + self.config.asr_audio_file, + self.config.asr_sample_rate, + ) + audio_duration_ms = audio_duration * 1000 + + logger.info(f"音频文件已加载: {self.config.asr_audio_file}") + logger.info(f" - 时长: {audio_duration:.2f} 秒") + logger.info(f" - 大小: {len(audio_data) / 1024:.1f} KB") + + results = [] + + for level in self.config.concurrency_levels: + logger.info(f"\n测试并发级别: {level}") + + # 预热 + logger.info(f" 预热中 ({self.config.warmup_requests} 次请求)...") + await self._run_asr_concurrent( + audio_data, audio_duration_ms, self.config.warmup_requests, level, + save_results=False + ) + + # 正式测试 + logger.info(f" 正式测试中...") + start_time = time.perf_counter() + metrics_list = await self._run_asr_concurrent( + audio_data, audio_duration_ms, level, level, + save_results=True # 正式测试时保存结果 + ) + total_time = time.perf_counter() - start_time + + # 统计 + aggregated = calculate_statistics(metrics_list, level, total_time) + results.append(aggregated) + + # 打印结果 + logger.info(f" 完成: 成功 {aggregated.successful_requests}/{aggregated.total_requests}") + logger.info(f" 首次响应延迟: {aggregated.first_latency_avg:.1f} ms (avg)") + logger.info(f" RTF: {aggregated.rtf_avg:.3f} (avg)") + + return results + + async def _run_asr_concurrent( + self, + audio_data: bytes, + audio_duration_ms: float, + num_requests: int, + concurrency_level: int, + save_results: bool = False, + ) -> List[ASRMetrics]: + """运行并发 ASR 请求""" + tasks = [] + + for _ in range(num_requests): + client = ASRWebSocketClient( + ws_url=self.config.asr_ws_url, + audio_data=audio_data, + audio_duration_ms=audio_duration_ms, + sample_rate=self.config.asr_sample_rate, + chunk_size=self.config.asr_chunk_size, + timeout=self.config.timeout_seconds, + save_result_dir=self.asr_output_dir if save_results else None, + ) + tasks.append(client.run_test()) + + results = await asyncio.gather(*tasks, return_exceptions=True) + + # 处理结果 + metrics_list = [] + for result in results: + if isinstance(result, ASRMetrics): + result.concurrency_level = concurrency_level + metrics_list.append(result) + else: + # 异常情况 + metrics = ASRMetrics( + request_id="error", + concurrency_level=concurrency_level, + start_time=0, + error_message=str(result), + ) + metrics_list.append(metrics) + + return metrics_list + + async def run_tts_benchmark(self) -> List[AggregatedMetrics]: + """ + 运行 TTS 并发测试 + + Returns: + 各并发级别的聚合指标列表 + """ + logger.info("开始 TTS 并发性能测试...") + + # 生成测试文本 + test_texts = generate_test_texts( + count=self.config.tts_text_count, + length_range=self.config.tts_text_length_range, + ) + logger.info(f"已生成 {len(test_texts)} 段测试文本") + + results = [] + + for level in self.config.concurrency_levels: + logger.info(f"\n测试并发级别: {level}") + + # 选择文本 (每个并发请求使用不同文本) + selected_texts = test_texts[:level] + + # 预热 + logger.info(f" 预热中 ({min(self.config.warmup_requests, level)} 次请求)...") + await self._run_tts_concurrent( + selected_texts[:min(self.config.warmup_requests, level)], + min(self.config.warmup_requests, level), + level, + save_audio=False, + ) + + # 正式测试 + logger.info(f" 正式测试中...") + start_time = time.perf_counter() + metrics_list = await self._run_tts_concurrent( + selected_texts, level, level, + save_audio=True, # 正式测试时保存音频 + ) + total_time = time.perf_counter() - start_time + + # 统计 + aggregated = calculate_statistics(metrics_list, level, total_time) + results.append(aggregated) + + # 打印结果 + logger.info(f" 完成: 成功 {aggregated.successful_requests}/{aggregated.total_requests}") + logger.info(f" 首包延迟: {aggregated.first_latency_avg:.1f} ms (avg)") + logger.info(f" RTF: {aggregated.rtf_avg:.3f} (avg)") + + return results + + async def _run_tts_concurrent( + self, + texts: List[str], + num_requests: int, + concurrency_level: int, + save_audio: bool = False, + ) -> List[TTSMetrics]: + """运行并发 TTS 请求""" + tasks = [] + + for i in range(num_requests): + text = texts[i % len(texts)] + # 第一个请求始终开启调试模式 + debug = (i == 0) + client = TTSWebSocketClient( + ws_url=self.config.tts_ws_url, + text=text, + voice=self.config.tts_voice, + audio_format=self.config.tts_format, + sample_rate=self.config.tts_sample_rate, + timeout=self.config.timeout_seconds, + chunk_interval=self.config.tts_chunk_interval, + debug=debug, + save_audio_dir=self.tts_output_dir if save_audio else None, + ) + tasks.append(client.run_test()) + + # 添加进度提示 + logger.info(f" 启动 {num_requests} 个并发请求...") + results = await asyncio.gather(*tasks, return_exceptions=True) + logger.info(f" 所有请求已完成") + + # 处理结果 + metrics_list = [] + for result in results: + if isinstance(result, TTSMetrics): + result.concurrency_level = concurrency_level + metrics_list.append(result) + else: + # 异常情况 + metrics = TTSMetrics( + request_id="error", + concurrency_level=concurrency_level, + start_time=0, + error_message=str(result), + ) + metrics_list.append(metrics) + + return metrics_list + + def generate_report( + self, + asr_results: List[AggregatedMetrics], + tts_results: List[AggregatedMetrics], + ) -> Path: + """ + 生成测试报告 + + Args: + asr_results: ASR 测试结果 + tts_results: TTS 测试结果 + + Returns: + 报告文件路径 + """ + output_dir = self.config.output_dir + output_dir.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + # 配置信息 + config_info = { + "host": self.config.host, + "port": self.config.port, + "concurrency_levels": self.config.concurrency_levels, + } + + # 生成 Markdown 报告 + report_path = output_dir / f"{self.config.report_name}_{timestamp}.md" + reporter = MarkdownReporter() + reporter.generate(asr_results, tts_results, report_path, config_info) + logger.info(f"Markdown 报告已生成: {report_path}") + + # 生成图表 + chart_generator = ChartGenerator() + chart_files = chart_generator.generate_all_charts( + asr_results, tts_results, output_dir, timestamp + ) + for chart_file in chart_files: + logger.info(f"图表已生成: {chart_file}") + + return report_path + + +def parse_args(): + """解析命令行参数""" + parser = argparse.ArgumentParser( + description="Qwen3-ASR 并发性能测试脚本", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +示例: + # 完整测试 (ASR + TTS) + python -m scripts.benchmark.run --audio-file test.wav + + # 仅测试 TTS + python -m scripts.benchmark.run --test-type tts + + # 自定义并发级别 + python -m scripts.benchmark.run --audio-file test.wav --concurrency 5 10 20 50 + """, + ) + + parser.add_argument( + "--host", + default="localhost", + help="服务器主机名 (默认: localhost)", + ) + parser.add_argument( + "--port", + type=int, + default=8000, + help="服务器端口 (默认: 8000)", + ) + parser.add_argument( + "--audio-file", + type=Path, + help="ASR 测试用音频文件路径 (测试 ASR 时必需)", + ) + parser.add_argument( + "--concurrency", + nargs="+", + type=int, + default=[5, 10, 20, 50], + help="并发级别列表 (默认: 5 10 20 50)", + ) + parser.add_argument( + "--test-type", + choices=["asr", "tts", "both"], + default="both", + help="测试类型 (默认: both)", + ) + parser.add_argument( + "--output", + type=Path, + default=Path("./benchmark_results"), + help="报告输出目录 (默认: ./benchmark_results)", + ) + parser.add_argument( + "--timeout", + type=float, + default=120.0, + help="请求超时时间 (秒, 默认: 120)", + ) + parser.add_argument( + "--voice", + default="中文女", + help="TTS 音色 (默认: 中文女)", + ) + + return parser.parse_args() + + +async def main(): + """主函数""" + args = parse_args() + + # 创建配置 + config = TestConfig( + host=args.host, + port=args.port, + concurrency_levels=args.concurrency, + asr_audio_file=args.audio_file, + output_dir=args.output, + timeout_seconds=args.timeout, + tts_voice=args.voice, + ) + + # 验证配置 + try: + config.validate(args.test_type) + except ValueError as e: + logger.error(f"配置错误: {e}") + return + + # 运行测试 + benchmark = ConcurrentBenchmark(config) + + asr_results = [] + tts_results = [] + + if args.test_type in ("asr", "both"): + asr_results = await benchmark.run_asr_benchmark() + + if args.test_type in ("tts", "both"): + tts_results = await benchmark.run_tts_benchmark() + + # 生成报告 + if asr_results or tts_results: + report_path = benchmark.generate_report(asr_results, tts_results) + logger.info(f"\n测试完成! 报告已保存到: {report_path}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scripts/benchmark/utils/__init__.py b/scripts/benchmark/utils/__init__.py new file mode 100644 index 0000000..68c179e --- /dev/null +++ b/scripts/benchmark/utils/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +from .audio_utils import load_audio_file, get_audio_duration +from .text_generator import generate_test_texts + +__all__ = ["load_audio_file", "get_audio_duration", "generate_test_texts"] diff --git a/scripts/benchmark/utils/audio_utils.py b/scripts/benchmark/utils/audio_utils.py new file mode 100644 index 0000000..8f140c5 --- /dev/null +++ b/scripts/benchmark/utils/audio_utils.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +""" +音频处理工具 +""" + +from pathlib import Path +from typing import Tuple +import numpy as np +import soundfile as sf + + +def load_audio_file( + audio_path: Path, + target_sample_rate: int = 16000, +) -> Tuple[bytes, float]: + """ + 加载音频文件并转换为 PCM 16-bit 格式 + + Args: + audio_path: 音频文件路径 + target_sample_rate: 目标采样率 + + Returns: + (pcm_bytes, duration_seconds): PCM 字节数据和音频时长(秒) + """ + # 读取音频文件 + audio_data, sample_rate = sf.read(audio_path, dtype="float32") + + # 如果是立体声,转换为单声道 + if len(audio_data.shape) > 1: + audio_data = np.mean(audio_data, axis=1) + + # 重采样到目标采样率 + if sample_rate != target_sample_rate: + audio_data = resample_audio(audio_data, sample_rate, target_sample_rate) + + # 计算时长 + duration_seconds = len(audio_data) / target_sample_rate + + # 转换为 16-bit PCM + pcm_data = (audio_data * 32767).astype(np.int16) + pcm_bytes = pcm_data.tobytes() + + return pcm_bytes, duration_seconds + + +def resample_audio( + audio_data: np.ndarray, + orig_sample_rate: int, + target_sample_rate: int, +) -> np.ndarray: + """ + 重采样音频 + + Args: + audio_data: 音频数据 + orig_sample_rate: 原始采样率 + target_sample_rate: 目标采样率 + + Returns: + 重采样后的音频数据 + """ + if orig_sample_rate == target_sample_rate: + return audio_data + + # 计算重采样比例 + ratio = target_sample_rate / orig_sample_rate + new_length = int(len(audio_data) * ratio) + + # 使用线性插值进行重采样 + x_old = np.linspace(0, 1, len(audio_data)) + x_new = np.linspace(0, 1, new_length) + resampled = np.interp(x_new, x_old, audio_data) + + return resampled.astype(np.float32) + + +def get_audio_duration(audio_path: Path) -> float: + """ + 获取音频文件时长 + + Args: + audio_path: 音频文件路径 + + Returns: + 时长(秒) + """ + info = sf.info(audio_path) + return info.duration + + +def split_audio_into_chunks( + pcm_bytes: bytes, + chunk_size: int, +) -> list: + """ + 将 PCM 数据分割成块 + + Args: + pcm_bytes: PCM 字节数据 + chunk_size: 每块的采样数 + + Returns: + 字节块列表 + """ + chunk_bytes = chunk_size * 2 # 16-bit = 2 bytes per sample + chunks = [] + + for i in range(0, len(pcm_bytes), chunk_bytes): + chunk = pcm_bytes[i : i + chunk_bytes] + chunks.append(chunk) + + return chunks diff --git a/scripts/benchmark/utils/text_generator.py b/scripts/benchmark/utils/text_generator.py new file mode 100644 index 0000000..dececb4 --- /dev/null +++ b/scripts/benchmark/utils/text_generator.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +""" +中文随机句子生成器 + +用于生成 TTS 测试文本,不依赖外部 AI API。 +""" + +import random +from typing import List, Tuple + +# 主语词库 +SUBJECTS = [ + "我", "你", "他", "她", "我们", "大家", "小明", "小红", "老师", "学生", + "医生", "工程师", "科学家", "艺术家", "音乐家", "作家", "记者", "警察", + "这位先生", "那位女士", "我的朋友", "他的同事", "她的家人", "公司", + "团队", "项目组", "研发部门", "市场部", "客户", "用户", +] + +# 时间词库 +TIME_PHRASES = [ + "今天", "明天", "昨天", "上周", "下周", "这个月", "上个月", "今年", + "最近", "刚才", "马上", "立刻", "很快", "不久前", "过去", + "早上", "中午", "下午", "晚上", "凌晨", "周末", "假期期间", +] + +# 地点词库 +LOCATIONS = [ + "在公司", "在家里", "在学校", "在图书馆", "在咖啡厅", "在会议室", + "在公园", "在商场", "在医院", "在机场", "在火车站", "在地铁站", + "在办公室", "在实验室", "在教室", "在操场", "在餐厅", "在酒店", +] + +# 动词短语词库 +VERB_PHRASES = [ + "正在开发一个新的功能", "完成了一项重要的任务", "参加了一个技术会议", + "学习了新的编程语言", "解决了一个复杂的问题", "提交了项目报告", + "设计了一套新的方案", "测试了最新的版本", "优化了系统性能", + "讨论了未来的发展计划", "制定了下一步的工作安排", "回顾了过去的工作成果", + "分析了市场数据", "研究了用户需求", "改进了产品体验", + "组织了团队活动", "培训了新员工", "更新了技术文档", + "修复了几个重要的问题", "部署了新的服务", "监控了系统运行状态", + "收集了用户反馈", "整理了项目资料", "准备了演示材料", +] + +# 形容词词库 +ADJECTIVES = [ + "高效的", "专业的", "创新的", "稳定的", "可靠的", "智能的", + "先进的", "实用的", "便捷的", "优秀的", "杰出的", "卓越的", +] + +# 名词词库 +NOUNS = [ + "系统", "平台", "应用", "服务", "方案", "产品", "技术", "工具", + "项目", "团队", "计划", "目标", "成果", "进展", "效率", "质量", +] + +# 连接词 +CONNECTORS = [ + "并且", "同时", "而且", "另外", "此外", "因此", "所以", "然后", +] + +# 结尾语 +ENDINGS = [ + "这是一个很好的开始。", + "我们对此感到非常满意。", + "期待能有更好的结果。", + "这将带来积极的影响。", + "相信未来会更加美好。", + "让我们继续努力。", + "这是值得庆祝的成就。", + "我们会继续保持这种势头。", + "这体现了团队的实力。", + "我们为此感到自豪。", +] + + +def generate_simple_sentence() -> str: + """生成简单句""" + subject = random.choice(SUBJECTS) + time_phrase = random.choice(TIME_PHRASES) if random.random() > 0.3 else "" + location = random.choice(LOCATIONS) if random.random() > 0.5 else "" + verb_phrase = random.choice(VERB_PHRASES) + + parts = [time_phrase, subject, location, verb_phrase] + parts = [p for p in parts if p] # 过滤空字符串 + return "".join(parts) + "。" + + +def generate_compound_sentence() -> str: + """生成复合句""" + sentence1 = generate_simple_sentence().rstrip("。") + connector = random.choice(CONNECTORS) + sentence2 = generate_simple_sentence().rstrip("。") + + return f"{sentence1},{connector}{sentence2}。" + + +def generate_descriptive_sentence() -> str: + """生成描述性句子""" + subject = random.choice(SUBJECTS) + adj = random.choice(ADJECTIVES) + noun = random.choice(NOUNS) + verb_phrase = random.choice(VERB_PHRASES) + + return f"{subject}开发了一个{adj}{noun},{verb_phrase}。" + + +def generate_single_text(length_range: Tuple[int, int] = (50, 100)) -> str: + """ + 生成单个测试文本 + + Args: + length_range: 文本长度范围 (min, max) + + Returns: + 生成的文本 + """ + min_len, max_len = length_range + target_len = random.randint(min_len, max_len) + + text = "" + sentence_generators = [ + generate_simple_sentence, + generate_compound_sentence, + generate_descriptive_sentence, + ] + + while len(text) < target_len: + generator = random.choice(sentence_generators) + sentence = generator() + text += sentence + + # 如果超出太多,截断到最近的句号 + if len(text) > max_len + 20: + # 找到目标长度附近的句号 + end_pos = text.rfind("。", 0, max_len + 10) + if end_pos > min_len: + text = text[: end_pos + 1] + + return text + + +def generate_test_texts( + count: int = 50, + length_range: Tuple[int, int] = (50, 100), +) -> List[str]: + """ + 生成测试文本列表 + + Args: + count: 生成数量 + length_range: 文本长度范围 + + Returns: + 文本列表 + """ + texts = [] + for _ in range(count): + text = generate_single_text(length_range) + texts.append(text) + + return texts + + +if __name__ == "__main__": + # 测试文本生成 + texts = generate_test_texts(5, (50, 100)) + for i, text in enumerate(texts, 1): + print(f"[{i}] ({len(text)}字): {text}") + print() diff --git a/scripts/build_docker.sh b/scripts/build_docker.sh new file mode 100644 index 0000000..74a6c7a --- /dev/null +++ b/scripts/build_docker.sh @@ -0,0 +1,469 @@ +#!/bin/bash +# +# Qwen3-ASR Docker Build Tool +# Step-by-step interactive or CLI parameter mode. + +set -euo pipefail + +# ============================================================================= +# Defaults +# ============================================================================= + +REGISTRY="unis" +IMAGE_NAME="qwen3-asr" +VERSION="latest" +BUILD_TYPE="all" +PLATFORM="linux/amd64" +PUSH="false" +EXPORT_TAR="false" +EXPORT_DIR="." +NO_CACHE="false" +LANG_MODE="zh" + +# ============================================================================= +# Localization +# ============================================================================= + +_resolve_lang() { + printf "%s" "$LANG_MODE" +} + +_msg() { + local k="$1" l; l="$(_resolve_lang)" + if [[ "$l" == "zh" ]]; then + case "$k" in + title) echo "Qwen3-ASR Docker 构建工具" ;; + subtitle) echo "按提示选择或直接回车使用默认值" ;; + q_lang) echo "界面语言" ;; + q_type) echo "构建目标" ;; + q_arch) echo "目标架构" ;; + q_version) echo "镜像版本" ;; + q_push) echo "推送到仓库" ;; + q_export) echo "导出 tar.gz" ;; + q_output) echo "输出目录" ;; + q_reg) echo "仓库命名空间" ;; + q_cache) echo "禁用构建缓存" ;; + q_confirm) echo "确认并开始构建" ;; + summary) echo "构建摘要" ;; + cancel) echo "已取消" ;; + start) echo "开始构建..." ;; + done) echo "构建完成" ;; + recent) echo "最近镜像" ;; + opt_cpu) echo "CPU" ;; + opt_gpu) echo "GPU" ;; + opt_metax) echo "沐曦 GPU" ;; + opt_iluvatar) echo "天数 GPU" ;; + opt_mthreads) echo "摩尔线程 GPU" ;; + opt_all) echo "全部 (CPU + GPU)" ;; + opt_amd64) echo "amd64 (x86_64)" ;; + opt_arm64) echo "arm64 (Apple Silicon / ARM)" ;; + opt_multi) echo "多架构 (amd64 + arm64)" ;; + err_inv) echo "无效输入" ;; + err_opt) echo "未知选项" ;; + err_gpu) echo "GPU 构建仅支持 amd64" ;; + err_bx) echo "需要 Docker Buildx" ;; + yes) echo "是" ;; + no) echo "否" ;; + esac + else + case "$k" in + title) echo "Qwen3-ASR Docker Build Tool" ;; + subtitle) echo "Select options or press Enter for defaults" ;; + q_lang) echo "Language" ;; + q_type) echo "Build target" ;; + q_arch) echo "Architecture" ;; + q_version) echo "Image version" ;; + q_push) echo "Push to registry" ;; + q_export) echo "Export tar.gz" ;; + q_output) echo "Output directory" ;; + q_reg) echo "Registry namespace" ;; + q_cache) echo "Disable build cache" ;; + q_confirm) echo "Confirm and start build" ;; + summary) echo "Build summary" ;; + cancel) echo "Cancelled" ;; + start) echo "Starting build..." ;; + done) echo "Build complete" ;; + recent) echo "Recent images" ;; + opt_cpu) echo "CPU" ;; + opt_gpu) echo "GPU" ;; + opt_metax) echo "MetaX GPU" ;; + opt_iluvatar) echo "Iluvatar GPU" ;; + opt_mthreads) echo "Moore Threads GPU" ;; + opt_all) echo "All (CPU + GPU)" ;; + opt_amd64) echo "amd64 (x86_64)" ;; + opt_arm64) echo "arm64 (Apple Silicon / ARM)" ;; + opt_multi) echo "Multi-arch (amd64 + arm64)" ;; + err_inv) echo "Invalid input" ;; + err_opt) echo "Unknown option" ;; + err_gpu) echo "GPU build only supports amd64" ;; + err_bx) echo "Docker Buildx is required" ;; + yes) echo "yes" ;; + no) echo "no" ;; + esac + fi +} + +_label_bool() { [[ "$1" == "true" ]] && _msg yes || _msg no; } + +# ============================================================================= +# Helpers +# ============================================================================= + +info() { echo "[INFO] $1" >&2; } +warn() { echo "[WARN] $1" >&2; } +die() { echo "[ERROR] $1" >&2; exit 1; } + +parse_arch() { + case "$1" in + amd64) echo "linux/amd64" ;; + arm64) echo "linux/arm64" ;; + multi) echo "linux/amd64,linux/arm64" ;; + *) die "$(_msg err_inv): $1" ;; + esac +} + +arch_label() { + case "$1" in + linux/amd64) echo "amd64" ;; + linux/arm64) echo "arm64" ;; + linux/amd64,linux/arm64) echo "multi" ;; + *) echo "$1" ;; + esac +} + +ensure_buildx() { + docker buildx version >/dev/null 2>&1 || die "$(_msg err_bx)" + if ! docker buildx inspect qwen3-asr-builder >/dev/null 2>&1; then + info "Creating buildx builder: qwen3-asr-builder" + docker buildx create --name qwen3-asr-builder --driver docker-container --use >/dev/null + else + docker buildx use qwen3-asr-builder >/dev/null + fi +} + +export_compressor() { + command -v pigz >/dev/null 2>&1 && echo "pigz -f" || echo "gzip -f" +} + +# ============================================================================= +# Build Core +# ============================================================================= + +build_image() { + local target="$1" dockerfile="$2" tag="$3" platform="$4" + local args=(buildx build --platform "$platform" -f "$dockerfile" -t "$tag") + + [[ "$NO_CACHE" == "true" ]] && args+=(--no-cache) + if [[ "$target" == "metax" ]]; then + args+=(--build-arg "METAX_BASE_IMAGE=${METAX_BASE_IMAGE}") + fi + if [[ "$target" == "iluvatar" ]]; then + args+=(--build-arg "ILUVATAR_BASE_IMAGE=${ILUVATAR_BASE_IMAGE}") + fi + if [[ "$target" == "mthreads" ]]; then + args+=(--build-arg "MTHREADS_BASE_IMAGE=${MTHREADS_BASE_IMAGE}") + fi + + if [[ "$platform" == *","* ]]; then + [[ "$EXPORT_TAR" == "true" ]] && warn "Multi-arch cannot export tar.gz" + args+=(--push) + elif [[ "$PUSH" == "true" ]]; then + args+=(--push) + elif [[ "$EXPORT_TAR" == "true" ]]; then + mkdir -p "$EXPORT_DIR" + local suffix="${target}-${VERSION}-$(basename "$platform")" + local tar="${EXPORT_DIR}/${IMAGE_NAME}-${suffix}.tar" + args+=(--output "type=docker,dest=${tar}") + else + args+=(--load) + fi + + info "Building ${target}: ${tag} (${platform})" + docker "${args[@]}" . + + if [[ "$EXPORT_TAR" == "true" && "$platform" != *","* && "$PUSH" != "true" ]]; then + local suffix="${target}-${VERSION}-$(basename "$platform")" + local tar="${EXPORT_DIR}/${IMAGE_NAME}-${suffix}.tar" + if [[ -f "$tar" ]]; then + info "Compressing ${tar}" + $(export_compressor) "$tar" + info "Exported ${tar}.gz" + fi + fi +} + +build_cpu() { + local tag="${REGISTRY}/${IMAGE_NAME}:${VERSION}" + [[ "$VERSION" == "latest" ]] && tag="${REGISTRY}/${IMAGE_NAME}:cpu-latest" + build_image "cpu" "Dockerfile.cpu" "$tag" "$PLATFORM" +} + +build_gpu() { + [[ "$PLATFORM" == *"arm64"* ]] && die "$(_msg err_gpu)" + build_image "gpu" "Dockerfile.gpu" "${REGISTRY}/${IMAGE_NAME}:gpu-${VERSION}" "linux/amd64" +} + +build_metax() { + [[ "$PLATFORM" == *"arm64"* ]] && die "$(_msg err_gpu)" + [[ -n "${METAX_BASE_IMAGE:-}" ]] || die "METAX_BASE_IMAGE is required for metax builds. Use an official MetaX vLLM image." + build_image "metax" "Dockerfile.metax" "${REGISTRY}/${IMAGE_NAME}:metax-${VERSION}" "linux/amd64" +} + +build_iluvatar() { + [[ "$PLATFORM" == *"arm64"* ]] && die "$(_msg err_gpu)" + [[ -n "${ILUVATAR_BASE_IMAGE:-}" ]] || die "ILUVATAR_BASE_IMAGE is required for iluvatar builds. Use the official Iluvatar vLLM image." + build_image "iluvatar" "Dockerfile.iluvatar" "${REGISTRY}/${IMAGE_NAME}:iluvatar-${VERSION}" "linux/amd64" +} + +build_mthreads() { + [[ "$PLATFORM" == *"arm64"* ]] && die "$(_msg err_gpu)" + [[ -n "${MTHREADS_BASE_IMAGE:-}" ]] || die "MTHREADS_BASE_IMAGE is required for mthreads builds. Use the official Moore Threads vLLM image." + build_image "mthreads" "Dockerfile.mthreads" "${REGISTRY}/${IMAGE_NAME}:mthreads-${VERSION}" "linux/amd64" +} + +# ============================================================================= +# Interactive: step-by-step +# ============================================================================= + +_ask() { + local __var_name="$1" prompt="$2" default="$3" l + local val + l="$(_resolve_lang)" + if [[ "$l" == "zh" ]]; then + read -r -p "${prompt} (默认: ${default}): " val + else + read -r -p "${prompt} (default: ${default}): " val + fi + printf -v "$__var_name" "%s" "${val:-$default}" +} + +_ask_bool() { + local __var_name="$1" prompt="$2" default="$3" hint l + local val + l="$(_resolve_lang)" + if [[ "$default" == "true" ]]; then + hint="[Y/n]" + else + hint="[y/N]" + fi + if [[ "$l" == "zh" ]]; then + read -r -p "${prompt} ${hint}: " val + else + read -r -p "${prompt} ${hint}: " val + fi + case "${val:-}" in + [Yy]|[Yy][Ee][Ss]|是) printf -v "$__var_name" "%s" "true" ;; + [Nn]|[Nn][Oo]|否) printf -v "$__var_name" "%s" "false" ;; + "") printf -v "$__var_name" "%s" "$default" ;; + *) warn "$(_msg err_inv): ${val}"; printf -v "$__var_name" "%s" "$default" ;; + esac +} + +_ask_choice() { + local __var_name="$1" prompt="$2" default="$3" l + shift 3 + local labels=() values=() i=1 + while [[ $# -ge 2 ]]; do + labels+=("$1") + values+=("$2") + shift 2 + done + + l="$(_resolve_lang)" + echo >&2 + echo "${prompt}:" >&2 + local idx=1 + for label in "${labels[@]}"; do + if [[ "$idx" -eq "$default" ]]; then + if [[ "$l" == "zh" ]]; then + echo " ${idx}) ${label} [默认]" >&2 + else + echo " ${idx}) ${label} [default]" >&2 + fi + else + echo " ${idx}) ${label}" >&2 + fi + ((idx++)) + done + + local val + if [[ "$l" == "zh" ]]; then + read -r -p "请选择 [1-${#labels[@]}] (默认: ${default}): " val + else + read -r -p "Select [1-${#labels[@]}] (default: ${default}): " val + fi + val="${val:-$default}" + + if [[ "$val" =~ ^[0-9]+$ ]] && [[ "$val" -ge 1 && "$val" -le "${#labels[@]}" ]]; then + printf -v "$__var_name" "%s" "${values[$((val-1))]}" + else + if [[ "$l" == "zh" ]]; then + warn "$(_msg err_inv): ${val},使用默认值 ${default}" + else + warn "$(_msg err_inv): ${val}, using default ${default}" + fi + printf -v "$__var_name" "%s" "${values[$((default-1))]}" + fi +} + +interactive_mode() { + echo + echo "========================================" + echo "$(_msg title)" + echo "$(_msg subtitle)" + echo "========================================" + echo + + # 1. Language first so subsequent prompts use it + _ask_choice LANG_MODE "$(_msg q_lang)" 1 \ + "中文" "zh" \ + "English" "en" + echo + + # 2. Build type (numbered) + _ask_choice BUILD_TYPE "$(_msg q_type)" 5 \ + "$(_msg opt_cpu)" "cpu" \ + "$(_msg opt_gpu)" "gpu" \ + "$(_msg opt_metax)" "metax" \ + "$(_msg opt_iluvatar)" "iluvatar" \ + "$(_msg opt_mthreads)" "mthreads" \ + "$(_msg opt_all)" "all" + + # 3. Architecture (numbered) + local default_arch=1 + [[ "$(arch_label "$PLATFORM")" == "arm64" ]] && default_arch=2 + [[ "$(arch_label "$PLATFORM")" == "multi" ]] && default_arch=3 + local arch_val + _ask_choice arch_val "$(_msg q_arch)" "$default_arch" \ + "$(_msg opt_amd64)" "amd64" \ + "$(_msg opt_arm64)" "arm64" \ + "$(_msg opt_multi)" "multi" + PLATFORM="$(parse_arch "$arch_val")" + + # 4. Free-form inputs + _ask VERSION "$(_msg q_version)" "$VERSION" + _ask_bool PUSH "$(_msg q_push)" "$PUSH" + _ask_bool EXPORT_TAR "$(_msg q_export)" "$EXPORT_TAR" + [[ "$EXPORT_TAR" == "true" ]] && _ask EXPORT_DIR "$(_msg q_output)" "$EXPORT_DIR" + _ask REGISTRY "$(_msg q_reg)" "$REGISTRY" + _ask_bool NO_CACHE "$(_msg q_cache)" "$NO_CACHE" + + # Summary + echo + echo "----------------------------------------" + echo "$(_msg summary)" + echo "----------------------------------------" + echo "$(_msg q_lang): $LANG_MODE" + echo "$(_msg q_type): $BUILD_TYPE" + echo "$(_msg q_arch): $(arch_label "$PLATFORM")" + echo "$(_msg q_version): $VERSION" + echo "$(_msg q_push): $(_label_bool "$PUSH")" + echo "$(_msg q_export): $(_label_bool "$EXPORT_TAR")" + [[ "$EXPORT_TAR" == "true" ]] && echo "$(_msg q_output): $EXPORT_DIR" + echo "$(_msg q_reg): $REGISTRY" + echo "$(_msg q_cache): $(_label_bool "$NO_CACHE")" + echo + + local confirm + read -r -p "$(_msg q_confirm) [Y/n]: " confirm + if [[ "$confirm" =~ ^[Nn]$ ]]; then info "$(_msg cancel)"; exit 0; fi + + info "$(_msg start)" +} + +# ============================================================================= +# CLI Mode +# ============================================================================= + +show_help() { + cat <&2 + echo "isolated" + return 0 + ;; + esac +} + +detect_accelerator() { + local configured="${ACCELERATOR:-auto}" + configured="${configured,,}" + + case "$configured" in + nvidia|metax|iluvatar|mthreads|cpu) + echo "$configured" + return 0 + ;; + cuda) + echo "nvidia" + return 0 + ;; + maca|muxi|mx) + echo "metax" + return 0 + ;; + ix|tianshu|天数) + echo "iluvatar" + return 0 + ;; + mthreads|musa|moorethreads|摩尔线程) + echo "mthreads" + return 0 + ;; + esac + + if command -v mx-smi >/dev/null 2>&1; then + echo "metax" + return 0 + fi + if command -v ixsmi >/dev/null 2>&1; then + echo "iluvatar" + return 0 + fi + if command -v mthreads-gmi >/dev/null 2>&1; then + echo "mthreads" + return 0 + fi + if command -v nvidia-smi >/dev/null 2>&1; then + echo "nvidia" + return 0 + fi + echo "cpu" +} + +list_nvidia_devices() { + if command -v nvidia-smi >/dev/null 2>&1; then + nvidia-smi --query-gpu=index --format=csv,noheader,nounits | sed '/^$/d' + fi +} + +list_metax_devices() { + if ! command -v mx-smi >/dev/null 2>&1; then + return 0 + fi + local output + output="$(mx-smi -L 2>/dev/null || true)" + if [[ -n "$output" ]]; then + echo "$output" | awk 'NF { print NR - 1 }' + return 0 + fi + output="$(mx-smi 2>/dev/null || true)" + if [[ -n "$output" ]]; then + echo "$output" | awk '/^[[:space:]]*\|[[:space:]]*[0-9]+[[:space:]]/ { print $2 }' + fi +} + +list_iluvatar_devices() { + if ! command -v ixsmi >/dev/null 2>&1; then + return 0 + fi + local output + output="$(ixsmi -L 2>/dev/null || true)" + if [[ -n "$output" ]]; then + echo "$output" | awk 'NF { print NR - 1 }' + return 0 + fi + output="$(ixsmi 2>/dev/null || true)" + if [[ -n "$output" ]]; then + echo "$output" | awk '/^[[:space:]]*\|[[:space:]]*[0-9]+[[:space:]]/ { print $2 }' + fi +} + +list_mthreads_devices() { + if ! command -v mthreads-gmi >/dev/null 2>&1; then + return 0 + fi + local output + output="$(mthreads-gmi -L 2>/dev/null || true)" + if [[ -n "$output" ]]; then + echo "$output" | awk 'NF { print NR - 1 }' + return 0 + fi + output="$(mthreads-gmi list 2>/dev/null || true)" + if [[ -n "$output" ]]; then + echo "$output" | awk '/^[[:space:]]*\|[[:space:]]*[0-9]+[[:space:]]/ { print $2 }' + return 0 + fi + output="$(mthreads-gmi 2>/dev/null || true)" + if [[ -n "$output" ]]; then + echo "$output" | awk '/^[[:space:]]*\|[[:space:]]*[0-9]+[[:space:]]/ { print $2 }' + fi +} + +normalize_visible_devices() { + local accelerator="$1" + local shared="${ASR_VISIBLE_DEVICES:-}" + shared="${shared// /}" + if [[ -n "$shared" && "$shared" != "none" && "$shared" != "void" ]]; then + if [[ "$shared" == "all" ]]; then + case "$accelerator" in + nvidia) mapfile -t gpu_indexes < <(list_nvidia_devices) ;; + metax) mapfile -t gpu_indexes < <(list_metax_devices) ;; + iluvatar) mapfile -t gpu_indexes < <(list_iluvatar_devices) ;; + mthreads) mapfile -t gpu_indexes < <(list_mthreads_devices) ;; + *) gpu_indexes=() ;; + esac + if [[ ${#gpu_indexes[@]} -eq 0 ]]; then + echo "" + return 0 + fi + local shared_joined + shared_joined=$(IFS=,; echo "${gpu_indexes[*]}") + echo "$shared_joined" + return 0 + fi + echo "$shared" + return 0 + fi + + local raw="" + case "$accelerator" in + nvidia) + raw="${CUDA_VISIBLE_DEVICES:-}" + ;; + metax) + raw="${METAX_VISIBLE_DEVICES:-${MACA_VISIBLE_DEVICES:-${MX_VISIBLE_DEVICES:-}}}" + ;; + iluvatar) + raw="${ILUVATAR_VISIBLE_DEVICES:-${IX_VISIBLE_DEVICES:-${CUDA_VISIBLE_DEVICES:-}}}" + ;; + mthreads) + raw="${MTHREADS_VISIBLE_DEVICES:-${MUSA_VISIBLE_DEVICES:-${CUDA_VISIBLE_DEVICES:-}}}" + ;; + *) + echo "" + return 0 + ;; + esac + raw="${raw// /}" + + if [[ -z "$raw" || "$raw" == "none" || "$raw" == "void" ]]; then + echo "" + return 0 + fi + + if [[ "$raw" == "all" ]]; then + case "$accelerator" in + nvidia) mapfile -t gpu_indexes < <(list_nvidia_devices) ;; + metax) mapfile -t gpu_indexes < <(list_metax_devices) ;; + iluvatar) mapfile -t gpu_indexes < <(list_iluvatar_devices) ;; + mthreads) mapfile -t gpu_indexes < <(list_mthreads_devices) ;; + esac + if [[ ${#gpu_indexes[@]} -eq 0 ]]; then + echo "" + return 0 + fi + local joined + joined=$(IFS=,; echo "${gpu_indexes[*]}") + echo "$joined" + return 0 + fi + + echo "$raw" +} + +export_visible_devices_aliases() { + local accelerator="$1" + local devices_csv="$2" + if [[ -z "$devices_csv" ]]; then + return 0 + fi + + export ASR_VISIBLE_DEVICES="$devices_csv" + case "$accelerator" in + nvidia) + export CUDA_VISIBLE_DEVICES="$devices_csv" + ;; + metax) + export METAX_VISIBLE_DEVICES="$devices_csv" + export MACA_VISIBLE_DEVICES="$devices_csv" + export MX_VISIBLE_DEVICES="$devices_csv" + ;; + iluvatar) + export ILUVATAR_VISIBLE_DEVICES="$devices_csv" + export IX_VISIBLE_DEVICES="$devices_csv" + export CUDA_VISIBLE_DEVICES="$devices_csv" + ;; + mthreads) + export MTHREADS_VISIBLE_DEVICES="$devices_csv" + export MUSA_VISIBLE_DEVICES="$devices_csv" + export CUDA_VISIBLE_DEVICES="$devices_csv" + ;; + esac +} + +supports_sharded_topology() { + local accelerator="$1" + case "$accelerator" in + nvidia|metax|iluvatar|mthreads) + return 0 + ;; + *) + return 1 + ;; + esac +} + +start_backend_process() { + local accelerator="$1" + local device="$2" + local port="$3" + local log_file="$4" + local bind_host="$5" + local workers="${WORKERS:-1}" + + if [[ "$accelerator" == "nvidia" && -n "$device" ]]; then + CUDA_VISIBLE_DEVICES="$device" \ + ACCELERATOR="nvidia" \ + DEVICE="cuda:0" \ + WORKERS="$workers" \ + HOST="$bind_host" \ + PORT="$port" \ + LOG_FILE="$log_file" \ + "$DEFAULT_CMD_1" "$DEFAULT_CMD_2" & + return 0 + fi + + if [[ "$accelerator" == "metax" && -n "$device" ]]; then + METAX_VISIBLE_DEVICES="$device" \ + MACA_VISIBLE_DEVICES="$device" \ + MX_VISIBLE_DEVICES="$device" \ + ACCELERATOR="metax" \ + DEVICE="cuda:0" \ + WORKERS="$workers" \ + HOST="$bind_host" \ + PORT="$port" \ + LOG_FILE="$log_file" \ + "$DEFAULT_CMD_1" "$DEFAULT_CMD_2" & + return 0 + fi + + if [[ "$accelerator" == "iluvatar" && -n "$device" ]]; then + ILUVATAR_VISIBLE_DEVICES="$device" \ + IX_VISIBLE_DEVICES="$device" \ + CUDA_VISIBLE_DEVICES="$device" \ + ACCELERATOR="iluvatar" \ + DEVICE="cuda:0" \ + WORKERS="$workers" \ + HOST="$bind_host" \ + PORT="$port" \ + LOG_FILE="$log_file" \ + "$DEFAULT_CMD_1" "$DEFAULT_CMD_2" & + return 0 + fi + + if [[ "$accelerator" == "mthreads" && -n "$device" ]]; then + MTHREADS_VISIBLE_DEVICES="$device" \ + MUSA_VISIBLE_DEVICES="$device" \ + CUDA_VISIBLE_DEVICES="$device" \ + ACCELERATOR="mthreads" \ + DEVICE="cuda:0" \ + WORKERS="$workers" \ + HOST="$bind_host" \ + PORT="$port" \ + LOG_FILE="$log_file" \ + "$DEFAULT_CMD_1" "$DEFAULT_CMD_2" & + return 0 + fi + + ACCELERATOR="$accelerator" \ + WORKERS="$workers" \ + HOST="$bind_host" \ + PORT="$port" \ + LOG_FILE="$log_file" \ + "$DEFAULT_CMD_1" "$DEFAULT_CMD_2" & +} + +exec_backend_process() { + local accelerator="$1" + local device="$2" + local port="$3" + local log_file="$4" + local bind_host="$5" + local workers="${WORKERS:-1}" + + if [[ "$accelerator" == "nvidia" && -n "$device" ]]; then + export CUDA_VISIBLE_DEVICES="$device" + export DEVICE="cuda:0" + elif [[ "$accelerator" == "metax" && -n "$device" ]]; then + export METAX_VISIBLE_DEVICES="$device" + export MACA_VISIBLE_DEVICES="$device" + export MX_VISIBLE_DEVICES="$device" + export DEVICE="cuda:0" + elif [[ "$accelerator" == "iluvatar" && -n "$device" ]]; then + export ILUVATAR_VISIBLE_DEVICES="$device" + export IX_VISIBLE_DEVICES="$device" + export CUDA_VISIBLE_DEVICES="$device" + export DEVICE="cuda:0" + elif [[ "$accelerator" == "mthreads" && -n "$device" ]]; then + export MTHREADS_VISIBLE_DEVICES="$device" + export MUSA_VISIBLE_DEVICES="$device" + export CUDA_VISIBLE_DEVICES="$device" + export DEVICE="cuda:0" + fi + + export ACCELERATOR="$accelerator" + export WORKERS="$workers" + export HOST="$bind_host" + export PORT="$port" + export LOG_FILE="$log_file" + exec "$DEFAULT_CMD_1" "$DEFAULT_CMD_2" +} + +start_sharded_backend_mode() { + local accelerator="$1" + local devices_csv="$2" + local bind_host="${HOST:-0.0.0.0}" + local public_port="${PORT:-8000}" + local log_file="${LOG_FILE:-/app/data/logs/qwen3-asr.log}" + local workers="${WORKERS:-1}" + local shard_size=0 + if [[ -n "$devices_csv" ]]; then + IFS=',' read -r -a devs <<< "$devices_csv" + shard_size="${#devs[@]}" + fi + + if ! supports_sharded_topology "$accelerator"; then + echo "[entrypoint] accelerator=${accelerator} does not support sharded topology, fallback to isolated" + return 1 + fi + if [[ -z "$devices_csv" ]]; then + echo "[entrypoint] sharded topology requires visible GPU devices, fallback to isolated" + return 1 + fi + if ! [[ "$shard_size" =~ ^[0-9]+$ ]] || (( shard_size < 2 )); then + echo "[entrypoint] sharded topology requires at least 2 devices, got shard_size=${shard_size}, fallback to isolated" + return 1 + fi + + echo "[entrypoint] Starting sharded backend: accelerator=${accelerator} devices=${devices_csv} shard_size=${shard_size} bind=${bind_host}:${public_port}" + export ASR_ACTIVE_TOPOLOGY="sharded" + export_visible_devices_aliases "$accelerator" "$devices_csv" + exec_backend_process "$accelerator" "" "$public_port" "$log_file" "$bind_host" +} + +wait_for_port() { + local host="$1" + local port="$2" + local timeout_sec="$3" + local deadline=$((SECONDS + timeout_sec)) + + while (( SECONDS < deadline )); do + if (echo >/dev/tcp/"$host"/"$port") >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + return 1 +} + +is_positive_integer() { + [[ "$1" =~ ^[0-9]+$ ]] && (( "$1" > 0 )) +} + +start_internal_nginx_mode() { + local devices_csv="$1" + local accelerator="${ACTIVE_ACCELERATOR:-auto}" + local bind_host="${MULTI_GPU_BIND_HOST:-127.0.0.1}" + local base_port="18000" + local public_port="${PORT:-8000}" + local ready_timeout="${MULTI_GPU_READY_TIMEOUT:-180}" + local rate_limit_rps="${NGINX_RATE_LIMIT_RPS:-0}" + local rate_limit_burst="${NGINX_RATE_LIMIT_BURST:-0}" + + local valid_devices=() + local devices=() + if [[ -n "$devices_csv" ]]; then + IFS=',' read -r -a devices <<< "$devices_csv" + for dev in "${devices[@]}"; do + if [[ -n "$dev" ]]; then + valid_devices+=("$dev") + fi + done + fi + + if [[ "$rate_limit_rps" != "0" ]] && ! is_positive_integer "$rate_limit_rps"; then + echo "[entrypoint] Invalid NGINX_RATE_LIMIT_RPS=${rate_limit_rps}, fallback to 0 (disabled)" + rate_limit_rps="0" + fi + if [[ "$rate_limit_burst" != "0" ]] && ! is_positive_integer "$rate_limit_burst"; then + echo "[entrypoint] Invalid NGINX_RATE_LIMIT_BURST=${rate_limit_burst}, fallback to 0" + rate_limit_burst="0" + fi + if [[ "$rate_limit_rps" != "0" && "$rate_limit_burst" == "0" ]]; then + # By default, give short burst headroom equal to rate limit. + rate_limit_burst="$rate_limit_rps" + fi + + if ! command -v nginx >/dev/null 2>&1; then + if [[ ${#valid_devices[@]} -le 1 ]]; then + local direct_device="" + local direct_log_file="${LOG_FILE:-/app/data/logs/qwen3-asr.log}" + local direct_bind_host="${HOST:-0.0.0.0}" + if [[ ${#valid_devices[@]} -eq 1 ]]; then + direct_device="${valid_devices[0]}" + fi + echo "[entrypoint] nginx not found; starting single backend directly on ${direct_bind_host}:${public_port}" + exec_backend_process "$accelerator" "$direct_device" "$public_port" "$direct_log_file" "$direct_bind_host" + fi + echo "[entrypoint] nginx not found in image; cannot start multi-backend proxy mode" + exit 1 + fi + + local backend_ports=() + local backend_pids=() + local idx=0 + local default_log_file="${LOG_FILE:-/app/data/logs/qwen3-asr.log}" + + if [[ ${#valid_devices[@]} -eq 0 ]]; then + local single_port="$base_port" + local single_log_file="${default_log_file%.log}-gpu0.log" + backend_ports+=("$single_port") + + echo "[entrypoint] No explicit multi-accelerator list detected, starting single backend instance (${accelerator})" + start_backend_process "$accelerator" "" "$single_port" "$single_log_file" "$bind_host" + backend_pids+=("$!") + else + if [[ ${#valid_devices[@]} -eq 1 ]]; then + echo "[entrypoint] Single accelerator device detected (${valid_devices[0]}), starting one backend instance (${accelerator})" + local single_gpu_port="$base_port" + local single_gpu_log_file="${default_log_file%.log}-gpu0.log" + backend_ports+=("$single_gpu_port") + + start_backend_process "$accelerator" "${valid_devices[0]}" "$single_gpu_port" "$single_gpu_log_file" "$bind_host" + backend_pids+=("$!") + else + echo "[entrypoint] Multi-accelerator detected, starting one instance per device (${accelerator}): ${valid_devices[*]}" + for dev in "${valid_devices[@]}"; do + local port=$((base_port + idx)) + local instance_log_file="${default_log_file%.log}-gpu${idx}.log" + backend_ports+=("$port") + + echo "[entrypoint] Starting ASR instance #${idx} on ${accelerator} device ${dev}, bind ${bind_host}:${port}" + WORKERS="1" start_backend_process "$accelerator" "$dev" "$port" "$instance_log_file" "$bind_host" + + backend_pids+=("$!") + idx=$((idx + 1)) + done + fi + fi + + local port + for port in "${backend_ports[@]}"; do + if ! wait_for_port "$bind_host" "$port" "$ready_timeout"; then + echo "[entrypoint] Backend instance on ${bind_host}:${port} failed to become ready in ${ready_timeout}s" + for pid in "${backend_pids[@]}"; do + kill "$pid" >/dev/null 2>&1 || true + done + wait >/dev/null 2>&1 || true + exit 1 + fi + done + + local nginx_conf="/tmp/qwen3-asr-internal-nginx.conf" + { + echo "worker_processes auto;" + echo "events { worker_connections 1024; }" + echo "http {" + echo " limit_req_status 429;" + if is_positive_integer "$rate_limit_rps"; then + # Global token bucket for the whole service, not per-client-IP. + echo " limit_req_zone \$server_name zone=api_rps:10m rate=${rate_limit_rps}r/s;" + fi + echo + echo " upstream qwen3_asr_upstream {" + echo " least_conn;" + for port in "${backend_ports[@]}"; do + echo " server ${bind_host}:${port} max_fails=3 fail_timeout=10s;" + done + echo " keepalive 128;" + echo " }" + echo + echo " map \$http_upgrade \$connection_upgrade {" + echo " default upgrade;" + echo " '' close;" + echo " }" + echo + echo " server {" + echo " listen ${public_port};" + echo " server_name _;" + echo " client_max_body_size 2048m;" + echo + echo " location / {" + if is_positive_integer "$rate_limit_rps"; then + echo " limit_req zone=api_rps burst=${rate_limit_burst} nodelay;" + fi + echo " proxy_pass http://qwen3_asr_upstream;" + echo " proxy_http_version 1.1;" + echo " proxy_set_header Host \$host;" + echo " proxy_set_header X-Real-IP \$remote_addr;" + echo " proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;" + echo " proxy_set_header X-Forwarded-Proto \$scheme;" + echo " proxy_set_header Upgrade \$http_upgrade;" + echo " proxy_set_header Connection \$connection_upgrade;" + echo " proxy_connect_timeout 10s;" + echo " proxy_send_timeout 3600s;" + echo " proxy_read_timeout 3600s;" + echo " send_timeout 3600s;" + echo " proxy_buffering off;" + echo " }" + echo " }" + echo "}" + } > "$nginx_conf" + + local nginx_pid="" + + cleanup() { + set +e + if [[ -n "$nginx_pid" ]]; then + kill "$nginx_pid" >/dev/null 2>&1 || true + fi + for pid in "${backend_pids[@]}"; do + kill "$pid" >/dev/null 2>&1 || true + done + wait >/dev/null 2>&1 || true + } + trap cleanup EXIT INT TERM + + echo "[entrypoint] Starting internal nginx load balancer on :${public_port}" + nginx -c "$nginx_conf" -g "daemon off;" & + nginx_pid="$!" + + while true; do + if ! kill -0 "$nginx_pid" >/dev/null 2>&1; then + echo "[entrypoint] nginx exited unexpectedly" + exit 1 + fi + for pid in "${backend_pids[@]}"; do + if ! kill -0 "$pid" >/dev/null 2>&1; then + echo "[entrypoint] backend process ${pid} exited unexpectedly" + exit 1 + fi + done + sleep 2 + done +} + +has_default_cmd=false +if [[ $# -eq 2 && "$1" == "$DEFAULT_CMD_1" && "$2" == "$DEFAULT_CMD_2" ]]; then + has_default_cmd=true +fi + +# If user passes a custom command, respect it and bypass auto multi-GPU logic. +if [[ $# -gt 0 && "$has_default_cmd" != "true" ]]; then + exec "$@" +fi + +ACTIVE_ACCELERATOR="$(detect_accelerator)" +devices_csv="$(normalize_visible_devices "$ACTIVE_ACCELERATOR")" +topology="$(normalize_topology)" + +case "$topology" in + isolated) + export ASR_ACTIVE_TOPOLOGY="isolated" + start_internal_nginx_mode "$devices_csv" + ;; + sharded) + if ! start_sharded_backend_mode "$ACTIVE_ACCELERATOR" "$devices_csv"; then + export ASR_ACTIVE_TOPOLOGY="isolated" + start_internal_nginx_mode "$devices_csv" + fi + ;; + auto) + if start_sharded_backend_mode "$ACTIVE_ACCELERATOR" "$devices_csv"; then + exit 0 + fi + export ASR_ACTIVE_TOPOLOGY="isolated" + start_internal_nginx_mode "$devices_csv" + ;; +esac diff --git a/scripts/docker/init_host_dirs.sh b/scripts/docker/init_host_dirs.sh new file mode 100644 index 0000000..4538b57 --- /dev/null +++ b/scripts/docker/init_host_dirs.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -euo pipefail + +MODEL_DIR="${MODEL_STORAGE_DIR:-/opt/dep/asr/models}" +DATA_DIR="${DATA_STORAGE_DIR:-/opt/dep/asr/data}" +LOG_DIR="${DATA_DIR}/logs" +TEMP_DIR="${DATA_DIR}/temp" +TASK_DIR="${DATA_DIR}/tasks" + +mkdir -p "$MODEL_DIR" "$LOG_DIR" "$TEMP_DIR" "$TASK_DIR" + +echo "已创建宿主机目录:" +echo " models: $MODEL_DIR" +echo " data: $DATA_DIR" +echo " logs: $LOG_DIR" +echo " temp: $TEMP_DIR" +echo " tasks: $TASK_DIR" diff --git a/scripts/download-models.sh b/scripts/download-models.sh new file mode 100644 index 0000000..3a88e72 --- /dev/null +++ b/scripts/download-models.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env bash +# +# Incremental model downloader. +# Downloads missing models into an existing models directory without deleting +# existing files and without requiring uv. +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +if [[ -f "${PROJECT_ROOT}/app/utils/download_models.py" ]]; then + DEFAULT_MODEL_DIR="${PROJECT_ROOT}/models" +else + DEFAULT_MODEL_DIR="/opt/dep/asr/models" +fi +MODEL_DIR="${MODEL_STORAGE_DIR:-${MODELS_DIR:-$DEFAULT_MODEL_DIR}}" +PYTHON_BIN="${PYTHON_BIN:-${PYTHON:-python3}}" +AUTO_MODE="false" +MODE="auto" +DOCKER_IMAGE="${ASR_IMAGE:-}" +ARG_COUNT="$#" +RUNTIME_MODELS_DIR="" +INTERACTIVE_DOCKER_TARGET="false" + +info() { echo "[INFO] $1"; } +die() { echo "[ERROR] $1" >&2; exit 1; } + +usage() { + cat </dev/null 2>&1 || die "Python not found: ${PYTHON_BIN}" + info " Runner: local python (${PYTHON_BIN})" + + local args=("$downloader" --models-dir "$MODEL_DIR" --cache-dir "${MODELSCOPE_CACHE:-$MODEL_PARENT_DIR}") + if [[ -n "$RUNTIME_MODELS_DIR" ]]; then + args+=(--runtime-models-dir "$RUNTIME_MODELS_DIR") + fi + if [[ "$AUTO_MODE" == "true" ]]; then + args+=(--auto-mode) + fi + "$PYTHON_BIN" "${args[@]}" +} + +run_docker_download() { + command -v docker >/dev/null 2>&1 || die "Docker not found; cannot use docker mode" + [[ -n "$DOCKER_IMAGE" ]] || die "Docker image not set; pass --docker-image IMAGE or set ASR_IMAGE in .env" + [[ -f "${SCRIPT_DIR}/download_models_standalone.py" ]] || die "Cannot find download_models_standalone.py" + info " Runner: docker image (${DOCKER_IMAGE})" + + local args=(/tmp/download_models_standalone.py --models-dir /app/models --cache-dir /app) + if [[ -n "$RUNTIME_MODELS_DIR" ]]; then + args+=(--runtime-models-dir "$RUNTIME_MODELS_DIR") + fi + if [[ "$AUTO_MODE" == "true" ]]; then + args+=(--auto-mode) + fi + + docker run --rm \ + -e MODELS_DIR=/app/models \ + -e MODELSCOPE_PATH=/app/models \ + -e MODELSCOPE_CACHE=/app \ + -e DATA_DIR=/app/data \ + -e TEMP_DIR=/app/data/temp \ + -e LOG_FILE=/app/data/logs/qwen3-asr.log \ + -v "${MODEL_DIR}:/app/models" \ + -v "${SCRIPT_DIR}/download_models_standalone.py:/tmp/download_models_standalone.py:ro" \ + "${DOCKER_IMAGE}" \ + python3 "${args[@]}" +} + +case "$MODE" in + local) + run_local_download + ;; + docker) + run_docker_download + ;; + auto) + if ! run_local_download; then + run_docker_download + fi + ;; +esac + +info "Model download complete" +info "Models directory: ${MODEL_DIR}" diff --git a/scripts/download_models_standalone.py b/scripts/download_models_standalone.py new file mode 100644 index 0000000..1f6f2c9 --- /dev/null +++ b/scripts/download_models_standalone.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Standalone incremental model downloader. + +This script intentionally does not import the application package. It is meant +for offline delivery hosts where only ModelScope and a Python runtime are +needed to fill /opt/dep/asr/models. +""" + +from __future__ import annotations + +import argparse +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Optional + +from modelscope.hub.snapshot_download import snapshot_download + + +@dataclass(frozen=True) +class ModelAsset: + model_id: str + description: str + revision: Optional[str] = None + required_patterns: tuple[str, ...] = () + alternative_required_patterns: tuple[tuple[str, ...], ...] = () + min_total_size_bytes: int = 0 + + +MODEL_ASSETS: tuple[ModelAsset, ...] = ( + ModelAsset( + model_id="damo/speech_fsmn_vad_zh-cn-16k-common-pytorch", + description="VAD", + revision="v2.0.2", + required_patterns=("configuration.json", "config.yaml", "model.pb"), + min_total_size_bytes=1_000_000, + ), + ModelAsset( + model_id="iic/speech_campplus_speaker-diarization_common", + description="CAM++ Diarization", + required_patterns=( + "configuration.json", + "config.yaml", + "onnx/asd.onnx", + "onnx/face_recog_ir101.onnx", + "onnx/fqa.onnx", + "onnx/version-RFB-320.onnx", + ), + min_total_size_bytes=50_000_000, + ), + ModelAsset( + model_id="iic/speech_campplus_sv_zh-cn_16k-common", + description="Configured Speaker Verification", + required_patterns=("configuration.json", "config.yaml", "campplus_cn_common.bin"), + min_total_size_bytes=10_000_000, + ), + ModelAsset( + model_id="iic/speech_eres2netv2_sv_zh-cn_16k-common", + description="Realtime Speaker Verification", + required_patterns=("configuration.json",), + min_total_size_bytes=10_000_000, + ), + ModelAsset( + model_id="damo/speech_campplus_sv_zh-cn_16k-common", + description="CAM++ Speaker Verification", + required_patterns=("configuration.json", "config.yaml", "campplus_cn_common.bin"), + min_total_size_bytes=10_000_000, + ), + ModelAsset( + model_id="damo/speech_campplus-transformer_scl_zh-cn_16k-common", + description="CAM++ Transformer", + required_patterns=("configuration.json", "campplus_cn_encoder.pt", "transformer_backend.pt"), + min_total_size_bytes=10_000_000, + ), + ModelAsset( + model_id="Qwen/Qwen3-ASR-0.6B", + description="Qwen3-ASR-0.6B Offline", + required_patterns=("config.json",), + alternative_required_patterns=(("model.safetensors",), ("model-*.safetensors",)), + min_total_size_bytes=500_000_000, + ), + ModelAsset( + model_id="Qwen/Qwen3-ForcedAligner-0.6B", + description="Qwen3 Forced Aligner", + required_patterns=("config.json", "model.safetensors"), + min_total_size_bytes=500_000_000, + ), + ModelAsset( + model_id="Qwen/Qwen3-ASR-1.7B", + description="Qwen3-ASR-1.7B Offline", + required_patterns=("config.json",), + alternative_required_patterns=(("model.safetensors",), ("model-*.safetensors",)), + min_total_size_bytes=500_000_000, + ), +) + + +def model_path(models_dir: Path, model_id: str) -> Path: + return models_dir / model_id + + +def find_missing_patterns(root: Path, patterns: tuple[str, ...]) -> list[str]: + return [pattern for pattern in patterns if not any(root.glob(pattern))] + + +def has_required_alternative(root: Path, pattern_groups: tuple[tuple[str, ...], ...]) -> bool: + if not pattern_groups: + return True + return any(not find_missing_patterns(root, group) for group in pattern_groups) + + +def directory_size(path: Path) -> int: + return sum(item.stat().st_size for item in path.rglob("*") if item.is_file()) + + +def check_asset(models_dir: Path, asset: ModelAsset) -> tuple[bool, str]: + path = model_path(models_dir, asset.model_id) + if not path.exists() or not path.is_dir(): + return False, "directory_missing" + if not any(path.iterdir()): + return False, "directory_empty" + missing = find_missing_patterns(path, asset.required_patterns) + if missing: + return False, "missing=" + ",".join(missing) + if not has_required_alternative(path, asset.alternative_required_patterns): + alternatives = " OR ".join(" + ".join(group) for group in asset.alternative_required_patterns) + return False, "missing=" + alternatives + size = directory_size(path) + if size < asset.min_total_size_bytes: + return False, f"too_small={size}" + return True, "ok" + + +def fix_camplusplus_config(models_dir: Path, runtime_models_dir: Optional[Path] = None) -> bool: + config_file = models_dir / "iic/speech_campplus_speaker-diarization_common/configuration.json" + if not config_file.exists(): + return False + + runtime_dir = runtime_models_dir or models_dir + replacements = { + "damo/speech_campplus_sv_zh-cn_16k-common": "damo/speech_campplus_sv_zh-cn_16k-common", + "iic/speech_campplus_sv_zh-cn_16k-common": "iic/speech_campplus_sv_zh-cn_16k-common", + "damo/speech_campplus-transformer_scl_zh-cn_16k-common": "damo/speech_campplus-transformer_scl_zh-cn_16k-common", + "damo/speech_campplus-transformer_scl_zh-cn-16k-common": "damo/speech_campplus-transformer_scl_zh-cn-16k-common", + "damo/speech_fsmn_vad_zh-cn-16k-common-pytorch": "damo/speech_fsmn_vad_zh-cn-16k-common-pytorch", + } + + try: + config = json.loads(config_file.read_text(encoding="utf-8")) + except Exception as exc: + print(f"⚠️ CAM++ 配置读取失败: {exc}") + return False + + modified = False + model_config = config.get("model") + if isinstance(model_config, dict): + for key in ("speaker_model", "change_locator", "vad_model"): + old_value = model_config.get(key) + relative_path = replacements.get(str(old_value)) + if not relative_path and isinstance(old_value, str): + for candidate in replacements.values(): + if old_value.endswith(candidate): + relative_path = candidate + break + if relative_path and (models_dir / relative_path).exists(): + model_config[key] = str(runtime_dir / relative_path) + modified = True + + if not modified: + return False + + config_file.write_text( + json.dumps(config, indent=4, ensure_ascii=False), + encoding="utf-8", + ) + return True + + +def download_missing( + models_dir: Path, + cache_dir: Path, + auto_mode: bool = False, + runtime_models_dir: Optional[Path] = None, +) -> bool: + models_dir.mkdir(parents=True, exist_ok=True) + cache_dir.mkdir(parents=True, exist_ok=True) + + missing: list[tuple[ModelAsset, str]] = [] + for asset in MODEL_ASSETS: + ok, reason = check_asset(models_dir, asset) + if not ok: + missing.append((asset, reason)) + + if not missing: + if not auto_mode: + print("✅ 所有模型已存在且完整,无需下载") + fix_camplusplus_config(models_dir, runtime_models_dir=runtime_models_dir) + return True + + print(f"📦 检测到 {len(missing)} 个模型需要下载/补齐") + if not auto_mode: + for asset, reason in missing: + print(f" - {asset.model_id} ({reason})") + + failed: list[tuple[str, str]] = [] + for index, (asset, _reason) in enumerate(missing, start=1): + if not auto_mode: + print(f"\n[{index}/{len(missing)}] {asset.description}") + print(f" 模型ID: {asset.model_id}") + try: + kwargs = { + "cache_dir": str(cache_dir), + "local_dir": str(model_path(models_dir, asset.model_id)), + } + if asset.revision: + kwargs["revision"] = asset.revision + snapshot_download(asset.model_id, **kwargs) + except Exception as exc: + print(f"❌ 下载失败: {asset.model_id}: {exc}") + failed.append((asset.model_id, str(exc))) + + if fix_camplusplus_config(models_dir, runtime_models_dir=runtime_models_dir) and not auto_mode: + print("✅ CAM++ 配置已修复为本地模型路径") + + if failed: + print("\n失败模型:") + for model_id, error in failed: + print(f" - {model_id}: {error}") + return False + + still_missing = [ + (asset, reason) + for asset in MODEL_ASSETS + for ok, reason in [check_asset(models_dir, asset)] + if not ok + ] + if still_missing: + print("\n仍不完整的模型:") + for asset, reason in still_missing: + print(f" - {asset.model_id}: {reason}") + return False + + print("✅ 所有模型准备就绪") + return True + + +def main() -> int: + parser = argparse.ArgumentParser(description="Download Qwen3-ASR models without importing app code") + parser.add_argument( + "--models-dir", + default=None, + help="Model directory to fill; default: MODELSCOPE_PATH, MODELS_DIR, or ./models", + ) + parser.add_argument( + "--cache-dir", + default=None, + help="ModelScope cache directory; default: parent of models-dir", + ) + parser.add_argument( + "--runtime-models-dir", + default=None, + help="Runtime model directory to write into CAM++ config; default: models-dir", + ) + parser.add_argument("--auto-mode", action="store_true", help="Reduce output") + args = parser.parse_args() + + import os + + models_dir = Path( + args.models_dir + or os.getenv("MODELSCOPE_PATH") + or os.getenv("MODELS_DIR") + or "./models" + ).resolve() + cache_dir = Path(args.cache_dir or os.getenv("MODELSCOPE_CACHE") or models_dir.parent).resolve() + runtime_models_dir = Path(args.runtime_models_dir).resolve() if args.runtime_models_dir else None + return 0 if download_missing( + models_dir, + cache_dir, + auto_mode=args.auto_mode, + runtime_models_dir=runtime_models_dir, + ) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/package_vendor_gpu_image.sh b/scripts/package_vendor_gpu_image.sh new file mode 100644 index 0000000..2c5bcbd --- /dev/null +++ b/scripts/package_vendor_gpu_image.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +VENDOR="" +BASE_IMAGE="" +REGISTRY="unis" +IMAGE_NAME="qwen3-asr" +VERSION="$(date +"%Y%m%d_%H%M%S")" +OUTPUT_DIR="${ROOT_DIR}/build-file" +EXPORT_TAR="true" +NO_CACHE="false" +PYTHON_BIN="" + +usage() { + cat <&2; exit 1; } +info() { echo "[INFO] $1" >&2; } +compressor() { command -v pigz >/dev/null 2>&1 && echo "pigz -f" || echo "gzip -f"; } + +while [[ $# -gt 0 ]]; do + case "$1" in + --vendor) VENDOR="$2"; shift 2 ;; + --base-image) BASE_IMAGE="$2"; shift 2 ;; + -v|--version) VERSION="$2"; shift 2 ;; + -r|--registry) REGISTRY="$2"; shift 2 ;; + -o|--output) OUTPUT_DIR="$2"; shift 2 ;; + --python-bin) PYTHON_BIN="$2"; shift 2 ;; + --no-export) EXPORT_TAR="false"; shift ;; + --no-cache) NO_CACHE="true"; shift ;; + -h|--help) usage; exit 0 ;; + *) die "Unknown argument: $1" ;; + esac +done + +[[ -n "$VENDOR" ]] || die "--vendor is required" +[[ -n "$BASE_IMAGE" ]] || die "--base-image is required" + +case "$VENDOR" in + metax) + dockerfile="Dockerfile.metax" + tag="${REGISTRY}/${IMAGE_NAME}:metax-${VERSION}" + target="metax" + build_arg_name="METAX_BASE_IMAGE" + default_python_bin="/opt/conda/bin/python" + ;; + iluvatar|ix|tianshu) + dockerfile="Dockerfile.iluvatar" + tag="${REGISTRY}/${IMAGE_NAME}:iluvatar-${VERSION}" + target="iluvatar" + build_arg_name="ILUVATAR_BASE_IMAGE" + default_python_bin="python3" + ;; + mthreads|musa|moorethreads) + dockerfile="Dockerfile.mthreads" + tag="${REGISTRY}/${IMAGE_NAME}:mthreads-${VERSION}" + target="mthreads" + build_arg_name="MTHREADS_BASE_IMAGE" + default_python_bin="python3" + ;; + *) + die "Unsupported vendor: ${VENDOR}" + ;; +esac + +PYTHON_BIN="${PYTHON_BIN:-$default_python_bin}" + +docker image inspect "$BASE_IMAGE" >/dev/null 2>&1 || { + die "Base image not found locally: ${BASE_IMAGE}. Run docker load first or docker pull it." +} + +args=( + build + -f "$dockerfile" + -t "$tag" + --build-arg "${build_arg_name}=${BASE_IMAGE}" + --build-arg "PYTHON_BIN=${PYTHON_BIN}" +) +[[ "$NO_CACHE" == "true" ]] && args+=(--no-cache) + +info "Building fused image: ${tag}" +info "Vendor base image: ${BASE_IMAGE}" +info "Base image Python: ${PYTHON_BIN}" +( + cd "$ROOT_DIR" + docker "${args[@]}" . +) + +if [[ "$EXPORT_TAR" == "true" ]]; then + mkdir -p "$OUTPUT_DIR" + tar_path="${OUTPUT_DIR}/${IMAGE_NAME}-${target}-${VERSION}-amd64.tar" + info "Exporting image archive: ${tar_path}" + docker save -o "$tar_path" "$tag" + info "Compressing archive" + $(compressor) "$tar_path" + info "Done: ${tar_path}.gz" +else + info "Built image: ${tag}" +fi diff --git a/scripts/prepare-models.sh b/scripts/prepare-models.sh new file mode 100644 index 0000000..4f0e07e --- /dev/null +++ b/scripts/prepare-models.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# +# Model Export Tool for Offline Deployment +# + +set -euo pipefail + +OUTPUT_DIR="./models" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +info() { echo "[INFO] $1"; } +die() { echo "[ERROR] $1" >&2; exit 1; } + +# Check if running from project root or scripts directory +if [ -f "$PROJECT_ROOT/app/utils/download_models.py" ]; then + cd "$PROJECT_ROOT" +elif [ -f "$SCRIPT_DIR/../app/utils/download_models.py" ]; then + cd "$SCRIPT_DIR/.." +else + die "Cannot find app/utils/download_models.py" +fi + +command -v uv >/dev/null 2>&1 || die "uv not found; install uv first" + +# Confirm +info "Export settings:" +info " Models: all declared offline/realtime/runtime models" +info " Output: ${OUTPUT_DIR}" +read -p "Start export? [Y/n]: " confirm +if [[ $confirm =~ ^[Nn]$ ]]; then + echo "Cancelled." + exit 0 +fi + +info "Exporting models..." + +# Remove existing models dir to ensure clean state +rm -rf "${OUTPUT_DIR}" + +# Run Python export +uv run python -m app.utils.download_models --export-dir "${OUTPUT_DIR}" + +info "Packaging..." + +PACKAGE="qwen3-asr-models-$(date +%Y%m%d-%H%M).tar.gz" + +# Use pigz for multi-threaded compression if available +if command -v pigz &> /dev/null; then + info "Using pigz for multi-threaded compression..." + # Get CPU cores (cross-platform: Linux and macOS) + if command -v nproc &> /dev/null; then + CPU_CORES=$(nproc) + elif command -v sysctl &> /dev/null; then + CPU_CORES=$(sysctl -n hw.ncpu) + else + CPU_CORES=4 + fi + tar -cf - "${OUTPUT_DIR}" | pigz -p "${CPU_CORES}" > "${PACKAGE}" +else + info "pigz not found, using standard gzip..." + tar -czf "${PACKAGE}" "${OUTPUT_DIR}" +fi + +SIZE=$(du -sh "${PACKAGE}" | cut -f1) + +# Done +info "Export complete" +info "Package: ${PACKAGE}" +info "Size: ${SIZE}" +echo "To deploy on an offline server:" +echo "1. Copy package: scp ${PACKAGE} user@server:/opt/qwen3-asr/" +echo "2. Extract: tar -xzvf ${PACKAGE}" +echo "3. Start service: docker-compose up -d" diff --git a/scripts/sync_accel_env.sh b/scripts/sync_accel_env.sh new file mode 100644 index 0000000..9fc8c1f --- /dev/null +++ b/scripts/sync_accel_env.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +configured="${ACCELERATOR:-auto}" +configured="${configured,,}" + +detect_accelerator() { + case "$configured" in + metax|maca|muxi|mx) + echo "metax" + return 0 + ;; + iluvatar|ix|tianshu|天数) + echo "iluvatar" + return 0 + ;; + mthreads|musa|moorethreads|摩尔线程) + echo "mthreads" + return 0 + ;; + nvidia|cuda) + echo "nvidia" + return 0 + ;; + cpu) + echo "cpu" + return 0 + ;; + esac + + if command -v mx-smi >/dev/null 2>&1; then + echo "metax" + return 0 + fi + if command -v ixsmi >/dev/null 2>&1; then + echo "iluvatar" + return 0 + fi + if command -v mthreads-gmi >/dev/null 2>&1; then + echo "mthreads" + return 0 + fi + if command -v nvidia-smi >/dev/null 2>&1; then + echo "nvidia" + return 0 + fi + echo "cpu" +} + +accelerator="$(detect_accelerator)" +echo "[sync_accel_env] detected accelerator=${accelerator}" + +case "$accelerator" in + metax) + exec "$ROOT_DIR/scripts/sync_metax_env.sh" "$@" + ;; + iluvatar) + exec "$ROOT_DIR/scripts/sync_iluvatar_env.sh" "$@" + ;; + mthreads) + exec "$ROOT_DIR/scripts/sync_mthreads_env.sh" "$@" + ;; + nvidia) + exec "$ROOT_DIR/scripts/sync_gpu_env.sh" "$@" + ;; + cpu) + exec "$ROOT_DIR/scripts/sync_cpu_env.sh" "$@" + ;; +esac diff --git a/scripts/sync_cpu_env.sh b/scripts/sync_cpu_env.sh new file mode 100644 index 0000000..192539e --- /dev/null +++ b/scripts/sync_cpu_env.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +export UV_PROJECT_ENVIRONMENT="${UV_PROJECT_ENVIRONMENT:-$ROOT_DIR/.venv}" + +cd "$ROOT_DIR" +exec uv sync --project "$ROOT_DIR/environments/cpu" --frozen "$@" diff --git a/scripts/sync_gpu_env.sh b/scripts/sync_gpu_env.sh new file mode 100644 index 0000000..4afad88 --- /dev/null +++ b/scripts/sync_gpu_env.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +export UV_PROJECT_ENVIRONMENT="${UV_PROJECT_ENVIRONMENT:-$ROOT_DIR/.venv}" + +cd "$ROOT_DIR" +exec uv sync --frozen "$@" diff --git a/scripts/sync_iluvatar_env.sh b/scripts/sync_iluvatar_env.sh new file mode 100644 index 0000000..c6db485 --- /dev/null +++ b/scripts/sync_iluvatar_env.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +usage() { + cat <<'EOF' +Usage: + ./scripts/sync_iluvatar_env.sh [uv sync args...] + +This syncs only the generic Qwen3-ASR dependencies for an Iluvatar host. +The Iluvatar GPU stack (IX runtime, PyTorch, vLLM, kernels) should come from +the official Iluvatar vLLM image or the vendor's matching installation package. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +cd "$ROOT_DIR" + +uv sync --project "$ROOT_DIR/environments/iluvatar" --no-dev --no-install-project "$@" + +echo "[sync_iluvatar_env] Synced common dependencies only." +echo "[sync_iluvatar_env] Use the official Iluvatar vLLM Docker image for the GPU runtime stack." diff --git a/scripts/sync_metax_env.sh b/scripts/sync_metax_env.sh new file mode 100644 index 0000000..4cbc219 --- /dev/null +++ b/scripts/sync_metax_env.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +export UV_PROJECT_ENVIRONMENT="${UV_PROJECT_ENVIRONMENT:-$ROOT_DIR/.venv}" + +METAX_PYPI_INDEX="${METAX_PYPI_INDEX:-https://repos.metax-tech.com/r/maca-pypi/simple}" +METAX_PYPI_HOST="${METAX_PYPI_HOST:-repos.metax-tech.com}" +METAX_INSTALL_GPU_DEPS="${METAX_INSTALL_GPU_DEPS:-false}" + +METAX_TORCH_SPEC="${METAX_TORCH_SPEC:-torch}" +METAX_TORCHAUDIO_SPEC="${METAX_TORCHAUDIO_SPEC:-}" +METAX_TORCHVISION_SPEC="${METAX_TORCHVISION_SPEC:-}" +METAX_VLLM_SPEC="${METAX_VLLM_SPEC:-vllm}" + +usage() { + cat </dev/null 2>&1; then + pip3 index versions "$LIST_PACKAGE" \ + -i "$METAX_PYPI_INDEX" \ + --trusted-host "$METAX_PYPI_HOST" + elif command -v pip >/dev/null 2>&1; then + pip index versions "$LIST_PACKAGE" \ + -i "$METAX_PYPI_INDEX" \ + --trusted-host "$METAX_PYPI_HOST" + else + python3 -m pip index versions "$LIST_PACKAGE" \ + -i "$METAX_PYPI_INDEX" \ + --trusted-host "$METAX_PYPI_HOST" + fi + exit 0 +fi + +uv venv "$UV_PROJECT_ENVIRONMENT" +uv sync --project "$ROOT_DIR/environments/metax" --no-dev --no-install-project "$@" + +gpu_install_args=(pip install --reinstall) +if [[ "$METAX_INSTALL_GPU_DEPS" != "true" ]]; then + gpu_install_args+=(--no-deps) +fi +gpu_install_args+=( + --index-url "$METAX_PYPI_INDEX" \ + --trusted-host "$METAX_PYPI_HOST" \ +) +for package_spec in "$METAX_TORCH_SPEC" "$METAX_TORCHAUDIO_SPEC" "$METAX_TORCHVISION_SPEC" "$METAX_VLLM_SPEC"; do + [[ -n "$package_spec" ]] && gpu_install_args+=("$package_spec") +done + +uv "${gpu_install_args[@]}" + +if [[ "$METAX_INSTALL_GPU_DEPS" != "true" ]]; then + echo "[sync_metax_env] Installed MetaX GPU stack with --no-deps to avoid PyPI/NVIDIA fallback." + echo "[sync_metax_env] Set METAX_INSTALL_GPU_DEPS=true only if the MetaX index hosts all required GPU-stack dependencies." +fi + +"$UV_PROJECT_ENVIRONMENT/bin/python" - <<'PY' +import importlib +import torch + +print("torch", torch.__version__) +print("torch.cuda.is_available", torch.cuda.is_available()) +for name in ("vllm",): + module = importlib.import_module(name) + print(name, getattr(module, "__version__", "unknown")) +PY diff --git a/scripts/sync_mthreads_env.sh b/scripts/sync_mthreads_env.sh new file mode 100644 index 0000000..b620237 --- /dev/null +++ b/scripts/sync_mthreads_env.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +usage() { + cat <<'EOF' +Usage: + ./scripts/sync_mthreads_env.sh [uv sync args...] + +This syncs only the generic Qwen3-ASR dependencies for a Moore Threads host. +The MUSA GPU stack (runtime, PyTorch, vLLM, kernels) should come from +the official Moore Threads vLLM image or the vendor's matching installation package. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +cd "$ROOT_DIR" + +uv sync --project "$ROOT_DIR/environments/mthreads" --no-dev --no-install-project "$@" + +echo "[sync_mthreads_env] Synced common dependencies only." +echo "[sync_mthreads_env] Use the official Moore Threads vLLM Docker image for the GPU runtime stack." diff --git a/start.py b/start.py new file mode 100644 index 0000000..c24cbff --- /dev/null +++ b/start.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Qwen3-ASR server CLI entrypoint.""" + +import os +import sys +import multiprocessing as mp + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from dotenv import load_dotenv + +load_dotenv() + +os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") +os.environ.setdefault("TQDM_DISABLE", "1") +os.environ.setdefault("DISABLE_TQDM", "1") +os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn") + + +def _disable_third_party_progress_bars() -> None: + try: + from transformers.utils import logging as transformers_logging + + transformers_logging.disable_progress_bar() + transformers_logging.set_verbosity_error() + except Exception: + pass + + +def _configure_multiprocessing() -> None: + # vLLM launches EngineCore in a child process. CUDA cannot be re-initialized + # in a forked subprocess, so we must force spawn before importing the app. + current = mp.get_start_method(allow_none=True) + if current != "spawn": + mp.set_start_method("spawn", force=True) + + +def _run_server(workers: int) -> None: + from app.core.config import settings + import uvicorn + + print(f"Qwen3-ASR | http://{settings.HOST}:{settings.PORT} | {settings.DEVICE}") + + if workers == 1: + from app.bootstrap import run_cli_preflight + + if not run_cli_preflight(): + sys.exit(1) + elif workers > 1: + print(f"多Worker模式({workers}),启动前仅进行最小 preflight") + + uvicorn.run( + "app.main:app", + host=settings.HOST, + port=settings.PORT, + workers=workers, + reload=settings.DEBUG if workers == 1 else False, + log_level="debug" if settings.DEBUG else settings.LOG_LEVEL.lower(), + access_log=True, + ) + + +def main() -> None: + """主入口""" + workers = int(os.getenv("WORKERS", "1")) + _disable_third_party_progress_bars() + _configure_multiprocessing() + + try: + _run_server(workers) + except KeyboardInterrupt: + print("\n已停止") + sys.exit(0) + except Exception as e: + print(f"启动失败: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/test_diarization.py b/test_diarization.py new file mode 100644 index 0000000..cc79b81 --- /dev/null +++ b/test_diarization.py @@ -0,0 +1,43 @@ +import time +import requests +import json + +url = "http://localhost:8000/v1/audio/transcriptions" +file_path = "/dep/qwen3-asr/TicNote客户服务_20260518_100301-录音.m4a" + +print(f"Starting transcription WITH Speaker Diarization (声纹分离) for {file_path}...") +start_time = time.time() + +try: + with open(file_path, "rb") as f: + files = {"file": f} + data = { + "response_format": "verbose_json", + "enable_speaker_diarization": "true", # 明确开启声纹分离 + } + + response = requests.post(url, files=files, data=data) + + cost_time = time.time() - start_time + + if response.status_code == 200: + res_json = response.json() + segments = res_json.get("segments", []) + + print(f"\n✅ Success! Total time elapsed: {cost_time:.2f} seconds") + print(f"Total segments identified: {len(segments)}") + print("\n--- 识别结果预览 (前 10 个对话片段) ---") + + for seg in segments[:10]: + # 兼容 OpenAI 格式和自定义格式的时间戳字段 + start = seg.get("start", seg.get("start_time", 0.0)) + end = seg.get("end", seg.get("end_time", 0.0)) + speaker = seg.get("speaker_id", seg.get("speaker", "Unknown")) + text = seg.get("text", "").strip() + print(f"[{speaker}] {start:.2f}s - {end:.2f}s : {text}") + + else: + print(f"❌ Failed! Status code: {response.status_code}") + print(response.text) +except Exception as e: + print(f"Error occurred: {e}") diff --git a/test_speed.py b/test_speed.py new file mode 100644 index 0000000..cee7136 --- /dev/null +++ b/test_speed.py @@ -0,0 +1,34 @@ +import time +import requests +import sys + +url = "http://localhost:8000/v1/audio/transcriptions" +file_path = "/dep/qwen3-asr/TicNote客户服务_20260518_100301-录音.m4a" + +print(f"Starting transcription test for {file_path} using qwen3-asr-0.6b...") +start_time = time.time() + +try: + with open(file_path, "rb") as f: + files = {"file": f} + data = { + "response_format": "verbose_json", + "enable_speaker_diarization": "false", # Disable to speed up and focus on ASR + } + + response = requests.post(url, files=files, data=data) + + end_time = time.time() + cost_time = end_time - start_time + + if response.status_code == 200: + res_json = response.json() + text_len = len(res_json.get("text", "")) + print(f"Success! Transcription length: {text_len} chars.") + print(f"Total time elapsed: {cost_time:.2f} seconds") + print(f"Audio processed: {file_path}") + else: + print(f"Failed! Status code: {response.status_code}") + print(response.text) +except Exception as e: + print(f"Error occurred: {e}") diff --git a/test_web/index.html b/test_web/index.html new file mode 100644 index 0000000..c0278cc --- /dev/null +++ b/test_web/index.html @@ -0,0 +1,4936 @@ + + + + + + Qwen3-ASR 会议识别测试 + + + + +
+
+
+
Qwen3-ASR Meeting Console
+

会议语音识别测试

+

左边配置,右边直接看会议气泡流和原始日志。

+
+
+ 服务未检查 + +
+
+ +
+
+
+ + + +
+ +
+
+
+
+

识别配置

+

保持像 demo 一样的单配置面板,常用参数直接可改。

+
+ 实时模式 +
+ + +
+
+ +
+
+
+
+

开始识别

+

普通测试只需要完成下面三步。

+
+ 等待音频 +
+ +
+
+
1
+
+

选择音频来源

+

上传测试文件,或填写可访问的音频/视频 URL、服务端本地路径。

+
+ + +
+ +
+
+ + +
+
+ + 也可以直接点“开始识别”,系统会自动上传。 +
+
+ + + 可用麦克风录一段会议测试音频。 +
+ + +
+ +
+
+ + +
+
+
+
+ +
+
2
+
+

确认识别选项

+
+ + + + +
+
+ 高级参数 +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+ +
+
3
+
+

启动任务

+

创建任务后会自动轮询进度,完成后展示分段结果。

+
+ + + +
+
+
+
+
+ +
+
+
+

声纹管理

+

声纹样本和会议录音分开录,避免误用。

+
+ +
+ +
+
+ + +
+
+ + +
+
+
+
声
+
+

录制 12 秒声纹样本

+

请让要注册或识别的人单独说话,倒计时结束后会自动停止并上传。

+
+ + + 12 秒 +
+
还没有声纹样本。
+ + + +
+
+
+ + +
+
+
+ +
+ 连接与原始响应 +
+
+
+ + +
+
+ + +
+
+
+

原始 JSON

+ +
+ +
+
+
+ +
+
+
+
+

会议总结测试

+

用于在线会议和离线会议结束后,对 ASR 内容做三级总结验证。

+
+ 真实模型 +
+ + +
+
+
+ + +
+
+ + + + diff --git a/tests/test_hotword_prompt.py b/tests/test_hotword_prompt.py new file mode 100644 index 0000000..99b7489 --- /dev/null +++ b/tests/test_hotword_prompt.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +import unittest + +from app.core.hotword_resolver import format_hotword_prompt_context +from app.core.hotword_resolver import strip_hotword_prompt_leakage + + +class HotwordPromptTest(unittest.TestCase): + def test_format_prompt_context_removes_weights(self) -> None: + hotwords = "KPI考核考核 0.2 华智 0.2 合川 0.2 商客市场拓展 0.2" + + self.assertEqual( + format_hotword_prompt_context(hotwords), + "热词列表:[KPI考核考核, 华智, 合川, 商客市场拓展]", + ) + + def test_strip_leaked_hotword_prompt(self) -> None: + text = "热词列表:[KPI考核考核, 华智, 合川, 商客市场拓展] 今天我们看一下执行力。" + + self.assertEqual( + strip_hotword_prompt_leakage(text), + "今天我们看一下执行力。", + ) + + def test_strip_legacy_context_leak_prefix(self) -> None: + text = ( + "Use this context when resolving named entities: " + "热词列表:[KPI考核考核, 华智] 今天继续看KPI考核。" + ) + + self.assertEqual( + strip_hotword_prompt_leakage(text), + "今天继续看KPI考核。", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_model_integrity.py b/tests/test_model_integrity.py new file mode 100644 index 0000000..290a094 --- /dev/null +++ b/tests/test_model_integrity.py @@ -0,0 +1,63 @@ +import tempfile +import unittest +from pathlib import Path + +from app.utils.model_loader import ModelIntegritySpec, _check_model_integrity_spec + + +class ModelIntegritySpecTest(unittest.TestCase): + def test_accepts_single_safetensors_weight(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + snapshot = root / "snapshots" / "main" + snapshot.mkdir(parents=True) + (snapshot / "config.json").write_text("{}", encoding="utf-8") + (snapshot / "model.safetensors").write_bytes(b"weights") + + result = _check_model_integrity_spec(_hf_qwen_spec(root)) + + self.assertTrue(result["ok"]) + + def test_accepts_sharded_safetensors_weight(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + snapshot = root / "snapshots" / "main" + snapshot.mkdir(parents=True) + (snapshot / "config.json").write_text("{}", encoding="utf-8") + (snapshot / "model.safetensors.index.json").write_text("{}", encoding="utf-8") + (snapshot / "model-00001-of-00002.safetensors").write_bytes(b"weights") + + result = _check_model_integrity_spec(_hf_qwen_spec(root)) + + self.assertTrue(result["ok"]) + + def test_rejects_missing_safetensors_weight(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + snapshot = root / "snapshots" / "main" + snapshot.mkdir(parents=True) + (snapshot / "config.json").write_text("{}", encoding="utf-8") + + result = _check_model_integrity_spec(_hf_qwen_spec(root)) + + self.assertFalse(result["ok"]) + self.assertEqual(result["reason"], "required_files_missing") + + +def _hf_qwen_spec(root: Path) -> ModelIntegritySpec: + return ModelIntegritySpec( + description="Qwen test", + path=root, + required_patterns=("snapshots/*/config.json",), + alternative_required_patterns=( + ("snapshots/*/model.safetensors",), + ( + "snapshots/*/model.safetensors.index.json", + "snapshots/*/model-*.safetensors", + ), + ), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_offline_model_selection.py b/tests/test_offline_model_selection.py new file mode 100644 index 0000000..7e79658 --- /dev/null +++ b/tests/test_offline_model_selection.py @@ -0,0 +1,55 @@ +import unittest +import sys +import types +from unittest.mock import patch + +fastapi_module = types.ModuleType("fastapi") +fastapi_module.Request = object + +fastapi_responses_module = types.ModuleType("fastapi.responses") +fastapi_responses_module.JSONResponse = object + +sys.modules.setdefault("fastapi", fastapi_module) +sys.modules.setdefault("fastapi.responses", fastapi_responses_module) + +manager_module = types.ModuleType("app.services.asr.manager") +manager_module.get_model_manager = lambda: None + +model_plan_module = types.ModuleType("app.services.asr.model_plan") +model_plan_module.get_active_qwen_model = lambda: "qwen3-asr-0.6b" +model_plan_module.get_default_model_id = lambda: "qwen3-asr-0.6b" +model_plan_module.get_runtime_model_ids = lambda: ["qwen3-asr-0.6b"] + +sys.modules.setdefault("app.services.asr.manager", manager_module) +sys.modules.setdefault("app.services.asr.model_plan", model_plan_module) + +from app.core.exceptions import InvalidParameterException +from app.services.asr.model_selection import validate_offline_model_id + + +class OfflineModelSelectionTest(unittest.TestCase): + def test_empty_model_uses_default_offline_model(self) -> None: + with ( + patch( + "app.services.asr.model_selection.get_offline_model_ids", + return_value=["qwen3-asr-0.6b"], + ), + patch( + "app.services.asr.model_selection.get_default_offline_model_id", + return_value="qwen3-asr-0.6b", + ), + ): + self.assertEqual(validate_offline_model_id(None), "qwen3-asr-0.6b") + self.assertEqual(validate_offline_model_id(""), "qwen3-asr-0.6b") + + def test_rejects_unavailable_offline_model(self) -> None: + with patch( + "app.services.asr.model_selection.get_offline_model_ids", + return_value=["qwen3-asr-0.6b"], + ): + with self.assertRaises(InvalidParameterException): + validate_offline_model_id("qwen3-asr-1.7b") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_qwenasr_cpu_features.py b/tests/test_qwenasr_cpu_features.py new file mode 100644 index 0000000..9a6eec4 --- /dev/null +++ b/tests/test_qwenasr_cpu_features.py @@ -0,0 +1,41 @@ +import unittest +from unittest.mock import patch + +from app.services.asr.qwenasr_rust import validate_qwenasr_cpu_features + + +class QwenASRCpuFeatureTest(unittest.TestCase): + def test_accepts_x86_avx2_fma_cpu(self) -> None: + with ( + patch("app.services.asr.qwenasr_rust.platform.machine", return_value="x86_64"), + patch( + "app.services.asr.qwenasr_rust._read_linux_cpu_flags", + return_value={"sse4_2", "avx2", "fma"}, + ), + ): + validate_qwenasr_cpu_features() + + def test_rejects_x86_cpu_without_required_features(self) -> None: + with ( + patch("app.services.asr.qwenasr_rust.platform.machine", return_value="x86_64"), + patch( + "app.services.asr.qwenasr_rust._read_linux_cpu_flags", + return_value={"sse4_2"}, + ), + ): + with self.assertRaisesRegex(RuntimeError, "Missing: avx2, fma"): + validate_qwenasr_cpu_features() + + def test_skips_non_x86_cpu(self) -> None: + with ( + patch("app.services.asr.qwenasr_rust.platform.machine", return_value="aarch64"), + patch( + "app.services.asr.qwenasr_rust._read_linux_cpu_flags", + return_value=set(), + ), + ): + validate_qwenasr_cpu_features() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_text_cleanup.py b/tests/test_text_cleanup.py new file mode 100644 index 0000000..b78b9f9 --- /dev/null +++ b/tests/test_text_cleanup.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +import unittest + +from app.core.text_cleanup import deduplicate_asr_text + + +class TextCleanupTest(unittest.TestCase): + def test_cleanup_repeated_meeting_words(self) -> None: + self.assertEqual( + deduplicate_asr_text("在在整个B C融合在在政街包邮上面"), + "在整个B C融合在政街包邮上面", + ) + self.assertEqual(deduplicate_asr_text("需要需要去做的"), "需要去做的") + self.assertEqual( + deduplicate_asr_text("这个这个审核的一个流程意识"), + "这个审核的一个流程意识", + ) + self.assertEqual( + deduplicate_asr_text("都是都是一个货开卷在"), + "都是一个货开卷在", + ) + + def test_cleanup_currency_artifacts_from_itn(self) -> None: + text = "都可以采取¥9000的,看采取¥9¥9000的手机档次还是¥5000的手机档次" + + self.assertEqual( + deduplicate_asr_text(text), + "都可以采取9000元的,看采取9000元的手机档次还是5000元的手机档次", + ) + + def test_cleanup_household_count_glued_with_noise(self) -> None: + text = "已经收集到240247户的一个清单。我们测算的话大概有114户,要占他的大概百分之接近50。" + + self.assertEqual( + deduplicate_asr_text(text), + "已经收集到247户的一个清单。我们测算的话大概有114户,要占他的大概百分之接近50。", + ) + + def test_cleanup_preserves_normal_numeric_contexts(self) -> None: + self.assertEqual(deduplicate_asr_text("目前已经完成了1115户"), "目前已经完成了1115户") + self.assertEqual(deduplicate_asr_text("合同金额有120000"), "合同金额有120000") + + def test_cleanup_collapses_degenerate_numeric_clauses(self) -> None: + self.assertEqual(deduplicate_asr_text("192。192。192。192。192。"), "192。") + self.assertEqual( + deduplicate_asr_text("现在还行,有点儿。对。你还都能弄来。172021。你们都会用。192。192。192。192。幺九"), + "现在还行,有点儿。对。你还都能弄来。172021。你们都会用。192。幺九", + ) + + def test_cleanup_collapses_degenerate_short_sentence_clauses(self) -> None: + self.assertEqual(deduplicate_asr_text("对。对。"), "对。对。") + self.assertEqual(deduplicate_asr_text("对。对。对。对。"), "对。") + self.assertEqual( + deduplicate_asr_text("把这个ifi的驱动文件上传到服务器。对。对。对。对"), + "把这个ifi的驱动文件上传到服务器。对。", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..8274db4 --- /dev/null +++ b/uv.lock @@ -0,0 +1,4744 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <3.13" +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] + +[[package]] +name = "addict" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/ef/fd7649da8af11d93979831e8f1f8097e85e82d5bfeabc8c68b39175d8e75/addict-2.4.0.tar.gz", hash = "sha256:b3b2210e0e067a281f5646c8c5db92e99b7231ea8b0eb5f74dbdf9e259d4e494", size = 9186, upload-time = "2020-11-21T16:21:31.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/00/b08f23b7d7e1e14ce01419a467b583edbb93c6cdb8654e54a9cc579cd61f/addict-2.4.0-py3-none-any.whl", hash = "sha256:249bb56bbfd3cdc2a004ea0ff4c2b6ddc84d53bc2194761636eb314d5cfa5dfc", size = 3832, upload-time = "2020-11-21T16:21:29.588Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +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" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/85/cebc47ee74d8b408749073a1a46c6fcba13d170dc8af7e61996c6c9394ac/aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b", size = 750547, upload-time = "2026-03-31T21:56:30.024Z" }, + { url = "https://files.pythonhosted.org/packages/05/98/afd308e35b9d3d8c9ec54c0918f1d722c86dc17ddfec272fcdbcce5a3124/aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5", size = 503535, upload-time = "2026-03-31T21:56:31.935Z" }, + { url = "https://files.pythonhosted.org/packages/6f/4d/926c183e06b09d5270a309eb50fbde7b09782bfd305dec1e800f329834fb/aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670", size = 497830, upload-time = "2026-03-31T21:56:33.654Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d6/f47d1c690f115a5c2a5e8938cce4a232a5be9aac5c5fb2647efcbbbda333/aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274", size = 1682474, upload-time = "2026-03-31T21:56:35.513Z" }, + { url = "https://files.pythonhosted.org/packages/01/44/056fd37b1bb52eac760303e5196acc74d9d546631b035704ae5927f7b4ac/aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a", size = 1655259, upload-time = "2026-03-31T21:56:37.843Z" }, + { url = "https://files.pythonhosted.org/packages/91/9f/78eb1a20c1c28ae02f6a3c0f4d7b0dcc66abce5290cadd53d78ce3084175/aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d", size = 1736204, upload-time = "2026-03-31T21:56:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/de/6c/d20d7de23f0b52b8c1d9e2033b2db1ac4dacbb470bb74c56de0f5f86bb4f/aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796", size = 1826198, upload-time = "2026-03-31T21:56:41.378Z" }, + { url = "https://files.pythonhosted.org/packages/2f/86/a6f3ff1fd795f49545a7c74b2c92f62729135d73e7e4055bf74da5a26c82/aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95", size = 1681329, upload-time = "2026-03-31T21:56:43.374Z" }, + { url = "https://files.pythonhosted.org/packages/fb/68/84cd3dab6b7b4f3e6fe9459a961acb142aaab846417f6e8905110d7027e5/aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5", size = 1560023, upload-time = "2026-03-31T21:56:45.031Z" }, + { url = "https://files.pythonhosted.org/packages/41/2c/db61b64b0249e30f954a65ab4cb4970ced57544b1de2e3c98ee5dc24165f/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a", size = 1652372, upload-time = "2026-03-31T21:56:47.075Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/e96988a6c982d047810c772e28c43c64c300c943b0ed5c1c0c4ce1e1027c/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73", size = 1662031, upload-time = "2026-03-31T21:56:48.835Z" }, + { url = "https://files.pythonhosted.org/packages/b7/26/a56feace81f3d347b4052403a9d03754a0ab23f7940780dada0849a38c92/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297", size = 1708118, upload-time = "2026-03-31T21:56:50.833Z" }, + { url = "https://files.pythonhosted.org/packages/78/6e/b6173a8ff03d01d5e1a694bc06764b5dad1df2d4ed8f0ceec12bb3277936/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074", size = 1548667, upload-time = "2026-03-31T21:56:52.81Z" }, + { url = "https://files.pythonhosted.org/packages/16/13/13296ffe2c132d888b3fe2c195c8b9c0c24c89c3fa5cc2c44464dc23b22e/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e", size = 1724490, upload-time = "2026-03-31T21:56:54.541Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1f1c287f4a79782ef36e5a6e62954c85343bc30470d862d30bd5f26c9fa2/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7", size = 1667109, upload-time = "2026-03-31T21:56:56.21Z" }, + { url = "https://files.pythonhosted.org/packages/ef/42/8461a2aaf60a8f4ea4549a4056be36b904b0eb03d97ca9a8a2604681a500/aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9", size = 439478, upload-time = "2026-03-31T21:56:58.292Z" }, + { url = "https://files.pythonhosted.org/packages/e5/71/06956304cb5ee439dfe8d86e1b2e70088bd88ed1ced1f42fb29e5d855f0e/aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76", size = 462047, upload-time = "2026-03-31T21:57:00.257Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, + { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" }, + { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, + { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, + { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, + { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, + { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" }, + { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "aliyun-python-sdk-core" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jmespath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/09/da9f58eb38b4fdb97ba6523274fbf445ef6a06be64b433693da8307b4bec/aliyun-python-sdk-core-2.16.0.tar.gz", hash = "sha256:651caad597eb39d4fad6cf85133dffe92837d53bdf62db9d8f37dab6508bb8f9", size = 449555, upload-time = "2024-10-09T06:01:01.762Z" } + +[[package]] +name = "aliyun-python-sdk-kms" +version = "2.16.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aliyun-python-sdk-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/2c/9877d0e6b18ecf246df671ac65a5d1d9fecbf85bdcb5d43efbde0d4662eb/aliyun-python-sdk-kms-2.16.5.tar.gz", hash = "sha256:f328a8a19d83ecbb965ffce0ec1e9930755216d104638cd95ecd362753b813b3", size = 12018, upload-time = "2024-08-30T09:01:20.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/5c/0132193d7da2c735669a1ed103b142fd63c9455984d48c5a88a1a516efaa/aliyun_python_sdk_kms-2.16.5-py2.py3-none-any.whl", hash = "sha256:24b6cdc4fd161d2942619479c8d050c63ea9cd22b044fe33b60bbb60153786f0", size = 99495, upload-time = "2024-08-30T09:01:18.462Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.96.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "distro", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "docstring-parser", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "httpx", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "jiter", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pydantic", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "sniffio", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/7e/672f533dee813028d2c699bfd2a7f52c9118d7353680d9aa44b9e23f717f/anthropic-0.96.0.tar.gz", hash = "sha256:9de947b737f39452f68aa520f1c2239d44119c9b73b0fb6d4e6ca80f00279ee6", size = 658210, upload-time = "2026-04-16T14:28:02.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/5a/72f33204064b6e87601a71a6baf8d855769f8a0c1eaae8d06a1094872371/anthropic-0.96.0-py3-none-any.whl", hash = "sha256:9a6e335a354602a521cd9e777e92bfd46ba6e115bf9bbfe6135311e8fb2015b2", size = 635930, upload-time = "2026-04-16T14:28:01.436Z" }, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "apache-tvm-ffi" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/60/1e787a0b5ebf318483235be2a689ee367173983067e441b8379564f667c0/apache_tvm_ffi-0.1.9.tar.gz", hash = "sha256:d2d402587e8906de0a07f4746aa78f3d452c7efe3625d4bb39ac2ad693bce530", size = 2513731, upload-time = "2026-02-27T19:28:06.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0a/827e4f9ae85e1be3037818abd59566d906ba1fe27295c6938b12cc482151/apache_tvm_ffi-0.1.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1c8dd4018420c0d14bace688594710909ce198056ff8ac2ad1cd462b30fe1bdd", size = 2231204, upload-time = "2026-02-27T19:27:04.734Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b6/f1ec5c528918c4dae03885ec472663072a984431d7d7fb04ca0798a2e13c/apache_tvm_ffi-0.1.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f6bc8846d570b8ce38692fc91b530b44cd6ae092c805a844da23970e81b12c0", size = 2323684, upload-time = "2026-02-27T19:27:06.284Z" }, + { url = "https://files.pythonhosted.org/packages/28/08/818836fbc0f198da1597896f82d7e6556bf5678cd5150d633214bf14b718/apache_tvm_ffi-0.1.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3ec9149f207a7af3ea3531cad7a0b0d04ded06df4f51a547479d5eb489428dd", size = 2160066, upload-time = "2026-02-27T19:27:07.897Z" }, + { url = "https://files.pythonhosted.org/packages/c8/6b/2e7d73d055523c2fb31394cd3d55593969a0680619e1c939c2128c2fdd36/apache_tvm_ffi-0.1.9-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eefcd17f61bf503ff0f4ad429e03ef6c241c7d13682f58281d883218b854c9bd", size = 2307014, upload-time = "2026-02-27T19:27:10.287Z" }, + { url = "https://files.pythonhosted.org/packages/42/b1/9f2cfd6d49b03c5d4ec5c12548d911e2e01265be783f343103b4df716765/apache_tvm_ffi-0.1.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c0449fc3802987c3652bea266ffda2934a6f69c80bba791a3f55b91040656a18", size = 2231154, upload-time = "2026-02-27T19:27:15.691Z" }, + { url = "https://files.pythonhosted.org/packages/55/43/63faedea83494e99122466a993bcdccd31cf93c7e8a0d56731120e82e2b9/apache_tvm_ffi-0.1.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6f16d73a82a9e68a439b7d233d48b1b929be17fe92df4bbf1ee2274e573144a3", size = 2323130, upload-time = "2026-02-27T19:27:17.259Z" }, + { url = "https://files.pythonhosted.org/packages/27/96/d735bc4c528efaf0a8a954076963c727aad2dde8577641aa9025ec4f2d52/apache_tvm_ffi-0.1.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01ebb1308b2666c206aa9a4015eb48f03a5d98ea2e9cfb002bd5e2ca0b9c7ef3", size = 2159854, upload-time = "2026-02-27T19:27:18.789Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3b/6cfc82a3ab5d9e501bbcee5df36eebe09da1c384461d7a55e2a17776d117/apache_tvm_ffi-0.1.9-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21365abd2a2a1a6d3b4e6e4f048309651125becfa795440c3607f3cc27d30ac7", size = 2307140, upload-time = "2026-02-27T19:27:20.222Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c0/6d3d54f50012255b41bc3e24944c086f63c4707c8686c7c6780e9283eb96/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d503029e66c43b1a1cb1a42a1e9bb428c8a28dcbdec31c28e705472ca648a3a", size = 2203712, upload-time = "2026-02-27T19:27:25.867Z" }, + { url = "https://files.pythonhosted.org/packages/c6/dd/2bab4c6cd86257dbf99e93452a1af833113f8dc3e25a25579f6e4e4c8a94/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28241371934ea8af10d5067087ba1229ebddded7b2c02d33a258ec2a96df8c46", size = 2299704, upload-time = "2026-02-27T19:27:27.477Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4a/b469bcb2e1014cb84d336d2a59f42958a058251c577a4c2680cacad346e2/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87cacce81df55685fc6a76e1e3c5db1200e85e87bf5974b692c59d131b7bc622", size = 2130865, upload-time = "2026-02-27T19:27:29.092Z" }, + { url = "https://files.pythonhosted.org/packages/70/ef/5402da5d37f5270fd88ea0348acca78dba9be8bdbf6c2bcae0935eb03ef1/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f45eb43499acac45ff6c93564f0ff2d3ca27b69656d540fd56ce59d51c0b4c65", size = 2278991, upload-time = "2026-02-27T19:27:30.729Z" }, +] + +[[package]] +name = "astor" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/21/75b771132fee241dfe601d39ade629548a9626d1d39f333fde31bc46febe/astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e", size = 35090, upload-time = "2019-12-10T01:50:35.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/88/97eef84f48fa04fbd6750e62dcceafba6c63c81b7ac1420856c8dcc0a3f9/astor-0.8.1-py2.py3-none-any.whl", hash = "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5", size = 27488, upload-time = "2019-12-10T01:50:33.628Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "asyncpg" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/d9/507c80bdac2e95e5a525644af94b03fa7f9a44596a84bd48a6e80f854f92/asyncpg-0.31.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:831712dd3cf117eec68575a9b50da711893fd63ebe277fc155ecae1c6c9f0f61", size = 644865, upload-time = "2025-11-24T23:25:23.527Z" }, + { url = "https://files.pythonhosted.org/packages/ea/03/f93b5e543f65c5f504e91405e8d21bb9e600548be95032951a754781a41d/asyncpg-0.31.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b17c89312c2f4ccea222a3a6571f7df65d4ba2c0e803339bfc7bed46a96d3be", size = 639297, upload-time = "2025-11-24T23:25:25.192Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/de2177e57e03a06e697f6c1ddf2a9a7fcfdc236ce69966f54ffc830fd481/asyncpg-0.31.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3faa62f997db0c9add34504a68ac2c342cfee4d57a0c3062fcf0d86c7f9cb1e8", size = 2816679, upload-time = "2025-11-24T23:25:26.718Z" }, + { url = "https://files.pythonhosted.org/packages/d0/98/1a853f6870ac7ad48383a948c8ff3c85dc278066a4d69fc9af7d3d4b1106/asyncpg-0.31.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ea599d45c361dfbf398cb67da7fd052affa556a401482d3ff1ee99bd68808a1", size = 2867087, upload-time = "2025-11-24T23:25:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/11/29/7e76f2a51f2360a7c90d2cf6d0d9b210c8bb0ae342edebd16173611a55c2/asyncpg-0.31.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:795416369c3d284e1837461909f58418ad22b305f955e625a4b3a2521d80a5f3", size = 2747631, upload-time = "2025-11-24T23:25:30.154Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3f/716e10cb57c4f388248db46555e9226901688fbfabd0afb85b5e1d65d5a7/asyncpg-0.31.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a8d758dac9d2e723e173d286ef5e574f0b350ec00e9186fce84d0fc5f6a8e6b8", size = 2855107, upload-time = "2025-11-24T23:25:31.888Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ec/3ebae9dfb23a1bd3f68acfd4f795983b65b413291c0e2b0d982d6ae6c920/asyncpg-0.31.0-cp310-cp310-win32.whl", hash = "sha256:2d076d42eb583601179efa246c5d7ae44614b4144bc1c7a683ad1222814ed095", size = 521990, upload-time = "2025-11-24T23:25:33.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/b4/9fbb4b0af4e36d96a61d026dd37acab3cf521a70290a09640b215da5ab7c/asyncpg-0.31.0-cp310-cp310-win_amd64.whl", hash = "sha256:9ea33213ac044171f4cac23740bed9a3805abae10e7025314cfbd725ec670540", size = 581629, upload-time = "2025-11-24T23:25:34.846Z" }, + { url = "https://files.pythonhosted.org/packages/08/17/cc02bc49bc350623d050fa139e34ea512cd6e020562f2a7312a7bcae4bc9/asyncpg-0.31.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eee690960e8ab85063ba93af2ce128c0f52fd655fdff9fdb1a28df01329f031d", size = 643159, upload-time = "2025-11-24T23:25:36.443Z" }, + { url = "https://files.pythonhosted.org/packages/a4/62/4ded7d400a7b651adf06f49ea8f73100cca07c6df012119594d1e3447aa6/asyncpg-0.31.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2657204552b75f8288de08ca60faf4a99a65deef3a71d1467454123205a88fab", size = 638157, upload-time = "2025-11-24T23:25:37.89Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/4179538a9a72166a0bf60ad783b1ef16efb7960e4d7b9afe9f77a5551680/asyncpg-0.31.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a429e842a3a4b4ea240ea52d7fe3f82d5149853249306f7ff166cb9948faa46c", size = 2918051, upload-time = "2025-11-24T23:25:39.461Z" }, + { url = "https://files.pythonhosted.org/packages/e6/35/c27719ae0536c5b6e61e4701391ffe435ef59539e9360959240d6e47c8c8/asyncpg-0.31.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0807be46c32c963ae40d329b3a686356e417f674c976c07fa49f1b30303f109", size = 2972640, upload-time = "2025-11-24T23:25:41.512Z" }, + { url = "https://files.pythonhosted.org/packages/43/f4/01ebb9207f29e645a64699b9ce0eefeff8e7a33494e1d29bb53736f7766b/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e5d5098f63beeae93512ee513d4c0c53dc12e9aa2b7a1af5a81cddf93fe4e4da", size = 2851050, upload-time = "2025-11-24T23:25:43.153Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f4/03ff1426acc87be0f4e8d40fa2bff5c3952bef0080062af9efc2212e3be8/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37fc6c00a814e18eef51833545d1891cac9aa69140598bb076b4cd29b3e010b9", size = 2962574, upload-time = "2025-11-24T23:25:44.942Z" }, + { url = "https://files.pythonhosted.org/packages/c7/39/cc788dfca3d4060f9d93e67be396ceec458dfc429e26139059e58c2c244d/asyncpg-0.31.0-cp311-cp311-win32.whl", hash = "sha256:5a4af56edf82a701aece93190cc4e094d2df7d33f6e915c222fb09efbb5afc24", size = 521076, upload-time = "2025-11-24T23:25:46.486Z" }, + { url = "https://files.pythonhosted.org/packages/28/fc/735af5384c029eb7f1ca60ccb8fa95521dbdaeef788edf4cecfc604c3cab/asyncpg-0.31.0-cp311-cp311-win_amd64.whl", hash = "sha256:480c4befbdf079c14c9ca43c8c5e1fe8b6296c96f1f927158d4f1e750aacc047", size = 584980, upload-time = "2025-11-24T23:25:47.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" }, + { url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321, upload-time = "2025-11-24T23:25:54.982Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685, upload-time = "2025-11-24T23:25:57.43Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858, upload-time = "2025-11-24T23:25:59.636Z" }, + { url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852, upload-time = "2025-11-24T23:26:01.084Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175, upload-time = "2025-11-24T23:26:02.564Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "audioread" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/4a/874ecf9b472f998130c2b5e145dcdb9f6131e84786111489103b66772143/audioread-3.1.0.tar.gz", hash = "sha256:1c4ab2f2972764c896a8ac61ac53e261c8d29f0c6ccd652f84e18f08a4cab190", size = 20082, upload-time = "2025-10-26T19:44:13.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/16/fbe8e1e185a45042f7cd3a282def5bb8d95bb69ab9e9ef6a5368aa17e426/audioread-3.1.0-py3-none-any.whl", hash = "sha256:b30d1df6c5d3de5dcef0fb0e256f6ea17bdcf5f979408df0297d8a408e2971b4", size = 23143, upload-time = "2025-10-26T19:44:12.016Z" }, +] + +[[package]] +name = "blake3" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/aa/abcd75e9600987a0bc6cfe9b6b2ff3f0e2cb08c170addc6e76035b5c4cb3/blake3-1.0.8.tar.gz", hash = "sha256:513cc7f0f5a7c035812604c2c852a0c1468311345573de647e310aca4ab165ba", size = 117308, upload-time = "2025-10-14T06:47:48.83Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/e5/4c37ced9358cece71f2f380a57f77a449f6e87cc6d9f450613237b7a3078/blake3-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:859cd57bac097a2cd63cb36d64c2f6f16c9edece5590f929e70157478e46dc9e", size = 371337, upload-time = "2025-10-14T06:45:12.296Z" }, + { url = "https://files.pythonhosted.org/packages/d1/df/0825da1cde7ca63a8bcdc785ca7f8647b025e9497eef18c75bb9754dbd26/blake3-1.0.8-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e1d70bf76c02846d0868a3d413eb6c430b76a315e12f1b2e59b5cf56c1f62a3", size = 374945, upload-time = "2025-10-14T06:45:13.99Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a3/43f10c623179dce789ca9e3b8f4064fb6312e99f05c1aae360d07ad95bb0/blake3-1.0.8-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3fe26f145fcb82931d1820b55c0279f72f8f8e49450dd9d74efbfd409b28423", size = 448766, upload-time = "2025-10-14T06:45:15.471Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/9431bf5fe0eedeb2aadb4fe81fb18945cf8d49adad98e7988fb3cdac76c2/blake3-1.0.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97c076d58ee37eb5b2d8d91bb9db59c5a008fd59c71845dc57fe438aeeabaf10", size = 507107, upload-time = "2025-10-14T06:45:17.055Z" }, + { url = "https://files.pythonhosted.org/packages/ac/55/3712cdaebaefa8d5acec46f8df7861ba1832e1e188bc1333dd5acd31f760/blake3-1.0.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78731ce7fca46f776ae45fb5271a2a76c4a92c9687dd4337e84b2ae9a174b28f", size = 393955, upload-time = "2025-10-14T06:45:18.718Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d0/add0441e7aaa6b358cac0ddc9246f0799b60d25f06bd542b554afe19fd85/blake3-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65e373c8b47174b969ee61a89ee56922f722972eb650192845c8546df8d9db9", size = 387577, upload-time = "2025-10-14T06:45:20.332Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/e4a61f5c0cad4d51a886e8f4367e590caaead8a4809892292bf724c4421d/blake3-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db54946792d2b8c6fa4be73e6e334519f13c1b52e7ff346b3e2ec8ad3eb59401", size = 550515, upload-time = "2025-10-14T06:45:21.867Z" }, + { url = "https://files.pythonhosted.org/packages/28/c7/90c01091465628acff96534e82d4b3bc16ca22c515f69916d2715273c0e3/blake3-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:67d9c42c42eb1c7aedcf901591c743266009fcf48babf6d6f8450f567cb94a84", size = 554650, upload-time = "2025-10-14T06:45:23.047Z" }, + { url = "https://files.pythonhosted.org/packages/f4/0a/515209b0c282c360e249b89cd85350d97cfd55fadbb4df736c67b77b27a1/blake3-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fcfe81b3ae3fb5d2e88be0d3259603ff95f0d5ed69f655c28fdaef31e49a470", size = 371092, upload-time = "2025-10-14T06:45:34.062Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/9d342a2bf5817f006bbe947335e5d387327541ea47590854947befd01251/blake3-1.0.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58ce8d45a5bb5326482de72ea1969a378634236186a970fef63058a5b7b8b435", size = 374859, upload-time = "2025-10-14T06:45:35.262Z" }, + { url = "https://files.pythonhosted.org/packages/5b/fc/ea4bef850a7ec9fbb383503fd3c56056dd9fa44e10c3bc61050ab7b2bac0/blake3-1.0.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83605dbf43f581d8b7175b7f3bfe5388bad5a7c6ac175c9c11d669da31133f4b", size = 448585, upload-time = "2025-10-14T06:45:36.542Z" }, + { url = "https://files.pythonhosted.org/packages/a5/67/167a65a4c431715407d07b1b8b1367698a3ad88e7260edb85f0c5293f08a/blake3-1.0.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b5573b052777142b2cecc453d022c3f21aa4aba75011258410bb98f41c1a727", size = 507519, upload-time = "2025-10-14T06:45:37.814Z" }, + { url = "https://files.pythonhosted.org/packages/32/e2/0886e192d634b264c613b0fbf380745b39992b424a0effc00ef08783644e/blake3-1.0.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe1b02ab49bfd969ef50b9f17482a2011c77536654af21807ba5c2674e0bb2a0", size = 393645, upload-time = "2025-10-14T06:45:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3b/7fb2fe615448caaa5f6632b2c7551117b38ccac747a3a5769181e9751641/blake3-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7780666dc6be809b49442d6d5ce06fdbe33024a87560b58471103ec17644682", size = 387640, upload-time = "2025-10-14T06:45:40.546Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8c/2bfc942c6c97cb3d20f341859343bb86ee20af723fedfc886373e606079b/blake3-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af394b50c6aa0b1b957a99453d1ee440ef67cd2d1b5669c731647dc723de8a3a", size = 550316, upload-time = "2025-10-14T06:45:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/7e/75/0252be37620699b79dbaa799c9b402d63142a131d16731df4ef09d135dd7/blake3-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c63ece266a43014cf29e772a82857cd8e90315ae3ed53e3c5204851596edd5f2", size = 554463, upload-time = "2025-10-14T06:45:43.22Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/85a4c0782f613de23d114a7a78fcce270f75b193b3ff3493a0de24ba104a/blake3-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:269f255b110840e52b6ce9db02217e39660ebad3e34ddd5bca8b8d378a77e4e1", size = 371296, upload-time = "2025-10-14T06:45:49.674Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/488475254976ed93fab57c67aa80d3b40df77f7d9db6528c9274bff53e08/blake3-1.0.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66ca28a673025c40db3eba21a9cac52f559f83637efa675b3f6bd8683f0415f3", size = 374516, upload-time = "2025-10-14T06:45:51.23Z" }, + { url = "https://files.pythonhosted.org/packages/7b/21/2a1c47fedb77fb396512677ec6d46caf42ac6e9a897db77edd0a2a46f7bb/blake3-1.0.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcb04966537777af56c1f399b35525aa70a1225816e121ff95071c33c0f7abca", size = 447911, upload-time = "2025-10-14T06:45:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7d/db0626df16029713e7e61b67314c4835e85c296d82bd907c21c6ea271da2/blake3-1.0.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5b5da177d62cc4b7edf0cea08fe4dec960c9ac27f916131efa890a01f747b93", size = 505420, upload-time = "2025-10-14T06:45:54.445Z" }, + { url = "https://files.pythonhosted.org/packages/5b/55/6e737850c2d58a6d9de8a76dad2ae0f75b852a23eb4ecb07a0b165e6e436/blake3-1.0.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38209b10482c97e151681ea3e91cc7141f56adbbf4820a7d701a923124b41e6a", size = 394189, upload-time = "2025-10-14T06:45:55.719Z" }, + { url = "https://files.pythonhosted.org/packages/5b/94/eafaa5cdddadc0c9c603a6a6d8339433475e1a9f60c8bb9c2eed2d8736b6/blake3-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504d1399b7fb91dfe5c25722d2807990493185faa1917456455480c36867adb5", size = 388001, upload-time = "2025-10-14T06:45:57.067Z" }, + { url = "https://files.pythonhosted.org/packages/17/81/735fa00d13de7f68b25e1b9cb36ff08c6f165e688d85d8ec2cbfcdedccc5/blake3-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c84af132aa09abeadf9a0118c8fb26f4528f3f42c10ef8be0fcf31c478774ec4", size = 550302, upload-time = "2025-10-14T06:45:58.657Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c6/d1fe8bdea4a6088bd54b5a58bc40aed89a4e784cd796af7722a06f74bae7/blake3-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a25db3d36b55f5ed6a86470155cc749fc9c5b91c949b8d14f48658f9d960d9ec", size = 554211, upload-time = "2025-10-14T06:46:00.269Z" }, +] + +[[package]] +name = "cachetools" +version = "7.0.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/7b/1755ed2c6bfabd1d98b37ae73152f8dcf94aa40fee119d163c19ed484704/cachetools-7.0.6.tar.gz", hash = "sha256:e5d524d36d65703a87243a26ff08ad84f73352adbeafb1cde81e207b456aaf24", size = 37526, upload-time = "2026-04-20T19:02:23.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/c4/cf76242a5da1410917107ff14551764aa405a5fd10cd10cf9a5ca8fa77f4/cachetools-7.0.6-py3-none-any.whl", hash = "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", size = 13976, upload-time = "2026-04-20T19:02:21.187Z" }, +] + +[[package]] +name = "cbor2" +version = "5.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/cb/09939728be094d155b5d4ac262e39877875f5f7e36eea66beb359f647bd0/cbor2-5.9.0.tar.gz", hash = "sha256:85c7a46279ac8f226e1059275221e6b3d0e370d2bb6bd0500f9780781615bcea", size = 111231, upload-time = "2026-03-22T15:56:50.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/7b/74c524ce81c1ddc6c44b4865028ffb7d3a8e7ae653b1061650375a28cbb1/cbor2-5.9.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095dc49e75572841a9534cbfdabc2a17487ea4ee33341436abc4a7ac7245a3a", size = 261074, upload-time = "2026-03-22T15:55:55.654Z" }, + { url = "https://files.pythonhosted.org/packages/4b/97/c496c71422b2ca18ff2acc5f49e8c45cd7294b7df1359eccec78021b428e/cbor2-5.9.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25bec7beb2089465382b1be72e78667fe9090598800826559c3e3008cf0db743", size = 255498, upload-time = "2026-03-22T15:55:57.256Z" }, + { url = "https://files.pythonhosted.org/packages/c2/40/9bd7e66dba7aea674a440e004faea406de42c49aeac23453954b67768532/cbor2-5.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cc5efec69055c3c470997935d95762be7e4bfd1248d88fb1a33bb7e0f45712e9", size = 255683, upload-time = "2026-03-22T15:55:58.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d1/fa3e158dbe4c08091495b720c604624b285bc272afdbcfac2150725d955b/cbor2-5.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:420d2490c7836c81151b4bd591c35cffc55391e33e7e333c50fda391bcea7d31", size = 250798, upload-time = "2026-03-22T15:55:59.953Z" }, + { url = "https://files.pythonhosted.org/packages/31/43/fe29b1f897770011a5e7497f4523c2712282ee4a6cbf775ea6383fb7afb9/cbor2-5.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9d6e4e0f988b0e766509a8071975a8ee99f930e14a524620bf38083106158d2", size = 268738, upload-time = "2026-03-22T15:56:05.222Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/e494568f3d8aafbcdfe361df44c3bcf5cdab5183e25ea08e3d3f9fcf4075/cbor2-5.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5326336f633cc89dfe543c78829c16c3a6449c2c03277d1ddba99086c3323363", size = 262571, upload-time = "2026-03-22T15:56:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/42/2e/92acd6f87382fd44a34d9d7e85cc45372e6ba664040b72d1d9df648b25d0/cbor2-5.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5e702b02d42a5ace45425b595ffe70fe35aebaf9a3cdfdc2c758b6189c744422", size = 262356, upload-time = "2026-03-22T15:56:08.236Z" }, + { url = "https://files.pythonhosted.org/packages/3f/68/52c039a28688baeeb78b0be7483855e6c66ea05884a937444deede0c87b8/cbor2-5.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2372d357d403e7912f104ff085950ffc82a5854d6d717f1ca1ce16a40a0ef5a7", size = 257604, upload-time = "2026-03-22T15:56:09.835Z" }, + { url = "https://files.pythonhosted.org/packages/09/fd/7ddf3d3153b54c69c3be77172b8d9aa3a9d74f62a7fbde614d53eaeed9a4/cbor2-5.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae6c706ac1d85a0b3cb3395308fd0c4d55e3202b4760773675957e93cdff45fc", size = 287865, upload-time = "2026-03-22T15:56:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/db/9d/7ede2cc42f9bb4260492e7d29d2aab781eacbbcfb09d983de1e695077199/cbor2-5.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4cd43d8fc374b31643b2830910f28177a606a7bc84975a62675dd3f2e320fc7b", size = 288246, upload-time = "2026-03-22T15:56:16.113Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9d/588ebc7c5bc5843f609b05fe07be8575c7dec987735b0bbc908ac9c1264a/cbor2-5.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4aa07b392cc3d76fb31c08a46a226b58c320d1c172ff3073e864409ced7bc50f", size = 280214, upload-time = "2026-03-22T15:56:17.519Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a1/6fc8f4b15c6a27e7fbb7966c30c2b4b18c274a3221fa2f5e6235502d34bc/cbor2-5.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:971d425b3a23b75953d8853d5f9911bdeefa09d759ee3b5e6b07b5ff3cbd9073", size = 282162, upload-time = "2026-03-22T15:56:18.975Z" }, + { url = "https://files.pythonhosted.org/packages/42/ff/b83492b096fbef26e9cb62c1a4bf2d3cef579ea7b33138c6c37c4ae66f67/cbor2-5.9.0-py3-none-any.whl", hash = "sha256:27695cbd70c90b8de5c4a284642c2836449b14e2c2e07e3ffe0744cb7669a01b", size = 24627, upload-time = "2026-03-22T15:56:48.847Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "compressed-tensors" +version = "0.15.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "loguru", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pydantic", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "torch", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "transformers", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/1b/c3c4a98ec5f2727656336f07a0c35862195c310d8eb0b2fa5b4be6848680/compressed_tensors-0.15.0.1.tar.gz", hash = "sha256:a8e93054e8a5ec49c980b09ed36c4c1249b4a8ee167920a8e461c4da26e78d99", size = 229412, upload-time = "2026-04-10T14:23:54.708Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/52/93833dc1610e017ac5b7dcd59b8304d8ef67d1114c2d124e728a2cbbea12/compressed_tensors-0.15.0.1-py3-none-any.whl", hash = "sha256:e1b1f322e82e475715e242bad46925a304ea8e5c98b5055a15b8eb22fb6bfea9", size = 194260, upload-time = "2026-04-10T14:23:53.098Z" }, +] + +[[package]] +name = "crcmod" +version = "1.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/b0/e595ce2a2527e169c3bcd6c33d2473c1918e0b7f6826a043ca1245dd4e5b/crcmod-1.7.tar.gz", hash = "sha256:dc7051a0db5f2bd48665a990d3ec1cc305a466a77358ca4492826f41f283601e", size = 89670, upload-time = "2010-06-27T14:35:29.538Z" } + +[[package]] +name = "cryptography" +version = "46.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, + { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/98/0666ee759cd2e5306f911cbc95d2c6c814326906ed6b9c09e817a4b4a7c8/cuda_bindings-13.0.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56e46a9e984bb754e56b9d060cf027fe99f08a97651ce6d8aa1c2032476d01e", size = 11762523, upload-time = "2025-10-21T15:08:45.913Z" }, + { url = "https://files.pythonhosted.org/packages/e1/36/2b2a43c8a6f8d8ff7a5ec7de1357ba3f1438ea69281c9deb90df29d55d56/cuda_bindings-13.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f797ce534a303525259be0ae7ee9cfcf4f7874b22f1f9b8e85555509dccb83", size = 12136098, upload-time = "2025-10-21T15:08:48.233Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/9e171ee6359d4aabf2d8202802c85487cae6c2eb52b9352bb7754583802f/cuda_bindings-13.0.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfd66c25a133365c4f93e3396c38c64b04400ccafd18c3a889ae251a1bfabaa1", size = 11807212, upload-time = "2025-10-21T15:08:52.988Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/d7036d9e402e6b5b57877a7496aba3bf2a0090f4fa3f072743fce3373eba/cuda_bindings-13.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9afede5937474864aa794eb57399dbdf5d2b05427aadcac275209b0857528a61", size = 12198791, upload-time = "2025-10-21T15:08:55.687Z" }, + { url = "https://files.pythonhosted.org/packages/61/3c/c33fd3aa5fcc89aa1c135e477a0561f29142ab5fe028ca425fc87f7f0a74/cuda_bindings-13.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b899e5a513c11eaa18648f9bf5265d8de2a93f76ef66a6bfca0a2887303965cd", size = 11709086, upload-time = "2025-10-21T15:09:00.005Z" }, + { url = "https://files.pythonhosted.org/packages/21/ac/6b34452a3836c9fbabcd360689a353409d15f500dd9d9ced7f837549e383/cuda_bindings-13.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf41d9e69019939aa15296fa66ea7d3fdb8d2c6383f729f4b1156c8b37808a06", size = 12128303, upload-time = "2025-10-21T15:09:02.889Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/d6/ac63065d33dd700fee7ebd7d287332401b54e31b9346e142f871e1f0b116/cuda_pathfinder-1.5.3-py3-none-any.whl", hash = "sha256:dff021123aedbb4117cc7ec81717bbfe198fb4e8b5f1ee57e0e084fec5c8577d", size = 49991, upload-time = "2026-04-14T20:09:27.037Z" }, +] + +[[package]] +name = "cuda-python" +version = "13.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "cuda-pathfinder", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/5f/beaa12a11b051027eec0b041df01c6690db4f02e3b2e8fadd5a0eeb4df52/cuda_python-13.0.3-py3-none-any.whl", hash = "sha256:914cd7e2dd075bd06a2d5121c1d9ccdd3d0c94b03ea5a44dbd98d24d8ed93bab", size = 7605, upload-time = "2025-10-21T15:48:59.222Z" }, +] + +[[package]] +name = "cuda-tile" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/c8/1687a83d0739151ca410a5716b5f1dfb6f8feb6381c7c695d72264f17e1c/cuda_tile-1.3.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:c55616c648f06f84808648a521c67f2d7c790574d6b53ddf8c3bfbc995d36d45", size = 245438, upload-time = "2026-04-20T15:51:18.862Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d0/0e4790cc7a536a685a961d2e04f26be54f86f0974e1eaea71c1b64ec4032/cuda_tile-1.3.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:8c71c2fd9b96c054c126a218f9927c8c8dde72441a532464551b865b416d452a", size = 246915, upload-time = "2026-04-20T15:51:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d6/753aecb3e8fcee80d20f9d32b4504276691c2f77fc10abbbd8e82197e24c/cuda_tile-1.3.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:59d9843fa723ceb4d680ec246e12e3ded857266e4c2bf5c5d21e530d6d765060", size = 245441, upload-time = "2026-04-20T15:51:06.618Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2d/8b416239413bf11d17d42ccee43258f3787da13bcea7b2e42e8bbf04b3da/cuda_tile-1.3.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:2888d6b89fae053a53ca7bb703c508a5cf90671d266934573c5b6c25978022c4", size = 246706, upload-time = "2026-04-20T15:51:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/f3/49/4592bc94ca05a07c7947ea114fd12734c8497f2daffee9faa79a03e39fb5/cuda_tile-1.3.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:375316b64c51ee7cfadb2f170a30c1547bc41eb39f1e233a6556713857d2e81f", size = 245744, upload-time = "2026-04-20T15:52:09.621Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/84cb68be463c827bf79da9fa0aa5140838de6455ef6f438bbe0ffa75d378/cuda_tile-1.3.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:e4865acbff1172aaee304bf9c550586088d8b4545a384423597a590899386709", size = 247301, upload-time = "2026-04-20T15:51:04.042Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, +] +curand = [ + { name = "nvidia-curand", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, +] + +[[package]] +name = "datasets" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, + { name = "filelock" }, + { name = "fsspec", extra = ["http"] }, + { name = "huggingface-hub" }, + { name = "multiprocess" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pyarrow" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/89/d3d6fef58a488f8569c82fd293ab7cbd4250244d67f425dcae64c63800ea/datasets-3.6.0.tar.gz", hash = "sha256:1b2bf43b19776e2787e181cfd329cb0ca1a358ea014780c3581e0f276375e041", size = 569336, upload-time = "2025-05-07T15:15:02.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/34/a08b0ee99715eaba118cbe19a71f7b5e2425c2718ef96007c325944a1152/datasets-3.6.0-py3-none-any.whl", hash = "sha256:25000c4a2c0873a710df127d08a202a06eab7bf42441a6bc278b499c2f72cd1b", size = 491546, upload-time = "2025-05-07T15:14:59.742Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "depyf" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astor", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "dill", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/35/83fb0178212279aa0af031031905804c6de5618435d229f41ed21bb9ad2c/depyf-0.20.0.tar.gz", hash = "sha256:fb7683bd72c44f67b56029df2c47721e9a02ffa4d7b19095f1c54c4ebf797a98", size = 6168761, upload-time = "2025-10-13T12:33:38.589Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/65/4df6936130b56e1429114e663e7c1576cf845f3aef1b2dd200c0a5d19dba/depyf-0.20.0-py3-none-any.whl", hash = "sha256:d31effad4261cebecb58955d832e448ace88f432328f95f82fd99c30fd9308d4", size = 39381, upload-time = "2025-10-13T12:33:33.647Z" }, +] + +[[package]] +name = "dill" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/17/4d/ac7ffa80c69ea1df30a8aa11b3578692a5118e7cd1aa157e3ef73b092d15/dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca", size = 184847, upload-time = "2024-01-27T23:42:16.145Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/7a/cef76fd8438a42f96db64ddaa85280485a9c395e7df3db8158cfec1eee34/dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7", size = 116252, upload-time = "2024-01-27T23:42:14.239Z" }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + +[[package]] +name = "editdistance" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/18/9f4f975ca87a390832b1c22478f3702fcdf739f83211e24d054b7551270d/editdistance-0.8.1.tar.gz", hash = "sha256:d1cdf80a5d5014b0c9126a69a42ce55a457b457f6986ff69ca98e4fe4d2d8fed", size = 50006, upload-time = "2024-02-10T07:44:53.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/c9/302658ce7f4c537a4e85cf578d11bbf7af120a712e1d78fedc6cb8823c65/editdistance-0.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:adeb705f32b93accc74960d227875abff150ee42d676e428536361fe5f8f5388", size = 106150, upload-time = "2024-02-10T07:43:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/45/80/0b3c7d2c0e183725986fea5dd2df11f0b4b46320e9a64f6077a121ab1f64/editdistance-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3de77951b105d0972deec7684a0b3d1a9dee69c9b5d34f6e2acc0d76cd4a1c52", size = 80551, upload-time = "2024-02-10T07:43:17.64Z" }, + { url = "https://files.pythonhosted.org/packages/b5/14/681460965c6a4a48321b07f88de2273d097fdca0491ff55db891aacbd291/editdistance-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e88efb052d45e924606c305cb833a80579dca3e8e4ff01309d50ba2c1c0bbd5", size = 79142, upload-time = "2024-02-10T07:43:19.195Z" }, + { url = "https://files.pythonhosted.org/packages/ed/0d/abdbc8e394a9461cf2ae27c16564fadaa65f52bd242dd1582ae5e7736dc3/editdistance-0.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0247e7a1e9c66ea75211a97e725366bff19a52aac2c838ed5f90025630e976dd", size = 396768, upload-time = "2024-02-10T07:43:20.912Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fb/2940d26ebda12efd280ae939436f17ac482930d862df9e774cb8b771ab03/editdistance-0.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67d143429a49ab552411505f550a0fb4285a1d4336e096804d233ec495ac20fc", size = 401846, upload-time = "2024-02-10T07:43:23.169Z" }, + { url = "https://files.pythonhosted.org/packages/53/cc/c63d75c7f387d4df0645682c1ab8706c2dfe5c9c0c4999723ce9a3ba0853/editdistance-0.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca9d3be2b10e5d44a950a4bd1e84bca9ebbecd364bce0cf5693bf8224c78eaef", size = 397543, upload-time = "2024-02-10T07:43:24.621Z" }, + { url = "https://files.pythonhosted.org/packages/8e/38/bb0f734a7571e093184606b930734b12da5b6bff2635eba9312fe4536dd9/editdistance-0.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5c72aa1df8535f2e2b3d8773a1a7da091bc1a7e52bb396e7e48d375ba687e7b2", size = 898934, upload-time = "2024-02-10T07:43:26.926Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/624fc7a09918f850a057465f02e86f269e139a457f48ff8cabfb12701756/editdistance-0.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9a606c34a2a6cc190e4fffc856b36333cdcf1f1fab5b22bd3088e585c22d6ca0", size = 959637, upload-time = "2024-02-10T07:43:28.997Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/7fa6cc277f91c477ee370807d51c1826891dc6dfc307544223ce7f2687de/editdistance-0.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5af173d442ffac33b7c7990132f97f88818a3abf4b21c0c702a7022df37c0c5c", size = 911024, upload-time = "2024-02-10T07:43:30.449Z" }, + { url = "https://files.pythonhosted.org/packages/ad/97/556215f71184291155aee340a6d34f0676e7238fdfd10615b6b775ce25fe/editdistance-0.8.1-cp310-cp310-win32.whl", hash = "sha256:fd64b58f5a7b59afd9d75982aaeeacd2a98498bf472fa0360c122ffe6ea4c871", size = 80834, upload-time = "2024-02-10T07:43:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d1/7ec5f5cbb95838d0eff7f980a660c81acd1363d658f2f5d4ceba38877c5a/editdistance-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:6c7c62c3cae45ca1fa01bb2722b297b9de1e3a244ac44cfba88bdcb488fe6aee", size = 79614, upload-time = "2024-02-10T07:43:33.255Z" }, + { url = "https://files.pythonhosted.org/packages/e2/dc/d0c29fd52d8f9e795653ed2b838a2a48c739cdfff04ac5b79c6c0ecbdf79/editdistance-0.8.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:486105603a273d73d12a54f347dffa70ab281749d7c3879658b377bc49e4b98c", size = 106079, upload-time = "2024-02-10T07:43:34.34Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/75fa45d7b78fbea6fd894f4e48895a75bd3c83d4a9a6b57673881d74d3e0/editdistance-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fad081f5f86a175c1a09a4e9e45b95c9349e454c21e181e842e01c85f1f536fc", size = 80580, upload-time = "2024-02-10T07:43:35.947Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a3/058d823b6285c3511dc94ed80620c3fb0c18b4aaa708f70ba71f3af28436/editdistance-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8cb78e125f6759398885a775f5eed07c2bb72b2f86da43e674c6b6a3335b273b", size = 79087, upload-time = "2024-02-10T07:43:36.923Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3a/0b13c7864c93b1e9b9952bd2a33c5ef3c4fd1bf70a5fad6924789e70e5eb/editdistance-0.8.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3778ca60aa89def9144b70e330bcec5330c7da1d69cb28c612e90b84510a1d3d", size = 409296, upload-time = "2024-02-10T07:43:38.52Z" }, + { url = "https://files.pythonhosted.org/packages/96/8a/db0fd79e8ddb9b5f86f274107c5d0a27ec4f2af88877df1f26c2c6d150cc/editdistance-0.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fba945eaa0436cf40bc53d7e299dc537c7c71353379a095b7459ff4af910da33", size = 412913, upload-time = "2024-02-10T07:43:39.852Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d2/98be7112750ff17b436dd76f988f1e38570dcec0df8578ee19ef046f22fe/editdistance-0.8.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:877f2a0d801f32bc1a1878901ffb947b974361e849c66e314a7f1d786a446b58", size = 407430, upload-time = "2024-02-10T07:43:41.048Z" }, + { url = "https://files.pythonhosted.org/packages/03/62/1815e3bf164910c47ba1948c8b5e937a40c7f9763b64e98fb6666b01dd06/editdistance-0.8.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e79d351ca40a6ead5f3763253fd7521572ee0d3e5d42538630e56d10f48db481", size = 909217, upload-time = "2024-02-10T07:43:42.916Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d3/a832cea7b507a9be54e4ac3d1340fb66dca5f9c16c70bf38d5039e8fdede/editdistance-0.8.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:70ed382b3052a51161bad0149d4665003bf3b949fce0b01bf1253a4cc1a88239", size = 969407, upload-time = "2024-02-10T07:43:44.912Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b4/db291d2a3845cbf8047b4b5aad3b3e038a8a2994d87027b40e1a1b0f4b74/editdistance-0.8.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a529bfb384c4000775d76739c4e64f73337f0f5a3784933b1321b577a62bed4e", size = 922112, upload-time = "2024-02-10T07:43:47.047Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/7ddeacada4982d0b892a28897e21871d0f25bca165e3663e37c3a272808a/editdistance-0.8.1-cp311-cp311-win32.whl", hash = "sha256:b082232429e731f181af7f7d2bcf79da6ca8fadd04e9086c11e2973f7d330c81", size = 80799, upload-time = "2024-02-10T07:43:48.231Z" }, + { url = "https://files.pythonhosted.org/packages/52/a1/778af8590b8b12f03f62eacc3c8744407ade9e3d69be6dabe38d0afbf2dd/editdistance-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:cef1a4359252a49f2c4718e64e9d40027d9d951b289d045bdb278656e59f6af8", size = 79698, upload-time = "2024-02-10T07:43:49.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4c/7f195588949b4e72436dc7fc902632381f96e586af829685b56daebb38b8/editdistance-0.8.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04af61b3fcdd287a07c15b6ae3b02af01c5e3e9c3aca76b8c1d13bd266b6f57", size = 106723, upload-time = "2024-02-10T07:43:50.268Z" }, + { url = "https://files.pythonhosted.org/packages/8d/82/31dc1640d830cd7d36865098329f34e4dad3b77f31cfb9404b347e700196/editdistance-0.8.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:18fc8b6eaae01bfd9cf999af726c1e8dcf667d120e81aa7dbd515bea7427f62f", size = 80998, upload-time = "2024-02-10T07:43:51.259Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2a/6b823e71cef694d6f070a1d82be2842706fa193541aab8856a8f42044cd0/editdistance-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a87839450a5987028738d061ffa5ef6a68bac2ddc68c9147a8aae9806629c7f", size = 79248, upload-time = "2024-02-10T07:43:52.873Z" }, + { url = "https://files.pythonhosted.org/packages/e1/31/bfb8e590f922089dc3471ed7828a6da2fc9453eba38c332efa9ee8749fd7/editdistance-0.8.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24b5f9c9673c823d91b5973d0af8b39f883f414a55ade2b9d097138acd10f31e", size = 415262, upload-time = "2024-02-10T07:43:54.498Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/57423942b2f847cdbbb46494568d00cd8a45500904ea026f0aad6ca01bc7/editdistance-0.8.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c59248eabfad603f0fba47b0c263d5dc728fb01c2b6b50fb6ca187cec547fdb3", size = 418905, upload-time = "2024-02-10T07:43:55.779Z" }, + { url = "https://files.pythonhosted.org/packages/1b/05/dfa4cdcce063596cbf0d7a32c46cd0f4fa70980311b7da64d35f33ad02a0/editdistance-0.8.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e239d88ff52821cf64023fabd06a1d9a07654f364b64bf1284577fd3a79d0e", size = 412511, upload-time = "2024-02-10T07:43:57.567Z" }, + { url = "https://files.pythonhosted.org/packages/0e/14/39608ff724a9523f187c4e28926d78bc68f2798f74777ac6757981108345/editdistance-0.8.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2f7f71698f83e8c83839ac0d876a0f4ef996c86c5460aebd26d85568d4afd0db", size = 917293, upload-time = "2024-02-10T07:43:59.559Z" }, + { url = "https://files.pythonhosted.org/packages/df/92/4a1c61d72da40dedfd0ff950fdc71ae83f478330c58a8bccfd776518bd67/editdistance-0.8.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:04e229d6f4ce0c12abc9f4cd4023a5b5fa9620226e0207b119c3c2778b036250", size = 975580, upload-time = "2024-02-10T07:44:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/47/3d/9877566e724c8a37f2228a84ec5cbf66dbfd0673515baf68a0fe07caff40/editdistance-0.8.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e16721636da6d6b68a2c09eaced35a94f4a4a704ec09f45756d4fd5e128ed18d", size = 929121, upload-time = "2024-02-10T07:44:02.764Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/8c50757d198b8ca30ddb91e8b8f0247a8dca04ff2ec30755245f0ab1ff0c/editdistance-0.8.1-cp312-cp312-win32.whl", hash = "sha256:87533cf2ebc3777088d991947274cd7e1014b9c861a8aa65257bcdc0ee492526", size = 81039, upload-time = "2024-02-10T07:44:04.134Z" }, + { url = "https://files.pythonhosted.org/packages/28/f0/65101e51dc7c850e7b7581a5d8fa8721a1d7479a0dca6c08386328e19882/editdistance-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:09f01ed51746d90178af7dd7ea4ebb41497ef19f53c7f327e864421743dffb0a", size = 79853, upload-time = "2024-02-10T07:44:05.687Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4c/c9d02eeb47815d35f8d324b52f6704ea7beb032bcb209358cac44047d413/editdistance-0.8.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a4a90c6b03094c07358572027a8d0a13cca7450b1aa6caca98a5f1fa4f0b8961", size = 76455, upload-time = "2024-02-10T07:44:36.838Z" }, + { url = "https://files.pythonhosted.org/packages/af/b0/2818fa6a24595dac069b0bfb9d05658406779a1ded8fd2b0c9066396cf99/editdistance-0.8.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:510a4f9ced348a4fd89ae2e102357d4d801a771e29bb2bc2f130a1692193407f", size = 84104, upload-time = "2024-02-10T07:44:37.928Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/3d5e09bcf7fdb7aed705bf74047a8634bd2b8fd92177c25a2547e6dbadfb/editdistance-0.8.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4787fa7228ba6a34b430066d174320f011d605015baa7299c2c4911e6ea6bd46", size = 89058, upload-time = "2024-02-10T07:44:39.113Z" }, + { url = "https://files.pythonhosted.org/packages/cd/88/fca5d7b1a1edf66ce1e5b6b60bff75842e6814b4f5facbdf4585d88c912d/editdistance-0.8.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee02601375073afccd6b4d811129ce1cb696d47db734784d8dbd1fddcea75447", size = 84635, upload-time = "2024-02-10T07:44:40.714Z" }, + { url = "https://files.pythonhosted.org/packages/a9/91/0e6285bbe2358d81fd16313d30306b2d0036387348f7bc11d8c076ca3c72/editdistance-0.8.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bc7ad9f9a20e6f351523de77c59249f005242e3f317b5de45d02c378d24f6531", size = 77389, upload-time = "2024-02-10T07:44:41.725Z" }, +] + +[[package]] +name = "einops" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "idna", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fastapi" +version = "0.128.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "fastapi-cli", extra = ["standard"], marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "httpx", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "jinja2", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pydantic-extra-types", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pydantic-settings", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "python-multipart", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "uvicorn", extra = ["standard"], marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "tomli", marker = "python_full_version < '3.11' and sys_platform != 'darwin'" }, + { name = "typer", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "uvicorn", extra = ["standard"], marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/58/74797ae9e4610cfa0c6b34c8309096d3b20bb29be3b8b5fbf1004d10fa5f/fastapi_cli-0.0.24.tar.gz", hash = "sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00", size = 19043, upload-time = "2026-02-24T10:45:10.476Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/4b/68f9fe268e535d79c76910519530026a4f994ce07189ac0dded45c6af825/fastapi_cli-0.0.24-py3-none-any.whl", hash = "sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc", size = 12304, upload-time = "2026-02-24T10:45:09.552Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "fastapi-cloud-cli", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "uvicorn", extra = ["standard"], marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastar", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "httpx", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pydantic", extra = ["email"], marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "rich-toolkit", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "rignore", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "sentry-sdk", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typer", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "uvicorn", extra = ["standard"], marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/79/66567c39c5fab6dbebf9e40b3a3fcb0e2ec359517c87a67434c76b06e60b/fastapi_cloud_cli-0.17.0.tar.gz", hash = "sha256:2b6c241b63427023bd1e23b3251f23234aba4b05428b245a050e92db1389823c", size = 47276, upload-time = "2026-04-15T13:17:56.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/31/fa442466bacadffec3d6611509d6ea391b6ca01b6ee0d4af835bfdea3483/fastapi_cloud_cli-0.17.0-py3-none-any.whl", hash = "sha256:b496e6998f037f572ab06a233ce257828b4c701488ce500b5c9d725e970a7cb1", size = 33936, upload-time = "2026-04-15T13:17:55.112Z" }, +] + +[[package]] +name = "fastapi-offline" +version = "1.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastapi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/8e/6f1d86982f09ecf9d326c7313d2e6be0a96d979130a87f61022c807a7414/fastapi_offline-1.7.6.tar.gz", hash = "sha256:c84d08584faa646932951b493106992caa79b838e454f14dd410cef9a1a5e07d", size = 745483, upload-time = "2026-01-15T04:29:43.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/56/10bd6900c99d9ecb3d803583b3736e8d741f19f04dd39e36aa7ad776c638/fastapi_offline-1.7.6-py3-none-any.whl", hash = "sha256:24d6851b5a94c50f669594b7ab9d1bbe3a4c0a53c4f4e9ce47798ff4591790d2", size = 744654, upload-time = "2026-01-15T04:29:41.943Z" }, +] + +[[package]] +name = "fastar" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/0f/0aeb3fc50046617702acc0078b277b58367fd62eb727b9ec733ae0e8bbcc/fastar-0.11.0.tar.gz", hash = "sha256:aa7f100f7313c03fdb20f1385927ba95671071ba308ad0c1763fef295e1895ce", size = 70238, upload-time = "2026-04-13T17:11:17.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/f6/97658dd992f4e45747d35adb24c0b100f6b6d451490685ae3fe8a3a2ee1b/fastar-0.11.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:56b50206aeedd99e22b83289e6fb3ff8f7d7da4407d2419902e4716b4f90585a", size = 869608, upload-time = "2026-04-13T17:09:08.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/fc/81c1ec4d8146a437399e7b95631b51be312f323a9ce64569f932db6c3914/fastar-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a1811a69ae81d469720df0c8af3f84f834a93b5e4f8be0e0e8bde6a52fa11f2", size = 762925, upload-time = "2026-04-13T17:07:52.788Z" }, + { url = "https://files.pythonhosted.org/packages/b9/35/49baf480ecb197aea7ce2515c503a2f25061958dd3b4c98e98a3a11cdcc7/fastar-0.11.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10486238c55589a3947c38f9cfb88a67d8a608eb8dddc722038237d0278a41d7", size = 759913, upload-time = "2026-04-13T17:08:07.324Z" }, + { url = "https://files.pythonhosted.org/packages/94/eb/946f1980267f2824efb7d7c518d47a49b89c0e9cd7c449301f5a7531558a/fastar-0.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1555ef9992d368a6ec39092276990cef8d329c39a1d86ebd847eaa3b10efd472", size = 926054, upload-time = "2026-04-13T17:08:22.196Z" }, + { url = "https://files.pythonhosted.org/packages/0c/19/d5eb611085ce054382570d8d4e24a5e2ff23cd6d2404528a6643841d6059/fastar-0.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1f4aca0a9620b76988bbf6225cdea6678a392902444ca18bb8a51495b165a89", size = 818594, upload-time = "2026-04-13T17:08:52.366Z" }, + { url = "https://files.pythonhosted.org/packages/4a/52/18e8d55c0d3d917713f381cb2d0cb793da00c209c802e011d8dc72018cd5/fastar-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75beeecac7d11a666a6c4a0b7f7e80842ae5cf523f2f890b99c78fc82b403545", size = 823005, upload-time = "2026-04-13T17:09:23.051Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/0fecdcf33e5aaffe777b96a1c10a3204fe0b05bf18e971033a0bfedafc1c/fastar-0.11.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a08cdf5d16daa401c65c9c7493a18db7dc515c52155a17071ec7098bb07da9d3", size = 887115, upload-time = "2026-04-13T17:08:37.385Z" }, + { url = "https://files.pythonhosted.org/packages/08/f8/2a6ad1c2523eb72a4595a9331162fc67ce0f0aee3348728598026c516986/fastar-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6e210375e5a7ba53586cbd6017aa417d2d2ceacbe8671682470281bd0a15e8ef", size = 973595, upload-time = "2026-04-13T17:10:09.258Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/2aa48843228673feacc2b80876b8924e63ea9c5f5f607bd7a72416b86bae/fastar-0.11.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a2988eb2604b8e15670f355425e8c800e4dcd4edfbcbfe194397f8f17b7eb19e", size = 1036988, upload-time = "2026-04-13T17:10:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/92/ac/3dd14b21c323e8484f47c910110d1d93139ba44621ac2c4c597dbe9fcdb7/fastar-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:34abc857b46068fdf91d157bd0203bfd6791dc7a432d1ed180f5af6c2f5bcce9", size = 1078267, upload-time = "2026-04-13T17:10:43.645Z" }, + { url = "https://files.pythonhosted.org/packages/de/a1/3f89e58d6fa99160c9e7e17220c8ab5040b5cc017c4fac2356c6ed18453d/fastar-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0d884be84e37a01053776395441fc960031974e0265801ce574efc3d05e0cdaf", size = 1032551, upload-time = "2026-04-13T17:11:00.667Z" }, + { url = "https://files.pythonhosted.org/packages/24/7c/0ed6dd38b9adc04b3a8ec3b7045908e7c2170ba0ff6e6d2c51bc9fc770f3/fastar-0.11.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a6931bebc1d8e95ddeef55732c195449e6b44ef33aa31b325505097ed3b4d6aa", size = 869663, upload-time = "2026-04-13T17:09:09.78Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/8b7fb3f23855accebaaf2d2637eac7f261a7a5d936f861a172079f1ef511/fastar-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f72ce42a5e28a74fbd4d5fbf1a3ac1a1163d13cbc200cbd005fb0fabc54bd", size = 762938, upload-time = "2026-04-13T17:07:54.51Z" }, + { url = "https://files.pythonhosted.org/packages/07/cc/5491e2b677bb841f768e3aba052d0344338a5c78aa5d4c18b443831a8e8d/fastar-0.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5b83c1f61f7017d6e1498568038f8745440cfc16ca2f697ec81bac83050108f6", size = 759232, upload-time = "2026-04-13T17:08:08.864Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/643630bdbd179e41e9fae31c03b4cf6061dbf4d6fbbae8425d16eb12545d/fastar-0.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db73a9b765a516e73983b25341e7b5e0189733878279e278b2295131b0e3a21e", size = 926271, upload-time = "2026-04-13T17:08:23.68Z" }, + { url = "https://files.pythonhosted.org/packages/09/5d/37ade50003b4540e0a53ef100f6692d7ab2ac1122d5acf39920cc09a3e8b/fastar-0.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:625827d52eb4e8fec942e0233f125ff8010fcf6a67c0a974a8e5f4666b771e3c", size = 818634, upload-time = "2026-04-13T17:08:54.268Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ff/135d177de32cc1e837c99019e4643e6e79352bde49544d4ece5b5eebf56b/fastar-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7f5fd8fa21ec0a88296a38dc5d7fc35efd3b26d46a17b8b7c73c5563925ca15", size = 822755, upload-time = "2026-04-13T17:09:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/27/cb/b835dbe76ceac7fa6105851468c259ffd06830eb9c029402e499d0ec153b/fastar-0.11.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8c15af91b8cd87ddf23ea55355ae513c1de3ab67178f26dad017c9e9c0af6096", size = 887101, upload-time = "2026-04-13T17:08:39.248Z" }, + { url = "https://files.pythonhosted.org/packages/9e/54/aa8289eb57fc550535470397cb051f5a58a7c89ca4de31d5502b916dd894/fastar-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a112395a8b0bff251423bd1564c012f0cc058ad8b6bd8fba96f3d7fc117e44", size = 973606, upload-time = "2026-04-13T17:10:10.98Z" }, + { url = "https://files.pythonhosted.org/packages/1f/fd/776d50a0897c01dc6bfd0926772ee913436fdae91b9affaf0a0cbd09f0a1/fastar-0.11.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f2994bb8f5f8c11eb12beae1e6e77a907173c9819236b8a4c8f0573652ceccce", size = 1036696, upload-time = "2026-04-13T17:10:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f1/cf0f9b499fb37ac065c8a01ec642f96a3c5eb849c38ae983b59f3b3245e0/fastar-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dcf99e4b5973d842c7f19c776c3a83cdc0977d505edce6206438505c0456b517", size = 1078182, upload-time = "2026-04-13T17:10:45.318Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9e/21e4701aec4a1123d4dc4d31578dc18875582b5710e4725f7ceb752a248b/fastar-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29c9c386dc0d5dda78845a8e6b1480d26ab861c1e0b68f42ae5735cb70ca07f1", size = 1032336, upload-time = "2026-04-13T17:11:02.364Z" }, + { url = "https://files.pythonhosted.org/packages/ab/69/9816d69ac8265c9e50456637a487ccfb7a9c566efd9dbcd673df9c2558c2/fastar-0.11.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd2f05666d4df7e14885b5c38fefd92a785917387513d33d837ff42ec143a22f", size = 863950, upload-time = "2026-04-13T17:09:11.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0d/f88daad53aff2e754b6b5ff2a7113f72447a34f6ef17cc23ca99988117b7/fastar-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e6e74aba1ae77ca4aedcaf1697cd413319f4c88a5ccbe5b42c709517c5097e", size = 760737, upload-time = "2026-04-13T17:07:55.958Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a6/82ef4ecd969d50d92ed3ed9dbd8fe77faa24be5e5736f716edc9f4ce8d62/fastar-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38ef77fe940bbc9b37a98bd838727f844b11731cd39358a2640ff864fb385086", size = 757603, upload-time = "2026-04-13T17:08:10.623Z" }, + { url = "https://files.pythonhosted.org/packages/03/35/50249f0d827251f8ac511495e2eacccebda80a00a0ad73e9615b8113b84f/fastar-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8955e61b32d6aff82c983217abf80933fd823b0e727586fc72f08043d996fd59", size = 923952, upload-time = "2026-04-13T17:08:25.526Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/faee41659e9c379d906d24eaee6d6833ac8cfef0a5df480e5c2a8d3efb33/fastar-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:483532442cdb08fbff0169510224eae0836f2f672cea6aacb52847d90fefdc46", size = 816574, upload-time = "2026-04-13T17:08:56.076Z" }, + { url = "https://files.pythonhosted.org/packages/22/47/0448ea7992b997dad2bf004bfd98eca74b5858630eae080b50c7b17d9ddc/fastar-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef5a6071121e05d8287fc75bccb054bcbac8bb0501200a0c0a8feeace5303ea4", size = 819382, upload-time = "2026-04-13T17:09:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/33/ef/0d63eb43586831b7a6f8b22c4d77125a7c594423af1f4f090fa9541b9b40/fastar-0.11.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e45e598af5afe8412197d4786efd6cf29be02e7d3d4f6a3461149eae5d7e94f1", size = 885254, upload-time = "2026-04-13T17:08:40.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/25/edd584675d69e49a165052c3ee886df1c5d574f3e7d813c990306387c623/fastar-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e160919b1c47ddb8538e7e8eb4cd527281b40f0bf75110a75993838ef61f286", size = 971239, upload-time = "2026-04-13T17:10:12.997Z" }, + { url = "https://files.pythonhosted.org/packages/a5/37/e8bb24f506ba2b08fbaf36c5800e843bd4d542954e9331f00418e2d23349/fastar-0.11.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4bb4dc0fc8f7a6807febcebce8a2f3626ba4955a9263d81ecc630aad83be84c0", size = 1035185, upload-time = "2026-04-13T17:10:30.207Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bf/be753736296338149ee4cb3e92e2b5423d6ba17c7b951d15218fd7e99bbf/fastar-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4ec95af56aa173f6e320e1183001bf108ba59beaf13edd1fc8200648db203588", size = 1072191, upload-time = "2026-04-13T17:10:47.072Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cd/a81c1aaafb5a22ce57c98ae22f39c89413ed53e4ee6e1b1444b0bd666a6c/fastar-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:136cf342735464091c39dc3708168f9fdeb9ebea40b1ead937c61afaf46143d9", size = 1028054, upload-time = "2026-04-13T17:11:04.293Z" }, + { url = "https://files.pythonhosted.org/packages/7e/36/8d4569e26473c72ccb02d1c5df3ed710073f1c06eca09c26d52ea79fd815/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8800e2387e463a0e5799416a1cbe72dd0fde7270a20e4bde684145e7878f6516", size = 870850, upload-time = "2026-04-13T17:09:21.439Z" }, + { url = "https://files.pythonhosted.org/packages/bf/46/724dc796e1756d3977970f820d30d59bb8cab8e3671b285f1d82ab513aec/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7496def0a2befd82d429cb004ef7ca831585cc887947bd6b9abb68a5ef852b0b", size = 764469, upload-time = "2026-04-13T17:08:05.638Z" }, + { url = "https://files.pythonhosted.org/packages/99/e3/74d6859e632e8fb9339a14f652fb9f800c2bd6aa53071e311c0be3fbab8b/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:878eaf15463eb572e3538af7ca3a8534e5e279cf8196db902d24e5725c4af86e", size = 761375, upload-time = "2026-04-13T17:08:20.669Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e7/cc70e2be5ef8731a7525552b1c35c1448cf9eae6a62cb3a56f12c1bf27ea/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0324ed1d1ef0186e1bbd843b17807d6d837d0906899d4c99378b02c5d86bdd9c", size = 928189, upload-time = "2026-04-13T17:08:35.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/33/c9a969e78dca323547276a6fee5f4f9588f7cd5ab45acec3778c67399589/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bdf9bd863205590beaf8ef6e66f315310196632180dceaf674985d01a876cac3", size = 820864, upload-time = "2026-04-13T17:09:06.366Z" }, + { url = "https://files.pythonhosted.org/packages/84/bd/6b9434b541fe55c125b5f2e017a565596a2d215aa09207e4555e4585064f/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59af8dbb683b24b90fb5b506de080faeab0a17a908e6c2a5d93a97260ed75d7b", size = 824060, upload-time = "2026-04-13T17:09:37.377Z" }, + { url = "https://files.pythonhosted.org/packages/24/8d/871d5f8cf4c6f13987119fb0a9ae8be131e34f2756c2524e9974adf33824/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:9f3df73a3c4292cfe15696cdf59cdb6c309ab59d30b34c733be13c6e32d9a264", size = 889217, upload-time = "2026-04-13T17:08:50.884Z" }, + { url = "https://files.pythonhosted.org/packages/d0/26/cca0fd2704f3ed20165e5613ed911549aef3aaf3b0b5b02fee0e8e23e6cc/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa3762cbb16e41a76b61f4a6914937a71aab3a7b6c2d82ca233bc686ebaf756b", size = 975418, upload-time = "2026-04-13T17:10:24.307Z" }, + { url = "https://files.pythonhosted.org/packages/99/94/8bbb0b13f5b6cbe2492f0b7cbba5103e6163976a3331466d010e781fa189/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:a8c7bc8ac74cb359bb546b199288c83236372d094b402e557c197e85527495cd", size = 1038492, upload-time = "2026-04-13T17:10:41.939Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d3/5b7df222a30eac2822ffd00f82fd4c2ce84fba4b369d1e1a03732fd177fc/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:587cbd060a2699c5f66281081395bb4657b2b1e0eef5c206b1aabf740019d670", size = 1080210, upload-time = "2026-04-13T17:10:58.462Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/56ef943ea524784598c035ccbd42e564e937da0438ae3f55f0e76cb95571/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6a1c56957ac82408be37a3f63594bc83e0919e8760492a4475e542f9f1828778", size = 1034886, upload-time = "2026-04-13T17:11:15.617Z" }, +] + +[[package]] +name = "fastsafetensors" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typer", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/69/e34a1e86a02b255896c57263bf0dfbae45b4708fd609b937f783c2202e7b/fastsafetensors-0.3.1.tar.gz", hash = "sha256:b7eb039a564d77280d17e5d63b27e9963ba5158ad02d2a3c1772c62072a81a53", size = 55665, upload-time = "2026-05-06T08:48:59.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/77/0913cd907585085b2f10f2d7eef2aa52c9445241dc66238de25ee24c8241/fastsafetensors-0.3.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:acc0971b006f381598d32c5f7aae3a07d9a10d87c701f26b65215083e15a4733", size = 1808292, upload-time = "2026-05-06T08:48:51.12Z" }, + { url = "https://files.pythonhosted.org/packages/e8/67/eaa10409a526242253926fe6981c652dfdb8aa4ec0d4cba4077a9376a1fd/fastsafetensors-0.3.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32cf4b531b5d77de41d777106ea69036a16bdea80062646dabc93a11b4cf88ee", size = 1828304, upload-time = "2026-05-06T08:48:53.15Z" }, + { url = "https://files.pythonhosted.org/packages/6f/50/909871d673bacd6dfc7fee5e59bcd4ec9fbd19775bafe567ad236a3adced/fastsafetensors-0.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac76f33e47959b7c31658fbbda1805df7540819828a3ce6a94eb34b4db0b1fa7", size = 1854825, upload-time = "2026-05-06T08:48:54.452Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + +[[package]] +name = "flashinfer-cubin" +version = "0.6.8.post1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/b7/5e3b1a8c67031b421a8bd29c2bc29b900a550bb3392e8bda18bb15b5e476/flashinfer_cubin-0.6.8.post1-py3-none-any.whl", hash = "sha256:43636d4cd39e694a83d76a89f87fefcdf4cecb4c4f7dd22dac25ec368c1e901f", size = 295154113, upload-time = "2026-04-18T18:28:21.738Z" }, +] + +[[package]] +name = "flashinfer-python" +version = "0.6.8.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apache-tvm-ffi", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "click", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "cuda-tile", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "einops", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "ninja", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "numpy", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-cudnn-frontend", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-cutlass-dsl", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-ml-py", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "packaging", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "requests", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "tabulate", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "torch", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "tqdm", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/1e/2760fef9e74abc4480961048e5790b4c9e955872fb4d7d97900cfddced5a/flashinfer_python-0.6.8.post1.tar.gz", hash = "sha256:b18e4121baf9b93fa9a9f368ba9b981a0342895f50ab9dddc224aeb964ed346f", size = 6675885, upload-time = "2026-04-18T18:28:13.299Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/6d/1e8a8533913e33a50a486332ce0673f4fdb860f6eb9ed450327c5c1762cb/flashinfer_python-0.6.8.post1-py3-none-any.whl", hash = "sha256:818f9b8cc2fe66c42a1f6264be4841ac8821ada703685a02cfccb2b5124a710b", size = 9385316, upload-time = "2026-04-18T18:28:10.285Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/f4/5721faf47b8c499e776bc34c6a8fc17efdf7fdef0b00f398128bc5dcb4ac/fsspec-2025.3.0.tar.gz", hash = "sha256:a935fd1ea872591f2b5148907d103488fc523295e6c64b835cfad8c3eca44972", size = 298491, upload-time = "2025-03-07T21:47:56.461Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/53/eb690efa8513166adef3e0669afd31e95ffde69fb3c52ec2ac7223ed6018/fsspec-2025.3.0-py3-none-any.whl", hash = "sha256:efb87af3efa9103f94ca91a7f8cb7a4df91af9f74fc106c9c7ea0efd7277c1b3", size = 193615, upload-time = "2025-03-07T21:47:54.809Z" }, +] + +[package.optional-dependencies] +http = [ + { name = "aiohttp" }, +] + +[[package]] +name = "funasr" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "editdistance" }, + { name = "hydra-core" }, + { name = "jaconv" }, + { name = "jamo" }, + { name = "jieba" }, + { name = "kaldiio" }, + { name = "librosa" }, + { name = "modelscope" }, + { name = "oss2" }, + { name = "pytorch-wpe" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "scipy" }, + { name = "sentencepiece" }, + { name = "soundfile" }, + { name = "tensorboardx" }, + { name = "torch-complex" }, + { name = "tqdm" }, + { name = "umap-learn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/dc/d5586cd788bdf75286d73e646f5f354f8b3220b75a61c0325a177689065d/funasr-1.3.1.tar.gz", hash = "sha256:ed813c0ecade7d24393943a82a91f7cee822178ab4bfd303adb8ad318605e0a5", size = 664362, upload-time = "2026-01-26T13:07:43.579Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/89/61c09967f0f4f091402367215000c3c060bfeaadbbdc9aca42856d299c95/funasr-1.3.1-py3-none-any.whl", hash = "sha256:f63050d7d625f287ec741b84a0325365699c49550a4bfd3ace4acb43e41a87a8", size = 811975, upload-time = "2026-01-26T13:07:41.866Z" }, +] + +[[package]] +name = "gguf" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pyyaml", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "requests", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "tqdm", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/26/7622a41c39db9d7090225a4bf8368550e59694dcf7313b44f9a82b501209/gguf-0.18.0.tar.gz", hash = "sha256:b4659093d5d0dccdb5902a904d54b327f4052879fe5e90946ad5fce9f8018c2e", size = 107170, upload-time = "2026-02-27T15:05:39.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/0c/e0f1eae7535a97476fb903f65301e35da2a66182b8161066b7eb312b2cb8/gguf-0.18.0-py3-none-any.whl", hash = "sha256:af93f7ef198a265cbde5fa6a6b3101528bca285903949ab0a3e591cd993a1864", size = 114244, upload-time = "2026-02-27T15:05:37.991Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.74.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/18/a746c8344152d368a5aac738d4c857012f2c5d1fd2eac7e17b647a7861bd/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1", size = 151254, upload-time = "2026-04-02T21:23:26.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload-time = "2026-04-02T21:22:49.108Z" }, +] + +[[package]] +name = "grpcio" +version = "1.80.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/cd/bb7b7e54084a344c03d68144450da7ddd5564e51a298ae1662de65f48e2d/grpcio-1.80.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:886457a7768e408cdce226ad1ca67d2958917d306523a0e21e1a2fdaa75c9c9c", size = 6050363, upload-time = "2026-03-30T08:46:20.894Z" }, + { url = "https://files.pythonhosted.org/packages/43/98/c910254eedf2cae368d78336a2de0678e66a7317d27c02522392f949b5c6/grpcio-1.80.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:33eb763f18f006dc7fee1e69831d38d23f5eccd15b2e0f92a13ee1d9242e5e02", size = 6602306, upload-time = "2026-03-30T08:46:27.593Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f8/88ca4e78c077b2b2113d95da1e1ab43efd43d723c9a0397d26529c2c1a56/grpcio-1.80.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:52d143637e3872633fc7dd7c3c6a1c84e396b359f3a72e215f8bf69fd82084fc", size = 7301535, upload-time = "2026-03-30T08:46:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f9/96/f28660fe2fe0f153288bf4a04e4910b7309d442395135c88ed4f5b3b8b40/grpcio-1.80.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c51bf8ac4575af2e0678bccfb07e47321fc7acb5049b4482832c5c195e04e13a", size = 6808669, upload-time = "2026-03-30T08:46:31.984Z" }, + { url = "https://files.pythonhosted.org/packages/47/eb/3f68a5e955779c00aeef23850e019c1c1d0e032d90633ba49c01ad5a96e0/grpcio-1.80.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:50a9871536d71c4fba24ee856abc03a87764570f0c457dd8db0b4018f379fed9", size = 7409489, upload-time = "2026-03-30T08:46:34.684Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a7/d2f681a4bfb881be40659a309771f3bdfbfdb1190619442816c3f0ffc079/grpcio-1.80.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a72d84ad0514db063e21887fbacd1fd7acb4d494a564cae22227cd45c7fbf199", size = 8423167, upload-time = "2026-03-30T08:46:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/29b4589c204959aa35ce5708400a05bba72181807c45c47b3ec000c39333/grpcio-1.80.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f7691a6788ad9196872f95716df5bc643ebba13c97140b7a5ee5c8e75d1dea81", size = 7846761, upload-time = "2026-03-30T08:46:40.091Z" }, + { url = "https://files.pythonhosted.org/packages/5d/db/1d56e5f5823257b291962d6c0ce106146c6447f405b60b234c4f222a7cde/grpcio-1.80.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a", size = 6055009, upload-time = "2026-03-30T08:46:46.265Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8e/e14966b435be2dda99fbe89db9525ea436edc79780431a1c2875a3582644/grpcio-1.80.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2", size = 6610297, upload-time = "2026-03-30T08:46:52.123Z" }, + { url = "https://files.pythonhosted.org/packages/cc/26/d5eb38f42ce0e3fdc8174ea4d52036ef8d58cc4426cb800f2610f625dd75/grpcio-1.80.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3cb8130ba457d2aa09fa6b7c3ed6b6e4e6a2685fce63cb803d479576c4d80e21", size = 7300208, upload-time = "2026-03-30T08:46:54.859Z" }, + { url = "https://files.pythonhosted.org/packages/25/51/bd267c989f85a17a5b3eea65a6feb4ff672af41ca614e5a0279cc0ea381c/grpcio-1.80.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab", size = 6813442, upload-time = "2026-03-30T08:46:57.056Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d9/d80eef735b19e9169e30164bbf889b46f9df9127598a83d174eb13a48b26/grpcio-1.80.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", size = 7414743, upload-time = "2026-03-30T08:46:59.682Z" }, + { url = "https://files.pythonhosted.org/packages/de/f2/567f5bd5054398ed6b0509b9a30900376dcf2786bd936812098808b49d8d/grpcio-1.80.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8502122a3cc1714038e39a0b071acb1207ca7844208d5ea0d091317555ee7106", size = 8426046, upload-time = "2026-03-30T08:47:02.474Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/73ef0141b4732ff5eacd68430ff2512a65c004696997f70476a83e548e7e/grpcio-1.80.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6", size = 7851641, upload-time = "2026-03-30T08:47:05.462Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" }, + { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, + { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hdbscan" +version = "0.8.41" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/22/32a66dd4ce72145ec1b792c794b98897be467bdf18c10aa8b48275530b11/hdbscan-0.8.41.tar.gz", hash = "sha256:e41e823e5bb21ff2173f252d226266b1dda82bdbba5d89106eafb251429dff3d", size = 7091384, upload-time = "2025-12-12T15:48:30.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/7a/8bc50300f7c1240284b8a69d6c69c59ba37e6349bc9ca097760d3efae077/hdbscan-0.8.41-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:0589ea22e225e4ed6fae8b0a6ac6d18c0aff15165b8ddea7561962e1006b7e63", size = 755274, upload-time = "2025-12-12T15:49:22.72Z" }, + { url = "https://files.pythonhosted.org/packages/10/d9/cf2dc6c14ff2a85f2f48a5c3e034df3690b655f5dc09e2e7db6bc140e0ce/hdbscan-0.8.41-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3970e33b7b370cdca5d0fc5d31171c4b5d588590ec6f04b83e08f743099ba950", size = 4162517, upload-time = "2025-12-12T15:48:27.787Z" }, + { url = "https://files.pythonhosted.org/packages/63/f1/37daced2420b5edaea6fb91875fa4c08e1ef72eaf6f84bc8d4442f45c1dc/hdbscan-0.8.41-cp310-cp310-win_amd64.whl", hash = "sha256:7a689386170d91d1bd9386665b521e4ae66b6a78e0b7e34265ea5b1aa1eb165f", size = 687021, upload-time = "2025-12-12T15:48:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/70/58/5c1cbfac6dd5fd4310da17b09950c68dba1a7c1bdd267eb31468b140bd3a/hdbscan-0.8.41-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be948fc76d0035d93d309920f14ab6a0185580a63c5a63b05739f08b45dc6c03", size = 1394443, upload-time = "2025-12-12T15:49:56.098Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f1/9a17849751488049003a6af08b270eac1e0135d1d29dfd006bcc4edcca00/hdbscan-0.8.41-cp311-cp311-win_amd64.whl", hash = "sha256:0af3e3bab1eb6b07ea497afc4d2db1b58122974fb052bd21f0ea4b42fcf8d535", size = 687100, upload-time = "2025-12-12T15:49:57.879Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6b/b589c0e903e00108c62f324e99840ab050f1da344fab9cf143ce8ebf1d38/hdbscan-0.8.41-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:07ae4c44098449bd9de12145ad17e92ef699754a43988cbea2dd1a95b89bf142", size = 1393282, upload-time = "2025-12-12T15:52:38.472Z" }, + { url = "https://files.pythonhosted.org/packages/59/ab/6314e52aee546cc14b74fbb575b8713eeec2255880ec99d6490838306d3a/hdbscan-0.8.41-cp312-cp312-win_amd64.whl", hash = "sha256:dce39272d2d4f1dde50dde9cc428cadb84ed16326de872b01761f7ec4f690419", size = 671752, upload-time = "2025-12-12T15:51:33.213Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477, upload-time = "2026-03-31T22:40:07.874Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/9f/9c23e4a447b8f83120798f9279d0297a4d1360bdbf59ef49ebec78fe2545/hf_xet-1.4.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d0da85329eaf196e03e90b84c2d0aca53bd4573d097a75f99609e80775f98025", size = 3805048, upload-time = "2026-03-31T22:39:53.105Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178, upload-time = "2026-03-31T22:39:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320, upload-time = "2026-03-31T22:39:42.922Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546, upload-time = "2026-03-31T22:39:41.335Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200, upload-time = "2026-03-31T22:40:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392, upload-time = "2026-03-31T22:40:03.492Z" }, + { url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359, upload-time = "2026-03-31T22:40:13.619Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664, upload-time = "2026-03-31T22:40:12.152Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "h11", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" }, + { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" }, + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "certifi", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "httpcore", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "idna", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/77/a9e27cdb6a2f8387c7b8f7e6a107458b7b96f156a5a78fab922d0d563df8/huggingface_hub-0.34.0.tar.gz", hash = "sha256:2c6f373fac66b1afc2fe47efd8e603a876935dbd669f966231f265da5c353e25", size = 456749, upload-time = "2025-07-25T08:52:20.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/ac/2f910d2572360ff2f3c4faf41b6c3e170b8934704be8bab2c7f80ff801e9/huggingface_hub-0.34.0-py3-none-any.whl", hash = "sha256:29737225f21dcc5c6b41bc9ab1073e65f9a2f84ecf9ad0a0c2fb92ae8cee173f", size = 558651, upload-time = "2025-07-25T08:52:18.787Z" }, +] + +[[package]] +name = "hydra-core" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "omegaconf" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/8e/07e42bc434a847154083b315779b0a81d567154504624e181caf2c71cd98/hydra-core-1.3.2.tar.gz", hash = "sha256:8a878ed67216997c3e9d88a8e72e7b4767e81af37afb4ea3334b269a4390a824", size = 3263494, upload-time = "2023-02-23T18:33:43.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/50/e0edd38dcd63fb26a8547f13d28f7a008bc4a3fd4eb4ff030673f22ad41a/hydra_core-1.3.2-py3-none-any.whl", hash = "sha256:fa0238a9e31df3373b35b0bfb672c34cc92718d21f81311d8996a16de1141d8b", size = 154547, upload-time = "2023-02-23T18:33:40.801Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +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 = "ijson" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/57/60d1a6a512f2f0508d0bc8b4f1cc5616fd3196619b66bd6a01f9155a1292/ijson-3.5.0.tar.gz", hash = "sha256:94688760720e3f5212731b3cb8d30267f9a045fb38fb3870254e7b9504246f31", size = 68658, upload-time = "2026-02-24T03:58:30.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e1/67dfe0774e4c7ca6ec8702e280e8764d356f3db54358999818cda6df7679/ijson-3.5.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:45887d5e84ff0d2b138c926cebd9071830733968afe8d9d12080b3c178c7f918", size = 126558, upload-time = "2026-02-24T03:56:43.922Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ef/23d614fc773d428caeb6e197218b7e32adcc668ff5b98777039149571208/ijson-3.5.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a70b575be8e57a28c80e90ed349ad3a851c3478524c70e36e07d6092ecd12c9", size = 133091, upload-time = "2026-02-24T03:56:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b8/80/99727603cd8a1d32edafa4392f4056b2420bf48c15afd34481c68a2d4435/ijson-3.5.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2adeecd45830bfd5580ca79a584154713aabef0b9607e16249133df5d2859813", size = 130249, upload-time = "2026-02-24T03:56:46.333Z" }, + { url = "https://files.pythonhosted.org/packages/0b/94/3a3d623ca80768e834be8a834ef05960e3b9e79af1a911704ff10c9e8792/ijson-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d873e72889e7fc5962ab58909f1adff338d7c2f49e450e5b5fe844eff8155a14", size = 133501, upload-time = "2026-02-24T03:56:47.54Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f6/df2c14ad340834eccee379046f155e4b66a16ddafd445429dee7b3323614/ijson-3.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9a88c559456a79708592234d697645d92b599718f4cbbeaa6515f83ac63ca0ae", size = 128438, upload-time = "2026-02-24T03:56:48.455Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7e/9ff5b8b5fee113f5607bc4149b707382a898eeb545153189b075e5ec8d59/ijson-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf83f58ad50dc0d39a2105cb26d4f359b38f42cef68b913170d4d47d97d97ba5", size = 131116, upload-time = "2026-02-24T03:56:49.737Z" }, + { url = "https://files.pythonhosted.org/packages/e4/68/474541998abbdecfd46a744536878335de89aceb9f085bff1aaf35575ceb/ijson-3.5.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c061314845c08163b1784b6076ea5f075372461a32e6916f4e5f211fd4130b64", size = 131988, upload-time = "2026-02-24T03:56:56.35Z" }, + { url = "https://files.pythonhosted.org/packages/cd/32/e05ff8b72a44fe9d192f41c5dcbc35cfa87efc280cdbfe539ffaf4a7535e/ijson-3.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1111a1c5ac79119c5d6e836f900c1a53844b50a18af38311baa6bb61e2645aca", size = 138669, upload-time = "2026-02-24T03:56:57.555Z" }, + { url = "https://files.pythonhosted.org/packages/49/b5/955a83b031102c7a602e2c06d03aff0a0e584212f09edb94ccc754d203ac/ijson-3.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e74aff8c681c24002b61b1822f9511d4c384f324f7dbc08c78538e01fdc9fcb", size = 135093, upload-time = "2026-02-24T03:56:59.267Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f2/30250cfcb4d2766669b31f6732689aab2bb91de426a15a3ebe482df7ee48/ijson-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:739a7229b1b0cc5f7e2785a6e7a5fc915e850d3fed9588d0e89a09f88a417253", size = 138715, upload-time = "2026-02-24T03:57:00.491Z" }, + { url = "https://files.pythonhosted.org/packages/a2/05/785a145d7e75e04e04480d59b6323cd4b1d9013a6cd8643fa635fbc93490/ijson-3.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ef88712160360cab3ca6471a4e5418243f8b267cf1fe1620879d1b5558babc71", size = 133194, upload-time = "2026-02-24T03:57:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/14/eb/80d6f8a748dead4034cea0939494a67d10ccf88d6413bf6e860393139676/ijson-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ca0d1b6b5f8166a6248f4309497585fb8553b04bc8179a0260fad636cfdb798", size = 135588, upload-time = "2026-02-24T03:57:03.131Z" }, + { url = "https://files.pythonhosted.org/packages/31/76/6f91bdb019dd978fce1bc5ea1cd620cfc096d258126c91db2c03a20a7f34/ijson-3.5.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d48dc2984af02eb3c56edfb3f13b3f62f2f3e4fe36f058c8cfc75d93adf4fed", size = 138977, upload-time = "2026-02-24T03:57:11.932Z" }, + { url = "https://files.pythonhosted.org/packages/11/be/bbc983059e48a54b0121ee60042979faed7674490bbe7b2c41560db3f436/ijson-3.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1e73a44844d9adbca9cf2c4132cd875933e83f3d4b23881fcaf82be83644c7d", size = 149785, upload-time = "2026-02-24T03:57:13.255Z" }, + { url = "https://files.pythonhosted.org/packages/6d/81/2fee58f9024a3449aee83edfa7167fb5ccd7e1af2557300e28531bb68e16/ijson-3.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7389a56b8562a19948bdf1d7bae3a2edc8c7f86fb59834dcb1c4c722818e645a", size = 149729, upload-time = "2026-02-24T03:57:14.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/56/f1706761fcc096c9d414b3dcd000b1e6e5c24364c21cfba429837f98ee8d/ijson-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3176f23f8ebec83f374ed0c3b4e5a0c4db7ede54c005864efebbed46da123608", size = 150697, upload-time = "2026-02-24T03:57:15.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6e/ee0d9c875a0193b632b3e9ccd1b22a50685fb510256ad57ba483b6529f77/ijson-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6babd88e508630c6ef86c9bebaaf13bb2fb8ec1d8f8868773a03c20253f599bc", size = 142873, upload-time = "2026-02-24T03:57:16.831Z" }, + { url = "https://files.pythonhosted.org/packages/d2/bf/f9d4399d0e6e3fd615035290a71e97c843f17f329b43638c0a01cf112d73/ijson-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dc1b3836b174b6db2fa8319f1926fb5445abd195dc963368092103f8579cb8ed", size = 151583, upload-time = "2026-02-24T03:57:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/ef/83/44dbd0231b0a8c6c14d27473d10c4e27dfbce7d5d9a833c79e3e6c33eb40/ijson-3.5.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e7dbff2c8d9027809b0cde663df44f3210da10ea377121d42896fb6ee405dd31", size = 71229, upload-time = "2026-02-24T03:58:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/cf84048b7c6cec888826e696a31f45bee7ebcac15e532b6be1fc4c2c9608/ijson-3.5.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4217a1edc278660679e1197c83a1a2a2d367792bfbb2a3279577f4b59b93730d", size = 71217, upload-time = "2026-02-24T03:58:28.021Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0a/e34c729a87ff67dc6540f6bcc896626158e691d433ab57db0086d73decd2/ijson-3.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04f0fc740311388ee745ba55a12292b722d6f52000b11acbb913982ba5fbdf87", size = 68618, upload-time = "2026-02-24T03:58:28.918Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "interegular" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/9d/8b6dde58a028a3962ce17e84d5fe73758df61378e00ef8ac3d85da34b0ff/interegular-0.3.3.tar.gz", hash = "sha256:d9b697b21b34884711399ba0f0376914b81899ce670032486d0d048344a76600", size = 24705, upload-time = "2024-01-06T23:01:22.372Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/01/72d6472f80651673716d1deda2a5bbb633e563ecf94f4479da5519d69d25/interegular-0.3.3-py37-none-any.whl", hash = "sha256:b0c07007d48c89d6d19f7204972d369b2a77222722e126b6aa63aa721dc3b19c", size = 23635, upload-time = "2024-01-06T23:01:20.829Z" }, +] + +[[package]] +name = "itntext" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "kaldifst" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/74/a993913031b21d5fcc9d22ef363ce1c589d9c89ea16cb4d6748be745008d/itntext-0.1.5.tar.gz", hash = "sha256:6cf0bf2deee51786f8523b35d61dc357d3ece0612a41021d9b9d2d17dfbcc31f", size = 484023, upload-time = "2026-06-12T04:52:18.238Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/04/0dd5e86a9e6f843cd286d7cec890ead69c90e398d7ccce85cc181786e3a8/itntext-0.1.5-py3-none-any.whl", hash = "sha256:1a89a8a2105f129ce07b90f44a9b8542ac3b3da4b369c97fea5b0fc52755c03c", size = 514874, upload-time = "2026-06-12T04:52:16.812Z" }, +] + +[[package]] +name = "jaconv" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/0e/9fffaacda59bdfa479372c71d18d72968d2af5a36a5a2086b02a60124b98/jaconv-0.5.0.tar.gz", hash = "sha256:53f6f968276846716f0f37100a6d5c7308cfa1e0c714eb41287d5bb09345c40f", size = 21816, upload-time = "2026-02-08T11:15:57.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/da/9657d637bcacdbaf6a914ce504000da5639f9d945f8d3552a940f021d6c0/jaconv-0.5.0-py3-none-any.whl", hash = "sha256:2914114fe761ca49fc7089e25e6ad4a400c26f262ffce84e13b176916b71610a", size = 16831, upload-time = "2026-02-08T11:15:55.322Z" }, +] + +[[package]] +name = "jamo" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/a2/bda770579809726e929ca6356743f9f50f64a2cbaee578fa9d4824afb00e/jamo-0.4.1.tar.gz", hash = "sha256:ea65cf9d35338d0e0af48d75ff426d8a369b0ebde6f07051c3ac37256f56d025", size = 7386, upload-time = "2017-11-06T19:28:51.729Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/cc/49812faae67f9a24be6ddaf58a2cf7e8c3cbfcf5b762d9414f7103d2ea2c/jamo-0.4.1-py3-none-any.whl", hash = "sha256:d4b94fd23324c606ed2fbc4037c603e2c3a7ae9390c05d3473aea1ccb6b1c3fb", size = 9543, upload-time = "2017-11-06T19:28:49.624Z" }, +] + +[[package]] +name = "jieba" +version = "0.42.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/cb/18eeb235f833b726522d7ebed54f2278ce28ba9438e3135ab0278d9792a2/jieba-0.42.1.tar.gz", hash = "sha256:055ca12f62674fafed09427f176506079bc135638a14e23e25be909131928db2", size = 19214172, upload-time = "2020-01-20T14:27:23.5Z" } + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d8/2040b9efa13c917f855c40890ae4119fe02c25b7c7677d5b4fa820a851fc/jiter-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cf4d4c109641f9cfaf4a7b6aebd51654e405cd00fa9ebbf87163b8b97b325aa", size = 347387, upload-time = "2026-04-10T14:25:44.212Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/655c0ad5ce6a8e90f9068c175b8a236877d753e460762b3183c136db1c5b/jiter-0.14.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80c7b41a628e6be2213ad0ece763c5f88aa5ee003fa394d58acaaee1f4b8342", size = 373083, upload-time = "2026-04-10T14:25:45.55Z" }, + { url = "https://files.pythonhosted.org/packages/f1/66/549c40fa068f08710b7570869c306a051eb67a29758bd64f4114f730554c/jiter-0.14.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb3dbf7cc0d4dbe73cce307ebe7eefa7f73a7d3d854dd119ea0c243f03e40927", size = 463639, upload-time = "2026-04-10T14:25:47.452Z" }, + { url = "https://files.pythonhosted.org/packages/25/2f/97a32a05fed14ed58a18e181fdfb619e05163f3726b54ee6080ec0539c09/jiter-0.14.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7054adcdeb06b46efd17b5734f75817a44a2d06d3748e36c3a023a1bb52af9ec", size = 380735, upload-time = "2026-04-10T14:25:49.305Z" }, + { url = "https://files.pythonhosted.org/packages/2a/3b/4347e1d6c2a973d653bbb7a2d671a2d2426e54b52ba735b8ff0d0a29b75c/jiter-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d597cd1bf6790376f3fffc7c708766e57301d99a19314824ea0ccc9c3c70e1e2", size = 358632, upload-time = "2026-04-10T14:25:50.931Z" }, + { url = "https://files.pythonhosted.org/packages/ef/24/ca452fbf2ea33548ed30ce68a39a50442d3f7c9bf0704a7af958a930c057/jiter-0.14.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:df63a14878da754427926281626fd3ee249424a186e25a274e78176d42945264", size = 359969, upload-time = "2026-04-10T14:25:52.381Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a3/94470a0d199287caabeb4da2bb2ae5f6d17f3cf05dfc975d7cb064d58e0f/jiter-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ea73187627bcc5810e085df715e8a99da8bdfd96a7eb36b4b4df700ba6d4c9c", size = 397529, upload-time = "2026-04-10T14:25:53.801Z" }, + { url = "https://files.pythonhosted.org/packages/cf/71/6768edc09d7c45c39f093feb3de105fa718a3e982b5208b8a2ed6382b44b/jiter-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9f541eaf7bb8382367a1a23d6fc3d6aad57f8dd8c18c3c17f838bee20f217220", size = 522342, upload-time = "2026-04-10T14:25:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6b/5c2e17559a0f4e96e934479f7137df46c939e983fa05244e674815befb73/jiter-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:107465250de4fce00fdb47166bcd51df8e634e049541174fe3c71848e44f52ce", size = 556784, upload-time = "2026-04-10T14:25:56.927Z" }, + { url = "https://files.pythonhosted.org/packages/ed/36/4c72e67180d4e71a4f5dcf7886d0840e83c49ab11788172177a77570326e/jiter-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c4a7ab56f746014874f2c525584c0daca1dec37f66fd707ecef3b7e5c2228c", size = 347393, upload-time = "2026-04-10T14:26:05.314Z" }, + { url = "https://files.pythonhosted.org/packages/bc/db/9b39e09ceafa9878235c0fc29e3e3f9b12a4c6a98ea3085b998cadf3accc/jiter-0.14.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:376e9dafff914253bb9d46cdc5f7965607fbe7feb0a491c34e35f92b2770702e", size = 372937, upload-time = "2026-04-10T14:26:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/b0/96/0dcba1d7a82c1b720774b48ef239376addbaf30df24c34742ac4a57b67b2/jiter-0.14.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23ad2a7a9da1935575c820428dd8d2490ce4d23189691ce33da1fc0a58e14e1c", size = 463646, upload-time = "2026-04-10T14:26:08.345Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e3/f61b71543e746e6b8b805e7755814fc242715c16f1dba58e1cbccb8032c2/jiter-0.14.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b3ddf5786bc7732d293bba3411ac637ecfa200a39983166d1df86a59a43c9f", size = 380225, upload-time = "2026-04-10T14:26:10.161Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5e/0ddeb7096aca099114abe36c4921016e8d251e6f35f5890240b31f1f60ae/jiter-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c001d5a646c2a50dc055dd526dad5d5245969e8234d2b1131d0451e81f3a373", size = 358682, upload-time = "2026-04-10T14:26:11.574Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/fe0c46cd7fda9cad8f1ff9ad217dc61f1e4280b21052ec6dfe88c1446ef2/jiter-0.14.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:834bb5bdabca2e91592a03d373838a8d0a1b8bbde7077ae6913fd2fc51812d00", size = 359973, upload-time = "2026-04-10T14:26:13.316Z" }, + { url = "https://files.pythonhosted.org/packages/ac/21/f5317f91729b501019184771c80d60abd89907009e7bfa6c7e348c5bdd44/jiter-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4e9178be60e229b1b2b0710f61b9e24d1f4f8556985a83ff4c4f95920eea7314", size = 397568, upload-time = "2026-04-10T14:26:15.212Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/79d8f33fb2bf168db0df5c9cd16fe440a8ada57e929d3677b22712c2568f/jiter-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7e4ccff04ec03614e62c613e976a3a5860dc9714ce8266f44328bdc8b1cab2c", size = 522535, upload-time = "2026-04-10T14:26:16.956Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/d1e3ff3d2a465e67f08507d74bafb2dcd29eba91dc939820e39e8dea38b8/jiter-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:69539d936fb5d55caf6ecd33e2e884de083ff0ea28579780d56c4403094bb8d9", size = 556709, upload-time = "2026-04-10T14:26:18.5Z" }, + { url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" }, + { url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" }, + { url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" }, + { url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" }, + { url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/c16db114ea1f2f532f198aa8dc39585026af45af362c69a0492f31bc4821/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:844e73b6c56b505e9e169234ea3bdea2ea43f769f847f47ac559ba1d2361ebea", size = 344816, upload-time = "2026-04-10T14:28:31.348Z" }, + { url = "https://files.pythonhosted.org/packages/99/8f/15e7741ff19e9bcd4d753f7ff22f988fd54592f134ca13701c13ea8c20e0/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52c076f187405fc21523c746c04399c9af8ece566077ed147b2126f2bcba577", size = 351445, upload-time = "2026-04-10T14:28:33.093Z" }, + { url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, +] + +[[package]] +name = "jmespath" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/56/3f325b1eef9791759784aa5046a8f6a1aff8f7c898a2e34506771d3b99d8/jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", size = 21607, upload-time = "2020-05-12T22:03:47.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/cb/5f001272b6faeb23c1c9e0acc04d48eaaf5c862c17709d20e3469c6e0139/jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f", size = 24489, upload-time = "2020-05-12T22:03:45.643Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "jsonschema-specifications", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "referencing", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "rpds-py", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "kaldifst" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/ad/3d2bbf5e91f05d78c385510d04d4749948856a2db3a01034d36904e2a6e9/kaldifst-1.8.0.tar.gz", hash = "sha256:80c8b819cb762ab6251578444e8b80379049a85a122ac74d37b2534faa45b63a", size = 154383, upload-time = "2026-04-09T13:14:36.102Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/f0/b4d00999db899ec2480aba1e122c2287befc1880a234ff6ed6b600aba4e5/kaldifst-1.8.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:15bd392fc1fc9f6fe4a4ea6a9bfe6de6cb3370cc0a037e1b7bddd3ffaf56879b", size = 2137735, upload-time = "2026-04-09T13:20:52.087Z" }, + { url = "https://files.pythonhosted.org/packages/68/d5/916e288ecdf4b24b148ee64d4b0bc29ec85d234d6793e0a7d7ca4fc78014/kaldifst-1.8.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b0dd41eaf512d1fc04d6a501d2b21b3288addf20eb5776cdd4c5103b94e9ebe1", size = 1131799, upload-time = "2026-04-09T13:27:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/a1/aa/262dea7f3227292df0ec108a31ccd4c5c9d47cade6f8d4e75167906fbeb4/kaldifst-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a233d587f89de95858ce3b23b39c12c4cdbba4d1d9a75398814b1d146dee08", size = 1015393, upload-time = "2026-04-09T13:20:23.202Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6d/76c6e153886cfae42f17206b65c367bdbd46723e449beb283819b049bcab/kaldifst-1.8.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b131922d7eac9dc1aebd38b072d61d2cb8068d02d955247e4e980829056f6c73", size = 1353357, upload-time = "2026-04-09T13:13:51.639Z" }, + { url = "https://files.pythonhosted.org/packages/c9/39/97c230b34c7a79e38d7d395cb686a6998b28553df7df81f51db4706ed61e/kaldifst-1.8.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da9fb560466cd6abde0300099194dfcf13f6383809c49c08f3d81df166d52661", size = 1461491, upload-time = "2026-04-09T13:14:43.087Z" }, + { url = "https://files.pythonhosted.org/packages/22/7f/83a9fbcd8f4378636b8169ef12757e1a014ec811f2f45f8cd94b3a228925/kaldifst-1.8.0-cp310-cp310-win32.whl", hash = "sha256:be8c18f15a7a7987c66ded61afaa44cb1015900b6f198f00c09bc367e79ce74c", size = 1005277, upload-time = "2026-04-09T13:15:17.189Z" }, + { url = "https://files.pythonhosted.org/packages/73/d5/e228103272986e7e755d02a4255b40e801327dc811704cf6de63be3d481b/kaldifst-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:ec9b5967d6d4089d45354fb5a13947b6da5bce9e5ca67338684650e8b4a2caa6", size = 1086453, upload-time = "2026-04-09T13:13:53.105Z" }, + { url = "https://files.pythonhosted.org/packages/72/53/775b58ff033951aadf5ee1cbe56dd8ea2681d437a79611bbc55b4a7881a2/kaldifst-1.8.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:8deeadd5d391663d6c6fdbbd4f7a1daf8c9392613b67fb6815f8bf14ba209298", size = 2142137, upload-time = "2026-04-09T13:30:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/8c/54/ae8afef4e9e84a79dfe516a9d3ae671746f0a7c7484d01bc6471a1d5dc60/kaldifst-1.8.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:04555763c04e1938dcbcbc0c60bd2f56db0a5bf29b2e7efa16d11c19aae14b06", size = 1133774, upload-time = "2026-04-09T13:01:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/00/3c/6e27e1dec2baa389171e3680baab9414f0d8005374ce700192fdafd023e6/kaldifst-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d04b56b00233afeab05eb781157840a444b7a56992c30a08fac8096f86be8389", size = 1017817, upload-time = "2026-04-09T13:19:28.516Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d4/8ffab37d491e1047041f7c2c936498a57a95b3a13a08ba3ee32f4c1722b3/kaldifst-1.8.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77dcbecd257cb0d6eb4920bd566af7ba4a380d493cbab8844fb5a51558d8de8a", size = 1360677, upload-time = "2026-04-09T13:13:35.467Z" }, + { url = "https://files.pythonhosted.org/packages/96/d3/e891ff8dbc1beaee04c0a081423595d6e276ee369342eb508f36a7e25767/kaldifst-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9d585ee47c37ebca2ad401bacef747675486f38a21d380aec94242e3a3b0a13", size = 1463829, upload-time = "2026-04-09T13:14:34.887Z" }, + { url = "https://files.pythonhosted.org/packages/85/36/e7e72868544fd04b9c216006401c5091d0e4d7b0db8844ae337b4b44e378/kaldifst-1.8.0-cp311-cp311-win32.whl", hash = "sha256:d8a4cfbb5ee09c0f6e515ac619b265a0b19c0676b5b78b84c54440bbf1892628", size = 1012695, upload-time = "2026-04-09T13:14:56.462Z" }, + { url = "https://files.pythonhosted.org/packages/f0/99/2001d401ecd2e48013b947ce3c99083ffa60d8273142a8f78c5511c2d668/kaldifst-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:8932f8bb9cd06491edb8d2bea0f454e67c34c27b2fb4a0ceb1110160d26a70d8", size = 1093455, upload-time = "2026-04-09T13:14:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/ec/1e/c1fddc62632014f10d891feb457929119845213418ab51d5353af8cff7df/kaldifst-1.8.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:4e7cec0eea5e72f87ad915c7a0762404331cf5f6f04517292fe50f8de4e98f39", size = 2151447, upload-time = "2026-04-09T13:12:29.577Z" }, + { url = "https://files.pythonhosted.org/packages/73/b3/6f1ac9dfad226fd940eca8e61d89c760fc315eaf18a3db6633c95988d48e/kaldifst-1.8.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:f86c756e09dddee70a0609b7f87aa749d965e2ca1acddbc1220fa0807c3d3e16", size = 1142310, upload-time = "2026-04-09T13:02:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/30/7f8f3b350dc8b2961da4b5bc5af9fd9478f6084173d5f13a0fe25e06686f/kaldifst-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6ff7845e27c58a71ff4841050fadbbab6c75444645d9e2bc9834f8dbae6c10e", size = 1019098, upload-time = "2026-04-09T13:41:15.94Z" }, + { url = "https://files.pythonhosted.org/packages/29/63/09324462016f14dac99f975c4e94cbe9ea90aa2795a9142e98424df931b5/kaldifst-1.8.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f4cc1ab32cdc68bad732154fec2bd69032a94ed498705b5423cd17499214ab0", size = 1357280, upload-time = "2026-04-09T13:13:29.875Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bd/dcf9d079cdf158360bdce69762b22d4fc3eccde89d6b08e180af7f42274c/kaldifst-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70e28ca1b1ed68ad61988d684754ff43ba6386d177ec24f8445bab316e10c3d0", size = 1466422, upload-time = "2026-04-09T13:14:56.638Z" }, + { url = "https://files.pythonhosted.org/packages/2b/1a/56b249c7cfdc80bd4e84b2076e06a68a76418cb27e277eb36a0be8bb3075/kaldifst-1.8.0-cp312-cp312-win32.whl", hash = "sha256:8512e2a9a175f7d8521b2956aa4e4dbed01026123bd6947f7f37c3c750832e04", size = 1013100, upload-time = "2026-04-09T13:15:09.981Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b2/6445e81706075c6ccaf0eba59df45fdf966f7e48e4e0c5f4585aa7458f02/kaldifst-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1d28cd0e25f9250b720b3fca9aa8f06b35fd09a7944bb0a1306c137702f9dd0", size = 1094573, upload-time = "2026-04-09T13:13:59.448Z" }, +] + +[[package]] +name = "kaldiio" +version = "2.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/85/92435e8e62eb3d43eded9f24643fc2a6dbce031cebceed11528147c7873f/kaldiio-2.18.1.tar.gz", hash = "sha256:0283d197fac6ac683f7a9e6af8d18aad9dbd2c4a997f22e45294f2ac1ee3c432", size = 35570, upload-time = "2025-03-06T15:57:52.375Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/e3/6c3b42233225f398f7a72988b524f654ae818cca0d441db847a2761203e9/kaldiio-2.18.1-py3-none-any.whl", hash = "sha256:397a4cd18977acaae7acabfba6807ee0a6978c620064381a266eac15b3c1a0a0", size = 29330, upload-time = "2025-03-06T15:57:50.82Z" }, +] + +[[package]] +name = "lark" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/60/bc7622aefb2aee1c0b4ba23c1446d3e30225c8770b38d7aedbfb65ca9d5a/lark-1.2.2.tar.gz", hash = "sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80", size = 252132, upload-time = "2024-08-13T19:49:00.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/00/d90b10b962b4277f5e64a78b6609968859ff86889f5b898c1a778c06ec00/lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c", size = 111036, upload-time = "2024-08-13T19:48:58.603Z" }, +] + +[[package]] +name = "lazy-loader" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294, upload-time = "2026-03-06T15:45:09.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl", hash = "sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005", size = 8044, upload-time = "2026-03-06T15:45:07.668Z" }, +] + +[[package]] +name = "librosa" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "audioread" }, + { name = "decorator" }, + { name = "joblib" }, + { name = "lazy-loader" }, + { name = "msgpack" }, + { name = "numba", version = "0.61.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numba", version = "0.65.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "numpy" }, + { name = "pooch" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy" }, + { name = "soundfile" }, + { name = "soxr" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/36/360b5aafa0238e29758729e9486c6ed92a6f37fa403b7875e06c115cdf4a/librosa-0.11.0.tar.gz", hash = "sha256:f5ed951ca189b375bbe2e33b2abd7e040ceeee302b9bbaeeffdfddb8d0ace908", size = 327001, upload-time = "2025-03-11T15:09:54.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/ba/c63c5786dfee4c3417094c4b00966e61e4a63efecee22cb7b4c0387dda83/librosa-0.11.0-py3-none-any.whl", hash = "sha256:0b6415c4fd68bff4c29288abe67c6d80b587e0e1e2cfb0aad23e4559504a7fa1", size = 260749, upload-time = "2025-03-11T15:09:52.982Z" }, +] + +[[package]] +name = "llguidance" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/48/3f7a9d3ff1b36bba92b5107a3a21286821227afe9ea464736133994d61fb/llguidance-1.3.0.tar.gz", hash = "sha256:861249afd51dc325646834462ea827e57a5c2b2042e108e6aae7059fdad9104d", size = 1070460, upload-time = "2025-10-20T19:58:44.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/11/44389d3d1526d7a5c38ffd587a5ebc61d7bee443ac1dea95f2089ad58f5f/llguidance-1.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f6caca5d78db7f76e1fbb0fff8607b861c32d47fa3d5dee2fc49de27ee269df", size = 2835242, upload-time = "2025-10-20T19:58:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/83/a8/1ff2bedb8f9acb46a2d2d603415d272bb622c142ea86f5b95445cc6e366c/llguidance-1.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc17e9dd602c3879bf91664a64bf72f54c74dbfbeb24ccfab6a5fe435b12f7aa", size = 3033133, upload-time = "2025-10-20T19:58:38.721Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", +] +sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880, upload-time = "2025-01-20T11:14:41.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/07/35e7c594b021ecb1938540f5bce543ddd8713cff97f71d81f021221edc1b/llvmlite-0.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:41e3839150db4330e1b2716c0be3b5c4672525b4c9005e17c7597f835f351ce2", size = 30332381, upload-time = "2025-01-20T11:12:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c6/258801143975a6d09a373f2641237992496e15567b907a4d401839d671b8/llvmlite-0.44.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955", size = 30331193, upload-time = "2025-01-20T11:13:26.976Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380, upload-time = "2025-01-20T11:14:02.442Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz", hash = "sha256:62031ce968ec74e95092184d4b0e857e444f8fdff0b8f9213707699570c33ccc", size = 193614, upload-time = "2026-03-31T18:29:53.497Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/f5/a1bde3aa8c43524b0acaf3f72fb3d80a32dd29dbb42d7dc434f84584cdcc/llvmlite-0.47.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41270b0b1310717f717cf6f2a9c68d3c43bd7905c33f003825aebc361d0d1b17", size = 37232772, upload-time = "2026-03-31T18:28:12.198Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fb/76d88fc05ee1f9c1a6efe39eb493c4a727e5d1690412469017cd23bcb776/llvmlite-0.47.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f9d118bc1dd7623e0e65ca9ac485ec6dd543c3b77bc9928ddc45ebd34e1e30a7", size = 56275179, upload-time = "2026-03-31T18:28:15.725Z" }, + { url = "https://files.pythonhosted.org/packages/4d/08/29da7f36217abd56a0c389ef9a18bea47960826e691ced1a36c92c6ce93c/llvmlite-0.47.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ea5cfb04a6ab5b18e46be72b41b015975ba5980c4ddb41f1975b83e19031063", size = 55128632, upload-time = "2026-03-31T18:28:19.946Z" }, + { url = "https://files.pythonhosted.org/packages/df/f8/5e12e9ed447d65f04acf6fcf2d79cded2355640b5131a46cee4c99a5949d/llvmlite-0.47.0-cp310-cp310-win_amd64.whl", hash = "sha256:166b896a2262a2039d5fc52df5ee1659bd1ccd081183df7a2fba1b74702dd5ea", size = 38138402, upload-time = "2026-03-31T18:28:23.327Z" }, + { url = "https://files.pythonhosted.org/packages/34/0b/b9d1911cfefa61399821dfb37f486d83e0f42630a8d12f7194270c417002/llvmlite-0.47.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74090f0dcfd6f24ebbef3f21f11e38111c4d7e6919b54c4416e1e357c3446b07", size = 37232770, upload-time = "2026-03-31T18:28:26.765Z" }, + { url = "https://files.pythonhosted.org/packages/46/27/5799b020e4cdfb25a7c951c06a96397c135efcdc21b78d853bbd9c814c7d/llvmlite-0.47.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca14f02e29134e837982497959a8e2193d6035235de1cb41a9cb2bd6da4eedbb", size = 56275177, upload-time = "2026-03-31T18:28:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/7e/51/48a53fedf01cb1f3f43ef200be17ebf83c8d9a04018d3783c1a226c342c2/llvmlite-0.47.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12a69d4bb05f402f30477e21eeabe81911e7c251cecb192bed82cd83c9db10d8", size = 55128631, upload-time = "2026-03-31T18:28:36.046Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/59227d06bdc96e23322713c381af4e77420949d8cd8a042c79e0043096cc/llvmlite-0.47.0-cp311-cp311-win_amd64.whl", hash = "sha256:c37d6eb7aaabfa83ab9c2ff5b5cdb95a5e6830403937b2c588b7490724e05327", size = 38138400, upload-time = "2026-03-31T18:28:40.076Z" }, + { url = "https://files.pythonhosted.org/packages/fa/48/4b7fe0e34c169fa2f12532916133e0b219d2823b540733651b34fdac509a/llvmlite-0.47.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:306a265f408c259067257a732c8e159284334018b4083a9e35f67d19792b164f", size = 37232769, upload-time = "2026-03-31T18:28:43.735Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4b/e3f2cd17822cf772a4a51a0a8080b0032e6d37b2dbe8cfb724eac4e31c52/llvmlite-0.47.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5853bf26160857c0c2573415ff4efe01c4c651e59e2c55c2a088740acfee51cd", size = 56275178, upload-time = "2026-03-31T18:28:48.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a3b4a543185305a9bdf3d9759d53646ed96e55e7dfd43f53e7a421b8fbae/llvmlite-0.47.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:003bcf7fa579e14db59c1a1e113f93ab8a06b56a4be31c7f08264d1d4072d077", size = 55128632, upload-time = "2026-03-31T18:28:52.901Z" }, + { url = "https://files.pythonhosted.org/packages/2f/f5/d281ae0f79378a5a91f308ea9fdb9f9cc068fddd09629edc0725a5a8fde1/llvmlite-0.47.0-cp312-cp312-win_amd64.whl", hash = "sha256:f3079f25bdc24cd9d27c4b2b5e68f5f60c4fdb7e8ad5ee2b9b006007558f9df7", size = 38138692, upload-time = "2026-03-31T18:28:57.147Z" }, +] + +[[package]] +name = "lm-format-enforcer" +version = "0.11.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "interegular", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "packaging", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pydantic", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pyyaml", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/d5/41cd417ba7dfdbbcfe46cebf81fb3dfd7c591b89897560ad05bb410a465d/lm_format_enforcer-0.11.3.tar.gz", hash = "sha256:e68081c108719cce284a9bcc889709b26ffb085a1945b5eba3a12cfa96d528da", size = 40258, upload-time = "2025-08-24T19:37:47.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/ef/11292bb0b85cf4c93447cab5a29f64576ed14d3ab4280e35ddd23486594a/lm_format_enforcer-0.11.3-py3-none-any.whl", hash = "sha256:cf586350875def1ae7a8fba84fcbbfc8371424b6c9d05c1fcba70aa233fbf06f", size = 45418, upload-time = "2025-08-24T19:37:46.325Z" }, +] + +[[package]] +name = "loguru" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/30/d87a423766b24db416a46e9335b9602b054a72b96a88a241f2b09b560fa8/loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac", size = 145103, upload-time = "2023-09-11T15:24:37.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/0a/4f6fed21aa246c6b49b561ca55facacc2a44b87d65b8b92362a8e99ba202/loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb", size = 62549, upload-time = "2023-09-11T15:24:35.016Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, +] + +[[package]] +name = "mcp" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "httpx", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "httpx-sse", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "jsonschema", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pydantic", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pydantic-settings", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pyjwt", extra = ["crypto"], marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "python-multipart", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "sse-starlette", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "starlette", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-inspection", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "uvicorn", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mistral-common" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "numpy", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pillow", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pydantic", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pydantic-extra-types", extra = ["pycountry"], marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "requests", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "tiktoken", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/97/753c85b5c0a19f4331ac99e0300ac8da06d4b29b629c9cb03064b38561bd/mistral_common-1.11.0.tar.gz", hash = "sha256:439b7fa38f9c3f020154af51bdf30eb81def507643017d8ce9f798384ec47ec3", size = 6355512, upload-time = "2026-04-01T13:54:12.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e4/73ad3c27e3fb613c3ce0953c928202c46cddebac3989b87be1b6f305a9f6/mistral_common-1.11.0-py3-none-any.whl", hash = "sha256:1d3ecaf7c3aa7338cb37b596fd0fb294485753958ee8e7254a6cc23eb30b249b", size = 6531513, upload-time = "2026-04-01T13:54:16.536Z" }, +] + +[package.optional-dependencies] +image = [ + { name = "opencv-python-headless", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] + +[[package]] +name = "ml-dtypes" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/79/7433f30ee04bd4faa303844048f55e1eb939131c8e5195a00a96a0939b64/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a", size = 5051883, upload-time = "2025-11-17T22:31:33.658Z" }, + { url = "https://files.pythonhosted.org/packages/10/b1/8938e8830b0ee2e167fc75a094dea766a1152bde46752cd9bfc57ee78a82/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270", size = 5030369, upload-time = "2025-11-17T22:31:35.595Z" }, + { url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, +] + +[[package]] +name = "model-hosting-container-standards" +version = "0.1.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastapi", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "httpx", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "jmespath", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pydantic", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "setuptools", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "starlette", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "supervisor", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/3d/cf5c6029648cb0a116f7b5c2f74aa155ab0c6dd723a1f204a6d7ff354526/model_hosting_container_standards-0.1.14.tar.gz", hash = "sha256:b6cf4c46d88ce6acd6e543a578bb88ffd55d1179a7c09c22e61ae1d8a567c564", size = 90386, upload-time = "2026-03-18T21:25:14.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/94/052452842d39c562237a70345c57ec213a9db22bd25bba998fd2b32d70a7/model_hosting_container_standards-0.1.14-py3-none-any.whl", hash = "sha256:d678be6745899b8ba1e8246c96b101e7802a6a4ea3fb5d90ae8d6eb4204e84c6", size = 121406, upload-time = "2026-03-18T21:25:12.932Z" }, +] + +[[package]] +name = "modelscope" +version = "1.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "requests" }, + { name = "setuptools" }, + { name = "tqdm" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/89/7a13bf70090e631f81a943797e875272d19d47e0a6984eda0a5a3f04e7e3/modelscope-1.34.0.tar.gz", hash = "sha256:c3041af301334aa9ca3f66f5b23e11ca33a2bdf28cc415dcceb75f68e4732aac", size = 4560273, upload-time = "2026-01-19T02:50:23.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/df/2c112a7c4160aa5e74dad87060019be5eca197d910af3f5b12e68ec090a9/modelscope-1.34.0-py3-none-any.whl", hash = "sha256:4629ace145972520b71b0ad02e4604282426c0cfae6a4b0922509898f3b269c8", size = 6050825, upload-time = "2026-01-19T02:50:20.018Z" }, +] + +[package.optional-dependencies] +framework = [ + { name = "addict" }, + { name = "attrs" }, + { name = "datasets" }, + { name = "einops" }, + { name = "pillow" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "scipy" }, + { name = "setuptools" }, + { name = "simplejson" }, + { name = "sortedcontainers" }, + { name = "transformers" }, + { name = "urllib3" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "msgpack" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" }, + { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" }, + { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" }, + { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" }, + { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, + { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, +] + +[[package]] +name = "msgspec" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/60/f79b9b013a16fa3a58350c9295ddc6789f2e335f36ea61ed10a21b215364/msgspec-0.21.1.tar.gz", hash = "sha256:2313508e394b0d208f8f56892ca9b2799e2561329de9763b19619595a6c0f72c", size = 319193, upload-time = "2026-04-12T21:44:50.394Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/95/7e591b4fa11fdbbf9891164473c23420a8c781ef553295abe416bf335f42/msgspec-0.21.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd677e3001fdfed9186de72eab434da2976303cd5eb9550921d3d0c3e3e168ce", size = 216565, upload-time = "2026-04-12T21:43:48.081Z" }, + { url = "https://files.pythonhosted.org/packages/19/86/714feeaf3b84cf2027235681725593840153dedd2868578f9f2715e296bb/msgspec-0.21.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f667b90b37fad734a91671abd68e0d7f4d066862771b87e91c53996dcb7a9027", size = 222689, upload-time = "2026-04-12T21:43:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b9/4384243e814f2579e5205e17d170b9c1a30121afd1393298d904817a7fa7/msgspec-0.21.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:49880fd20fdbcfe1b793f07dd83f12572bab679c9800352c8b2240289aa46a06", size = 222343, upload-time = "2026-04-12T21:43:50.612Z" }, + { url = "https://files.pythonhosted.org/packages/04/01/4b227d9c4057346271043632bad41979cf8c3dca372e41bb1f7d546395b2/msgspec-0.21.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae0162e22849a5e91eaad907766525107523b0daea3df267a9fcb5ba4e0936ae", size = 225607, upload-time = "2026-04-12T21:43:52.129Z" }, + { url = "https://files.pythonhosted.org/packages/fa/54/d24ddeaa65b5278c9e67f48ce3c17a9831e8f3722f3c8322ee120aca22ef/msgspec-0.21.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3124010b3815451494c85ff345e693cb9fe5889cfcbbef39ed8622e0e72319c", size = 215158, upload-time = "2026-04-12T21:43:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/9f/75/bb79c8b89a93ae23cd33c0d802373f16feaf9633f05d8af77091350dda0a/msgspec-0.21.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6badc03b9725352219cca017bfe71c61f2fbd0fb5982b410ac17c97c213deb30", size = 219856, upload-time = "2026-04-12T21:44:00.015Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/c5ca26b46f0ebbd3a6683695ef89396712cb9e4199fd1f0bc1dd968216b1/msgspec-0.21.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5d2d4116ebe3035a78d9ec76e99a9d64e5fa6d44fe61a9c5de7fd1acf54bcc69", size = 220314, upload-time = "2026-04-12T21:44:01.548Z" }, + { url = "https://files.pythonhosted.org/packages/c8/31/645a351c4285dce40ed6755c3dcc0aa648e26dacb20a98018fe2cce5e87b/msgspec-0.21.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0d1009f6715f5bff3b54d4ff5c7428ad96197e0534e1645b8e9b955890c84664", size = 223215, upload-time = "2026-04-12T21:44:02.884Z" }, + { url = "https://files.pythonhosted.org/packages/8a/37/655101799590bcc5fddb2bd3fe0e6194e816c2d1da7c361725f5eb89a910/msgspec-0.21.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:846758412e9518252b2ac9bffd6f0e54d9ff614f5f9488df7749f81ff5c80920", size = 218871, upload-time = "2026-04-12T21:44:09.917Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d1/d4cd9fe89c7d400d7a18f86ccc94daa3f0927f53558846fcb60791dce5d6/msgspec-0.21.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21995e74b5c598c2e004110ad66ec7f1b8c20bf2bcf3b2de8fd9a3094422d3ff", size = 225025, upload-time = "2026-04-12T21:44:11.191Z" }, + { url = "https://files.pythonhosted.org/packages/24/bf/e20549e602b9edccadeeff98760345a416f9cce846a657e8b18e3396b212/msgspec-0.21.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6129f0cca52992e898fd5344187f7c8127b63d810b2fd73e36fca73b4c6475ee", size = 222672, upload-time = "2026-04-12T21:44:12.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/68/04d7a8f0f786545cf9b8c280c57aa6befb5977af6e884b8b54191cbe44b3/msgspec-0.21.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ef3ec2296248d1f8b9231acb051b6d471dfde8f21819e86c9adaaa9f42918521", size = 227303, upload-time = "2026-04-12T21:44:13.709Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, + { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, + { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, + { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, + { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "multiprocess" +version = "0.70.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/ae/04f39c5d0d0def03247c2893d6f2b83c136bf3320a2154d7b8858f2ba72d/multiprocess-0.70.16.tar.gz", hash = "sha256:161af703d4652a0e1410be6abccecde4a7ddffd19341be0a7011b94aeb171ac1", size = 1772603, upload-time = "2024-01-28T18:52:34.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/76/6e712a2623d146d314f17598df5de7224c85c0060ef63fd95cc15a25b3fa/multiprocess-0.70.16-pp310-pypy310_pp73-macosx_10_13_x86_64.whl", hash = "sha256:476887be10e2f59ff183c006af746cb6f1fd0eadcfd4ef49e605cbe2659920ee", size = 134980, upload-time = "2024-01-28T18:52:15.731Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ab/1e6e8009e380e22254ff539ebe117861e5bdb3bff1fc977920972237c6c7/multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec", size = 134982, upload-time = "2024-01-28T18:52:17.783Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f7/7ec7fddc92e50714ea3745631f79bd9c96424cb2702632521028e57d3a36/multiprocess-0.70.16-py310-none-any.whl", hash = "sha256:c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02", size = 134824, upload-time = "2024-01-28T18:52:26.062Z" }, + { url = "https://files.pythonhosted.org/packages/50/15/b56e50e8debaf439f44befec5b2af11db85f6e0f344c3113ae0be0593a91/multiprocess-0.70.16-py311-none-any.whl", hash = "sha256:af4cabb0dac72abfb1e794fa7855c325fd2b55a10a44628a3c1ad3311c04127a", size = 143519, upload-time = "2024-01-28T18:52:28.115Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7d/a988f258104dcd2ccf1ed40fdc97e26c4ac351eeaf81d76e266c52d84e2f/multiprocess-0.70.16-py312-none-any.whl", hash = "sha256:fc0544c531920dde3b00c29863377f87e1632601092ea2daca74e4beb40faa2e", size = 146741, upload-time = "2024-01-28T18:52:29.395Z" }, + { url = "https://files.pythonhosted.org/packages/ea/89/38df130f2c799090c978b366cfdf5b96d08de5b29a4a293df7f7429fa50b/multiprocess-0.70.16-py38-none-any.whl", hash = "sha256:a71d82033454891091a226dfc319d0cfa8019a4e888ef9ca910372a446de4435", size = 132628, upload-time = "2024-01-28T18:52:30.853Z" }, + { url = "https://files.pythonhosted.org/packages/da/d9/f7f9379981e39b8c2511c9e0326d212accacb82f12fbfdc1aa2ce2a7b2b6/multiprocess-0.70.16-py39-none-any.whl", hash = "sha256:a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3", size = 133351, upload-time = "2024-01-28T18:52:31.981Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "ninja" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/73/79a0b22fc731989c708068427579e840a6cf4e937fe7ae5c5d0b7356ac22/ninja-1.13.0.tar.gz", hash = "sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978", size = 242558, upload-time = "2025-08-11T15:10:19.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/de/6e1cd6b84b412ac1ef327b76f0641aeb5dcc01e9d3f9eee0286d0c34fd93/ninja-1.13.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630", size = 177467, upload-time = "2025-08-11T15:09:52.767Z" }, + { url = "https://files.pythonhosted.org/packages/c8/83/49320fb6e58ae3c079381e333575fdbcf1cca3506ee160a2dcce775046fa/ninja-1.13.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c", size = 187834, upload-time = "2025-08-11T15:09:54.115Z" }, + { url = "https://files.pythonhosted.org/packages/56/c7/ba22748fb59f7f896b609cd3e568d28a0a367a6d953c24c461fe04fc4433/ninja-1.13.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e", size = 202736, upload-time = "2025-08-11T15:09:55.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/22/d1de07632b78ac8e6b785f41fa9aad7a978ec8c0a1bf15772def36d77aac/ninja-1.13.0-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1c97223cdda0417f414bf864cfb73b72d8777e57ebb279c5f6de368de0062988", size = 179034, upload-time = "2025-08-11T15:09:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/ed/de/0e6edf44d6a04dabd0318a519125ed0415ce437ad5a1ec9b9be03d9048cf/ninja-1.13.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa", size = 180716, upload-time = "2025-08-11T15:09:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/938b562f9057aaa4d6bfbeaa05e81899a47aebb3ba6751e36c027a7f5ff7/ninja-1.13.0-py3-none-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4be9c1b082d244b1ad7ef41eb8ab088aae8c109a9f3f0b3e56a252d3e00f42c1", size = 146843, upload-time = "2025-08-11T15:10:00.046Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fb/d06a3838de4f8ab866e44ee52a797b5491df823901c54943b2adb0389fbb/ninja-1.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6739d3352073341ad284246f81339a384eec091d9851a886dfa5b00a6d48b3e2", size = 154402, upload-time = "2025-08-11T15:10:01.657Z" }, + { url = "https://files.pythonhosted.org/packages/31/bf/0d7808af695ceddc763cf251b84a9892cd7f51622dc8b4c89d5012779f06/ninja-1.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11be2d22027bde06f14c343f01d31446747dbb51e72d00decca2eb99be911e2f", size = 552388, upload-time = "2025-08-11T15:10:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c99d0c2c809f992752453cce312848abb3b1607e56d4cd1b6cded317351a/ninja-1.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aa45b4037b313c2f698bc13306239b8b93b4680eb47e287773156ac9e9304714", size = 472501, upload-time = "2025-08-11T15:10:04.735Z" }, + { url = "https://files.pythonhosted.org/packages/9f/43/c217b1153f0e499652f5e0766da8523ce3480f0a951039c7af115e224d55/ninja-1.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f8e1e8a1a30835eeb51db05cf5a67151ad37542f5a4af2a438e9490915e5b72", size = 638280, upload-time = "2025-08-11T15:10:06.512Z" }, + { url = "https://files.pythonhosted.org/packages/8c/45/9151bba2c8d0ae2b6260f71696330590de5850e5574b7b5694dce6023e20/ninja-1.13.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:3d7d7779d12cb20c6d054c61b702139fd23a7a964ec8f2c823f1ab1b084150db", size = 642420, upload-time = "2025-08-11T15:10:08.35Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/95752eb635bb8ad27d101d71bef15bc63049de23f299e312878fc21cb2da/ninja-1.13.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:d741a5e6754e0bda767e3274a0f0deeef4807f1fec6c0d7921a0244018926ae5", size = 585106, upload-time = "2025-08-11T15:10:09.818Z" }, + { url = "https://files.pythonhosted.org/packages/c1/31/aa56a1a286703800c0cbe39fb4e82811c277772dc8cd084f442dd8e2938a/ninja-1.13.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:e8bad11f8a00b64137e9b315b137d8bb6cbf3086fbdc43bf1f90fd33324d2e96", size = 707138, upload-time = "2025-08-11T15:10:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/34/6f/5f5a54a1041af945130abdb2b8529cbef0cdcbbf9bcf3f4195378319d29a/ninja-1.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b4f2a072db3c0f944c32793e91532d8948d20d9ab83da9c0c7c15b5768072200", size = 581758, upload-time = "2025-08-11T15:10:13.295Z" }, +] + +[[package]] +name = "numba" +version = "0.61.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", +] +dependencies = [ + { name = "llvmlite", version = "0.44.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615, upload-time = "2025-04-09T02:58:07.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/c6/c2fb11e50482cb310afae87a997707f6c7d8a48967b9696271347441f650/numba-0.61.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae45830b129c6137294093b269ef0a22998ccc27bf7cf096ab8dcf7bca8946f9", size = 2831612, upload-time = "2025-04-09T02:57:41.559Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a4/2b309a6a9f6d4d8cfba583401c7c2f9ff887adb5d54d8e2e130274c0973f/numba-0.61.2-cp311-cp311-win_amd64.whl", hash = "sha256:76bcec9f46259cedf888041b9886e257ae101c6268261b19fda8cfbc52bec9d1", size = 2831505, upload-time = "2025-04-09T02:57:50.108Z" }, + { url = "https://files.pythonhosted.org/packages/68/1d/ddb3e704c5a8fb90142bf9dc195c27db02a08a99f037395503bfbc1d14b3/numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18", size = 2831929, upload-time = "2025-04-09T02:57:58.45Z" }, +] + +[[package]] +name = "numba" +version = "0.65.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "llvmlite", version = "0.47.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "numpy", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/61/7299643b9c18d669e04be7c5bcb64d985070d07553274817b45b049e7bfe/numba-0.65.0.tar.gz", hash = "sha256:edad0d9f6682e93624c00125a471ae4df186175d71fd604c983c377cdc03e68b", size = 2764131, upload-time = "2026-04-01T03:52:01.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/9b/e8453d93d5cb3f53cc956f135024be09d52f4f99643acaf8fdca090a8f3c/numba-0.65.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:dff9fd5fbc9a35c517359c5823ea705d9b65f01fb46e42e35a2eabe5a52c2e96", size = 2680537, upload-time = "2026-04-01T03:51:17.325Z" }, + { url = "https://files.pythonhosted.org/packages/07/95/d6a2f0625e1092624228301eea11cdaff21ddcaf917ef3d631846a38b2f4/numba-0.65.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4c894c94afa5ffd627c7e3b693df10cb0d905bd5eb06de3dfc31775140cf4f89", size = 3739444, upload-time = "2026-04-01T03:51:19.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/fe518c97af035e4ec670c2edc3f0ff7a518cbed2f0b5053124d7c979bd8a/numba-0.65.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7325b1aab88f0339057288ee32f39dc660e14f93872a6fda14fa6eb9f95b047", size = 3446390, upload-time = "2026-04-01T03:51:21.55Z" }, + { url = "https://files.pythonhosted.org/packages/d0/06/5010939854249c290c6217e3fb7404914f4ed953f9923e340c3e166bcaf0/numba-0.65.0-cp310-cp310-win_amd64.whl", hash = "sha256:71e72e9ca2f619df4768f9c3962bfec60191a5a26fe2b6a8c6a07532b6146169", size = 2747200, upload-time = "2026-04-01T03:51:23.674Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ce/d67c499703eb5479ce02420e8ccd65c5753d87d2e16d563f152d71405346/numba-0.65.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:28e547d0b18024f19cbaf9de02fc5c145790213d9be8a2c95b43f93ec162b9e4", size = 2680228, upload-time = "2026-04-01T03:51:25.401Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a7/11e2b24251d57cf41fc9ad83f378d890d61a890e3f8eb6338b39833f67a4/numba-0.65.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:032b0b8e879512cd424d79eed6d772a1399c6387ded184c2cf3cc22c08d750a6", size = 3744674, upload-time = "2026-04-01T03:51:27.311Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0b/7c63eb742859a6243f42288441f65ac9dac96ea59f409e43b713aafbe867/numba-0.65.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af143d823624033a128b5950c0aaf9ffc2386dfe954eb757119cf0432335534c", size = 3450620, upload-time = "2026-04-01T03:51:29.092Z" }, + { url = "https://files.pythonhosted.org/packages/53/ff/1371cbbe955be340a46093a10b61462437e0fadc7a63290473a0e584cb03/numba-0.65.0-cp311-cp311-win_amd64.whl", hash = "sha256:15d159578e59a39df246b83480f78d7794b0fca40153b5684d3849a99c48a0fb", size = 2747081, upload-time = "2026-04-01T03:51:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2f/8bd31a1ea43c01ac215283d83aa5f8d5acbe7a36c85b82f1757bfe9ccb31/numba-0.65.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:b27ee4847e1bfb17e9604d100417ee7c1d10f15a6711c6213404b3da13a0b2aa", size = 2680705, upload-time = "2026-04-01T03:51:32.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/36/88406bd58600cc696417b8e5dd6a056478da808f3eaf48d18e2421e0c2d9/numba-0.65.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a52d92ffd297c10364bce60cd1fcb88f99284ab5df085f2c6bcd1cb33b529a6f", size = 3801411, upload-time = "2026-04-01T03:51:34.321Z" }, + { url = "https://files.pythonhosted.org/packages/0c/61/ce753a1d7646dd477e16d15e89473703faebb8995d2f71d7ad69a540b565/numba-0.65.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da8e371e328c06d0010c3d8b44b21858652831b85bcfba78cb22c042e22dbd8e", size = 3501622, upload-time = "2026-04-01T03:51:36.348Z" }, + { url = "https://files.pythonhosted.org/packages/7d/86/db87a5393f1b1fabef53ac3ba4e6b938bb27e40a04ad7cc512098fcae032/numba-0.65.0-cp312-cp312-win_amd64.whl", hash = "sha256:59bb9f2bb9f1238dfd8e927ba50645c18ae769fef4f3d58ea0ea22a2683b91f5", size = 2749979, upload-time = "2026-04-01T03:51:37.88Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +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/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 = "nvidia-cublas" +version = "13.1.0.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226, upload-time = "2025-10-09T08:59:04.818Z" }, + { url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236, upload-time = "2025-10-09T08:59:32.536Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.19.0.56" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/0b4b932655d17a6da1b92fa92ab12844b053bb2ac2475e179ba6f043da1e/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf", size = 366066321, upload-time = "2026-02-03T20:44:52.837Z" }, +] + +[[package]] +name = "nvidia-cudnn-frontend" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/be/f5a1e633c524c13c0182213ab27dab42dca29a3c785be5ff74d2d185aed1/nvidia_cudnn_frontend-1.18.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:baa6fbc8e7c55f1c78c0374ed9a890e1cf81acaca0c92d6135d18a8e3c985244", size = 2023500, upload-time = "2026-01-27T23:31:34.747Z" }, + { url = "https://files.pythonhosted.org/packages/82/a7/765a17c6a9496196c34f269d17dfb902b6c618c0261c0962511e95302e81/nvidia_cudnn_frontend-1.18.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4bcca42259e358002c8867e3624a558f66cd5dff2cc6c3aafd860ef2f41730", size = 2154278, upload-time = "2026-01-27T23:06:55.784Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9a/83d3d080118de4a7810fa019349edec634b8b37b9cafaacd05719de62dd6/nvidia_cudnn_frontend-1.18.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6d4d0b88d617b233a503c84980b54d840b60b2734497d1a7a071ec5293daec2", size = 2023709, upload-time = "2026-01-27T23:32:10.912Z" }, + { url = "https://files.pythonhosted.org/packages/13/c7/c3624b3ed77b102618f26295e816b27f1c3ebb1143730237a9f51d403c3f/nvidia_cudnn_frontend-1.18.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:382ea063b92cbfd5b442cb75ff8422932d78276aecf139e46713ed1ad3d07af4", size = 2155568, upload-time = "2026-01-27T23:07:13.277Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b4/604e230378680ee117849a4e1045baca092f93161a829291a84d5acce70c/nvidia_cudnn_frontend-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:310b417f2848a83d1437203fcaeea320a74fb7f28af20bf42bf5afc9c01f1c12", size = 2027408, upload-time = "2026-01-27T23:32:46.576Z" }, + { url = "https://files.pythonhosted.org/packages/c6/52/08f98262e77b1cbcc834cc1a5db494d0661ea1dbdea58c2e2d51a57fdaca/nvidia_cudnn_frontend-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c023539ca6de99234cf5102c3ec0d6af817f5396fc93028a22ba5b834a35b8a", size = 2159245, upload-time = "2026-01-27T23:07:32.664Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-cusparse", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277, upload-time = "2025-08-13T19:22:40.982Z" }, + { url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119, upload-time = "2025-08-13T19:23:41.967Z" }, +] + +[[package]] +name = "nvidia-cutlass-dsl" +version = "4.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cutlass-dsl-libs-base", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/03/678dab0383db1ddfc449da216220f40404189eb36eeed9d87a4fa4bdb0e6/nvidia_cutlass_dsl-4.4.2-py3-none-any.whl", hash = "sha256:7cfb9ef19062b055b9372c7a627004724e2755e4c8b16c3cc88807d64501a4ae", size = 10167, upload-time = "2026-03-16T02:18:59.043Z" }, +] + +[[package]] +name = "nvidia-cutlass-dsl-libs-base" +version = "4.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-python", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "numpy", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/07/af1b456b5b6dd4a49e71a952a182a99fc863f70b9f78725324f89e0384e5/nvidia_cutlass_dsl_libs_base-4.4.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:06acb3acff3dcf4bf6630476efac7de94de30b988ded4fa00b647bbcec4224ff", size = 75471025, upload-time = "2026-03-16T02:23:49.61Z" }, + { url = "https://files.pythonhosted.org/packages/b1/12/f0770811d2874af7e04623d3baa83c445c49f38c00c4e5d20e1daae54b5d/nvidia_cutlass_dsl_libs_base-4.4.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:916bf612fba5fbc5162e300fe18196e960dac2328c1c1360c0939d3be05c7c71", size = 74355272, upload-time = "2026-03-16T02:24:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/60/bf/b9d0fd1ba281b111c941d9616dd9f98a509d84bf35076e60fef27ec7abd6/nvidia_cutlass_dsl_libs_base-4.4.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:261832dafe7579dc83cd3816ab9ea845e3de3737d876c215f01fb4edff1f4473", size = 75476977, upload-time = "2026-03-16T02:26:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/86dda6d69a3fc29d0cde2a8b54c056ad69b73a6e5e230e18d906d2ec3b7c/nvidia_cutlass_dsl_libs_base-4.4.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:40c2352b2fcc80789a216cbeb9b2ee10c85c15de839cda8f5c1d18166b8249df", size = 74356100, upload-time = "2026-03-16T02:26:12.778Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7d/0df5e38d11e52cc72095a14d6448bc1c5d0d4b00b069a1189ca417fb225b/nvidia_cutlass_dsl_libs_base-4.4.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2ec8812eeadcbb6fe20bda2e295ed9c00653f8253b78e33cf0ab65a47b829e73", size = 75473821, upload-time = "2026-03-16T02:27:08.371Z" }, + { url = "https://files.pythonhosted.org/packages/56/98/e264964741d9cc9816625d9600d17a5249fd5cbd8c2d166fb0d0c34dfe5a/nvidia_cutlass_dsl_libs_base-4.4.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:22e37b58f7a6f2f43bba533c4df8a088012122e0b4e9a632eca23937adeafb39", size = 74355593, upload-time = "2026-03-16T02:25:11.762Z" }, +] + +[[package]] +name = "nvidia-ml-py" +version = "13.595.45" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/49/c29f6e30d8662d2e94fef17739ea7309cc76aba269922ae999e4cc07f268/nvidia_ml_py-13.595.45.tar.gz", hash = "sha256:c9f34897fe0441ff35bc8f35baf80f830a20b0f4e6ce71e0a325bc0e66acf079", size = 50780, upload-time = "2026-03-19T16:59:44.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/24/fc256107d23597fa33d319505ce77160fa1a2349c096d01901ffc7cb7fc4/nvidia_ml_py-13.595.45-py3-none-any.whl", hash = "sha256:b65a7977f503d56154b14d683710125ef93594adb63fbf7e559336e3318f1376", size = 51776, upload-time = "2026-03-19T16:59:43.603Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.28.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/55/1920646a2e43ffd4fc958536b276197ed740e9e0c54105b4bb3521591fc7/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643", size = 196561677, upload-time = "2025-11-18T05:49:03.45Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b4/878fefaad5b2bcc6fcf8d474a25e3e3774bc5133e4b58adff4d0bca238bc/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42", size = 196493177, upload-time = "2025-11-18T05:49:17.677Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "omegaconf" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, +] + +[[package]] +name = "openai" +version = "2.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "distro", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "httpx", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "jiter", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pydantic", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "sniffio", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "tqdm", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/59/bdcc6b759b8c42dd73afaf5bf8f902c04b37987a5514dbc1c64dba390fef/openai-2.32.0.tar.gz", hash = "sha256:c54b27a9e4cb8d51f0dd94972ffd1a04437efeb259a9e60d8922b8bd26fe55e0", size = 693286, upload-time = "2026-04-15T22:28:19.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/c1/d6e64ccd0536bf616556f0cad2b6d94a8125f508d25cfd814b1d2db4e2f1/openai-2.32.0-py3-none-any.whl", hash = "sha256:4dcc9badeb4bf54ad0d187453742f290226d30150890b7890711bda4f32f192f", size = 1162570, upload-time = "2026-04-15T22:28:17.714Z" }, +] + +[[package]] +name = "openai-harmony" +version = "0.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/92/2d038d096f29179c7c9571b431f9e739f87a487121901725e23fe338dd9d/openai_harmony-0.0.8.tar.gz", hash = "sha256:6e43f98e6c242fa2de6f8ea12eab24af63fa2ed3e89c06341fb9d92632c5cbdf", size = 284777, upload-time = "2025-11-05T19:07:06.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/d2/ce6953ca87db9cae3e775024184da7d1c5cb88cead19a2d75b42f00a959c/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4f709815924ec325b9a890e6ab2bbb0ceec8e319a4e257328eb752cf36b2efc", size = 2948463, upload-time = "2025-11-05T19:06:48.17Z" }, + { url = "https://files.pythonhosted.org/packages/fa/4c/b553c9651662d6ce102ca7f3629d268b23df1abe5841e24bed81e8a8e949/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5cfcfd963b50a41fc656c84d3440ca6eecdccd6c552158ce790b8f2e33dfb5a9", size = 2704083, upload-time = "2025-11-05T19:06:50.205Z" }, + { url = "https://files.pythonhosted.org/packages/9b/af/4eec8f9ab9c27bcdb444460c72cf43011d176fc44c79d6e113094ca1e152/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a3a16972aa1cee38ea958470cd04ac9a2d5ac38fdcf77ab686611246220c158", size = 2959765, upload-time = "2025-11-05T19:06:53.62Z" }, + { url = "https://files.pythonhosted.org/packages/11/3c/33f3374e4624e0e776f6b13b73c45a7ead7f9c4529f8369ed5bfcaa30cac/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4d5cfa168e74d08f8ba6d58a7e49bc7daef4d58951ec69b66b0d56f4927a68d", size = 3427031, upload-time = "2025-11-05T19:06:51.829Z" }, + { url = "https://files.pythonhosted.org/packages/25/3f/1a192b93bb47c6b44cd98ba8cc1d3d2a9308f1bb700c3017e6352da11bda/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c007d277218a50db8839e599ed78e0fffe5130f614c3f6d93ae257f282071a29", size = 2953260, upload-time = "2025-11-05T19:06:55.406Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/93b582cad3531797c3db7c2db5400fd841538ccddfd9f5e3df61be99a630/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8565d4f5a0638da1bffde29832ed63c9e695c558611053add3b2dc0b56c92dbc", size = 3127044, upload-time = "2025-11-05T19:06:59.553Z" }, + { url = "https://files.pythonhosted.org/packages/1d/10/4327dbf87f75ae813405fd9a9b4a5cde63d506ffed0a096a440a4cabd89c/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:cbaa3bda75ef0d8836e1f8cc84af62f971b1d756d740efc95c38c3e04c0bfde2", size = 2932931, upload-time = "2025-11-05T19:07:01.437Z" }, + { url = "https://files.pythonhosted.org/packages/8a/c8/1774eec4f6f360ef57618fb8f52e3d3af245b2491bd0297513aa09eec04b/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:772922a9bd24e133950fad71eb1550836f415a88e8c77870e12d0c3bd688ddc2", size = 2996140, upload-time = "2025-11-05T19:07:03.438Z" }, + { url = "https://files.pythonhosted.org/packages/60/c3/3d1e01e2dba517a91760e4a03e4f20ffc75039a6fe584d0e6f9b5c78fd15/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:007b0476a1f331f8130783f901f1da6f5a7057af1a4891f1b6a31dec364189b5", size = 3205080, upload-time = "2025-11-05T19:07:05.078Z" }, +] + +[[package]] +name = "opencv-python-headless" +version = "4.13.0.92" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/76/9417a6aef9def70e467a5bf560579f816148a4c658b7d525581b356eda9e/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb", size = 33703709, upload-time = "2026-02-05T10:24:46.469Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/bd17ff5772938267fd49716e94ca24f616ff4cb1ff4c6be13085108037be/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22", size = 56016764, upload-time = "2026-02-05T10:26:48.904Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b4/b7bcbf7c874665825a8c8e1097e93ea25d1f1d210a3e20d4451d01da30aa/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d", size = 35010236, upload-time = "2026-02-05T10:28:11.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/33/b5db29a6c00eb8f50708110d8d453747ca125c8b805bc437b289dbdcc057/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0bd48544f77c68b2941392fcdf9bcd2b9cdf00e98cb8c29b2455d194763cf99e", size = 60391106, upload-time = "2026-02-05T10:30:14.236Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/8e/3778a7e87801d994869a9396b9fc2a289e5f9be91ff54a27d41eace494b0/opentelemetry_api-1.41.0.tar.gz", hash = "sha256:9421d911326ec12dee8bc933f7839090cad7a3f13fcfb0f9e82f8174dc003c09", size = 71416, upload-time = "2026-04-09T14:38:34.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/ee/99ab786653b3bda9c37ade7e24a7b607a1b1f696063172768417539d876d/opentelemetry_api-1.41.0-py3-none-any.whl", hash = "sha256:0e77c806e6a89c9e4f8d372034622f3e1418a11bdbe1c80a50b3d3397ad0fa4f", size = 69007, upload-time = "2026-04-09T14:38:11.833Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp" +version = "1.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-exporter-otlp-proto-grpc", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "opentelemetry-exporter-otlp-proto-http", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/b7/845565a2ab5d22c1486bc7729a06b05cd0964c61539d766e1f107c9eea0c/opentelemetry_exporter_otlp-1.41.0.tar.gz", hash = "sha256:97ff847321f8d4c919032a67d20d3137fb7b34eac0c47f13f71112858927fc5b", size = 6152, upload-time = "2026-04-09T14:38:35.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f2/f1076fff152858773f22cda146713f9ae3661795af6bacd411a76f2151ac/opentelemetry_exporter_otlp-1.41.0-py3-none-any.whl", hash = "sha256:443b6a45c990ae4c55e147f97049a86c5f5b704f3d78b48b44a073a886ec4d6e", size = 7022, upload-time = "2026-04-09T14:38:13.934Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/28/e8eca94966fe9a1465f6094dc5ddc5398473682180279c94020bc23b4906/opentelemetry_exporter_otlp_proto_common-1.41.0.tar.gz", hash = "sha256:966bbce537e9edb166154779a7c4f8ab6b8654a03a28024aeaf1a3eacb07d6ee", size = 20411, upload-time = "2026-04-09T14:38:36.572Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/c4/78b9bf2d9c1d5e494f44932988d9d91c51a66b9a7b48adf99b62f7c65318/opentelemetry_exporter_otlp_proto_common-1.41.0-py3-none-any.whl", hash = "sha256:7a99177bf61f85f4f9ed2072f54d676364719c066f6d11f515acc6c745c7acf0", size = 18366, upload-time = "2026-04-09T14:38:15.135Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "grpcio", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "opentelemetry-api", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "opentelemetry-exporter-otlp-proto-common", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "opentelemetry-proto", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "opentelemetry-sdk", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/46/d75a3f8c91915f2e58f61d0a2e4ada63891e7c7a37a20ff7949ba184a6b2/opentelemetry_exporter_otlp_proto_grpc-1.41.0.tar.gz", hash = "sha256:f704201251c6f65772b11bddea1c948000554459101bdbb0116e0a01b70592f6", size = 25754, upload-time = "2026-04-09T14:38:37.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/f6/b09e2e0c9f0b5750cebc6eaf31527b910821453cef40a5a0fe93550422b2/opentelemetry_exporter_otlp_proto_grpc-1.41.0-py3-none-any.whl", hash = "sha256:3a1a86bd24806ccf136ec9737dbfa4c09b069f9130ff66b0acb014f9c5255fd1", size = 20299, upload-time = "2026-04-09T14:38:17.01Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "opentelemetry-api", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "opentelemetry-exporter-otlp-proto-common", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "opentelemetry-proto", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "opentelemetry-sdk", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "requests", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/63/d9f43cd75f3fabb7e01148c89cfa9491fc18f6580a6764c554ff7c953c46/opentelemetry_exporter_otlp_proto_http-1.41.0.tar.gz", hash = "sha256:dcd6e0686f56277db4eecbadd5262124e8f2cc739cadbc3fae3d08a12c976cf5", size = 24139, upload-time = "2026-04-09T14:38:38.128Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b5/a214cd907eedc17699d1c2d602288ae17cb775526df04db3a3b3585329d2/opentelemetry_exporter_otlp_proto_http-1.41.0-py3-none-any.whl", hash = "sha256:a9c4ee69cce9c3f4d7ee736ad1b44e3c9654002c0816900abbafd9f3cf289751", size = 22673, upload-time = "2026-04-09T14:38:18.349Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/d9/08e3dc6156878713e8c811682bc76151f5fe1a3cb7f3abda3966fd56e71e/opentelemetry_proto-1.41.0.tar.gz", hash = "sha256:95d2e576f9fb1800473a3e4cfcca054295d06bdb869fda4dc9f4f779dc68f7b6", size = 45669, upload-time = "2026-04-09T14:38:45.978Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/8c/65ef7a9383a363864772022e822b5d5c6988e6f9dabeebb9278f5b86ebc3/opentelemetry_proto-1.41.0-py3-none-any.whl", hash = "sha256:b970ab537309f9eed296be482c3e7cca05d8aca8165346e929f658dbe153b247", size = 72074, upload-time = "2026-04-09T14:38:29.38Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "opentelemetry-semantic-conventions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/0e/a586df1186f9f56b5a0879d52653effc40357b8e88fc50fe300038c3c08b/opentelemetry_sdk-1.41.0.tar.gz", hash = "sha256:7bddf3961131b318fc2d158947971a8e37e38b1cd23470cfb72b624e7cc108bd", size = 230181, upload-time = "2026-04-09T14:38:47.225Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/13/a7825118208cb32e6a4edcd0a99f925cbef81e77b3b0aedfd9125583c543/opentelemetry_sdk-1.41.0-py3-none-any.whl", hash = "sha256:a596f5687964a3e0d7f8edfdcf5b79cbca9c93c7025ebf5fb00f398a9443b0bd", size = 180214, upload-time = "2026-04-09T14:38:30.657Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.62b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/b0/c14f723e86c049b7bf8ff431160d982519b97a7be2857ed2247377397a24/opentelemetry_semantic_conventions-0.62b0.tar.gz", hash = "sha256:cbfb3c8fc259575cf68a6e1b94083cc35adc4a6b06e8cf431efa0d62606c0097", size = 145753, upload-time = "2026-04-09T14:38:48.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/6c/5e86fa1759a525ef91c2d8b79d668574760ff3f900d114297765eb8786cb/opentelemetry_semantic_conventions-0.62b0-py3-none-any.whl", hash = "sha256:0ddac1ce59eaf1a827d9987ab60d9315fb27aea23304144242d1fcad9e16b489", size = 231619, upload-time = "2026-04-09T14:38:32.394Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions-ai" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-sdk", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "opentelemetry-semantic-conventions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/02/10aeacc37a38a3a8fa16ff67bec1ae3bf882539f6f9efb0f70acf802ca2d/opentelemetry_semantic_conventions_ai-0.5.1.tar.gz", hash = "sha256:153906200d8c1d2f8e09bd78dbef526916023de85ac3dab35912bfafb69ff04c", size = 26533, upload-time = "2026-03-26T14:20:38.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/22/41fb05f1dc5fda2c468e05a41814c20859016c85117b66c8a257cae814f6/opentelemetry_semantic_conventions_ai-0.5.1-py3-none-any.whl", hash = "sha256:25aeb22bd261543b4898a73824026d96770e5351209c7d07a0b1314762b1f6e4", size = 11250, upload-time = "2026-03-26T14:20:37.108Z" }, +] + +[[package]] +name = "oss2" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aliyun-python-sdk-core" }, + { name = "aliyun-python-sdk-kms" }, + { name = "crcmod" }, + { name = "pycryptodome" }, + { name = "requests" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/b5/f2cb1950dda46ac2284d6c950489fdacd0e743c2d79a347924d3cc44b86f/oss2-2.19.1.tar.gz", hash = "sha256:a8ab9ee7eb99e88a7e1382edc6ea641d219d585a7e074e3776e9dec9473e59c1", size = 298845, upload-time = "2024-10-25T11:37:46.638Z" } + +[[package]] +name = "outlines-core" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/04/4a0812eb27c086cfd2e66e7ec9150f33e105912a9b7f8b335e3479f03a06/outlines_core-0.2.14.tar.gz", hash = "sha256:64808deed1591ca3029ff64346ceb974cd5d780c916ea82504951fe83523039e", size = 191539, upload-time = "2026-01-09T15:59:10.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/cc/6f5d1b92e79b30c2ac187b23ab66b0365e06007a9405b35709d02e94615b/outlines_core-0.2.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e4b5b7c8e50489bea444b095692ddb5d8fb92ea6b949c4a6a3381eca9b691a7", size = 2339071, upload-time = "2026-01-09T15:58:12.488Z" }, + { url = "https://files.pythonhosted.org/packages/66/f7/252c0d4ecc5020d698b23257b9860fbab020c1120c15417ed02a9fd82d19/outlines_core-0.2.14-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b582b5d2f773cff966f37d7a5680d97506792647c93fb2e522283e8a14726e9d", size = 2236310, upload-time = "2026-01-09T15:58:13.942Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/e0be45d4c8ad7d301cdc9917d22ff39211da1e830f92fb07b29c9221b5c4/outlines_core-0.2.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:615566bf8257d2bba8ac192cdfc29d1c4357f57b53672fbd622e821215e4f1bd", size = 2338968, upload-time = "2026-01-09T15:58:23.317Z" }, + { url = "https://files.pythonhosted.org/packages/f2/67/9dab90313460eb250f926e7985d62cebfc33c7580197be8a496de6e9f7c4/outlines_core-0.2.14-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:81d01cfae29de5671bc5013fd6b2008621157bec3d8be284da7da2dc0672745c", size = 2236169, upload-time = "2026-01-09T15:58:24.575Z" }, + { url = "https://files.pythonhosted.org/packages/29/29/3a04944407207a5d214879ca5ca33c2bd3e65199a4e927051c1bdaaa4d50/outlines_core-0.2.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bb2060c240c4507f334965a8948dbeeb22007560d797f6debd92346c0b620cb", size = 2341426, upload-time = "2026-01-09T15:58:33.553Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/a77f746272504bac3f628047d56ea1731b61549a3e1d9bbfd226f2968246/outlines_core-0.2.14-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1de34681c7e0e7e1551fc9036e4fa3c57986336c905a10536591ceb6d869c258", size = 2236941, upload-time = "2026-01-09T15:58:35.118Z" }, +] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, + { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/35/6411db530c618e0e0005187e35aa02ce60ae4c4c4d206964a2f978217c27/pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0", size = 10326926, upload-time = "2026-03-31T06:46:08.29Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c", size = 9926987, upload-time = "2026-03-31T06:46:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/52/77/9b1c2d6070b5dbe239a7bc889e21bfa58720793fb902d1e070695d87c6d0/pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb", size = 10757067, upload-time = "2026-03-31T06:46:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76", size = 11258787, upload-time = "2026-03-31T06:46:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/90/e3/3f1126d43d3702ca8773871a81c9f15122a1f412342cc56284ffda5b1f70/pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e", size = 11771616, upload-time = "2026-03-31T06:46:20.532Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cf/0f4e268e1f5062e44a6bda9f925806721cd4c95c2b808a4c82ebe914f96b/pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa", size = 12337623, upload-time = "2026-03-31T06:46:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df", size = 9897372, upload-time = "2026-03-31T06:46:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/781516b808a99ddf288143cec46b342b3016c3414d137da1fdc3290d8860/pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f", size = 9154922, upload-time = "2026-03-31T06:46:30.284Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, + { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, +] + +[[package]] +name = "partial-json-parser" +version = "0.2.1.1.post7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/6d/eed37d7ebc1e0bcd27b831c0cf1fe94881934316187c4b30d23f29ea0bd4/partial_json_parser-0.2.1.1.post7.tar.gz", hash = "sha256:86590e1ba6bcb6739a2dfc17d2323f028cb5884f4c6ce23db376999132c9a922", size = 10296, upload-time = "2025-11-17T07:27:41.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/32/658973117bf0fd82a24abbfb94fe73a5e86216e49342985e10acce54775a/partial_json_parser-0.2.1.1.post7-py3-none-any.whl", hash = "sha256:145119e5eabcf80cbb13844a6b50a85c68bf99d376f8ed771e2a3c3b03e653ae", size = 10877, upload-time = "2025-11-17T07:27:40.457Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, + { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, + { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, + { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pooch" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "platformdirs" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/43/85ef45e8b36c6a48546af7b266592dc32d7f67837a6514d111bced6d7d75/pooch-1.9.0.tar.gz", hash = "sha256:de46729579b9857ffd3e741987a2f6d5e0e03219892c167c6578c0091fb511ed", size = 61788, upload-time = "2026-01-30T19:15:09.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl", hash = "sha256:f265597baa9f760d25ceb29d0beb8186c243d6607b0f60b83ecf14078dbc703b", size = 67175, upload-time = "2026-01-30T19:15:08.36Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" }, +] + +[[package]] +name = "prometheus-fastapi-instrumentator" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prometheus-client", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "starlette", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/6d/24d53033cf93826aa7857699a4450c1c67e5b9c710e925b1ed2b320c04df/prometheus_fastapi_instrumentator-7.1.0.tar.gz", hash = "sha256:be7cd61eeea4e5912aeccb4261c6631b3f227d8924542d79eaf5af3f439cbe5e", size = 20220, upload-time = "2025-03-19T19:35:05.351Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/72/0824c18f3bc75810f55dacc2dd933f6ec829771180245ae3cc976195dec0/prometheus_fastapi_instrumentator-7.1.0-py3-none-any.whl", hash = "sha256:978130f3c0bb7b8ebcc90d35516a6fe13e02d2eb358c8f83887cdef7020c31e9", size = 19296, upload-time = "2025-03-19T19:35:04.323Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, +] + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, +] + +[[package]] +name = "pyarrow" +version = "23.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/a8/24e5dc6855f50a62936ceb004e6e9645e4219a8065f304145d7fb8a79d5d/pyarrow-23.0.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:3fab8f82571844eb3c460f90a75583801d14ca0cc32b1acc8c361650e006fd56", size = 34307390, upload-time = "2026-02-16T10:08:08.654Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8e/4be5617b4aaae0287f621ad31c6036e5f63118cfca0dc57d42121ff49b51/pyarrow-23.0.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:3f91c038b95f71ddfc865f11d5876c42f343b4495535bd262c7b321b0b94507c", size = 35853761, upload-time = "2026-02-16T10:08:17.811Z" }, + { url = "https://files.pythonhosted.org/packages/2e/08/3e56a18819462210432ae37d10f5c8eed3828be1d6c751b6e6a2e93c286a/pyarrow-23.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:d0744403adabef53c985a7f8a082b502a368510c40d184df349a0a8754533258", size = 44493116, upload-time = "2026-02-16T10:08:25.792Z" }, + { url = "https://files.pythonhosted.org/packages/f8/82/c40b68001dbec8a3faa4c08cd8c200798ac732d2854537c5449dc859f55a/pyarrow-23.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c33b5bf406284fd0bba436ed6f6c3ebe8e311722b441d89397c54f871c6863a2", size = 47564532, upload-time = "2026-02-16T10:08:34.27Z" }, + { url = "https://files.pythonhosted.org/packages/20/bc/73f611989116b6f53347581b02177f9f620efdf3cd3f405d0e83cdf53a83/pyarrow-23.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ddf743e82f69dcd6dbbcb63628895d7161e04e56794ef80550ac6f3315eeb1d5", size = 48183685, upload-time = "2026-02-16T10:08:42.889Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cc/6c6b3ecdae2a8c3aced99956187e8302fc954cc2cca2a37cf2111dad16ce/pyarrow-23.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e052a211c5ac9848ae15d5ec875ed0943c0221e2fcfe69eee80b604b4e703222", size = 50605582, upload-time = "2026-02-16T10:08:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/8d/94/d359e708672878d7638a04a0448edf7c707f9e5606cee11e15aaa5c7535a/pyarrow-23.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:5abde149bb3ce524782d838eb67ac095cd3fd6090eba051130589793f1a7f76d", size = 27521148, upload-time = "2026-02-16T10:08:58.077Z" }, + { url = "https://files.pythonhosted.org/packages/b0/41/8e6b6ef7e225d4ceead8459427a52afdc23379768f54dd3566014d7618c1/pyarrow-23.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6f0147ee9e0386f519c952cc670eb4a8b05caa594eeffe01af0e25f699e4e9bb", size = 34302230, upload-time = "2026-02-16T10:09:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4a/1472c00392f521fea03ae93408bf445cc7bfa1ab81683faf9bc188e36629/pyarrow-23.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:0ae6e17c828455b6265d590100c295193f93cc5675eb0af59e49dbd00d2de350", size = 35850050, upload-time = "2026-02-16T10:09:11.877Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b2/bd1f2f05ded56af7f54d702c8364c9c43cd6abb91b0e9933f3d77b4f4132/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:fed7020203e9ef273360b9e45be52a2a47d3103caf156a30ace5247ffb51bdbd", size = 44491918, upload-time = "2026-02-16T10:09:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/0b/62/96459ef5b67957eac38a90f541d1c28833d1b367f014a482cb63f3b7cd2d/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9", size = 47562811, upload-time = "2026-02-16T10:09:25.792Z" }, + { url = "https://files.pythonhosted.org/packages/7d/94/1170e235add1f5f45a954e26cd0e906e7e74e23392dcb560de471f7366ec/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c30143b17161310f151f4a2bcfe41b5ff744238c1039338779424e38579d701", size = 48183766, upload-time = "2026-02-16T10:09:34.645Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/39a42af4570377b99774cdb47f63ee6c7da7616bd55b3d5001aa18edfe4f/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db2190fa79c80a23fdd29fef4b8992893f024ae7c17d2f5f4db7171fa30c2c78", size = 50607669, upload-time = "2026-02-16T10:09:44.153Z" }, + { url = "https://files.pythonhosted.org/packages/00/ca/db94101c187f3df742133ac837e93b1f269ebdac49427f8310ee40b6a58f/pyarrow-23.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:f00f993a8179e0e1c9713bcc0baf6d6c01326a406a9c23495ec1ba9c9ebf2919", size = 27527698, upload-time = "2026-02-16T10:09:50.263Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", size = 34214575, upload-time = "2026-02-16T10:09:56.225Z" }, + { url = "https://files.pythonhosted.org/packages/e1/da/3f941e3734ac8088ea588b53e860baeddac8323ea40ce22e3d0baa865cc9/pyarrow-23.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", size = 35832540, upload-time = "2026-02-16T10:10:03.428Z" }, + { url = "https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", size = 44470940, upload-time = "2026-02-16T10:10:10.704Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", size = 47586063, upload-time = "2026-02-16T10:10:17.95Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/b7d2ebcff47a514f47f9da1e74b7949138c58cfeb108cdd4ee62f43f0cf3/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", size = 48173045, upload-time = "2026-02-16T10:10:25.363Z" }, + { url = "https://files.pythonhosted.org/packages/43/b2/b40961262213beaba6acfc88698eb773dfce32ecdf34d19291db94c2bd73/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", size = 50621741, upload-time = "2026-02-16T10:10:33.477Z" }, + { url = "https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", size = 27620678, upload-time = "2026-02-16T10:10:39.31Z" }, +] + +[[package]] +name = "pybase64" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/b8/4ed5c7ad5ec15b08d35cc79ace6145d5c1ae426e46435f4987379439dfea/pybase64-1.4.3.tar.gz", hash = "sha256:c2ed274c9e0ba9c8f9c4083cfe265e66dd679126cd9c2027965d807352f3f053", size = 137272, upload-time = "2025-12-06T13:27:04.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/14/4365fa33222edcc46b6db4973f9e22bda82adfb6ab2a01afff591f1e41c8/pybase64-1.4.3-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5f2b8aef86f35cd5894c13681faf433a1fffc5b2e76544dcb5416a514a1a8347", size = 65978, upload-time = "2025-12-06T13:22:30.191Z" }, + { url = "https://files.pythonhosted.org/packages/1c/22/e89739d8bc9b96c68ead44b4eec42fe555683d9997e4ba65216d384920fc/pybase64-1.4.3-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6ec7e53dd09b0a8116ccf5c3265c7c7fce13c980747525be76902aef36a514a", size = 68903, upload-time = "2025-12-06T13:22:31.29Z" }, + { url = "https://files.pythonhosted.org/packages/77/e1/7e59a19f8999cdefe9eb0d56bfd701dd38263b0f6fb4a4d29fce165a1b36/pybase64-1.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7528604cd69c538e1dbaafded46e9e4915a2adcd6f2a60fcef6390d87ca922ea", size = 57516, upload-time = "2025-12-06T13:22:32.395Z" }, + { url = "https://files.pythonhosted.org/packages/42/ad/f47dc7e6fe32022b176868b88b671a32dab389718c8ca905cab79280aaaf/pybase64-1.4.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:4ec645f32b50593879031e09158f8681a1db9f5df0f72af86b3969a1c5d1fa2b", size = 54533, upload-time = "2025-12-06T13:22:33.457Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/7ab312b5a324833953b00e47b23eb4f83d45bd5c5c854b4b4e51b2a0cf5b/pybase64-1.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:634a000c5b3485ccc18bb9b244e0124f74b6fbc7f43eade815170237a7b34c64", size = 57187, upload-time = "2025-12-06T13:22:34.566Z" }, + { url = "https://files.pythonhosted.org/packages/2c/84/80acab1fcbaaae103e6b862ef5019192c8f2cd8758433595a202179a0d1d/pybase64-1.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:309ea32ad07639a485580af1be0ad447a434deb1924e76adced63ac2319cfe15", size = 57730, upload-time = "2025-12-06T13:22:35.581Z" }, + { url = "https://files.pythonhosted.org/packages/1f/24/84256d472400ea3163d7d69c44bb7e2e1027f0f1d4d20c47629a7dc4578e/pybase64-1.4.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:d10d517566b748d3f25f6ac7162af779360c1c6426ad5f962927ee205990d27c", size = 53036, upload-time = "2025-12-06T13:22:36.621Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/33aecbed312ee0431798a73fa25e00dedbffdd91389ee23121fed397c550/pybase64-1.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a74cc0f4d835400857cc5c6d27ec854f7949491e07a04e6d66e2137812831f4c", size = 56321, upload-time = "2025-12-06T13:22:37.7Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1c/a341b050746658cbec8cab3c733aeb3ef52ce8f11e60d0d47adbdf729ebf/pybase64-1.4.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1b591d774ac09d5eb73c156a03277cb271438fbd8042bae4109ff3a827cd218c", size = 50114, upload-time = "2025-12-06T13:22:38.752Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d3/f7e6680ae6dc4ddff39112ad66e0fa6b2ec346e73881bafc08498c560bc0/pybase64-1.4.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5eb588d35a04302ef6157d17db62354a787ac6f8b1585dd0b90c33d63a97a550", size = 66570, upload-time = "2025-12-06T13:22:40.221Z" }, + { url = "https://files.pythonhosted.org/packages/4c/71/774748eecc7fe23869b7e5df028e3c4c2efa16b506b83ea3fa035ea95dc2/pybase64-1.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df8b122d5be2c96962231cc4831d9c2e1eae6736fb12850cec4356d8b06fe6f8", size = 55700, upload-time = "2025-12-06T13:22:41.289Z" }, + { url = "https://files.pythonhosted.org/packages/b3/91/dd15075bb2fe0086193e1cd4bad80a43652c38d8a572f9218d46ba721802/pybase64-1.4.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:31b7a85c661fc591bbcce82fb8adaebe2941e6a83b08444b0957b77380452a4b", size = 52491, upload-time = "2025-12-06T13:22:42.628Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/f357d63ea3774c937fc47160e040419ed528827aa3d4306d5ec9826259c0/pybase64-1.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e6d7beaae65979fef250e25e66cf81c68a8f81910bcda1a2f43297ab486a7e4e", size = 53957, upload-time = "2025-12-06T13:22:44.615Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c3/243693771701a54e67ff5ccbf4c038344f429613f5643169a7befc51f007/pybase64-1.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4a6276bc3a3962d172a2b5aba544d89881c4037ea954517b86b00892c703d007", size = 68422, upload-time = "2025-12-06T13:22:45.641Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/bb06a5b9885e7d853ac1e801c4d8abfdb4c8506deee33e53d55aa6690e67/pybase64-1.4.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f9ef0388878bc15a084bd9bf73ec1b2b4ee513d11009b1506375e10a7aae5032", size = 68331, upload-time = "2025-12-06T13:22:54.197Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/8d60b9ec5e658185fc2ee3333e01a6e30d717cf677b24f47cbb3a859d13c/pybase64-1.4.3-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95a57cccf106352a72ed8bc8198f6820b16cc7d55aa3867a16dea7011ae7c218", size = 71370, upload-time = "2025-12-06T13:22:55.517Z" }, + { url = "https://files.pythonhosted.org/packages/ac/29/a3e5c1667cc8c38d025a4636855de0fc117fc62e2afeb033a3c6f12c6a22/pybase64-1.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cd1c47dfceb9c7bd3de210fb4e65904053ed2d7c9dce6d107f041ff6fbd7e21", size = 59834, upload-time = "2025-12-06T13:22:56.682Z" }, + { url = "https://files.pythonhosted.org/packages/a9/00/8ffcf9810bd23f3984698be161cf7edba656fd639b818039a7be1d6405d4/pybase64-1.4.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9fe9922698f3e2f72874b26890d53a051c431d942701bb3a37aae94da0b12107", size = 56652, upload-time = "2025-12-06T13:22:57.724Z" }, + { url = "https://files.pythonhosted.org/packages/81/62/379e347797cdea4ab686375945bc77ad8d039c688c0d4d0cfb09d247beb9/pybase64-1.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:af5f4bd29c86b59bb4375e0491d16ec8a67548fa99c54763aaedaf0b4b5a6632", size = 59382, upload-time = "2025-12-06T13:22:58.758Z" }, + { url = "https://files.pythonhosted.org/packages/c6/f2/9338ffe2f487086f26a2c8ca175acb3baa86fce0a756ff5670a0822bb877/pybase64-1.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c302f6ca7465262908131411226e02100f488f531bb5e64cb901aa3f439bccd9", size = 59990, upload-time = "2025-12-06T13:23:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a4/85a6142b65b4df8625b337727aa81dc199642de3d09677804141df6ee312/pybase64-1.4.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2f3f439fa4d7fde164ebbbb41968db7d66b064450ab6017c6c95cef0afa2b349", size = 54923, upload-time = "2025-12-06T13:23:02.369Z" }, + { url = "https://files.pythonhosted.org/packages/ac/00/e40215d25624012bf5b7416ca37f168cb75f6dd15acdb91ea1f2ea4dc4e7/pybase64-1.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a23c6866551043f8b681a5e1e0d59469148b2920a3b4fc42b1275f25ea4217a", size = 58664, upload-time = "2025-12-06T13:23:03.378Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/d7e19a63e795c13837f2356268d95dc79d1180e756f57ced742a1e52fdeb/pybase64-1.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:56e6526f8565642abc5f84338cc131ce298a8ccab696b19bdf76fa6d7dc592ef", size = 52338, upload-time = "2025-12-06T13:23:04.458Z" }, + { url = "https://files.pythonhosted.org/packages/f2/32/3c746d7a310b69bdd9df77ffc85c41b80bce00a774717596f869b0d4a20e/pybase64-1.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6a792a8b9d866ffa413c9687d9b611553203753987a3a582d68cbc51cf23da45", size = 68993, upload-time = "2025-12-06T13:23:05.526Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b3/63cec68f9d6f6e4c0b438d14e5f1ef536a5fe63ce14b70733ac5e31d7ab8/pybase64-1.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:62ad29a5026bb22cfcd1ca484ec34b0a5ced56ddba38ceecd9359b2818c9c4f9", size = 58055, upload-time = "2025-12-06T13:23:06.931Z" }, + { url = "https://files.pythonhosted.org/packages/d5/cb/7acf7c3c06f9692093c07f109668725dc37fb9a3df0fa912b50add645195/pybase64-1.4.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11b9d1d2d32ec358c02214363b8fc3651f6be7dd84d880ecd597a6206a80e121", size = 54430, upload-time = "2025-12-06T13:23:07.936Z" }, + { url = "https://files.pythonhosted.org/packages/33/39/4eb33ff35d173bfff4002e184ce8907f5d0a42d958d61cd9058ef3570179/pybase64-1.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0aebaa7f238caa0a0d373616016e2040c6c879ebce3ba7ab3c59029920f13640", size = 56272, upload-time = "2025-12-06T13:23:09.253Z" }, + { url = "https://files.pythonhosted.org/packages/19/97/a76d65c375a254e65b730c6f56bf528feca91305da32eceab8bcc08591e6/pybase64-1.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e504682b20c63c2b0c000e5f98a80ea867f8d97642e042a5a39818e44ba4d599", size = 70904, upload-time = "2025-12-06T13:23:10.336Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/9a8cab0042b464e9a876d5c65fe5127445a2436da36fda64899b119b1a1b/pybase64-1.4.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f0b3f200c3e06316f6bebabd458b4e4bcd4c2ca26af7c0c766614d91968dee27", size = 68210, upload-time = "2025-12-06T13:23:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/62/f7/965b79ff391ad208b50e412b5d3205ccce372a2d27b7218ae86d5295b105/pybase64-1.4.3-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb632edfd132b3eaf90c39c89aa314beec4e946e210099b57d40311f704e11d4", size = 71599, upload-time = "2025-12-06T13:23:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/03/4b/a3b5175130b3810bbb8ccfa1edaadbd3afddb9992d877c8a1e2f274b476e/pybase64-1.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:356ef1d74648ce997f5a777cf8f1aefecc1c0b4fe6201e0ef3ec8a08170e1b54", size = 59922, upload-time = "2025-12-06T13:23:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/5d/c38d1572027fc601b62d7a407721688b04b4d065d60ca489912d6893e6cf/pybase64-1.4.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:c48361f90db32bacaa5518419d4eb9066ba558013aaf0c7781620279ecddaeb9", size = 56712, upload-time = "2025-12-06T13:23:22.77Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d4/4e04472fef485caa8f561d904d4d69210a8f8fc1608ea15ebd9012b92655/pybase64-1.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:702bcaa16ae02139d881aeaef5b1c8ffb4a3fae062fe601d1e3835e10310a517", size = 59300, upload-time = "2025-12-06T13:23:24.543Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/16e29721b86734b881d09b7e23dfd7c8408ad01a4f4c7525f3b1088e25ec/pybase64-1.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:53d0ffe1847b16b647c6413d34d1de08942b7724273dd57e67dcbdb10c574045", size = 60278, upload-time = "2025-12-06T13:23:25.608Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/18515f211d7c046be32070709a8efeeef8a0203de4fd7521e6b56404731b/pybase64-1.4.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9a1792e8b830a92736dae58f0c386062eb038dfe8004fb03ba33b6083d89cd43", size = 54817, upload-time = "2025-12-06T13:23:26.633Z" }, + { url = "https://files.pythonhosted.org/packages/e7/be/14e29d8e1a481dbff151324c96dd7b5d2688194bb65dc8a00ca0e1ad1e86/pybase64-1.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d468b1b1ac5ad84875a46eaa458663c3721e8be5f155ade356406848d3701f6", size = 58611, upload-time = "2025-12-06T13:23:27.684Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8a/a2588dfe24e1bbd742a554553778ab0d65fdf3d1c9a06d10b77047d142aa/pybase64-1.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e97b7bdbd62e71898cd542a6a9e320d9da754ff3ebd02cb802d69087ee94d468", size = 52404, upload-time = "2025-12-06T13:23:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/afcda7445bebe0cbc38cafdd7813234cdd4fc5573ff067f1abf317bb0cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b33aeaa780caaa08ffda87fc584d5eab61e3d3bbb5d86ead02161dc0c20d04bc", size = 68817, upload-time = "2025-12-06T13:23:30.079Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3a/87c3201e555ed71f73e961a787241a2438c2bbb2ca8809c29ddf938a3157/pybase64-1.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c0efcf78f11cf866bed49caa7b97552bc4855a892f9cc2372abcd3ed0056f0d", size = 57854, upload-time = "2025-12-06T13:23:31.17Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7d/931c2539b31a7b375e7d595b88401eeb5bd6c5ce1059c9123f9b608aaa14/pybase64-1.4.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:66e3791f2ed725a46593f8bd2761ff37d01e2cdad065b1dceb89066f476e50c6", size = 54333, upload-time = "2025-12-06T13:23:32.422Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/537601e02cc01f27e9d75f440f1a6095b8df44fc28b1eef2cd739aea8cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:72bb0b6bddadab26e1b069bb78e83092711a111a80a0d6b9edcb08199ad7299b", size = 56492, upload-time = "2025-12-06T13:23:33.515Z" }, + { url = "https://files.pythonhosted.org/packages/96/97/2a2e57acf8f5c9258d22aba52e71f8050e167b29ed2ee1113677c1b600c1/pybase64-1.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5b3365dbcbcdb0a294f0f50af0c0a16b27a232eddeeb0bceeefd844ef30d2a23", size = 70974, upload-time = "2025-12-06T13:23:36.27Z" }, + { url = "https://files.pythonhosted.org/packages/bf/44/d4b7adc7bf4fd5b52d8d099121760c450a52c390223806b873f0b6a2d551/pybase64-1.4.3-graalpy311-graalpy242_311_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a492518f3078a4e3faaef310697d21df9c6bc71908cebc8c2f6fbfa16d7d6b1f", size = 43227, upload-time = "2025-12-06T13:26:21.845Z" }, + { url = "https://files.pythonhosted.org/packages/08/86/2ba2d8734ef7939debeb52cf9952e457ba7aa226cae5c0e6dd631f9b851f/pybase64-1.4.3-graalpy311-graalpy242_311_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae1a0f47784fd16df90d8acc32011c8d5fcdd9ab392c9ec49543e5f6a9c43a4", size = 35804, upload-time = "2025-12-06T13:26:23.149Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8f/43c3bb11ca9bacf81cb0b7a71500bb65b2eda6d5fe07433c09b543de97f3/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5c29a582b0ea3936d02bd6fe9bf674ab6059e6e45ab71c78404ab2c913224414", size = 43461, upload-time = "2025-12-06T13:26:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4c/2a5258329200be57497d3972b5308558c6de42e3749c6cc2aa1cbe34b25a/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6b664758c804fa919b4f1257aa8cf68e95db76fc331de5f70bfc3a34655afe1", size = 36058, upload-time = "2025-12-06T13:26:30.092Z" }, + { url = "https://files.pythonhosted.org/packages/20/f2/17546f97befe429c73f622bbd869ceebb518c40fdb0dec4c4f98312e80a5/pybase64-1.4.3-pp310-pypy310_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:03d0aa3761a99034960496280c02aa063f856a3cc9b33771bc4eab0e4e72b5c2", size = 40682, upload-time = "2025-12-06T13:26:35.168Z" }, + { url = "https://files.pythonhosted.org/packages/92/a0/464b36d5dfb61f3da17858afaeaa876a9342d58e9f17803ce7f28b5de9e8/pybase64-1.4.3-pp310-pypy310_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7ca5b1ce768520acd6440280cdab35235b27ad2faacfcec064bc9c3377066ef1", size = 41306, upload-time = "2025-12-06T13:26:36.351Z" }, + { url = "https://files.pythonhosted.org/packages/07/c9/a748dfc0969a8d960ecf1e82c8a2a16046ffec22f8e7ece582aa3b1c6cf9/pybase64-1.4.3-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3caa1e2ddad1c50553ffaaa1c86b74b3f9fbd505bea9970326ab88fc68c4c184", size = 35452, upload-time = "2025-12-06T13:26:37.772Z" }, + { url = "https://files.pythonhosted.org/packages/d3/22/832a2f9e76cdf39b52e01e40d8feeb6a04cf105494f2c3e3126d0149717f/pybase64-1.4.3-pp311-pypy311_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:bd4d2293de9fd212e294c136cec85892460b17d24e8c18a6ba18750928037750", size = 40681, upload-time = "2025-12-06T13:26:43.782Z" }, + { url = "https://files.pythonhosted.org/packages/12/d7/6610f34a8972415fab3bb4704c174a1cc477bffbc3c36e526428d0f3957d/pybase64-1.4.3-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af6d0d3a691911cc4c9a625f3ddcd3af720738c21be3d5c72de05629139d393", size = 41294, upload-time = "2025-12-06T13:26:44.936Z" }, + { url = "https://files.pythonhosted.org/packages/64/25/ed24400948a6c974ab1374a233cb7e8af0a5373cea0dd8a944627d17c34a/pybase64-1.4.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfc8c49a28322d82242088378f8542ce97459866ba73150b062a7073e82629d", size = 35447, upload-time = "2025-12-06T13:26:46.098Z" }, +] + +[[package]] +name = "pycountry" +version = "26.2.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/061b9e7a48b85cfd69f33c33d2ef784a531c359399ad764243399673c8f5/pycountry-26.2.16.tar.gz", hash = "sha256:5b6027d453fcd6060112b951dd010f01f168b51b4bf8a1f1fc8c95c8d94a0801", size = 7711342, upload-time = "2026-02-17T03:42:52.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/42/7703bd45b62fecd44cd7d3495423097e2f7d28bc2e99e7c1af68892ab157/pycountry-26.2.16-py3-none-any.whl", hash = "sha256:115c4baf7cceaa30f59a4694d79483c9167dbce7a9de4d3d571c5f3ea77c305a", size = 8044600, upload-time = "2026-02-17T03:42:49.777Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" }, + { url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" }, + { url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" }, + { url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/da/b8a7ee04378a53f6fefefc0c5e05570a3ebfdfa0523a878bcd3b475683ee/pydantic-2.12.0.tar.gz", hash = "sha256:c1a077e6270dbfb37bfd8b498b3981e2bb18f68103720e51fa6c306a5a9af563", size = 814760, upload-time = "2025-10-07T15:58:03.467Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/9d/d5c855424e2e5b6b626fbc6ec514d8e655a600377ce283008b115abb7445/pydantic-2.12.0-py3-none-any.whl", hash = "sha256:f6a1da352d42790537e95e83a8bdfb91c7efbae63ffd0b86fa823899e807116f", size = 459730, upload-time = "2025-10-07T15:58:01.576Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/14/12b4a0d2b0b10d8e1d9a24ad94e7bbb43335eaf29c0c4e57860e8a30734a/pydantic_core-2.41.1.tar.gz", hash = "sha256:1ad375859a6d8c356b7704ec0f547a58e82ee80bb41baa811ad710e124bc8f2f", size = 454870, upload-time = "2025-10-07T10:50:45.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2c/a5c4640dc7132540109f67fe83b566fbc7512ccf2a068cfa22a243df70c7/pydantic_core-2.41.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e63036298322e9aea1c8b7c0a6c1204d615dbf6ec0668ce5b83ff27f07404a61", size = 2113814, upload-time = "2025-10-06T21:09:50.892Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e7/a8694c3454a57842095d69c7a4ab3cf81c3c7b590f052738eabfdfc2e234/pydantic_core-2.41.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:241299ca91fc77ef64f11ed909d2d9220a01834e8e6f8de61275c4dd16b7c936", size = 1916660, upload-time = "2025-10-06T21:09:52.783Z" }, + { url = "https://files.pythonhosted.org/packages/9c/58/29f12e65b19c1877a0269eb4f23c5d2267eded6120a7d6762501ab843dc9/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab7e594a2a5c24ab8013a7dc8cfe5f2260e80e490685814122081705c2cf2b0", size = 1975071, upload-time = "2025-10-06T21:09:54.009Z" }, + { url = "https://files.pythonhosted.org/packages/98/26/4e677f2b7ec3fbdd10be6b586a82a814c8ebe3e474024c8df2d4260e564e/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b054ef1a78519cb934b58e9c90c09e93b837c935dcd907b891f2b265b129eb6e", size = 2067271, upload-time = "2025-10-06T21:09:55.175Z" }, + { url = "https://files.pythonhosted.org/packages/29/50/50614bd906089904d7ca1be3b9ecf08c00a327143d48f1decfdc21b3c302/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f2ab7d10d0ab2ed6da54c757233eb0f48ebfb4f86e9b88ccecb3f92bbd61a538", size = 2253207, upload-time = "2025-10-06T21:09:56.709Z" }, + { url = "https://files.pythonhosted.org/packages/ea/58/b1e640b4ca559273cca7c28e0fe8891d5d8e9a600f5ab4882670ec107549/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2757606b7948bb853a27e4040820306eaa0ccb9e8f9f8a0fa40cb674e170f350", size = 2375052, upload-time = "2025-10-06T21:09:57.97Z" }, + { url = "https://files.pythonhosted.org/packages/53/25/cd47df3bfb24350e03835f0950288d1054f1cc9a8023401dabe6d4ff2834/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cec0e75eb61f606bad0a32f2be87507087514e26e8c73db6cbdb8371ccd27917", size = 2076834, upload-time = "2025-10-06T21:09:59.58Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b4/71b2c77e5df527fbbc1a03e72c3fd96c44cd10d4241a81befef8c12b9fc4/pydantic_core-2.41.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0234236514f44a5bf552105cfe2543a12f48203397d9d0f866affa569345a5b5", size = 2195374, upload-time = "2025-10-06T21:10:01.18Z" }, + { url = "https://files.pythonhosted.org/packages/aa/08/4b8a50733005865efde284fec45da75fe16a258f706e16323c5ace4004eb/pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1b974e41adfbb4ebb0f65fc4ca951347b17463d60893ba7d5f7b9bb087c83897", size = 2156060, upload-time = "2025-10-06T21:10:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/83/c3/1037cb603ef2130c210150a51b1710d86825b5c28df54a55750099f91196/pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:248dafb3204136113c383e91a4d815269f51562b6659b756cf3df14eefc7d0bb", size = 2331640, upload-time = "2025-10-06T21:10:04.39Z" }, + { url = "https://files.pythonhosted.org/packages/56/4c/52d111869610e6b1a46e1f1035abcdc94d0655587e39104433a290e9f377/pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:678f9d76a91d6bcedd7568bbf6beb77ae8447f85d1aeebaab7e2f0829cfc3a13", size = 2329844, upload-time = "2025-10-06T21:10:05.68Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/4b435f0b52ab543967761aca66b84ad3f0026e491e57de47693d15d0a8db/pydantic_core-2.41.1-cp310-cp310-win32.whl", hash = "sha256:dff5bee1d21ee58277900692a641925d2dddfde65182c972569b1a276d2ac8fb", size = 1991289, upload-time = "2025-10-06T21:10:07.199Z" }, + { url = "https://files.pythonhosted.org/packages/88/52/31b4deafc1d3cb96d0e7c0af70f0dc05454982d135d07f5117e6336153e8/pydantic_core-2.41.1-cp310-cp310-win_amd64.whl", hash = "sha256:5042da12e5d97d215f91567110fdfa2e2595a25f17c19b9ff024f31c34f9b53e", size = 2027747, upload-time = "2025-10-06T21:10:08.503Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a9/ec440f02e57beabdfd804725ef1e38ac1ba00c49854d298447562e119513/pydantic_core-2.41.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4f276a6134fe1fc1daa692642a3eaa2b7b858599c49a7610816388f5e37566a1", size = 2111456, upload-time = "2025-10-06T21:10:09.824Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f9/6bc15bacfd8dcfc073a1820a564516d9c12a435a9a332d4cbbfd48828ddd/pydantic_core-2.41.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07588570a805296ece009c59d9a679dc08fab72fb337365afb4f3a14cfbfc176", size = 1915012, upload-time = "2025-10-06T21:10:11.599Z" }, + { url = "https://files.pythonhosted.org/packages/38/8a/d9edcdcdfe80bade17bed424284427c08bea892aaec11438fa52eaeaf79c/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28527e4b53400cd60ffbd9812ccb2b5135d042129716d71afd7e45bf42b855c0", size = 1973762, upload-time = "2025-10-06T21:10:13.154Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b3/ff225c6d49fba4279de04677c1c876fc3dc6562fd0c53e9bfd66f58c51a8/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46a1c935c9228bad738c8a41de06478770927baedf581d172494ab36a6b96575", size = 2065386, upload-time = "2025-10-06T21:10:14.436Z" }, + { url = "https://files.pythonhosted.org/packages/47/ba/183e8c0be4321314af3fd1ae6bfc7eafdd7a49bdea5da81c56044a207316/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:447ddf56e2b7d28d200d3e9eafa936fe40485744b5a824b67039937580b3cb20", size = 2252317, upload-time = "2025-10-06T21:10:15.719Z" }, + { url = "https://files.pythonhosted.org/packages/57/c5/aab61e94fd02f45c65f1f8c9ec38bb3b33fbf001a1837c74870e97462572/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63892ead40c1160ac860b5debcc95c95c5a0035e543a8b5a4eac70dd22e995f4", size = 2373405, upload-time = "2025-10-06T21:10:17.017Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4f/3aaa3bd1ea420a15acc42d7d3ccb3b0bbc5444ae2f9dbc1959f8173e16b8/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4a9543ca355e6df8fbe9c83e9faab707701e9103ae857ecb40f1c0cf8b0e94d", size = 2073794, upload-time = "2025-10-06T21:10:18.383Z" }, + { url = "https://files.pythonhosted.org/packages/58/bd/e3975cdebe03ec080ef881648de316c73f2a6be95c14fc4efb2f7bdd0d41/pydantic_core-2.41.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2611bdb694116c31e551ed82e20e39a90bea9b7ad9e54aaf2d045ad621aa7a1", size = 2194430, upload-time = "2025-10-06T21:10:19.638Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/6b7e7217f147d3b3105b57fb1caec3c4f667581affdfaab6d1d277e1f749/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fecc130893a9b5f7bfe230be1bb8c61fe66a19db8ab704f808cb25a82aad0bc9", size = 2154611, upload-time = "2025-10-06T21:10:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/239c2fe76bd8b7eef9ae2140d737368a3c6fea4fd27f8f6b4cde6baa3ce9/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:1e2df5f8344c99b6ea5219f00fdc8950b8e6f2c422fbc1cc122ec8641fac85a1", size = 2329809, upload-time = "2025-10-06T21:10:22.678Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/77a821a67ff0786f2f14856d6bd1348992f695ee90136a145d7a445c1ff6/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:35291331e9d8ed94c257bab6be1cb3a380b5eee570a2784bffc055e18040a2ea", size = 2327907, upload-time = "2025-10-06T21:10:24.447Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9a/b54512bb9df7f64c586b369328c30481229b70ca6a5fcbb90b715e15facf/pydantic_core-2.41.1-cp311-cp311-win32.whl", hash = "sha256:2876a095292668d753f1a868c4a57c4ac9f6acbd8edda8debe4218d5848cf42f", size = 1989964, upload-time = "2025-10-06T21:10:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/9d/72/63c9a4f1a5c950e65dd522d7dd67f167681f9d4f6ece3b80085a0329f08f/pydantic_core-2.41.1-cp311-cp311-win_amd64.whl", hash = "sha256:b92d6c628e9a338846a28dfe3fcdc1a3279388624597898b105e078cdfc59298", size = 2025158, upload-time = "2025-10-06T21:10:27.522Z" }, + { url = "https://files.pythonhosted.org/packages/d8/16/4e2706184209f61b50c231529257c12eb6bd9eb36e99ea1272e4815d2200/pydantic_core-2.41.1-cp311-cp311-win_arm64.whl", hash = "sha256:7d82ae99409eb69d507a89835488fb657faa03ff9968a9379567b0d2e2e56bc5", size = 1972297, upload-time = "2025-10-06T21:10:28.814Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bc/5f520319ee1c9e25010412fac4154a72e0a40d0a19eb00281b1f200c0947/pydantic_core-2.41.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:db2f82c0ccbce8f021ad304ce35cbe02aa2f95f215cac388eed542b03b4d5eb4", size = 2099300, upload-time = "2025-10-06T21:10:30.463Z" }, + { url = "https://files.pythonhosted.org/packages/31/14/010cd64c5c3814fb6064786837ec12604be0dd46df3327cf8474e38abbbd/pydantic_core-2.41.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47694a31c710ced9205d5f1e7e8af3ca57cbb8a503d98cb9e33e27c97a501601", size = 1910179, upload-time = "2025-10-06T21:10:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/8e/2e/23fc2a8a93efad52df302fdade0a60f471ecc0c7aac889801ac24b4c07d6/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e9decce94daf47baf9e9d392f5f2557e783085f7c5e522011545d9d6858e00", size = 1957225, upload-time = "2025-10-06T21:10:33.11Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b6/6db08b2725b2432b9390844852e11d320281e5cea8a859c52c68001975fa/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab0adafdf2b89c8b84f847780a119437a0931eca469f7b44d356f2b426dd9741", size = 2053315, upload-time = "2025-10-06T21:10:34.87Z" }, + { url = "https://files.pythonhosted.org/packages/61/d9/4de44600f2d4514b44f3f3aeeda2e14931214b6b5bf52479339e801ce748/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5da98cc81873f39fd56882e1569c4677940fbc12bce6213fad1ead784192d7c8", size = 2224298, upload-time = "2025-10-06T21:10:36.233Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ae/dbe51187a7f35fc21b283c5250571a94e36373eb557c1cba9f29a9806dcf/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:209910e88afb01fd0fd403947b809ba8dba0e08a095e1f703294fda0a8fdca51", size = 2351797, upload-time = "2025-10-06T21:10:37.601Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a7/975585147457c2e9fb951c7c8dab56deeb6aa313f3aa72c2fc0df3f74a49/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365109d1165d78d98e33c5bfd815a9b5d7d070f578caefaabcc5771825b4ecb5", size = 2074921, upload-time = "2025-10-06T21:10:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/62/37/ea94d1d0c01dec1b7d236c7cec9103baab0021f42500975de3d42522104b/pydantic_core-2.41.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:706abf21e60a2857acdb09502bc853ee5bce732955e7b723b10311114f033115", size = 2187767, upload-time = "2025-10-06T21:10:40.651Z" }, + { url = "https://files.pythonhosted.org/packages/d3/fe/694cf9fdd3a777a618c3afd210dba7b414cb8a72b1bd29b199c2e5765fee/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bf0bd5417acf7f6a7ec3b53f2109f587be176cb35f9cf016da87e6017437a72d", size = 2136062, upload-time = "2025-10-06T21:10:42.09Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/174aeabd89916fbd2988cc37b81a59e1186e952afd2a7ed92018c22f31ca/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2e71b1c6ceb9c78424ae9f63a07292fb769fb890a4e7efca5554c47f33a60ea5", size = 2317819, upload-time = "2025-10-06T21:10:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/65/e8/e9aecafaebf53fc456314f72886068725d6fba66f11b013532dc21259343/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:80745b9770b4a38c25015b517451c817799bfb9d6499b0d13d8227ec941cb513", size = 2312267, upload-time = "2025-10-06T21:10:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/35/2f/1c2e71d2a052f9bb2f2df5a6a05464a0eb800f9e8d9dd800202fe31219e1/pydantic_core-2.41.1-cp312-cp312-win32.whl", hash = "sha256:83b64d70520e7890453f1aa21d66fda44e7b35f1cfea95adf7b4289a51e2b479", size = 1990927, upload-time = "2025-10-06T21:10:46.738Z" }, + { url = "https://files.pythonhosted.org/packages/b1/78/562998301ff2588b9c6dcc5cb21f52fa919d6e1decc75a35055feb973594/pydantic_core-2.41.1-cp312-cp312-win_amd64.whl", hash = "sha256:377defd66ee2003748ee93c52bcef2d14fde48fe28a0b156f88c3dbf9bc49a50", size = 2034703, upload-time = "2025-10-06T21:10:48.524Z" }, + { url = "https://files.pythonhosted.org/packages/b2/53/d95699ce5a5cdb44bb470bd818b848b9beadf51459fd4ea06667e8ede862/pydantic_core-2.41.1-cp312-cp312-win_arm64.whl", hash = "sha256:c95caff279d49c1d6cdfe2996e6c2ad712571d3b9caaa209a404426c326c4bde", size = 1972719, upload-time = "2025-10-06T21:10:50.256Z" }, + { url = "https://files.pythonhosted.org/packages/16/89/d0afad37ba25f5801735af1472e650b86baad9fe807a42076508e4824a2a/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:68f2251559b8efa99041bb63571ec7cdd2d715ba74cc82b3bc9eff824ebc8bf0", size = 2124001, upload-time = "2025-10-07T10:49:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/8e/c4/08609134b34520568ddebb084d9ed0a2a3f5f52b45739e6e22cb3a7112eb/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:c7bc140c596097cb53b30546ca257dbe3f19282283190b1b5142928e5d5d3a20", size = 1941841, upload-time = "2025-10-07T10:49:56.248Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/94a4877094e5fe19a3f37e7e817772263e2c573c94f1e3fa2b1eee56ef3b/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2896510fce8f4725ec518f8b9d7f015a00db249d2fd40788f442af303480063d", size = 1961129, upload-time = "2025-10-07T10:49:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/a2/30/23a224d7e25260eb5f69783a63667453037e07eb91ff0e62dabaadd47128/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ced20e62cfa0f496ba68fa5d6c7ee71114ea67e2a5da3114d6450d7f4683572a", size = 2148770, upload-time = "2025-10-07T10:49:59.959Z" }, + { url = "https://files.pythonhosted.org/packages/2b/3e/a51c5f5d37b9288ba30683d6e96f10fa8f1defad1623ff09f1020973b577/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b04fa9ed049461a7398138c604b00550bc89e3e1151d84b81ad6dc93e39c4c06", size = 2115344, upload-time = "2025-10-07T10:50:02.466Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/389504c9e0600ef4502cd5238396b527afe6ef8981a6a15cd1814fc7b434/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b3b7d9cfbfdc43c80a16638c6dc2768e3956e73031fca64e8e1a3ae744d1faeb", size = 1927994, upload-time = "2025-10-07T10:50:04.379Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9c/5111c6b128861cb792a4c082677e90dac4f2e090bb2e2fe06aa5b2d39027/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eec83fc6abef04c7f9bec616e2d76ee9a6a4ae2a359b10c21d0f680e24a247ca", size = 1959394, upload-time = "2025-10-07T10:50:06.335Z" }, + { url = "https://files.pythonhosted.org/packages/14/3f/cfec8b9a0c48ce5d64409ec5e1903cb0b7363da38f14b41de2fcb3712700/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6771a2d9f83c4038dfad5970a3eef215940682b2175e32bcc817bdc639019b28", size = 2147365, upload-time = "2025-10-07T10:50:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/f403d7ca8352e3e4df352ccacd200f5f7f7fe81cef8e458515f015091625/pydantic_core-2.41.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fabcbdb12de6eada8d6e9a759097adb3c15440fafc675b3e94ae5c9cb8d678a0", size = 2114268, upload-time = "2025-10-07T10:50:10.257Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b5/334473b6d2810df84db67f03d4f666acacfc538512c2d2a254074fee0889/pydantic_core-2.41.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:80e97ccfaf0aaf67d55de5085b0ed0d994f57747d9d03f2de5cc9847ca737b08", size = 1935786, upload-time = "2025-10-07T10:50:12.333Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5e/45513e4dc621f47397cfa5fef12ba8fa5e8b1c4c07f2ff2a5fef8ff81b25/pydantic_core-2.41.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34df1fe8fea5d332484a763702e8b6a54048a9d4fe6ccf41e34a128238e01f52", size = 1971995, upload-time = "2025-10-07T10:50:14.071Z" }, + { url = "https://files.pythonhosted.org/packages/22/e3/f1797c168e5f52b973bed1c585e99827a22d5e579d1ed57d51bc15b14633/pydantic_core-2.41.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:421b5595f845842fc093f7250e24ee395f54ca62d494fdde96f43ecf9228ae01", size = 2191264, upload-time = "2025-10-07T10:50:15.788Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e1/24ef4c3b4ab91c21c3a09a966c7d2cffe101058a7bfe5cc8b2c7c7d574e2/pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dce8b22663c134583aaad24827863306a933f576c79da450be3984924e2031d1", size = 2152430, upload-time = "2025-10-07T10:50:18.018Z" }, + { url = "https://files.pythonhosted.org/packages/35/74/70c1e225d67f7ef3fdba02c506d9011efaf734020914920b2aa3d1a45e61/pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:300a9c162fea9906cc5c103893ca2602afd84f0ec90d3be36f4cc360125d22e1", size = 2324691, upload-time = "2025-10-07T10:50:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bf/dd4d21037c8bef0d8cce90a86a3f2dcb011c30086db2a10113c3eea23eba/pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e019167628f6e6161ae7ab9fb70f6d076a0bf0d55aa9b20833f86a320c70dd65", size = 2324493, upload-time = "2025-10-07T10:50:21.568Z" }, + { url = "https://files.pythonhosted.org/packages/7e/78/3093b334e9c9796c8236a4701cd2ddef1c56fb0928fe282a10c797644380/pydantic_core-2.41.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:13ab9cc2de6f9d4ab645a050ae5aee61a2424ac4d3a16ba23d4c2027705e0301", size = 2146156, upload-time = "2025-10-07T10:50:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6c/fa3e45c2b054a1e627a89a364917f12cbe3abc3e91b9004edaae16e7b3c5/pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:af2385d3f98243fb733862f806c5bb9122e5fba05b373e3af40e3c82d711cef1", size = 2112094, upload-time = "2025-10-07T10:50:25.513Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/7eebc38b4658cc8e6902d0befc26388e4c2a5f2e179c561eeb43e1922c7b/pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6550617a0c2115be56f90c31a5370261d8ce9dbf051c3ed53b51172dd34da696", size = 1935300, upload-time = "2025-10-07T10:50:27.715Z" }, + { url = "https://files.pythonhosted.org/packages/2b/00/9fe640194a1717a464ab861d43595c268830f98cb1e2705aa134b3544b70/pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc17b6ecf4983d298686014c92ebc955a9f9baf9f57dad4065e7906e7bee6222", size = 1970417, upload-time = "2025-10-07T10:50:29.573Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ad/f4cdfaf483b78ee65362363e73b6b40c48e067078d7b146e8816d5945ad6/pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42ae9352cf211f08b04ea110563d6b1e415878eea5b4c70f6bdb17dca3b932d2", size = 2190745, upload-time = "2025-10-07T10:50:31.48Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c1/18f416d40a10f44e9387497ba449f40fdb1478c61ba05c4b6bdb82300362/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e82947de92068b0a21681a13dd2102387197092fbe7defcfb8453e0913866506", size = 2150888, upload-time = "2025-10-07T10:50:33.477Z" }, + { url = "https://files.pythonhosted.org/packages/42/30/134c8a921630d8a88d6f905a562495a6421e959a23c19b0f49b660801d67/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e244c37d5471c9acdcd282890c6c4c83747b77238bfa19429b8473586c907656", size = 2324489, upload-time = "2025-10-07T10:50:36.48Z" }, + { url = "https://files.pythonhosted.org/packages/9c/48/a9263aeaebdec81e941198525b43edb3b44f27cfa4cb8005b8d3eb8dec72/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1e798b4b304a995110d41ec93653e57975620ccb2842ba9420037985e7d7284e", size = 2322763, upload-time = "2025-10-07T10:50:38.751Z" }, + { url = "https://files.pythonhosted.org/packages/1d/62/755d2bd2593f701c5839fc084e9c2c5e2418f460383ad04e3b5d0befc3ca/pydantic_core-2.41.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f1fc716c0eb1663c59699b024428ad5ec2bcc6b928527b8fe28de6cb89f47efb", size = 2144046, upload-time = "2025-10-07T10:50:40.686Z" }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, +] + +[package.optional-dependencies] +pycountry = [ + { name = "pycountry", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "python-dotenv", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-inspection", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/98/c8345dccdc31de4228c039a98f6467a941e39558da41c1744fbe29fa5666/pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d", size = 235709, upload-time = "2026-04-20T13:37:40.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/dd/bebff3040138f00ae8a102d426b27349b9a49acc310fcae7f92112d867e3/pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e", size = 60940, upload-time = "2026-04-20T13:37:38.586Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11' and sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] + +[[package]] +name = "pynndescent" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "llvmlite", version = "0.44.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "llvmlite", version = "0.47.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "numba", version = "0.61.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numba", version = "0.65.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/fb/7f58c397fb31666756457ee2ac4c0289ef2daad57f4ae4be8dec12f80b03/pynndescent-0.6.0.tar.gz", hash = "sha256:7ffde0fb5b400741e055a9f7d377e3702e02250616834231f6c209e39aac24f5", size = 2992987, upload-time = "2026-01-08T21:29:58.943Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/e6/94145d714402fd5ade00b5661f2d0ab981219e07f7db9bfa16786cdb9c04/pynndescent-0.6.0-py3-none-any.whl", hash = "sha256:dc8c74844e4c7f5cbd1e0cd6909da86fdc789e6ff4997336e344779c3d5538ef", size = 73511, upload-time = "2026-01-08T21:29:57.306Z" }, +] + +[[package]] +name = "pypinyin" +version = "0.55.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/a4/784cf98c09e0dc22776b0d7d8a4a5b761218bcae4608c2416ce1e167c8af/pypinyin-0.55.0.tar.gz", hash = "sha256:b5711b3a0c6f76e67408ec6b2e3c4987a3a806b7c528076e7c7b86fcf0eaa66b", size = 839836, upload-time = "2025-07-20T12:01:50.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/7b/4cabc76fcc21c3c7d5c671d8783984d30ac9d3bb387c4ba784fca3cdfa3a/pypinyin-0.55.0-py2.py3-none-any.whl", hash = "sha256:d53b1e8ad2cdb815fb2cb604ed3123372f5a28c6f447571244aca36fc62a286f", size = 840203, upload-time = "2025-07-20T12:01:48.535Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-json-logger" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/ff/3cc9165fd44106973cd7ac9facb674a65ed853494592541d339bdc9a30eb/python_json_logger-4.1.0.tar.gz", hash = "sha256:b396b9e3ed782b09ff9d6e4f1683d46c83ad0d35d2e407c09a9ebbf038f88195", size = 17573, upload-time = "2026-03-29T04:39:56.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/be/0631a861af4d1c875f096c07d34e9a63639560a717130e7a87cbc82b7e3f/python_json_logger-4.1.0-py3-none-any.whl", hash = "sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2", size = 15021, upload-time = "2026-03-29T04:39:55.266Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[package]] +name = "pytorch-wpe" +version = "0.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/39/8d94737fd6fab4028687575099566a125100f3ba8c638f861506747d7b7c/pytorch_wpe-0.0.1.tar.gz", hash = "sha256:fc7e706b5411800c4483fe94db7dcd82ecf6c57bc013af529ab4fb675c9cc29c", size = 4457, upload-time = "2021-03-05T10:10:09.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/de/c47967a11bfe68cb28d2f19e55c7027993c3721eba79813db65d245e4ced/pytorch_wpe-0.0.1-py3-none-any.whl", hash = "sha256:fa0dc9f818fba81b36c1a51a53331cf6ed975f29b33f23e07b0deb4bee82eaad", size = 8080, upload-time = "2021-03-05T10:10:08.686Z" }, +] + +[[package]] +name = "pytz" +version = "2026.1.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "(python_full_version < '3.11' and implementation_name == 'pypy' and sys_platform == 'emscripten') or (python_full_version < '3.11' and implementation_name == 'pypy' and sys_platform == 'win32') or (implementation_name == 'pypy' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, + { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, + { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, + { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, +] + +[[package]] +name = "quack-kernels" +version = "0.3.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apache-tvm-ffi", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-cutlass-dsl", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "torch", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "torch-c-dlpack-ext", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/34/bcc87d1ee53cf245bf58ea563b276b9bd86a405bda5a42e7bd1386db9941/quack_kernels-0.3.11.tar.gz", hash = "sha256:d589417476030fb62e70730c4bd0732339a04b8bb91fd49bf4cc70e20a27170b", size = 246675, upload-time = "2026-04-20T01:08:12.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/63/e80a50a1af53f102535fd701abaeb8f1d5c294223027b794fd5279b30a9e/quack_kernels-0.3.11-py3-none-any.whl", hash = "sha256:9a0fb71fd5f1efd909b2aef3d4965df831328fbbe6f57641f77ffd0da90fee3b", size = 240043, upload-time = "2026-04-20T01:08:10.747Z" }, +] + +[[package]] +name = "qwen3-asr" +version = "1.0.1" +source = { virtual = "." } +dependencies = [ + { name = "addict" }, + { name = "asyncpg" }, + { name = "datasets" }, + { name = "fastapi" }, + { name = "fastapi-offline" }, + { name = "funasr" }, + { name = "hdbscan" }, + { name = "huggingface-hub" }, + { name = "itntext" }, + { name = "librosa" }, + { name = "loguru" }, + { name = "modelscope", extra = ["framework"] }, + { name = "pydantic" }, + { name = "pypinyin" }, + { name = "python-dotenv" }, + { name = "python-multipart" }, + { name = "requests" }, + { name = "rich" }, + { name = "scipy" }, + { name = "setuptools" }, + { name = "soundfile" }, + { name = "torch", marker = "sys_platform == 'linux'" }, + { name = "torchaudio", marker = "sys_platform == 'linux'" }, + { name = "torchvision", marker = "sys_platform == 'linux'" }, + { name = "transformers" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "vllm", marker = "sys_platform == 'linux'" }, + { name = "websockets" }, +] + +[package.metadata] +requires-dist = [ + { name = "addict", specifier = "==2.4.0" }, + { name = "asyncpg", specifier = "==0.31.0" }, + { name = "datasets", specifier = "==3.6.0" }, + { name = "fastapi", specifier = "==0.128.0" }, + { name = "fastapi-offline", specifier = "==1.7.6" }, + { name = "funasr", specifier = "==1.3.1" }, + { name = "hdbscan", specifier = "==0.8.41" }, + { name = "huggingface-hub", specifier = "==0.34.0" }, + { name = "itntext", specifier = "==0.1.5" }, + { name = "librosa", specifier = "==0.11.0" }, + { name = "loguru", specifier = "==0.7.2" }, + { name = "modelscope", extras = ["framework"], specifier = "==1.34.0" }, + { name = "pydantic", specifier = "==2.12.0" }, + { name = "pypinyin", specifier = "==0.55.0" }, + { name = "python-dotenv", specifier = "==1.2.1" }, + { name = "python-multipart", specifier = "==0.0.22" }, + { name = "requests", specifier = "==2.32.5" }, + { name = "rich", specifier = ">=13.9,<14" }, + { name = "scipy", specifier = "==1.15.3" }, + { name = "setuptools", specifier = ">=70.0.0,<81" }, + { name = "soundfile", specifier = "==0.13.1" }, + { name = "torch", marker = "sys_platform == 'linux'", specifier = "==2.11.0", index = "https://download.pytorch.org/whl/cu130" }, + { name = "torchaudio", marker = "sys_platform == 'linux'", specifier = "==2.11.0", index = "https://download.pytorch.org/whl/cu130" }, + { name = "torchvision", marker = "sys_platform == 'linux'", specifier = "==0.26.0", index = "https://download.pytorch.org/whl/cu130" }, + { name = "transformers", specifier = ">=4.56.0,<5" }, + { name = "uvicorn", extras = ["standard"], specifier = "==0.40.0" }, + { name = "vllm", marker = "sys_platform == 'linux'", specifier = "==0.20.0" }, + { name = "websockets", specifier = "==16.0" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "rpds-py", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/3a246dbf05666918bd3664d9d787f84a9108f6f43cc953a077e4a7dfdb7e/regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423", size = 416000, upload-time = "2026-04-03T20:56:28.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/59/fd98f8fd54b3feaa76a855324c676c17668c5a1121ec91b7ec96b01bf865/regex-2026.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:74fa82dcc8143386c7c0392e18032009d1db715c25f4ba22d23dc2e04d02a20f", size = 489403, upload-time = "2026-04-03T20:52:39.742Z" }, + { url = "https://files.pythonhosted.org/packages/6c/64/d0f222f68e3579d50babf0e4fcc9c9639ef0587fecc00b15e1e46bfc32fa/regex-2026.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a85b620a388d6c9caa12189233109e236b3da3deffe4ff11b84ae84e218a274f", size = 291208, upload-time = "2026-04-03T20:52:42.943Z" }, + { url = "https://files.pythonhosted.org/packages/16/7f/3fab9709b0b0060ba81a04b8a107b34147cd14b9c5551b772154d6505504/regex-2026.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2895506ebe32cc63eeed8f80e6eae453171cfccccab35b70dc3129abec35a5b8", size = 289214, upload-time = "2026-04-03T20:52:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/14/bc/f5dcf04fd462139dcd75495c02eee22032ef741cfa151386a39c3f5fc9b5/regex-2026.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6780f008ee81381c737634e75c24e5a6569cc883c4f8e37a37917ee79efcafd9", size = 785505, upload-time = "2026-04-03T20:52:46.35Z" }, + { url = "https://files.pythonhosted.org/packages/37/36/8a906e216d5b4de7ec3788c1d589b45db40c1c9580cd7b326835cfc976d4/regex-2026.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:88e9b048345c613f253bea4645b2fe7e579782b82cac99b1daad81e29cc2ed8e", size = 852129, upload-time = "2026-04-03T20:52:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/a5/bb/bad2d79be0917a6ef31f5e0f161d9265cb56fd90a3ae1d2e8d991882a48b/regex-2026.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:be061028481186ba62a0f4c5f1cc1e3d5ab8bce70c89236ebe01023883bc903b", size = 899578, upload-time = "2026-04-03T20:52:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b9/7cd0ceb58cd99c70806241636640ae15b4a3fe62e22e9b99afa67a0d7965/regex-2026.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2228c02b368d69b724c36e96d3d1da721561fb9cc7faa373d7bf65e07d75cb5", size = 793634, upload-time = "2026-04-03T20:52:53Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fb/c58e3ea40ed183806ccbac05c29a3e8c2f88c1d3a66ed27860d5cad7c62d/regex-2026.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0540e5b733618a2f84e9cb3e812c8afa82e151ca8e19cf6c4e95c5a65198236f", size = 786210, upload-time = "2026-04-03T20:52:54.713Z" }, + { url = "https://files.pythonhosted.org/packages/54/a9/53790fc7a6c948a7be2bc7214fd9cabdd0d1ba561b0f401c91f4ff0357f0/regex-2026.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf9b1b2e692d4877880388934ac746c99552ce6bf40792a767fd42c8c99f136d", size = 769930, upload-time = "2026-04-03T20:52:56.825Z" }, + { url = "https://files.pythonhosted.org/packages/e3/3c/29ca44729191c79f5476538cd0fa04fa2553b3c45508519ecea4c7afa8f6/regex-2026.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:011bb48bffc1b46553ac704c975b3348717f4e4aa7a67522b51906f99da1820c", size = 774892, upload-time = "2026-04-03T20:52:58.934Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/6ae74ef8a4cfead341c367e4eed45f71fb1aaba35827a775eed4f1ba4f74/regex-2026.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8512fcdb43f1bf18582698a478b5ab73f9c1667a5b7548761329ef410cd0a760", size = 848816, upload-time = "2026-04-03T20:53:00.684Z" }, + { url = "https://files.pythonhosted.org/packages/53/9a/f7f2c1c6b610d7c6de1c3dc5951effd92c324b1fde761af2044b4721020f/regex-2026.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:867bddc63109a0276f5a31999e4c8e0eb7bbbad7d6166e28d969a2c1afeb97f9", size = 758363, upload-time = "2026-04-03T20:53:02.155Z" }, + { url = "https://files.pythonhosted.org/packages/dd/55/e5386d393bbf8b43c8b084703a46d635e7b2bdc6e0f5909a2619ea1125f1/regex-2026.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1b9a00b83f3a40e09859c78920571dcb83293c8004079653dd22ec14bbfa98c7", size = 837122, upload-time = "2026-04-03T20:53:03.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/da/cc78710ea2e60b10bacfcc9beb18c67514200ab03597b3b2b319995785c2/regex-2026.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e355be718caf838aa089870259cf1776dc2a4aa980514af9d02c59544d9a8b22", size = 782140, upload-time = "2026-04-03T20:53:05.608Z" }, + { url = "https://files.pythonhosted.org/packages/a2/5f/c7bcba41529105d6c2ca7080ecab7184cd00bee2e1ad1fdea80e618704ea/regex-2026.4.4-cp310-cp310-win32.whl", hash = "sha256:33bfda9684646d323414df7abe5692c61d297dbb0530b28ec66442e768813c59", size = 266225, upload-time = "2026-04-03T20:53:07.342Z" }, + { url = "https://files.pythonhosted.org/packages/eb/26/a745729c2c49354ec4f4bce168f29da932ca01b4758227686cc16c7dde1b/regex-2026.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:0709f22a56798457ae317bcce42aacee33c680068a8f14097430d9f9ba364bee", size = 278393, upload-time = "2026-04-03T20:53:08.65Z" }, + { url = "https://files.pythonhosted.org/packages/87/8b/4327eeb9dbb4b098ebecaf02e9f82b79b6077beeb54c43d9a0660cf7c44c/regex-2026.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:ee9627de8587c1a22201cb16d0296ab92b4df5cdcb5349f4e9744d61db7c7c98", size = 270470, upload-time = "2026-04-03T20:53:10.018Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7a/617356cbecdb452812a5d42f720d6d5096b360d4a4c1073af700ea140ad2/regex-2026.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4c36a85b00fadb85db9d9e90144af0a980e1a3d2ef9cd0f8a5bef88054657c6", size = 489415, upload-time = "2026-04-03T20:53:11.645Z" }, + { url = "https://files.pythonhosted.org/packages/20/e6/bf057227144d02e3ba758b66649e87531d744dda5f3254f48660f18ae9d8/regex-2026.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dcb5453ecf9cd58b562967badd1edbf092b0588a3af9e32ee3d05c985077ce87", size = 291205, upload-time = "2026-04-03T20:53:13.289Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3b/637181b787dd1a820ba1c712cee2b4144cd84a32dc776ca067b12b2d70c8/regex-2026.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6aa809ed4dc3706cc38594d67e641601bd2f36d5555b2780ff074edfcb136cf8", size = 289225, upload-time = "2026-04-03T20:53:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/05/21/bac05d806ed02cd4b39d9c8e5b5f9a2998c94c3a351b7792e80671fa5315/regex-2026.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33424f5188a7db12958246a54f59a435b6cb62c5cf9c8d71f7cc49475a5fdada", size = 792434, upload-time = "2026-04-03T20:53:17.414Z" }, + { url = "https://files.pythonhosted.org/packages/d9/17/c65d1d8ae90b772d5758eb4014e1e011bb2db353fc4455432e6cc9100df7/regex-2026.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d346fccdde28abba117cc9edc696b9518c3307fbfcb689e549d9b5979018c6d", size = 861730, upload-time = "2026-04-03T20:53:18.903Z" }, + { url = "https://files.pythonhosted.org/packages/ad/64/933321aa082a2c6ee2785f22776143ba89840189c20d3b6b1d12b6aae16b/regex-2026.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:415a994b536440f5011aa77e50a4274d15da3245e876e5c7f19da349caaedd87", size = 906495, upload-time = "2026-04-03T20:53:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/01/ea/4c8d306e9c36ac22417336b1e02e7b358152c34dc379673f2d331143725f/regex-2026.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21e5eb86179b4c67b5759d452ea7c48eb135cd93308e7a260aa489ed2eb423a4", size = 799810, upload-time = "2026-04-03T20:53:22.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/ce/7605048f00e1379eba89d610c7d644d8f695dc9b26d3b6ecfa3132b872ff/regex-2026.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:312ec9dd1ae7d96abd8c5a36a552b2139931914407d26fba723f9e53c8186f86", size = 774242, upload-time = "2026-04-03T20:53:25.015Z" }, + { url = "https://files.pythonhosted.org/packages/e9/77/283e0d5023fde22cd9e86190d6d9beb21590a452b195ffe00274de470691/regex-2026.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0d2b28aa1354c7cd7f71b7658c4326f7facac106edd7f40eda984424229fd59", size = 781257, upload-time = "2026-04-03T20:53:26.918Z" }, + { url = "https://files.pythonhosted.org/packages/8b/fb/7f3b772be101373c8626ed34c5d727dcbb8abd42a7b1219bc25fd9a3cc04/regex-2026.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:349d7310eddff40429a099c08d995c6d4a4bfaf3ff40bd3b5e5cb5a5a3c7d453", size = 854490, upload-time = "2026-04-03T20:53:29.065Z" }, + { url = "https://files.pythonhosted.org/packages/85/30/56547b80f34f4dd2986e1cdd63b1712932f63b6c4ce2f79c50a6cd79d1c2/regex-2026.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e7ab63e9fe45a9ec3417509e18116b367e89c9ceb6219222a3396fa30b147f80", size = 763544, upload-time = "2026-04-03T20:53:30.917Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2f/ce060fdfea8eff34a8997603532e44cdb7d1f35e3bc253612a8707a90538/regex-2026.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fe896e07a5a2462308297e515c0054e9ec2dd18dfdc9427b19900b37dfe6f40b", size = 844442, upload-time = "2026-04-03T20:53:32.463Z" }, + { url = "https://files.pythonhosted.org/packages/e5/44/810cb113096a1dacbe82789fbfab2823f79d19b7f1271acecb7009ba9b88/regex-2026.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb59c65069498dbae3c0ef07bbe224e1eaa079825a437fb47a479f0af11f774f", size = 789162, upload-time = "2026-04-03T20:53:34.039Z" }, + { url = "https://files.pythonhosted.org/packages/20/96/9647dd7f2ecf6d9ce1fb04dfdb66910d094e10d8fe53e9c15096d8aa0bd2/regex-2026.4.4-cp311-cp311-win32.whl", hash = "sha256:2a5d273181b560ef8397c8825f2b9d57013de744da9e8257b8467e5da8599351", size = 266227, upload-time = "2026-04-03T20:53:35.601Z" }, + { url = "https://files.pythonhosted.org/packages/33/80/74e13262460530c3097ff343a17de9a34d040a5dc4de9cf3a8241faab51c/regex-2026.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:9542ccc1e689e752594309444081582f7be2fdb2df75acafea8a075108566735", size = 278399, upload-time = "2026-04-03T20:53:37.021Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/39f19f47f19dcefa3403f09d13562ca1c0fd07ab54db2bc03148f3f6b46a/regex-2026.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:b5f9fb784824a042be3455b53d0b112655686fdb7a91f88f095f3fee1e2a2a54", size = 270473, upload-time = "2026-04-03T20:53:38.633Z" }, + { url = "https://files.pythonhosted.org/packages/e5/28/b972a4d3df61e1d7bcf1b59fdb3cddef22f88b6be43f161bb41ebc0e4081/regex-2026.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c07ab8794fa929e58d97a0e1796b8b76f70943fa39df225ac9964615cf1f9d52", size = 490434, upload-time = "2026-04-03T20:53:40.219Z" }, + { url = "https://files.pythonhosted.org/packages/84/20/30041446cf6dc3e0eab344fc62770e84c23b6b68a3b657821f9f80cb69b4/regex-2026.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c785939dc023a1ce4ec09599c032cc9933d258a998d16ca6f2b596c010940eb", size = 292061, upload-time = "2026-04-03T20:53:41.862Z" }, + { url = "https://files.pythonhosted.org/packages/62/c8/3baa06d75c98c46d4cc4262b71fd2edb9062b5665e868bca57859dadf93a/regex-2026.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b1ce5c81c9114f1ce2f9288a51a8fd3aeea33a0cc440c415bf02da323aa0a76", size = 289628, upload-time = "2026-04-03T20:53:43.701Z" }, + { url = "https://files.pythonhosted.org/packages/31/87/3accf55634caad8c0acab23f5135ef7d4a21c39f28c55c816ae012931408/regex-2026.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ef21c17d8e6a4fe8cf406a97cf2806a4df93416ccc82fc98d25b1c20425be", size = 796651, upload-time = "2026-04-03T20:53:45.379Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0c/aaa2c83f34efedbf06f61cb1942c25f6cf1ee3b200f832c4d05f28306c2e/regex-2026.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7088fcdcb604a4417c208e2169715800d28838fefd7455fbe40416231d1d47c1", size = 865916, upload-time = "2026-04-03T20:53:47.064Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f6/8c6924c865124643e8f37823eca845dc27ac509b2ee58123685e71cd0279/regex-2026.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07edca1ba687998968f7db5bc355288d0c6505caa7374f013d27356d93976d13", size = 912287, upload-time = "2026-04-03T20:53:49.422Z" }, + { url = "https://files.pythonhosted.org/packages/11/0e/a9f6f81013e0deaf559b25711623864970fe6a098314e374ccb1540a4152/regex-2026.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f657a7c1c6ec51b5e0ba97c9817d06b84ea5fa8d82e43b9405de0defdc2b9", size = 801126, upload-time = "2026-04-03T20:53:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/71/61/3a0cc8af2dc0c8deb48e644dd2521f173f7e6513c6e195aad9aa8dd77ac5/regex-2026.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b69102a743e7569ebee67e634a69c4cb7e59d6fa2e1aa7d3bdbf3f61435f62d", size = 776788, upload-time = "2026-04-03T20:53:52.889Z" }, + { url = "https://files.pythonhosted.org/packages/64/0b/8bb9cbf21ef7dee58e49b0fdb066a7aded146c823202e16494a36777594f/regex-2026.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dac006c8b6dda72d86ea3d1333d45147de79a3a3f26f10c1cf9287ca4ca0ac3", size = 785184, upload-time = "2026-04-03T20:53:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/99/c2/d3e80e8137b25ee06c92627de4e4d98b94830e02b3e6f81f3d2e3f504cf5/regex-2026.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:50a766ee2010d504554bfb5f578ed2e066898aa26411d57e6296230627cdefa0", size = 859913, upload-time = "2026-04-03T20:53:57.249Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/9d5d876157d969c804622456ef250017ac7a8f83e0e14f903b9e6df5ce95/regex-2026.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9e2f5217648f68e3028c823df58663587c1507a5ba8419f4fdfc8a461be76043", size = 765732, upload-time = "2026-04-03T20:53:59.428Z" }, + { url = "https://files.pythonhosted.org/packages/82/80/b568935b4421388561c8ed42aff77247285d3ae3bb2a6ca22af63bae805e/regex-2026.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39d8de85a08e32632974151ba59c6e9140646dcc36c80423962b1c5c0a92e244", size = 852152, upload-time = "2026-04-03T20:54:01.505Z" }, + { url = "https://files.pythonhosted.org/packages/39/29/f0f81217e21cd998245da047405366385d5c6072048038a3d33b37a79dc0/regex-2026.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55d9304e0e7178dfb1e106c33edf834097ddf4a890e2f676f6c5118f84390f73", size = 789076, upload-time = "2026-04-03T20:54:03.323Z" }, + { url = "https://files.pythonhosted.org/packages/49/1d/1d957a61976ab9d4e767dd4f9d04b66cc0c41c5e36cf40e2d43688b5ae6f/regex-2026.4.4-cp312-cp312-win32.whl", hash = "sha256:04bb679bc0bde8a7bfb71e991493d47314e7b98380b083df2447cda4b6edb60f", size = 266700, upload-time = "2026-04-03T20:54:05.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/bf575d396aeb58ea13b06ef2adf624f65b70fafef6950a80fc3da9cae3bc/regex-2026.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:db0ac18435a40a2543dbb3d21e161a6c78e33e8159bd2e009343d224bb03bb1b", size = 277768, upload-time = "2026-04-03T20:54:07.312Z" }, + { url = "https://files.pythonhosted.org/packages/c9/27/049df16ec6a6828ccd72add3c7f54b4df029669bea8e9817df6fff58be90/regex-2026.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:4ce255cc05c1947a12989c6db801c96461947adb7a59990f1360b5983fab4983", size = 270568, upload-time = "2026-04-03T20:54:09.484Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, +] + +[[package]] +name = "rich-toolkit" +version = "0.19.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "rich", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/ba/dae9e3096651042754da419a4042bc1c75e07d615f9b15066d738838e4df/rich_toolkit-0.19.7.tar.gz", hash = "sha256:133c0915872da91d4c25d85342d5ec1dfacc69b63448af1a08a0d4b4f23ef46e", size = 195877, upload-time = "2026-02-24T16:06:20.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/3c/c923619f6d2f5fafcc96fec0aaf9550a46cd5b6481f06e0c6b66a2a4fed0/rich_toolkit-0.19.7-py3-none-any.whl", hash = "sha256:0288e9203728c47c5a4eb60fd2f0692d9df7455a65901ab6f898437a2ba5989d", size = 32963, upload-time = "2026-02-24T16:06:22.066Z" }, +] + +[[package]] +name = "rignore" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/2e/05a1e61f04cf2548524224f0b5f21ca19ea58f7273a863bac10846b8ff69/rignore-0.7.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bde7c5835fa3905bfb7e329a4f1d7eccb676de63da7a3f934ddd5c06df20597", size = 899121, upload-time = "2025-11-05T20:40:48.94Z" }, + { url = "https://files.pythonhosted.org/packages/ff/35/71518847e10bdbf359badad8800e4681757a01f4777b3c5e03dbde8a42d8/rignore-0.7.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:626c3d4ba03af266694d25101bc1d8d16eda49c5feb86cedfec31c614fceca7d", size = 873813, upload-time = "2025-11-05T20:41:04.71Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c8/32ae405d3e7fd4d9f9b7838f2fcca0a5005bb87fa514b83f83fd81c0df22/rignore-0.7.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a43841e651e7a05a4274b9026cc408d1912e64016ede8cd4c145dae5d0635be", size = 1168019, upload-time = "2025-11-05T20:41:20.723Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/013c955982bc5b4719bf9a5bea58be317eea28aa12bfd004025e3cd7c000/rignore-0.7.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7978c498dbf7f74d30cdb8859fe612167d8247f0acd377ae85180e34490725da", size = 942822, upload-time = "2025-11-05T20:41:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/90/fb/9a3f3156c6ed30bcd597e63690353edac1fcffe9d382ad517722b56ac195/rignore-0.7.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d22f72ab695c07d2d96d2a645208daff17084441b5d58c07378c9dd6f9c4c87", size = 959820, upload-time = "2025-11-05T20:42:06.364Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b2/93bf609633021e9658acaff24cfb055d8cdaf7f5855d10ebb35307900dda/rignore-0.7.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5bd8e1a91ed1a789b2cbe39eeea9204a6719d4f2cf443a9544b521a285a295f", size = 985050, upload-time = "2025-11-05T20:41:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/ec2d040469bdfd7b743df10f2201c5d285009a4263d506edbf7a06a090bb/rignore-0.7.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fc03efad5789365018e94ac4079f851a999bc154d1551c45179f7fcf45322", size = 1079164, upload-time = "2025-11-05T21:40:10.368Z" }, + { url = "https://files.pythonhosted.org/packages/df/26/4b635f4ea5baf4baa8ba8eee06163f6af6e76dfbe72deb57da34bb24b19d/rignore-0.7.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ce2617fe28c51367fd8abfd4eeea9e61664af63c17d4ea00353d8ef56dfb95fa", size = 1139028, upload-time = "2025-11-05T21:40:27.977Z" }, + { url = "https://files.pythonhosted.org/packages/6a/54/a3147ebd1e477b06eb24e2c2c56d951ae5faa9045b7b36d7892fec5080d9/rignore-0.7.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7c4ad2cee85068408e7819a38243043214e2c3047e9bd4c506f8de01c302709e", size = 1119024, upload-time = "2025-11-05T21:40:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f4/27475db769a57cff18fe7e7267b36e6cdb5b1281caa185ba544171106cba/rignore-0.7.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:02cd240bfd59ecc3907766f4839cbba20530a2e470abca09eaa82225e4d946fb", size = 1128531, upload-time = "2025-11-05T21:41:02.734Z" }, + { url = "https://files.pythonhosted.org/packages/f5/81/4087453df35a90b07370647b19017029324950c1b9137d54bf1f33843f17/rignore-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16b63047648a916a87be1e51bb5c009063f1b8b6f5afe4f04f875525507e63dc", size = 899362, upload-time = "2025-11-05T20:40:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c9/390a8fdfabb76d71416be773bd9f162977bd483084f68daf19da1dec88a6/rignore-0.7.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba5524f5178deca4d7695e936604ebc742acb8958f9395776e1fcb8133f8257a", size = 873633, upload-time = "2025-11-05T20:41:06.193Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/79404fcb0faa76edfbc9df0901f8ef18568d1104919ebbbad6d608c888d1/rignore-0.7.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62020dbb89a1dd4b84ab3d60547b3b2eb2723641d5fb198463643f71eaaed57d", size = 1167633, upload-time = "2025-11-05T20:41:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/b3466d32d445d158a0aceb80919085baaae495b1f540fb942f91d93b5e5b/rignore-0.7.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b34acd532769d5a6f153a52a98dcb81615c949ab11697ce26b2eb776af2e174d", size = 941434, upload-time = "2025-11-05T20:41:38.151Z" }, + { url = "https://files.pythonhosted.org/packages/e8/40/9cd949761a7af5bc27022a939c91ff622d29c7a0b66d0c13a863097dde2d/rignore-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c5e53b752f9de44dff7b3be3c98455ce3bf88e69d6dc0cf4f213346c5e3416c", size = 959461, upload-time = "2025-11-05T20:42:08.476Z" }, + { url = "https://files.pythonhosted.org/packages/b5/87/1e1a145731f73bdb7835e11f80da06f79a00d68b370d9a847de979575e6d/rignore-0.7.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25b3536d13a5d6409ce85f23936f044576eeebf7b6db1d078051b288410fc049", size = 985323, upload-time = "2025-11-05T20:41:52.735Z" }, + { url = "https://files.pythonhosted.org/packages/6c/31/1ecff992fc3f59c4fcdcb6c07d5f6c1e6dfb55ccda19c083aca9d86fa1c6/rignore-0.7.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e01cad2b0b92f6b1993f29fc01f23f2d78caf4bf93b11096d28e9d578eb08ce", size = 1079173, upload-time = "2025-11-05T21:40:12.007Z" }, + { url = "https://files.pythonhosted.org/packages/17/18/162eedadb4c2282fa4c521700dbf93c9b14b8842e8354f7d72b445b8d593/rignore-0.7.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5991e46ab9b4868334c9e372ab0892b0150f3f586ff2b1e314272caeb38aaedb", size = 1139012, upload-time = "2025-11-05T21:40:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/78/96/a9ca398a8af74bb143ad66c2a31303c894111977e28b0d0eab03867f1b43/rignore-0.7.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c8ae562e5d1246cba5eaeb92a47b2a279e7637102828dde41dcbe291f529a3e", size = 1118827, upload-time = "2025-11-05T21:40:46.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/22/1c1a65047df864def9a047dbb40bc0b580b8289a4280e62779cd61ae21f2/rignore-0.7.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aaf938530dcc0b47c4cfa52807aa2e5bfd5ca6d57a621125fe293098692f6345", size = 1128182, upload-time = "2025-11-05T21:41:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/5b00bc2a6bc1701e6878fca798cf5d9125eb3113193e33078b6fc0d99123/rignore-0.7.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", size = 942942, upload-time = "2025-11-05T20:41:39.393Z" }, + { url = "https://files.pythonhosted.org/packages/85/e5/7f99bd0cc9818a91d0e8b9acc65b792e35750e3bdccd15a7ee75e64efca4/rignore-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", size = 959787, upload-time = "2025-11-05T20:42:09.765Z" }, + { url = "https://files.pythonhosted.org/packages/55/54/2ffea79a7c1eabcede1926347ebc2a81bc6b81f447d05b52af9af14948b9/rignore-0.7.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c7aa109d41e593785c55fdaa89ad80b10330affa9f9d3e3a51fa695f739b20", size = 984245, upload-time = "2025-11-05T20:41:54.062Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/e80f55dfe0f35787fa482aa18689b9c8251e045076c35477deb0007b3277/rignore-0.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", size = 1078647, upload-time = "2025-11-05T21:40:13.463Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/a86c84909ccc24af0d094b50d54697951e576c252a4d9f21b47b52af9598/rignore-0.7.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", size = 1117604, upload-time = "2025-11-05T21:40:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2c/7d7bad116e09a04e9e1688c6f891fa2d4fd33f11b69ac0bd92419ddebeae/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cab9ff2e436ce7240d7ee301c8ef806ed77c1fd6b8a8239ff65f9bbbcb5b8a3", size = 900922, upload-time = "2025-11-05T20:41:00.361Z" }, + { url = "https://files.pythonhosted.org/packages/09/ba/e5ea89fbde8e37a90ce456e31c5e9d85512cef5ae38e0f4d2426eb776a19/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1a6671b2082c13bfd9a5cf4ce64670f832a6d41470556112c4ab0b6519b2fc4", size = 876987, upload-time = "2025-11-05T20:41:16.219Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fb/93d14193f0ec0c3d35b763f0a000e9780f63b2031f3d3756442c2152622d/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2468729b4c5295c199d084ab88a40afcb7c8b974276805105239c07855bbacee", size = 1171110, upload-time = "2025-11-05T20:41:32.631Z" }, + { url = "https://files.pythonhosted.org/packages/9e/46/08436312ff96ffa29cfa4e1a987efc37e094531db46ba5e9fda9bb792afd/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:775710777fd71e5fdf54df69cdc249996a1d6f447a2b5bfb86dbf033fddd9cf9", size = 943339, upload-time = "2025-11-05T20:41:47.128Z" }, + { url = "https://files.pythonhosted.org/packages/34/28/3b3c51328f505cfaf7e53f408f78a1e955d561135d02f9cb0341ea99f69a/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4565407f4a77f72cf9d91469e75d15d375f755f0a01236bb8aaa176278cc7085", size = 961680, upload-time = "2025-11-05T20:42:18.061Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9e/cbff75c8676d4f4a90bd58a1581249d255c7305141b0868f0abc0324836b/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc44c33f8fb2d5c9da748de7a6e6653a78aa740655e7409895e94a247ffa97c8", size = 987045, upload-time = "2025-11-05T20:42:02.315Z" }, + { url = "https://files.pythonhosted.org/packages/8c/25/d802d1d369502a7ddb8816059e7c79d2d913e17df975b863418e0aca4d8a/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8f32478f05540513c11923e8838afab9efef0131d66dca7f67f0e1bbd118af6a", size = 1080310, upload-time = "2025-11-05T21:40:23.184Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/250b785c2e473b1ab763eaf2be820934c2a5409a722e94b279dddac21c7d/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:1b63a3dd76225ea35b01dd6596aa90b275b5d0f71d6dc28fce6dd295d98614aa", size = 1140998, upload-time = "2025-11-05T21:40:40.603Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d6/bb42fd2a8bba6aea327962656e20621fd495523259db40cfb4c5f760f05c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:fe6c41175c36554a4ef0994cd1b4dbd6d73156fca779066456b781707402048e", size = 1121178, upload-time = "2025-11-05T21:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/97/f4/aeb548374129dce3dc191a4bb598c944d9ed663f467b9af830315d86059c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9a0c6792406ae36f4e7664dc772da909451d46432ff8485774526232d4885063", size = 1130190, upload-time = "2025-11-05T21:41:16.403Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d2/1b264f56132264ea609d3213ab603d6a27016b19559a1a1ede1a66a03dcd/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22baa462abdc36fdd5a5e2dae423107723351b85ff093762f9261148b9d0a04a", size = 899739, upload-time = "2025-11-05T20:41:01.518Z" }, + { url = "https://files.pythonhosted.org/packages/55/e4/b3c5dfdd8d8a10741dfe7199ef45d19a0e42d0c13aa377c83bd6caf65d90/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53fb28882d2538cb2d231972146c4927a9d9455e62b209f85d634408c4103538", size = 874843, upload-time = "2025-11-05T20:41:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/10/d6f3750233881a2a154cefc9a6a0a9b19da526b19f7f08221b552c6f827d/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87409f7eeb1103d6b77f3472a3a0d9a5953e3ae804a55080bdcb0120ee43995b", size = 1170348, upload-time = "2025-11-05T20:41:34.21Z" }, + { url = "https://files.pythonhosted.org/packages/6e/10/ad98ca05c9771c15af734cee18114a3c280914b6e34fde9ffea2e61e88aa/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684014e42e4341ab3ea23a203551857fcc03a7f8ae96ca3aefb824663f55db32", size = 942315, upload-time = "2025-11-05T20:41:48.508Z" }, + { url = "https://files.pythonhosted.org/packages/de/00/ab5c0f872acb60d534e687e629c17e0896c62da9b389c66d3aa16b817aa8/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77356ebb01ba13f8a425c3d30fcad40e57719c0e37670d022d560884a30e4767", size = 961047, upload-time = "2025-11-05T20:42:19.403Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/3030fdc363a8f0d1cd155b4c453d6db9bab47a24fcc64d03f61d9d78fe6a/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6cbd8a48abbd3747a6c830393cd578782fab5d43f4deea48c5f5e344b8fed2b0", size = 986090, upload-time = "2025-11-05T20:42:03.581Z" }, + { url = "https://files.pythonhosted.org/packages/33/b8/133aa4002cee0ebbb39362f94e4898eec7fbd09cec9fcbce1cd65b355b7f/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2673225dcec7f90497e79438c35e34638d0d0391ccea3cbb79bfb9adc0dc5bd7", size = 1079656, upload-time = "2025-11-05T21:40:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/67/56/36d5d34210e5e7dfcd134eed8335b19e80ae940ee758f493e4f2b344dd70/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:c081f17290d8a2b96052b79207622aa635686ea39d502b976836384ede3d303c", size = 1139789, upload-time = "2025-11-05T21:40:42.119Z" }, + { url = "https://files.pythonhosted.org/packages/6b/5b/bb4f9420802bf73678033a4a55ab1bede36ce2e9b41fec5f966d83d932b3/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:57e8327aacc27f921968cb2a174f9e47b084ce9a7dd0122c8132d22358f6bd79", size = 1120308, upload-time = "2025-11-05T21:40:59.402Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8b/a1299085b28a2f6135e30370b126e3c5055b61908622f2488ade67641479/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:d8955b57e42f2a5434670d5aa7b75eaf6e74602ccd8955dddf7045379cd762fb", size = 1129444, upload-time = "2025-11-05T21:41:17.906Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "numpy", marker = "python_full_version < '3.11'" }, + { name = "scipy", marker = "python_full_version < '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" }, + { url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" }, + { url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" }, + { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" }, + { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" }, + { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" }, + { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" }, + { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "joblib", marker = "python_full_version >= '3.11'" }, + { name = "numpy", marker = "python_full_version >= '3.11'" }, + { name = "scipy", marker = "python_full_version >= '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, +] + +[[package]] +name = "sentencepiece" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/15/2e7a025fc62d764b151ae6d0f2a92f8081755ebe8d4a64099accc6f77ba6/sentencepiece-0.2.1.tar.gz", hash = "sha256:8138cec27c2f2282f4a34d9a016e3374cd40e5c6e9cb335063db66a0a3b71fad", size = 3228515, upload-time = "2025-08-12T07:00:51.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/31/5b7cccb307b485db1a2372d6d2980b0a65d067f8be5ca943a103b4acd5b3/sentencepiece-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e10fa50bdbaa5e2445dbd387979980d391760faf0ec99a09bd7780ff37eaec44", size = 1942557, upload-time = "2025-08-12T06:59:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/1f/41/0ac923a8e685ad290c5afc8ae55c5844977b8d75076fcc04302b9a324274/sentencepiece-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f27ae6deea72efdb6f361750c92f6c21fd0ad087445082770cc34015213c526", size = 1325384, upload-time = "2025-08-12T06:59:14.334Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ef/3751555d67daf9003384978f169d31c775cb5c7baf28633caaf1eb2b2b4d/sentencepiece-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:60937c959e6f44159fdd9f56fbdd302501f96114a5ba436829496d5f32d8de3f", size = 1253317, upload-time = "2025-08-12T06:59:16.247Z" }, + { url = "https://files.pythonhosted.org/packages/46/a5/742c69b7bd144eb32b6e5fd50dbd8abbbc7a95fce2fe16e50156fa400e3b/sentencepiece-0.2.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8b1d91545578852f128650b8cce4ec20f93d39b378ff554ebe66290f2dabb92", size = 1316379, upload-time = "2025-08-12T06:59:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/8deeafbba2871e8fa10f20f17447786f4ac38085925335728d360eaf4cae/sentencepiece-0.2.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27e38eee653abc3d387862e67bc5c8b6f428cd604e688b85d29170b7e725c26c", size = 1387926, upload-time = "2025-08-12T06:59:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ca/67fe73005f0ab617c6a970b199754e28e524b6873aa7025224fad3cda252/sentencepiece-0.2.1-cp310-cp310-win32.whl", hash = "sha256:251874d720ac7f28024a168501f3c7bb15d1802245f6e66de565f18bbb9b5eaa", size = 999550, upload-time = "2025-08-12T06:59:20.844Z" }, + { url = "https://files.pythonhosted.org/packages/6d/33/dc5b54042050d2dda4229c3ce1f862541c99966390b6aa20f54d520d2dc2/sentencepiece-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:e52144670738b4b477fade6c2a9b6af71a8d0094514c9853ac9f6fc1fcfabae7", size = 1054613, upload-time = "2025-08-12T06:59:22.255Z" }, + { url = "https://files.pythonhosted.org/packages/fa/19/1ea47f46ff97fe04422b78997da1a37cd632f414aae042d27a9009c5b733/sentencepiece-0.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:9076430ac25dfa7147d9d05751dbc66a04bc1aaac371c07f84952979ea59f0d0", size = 1033884, upload-time = "2025-08-12T06:59:24.194Z" }, + { url = "https://files.pythonhosted.org/packages/d8/15/46afbab00733d81788b64be430ca1b93011bb9388527958e26cc31832de5/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6356d0986b8b8dc351b943150fcd81a1c6e6e4d439772e8584c64230e58ca987", size = 1942560, upload-time = "2025-08-12T06:59:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/fa/79/7c01b8ef98a0567e9d84a4e7a910f8e7074fcbf398a5cd76f93f4b9316f9/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f8ba89a3acb3dc1ae90f65ec1894b0b9596fdb98ab003ff38e058f898b39bc7", size = 1325385, upload-time = "2025-08-12T06:59:27.722Z" }, + { url = "https://files.pythonhosted.org/packages/bb/88/2b41e07bd24f33dcf2f18ec3b74247aa4af3526bad8907b8727ea3caba03/sentencepiece-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02593eca45440ef39247cee8c47322a34bdcc1d8ae83ad28ba5a899a2cf8d79a", size = 1253319, upload-time = "2025-08-12T06:59:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a0/54/38a1af0c6210a3c6f95aa46d23d6640636d020fba7135cd0d9a84ada05a7/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a0d15781a171d188b661ae4bde1d998c303f6bd8621498c50c671bd45a4798e", size = 1316162, upload-time = "2025-08-12T06:59:30.914Z" }, + { url = "https://files.pythonhosted.org/packages/ef/66/fb191403ade791ad2c3c1e72fe8413e63781b08cfa3aa4c9dfc536d6e795/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f5a3e0d9f445ed9d66c0fec47d4b23d12cfc858b407a03c194c1b26c2ac2a63", size = 1387785, upload-time = "2025-08-12T06:59:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2d/3bd9b08e70067b2124518b308db6a84a4f8901cc8a4317e2e4288cdd9b4d/sentencepiece-0.2.1-cp311-cp311-win32.whl", hash = "sha256:6d297a1748d429ba8534eebe5535448d78b8acc32d00a29b49acf28102eeb094", size = 999555, upload-time = "2025-08-12T06:59:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/32/b8/f709977f5fda195ae1ea24f24e7c581163b6f142b1005bc3d0bbfe4d7082/sentencepiece-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:82d9ead6591015f009cb1be1cb1c015d5e6f04046dbb8c9588b931e869a29728", size = 1054617, upload-time = "2025-08-12T06:59:36.461Z" }, + { url = "https://files.pythonhosted.org/packages/7a/40/a1fc23be23067da0f703709797b464e8a30a1c78cc8a687120cd58d4d509/sentencepiece-0.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:39f8651bd10974eafb9834ce30d9bcf5b73e1fc798a7f7d2528f9820ca86e119", size = 1033877, upload-time = "2025-08-12T06:59:38.391Z" }, + { url = "https://files.pythonhosted.org/packages/4a/be/32ce495aa1d0e0c323dcb1ba87096037358edee539cac5baf8755a6bd396/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57cae326c8727de58c85977b175af132a7138d84c764635d7e71bbee7e774133", size = 1943152, upload-time = "2025-08-12T06:59:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/88/7e/ff23008899a58678e98c6ff592bf4d368eee5a71af96d0df6b38a039dd4f/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56dd39a3c4d6493db3cdca7e8cc68c6b633f0d4195495cbadfcf5af8a22d05a6", size = 1325651, upload-time = "2025-08-12T06:59:41.536Z" }, + { url = "https://files.pythonhosted.org/packages/19/84/42eb3ce4796777a1b5d3699dfd4dca85113e68b637f194a6c8d786f16a04/sentencepiece-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9381351182ff9888cc80e41c632e7e274b106f450de33d67a9e8f6043da6f76", size = 1253645, upload-time = "2025-08-12T06:59:42.903Z" }, + { url = "https://files.pythonhosted.org/packages/89/fa/d3d5ebcba3cb9e6d3775a096251860c41a6bc53a1b9461151df83fe93255/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f955df238021bf11f0fc37cdb54fd5e5b5f7fd30ecc3d93fb48b6815437167", size = 1316273, upload-time = "2025-08-12T06:59:44.476Z" }, + { url = "https://files.pythonhosted.org/packages/04/88/14f2f4a2b922d8b39be45bf63d79e6cd3a9b2f248b2fcb98a69b12af12f5/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdfecef430d985f1c2bcbfff3defd1d95dae876fbd0173376012d2d7d24044b", size = 1387881, upload-time = "2025-08-12T06:59:46.09Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b8/903e5ccb77b4ef140605d5d71b4f9e0ad95d456d6184688073ed11712809/sentencepiece-0.2.1-cp312-cp312-win32.whl", hash = "sha256:a483fd29a34c3e34c39ac5556b0a90942bec253d260235729e50976f5dba1068", size = 999540, upload-time = "2025-08-12T06:59:48.023Z" }, + { url = "https://files.pythonhosted.org/packages/2d/81/92df5673c067148c2545b1bfe49adfd775bcc3a169a047f5a0e6575ddaca/sentencepiece-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4cdc7c36234fda305e85c32949c5211faaf8dd886096c7cea289ddc12a2d02de", size = 1054671, upload-time = "2025-08-12T06:59:49.895Z" }, + { url = "https://files.pythonhosted.org/packages/fe/02/c5e3bc518655d714622bec87d83db9cdba1cd0619a4a04e2109751c4f47f/sentencepiece-0.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:daeb5e9e9fcad012324807856113708614d534f596d5008638eb9b40112cd9e4", size = 1033923, upload-time = "2025-08-12T06:59:51.952Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.58.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "urllib3", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/b3/fb8291170d0e844173164709fc0fa0c221ed75a5da740c8746f2a83b4eb1/sentry_sdk-2.58.0.tar.gz", hash = "sha256:c1144d947352d54e5b7daa63596d9f848adf684989c06c4f5a659f0c85a18f6f", size = 438764, upload-time = "2026-04-13T17:23:26.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/eb/d875669993b762556ae8b2efd86219943b4c0864d22204d622a9aee3052b/sentry_sdk-2.58.0-py2.py3-none-any.whl", hash = "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", size = 460919, upload-time = "2026-04-13T17:23:24.675Z" }, +] + +[[package]] +name = "setproctitle" +version = "1.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002, upload-time = "2025-09-05T12:51:25.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/f8/17bda581c517678260e6541b600eeb67745f53596dc077174141ba2f6702/setproctitle-1.3.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:00afa6fc507967d8c9d592a887cdc6c1f5742ceac6a4354d111ca0214847732c", size = 31793, upload-time = "2025-09-05T12:49:10.297Z" }, + { url = "https://files.pythonhosted.org/packages/27/d1/76a33ae80d4e788ecab9eb9b53db03e81cfc95367ec7e3fbf4989962fedd/setproctitle-1.3.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e02667f6b9fc1238ba753c0f4b0a37ae184ce8f3bbbc38e115d99646b3f4cd3", size = 32779, upload-time = "2025-09-05T12:49:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/59/27/1a07c38121967061564f5e0884414a5ab11a783260450172d4fc68c15621/setproctitle-1.3.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83fcd271567d133eb9532d3b067c8a75be175b2b3b271e2812921a05303a693f", size = 34578, upload-time = "2025-09-05T12:49:13.393Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d4/725e6353935962d8bb12cbf7e7abba1d0d738c7f6935f90239d8e1ccf913/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13fe37951dda1a45c35d77d06e3da5d90e4f875c4918a7312b3b4556cfa7ff64", size = 32030, upload-time = "2025-09-05T12:49:15.362Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/e4677ae8e1cb0d549ab558b12db10c175a889be0974c589c428fece5433e/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a05509cfb2059e5d2ddff701d38e474169e9ce2a298cf1b6fd5f3a213a553fe5", size = 33363, upload-time = "2025-09-05T12:49:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/55/d4/69ce66e4373a48fdbb37489f3ded476bb393e27f514968c3a69a67343ae0/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6da835e76ae18574859224a75db6e15c4c2aaa66d300a57efeaa4c97ca4c7381", size = 31508, upload-time = "2025-09-05T12:49:18.032Z" }, + { url = "https://files.pythonhosted.org/packages/18/2e/bd03ff02432a181c1787f6fc2a678f53b7dacdd5ded69c318fe1619556e8/setproctitle-1.3.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1607b963e7b53e24ec8a2cb4e0ab3ae591d7c6bf0a160feef0551da63452b37f", size = 32191, upload-time = "2025-09-05T12:49:24.567Z" }, + { url = "https://files.pythonhosted.org/packages/28/78/1e62fc0937a8549f2220445ed2175daacee9b6764c7963b16148119b016d/setproctitle-1.3.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a20fb1a3974e2dab857870cf874b325b8705605cb7e7e8bcbb915bca896f52a9", size = 33203, upload-time = "2025-09-05T12:49:25.871Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/65edc65db3fa3df400cf13b05e9d41a3c77517b4839ce873aa6b4043184f/setproctitle-1.3.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8d961bba676e07d77665204f36cffaa260f526e7b32d07ab3df6a2c1dfb44ba", size = 34963, upload-time = "2025-09-05T12:49:27.044Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/89157e3de997973e306e44152522385f428e16f92f3cf113461489e1e2ee/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:db0fd964fbd3a9f8999b502f65bd2e20883fdb5b1fae3a424e66db9a793ed307", size = 32398, upload-time = "2025-09-05T12:49:28.909Z" }, + { url = "https://files.pythonhosted.org/packages/4a/18/77a765a339ddf046844cb4513353d8e9dcd8183da9cdba6e078713e6b0b2/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:db116850fcf7cca19492030f8d3b4b6e231278e8fe097a043957d22ce1bdf3ee", size = 33657, upload-time = "2025-09-05T12:49:30.323Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/f0b6205c64d74d2a24a58644a38ec77bdbaa6afc13747e75973bf8904932/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:316664d8b24a5c91ee244460bdaf7a74a707adaa9e14fbe0dc0a53168bb9aba1", size = 31836, upload-time = "2025-09-05T12:49:32.309Z" }, + { url = "https://files.pythonhosted.org/packages/d0/99/71630546b9395b095f4082be41165d1078204d1696c2d9baade3de3202d0/setproctitle-1.3.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2906b6c7959cdb75f46159bf0acd8cc9906cf1361c9e1ded0d065fe8f9039629", size = 32932, upload-time = "2025-09-05T12:49:39.271Z" }, + { url = "https://files.pythonhosted.org/packages/50/22/cee06af4ffcfb0e8aba047bd44f5262e644199ae7527ae2c1f672b86495c/setproctitle-1.3.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6915964a6dda07920a1159321dcd6d94fc7fc526f815ca08a8063aeca3c204f1", size = 33736, upload-time = "2025-09-05T12:49:40.565Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/a5949a8bb06ef5e7df214fc393bb2fb6aedf0479b17214e57750dfdd0f24/setproctitle-1.3.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cff72899861c765bd4021d1ff1c68d60edc129711a2fdba77f9cb69ef726a8b6", size = 35605, upload-time = "2025-09-05T12:49:42.362Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3a/50caca532a9343828e3bf5778c7a84d6c737a249b1796d50dd680290594d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7cb05bd446687ff816a3aaaf831047fc4c364feff7ada94a66024f1367b448c", size = 33143, upload-time = "2025-09-05T12:49:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ca/14/b843a251296ce55e2e17c017d6b9f11ce0d3d070e9265de4ecad948b913d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3a57b9a00de8cae7e2a1f7b9f0c2ac7b69372159e16a7708aa2f38f9e5cc987a", size = 34434, upload-time = "2025-09-05T12:49:45.31Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b7/06145c238c0a6d2c4bc881f8be230bb9f36d2bf51aff7bddcb796d5eed67/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d8828b356114f6b308b04afe398ed93803d7fca4a955dd3abe84430e28d33739", size = 32795, upload-time = "2025-09-05T12:49:46.419Z" }, + { url = "https://files.pythonhosted.org/packages/41/89/5b6f2faedd6ced3d3c085a5efbd91380fb1f61f4c12bc42acad37932f4e9/setproctitle-1.3.7-pp310-pypy310_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:502b902a0e4c69031b87870ff4986c290ebbb12d6038a70639f09c331b18efb2", size = 14284, upload-time = "2025-09-05T12:51:18.393Z" }, + { url = "https://files.pythonhosted.org/packages/73/02/b9eadc226195dcfa90eed37afe56b5dd6fa2f0e5220ab8b7867b8862b926/setproctitle-1.3.7-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1704c9e041f2b1dc38f5be4552e141e1432fba3dd52c72eeffd5bc2db04dc65", size = 14286, upload-time = "2025-09-05T12:51:22.61Z" }, +] + +[[package]] +name = "setuptools" +version = "80.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "simplejson" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/91/b0e7a38d63706dde006d1213f9c394ad7702df841c019fb4cf0e3295c58c/simplejson-4.0.1.tar.gz", hash = "sha256:bc13170567a5c856a0e6c16620c0b0388722f7d6382acd8007857624c3dedf3e", size = 115959, upload-time = "2026-04-18T22:46:17.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/65/461422d2f596f2de331c5fbc671135ff863313592ac59f2590bcb2f8dabf/simplejson-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1dcbdf164019b6cdaf043100b44b394a0e79287d3651b7553bd2fc456dc7557b", size = 110085, upload-time = "2026-04-18T22:44:10.161Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/c98bbe63ecac3effd5af83e77438c65aba8f1c6eabe5ada4d157577bb04b/simplejson-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5145b49d2f0fdb81c1229c84001176979662731c6e18a5255a6735941aa25cfd", size = 89650, upload-time = "2026-04-18T22:44:11.3Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c6/88108bb6b080058dcfe1e46eaee2586986fb063772bba3e820782b810957/simplejson-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:35a08c66326400e6b47f91c6bb1a6ef879131b0ca64a56141bea2359d44dad59", size = 90189, upload-time = "2026-04-18T22:44:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/40/23/d25e9ec5aa9f51b8363d2d643d81acd2648440d3d36451ce2fd4545f4d85/simplejson-4.0.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1e6374020ffcd9589d7b1b9c693df24dba3c2c2bcd6da590ac2a4995c7057df5", size = 165507, upload-time = "2026-04-18T22:44:13.998Z" }, + { url = "https://files.pythonhosted.org/packages/e7/64/b4273bfef76e405567fa49307e0490c8c1034783fd4270111ce1207a2236/simplejson-4.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6cf43a90797bca6af4306745f8b0bc9493377dda0f52e1f3ecd393b73078491e", size = 164871, upload-time = "2026-04-18T22:44:15.579Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5a/5cdad85bf0a58ec8d2ced5390bf48f01d3c9e703342658459dc918be2d71/simplejson-4.0.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35c3436e48ae4c983d1910996fed585b8aca68f20eea12d71f975067a905f061", size = 173120, upload-time = "2026-04-18T22:44:16.806Z" }, + { url = "https://files.pythonhosted.org/packages/55/65/86d8b277b14bd765d71e2b9ac6fccc926e65d756947c4ae7dbb20a215019/simplejson-4.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:82284dd4adbdba0b0147860b62bdeed8003aff1b700366e3097213394ee40664", size = 163497, upload-time = "2026-04-18T22:44:18.184Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ed/e30df26965e918ffdbd1cd8fce01f44e6897e601bf1e18085d97fead713a/simplejson-4.0.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5f44a93c117ea38feb2d773289d2900cbafadecc3c2db3c526d0bc9cbaf5f27b", size = 170619, upload-time = "2026-04-18T22:44:19.556Z" }, + { url = "https://files.pythonhosted.org/packages/45/3c/2e99ac18f017b6afec4026e4921afe9a10f2cece4f650673c08e3fd23c1b/simplejson-4.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:48274b6bdc34740f6e309ed6c684e4ac6b7992d00f63a5cad94b384072265f6e", size = 164182, upload-time = "2026-04-18T22:44:21.098Z" }, + { url = "https://files.pythonhosted.org/packages/ce/85/fb7f5d82f620905f442119b8717cf97b622df33cc1a2f9578533337b584e/simplejson-4.0.1-cp310-cp310-win32.whl", hash = "sha256:d450fae24e081703506cdecb7e22477b6c53b055f6a81ac8c13476530ad8d4f5", size = 88000, upload-time = "2026-04-18T22:44:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/00/8b/6fc1e1222164fd3c0606253745f3c34ae2730f97e1279f06e54f23e91c4c/simplejson-4.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:f70d78eba637f3aea597d6959489f5ec4c6f10a42749dc2bb228bcf56298c6fc", size = 90056, upload-time = "2026-04-18T22:44:24.064Z" }, + { url = "https://files.pythonhosted.org/packages/47/39/2957376ab297643d03d799677eb7e539174c340a498582762989664b97ec/simplejson-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aad360482f1c8a779b6d0610d12b66ef01e481ba8af8bfce2693549b102a9875", size = 108970, upload-time = "2026-04-18T22:44:25.184Z" }, + { url = "https://files.pythonhosted.org/packages/db/13/70375fe52b3a7716999c17eb53c3fa88ee6da9d883773f1f819ecb1602b1/simplejson-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:67128132d5d509177a0c9aabcb6f7eea7be9e5a846e1b79e2b5a6b1c7cd6fb49", size = 89159, upload-time = "2026-04-18T22:44:26.326Z" }, + { url = "https://files.pythonhosted.org/packages/12/60/a4772eca1efb4b8e14adcadf9d9b93af4ea8889c61160022066320b8784b/simplejson-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49990f02d0ee7681c01c095de3aa08d379e713f1b84c226cc70ac6701f2698a5", size = 89575, upload-time = "2026-04-18T22:44:27.549Z" }, + { url = "https://files.pythonhosted.org/packages/86/c6/1f5290baaaba5ea1b13ef371b42bd9638c87d562a61fb4e3e3963f6fb955/simplejson-4.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:af7e2afde556391281ca98917b72075a642c2988afb9be9a768ba4e40ff6b5d7", size = 174949, upload-time = "2026-04-18T22:44:28.645Z" }, + { url = "https://files.pythonhosted.org/packages/40/98/f12acc888a3bec439c54c6714d5734e44c2ab60c681f68bd2d0086c6dad5/simplejson-4.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:784476f9048049e0bbd490521994505c2f7e0778d94e1f91ce2ac4d90d09471f", size = 172432, upload-time = "2026-04-18T22:44:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/6f/61/9243858301787d396533d705d4ebdb404a2c75676c7450639cc6b13bb864/simplejson-4.0.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e2dfe5d6fb354e7148c15e1fa29dae87f468e7eb99a0996aab8516bd891f6e34", size = 182000, upload-time = "2026-04-18T22:44:31.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8f/fae6220cdcf7c376d731fc179d49edd2ad0855ad97c9979fb0710e84b894/simplejson-4.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dda7c9c89626ea6b5a23fe8077bee3c37d49626ce76f4e1b4c1c315c62d7127f", size = 170291, upload-time = "2026-04-18T22:44:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/66/9b/8371c0f58214b43746e97b9d91ebcaf3fb03bab1039a29e338f6aab65cad/simplejson-4.0.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3628583e322fb83ed1ba99d7f3fd2e3ee4879e17c1c8d9982f6867eecea5bca7", size = 178934, upload-time = "2026-04-18T22:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/7d/60/e83933422071b8712d1f7d3f720ce975d3988d489cf88c418478ddd7576a/simplejson-4.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71fe98c69b876dfde468e0a7e5fd045b5bc60567b30905c78e52a58c03a52946", size = 173585, upload-time = "2026-04-18T22:44:36.519Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8f/47998fcf7b909a8c8adbb494a88bbaa0e10bf4a48ba1c087e187beb07fe8/simplejson-4.0.1-cp311-cp311-win32.whl", hash = "sha256:2f322ac3cb80711e9fdd5c8e625a10db572e116b5e85e22dd302257c49f02953", size = 87861, upload-time = "2026-04-18T22:44:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d5/54e79dd81d3aac52779ddcf3df0205cedd6e2356f790f81c825fd943a61b/simplejson-4.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:e6593307239ded14f9a528d633ff054f00ebc874ab3c5dea3c49f1988c9b5f06", size = 89921, upload-time = "2026-04-18T22:44:39.604Z" }, + { url = "https://files.pythonhosted.org/packages/ca/21/03ec9b2cceff7966637bd7c96aa4f4df7d59edf4f481fcd76ada5d883f20/simplejson-4.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5be5c2b2ea9e81528b3622f8fd486da04e80e983829f2e69a7acb94302792ef2", size = 110113, upload-time = "2026-04-18T22:44:40.74Z" }, + { url = "https://files.pythonhosted.org/packages/a5/41/77b6f4301566206998ba1911ec334cbe3c574fa5095f846ea99e50e81242/simplejson-4.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aef5610bbbdffc24820daba4f4c7dd1e52bf0f4e016e2860484b562bb964dde5", size = 89804, upload-time = "2026-04-18T22:44:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/8f/48/859d3d12a5f2d6acd1ef1ce75bdd8d25758d63d47223e66eccf3b02461e6/simplejson-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82b34e268f1c7acb9d1bed942b376cd01c418cee9bf05a207ac962c6ce28d29a", size = 90103, upload-time = "2026-04-18T22:44:43.446Z" }, + { url = "https://files.pythonhosted.org/packages/12/17/5135ac3005d2db660182117053d63b97d4ae841efbf77c02ad3aa8b36e3d/simplejson-4.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:905d03e5336b20c6d8ac2a648cb178259807836fb5c8a1ee091ca4601962ec47", size = 183967, upload-time = "2026-04-18T22:44:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e4/3f61fc83ca56256b581c2c55331ba4a51fc50e28d0153f40baebfeb9b90c/simplejson-4.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95c6a6e1f66b6fda46fb9f44809911a90715f05f6df1c6d6db941a8ad2872c35", size = 181209, upload-time = "2026-04-18T22:44:45.899Z" }, + { url = "https://files.pythonhosted.org/packages/3d/26/809a1ed84241a96868fd3915aad173c7e983acb0f7896b7a895635ab1577/simplejson-4.0.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99f12a0d959d7103403e4774189b562f8b1e0529a41561248e2a3c25b6c80797", size = 189815, upload-time = "2026-04-18T22:44:47.175Z" }, + { url = "https://files.pythonhosted.org/packages/08/29/c157b85210df6e286f0f52f8683040d49e654ed51d30126f9eea0f669725/simplejson-4.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5deb4b6c77a02c2e2267135c5afd0d4a9acd3c83c3f73674816d2377b475630", size = 177993, upload-time = "2026-04-18T22:44:48.771Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f5/b8259ecfad5b717199a9ed1c01c54dff0d858934e226dbb6013bfe4a4995/simplejson-4.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c95f7c5c53c3e62bde8c452f41d6795f541e5dc951926275771ff7f2bbae0511", size = 186796, upload-time = "2026-04-18T22:44:50.181Z" }, + { url = "https://files.pythonhosted.org/packages/78/ee/d3dcfaf22d9eb620f8840390a655c22c858e9c2c75073883d7090e9ccf78/simplejson-4.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f057dd469501aff95bb438fc9593d0c2f353dba83f00c0a3db6447ad79f5728", size = 181521, upload-time = "2026-04-18T22:44:51.413Z" }, + { url = "https://files.pythonhosted.org/packages/24/ed/6f13edaff9106e657aab61320e1adceccbd30f50afd877e99d0de292bc48/simplejson-4.0.1-cp312-cp312-win32.whl", hash = "sha256:cc962d5029509f2170ef40e5e7db4df87f71208a931aca6e8bdc0c76e1ff2b3a", size = 88003, upload-time = "2026-04-18T22:44:53.045Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7d/1eebe8ef0682da987d637606aa2eaae0e211010a6fce21abd2c680cf65a6/simplejson-4.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:d2031d87911379c7a778e5f902eeffc161355c9137c695dd12dc219456e976e9", size = 90055, upload-time = "2026-04-18T22:44:54.491Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2d/93a5b862ac29f182a658eb3fc2c98fe28acbb5c05a9076402572c7eb6966/simplejson-4.0.1-py3-none-any.whl", hash = "sha256:dfa6e9923c0ec2880738d09e5ce045741eb6cd4551e261dcd6c3625d26666075", size = 69242, upload-time = "2026-04-18T22:46:16.183Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "soundfile" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751, upload-time = "2025-01-25T09:16:44.235Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250, upload-time = "2025-01-25T09:16:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406, upload-time = "2025-01-25T09:16:49.662Z" }, + { url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729, upload-time = "2025-01-25T09:16:53.018Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646, upload-time = "2025-01-25T09:16:54.872Z" }, + { url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881, upload-time = "2025-01-25T09:16:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" }, +] + +[[package]] +name = "soxr" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/7e/f4b461944662ad75036df65277d6130f9411002bfb79e9df7dff40a31db9/soxr-1.0.0.tar.gz", hash = "sha256:e07ee6c1d659bc6957034f4800c60cb8b98de798823e34d2a2bba1caa85a4509", size = 171415, upload-time = "2025-09-07T13:22:21.317Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/a7/11c36d71595b52fe84a220040ace679035953acf06b83bf2c7117c565d2c/soxr-1.0.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:b876a3156f67c76aef0cff1084eaf4088d9ca584bb569cb993f89a52ec5f399f", size = 206459, upload-time = "2025-09-07T13:21:46.904Z" }, + { url = "https://files.pythonhosted.org/packages/43/5e/8962f2aeea7777d2a6e65a24a2b83c6aea1a28badeda027fd328f7f03bb7/soxr-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d3b957a7b0cc19ae6aa45d40b2181474e53a8dd00efd7bce6bcf4e60e020892", size = 164808, upload-time = "2025-09-07T13:21:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/fc/91/00384166f110a3888ea8efd44523ba7168dd2dc39e3e43c931cc2d069fa9/soxr-1.0.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89685faedebc45af71f08f9957b61cc6143bc94ba43fe38e97067f81e272969", size = 208586, upload-time = "2025-09-07T13:21:50.341Z" }, + { url = "https://files.pythonhosted.org/packages/75/34/e18f1003e242aabed44ed8902534814d3e64209e4d1d874f5b9b67d73cde/soxr-1.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d255741b2f0084fd02d4a2ddd77cd495be9e7e7b6f9dba1c9494f86afefac65b", size = 242310, upload-time = "2025-09-07T13:21:51.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/9c/a1c5ed106b40cc1e2e12cd58831b7f1b61c5fbdb8eceeca4b3a0b0dbef6c/soxr-1.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:158a4a9055958c4b95ef91dbbe280cabb00946b5423b25a9b0ce31bd9e0a271e", size = 173561, upload-time = "2025-09-07T13:21:53.03Z" }, + { url = "https://files.pythonhosted.org/packages/65/ce/a3262bc8733d3a4ce5f660ed88c3d97f4b12658b0909e71334cba1721dcb/soxr-1.0.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:28e19d74a5ef45c0d7000f3c70ec1719e89077379df2a1215058914d9603d2d8", size = 206739, upload-time = "2025-09-07T13:21:54.572Z" }, + { url = "https://files.pythonhosted.org/packages/64/dc/e8cbd100b652697cc9865dbed08832e7e135ff533f453eb6db9e6168d153/soxr-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8dc69fc18884e53b72f6141fdf9d80997edbb4fec9dc2942edcb63abbe0d023", size = 165233, upload-time = "2025-09-07T13:21:55.887Z" }, + { url = "https://files.pythonhosted.org/packages/75/12/4b49611c9ba5e9fe6f807d0a83352516808e8e573f8b4e712fc0c17f3363/soxr-1.0.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f15450e6f65f22f02fcd4c5a9219c873b1e583a73e232805ff160c759a6b586", size = 208867, upload-time = "2025-09-07T13:21:57.076Z" }, + { url = "https://files.pythonhosted.org/packages/cc/70/92146ab970a3ef8c43ac160035b1e52fde5417f89adb10572f7e788d9596/soxr-1.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f73f57452f9df37b4de7a4052789fcbd474a5b28f38bba43278ae4b489d4384", size = 242633, upload-time = "2025-09-07T13:21:58.621Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a7/628479336206959463d08260bffed87905e7ba9e3bd83ca6b405a0736e94/soxr-1.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:9f417c3d69236051cf5a1a7bad7c4bff04eb3d8fcaa24ac1cb06e26c8d48d8dc", size = 173814, upload-time = "2025-09-07T13:21:59.798Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c7/f92b81f1a151c13afb114f57799b86da9330bec844ea5a0d3fe6a8732678/soxr-1.0.0-cp312-abi3-macosx_10_14_x86_64.whl", hash = "sha256:abecf4e39017f3fadb5e051637c272ae5778d838e5c3926a35db36a53e3a607f", size = 205508, upload-time = "2025-09-07T13:22:01.252Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1d/c945fea9d83ea1f2be9d116b3674dbaef26ed090374a77c394b31e3b083b/soxr-1.0.0-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:e973d487ee46aa8023ca00a139db6e09af053a37a032fe22f9ff0cc2e19c94b4", size = 163568, upload-time = "2025-09-07T13:22:03.558Z" }, + { url = "https://files.pythonhosted.org/packages/b5/80/10640970998a1d2199bef6c4d92205f36968cddaf3e4d0e9fe35ddd405bd/soxr-1.0.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8ce273cca101aff3d8c387db5a5a41001ba76ef1837883438d3c652507a9ccc", size = 204707, upload-time = "2025-09-07T13:22:05.125Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/2726603c13c2126cb8ded9e57381b7377f4f0df6ba4408e1af5ddbfdc3dd/soxr-1.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8f2a69686f2856d37823bbb7b78c3d44904f311fe70ba49b893af11d6b6047b", size = 238032, upload-time = "2025-09-07T13:22:06.428Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/530252227f4d0721a5524a936336485dfb429bb206a66baf8e470384f4a2/soxr-1.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:2a3b77b115ae7c478eecdbd060ed4f61beda542dfb70639177ac263aceda42a2", size = 172070, upload-time = "2025-09-07T13:22:07.62Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "starlette", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[package]] +name = "supervisor" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/b5/37e7a3706de436a8a2d75334711dad1afb4ddffab09f25e31d89e467542f/supervisor-4.3.0.tar.gz", hash = "sha256:4a2bf149adf42997e1bb44b70c43b613275ec9852c3edacca86a9166b27e945e", size = 468912, upload-time = "2025-08-23T18:25:02.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/65/5e726c372da8a5e35022a94388b12252710aad0c2351699c3d76ae8dba78/supervisor-4.3.0-py2.py3-none-any.whl", hash = "sha256:0bcb763fddafba410f35cbde226aa7f8514b9fb82eb05a0c85f6588d1c13f8db", size = 320736, upload-time = "2025-08-23T18:25:00.767Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tabulate" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, +] + +[[package]] +name = "tensorboardx" +version = "2.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/a9/fc520ea91ab1f3ba51cbf3fe24f2b6364ed3b49046969e0868d46d6da372/tensorboardx-2.6.5.tar.gz", hash = "sha256:ca176db3997ee8c07d2eb77381225956a3fd1c10c91beafab1f17069adc47017", size = 4770195, upload-time = "2026-04-03T15:40:23.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/0f/69fbab4c30b2f3a76e6de67585ea72a8eccf381751f5c0046b966fde9658/tensorboardx-2.6.5-py3-none-any.whl", hash = "sha256:c10b891d00af306537cb8b58a039b2ba41571f0da06f433a41c4ca8d6abe1373", size = 87510, upload-time = "2026-04-03T15:40:22.111Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "requests", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, + { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, + { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, +] + +[[package]] +name = "tilelang" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apache-tvm-ffi", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "cloudpickle", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "ml-dtypes", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "numpy", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "psutil", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "torch", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "torch-c-dlpack-ext", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "tqdm", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "z3-solver", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/70/5051f65821baa30a3d61fc48f8ba10c776490315e8c90f82559b92089756/tilelang-0.1.9.tar.gz", hash = "sha256:287f727c913bb648fcf6c1968809ba3390e55eeed257a5c6bb9a80bc05966af4", size = 93395292, upload-time = "2026-04-22T09:19:11.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/8a/1cbeee79d62abaa02441c2d00621554e41aa62dbf3b94a4feb3867184b01/tilelang-0.1.9-cp38-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bbccfe9035aed775ffafb6dc25a5994504b24e2c5d95d0f39643edfafa7bf12", size = 45419374, upload-time = "2026-04-22T09:15:56.014Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a7/f4bfb86f87e107703146e703204cec2c0eae2492b633e0052b0ace3febb6/tilelang-0.1.9-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:77ab0ee2f40f66ea015b6b21426d482751e28cbc635ef9d1198cbd6502454a7c", size = 42110365, upload-time = "2026-04-22T09:17:18.292Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "torch" +version = "2.11.0+cu130" +source = { registry = "https://download.pytorch.org/whl/cu130" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "fsspec", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "jinja2", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform != 'darwin'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "sympy", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.11.0%2Bcu130-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4af01fad0822353e766770ff2c7d6bdc2cbcc2ac7fcd6da93a9e3c6f3f932b21", upload-time = "2026-04-27T19:56:16Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.11.0%2Bcu130-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:4c5be01584b7fee22d3c0d04062fd28026044acd07ffd0ee64cbd54b60e62d39", upload-time = "2026-04-27T19:56:40Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.11.0%2Bcu130-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6304535e9e4cd1beeab449e407712602aa473a97e7b310dc5650ef50940bd94f", upload-time = "2026-04-27T19:58:56Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.11.0%2Bcu130-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:225b22e0a4e36ea573d3a68796e6816a160616f67e8b8c55683a88bf7777f4cd", upload-time = "2026-04-27T19:59:21Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.11.0%2Bcu130-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:252f237d417fac3ba59b1635815c1f035a8241f2af038f2c076ed430932d89f1", upload-time = "2026-04-27T20:01:46Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.11.0%2Bcu130-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:96911323dcfcd42028c7e8edde7bdf25bb187753234e8775f0f3f112e86a22db", upload-time = "2026-04-27T20:02:14Z" }, +] + +[[package]] +name = "torch-c-dlpack-ext" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "torch", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/de/921b6491efce5c389a5ef9bbed3d2d6660005840dae488124173180859ab/torch_c_dlpack_ext-0.1.5.tar.gz", hash = "sha256:d06f0357d575d22a168cc77acb9020fc4bae30968ceb6718a055dcbe92bacabe", size = 12913, upload-time = "2026-01-12T11:25:08.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/28/d2d6bf90e01a1f4da3277c9a56d9ecac648b6d6adaa8e20c17f802deb7fb/torch_c_dlpack_ext-0.1.5-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba3d88f0f7d5e1d9c3d4a3179037fc8e261c3b77ac1fad23edc0d3a9214ef193", size = 432066, upload-time = "2026-01-12T11:24:33.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e9/a1f9584a3af4ac6ae5ad5cf86927d8c3a9b6bb50d54e54d19313411216a0/torch_c_dlpack_ext-0.1.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7468df84ec152d930fbc3acf460c44a60b3462b95af3d3a676d133629c7e176", size = 879488, upload-time = "2026-01-12T11:24:34.837Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/64e1e579d107064785549e70758e38a42376ab7e73d86897ed4beab10e74/torch_c_dlpack_ext-0.1.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fba674110e1fab0b176bb5a28223e157db65c90767d4ba74abdbee9f537b0e9d", size = 440949, upload-time = "2026-01-12T11:24:39.716Z" }, + { url = "https://files.pythonhosted.org/packages/64/5c/3e1382a620824f92920ab3fae132d8fb4e85898284c99e0c6a7764e452ce/torch_c_dlpack_ext-0.1.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3448c4f0d64104d0b2e58080a7efa72304a04960c18f338024b80b13cd3eca26", size = 897768, upload-time = "2026-01-12T11:24:41.209Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8d760997307a5c3be4384424667bf31aae0a42060838c532c7d846516175/torch_c_dlpack_ext-0.1.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3562ee411258676f9c38b8ad39306d1c8d027b6a86f6a87c920d2d009a9d1510", size = 443069, upload-time = "2026-01-12T11:24:45.451Z" }, + { url = "https://files.pythonhosted.org/packages/e2/79/a914539b4785f3e44f891aa012a886edb8bc10fe081c440981c57543ce21/torch_c_dlpack_ext-0.1.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6f9da4bb9af70e27facc777458be62e10dbbbddda7672d16138db0553c5a524", size = 897846, upload-time = "2026-01-12T11:24:48.168Z" }, +] + +[[package]] +name = "torch-complex" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/2b/17cb15a383cf2135330371e034d13b9043dc6d8bd07c871b5aa3064fbed1/torch_complex-0.4.4.tar.gz", hash = "sha256:4153fd6b24a0bad689e6f193bfbd00f38283b1890d808bef684ddc6d1f63fd3f", size = 10025, upload-time = "2024-06-28T07:10:28.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/c5/9b4d756a7ada951e9b17dcc636f98ed1073c737ae809b150ef408afb6298/torch_complex-0.4.4-py3-none-any.whl", hash = "sha256:6ab4ecd4f3a16e3adb70a7f7cd2e769a9dfd07d7a8e27d04ff9c621ebbe34b13", size = 9125, upload-time = "2024-06-28T07:10:26.651Z" }, +] + +[[package]] +name = "torchaudio" +version = "2.11.0+cu130" +source = { registry = "https://download.pytorch.org/whl/cu130" } +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu130/torchaudio-2.11.0%2Bcu130-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:408834847432c161aacd4d089c86379484b061df563f46eb05246a52be5aba4f", upload-time = "2026-03-23T15:50:26Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchaudio-2.11.0%2Bcu130-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91497b6525569dcd0871991d32bb4beda90775bcbe9050328ea7b8022106e2fa", upload-time = "2026-03-23T15:50:26Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchaudio-2.11.0%2Bcu130-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:fa4de5984cc666557d4e5eea3c50735b1ccb39b2ae284efb14b721e642eef652", upload-time = "2026-03-23T15:50:26Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchaudio-2.11.0%2Bcu130-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f6e1e3ab31abfa81f5c8801a0eab769d5308514dc644bb39dcf0a9057f364f16", upload-time = "2026-03-23T15:50:26Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchaudio-2.11.0%2Bcu130-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:7171f810887e7cd1a4763974d5a1f2e1466692404315bb70705e0f49fb3a28e0", upload-time = "2026-03-23T15:50:26Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchaudio-2.11.0%2Bcu130-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3fba988f4301fe13547fe5e99c76d9ae36a27e19ded82eeffed9d2456e12edef", upload-time = "2026-03-23T15:50:26Z" }, +] + +[[package]] +name = "torchvision" +version = "0.26.0+cu130" +source = { registry = "https://download.pytorch.org/whl/cu130" } +dependencies = [ + { name = "numpy", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pillow", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "torch", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.26.0%2Bcu130-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3094bed175eee817f9fc61d1c8bdecb1d807c25f49517fbfe60f15d24135fcde", upload-time = "2026-03-23T15:36:25Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.26.0%2Bcu130-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7d820708732d2467caf2ef16c3ad60c8ff402bd05bd246b108ee080f3fbfdc6e", upload-time = "2026-03-23T15:36:25Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.26.0%2Bcu130-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:31f87cd00c09e071980d6a4ce218289a73302ad6a7ce0b3b62a74a4081fc339d", upload-time = "2026-03-23T15:36:25Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.26.0%2Bcu130-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3b53e3b611561e03ac261d06cb3f38782120ad9e0b4cd9f01549799097c713a6", upload-time = "2026-03-23T15:36:25Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.26.0%2Bcu130-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e2b39db78be674ee4ce7e921f54b70e5c281594c9267d981c061684ed38df936", upload-time = "2026-03-23T15:36:26Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torchvision-0.26.0%2Bcu130-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0f030a9bd8ada1a31b7111ea1589c1ecb5fa0884fee700a203e731b4cf378a98", upload-time = "2026-03-23T15:36:26Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "transformers" +version = "4.57.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/ba/b1b04f4b291a3205d95ebd24465de0e5bf010a2df27a4e58a9b5f039d8f2/triton-3.6.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c723cfb12f6842a0ae94ac307dba7e7a44741d720a40cf0e270ed4a4e3be781", size = 175972180, upload-time = "2026-01-20T16:15:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f7/f1c9d3424ab199ac53c2da567b859bcddbb9c9e7154805119f8bd95ec36f/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea", size = 188105201, upload-time = "2026-01-20T16:00:29.272Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2c/96f92f3c60387e14cc45aed49487f3486f89ea27106c1b1376913c62abe4/triton-3.6.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651", size = 176081190, upload-time = "2026-01-20T16:16:00.523Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "click", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "rich", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "shellingham", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, +] + +[[package]] +name = "umap-learn" +version = "0.5.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numba", version = "0.61.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numba", version = "0.65.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "numpy" }, + { name = "pynndescent" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/ee/af4171241117f85c74b5ca6448ea1033cc28d599c13651d67289bacd4083/umap_learn-0.5.12.tar.gz", hash = "sha256:6aff02ecac5f2aad9f3c65ee518d7ae93e1a985ae38721fdcffceee4232c33c7", size = 96672, upload-time = "2026-04-08T20:03:54.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/98/f63318ccbe75c810011fe9233884c5d348d94d90005de1b79e5f93bef9c0/umap_learn-0.5.12-py3-none-any.whl", hash = "sha256:f2a85d2a2adcb52b541bed9b27a23ca169b56bb1b23283abeebfb8dfb8a42fe5", size = 91849, upload-time = "2026-04-08T20:03:52.561Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, +] + +[[package]] +name = "vllm" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "anthropic", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "apache-tvm-ffi", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "blake3", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "cachetools", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "cbor2", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "cloudpickle", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "compressed-tensors", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "depyf", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "diskcache", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "einops", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "fastapi", extra = ["standard"], marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "fastsafetensors", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "filelock", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "flashinfer-cubin", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "flashinfer-python", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "gguf", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "ijson", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "lark", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "llguidance", marker = "(python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'emscripten') or (python_full_version < '3.11' and platform_machine == 'arm64' and sys_platform == 'emscripten') or (python_full_version < '3.11' and platform_machine == 'ppc64le' and sys_platform == 'emscripten') or (python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'emscripten') or (python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'win32') or (python_full_version < '3.11' and platform_machine == 'arm64' and sys_platform == 'win32') or (python_full_version < '3.11' and platform_machine == 'ppc64le' and sys_platform == 'win32') or (python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32') or (platform_machine == 'arm64' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32') or (platform_machine == 'ppc64le' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32') or (platform_machine == 'x86_64' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "lm-format-enforcer", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "mcp", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "mistral-common", extra = ["image"], marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "model-hosting-container-standards", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "msgspec", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "ninja", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "numba", version = "0.65.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "numpy", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-cudnn-frontend", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-cutlass-dsl", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "openai", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "openai-harmony", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "opencv-python-headless", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "opentelemetry-api", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "opentelemetry-exporter-otlp", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "opentelemetry-sdk", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "opentelemetry-semantic-conventions-ai", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "outlines-core", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "partial-json-parser", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pillow", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "prometheus-client", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "prometheus-fastapi-instrumentator", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "protobuf", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "psutil", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "py-cpuinfo", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pybase64", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pydantic", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "python-json-logger", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pyyaml", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pyzmq", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "quack-kernels", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "regex", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "requests", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "sentencepiece", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "setproctitle", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "setuptools", marker = "python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "six", marker = "python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "tiktoken", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "tilelang", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "tokenizers", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "torch", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "torchaudio", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "torchvision", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "tqdm", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "transformers", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "watchfiles", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "xgrammar", marker = "(python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'emscripten') or (python_full_version < '3.11' and platform_machine == 'arm64' and sys_platform == 'emscripten') or (python_full_version < '3.11' and platform_machine == 'ppc64le' and sys_platform == 'emscripten') or (python_full_version < '3.11' and platform_machine == 's390x' and sys_platform == 'emscripten') or (python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'emscripten') or (python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'win32') or (python_full_version < '3.11' and platform_machine == 'arm64' and sys_platform == 'win32') or (python_full_version < '3.11' and platform_machine == 'ppc64le' and sys_platform == 'win32') or (python_full_version < '3.11' and platform_machine == 's390x' and sys_platform == 'win32') or (python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32') or (platform_machine == 'arm64' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32') or (platform_machine == 'ppc64le' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32') or (platform_machine == 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32') or (platform_machine == 'x86_64' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/80/9798ce5e16af5754183ef33a63dc27017e2b51c87f51cc741832ce47a2d5/vllm-0.20.0.tar.gz", hash = "sha256:a6d50152936ee292455af3ffbe359f7a284ac43bf3b68caccf29f368e196cc72", size = 33508260, upload-time = "2026-04-27T11:08:04.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/5b/26379d3c522379373e50b9f77adf55eb94f4a0f62a6c8e3e7fe3f0bf0d39/vllm-0.20.0-cp38-abi3-manylinux_2_35_aarch64.whl", hash = "sha256:29a135ca0d70650f057f15c7c0b560d24659524c771f70fbddc24597c861c118", size = 235776358, upload-time = "2026-04-27T11:07:22.058Z" }, + { url = "https://files.pythonhosted.org/packages/47/bb/cb02d1e9679fce892a674f86caee25acc9ddd64d7dafa4cfe29e899993a8/vllm-0.20.0-cp38-abi3-manylinux_2_35_x86_64.whl", hash = "sha256:24d28892e210200f6e1bd13f699c42a74cd2bb7364c11248e2348f677c7f6dfb", size = 244415937, upload-time = "2026-04-27T11:07:48.135Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, +] + +[[package]] +name = "xgrammar" +version = "0.1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pydantic", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "torch", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "transformers", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/43/e5dfddb1d2a4fccf3e3a88f103e88698cdefc3182f4e169a359ffe1c1794/xgrammar-0.1.33.tar.gz", hash = "sha256:8dbe5fc3d76651ab1fac7a68fc2a118b885fa0ec7189927fb6e0dce0081aea99", size = 2398956, upload-time = "2026-03-27T10:16:36.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/7f/61485241556663db7cabefd1ffc6039ec42145821c8c8008996b6d96c3aa/xgrammar-0.1.33-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb61a21f44f0c3bd4884426b2d18479622202e2fd0928b63c10a0168972c502e", size = 42131816, upload-time = "2026-03-27T10:14:39.76Z" }, + { url = "https://files.pythonhosted.org/packages/62/a9/69fbbcde74ac0ccc914e0eb222ac0916688fe291f9f234852dc77912a1bf/xgrammar-0.1.33-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b8aad12a149e3b689b5c4aef8ff529e58e55aee1406bd372c850b504131a8b7", size = 42204408, upload-time = "2026-03-27T10:14:43.692Z" }, + { url = "https://files.pythonhosted.org/packages/4b/16/f8297e0e3b468636d8e0190002badfe4a6d8d1c2af295fea2d164e7b5a8a/xgrammar-0.1.33-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5f561e676df8c9e941c7a2f6df9612bbf645bf1fc714b4a9282cf75cff532f8", size = 42132308, upload-time = "2026-03-27T10:14:58.545Z" }, + { url = "https://files.pythonhosted.org/packages/12/e0/629b892a3810446097635dd1be7e4d977107c42232efb229d70e5c827227/xgrammar-0.1.33-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bc9151d9f0d05862c253998c533f04c000273f57180fb6a4e3623e321fd47db", size = 42204526, upload-time = "2026-03-27T10:15:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/4e/04/43d4baca876f5ae1b45897ec30a59801a2da37f16da1fcd85f9555e4c125/xgrammar-0.1.33-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c803e60d791854c5d1f271ece7e1f34d73c82dd4a8b2a06b7af5331482a78ac", size = 42133168, upload-time = "2026-03-27T10:15:16.994Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a8/672833a3cff027253793aa999401d8364896ebf396967e475c7a878b895f/xgrammar-0.1.33-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b8eaa533282a0efb0835db6998ae72e7b3c7875d7a52e360ffebff9b78c30a", size = 42205803, upload-time = "2026-03-27T10:15:21.599Z" }, +] + +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/ee/f9f1d656ad168681bb0f6b092372c1e533c4416b8069b1896a175c46e484/xxhash-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71", size = 32845, upload-time = "2025-10-02T14:33:51.573Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/93508d9460b292c74a09b83d16750c52a0ead89c51eea9951cb97a60d959/xxhash-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d", size = 30807, upload-time = "2025-10-02T14:33:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/28c93a3662f2d200c70704efe74aab9640e824f8ce330d8d3943bf7c9b3c/xxhash-3.6.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8", size = 193786, upload-time = "2025-10-02T14:33:54.272Z" }, + { url = "https://files.pythonhosted.org/packages/c1/96/fec0be9bb4b8f5d9c57d76380a366f31a1781fb802f76fc7cda6c84893c7/xxhash-3.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058", size = 212830, upload-time = "2025-10-02T14:33:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a0/c706845ba77b9611f81fd2e93fad9859346b026e8445e76f8c6fd057cc6d/xxhash-3.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2", size = 211606, upload-time = "2025-10-02T14:33:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/164126a2999e5045f04a69257eea946c0dc3e86541b400d4385d646b53d7/xxhash-3.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc", size = 444872, upload-time = "2025-10-02T14:33:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4b/55ab404c56cd70a2cf5ecfe484838865d0fea5627365c6c8ca156bd09c8f/xxhash-3.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc", size = 193217, upload-time = "2025-10-02T14:33:59.724Z" }, + { url = "https://files.pythonhosted.org/packages/45/e6/52abf06bac316db33aa269091ae7311bd53cfc6f4b120ae77bac1b348091/xxhash-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07", size = 210139, upload-time = "2025-10-02T14:34:02.041Z" }, + { url = "https://files.pythonhosted.org/packages/34/37/db94d490b8691236d356bc249c08819cbcef9273a1a30acf1254ff9ce157/xxhash-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4", size = 197669, upload-time = "2025-10-02T14:34:03.664Z" }, + { url = "https://files.pythonhosted.org/packages/b7/36/c4f219ef4a17a4f7a64ed3569bc2b5a9c8311abdb22249ac96093625b1a4/xxhash-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06", size = 210018, upload-time = "2025-10-02T14:34:05.325Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/bfac889a374fc2fc439a69223d1750eed2e18a7db8514737ab630534fa08/xxhash-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4", size = 413058, upload-time = "2025-10-02T14:34:06.925Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d1/555d8447e0dd32ad0930a249a522bb2e289f0d08b6b16204cfa42c1f5a0c/xxhash-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b", size = 190628, upload-time = "2025-10-02T14:34:08.669Z" }, + { url = "https://files.pythonhosted.org/packages/d1/15/8751330b5186cedc4ed4b597989882ea05e0408b53fa47bcb46a6125bfc6/xxhash-3.6.0-cp310-cp310-win32.whl", hash = "sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b", size = 30577, upload-time = "2025-10-02T14:34:10.234Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cc/53f87e8b5871a6eb2ff7e89c48c66093bda2be52315a8161ddc54ea550c4/xxhash-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb", size = 31487, upload-time = "2025-10-02T14:34:11.618Z" }, + { url = "https://files.pythonhosted.org/packages/9f/00/60f9ea3bb697667a14314d7269956f58bf56bb73864f8f8d52a3c2535e9a/xxhash-3.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d", size = 27863, upload-time = "2025-10-02T14:34:12.619Z" }, + { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, + { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, + { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, + { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, + { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, + { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, + { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, + { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, + { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, + { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, + { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0d/9cc638702f6fc3c7a3685bcc8cf2a9ed7d6206e932a49f5242658047ef51/yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", size = 123764, upload-time = "2026-03-01T22:04:09.7Z" }, + { url = "https://files.pythonhosted.org/packages/7a/35/5a553687c5793df5429cd1db45909d4f3af7eee90014888c208d086a44f0/yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", size = 86282, upload-time = "2026-03-01T22:04:11.892Z" }, + { url = "https://files.pythonhosted.org/packages/68/2e/c5a2234238f8ce37a8312b52801ee74117f576b1539eec8404a480434acc/yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", size = 86053, upload-time = "2026-03-01T22:04:13.292Z" }, + { url = "https://files.pythonhosted.org/packages/74/3f/bbd8ff36fb038622797ffbaf7db314918bb4d76f1cc8a4f9ca7a55fe5195/yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", size = 99395, upload-time = "2026-03-01T22:04:15.133Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/9516bc4e269d2a3ec9c6779fcdeac51ce5b3a9b0156f06ac7152e5bba864/yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", size = 92143, upload-time = "2026-03-01T22:04:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/c7/63/88802d1f6b1cb1fc67d67a58cd0cf8a1790de4ce7946e434240f1d60ab4a/yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", size = 107643, upload-time = "2026-03-01T22:04:18.519Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/4f9b838f4d8bdd6f0f385aed8bbf21c71ed11a0b9983305c302cbd557815/yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", size = 108700, upload-time = "2026-03-01T22:04:20.373Z" }, + { url = "https://files.pythonhosted.org/packages/50/12/95a1d33f04a79c402664070d43b8b9f72dc18914e135b345b611b0b1f8cc/yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", size = 102769, upload-time = "2026-03-01T22:04:23.055Z" }, + { url = "https://files.pythonhosted.org/packages/86/65/91a0285f51321369fd1a8308aa19207520c5f0587772cfc2e03fc2467e90/yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", size = 101114, upload-time = "2026-03-01T22:04:25.031Z" }, + { url = "https://files.pythonhosted.org/packages/58/80/c7c8244fc3e5bc483dc71a09560f43b619fab29301a0f0a8f936e42865c7/yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", size = 98883, upload-time = "2026-03-01T22:04:27.281Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/71ca9cc9ca79c0b7d491216177d1aed559d632947b8ffb0ee60f7d8b23e3/yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", size = 94172, upload-time = "2026-03-01T22:04:28.554Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3f/6c6c8a0fe29c26fb2db2e8d32195bb84ec1bfb8f1d32e7f73b787fcf349b/yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", size = 107010, upload-time = "2026-03-01T22:04:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/56/38/12730c05e5ad40a76374d440ed8b0899729a96c250516d91c620a6e38fc2/yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", size = 100285, upload-time = "2026-03-01T22:04:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/34/92/6a7be9239f2347234e027284e7a5f74b1140cc86575e7b469d13fba1ebfe/yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", size = 108230, upload-time = "2026-03-01T22:04:33.844Z" }, + { url = "https://files.pythonhosted.org/packages/5e/81/4aebccfa9376bd98b9d8bfad20621a57d3e8cfc5b8631c1fa5f62cdd03f4/yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", size = 103008, upload-time = "2026-03-01T22:04:35.856Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/0b4e3edcec794a86b853b0c6396c0a888d72dfce19b2d88c02ac289fb6c1/yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", size = 83073, upload-time = "2026-03-01T22:04:38.268Z" }, + { url = "https://files.pythonhosted.org/packages/a0/71/ad95c33da18897e4c636528bbc24a1dd23fe16797de8bc4ec667b8db0ba4/yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", size = 87328, upload-time = "2026-03-01T22:04:39.558Z" }, + { url = "https://files.pythonhosted.org/packages/e2/14/dfa369523c79bccf9c9c746b0a63eb31f65db9418ac01275f7950962e504/yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", size = 82463, upload-time = "2026-03-01T22:04:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, + { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, + { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, + { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] + +[[package]] +name = "z3-solver" +version = "4.15.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/8e/0c8f17309549d2e5cde9a3ccefa6365437f1e7bafe71878eaf9478e47b18/z3_solver-4.15.4.0.tar.gz", hash = "sha256:928c29b58c4eb62106da51c1914f6a4a55d0441f8f48a81b9da07950434a8946", size = 5018600, upload-time = "2025-10-29T18:12:03.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/c9/bb51a96af0091324c81b803f16c49f719f9f6ea0b0bb52200f5c97ec4892/z3_solver-4.15.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e103a6f203f505b8b8b8e5c931cc407c95b61556512d4921c1ddc0b3f41b08e", size = 29268352, upload-time = "2025-10-29T18:11:53.032Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2e/0b49f7e4e53817cfb09a0f6585012b782dfe0b666e8abefcb4fac0570606/z3_solver-4.15.4.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:62c7e9cbdd711932301f29919ad9158de9b2f58b4d281dd259bbcd0a2f408ba1", size = 27226534, upload-time = "2025-10-29T18:11:55.59Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, +] diff --git a/vendor/qwenasr/.cargo/config.toml b/vendor/qwenasr/.cargo/config.toml new file mode 100644 index 0000000..4e89860 --- /dev/null +++ b/vendor/qwenasr/.cargo/config.toml @@ -0,0 +1,14 @@ +# iOS targets +[target.aarch64-apple-ios] +rustflags = ["-C", "target-cpu=apple-a14"] + +[target.aarch64-apple-ios-sim] +rustflags = ["-C", "target-cpu=apple-a14"] + +# Android targets +[target.aarch64-linux-android] +# Set linker via environment: CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android28-clang +rustflags = ["-C", "target-cpu=cortex-a76"] + +[target.armv7-linux-androideabi] +rustflags = ["-C", "target-cpu=cortex-a53"] diff --git a/vendor/qwenasr/Cargo.lock b/vendor/qwenasr/Cargo.lock new file mode 100644 index 0000000..2aec65f --- /dev/null +++ b/vendor/qwenasr/Cargo.lock @@ -0,0 +1,875 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" +dependencies = [ + "bindgen", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ctrlc" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "qwen-asr" +version = "0.5.0" +dependencies = [ + "libc", +] + +[[package]] +name = "qwen-asr-cli" +version = "0.6.0" +dependencies = [ + "coreaudio-sys", + "ctrlc", + "qwen-asr", + "ureq", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/vendor/qwenasr/Cargo.toml b/vendor/qwenasr/Cargo.toml new file mode 100644 index 0000000..c9f7b63 --- /dev/null +++ b/vendor/qwenasr/Cargo.toml @@ -0,0 +1,17 @@ +[workspace] +resolver = "2" +members = ["crates/qwen-asr", "crates/qwen-asr-cli"] +exclude = ["flutter/qwen_asr/rust"] # Cargokit builds independently + +[workspace.package] +version = "0.1.2" +edition = "2021" +license = "MIT" + +[workspace.dependencies] +# x-release-please-version +qwen-asr = { version = "0.5.0", path = "crates/qwen-asr" } + +[profile.release] +opt-level = 3 +lto = "thin" diff --git a/vendor/qwenasr/LICENSE b/vendor/qwenasr/LICENSE new file mode 100644 index 0000000..83f93ec --- /dev/null +++ b/vendor/qwenasr/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Li Zhuo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/qwenasr/VENDORED_FROM.md b/vendor/qwenasr/VENDORED_FROM.md new file mode 100644 index 0000000..93d8b97 --- /dev/null +++ b/vendor/qwenasr/VENDORED_FROM.md @@ -0,0 +1,14 @@ +# Vendored Source + +This directory vendors the upstream `QwenASR` Rust workspace into this repository. + +- Upstream repository: `https://github.com/huanglizhuo/QwenASR` +- Vendored commit: `4e85a19b05f034e106a345d279c68f50df718ab8` +- License: `MIT` + +Local modifications included directly in the vendored source: + +- Expose `qwen_asr_stream_set_past_text` in the C API for correct streaming behavior. +- Expose `qwen_asr_force_align_file` in the C API for service-side word timestamp alignment. + +The vendored source is now the build source of truth for the CPU Rust backend. diff --git a/vendor/qwenasr/crates/qwen-asr-cli/CHANGELOG.md b/vendor/qwenasr/crates/qwen-asr-cli/CHANGELOG.md new file mode 100644 index 0000000..acc1378 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr-cli/CHANGELOG.md @@ -0,0 +1,97 @@ +# Changelog + +## [0.6.0](https://github.com/huanglizhuo/QwenASR/compare/qwen-asr-cli-v0.5.0...qwen-asr-cli-v0.6.0) (2026-03-21) + + +### Features + +* add missing parameter to qwen asr offline model ([f56e8b1](https://github.com/huanglizhuo/QwenASR/commit/f56e8b1e58731344fad92a7ed38c59a9f09267f6)) +* add missing parameter to qwen asr offline model ([6d1e38d](https://github.com/huanglizhuo/QwenASR/commit/6d1e38da19cbae46c2afe2e1af03a5d437679ef8)) +* improve the live stream performace on macos ([ba47230](https://github.com/huanglizhuo/QwenASR/commit/ba47230403f897bde2486b3235cfd3f5ca24e293)) +* publish qwen-asr-cli binary to crates.io ([31ae992](https://github.com/huanglizhuo/QwenASR/commit/31ae99221d71fdcd6c40b6cfe4d77e7f67642b76)) +* support live from blackhold for macos for qwen-asr-cli ([724ead1](https://github.com/huanglizhuo/QwenASR/commit/724ead1fe121d0ed0a0f7ef874142f665e7d0da3)) + + +### Bug Fixes + +* bump qwen-asr dependency version for cli ([efec6b1](https://github.com/huanglizhuo/QwenASR/commit/efec6b14dfb07934c7e6cbb459d26573dcfe5912)) +* clean up the code ([346d112](https://github.com/huanglizhuo/QwenASR/commit/346d112595c0d93f58c54339a7f32ca6e1e648d8)) +* publish 0.2.3 with tag-driven flow ([3637ec8](https://github.com/huanglizhuo/QwenASR/commit/3637ec80f5519ecbd0a034f6c1f23f78156cd0fe)) +* publish 0.2.3 with tag-driven flow ([e7bbd18](https://github.com/huanglizhuo/QwenASR/commit/e7bbd18dc009c3bd87f32e2346c196f65c618b19)) +* **qwen-asr-cli:** add homepage metadata to trigger release ([642c6a2](https://github.com/huanglizhuo/QwenASR/commit/642c6a2dbdcd01c4351d6a56dfdf2c6b99fa5488)) +* **qwen-asr-cli:** update qwen-asr dependency to v0.4.2 ([d9915ea](https://github.com/huanglizhuo/QwenASR/commit/d9915ea8eba41f3a7a129d0ea5c6cea939a33c6d)) +* **qwen-asr-cli:** use workspace dependency to keep qwen-asr version in sync ([5ac7e01](https://github.com/huanglizhuo/QwenASR/commit/5ac7e01bdaf95b3026eec7a12f04e9f5d78d0f3b)) +* release flow for cli ([8dfc4b7](https://github.com/huanglizhuo/QwenASR/commit/8dfc4b7820ceef98efd992974ab3a0b7fcdd9b10)) +* trigger patch release 0.2.1 for flutter ([b5785f9](https://github.com/huanglizhuo/QwenASR/commit/b5785f9e0a6e4cab3a4796bbd1bd401876ea5926)) +* update the release flow to support PAT ([2b9be6c](https://github.com/huanglizhuo/QwenASR/commit/2b9be6c21b7e74e51bf1d1f15e6959679db70542)) + +## [0.4.0](https://github.com/huanglizhuo/QwenASR/compare/qwen-asr-cli-v0.3.4...qwen-asr-cli-v0.4.0) (2026-03-19) + + +### Features + +* add missing parameter to qwen asr offline model ([f56e8b1](https://github.com/huanglizhuo/QwenASR/commit/f56e8b1e58731344fad92a7ed38c59a9f09267f6)) +* add missing parameter to qwen asr offline model ([6d1e38d](https://github.com/huanglizhuo/QwenASR/commit/6d1e38da19cbae46c2afe2e1af03a5d437679ef8)) +* improve the live stream performace on macos ([ba47230](https://github.com/huanglizhuo/QwenASR/commit/ba47230403f897bde2486b3235cfd3f5ca24e293)) +* publish qwen-asr-cli binary to crates.io ([31ae992](https://github.com/huanglizhuo/QwenASR/commit/31ae99221d71fdcd6c40b6cfe4d77e7f67642b76)) +* support live from blackhold for macos for qwen-asr-cli ([724ead1](https://github.com/huanglizhuo/QwenASR/commit/724ead1fe121d0ed0a0f7ef874142f665e7d0da3)) + + +### Bug Fixes + +* bump qwen-asr dependency version for cli ([efec6b1](https://github.com/huanglizhuo/QwenASR/commit/efec6b14dfb07934c7e6cbb459d26573dcfe5912)) +* clean up the code ([346d112](https://github.com/huanglizhuo/QwenASR/commit/346d112595c0d93f58c54339a7f32ca6e1e648d8)) +* publish 0.2.3 with tag-driven flow ([3637ec8](https://github.com/huanglizhuo/QwenASR/commit/3637ec80f5519ecbd0a034f6c1f23f78156cd0fe)) +* publish 0.2.3 with tag-driven flow ([e7bbd18](https://github.com/huanglizhuo/QwenASR/commit/e7bbd18dc009c3bd87f32e2346c196f65c618b19)) +* **qwen-asr-cli:** add homepage metadata to trigger release ([642c6a2](https://github.com/huanglizhuo/QwenASR/commit/642c6a2dbdcd01c4351d6a56dfdf2c6b99fa5488)) +* **qwen-asr-cli:** update qwen-asr dependency to v0.4.2 ([d9915ea](https://github.com/huanglizhuo/QwenASR/commit/d9915ea8eba41f3a7a129d0ea5c6cea939a33c6d)) +* release flow for cli ([8dfc4b7](https://github.com/huanglizhuo/QwenASR/commit/8dfc4b7820ceef98efd992974ab3a0b7fcdd9b10)) +* trigger patch release 0.2.1 for flutter ([b5785f9](https://github.com/huanglizhuo/QwenASR/commit/b5785f9e0a6e4cab3a4796bbd1bd401876ea5926)) +* update the release flow to support PAT ([2b9be6c](https://github.com/huanglizhuo/QwenASR/commit/2b9be6c21b7e74e51bf1d1f15e6959679db70542)) + +## [0.3.4](https://github.com/huanglizhuo/QwenASR/compare/qwen-asr-cli-v0.3.3...qwen-asr-cli-v0.3.4) (2026-03-19) + + +### Bug Fixes + +* **qwen-asr-cli:** update qwen-asr dependency to v0.4.2 ([d9915ea](https://github.com/huanglizhuo/QwenASR/commit/d9915ea8eba41f3a7a129d0ea5c6cea939a33c6d)) + +## [0.3.3](https://github.com/huanglizhuo/QwenASR/compare/qwen-asr-cli-v0.3.2...qwen-asr-cli-v0.3.3) (2026-03-14) + + +### Bug Fixes + +* clean up the code ([346d112](https://github.com/huanglizhuo/QwenASR/commit/346d112595c0d93f58c54339a7f32ca6e1e648d8)) + +## [0.3.2](https://github.com/huanglizhuo/QwenASR/compare/qwen-asr-cli-v0.3.1...qwen-asr-cli-v0.3.2) (2026-03-13) + + +### Bug Fixes + +* release flow for cli ([8dfc4b7](https://github.com/huanglizhuo/QwenASR/commit/8dfc4b7820ceef98efd992974ab3a0b7fcdd9b10)) + +## [0.3.1](https://github.com/huanglizhuo/QwenASR/compare/qwen-asr-cli-v0.3.0...qwen-asr-cli-v0.3.1) (2026-02-23) + + +### Bug Fixes + +* bump qwen-asr dependency version for cli ([efec6b1](https://github.com/huanglizhuo/QwenASR/commit/efec6b14dfb07934c7e6cbb459d26573dcfe5912)) + +## [0.3.0](https://github.com/huanglizhuo/QwenASR/compare/qwen-asr-cli-v0.2.4...qwen-asr-cli-v0.3.0) (2026-02-23) + + +### Features + +* add missing parameter to qwen asr offline model ([f56e8b1](https://github.com/huanglizhuo/QwenASR/commit/f56e8b1e58731344fad92a7ed38c59a9f09267f6)) +* add missing parameter to qwen asr offline model ([6d1e38d](https://github.com/huanglizhuo/QwenASR/commit/6d1e38da19cbae46c2afe2e1af03a5d437679ef8)) +* improve the live stream performace on macos ([ba47230](https://github.com/huanglizhuo/QwenASR/commit/ba47230403f897bde2486b3235cfd3f5ca24e293)) +* publish qwen-asr-cli binary to crates.io ([31ae992](https://github.com/huanglizhuo/QwenASR/commit/31ae99221d71fdcd6c40b6cfe4d77e7f67642b76)) +* support live from blackhold for macos for qwen-asr-cli ([724ead1](https://github.com/huanglizhuo/QwenASR/commit/724ead1fe121d0ed0a0f7ef874142f665e7d0da3)) + + +### Bug Fixes + +* publish 0.2.3 with tag-driven flow ([3637ec8](https://github.com/huanglizhuo/QwenASR/commit/3637ec80f5519ecbd0a034f6c1f23f78156cd0fe)) +* publish 0.2.3 with tag-driven flow ([e7bbd18](https://github.com/huanglizhuo/QwenASR/commit/e7bbd18dc009c3bd87f32e2346c196f65c618b19)) +* trigger patch release 0.2.1 for flutter ([b5785f9](https://github.com/huanglizhuo/QwenASR/commit/b5785f9e0a6e4cab3a4796bbd1bd401876ea5926)) +* update the release flow to support PAT ([2b9be6c](https://github.com/huanglizhuo/QwenASR/commit/2b9be6c21b7e74e51bf1d1f15e6959679db70542)) diff --git a/vendor/qwenasr/crates/qwen-asr-cli/Cargo.toml b/vendor/qwenasr/crates/qwen-asr-cli/Cargo.toml new file mode 100644 index 0000000..56f318d --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr-cli/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "qwen-asr-cli" +version = "0.6.0" +edition.workspace = true +license.workspace = true +description = "CLI for qwen-asr: Qwen3-ASR speech-to-text" +keywords = ["asr", "speech-recognition", "qwen", "cli"] +categories = ["command-line-utilities", "multimedia::audio"] +repository = "https://github.com/huanglizhuo/QwenASR" +homepage = "https://github.com/huanglizhuo/QwenASR" +readme = "README.md" + +[[bin]] +name = "qwen-asr" +path = "src/main.rs" + +[dependencies] +qwen-asr = { workspace = true } +ureq = "2" +ctrlc = "3" + +[target.'cfg(target_os = "macos")'.dependencies] +coreaudio-sys = "0.2" diff --git a/vendor/qwenasr/crates/qwen-asr-cli/README.md b/vendor/qwenasr/crates/qwen-asr-cli/README.md new file mode 100644 index 0000000..d80b35f --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr-cli/README.md @@ -0,0 +1,48 @@ +# qwen-asr-cli + +CLI for [qwen-asr](https://crates.io/crates/qwen-asr): CPU-only Qwen3-ASR speech-to-text in pure Rust. + +## Install + +```bash +cargo install qwen-asr-cli + +# Recommended: enable native CPU SIMD tuning +RUSTFLAGS="-C target-cpu=native" cargo install qwen-asr-cli +``` + +vDSP/Accelerate is auto-enabled on macOS via default features. + +## Download Model + +```bash +qwen-asr download qwen3-asr-0.6b +``` + +## Usage + +```bash +# Transcribe a file +qwen-asr -d qwen3-asr-0.6b -i audio.wav + +# Streaming mode +qwen-asr -d qwen3-asr-0.6b -i audio.wav --stream + +# Live capture (macOS) +qwen-asr -d qwen3-asr-0.6b --live --stream --device "BlackHole 2ch" + +# VAD live mode (macOS) +qwen-asr -d qwen3-asr-0.6b --live --vad --device "BlackHole 2ch" + +# Forced alignment +qwen-asr -d qwen3-aligner-0.6b -i audio.wav --align "Hello world" + +# All options +qwen-asr -h +``` + +See the [project README](https://github.com/huanglizhuo/QwenASR) for full documentation. + +## License + +MIT diff --git a/vendor/qwenasr/crates/qwen-asr-cli/src/download.rs b/vendor/qwenasr/crates/qwen-asr-cli/src/download.rs new file mode 100644 index 0000000..38abf88 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr-cli/src/download.rs @@ -0,0 +1,316 @@ +//! Model download from HuggingFace with progress display. + +use std::fs; +use std::io::{self, Read, Write}; +use std::path::Path; + +// ======================================================================== +// Model Registry +// ======================================================================== + +pub struct ModelInfo { + pub name: &'static str, + pub repo: &'static str, + pub files: &'static [&'static str], + pub description: &'static str, +} + +pub const KNOWN_MODELS: &[ModelInfo] = &[ + ModelInfo { + name: "qwen3-asr-0.6b", + repo: "Qwen/Qwen3-ASR-0.6B", + files: &["model.safetensors", "vocab.json", "merges.txt"], + description: "Qwen3-ASR 0.6B — fast, ~490 MB", + }, + ModelInfo { + name: "qwen3-asr-1.7b", + repo: "Qwen/Qwen3-ASR-1.7B", + files: &[ + "model.safetensors.index.json", + "model-00001-of-00002.safetensors", + "model-00002-of-00002.safetensors", + "vocab.json", + "merges.txt", + ], + description: "Qwen3-ASR 1.7B — higher accuracy, ~3.4 GB", + }, + ModelInfo { + name: "qwen3-aligner-0.6b", + repo: "Qwen/Qwen3-ASR-ForcedAligner-0.6B", + files: &[ + "model.safetensors.index.json", + "model-00001-of-00002.safetensors", + "model-00002-of-00002.safetensors", + "vocab.json", + "merges.txt", + ], + description: "Qwen3-ASR ForcedAligner 0.6B — word-level timestamps, ~1.6 GB", + }, +]; + +pub fn find_model(name: &str) -> Option<&'static ModelInfo> { + let name_lower = name.to_lowercase(); + KNOWN_MODELS.iter().find(|m| m.name == name_lower) +} + +// ======================================================================== +// List Models +// ======================================================================== + +pub fn list_models() { + eprintln!("Available models:\n"); + for m in KNOWN_MODELS { + eprintln!(" {:<24} {}", m.name, m.description); + } + eprintln!(); + eprintln!("Usage: qwen-asr download [--output ]"); +} + +// ======================================================================== +// Download +// ======================================================================== + +fn hf_url(repo: &str, file: &str) -> String { + format!( + "https://huggingface.co/{}/resolve/main/{}", + repo, file + ) +} + +/// Format bytes as human-readable size. +fn format_bytes(bytes: u64) -> String { + if bytes >= 1_073_741_824 { + format!("{:.1} GB", bytes as f64 / 1_073_741_824.0) + } else if bytes >= 1_048_576 { + format!("{:.1} MB", bytes as f64 / 1_048_576.0) + } else if bytes >= 1024 { + format!("{:.1} KB", bytes as f64 / 1024.0) + } else { + format!("{} B", bytes) + } +} + +/// Download a single file with progress display and resume support. +fn download_file(url: &str, dest: &Path) -> Result<(), String> { + let mut start_byte: u64 = 0; + + // Check for partial download (resume support) + let part_path = dest.with_extension( + dest.extension() + .map(|e| format!("{}.part", e.to_string_lossy())) + .unwrap_or_else(|| "part".to_string()), + ); + if part_path.exists() { + start_byte = fs::metadata(&part_path) + .map(|m| m.len()) + .unwrap_or(0); + } + + // Already fully downloaded? + if dest.exists() { + return Ok(()); + } + + // Build request + let mut req = ureq::get(url); + if start_byte > 0 { + req = req.set("Range", &format!("bytes={}-", start_byte)); + eprint!( + " Resuming from {} ... ", + format_bytes(start_byte) + ); + } + + let resp = req.call().map_err(|e| format!("HTTP request failed: {}", e))?; + + // Parse content length + let total_bytes = if start_byte > 0 { + // For Range requests, Content-Range: bytes start-end/total + resp.header("Content-Range") + .and_then(|cr| cr.rsplit('/').next()) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0) + } else { + resp.header("Content-Length") + .and_then(|s| s.parse::().ok()) + .unwrap_or(0) + }; + + let mut reader = resp.into_reader(); + let mut file = fs::OpenOptions::new() + .create(true) + .append(true) + .open(&part_path) + .map_err(|e| format!("Cannot open {}: {}", part_path.display(), e))?; + + let mut downloaded = start_byte; + let mut buf = vec![0u8; 256 * 1024]; // 256 KB buffer + let mut last_progress = std::time::Instant::now(); + let start_time = std::time::Instant::now(); + + loop { + let n = reader.read(&mut buf).map_err(|e| format!("Read error: {}", e))?; + if n == 0 { + break; + } + file.write_all(&buf[..n]) + .map_err(|e| format!("Write error: {}", e))?; + downloaded += n as u64; + + // Update progress ~4 times per second + let now = std::time::Instant::now(); + if now.duration_since(last_progress).as_millis() >= 250 || n == 0 { + last_progress = now; + let elapsed = now.duration_since(start_time).as_secs_f64(); + let speed = if elapsed > 0.0 { + (downloaded - start_byte) as f64 / elapsed + } else { + 0.0 + }; + + if total_bytes > 0 { + let pct = (downloaded as f64 / total_bytes as f64 * 100.0).min(100.0); + eprint!( + "\r {} / {} ({:.0}%) {}/s ", + format_bytes(downloaded), + format_bytes(total_bytes), + pct, + format_bytes(speed as u64), + ); + } else { + eprint!( + "\r {} downloaded, {}/s ", + format_bytes(downloaded), + format_bytes(speed as u64), + ); + } + } + } + + eprintln!(); // newline after progress + + // Rename .part to final destination + fs::rename(&part_path, dest) + .map_err(|e| format!("Cannot rename {} → {}: {}", part_path.display(), dest.display(), e))?; + + Ok(()) +} + +/// Download all files for a model. +pub fn download_model(model: &ModelInfo, output_dir: &str) -> Result<(), String> { + let dir = Path::new(output_dir); + fs::create_dir_all(dir) + .map_err(|e| format!("Cannot create directory {}: {}", output_dir, e))?; + + let total_files = model.files.len(); + for (i, file_name) in model.files.iter().enumerate() { + let dest = dir.join(file_name); + if dest.exists() { + eprintln!( + "[{}/{}] {} — already exists, skipping", + i + 1, + total_files, + file_name + ); + continue; + } + + let url = hf_url(model.repo, file_name); + eprintln!("[{}/{}] Downloading {} ...", i + 1, total_files, file_name); + download_file(&url, &dest)?; + } + + eprintln!("\n✓ Model '{}' downloaded to {}", model.name, output_dir); + Ok(()) +} + +// ======================================================================== +// Interactive Prompt +// ======================================================================== + +/// Prompt user to download a model. Returns true if they accepted. +pub fn prompt_download(model_name: &str) -> bool { + let model = match find_model(model_name) { + Some(m) => m, + None => return false, + }; + + eprintln!("Model directory '{}' not found.\n", model_name); + eprintln!(" {} — {}\n", model.name, model.description); + eprint!("Download now? [Y/n]: "); + io::stderr().flush().ok(); + + let mut input = String::new(); + if io::stdin().read_line(&mut input).is_err() { + return false; + } + let answer = input.trim().to_lowercase(); + answer.is_empty() || answer == "y" || answer == "yes" +} + +// ======================================================================== +// CLI Entry Point +// ======================================================================== + +/// Handle the `download` subcommand. Returns true if handled (caller should exit). +pub fn handle_download_command(args: &[String]) -> bool { + // Parse: download [--list] [] [--output ] + let mut model_name: Option = None; + let mut output_dir: Option = None; + let mut show_list = false; + + let mut i = 0; + while i < args.len() { + match args[i].as_str() { + "--list" | "-l" => { + show_list = true; + } + "--output" | "-o" => { + i += 1; + output_dir = args.get(i).cloned(); + } + "-h" | "--help" => { + eprintln!("Usage: qwen-asr download [--list] [] [--output ]\n"); + eprintln!("Options:"); + eprintln!(" --list, -l List available models"); + eprintln!(" --output, -o Download directory (default: .//)"); + eprintln!(" -h, --help Show this help"); + return true; + } + other => { + if other.starts_with('-') { + eprintln!("Unknown option for download: {}", other); + return true; + } + model_name = Some(other.to_string()); + } + } + i += 1; + } + + if show_list || model_name.is_none() { + list_models(); + return true; + } + + let name = model_name.unwrap(); + let model = match find_model(&name) { + Some(m) => m, + None => { + eprintln!("Unknown model: '{}'\n", name); + list_models(); + std::process::exit(1); + } + }; + + let dir = output_dir.unwrap_or_else(|| name.clone()); + match download_model(model, &dir) { + Ok(()) => {} + Err(e) => { + eprintln!("\nError: {}", e); + std::process::exit(1); + } + } + + true +} diff --git a/vendor/qwenasr/crates/qwen-asr-cli/src/live_capture.rs b/vendor/qwenasr/crates/qwen-asr-cli/src/live_capture.rs new file mode 100644 index 0000000..0288ddc --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr-cli/src/live_capture.rs @@ -0,0 +1,496 @@ +//! CoreAudio live audio capture for macOS. +//! +//! Enumerates input devices, captures audio via AudioUnit (HAL Input), +//! and resamples to 16 kHz mono f32 for the ASR pipeline. + +use coreaudio_sys::*; +use std::ffi::CStr; +use std::mem; +use std::os::raw::c_void; +use std::ptr; +use std::sync::mpsc; + +// ======================================================================== +// Device Enumeration +// ======================================================================== + +/// An audio input device. +pub struct AudioDevice { + pub id: AudioDeviceID, + pub name: String, + pub input_channels: u32, +} + +/// Get the list of audio input devices. +pub fn list_input_devices() -> Vec { + let mut devices = Vec::new(); + + // Get all audio devices + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioHardwarePropertyDevices, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + + let mut data_size: u32 = 0; + let status = unsafe { + AudioObjectGetPropertyDataSize( + kAudioObjectSystemObject, + &property_address, + 0, + ptr::null(), + &mut data_size, + ) + }; + if status != 0 || data_size == 0 { + return devices; + } + + let device_count = data_size as usize / mem::size_of::(); + let mut device_ids = vec![0u32; device_count]; + + let status = unsafe { + AudioObjectGetPropertyData( + kAudioObjectSystemObject, + &property_address, + 0, + ptr::null(), + &mut data_size, + device_ids.as_mut_ptr() as *mut c_void, + ) + }; + if status != 0 { + return devices; + } + + for &device_id in &device_ids { + // Check if device has input channels + let input_channels = get_input_channel_count(device_id); + if input_channels == 0 { + continue; + } + + let name = get_device_name(device_id); + devices.push(AudioDevice { + id: device_id, + name, + input_channels, + }); + } + + devices +} + +fn get_device_name(device_id: AudioDeviceID) -> String { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyDeviceNameCFString, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + + let mut name_ref: CFStringRef = ptr::null(); + let mut data_size = mem::size_of::() as u32; + + let status = unsafe { + AudioObjectGetPropertyData( + device_id, + &property_address, + 0, + ptr::null(), + &mut data_size, + &mut name_ref as *mut _ as *mut c_void, + ) + }; + + if status != 0 || name_ref.is_null() { + return format!("Device {}", device_id); + } + + // Convert CFString to Rust String + let c_str = unsafe { CFStringGetCStringPtr(name_ref, kCFStringEncodingUTF8) }; + let name = if !c_str.is_null() { + unsafe { CStr::from_ptr(c_str) } + .to_string_lossy() + .into_owned() + } else { + // Fallback: use CFStringGetCString + let mut buf = [0i8; 256]; + let ok = unsafe { + CFStringGetCString( + name_ref, + buf.as_mut_ptr(), + buf.len() as CFIndex, + kCFStringEncodingUTF8, + ) + }; + if ok != 0 { + unsafe { CStr::from_ptr(buf.as_ptr()) } + .to_string_lossy() + .into_owned() + } else { + format!("Device {}", device_id) + } + }; + + unsafe { CFRelease(name_ref as *const c_void) }; + name +} + +fn get_input_channel_count(device_id: AudioDeviceID) -> u32 { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyStreamConfiguration, + mScope: kAudioObjectPropertyScopeInput, + mElement: kAudioObjectPropertyElementMain, + }; + + let mut data_size: u32 = 0; + let status = unsafe { + AudioObjectGetPropertyDataSize( + device_id, + &property_address, + 0, + ptr::null(), + &mut data_size, + ) + }; + if status != 0 || data_size == 0 { + return 0; + } + + let mut buf = vec![0u8; data_size as usize]; + let status = unsafe { + AudioObjectGetPropertyData( + device_id, + &property_address, + 0, + ptr::null(), + &mut data_size, + buf.as_mut_ptr() as *mut c_void, + ) + }; + if status != 0 { + return 0; + } + + let buffer_list = unsafe { &*(buf.as_ptr() as *const AudioBufferList) }; + let mut total_channels: u32 = 0; + + let n_buffers = buffer_list.mNumberBuffers as usize; + if n_buffers == 0 { + return 0; + } + + // Access the variable-length mBuffers array + let buffers_ptr = &buffer_list.mBuffers as *const AudioBuffer; + for i in 0..n_buffers { + let ab = unsafe { &*buffers_ptr.add(i) }; + total_channels += ab.mNumberChannels; + } + + total_channels +} + +/// Find an input device by name (case-insensitive substring match). +pub fn find_device_by_name(name: &str) -> Option { + let name_lower = name.to_lowercase(); + let devices = list_input_devices(); + devices + .into_iter() + .find(|d| d.name.to_lowercase().contains(&name_lower)) +} + +/// Get the default input device. +pub fn default_input_device() -> Option { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioHardwarePropertyDefaultInputDevice, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + + let mut device_id: AudioDeviceID = 0; + let mut data_size = mem::size_of::() as u32; + + let status = unsafe { + AudioObjectGetPropertyData( + kAudioObjectSystemObject, + &property_address, + 0, + ptr::null(), + &mut data_size, + &mut device_id as *mut _ as *mut c_void, + ) + }; + + if status != 0 || device_id == kAudioObjectUnknown { + None + } else { + Some(device_id) + } +} + +/// Print all input devices to stderr. +pub fn print_devices() { + let devices = list_input_devices(); + if devices.is_empty() { + eprintln!("No audio input devices found."); + return; + } + + let default_id = default_input_device(); + + eprintln!("Audio input devices:\n"); + for d in &devices { + let marker = if Some(d.id) == default_id { " (default)" } else { "" }; + eprintln!(" {:30} {} ch{}", d.name, d.input_channels, marker); + } + eprintln!(); +} + +// ======================================================================== +// Audio Capture +// ======================================================================== + +/// Capture handle — drop to stop capture. +pub struct CaptureHandle { + audio_unit: AudioUnit, + _state: Box, +} + +/// Callback state passed to the AudioUnit render callback via ref_con. +struct CaptureCallbackState { + tx: mpsc::Sender>, + audio_unit: AudioUnit, +} + +/// Actual render callback using CaptureCallbackState. +unsafe extern "C" fn render_callback( + in_ref_con: *mut c_void, + io_action_flags: *mut AudioUnitRenderActionFlags, + in_time_stamp: *const AudioTimeStamp, + in_bus_number: u32, + in_number_frames: u32, + _io_data: *mut AudioBufferList, +) -> OSStatus { + let state = &*(in_ref_con as *const CaptureCallbackState); + + let n = in_number_frames as usize; + let mut samples = vec![0f32; n]; + + let buffer = AudioBuffer { + mNumberChannels: 1, + mDataByteSize: (n * mem::size_of::()) as u32, + mData: samples.as_mut_ptr() as *mut c_void, + }; + + let mut buffer_list = AudioBufferList { + mNumberBuffers: 1, + mBuffers: [buffer], + }; + + let status = AudioUnitRender( + state.audio_unit, + io_action_flags, + in_time_stamp, + in_bus_number, + in_number_frames, + &mut buffer_list, + ); + + if status != 0 { + return status; + } + + let _ = state.tx.send(samples); + 0 +} + +/// Start capturing audio from a device. Returns a channel receiver for audio +/// chunks (f32, mono, at device sample rate) and a handle to stop capture. +pub fn start_capture( + device_id: AudioDeviceID, +) -> Result<(mpsc::Receiver>, CaptureHandle, f64), String> { + // Get device's native sample rate + let sample_rate = get_device_sample_rate(device_id)?; + + // Create AUHAL AudioUnit + let comp_desc = AudioComponentDescription { + componentType: kAudioUnitType_Output, + componentSubType: kAudioUnitSubType_HALOutput, + componentManufacturer: kAudioUnitManufacturer_Apple, + componentFlags: 0, + componentFlagsMask: 0, + }; + + let component = unsafe { AudioComponentFindNext(ptr::null_mut(), &comp_desc) }; + if component.is_null() { + return Err("Cannot find HAL Output AudioComponent".into()); + } + + let mut audio_unit: AudioUnit = ptr::null_mut(); + let status = unsafe { AudioComponentInstanceNew(component, &mut audio_unit) }; + if status != 0 { + return Err(format!("AudioComponentInstanceNew failed: {}", status)); + } + + // Enable input on bus 1 (input element) + let enable_io: u32 = 1; + let status = unsafe { + AudioUnitSetProperty( + audio_unit, + kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Input, + 1, // input element + &enable_io as *const _ as *const c_void, + mem::size_of::() as u32, + ) + }; + if status != 0 { + return Err(format!("Enable input IO failed: {}", status)); + } + + // Disable output on bus 0 (output element) + let disable_io: u32 = 0; + let status = unsafe { + AudioUnitSetProperty( + audio_unit, + kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Output, + 0, // output element + &disable_io as *const _ as *const c_void, + mem::size_of::() as u32, + ) + }; + if status != 0 { + return Err(format!("Disable output IO failed: {}", status)); + } + + // Set the input device + let status = unsafe { + AudioUnitSetProperty( + audio_unit, + kAudioOutputUnitProperty_CurrentDevice, + kAudioUnitScope_Global, + 0, + &device_id as *const _ as *const c_void, + mem::size_of::() as u32, + ) + }; + if status != 0 { + return Err(format!("Set current device failed: {}", status)); + } + + // Set output format of bus 1 (what we read from the callback): + // Float32, mono, device sample rate + let stream_format = AudioStreamBasicDescription { + mSampleRate: sample_rate, + mFormatID: kAudioFormatLinearPCM, + mFormatFlags: kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked | kAudioFormatFlagIsNonInterleaved, + mBytesPerPacket: 4, + mFramesPerPacket: 1, + mBytesPerFrame: 4, + mChannelsPerFrame: 1, + mBitsPerChannel: 32, + mReserved: 0, + }; + + let status = unsafe { + AudioUnitSetProperty( + audio_unit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, + 1, // output scope of input element = data we receive + &stream_format as *const _ as *const c_void, + mem::size_of::() as u32, + ) + }; + if status != 0 { + return Err(format!("Set stream format failed: {}", status)); + } + + // Create channel and state + let (tx, rx) = mpsc::channel::>(); + + let state = Box::new(CaptureCallbackState { + tx, + audio_unit, + }); + + // Set input callback + let callback_struct = AURenderCallbackStruct { + inputProc: Some(render_callback), + inputProcRefCon: &*state as *const CaptureCallbackState as *mut c_void, + }; + + let status = unsafe { + AudioUnitSetProperty( + audio_unit, + kAudioOutputUnitProperty_SetInputCallback, + kAudioUnitScope_Global, + 0, + &callback_struct as *const _ as *const c_void, + mem::size_of::() as u32, + ) + }; + if status != 0 { + return Err(format!("Set input callback failed: {}", status)); + } + + // Initialize and start + let status = unsafe { AudioUnitInitialize(audio_unit) }; + if status != 0 { + return Err(format!("AudioUnitInitialize failed: {}", status)); + } + + let status = unsafe { AudioOutputUnitStart(audio_unit) }; + if status != 0 { + return Err(format!("AudioOutputUnitStart failed: {}", status)); + } + + let handle = CaptureHandle { + audio_unit, + _state: state, + }; + + Ok((rx, handle, sample_rate)) +} + +impl Drop for CaptureHandle { + fn drop(&mut self) { + unsafe { + AudioOutputUnitStop(self.audio_unit); + AudioUnitUninitialize(self.audio_unit); + AudioComponentInstanceDispose(self.audio_unit); + } + } +} + +fn get_device_sample_rate(device_id: AudioDeviceID) -> Result { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyNominalSampleRate, + mScope: kAudioObjectPropertyScopeInput, + mElement: kAudioObjectPropertyElementMain, + }; + + let mut sample_rate: f64 = 0.0; + let mut data_size = mem::size_of::() as u32; + + let status = unsafe { + AudioObjectGetPropertyData( + device_id, + &property_address, + 0, + ptr::null(), + &mut data_size, + &mut sample_rate as *mut _ as *mut c_void, + ) + }; + + if status != 0 { + return Err(format!( + "Cannot get sample rate for device {}: error {}", + device_id, status + )); + } + + Ok(sample_rate) +} diff --git a/vendor/qwenasr/crates/qwen-asr-cli/src/main.rs b/vendor/qwenasr/crates/qwen-asr-cli/src/main.rs new file mode 100644 index 0000000..df79340 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr-cli/src/main.rs @@ -0,0 +1,937 @@ +mod download; +#[cfg(target_os = "macos")] +mod live_capture; + +use qwen_asr::{audio, config, context, kernels, transcribe, align}; +use config::*; +use context::QwenCtx; + +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +fn stream_token(piece: &str) { + use std::io::Write; + print!("{}", piece); + std::io::stdout().flush().ok(); +} + +fn usage(prog: &str) { + eprintln!("qwen-asr — Qwen3-ASR speech-to-text (pure Rust)\n"); + eprintln!("Usage: {} -d (-i | --stdin | --live) [options]\n", prog); + eprintln!("Required:"); + eprintln!(" -d Model directory (with *.safetensors, vocab.json)"); + eprintln!(" -i Input WAV file (16-bit PCM, any sample rate)"); + eprintln!(" --stdin Read audio from stdin (auto-detect WAV or raw s16le 16kHz mono)"); + eprintln!("\nLive capture (macOS only):"); + eprintln!(" --live Capture from audio input device in real time"); + eprintln!(" --device Input device name (default: system default)"); + eprintln!(" --list-devices List available audio input devices and exit"); + eprintln!(" --vad Live VAD mode: detect speech segments, transcribe each"); + eprintln!("\nOptions:"); + eprintln!(" -t Number of threads (default: all CPUs)"); + eprintln!(" -S Segment target seconds (default: 0 = full-audio decode)"); + eprintln!(" -W Segment-cutting silence search window ± seconds (default: 3.0)"); + eprintln!(" --stream Streaming mode: process in chunks with prefix rollback"); + eprintln!(" --stream-max-new-tokens Max generated tokens per stream step (default: 32)"); + eprintln!(" --stream-chunk-sec Chunk size for streaming (default: 2.0, min ~1.0)"); + eprintln!(" --enc-window-sec Encoder attention window in seconds (1..8, default 8)"); + eprintln!(" --past-text Reuse previously decoded text as context"); + eprintln!(" --skip-silence Drop long silent spans before inference"); + eprintln!(" --prompt System prompt for biasing"); + eprintln!(" --language Force output language"); + eprintln!("\nAlignment mode (requires ForcedAligner model):"); + eprintln!(" --align Align transcript to audio (word-level timestamps)"); + eprintln!(" --align-language Language for word splitting (default: English)"); + eprintln!(" --profile Print per-operation timing breakdown"); + eprintln!(" --debug Debug output (per-layer details)"); + eprintln!(" --silent No status output (only transcription on stdout)"); + eprintln!("\nModel management:"); + eprintln!(" {} download [--list] [] [--output ]", prog); + eprintln!(" -h Show this help"); +} + +fn parse_past_text_mode(s: &str) -> Option { + match s.to_lowercase().as_str() { + "yes" => Some(1), + "no" => Some(0), + "auto" => Some(-1), + _ => None, + } +} + +fn main() { + let args: Vec = std::env::args().collect(); + + // Handle `download` subcommand: qwen-asr download [args...] + if args.len() >= 2 && args[1] == "download" { + download::handle_download_command(&args[2..]); + return; + } + + // Handle --list-devices (no model needed) + if args.iter().any(|a| a == "--list-devices") { + #[cfg(target_os = "macos")] + { + live_capture::print_devices(); + } + #[cfg(not(target_os = "macos"))] + { + eprintln!("--list-devices is only supported on macOS."); + eprintln!("On Linux, use: arecord -l"); + } + return; + } + + let mut model_dir: Option = None; + let mut input_wav: Option = None; + let mut verbosity = 1i32; + let mut use_stdin = false; + let mut live_mode = false; + let mut device_name: Option = None; + let mut n_threads = 0i32; + let mut segment_sec: f32 = -1.0; + let mut search_sec: f32 = -1.0; + let mut stream_mode = false; + let mut vad_mode = false; + let mut stream_max_new_tokens: i32 = -1; + let mut stream_chunk_sec: f32 = -1.0; + let mut enc_window_sec: f32 = -1.0; + let mut prompt_text: Option = None; + let mut force_language: Option = None; + let mut past_text_mode: i32 = -1; // -1 auto, 0 off, 1 on + let mut skip_silence = false; + let mut profile = false; + let mut align_text: Option = None; + let mut align_language: Option = None; + + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "-d" => { + i += 1; + model_dir = args.get(i).cloned(); + } + "-i" => { + i += 1; + input_wav = args.get(i).cloned(); + } + "-t" => { + i += 1; + n_threads = args.get(i).and_then(|s| s.parse().ok()).unwrap_or(0); + } + "-S" => { + i += 1; + segment_sec = args.get(i).and_then(|s| s.parse().ok()).unwrap_or(-1.0); + } + "-W" => { + i += 1; + search_sec = args.get(i).and_then(|s| s.parse().ok()).unwrap_or(-1.0); + } + "--stream" => { + stream_mode = true; + } + "--vad" => { + vad_mode = true; + } + "--stream-max-new-tokens" => { + i += 1; + stream_max_new_tokens = args.get(i).and_then(|s| s.parse().ok()).unwrap_or(-1); + } + "--stream-chunk-sec" => { + i += 1; + stream_chunk_sec = args.get(i).and_then(|s| s.parse().ok()).unwrap_or(-1.0); + } + "--enc-window-sec" => { + i += 1; + enc_window_sec = args.get(i).and_then(|s| s.parse().ok()).unwrap_or(-1.0); + } + "--past-text" => { + i += 1; + if let Some(s) = args.get(i) { + match parse_past_text_mode(s) { + Some(m) => past_text_mode = m, + None => { + eprintln!("Error: --past-text must be one of yes|no|auto, got '{}'", s); + std::process::exit(1); + } + } + } + } + "--skip-silence" => { + skip_silence = true; + } + "--prompt" => { + i += 1; + prompt_text = args.get(i).cloned(); + } + "--language" => { + i += 1; + force_language = args.get(i).cloned(); + } + "--align" => { + i += 1; + align_text = args.get(i).cloned(); + } + "--align-language" => { + i += 1; + align_language = args.get(i).cloned(); + } + "--stdin" => { + use_stdin = true; + } + "--live" => { + live_mode = true; + } + "--device" => { + i += 1; + device_name = args.get(i).cloned(); + } + "--list-devices" => { + // Already handled above, but don't error + } + "--profile" => { + profile = true; + } + "--debug" => { + verbosity = 2; + } + "--silent" => { + verbosity = 0; + } + "-h" | "--help" => { + usage(&args[0]); + return; + } + other => { + eprintln!("Unknown option: {}", other); + usage(&args[0]); + std::process::exit(1); + } + } + i += 1; + } + + let model_dir = match model_dir { + Some(d) => d, + None => { + usage(&args[0]); + std::process::exit(1); + } + }; + + // Auto-prompt to download if model directory doesn't exist + if !std::path::Path::new(&model_dir).exists() { + if let Some(model) = download::find_model(&model_dir) { + if download::prompt_download(&model_dir) { + if let Err(e) = download::download_model(model, &model_dir) { + eprintln!("Download failed: {}", e); + std::process::exit(1); + } + eprintln!(); // blank line before model loading + } else { + eprintln!("Aborted."); + std::process::exit(1); + } + } else { + eprintln!("Error: Model directory '{}' not found.", model_dir); + eprintln!(); + download::list_models(); + std::process::exit(1); + } + } + + if input_wav.is_none() && !use_stdin && !live_mode { + usage(&args[0]); + std::process::exit(1); + } + + // Check mutual exclusivity of input modes + let input_count = [input_wav.is_some(), use_stdin, live_mode].iter().filter(|&&x| x).count(); + if input_count > 1 { + eprintln!("Error: -i, --stdin, and --live are mutually exclusive"); + std::process::exit(1); + } + + kernels::set_verbose(verbosity); + if profile { + kernels::set_profile(true); + kernels::profile_reset(); + } + let emit_tokens = verbosity > 0; + + // Initialize thread pool + if n_threads <= 0 { + n_threads = kernels::get_num_cpus() as i32; + } + kernels::set_threads(n_threads as usize); + + // Print optimization info + if verbosity >= 1 { + let opts = qwen_asr::optimization_flags(); + eprintln!( + "Optimizations: {} | {} threads | {}", + opts.join(", "), + n_threads, + std::env::consts::ARCH, + ); + } + + // Load model + let mut ctx = match QwenCtx::load(&model_dir) { + Some(c) => c, + None => { + eprintln!("Failed to load model from {}", model_dir); + std::process::exit(1); + } + }; + + // Apply settings + if segment_sec >= 0.0 { + ctx.segment_sec = segment_sec; + } + if search_sec >= 0.0 { + ctx.search_sec = search_sec; + } + if enc_window_sec >= 0.0 { + let window_frames = (enc_window_sec * 100.0 + 0.5) as usize; + ctx.config.enc_n_window_infer = window_frames.clamp(100, 800); + } + if stream_max_new_tokens > 0 { + ctx.stream_max_new_tokens = stream_max_new_tokens; + } + if stream_chunk_sec > 0.0 { + ctx.stream_chunk_sec = stream_chunk_sec; + } + if past_text_mode >= 0 { + ctx.past_text_conditioning = past_text_mode == 1; + } else if stream_mode { + ctx.past_text_conditioning = true; + } + if skip_silence { + ctx.skip_silence = true; + } + if let Some(ref prompt) = prompt_text { + if ctx.set_prompt(prompt).is_err() { + eprintln!("Failed to set --prompt text"); + std::process::exit(1); + } + } + if let Some(ref lang) = force_language { + if ctx.set_force_language(lang).is_err() { + eprintln!("Unsupported language for --language: {}", lang); + eprintln!( + "Supported languages: {}", + SUPPORTED_LANGUAGES.join(",") + ); + std::process::exit(1); + } + } + + // Alignment mode + if let Some(ref atext) = align_text { + let lang = align_language.as_deref().unwrap_or("English"); + let lang_normalized = match normalize_language(lang) { + Some(l) => l, + None => { + eprintln!("Unsupported --align-language: {}", lang); + eprintln!("Supported languages: {}", SUPPORTED_LANGUAGES.join(",")); + std::process::exit(1); + } + }; + + let samples = if use_stdin { + audio::read_pcm_stdin() + } else { + audio::load_wav(input_wav.as_ref().unwrap()) + }; + let samples = match samples { + Some(s) => s, + None => { + eprintln!("Failed to load audio"); + std::process::exit(1); + } + }; + + match align::forced_align(&mut ctx, &samples, atext, &lang_normalized) { + Some(results) => { + // Output JSON array + println!("["); + for (i, r) in results.iter().enumerate() { + let comma = if i + 1 < results.len() { "," } else { "" }; + // Escape the text for JSON + let escaped = r.text.replace('\\', "\\\\").replace('"', "\\\""); + println!( + " {{\"text\": \"{}\", \"start\": {:.0}, \"end\": {:.0}}}{}", + escaped, r.start_ms, r.end_ms, comma + ); + } + println!("]"); + } + None => { + eprintln!("Alignment failed"); + std::process::exit(1); + } + } + + if verbosity >= 1 { + eprintln!( + "Alignment: {:.0} ms (encoding: {:.0}ms, decoding: {:.0}ms)", + ctx.perf_total_ms, ctx.perf_encode_ms, ctx.perf_decode_ms + ); + } + + if profile { + kernels::profile_report(); + } + return; + } + + // Set token callback + if emit_tokens { + ctx.token_cb = Some(Box::new(stream_token)); + } + + // Live capture mode + if live_mode { + #[cfg(not(target_os = "macos"))] + { + eprintln!("Error: --live is only supported on macOS."); + eprintln!("On Linux, pipe audio via: arecord -f S16_LE -r 16000 -c 1 | qwen-asr -d --stdin"); + std::process::exit(1); + } + + #[cfg(target_os = "macos")] + { + run_live_capture(&mut ctx, device_name.as_deref(), stream_mode, vad_mode, verbosity, profile); + return; + } + } + + // Transcribe + let text = if stream_mode { + let samples = if use_stdin { + audio::read_pcm_stdin() + } else { + audio::load_wav(input_wav.as_ref().unwrap()) + }; + match samples { + Some(s) => transcribe::transcribe_stream(&mut ctx, &s), + None => None, + } + } else if use_stdin { + transcribe::transcribe_stdin(&mut ctx) + } else { + transcribe::transcribe(&mut ctx, input_wav.as_ref().unwrap()) + }; + + match text { + Some(t) => { + if emit_tokens { + println!(); + } else { + println!("{}", t); + } + } + None => { + eprintln!("Transcription failed"); + std::process::exit(1); + } + } + + if verbosity >= 1 { + let tokens_per_sec = if ctx.perf_total_ms > 0.0 { + 1000.0 * ctx.perf_text_tokens as f64 / ctx.perf_total_ms + } else { + 0.0 + }; + eprintln!( + "Inference: {:.0} ms, {} text tokens ({:.2} tok/s, encoding: {:.0}ms, decoding: {:.0}ms)", + ctx.perf_total_ms, ctx.perf_text_tokens, tokens_per_sec, + ctx.perf_encode_ms, ctx.perf_decode_ms + ); + if ctx.perf_audio_ms > 0.0 && ctx.perf_total_ms > 0.0 { + let audio_s = ctx.perf_audio_ms / 1000.0; + let infer_s = ctx.perf_total_ms / 1000.0; + eprintln!( + "Audio: {:.1} s processed in {:.1} s ({:.2}x realtime)", + audio_s, infer_s, audio_s / infer_s + ); + } + } + + if profile { + kernels::profile_report(); + } +} + +// ======================================================================== +// Live Capture Loop (macOS only) +// ======================================================================== + +#[cfg(target_os = "macos")] +fn run_live_capture( + ctx: &mut QwenCtx, + device_name: Option<&str>, + stream_mode: bool, + vad_mode: bool, + verbosity: i32, + profile: bool, +) { + use std::time::Duration; + + // Resolve device + let device_id = if let Some(name) = device_name { + match live_capture::find_device_by_name(name) { + Some(dev) => { + if verbosity >= 1 { + eprintln!("Using input device: {} ({} ch)", dev.name, dev.input_channels); + } + dev.id + } + None => { + eprintln!("Error: No input device matching '{}'", name); + if name.to_lowercase().contains("blackhole") { + eprintln!(); + eprintln!("BlackHole does not appear to be installed."); + eprintln!("Install it with: brew install blackhole-2ch"); + eprintln!("Then set it up as a Multi-Output Device in Audio MIDI Setup."); + eprintln!("See: https://github.com/ExistentialAudio/BlackHole"); + } + eprintln!(); + live_capture::print_devices(); + std::process::exit(1); + } + } + } else { + match live_capture::default_input_device() { + Some(id) => { + if verbosity >= 1 { + let devices = live_capture::list_input_devices(); + if let Some(dev) = devices.iter().find(|d| d.id == id) { + eprintln!("Using default input device: {}", dev.name); + } + } + id + } + None => { + eprintln!("Error: No default input device found"); + std::process::exit(1); + } + } + }; + + // Start capture + let (rx, _handle, device_rate) = match live_capture::start_capture(device_id) { + Ok(r) => r, + Err(e) => { + eprintln!("Error: Failed to start audio capture: {}", e); + std::process::exit(1); + } + }; + + let mode_label = if stream_mode { "streaming" } else if vad_mode { "VAD" } else { "segmented" }; + if verbosity >= 1 { + if vad_mode { + eprintln!("Listening (VAD segmented)... press Ctrl+C to stop\n"); + } else { + eprintln!( + "Listening ({}, {:.1}s chunks)... press Ctrl+C to stop\n", + mode_label, + if stream_mode { ctx.stream_chunk_sec } else { ctx.segment_sec } + ); + } + } + + // Set up Ctrl+C handler + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }) + .expect("Error setting Ctrl+C handler"); + + // Configure context + ctx.past_text_conditioning = true; + ctx.reset_perf(); + + // Audio accumulation + let target_rate = 16000; + let mut raw_buf: Vec = Vec::new(); + let mut resampled_buf: Vec = Vec::new(); + let needs_resample = (device_rate - target_rate as f64).abs() > 1.0; + let wall_start = std::time::Instant::now(); + + if stream_mode { + // ---- Streaming mode: incremental stream_push_audio ---- + // + // We accumulate audio and call stream_push_audio() which only + // processes NEW audio incrementally (persistent encoder cache, + // LCP-reused decoder prefill, monotonic token commit). + // + // Buffer reset after ~120s to bound memory. + let max_window_samples: usize = 120 * target_rate as usize; + let mut stream_state = transcribe::StreamState::new(); + + // Set token callback for direct printing + ctx.token_cb = None; // stream_push_audio returns delta text, we print it + + // Text-emission timeout: flush rollback tokens after no new text for 5s + let mut last_text_time: Option = None; + let text_flush_secs = 5.0_f32; + let mut flushed = false; + + while running.load(Ordering::SeqCst) { + // Receive audio + match rx.recv_timeout(Duration::from_millis(100)) { + Ok(chunk) => raw_buf.extend_from_slice(&chunk), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} + Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, + } + while let Ok(chunk) = rx.try_recv() { + raw_buf.extend_from_slice(&chunk); + } + + // Resample + if needs_resample { + if !raw_buf.is_empty() { + let resampled = qwen_asr::audio::resample( + &raw_buf, device_rate as i32, target_rate, + ); + resampled_buf.extend_from_slice(&resampled); + raw_buf.clear(); + } + } else { + resampled_buf.append(&mut raw_buf); + } + + // Reset window if buffer exceeds max + if resampled_buf.len() > max_window_samples { + // Flush rollback tokens before reset + if let Some(delta) = transcribe::stream_push_audio( + ctx, &resampled_buf, &mut stream_state, true + ) { + if !delta.is_empty() { + print!("{}", delta); + } + } + println!(); + resampled_buf.clear(); + stream_state.reset(); + last_text_time = None; + flushed = false; + continue; + } + + // Determine if we should finalize: flush rollback tokens + // when no new text has been emitted for 5 seconds + let finalize = !flushed + && last_text_time.is_some_and(|t| t.elapsed().as_secs_f32() >= text_flush_secs); + + // Process all available full chunks + if resampled_buf.len() > stream_state.audio_cursor() { + if let Some(delta) = transcribe::stream_push_audio( + ctx, &resampled_buf, &mut stream_state, finalize + ) { + if !delta.is_empty() { + print!("{}", delta); + std::io::Write::flush(&mut std::io::stdout()).ok(); + last_text_time = Some(std::time::Instant::now()); + flushed = false; + } else if finalize { + flushed = true; // Don't keep calling finalize + } + } + } + } + + // Final flush + if !raw_buf.is_empty() && needs_resample { + let resampled = qwen_asr::audio::resample( + &raw_buf, device_rate as i32, target_rate, + ); + resampled_buf.extend_from_slice(&resampled); + } else { + resampled_buf.append(&mut raw_buf); + } + + if resampled_buf.len() > stream_state.audio_cursor() { + if let Some(delta) = transcribe::stream_push_audio( + ctx, &resampled_buf, &mut stream_state, true // finalize: flush rollback + ) { + if !delta.is_empty() { + print!("{}", delta); + } + } + std::io::Write::flush(&mut std::io::stdout()).ok(); + } + println!(); + } else if vad_mode { + // ---- VAD mode: energy-based speech detection + segment transcription ---- + // + // Detect speech using RMS energy. When speech ends (silence > 1.5s), + // transcribe the accumulated speech segment using transcribe_audio(). + // This gives better accuracy than streaming (full segment context) + // with automatic speech boundary detection. + let speech_threshold: f32 = 0.001; + let silence_hangover_secs = 1.5_f32; + let min_segment_secs = 0.5_f32; + let max_segment_secs = 30.0_f32; + let min_segment_samples = (min_segment_secs * target_rate as f32) as usize; + let max_segment_samples = (max_segment_secs * target_rate as f32) as usize; + let check_samples = (target_rate as usize) * 30 / 1000; // 30ms window for RMS + + let mut speech_active = false; + let mut silence_start: Option = None; + let mut speech_start_idx: usize = 0; + + // Keep a small pre-speech buffer to avoid clipping word beginnings + let pre_speech_samples = (target_rate as usize) / 4; // 250ms lookback + + // Disable token callback — we print the full result after each segment + ctx.token_cb = None; + + // Cross-segment context: accumulate text to use as prompt for next segment + let mut accumulated_text = String::new(); + + while running.load(Ordering::SeqCst) { + // Receive audio + match rx.recv_timeout(Duration::from_millis(50)) { + Ok(chunk) => raw_buf.extend_from_slice(&chunk), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} + Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, + } + while let Ok(chunk) = rx.try_recv() { + raw_buf.extend_from_slice(&chunk); + } + + // Resample + if needs_resample { + if !raw_buf.is_empty() { + let resampled = qwen_asr::audio::resample( + &raw_buf, device_rate as i32, target_rate, + ); + resampled_buf.extend_from_slice(&resampled); + raw_buf.clear(); + } + } else { + resampled_buf.append(&mut raw_buf); + } + + // Compute RMS energy of latest 30ms + let buf_len = resampled_buf.len(); + let rms = if buf_len >= check_samples { + let tail = &resampled_buf[buf_len - check_samples..]; + let sum_sq: f32 = tail.iter().map(|&s| s * s).sum(); + (sum_sq / check_samples as f32).sqrt() + } else { + 0.0 + }; + let is_speech = rms >= speech_threshold; + + // Periodic RMS debug output + if verbosity >= 2 && buf_len % (target_rate as usize * 2) < check_samples { + eprintln!(" [VAD] rms={:.6} threshold={:.4} speech={}", + rms, speech_threshold, if speech_active { "active" } else { "inactive" }); + } + + if !speech_active { + if is_speech { + // Speech started — mark the start with lookback + speech_active = true; + silence_start = None; + speech_start_idx = buf_len.saturating_sub(pre_speech_samples); + if verbosity >= 2 { + eprintln!(" [VAD] speech start at {:.1}s", + buf_len as f32 / target_rate as f32); + } + } else { + // No speech — bound buffer to avoid unlimited growth + // Keep only last 0.5s for lookback context + let keep = (target_rate as usize) / 2; + if resampled_buf.len() > keep * 4 { + let drain = resampled_buf.len() - keep; + resampled_buf.drain(..drain); + } + } + } else { + // Speech is active + let segment_len = buf_len - speech_start_idx; + + if is_speech { + // Still speaking — reset silence timer + silence_start = None; + + // Force-flush if segment exceeds max duration + if segment_len >= max_segment_samples { + if verbosity >= 2 { + eprintln!(" [VAD] max segment reached ({:.1}s), flushing", + segment_len as f32 / target_rate as f32); + } + let segment = &resampled_buf[speech_start_idx..]; + // Set previous text as context + if !accumulated_text.is_empty() { + ctx.prompt = Some(accumulated_text.clone()); + ctx.prompt_tokens_ready = false; + } + ctx.reset_perf(); + if let Some(text) = transcribe::transcribe_audio(ctx, segment) { + if !text.is_empty() { + println!("{}", text); + accumulated_text.push_str(&text); + } + } + resampled_buf.clear(); + speech_active = false; + silence_start = None; + } + } else { + // Silence during speech — track duration + if silence_start.is_none() { + silence_start = Some(std::time::Instant::now()); + } + if let Some(start) = silence_start { + if start.elapsed().as_secs_f32() >= silence_hangover_secs { + // End of utterance — transcribe the segment + if segment_len >= min_segment_samples { + // Trim trailing silence (keep only 200ms of it) + let trail_keep = (target_rate as usize) / 5; + let seg_end = (buf_len - check_samples + trail_keep).min(buf_len); + let segment = &resampled_buf[speech_start_idx..seg_end]; + + if verbosity >= 2 { + eprintln!(" [VAD] speech end, segment {:.1}s", + segment.len() as f32 / target_rate as f32); + } + + ctx.reset_perf(); + // Set previous text as context + if !accumulated_text.is_empty() { + ctx.prompt = Some(accumulated_text.clone()); + ctx.prompt_tokens_ready = false; + } + let t0 = std::time::Instant::now(); + if let Some(text) = transcribe::transcribe_audio(ctx, segment) { + if !text.is_empty() { + println!("{}", text); + accumulated_text.push_str(&text); + if verbosity >= 1 { + let audio_secs = segment.len() as f32 / target_rate as f32; + let compute_secs = t0.elapsed().as_secs_f32(); + eprintln!( + " ({:.1}s audio in {:.1}s, {:.1}x realtime)", + audio_secs, compute_secs, + audio_secs / compute_secs.max(0.001) + ); + } + } + } + } else if verbosity >= 2 { + eprintln!(" [VAD] segment too short ({:.2}s), discarding", + segment_len as f32 / target_rate as f32); + } + + resampled_buf.clear(); + speech_active = false; + silence_start = None; + } + } + } + } + } + + // Flush remaining speech on Ctrl+C + if speech_active && resampled_buf.len() > speech_start_idx + min_segment_samples { + let segment = &resampled_buf[speech_start_idx..]; + ctx.reset_perf(); + if let Some(text) = transcribe::transcribe_audio(ctx, segment) { + if !text.is_empty() { + println!("{}", text); + } + } + } + } else { + // ---- Segmented mode: independent segments ---- + if ctx.segment_sec <= 0.0 { + ctx.segment_sec = 5.0; + } + let segment_samples_16k = (ctx.segment_sec * target_rate as f32) as usize; + + while running.load(Ordering::SeqCst) { + match rx.recv_timeout(Duration::from_millis(100)) { + Ok(chunk) => raw_buf.extend_from_slice(&chunk), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} + Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, + } + while let Ok(chunk) = rx.try_recv() { + raw_buf.extend_from_slice(&chunk); + } + + if needs_resample { + if !raw_buf.is_empty() { + let resampled = qwen_asr::audio::resample( + &raw_buf, device_rate as i32, target_rate, + ); + resampled_buf.extend_from_slice(&resampled); + raw_buf.clear(); + } + } else { + resampled_buf.append(&mut raw_buf); + } + + if resampled_buf.len() >= segment_samples_16k { + ctx.reset_perf(); + let _text = transcribe::transcribe_audio(ctx, &resampled_buf); + resampled_buf.clear(); + if verbosity > 0 { + println!(); + } + } + } + + // Flush remaining + if !raw_buf.is_empty() && needs_resample { + let resampled = qwen_asr::audio::resample( + &raw_buf, device_rate as i32, target_rate, + ); + resampled_buf.extend_from_slice(&resampled); + } else { + resampled_buf.append(&mut raw_buf); + } + if !resampled_buf.is_empty() { + ctx.reset_perf(); + let _text = transcribe::transcribe_audio(ctx, &resampled_buf); + if verbosity > 0 { + println!(); + } + } + } + + // ---- Benchmark summary ---- + let wall_ms = wall_start.elapsed().as_secs_f64() * 1000.0; + let audio_s = resampled_buf.len() as f64 / target_rate as f64; + + if verbosity >= 1 { + eprintln!("\nStopped."); + let tokens_per_sec = if ctx.perf_total_ms > 0.0 { + 1000.0 * ctx.perf_text_tokens as f64 / ctx.perf_total_ms + } else { + 0.0 + }; + eprintln!( + "Inference: {:.0} ms, {} text tokens ({:.2} tok/s, encoding: {:.0}ms, decoding: {:.0}ms)", + ctx.perf_total_ms, ctx.perf_text_tokens, tokens_per_sec, + ctx.perf_encode_ms, ctx.perf_decode_ms + ); + if audio_s > 0.0 && ctx.perf_total_ms > 0.0 { + let infer_s = ctx.perf_total_ms / 1000.0; + eprintln!( + "Audio: {:.1} s processed in {:.1} s compute ({:.2}x realtime), {:.1} s wall clock", + audio_s, infer_s, audio_s / infer_s, wall_ms / 1000.0 + ); + } + } + + if profile { + kernels::profile_report(); + } +} diff --git a/vendor/qwenasr/crates/qwen-asr/CHANGELOG.md b/vendor/qwenasr/crates/qwen-asr/CHANGELOG.md new file mode 100644 index 0000000..6076cb6 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/CHANGELOG.md @@ -0,0 +1,88 @@ +# Changelog + +## [0.5.0](https://github.com/huanglizhuo/QwenASR/compare/qwen-asr-v0.4.2...qwen-asr-v0.5.0) (2026-03-20) + + +### Features + +* optimize the speed with auto research pattern ([9c13daa](https://github.com/huanglizhuo/QwenASR/commit/9c13daaadd964a6c2d79bed99364b26a368c305f)) + +## [0.4.2](https://github.com/huanglizhuo/QwenASR/compare/qwen-asr-v0.4.1...qwen-asr-v0.4.2) (2026-03-14) + + +### Bug Fixes + +* install OpenBLAS on CI to support build ([e995cad](https://github.com/huanglizhuo/QwenASR/commit/e995cad2c18999a39e74c56c26a2d6526020fd53)) + +## [0.4.1](https://github.com/huanglizhuo/QwenASR/compare/qwen-asr-v0.4.0...qwen-asr-v0.4.1) (2026-03-14) + + +### Bug Fixes + +* clean up the code ([346d112](https://github.com/huanglizhuo/QwenASR/commit/346d112595c0d93f58c54339a7f32ca6e1e648d8)) + +## [0.4.0](https://github.com/huanglizhuo/QwenASR/compare/qwen-asr-v0.3.0...qwen-asr-v0.4.0) (2026-03-13) + + +### Features + +* expose streaming C API for native integration ([a58befe](https://github.com/huanglizhuo/QwenASR/commit/a58befe991393405867ece18621d60ad5dc7cfd6)) +* expose streaming C API for native macOS/iOS integration ([51dc917](https://github.com/huanglizhuo/QwenASR/commit/51dc917fc214a38ab6a85db55e2cc53ecdfabb6d)) + + +### Bug Fixes + +* handle split UTF-8 sequences in BPE token decoding ([75c3e31](https://github.com/huanglizhuo/QwenASR/commit/75c3e3172b33e412fdde0bee6f8009277e9bb35e)) +* handle split UTF-8 sequences in BPE token decoding ([c86fae2](https://github.com/huanglizhuo/QwenASR/commit/c86fae2918c50f5ef74097fb4ad1e97a19387a18)) + +## [0.3.0](https://github.com/huanglizhuo/QwenASR/compare/qwen-asr-v0.2.4...qwen-asr-v0.3.0) (2026-02-23) + + +### Features + +* add missing parameter to qwen asr offline model ([f56e8b1](https://github.com/huanglizhuo/QwenASR/commit/f56e8b1e58731344fad92a7ed38c59a9f09267f6)) +* add missing parameter to qwen asr offline model ([6d1e38d](https://github.com/huanglizhuo/QwenASR/commit/6d1e38da19cbae46c2afe2e1af03a5d437679ef8)) +* improve the live stream performace on macos ([ba47230](https://github.com/huanglizhuo/QwenASR/commit/ba47230403f897bde2486b3235cfd3f5ca24e293)) +* refine the stream mode ([7ac71a9](https://github.com/huanglizhuo/QwenASR/commit/7ac71a9a4f30b13ff155ff2eb345db85bce1b91c)) +* support live from blackhold for macos for qwen-asr-cli ([724ead1](https://github.com/huanglizhuo/QwenASR/commit/724ead1fe121d0ed0a0f7ef874142f665e7d0da3)) +* update readme ([cde2178](https://github.com/huanglizhuo/QwenASR/commit/cde21787bb545e12c154045562883b9ced00514d)) + + +### Bug Fixes + +* publish 0.2.3 with tag-driven flow ([3637ec8](https://github.com/huanglizhuo/QwenASR/commit/3637ec80f5519ecbd0a034f6c1f23f78156cd0fe)) +* publish 0.2.3 with tag-driven flow ([e7bbd18](https://github.com/huanglizhuo/QwenASR/commit/e7bbd18dc009c3bd87f32e2346c196f65c618b19)) +* trigger patch release 0.2.1 for flutter ([b5785f9](https://github.com/huanglizhuo/QwenASR/commit/b5785f9e0a6e4cab3a4796bbd1bd401876ea5926)) +* update the both library readme to mention this is WIP project ([139a591](https://github.com/huanglizhuo/QwenASR/commit/139a5915205083abc4b87fd0228ccf4c725c99c0)) +* update the release flow to support PAT ([2b9be6c](https://github.com/huanglizhuo/QwenASR/commit/2b9be6c21b7e74e51bf1d1f15e6959679db70542)) + +## [0.2.3](https://github.com/huanglizhuo/QwenASR/compare/qwen-asr-v0.2.2...qwen-asr-v0.2.3) (2026-02-22) + + +### Bug Fixes + +* update the release flow to support PAT ([2b9be6c](https://github.com/huanglizhuo/QwenASR/commit/2b9be6c21b7e74e51bf1d1f15e6959679db70542)) + +## [0.2.2](https://github.com/huanglizhuo/QwenASR/compare/qwen-asr-v0.2.1...qwen-asr-v0.2.2) (2026-02-22) + + +### Bug Fixes + +* publish 0.2.3 with tag-driven flow ([3637ec8](https://github.com/huanglizhuo/QwenASR/commit/3637ec80f5519ecbd0a034f6c1f23f78156cd0fe)) +* publish 0.2.3 with tag-driven flow ([e7bbd18](https://github.com/huanglizhuo/QwenASR/commit/e7bbd18dc009c3bd87f32e2346c196f65c618b19)) + +## [0.2.1](https://github.com/huanglizhuo/QwenASR/compare/qwen-asr-v0.2.0...qwen-asr-v0.2.1) (2026-02-22) + + +### Bug Fixes + +* trigger patch release 0.2.1 for flutter ([b5785f9](https://github.com/huanglizhuo/QwenASR/commit/b5785f9e0a6e4cab3a4796bbd1bd401876ea5926)) +* update the both library readme to mention this is WIP project ([139a591](https://github.com/huanglizhuo/QwenASR/commit/139a5915205083abc4b87fd0228ccf4c725c99c0)) + +## [0.2.0](https://github.com/huanglizhuo/QwenASR/compare/qwen-asr-v0.1.2...qwen-asr-v0.2.0) (2026-02-22) + + +### Features + +* add missing parameter to qwen asr offline model ([f56e8b1](https://github.com/huanglizhuo/QwenASR/commit/f56e8b1e58731344fad92a7ed38c59a9f09267f6)) +* add missing parameter to qwen asr offline model ([6d1e38d](https://github.com/huanglizhuo/QwenASR/commit/6d1e38da19cbae46c2afe2e1af03a5d437679ef8)) diff --git a/vendor/qwenasr/crates/qwen-asr/Cargo.toml b/vendor/qwenasr/crates/qwen-asr/Cargo.toml new file mode 100644 index 0000000..45a1d1f --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "qwen-asr" +version = "0.5.0" +edition.workspace = true +license.workspace = true +description = "CPU-only Qwen3-ASR speech recognition (pure Rust)" +keywords = ["asr", "speech-recognition", "qwen", "inference"] +categories = ["multimedia::audio"] +repository = "https://github.com/huanglizhuo/QwenASR" +documentation = "https://docs.rs/qwen-asr" +readme = "README.md" + +[lib] +crate-type = ["cdylib", "staticlib", "rlib"] +name = "qwen_asr" + +[dependencies] +libc = "0.2" + +[package.metadata.docs.rs] +no-default-features = true + +[features] +default = ["blas", "vdsp"] +blas = [] +vdsp = [] +ffi = [] +ios = ["ffi"] +android = ["ffi"] +macos-ffi = ["ffi"] diff --git a/vendor/qwenasr/crates/qwen-asr/README.md b/vendor/qwenasr/crates/qwen-asr/README.md new file mode 100644 index 0000000..c105674 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/README.md @@ -0,0 +1,177 @@ +# qwen_asr + +CPU-only Qwen3-ASR speech recognition in pure Rust. No Python, no ONNX runtime, +no framework dependencies — just `libc` and BLAS. BF16 weights stay memory-mapped +for minimal RAM usage; SIMD kernels (NEON / AVX2+FMA) accelerate inference. + +## Prerequisites + +- Rust 1.70+ +- BLAS: Accelerate (macOS, linked automatically) or OpenBLAS (Linux) + +## Building + +Platform-specific optimizations are detected automatically at compile time: + +| Platform | BLAS | SIMD | +|----------|------|------| +| macOS (Apple Silicon) | Accelerate + vDSP | NEON (always available) | +| macOS (Intel) | Accelerate + vDSP | AVX2+FMA | +| Linux (x86_64) | OpenBLAS | AVX2+FMA | +| Linux (aarch64) | OpenBLAS | NEON | +| Other | OpenBLAS | Generic scalar fallback | + +For best performance, build with native CPU tuning so the compiler can emit +AVX2+FMA instructions on x86_64: + +```bash +RUSTFLAGS="-C target-cpu=native" cargo build --release +``` + +On AArch64 (Apple Silicon, ARM Linux) NEON is baseline — no extra flags needed, +though `-C target-cpu=native` is still recommended for other micro-architecture +tuning. + +**Important:** Always use `--release` mode. Debug builds are 10-50x slower due +to missing optimizations and are not usable for real-time inference. + +## Model Download + +```bash +# Install modelscope if needed +pip install modelscope + +# Download the 0.6B model (~1.3 GB) +python -c "from modelscope.hub.snapshot_download import snapshot_download; snapshot_download('Qwen/Qwen3-ASR-0.6B', cache_dir='.')" + +# Download the 0.6B forced-aligner model (~1.3 GB) +python -c "from modelscope.hub.snapshot_download import snapshot_download; snapshot_download('Qwen/Qwen3-ForcedAligner-0.6B', cache_dir='.')" +``` + +## Usage + +```rust,no_run +use qwen_asr::context::QwenCtx; +use qwen_asr::transcribe; + +fn main() { + // Load model (returns None on failure) + let mut ctx = QwenCtx::load("qwen3-asr-0.6b").expect("failed to load model"); + + // Transcribe a WAV file + let text = transcribe::transcribe(&mut ctx, "audio.wav").unwrap(); + println!("{}", text); +} +``` + +### Segmented Mode + +For long audio files, split into overlapping segments to reduce memory usage +and improve accuracy: + +```rust,no_run +use qwen_asr::context::QwenCtx; +use qwen_asr::transcribe; + +let mut ctx = QwenCtx::load("qwen3-asr-0.6b").unwrap(); +ctx.segment_sec = 30.0; // split every ~30 seconds + +let text = transcribe::transcribe(&mut ctx, "long-meeting.wav").unwrap(); +``` + +### Raw PCM Input + +```rust,no_run +use qwen_asr::context::QwenCtx; +use qwen_asr::transcribe; + +let mut ctx = QwenCtx::load("qwen3-asr-0.6b").unwrap(); + +// f32 samples at 16 kHz, mono, range [-1, 1] +let samples: Vec = load_audio_somehow(); +let text = transcribe::transcribe_audio(&mut ctx, &samples).unwrap(); +``` + +### Streaming API + +For real-time incremental transcription, use `StreamState` and `stream_push_audio`: + +```rust,no_run +use qwen_asr::context::QwenCtx; +use qwen_asr::transcribe::{StreamState, stream_push_audio}; + +let mut ctx = QwenCtx::load("qwen3-asr-0.6b").unwrap(); +let mut state = StreamState::new(); + +// As audio arrives (e.g., from a microphone), accumulate samples +let mut all_samples: Vec = Vec::new(); +loop { + let new_audio = get_audio_chunk(); // your audio source + all_samples.extend_from_slice(&new_audio); + + // Push all accumulated audio; stream_push_audio tracks its own cursor + if let Some(delta) = stream_push_audio(&mut ctx, &all_samples, &mut state, false) { + if !delta.is_empty() { + print!("{}", delta); // incremental output + } + } +} + +// Finalize to flush remaining tokens +stream_push_audio(&mut ctx, &all_samples, &mut state, true); +``` + +### Forced Alignment + +Produce word-level timestamps for a known transcript. Requires the +ForcedAligner model variant (`Qwen3-ASR-0.6B-Aligner`). + +```rust,no_run +use qwen_asr::context::QwenCtx; +use qwen_asr::align; + +let mut ctx = QwenCtx::load("qwen3-aligner-0.6b").unwrap(); +let samples: Vec = load_audio_somehow(); + +let results = align::forced_align(&mut ctx, &samples, "Hello world", "English") + .expect("alignment failed"); + +for r in &results { + println!("{}: {:.0} ms – {:.0} ms", r.text, r.start_ms, r.end_ms); +} +``` + +CLI: + +```bash +qwen-asr -d qwen3-aligner-0.6b -i audio.wav --align "Hello world" --align-language English +``` + +Each `AlignResult` contains the word text, `start_ms`, and `end_ms` timestamps. +For CJK languages the text is split at character level; for others it is split on +whitespace. + +## Feature Flags + +| Feature | Default | Description | +|-----------|---------|-------------| +| `blas` | yes | Link Accelerate (macOS) or OpenBLAS (Linux) for matrix ops | +| `vdsp` | yes | Use vDSP/vForce from Accelerate for dot products and exp (macOS only) | +| `ios` | no | Build C-FFI API for iOS integration | +| `android` | no | Build C-FFI + JNI API for Android integration | + +## Performance + +Benchmarks on Apple M2 Pro (10-core), 0.6B model: + +| Mode | Audio | Wall Time | Realtime Factor | +|------|-------|-----------|-----------------| +| Offline | 11 s | 1.8 s | 6.2x | +| Offline | 28 s | 4.0 s | 7.0x | +| Segmented (-S 30) | 45 s | 4.6 s | 9.8x | +| Streaming | 28 s | 10.4 s | 2.7x | +| Streaming (live) | 51 s | 14.1 s | 3.6x | + +## License + +MIT diff --git a/vendor/qwenasr/crates/qwen-asr/build.rs b/vendor/qwenasr/crates/qwen-asr/build.rs new file mode 100644 index 0000000..234e01c --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/build.rs @@ -0,0 +1,18 @@ +fn main() { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + + // Check if blas feature is enabled via CARGO_FEATURE_BLAS env var + if std::env::var("CARGO_FEATURE_BLAS").is_ok() { + match target_os.as_str() { + "macos" => { + println!("cargo:rustc-link-lib=framework=Accelerate"); + } + "linux" => { + println!("cargo:rustc-link-lib=openblas"); + } + _ => { + // No BLAS available, will use fallback matmul + } + } + } +} diff --git a/vendor/qwenasr/crates/qwen-asr/src/align.rs b/vendor/qwenasr/crates/qwen-asr/src/align.rs new file mode 100644 index 0000000..35cd16e --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/src/align.rs @@ -0,0 +1,389 @@ +//! Forced alignment: word- and character-level timestamps. + +use crate::audio; +use crate::config::*; +use crate::context::QwenCtx; +use crate::decoder::{self, tok_embed_bf16_to_f32}; +use crate::kernels; +use crate::tokenizer::QwenTokenizer; + +use std::time::Instant; + +// Reuse the same prompt structure as transcribe +const PREFIX_HEAD: &[i32] = &[151644, 8948, 198]; +const PREFIX_TAIL: &[i32] = &[151645, 198, 151644, 872, 198, 151669]; +const SUFFIX_BASE: &[i32] = &[151670, 151645, 198, 151644, 77091, 198]; + +/// A single word (or character for CJK) with its aligned time span. +/// +/// * `text` – the word or character this entry covers. +/// * `start_ms` – start time in milliseconds from the beginning of the audio. +/// * `end_ms` – end time in milliseconds. +#[derive(Debug, Clone)] +pub struct AlignResult { + pub text: String, + pub start_ms: f32, + pub end_ms: f32, +} + +fn get_time_ms() -> f64 { + static START: std::sync::OnceLock = std::sync::OnceLock::new(); + let start = START.get_or_init(Instant::now); + start.elapsed().as_secs_f64() * 1000.0 +} + +fn elapsed_ms(t0: f64) -> f64 { + get_time_ms() - t0 +} + +/// Split text into words based on language. +/// English/space-delimited: split on whitespace. +/// CJK (Chinese, Japanese, Korean, Cantonese): character-level split. +fn split_words(text: &str, language: &str) -> Vec { + let is_cjk = matches!(language, "Chinese" | "Japanese" | "Korean" | "Cantonese"); + + if is_cjk { + // Character-level split (each Unicode character is a "word") + text.chars() + .filter(|c| !c.is_whitespace()) + .map(|c| c.to_string()) + .collect() + } else { + // Space-delimited split + text.split_whitespace().map(|s| s.to_string()).collect() + } +} + +/// Build the input token sequence for forced alignment. +/// Interleaves between words and appends at end. +/// Returns (word_list, token_ids) where token_ids includes audio markers + text + timestamps. +fn encode_timestamp( + text: &str, + language: &str, + tokenizer: &QwenTokenizer, +) -> Option<(Vec, Vec)> { + let words = split_words(text, language); + if words.is_empty() { + return None; + } + + // Build the text with interleaved timestamp tokens: + // word1 word2 ... wordN + // But we tokenize each word separately and insert timestamp token IDs between them. + let mut token_ids: Vec = Vec::new(); + + for (i, word) in words.iter().enumerate() { + let word_tokens = tokenizer.encode(word)?; + token_ids.extend_from_slice(&word_tokens); + // Append after each word (including the last) + token_ids.push(TOKEN_TIMESTAMP); + token_ids.push(TOKEN_TIMESTAMP); + + if i == 0 && word_tokens.is_empty() { + return None; // First word must produce tokens + } + } + + Some((words, token_ids)) +} + +/// Find the longest increasing subsequence indices. +fn longest_increasing_subsequence(vals: &[f32]) -> Vec { + let n = vals.len(); + if n == 0 { + return Vec::new(); + } + + // dp[i] = length of LIS ending at i + let mut dp = vec![1usize; n]; + let mut prev = vec![usize::MAX; n]; + + for i in 1..n { + for j in 0..i { + if vals[j] <= vals[i] && dp[j] + 1 > dp[i] { + dp[i] = dp[j] + 1; + prev[i] = j; + } + } + } + + // Find the end of the longest sequence + let mut best_len = 0; + let mut best_end = 0; + for (i, &dp_val) in dp.iter().enumerate().take(n) { + if dp_val > best_len { + best_len = dp_val; + best_end = i; + } + } + + // Trace back + let mut lis_indices = Vec::with_capacity(best_len); + let mut idx = best_end; + loop { + lis_indices.push(idx); + if prev[idx] == usize::MAX { + break; + } + idx = prev[idx]; + } + lis_indices.reverse(); + lis_indices +} + +/// Fix anomalous timestamps using LIS + interpolation. +fn fix_timestamps(timestamps: &mut [f32]) { + if timestamps.len() <= 1 { + return; + } + + let lis_indices = longest_increasing_subsequence(timestamps); + if lis_indices.len() == timestamps.len() { + return; // Already monotonically increasing + } + + // Mark which indices are in the LIS (normal) + let n = timestamps.len(); + let mut is_normal = vec![false; n]; + for &idx in &lis_indices { + is_normal[idx] = true; + } + + // Fix anomalous regions + let mut i = 0; + while i < n { + if is_normal[i] { + i += 1; + continue; + } + + // Find the extent of this anomalous block + let block_start = i; + while i < n && !is_normal[i] { + i += 1; + } + let block_end = i; // exclusive + let block_len = block_end - block_start; + + // Get boundary values + let left_val = if block_start > 0 { + timestamps[block_start - 1] + } else { + 0.0 + }; + let right_val = if block_end < n { + timestamps[block_end] + } else { + left_val + block_len as f32 * 80.0 + }; + + if block_len <= 2 { + // Small block: fill with nearest normal value + let fill = if block_start > 0 { left_val } else { right_val }; + for ts in timestamps.iter_mut().take(block_end).skip(block_start) { + *ts = fill; + } + } else { + // Larger block: linearly interpolate + for j in 0..block_len { + let t = (j + 1) as f32 / (block_len + 1) as f32; + timestamps[block_start + j] = left_val + t * (right_val - left_val); + } + } + } +} + +/// Perform forced alignment on audio samples with a known transcript. +/// +/// Requires a ForcedAligner model (one where `config.classify_num > 0`). +/// Returns `None` if the model is not an aligner, the text is empty, or +/// encoding fails. +/// +/// `language` controls how `text` is split into units: for CJK languages +/// (`"Chinese"`, `"Japanese"`, `"Korean"`, `"Cantonese"`) each character +/// becomes a separate entry; for all other languages the text is split on +/// whitespace. +/// +/// The returned vector has one [`AlignResult`] per word/character with +/// monotonically non-decreasing timestamps (anomalies are corrected via +/// LIS + interpolation). +pub fn forced_align( + ctx: &mut QwenCtx, + samples: &[f32], + text: &str, + language: &str, +) -> Option> { + let shared = ctx.shared.clone(); + let cfg = &shared.config.clone(); + let dim = cfg.dec_hidden; + let seg_time = cfg.timestamp_segment_time; + + if !cfg.is_aligner() { + eprintln!("align: model is not a forced aligner (classify_num=0)"); + return None; + } + + let vocab_path = format!("{}/vocab.json", shared.model_dir); + let tokenizer = QwenTokenizer::load(&vocab_path)?; + + ctx.reset_perf(); + ctx.perf_audio_ms = 1000.0 * samples.len() as f64 / SAMPLE_RATE as f64; + + let seg_t0 = get_time_ms(); + // Step 1: Tokenize text with timestamp interleaving + let (words, text_tokens) = encode_timestamp(text, language, &tokenizer)?; + + if kernels::verbose() >= 2 { + eprintln!( + " Align: {} words, {} text tokens", + words.len(), + text_tokens.len() + ); + } + + // Step 2: Mel spectrogram + encoder + let t0 = get_time_ms(); + let (mel, mel_frames) = audio::mel_spectrogram(samples)?; + let mel_ms = elapsed_ms(t0); + + let t0 = get_time_ms(); + let (enc_output, enc_seq_len) = + shared + .encoder + .forward(cfg, &mel, mel_frames, Some(&mut ctx.enc_bufs))?; + let enc_ms = elapsed_ms(t0); + + if kernels::verbose() >= 2 { + eprintln!( + " Mel: {} frames ({:.0} ms), Encoder: {} tokens ({:.0} ms)", + mel_frames, mel_ms, enc_seq_len, enc_ms + ); + } + + // Step 3: Build input embeddings + // Structure: PREFIX_HEAD + PREFIX_TAIL + [encoder output] + SUFFIX_BASE + text_tokens + // (No prompt tokens or language forcing for alignment) + let prefix_len = PREFIX_HEAD.len() + PREFIX_TAIL.len(); + let suffix_len = SUFFIX_BASE.len(); + let total_seq = prefix_len + enc_seq_len + suffix_len + text_tokens.len(); + + let mut input_embeds = vec![0.0f32; total_seq * dim]; + let tok_emb = shared.decoder.tok_embeddings_bf16; + + let mut off = 0; + for &tok in PREFIX_HEAD { + unsafe { + tok_embed_bf16_to_f32( + &mut input_embeds[off * dim..(off + 1) * dim], + tok_emb, + tok, + dim, + ); + } + off += 1; + } + for &tok in PREFIX_TAIL { + unsafe { + tok_embed_bf16_to_f32( + &mut input_embeds[off * dim..(off + 1) * dim], + tok_emb, + tok, + dim, + ); + } + off += 1; + } + + // Encoder output + for i in 0..enc_seq_len { + input_embeds[(prefix_len + i) * dim..(prefix_len + i + 1) * dim] + .copy_from_slice(&enc_output[i * dim..(i + 1) * dim]); + } + + // Suffix + let suffix_off = prefix_len + enc_seq_len; + for (i, &tok) in SUFFIX_BASE.iter().enumerate() { + unsafe { + tok_embed_bf16_to_f32( + &mut input_embeds[(suffix_off + i) * dim..(suffix_off + i + 1) * dim], + tok_emb, + tok, + dim, + ); + } + } + + // Text tokens (with interleaved tokens) + let text_off = suffix_off + suffix_len; + for (i, &tok) in text_tokens.iter().enumerate() { + unsafe { + tok_embed_bf16_to_f32( + &mut input_embeds[(text_off + i) * dim..(text_off + i + 1) * dim], + tok_emb, + tok, + dim, + ); + } + } + + // Step 4: Single prefill pass → logits for all positions + let t0 = get_time_ms(); + ctx.kv_cache.len = 0; + + let logits = decoder::decoder_prefill_logits( + &shared.decoder, + cfg, + &mut ctx.kv_cache, + &mut ctx.rope_cache, + &mut ctx.dec_bufs, + &input_embeds, + total_seq, + ); + let prefill_ms = elapsed_ms(t0); + + if kernels::verbose() >= 2 { + eprintln!(" Prefill: {} tokens ({:.0} ms)", total_seq, prefill_ms); + } + + // Step 5: Extract timestamps from positions + let out_dim = cfg.lm_head_dim(); + let mut raw_timestamps: Vec = Vec::new(); + + for (i, &tok) in text_tokens.iter().enumerate() { + if tok == TOKEN_TIMESTAMP { + let pos = text_off + i; + let logit_row = &logits[pos * out_dim..(pos + 1) * out_dim]; + let mut best_idx = 0; + let mut best_val = logit_row[0]; + for (j, &val) in logit_row.iter().enumerate().take(out_dim).skip(1) { + if val > best_val { + best_val = val; + best_idx = j; + } + } + raw_timestamps.push(best_idx as f32 * seg_time); + } + } + + // Step 6: Fix timestamps (LIS + interpolation) + fix_timestamps(&mut raw_timestamps); + + // Step 7: Pair consecutive timestamps into (start, end) per word + // Each word has 2 timestamps: start, end + let mut results = Vec::with_capacity(words.len()); + for (i, word) in words.iter().enumerate() { + let start_ms = raw_timestamps.get(i * 2).copied().unwrap_or(0.0); + let end_ms = raw_timestamps.get(i * 2 + 1).copied().unwrap_or(start_ms); + results.push(AlignResult { + text: word.clone(), + start_ms, + end_ms, + }); + } + + ctx.perf_total_ms += elapsed_ms(seg_t0); + ctx.perf_encode_ms += mel_ms + enc_ms; + ctx.perf_decode_ms += prefill_ms; + + Some(results) +} diff --git a/vendor/qwenasr/crates/qwen-asr/src/audio.rs b/vendor/qwenasr/crates/qwen-asr/src/audio.rs new file mode 100644 index 0000000..5a90b8c --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/src/audio.rs @@ -0,0 +1,479 @@ +//! WAV loading, resampling, and mel spectrogram computation. + +use crate::config::*; +use crate::kernels; + +const N_FFT: usize = 400; +const N_FREQ: usize = N_FFT / 2 + 1; // 201 + +fn read_u16_le(data: &[u8]) -> u16 { + u16::from_le_bytes([data[0], data[1]]) +} + +fn read_u32_le(data: &[u8]) -> u32 { + u32::from_le_bytes([data[0], data[1], data[2], data[3]]) +} + +/// Parse a WAV file buffer into f32 samples at 16 kHz mono. +/// +/// Accepts 16-bit PCM WAV at any sample rate (resampled automatically) and +/// any channel count (downmixed to mono). Returns `None` if the buffer is not +/// a valid WAV or uses an unsupported encoding (e.g. float, compressed). +pub fn parse_wav_buffer(data: &[u8]) -> Option> { + if data.len() < 44 || &data[0..4] != b"RIFF" || &data[8..12] != b"WAVE" { + eprintln!("parse_wav_buffer: not a valid WAV file"); + return None; + } + + let mut channels = 0i32; + let mut sample_rate = 0i32; + let mut bits_per_sample = 0i32; + let mut audio_format = 0i32; + let mut pcm_data: Option<&[u8]> = None; + + let mut p = 12; + while p + 8 <= data.len() { + let chunk_id = &data[p..p + 4]; + let chunk_size = read_u32_le(&data[p + 4..]) as usize; + if p + 8 + chunk_size > data.len() { + break; + } + + if chunk_id == b"fmt " && chunk_size >= 16 { + audio_format = read_u16_le(&data[p + 8..]) as i32; + channels = read_u16_le(&data[p + 10..]) as i32; + sample_rate = read_u32_le(&data[p + 12..]) as i32; + bits_per_sample = read_u16_le(&data[p + 22..]) as i32; + } else if chunk_id == b"data" { + let end = (p + 8 + chunk_size).min(data.len()); + pcm_data = Some(&data[p + 8..end]); + } + + p += 8 + chunk_size; + if chunk_size & 1 != 0 { + p += 1; + } + } + + if audio_format != 1 || bits_per_sample != 16 || channels < 1 { + eprintln!( + "parse_wav_buffer: unsupported format (need 16-bit PCM, got fmt={} bits={})", + audio_format, bits_per_sample + ); + return None; + } + + let pcm = pcm_data?; + let n_frames = pcm.len() / (channels as usize * 2); + let mut samples = vec![0.0f32; n_frames]; + + for i in 0..n_frames { + if channels == 1 { + let val = i16::from_le_bytes([pcm[i * 2], pcm[i * 2 + 1]]); + samples[i] = val as f32 / 32768.0; + } else { + let mut sum = 0.0f32; + for c in 0..channels as usize { + let off = (i * channels as usize + c) * 2; + let val = i16::from_le_bytes([pcm[off], pcm[off + 1]]); + sum += val as f32; + } + samples[i] = (sum / channels as f32) / 32768.0; + } + } + + // Resample to 16kHz if needed + if sample_rate != SAMPLE_RATE { + samples = resample(&samples, sample_rate, SAMPLE_RATE); + } + + Some(samples) +} + +/// Read a WAV file from disk and return f32 samples at 16 kHz mono. +/// +/// Equivalent to `std::fs::read` + [`parse_wav_buffer`]. +pub fn load_wav(path: &str) -> Option> { + let data = std::fs::read(path).ok()?; + parse_wav_buffer(&data) +} + +/// Read audio from stdin (auto-detect WAV or raw s16le 16kHz mono). +pub fn read_pcm_stdin() -> Option> { + use std::io::Read; + let mut buf = Vec::new(); + std::io::stdin().read_to_end(&mut buf).ok()?; + + if buf.len() < 4 { + eprintln!("read_pcm_stdin: no data on stdin"); + return None; + } + + if &buf[0..4] == b"RIFF" { + if kernels::verbose() >= 2 { + eprintln!("Detected WAV format on stdin"); + } + return parse_wav_buffer(&buf); + } + + // Raw s16le 16kHz mono + if kernels::verbose() >= 2 { + eprintln!("Treating stdin as raw s16le 16kHz mono"); + } + let n_frames = buf.len() / 2; + let mut samples = vec![0.0f32; n_frames]; + for i in 0..n_frames { + let val = i16::from_le_bytes([buf[i * 2], buf[i * 2 + 1]]); + samples[i] = val as f32 / 32768.0; + } + Some(samples) +} + +/// Kaiser-windowed sinc resampler. +pub fn resample(samples: &[f32], from_rate: i32, to_rate: i32) -> Vec { + let n_frames = samples.len(); + let new_n = (n_frames as i64 * to_rate as i64 / from_rate as i64) as usize; + let mut resampled = vec![0.0f32; new_n]; + + let sinc_half = 16; + let kaiser_beta = 6.0f64; + let ratio = to_rate as f64 / from_rate as f64; + let cutoff = if ratio < 1.0 { ratio } else { 1.0 }; + + // Bessel I0 approximation + fn bessel_i0(x: f64) -> f64 { + let mut sum = 1.0; + let mut term = 1.0; + let xx = x * x; + for k in 1..=20 { + term *= xx / (4.0 * k as f64 * k as f64); + sum += term; + } + sum + } + + let inv_i0_beta = 1.0 / bessel_i0(kaiser_beta); + + for (i, resampled_val) in resampled.iter_mut().enumerate().take(new_n) { + let src_pos = i as f64 / ratio; + let center = src_pos as i32; + let mut acc = 0.0f64; + let mut wsum = 0.0f64; + + let j_lo = center - sinc_half + 1; + let j_hi = center + sinc_half; + + for j in j_lo..=j_hi { + let d = j as f64 - src_pos; + let x = d * cutoff; + + // Sinc + let s = if x.abs() < 1e-9 { + 1.0 + } else { + (std::f64::consts::PI * x).sin() / (std::f64::consts::PI * x) + }; + + // Kaiser window + let npos = d / sinc_half as f64; + let w = if npos <= -1.0 || npos >= 1.0 { + 0.0 + } else { + bessel_i0(kaiser_beta * (1.0 - npos * npos).sqrt()) * inv_i0_beta + }; + + let coeff = s * w * cutoff; + if j >= 0 && (j as usize) < n_frames { + acc += samples[j as usize] as f64 * coeff; + } + wsum += coeff; + } + + *resampled_val = if wsum > 1e-9 { (acc / wsum) as f32 } else { 0.0 }; + } + + resampled +} + +// ======================================================================== +// Mel Filter Bank (Slaney-style) +// ======================================================================== + +fn hertz_to_mel(freq: f32) -> f32 { + let min_log_hertz = 1000.0f32; + let min_log_mel = 15.0f32; + let logstep = 27.0 / (6.4f32).ln(); + let mels = 3.0 * freq / 200.0; + if freq >= min_log_hertz { + min_log_mel + (freq / min_log_hertz).ln() * logstep + } else { + mels + } +} + +fn mel_to_hertz(mels: f32) -> f32 { + let min_log_hertz = 1000.0f32; + let min_log_mel = 15.0f32; + let logstep = (6.4f32).ln() / 27.0; + if mels >= min_log_mel { + min_log_hertz * (logstep * (mels - min_log_mel)).exp() + } else { + 200.0 * mels / 3.0 + } +} + +fn build_mel_filters() -> Vec { + let mut filters = vec![0.0f32; MEL_BINS * N_FREQ]; + + let mut fft_freqs = vec![0.0f32; N_FREQ]; + for (i, freq) in fft_freqs.iter_mut().enumerate().take(N_FREQ) { + *freq = i as f32 * (SAMPLE_RATE as f32 / 2.0) / (N_FREQ - 1) as f32; + } + + let mel_min = hertz_to_mel(0.0); + let mel_max = hertz_to_mel(SAMPLE_RATE as f32 / 2.0); + + let n_filters = MEL_BINS; + let mut filter_freqs = vec![0.0f32; n_filters + 2]; + let mut filter_diff = vec![0.0f32; n_filters + 1]; + + for (i, filter_freq) in filter_freqs.iter_mut().enumerate().take(n_filters + 2) { + let mel = mel_min + (mel_max - mel_min) * i as f32 / (n_filters + 1) as f32; + *filter_freq = mel_to_hertz(mel); + } + for i in 0..n_filters + 1 { + filter_diff[i] = filter_freqs[i + 1] - filter_freqs[i]; + if filter_diff[i] == 0.0 { + filter_diff[i] = 1e-6; + } + } + + for m in 0..n_filters { + let enorm = 2.0 / (filter_freqs[m + 2] - filter_freqs[m]); + for f in 0..N_FREQ { + let down = (fft_freqs[f] - filter_freqs[m]) / filter_diff[m]; + let up = (filter_freqs[m + 2] - fft_freqs[f]) / filter_diff[m + 1]; + let val = down.min(up).max(0.0); + filters[m * N_FREQ + f] = val * enorm; + } + } + + filters +} + +// ======================================================================== +// Mel Spectrogram +// ======================================================================== + +/// Compute a 128-bin log-mel spectrogram from 16 kHz audio samples. +/// +/// Returns `(mel_flat, n_frames)` where `mel_flat` has shape `[128, n_frames]` +/// in row-major order. Returns `None` if the audio is too short to produce +/// even one frame. +pub fn mel_spectrogram(samples: &[f32]) -> Option<(Vec, usize)> { + let n_samples = samples.len(); + let n_fft = N_FFT; + let n_freqs = N_FREQ; + let pad_len = n_fft / 2; + + // Reflect-pad the signal + let padded_len = n_samples + 2 * pad_len; + let mut padded = vec![0.0f32; padded_len]; + + for (i, padded_val) in padded.iter_mut().enumerate().take(pad_len) { + let src = pad_len - i; + *padded_val = if src < n_samples { samples[src] } else { 0.0 }; + } + padded[pad_len..pad_len + n_samples].copy_from_slice(samples); + for i in 0..pad_len { + let src = n_samples as i32 - 2 - i as i32; + padded[pad_len + n_samples + i] = if src >= 0 { samples[src as usize] } else { 0.0 }; + } + + let n_frames_total = (padded_len - n_fft) / HOP_LENGTH + 1; + let n_frames = n_frames_total - 1; // drop last frame + if n_frames == 0 { + eprintln!("mel_spectrogram: audio too short ({} samples)", n_samples); + return None; + } + + static MEL_FILTERS: std::sync::OnceLock> = std::sync::OnceLock::new(); + let mel_filters = MEL_FILTERS.get_or_init(build_mel_filters); + + // Periodic Hann window (cached) + static HANN_WINDOW: std::sync::OnceLock> = std::sync::OnceLock::new(); + let window = HANN_WINDOW.get_or_init(|| { + let mut w = vec![0.0f32; WINDOW_SIZE]; + for (i, w_val) in w.iter_mut().enumerate().take(WINDOW_SIZE) { + *w_val = 0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / WINDOW_SIZE as f32).cos()); + } + w + }); + + // Precompute DFT tables (cached) + static DFT_TABLES: std::sync::OnceLock<(Vec, Vec)> = std::sync::OnceLock::new(); + let (dft_cos, dft_sin) = DFT_TABLES.get_or_init(|| { + let mut cos_tbl = vec![0.0f32; N_FREQ * N_FFT]; + let mut sin_tbl = vec![0.0f32; N_FREQ * N_FFT]; + for k in 0..N_FREQ { + for n in 0..N_FFT { + let angle = 2.0 * std::f32::consts::PI * k as f32 * n as f32 / N_FFT as f32; + cos_tbl[k * N_FFT + n] = angle.cos(); + sin_tbl[k * N_FFT + n] = angle.sin(); + } + } + (cos_tbl, sin_tbl) + }); + + // Batched computation via BLAS sgemm: + // 1. Pre-compute all windowed frames: windowed[N_FFT × n_frames] column-major + let mut windowed_all = vec![0.0f32; N_FFT * n_frames]; + for t in 0..n_frames { + let start = t * HOP_LENGTH; + for n in 0..N_FFT { + windowed_all[n * n_frames + t] = padded[start + n] * window[n]; + } + } + + // 2. DFT via BLAS: re = dft_cos @ windowed_all, im = dft_sin @ windowed_all + // [N_FREQ × N_FFT] @ [N_FFT × n_frames] = [N_FREQ × n_frames] + let mut re = vec![0.0f32; n_freqs * n_frames]; + let mut im = vec![0.0f32; n_freqs * n_frames]; + kernels::matmul_nn(&mut re, dft_cos, &windowed_all, n_freqs, N_FFT, n_frames); + kernels::matmul_nn(&mut im, dft_sin, &windowed_all, n_freqs, N_FFT, n_frames); + drop(windowed_all); + + // 3. Power spectrum: power[k * n_frames + t] = re² + im² + let mut power = vec![0.0f32; n_freqs * n_frames]; + for i in 0..n_freqs * n_frames { + power[i] = re[i] * re[i] + im[i] * im[i]; + } + drop(re); + drop(im); + + // 4. Mel filter bank via BLAS: mel_raw = mel_filters @ power + // [MEL_BINS × N_FREQ] @ [N_FREQ × n_frames] = [MEL_BINS × n_frames] + let mut mel = vec![0.0f32; MEL_BINS * n_frames]; + kernels::matmul_nn(&mut mel, mel_filters, &power, MEL_BINS, n_freqs, n_frames); + drop(power); + + // 5. Log, clamp, normalize + let mut global_max = -1e30f32; + for val in mel.iter_mut() { + *val = (*val).max(1e-10).log10(); + if *val > global_max { global_max = *val; } + } + let min_val = global_max - 8.0; + for val in mel.iter_mut() { + *val = ((*val).max(min_val) + 4.0) / 4.0; + } + + Some((mel, n_frames)) +} + +/// Drop long silent spans. Adaptive RMS gating with spike rejection. +pub fn compact_silence(samples: &[f32]) -> Vec { + let n_samples = samples.len(); + if n_samples == 0 { + return Vec::new(); + } + + let win = 160; // 10ms at 16kHz + let base_thresh = 0.002f32; + let max_thresh = 0.025f32; + let smooth_alpha = 0.2f32; + let min_voice_windows = 5; + let pad_voice_windows = 3; + let pass_windows = 60; + + let n_win = n_samples.div_ceil(win); + let mut rms_vals = vec![0.0f32; n_win]; + + for (w, rms_val) in rms_vals.iter_mut().enumerate().take(n_win) { + let start = w * win; + let end = (start + win).min(n_samples); + let len = end - start; + let mut energy = 0.0f32; + for sample in samples.iter().take(end).skip(start) { + energy += sample * sample; + } + *rms_val = (energy / len.max(1) as f32).sqrt(); + } + + // Smooth RMS + let mut smooth_vals = vec![0.0f32; n_win]; + let mut smooth = rms_vals[0]; + for w in 0..n_win { + smooth = (1.0 - smooth_alpha) * smooth + smooth_alpha * rms_vals[w]; + smooth_vals[w] = smooth; + } + + // Adaptive threshold from 25th percentile + let mut sorted = smooth_vals.clone(); + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let p25 = ((n_win - 1) as f32 * 0.25) as usize; + let noise_floor = sorted[p25]; + let thresh = (noise_floor * 1.8).clamp(base_thresh, max_thresh); + + let mut is_voice = vec![false; n_win]; + for w in 0..n_win { + is_voice[w] = smooth_vals[w] > thresh; + } + + // Remove short voice bursts + let mut i = 0; + while i < n_win { + if !is_voice[i] { + i += 1; + continue; + } + let mut j = i + 1; + while j < n_win && is_voice[j] { + j += 1; + } + if j - i < min_voice_windows { + for is_voice_val in is_voice.iter_mut().take(j).skip(i) { + *is_voice_val = false; + } + } + i = j; + } + + // Pad voice edges + let mut padded_voice = vec![false; n_win]; + for (w, &voice) in is_voice.iter().enumerate().take(n_win) { + if !voice { + continue; + } + let a = w.saturating_sub(pad_voice_windows); + let b = (w + pad_voice_windows).min(n_win - 1); + for padded_val in padded_voice.iter_mut().take(b + 1).skip(a) { + *padded_val = true; + } + } + + let mut out = Vec::with_capacity(n_samples); + let mut silence_count = 0; + + for (w, &pv) in padded_voice.iter().enumerate().take(n_win) { + let start = w * win; + let end = (start + win).min(n_samples); + + if pv { + out.extend_from_slice(&samples[start..end]); + silence_count = 0; + } else { + silence_count += 1; + if silence_count <= pass_windows { + out.extend_from_slice(&samples[start..end]); + } + } + } + + if out.is_empty() { + let keep = n_samples.min(SAMPLE_RATE as usize / 2); + out.extend_from_slice(&samples[..keep]); + } + + out +} diff --git a/vendor/qwenasr/crates/qwen-asr/src/c_api.rs b/vendor/qwenasr/crates/qwen-asr/src/c_api.rs new file mode 100644 index 0000000..0f988f9 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/src/c_api.rs @@ -0,0 +1,386 @@ +//! C-FFI API for iOS and native integration. +//! +//! Compile with: `cargo build --release --target aarch64-apple-ios --features ios` + +use std::ffi::{CStr, CString}; +use std::fmt::Write; +use std::os::raw::c_char; + +use crate::align; +use crate::audio; +use crate::context::QwenCtx; +use crate::kernels; +use crate::transcribe; + +/// Opaque handle to the ASR engine. +pub struct QwenAsrEngine { + ctx: QwenCtx, +} + +fn push_json_string(dst: &mut String, value: &str) { + dst.push('"'); + for ch in value.chars() { + match ch { + '"' => dst.push_str("\\\""), + '\\' => dst.push_str("\\\\"), + '\n' => dst.push_str("\\n"), + '\r' => dst.push_str("\\r"), + '\t' => dst.push_str("\\t"), + c if c.is_control() => { + let _ = write!(dst, "\\u{:04x}", c as u32); + } + c => dst.push(c), + } + } + dst.push('"'); +} + +/// Load model from a directory path. Returns null on failure. +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_load_model( + model_dir: *const c_char, + n_threads: i32, + verbosity: i32, +) -> *mut QwenAsrEngine { + if model_dir.is_null() { + return std::ptr::null_mut(); + } + let dir = match CStr::from_ptr(model_dir).to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + + kernels::set_verbose(verbosity); + + let threads = if n_threads <= 0 { + kernels::get_num_cpus() + } else { + n_threads as usize + }; + kernels::set_threads(threads); + + match QwenCtx::load(dir) { + Some(ctx) => Box::into_raw(Box::new(QwenAsrEngine { ctx })), + None => std::ptr::null_mut(), + } +} + +/// Transcribe a WAV file. Returns a heap-allocated C string (caller must free with qwen_asr_free_string). +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_transcribe_file( + engine: *mut QwenAsrEngine, + wav_path: *const c_char, +) -> *mut c_char { + if engine.is_null() || wav_path.is_null() { + return std::ptr::null_mut(); + } + let eng = &mut *engine; + let path = match CStr::from_ptr(wav_path).to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + + match transcribe::transcribe(&mut eng.ctx, path) { + Some(text) => match CString::new(text) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Transcribe raw PCM samples (f32, 16kHz, mono). +/// Returns a heap-allocated C string (caller must free with qwen_asr_free_string). +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_transcribe_pcm( + engine: *mut QwenAsrEngine, + samples: *const f32, + n_samples: i32, +) -> *mut c_char { + if engine.is_null() || samples.is_null() || n_samples <= 0 { + return std::ptr::null_mut(); + } + let eng = &mut *engine; + let pcm = std::slice::from_raw_parts(samples, n_samples as usize); + + match transcribe::transcribe_audio(&mut eng.ctx, pcm) { + Some(text) => match CString::new(text) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Transcribe raw WAV buffer (entire file contents including header). +/// Returns a heap-allocated C string (caller must free with qwen_asr_free_string). +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_transcribe_wav_buffer( + engine: *mut QwenAsrEngine, + wav_data: *const u8, + wav_len: i32, +) -> *mut c_char { + if engine.is_null() || wav_data.is_null() || wav_len <= 0 { + return std::ptr::null_mut(); + } + let eng = &mut *engine; + let data = std::slice::from_raw_parts(wav_data, wav_len as usize); + + let samples = match audio::parse_wav_buffer(data) { + Some(s) => s, + None => return std::ptr::null_mut(), + }; + + match transcribe::transcribe_audio(&mut eng.ctx, &samples) { + Some(text) => match CString::new(text) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Force-align a WAV file against a known transcript. +/// Returns a heap-allocated JSON string: +/// [{"text":"Hello","start_ms":80.0,"end_ms":520.0}, ...] +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_force_align_file( + engine: *mut QwenAsrEngine, + wav_path: *const c_char, + text: *const c_char, + language: *const c_char, +) -> *mut c_char { + if engine.is_null() || wav_path.is_null() || text.is_null() || language.is_null() { + return std::ptr::null_mut(); + } + let eng = &mut *engine; + let path = match CStr::from_ptr(wav_path).to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + let transcript = match CStr::from_ptr(text).to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + let align_language = match CStr::from_ptr(language).to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + + let samples = match audio::load_wav(path) { + Some(s) => s, + None => return std::ptr::null_mut(), + }; + let results = match align::forced_align(&mut eng.ctx, &samples, transcript, align_language) { + Some(items) => items, + None => return std::ptr::null_mut(), + }; + + let mut json = String::from("["); + for (index, item) in results.iter().enumerate() { + if index > 0 { + json.push(','); + } + json.push_str("{\"text\":"); + push_json_string(&mut json, &item.text); + let _ = write!( + &mut json, + ",\"start_ms\":{:.3},\"end_ms\":{:.3}}}", + item.start_ms, + item.end_ms, + ); + } + json.push(']'); + + match CString::new(json) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Set segmentation seconds (0 = no segmentation). +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_set_segment_sec(engine: *mut QwenAsrEngine, sec: f32) { + if !engine.is_null() { + (*engine).ctx.segment_sec = sec; + } +} + +/// Set language (e.g. "English", "Chinese"). Empty string = auto-detect. +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_set_language( + engine: *mut QwenAsrEngine, + language: *const c_char, +) -> i32 { + if engine.is_null() || language.is_null() { + return -1; + } + let lang = match CStr::from_ptr(language).to_str() { + Ok(s) => s, + Err(_) => return -1, + }; + match (*engine).ctx.set_force_language(lang) { + Ok(()) => 0, + Err(()) => -1, + } +} + +/// Free a string returned by qwen_asr_transcribe_*. +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_free_string(s: *mut c_char) { + if !s.is_null() { + drop(CString::from_raw(s)); + } +} + +/// Free the engine. +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_free(engine: *mut QwenAsrEngine) { + if !engine.is_null() { + drop(Box::from_raw(engine)); + } +} + +// ======================================================================== +// Streaming API +// ======================================================================== + +/// Opaque handle to streaming state. +pub struct QwenAsrStreamState { + state: transcribe::StreamState, + /// Accumulated audio buffer (stream_push_audio requires full buffer). + audio_buf: Vec, +} + +/// Create a new streaming state. Returns null on failure. +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_stream_new() -> *mut QwenAsrStreamState { + Box::into_raw(Box::new(QwenAsrStreamState { + state: transcribe::StreamState::new(), + audio_buf: Vec::new(), + })) +} + +/// Free a streaming state. +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_stream_free(stream: *mut QwenAsrStreamState) { + if !stream.is_null() { + drop(Box::from_raw(stream)); + } +} + +/// Reset streaming state for a new utterance (reuses allocations). +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_stream_reset(stream: *mut QwenAsrStreamState) { + if !stream.is_null() { + let s = &mut *stream; + s.state.reset(); + s.audio_buf.clear(); + } +} + +/// Push new audio samples and get incremental text delta. +/// +/// `samples` / `n_samples`: new PCM chunk (f32, 16 kHz, mono). +/// `finalize`: set to 1 to signal end-of-stream and flush remaining tokens. +/// +/// Returns a heap-allocated C string with newly emitted text (may be empty), +/// or null if nothing was emitted. Caller must free with `qwen_asr_free_string`. +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_stream_push( + engine: *mut QwenAsrEngine, + stream: *mut QwenAsrStreamState, + samples: *const f32, + n_samples: i32, + finalize: i32, +) -> *mut c_char { + if engine.is_null() || stream.is_null() { + return std::ptr::null_mut(); + } + + let eng = &mut *engine; + let s = &mut *stream; + + // Append new samples to accumulated buffer + if !samples.is_null() && n_samples > 0 { + let new_samples = std::slice::from_raw_parts(samples, n_samples as usize); + s.audio_buf.extend_from_slice(new_samples); + } + + // Call the Rust streaming API with the full accumulated buffer + match transcribe::stream_push_audio( + &mut eng.ctx, + &s.audio_buf, + &mut s.state, + finalize != 0, + ) { + Some(delta) if !delta.is_empty() => match CString::new(delta) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + _ => std::ptr::null_mut(), + } +} + +/// Get the full accumulated transcription result so far. +/// Returns a heap-allocated C string. Caller must free with `qwen_asr_free_string`. +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_stream_get_result( + stream: *mut QwenAsrStreamState, +) -> *mut c_char { + if stream.is_null() { + return std::ptr::null_mut(); + } + let s = &*stream; + let text = s.state.text(); + if text.is_empty() { + return std::ptr::null_mut(); + } + match CString::new(text) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Configure streaming chunk size in seconds (default 2.0). +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_stream_set_chunk_sec(engine: *mut QwenAsrEngine, sec: f32) { + if !engine.is_null() && sec > 0.0 { + (*engine).ctx.stream_chunk_sec = sec; + } +} + +/// Configure token rollback window (default 5). +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_stream_set_rollback(engine: *mut QwenAsrEngine, tokens: i32) { + if !engine.is_null() && tokens >= 0 { + (*engine).ctx.stream_rollback = tokens; + } +} + +/// Configure unfixed chunks count before emitting (default 2). +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_stream_set_unfixed_chunks(engine: *mut QwenAsrEngine, chunks: i32) { + if !engine.is_null() && chunks >= 0 { + (*engine).ctx.stream_unfixed_chunks = chunks; + } +} + +/// Configure max new tokens per chunk (default 32). +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_stream_set_max_new_tokens(engine: *mut QwenAsrEngine, tokens: i32) { + if !engine.is_null() && tokens > 0 { + (*engine).ctx.stream_max_new_tokens = tokens; + } +} + +/// Configure whether past text is reused as streaming context. +#[no_mangle] +pub unsafe extern "C" fn qwen_asr_stream_set_past_text( + engine: *mut QwenAsrEngine, + enabled: i32, +) { + if !engine.is_null() { + (*engine).ctx.past_text_conditioning = enabled != 0; + } +} diff --git a/vendor/qwenasr/crates/qwen-asr/src/config.rs b/vendor/qwenasr/crates/qwen-asr/src/config.rs new file mode 100644 index 0000000..f4cd7a9 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/src/config.rs @@ -0,0 +1,182 @@ +//! Model configuration and automatic variant detection. + +pub const SAMPLE_RATE: i32 = 16000; +pub const MEL_BINS: usize = 128; +pub const HOP_LENGTH: usize = 160; +pub const WINDOW_SIZE: usize = 400; +pub const VOCAB_SIZE: usize = 151936; + +pub const MAX_ENC_LAYERS: usize = 24; +pub const MAX_DEC_LAYERS: usize = 28; + +// Special token IDs +pub const TOKEN_IM_START: i32 = 151644; +pub const TOKEN_IM_END: i32 = 151645; +pub const TOKEN_ENDOFTEXT: i32 = 151643; +pub const TOKEN_AUDIO_START: i32 = 151669; +pub const TOKEN_AUDIO_END: i32 = 151670; +pub const TOKEN_AUDIO_PAD: i32 = 151676; +pub const TOKEN_ASR_TEXT: i32 = 151704; +pub const TOKEN_TIMESTAMP: i32 = 151705; + +// Conv2D stem constants +pub const CONV_HIDDEN: usize = 480; +pub const CONV_KERNEL: usize = 3; + +#[derive(Clone)] +pub struct QwenConfig { + // Audio encoder + pub enc_d_model: usize, + pub enc_layers: usize, + pub enc_heads: usize, + pub enc_head_dim: usize, + pub enc_ffn_dim: usize, + pub enc_output_dim: usize, + pub enc_n_window: usize, + pub enc_n_window_infer: usize, + pub enc_chunk_size: usize, + pub enc_conv_proj_dim: usize, + + // LLM decoder + pub dec_hidden: usize, + pub dec_layers: usize, + pub dec_heads: usize, + pub dec_kv_heads: usize, + pub dec_head_dim: usize, + pub dec_intermediate: usize, + pub vocab_size: usize, + pub dec_rms_norm_eps: f32, + pub dec_rope_theta: f32, + + // Forced aligner fields (0 = normal ASR model) + pub classify_num: usize, + pub timestamp_segment_time: f32, +} + +impl Default for QwenConfig { + fn default() -> Self { + Self { + enc_d_model: 0, + enc_layers: 0, + enc_heads: 0, + enc_head_dim: 0, + enc_ffn_dim: 0, + enc_output_dim: 0, + enc_n_window: 50, + enc_n_window_infer: 800, + enc_chunk_size: 100, + enc_conv_proj_dim: CONV_HIDDEN * 16, + dec_hidden: 0, + dec_layers: 28, + dec_heads: 16, + dec_kv_heads: 8, + dec_head_dim: 128, + dec_intermediate: 0, + vocab_size: VOCAB_SIZE, + dec_rms_norm_eps: 1e-6, + dec_rope_theta: 1e6, + classify_num: 0, + timestamp_segment_time: 0.0, + } + } +} + +impl QwenConfig { + /// Returns the effective lm_head output dimension. + pub fn lm_head_dim(&self) -> usize { + if self.classify_num > 0 { self.classify_num } else { self.vocab_size } + } + + /// Whether this config is for a forced aligner model. + pub fn is_aligner(&self) -> bool { + self.classify_num > 0 + } +} + +/// Tensor shape info passed from safetensors for model detection. +pub struct DetectInfo<'a> { + pub has_enc_layer_18: bool, + /// Shape of thinker.lm_head.weight (if present) + pub lm_head_shape: Option<&'a [i64]>, + /// Shape of thinker.model.embed_tokens.weight + pub embed_tokens_shape: Option<&'a [i64]>, + /// Shape of thinker.model.layers.0.mlp.gate_proj.weight + pub gate_proj_shape: Option<&'a [i64]>, +} + +impl QwenConfig { + /// Detect model variant from safetensors tensor shapes. + /// Handles ASR 0.6B, ASR 1.7B, and ForcedAligner 0.6B (which has 1.7B encoder + 0.6B decoder). + pub fn detect(info: &DetectInfo) -> Self { + let mut cfg = Self::default(); + + // Determine decoder hidden size from embed_tokens shape [vocab_size, hidden_dim] + let dec_hidden = info.embed_tokens_shape + .and_then(|s| if s.len() == 2 { Some(s[1] as usize) } else { None }) + .unwrap_or(if info.has_enc_layer_18 { 2048 } else { 1024 }); + + // Determine decoder intermediate from gate_proj shape [intermediate, hidden] + let dec_intermediate = info.gate_proj_shape + .and_then(|s| if s.len() == 2 { Some(s[0] as usize) } else { None }) + .unwrap_or(if dec_hidden >= 2048 { 6144 } else { 3072 }); + + // Encoder architecture: 24 layers = "large" encoder, 18 layers = "small" encoder + if info.has_enc_layer_18 { + // Large encoder (used by both 1.7B ASR and aligner 0.6B) + cfg.enc_d_model = 1024; + cfg.enc_layers = 24; + cfg.enc_heads = 16; + cfg.enc_head_dim = 64; + cfg.enc_ffn_dim = 4096; + } else { + // Small encoder (0.6B ASR) + cfg.enc_d_model = 896; + cfg.enc_layers = 18; + cfg.enc_heads = 14; + cfg.enc_head_dim = 64; + cfg.enc_ffn_dim = 3584; + } + + // enc_output_dim always matches dec_hidden (proj projects encoder output to decoder space) + cfg.enc_output_dim = dec_hidden; + cfg.dec_hidden = dec_hidden; + cfg.dec_intermediate = dec_intermediate; + + // Detect forced aligner: lm_head has shape [classify_num, hidden_dim] + // where classify_num != vocab_size (typically 5000) + if let Some(shape) = info.lm_head_shape { + if shape.len() == 2 && (shape[0] as usize) != VOCAB_SIZE { + cfg.classify_num = shape[0] as usize; + cfg.timestamp_segment_time = 80.0; // 80ms per time bin + } + } + + cfg.enc_chunk_size = cfg.enc_n_window * 2; + cfg + } +} + +pub const SUPPORTED_LANGUAGES: &[&str] = &[ + "Chinese", "English", "Cantonese", "Arabic", "German", "French", + "Spanish", "Portuguese", "Indonesian", "Italian", "Korean", "Russian", + "Thai", "Vietnamese", "Japanese", "Turkish", "Hindi", "Malay", "Dutch", + "Swedish", "Danish", "Finnish", "Polish", "Czech", "Filipino", + "Persian", "Greek", "Romanian", "Hungarian", "Macedonian", +]; + +pub fn normalize_language(language: &str) -> Option { + let trimmed = language.trim(); + if trimmed.is_empty() { + return None; + } + let mut chars = trimmed.chars(); + let first = chars.next()?.to_uppercase().to_string(); + let rest: String = chars.map(|c| c.to_lowercase().next().unwrap_or(c)).collect(); + let normalized = format!("{}{}", first, rest); + + if SUPPORTED_LANGUAGES.contains(&normalized.as_str()) { + Some(normalized) + } else { + None + } +} diff --git a/vendor/qwenasr/crates/qwen-asr/src/context.rs b/vendor/qwenasr/crates/qwen-asr/src/context.rs new file mode 100644 index 0000000..beb77e2 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/src/context.rs @@ -0,0 +1,309 @@ +//! Top-level engine state ([`QwenCtx`]) with shared model weights and private runtime buffers. + +use crate::config::*; +use crate::decoder::*; +use crate::encoder::EncoderBuffers; +use crate::encoder::*; +use crate::kernels; +use crate::safetensors::MultiSafetensors; +use crate::tokenizer::QwenTokenizer; +use std::collections::HashMap; +use std::ops::Deref; +use std::sync::{Arc, Mutex, OnceLock, Weak}; + +pub type TokenCallback = Box; + +/// Shared read-only model weights reused across multiple runtime contexts. +pub struct SharedQwenModel { + pub config: QwenConfig, + pub encoder: Encoder, + pub decoder: Decoder, + pub _safetensors: MultiSafetensors, // kept alive for mmap'd BF16 pointers + pub model_dir: String, +} + +type SharedModelCache = Mutex>>; + +fn shared_model_cache() -> &'static SharedModelCache { + static CACHE: OnceLock = OnceLock::new(); + CACHE.get_or_init(|| Mutex::new(HashMap::new())) +} + +impl SharedQwenModel { + fn load(model_dir: &str) -> Option> { + if let Ok(cache) = shared_model_cache().lock() { + if let Some(shared) = cache.get(model_dir).and_then(Weak::upgrade) { + if kernels::verbose() >= 1 { + eprintln!("Reusing shared model weights from {}", model_dir); + } + return Some(shared); + } + } + + if kernels::verbose() >= 1 { + eprintln!("Loading model from {}", model_dir); + } + + let ms = MultiSafetensors::open(model_dir)?; + + // Detect model variant from tensor shapes + let info = crate::config::DetectInfo { + has_enc_layer_18: ms + .has_tensor("thinker.audio_tower.layers.18.self_attn.q_proj.weight"), + lm_head_shape: ms + .find("thinker.lm_head.weight") + .map(|(_, t)| t.shape.as_slice()), + embed_tokens_shape: ms + .find("thinker.model.embed_tokens.weight") + .map(|(_, t)| t.shape.as_slice()), + gate_proj_shape: ms + .find("thinker.model.layers.0.mlp.gate_proj.weight") + .map(|(_, t)| t.shape.as_slice()), + }; + let cfg = QwenConfig::detect(&info); + + if kernels::verbose() >= 1 { + let variant = if cfg.dec_hidden >= 2048 { + "1.7B" + } else { + "0.6B" + }; + let model_type = if cfg.is_aligner() { + "ForcedAligner" + } else { + "ASR" + }; + eprintln!("Detected: Qwen3-{}-{}", model_type, variant); + if cfg.is_aligner() { + eprintln!( + " classify_num={}, timestamp_segment_time={:.0}ms", + cfg.classify_num, cfg.timestamp_segment_time + ); + eprintln!( + " encoder: {}d {}L, decoder: {}d {}L", + cfg.enc_d_model, cfg.enc_layers, cfg.dec_hidden, cfg.dec_layers + ); + } + } + + if kernels::verbose() >= 1 { + eprintln!("Loading encoder weights..."); + } + let encoder = Encoder::load(&ms, &cfg)?; + + if kernels::verbose() >= 1 { + eprintln!("Loading decoder weights..."); + } + let decoder = Decoder::load(&ms, &cfg)?; + + if kernels::verbose() >= 1 { + eprintln!("Model weights loaded."); + } + + let shared = Arc::new(SharedQwenModel { + config: cfg, + encoder, + decoder, + _safetensors: ms, + model_dir: model_dir.to_string(), + }); + + if let Ok(mut cache) = shared_model_cache().lock() { + cache.insert(model_dir.to_string(), Arc::downgrade(&shared)); + } + + Some(shared) + } +} + +/// Top-level ASR engine state owning runtime caches, prompts, and scratch buffers. +/// +/// Create with [`QwenCtx::load`], then pass to functions in the [`crate::transcribe`] module. +/// +/// # Configurable fields +/// +/// | Field | Default | Description | +/// |-------|---------|-------------| +/// | `segment_sec` | 0.0 | Segment duration for long audio (0 = no splitting) | +/// | `skip_silence` | false | Drop silent spans before transcription | +/// | `token_cb` | None | Streaming callback invoked for each decoded token | +/// | `prompt` | None | Optional text prompt (set via [`QwenCtx::set_prompt`]) | +/// | `force_language` | None | Force a language (set via [`QwenCtx::set_force_language`]) | +pub struct QwenCtx { + pub shared: Arc, + + // KV cache + pub kv_cache: KvCache, + + // Decoder buffers + pub dec_bufs: DecoderBuffers, + + // Encoder scratch buffers (reusable across calls) + pub enc_bufs: EncoderBuffers, + + // RoPE cache + pub rope_cache: RopeCache, + + // Token streaming callback + pub token_cb: Option, + + // Segmentation settings + pub segment_sec: f32, + pub search_sec: f32, + + // Streaming settings + pub stream_chunk_sec: f32, + pub stream_rollback: i32, + pub stream_unfixed_chunks: i32, + pub stream_max_new_tokens: i32, + pub past_text_conditioning: bool, + pub skip_silence: bool, + + // Optional prompt/language + pub prompt: Option, + pub force_language: Option, + pub prompt_tokens: Option>, + pub force_prompt_tokens: Option>, + pub prompt_tokens_ready: bool, + + // Perf stats + pub perf_total_ms: f64, + pub perf_text_tokens: i32, + pub perf_audio_ms: f64, + /// Mel spectrogram + encoder forward pass time combined. + pub perf_encode_ms: f64, + pub perf_decode_ms: f64, +} + +impl Deref for QwenCtx { + type Target = SharedQwenModel; + + fn deref(&self) -> &Self::Target { + &self.shared + } +} + +impl QwenCtx { + /// Load a Qwen3-ASR model from `model_dir`. + /// + /// The directory must contain `model*.safetensors` and `vocab.json`. + /// Returns `None` if any required file is missing or malformed. + /// + /// ```rust,no_run + /// use qwen_asr::context::QwenCtx; + /// let ctx = QwenCtx::load("qwen3-asr-0.6b").expect("failed to load"); + /// ``` + pub fn load(model_dir: &str) -> Option { + let shared = SharedQwenModel::load(model_dir)?; + let cfg = shared.config.clone(); + let kv_cache = KvCache::new(cfg.dec_layers, 2048, cfg.dec_kv_heads, cfg.dec_head_dim); + let dec_bufs = DecoderBuffers::new(&cfg); + + if kernels::verbose() >= 1 { + eprintln!("Runtime buffers initialized."); + } + + Some(QwenCtx { + shared, + kv_cache, + dec_bufs, + enc_bufs: EncoderBuffers::new(), + rope_cache: RopeCache::new(), + token_cb: None, + segment_sec: 0.0, + search_sec: 3.0, + stream_chunk_sec: 2.0, + stream_rollback: 5, + stream_unfixed_chunks: 2, + stream_max_new_tokens: 32, + past_text_conditioning: false, + skip_silence: false, + prompt: None, + force_language: None, + prompt_tokens: None, + force_prompt_tokens: None, + prompt_tokens_ready: false, + perf_total_ms: 0.0, + perf_text_tokens: 0, + perf_audio_ms: 0.0, + perf_encode_ms: 0.0, + perf_decode_ms: 0.0, + }) + } + + /// Set an optional text prompt to guide transcription. Pass an empty string to clear. + #[allow(clippy::result_unit_err)] + pub fn set_prompt(&mut self, prompt: &str) -> Result<(), ()> { + if prompt.is_empty() { + self.prompt = None; + } else { + self.prompt = Some(prompt.to_string()); + } + self.prompt_tokens_ready = false; + Ok(()) + } + + /// Force a specific language (e.g. `"English"`, `"Chinese"`). Pass an empty + /// string for auto-detection. Returns `Err(())` if the language is not recognized. + #[allow(clippy::result_unit_err)] + pub fn set_force_language(&mut self, language: &str) -> Result<(), ()> { + if language.is_empty() { + self.force_language = None; + self.prompt_tokens_ready = false; + return Ok(()); + } + + match normalize_language(language) { + Some(normalized) => { + self.force_language = Some(normalized); + self.prompt_tokens_ready = false; + Ok(()) + } + None => Err(()), + } + } + + pub fn prepare_prompt_tokens(&mut self, tokenizer: &QwenTokenizer) -> bool { + if self.prompt_tokens_ready { + return true; + } + + self.prompt_tokens = None; + self.force_prompt_tokens = None; + + if let Some(ref prompt) = self.prompt { + match tokenizer.encode(prompt) { + Some(tokens) => self.prompt_tokens = Some(tokens), + None => { + eprintln!("qwen: failed to encode --prompt text"); + return false; + } + } + } + + if let Some(ref lang) = self.force_language { + let force_text = format!("language {}", lang); + match tokenizer.encode(&force_text) { + Some(mut lang_tokens) => { + lang_tokens.push(TOKEN_ASR_TEXT); + self.force_prompt_tokens = Some(lang_tokens); + } + None => { + eprintln!("qwen: failed to encode --language text"); + return false; + } + } + } + + self.prompt_tokens_ready = true; + true + } + + pub fn reset_perf(&mut self) { + self.perf_total_ms = 0.0; + self.perf_text_tokens = 0; + self.perf_audio_ms = 0.0; + self.perf_encode_ms = 0.0; + self.perf_decode_ms = 0.0; + } +} diff --git a/vendor/qwenasr/crates/qwen-asr/src/decoder.rs b/vendor/qwenasr/crates/qwen-asr/src/decoder.rs new file mode 100644 index 0000000..aa92cb8 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/src/decoder.rs @@ -0,0 +1,1064 @@ +//! Qwen3 LLM decoder with GQA, KV cache, and generation. + +use crate::config::*; +use crate::kernels; +use crate::safetensors::MultiSafetensors; + +pub struct DecLayer { + pub wq_weight_bf16: *const u16, + pub wk_weight_bf16: *const u16, + pub wv_weight_bf16: *const u16, + pub wo_weight_bf16: *const u16, + pub q_norm_weight: Vec, + pub k_norm_weight: Vec, + pub input_norm: Vec, + pub post_attn_norm: Vec, + pub gate_weight_bf16: *const u16, + pub up_weight_bf16: *const u16, + pub down_weight_bf16: *const u16, + pub gate_up_fused_bf16: Vec, // owned, interleaved + pub gate_up_fused_f32: Vec, + pub down_weight_f32: Vec, + /// INT8 quantized attention weights + per-row scales + pub wq_int8: Vec, + pub wq_int8_scales: Vec, + pub wk_int8: Vec, + pub wk_int8_scales: Vec, + pub wv_int8: Vec, + pub wv_int8_scales: Vec, + pub wo_int8: Vec, + pub wo_int8_scales: Vec, + /// INT8 quantized FFN weights + per-row scales + pub gate_up_int8: Vec, + pub gate_up_int8_scales: Vec, + pub down_int8: Vec, + pub down_int8_scales: Vec, +} + +unsafe impl Send for DecLayer {} +unsafe impl Sync for DecLayer {} + +pub struct Decoder { + pub tok_embeddings_bf16: *const u16, + pub layers: Vec, + pub norm: Vec, + /// Separate lm_head for forced aligner (None = tied weights with tok_embeddings) + pub lm_head_bf16: Option<*const u16>, + /// INT8 quantized lm_head weights for fast argmax + pub lm_head_int8: Option>, + pub lm_head_int8_scales: Option>, +} + +unsafe impl Send for Decoder {} +unsafe impl Sync for Decoder {} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum DecodeKernelMode { + Int8, + Bf16, +} + +const fn decode_kernel_mode() -> DecodeKernelMode { + #[cfg(target_arch = "aarch64")] + { + DecodeKernelMode::Int8 + } + + #[cfg(not(target_arch = "aarch64"))] + { + DecodeKernelMode::Bf16 + } +} + +const fn materialize_int8_decoder_weights() -> bool { + matches!(decode_kernel_mode(), DecodeKernelMode::Int8) +} + +const fn materialize_f32_ffn_weights() -> bool { + matches!(decode_kernel_mode(), DecodeKernelMode::Bf16) +} + +fn load_f32(ms: &MultiSafetensors, name: &str) -> Option> { + let result = ms.get_f32(name); + if result.is_none() { + eprintln!("decoder: weight not found: {}", name); + } + result +} + +fn load_bf16_direct(ms: &MultiSafetensors, name: &str) -> Option<*const u16> { + let result = ms.get_bf16_direct(name); + if result.is_none() { + eprintln!("decoder: weight not found: {}", name); + } + result +} + +fn bf16_ptr_to_f32_vec(src: *const u16, n: usize) -> Vec { + let mut out = vec![0.0f32; n]; + let src_slice = unsafe { std::slice::from_raw_parts(src, n) }; + kernels::bf16_to_f32_buf(&mut out, src_slice); + out +} + +fn bf16_slice_to_f32_vec(src: &[u16]) -> Vec { + let mut out = vec![0.0f32; src.len()]; + kernels::bf16_to_f32_buf(&mut out, src); + out +} + +impl Decoder { + pub fn load(ms: &MultiSafetensors, cfg: &QwenConfig) -> Option { + let tok_embeddings_bf16 = load_bf16_direct(ms, "thinker.model.embed_tokens.weight")?; + + let mut layers = Vec::new(); + for i in 0..cfg.dec_layers { + let lp = format!("thinker.model.layers.{}", i); + + let wq = load_bf16_direct(ms, &format!("{}.self_attn.q_proj.weight", lp))?; + let wk = load_bf16_direct(ms, &format!("{}.self_attn.k_proj.weight", lp))?; + let wv = load_bf16_direct(ms, &format!("{}.self_attn.v_proj.weight", lp))?; + let wo = load_bf16_direct(ms, &format!("{}.self_attn.o_proj.weight", lp))?; + + let q_norm = load_f32(ms, &format!("{}.self_attn.q_norm.weight", lp))?; + let k_norm = load_f32(ms, &format!("{}.self_attn.k_norm.weight", lp))?; + let input_norm = load_f32(ms, &format!("{}.input_layernorm.weight", lp))?; + let post_attn_norm = load_f32(ms, &format!("{}.post_attention_layernorm.weight", lp))?; + + let gate_bf16 = load_bf16_direct(ms, &format!("{}.mlp.gate_proj.weight", lp))?; + let up_bf16 = load_bf16_direct(ms, &format!("{}.mlp.up_proj.weight", lp))?; + let down_bf16 = load_bf16_direct(ms, &format!("{}.mlp.down_proj.weight", lp))?; + + // Fuse gate+up: interleave rows + let inter = cfg.dec_intermediate; + let hidden = cfg.dec_hidden; + let mut gate_up_fused = vec![0u16; 2 * inter * hidden]; + unsafe { + let gate_slice = std::slice::from_raw_parts(gate_bf16, inter * hidden); + let up_slice = std::slice::from_raw_parts(up_bf16, inter * hidden); + for r in 0..inter { + gate_up_fused[2 * r * hidden..(2 * r + 1) * hidden] + .copy_from_slice(&gate_slice[r * hidden..(r + 1) * hidden]); + gate_up_fused[(2 * r + 1) * hidden..(2 * r + 2) * hidden] + .copy_from_slice(&up_slice[r * hidden..(r + 1) * hidden]); + } + } + + let q_dim = cfg.dec_heads * cfg.dec_head_dim; + let kv_dim = cfg.dec_kv_heads * cfg.dec_head_dim; + let (wq_int8, wq_int8_scales, wk_int8, wk_int8_scales, wv_int8, wv_int8_scales, wo_int8, wo_int8_scales, gate_up_int8, gate_up_int8_scales, down_int8, down_int8_scales) = + if materialize_int8_decoder_weights() { + let (wq_int8, wq_int8_scales) = + kernels::quantize_bf16_weights_to_int8(wq, q_dim, hidden); + let (wk_int8, wk_int8_scales) = + kernels::quantize_bf16_weights_to_int8(wk, kv_dim, hidden); + let (wv_int8, wv_int8_scales) = + kernels::quantize_bf16_weights_to_int8(wv, kv_dim, hidden); + let (wo_int8, wo_int8_scales) = + kernels::quantize_bf16_weights_to_int8(wo, hidden, q_dim); + let (gate_up_int8, gate_up_int8_scales) = + kernels::quantize_bf16_weights_to_int8(gate_up_fused.as_ptr(), 2 * inter, hidden); + let (down_int8, down_int8_scales) = + kernels::quantize_bf16_weights_to_int8(down_bf16, hidden, inter); + ( + wq_int8, + wq_int8_scales, + wk_int8, + wk_int8_scales, + wv_int8, + wv_int8_scales, + wo_int8, + wo_int8_scales, + gate_up_int8, + gate_up_int8_scales, + down_int8, + down_int8_scales, + ) + } else { + ( + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + ) + }; + + let (gate_up_fused_f32, down_weight_f32) = if materialize_f32_ffn_weights() { + ( + bf16_slice_to_f32_vec(&gate_up_fused), + bf16_ptr_to_f32_vec(down_bf16, hidden * inter), + ) + } else { + (Vec::new(), Vec::new()) + }; + layers.push(DecLayer { + wq_weight_bf16: wq, + wk_weight_bf16: wk, + wv_weight_bf16: wv, + wo_weight_bf16: wo, + q_norm_weight: q_norm, + k_norm_weight: k_norm, + input_norm, + post_attn_norm, + gate_weight_bf16: gate_bf16, + up_weight_bf16: up_bf16, + down_weight_bf16: down_bf16, + gate_up_fused_bf16: gate_up_fused, + gate_up_fused_f32, + down_weight_f32, + wq_int8, + wq_int8_scales, + wk_int8, + wk_int8_scales, + wv_int8, + wv_int8_scales, + wo_int8, + wo_int8_scales, + gate_up_int8, + gate_up_int8_scales, + down_int8, + down_int8_scales, + }); + } + + let norm = load_f32(ms, "thinker.model.norm.weight")?; + + // Load separate lm_head if present (forced aligner has untied lm_head) + let lm_head_bf16 = if cfg.classify_num > 0 { + let ptr = load_bf16_direct(ms, "thinker.lm_head.weight")?; + Some(ptr) + } else { + // For normal ASR, lm_head is tied with tok_embeddings (no separate weight) + ms.get_bf16_direct("thinker.lm_head.weight") + }; + + let lm_weight = lm_head_bf16.unwrap_or(tok_embeddings_bf16); + let lm_out_dim = cfg.lm_head_dim(); + let lm_in_dim = cfg.dec_hidden; + let (lm_head_int8, lm_head_int8_scales) = if materialize_int8_decoder_weights() { + let (lm_int8, lm_scales) = + kernels::quantize_bf16_weights_to_int8(lm_weight, lm_out_dim, lm_in_dim); + (Some(lm_int8), Some(lm_scales)) + } else { + (None, None) + }; + + Some(Decoder { + tok_embeddings_bf16, + layers, + norm, + lm_head_bf16, + lm_head_int8, + lm_head_int8_scales, + }) + } +} + +// ======================================================================== +// KV Cache +// ======================================================================== + +pub struct KvCache { + pub k: Vec, + pub v: Vec, + pub len: usize, + pub max_seq: usize, + pub n_layers: usize, + pub n_kv_heads: usize, + pub head_dim: usize, +} + +impl KvCache { + /// Layout: `[layer][head][pos][head_dim]` — head-contiguous for cache-friendly attention. + pub fn new(n_layers: usize, max_seq: usize, n_kv_heads: usize, head_dim: usize) -> Self { + let total = n_layers * n_kv_heads * max_seq * head_dim; + KvCache { + k: vec![0.0f32; total], + v: vec![0.0f32; total], + len: 0, + max_seq, + n_layers, + n_kv_heads, + head_dim, + } + } + + pub fn grow(&mut self, required: usize) { + if required <= self.max_seq { + return; + } + + let mut new_max = self.max_seq; + while new_max < required { + new_max *= 2; + } + + let old_head_stride = self.max_seq * self.head_dim; + let new_head_stride = new_max * self.head_dim; + let total = self.n_layers * self.n_kv_heads * new_head_stride; + + let mut new_k = vec![0.0f32; total]; + let mut new_v = vec![0.0f32; total]; + + let copy_len = self.len * self.head_dim; + for l in 0..self.n_layers { + for h in 0..self.n_kv_heads { + let old_off = (l * self.n_kv_heads + h) * old_head_stride; + let new_off = (l * self.n_kv_heads + h) * new_head_stride; + new_k[new_off..new_off + copy_len] + .copy_from_slice(&self.k[old_off..old_off + copy_len]); + new_v[new_off..new_off + copy_len] + .copy_from_slice(&self.v[old_off..old_off + copy_len]); + } + } + + self.k = new_k; + self.v = new_v; + self.max_seq = new_max; + } + + /// Write K for all heads at a given position (from interleaved kv_dim buffer). + pub fn k_write_pos(&mut self, layer: usize, pos: usize, src: &[f32]) { + let head_stride = self.max_seq * self.head_dim; + for h in 0..self.n_kv_heads { + let dst_off = (layer * self.n_kv_heads + h) * head_stride + pos * self.head_dim; + let src_off = h * self.head_dim; + self.k[dst_off..dst_off + self.head_dim] + .copy_from_slice(&src[src_off..src_off + self.head_dim]); + } + } + + /// Write V for all heads at a given position (from interleaved kv_dim buffer). + pub fn v_write_pos(&mut self, layer: usize, pos: usize, src: &[f32]) { + let head_stride = self.max_seq * self.head_dim; + for h in 0..self.n_kv_heads { + let dst_off = (layer * self.n_kv_heads + h) * head_stride + pos * self.head_dim; + let src_off = h * self.head_dim; + self.v[dst_off..dst_off + self.head_dim] + .copy_from_slice(&src[src_off..src_off + self.head_dim]); + } + } + + /// Base pointer for K data of a specific layer (head-contiguous layout). + /// Layout within layer: `[head][pos][head_dim]`, stride between heads = max_seq * head_dim. + pub fn k_layer_base(&self, layer: usize) -> *const f32 { + let off = layer * self.n_kv_heads * self.max_seq * self.head_dim; + unsafe { self.k.as_ptr().add(off) } + } + + pub fn v_layer_base(&self, layer: usize) -> *const f32 { + let off = layer * self.n_kv_heads * self.max_seq * self.head_dim; + unsafe { self.v.as_ptr().add(off) } + } + + /// Stride between heads (in floats): max_seq * head_dim. + pub fn head_stride(&self) -> usize { + self.max_seq * self.head_dim + } +} + +// ======================================================================== +// RoPE Cache +// ======================================================================== + +pub struct RopeCache { + pub cos: Vec, + pub sin: Vec, + pub inv_freq: Vec, + pub cap: usize, + pub head_dim: usize, +} + +impl Default for RopeCache { + fn default() -> Self { + Self::new() + } +} + +impl RopeCache { + pub fn new() -> Self { + RopeCache { + cos: Vec::new(), + sin: Vec::new(), + inv_freq: Vec::new(), + cap: 0, + head_dim: 0, + } + } + + pub fn ensure(&mut self, required_pos: usize, head_dim: usize, theta: f32) { + if self.head_dim != head_dim || self.inv_freq.is_empty() { + let half = head_dim / 2; + self.inv_freq = (0..half) + .map(|d| 1.0 / theta.powf((2 * d) as f32 / head_dim as f32)) + .collect(); + self.head_dim = head_dim; + } + + if required_pos <= self.cap { + return; + } + + let mut new_cap = if self.cap > 0 { self.cap } else { 1024 }; + while new_cap < required_pos { + new_cap *= 2; + } + + self.cos.resize(new_cap * head_dim, 0.0); + self.sin.resize(new_cap * head_dim, 0.0); + + let half = head_dim / 2; + for pos in self.cap..new_cap { + let p = pos as f32; + for d in 0..half { + let angle = p * self.inv_freq[d]; + let c = angle.cos(); + let s = angle.sin(); + self.cos[pos * head_dim + d] = c; + self.cos[pos * head_dim + half + d] = c; + self.sin[pos * head_dim + d] = s; + self.sin[pos * head_dim + half + d] = s; + } + } + + self.cap = new_cap; + } + + pub fn cos_at(&self, pos: usize) -> &[f32] { + &self.cos[pos * self.head_dim..(pos + 1) * self.head_dim] + } + + pub fn sin_at(&self, pos: usize) -> &[f32] { + &self.sin[pos * self.head_dim..(pos + 1) * self.head_dim] + } + + pub fn cos_range(&self, start: usize, len: usize) -> &[f32] { + &self.cos[start * self.head_dim..(start + len) * self.head_dim] + } + + pub fn sin_range(&self, start: usize, len: usize) -> &[f32] { + &self.sin[start * self.head_dim..(start + len) * self.head_dim] + } +} + +// ======================================================================== +// Decoder Forward +// ======================================================================== + +pub struct DecoderBuffers { + // Single-token decode buffers + pub x: Vec, + pub x_norm: Vec, + pub q: Vec, + pub k: Vec, + pub v: Vec, + pub attn_out: Vec, + pub proj_out: Vec, + pub gate_buf: Vec, + pub ffn_out: Vec, + + // Prefill buffers + pub pref_x: Vec, + pub pref_x_norm: Vec, + pub pref_q: Vec, + pub pref_k: Vec, + pub pref_v: Vec, + pub pref_attn_out: Vec, + pub pref_proj_out: Vec, + pub pref_ffn_out: Vec, + pub pref_gate_up: Vec, + pub pref_gate: Vec, + pub pref_seq_cap: usize, + + // Reusable scratch for BF16→F32 conversion in prefill path + pub bf16_scratch: Vec, +} + +impl DecoderBuffers { + pub fn new(cfg: &QwenConfig) -> Self { + let dim = cfg.dec_hidden; + let q_dim = cfg.dec_heads * cfg.dec_head_dim; + let kv_dim = cfg.dec_kv_heads * cfg.dec_head_dim; + let intermediate = cfg.dec_intermediate; + + // Largest weight matrix is gate_up_fused: 2 * intermediate * hidden + let max_weight = (2 * intermediate * dim).max(q_dim * dim).max(kv_dim * dim); + + DecoderBuffers { + x: vec![0.0f32; dim], + x_norm: vec![0.0f32; dim], + q: vec![0.0f32; q_dim], + k: vec![0.0f32; kv_dim], + v: vec![0.0f32; kv_dim], + attn_out: vec![0.0f32; q_dim], + proj_out: vec![0.0f32; dim], + gate_buf: vec![0.0f32; 2 * intermediate], + ffn_out: vec![0.0f32; intermediate], + pref_x: Vec::new(), + pref_x_norm: Vec::new(), + pref_q: Vec::new(), + pref_k: Vec::new(), + pref_v: Vec::new(), + pref_attn_out: Vec::new(), + pref_proj_out: Vec::new(), + pref_ffn_out: Vec::new(), + pref_gate_up: Vec::new(), + pref_gate: Vec::new(), + pref_seq_cap: 0, + bf16_scratch: vec![0.0f32; max_weight], + } + } + + pub fn ensure_prefill(&mut self, seq_len: usize, cfg: &QwenConfig) { + if seq_len <= self.pref_seq_cap { + return; + } + + let dim = cfg.dec_hidden; + let q_dim = cfg.dec_heads * cfg.dec_head_dim; + let kv_dim = cfg.dec_kv_heads * cfg.dec_head_dim; + let intermediate = cfg.dec_intermediate; + + let mut new_cap = if self.pref_seq_cap > 0 { + self.pref_seq_cap + } else { + 64 + }; + while new_cap < seq_len { + new_cap *= 2; + } + + self.pref_x.resize(new_cap * dim, 0.0); + self.pref_x_norm.resize(new_cap * dim, 0.0); + self.pref_q.resize(new_cap * q_dim, 0.0); + self.pref_k.resize(new_cap * kv_dim, 0.0); + self.pref_v.resize(new_cap * kv_dim, 0.0); + self.pref_attn_out.resize(new_cap * q_dim, 0.0); + self.pref_proj_out.resize(new_cap * dim, 0.0); + self.pref_ffn_out.resize(new_cap * dim, 0.0); + self.pref_gate_up.resize(new_cap * 2 * intermediate, 0.0); + self.pref_gate.resize(new_cap * intermediate, 0.0); + self.pref_seq_cap = new_cap; + } +} + +/// Decoder prefill: process multiple tokens. +pub fn decoder_prefill( + decoder: &Decoder, + cfg: &QwenConfig, + kv_cache: &mut KvCache, + rope: &mut RopeCache, + bufs: &mut DecoderBuffers, + input_embeds: &[f32], + seq_len: usize, +) { + let dim = cfg.dec_hidden; + let n_heads = cfg.dec_heads; + let n_kv_heads = cfg.dec_kv_heads; + let head_dim = cfg.dec_head_dim; + let intermediate = cfg.dec_intermediate; + let eps = cfg.dec_rms_norm_eps; + let theta = cfg.dec_rope_theta; + let q_dim = n_heads * head_dim; + let kv_dim = n_kv_heads * head_dim; + // Ensure KV cache + let needed = kv_cache.len + seq_len; + if needed > kv_cache.max_seq { + kv_cache.grow(needed + 1024); + } + + bufs.ensure_prefill(seq_len, cfg); + + let x = &mut bufs.pref_x[..seq_len * dim]; + x.copy_from_slice(&input_embeds[..seq_len * dim]); + + let start_pos = kv_cache.len; + rope.ensure(start_pos + seq_len, head_dim, theta); + let rope_cos = rope.cos_range(start_pos, seq_len); + let rope_sin = rope.sin_range(start_pos, seq_len); + + let scale = 1.0 / (head_dim as f32).sqrt(); + + for (layer_idx, layer) in decoder.layers.iter().enumerate() { + let x_norm = &mut bufs.pref_x_norm[..seq_len * dim]; + kernels::rms_norm( + x_norm, + &bufs.pref_x[..seq_len * dim], + &layer.input_norm, + seq_len, + dim, + eps, + ); + + let q = &mut bufs.pref_q[..seq_len * q_dim]; + let k = &mut bufs.pref_k[..seq_len * kv_dim]; + let v = &mut bufs.pref_v[..seq_len * kv_dim]; + + unsafe { + kernels::linear_nobias_bf16_scratch( + q, + x_norm, + layer.wq_weight_bf16, + seq_len, + dim, + q_dim, + &mut bufs.bf16_scratch, + ); + } + unsafe { + kernels::linear_nobias_bf16_scratch( + k, + x_norm, + layer.wk_weight_bf16, + seq_len, + dim, + kv_dim, + &mut bufs.bf16_scratch, + ); + } + unsafe { + kernels::linear_nobias_bf16_scratch( + v, + x_norm, + layer.wv_weight_bf16, + seq_len, + dim, + kv_dim, + &mut bufs.bf16_scratch, + ); + } + + kernels::rms_norm_per_head(q, &layer.q_norm_weight, seq_len, n_heads, head_dim, eps); + kernels::rms_norm_per_head(k, &layer.k_norm_weight, seq_len, n_kv_heads, head_dim, eps); + + kernels::apply_rope_neox(q, rope_cos, rope_sin, seq_len, n_heads, head_dim); + kernels::apply_rope_neox(k, rope_cos, rope_sin, seq_len, n_kv_heads, head_dim); + + // Store K, V in cache (scatter to head-contiguous layout) + for s in 0..seq_len { + kv_cache.k_write_pos( + layer_idx, + start_pos + s, + &bufs.pref_k[s * kv_dim..(s + 1) * kv_dim], + ); + kv_cache.v_write_pos( + layer_idx, + start_pos + s, + &bufs.pref_v[s * kv_dim..(s + 1) * kv_dim], + ); + } + + let total_seq = start_pos + seq_len; + let k_base = kv_cache.k_layer_base(layer_idx); + let v_base = kv_cache.v_layer_base(layer_idx); + let head_stride = kv_cache.head_stride(); + + let attn_out = &mut bufs.pref_attn_out[..seq_len * q_dim]; + kernels::causal_attention( + attn_out, + q, + k_base, + v_base, + head_stride, + seq_len, + total_seq, + n_heads, + n_kv_heads, + head_dim, + scale, + start_pos, + ); + + let proj_out = &mut bufs.pref_proj_out[..seq_len * dim]; + unsafe { + kernels::linear_nobias_bf16_scratch( + proj_out, + attn_out, + layer.wo_weight_bf16, + seq_len, + q_dim, + dim, + &mut bufs.bf16_scratch, + ); + } + kernels::add_inplace(&mut bufs.pref_x[..seq_len * dim], proj_out, seq_len * dim); + + // Post-attention RMSNorm + SwiGLU MLP + let x_norm2 = &mut bufs.pref_x_norm[..seq_len * dim]; + kernels::rms_norm( + x_norm2, + &bufs.pref_x[..seq_len * dim], + &layer.post_attn_norm, + seq_len, + dim, + eps, + ); + + let gate_up = &mut bufs.pref_gate_up[..seq_len * 2 * intermediate]; + if !layer.gate_up_fused_f32.is_empty() { + kernels::linear_nobias(gate_up, x_norm2, &layer.gate_up_fused_f32, seq_len, dim, 2 * intermediate); + } else { + unsafe { + kernels::linear_nobias_bf16_scratch( + gate_up, + x_norm2, + layer.gate_up_fused_bf16.as_ptr(), + seq_len, + dim, + 2 * intermediate, + &mut bufs.bf16_scratch, + ); + } + } + + let gate = &mut bufs.pref_gate[..seq_len * intermediate]; + kernels::swiglu_multiply(gate, gate_up, seq_len, intermediate); + + let ffn_out = &mut bufs.pref_ffn_out[..seq_len * dim]; + if !layer.down_weight_f32.is_empty() { + kernels::linear_nobias(ffn_out, gate, &layer.down_weight_f32, seq_len, intermediate, dim); + } else { + unsafe { + kernels::linear_nobias_bf16_scratch( + ffn_out, + gate, + layer.down_weight_bf16, + seq_len, + intermediate, + dim, + &mut bufs.bf16_scratch, + ); + } + } + kernels::add_inplace(&mut bufs.pref_x[..seq_len * dim], ffn_out, seq_len * dim); + } + + kv_cache.len = start_pos + seq_len; +} + +/// Decoder single-token forward: returns greedy token ID. +pub fn decoder_forward( + decoder: &Decoder, + cfg: &QwenConfig, + kv_cache: &mut KvCache, + rope: &mut RopeCache, + bufs: &mut DecoderBuffers, + input_embed: &[f32], +) -> i32 { + let mode = decode_kernel_mode(); + let dim = cfg.dec_hidden; + let n_heads = cfg.dec_heads; + let n_kv_heads = cfg.dec_kv_heads; + let head_dim = cfg.dec_head_dim; + let intermediate = cfg.dec_intermediate; + let eps = cfg.dec_rms_norm_eps; + let theta = cfg.dec_rope_theta; + let q_dim = n_heads * head_dim; + let kv_dim = n_kv_heads * head_dim; + + bufs.x[..dim].copy_from_slice(&input_embed[..dim]); + + let pos = kv_cache.len; + + if pos >= kv_cache.max_seq { + kv_cache.grow(pos + 1024); + } + + rope.ensure(pos + 1, head_dim, theta); + let rope_cos = rope.cos_at(pos); + let rope_sin = rope.sin_at(pos); + + let scale = 1.0 / (head_dim as f32).sqrt(); + + for (layer_idx, layer) in decoder.layers.iter().enumerate() { + kernels::rms_norm( + &mut bufs.x_norm[..dim], + &bufs.x[..dim], + &layer.input_norm, + 1, + dim, + eps, + ); + + match mode { + DecodeKernelMode::Bf16 => unsafe { + kernels::linear_nobias_bf16_scratch( + &mut bufs.q[..q_dim], + &bufs.x_norm[..dim], + layer.wq_weight_bf16, + 1, + dim, + q_dim, + &mut bufs.bf16_scratch, + ); + kernels::linear_nobias_bf16_scratch( + &mut bufs.k[..kv_dim], + &bufs.x_norm[..dim], + layer.wk_weight_bf16, + 1, + dim, + kv_dim, + &mut bufs.bf16_scratch, + ); + kernels::linear_nobias_bf16_scratch( + &mut bufs.v[..kv_dim], + &bufs.x_norm[..dim], + layer.wv_weight_bf16, + 1, + dim, + kv_dim, + &mut bufs.bf16_scratch, + ); + }, + DecodeKernelMode::Int8 => { + kernels::linear_nobias_int8_qkv( + &mut bufs.q[..q_dim], + &mut bufs.k[..kv_dim], + &mut bufs.v[..kv_dim], + &bufs.x_norm[..dim], + &layer.wq_int8, + &layer.wq_int8_scales, + &layer.wk_int8, + &layer.wk_int8_scales, + &layer.wv_int8, + &layer.wv_int8_scales, + dim, + q_dim, + kv_dim, + ); + } + } + + kernels::rms_norm_per_head( + &mut bufs.q[..q_dim], + &layer.q_norm_weight, + 1, + n_heads, + head_dim, + eps, + ); + kernels::rms_norm_per_head( + &mut bufs.k[..kv_dim], + &layer.k_norm_weight, + 1, + n_kv_heads, + head_dim, + eps, + ); + + kernels::apply_rope_neox( + &mut bufs.q[..q_dim], + rope_cos, + rope_sin, + 1, + n_heads, + head_dim, + ); + kernels::apply_rope_neox( + &mut bufs.k[..kv_dim], + rope_cos, + rope_sin, + 1, + n_kv_heads, + head_dim, + ); + + kv_cache.k_write_pos(layer_idx, pos, &bufs.k[..kv_dim]); + kv_cache.v_write_pos(layer_idx, pos, &bufs.v[..kv_dim]); + + let total_seq = pos + 1; + let k_base = kv_cache.k_layer_base(layer_idx); + let v_base = kv_cache.v_layer_base(layer_idx); + let head_stride = kv_cache.head_stride(); + + kernels::causal_attention( + &mut bufs.attn_out[..q_dim], + &bufs.q[..q_dim], + k_base, + v_base, + head_stride, + 1, + total_seq, + n_heads, + n_kv_heads, + head_dim, + scale, + pos, + ); + + match mode { + DecodeKernelMode::Bf16 => unsafe { + kernels::linear_nobias_bf16_scratch( + &mut bufs.x_norm[..dim], + &bufs.attn_out[..q_dim], + layer.wo_weight_bf16, + 1, + q_dim, + dim, + &mut bufs.bf16_scratch, + ); + kernels::add_inplace(&mut bufs.x[..dim], &bufs.x_norm[..dim], dim); + }, + DecodeKernelMode::Int8 => { + kernels::linear_nobias_int8_addto( + &mut bufs.x[..dim], + &bufs.attn_out[..q_dim], + &layer.wo_int8, + &layer.wo_int8_scales, + q_dim, + dim, + ); + } + } + + kernels::rms_norm( + &mut bufs.x_norm[..dim], + &bufs.x[..dim], + &layer.post_attn_norm, + 1, + dim, + eps, + ); + + match mode { + DecodeKernelMode::Bf16 => unsafe { + kernels::linear_nobias_bf16_scratch( + &mut bufs.gate_buf[..2 * intermediate], + &bufs.x_norm[..dim], + layer.gate_up_fused_bf16.as_ptr(), + 1, + dim, + 2 * intermediate, + &mut bufs.bf16_scratch, + ); + kernels::swiglu_multiply( + &mut bufs.ffn_out[..intermediate], + &bufs.gate_buf[..2 * intermediate], + 1, + intermediate, + ); + kernels::linear_nobias_bf16_scratch( + &mut bufs.x_norm[..dim], + &bufs.ffn_out[..intermediate], + layer.down_weight_bf16, + 1, + intermediate, + dim, + &mut bufs.bf16_scratch, + ); + kernels::add_inplace(&mut bufs.x[..dim], &bufs.x_norm[..dim], dim); + }, + DecodeKernelMode::Int8 => { + kernels::linear_nobias_int8_swiglu( + &mut bufs.ffn_out[..intermediate], + &bufs.x_norm[..dim], + &layer.gate_up_int8, + &layer.gate_up_int8_scales, + dim, + intermediate, + ); + kernels::linear_nobias_int8_addto( + &mut bufs.x[..dim], + &bufs.ffn_out[..intermediate], + &layer.down_int8, + &layer.down_int8_scales, + intermediate, + dim, + ); + } + } + } + + kv_cache.len = pos + 1; + + // Final norm + streaming argmax (use x_norm as temp to avoid heap allocation) + kernels::rms_norm( + &mut bufs.x_norm[..dim], + &bufs.x[..dim], + &decoder.norm, + 1, + dim, + eps, + ); + bufs.x[..dim].copy_from_slice(&bufs.x_norm[..dim]); + let lm_out_dim = cfg.lm_head_dim(); + + if matches!(mode, DecodeKernelMode::Int8) { + if let (Some(ref int8_data), Some(ref scales)) = + (&decoder.lm_head_int8, &decoder.lm_head_int8_scales) + { + return kernels::argmax_matvec_int8(&bufs.x[..dim], int8_data, scales, dim, lm_out_dim) + as i32; + } + } + + let lm_weight = decoder.lm_head_bf16.unwrap_or(decoder.tok_embeddings_bf16); + kernels::argmax_matvec_bf16(&bufs.x[..dim], lm_weight, dim, lm_out_dim) as i32 +} + +/// Decoder prefill that returns per-position logits (for forced aligner). +/// Returns `[seq_len × out_dim]` logits where out_dim = classify_num. +pub fn decoder_prefill_logits( + decoder: &Decoder, + cfg: &QwenConfig, + kv_cache: &mut KvCache, + rope: &mut RopeCache, + bufs: &mut DecoderBuffers, + input_embeds: &[f32], + seq_len: usize, +) -> Vec { + let dim = cfg.dec_hidden; + let eps = cfg.dec_rms_norm_eps; + let out_dim = cfg.lm_head_dim(); + + // Run the standard prefill to get hidden states + decoder_prefill(decoder, cfg, kv_cache, rope, bufs, input_embeds, seq_len); + + // After prefill, pref_x contains the final hidden states for all positions. + // Apply final RMS norm and lm_head projection. + let x = &bufs.pref_x[..seq_len * dim]; + let mut x_norm = vec![0.0f32; seq_len * dim]; + kernels::rms_norm(&mut x_norm, x, &decoder.norm, seq_len, dim, eps); + + let lm_weight = decoder.lm_head_bf16.unwrap_or(decoder.tok_embeddings_bf16); + + // Project each position through lm_head: [seq_len × dim] × [out_dim × dim]^T → [seq_len × out_dim] + let mut logits = vec![0.0f32; seq_len * out_dim]; + unsafe { + kernels::linear_nobias_bf16_scratch( + &mut logits, + &x_norm, + lm_weight, + seq_len, + dim, + out_dim, + &mut bufs.bf16_scratch, + ); + } + + logits +} + + +/// Convert a token embedding from bf16 to f32. +/// +/// # Safety +/// tok_emb_bf16 must point to valid memory for at least (token_id + 1) * dim bf16 values. +pub unsafe fn tok_embed_bf16_to_f32( + dst: &mut [f32], + tok_emb_bf16: *const u16, + token_id: i32, + dim: usize, +) { + let src = unsafe { std::slice::from_raw_parts(tok_emb_bf16.add(token_id as usize * dim), dim) }; + kernels::bf16_to_f32_buf(dst, src); +} diff --git a/vendor/qwenasr/crates/qwen-asr/src/encoder.rs b/vendor/qwenasr/crates/qwen-asr/src/encoder.rs new file mode 100644 index 0000000..a55f797 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/src/encoder.rs @@ -0,0 +1,359 @@ +//! Audio encoder: Conv2D stem + windowed transformer + projection cascade. + +use crate::config::*; +use crate::kernels; +use crate::safetensors::MultiSafetensors; + +pub struct EncLayer { + pub wq_weight: Vec, + pub wq_bias: Vec, + pub wk_weight: Vec, + pub wk_bias: Vec, + pub wv_weight: Vec, + pub wv_bias: Vec, + pub wo_weight: Vec, + pub wo_bias: Vec, + pub attn_norm_weight: Vec, + pub attn_norm_bias: Vec, + pub fc1_weight: Vec, + pub fc1_bias: Vec, + pub fc2_weight: Vec, + pub fc2_bias: Vec, + pub ffn_norm_weight: Vec, + pub ffn_norm_bias: Vec, +} + +pub struct EncoderBuffers { + pub x_norm: Vec, + pub q: Vec, + pub k: Vec, + pub v: Vec, + pub attn_out: Vec, + pub proj_out: Vec, + pub ffn_mid: Vec, + pub ffn_out: Vec, + pub cap_tokens: usize, +} + +impl Default for EncoderBuffers { + fn default() -> Self { + Self::new() + } +} + +impl EncoderBuffers { + pub fn new() -> Self { + EncoderBuffers { + x_norm: Vec::new(), + q: Vec::new(), + k: Vec::new(), + v: Vec::new(), + attn_out: Vec::new(), + proj_out: Vec::new(), + ffn_mid: Vec::new(), + ffn_out: Vec::new(), + cap_tokens: 0, + } + } + + pub fn ensure(&mut self, total_tokens: usize, d_model: usize, ffn_dim: usize) { + if total_tokens <= self.cap_tokens { + return; + } + let mut new_cap = if self.cap_tokens > 0 { self.cap_tokens } else { 256 }; + while new_cap < total_tokens { + new_cap *= 2; + } + self.x_norm.resize(new_cap * d_model, 0.0); + self.q.resize(new_cap * d_model, 0.0); + self.k.resize(new_cap * d_model, 0.0); + self.v.resize(new_cap * d_model, 0.0); + self.attn_out.resize(new_cap * d_model, 0.0); + self.proj_out.resize(new_cap * d_model, 0.0); + self.ffn_mid.resize(new_cap * ffn_dim, 0.0); + self.ffn_out.resize(new_cap * d_model, 0.0); + self.cap_tokens = new_cap; + } +} + +pub struct Encoder { + pub conv1_weight: Vec, + pub conv1_bias: Vec, + pub conv2_weight: Vec, + pub conv2_bias: Vec, + pub conv3_weight: Vec, + pub conv3_bias: Vec, + pub conv_out_weight: Vec, + pub layers: Vec, + pub ln_post_weight: Vec, + pub ln_post_bias: Vec, + pub proj1_weight: Vec, + pub proj1_bias: Vec, + pub proj2_weight: Vec, + pub proj2_bias: Vec, +} + +const ENC_PREFIX: &str = "thinker.audio_tower."; + +fn load_f32(ms: &MultiSafetensors, name: &str) -> Option> { + let result = ms.get_f32(name); + if result.is_none() { + eprintln!("encoder: weight not found: {}", name); + } + result +} + +fn load_bf16_as_f32(ms: &MultiSafetensors, name: &str) -> Option> { + let (si, t) = ms.find(name).or_else(|| { + eprintln!("encoder: weight not found: {}", name); + None + })?; + + let bf16_ptr = ms.shards[si].get_bf16_direct(t)?; + let n = t.numel(); + let mut f32_data = vec![0.0f32; n]; + let src = unsafe { std::slice::from_raw_parts(bf16_ptr, n) }; + for i in 0..n { + f32_data[i] = f32::from_bits((src[i] as u32) << 16); + } + Some(f32_data) +} + +impl Encoder { + pub fn load(ms: &MultiSafetensors, cfg: &QwenConfig) -> Option { + let p = ENC_PREFIX; + + let conv1_weight = load_f32(ms, &format!("{}conv2d1.weight", p))?; + let conv1_bias = load_f32(ms, &format!("{}conv2d1.bias", p))?; + let conv2_weight = load_f32(ms, &format!("{}conv2d2.weight", p))?; + let conv2_bias = load_f32(ms, &format!("{}conv2d2.bias", p))?; + let conv3_weight = load_f32(ms, &format!("{}conv2d3.weight", p))?; + let conv3_bias = load_f32(ms, &format!("{}conv2d3.bias", p))?; + let conv_out_weight = load_bf16_as_f32(ms, &format!("{}conv_out.weight", p))?; + + let mut layers = Vec::new(); + for i in 0..cfg.enc_layers { + let lp = format!("{}layers.{}", p, i); + + let layer = EncLayer { + wq_weight: load_bf16_as_f32(ms, &format!("{}.self_attn.q_proj.weight", lp))?, + wq_bias: load_f32(ms, &format!("{}.self_attn.q_proj.bias", lp))?, + wk_weight: load_bf16_as_f32(ms, &format!("{}.self_attn.k_proj.weight", lp))?, + wk_bias: load_f32(ms, &format!("{}.self_attn.k_proj.bias", lp))?, + wv_weight: load_bf16_as_f32(ms, &format!("{}.self_attn.v_proj.weight", lp))?, + wv_bias: load_f32(ms, &format!("{}.self_attn.v_proj.bias", lp))?, + wo_weight: load_bf16_as_f32(ms, &format!("{}.self_attn.out_proj.weight", lp))?, + wo_bias: load_f32(ms, &format!("{}.self_attn.out_proj.bias", lp))?, + attn_norm_weight: load_f32(ms, &format!("{}.self_attn_layer_norm.weight", lp))?, + attn_norm_bias: load_f32(ms, &format!("{}.self_attn_layer_norm.bias", lp))?, + fc1_weight: load_bf16_as_f32(ms, &format!("{}.fc1.weight", lp))?, + fc1_bias: load_f32(ms, &format!("{}.fc1.bias", lp))?, + fc2_weight: load_bf16_as_f32(ms, &format!("{}.fc2.weight", lp))?, + fc2_bias: load_f32(ms, &format!("{}.fc2.bias", lp))?, + ffn_norm_weight: load_f32(ms, &format!("{}.final_layer_norm.weight", lp))?, + ffn_norm_bias: load_f32(ms, &format!("{}.final_layer_norm.bias", lp))?, + }; + layers.push(layer); + } + + let ln_post_weight = load_f32(ms, &format!("{}ln_post.weight", p))?; + let ln_post_bias = load_f32(ms, &format!("{}ln_post.bias", p))?; + let proj1_weight = load_bf16_as_f32(ms, &format!("{}proj1.weight", p))?; + let proj1_bias = load_f32(ms, &format!("{}proj1.bias", p))?; + let proj2_weight = load_bf16_as_f32(ms, &format!("{}proj2.weight", p))?; + let proj2_bias = load_f32(ms, &format!("{}proj2.bias", p))?; + + Some(Encoder { + conv1_weight, + conv1_bias, + conv2_weight, + conv2_bias, + conv3_weight, + conv3_bias, + conv_out_weight, + layers, + ln_post_weight, + ln_post_bias, + proj1_weight, + proj1_bias, + proj2_weight, + proj2_bias, + }) + } + + /// Run encoder forward pass on mel spectrogram. + /// mel: [128, mel_frames], returns [total_tokens, output_dim]. + pub fn forward(&self, cfg: &QwenConfig, mel: &[f32], mel_frames: usize, enc_bufs: Option<&mut EncoderBuffers>) -> Option<(Vec, usize)> { + let d_model = cfg.enc_d_model; + let n_heads = cfg.enc_heads; + let head_dim = cfg.enc_head_dim; + let ffn_dim = cfg.enc_ffn_dim; + let output_dim = cfg.enc_output_dim; + let chunk_size = cfg.enc_chunk_size; + let n_window_infer = cfg.enc_n_window_infer; + + // Determine tokens per full chunk + let tokens_per_chunk = { + let w = chunk_size; + let w1 = (w + 2 - 3) / 2 + 1; + let w2 = (w1 + 2 - 3) / 2 + 1; + (w2 + 2 - 3) / 2 + 1 + }; + + let n_chunks = mel_frames.div_ceil(chunk_size); + + // Pre-calculate total tokens + let mut total_tokens = 0; + let mut chunk_sizes = Vec::new(); + for c in 0..n_chunks { + let start = c * chunk_size; + let end = (start + chunk_size).min(mel_frames); + let chunk_w = end - start; + let w1 = (chunk_w + 2 - 3) / 2 + 1; + let w2 = (w1 + 2 - 3) / 2 + 1; + let w3 = (w2 + 2 - 3) / 2 + 1; + total_tokens += w3; + chunk_sizes.push((start, end, w3)); + } + + // Main sequence buffer: [total_tokens, d_model] + let mut x = vec![0.0f32; total_tokens * d_model]; + let mut token_offset = 0; + + // Process each chunk through Conv2D + reshape + project + sinusoidal PE + for &(start, end, w3) in &chunk_sizes { + let chunk_w = end - start; + + // Extract chunk mel: [128, chunk_w] + let mut chunk_mel = vec![0.0f32; 128 * chunk_w]; + for m in 0..128 { + chunk_mel[m * chunk_w..(m + 1) * chunk_w] + .copy_from_slice(&mel[m * mel_frames + start..m * mel_frames + end]); + } + + // Conv2D layer 1: [1, 128, chunk_w] -> [480, h1, w1] + let h1 = (128 + 2 - 3) / 2 + 1; // 64 + let w1 = (chunk_w + 2 - 3) / 2 + 1; + let mut c1 = vec![0.0f32; CONV_HIDDEN * h1 * w1]; + kernels::conv2d( + &mut c1, &chunk_mel, &self.conv1_weight, Some(&self.conv1_bias), + 1, CONV_HIDDEN, 128, chunk_w, 3, 3, 2, 1, + ); + kernels::gelu(&mut c1, CONV_HIDDEN * h1 * w1); + + // Conv2D layer 2: [480, h1, w1] -> [480, h2, w2] + let h2 = (h1 + 2 - 3) / 2 + 1; // 32 + let w2 = (w1 + 2 - 3) / 2 + 1; + let mut c2 = vec![0.0f32; CONV_HIDDEN * h2 * w2]; + kernels::conv2d( + &mut c2, &c1, &self.conv2_weight, Some(&self.conv2_bias), + CONV_HIDDEN, CONV_HIDDEN, h1, w1, 3, 3, 2, 1, + ); + kernels::gelu(&mut c2, CONV_HIDDEN * h2 * w2); + + // Conv2D layer 3: [480, h2, w2] -> [480, h3, w3] + let h3 = (h2 + 2 - 3) / 2 + 1; // 16 + let _w3_calc = (w2 + 2 - 3) / 2 + 1; + debug_assert_eq!(_w3_calc, w3); + let mut c3 = vec![0.0f32; CONV_HIDDEN * h3 * w3]; + kernels::conv2d( + &mut c3, &c2, &self.conv3_weight, Some(&self.conv3_bias), + CONV_HIDDEN, CONV_HIDDEN, h2, w2, 3, 3, 2, 1, + ); + kernels::gelu(&mut c3, CONV_HIDDEN * h3 * w3); + + // Reshape [480, h3, w3] -> [w3, 480*h3] + // Loop order: ch → f → t for sequential reads from c3 + let conv_proj_dim = CONV_HIDDEN * h3; + let mut reshaped = vec![0.0f32; w3 * conv_proj_dim]; + for ch in 0..CONV_HIDDEN { + for f in 0..h3 { + let src_off = ch * h3 * w3 + f * w3; + let dst_col = ch * h3 + f; + for t in 0..w3 { + reshaped[t * conv_proj_dim + dst_col] = c3[src_off + t]; + } + } + } + + // Project: [w3, 7680] -> [w3, d_model] + let projected = &mut x[token_offset * d_model..(token_offset + w3) * d_model]; + kernels::linear_nobias(projected, &reshaped, &self.conv_out_weight, w3, conv_proj_dim, d_model); + + // Add per-chunk sinusoidal PE + let mut pe = vec![0.0f32; w3 * d_model]; + kernels::sinusoidal_pe(&mut pe, w3, d_model); + kernels::add_inplace(projected, &pe, w3 * d_model); + + token_offset += w3; + } + + // Build attention window boundaries + let window_token_size = tokens_per_chunk * (n_window_infer / chunk_size); + let n_windows = total_tokens.div_ceil(window_token_size); + let mut window_starts = vec![0i32; n_windows + 1]; + for (w, ws) in window_starts.iter_mut().enumerate().take(n_windows) { + *ws = (w * window_token_size) as i32; + } + window_starts[n_windows] = total_tokens as i32; + + // Transformer layer scratch buffers (reusable or fresh) + let mut _owned_bufs; + let bufs: &mut EncoderBuffers = match enc_bufs { + Some(b) => { b.ensure(total_tokens, d_model, ffn_dim); b } + None => { _owned_bufs = EncoderBuffers::new(); _owned_bufs.ensure(total_tokens, d_model, ffn_dim); &mut _owned_bufs } + }; + + let scale = 1.0 / (head_dim as f32).sqrt(); + let td = total_tokens * d_model; + let tf = total_tokens * ffn_dim; + + for layer in &self.layers { + // Self-attention + kernels::layer_norm(&mut bufs.x_norm[..td], &x, &layer.attn_norm_weight, &layer.attn_norm_bias, + total_tokens, d_model, 1e-5); + + kernels::linear(&mut bufs.q[..td], &bufs.x_norm[..td], &layer.wq_weight, Some(&layer.wq_bias), + total_tokens, d_model, d_model); + kernels::linear(&mut bufs.k[..td], &bufs.x_norm[..td], &layer.wk_weight, Some(&layer.wk_bias), + total_tokens, d_model, d_model); + kernels::linear(&mut bufs.v[..td], &bufs.x_norm[..td], &layer.wv_weight, Some(&layer.wv_bias), + total_tokens, d_model, d_model); + + kernels::bidirectional_attention(&mut bufs.attn_out[..td], &bufs.q[..td], &bufs.k[..td], &bufs.v[..td], + total_tokens, n_heads, head_dim, scale, + &window_starts, n_windows); + + // Fused: x += wo_bias + attn_out @ wo_weight.T + kernels::linear_accumulate(&mut x, &bufs.attn_out[..td], &layer.wo_weight, Some(&layer.wo_bias), + total_tokens, d_model, d_model); + + // FFN + kernels::layer_norm(&mut bufs.x_norm[..td], &x, &layer.ffn_norm_weight, &layer.ffn_norm_bias, + total_tokens, d_model, 1e-5); + + kernels::linear(&mut bufs.ffn_mid[..tf], &bufs.x_norm[..td], &layer.fc1_weight, Some(&layer.fc1_bias), + total_tokens, d_model, ffn_dim); + kernels::gelu(&mut bufs.ffn_mid[..tf], tf); + // Fused: x += fc2_bias + ffn_mid @ fc2_weight.T + kernels::linear_accumulate(&mut x, &bufs.ffn_mid[..tf], &layer.fc2_weight, Some(&layer.fc2_bias), + total_tokens, ffn_dim, d_model); + } + + // Final LayerNorm: use x_norm as temp, then swap into x + kernels::layer_norm(&mut bufs.x_norm[..td], &x, &self.ln_post_weight, &self.ln_post_bias, + total_tokens, d_model, 1e-5); + x[..td].copy_from_slice(&bufs.x_norm[..td]); + + // Projection: proj1 (GELU) -> proj2 (reuse scratch buffers) + kernels::linear(&mut bufs.q[..td], &x, &self.proj1_weight, Some(&self.proj1_bias), + total_tokens, d_model, d_model); + kernels::gelu(&mut bufs.q[..td], td); + + let mut enc_output = vec![0.0f32; total_tokens * output_dim]; + kernels::linear(&mut enc_output, &bufs.q[..td], &self.proj2_weight, Some(&self.proj2_bias), + total_tokens, d_model, output_dim); + + Some((enc_output, total_tokens)) + } +} diff --git a/vendor/qwenasr/crates/qwen-asr/src/jni_api.rs b/vendor/qwenasr/crates/qwen-asr/src/jni_api.rs new file mode 100644 index 0000000..0a31e20 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/src/jni_api.rs @@ -0,0 +1,179 @@ +/// JNI API for Android integration. +/// Build with: cargo ndk -t arm64-v8a build --release --features android +/// +/// Java class: com.qwenasr.QAsrEngine +/// +/// ```java +/// public class QAsrEngine { +/// static { System.loadLibrary("qwen_asr"); } +/// private long nativeHandle; +/// public native boolean loadModel(String modelDir, int nThreads); +/// public native String transcribePcm(float[] samples); +/// public native String transcribeWav(byte[] wavData); +/// public native void setSegmentSec(float sec); +/// public native void setLanguage(String language); +/// public native void free(); +/// } +/// ``` + +use std::os::raw::{c_char, c_void}; + +// JNI types +type JNIEnv = *mut c_void; +type JClass = *mut c_void; +type JString = *mut c_void; +type JObject = *mut c_void; +type JFloatArray = *mut c_void; +type JByteArray = *mut c_void; +type JLong = i64; +type JInt = i32; +type JFloat = f32; +type JBoolean = u8; + +// JNI function signatures we need +extern "C" { + fn __android_log_write(prio: i32, tag: *const c_char, text: *const c_char) -> i32; +} + +// JNI string helpers (accessed via function pointers in JNIEnv) +// These are simplified - a real implementation would use jni-rs or manual vtable access. +// For now, we use the C-FFI API underneath. + +/// Load model. Returns native handle as jlong. +#[no_mangle] +pub unsafe extern "C" fn Java_com_qwenasr_QAsrEngine_nativeLoadModel( + env: JNIEnv, + _obj: JObject, + model_dir: JString, + n_threads: JInt, +) -> JLong { + // Use the C-FFI API for the actual implementation + let c_api_engine = crate::c_api::qwen_asr_load_model( + jstring_to_cstr(env, model_dir), + n_threads, + 0, // silent + ); + c_api_engine as JLong +} + +/// Transcribe PCM float array. +#[no_mangle] +pub unsafe extern "C" fn Java_com_qwenasr_QAsrEngine_nativeTranscribePcm( + env: JNIEnv, + _obj: JObject, + handle: JLong, + samples: JFloatArray, + n_samples: JInt, +) -> JString { + if handle == 0 { + return std::ptr::null_mut(); + } + + let samples_ptr = jfloat_array_get(env, samples); + if samples_ptr.is_null() { + return std::ptr::null_mut(); + } + + let result = crate::c_api::qwen_asr_transcribe_pcm( + handle as *mut crate::c_api::QwenAsrEngine, + samples_ptr, + n_samples, + ); + + jfloat_array_release(env, samples, samples_ptr); + + if result.is_null() { + return std::ptr::null_mut(); + } + + let jstr = new_jstring(env, result); + crate::c_api::qwen_asr_free_string(result); + jstr +} + +/// Transcribe WAV byte array. +#[no_mangle] +pub unsafe extern "C" fn Java_com_qwenasr_QAsrEngine_nativeTranscribeWav( + env: JNIEnv, + _obj: JObject, + handle: JLong, + wav_data: JByteArray, + wav_len: JInt, +) -> JString { + if handle == 0 { + return std::ptr::null_mut(); + } + + let data_ptr = jbyte_array_get(env, wav_data); + if data_ptr.is_null() { + return std::ptr::null_mut(); + } + + let result = crate::c_api::qwen_asr_transcribe_wav_buffer( + handle as *mut crate::c_api::QwenAsrEngine, + data_ptr as *const u8, + wav_len, + ); + + jbyte_array_release(env, wav_data, data_ptr); + + if result.is_null() { + return std::ptr::null_mut(); + } + + let jstr = new_jstring(env, result); + crate::c_api::qwen_asr_free_string(result); + jstr +} + +/// Free engine. +#[no_mangle] +pub unsafe extern "C" fn Java_com_qwenasr_QAsrEngine_nativeFree( + _env: JNIEnv, + _obj: JObject, + handle: JLong, +) { + if handle != 0 { + crate::c_api::qwen_asr_free(handle as *mut crate::c_api::QwenAsrEngine); + } +} + +/// Set segment seconds. +#[no_mangle] +pub unsafe extern "C" fn Java_com_qwenasr_QAsrEngine_nativeSetSegmentSec( + _env: JNIEnv, + _obj: JObject, + handle: JLong, + sec: JFloat, +) { + if handle != 0 { + crate::c_api::qwen_asr_set_segment_sec(handle as *mut crate::c_api::QwenAsrEngine, sec); + } +} + +// ======================================================================== +// JNI helper stubs - these use raw JNIEnv vtable access +// In production, use the `jni` crate. These are minimal stubs for compilation. +// ======================================================================== + +unsafe fn jstring_to_cstr(_env: JNIEnv, _jstr: JString) -> *const c_char { + // Real implementation: (*(*env)).GetStringUTFChars(env, jstr, null) + // Stub returns null - will need jni crate for real Android builds + std::ptr::null() +} + +unsafe fn jfloat_array_get(_env: JNIEnv, _arr: JFloatArray) -> *const f32 { + std::ptr::null() +} + +unsafe fn jfloat_array_release(_env: JNIEnv, _arr: JFloatArray, _ptr: *const f32) {} + +unsafe fn jbyte_array_get(_env: JNIEnv, _arr: JByteArray) -> *const i8 { + std::ptr::null() +} + +unsafe fn jbyte_array_release(_env: JNIEnv, _arr: JByteArray, _ptr: *const i8) {} + +unsafe fn new_jstring(_env: JNIEnv, _cstr: *const c_char) -> JString { + std::ptr::null_mut() +} diff --git a/vendor/qwenasr/crates/qwen-asr/src/kernels/avx.rs b/vendor/qwenasr/crates/qwen-asr/src/kernels/avx.rs new file mode 100644 index 0000000..55c53b4 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/src/kernels/avx.rs @@ -0,0 +1,957 @@ +// x86 AVX2+FMA implementations of hot kernels. +#[cfg(target_arch = "x86_64")] +use core::arch::x86_64::*; + +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2")] +pub unsafe fn bf16_to_f32_buf(dst: &mut [f32], src: &[u16]) { + let n = src.len(); + let mut i = 0usize; + + while i + 8 <= n { + let raw = _mm_loadu_si128(src.as_ptr().add(i) as *const __m128i); + let wide = _mm256_cvtepu16_epi32(raw); + let shifted = _mm256_slli_epi32(wide, 16); + _mm256_storeu_ps(dst.as_mut_ptr().add(i), _mm256_castsi256_ps(shifted)); + i += 8; + } + + while i < n { + dst[i] = f32::from_bits((src[i] as u32) << 16); + i += 1; + } +} + +/// Convert 8 BF16 values (in a __m128i) to 8 f32 values (in a __m256). +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2")] +#[inline] +unsafe fn bf16x8_to_f32(raw: __m128i) -> __m256 { + // Zero-extend u16 -> u32, shift left 16 to put BF16 bits in f32 position + let wide = _mm256_cvtepu16_epi32(raw); + let shifted = _mm256_slli_epi32(wide, 16); + _mm256_castsi256_ps(shifted) +} + +/// Horizontal sum of __m256 -> f32 +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2")] +#[inline] +unsafe fn hsum_ps(v: __m256) -> f32 { + // Add high 128 to low 128 + let hi = _mm256_extractf128_ps(v, 1); + let lo = _mm256_castps256_ps128(v); + let sum128 = _mm_add_ps(lo, hi); + // Horizontal add twice to reduce 4 -> 2 -> 1 + let shuf = _mm_movehdup_ps(sum128); // [1,1,3,3] + let sum64 = _mm_add_ps(sum128, shuf); // [0+1, _, 2+3, _] + let hi64 = _mm_movehl_ps(sum64, sum64); // [2+3, _, _, _] + let sum32 = _mm_add_ss(sum64, hi64); + _mm_cvtss_f32(sum32) +} + +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2")] +#[inline] +unsafe fn hsum_epi32(v: __m256i) -> i32 { + let mut lanes = [0i32; 8]; + _mm256_storeu_si256(lanes.as_mut_ptr() as *mut __m256i, v); + lanes.into_iter().sum() +} + +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2")] +#[inline] +unsafe fn dot_i8_i8(x: *const i8, w: *const i8, n: usize) -> i32 { + let mut k = 0usize; + let mut acc0 = _mm256_setzero_si256(); + let mut acc1 = _mm256_setzero_si256(); + let mut acc2 = _mm256_setzero_si256(); + let mut acc3 = _mm256_setzero_si256(); + + while k + 64 <= n { + let x0 = _mm_loadu_si128(x.add(k) as *const __m128i); + let w0 = _mm_loadu_si128(w.add(k) as *const __m128i); + let x1 = _mm_loadu_si128(x.add(k + 16) as *const __m128i); + let w1 = _mm_loadu_si128(w.add(k + 16) as *const __m128i); + let x2 = _mm_loadu_si128(x.add(k + 32) as *const __m128i); + let w2 = _mm_loadu_si128(w.add(k + 32) as *const __m128i); + let x3 = _mm_loadu_si128(x.add(k + 48) as *const __m128i); + let w3 = _mm_loadu_si128(w.add(k + 48) as *const __m128i); + + acc0 = _mm256_add_epi32( + acc0, + _mm256_madd_epi16(_mm256_cvtepi8_epi16(x0), _mm256_cvtepi8_epi16(w0)), + ); + acc1 = _mm256_add_epi32( + acc1, + _mm256_madd_epi16(_mm256_cvtepi8_epi16(x1), _mm256_cvtepi8_epi16(w1)), + ); + acc2 = _mm256_add_epi32( + acc2, + _mm256_madd_epi16(_mm256_cvtepi8_epi16(x2), _mm256_cvtepi8_epi16(w2)), + ); + acc3 = _mm256_add_epi32( + acc3, + _mm256_madd_epi16(_mm256_cvtepi8_epi16(x3), _mm256_cvtepi8_epi16(w3)), + ); + k += 64; + } + + while k + 16 <= n { + let xv = _mm_loadu_si128(x.add(k) as *const __m128i); + let wv = _mm_loadu_si128(w.add(k) as *const __m128i); + acc0 = _mm256_add_epi32( + acc0, + _mm256_madd_epi16(_mm256_cvtepi8_epi16(xv), _mm256_cvtepi8_epi16(wv)), + ); + k += 16; + } + + let mut sum = hsum_epi32(_mm256_add_epi32( + _mm256_add_epi32(acc0, acc1), + _mm256_add_epi32(acc2, acc3), + )); + + while k < n { + sum += (*x.add(k) as i32) * (*w.add(k) as i32); + k += 1; + } + sum +} + +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2", enable = "fma")] +pub unsafe fn bf16_matvec_fused( + y: &mut [f32], + x: &[f32], + w_bf16: *const u16, + bias: Option<&[f32]>, + in_dim: usize, + out_dim: usize, +) { + let mut o = 0usize; + + // Process 2 output rows at a time + while o + 1 < out_dim { + let w0 = w_bf16.add(o * in_dim); + let w1 = w_bf16.add((o + 1) * in_dim); + let mut s0 = bias.map_or(0.0f32, |b| b[o]); + let mut s1 = bias.map_or(0.0f32, |b| b[o + 1]); + + let mut a0 = _mm256_setzero_ps(); + let mut a1 = _mm256_setzero_ps(); + let mut b0 = _mm256_setzero_ps(); + let mut b1 = _mm256_setzero_ps(); + let mut k = 0usize; + + // Main loop: 16 elements per iteration + while k + 16 <= in_dim { + let xlo = _mm256_loadu_ps(x.as_ptr().add(k)); + let xhi = _mm256_loadu_ps(x.as_ptr().add(k + 8)); + + // Row 0 + let raw0lo = _mm_loadu_si128(w0.add(k) as *const __m128i); + let raw0hi = _mm_loadu_si128(w0.add(k + 8) as *const __m128i); + let w0lo = bf16x8_to_f32(raw0lo); + let w0hi = bf16x8_to_f32(raw0hi); + a0 = _mm256_fmadd_ps(w0lo, xlo, a0); + a1 = _mm256_fmadd_ps(w0hi, xhi, a1); + + // Row 1 + let raw1lo = _mm_loadu_si128(w1.add(k) as *const __m128i); + let raw1hi = _mm_loadu_si128(w1.add(k + 8) as *const __m128i); + let w1lo = bf16x8_to_f32(raw1lo); + let w1hi = bf16x8_to_f32(raw1hi); + b0 = _mm256_fmadd_ps(w1lo, xlo, b0); + b1 = _mm256_fmadd_ps(w1hi, xhi, b1); + + k += 16; + } + + // 8-element cleanup + while k + 8 <= in_dim { + let xv = _mm256_loadu_ps(x.as_ptr().add(k)); + let r0 = bf16x8_to_f32(_mm_loadu_si128(w0.add(k) as *const __m128i)); + let r1 = bf16x8_to_f32(_mm_loadu_si128(w1.add(k) as *const __m128i)); + a0 = _mm256_fmadd_ps(r0, xv, a0); + b0 = _mm256_fmadd_ps(r1, xv, b0); + k += 8; + } + + s0 += hsum_ps(_mm256_add_ps(a0, a1)); + s1 += hsum_ps(_mm256_add_ps(b0, b1)); + + // Scalar tail + while k < in_dim { + let wv0 = f32::from_bits((*w0.add(k) as u32) << 16); + let wv1 = f32::from_bits((*w1.add(k) as u32) << 16); + s0 += wv0 * x[k]; + s1 += wv1 * x[k]; + k += 1; + } + + y[o] = s0; + y[o + 1] = s1; + o += 2; + } + + // Handle remaining odd row + while o < out_dim { + let w_row = w_bf16.add(o * in_dim); + let mut sum = bias.map_or(0.0f32, |b| b[o]); + let mut k = 0usize; + + let mut acc0 = _mm256_setzero_ps(); + let mut acc1 = _mm256_setzero_ps(); + + while k + 16 <= in_dim { + let xlo = _mm256_loadu_ps(x.as_ptr().add(k)); + let xhi = _mm256_loadu_ps(x.as_ptr().add(k + 8)); + let wlo = bf16x8_to_f32(_mm_loadu_si128(w_row.add(k) as *const __m128i)); + let whi = bf16x8_to_f32(_mm_loadu_si128(w_row.add(k + 8) as *const __m128i)); + acc0 = _mm256_fmadd_ps(wlo, xlo, acc0); + acc1 = _mm256_fmadd_ps(whi, xhi, acc1); + k += 16; + } + + while k + 8 <= in_dim { + let xv = _mm256_loadu_ps(x.as_ptr().add(k)); + let wv = bf16x8_to_f32(_mm_loadu_si128(w_row.add(k) as *const __m128i)); + acc0 = _mm256_fmadd_ps(wv, xv, acc0); + k += 8; + } + + sum += hsum_ps(_mm256_add_ps(acc0, acc1)); + + while k < in_dim { + let w_val = f32::from_bits((*w_row.add(k) as u32) << 16); + sum += w_val * x[k]; + k += 1; + } + y[o] = sum; + o += 1; + } +} + +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2", enable = "fma")] +pub unsafe fn argmax_bf16_range( + x: &[f32], + w_bf16: *const u16, + in_dim: usize, + start: usize, + end: usize, +) -> (usize, f32) { + let mut best = start; + let mut best_val = -1e30f32; + let mut o = start; + + // Process 2 rows at a time + while o + 1 < end { + let w0 = w_bf16.add(o * in_dim); + let w1 = w_bf16.add((o + 1) * in_dim); + let mut a0 = _mm256_setzero_ps(); + let mut a1 = _mm256_setzero_ps(); + let mut b0 = _mm256_setzero_ps(); + let mut b1 = _mm256_setzero_ps(); + let mut k = 0usize; + + while k + 16 <= in_dim { + let xlo = _mm256_loadu_ps(x.as_ptr().add(k)); + let xhi = _mm256_loadu_ps(x.as_ptr().add(k + 8)); + + let r0lo = bf16x8_to_f32(_mm_loadu_si128(w0.add(k) as *const __m128i)); + let r0hi = bf16x8_to_f32(_mm_loadu_si128(w0.add(k + 8) as *const __m128i)); + a0 = _mm256_fmadd_ps(r0lo, xlo, a0); + a1 = _mm256_fmadd_ps(r0hi, xhi, a1); + + let r1lo = bf16x8_to_f32(_mm_loadu_si128(w1.add(k) as *const __m128i)); + let r1hi = bf16x8_to_f32(_mm_loadu_si128(w1.add(k + 8) as *const __m128i)); + b0 = _mm256_fmadd_ps(r1lo, xlo, b0); + b1 = _mm256_fmadd_ps(r1hi, xhi, b1); + + k += 16; + } + + let mut s0 = hsum_ps(_mm256_add_ps(a0, a1)); + let mut s1 = hsum_ps(_mm256_add_ps(b0, b1)); + + while k < in_dim { + let wv0 = f32::from_bits((*w0.add(k) as u32) << 16); + let wv1 = f32::from_bits((*w1.add(k) as u32) << 16); + s0 += wv0 * x[k]; + s1 += wv1 * x[k]; + k += 1; + } + + if s0 > best_val { + best_val = s0; + best = o; + } + if s1 > best_val { + best_val = s1; + best = o + 1; + } + o += 2; + } + + while o < end { + let w_row = w_bf16.add(o * in_dim); + let mut sum = 0.0f32; + let mut k = 0usize; + + let mut acc0 = _mm256_setzero_ps(); + let mut acc1 = _mm256_setzero_ps(); + while k + 16 <= in_dim { + let xlo = _mm256_loadu_ps(x.as_ptr().add(k)); + let xhi = _mm256_loadu_ps(x.as_ptr().add(k + 8)); + let wlo = bf16x8_to_f32(_mm_loadu_si128(w_row.add(k) as *const __m128i)); + let whi = bf16x8_to_f32(_mm_loadu_si128(w_row.add(k + 8) as *const __m128i)); + acc0 = _mm256_fmadd_ps(wlo, xlo, acc0); + acc1 = _mm256_fmadd_ps(whi, xhi, acc1); + k += 16; + } + sum += hsum_ps(_mm256_add_ps(acc0, acc1)); + + while k < in_dim { + let w_val = f32::from_bits((*w_row.add(k) as u32) << 16); + sum += w_val * x[k]; + k += 1; + } + if sum > best_val { + best_val = sum; + best = o; + } + o += 1; + } + + (best, best_val) +} + +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2")] +pub unsafe fn matvec_int8( + y: &mut [f32], + x_int8: *const i8, + x_scale: f32, + w_int8: *const i8, + w_scales: &[f32], + bias: Option<&[f32]>, + in_dim: usize, + out_dim: usize, +) { + let mut row = 0usize; + while row < out_dim { + let weights = w_int8.add(row * in_dim); + let acc = dot_i8_i8(x_int8, weights, in_dim); + let mut value = (acc as f32) * x_scale * w_scales[row]; + if let Some(bias_values) = bias { + value += bias_values[row]; + } + y[row] = value; + row += 1; + } +} + +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2")] +pub unsafe fn argmax_int8_range( + x_int8: *const i8, + x_scale: f32, + w_int8: *const i8, + w_scales: &[f32], + in_dim: usize, + start: usize, + end: usize, +) -> (usize, f32) { + let mut best = start; + let mut best_val = f32::NEG_INFINITY; + + for row in start..end { + let acc = dot_i8_i8(x_int8, w_int8.add(row * in_dim), in_dim); + let value = (acc as f32) * x_scale * w_scales[row]; + if value > best_val { + best = row; + best_val = value; + } + } + + (best, best_val) +} + +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2", enable = "fma")] +pub unsafe fn dot_f32(a: &[f32], b: &[f32], n: usize) -> f32 { + let mut i = 0usize; + let mut acc0 = _mm256_setzero_ps(); + let mut acc1 = _mm256_setzero_ps(); + let mut acc2 = _mm256_setzero_ps(); + let mut acc3 = _mm256_setzero_ps(); + + while i + 32 <= n { + acc0 = _mm256_fmadd_ps( + _mm256_loadu_ps(a.as_ptr().add(i)), + _mm256_loadu_ps(b.as_ptr().add(i)), + acc0, + ); + acc1 = _mm256_fmadd_ps( + _mm256_loadu_ps(a.as_ptr().add(i + 8)), + _mm256_loadu_ps(b.as_ptr().add(i + 8)), + acc1, + ); + acc2 = _mm256_fmadd_ps( + _mm256_loadu_ps(a.as_ptr().add(i + 16)), + _mm256_loadu_ps(b.as_ptr().add(i + 16)), + acc2, + ); + acc3 = _mm256_fmadd_ps( + _mm256_loadu_ps(a.as_ptr().add(i + 24)), + _mm256_loadu_ps(b.as_ptr().add(i + 24)), + acc3, + ); + i += 32; + } + + while i + 8 <= n { + acc0 = _mm256_fmadd_ps( + _mm256_loadu_ps(a.as_ptr().add(i)), + _mm256_loadu_ps(b.as_ptr().add(i)), + acc0, + ); + i += 8; + } + + let mut sum = hsum_ps(_mm256_add_ps( + _mm256_add_ps(acc0, acc1), + _mm256_add_ps(acc2, acc3), + )); + + while i < n { + sum += a[i] * b[i]; + i += 1; + } + sum +} + +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2")] +pub unsafe fn vec_scale_inplace(dst: &mut [f32], scale: f32, n: usize) { + let mut i = 0usize; + let s = _mm256_set1_ps(scale); + + while i + 32 <= n { + _mm256_storeu_ps( + dst.as_mut_ptr().add(i), + _mm256_mul_ps(_mm256_loadu_ps(dst.as_ptr().add(i)), s), + ); + _mm256_storeu_ps( + dst.as_mut_ptr().add(i + 8), + _mm256_mul_ps(_mm256_loadu_ps(dst.as_ptr().add(i + 8)), s), + ); + _mm256_storeu_ps( + dst.as_mut_ptr().add(i + 16), + _mm256_mul_ps(_mm256_loadu_ps(dst.as_ptr().add(i + 16)), s), + ); + _mm256_storeu_ps( + dst.as_mut_ptr().add(i + 24), + _mm256_mul_ps(_mm256_loadu_ps(dst.as_ptr().add(i + 24)), s), + ); + i += 32; + } + + while i + 8 <= n { + _mm256_storeu_ps( + dst.as_mut_ptr().add(i), + _mm256_mul_ps(_mm256_loadu_ps(dst.as_ptr().add(i)), s), + ); + i += 8; + } + + while i < n { + dst[i] *= scale; + i += 1; + } +} + +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2", enable = "fma")] +pub unsafe fn vec_axpy_inplace(dst: &mut [f32], src: &[f32], alpha: f32, n: usize) { + let mut i = 0usize; + let a = _mm256_set1_ps(alpha); + + while i + 32 <= n { + _mm256_storeu_ps( + dst.as_mut_ptr().add(i), + _mm256_fmadd_ps( + _mm256_loadu_ps(src.as_ptr().add(i)), + a, + _mm256_loadu_ps(dst.as_ptr().add(i)), + ), + ); + _mm256_storeu_ps( + dst.as_mut_ptr().add(i + 8), + _mm256_fmadd_ps( + _mm256_loadu_ps(src.as_ptr().add(i + 8)), + a, + _mm256_loadu_ps(dst.as_ptr().add(i + 8)), + ), + ); + _mm256_storeu_ps( + dst.as_mut_ptr().add(i + 16), + _mm256_fmadd_ps( + _mm256_loadu_ps(src.as_ptr().add(i + 16)), + a, + _mm256_loadu_ps(dst.as_ptr().add(i + 16)), + ), + ); + _mm256_storeu_ps( + dst.as_mut_ptr().add(i + 24), + _mm256_fmadd_ps( + _mm256_loadu_ps(src.as_ptr().add(i + 24)), + a, + _mm256_loadu_ps(dst.as_ptr().add(i + 24)), + ), + ); + i += 32; + } + + while i + 8 <= n { + _mm256_storeu_ps( + dst.as_mut_ptr().add(i), + _mm256_fmadd_ps( + _mm256_loadu_ps(src.as_ptr().add(i)), + a, + _mm256_loadu_ps(dst.as_ptr().add(i)), + ), + ); + i += 8; + } + + while i < n { + dst[i] += alpha * src[i]; + i += 1; + } +} + +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2", enable = "fma")] +pub unsafe fn vec_scale_add(dst: &mut [f32], src: &[f32], correction: f32, n: usize) { + let mut i = 0usize; + let c = _mm256_set1_ps(correction); + + while i + 32 <= n { + _mm256_storeu_ps( + dst.as_mut_ptr().add(i), + _mm256_fmadd_ps( + _mm256_loadu_ps(dst.as_ptr().add(i)), + c, + _mm256_loadu_ps(src.as_ptr().add(i)), + ), + ); + _mm256_storeu_ps( + dst.as_mut_ptr().add(i + 8), + _mm256_fmadd_ps( + _mm256_loadu_ps(dst.as_ptr().add(i + 8)), + c, + _mm256_loadu_ps(src.as_ptr().add(i + 8)), + ), + ); + _mm256_storeu_ps( + dst.as_mut_ptr().add(i + 16), + _mm256_fmadd_ps( + _mm256_loadu_ps(dst.as_ptr().add(i + 16)), + c, + _mm256_loadu_ps(src.as_ptr().add(i + 16)), + ), + ); + _mm256_storeu_ps( + dst.as_mut_ptr().add(i + 24), + _mm256_fmadd_ps( + _mm256_loadu_ps(dst.as_ptr().add(i + 24)), + c, + _mm256_loadu_ps(src.as_ptr().add(i + 24)), + ), + ); + i += 32; + } + + while i + 8 <= n { + _mm256_storeu_ps( + dst.as_mut_ptr().add(i), + _mm256_fmadd_ps( + _mm256_loadu_ps(dst.as_ptr().add(i)), + c, + _mm256_loadu_ps(src.as_ptr().add(i)), + ), + ); + i += 8; + } + + while i < n { + dst[i] = dst[i] * correction + src[i]; + i += 1; + } +} + +/// AVX2-accelerated RMS norm for a single row. +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2", enable = "fma")] +pub unsafe fn rms_norm_row(out: &mut [f32], x: &[f32], weight: &[f32], hidden: usize, eps: f32) { + let mut i = 0usize; + let mut acc0 = _mm256_setzero_ps(); + let mut acc1 = _mm256_setzero_ps(); + + while i + 16 <= hidden { + let x0 = _mm256_loadu_ps(x.as_ptr().add(i)); + let x1 = _mm256_loadu_ps(x.as_ptr().add(i + 8)); + acc0 = _mm256_fmadd_ps(x0, x0, acc0); + acc1 = _mm256_fmadd_ps(x1, x1, acc1); + i += 16; + } + while i + 8 <= hidden { + let xv = _mm256_loadu_ps(x.as_ptr().add(i)); + acc0 = _mm256_fmadd_ps(xv, xv, acc0); + i += 8; + } + + let mut sum_sq = hsum_ps(_mm256_add_ps(acc0, acc1)); + while i < hidden { + sum_sq += x[i] * x[i]; + i += 1; + } + + let rms_inv = 1.0 / (sum_sq / hidden as f32 + eps).sqrt(); + let rms_v = _mm256_set1_ps(rms_inv); + + i = 0; + while i + 8 <= hidden { + let xv = _mm256_loadu_ps(x.as_ptr().add(i)); + let wv = _mm256_loadu_ps(weight.as_ptr().add(i)); + _mm256_storeu_ps( + out.as_mut_ptr().add(i), + _mm256_mul_ps(_mm256_mul_ps(xv, rms_v), wv), + ); + i += 8; + } + while i < hidden { + out[i] = x[i] * rms_inv * weight[i]; + i += 1; + } +} + +/// AVX2-accelerated layer norm for a single row. +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2", enable = "fma")] +pub unsafe fn layer_norm_row( + out: &mut [f32], + x: &[f32], + weight: &[f32], + bias: &[f32], + hidden: usize, + eps: f32, +) { + // Pass 1: compute mean + let mut i = 0usize; + let mut sum0 = _mm256_setzero_ps(); + let mut sum1 = _mm256_setzero_ps(); + while i + 16 <= hidden { + sum0 = _mm256_add_ps(sum0, _mm256_loadu_ps(x.as_ptr().add(i))); + sum1 = _mm256_add_ps(sum1, _mm256_loadu_ps(x.as_ptr().add(i + 8))); + i += 16; + } + while i + 8 <= hidden { + sum0 = _mm256_add_ps(sum0, _mm256_loadu_ps(x.as_ptr().add(i))); + i += 8; + } + let mut mean = hsum_ps(_mm256_add_ps(sum0, sum1)); + while i < hidden { + mean += x[i]; + i += 1; + } + mean /= hidden as f32; + + // Pass 2: compute variance + let mean_v = _mm256_set1_ps(mean); + i = 0; + let mut var0 = _mm256_setzero_ps(); + let mut var1 = _mm256_setzero_ps(); + while i + 16 <= hidden { + let d0 = _mm256_sub_ps(_mm256_loadu_ps(x.as_ptr().add(i)), mean_v); + let d1 = _mm256_sub_ps(_mm256_loadu_ps(x.as_ptr().add(i + 8)), mean_v); + var0 = _mm256_fmadd_ps(d0, d0, var0); + var1 = _mm256_fmadd_ps(d1, d1, var1); + i += 16; + } + while i + 8 <= hidden { + let d = _mm256_sub_ps(_mm256_loadu_ps(x.as_ptr().add(i)), mean_v); + var0 = _mm256_fmadd_ps(d, d, var0); + i += 8; + } + let mut var = hsum_ps(_mm256_add_ps(var0, var1)); + while i < hidden { + let d = x[i] - mean; + var += d * d; + i += 1; + } + + let inv_std = 1.0 / (var / hidden as f32 + eps).sqrt(); + let inv_v = _mm256_set1_ps(inv_std); + + // Pass 3: normalize + i = 0; + while i + 8 <= hidden { + let xv = _mm256_sub_ps(_mm256_loadu_ps(x.as_ptr().add(i)), mean_v); + let wv = _mm256_loadu_ps(weight.as_ptr().add(i)); + let bv = _mm256_loadu_ps(bias.as_ptr().add(i)); + _mm256_storeu_ps( + out.as_mut_ptr().add(i), + _mm256_fmadd_ps(_mm256_mul_ps(xv, inv_v), wv, bv), + ); + i += 8; + } + while i < hidden { + out[i] = (x[i] - mean) * inv_std * weight[i] + bias[i]; + i += 1; + } +} + +/// Fast exp approximation using AVX2+FMA (~1e-4 relative error for |x| < 88). +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2", enable = "fma")] +#[inline] +unsafe fn fast_exp_avx(x: __m256) -> __m256 { + let log2e = _mm256_set1_ps(1.442695041); + let ln2 = _mm256_set1_ps(0.6931471806); + + let val = _mm256_mul_ps(x, log2e); + let val = _mm256_min_ps(val, _mm256_set1_ps(126.0)); + let val = _mm256_max_ps(val, _mm256_set1_ps(-126.0)); + + let ipart = _mm256_cvtps_epi32(val); + let fpart = _mm256_sub_ps(val, _mm256_cvtepi32_ps(ipart)); + + let exp_i = _mm256_castsi256_ps(_mm256_slli_epi32( + _mm256_add_epi32(ipart, _mm256_set1_epi32(127)), + 23, + )); + + let f = _mm256_mul_ps(fpart, ln2); + let c2 = _mm256_set1_ps(0.5); + let c3 = _mm256_set1_ps(1.0 / 6.0); + let c4 = _mm256_set1_ps(1.0 / 24.0); + let c5 = _mm256_set1_ps(1.0 / 120.0); + + let mut p = _mm256_fmadd_ps(c5, f, c4); + p = _mm256_fmadd_ps(p, f, c3); + p = _mm256_fmadd_ps(p, f, c2); + p = _mm256_fmadd_ps(p, f, _mm256_set1_ps(1.0)); + p = _mm256_fmadd_ps(p, f, _mm256_set1_ps(1.0)); + + _mm256_mul_ps(exp_i, p) +} + +/// AVX2-accelerated exp() in-place using fast polynomial approximation. +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2", enable = "fma")] +pub unsafe fn exp_inplace(x: &mut [f32]) { + let n = x.len(); + let mut i = 0usize; + while i + 8 <= n { + let v = _mm256_loadu_ps(x.as_ptr().add(i)); + _mm256_storeu_ps(x.as_mut_ptr().add(i), fast_exp_avx(v)); + i += 8; + } + while i < n { + x[i] = x[i].exp(); + i += 1; + } +} + +/// AVX2-accelerated GELU (tanh approximation). +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2", enable = "fma")] +pub unsafe fn gelu_inplace(x: &mut [f32], n: usize) { + let half = _mm256_set1_ps(0.5); + let one = _mm256_set1_ps(1.0); + let two = _mm256_set1_ps(2.0); + let coeff = _mm256_set1_ps(0.7978845608028654); + let c3 = _mm256_set1_ps(0.044715); + let mut i = 0usize; + + while i + 8 <= n { + let v = _mm256_loadu_ps(x.as_ptr().add(i)); + let v2 = _mm256_mul_ps(v, v); + let v3 = _mm256_mul_ps(v2, v); + let inner = _mm256_mul_ps(coeff, _mm256_fmadd_ps(c3, v3, v)); + let exp2x = fast_exp_avx(_mm256_mul_ps(two, inner)); + let tanh_v = _mm256_sub_ps(one, _mm256_div_ps(two, _mm256_add_ps(exp2x, one))); + let result = _mm256_mul_ps(half, _mm256_mul_ps(v, _mm256_add_ps(one, tanh_v))); + _mm256_storeu_ps(x.as_mut_ptr().add(i), result); + i += 8; + } + + while i < n { + let val = x[i]; + let x3 = val * val * val; + let inner = 0.7978845608028654f32 * (val + 0.044715 * x3); + x[i] = 0.5 * val * (1.0 + inner.tanh()); + i += 1; + } +} + +/// AVX2-accelerated SwiGLU with interleaved gate/up. +#[cfg(target_arch = "x86_64")] +#[target_feature(enable = "avx2", enable = "fma")] +pub unsafe fn swiglu_interleaved(out: &mut [f32], gate_up: &[f32], n: usize) { + let one = _mm256_set1_ps(1.0); + let mut j = 0usize; + + while j + 8 <= n { + // Load 16 floats: [g0,u0,g1,u1,g2,u2,g3,u3] x2 + let lo = _mm256_loadu_ps(gate_up.as_ptr().add(2 * j)); + let hi = _mm256_loadu_ps(gate_up.as_ptr().add(2 * j + 8)); + + // Deinterleave using shuffle + permute + let shuf_lo = _mm256_shuffle_ps(lo, hi, 0b10_00_10_00); // g0,g1,g4,g5,g2,g3,g6,g7 + let shuf_hi = _mm256_shuffle_ps(lo, hi, 0b11_01_11_01); // u0,u1,u4,u5,u2,u3,u6,u7 + let gates = _mm256_permutevar8x32_ps(shuf_lo, _mm256_setr_epi32(0, 1, 4, 5, 2, 3, 6, 7)); + let ups = _mm256_permutevar8x32_ps(shuf_hi, _mm256_setr_epi32(0, 1, 4, 5, 2, 3, 6, 7)); + + let neg_g = _mm256_sub_ps(_mm256_setzero_ps(), gates); + let exp_ng = fast_exp_avx(neg_g); + let denom = _mm256_add_ps(one, exp_ng); + let silu_g = _mm256_div_ps(gates, denom); + + _mm256_storeu_ps(out.as_mut_ptr().add(j), _mm256_mul_ps(silu_g, ups)); + j += 8; + } + + while j < n { + let g = gate_up[2 * j]; + let u = gate_up[2 * j + 1]; + let g_silu = g / (1.0 + (-g).exp()); + out[j] = g_silu * u; + j += 1; + } +} + +#[cfg(test)] +mod tests { + #[cfg(target_arch = "x86_64")] + use super::{argmax_int8_range, matvec_int8}; + + #[cfg(target_arch = "x86_64")] + fn scalar_matvec_int8( + x_int8: &[i8], + x_scale: f32, + w_int8: &[i8], + w_scales: &[f32], + bias: Option<&[f32]>, + in_dim: usize, + out_dim: usize, + ) -> Vec { + let mut out = vec![0.0f32; out_dim]; + for row in 0..out_dim { + let mut acc = 0i32; + for col in 0..in_dim { + acc += (x_int8[col] as i32) * (w_int8[row * in_dim + col] as i32); + } + let mut value = (acc as f32) * x_scale * w_scales[row]; + if let Some(bias_values) = bias { + value += bias_values[row]; + } + out[row] = value; + } + out + } + + #[cfg(target_arch = "x86_64")] + #[test] + fn avx_int8_matvec_matches_scalar() { + if !std::is_x86_feature_detected!("avx2") { + return; + } + + let in_dim = 96usize; + let out_dim = 13usize; + let x_scale = 0.03125f32; + let x_int8: Vec = (0..in_dim).map(|i| ((i as i32 % 15) - 7) as i8).collect(); + let w_int8: Vec = (0..(out_dim * in_dim)) + .map(|i| (((i as i32 * 7) % 17) - 8) as i8) + .collect(); + let w_scales: Vec = (0..out_dim).map(|i| 0.01 + i as f32 * 0.003).collect(); + let bias: Vec = (0..out_dim).map(|i| -0.2 + i as f32 * 0.05).collect(); + + let expected = scalar_matvec_int8( + &x_int8, + x_scale, + &w_int8, + &w_scales, + Some(&bias), + in_dim, + out_dim, + ); + let mut actual = vec![0.0f32; out_dim]; + + unsafe { + matvec_int8( + &mut actual, + x_int8.as_ptr(), + x_scale, + w_int8.as_ptr(), + &w_scales, + Some(&bias), + in_dim, + out_dim, + ); + } + + for (lhs, rhs) in actual.iter().zip(expected.iter()) { + assert!((lhs - rhs).abs() < 1e-4, "lhs={lhs} rhs={rhs}"); + } + } + + #[cfg(target_arch = "x86_64")] + #[test] + fn avx_int8_argmax_matches_scalar() { + if !std::is_x86_feature_detected!("avx2") { + return; + } + + let in_dim = 80usize; + let out_dim = 19usize; + let x_scale = 0.0625f32; + let x_int8: Vec = (0..in_dim) + .map(|i| (((i as i32 * 5) % 23) - 11) as i8) + .collect(); + let w_int8: Vec = (0..(out_dim * in_dim)) + .map(|i| (((i as i32 * 3) % 29) - 14) as i8) + .collect(); + let w_scales: Vec = (0..out_dim).map(|i| 0.02 + i as f32 * 0.002).collect(); + + let expected = + scalar_matvec_int8(&x_int8, x_scale, &w_int8, &w_scales, None, in_dim, out_dim); + let expected_idx = expected + .iter() + .enumerate() + .max_by(|a, b| a.1.partial_cmp(b.1).unwrap()) + .map(|(idx, _)| idx) + .unwrap(); + + let (actual_idx, actual_val) = unsafe { + argmax_int8_range( + x_int8.as_ptr(), + x_scale, + w_int8.as_ptr(), + &w_scales, + in_dim, + 0, + out_dim, + ) + }; + + assert_eq!(actual_idx, expected_idx); + assert!((actual_val - expected[expected_idx]).abs() < 1e-4); + } +} diff --git a/vendor/qwenasr/crates/qwen-asr/src/kernels/generic.rs b/vendor/qwenasr/crates/qwen-asr/src/kernels/generic.rs new file mode 100644 index 0000000..d17751d --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/src/kernels/generic.rs @@ -0,0 +1,78 @@ +//! Generic (portable) implementations of hot kernels. + +#[inline] +pub fn bf16_to_f32(bf16: u16) -> f32 { + f32::from_bits((bf16 as u32) << 16) +} + +/// # Safety +/// w_bf16 must point to at least out_dim * in_dim valid bf16 values. +pub unsafe fn bf16_matvec_fused( + y: &mut [f32], + x: &[f32], + w_bf16: *const u16, + bias: Option<&[f32]>, + in_dim: usize, + out_dim: usize, +) { + for o in 0..out_dim { + let w_row = unsafe { std::slice::from_raw_parts(w_bf16.add(o * in_dim), in_dim) }; + let mut sum = bias.map_or(0.0f32, |b| b[o]); + for k in 0..in_dim { + sum += bf16_to_f32(w_row[k]) * x[k]; + } + y[o] = sum; + } +} + +/// # Safety +/// w_bf16 must point to at least end * in_dim valid bf16 values. +pub unsafe fn argmax_bf16_range( + x: &[f32], + w_bf16: *const u16, + in_dim: usize, + start: usize, + end: usize, +) -> (usize, f32) { + let mut best = start; + let mut best_val = -1e30f32; + + for o in start..end { + let w_row = unsafe { std::slice::from_raw_parts(w_bf16.add(o * in_dim), in_dim) }; + let mut sum = 0.0f32; + for k in 0..in_dim { + sum += bf16_to_f32(w_row[k]) * x[k]; + } + if sum > best_val { + best_val = sum; + best = o; + } + } + (best, best_val) +} + +pub fn dot_f32(a: &[f32], b: &[f32], n: usize) -> f32 { + let mut sum = 0.0f32; + for i in 0..n { + sum += a[i] * b[i]; + } + sum +} + +pub fn vec_scale_inplace(dst: &mut [f32], scale: f32, n: usize) { + for val in dst.iter_mut().take(n) { + *val *= scale; + } +} + +pub fn vec_axpy_inplace(dst: &mut [f32], src: &[f32], alpha: f32, n: usize) { + for i in 0..n { + dst[i] += alpha * src[i]; + } +} + +pub fn vec_scale_add(dst: &mut [f32], src: &[f32], correction: f32, n: usize) { + for i in 0..n { + dst[i] = dst[i] * correction + src[i]; + } +} diff --git a/vendor/qwenasr/crates/qwen-asr/src/kernels/mod.rs b/vendor/qwenasr/crates/qwen-asr/src/kernels/mod.rs new file mode 100644 index 0000000..69144b9 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/src/kernels/mod.rs @@ -0,0 +1,2878 @@ +//! BLAS/vDSP bindings, thread pool, and SIMD kernel dispatch. + +#[cfg(target_arch = "x86_64")] +pub mod avx; +pub mod generic; +#[cfg(target_arch = "aarch64")] +pub mod neon; + +use std::thread; + +// BLAS extern bindings +#[cfg(all(feature = "blas", target_vendor = "apple"))] +#[link(name = "Accelerate", kind = "framework")] +extern "C" { + fn cblas_sgemm( + order: i32, + transa: i32, + transb: i32, + m: i32, + n: i32, + k: i32, + alpha: f32, + a: *const f32, + lda: i32, + b: *const f32, + ldb: i32, + beta: f32, + c: *mut f32, + ldc: i32, + ); +} + +// vDSP/vForce bindings (macOS Accelerate) +#[cfg(all(feature = "vdsp", target_vendor = "apple"))] +#[link(name = "Accelerate", kind = "framework")] +extern "C" { + fn vDSP_dotpr( + a: *const f32, + a_stride: i32, + b: *const f32, + b_stride: i32, + result: *mut f32, + n: u64, + ); + fn vDSP_vsmul( + a: *const f32, + a_stride: i32, + scalar: *const f32, + c: *mut f32, + c_stride: i32, + n: u64, + ); + fn vDSP_vsma( + a: *const f32, + a_stride: i32, + scalar: *const f32, + b: *const f32, + b_stride: i32, + c: *mut f32, + c_stride: i32, + n: u64, + ); + fn vvexpf(dst: *mut f32, src: *const f32, n: *const i32); +} + +#[cfg(all(feature = "blas", not(target_vendor = "apple")))] +extern "C" { + fn cblas_sgemm( + order: i32, + transa: i32, + transb: i32, + m: i32, + n: i32, + k: i32, + alpha: f32, + a: *const f32, + lda: i32, + b: *const f32, + ldb: i32, + beta: f32, + c: *mut f32, + ldc: i32, + ); +} + +#[cfg(feature = "blas")] +const CBLAS_ROW_MAJOR: i32 = 101; +#[cfg(feature = "blas")] +const CBLAS_NO_TRANS: i32 = 111; +#[cfg(feature = "blas")] +const CBLAS_TRANS: i32 = 112; + +// Verbose flag +static VERBOSE: AtomicI32 = AtomicI32::new(0); + +// ======================================================================== +// Profiling support +// ======================================================================== + +use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, AtomicUsize, Ordering}; +use std::time::Instant; + +static PROFILE_ENABLED: AtomicBool = AtomicBool::new(false); + +pub fn set_profile(enabled: bool) { + PROFILE_ENABLED.store(enabled, Ordering::Relaxed); +} + +pub fn is_profiling() -> bool { + PROFILE_ENABLED.load(Ordering::Relaxed) +} + +macro_rules! define_profile_counters { + ($($name:ident),+) => { + pub struct ProfileCounters { + $(pub $name: (AtomicU64, AtomicU64),)+ // (total_ns, call_count) + } + + impl ProfileCounters { + pub const fn new() -> Self { + ProfileCounters { + $($name: (AtomicU64::new(0), AtomicU64::new(0)),)+ + } + } + } + + impl Default for ProfileCounters { + fn default() -> Self { + Self::new() + } + } + + impl ProfileCounters { + pub fn reset(&self) { + $( + self.$name.0.store(0, Ordering::Relaxed); + self.$name.1.store(0, Ordering::Relaxed); + )+ + } + + pub fn report(&self) { + $( + let ns = self.$name.0.load(Ordering::Relaxed); + let calls = self.$name.1.load(Ordering::Relaxed); + if calls > 0 { + let ms = ns as f64 / 1_000_000.0; + let avg = ms / calls as f64; + eprintln!("[profile] {}: {:.1}ms ({} calls, {:.2}ms avg)", + stringify!($name), ms, calls, avg); + } + )+ + } + } + } +} + +define_profile_counters!( + rms_norm, + layer_norm, + gelu, + swiglu, + bf16_matvec, + bf16_to_f32_conv, + attention_bidir, + attention_causal, + sgemm, + conv2d_op, + rope, + add_inplace_op +); + +pub static PROF: ProfileCounters = ProfileCounters::new(); + +pub struct ProfileGuard { + start: Instant, + counter: &'static (AtomicU64, AtomicU64), +} + +impl ProfileGuard { + #[inline] + pub fn new(counter: &'static (AtomicU64, AtomicU64)) -> Option { + if PROFILE_ENABLED.load(Ordering::Relaxed) { + Some(ProfileGuard { + start: Instant::now(), + counter, + }) + } else { + None + } + } +} + +impl Drop for ProfileGuard { + #[inline] + fn drop(&mut self) { + let ns = self.start.elapsed().as_nanos() as u64; + self.counter.0.fetch_add(ns, Ordering::Relaxed); + self.counter.1.fetch_add(1, Ordering::Relaxed); + } +} + +// Convenience: unused ProfileTimer alias removed + +pub fn profile_reset() { + PROF.reset(); +} +pub fn profile_report() { + PROF.report(); +} + +pub fn set_verbose(v: i32) { + VERBOSE.store(v, Ordering::Relaxed); +} + +pub fn verbose() -> i32 { + VERBOSE.load(Ordering::Relaxed) +} + +// ======================================================================== +// Thread Pool (persistent, mutex+condvar, matches C approach) +// ======================================================================== + +use std::sync::{Arc, Condvar, Mutex, OnceLock}; + +const MAX_THREADS: usize = 16; + +struct ThreadPool { + // Mutex+condvar only used as slow-path fallback when spin-wait misses + state: Mutex, // shutdown flag only + work_cv: Condvar, + // All dispatch data is lock-free via atomics + gen_atomic: AtomicU64, + done_atomic: AtomicUsize, + fn_ptr_atomic: AtomicUsize, + fn_call_atomic: AtomicUsize, + n_threads_atomic: AtomicUsize, +} + +static THREAD_POOL: OnceLock> = OnceLock::new(); + +fn get_pool() -> &'static Arc { + THREAD_POOL.get_or_init(|| { + Arc::new(ThreadPool { + state: Mutex::new(false), + work_cv: Condvar::new(), + gen_atomic: AtomicU64::new(0), + done_atomic: AtomicUsize::new(0), + fn_ptr_atomic: AtomicUsize::new(0), + fn_call_atomic: AtomicUsize::new(0), + n_threads_atomic: AtomicUsize::new(1), + }) + }) +} + +fn pool_worker(pool: Arc, tid: usize) { + let mut last_gen: u64 = 0; + loop { + // Fast path: spin briefly on atomic generation counter + let mut found = false; + for _ in 0..512 { + let gen = pool.gen_atomic.load(Ordering::Acquire); + if gen != last_gen { + last_gen = gen; + found = true; + break; + } + core::hint::spin_loop(); + } + + if !found { + // Slow path: condvar wait (mutex only protects shutdown flag) + let mut shutdown = match pool.state.lock() { + Ok(s) => s, + Err(p) => p.into_inner(), + }; + while !*shutdown && pool.gen_atomic.load(Ordering::Relaxed) == last_gen { + shutdown = match pool.work_cv.wait(shutdown) { + Ok(s) => s, + Err(p) => p.into_inner(), + }; + } + if *shutdown { + return; + } + last_gen = pool.gen_atomic.load(Ordering::Acquire); + } + + // Read dispatch data from atomics (ordered by gen_atomic Acquire) + let fn_ptr = pool.fn_ptr_atomic.load(Ordering::Relaxed) as *const (); + let fn_call: fn(*const (), usize, usize) = + unsafe { core::mem::transmute(pool.fn_call_atomic.load(Ordering::Relaxed)) }; + let n_threads = pool.n_threads_atomic.load(Ordering::Relaxed); + + fn_call(fn_ptr, tid, n_threads); + pool.done_atomic.fetch_add(1, Ordering::Release); + } +} + +static SPAWNED_THREADS: AtomicUsize = AtomicUsize::new(0); + +fn ensure_workers(pool: &Arc, n_threads: usize) { + let spawned = SPAWNED_THREADS.load(Ordering::Relaxed); + if spawned >= n_threads - 1 { + return; + } + let start = spawned + 1; + for tid in start..n_threads { + let p = pool.clone(); + thread::Builder::new() + .name(format!("qwen-worker-{}", tid)) + .spawn(move || pool_worker(p, tid)) + .expect("failed to spawn worker thread"); + } + SPAWNED_THREADS.store(n_threads - 1, Ordering::Relaxed); +} + +static THREAD_POOL_THREADS: AtomicUsize = AtomicUsize::new(1); + +pub fn set_threads(n: usize) { + let n = n.clamp(1, MAX_THREADS); + THREAD_POOL_THREADS.store(n, Ordering::Relaxed); + if n > 1 { + let pool = get_pool(); + ensure_workers(pool, n); + } + if verbose() >= 2 { + eprintln!("Thread pool: {} threads", n); + } +} + +pub fn get_num_threads() -> usize { + THREAD_POOL_THREADS.load(Ordering::Relaxed) +} + +pub fn get_num_cpus() -> usize { + // On Apple Silicon, prefer performance cores only (E-cores bottleneck parallel_for). + #[cfg(target_os = "macos")] + { + let perf_cores = get_perf_core_count(); + if perf_cores > 0 { + return perf_cores; + } + } + std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(1) +} + +#[cfg(target_os = "macos")] +fn get_perf_core_count() -> usize { + extern "C" { + fn sysctlbyname( + name: *const i8, + oldp: *mut libc::c_void, + oldlenp: *mut usize, + newp: *const libc::c_void, + newlen: usize, + ) -> i32; + } + let name = c"hw.perflevel0.physicalcpu"; + let mut val: i32 = 0; + let mut len = std::mem::size_of::(); + let ret = unsafe { + sysctlbyname( + name.as_ptr(), + &mut val as *mut i32 as *mut libc::c_void, + &mut len, + std::ptr::null(), + 0, + ) + }; + if ret == 0 && val > 0 { + val as usize + } else { + 0 + } +} + +/// Run a closure in parallel using the persistent thread pool. +/// The closure takes (thread_id, n_threads). +fn parallel_for(f: F) { + let n_threads = get_num_threads(); + if n_threads <= 1 { + f(0, 1); + return; + } + + let pool = get_pool(); + + // Trampoline: cast *const () back to &F and call it + fn trampoline(ptr: *const (), tid: usize, nt: usize) { + let f = unsafe { &*(ptr as *const F) }; + f(tid, nt); + } + + // Publish dispatch data via atomics (Relaxed OK: gen_atomic Release provides ordering) + pool.done_atomic.store(0, Ordering::Relaxed); + pool.fn_ptr_atomic + .store(&f as *const F as *const () as usize, Ordering::Relaxed); + pool.fn_call_atomic + .store(trampoline:: as *const () as usize, Ordering::Relaxed); + pool.n_threads_atomic.store(n_threads, Ordering::Relaxed); + // Release: ensures all stores above are visible to workers that Acquire gen_atomic + pool.gen_atomic.fetch_add(1, Ordering::Release); + + // Wake workers that fell through to condvar wait + // Lock scope is minimal: just notify, no data to write + { + let _guard = match pool.state.lock() { + Ok(s) => s, + Err(p) => p.into_inner(), + }; + pool.work_cv.notify_all(); + } + + // Main thread does tid=0 + f(0, n_threads); + + // Wait for workers: spin on atomic done counter + let expected = n_threads - 1; + loop { + if pool.done_atomic.load(Ordering::Acquire) >= expected { + break; + } + core::hint::spin_loop(); + } +} + +// ======================================================================== +// Dispatch helpers - pick NEON/AVX/generic at compile time +// ======================================================================== + +#[inline] +pub fn bf16_to_f32(bf16: u16) -> f32 { + f32::from_bits((bf16 as u32) << 16) +} + +pub fn bf16_to_f32_buf(dst: &mut [f32], src: &[u16]) { + #[cfg(target_arch = "aarch64")] + { + unsafe { + neon::bf16_to_f32_buf(dst, src); + } + } + + #[cfg(target_arch = "x86_64")] + { + unsafe { + avx::bf16_to_f32_buf(dst, src); + } + } + + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + for i in 0..src.len() { + dst[i] = bf16_to_f32(src[i]); + } +} + +fn bf16_matvec_fused( + y: &mut [f32], + x: &[f32], + w_bf16: *const u16, + bias: Option<&[f32]>, + in_dim: usize, + out_dim: usize, +) { + #[cfg(target_arch = "aarch64")] + { + unsafe { + neon::bf16_matvec_fused(y, x, w_bf16, bias, in_dim, out_dim); + } + } + + #[cfg(target_arch = "x86_64")] + { + unsafe { + avx::bf16_matvec_fused(y, x, w_bf16, bias, in_dim, out_dim); + } + } + + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + generic::bf16_matvec_fused(y, x, w_bf16, bias, in_dim, out_dim); +} + +fn argmax_bf16_range( + x: &[f32], + w_bf16: *const u16, + in_dim: usize, + start: usize, + end: usize, +) -> (usize, f32) { + #[cfg(target_arch = "aarch64")] + { + unsafe { neon::argmax_bf16_range(x, w_bf16, in_dim, start, end) } + } + + #[cfg(target_arch = "x86_64")] + { + unsafe { avx::argmax_bf16_range(x, w_bf16, in_dim, start, end) } + } + + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + generic::argmax_bf16_range(x, w_bf16, in_dim, start, end) +} + +#[inline] +pub fn dot_f32(a: &[f32], b: &[f32], n: usize) -> f32 { + #[cfg(all(feature = "vdsp", target_vendor = "apple"))] + { + let mut result = 0.0f32; + unsafe { + vDSP_dotpr(a.as_ptr(), 1, b.as_ptr(), 1, &mut result, n as u64); + } + result + } + + #[cfg(all( + target_arch = "aarch64", + not(all(feature = "vdsp", target_vendor = "apple")) + ))] + { + unsafe { neon::dot_f32(a, b, n) } + } + + #[cfg(all( + target_arch = "x86_64", + not(all(feature = "vdsp", target_vendor = "apple")) + ))] + { + unsafe { avx::dot_f32(a, b, n) } + } + + #[cfg(not(any( + target_arch = "aarch64", + target_arch = "x86_64", + all(feature = "vdsp", target_vendor = "apple") + )))] + generic::dot_f32(a, b, n) +} + +#[inline] +pub fn vec_scale_inplace(dst: &mut [f32], scale: f32, n: usize) { + #[cfg(all(feature = "vdsp", target_vendor = "apple"))] + { + unsafe { + vDSP_vsmul(dst.as_ptr(), 1, &scale, dst.as_mut_ptr(), 1, n as u64); + } + } + + #[cfg(all( + target_arch = "aarch64", + not(all(feature = "vdsp", target_vendor = "apple")) + ))] + { + unsafe { + neon::vec_scale_inplace(dst, scale, n); + } + } + + #[cfg(all( + target_arch = "x86_64", + not(all(feature = "vdsp", target_vendor = "apple")) + ))] + { + unsafe { + avx::vec_scale_inplace(dst, scale, n); + } + } + + #[cfg(not(any( + target_arch = "aarch64", + target_arch = "x86_64", + all(feature = "vdsp", target_vendor = "apple") + )))] + generic::vec_scale_inplace(dst, scale, n); +} + +#[inline] +pub fn vec_axpy_inplace(dst: &mut [f32], src: &[f32], alpha: f32, n: usize) { + #[cfg(all(feature = "vdsp", target_vendor = "apple"))] + { + unsafe { + vDSP_vsma( + src.as_ptr(), + 1, + &alpha, + dst.as_ptr(), + 1, + dst.as_mut_ptr(), + 1, + n as u64, + ); + } + } + + #[cfg(all( + target_arch = "aarch64", + not(all(feature = "vdsp", target_vendor = "apple")) + ))] + { + unsafe { + neon::vec_axpy_inplace(dst, src, alpha, n); + } + } + + #[cfg(all( + target_arch = "x86_64", + not(all(feature = "vdsp", target_vendor = "apple")) + ))] + { + unsafe { + avx::vec_axpy_inplace(dst, src, alpha, n); + } + } + + #[cfg(not(any( + target_arch = "aarch64", + target_arch = "x86_64", + all(feature = "vdsp", target_vendor = "apple") + )))] + generic::vec_axpy_inplace(dst, src, alpha, n); +} + +#[inline] +pub fn vec_scale_add(dst: &mut [f32], src: &[f32], correction: f32, n: usize) { + #[cfg(target_arch = "aarch64")] + { + unsafe { + neon::vec_scale_add(dst, src, correction, n); + } + } + + #[cfg(target_arch = "x86_64")] + { + unsafe { + avx::vec_scale_add(dst, src, correction, n); + } + } + + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + generic::vec_scale_add(dst, src, correction, n); +} + +// ======================================================================== +// Basic Operations +// ======================================================================== + +pub fn add_inplace(a: &mut [f32], b: &[f32], n: usize) { + let _pg = ProfileGuard::new(&PROF.add_inplace_op); + for i in 0..n { + a[i] += b[i]; + } +} + +// ======================================================================== +// Matrix Operations +// ======================================================================== + +/// C = A @ B (no transpose): A[M,K], B[K,N], C[M,N] +pub fn matmul_nn(c: &mut [f32], a: &[f32], b: &[f32], m: usize, k: usize, n: usize) { + #[cfg(feature = "blas")] + unsafe { + cblas_sgemm( + CBLAS_ROW_MAJOR, + CBLAS_NO_TRANS, + CBLAS_NO_TRANS, + m as i32, + n as i32, + k as i32, + 1.0, + a.as_ptr(), + k as i32, + b.as_ptr(), + n as i32, + 0.0, + c.as_mut_ptr(), + n as i32, + ); + } + + #[cfg(not(feature = "blas"))] + { + for mi in 0..m { + for ni in 0..n { + let mut sum = 0.0f32; + for ki in 0..k { + sum += a[mi * k + ki] * b[ki * n + ni]; + } + c[mi * n + ni] = sum; + } + } + } +} + +/// C = A @ B^T: A[M,K], B[N,K], C[M,N] +pub fn matmul_t(c: &mut [f32], a: &[f32], b: &[f32], m: usize, k: usize, n: usize) { + #[cfg(feature = "blas")] + unsafe { + cblas_sgemm( + CBLAS_ROW_MAJOR, + CBLAS_NO_TRANS, + CBLAS_TRANS, + m as i32, + n as i32, + k as i32, + 1.0, + a.as_ptr(), + k as i32, + b.as_ptr(), + k as i32, + 0.0, + c.as_mut_ptr(), + n as i32, + ); + } + + #[cfg(not(feature = "blas"))] + { + for mi in 0..m { + for ni in 0..n { + let mut sum = 0.0f32; + for ki in 0..k { + sum += a[mi * k + ki] * b[ni * k + ki]; + } + c[mi * n + ni] = sum; + } + } + } +} + +/// y = x @ W^T + b: x[seq,in], W[out,in], b[out], y[seq,out] +pub fn linear( + y: &mut [f32], + x: &[f32], + w: &[f32], + b: Option<&[f32]>, + seq_len: usize, + in_dim: usize, + out_dim: usize, +) { + let _pg = ProfileGuard::new(&PROF.sgemm); + #[cfg(feature = "blas")] + unsafe { + cblas_sgemm( + CBLAS_ROW_MAJOR, + CBLAS_NO_TRANS, + CBLAS_TRANS, + seq_len as i32, + out_dim as i32, + in_dim as i32, + 1.0, + x.as_ptr(), + in_dim as i32, + w.as_ptr(), + in_dim as i32, + 0.0, + y.as_mut_ptr(), + out_dim as i32, + ); + if let Some(b) = b { + for s in 0..seq_len { + for o in 0..out_dim { + y[s * out_dim + o] += b[o]; + } + } + } + } + + #[cfg(not(feature = "blas"))] + { + for s in 0..seq_len { + let x_row = &x[s * in_dim..(s + 1) * in_dim]; + for o in 0..out_dim { + let w_row = &w[o * in_dim..(o + 1) * in_dim]; + let mut sum = b.map_or(0.0, |b| b[o]); + for i in 0..in_dim { + sum += x_row[i] * w_row[i]; + } + y[s * out_dim + o] = sum; + } + } + } +} + +pub fn linear_nobias( + y: &mut [f32], + x: &[f32], + w: &[f32], + seq_len: usize, + in_dim: usize, + out_dim: usize, +) { + linear(y, x, w, None, seq_len, in_dim, out_dim); +} + +/// y += bias + x @ w.T (accumulate into existing y, fusing residual add) +pub fn linear_accumulate( + y: &mut [f32], + x: &[f32], + w: &[f32], + b: Option<&[f32]>, + seq_len: usize, + in_dim: usize, + out_dim: usize, +) { + let _pg = ProfileGuard::new(&PROF.sgemm); + #[cfg(feature = "blas")] + unsafe { + // Add bias to y first (y already has residual) + if let Some(b) = b { + for s in 0..seq_len { + let row = &mut y[s * out_dim..(s + 1) * out_dim]; + for o in 0..out_dim { + row[o] += b[o]; + } + } + } + // y = 1.0 * x @ w.T + 1.0 * y (accumulate matmul into y) + cblas_sgemm( + CBLAS_ROW_MAJOR, + CBLAS_NO_TRANS, + CBLAS_TRANS, + seq_len as i32, + out_dim as i32, + in_dim as i32, + 1.0, + x.as_ptr(), + in_dim as i32, + w.as_ptr(), + in_dim as i32, + 1.0, + y.as_mut_ptr(), + out_dim as i32, + ); + } + + #[cfg(not(feature = "blas"))] + { + for s in 0..seq_len { + let x_row = &x[s * in_dim..(s + 1) * in_dim]; + for o in 0..out_dim { + let w_row = &w[o * in_dim..(o + 1) * in_dim]; + let mut sum = b.map_or(0.0, |bb| bb[o]); + for i in 0..in_dim { + sum += x_row[i] * w_row[i]; + } + y[s * out_dim + o] += sum; + } + } + } +} + +fn bf16_to_f32_view(src: *const u16, n: usize) -> Vec { + let mut buf = vec![0.0f32; n]; + let src_slice = unsafe { std::slice::from_raw_parts(src, n) }; + bf16_to_f32_buf(&mut buf, src_slice); + buf +} + +/// Threaded bf16 matvec +fn bf16_matvec_threaded( + y: &mut [f32], + x: &[f32], + w_bf16: *const u16, + bias: Option<&[f32]>, + in_dim: usize, + out_dim: usize, +) { + let n_threads = get_num_threads(); + if n_threads <= 1 { + bf16_matvec_fused(y, x, w_bf16, bias, in_dim, out_dim); + return; + } + + let y_ptr = y.as_mut_ptr(); + let x_ptr = x.as_ptr(); + let w_ptr = w_bf16; + let bias_ptr = bias.map(|b| b.as_ptr()); + + // SAFETY: Each thread writes to non-overlapping segments of y + let y_send = y_ptr as usize; + let x_send = x_ptr as usize; + let w_send = w_ptr as usize; + let bias_send = bias_ptr.map(|p| p as usize); + + parallel_for(|tid, nt| { + let chunk = out_dim.div_ceil(nt); + let start = tid * chunk; + let end = (start + chunk).min(out_dim); + if start >= end { + return; + } + + let y_local = + unsafe { std::slice::from_raw_parts_mut((y_send as *mut f32).add(start), end - start) }; + let x_local = unsafe { std::slice::from_raw_parts(x_send as *const f32, in_dim) }; + let w_local = unsafe { (w_send as *const u16).add(start * in_dim) }; + let bias_local = bias_send.map(|p| unsafe { + std::slice::from_raw_parts((p as *const f32).add(start), end - start) + }); + + bf16_matvec_fused(y_local, x_local, w_local, bias_local, in_dim, end - start); + }); +} + +/// Like linear_nobias_bf16 for seq_len=1, but ADDS to the destination: y[i] += W[i] @ x. +/// Achieves fused residual add by passing y as its own "bias". +pub fn linear_nobias_bf16_addto( + y: &mut [f32], + x: &[f32], + w_bf16: *const u16, + in_dim: usize, + out_dim: usize, +) { + let _pg = ProfileGuard::new(&PROF.bf16_matvec); + // SAFETY: bf16_matvec_fused reads bias[i] before writing y[i], so aliasing y as bias is safe. + let bias = unsafe { std::slice::from_raw_parts(y.as_ptr(), out_dim) }; + bf16_matvec_threaded(y, x, w_bf16, Some(bias), in_dim, out_dim); +} + +pub fn linear_nobias_bf16( + y: &mut [f32], + x: &[f32], + w_bf16: *const u16, + seq_len: usize, + in_dim: usize, + out_dim: usize, +) { + let _pg = ProfileGuard::new(&PROF.bf16_matvec); + if seq_len == 1 { + bf16_matvec_threaded(y, x, w_bf16, None, in_dim, out_dim); + return; + } + let w_f32 = bf16_to_f32_view(w_bf16, out_dim * in_dim); + linear_nobias(y, x, &w_f32, seq_len, in_dim, out_dim); +} + +/// Like linear_nobias_bf16 but reuses a caller-provided scratch buffer for bf16→f32 conversion. +/// # Safety +/// Caller must ensure w_bf16 points to at least out_dim * in_dim valid bf16 values. +pub unsafe fn linear_nobias_bf16_scratch( + y: &mut [f32], + x: &[f32], + w_bf16: *const u16, + seq_len: usize, + in_dim: usize, + out_dim: usize, + scratch: &mut [f32], +) { + let _pg = ProfileGuard::new(&PROF.bf16_matvec); + if seq_len == 1 { + bf16_matvec_threaded(y, x, w_bf16, None, in_dim, out_dim); + return; + } + let n = out_dim * in_dim; + let src = unsafe { std::slice::from_raw_parts(w_bf16, n) }; + bf16_to_f32_buf(&mut scratch[..n], src); + linear_nobias(y, x, &scratch[..n], seq_len, in_dim, out_dim); +} + +pub fn linear_bf16( + y: &mut [f32], + x: &[f32], + w_bf16: *const u16, + b: Option<&[f32]>, + seq_len: usize, + in_dim: usize, + out_dim: usize, +) { + if seq_len == 1 { + bf16_matvec_threaded(y, x, w_bf16, b, in_dim, out_dim); + return; + } + let w_f32 = bf16_to_f32_view(w_bf16, out_dim * in_dim); + linear(y, x, &w_f32, b, seq_len, in_dim, out_dim); +} + +/// Fused Q/K/V matvec for single-token decode +#[allow(clippy::too_many_arguments)] +pub fn linear_nobias_bf16_qkv( + q: &mut [f32], + k: &mut [f32], + v: &mut [f32], + x: &[f32], + wq: *const u16, + wk: *const u16, + wv: *const u16, + in_dim: usize, + q_dim: usize, + kv_dim: usize, +) { + let n_threads = get_num_threads(); + if n_threads <= 1 { + bf16_matvec_fused(q, x, wq, None, in_dim, q_dim); + bf16_matvec_fused(k, x, wk, None, in_dim, kv_dim); + bf16_matvec_fused(v, x, wv, None, in_dim, kv_dim); + return; + } + + let total_dim = q_dim + 2 * kv_dim; + let q_ptr = q.as_mut_ptr() as usize; + let k_ptr = k.as_mut_ptr() as usize; + let v_ptr = v.as_mut_ptr() as usize; + let x_ptr = x.as_ptr() as usize; + let wq_ptr = wq as usize; + let wk_ptr = wk as usize; + let wv_ptr = wv as usize; + + parallel_for(|tid, nt| { + let chunk = total_dim.div_ceil(nt); + let start = tid * chunk; + let end = (start + chunk).min(total_dim); + if start >= end { + return; + } + + let x_local = unsafe { std::slice::from_raw_parts(x_ptr as *const f32, in_dim) }; + let q_end = q_dim; + let k_end = q_end + kv_dim; + + // Q range + if start < q_end { + let s = start; + let e = end.min(q_end); + if s < e { + let y_local = + unsafe { std::slice::from_raw_parts_mut((q_ptr as *mut f32).add(s), e - s) }; + let w_local = unsafe { (wq_ptr as *const u16).add(s * in_dim) }; + bf16_matvec_fused(y_local, x_local, w_local, None, in_dim, e - s); + } + } + + // K range + if end > q_end && start < k_end { + let s = start.saturating_sub(q_end); + let e_abs = end.min(k_end); + let e = e_abs - q_end; + if s < e { + let y_local = + unsafe { std::slice::from_raw_parts_mut((k_ptr as *mut f32).add(s), e - s) }; + let w_local = unsafe { (wk_ptr as *const u16).add(s * in_dim) }; + bf16_matvec_fused(y_local, x_local, w_local, None, in_dim, e - s); + } + } + + // V range + if end > k_end { + let s = start.saturating_sub(k_end); + let e_abs = end.min(total_dim); + let e = e_abs - k_end; + if s < e { + let y_local = + unsafe { std::slice::from_raw_parts_mut((v_ptr as *mut f32).add(s), e - s) }; + let w_local = unsafe { (wv_ptr as *const u16).add(s * in_dim) }; + bf16_matvec_fused(y_local, x_local, w_local, None, in_dim, e - s); + } + } + }); +} + +/// Fused gate_up matvec + SwiGLU for single-token decode. +/// Computes: ffn_out[j] = silu(gate[j]) * up[j] where gate/up come from interleaved gate_up_fused matvec. +/// Keeps gate_up output in L1 cache for the SwiGLU operation. +pub fn linear_nobias_bf16_swiglu( + ffn_out: &mut [f32], + x: &[f32], + gate_up_bf16: *const u16, + in_dim: usize, + intermediate: usize, +) { + let _pg = ProfileGuard::new(&PROF.bf16_matvec); + let n_threads = get_num_threads(); + + if n_threads <= 1 { + // Single-threaded: compute gate_up, then SwiGLU inline + let mut gate_buf = vec![0.0f32; 2 * intermediate]; + bf16_matvec_fused( + &mut gate_buf, + x, + gate_up_bf16, + None, + in_dim, + 2 * intermediate, + ); + for j in 0..intermediate { + let g = gate_buf[2 * j]; + let u = gate_buf[2 * j + 1]; + ffn_out[j] = g / (1.0 + (-g).exp()) * u; + } + return; + } + + let x_ptr = x.as_ptr() as usize; + let w_ptr = gate_up_bf16 as usize; + let ffn_ptr = ffn_out.as_mut_ptr() as usize; + + parallel_for(|tid, nt| { + let chunk = intermediate.div_ceil(nt); + let start = tid * chunk; + let end = (start + chunk).min(intermediate); + if start >= end { + return; + } + let n_rows = end - start; + + let x_local = unsafe { std::slice::from_raw_parts(x_ptr as *const f32, in_dim) }; + let w_local = unsafe { (w_ptr as *const u16).add(2 * start * in_dim) }; + + // Compute gate_up for this chunk (thread-local stack buffer) + let mut gate_up_local = vec![0.0f32; 2 * n_rows]; + bf16_matvec_fused( + &mut gate_up_local, + x_local, + w_local, + None, + in_dim, + 2 * n_rows, + ); + + // Apply SwiGLU inline while data is hot in L1 + let ffn_local = + unsafe { std::slice::from_raw_parts_mut((ffn_ptr as *mut f32).add(start), n_rows) }; + for j in 0..n_rows { + let g = gate_up_local[2 * j]; + let u = gate_up_local[2 * j + 1]; + ffn_local[j] = g / (1.0 + (-g).exp()) * u; + } + }); +} + +/// INT8 threaded matvec: y = W_int8 @ x + bias (x is f32, quantized on the fly) +fn int8_matvec_threaded( + y: &mut [f32], + x: &[f32], + w_int8: &[i8], + w_scales: &[f32], + bias: Option<&[f32]>, + in_dim: usize, + out_dim: usize, +) { + let (x_int8, x_scale) = quantize_f32_to_int8(x); + let n_threads = get_num_threads(); + + if n_threads <= 1 || out_dim <= 1 { + int8_matvec_arch( + y, + x_int8.as_ptr(), + x_scale, + w_int8.as_ptr(), + w_scales, + bias, + in_dim, + out_dim, + ); + return; + } + + let x_int8_ptr = x_int8.as_ptr() as usize; + let w_int8_ptr = w_int8.as_ptr() as usize; + let w_scales_ptr = w_scales.as_ptr() as usize; + let y_ptr = y.as_mut_ptr() as usize; + let bias_ptr = bias.map(|values| values.as_ptr() as usize); + + parallel_for(|tid, nt| { + let chunk = out_dim.div_ceil(nt); + let start = tid * chunk; + let end = (start + chunk).min(out_dim); + if start >= end { + return; + } + + let y_local = + unsafe { std::slice::from_raw_parts_mut((y_ptr as *mut f32).add(start), end - start) }; + let w_local = unsafe { (w_int8_ptr as *const i8).add(start * in_dim) }; + let w_scales_local = unsafe { + std::slice::from_raw_parts((w_scales_ptr as *const f32).add(start), end - start) + }; + let bias_local = bias_ptr.map(|ptr| unsafe { + std::slice::from_raw_parts((ptr as *const f32).add(start), end - start) + }); + + int8_matvec_arch( + y_local, + x_int8_ptr as *const i8, + x_scale, + w_local, + w_scales_local, + bias_local, + in_dim, + end - start, + ); + }); +} + +#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] +#[inline] +fn int8_matvec_scalar( + y: &mut [f32], + x_int8: *const i8, + x_scale: f32, + w_int8: *const i8, + w_scales: &[f32], + bias: Option<&[f32]>, + in_dim: usize, + out_dim: usize, +) { + for row in 0..out_dim { + let mut acc = 0i32; + for col in 0..in_dim { + acc += unsafe { *x_int8.add(col) as i32 } + * unsafe { *w_int8.add(row * in_dim + col) as i32 }; + } + let mut value = (acc as f32) * x_scale * w_scales[row]; + if let Some(bias_values) = bias { + value += bias_values[row]; + } + y[row] = value; + } +} + +#[inline] +fn int8_matvec_arch( + y: &mut [f32], + x_int8: *const i8, + x_scale: f32, + w_int8: *const i8, + w_scales: &[f32], + bias: Option<&[f32]>, + in_dim: usize, + out_dim: usize, +) { + #[cfg(target_arch = "aarch64")] + unsafe { + neon::matvec_int8(y, x_int8, x_scale, w_int8, w_scales, bias, in_dim, out_dim); + return; + } + + #[cfg(target_arch = "x86_64")] + unsafe { + avx::matvec_int8(y, x_int8, x_scale, w_int8, w_scales, bias, in_dim, out_dim); + return; + } + + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + { + int8_matvec_scalar(y, x_int8, x_scale, w_int8, w_scales, bias, in_dim, out_dim); + } +} + +#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] +#[inline] +fn int8_argmax_scalar( + x_int8: *const i8, + x_scale: f32, + w_int8: *const i8, + w_scales: &[f32], + in_dim: usize, + start: usize, + end: usize, +) -> (usize, f32) { + let mut best_idx = start; + let mut best_val = f32::NEG_INFINITY; + for row in start..end { + let mut acc = 0i32; + for col in 0..in_dim { + acc += unsafe { *x_int8.add(col) as i32 } + * unsafe { *w_int8.add(row * in_dim + col) as i32 }; + } + let value = (acc as f32) * x_scale * w_scales[row]; + if value > best_val { + best_idx = row; + best_val = value; + } + } + (best_idx, best_val) +} + +#[inline] +fn int8_argmax_arch( + x_int8: *const i8, + x_scale: f32, + w_int8: *const i8, + w_scales: &[f32], + in_dim: usize, + start: usize, + end: usize, +) -> (usize, f32) { + #[cfg(target_arch = "aarch64")] + unsafe { + return neon::argmax_int8_range(x_int8, x_scale, w_int8, w_scales, in_dim, start, end); + } + + #[cfg(target_arch = "x86_64")] + unsafe { + return avx::argmax_int8_range(x_int8, x_scale, w_int8, w_scales, in_dim, start, end); + } + + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + { + int8_argmax_scalar(x_int8, x_scale, w_int8, w_scales, in_dim, start, end) + } +} + +#[inline] +fn int8_swiglu_arch(out: &mut [f32], gate_up: &[f32], n: usize) { + #[cfg(target_arch = "aarch64")] + unsafe { + neon::swiglu_interleaved(out, gate_up, n); + return; + } + + #[cfg(target_arch = "x86_64")] + unsafe { + avx::swiglu_interleaved(out, gate_up, n); + return; + } + + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + { + for j in 0..n { + let g = gate_up[2 * j]; + let u = gate_up[2 * j + 1]; + out[j] = g / (1.0 + (-g).exp()) * u; + } + } +} + +/// INT8 fused QKV matvec for single-token decode +#[allow(clippy::too_many_arguments)] +pub fn linear_nobias_int8_qkv( + q: &mut [f32], + k: &mut [f32], + v: &mut [f32], + x: &[f32], + wq_int8: &[i8], + wq_scales: &[f32], + wk_int8: &[i8], + wk_scales: &[f32], + wv_int8: &[i8], + wv_scales: &[f32], + in_dim: usize, + q_dim: usize, + kv_dim: usize, +) { + let _pg = ProfileGuard::new(&PROF.bf16_matvec); + let (x_int8, x_scale) = quantize_f32_to_int8(x); + let n_threads = get_num_threads(); + if n_threads <= 1 { + int8_matvec_arch( + q, + x_int8.as_ptr(), + x_scale, + wq_int8.as_ptr(), + wq_scales, + None, + in_dim, + q_dim, + ); + int8_matvec_arch( + k, + x_int8.as_ptr(), + x_scale, + wk_int8.as_ptr(), + wk_scales, + None, + in_dim, + kv_dim, + ); + int8_matvec_arch( + v, + x_int8.as_ptr(), + x_scale, + wv_int8.as_ptr(), + wv_scales, + None, + in_dim, + kv_dim, + ); + return; + } + + let total_dim = q_dim + 2 * kv_dim; + let q_ptr = q.as_mut_ptr() as usize; + let k_ptr = k.as_mut_ptr() as usize; + let v_ptr = v.as_mut_ptr() as usize; + let x_int8_ptr = x_int8.as_ptr() as usize; + let wq_ptr = wq_int8.as_ptr() as usize; + let wk_ptr = wk_int8.as_ptr() as usize; + let wv_ptr = wv_int8.as_ptr() as usize; + let wq_scales_ptr = wq_scales.as_ptr() as usize; + let wk_scales_ptr = wk_scales.as_ptr() as usize; + let wv_scales_ptr = wv_scales.as_ptr() as usize; + + parallel_for(|tid, nt| { + let chunk = total_dim.div_ceil(nt); + let start = tid * chunk; + let end = (start + chunk).min(total_dim); + if start >= end { + return; + } + + let q_end = q_dim; + let k_end = q_end + kv_dim; + + if start < q_end { + let s = start; + let e = end.min(q_end); + if s < e { + let y_local = + unsafe { std::slice::from_raw_parts_mut((q_ptr as *mut f32).add(s), e - s) }; + let w_local = unsafe { (wq_ptr as *const i8).add(s * in_dim) }; + let scales_local = unsafe { + std::slice::from_raw_parts((wq_scales_ptr as *const f32).add(s), e - s) + }; + int8_matvec_arch( + y_local, + x_int8_ptr as *const i8, + x_scale, + w_local, + scales_local, + None, + in_dim, + e - s, + ); + } + } + if start < k_end && end > q_end { + let s = start.max(q_end) - q_end; + let e = end.min(k_end) - q_end; + if s < e { + let y_local = + unsafe { std::slice::from_raw_parts_mut((k_ptr as *mut f32).add(s), e - s) }; + let w_local = unsafe { (wk_ptr as *const i8).add(s * in_dim) }; + let scales_local = unsafe { + std::slice::from_raw_parts((wk_scales_ptr as *const f32).add(s), e - s) + }; + int8_matvec_arch( + y_local, + x_int8_ptr as *const i8, + x_scale, + w_local, + scales_local, + None, + in_dim, + e - s, + ); + } + } + if end > k_end { + let s = start.max(k_end) - k_end; + let e = end - k_end; + if s < e { + let y_local = + unsafe { std::slice::from_raw_parts_mut((v_ptr as *mut f32).add(s), e - s) }; + let w_local = unsafe { (wv_ptr as *const i8).add(s * in_dim) }; + let scales_local = unsafe { + std::slice::from_raw_parts((wv_scales_ptr as *const f32).add(s), e - s) + }; + int8_matvec_arch( + y_local, + x_int8_ptr as *const i8, + x_scale, + w_local, + scales_local, + None, + in_dim, + e - s, + ); + } + } + }); +} + +/// INT8 fused gate_up + SwiGLU +pub fn linear_nobias_int8_swiglu( + ffn_out: &mut [f32], + x: &[f32], + w_int8: &[i8], + w_scales: &[f32], + in_dim: usize, + intermediate: usize, +) { + let _pg = ProfileGuard::new(&PROF.bf16_matvec); + let (x_int8, x_scale) = quantize_f32_to_int8(x); + let n_threads = get_num_threads(); + if n_threads <= 1 || intermediate <= 1 { + let mut gate_buf = vec![0.0f32; 2 * intermediate]; + int8_matvec_arch( + &mut gate_buf, + x_int8.as_ptr(), + x_scale, + w_int8.as_ptr(), + w_scales, + None, + in_dim, + 2 * intermediate, + ); + int8_swiglu_arch(ffn_out, &gate_buf, intermediate); + return; + } + + let x_int8_ptr = x_int8.as_ptr() as usize; + let w_int8_ptr = w_int8.as_ptr() as usize; + let w_scales_ptr = w_scales.as_ptr() as usize; + let ffn_ptr = ffn_out.as_mut_ptr() as usize; + + parallel_for(|tid, nt| { + let chunk = intermediate.div_ceil(nt); + let start = tid * chunk; + let end = (start + chunk).min(intermediate); + if start >= end { + return; + } + let n_rows = end - start; + + let w_local = unsafe { (w_int8_ptr as *const i8).add(2 * start * in_dim) }; + let w_scales_local = unsafe { + std::slice::from_raw_parts((w_scales_ptr as *const f32).add(2 * start), 2 * n_rows) + }; + + let mut gate_up_local = vec![0.0f32; 2 * n_rows]; + int8_matvec_arch( + &mut gate_up_local, + x_int8_ptr as *const i8, + x_scale, + w_local, + w_scales_local, + None, + in_dim, + 2 * n_rows, + ); + + let ffn_local = + unsafe { std::slice::from_raw_parts_mut((ffn_ptr as *mut f32).add(start), n_rows) }; + int8_swiglu_arch(ffn_local, &gate_up_local, n_rows); + }); +} + +/// INT8 matvec with fused residual add: y += W_int8 @ x (y acts as bias) +pub fn linear_nobias_int8_addto( + y: &mut [f32], + x: &[f32], + w_int8: &[i8], + w_scales: &[f32], + in_dim: usize, + out_dim: usize, +) { + let _pg = ProfileGuard::new(&PROF.bf16_matvec); + let bias = unsafe { std::slice::from_raw_parts(y.as_ptr(), out_dim) }; + int8_matvec_threaded(y, x, w_int8, w_scales, Some(bias), in_dim, out_dim); +} + +pub fn matmul_t_bf16(c: &mut [f32], a: &[f32], b_bf16: *const u16, m: usize, k: usize, n: usize) { + if m == 1 { + bf16_matvec_threaded(c, a, b_bf16, None, k, n); + } else { + let b_f32 = bf16_to_f32_view(b_bf16, n * k); + matmul_t(c, a, &b_f32, m, k, n); + } +} + +// ======================================================================== +// 2D Convolution (im2col + BLAS sgemm) +// ======================================================================== + +#[allow(clippy::too_many_arguments)] +fn im2col( + input: &[f32], + cols: &mut [f32], + c_in: usize, + h_in: usize, + w_in: usize, + kh: usize, + kw: usize, + stride: usize, + padding: usize, + h_out: usize, + w_out: usize, +) { + let col_len = h_out * w_out; + for ic in 0..c_in { + for ki in 0..kh { + for kj in 0..kw { + let col_row = (ic * kh + ki) * kw + kj; + for oh in 0..h_out { + let ih = oh * stride + ki; + let ih = ih as isize - padding as isize; + for ow in 0..w_out { + let iw = ow * stride + kj; + let iw = iw as isize - padding as isize; + let val = + if ih >= 0 && (ih as usize) < h_in && iw >= 0 && (iw as usize) < w_in { + input[ic * h_in * w_in + ih as usize * w_in + iw as usize] + } else { + 0.0 + }; + cols[col_row * col_len + oh * w_out + ow] = val; + } + } + } + } + } +} + +#[allow(clippy::too_many_arguments)] +pub fn conv2d( + out: &mut [f32], + input: &[f32], + weight: &[f32], + bias: Option<&[f32]>, + c_in: usize, + c_out: usize, + h_in: usize, + w_in: usize, + kh: usize, + kw: usize, + stride: usize, + padding: usize, +) { + let _pg = ProfileGuard::new(&PROF.conv2d_op); + let h_out = (h_in + 2 * padding - kh) / stride + 1; + let w_out = (w_in + 2 * padding - kw) / stride + 1; + let patch_size = c_in * kh * kw; + let spatial_out = h_out * w_out; + + let mut cols = vec![0.0f32; patch_size * spatial_out]; + + // Thread im2col across col_rows (each row is independent) + let n_threads = get_num_threads(); + if n_threads > 1 && patch_size >= 16 { + let input_ptr = input.as_ptr() as usize; + let cols_ptr = cols.as_mut_ptr() as usize; + parallel_for(|tid, nt| { + let chunk = patch_size.div_ceil(nt); + let start = tid * chunk; + let end = (start + chunk).min(patch_size); + if start >= end { + return; + } + for col_row in start..end { + let ic = col_row / (kh * kw); + let rem = col_row % (kh * kw); + let ki = rem / kw; + let kj = rem % kw; + for oh in 0..h_out { + let ih = (oh * stride + ki) as isize - padding as isize; + for ow in 0..w_out { + let iw = (ow * stride + kj) as isize - padding as isize; + let val = + if ih >= 0 && (ih as usize) < h_in && iw >= 0 && (iw as usize) < w_in { + unsafe { + *(input_ptr as *const f32) + .add(ic * h_in * w_in + ih as usize * w_in + iw as usize) + } + } else { + 0.0 + }; + unsafe { + *(cols_ptr as *mut f32).add(col_row * spatial_out + oh * w_out + ow) = + val; + } + } + } + } + }); + } else { + im2col( + input, &mut cols, c_in, h_in, w_in, kh, kw, stride, padding, h_out, w_out, + ); + } + + // GEMM: weight[c_out, patch_size] @ cols[patch_size, spatial_out] = out[c_out, spatial_out] + #[cfg(feature = "blas")] + unsafe { + cblas_sgemm( + CBLAS_ROW_MAJOR, + CBLAS_NO_TRANS, + CBLAS_NO_TRANS, + c_out as i32, + spatial_out as i32, + patch_size as i32, + 1.0, + weight.as_ptr(), + patch_size as i32, + cols.as_ptr(), + spatial_out as i32, + 0.0, + out.as_mut_ptr(), + spatial_out as i32, + ); + } + + #[cfg(not(feature = "blas"))] + { + for oc in 0..c_out { + for s in 0..spatial_out { + let mut sum = 0.0f32; + for p in 0..patch_size { + sum += weight[oc * patch_size + p] * cols[p * spatial_out + s]; + } + out[oc * spatial_out + s] = sum; + } + } + } + + if let Some(bias) = bias { + for oc in 0..c_out { + let b = bias[oc]; + for s in 0..spatial_out { + out[oc * spatial_out + s] += b; + } + } + } +} + +// ======================================================================== +// Normalization +// ======================================================================== + +pub fn layer_norm( + out: &mut [f32], + x: &[f32], + weight: &[f32], + bias: &[f32], + seq_len: usize, + hidden: usize, + eps: f32, +) { + let _pg = ProfileGuard::new(&PROF.layer_norm); + for s in 0..seq_len { + let x_row = &x[s * hidden..(s + 1) * hidden]; + let out_row = &mut out[s * hidden..(s + 1) * hidden]; + + #[cfg(target_arch = "aarch64")] + { + unsafe { + neon::layer_norm_row(out_row, x_row, weight, bias, hidden, eps); + } + continue; + } + + #[cfg(target_arch = "x86_64")] + { + unsafe { + avx::layer_norm_row(out_row, x_row, weight, bias, hidden, eps); + } + continue; + } + + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + { + let mean: f32 = x_row.iter().sum::() / hidden as f32; + + let var: f32 = x_row + .iter() + .map(|&v| { + let d = v - mean; + d * d + }) + .sum::() + / hidden as f32; + + let inv_std = 1.0 / (var + eps).sqrt(); + + for i in 0..hidden { + out_row[i] = (x_row[i] - mean) * inv_std * weight[i] + bias[i]; + } + } + } +} + +pub fn rms_norm( + out: &mut [f32], + x: &[f32], + weight: &[f32], + seq_len: usize, + hidden: usize, + eps: f32, +) { + let _pg = ProfileGuard::new(&PROF.rms_norm); + for s in 0..seq_len { + let x_row = &x[s * hidden..(s + 1) * hidden]; + let out_row = &mut out[s * hidden..(s + 1) * hidden]; + + #[cfg(target_arch = "aarch64")] + { + unsafe { + neon::rms_norm_row(out_row, x_row, weight, hidden, eps); + } + continue; + } + + #[cfg(target_arch = "x86_64")] + { + unsafe { + avx::rms_norm_row(out_row, x_row, weight, hidden, eps); + } + continue; + } + + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + { + let sum_sq: f32 = x_row.iter().map(|&v| v * v).sum(); + let rms_inv = 1.0 / (sum_sq / hidden as f32 + eps).sqrt(); + for i in 0..hidden { + out_row[i] = x_row[i] * rms_inv * weight[i]; + } + } + } +} + +pub fn rms_norm_per_head( + x: &mut [f32], + weight: &[f32], + seq_len: usize, + n_heads: usize, + head_dim: usize, + eps: f32, +) { + let hidden = n_heads * head_dim; + for s in 0..seq_len { + for h in 0..n_heads { + let off = s * hidden + h * head_dim; + + #[cfg(target_arch = "aarch64")] + { + let vec = &mut x[off..off + head_dim]; + unsafe { + neon::rms_norm_inplace(vec, weight, head_dim, eps); + } + continue; + } + + #[cfg(not(target_arch = "aarch64"))] + { + let vec = &mut x[off..off + head_dim]; + let sum_sq: f32 = vec.iter().map(|&v| v * v).sum(); + let rms_inv = 1.0 / (sum_sq / head_dim as f32 + eps).sqrt(); + for d in 0..head_dim { + vec[d] = vec[d] * rms_inv * weight[d]; + } + } + } + } +} + +// ======================================================================== +// Activation Functions +// ======================================================================== + +pub fn silu(x: &mut [f32], n: usize) { + for val in x.iter_mut().take(n) { + *val = *val / (1.0 + (-*val).exp()); + } +} + +pub fn gelu(x: &mut [f32], n: usize) { + let _pg = ProfileGuard::new(&PROF.gelu); + let n_threads = get_num_threads(); + // Thread GELU for large buffers (encoder FFN: ~320K floats) + if n_threads > 1 && n > 4096 { + let x_ptr = x.as_mut_ptr() as usize; + parallel_for(|tid, nt| { + let chunk = n.div_ceil(nt); + let start = tid * chunk; + let end = (start + chunk).min(n); + if start >= end { + return; + } + let x_local = unsafe { + std::slice::from_raw_parts_mut((x_ptr as *mut f32).add(start), end - start) + }; + #[cfg(target_arch = "aarch64")] + unsafe { + neon::gelu_inplace(x_local, end - start); + } + #[cfg(target_arch = "x86_64")] + unsafe { + avx::gelu_inplace(x_local, end - start); + } + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + for i in 0..(end - start) { + let val = x_local[i]; + let x3 = val * val * val; + let inner = 0.7978845608028654f32 * (val + 0.044715 * x3); + x_local[i] = 0.5 * val * (1.0 + inner.tanh()); + } + }); + return; + } + #[cfg(target_arch = "aarch64")] + { + unsafe { + neon::gelu_inplace(x, n); + } + } + + #[cfg(target_arch = "x86_64")] + { + unsafe { + avx::gelu_inplace(x, n); + } + } + + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + for i in 0..n { + let val = x[i]; + let x3 = val * val * val; + let inner = 0.7978845608028654f32 * (val + 0.044715 * x3); + x[i] = 0.5 * val * (1.0 + inner.tanh()); + } +} + +pub fn swiglu_multiply(out: &mut [f32], gate_up: &[f32], seq_len: usize, intermediate: usize) { + let _pg = ProfileGuard::new(&PROF.swiglu); + let total = seq_len * intermediate; + let n_threads = get_num_threads(); + + // Thread SwiGLU for large prefill buffers + if n_threads > 1 && total > 4096 { + let out_ptr = out.as_mut_ptr() as usize; + let gu_ptr = gate_up.as_ptr() as usize; + parallel_for(|tid, nt| { + let chunk = seq_len.div_ceil(nt); + let start = tid * chunk; + let end = (start + chunk).min(seq_len); + if start >= end { + return; + } + for s in start..end { + let gu = unsafe { + std::slice::from_raw_parts( + (gu_ptr as *const f32).add(s * 2 * intermediate), + 2 * intermediate, + ) + }; + let o = unsafe { + std::slice::from_raw_parts_mut( + (out_ptr as *mut f32).add(s * intermediate), + intermediate, + ) + }; + #[cfg(target_arch = "aarch64")] + { + unsafe { + neon::swiglu_interleaved(o, gu, intermediate); + } + continue; + } + #[cfg(target_arch = "x86_64")] + { + unsafe { + avx::swiglu_interleaved(o, gu, intermediate); + } + continue; + } + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + for j in 0..intermediate { + let g = gu[2 * j]; + let u = gu[2 * j + 1]; + o[j] = g / (1.0 + (-g).exp()) * u; + } + } + }); + return; + } + + for s in 0..seq_len { + let gu = &gate_up[s * 2 * intermediate..s * 2 * intermediate + 2 * intermediate]; + let o = &mut out[s * intermediate..(s + 1) * intermediate]; + + #[cfg(target_arch = "aarch64")] + { + unsafe { + neon::swiglu_interleaved(o, gu, intermediate); + } + continue; + } + + #[cfg(target_arch = "x86_64")] + { + unsafe { + avx::swiglu_interleaved(o, gu, intermediate); + } + continue; + } + + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + for j in 0..intermediate { + let g = gu[2 * j]; + let u = gu[2 * j + 1]; + let g_silu = g / (1.0 + (-g).exp()); + o[j] = g_silu * u; + } + } +} + +pub fn softmax(x: &mut [f32], rows: usize, cols: usize) { + for r in 0..rows { + let row = &mut x[r * cols..(r + 1) * cols]; + let max_val = row.iter().cloned().fold(f32::NEG_INFINITY, f32::max); + for val in row.iter_mut().take(cols) { + *val -= max_val; + } + + #[cfg(all(feature = "vdsp", target_vendor = "apple"))] + { + let n = cols as i32; + unsafe { + vvexpf(row.as_mut_ptr(), row.as_ptr(), &n); + } + } + #[cfg(not(all(feature = "vdsp", target_vendor = "apple")))] + { + #[cfg(target_arch = "aarch64")] + { + unsafe { + neon::exp_inplace(row); + } + } + + #[cfg(target_arch = "x86_64")] + { + unsafe { + avx::exp_inplace(row); + } + } + + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + for c in 0..cols { + row[c] = row[c].exp(); + } + } + + let mut sum = 0.0f32; + for val in row.iter().take(cols) { + sum += val; + } + let inv_sum = 1.0 / sum; + for val in row.iter_mut().take(cols) { + *val *= inv_sum; + } + } +} + +// ======================================================================== +// Attention Operations +// ======================================================================== + +#[allow(clippy::too_many_arguments)] +fn bidirectional_attention_heads( + out: &mut [f32], + q: &[f32], + k: &[f32], + v: &[f32], + n_heads: usize, + head_dim: usize, + scale: f32, + window_starts: &[i32], + n_windows: usize, + head_start: usize, + head_end: usize, +) { + let hidden = n_heads * head_dim; + + for h in head_start..head_end { + for w in 0..n_windows { + let ws = window_starts[w] as usize; + let we = window_starts[w + 1] as usize; + + for i in ws..we { + let q_off = i * hidden + h * head_dim; + let q_row = &q[q_off..q_off + head_dim]; + let o_row = + &mut out[i * hidden + h * head_dim..i * hidden + h * head_dim + head_dim]; + + let mut max_score = -1e30f32; + let mut sum_exp = 0.0f32; + for val in o_row.iter_mut().take(head_dim) { + *val = 0.0; + } + + for j in ws..we { + let k_off = j * hidden + h * head_dim; + let v_off = j * hidden + h * head_dim; + let k_row = &k[k_off..k_off + head_dim]; + let v_row = &v[v_off..v_off + head_dim]; + + let score = dot_f32(q_row, k_row, head_dim) * scale; + + if score > max_score { + let correction = (max_score - score).exp(); + sum_exp = sum_exp * correction + 1.0; + vec_scale_add(o_row, v_row, correction, head_dim); + max_score = score; + } else { + let wt = (score - max_score).exp(); + sum_exp += wt; + vec_axpy_inplace(o_row, v_row, wt, head_dim); + } + } + + if sum_exp > 0.0 { + let inv_sum = 1.0 / sum_exp; + vec_scale_inplace(o_row, inv_sum, head_dim); + } + } + } + } +} + +#[allow(clippy::too_many_arguments)] +pub fn bidirectional_attention( + out: &mut [f32], + q: &[f32], + k: &[f32], + v: &[f32], + seq: usize, + n_heads: usize, + head_dim: usize, + scale: f32, + window_starts: &[i32], + n_windows: usize, +) { + let _pg = ProfileGuard::new(&PROF.attention_bidir); + let n_threads = get_num_threads(); + let hidden = n_heads * head_dim; + + if n_threads > 1 && n_heads >= 2 { + let out_ptr = out.as_mut_ptr() as usize; + let q_ptr = q.as_ptr() as usize; + let k_ptr = k.as_ptr() as usize; + let v_ptr = v.as_ptr() as usize; + let ws_ptr = window_starts.as_ptr() as usize; + + parallel_for(|tid, nt| { + let chunk = n_heads.div_ceil(nt); + let h0 = tid * chunk; + let h1 = (h0 + chunk).min(n_heads); + if h0 >= h1 { + return; + } + + let out_local = + unsafe { std::slice::from_raw_parts_mut(out_ptr as *mut f32, seq * hidden) }; + let q_local = unsafe { std::slice::from_raw_parts(q_ptr as *const f32, seq * hidden) }; + let k_local = unsafe { std::slice::from_raw_parts(k_ptr as *const f32, seq * hidden) }; + let v_local = unsafe { std::slice::from_raw_parts(v_ptr as *const f32, seq * hidden) }; + let ws_local = + unsafe { std::slice::from_raw_parts(ws_ptr as *const i32, n_windows + 1) }; + + bidirectional_attention_heads( + out_local, q_local, k_local, v_local, n_heads, head_dim, scale, ws_local, + n_windows, h0, h1, + ); + }); + return; + } + + bidirectional_attention_heads( + out, + q, + k, + v, + n_heads, + head_dim, + scale, + window_starts, + n_windows, + 0, + n_heads, + ); +} + +/// Two-pass causal attention using BLAS sgemm with head-contiguous KV cache. +/// K/V layout: `[head][pos][head_dim]` — each head's data is contiguous across positions. +/// +/// Single-token (seq_q=1): online softmax with NEON dot products — avoids BLAS overhead, +/// scores allocation, and fuses all 3 passes into a single scan over KV positions. +/// +/// Multi-token (seq_q>1): for long sequences, use per-head batched GEMMs; +/// otherwise fall back to the simpler row-wise 3-pass BLAS path. +#[cfg(feature = "blas")] +#[allow(clippy::too_many_arguments)] +fn causal_attention_heads( + out: &mut [f32], + q: &[f32], + k_base: *const f32, + v_base: *const f32, + head_stride: usize, + seq_q: usize, + seq_k: usize, + n_heads: usize, + n_kv_heads: usize, + head_dim: usize, + scale: f32, + q_offset: usize, + head_start: usize, + head_end: usize, +) { + let heads_per_kv = n_heads / n_kv_heads; + let q_hidden = n_heads * head_dim; + const BATCHED_CAUSAL_ATTENTION_THRESHOLD: usize = 256; + + // Single-token path: online softmax without allocation or BLAS + if seq_q == 1 { + for h in head_start..head_end { + let kv_h = h / heads_per_kv; + let k_head = unsafe { k_base.add(kv_h * head_stride) }; + let v_head = unsafe { v_base.add(kv_h * head_stride) }; + let q_off = h * head_dim; + let o_row = &mut out[q_off..q_off + head_dim]; + let k_end = (q_offset + 1).min(seq_k); + + if k_end == 0 { + for val in o_row.iter_mut().take(head_dim) { + *val = 0.0; + } + continue; + } + + let q_row = &q[q_off..q_off + head_dim]; + + // Online softmax: single pass over KV positions + let mut max_score = -1e30f32; + let mut sum_exp = 0.0f32; + for val in o_row.iter_mut().take(head_dim) { + *val = 0.0; + } + + for j in 0..k_end { + let k_row = + unsafe { std::slice::from_raw_parts(k_head.add(j * head_dim), head_dim) }; + let v_row = + unsafe { std::slice::from_raw_parts(v_head.add(j * head_dim), head_dim) }; + + let score = dot_f32(q_row, k_row, head_dim) * scale; + + if score > max_score { + let correction = (max_score - score).exp(); + sum_exp = sum_exp * correction + 1.0; + vec_scale_add(o_row, v_row, correction, head_dim); + max_score = score; + } else { + let wt = (score - max_score).exp(); + sum_exp += wt; + vec_axpy_inplace(o_row, v_row, wt, head_dim); + } + } + + if sum_exp > 0.0 { + let inv_sum = 1.0 / sum_exp; + vec_scale_inplace(o_row, inv_sum, head_dim); + } + } + return; + } + + if seq_q >= BATCHED_CAUSAL_ATTENTION_THRESHOLD { + let mut q_head = vec![0.0f32; seq_q * head_dim]; + let mut scores = vec![0.0f32; seq_q * seq_k]; + let mut out_head = vec![0.0f32; seq_q * head_dim]; + + for h in head_start..head_end { + let kv_h = h / heads_per_kv; + let k_head = unsafe { k_base.add(kv_h * head_stride) }; + let v_head = unsafe { v_base.add(kv_h * head_stride) }; + let head_offset = h * head_dim; + + for i in 0..seq_q { + let src_off = i * q_hidden + head_offset; + let dst_off = i * head_dim; + q_head[dst_off..dst_off + head_dim] + .copy_from_slice(&q[src_off..src_off + head_dim]); + } + + unsafe { + cblas_sgemm( + CBLAS_ROW_MAJOR, + CBLAS_NO_TRANS, + CBLAS_TRANS, + seq_q as i32, + seq_k as i32, + head_dim as i32, + scale, + q_head.as_ptr(), + head_dim as i32, + k_head, + head_dim as i32, + 0.0, + scores.as_mut_ptr(), + seq_k as i32, + ); + } + + for i in 0..seq_q { + let row = &mut scores[i * seq_k..(i + 1) * seq_k]; + let valid = (q_offset + i + 1).min(seq_k); + if valid == 0 { + row.fill(0.0); + continue; + } + + let mut max_s = row[0]; + for &score in row.iter().take(valid).skip(1) { + if score > max_s { + max_s = score; + } + } + for score in row.iter_mut().take(valid) { + *score = (*score - max_s).exp(); + } + + let mut sum_exp = 0.0f32; + for &score in row.iter().take(valid) { + sum_exp += score; + } + if sum_exp > 0.0 { + let inv = 1.0 / sum_exp; + for score in row.iter_mut().take(valid) { + *score *= inv; + } + } + for score in row.iter_mut().skip(valid) { + *score = 0.0; + } + } + + unsafe { + cblas_sgemm( + CBLAS_ROW_MAJOR, + CBLAS_NO_TRANS, + CBLAS_NO_TRANS, + seq_q as i32, + head_dim as i32, + seq_k as i32, + 1.0, + scores.as_ptr(), + seq_k as i32, + v_head, + head_dim as i32, + 0.0, + out_head.as_mut_ptr(), + head_dim as i32, + ); + } + + for i in 0..seq_q { + let src_off = i * head_dim; + let dst_off = i * q_hidden + head_offset; + out[dst_off..dst_off + head_dim] + .copy_from_slice(&out_head[src_off..src_off + head_dim]); + } + } + return; + } + + // Multi-token path: row-wise 3-pass BLAS sgemm + let mut scores = vec![0.0f32; seq_k]; + + for h in head_start..head_end { + let kv_h = h / heads_per_kv; + let k_head = unsafe { k_base.add(kv_h * head_stride) }; + let v_head = unsafe { v_base.add(kv_h * head_stride) }; + + for i in 0..seq_q { + let q_off = i * q_hidden + h * head_dim; + let o_off = i * q_hidden + h * head_dim; + let o_row = &mut out[o_off..o_off + head_dim]; + let global_pos = q_offset + i; + let k_end = (global_pos + 1).min(seq_k); + + if k_end == 0 { + for val in o_row.iter_mut().take(head_dim) { + *val = 0.0; + } + continue; + } + + // Pass 1: scores = K_h @ q_h + unsafe { + cblas_sgemm( + CBLAS_ROW_MAJOR, + CBLAS_NO_TRANS, + CBLAS_NO_TRANS, + k_end as i32, + 1, + head_dim as i32, + scale, + k_head, + head_dim as i32, + q.as_ptr().add(q_off), + 1, + 0.0, + scores.as_mut_ptr(), + 1, + ); + } + + // Pass 2: Softmax + let mut max_s = scores[0]; + for j in 1..k_end { + if scores[j] > max_s { + max_s = scores[j]; + } + } + for j in 0..k_end { + scores[j] -= max_s; + } + + #[cfg(all(feature = "vdsp", target_vendor = "apple"))] + { + let n = k_end as i32; + unsafe { + vvexpf(scores.as_mut_ptr(), scores.as_ptr(), &n); + } + } + #[cfg(not(all(feature = "vdsp", target_vendor = "apple")))] + { + for j in 0..k_end { + scores[j] = scores[j].exp(); + } + } + + let mut sum_exp = 0.0f32; + for j in 0..k_end { + sum_exp += scores[j]; + } + if sum_exp > 0.0 { + let inv = 1.0 / sum_exp; + for j in 0..k_end { + scores[j] *= inv; + } + } + + // Pass 3: out = V_h^T @ softmax_scores + unsafe { + cblas_sgemm( + CBLAS_ROW_MAJOR, + CBLAS_TRANS, + CBLAS_NO_TRANS, + head_dim as i32, + 1, + k_end as i32, + 1.0, + v_head, + head_dim as i32, + scores.as_ptr(), + 1, + 0.0, + o_row.as_mut_ptr(), + 1, + ); + } + } + } +} + +/// Fallback: online softmax causal attention (no BLAS), head-contiguous KV layout. +#[cfg(not(feature = "blas"))] +#[allow(clippy::too_many_arguments)] +fn causal_attention_heads( + out: &mut [f32], + q: &[f32], + k_base: *const f32, + v_base: *const f32, + head_stride: usize, + seq_q: usize, + seq_k: usize, + n_heads: usize, + n_kv_heads: usize, + head_dim: usize, + scale: f32, + q_offset: usize, + head_start: usize, + head_end: usize, +) { + let heads_per_kv = n_heads / n_kv_heads; + let q_hidden = n_heads * head_dim; + + for h in head_start..head_end { + let kv_h = h / heads_per_kv; + + for i in 0..seq_q { + let q_off = i * q_hidden + h * head_dim; + let q_row = &q[q_off..q_off + head_dim]; + let o_row = + &mut out[i * q_hidden + h * head_dim..i * q_hidden + h * head_dim + head_dim]; + let global_pos = q_offset + i; + let k_end = (global_pos + 1).min(seq_k); + + let mut max_score = -1e30f32; + let mut sum_exp = 0.0f32; + for val in o_row.iter_mut().take(head_dim) { + *val = 0.0; + } + + for j in 0..k_end { + let k_row = unsafe { + std::slice::from_raw_parts( + k_base.add(kv_h * head_stride + j * head_dim), + head_dim, + ) + }; + let v_row = unsafe { + std::slice::from_raw_parts( + v_base.add(kv_h * head_stride + j * head_dim), + head_dim, + ) + }; + + let score = dot_f32(q_row, k_row, head_dim) * scale; + + if score > max_score { + let correction = (max_score - score).exp(); + sum_exp = sum_exp * correction + 1.0; + vec_scale_add(o_row, v_row, correction, head_dim); + max_score = score; + } else { + let wt = (score - max_score).exp(); + sum_exp += wt; + vec_axpy_inplace(o_row, v_row, wt, head_dim); + } + } + + if sum_exp > 0.0 { + let inv_sum = 1.0 / sum_exp; + vec_scale_inplace(o_row, inv_sum, head_dim); + } + } + } +} + +#[allow(clippy::too_many_arguments)] +pub fn causal_attention( + out: &mut [f32], + q: &[f32], + k_base: *const f32, + v_base: *const f32, + head_stride: usize, + seq_q: usize, + seq_k: usize, + n_heads: usize, + n_kv_heads: usize, + head_dim: usize, + scale: f32, + q_offset: usize, +) { + let _pg = ProfileGuard::new(&PROF.attention_causal); + let n_threads = get_num_threads(); + if n_threads > 1 && n_heads >= 2 { + let out_ptr = out.as_mut_ptr() as usize; + let q_ptr = q.as_ptr() as usize; + let k_ptr = k_base as usize; + let v_ptr = v_base as usize; + let q_hidden = n_heads * head_dim; + + parallel_for(|tid, nt| { + let chunk = n_heads.div_ceil(nt); + let h0 = tid * chunk; + let h1 = (h0 + chunk).min(n_heads); + if h0 >= h1 { + return; + } + + let out_local = + unsafe { std::slice::from_raw_parts_mut(out_ptr as *mut f32, seq_q * q_hidden) }; + let q_local = + unsafe { std::slice::from_raw_parts(q_ptr as *const f32, seq_q * q_hidden) }; + + causal_attention_heads( + out_local, + q_local, + k_ptr as *const f32, + v_ptr as *const f32, + head_stride, + seq_q, + seq_k, + n_heads, + n_kv_heads, + head_dim, + scale, + q_offset, + h0, + h1, + ); + }); + return; + } + + causal_attention_heads( + out, + q, + k_base, + v_base, + head_stride, + seq_q, + seq_k, + n_heads, + n_kv_heads, + head_dim, + scale, + q_offset, + 0, + n_heads, + ); +} + +// ======================================================================== +// Position Embeddings +// ======================================================================== + +pub fn sinusoidal_pe(pe: &mut [f32], n_pos: usize, d_model: usize) { + let half = d_model / 2; + let log_timescale = (10000.0f32).ln() / (half - 1) as f32; + + for p in 0..n_pos { + let row = &mut pe[p * d_model..(p + 1) * d_model]; + for d in 0..half { + let inv_timescale = (-(d as f32) * log_timescale).exp(); + let angle = p as f32 * inv_timescale; + row[d] = angle.sin(); + row[half + d] = angle.cos(); + } + } +} + +pub fn compute_rope_neox( + cos_out: &mut [f32], + sin_out: &mut [f32], + positions: &[i32], + seq: usize, + head_dim: usize, + theta: f32, +) { + let half = head_dim / 2; + + for s in 0..seq { + let pos = positions[s] as f32; + for d in 0..half { + let freq = 1.0 / theta.powf((2 * d) as f32 / head_dim as f32); + let angle = pos * freq; + let c = angle.cos(); + let sn = angle.sin(); + cos_out[s * head_dim + d] = c; + cos_out[s * head_dim + half + d] = c; + sin_out[s * head_dim + d] = sn; + sin_out[s * head_dim + half + d] = sn; + } + } +} + +pub fn apply_rope_neox( + x: &mut [f32], + cos_vals: &[f32], + sin_vals: &[f32], + seq: usize, + n_heads: usize, + head_dim: usize, +) { + let _pg = ProfileGuard::new(&PROF.rope); + let half = head_dim / 2; + let hidden = n_heads * head_dim; + + for s in 0..seq { + let c = &cos_vals[s * head_dim..]; + let sn = &sin_vals[s * head_dim..]; + + for h in 0..n_heads { + let base = s * hidden + h * head_dim; + let vec = &mut x[base..base + head_dim]; + + #[cfg(target_arch = "aarch64")] + { + let mut d = 0usize; + while d + 4 <= half { + unsafe { + use core::arch::aarch64::*; + let x1 = vld1q_f32(vec.as_ptr().add(d)); + let x2 = vld1q_f32(vec.as_ptr().add(half + d)); + let cv = vld1q_f32(c.as_ptr().add(d)); + let sv = vld1q_f32(sn.as_ptr().add(d)); + // vec[d] = x1*cos - x2*sin + let new1 = vfmsq_f32(vmulq_f32(x1, cv), x2, sv); + // vec[half+d] = x2*cos + x1*sin (cos[half+d]==cos[d]) + let new2 = vfmaq_f32(vmulq_f32(x2, cv), x1, sv); + vst1q_f32(vec.as_mut_ptr().add(d), new1); + vst1q_f32(vec.as_mut_ptr().add(half + d), new2); + } + d += 4; + } + while d < half { + let x1 = vec[d]; + let x2 = vec[half + d]; + vec[d] = x1 * c[d] - x2 * sn[d]; + vec[half + d] = x2 * c[d] + x1 * sn[d]; + d += 1; + } + } + + #[cfg(not(target_arch = "aarch64"))] + { + for d in 0..half { + let x1 = vec[d]; + let x2 = vec[half + d]; + vec[d] = x1 * c[d] + (-x2) * sn[d]; + vec[half + d] = x2 * c[half + d] + x1 * sn[half + d]; + } + } + } + } +} + +/// Streaming argmax: finds argmax(W_bf16 @ x) without materializing full logits. +/// Quantize x (f32) to int8 with absmax scaling. Returns (x_int8, scale). +pub fn quantize_f32_to_int8(x: &[f32]) -> (Vec, f32) { + let mut max_abs = 0.0f32; + for &v in x { + max_abs = max_abs.max(v.abs()); + } + let scale = if max_abs > 0.0 { max_abs / 127.0 } else { 1.0 }; + let inv_scale = 127.0 / max_abs.max(1e-10); + let int8: Vec = x + .iter() + .map(|&v| (v * inv_scale).round().clamp(-127.0, 127.0) as i8) + .collect(); + (int8, scale) +} + +/// Quantize BF16 weights to INT8 per-row. Returns (int8_data, per_row_scales). +pub fn quantize_bf16_weights_to_int8( + w_bf16: *const u16, + out_dim: usize, + in_dim: usize, +) -> (Vec, Vec) { + #[cfg(target_arch = "aarch64")] + unsafe { + return neon::quantize_bf16_to_int8(w_bf16, out_dim, in_dim); + } + #[cfg(not(target_arch = "aarch64"))] + { + let mut int8_data = vec![0i8; out_dim * in_dim]; + let mut scales = vec![0.0f32; out_dim]; + let src = unsafe { std::slice::from_raw_parts(w_bf16, out_dim * in_dim) }; + for row in 0..out_dim { + let mut max_abs = 0.0f32; + for k in 0..in_dim { + let v = f32::from_bits((src[row * in_dim + k] as u32) << 16).abs(); + if v > max_abs { + max_abs = v; + } + } + let scale = if max_abs > 0.0 { max_abs / 127.0 } else { 1.0 }; + let inv_scale = 127.0 / max_abs.max(1e-10); + scales[row] = scale; + for k in 0..in_dim { + let v = f32::from_bits((src[row * in_dim + k] as u32) << 16); + int8_data[row * in_dim + k] = (v * inv_scale).round().clamp(-127.0, 127.0) as i8; + } + } + (int8_data, scales) + } +} + +/// INT8 threaded argmax: find argmax(x @ W.T) using INT8 quantized weights. +pub fn argmax_matvec_int8( + x: &[f32], + w_int8: &[i8], + w_scales: &[f32], + in_dim: usize, + out_dim: usize, +) -> usize { + let (x_int8, x_scale) = quantize_f32_to_int8(x); + let n_threads = get_num_threads(); + if n_threads <= 1 || out_dim <= 1 { + return int8_argmax_arch( + x_int8.as_ptr(), + x_scale, + w_int8.as_ptr(), + w_scales, + in_dim, + 0, + out_dim, + ) + .0; + } + + let mut best_indices = vec![0usize; n_threads]; + let mut best_vals = vec![f32::NEG_INFINITY; n_threads]; + + let x_int8_ptr = x_int8.as_ptr() as usize; + let w_int8_ptr = w_int8.as_ptr() as usize; + let bi_ptr = best_indices.as_mut_ptr() as usize; + let bv_ptr = best_vals.as_mut_ptr() as usize; + + parallel_for(|tid, nt| { + let chunk = out_dim.div_ceil(nt); + let start = tid * chunk; + let end = (start + chunk).min(out_dim); + if start >= end { + unsafe { + *(bv_ptr as *mut f32).add(tid) = f32::NEG_INFINITY; + *(bi_ptr as *mut usize).add(tid) = 0; + } + return; + } + + let (best, best_val) = int8_argmax_arch( + x_int8_ptr as *const i8, + x_scale, + w_int8_ptr as *const i8, + w_scales, + in_dim, + start, + end, + ); + unsafe { + *(bi_ptr as *mut usize).add(tid) = best; + *(bv_ptr as *mut f32).add(tid) = best_val; + } + }); + + let mut best = best_indices[0]; + let mut best_val = best_vals[0]; + for i in 1..n_threads { + if best_vals[i] > best_val { + best_val = best_vals[i]; + best = best_indices[i]; + } + } + best +} + +pub fn argmax_matvec_bf16(x: &[f32], w_bf16: *const u16, in_dim: usize, out_dim: usize) -> usize { + let n_threads = get_num_threads(); + if n_threads <= 1 { + let (best, _) = argmax_bf16_range(x, w_bf16, in_dim, 0, out_dim); + return best; + } + + let mut best_indices = vec![0usize; n_threads]; + let mut best_vals = vec![-1e30f32; n_threads]; + + let x_ptr = x.as_ptr() as usize; + let w_ptr = w_bf16 as usize; + let bi_ptr = best_indices.as_mut_ptr() as usize; + let bv_ptr = best_vals.as_mut_ptr() as usize; + + parallel_for(|tid, nt| { + let chunk = out_dim.div_ceil(nt); + let start = tid * chunk; + let end = (start + chunk).min(out_dim); + if start >= end { + unsafe { + *(bv_ptr as *mut f32).add(tid) = -1e30; + *(bi_ptr as *mut usize).add(tid) = 0; + } + return; + } + + let x_local = unsafe { std::slice::from_raw_parts(x_ptr as *const f32, in_dim) }; + let (best, best_val) = argmax_bf16_range(x_local, w_ptr as *const u16, in_dim, start, end); + unsafe { + *(bi_ptr as *mut usize).add(tid) = best; + *(bv_ptr as *mut f32).add(tid) = best_val; + } + }); + + let mut best = best_indices[0]; + let mut best_val = best_vals[0]; + for i in 1..n_threads { + if best_vals[i] > best_val { + best_val = best_vals[i]; + best = best_indices[i]; + } + } + best +} diff --git a/vendor/qwenasr/crates/qwen-asr/src/kernels/neon.rs b/vendor/qwenasr/crates/qwen-asr/src/kernels/neon.rs new file mode 100644 index 0000000..45a2101 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/src/kernels/neon.rs @@ -0,0 +1,1113 @@ +/// ARM NEON implementations of hot kernels. +#[cfg(target_arch = "aarch64")] +use core::arch::aarch64::*; + +/// # Safety +/// Uses NEON intrinsics; caller must ensure slices have equal lengths. +#[cfg(target_arch = "aarch64")] +pub unsafe fn bf16_to_f32_buf(dst: &mut [f32], src: &[u16]) { + let n = src.len(); + let mut i = 0usize; + + while i + 8 <= n { + let raw = vld1q_u16(src.as_ptr().add(i)); + let lo = vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(raw), 16)); + let hi = vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(raw), 16)); + vst1q_f32(dst.as_mut_ptr().add(i), lo); + vst1q_f32(dst.as_mut_ptr().add(i + 4), hi); + i += 8; + } + + while i < n { + dst[i] = f32::from_bits((src[i] as u32) << 16); + i += 1; + } +} + +/// # Safety +/// w_bf16 must point to at least out_dim * in_dim valid bf16 values. +#[cfg(target_arch = "aarch64")] +pub unsafe fn bf16_matvec_fused( + y: &mut [f32], + x: &[f32], + w_bf16: *const u16, + bias: Option<&[f32]>, + in_dim: usize, + out_dim: usize, +) { + let mut o = 0usize; + + // Process 2 output rows at a time + while o + 1 < out_dim { + let w0 = w_bf16.add(o * in_dim); + let w1 = w_bf16.add((o + 1) * in_dim); + let mut s0 = bias.map_or(0.0f32, |b| b[o]); + let mut s1 = bias.map_or(0.0f32, |b| b[o + 1]); + + let mut a0 = vdupq_n_f32(0.0); + let mut a1 = vdupq_n_f32(0.0); + let mut a2 = vdupq_n_f32(0.0); + let mut a3 = vdupq_n_f32(0.0); + let mut b0 = vdupq_n_f32(0.0); + let mut b1 = vdupq_n_f32(0.0); + let mut b2 = vdupq_n_f32(0.0); + let mut b3 = vdupq_n_f32(0.0); + let mut k = 0usize; + + while k + 32 <= in_dim { + let x0 = vld1q_f32(x.as_ptr().add(k)); + let x1 = vld1q_f32(x.as_ptr().add(k + 4)); + let x2 = vld1q_f32(x.as_ptr().add(k + 8)); + let x3 = vld1q_f32(x.as_ptr().add(k + 12)); + let x4 = vld1q_f32(x.as_ptr().add(k + 16)); + let x5 = vld1q_f32(x.as_ptr().add(k + 20)); + let x6 = vld1q_f32(x.as_ptr().add(k + 24)); + let x7 = vld1q_f32(x.as_ptr().add(k + 28)); + + let r0a = vld1q_u16(w0.add(k)); + let r0b = vld1q_u16(w0.add(k + 8)); + let r0c = vld1q_u16(w0.add(k + 16)); + let r0d = vld1q_u16(w0.add(k + 24)); + a0 = vfmaq_f32( + a0, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r0a), 16)), + x0, + ); + a1 = vfmaq_f32( + a1, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r0a), 16)), + x1, + ); + a2 = vfmaq_f32( + a2, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r0b), 16)), + x2, + ); + a3 = vfmaq_f32( + a3, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r0b), 16)), + x3, + ); + a0 = vfmaq_f32( + a0, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r0c), 16)), + x4, + ); + a1 = vfmaq_f32( + a1, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r0c), 16)), + x5, + ); + a2 = vfmaq_f32( + a2, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r0d), 16)), + x6, + ); + a3 = vfmaq_f32( + a3, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r0d), 16)), + x7, + ); + + let r1a = vld1q_u16(w1.add(k)); + let r1b = vld1q_u16(w1.add(k + 8)); + let r1c = vld1q_u16(w1.add(k + 16)); + let r1d = vld1q_u16(w1.add(k + 24)); + b0 = vfmaq_f32( + b0, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r1a), 16)), + x0, + ); + b1 = vfmaq_f32( + b1, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r1a), 16)), + x1, + ); + b2 = vfmaq_f32( + b2, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r1b), 16)), + x2, + ); + b3 = vfmaq_f32( + b3, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r1b), 16)), + x3, + ); + b0 = vfmaq_f32( + b0, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r1c), 16)), + x4, + ); + b1 = vfmaq_f32( + b1, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r1c), 16)), + x5, + ); + b2 = vfmaq_f32( + b2, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r1d), 16)), + x6, + ); + b3 = vfmaq_f32( + b3, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r1d), 16)), + x7, + ); + + k += 32; + } + while k + 8 <= in_dim { + let xv0 = vld1q_f32(x.as_ptr().add(k)); + let xv1 = vld1q_f32(x.as_ptr().add(k + 4)); + let r0 = vld1q_u16(w0.add(k)); + let r1 = vld1q_u16(w1.add(k)); + a0 = vfmaq_f32( + a0, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r0), 16)), + xv0, + ); + a1 = vfmaq_f32( + a1, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r0), 16)), + xv1, + ); + b0 = vfmaq_f32( + b0, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r1), 16)), + xv0, + ); + b1 = vfmaq_f32( + b1, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r1), 16)), + xv1, + ); + k += 8; + } + s0 += vaddvq_f32(vaddq_f32(vaddq_f32(a0, a2), vaddq_f32(a1, a3))); + s1 += vaddvq_f32(vaddq_f32(vaddq_f32(b0, b2), vaddq_f32(b1, b3))); + + while k < in_dim { + let wv0 = f32::from_bits(((*w0.add(k)) as u32) << 16); + let wv1 = f32::from_bits(((*w1.add(k)) as u32) << 16); + s0 += wv0 * x[k]; + s1 += wv1 * x[k]; + k += 1; + } + y[o] = s0; + y[o + 1] = s1; + o += 2; + } + + // Handle remaining odd row + while o < out_dim { + let w_row = w_bf16.add(o * in_dim); + let mut sum = bias.map_or(0.0f32, |b| b[o]); + let mut k = 0usize; + + let mut acc0 = vdupq_n_f32(0.0); + let mut acc1 = vdupq_n_f32(0.0); + while k + 8 <= in_dim { + let bf = vld1q_u16(w_row.add(k)); + acc0 = vfmaq_f32( + acc0, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(bf), 16)), + vld1q_f32(x.as_ptr().add(k)), + ); + acc1 = vfmaq_f32( + acc1, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(bf), 16)), + vld1q_f32(x.as_ptr().add(k + 4)), + ); + k += 8; + } + sum += vaddvq_f32(vaddq_f32(acc0, acc1)); + + while k < in_dim { + let w_val = f32::from_bits(((*w_row.add(k)) as u32) << 16); + sum += w_val * x[k]; + k += 1; + } + y[o] = sum; + o += 1; + } +} + +/// # Safety +/// w_bf16 must point to at least end * in_dim valid bf16 values. +#[cfg(target_arch = "aarch64")] +pub unsafe fn argmax_bf16_range( + x: &[f32], + w_bf16: *const u16, + in_dim: usize, + start: usize, + end: usize, +) -> (usize, f32) { + let mut best = start; + let mut best_val = -1e30f32; + let mut o = start; + + // Process 2 rows at a time + while o + 1 < end { + let w0 = w_bf16.add(o * in_dim); + let w1 = w_bf16.add((o + 1) * in_dim); + let mut a0 = vdupq_n_f32(0.0); + let mut a1 = vdupq_n_f32(0.0); + let mut a2 = vdupq_n_f32(0.0); + let mut a3 = vdupq_n_f32(0.0); + let mut b0 = vdupq_n_f32(0.0); + let mut b1 = vdupq_n_f32(0.0); + let mut b2 = vdupq_n_f32(0.0); + let mut b3 = vdupq_n_f32(0.0); + let mut k = 0usize; + + while k + 32 <= in_dim { + let x0 = vld1q_f32(x.as_ptr().add(k)); + let x1 = vld1q_f32(x.as_ptr().add(k + 4)); + let x2 = vld1q_f32(x.as_ptr().add(k + 8)); + let x3 = vld1q_f32(x.as_ptr().add(k + 12)); + let x4 = vld1q_f32(x.as_ptr().add(k + 16)); + let x5 = vld1q_f32(x.as_ptr().add(k + 20)); + let x6 = vld1q_f32(x.as_ptr().add(k + 24)); + let x7 = vld1q_f32(x.as_ptr().add(k + 28)); + + let r0a = vld1q_u16(w0.add(k)); + let r0b = vld1q_u16(w0.add(k + 8)); + let r0c = vld1q_u16(w0.add(k + 16)); + let r0d = vld1q_u16(w0.add(k + 24)); + a0 = vfmaq_f32( + a0, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r0a), 16)), + x0, + ); + a1 = vfmaq_f32( + a1, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r0a), 16)), + x1, + ); + a2 = vfmaq_f32( + a2, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r0b), 16)), + x2, + ); + a3 = vfmaq_f32( + a3, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r0b), 16)), + x3, + ); + a0 = vfmaq_f32( + a0, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r0c), 16)), + x4, + ); + a1 = vfmaq_f32( + a1, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r0c), 16)), + x5, + ); + a2 = vfmaq_f32( + a2, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r0d), 16)), + x6, + ); + a3 = vfmaq_f32( + a3, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r0d), 16)), + x7, + ); + + let r1a = vld1q_u16(w1.add(k)); + let r1b = vld1q_u16(w1.add(k + 8)); + let r1c = vld1q_u16(w1.add(k + 16)); + let r1d = vld1q_u16(w1.add(k + 24)); + b0 = vfmaq_f32( + b0, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r1a), 16)), + x0, + ); + b1 = vfmaq_f32( + b1, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r1a), 16)), + x1, + ); + b2 = vfmaq_f32( + b2, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r1b), 16)), + x2, + ); + b3 = vfmaq_f32( + b3, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r1b), 16)), + x3, + ); + b0 = vfmaq_f32( + b0, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r1c), 16)), + x4, + ); + b1 = vfmaq_f32( + b1, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r1c), 16)), + x5, + ); + b2 = vfmaq_f32( + b2, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r1d), 16)), + x6, + ); + b3 = vfmaq_f32( + b3, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r1d), 16)), + x7, + ); + + k += 32; + } + + let s0_v = vaddvq_f32(vaddq_f32(vaddq_f32(a0, a2), vaddq_f32(a1, a3))); + let s1_v = vaddvq_f32(vaddq_f32(vaddq_f32(b0, b2), vaddq_f32(b1, b3))); + + let mut s0 = s0_v; + let mut s1 = s1_v; + while k < in_dim { + let wv0 = f32::from_bits(((*w0.add(k)) as u32) << 16); + let wv1 = f32::from_bits(((*w1.add(k)) as u32) << 16); + s0 += wv0 * x[k]; + s1 += wv1 * x[k]; + k += 1; + } + + if s0 > best_val { + best_val = s0; + best = o; + } + if s1 > best_val { + best_val = s1; + best = o + 1; + } + o += 2; + } + + while o < end { + let w_row = w_bf16.add(o * in_dim); + let mut sum = 0.0f32; + let mut k = 0usize; + + let mut acc0 = vdupq_n_f32(0.0); + let mut acc1 = vdupq_n_f32(0.0); + while k + 8 <= in_dim { + let bf = vld1q_u16(w_row.add(k)); + acc0 = vfmaq_f32( + acc0, + vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(bf), 16)), + vld1q_f32(x.as_ptr().add(k)), + ); + acc1 = vfmaq_f32( + acc1, + vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(bf), 16)), + vld1q_f32(x.as_ptr().add(k + 4)), + ); + k += 8; + } + sum += vaddvq_f32(vaddq_f32(acc0, acc1)); + + while k < in_dim { + let w_val = f32::from_bits(((*w_row.add(k)) as u32) << 16); + sum += w_val * x[k]; + k += 1; + } + if sum > best_val { + best_val = sum; + best = o; + } + o += 1; + } + + (best, best_val) +} + +/// # Safety +/// Uses NEON intrinsics; slices must have at least n elements. +#[cfg(target_arch = "aarch64")] +pub unsafe fn dot_f32(a: &[f32], b: &[f32], n: usize) -> f32 { + let mut i = 0usize; + let mut acc0 = vdupq_n_f32(0.0); + let mut acc1 = vdupq_n_f32(0.0); + let mut acc2 = vdupq_n_f32(0.0); + let mut acc3 = vdupq_n_f32(0.0); + + while i + 16 <= n { + acc0 = vfmaq_f32( + acc0, + vld1q_f32(a.as_ptr().add(i)), + vld1q_f32(b.as_ptr().add(i)), + ); + acc1 = vfmaq_f32( + acc1, + vld1q_f32(a.as_ptr().add(i + 4)), + vld1q_f32(b.as_ptr().add(i + 4)), + ); + acc2 = vfmaq_f32( + acc2, + vld1q_f32(a.as_ptr().add(i + 8)), + vld1q_f32(b.as_ptr().add(i + 8)), + ); + acc3 = vfmaq_f32( + acc3, + vld1q_f32(a.as_ptr().add(i + 12)), + vld1q_f32(b.as_ptr().add(i + 12)), + ); + i += 16; + } + + while i + 4 <= n { + acc0 = vfmaq_f32( + acc0, + vld1q_f32(a.as_ptr().add(i)), + vld1q_f32(b.as_ptr().add(i)), + ); + i += 4; + } + + let mut sum = vaddvq_f32(vaddq_f32(vaddq_f32(acc0, acc2), vaddq_f32(acc1, acc3))); + while i < n { + sum += a[i] * b[i]; + i += 1; + } + sum +} + +/// # Safety +/// Uses NEON intrinsics; dst must have at least n elements. +#[cfg(target_arch = "aarch64")] +pub unsafe fn vec_scale_inplace(dst: &mut [f32], scale: f32, n: usize) { + let mut i = 0usize; + let s = vdupq_n_f32(scale); + while i + 8 <= n { + let d0 = vld1q_f32(dst.as_ptr().add(i)); + let d1 = vld1q_f32(dst.as_ptr().add(i + 4)); + vst1q_f32(dst.as_mut_ptr().add(i), vmulq_f32(d0, s)); + vst1q_f32(dst.as_mut_ptr().add(i + 4), vmulq_f32(d1, s)); + i += 8; + } + while i < n { + dst[i] *= scale; + i += 1; + } +} + +/// # Safety +/// Uses NEON intrinsics; dst and src must have at least n elements. +#[cfg(target_arch = "aarch64")] +pub unsafe fn vec_axpy_inplace(dst: &mut [f32], src: &[f32], alpha: f32, n: usize) { + let mut i = 0usize; + let a = vdupq_n_f32(alpha); + while i + 8 <= n { + let d0 = vld1q_f32(dst.as_ptr().add(i)); + let s0 = vld1q_f32(src.as_ptr().add(i)); + let d1 = vld1q_f32(dst.as_ptr().add(i + 4)); + let s1 = vld1q_f32(src.as_ptr().add(i + 4)); + vst1q_f32(dst.as_mut_ptr().add(i), vfmaq_f32(d0, s0, a)); + vst1q_f32(dst.as_mut_ptr().add(i + 4), vfmaq_f32(d1, s1, a)); + i += 8; + } + while i < n { + dst[i] += alpha * src[i]; + i += 1; + } +} + +/// # Safety +/// Uses NEON intrinsics; dst and src must have at least n elements. +#[cfg(target_arch = "aarch64")] +pub unsafe fn vec_scale_add(dst: &mut [f32], src: &[f32], correction: f32, n: usize) { + let mut i = 0usize; + let c = vdupq_n_f32(correction); + while i + 8 <= n { + let d0 = vld1q_f32(dst.as_ptr().add(i)); + let s0 = vld1q_f32(src.as_ptr().add(i)); + let d1 = vld1q_f32(dst.as_ptr().add(i + 4)); + let s1 = vld1q_f32(src.as_ptr().add(i + 4)); + vst1q_f32(dst.as_mut_ptr().add(i), vfmaq_f32(s0, d0, c)); + vst1q_f32(dst.as_mut_ptr().add(i + 4), vfmaq_f32(s1, d1, c)); + i += 8; + } + while i < n { + dst[i] = dst[i] * correction + src[i]; + i += 1; + } +} + +/// NEON-accelerated RMS norm for a single row. +/// +/// # Safety +/// Uses NEON intrinsics; all slices must have at least hidden elements. +#[cfg(target_arch = "aarch64")] +pub unsafe fn rms_norm_row(out: &mut [f32], x: &[f32], weight: &[f32], hidden: usize, eps: f32) { + // Sum of squares + let mut i = 0usize; + let mut acc0 = vdupq_n_f32(0.0); + let mut acc1 = vdupq_n_f32(0.0); + while i + 8 <= hidden { + let x0 = vld1q_f32(x.as_ptr().add(i)); + let x1 = vld1q_f32(x.as_ptr().add(i + 4)); + acc0 = vfmaq_f32(acc0, x0, x0); + acc1 = vfmaq_f32(acc1, x1, x1); + i += 8; + } + let mut sum_sq = vaddvq_f32(vaddq_f32(acc0, acc1)); + while i < hidden { + sum_sq += x[i] * x[i]; + i += 1; + } + + let rms_inv = 1.0 / (sum_sq / hidden as f32 + eps).sqrt(); + let rms_v = vdupq_n_f32(rms_inv); + + // Scale: out = x * rms_inv * weight + i = 0; + while i + 8 <= hidden { + let x0 = vld1q_f32(x.as_ptr().add(i)); + let x1 = vld1q_f32(x.as_ptr().add(i + 4)); + let w0 = vld1q_f32(weight.as_ptr().add(i)); + let w1 = vld1q_f32(weight.as_ptr().add(i + 4)); + vst1q_f32(out.as_mut_ptr().add(i), vmulq_f32(vmulq_f32(x0, rms_v), w0)); + vst1q_f32( + out.as_mut_ptr().add(i + 4), + vmulq_f32(vmulq_f32(x1, rms_v), w1), + ); + i += 8; + } + while i < hidden { + out[i] = x[i] * rms_inv * weight[i]; + i += 1; + } +} + +/// NEON-accelerated in-place RMS norm for a single row: x[i] = x[i] * rms_inv * weight[i]. +/// +/// # Safety +/// Uses NEON intrinsics; x and weight must have at least `hidden` elements. +#[cfg(target_arch = "aarch64")] +pub unsafe fn rms_norm_inplace(x: &mut [f32], weight: &[f32], hidden: usize, eps: f32) { + let ptr = x.as_mut_ptr(); + // Sum of squares + let mut i = 0usize; + let mut acc0 = vdupq_n_f32(0.0); + let mut acc1 = vdupq_n_f32(0.0); + while i + 8 <= hidden { + let x0 = vld1q_f32(ptr.add(i)); + let x1 = vld1q_f32(ptr.add(i + 4)); + acc0 = vfmaq_f32(acc0, x0, x0); + acc1 = vfmaq_f32(acc1, x1, x1); + i += 8; + } + let mut sum_sq = vaddvq_f32(vaddq_f32(acc0, acc1)); + while i < hidden { + sum_sq += *ptr.add(i) * *ptr.add(i); + i += 1; + } + + let rms_inv = 1.0 / (sum_sq / hidden as f32 + eps).sqrt(); + let rms_v = vdupq_n_f32(rms_inv); + + // Scale in-place: x[i] = x[i] * rms_inv * weight[i] + i = 0; + while i + 8 <= hidden { + let x0 = vld1q_f32(ptr.add(i)); + let x1 = vld1q_f32(ptr.add(i + 4)); + let w0 = vld1q_f32(weight.as_ptr().add(i)); + let w1 = vld1q_f32(weight.as_ptr().add(i + 4)); + vst1q_f32(ptr.add(i), vmulq_f32(vmulq_f32(x0, rms_v), w0)); + vst1q_f32(ptr.add(i + 4), vmulq_f32(vmulq_f32(x1, rms_v), w1)); + i += 8; + } + while i < hidden { + *ptr.add(i) = *ptr.add(i) * rms_inv * weight[i]; + i += 1; + } +} + +/// NEON-accelerated layer norm for a single row. +/// +/// # Safety +/// Uses NEON intrinsics; all slices must have at least hidden elements. +#[cfg(target_arch = "aarch64")] +pub unsafe fn layer_norm_row( + out: &mut [f32], + x: &[f32], + weight: &[f32], + bias: &[f32], + hidden: usize, + eps: f32, +) { + // Pass 1: compute mean + let mut i = 0usize; + let mut sum0 = vdupq_n_f32(0.0); + let mut sum1 = vdupq_n_f32(0.0); + while i + 8 <= hidden { + sum0 = vaddq_f32(sum0, vld1q_f32(x.as_ptr().add(i))); + sum1 = vaddq_f32(sum1, vld1q_f32(x.as_ptr().add(i + 4))); + i += 8; + } + let mut mean = vaddvq_f32(vaddq_f32(sum0, sum1)); + while i < hidden { + mean += x[i]; + i += 1; + } + mean /= hidden as f32; + + // Pass 2: compute variance + let mean_v = vdupq_n_f32(mean); + i = 0; + let mut var0 = vdupq_n_f32(0.0); + let mut var1 = vdupq_n_f32(0.0); + while i + 8 <= hidden { + let d0 = vsubq_f32(vld1q_f32(x.as_ptr().add(i)), mean_v); + let d1 = vsubq_f32(vld1q_f32(x.as_ptr().add(i + 4)), mean_v); + var0 = vfmaq_f32(var0, d0, d0); + var1 = vfmaq_f32(var1, d1, d1); + i += 8; + } + let mut var = vaddvq_f32(vaddq_f32(var0, var1)); + while i < hidden { + let d = x[i] - mean; + var += d * d; + i += 1; + } + + let inv_std = 1.0 / (var / hidden as f32 + eps).sqrt(); + let inv_v = vdupq_n_f32(inv_std); + + // Pass 3: normalize + i = 0; + while i + 8 <= hidden { + let x0 = vsubq_f32(vld1q_f32(x.as_ptr().add(i)), mean_v); + let x1 = vsubq_f32(vld1q_f32(x.as_ptr().add(i + 4)), mean_v); + let w0 = vld1q_f32(weight.as_ptr().add(i)); + let w1 = vld1q_f32(weight.as_ptr().add(i + 4)); + let b0 = vld1q_f32(bias.as_ptr().add(i)); + let b1 = vld1q_f32(bias.as_ptr().add(i + 4)); + vst1q_f32( + out.as_mut_ptr().add(i), + vfmaq_f32(b0, vmulq_f32(x0, inv_v), w0), + ); + vst1q_f32( + out.as_mut_ptr().add(i + 4), + vfmaq_f32(b1, vmulq_f32(x1, inv_v), w1), + ); + i += 8; + } + while i < hidden { + out[i] = (x[i] - mean) * inv_std * weight[i] + bias[i]; + i += 1; + } +} + +/// Fast exp approximation using NEON (7th-order polynomial, ~1e-4 relative error for |x| < 88). +#[cfg(target_arch = "aarch64")] +#[inline] +unsafe fn fast_exp_neon(x: float32x4_t) -> float32x4_t { + // exp(x) ≈ 2^(x * log2e) using integer trick + polynomial refinement + let log2e = vdupq_n_f32(std::f32::consts::LOG2_E); + let ln2 = vdupq_n_f32(std::f32::consts::LN_2); + + let val = vmulq_f32(x, log2e); + // Clamp to prevent overflow + let val = vminq_f32(val, vdupq_n_f32(126.0)); + let val = vmaxq_f32(val, vdupq_n_f32(-126.0)); + + // Integer part + let ipart = vcvtq_s32_f32(val); + let fpart = vsubq_f32(val, vcvtq_f32_s32(ipart)); + + // 2^ipart using bit manipulation + let exp_i = vreinterpretq_f32_s32(vshlq_n_s32(vaddq_s32(ipart, vdupq_n_s32(127)), 23)); + + // 2^fpart using polynomial: 1 + fpart*ln2*(1 + fpart*ln2/2*(1 + fpart*ln2/3*(1 + ...))) + let f = vmulq_f32(fpart, ln2); + let c2 = vdupq_n_f32(0.5); + let c3 = vdupq_n_f32(1.0 / 6.0); + let c4 = vdupq_n_f32(1.0 / 24.0); + let c5 = vdupq_n_f32(1.0 / 120.0); + + let mut p = vfmaq_f32(c4, c5, f); + p = vfmaq_f32(c3, p, f); + p = vfmaq_f32(c2, p, f); + p = vfmaq_f32(vdupq_n_f32(1.0), p, f); + p = vfmaq_f32(vdupq_n_f32(1.0), p, f); + + vmulq_f32(exp_i, p) +} + +/// NEON-accelerated exp() in-place using fast polynomial approximation. +/// +/// # Safety +/// Uses NEON intrinsics. +#[cfg(target_arch = "aarch64")] +pub unsafe fn exp_inplace(x: &mut [f32]) { + let n = x.len(); + let mut i = 0usize; + while i + 4 <= n { + let v = vld1q_f32(x.as_ptr().add(i)); + vst1q_f32(x.as_mut_ptr().add(i), fast_exp_neon(v)); + i += 4; + } + while i < n { + x[i] = x[i].exp(); + i += 1; + } +} + +/// NEON-accelerated SwiGLU: out[j] = silu(gate[2j]) * gate[2j+1] for interleaved gate/up. +/// +/// # Safety +/// Uses NEON intrinsics; gate_up must have at least 2*n elements. +#[cfg(target_arch = "aarch64")] +pub unsafe fn swiglu_interleaved(out: &mut [f32], gate_up: &[f32], n: usize) { + let one = vdupq_n_f32(1.0); + let mut j = 0usize; + + // Process 8 elements per iteration (2x float32x4) + while j + 8 <= n { + let pair0 = vld1q_f32(gate_up.as_ptr().add(2 * j)); + let pair1 = vld1q_f32(gate_up.as_ptr().add(2 * j + 4)); + let gates0 = vuzp1q_f32(pair0, pair1); + let ups0 = vuzp2q_f32(pair0, pair1); + + let pair2 = vld1q_f32(gate_up.as_ptr().add(2 * j + 8)); + let pair3 = vld1q_f32(gate_up.as_ptr().add(2 * j + 12)); + let gates1 = vuzp1q_f32(pair2, pair3); + let ups1 = vuzp2q_f32(pair2, pair3); + + let exp0 = fast_exp_neon(vnegq_f32(gates0)); + let exp1 = fast_exp_neon(vnegq_f32(gates1)); + let silu0 = vdivq_f32(gates0, vaddq_f32(one, exp0)); + let silu1 = vdivq_f32(gates1, vaddq_f32(one, exp1)); + + vst1q_f32(out.as_mut_ptr().add(j), vmulq_f32(silu0, ups0)); + vst1q_f32(out.as_mut_ptr().add(j + 4), vmulq_f32(silu1, ups1)); + j += 8; + } + + while j + 4 <= n { + let pair0 = vld1q_f32(gate_up.as_ptr().add(2 * j)); + let pair1 = vld1q_f32(gate_up.as_ptr().add(2 * j + 4)); + let gates = vuzp1q_f32(pair0, pair1); + let ups = vuzp2q_f32(pair0, pair1); + let exp_ng = fast_exp_neon(vnegq_f32(gates)); + let silu_g = vdivq_f32(gates, vaddq_f32(one, exp_ng)); + vst1q_f32(out.as_mut_ptr().add(j), vmulq_f32(silu_g, ups)); + j += 4; + } + + while j < n { + let g = gate_up[2 * j]; + let u = gate_up[2 * j + 1]; + let g_silu = g / (1.0 + (-g).exp()); + out[j] = g_silu * u; + j += 1; + } +} + +/// NEON-accelerated GELU (tanh approximation). +/// +/// # Safety +/// Uses NEON intrinsics; x must have at least n elements. +#[cfg(target_arch = "aarch64")] +pub unsafe fn gelu_inplace(x: &mut [f32], n: usize) { + let half = vdupq_n_f32(0.5); + let one = vdupq_n_f32(1.0); + let coeff = vdupq_n_f32(0.797_884_6); // sqrt(2/pi) + let c3 = vdupq_n_f32(0.044715); + let mut i = 0usize; + + while i + 4 <= n { + let v = vld1q_f32(x.as_ptr().add(i)); + let v3 = vmulq_f32(vmulq_f32(v, v), v); + let inner = vmulq_f32(coeff, vfmaq_f32(v, c3, v3)); // coeff * (v + c3 * v^3) + // tanh(x) ≈ (1 - 2/(exp(2x)+1)) = approximate via fast_exp + let exp2x = fast_exp_neon(vmulq_f32(vdupq_n_f32(2.0), inner)); + let tanh_v = vsubq_f32(one, vdivq_f32(vdupq_n_f32(2.0), vaddq_f32(exp2x, one))); + let result = vmulq_f32(half, vmulq_f32(v, vaddq_f32(one, tanh_v))); + vst1q_f32(x.as_mut_ptr().add(i), result); + i += 4; + } + + while i < n { + let val = x[i]; + let x3 = val * val * val; + let inner = 0.797_884_6_f32 * (val + 0.044715 * x3); + x[i] = 0.5 * val * (1.0 + inner.tanh()); + i += 1; + } +} + +/// Quantize BF16 weight matrix to INT8 per-row with absmax scaling. +/// Returns (int8_data, scales) where scales[row] is the per-row scale factor. +/// +/// # Safety +/// w_bf16 must point to at least out_dim * in_dim valid bf16 values. +/// in_dim must be a multiple of 16 for alignment. +#[cfg(target_arch = "aarch64")] +pub unsafe fn quantize_bf16_to_int8( + w_bf16: *const u16, + out_dim: usize, + in_dim: usize, +) -> (Vec, Vec) { + let mut int8_data = vec![0i8; out_dim * in_dim]; + let mut scales = vec![0.0f32; out_dim]; + + for row in 0..out_dim { + let w_row = w_bf16.add(row * in_dim); + + // Find absmax of the row + let mut k = 0; + let mut vmax = vdupq_n_f32(0.0); + let abs_mask = vdupq_n_u32(0x7FFF_FFFF); + while k + 8 <= in_dim { + let r0 = vld1q_u16(w_row.add(k)); + let f0 = vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r0), 16)); + let f1 = vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r0), 16)); + let a0 = vreinterpretq_f32_u32(vandq_u32(vreinterpretq_u32_f32(f0), abs_mask)); + let a1 = vreinterpretq_f32_u32(vandq_u32(vreinterpretq_u32_f32(f1), abs_mask)); + vmax = vmaxq_f32(vmax, vmaxq_f32(a0, a1)); + k += 8; + } + let mut max_abs = vmaxvq_f32(vmax); + while k < in_dim { + let v = f32::from_bits((*w_row.add(k) as u32) << 16).abs(); + if v > max_abs { + max_abs = v; + } + k += 1; + } + + let scale = if max_abs > 0.0 { max_abs / 127.0 } else { 1.0 }; + let inv_scale = 127.0 / max_abs.max(1e-10); + scales[row] = scale; + + // Quantize row + let dst = &mut int8_data[row * in_dim..(row + 1) * in_dim]; + k = 0; + let inv_s = vdupq_n_f32(inv_scale); + while k + 8 <= in_dim { + let r0 = vld1q_u16(w_row.add(k)); + let f0 = vreinterpretq_f32_u32(vshll_n_u16(vget_low_u16(r0), 16)); + let f1 = vreinterpretq_f32_u32(vshll_n_u16(vget_high_u16(r0), 16)); + let q0 = vcvtq_s32_f32(vmulq_f32(f0, inv_s)); + let q1 = vcvtq_s32_f32(vmulq_f32(f1, inv_s)); + let q16 = vqmovn_s32(q0); + let q16b = vqmovn_s32(q1); + let q8 = vqmovn_s16(vcombine_s16(q16, q16b)); + vst1_s8(dst.as_mut_ptr().add(k) as *mut i8, q8); + k += 8; + } + while k < in_dim { + let v = f32::from_bits((*w_row.add(k) as u32) << 16); + let q = (v * inv_scale).round().clamp(-127.0, 127.0) as i8; + dst[k] = q; + k += 1; + } + } + + (int8_data, scales) +} + +/// SDOT via inline assembly (stable Rust, avoids unstable vdotq_s32) +#[cfg(all(target_arch = "aarch64", target_feature = "dotprod"))] +#[inline(always)] +unsafe fn sdot_s32(mut acc: int32x4_t, a: int8x16_t, b: int8x16_t) -> int32x4_t { + core::arch::asm!( + "sdot {acc:v}.4s, {a:v}.16b, {b:v}.16b", + acc = inout(vreg) acc, + a = in(vreg) a, + b = in(vreg) b, + options(pure, nomem, nostack, preserves_flags), + ); + acc +} + +/// Portable aarch64 fallback when dotprod is unavailable in the target. +/// +/// This keeps the same call signature as the fast SDOT path, but collapses the +/// accumulated value into lane 0 because current callers only consume the final +/// sum via `vaddvq_s32(...)`. +#[cfg(all(target_arch = "aarch64", not(target_feature = "dotprod")))] +#[inline(always)] +unsafe fn sdot_s32(acc: int32x4_t, a: int8x16_t, b: int8x16_t) -> int32x4_t { + let acc_sum = vaddvq_s32(acc); + let a_vals: [i8; 16] = core::mem::transmute(a); + let b_vals: [i8; 16] = core::mem::transmute(b); + let dot = a_vals + .iter() + .zip(b_vals.iter()) + .fold(0i32, |sum, (&lhs, &rhs)| sum + lhs as i32 * rhs as i32); + vsetq_lane_s32(acc_sum + dot, vdupq_n_s32(0), 0) +} + +/// INT8 matvec: y = W_int8 @ x_int8 * (x_scale * w_scales[row]) +/// Produces f32 output. Optionally adds bias (for fused residual add). +/// +/// # Safety +/// Uses NEON SDOT via inline asm. +#[cfg(target_arch = "aarch64")] +pub unsafe fn matvec_int8( + y: &mut [f32], + x_int8: *const i8, + x_scale: f32, + w_int8: *const i8, + w_scales: &[f32], + bias: Option<&[f32]>, + in_dim: usize, + out_dim: usize, +) { + let mut o = 0; + while o + 1 < out_dim { + let w0 = w_int8.add(o * in_dim); + let w1 = w_int8.add((o + 1) * in_dim); + let mut acc0a = vdupq_n_s32(0); + let mut acc0b = vdupq_n_s32(0); + let mut acc1a = vdupq_n_s32(0); + let mut acc1b = vdupq_n_s32(0); + let mut k = 0; + + while k + 32 <= in_dim { + let x0 = vld1q_s8(x_int8.add(k)); + let x1 = vld1q_s8(x_int8.add(k + 16)); + acc0a = sdot_s32(acc0a, x0, vld1q_s8(w0.add(k))); + acc0b = sdot_s32(acc0b, x1, vld1q_s8(w0.add(k + 16))); + acc1a = sdot_s32(acc1a, x0, vld1q_s8(w1.add(k))); + acc1b = sdot_s32(acc1b, x1, vld1q_s8(w1.add(k + 16))); + k += 32; + } + while k + 16 <= in_dim { + let xv = vld1q_s8(x_int8.add(k)); + acc0a = sdot_s32(acc0a, xv, vld1q_s8(w0.add(k))); + acc1a = sdot_s32(acc1a, xv, vld1q_s8(w1.add(k))); + k += 16; + } + + let sum0 = vaddvq_s32(vaddq_s32(acc0a, acc0b)); + let sum1 = vaddvq_s32(vaddq_s32(acc1a, acc1b)); + + let mut v0 = sum0 as f32 * x_scale * w_scales[o]; + let mut v1 = sum1 as f32 * x_scale * w_scales[o + 1]; + + // Scalar tail + while k < in_dim { + let xv = *x_int8.add(k) as i32; + v0 += xv as f32 * (*w0.add(k) as i32) as f32 * x_scale * w_scales[o]; + v1 += xv as f32 * (*w1.add(k) as i32) as f32 * x_scale * w_scales[o + 1]; + k += 1; + } + + if let Some(b) = bias { + v0 += b[o]; + v1 += b[o + 1]; + } + y[o] = v0; + y[o + 1] = v1; + o += 2; + } + while o < out_dim { + let w_row = w_int8.add(o * in_dim); + let mut acc0 = vdupq_n_s32(0); + let mut acc1 = vdupq_n_s32(0); + let mut k = 0; + while k + 32 <= in_dim { + acc0 = sdot_s32(acc0, vld1q_s8(x_int8.add(k)), vld1q_s8(w_row.add(k))); + acc1 = sdot_s32( + acc1, + vld1q_s8(x_int8.add(k + 16)), + vld1q_s8(w_row.add(k + 16)), + ); + k += 32; + } + while k + 16 <= in_dim { + acc0 = sdot_s32(acc0, vld1q_s8(x_int8.add(k)), vld1q_s8(w_row.add(k))); + k += 16; + } + let mut val = vaddvq_s32(vaddq_s32(acc0, acc1)) as f32 * x_scale * w_scales[o]; + while k < in_dim { + val += (*x_int8.add(k) as f32) * (*w_row.add(k) as f32) * x_scale * w_scales[o]; + k += 1; + } + if let Some(b) = bias { + val += b[o]; + } + y[o] = val; + o += 1; + } +} + +/// INT8 argmax: find argmax of x @ W.T where W is int8-quantized. +/// x_int8: quantized input [in_dim], x_scale: input quantization scale +/// w_int8: quantized weights [out_dim, in_dim], w_scales: per-row scales [out_dim] +/// +/// # Safety +/// Uses NEON SDOT via inline asm. in_dim should be a multiple of 16 for best perf. +#[cfg(target_arch = "aarch64")] +pub unsafe fn argmax_int8_range( + x_int8: *const i8, + x_scale: f32, + w_int8: *const i8, + w_scales: &[f32], + in_dim: usize, + start: usize, + end: usize, +) -> (usize, f32) { + let mut best = start; + let mut best_val = -1e30f32; + + for o in start..end { + let w_row = w_int8.add(o * in_dim); + let mut acc0 = vdupq_n_s32(0); + let mut acc1 = vdupq_n_s32(0); + let mut acc2 = vdupq_n_s32(0); + let mut acc3 = vdupq_n_s32(0); + let mut k = 0; + + while k + 64 <= in_dim { + acc0 = sdot_s32(acc0, vld1q_s8(x_int8.add(k)), vld1q_s8(w_row.add(k))); + acc1 = sdot_s32( + acc1, + vld1q_s8(x_int8.add(k + 16)), + vld1q_s8(w_row.add(k + 16)), + ); + acc2 = sdot_s32( + acc2, + vld1q_s8(x_int8.add(k + 32)), + vld1q_s8(w_row.add(k + 32)), + ); + acc3 = sdot_s32( + acc3, + vld1q_s8(x_int8.add(k + 48)), + vld1q_s8(w_row.add(k + 48)), + ); + k += 64; + } + + while k + 16 <= in_dim { + acc0 = sdot_s32(acc0, vld1q_s8(x_int8.add(k)), vld1q_s8(w_row.add(k))); + k += 16; + } + + let sum_i32 = vaddvq_s32(vaddq_s32(vaddq_s32(acc0, acc2), vaddq_s32(acc1, acc3))); + let val = sum_i32 as f32 * x_scale * w_scales[o]; + + // Scalar tail + let mut tail_sum = 0i32; + while k < in_dim { + tail_sum += (*x_int8.add(k) as i32) * (*w_row.add(k) as i32); + k += 1; + } + let val = val + tail_sum as f32 * x_scale * w_scales[o]; + + if val > best_val { + best_val = val; + best = o; + } + } + + (best, best_val) +} diff --git a/vendor/qwenasr/crates/qwen-asr/src/lib.rs b/vendor/qwenasr/crates/qwen-asr/src/lib.rs new file mode 100644 index 0000000..6e0ba48 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/src/lib.rs @@ -0,0 +1,106 @@ +//! CPU-only Qwen3-ASR speech recognition in pure Rust. +//! +//! BLAS and SIMD optimizations are selected automatically at compile time based +//! on the target platform — Accelerate + NEON on macOS/aarch64, OpenBLAS + AVX2 +//! on Linux/x86_64, etc. For best performance on x86_64, build with: +//! +//! ```bash +//! RUSTFLAGS="-C target-cpu=native" cargo build --release +//! ``` +//! +//! **Important:** Always build in release mode (`--release`). Debug builds are +//! 10–50x slower and unusable for real-time inference. +//! +//! # Quick Start +//! +//! ```rust,no_run +//! use qwen_asr::context::QwenCtx; +//! use qwen_asr::transcribe; +//! +//! let mut ctx = QwenCtx::load("qwen3-asr-0.6b").expect("model not found"); +//! let text = transcribe::transcribe(&mut ctx, "audio.wav").unwrap(); +//! println!("{text}"); +//! ``` +//! +//! # Forced Alignment +//! +//! With the aligner model variant you can obtain word-level timestamps for a +//! known transcript: +//! +//! ```rust,no_run +//! use qwen_asr::context::QwenCtx; +//! use qwen_asr::align; +//! +//! let mut ctx = QwenCtx::load("qwen3-aligner-0.6b").expect("aligner model not found"); +//! let samples: Vec = vec![]; // 16 kHz mono f32 PCM +//! let results = align::forced_align(&mut ctx, &samples, "Hello world", "English").unwrap(); +//! for r in &results { +//! println!("{}: {:.0} – {:.0} ms", r.text, r.start_ms, r.end_ms); +//! } +//! ``` +//! +//! # Module Guide +//! +//! | Module | Purpose | +//! |--------|---------| +//! | [`context`] | Engine state — start here with [`context::QwenCtx::load`] | +//! | [`transcribe`] | Offline, segmented, and streaming transcription | +//! | [`audio`] | WAV loading, resampling, mel spectrogram | +//! | [`align`] | Forced alignment (word/character timestamps) | +//! | [`config`] | Model configuration and variant detection | +//! | [`tokenizer`] | GPT-2 byte-level BPE tokenizer | +//! +//! The remaining modules (`encoder`, `decoder`, `kernels`, `safetensors`) are +//! implementation details and not intended for direct use. + +pub mod align; +pub mod audio; +#[cfg(any(feature = "ios", feature = "android", feature = "ffi"))] +pub mod c_api; +pub mod config; +pub mod context; +pub mod decoder; +pub mod encoder; +#[cfg(feature = "android")] +pub mod jni_api; +pub mod kernels; +pub mod safetensors; +pub mod tokenizer; +pub mod transcribe; + +/// Returns a list of compile-time optimization flags enabled for this build. +pub fn optimization_flags() -> Vec<&'static str> { + let mut flags = Vec::new(); + + if cfg!(feature = "vdsp") { + flags.push("vDSP/Accelerate"); + } + if cfg!(feature = "blas") && !cfg!(feature = "vdsp") { + flags.push("BLAS"); + } + + // Architecture-specific SIMD + if cfg!(target_arch = "aarch64") { + flags.push("NEON"); + if cfg!(target_feature = "dotprod") { + flags.push("DotProd"); + } + } else if cfg!(target_arch = "x86_64") { + if cfg!(target_feature = "avx2") { + flags.push("AVX2"); + } else if cfg!(target_feature = "avx") { + flags.push("AVX"); + } else if cfg!(target_feature = "sse4.1") { + flags.push("SSE4.1"); + } + if cfg!(target_feature = "fma") { + flags.push("FMA"); + } + } + + if flags.is_empty() { + flags.push("generic"); + } + + flags +} diff --git a/vendor/qwenasr/crates/qwen-asr/src/safetensors.rs b/vendor/qwenasr/crates/qwen-asr/src/safetensors.rs new file mode 100644 index 0000000..3e9b295 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/src/safetensors.rs @@ -0,0 +1,595 @@ +//! Safetensors mmap reader with multi-shard support. + +use std::collections::HashMap; +use std::os::unix::io::RawFd; +use std::path::Path; + +const MAX_TENSORS: usize = 1024; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Dtype { + F32, + F16, + BF16, + I32, + I64, + Bool, + Unknown, +} + +impl Dtype { + fn from_str(s: &str) -> Self { + match s { + "F32" => Dtype::F32, + "F16" => Dtype::F16, + "BF16" => Dtype::BF16, + "I32" => Dtype::I32, + "I64" => Dtype::I64, + "BOOL" => Dtype::Bool, + _ => Dtype::Unknown, + } + } + + pub fn element_size(&self) -> usize { + match self { + Dtype::F32 | Dtype::I32 => 4, + Dtype::F16 | Dtype::BF16 => 2, + Dtype::I64 => 8, + Dtype::Bool => 1, + Dtype::Unknown => 0, + } + } +} + +#[derive(Clone, Debug)] +pub struct TensorMeta { + pub name: String, + pub dtype: Dtype, + pub shape: Vec, + pub data_offset: usize, + pub data_size: usize, +} + +impl TensorMeta { + pub fn numel(&self) -> usize { + self.shape.iter().product::() as usize + } +} + +pub struct SafetensorsFile { + _fd: RawFd, + data: *mut u8, + file_size: usize, + header_size: usize, + pub tensors: Vec, + tensor_map: HashMap, +} + +unsafe impl Send for SafetensorsFile {} +unsafe impl Sync for SafetensorsFile {} + +impl SafetensorsFile { + pub fn open(path: &str) -> Option { + use libc::*; + use std::ffi::CString; + + let c_path = CString::new(path).ok()?; + let fd = unsafe { open(c_path.as_ptr(), O_RDONLY) }; + if fd < 0 { + return None; + } + + let mut stat_buf = unsafe { std::mem::zeroed::() }; + if unsafe { fstat(fd, &mut stat_buf) } < 0 { + unsafe { close(fd); } + return None; + } + + let file_size = stat_buf.st_size as usize; + if file_size < 8 { + unsafe { close(fd); } + return None; + } + + let data = unsafe { + mmap( + std::ptr::null_mut(), + file_size, + PROT_READ, + MAP_PRIVATE, + fd, + 0, + ) + }; + // Keep fd open for mmap lifetime? Actually mmap doesn't need it. + // But we close it since MAP_PRIVATE doesn't need fd after mmap. + let raw_fd = fd; + unsafe { close(fd); } + + if data == libc::MAP_FAILED { + return None; + } + let data = data as *mut u8; + + // Read header size (first 8 bytes, little-endian u64) + let header_size = unsafe { + let mut buf = [0u8; 8]; + std::ptr::copy_nonoverlapping(data, buf.as_mut_ptr(), 8); + u64::from_le_bytes(buf) as usize + }; + + if header_size > file_size - 8 { + unsafe { munmap(data as *mut _, file_size); } + return None; + } + + // Parse JSON header + let header_json = unsafe { + let slice = std::slice::from_raw_parts(data.add(8), header_size); + std::str::from_utf8(slice).ok()? + }; + + let tensors = parse_header(header_json)?; + let mut tensor_map = HashMap::new(); + for (i, t) in tensors.iter().enumerate() { + tensor_map.insert(t.name.clone(), i); + } + + Some(SafetensorsFile { + _fd: raw_fd, + data, + file_size, + header_size, + tensors, + tensor_map, + }) + } + + pub fn find(&self, name: &str) -> Option<&TensorMeta> { + self.tensor_map.get(name).map(|&i| &self.tensors[i]) + } + + pub fn data_ptr(&self, tensor: &TensorMeta) -> *const u8 { + unsafe { self.data.add(8 + self.header_size + tensor.data_offset) } + } + + /// Get tensor data as f32 Vec (converts from BF16 if needed). + pub fn get_f32(&self, tensor: &TensorMeta) -> Option> { + let n = tensor.numel(); + if n == 0 { + return None; + } + + let ptr = self.data_ptr(tensor); + match tensor.dtype { + Dtype::F32 => { + let mut out = vec![0.0f32; n]; + unsafe { + std::ptr::copy_nonoverlapping( + ptr as *const f32, + out.as_mut_ptr(), + n, + ); + } + Some(out) + } + Dtype::BF16 => { + let src = unsafe { std::slice::from_raw_parts(ptr as *const u16, n) }; + let mut out = vec![0.0f32; n]; + for i in 0..n { + out[i] = f32::from_bits((src[i] as u32) << 16); + } + Some(out) + } + _ => None, + } + } + + /// Get direct pointer to BF16 data in mmap. + pub fn get_bf16_direct(&self, tensor: &TensorMeta) -> Option<*const u16> { + if tensor.dtype != Dtype::BF16 { + return None; + } + Some(self.data_ptr(tensor) as *const u16) + } +} + +impl Drop for SafetensorsFile { + fn drop(&mut self) { + if !self.data.is_null() { + unsafe { + libc::munmap(self.data as *mut _, self.file_size); + } + } + } +} + +pub struct MultiSafetensors { + pub shards: Vec, +} + +impl MultiSafetensors { + pub fn open(model_dir: &str) -> Option { + // Try single file first + let single_path = format!("{}/model.safetensors", model_dir); + if let Some(sf) = SafetensorsFile::open(&single_path) { + return Some(MultiSafetensors { shards: vec![sf] }); + } + + // Scan directory for shard files + let dir = Path::new(model_dir); + let mut shard_names: Vec = Vec::new(); + + if let Ok(entries) = std::fs::read_dir(dir) { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if name.starts_with("model-") && name.ends_with(".safetensors") { + shard_names.push(name); + } + } + } + + if shard_names.is_empty() { + eprintln!("multi_safetensors_open: no safetensors files in {}", model_dir); + return None; + } + + shard_names.sort(); + + let mut shards = Vec::new(); + for name in &shard_names { + let path = format!("{}/{}", model_dir, name); + match SafetensorsFile::open(&path) { + Some(sf) => shards.push(sf), + None => { + eprintln!("multi_safetensors_open: failed to open {}", path); + return None; + } + } + } + + Some(MultiSafetensors { shards }) + } + + /// Find a tensor by name across all shards. + /// Returns (shard_index, TensorMeta). + pub fn find(&self, name: &str) -> Option<(usize, &TensorMeta)> { + for (si, shard) in self.shards.iter().enumerate() { + if let Some(t) = shard.find(name) { + return Some((si, t)); + } + } + None + } + + /// Convenience: get f32 data for a named tensor. + pub fn get_f32(&self, name: &str) -> Option> { + let (si, t) = self.find(name)?; + self.shards[si].get_f32(t) + } + + /// Convenience: get direct BF16 pointer for a named tensor. + pub fn get_bf16_direct(&self, name: &str) -> Option<*const u16> { + let (si, t) = self.find(name)?; + self.shards[si].get_bf16_direct(t) + } + + /// Check if a tensor exists. + pub fn has_tensor(&self, name: &str) -> bool { + self.find(name).is_some() + } +} + +// ======================================================================== +// Minimal JSON parser for safetensors header +// ======================================================================== + +fn parse_header(json: &str) -> Option> { + let mut tensors = Vec::new(); + let bytes = json.as_bytes(); + let mut pos = 0; + + skip_whitespace(bytes, &mut pos); + if pos >= bytes.len() || bytes[pos] != b'{' { + return None; + } + pos += 1; + + loop { + skip_whitespace(bytes, &mut pos); + if pos >= bytes.len() { + break; + } + if bytes[pos] == b'}' { + break; + } + if bytes[pos] == b',' { + pos += 1; + continue; + } + + // Parse key + let key = parse_json_string(bytes, &mut pos)?; + skip_whitespace(bytes, &mut pos); + if pos >= bytes.len() || bytes[pos] != b':' { + return None; + } + pos += 1; + + if key == "__metadata__" { + skip_json_value(bytes, &mut pos); + continue; + } + + // Parse tensor entry + let tensor = parse_tensor_entry(bytes, &mut pos, &key)?; + tensors.push(tensor); + + if tensors.len() >= MAX_TENSORS { + break; + } + } + + Some(tensors) +} + +fn skip_whitespace(bytes: &[u8], pos: &mut usize) { + while *pos < bytes.len() { + match bytes[*pos] { + b' ' | b'\n' | b'\r' | b'\t' => *pos += 1, + _ => break, + } + } +} + +fn parse_json_string(bytes: &[u8], pos: &mut usize) -> Option { + skip_whitespace(bytes, pos); + if *pos >= bytes.len() || bytes[*pos] != b'"' { + return None; + } + *pos += 1; + + let mut result = Vec::new(); + while *pos < bytes.len() && bytes[*pos] != b'"' { + if bytes[*pos] == b'\\' { + *pos += 1; + if *pos >= bytes.len() { + return None; + } + match bytes[*pos] { + b'n' => result.push(b'\n'), + b't' => result.push(b'\t'), + b'"' => result.push(b'"'), + b'\\' => result.push(b'\\'), + b'/' => result.push(b'/'), + b'u' => { + *pos += 1; + let mut cp = 0u32; + for _ in 0..4 { + if *pos >= bytes.len() { + return None; + } + cp <<= 4; + let c = bytes[*pos]; + cp |= match c { + b'0'..=b'9' => (c - b'0') as u32, + b'a'..=b'f' => (c - b'a' + 10) as u32, + b'A'..=b'F' => (c - b'A' + 10) as u32, + _ => return None, + }; + *pos += 1; + } + // Encode cp as UTF-8 + let ch = char::from_u32(cp).unwrap_or('?'); + let mut buf = [0u8; 4]; + let s = ch.encode_utf8(&mut buf); + result.extend_from_slice(s.as_bytes()); + continue; // skip the pos += 1 below + } + other => result.push(other), + } + } else { + result.push(bytes[*pos]); + } + *pos += 1; + } + + if *pos >= bytes.len() || bytes[*pos] != b'"' { + return None; + } + *pos += 1; + + String::from_utf8(result).ok() +} + +fn parse_json_int(bytes: &[u8], pos: &mut usize) -> Option { + skip_whitespace(bytes, pos); + let mut neg = false; + if *pos < bytes.len() && bytes[*pos] == b'-' { + neg = true; + *pos += 1; + } + let mut val: i64 = 0; + let mut found = false; + while *pos < bytes.len() && bytes[*pos].is_ascii_digit() { + val = val * 10 + (bytes[*pos] - b'0') as i64; + *pos += 1; + found = true; + } + if !found { + return None; + } + Some(if neg { -val } else { val }) +} + +fn parse_tensor_entry(bytes: &[u8], pos: &mut usize, name: &str) -> Option { + skip_whitespace(bytes, pos); + if *pos >= bytes.len() || bytes[*pos] != b'{' { + return None; + } + *pos += 1; + + let mut dtype = Dtype::Unknown; + let mut shape = Vec::new(); + let mut data_offset: usize = 0; + let mut data_size: usize = 0; + + loop { + skip_whitespace(bytes, pos); + if *pos >= bytes.len() { + break; + } + if bytes[*pos] == b'}' { + *pos += 1; + break; + } + if bytes[*pos] == b',' { + *pos += 1; + continue; + } + + let key = parse_json_string(bytes, pos)?; + skip_whitespace(bytes, pos); + if *pos >= bytes.len() || bytes[*pos] != b':' { + return None; + } + *pos += 1; + skip_whitespace(bytes, pos); + + match key.as_str() { + "dtype" => { + let dtype_str = parse_json_string(bytes, pos)?; + dtype = Dtype::from_str(&dtype_str); + } + "shape" => { + if *pos >= bytes.len() || bytes[*pos] != b'[' { + return None; + } + *pos += 1; + loop { + skip_whitespace(bytes, pos); + if *pos >= bytes.len() { + break; + } + if bytes[*pos] == b']' { + *pos += 1; + break; + } + if bytes[*pos] == b',' { + *pos += 1; + continue; + } + let dim = parse_json_int(bytes, pos)?; + shape.push(dim); + } + } + "data_offsets" => { + if *pos >= bytes.len() || bytes[*pos] != b'[' { + return None; + } + *pos += 1; + skip_whitespace(bytes, pos); + let start = parse_json_int(bytes, pos)? as usize; + skip_whitespace(bytes, pos); + if *pos < bytes.len() && bytes[*pos] == b',' { + *pos += 1; + } + skip_whitespace(bytes, pos); + let end = parse_json_int(bytes, pos)? as usize; + skip_whitespace(bytes, pos); + if *pos < bytes.len() && bytes[*pos] == b']' { + *pos += 1; + } + data_offset = start; + data_size = end - start; + } + _ => { + skip_json_value(bytes, pos); + } + } + } + + Some(TensorMeta { + name: name.to_string(), + dtype, + shape, + data_offset, + data_size, + }) +} + +fn skip_json_value(bytes: &[u8], pos: &mut usize) { + skip_whitespace(bytes, pos); + if *pos >= bytes.len() { + return; + } + + match bytes[*pos] { + b'"' => { + *pos += 1; + while *pos < bytes.len() && bytes[*pos] != b'"' { + if bytes[*pos] == b'\\' { + *pos += 1; + } + if *pos < bytes.len() { + *pos += 1; + } + } + if *pos < bytes.len() { + *pos += 1; + } + } + b'[' => { + let mut depth = 1; + *pos += 1; + while *pos < bytes.len() && depth > 0 { + match bytes[*pos] { + b'[' => depth += 1, + b']' => depth -= 1, + b'"' => { + *pos += 1; + while *pos < bytes.len() && bytes[*pos] != b'"' { + if bytes[*pos] == b'\\' { + *pos += 1; + } + if *pos < bytes.len() { + *pos += 1; + } + } + } + _ => {} + } + *pos += 1; + } + } + b'{' => { + let mut depth = 1; + *pos += 1; + while *pos < bytes.len() && depth > 0 { + match bytes[*pos] { + b'{' => depth += 1, + b'}' => depth -= 1, + b'"' => { + *pos += 1; + while *pos < bytes.len() && bytes[*pos] != b'"' { + if bytes[*pos] == b'\\' { + *pos += 1; + } + if *pos < bytes.len() { + *pos += 1; + } + } + } + _ => {} + } + *pos += 1; + } + } + _ => { + // Number, bool, null + while *pos < bytes.len() && bytes[*pos] != b',' && bytes[*pos] != b'}' && bytes[*pos] != b']' { + *pos += 1; + } + } + } +} diff --git a/vendor/qwenasr/crates/qwen-asr/src/tokenizer.rs b/vendor/qwenasr/crates/qwen-asr/src/tokenizer.rs new file mode 100644 index 0000000..9612296 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/src/tokenizer.rs @@ -0,0 +1,627 @@ +//! GPT-2 byte-level BPE tokenizer for Qwen. + +use std::collections::HashMap; + +// GPT-2 bytes-to-unicode mapping +fn init_gpt2_mapping() -> ([i32; 256], [i32; 512]) { + let mut byte_to_unicode = [0i32; 256]; + let mut unicode_to_byte = [-1i32; 512]; + + let mut n = 0i32; + for b in 0..256i32 { + let is_normal = (33..=126).contains(&b) + || (161..=172).contains(&b) + || (174..=255).contains(&b); + + if is_normal { + byte_to_unicode[b as usize] = b; + } else { + byte_to_unicode[b as usize] = 256 + n; + n += 1; + } + } + + for (b, &bu) in byte_to_unicode.iter().enumerate() { + let cp = bu as usize; + if cp < 512 { + unicode_to_byte[cp] = b as i32; + } + } + + (byte_to_unicode, unicode_to_byte) +} + +fn utf8_encode_cp(cp: u32) -> Vec { + if cp < 0x80 { + vec![cp as u8] + } else if cp < 0x800 { + vec![ + (0xC0 | (cp >> 6)) as u8, + (0x80 | (cp & 0x3F)) as u8, + ] + } else { + vec![ + (0xE0 | (cp >> 12)) as u8, + (0x80 | ((cp >> 6) & 0x3F)) as u8, + (0x80 | (cp & 0x3F)) as u8, + ] + } +} + +/// Decode a GPT-2 encoded token string (vocab key) to raw bytes. +/// Returns raw bytes instead of String because a single BPE token may +/// represent only a partial UTF-8 sequence (e.g. 2 of 3 bytes for a CJK +/// character). The caller must accumulate bytes from multiple tokens and +/// convert to UTF-8 only after the full sequence is available. +fn decode_gpt2_token_bytes(token_str: &str, unicode_to_byte: &[i32; 512]) -> Vec { + let mut bytes = Vec::new(); + + for ch in token_str.chars() { + let cp = ch as u32; + if cp < 512 && unicode_to_byte[cp as usize] >= 0 { + bytes.push(unicode_to_byte[cp as usize] as u8); + } else { + bytes.push(b'?'); + } + } + + bytes +} + +/// Convert UTF-8 bytes to GPT-2 byte-level unicode string. +fn text_to_bpe_unicode(text: &str, byte_to_unicode: &[i32; 256]) -> String { + let mut out = String::new(); + for &b in text.as_bytes() { + let cp = byte_to_unicode[b as usize] as u32; + for byte in utf8_encode_cp(cp) { + out.push(byte as char); + } + } + // Actually, we need to push the encoded codepoint as a char + let mut out2 = String::new(); + for &b in text.as_bytes() { + let cp = byte_to_unicode[b as usize] as u32; + if let Some(ch) = char::from_u32(cp) { + out2.push(ch); + } + } + out2 +} + +fn utf8_char_len(c: u8) -> usize { + if c & 0x80 == 0 { 1 } + else if c & 0xE0 == 0xC0 { 2 } + else if c & 0xF0 == 0xE0 { 3 } + else if c & 0xF8 == 0xF0 { 4 } + else { 1 } +} + +fn split_utf8_symbols(s: &str) -> Vec { + let bytes = s.as_bytes(); + let mut syms = Vec::new(); + let mut i = 0; + while i < bytes.len() { + let len = utf8_char_len(bytes[i]); + let end = (i + len).min(bytes.len()); + if let Ok(ch) = std::str::from_utf8(&bytes[i..end]) { + syms.push(ch.to_string()); + } + i = end; + } + syms +} + +pub struct QwenTokenizer { + pub vocab_size: usize, + id_to_text: Vec>, + id_to_bytes: Vec>>, + #[allow(dead_code)] + id_to_bpe: Vec>, + vocab_map: HashMap, + merge_map: HashMap, + byte_to_unicode: [i32; 256], + #[allow(dead_code)] + unicode_to_byte: [i32; 512], +} + +impl QwenTokenizer { + pub fn load(vocab_json_path: &str) -> Option { + let (byte_to_unicode, unicode_to_byte) = init_gpt2_mapping(); + + // Read vocab.json + let json = std::fs::read_to_string(vocab_json_path).ok()?; + + // Parse vocab.json: { "token": id, ... } + let mut max_id = 0i32; + let mut entries: Vec<(String, i32)> = Vec::new(); + + let bytes = json.as_bytes(); + let mut pos = 0; + skip_ws(bytes, &mut pos); + if pos >= bytes.len() || bytes[pos] != b'{' { + return None; + } + pos += 1; + + loop { + skip_ws(bytes, &mut pos); + if pos >= bytes.len() || bytes[pos] == b'}' { + break; + } + if bytes[pos] == b',' { + pos += 1; + continue; + } + + let key = parse_json_string_tok(bytes, &mut pos)?; + skip_ws(bytes, &mut pos); + if pos >= bytes.len() || bytes[pos] != b':' { + return None; + } + pos += 1; + let id = parse_json_int_tok(bytes, &mut pos)? as i32; + + if id > max_id { + max_id = id; + } + entries.push((key, id)); + } + + let vocab_size = (max_id + 1) as usize; + let mut id_to_text = vec![None; vocab_size]; + let mut id_to_bytes: Vec>> = vec![None; vocab_size]; + let mut id_to_bpe = vec![None; vocab_size]; + let mut vocab_map = HashMap::new(); + + for (key, id) in entries { + let idx = id as usize; + if idx < vocab_size { + let raw_bytes = decode_gpt2_token_bytes(&key, &unicode_to_byte); + // id_to_text: lossy UTF-8 for display/legacy use + let text = String::from_utf8_lossy(&raw_bytes).into_owned(); + id_to_text[idx] = Some(text); + id_to_bytes[idx] = Some(raw_bytes); + vocab_map.insert(key.clone(), id); + id_to_bpe[idx] = Some(key); + } + } + + // Load merges.txt + let merge_map = load_merges(vocab_json_path); + + Some(QwenTokenizer { + vocab_size, + id_to_text, + id_to_bytes, + id_to_bpe, + vocab_map, + merge_map, + byte_to_unicode, + unicode_to_byte, + }) + } + + pub fn decode(&self, token_id: i32) -> &str { + if token_id < 0 || token_id as usize >= self.vocab_size { + return ""; + } + match &self.id_to_text[token_id as usize] { + Some(s) => s.as_str(), + None => "", + } + } + + /// Decode a token to its raw bytes. Unlike `decode()`, this preserves + /// partial UTF-8 sequences so that the caller can accumulate bytes from + /// multiple tokens before converting to a valid UTF-8 string. + pub fn decode_bytes(&self, token_id: i32) -> &[u8] { + if token_id < 0 || token_id as usize >= self.vocab_size { + return b""; + } + match &self.id_to_bytes[token_id as usize] { + Some(b) => b.as_slice(), + None => b"", + } + } + + pub fn encode(&self, text: &str) -> Option> { + if text.is_empty() { + return None; + } + + let mapped = text_to_bpe_unicode(text, &self.byte_to_unicode); + let ids = self.encode_bpe_word(&mapped)?; + Some(ids) + } + + fn encode_bpe_word(&self, mapped: &str) -> Option> { + if mapped.is_empty() { + return Some(Vec::new()); + } + + let mut syms = split_utf8_symbols(mapped); + if syms.is_empty() { + return Some(Vec::new()); + } + + while syms.len() > 1 { + let mut best_rank = i32::MAX; + let mut best_i = -1i32; + + for i in 0..syms.len() - 1 { + let pair = format!("{} {}", syms[i], syms[i + 1]); + if let Some(&rank) = self.merge_map.get(&pair) { + if rank < best_rank { + best_rank = rank; + best_i = i as i32; + } + } + } + + if best_i < 0 || best_rank == i32::MAX { + break; + } + + let i = best_i as usize; + let merged = format!("{}{}", syms[i], syms[i + 1]); + syms[i] = merged; + syms.remove(i + 1); + } + + let mut ids = Vec::new(); + for sym in &syms { + let id = self.vocab_map.get(sym.as_str()).copied()?; + ids.push(id); + } + + Some(ids) + } +} + +fn load_merges(vocab_path: &str) -> HashMap { + let mut merge_map = HashMap::new(); + + // Derive merges.txt path from vocab.json path + let merges_path = if let Some(slash) = vocab_path.rfind('/') { + format!("{}/merges.txt", &vocab_path[..slash]) + } else { + "merges.txt".to_string() + }; + + let content = match std::fs::read_to_string(&merges_path) { + Ok(c) => c, + Err(_) => return merge_map, + }; + + let mut rank = 0i32; + for line in content.lines() { + let line = line.trim_end(); + if line.is_empty() || line.starts_with('#') { + continue; + } + if let Some(space_pos) = line.find(' ') { + let a = &line[..space_pos]; + let b = line[space_pos + 1..].trim_start(); + if !a.is_empty() && !b.is_empty() { + let key = format!("{} {}", a, b); + merge_map.insert(key, rank); + rank += 1; + } + } + } + + merge_map +} + +// Minimal JSON parsing helpers +fn skip_ws(bytes: &[u8], pos: &mut usize) { + while *pos < bytes.len() { + match bytes[*pos] { + b' ' | b'\n' | b'\r' | b'\t' => *pos += 1, + _ => break, + } + } +} + +fn parse_json_string_tok(bytes: &[u8], pos: &mut usize) -> Option { + skip_ws(bytes, pos); + if *pos >= bytes.len() || bytes[*pos] != b'"' { + return None; + } + *pos += 1; + + let mut result = Vec::new(); + while *pos < bytes.len() && bytes[*pos] != b'"' { + if bytes[*pos] == b'\\' { + *pos += 1; + if *pos >= bytes.len() { + return None; + } + match bytes[*pos] { + b'n' => result.push(b'\n'), + b't' => result.push(b'\t'), + b'"' => result.push(b'"'), + b'\\' => result.push(b'\\'), + b'/' => result.push(b'/'), + b'u' => { + *pos += 1; + let mut cp = 0u32; + for _ in 0..4 { + if *pos >= bytes.len() { + return None; + } + cp <<= 4; + let c = bytes[*pos]; + cp |= match c { + b'0'..=b'9' => (c - b'0') as u32, + b'a'..=b'f' => (c - b'a' + 10) as u32, + b'A'..=b'F' => (c - b'A' + 10) as u32, + _ => return None, + }; + *pos += 1; + } + if let Some(ch) = char::from_u32(cp) { + let mut buf = [0u8; 4]; + let s = ch.encode_utf8(&mut buf); + result.extend_from_slice(s.as_bytes()); + } + continue; + } + other => result.push(other), + } + } else { + result.push(bytes[*pos]); + } + *pos += 1; + } + + if *pos >= bytes.len() || bytes[*pos] != b'"' { + return None; + } + *pos += 1; + + String::from_utf8(result).ok() +} + +fn parse_json_int_tok(bytes: &[u8], pos: &mut usize) -> Option { + skip_ws(bytes, pos); + let mut neg = false; + if *pos < bytes.len() && bytes[*pos] == b'-' { + neg = true; + *pos += 1; + } + let mut val: i64 = 0; + let mut found = false; + while *pos < bytes.len() && bytes[*pos].is_ascii_digit() { + val = val * 10 + (bytes[*pos] - b'0') as i64; + *pos += 1; + found = true; + } + if !found { + return None; + } + Some(if neg { -val } else { val }) +} + +// ======================================================================== +// Tests +// ======================================================================== + +#[cfg(test)] +mod tests { + use super::*; + + /// Helper: convert raw bytes to a GPT-2 BPE token string using byte→unicode mapping. + fn bytes_to_gpt2_token(raw_bytes: &[u8], byte_to_unicode: &[i32; 256]) -> String { + let mut s = String::new(); + for &b in raw_bytes { + if let Some(ch) = char::from_u32(byte_to_unicode[b as usize] as u32) { + s.push(ch); + } + } + s + } + + // ---------------------------------------------------------------- + // decode_gpt2_token_bytes: round-trip correctness + // ---------------------------------------------------------------- + + #[test] + fn test_roundtrip_ascii() { + let (btu, utb) = init_gpt2_mapping(); + let original = b"hello"; + let token_str = bytes_to_gpt2_token(original, &btu); + let decoded = decode_gpt2_token_bytes(&token_str, &utb); + assert_eq!(decoded, original); + assert_eq!(String::from_utf8(decoded).unwrap(), "hello"); + } + + #[test] + fn test_roundtrip_cjk_full_char() { + let (btu, utb) = init_gpt2_mapping(); + // "地" = UTF-8 [0xE5, 0x9C, 0xB0] + let original = "地".as_bytes(); + let token_str = bytes_to_gpt2_token(original, &btu); + let decoded = decode_gpt2_token_bytes(&token_str, &utb); + assert_eq!(decoded, original); + assert_eq!(String::from_utf8(decoded).unwrap(), "地"); + } + + // ---------------------------------------------------------------- + // Core regression test: split UTF-8 across two BPE tokens + // ---------------------------------------------------------------- + + #[test] + fn test_decode_bytes_split_utf8_cjk() { + // "地" = UTF-8 [0xE5, 0x9C, 0xB0] + // Simulate BPE splitting into two tokens: + // Token A covers bytes [0xE5, 0x9C] (first 2 of 3) + // Token B covers byte [0xB0] (last 1 of 3) + let (btu, utb) = init_gpt2_mapping(); + + let part1_bytes: &[u8] = &[0xE5, 0x9C]; + let part2_bytes: &[u8] = &[0xB0]; + + let token_a = bytes_to_gpt2_token(part1_bytes, &btu); + let token_b = bytes_to_gpt2_token(part2_bytes, &btu); + + let decoded_a = decode_gpt2_token_bytes(&token_a, &utb); + let decoded_b = decode_gpt2_token_bytes(&token_b, &utb); + + // Each part alone is NOT valid UTF-8 + assert!(String::from_utf8(decoded_a.clone()).is_err(), + "Part 1 alone should NOT be valid UTF-8"); + assert!(String::from_utf8(decoded_b.clone()).is_err(), + "Part 2 alone should NOT be valid UTF-8"); + + // But concatenated they form valid UTF-8 for "地" + let mut combined = decoded_a; + combined.extend_from_slice(&decoded_b); + assert_eq!(combined, vec![0xE5, 0x9C, 0xB0]); + assert_eq!(String::from_utf8(combined).unwrap(), "地"); + } + + #[test] + fn test_decode_bytes_split_utf8_2byte_char() { + // "é" = UTF-8 [0xC3, 0xA9] + // Simulate BPE splitting each byte into its own token + let (btu, utb) = init_gpt2_mapping(); + + let part1: &[u8] = &[0xC3]; + let part2: &[u8] = &[0xA9]; + + let decoded_1 = decode_gpt2_token_bytes( + &bytes_to_gpt2_token(part1, &btu), &utb); + let decoded_2 = decode_gpt2_token_bytes( + &bytes_to_gpt2_token(part2, &btu), &utb); + + assert!(String::from_utf8(decoded_1.clone()).is_err()); + assert!(String::from_utf8(decoded_2.clone()).is_err()); + + let mut combined = decoded_1; + combined.extend_from_slice(&decoded_2); + assert_eq!(String::from_utf8(combined).unwrap(), "é"); + } + + #[test] + fn test_decode_bytes_split_utf8_4byte_emoji() { + // "🦀" = UTF-8 [0xF0, 0x9F, 0xA6, 0x80] + // Simulate BPE splitting into two halves + let (btu, utb) = init_gpt2_mapping(); + + let part1: &[u8] = &[0xF0, 0x9F]; + let part2: &[u8] = &[0xA6, 0x80]; + + let decoded_1 = decode_gpt2_token_bytes( + &bytes_to_gpt2_token(part1, &btu), &utb); + let decoded_2 = decode_gpt2_token_bytes( + &bytes_to_gpt2_token(part2, &btu), &utb); + + assert!(String::from_utf8(decoded_1.clone()).is_err()); + assert!(String::from_utf8(decoded_2.clone()).is_err()); + + let mut combined = decoded_1; + combined.extend_from_slice(&decoded_2); + assert_eq!(String::from_utf8(combined).unwrap(), "🦀"); + } + + // ---------------------------------------------------------------- + // Mixed content: ASCII + Emoji + CJK + // ---------------------------------------------------------------- + + #[test] + fn test_decode_bytes_mixed_content_rust_crab_chinese() { + let (btu, utb) = init_gpt2_mapping(); + + let full_text = "Rust🦀真棒"; + let all_bytes = full_text.as_bytes(); + + // Decode each byte as its own token, accumulate + let mut accumulated = Vec::new(); + for &b in all_bytes { + let token_str = bytes_to_gpt2_token(&[b], &btu); + let decoded = decode_gpt2_token_bytes(&token_str, &utb); + accumulated.extend_from_slice(&decoded); + } + + assert_eq!(accumulated, all_bytes); + assert_eq!(String::from_utf8(accumulated).unwrap(), "Rust🦀真棒"); + } + + // ---------------------------------------------------------------- + // Regression: ensure old lossy path would have corrupted + // ---------------------------------------------------------------- + + #[test] + fn test_lossy_corruption_proof() { + // Demonstrate that per-token String::from_utf8_lossy WOULD corrupt, + // while byte-level accumulation does NOT. + let (btu, utb) = init_gpt2_mapping(); + + // "地址" = [E5 9C B0] [E5 9D 80] + // Split: token_a=[E5,9C], token_b=[B0,E5], token_c=[9D,80] + let splits: &[&[u8]] = &[ + &[0xE5, 0x9C], + &[0xB0, 0xE5], + &[0x9D, 0x80], + ]; + + // Old approach: decode each token to String independently (lossy) + let mut lossy_result = String::new(); + for &part in splits { + let token_str = bytes_to_gpt2_token(part, &btu); + let decoded = decode_gpt2_token_bytes(&token_str, &utb); + lossy_result.push_str(&String::from_utf8_lossy(&decoded)); + } + // Lossy approach produces replacement characters + assert!(lossy_result.contains('\u{FFFD}'), + "Lossy per-token decode SHOULD produce U+FFFD, got: {:?}", lossy_result); + assert_ne!(lossy_result, "地址"); + + // New approach: accumulate bytes, convert once + let mut byte_buf = Vec::new(); + for &part in splits { + let token_str = bytes_to_gpt2_token(part, &btu); + let decoded = decode_gpt2_token_bytes(&token_str, &utb); + byte_buf.extend_from_slice(&decoded); + } + let correct_result = String::from_utf8(byte_buf).unwrap(); + assert_eq!(correct_result, "地址"); + assert!(!correct_result.contains('\u{FFFD}')); + } + + // ---------------------------------------------------------------- + // GPT-2 mapping correctness + // ---------------------------------------------------------------- + + #[test] + fn test_gpt2_mapping_all_256_bytes_roundtrip() { + let (btu, utb) = init_gpt2_mapping(); + + // Every byte value 0..255 must survive a round-trip + for b in 0u8..=255 { + let cp = btu[b as usize]; + assert!(cp >= 0 && cp < 512, + "byte {:#04x} mapped to out-of-range codepoint {}", b, cp); + + let recovered = utb[cp as usize]; + assert_eq!(recovered, b as i32, + "byte {:#04x} → cp {} → byte {:#04x} (expected {:#04x})", + b, cp, recovered, b); + } + } + + #[test] + fn test_gpt2_mapping_is_bijective() { + let (btu, _utb) = init_gpt2_mapping(); + + // All 256 codepoints must be distinct + let mut seen = std::collections::HashSet::new(); + for b in 0..256 { + let cp = btu[b]; + assert!(seen.insert(cp), + "byte {} and another byte both map to codepoint {}", b, cp); + } + assert_eq!(seen.len(), 256); + } +} diff --git a/vendor/qwenasr/crates/qwen-asr/src/transcribe.rs b/vendor/qwenasr/crates/qwen-asr/src/transcribe.rs new file mode 100644 index 0000000..53569e0 --- /dev/null +++ b/vendor/qwenasr/crates/qwen-asr/src/transcribe.rs @@ -0,0 +1,1308 @@ +//! Offline, segmented, and streaming transcription orchestration. + +use crate::audio; +use crate::config::*; +use crate::context::QwenCtx; +use crate::decoder::{self, tok_embed_bf16_to_f32}; +use crate::kernels; +use crate::tokenizer::QwenTokenizer; + +use std::time::Instant; + +// Prompt token sequences +const PREFIX_HEAD: &[i32] = &[151644, 8948, 198]; +const PREFIX_TAIL: &[i32] = &[151645, 198, 151644, 872, 198, 151669]; +const SUFFIX_BASE: &[i32] = &[151670, 151645, 198, 151644, 77091, 198]; + +// Streaming robustness constants (matching C reference) +const STREAM_DEGEN_MAX_PERIOD: usize = 6; +const STREAM_DEGEN_MIN_REPEATS: usize = 4; +const STREAM_STALE_CHUNKS: i32 = 4; +const STREAM_RESET_INTERVAL_CHUNKS: i32 = 45; +const STREAM_RESET_CARRY_TOKENS: usize = 24; +const STREAM_MAX_ENC_WINDOWS: usize = 4; + +fn get_time_ms() -> f64 { + // Use monotonic clock + static START: std::sync::OnceLock = std::sync::OnceLock::new(); + let start = START.get_or_init(Instant::now); + start.elapsed().as_secs_f64() * 1000.0 +} + +fn elapsed_ms(t0: f64) -> f64 { + get_time_ms() - t0 +} + +/// Returns `(best_reps, best_period)`: how many times a block of `best_period` +/// tokens repeats at the tail of `tokens`. Used for streaming degeneracy detection. +fn stream_tail_repeat_blocks(tokens: &[i32], max_period: usize) -> (usize, usize) { + let n = tokens.len(); + if n < 2 { return (1, 0); } + let period_cap = (n / 2).min(if max_period > 0 { max_period } else { n / 2 }); + let mut best_reps = 1usize; + let mut best_period = 0usize; + for p in 1..=period_cap { + let mut reps = 1usize; + while (reps + 1) * p <= n { + let a = &tokens[n - (reps + 1) * p .. n - reps * p]; + let b = &tokens[n - reps * p .. n - (reps - 1) * p]; + if a != b { break; } + reps += 1; + } + if reps > best_reps { best_reps = reps; best_period = p; } + } + (best_reps, best_period) +} + +fn load_tokenizer(model_dir: &str) -> Option { + let vocab_path = format!("{}/vocab.json", model_dir); + QwenTokenizer::load(&vocab_path) +} + +/// Transcribe a single segment. Returns (text, n_text_tokens). +fn transcribe_segment( + ctx: &mut QwenCtx, + samples: &[f32], + tokenizer: &QwenTokenizer, + past_tokens: Option<&[i32]>, +) -> Option<(String, i32)> { + let shared = ctx.shared.clone(); + let cfg = &shared.config.clone(); + let dim = cfg.dec_hidden; + let seg_t0 = get_time_ms(); + let mut n_text_tokens = 0i32; + + // Mel spectrogram + let t0 = get_time_ms(); + let (mel, mel_frames) = audio::mel_spectrogram(samples)?; + let mel_ms = elapsed_ms(t0); + + if kernels::verbose() >= 2 { + eprintln!(" Mel: {} frames ({:.0} ms)", mel_frames, mel_ms); + } + + // Encoder + let t0 = get_time_ms(); + let (enc_output, enc_seq_len) = shared.encoder.forward(cfg, &mel, mel_frames, Some(&mut ctx.enc_bufs))?; + let enc_ms = elapsed_ms(t0); + + if kernels::verbose() >= 2 { + eprintln!(" Encoder: {} tokens ({:.0} ms)", enc_seq_len, enc_ms); + } + + if !ctx.prepare_prompt_tokens(tokenizer) { + return None; + } + + // Build input embeddings + let n_prompt_tokens = ctx.prompt_tokens.as_ref().map_or(0, |t| t.len()); + let n_force_prompt_tokens = ctx.force_prompt_tokens.as_ref().map_or(0, |t| t.len()); + let n_past = past_tokens.map_or(0, |t| t.len()); + let n_past_prompt_tokens = if n_past > 0 { n_past + 1 } else { 0 }; // +1 for + + let prefix_len = PREFIX_HEAD.len() + n_prompt_tokens + PREFIX_TAIL.len(); + let suffix_len = SUFFIX_BASE.len() + n_force_prompt_tokens; + let total_seq = prefix_len + enc_seq_len + suffix_len + n_past_prompt_tokens; + + let mut input_embeds = vec![0.0f32; total_seq * dim]; + let tok_emb = shared.decoder.tok_embeddings_bf16; + + // Embed prefix head + let mut off = 0; + for &tok in PREFIX_HEAD { + unsafe { tok_embed_bf16_to_f32(&mut input_embeds[off * dim..(off + 1) * dim], tok_emb, tok, dim) }; + off += 1; + } + + // Optional prompt + if let Some(ref ptoks) = ctx.prompt_tokens { + for &tok in ptoks { + unsafe { tok_embed_bf16_to_f32(&mut input_embeds[off * dim..(off + 1) * dim], tok_emb, tok, dim) }; + off += 1; + } + } + + // Prefix tail + for &tok in PREFIX_TAIL { + unsafe { tok_embed_bf16_to_f32(&mut input_embeds[off * dim..(off + 1) * dim], tok_emb, tok, dim) }; + off += 1; + } + + // Encoder output + for i in 0..enc_seq_len { + input_embeds[(prefix_len + i) * dim..(prefix_len + i + 1) * dim] + .copy_from_slice(&enc_output[i * dim..(i + 1) * dim]); + } + + // Suffix base + let suffix_off = prefix_len + enc_seq_len; + for (i, &tok) in SUFFIX_BASE.iter().enumerate() { + unsafe { tok_embed_bf16_to_f32( + &mut input_embeds[(suffix_off + i) * dim..(suffix_off + i + 1) * dim], + tok_emb, tok, dim, + ) }; + } + + // Force language tokens + if let Some(ref ftoks) = ctx.force_prompt_tokens { + for (i, &tok) in ftoks.iter().enumerate() { + unsafe { tok_embed_bf16_to_f32( + &mut input_embeds[(suffix_off + SUFFIX_BASE.len() + i) * dim + ..(suffix_off + SUFFIX_BASE.len() + i + 1) * dim], + tok_emb, tok, dim, + ) }; + } + } + + // Past text conditioning tokens + let past_off = suffix_off + suffix_len; + if let Some(ptoks) = past_tokens { + for (i, &tok) in ptoks.iter().enumerate() { + unsafe { tok_embed_bf16_to_f32( + &mut input_embeds[(past_off + i) * dim..(past_off + i + 1) * dim], + tok_emb, tok, dim, + ) }; + } + unsafe { tok_embed_bf16_to_f32( + &mut input_embeds[(past_off + ptoks.len()) * dim..(past_off + ptoks.len() + 1) * dim], + tok_emb, TOKEN_ASR_TEXT, dim, + ) }; + } + + // Decoder prefill + let t0 = get_time_ms(); + ctx.kv_cache.len = 0; + let prefill_len = total_seq - 1; + decoder::decoder_prefill( + &shared.decoder, cfg, &mut ctx.kv_cache, &mut ctx.rope_cache, + &mut ctx.dec_bufs, &input_embeds, prefill_len, + ); + + // First token from last prefill position + let last_embed = &input_embeds[prefill_len * dim..(prefill_len + 1) * dim]; + let mut token = decoder::decoder_forward( + &shared.decoder, cfg, &mut ctx.kv_cache, &mut ctx.rope_cache, + &mut ctx.dec_bufs, last_embed, + ); + + let prefill_ms = elapsed_ms(t0); + if kernels::verbose() >= 2 { + eprintln!(" Prefill: {} tokens ({:.0} ms)", total_seq, prefill_ms); + } + + // Autoregressive decode + let t0 = get_time_ms(); + let max_tokens = 2048; + let mut n_generated = 0; + let mut past_asr_text = n_force_prompt_tokens > 0 || n_past > 0; + + let mut text_bytes: Vec = Vec::new(); + let mut tmp_embed = vec![0.0f32; dim]; + + while n_generated < max_tokens { + n_generated += 1; + + if token == TOKEN_ENDOFTEXT || token == TOKEN_IM_END { + break; + } + + if token == TOKEN_ASR_TEXT { + past_asr_text = true; + } else if past_asr_text { + let piece_bytes = tokenizer.decode_bytes(token); + text_bytes.extend_from_slice(piece_bytes); + n_text_tokens += 1; + + if let Some(ref cb) = ctx.token_cb { + // For the callback, provide lossy UTF-8 for display purposes + cb(&String::from_utf8_lossy(piece_bytes)); + } + } + + unsafe { tok_embed_bf16_to_f32(&mut tmp_embed, tok_emb, token, dim) }; + token = decoder::decoder_forward( + &shared.decoder, cfg, &mut ctx.kv_cache, &mut ctx.rope_cache, + &mut ctx.dec_bufs, &tmp_embed, + ); + } + + let decode_ms = elapsed_ms(t0); + if kernels::verbose() >= 2 { + eprintln!( + " Decode: {} tokens ({:.0} ms, {:.1} ms/token)", + n_generated, decode_ms, + if n_generated > 0 { decode_ms / n_generated as f64 } else { 0.0 } + ); + } + + // Trim whitespace — convert accumulated bytes to UTF-8 first + let text = String::from_utf8_lossy(&text_bytes); + let trimmed = text.trim().to_string(); + + ctx.perf_total_ms += elapsed_ms(seg_t0); + ctx.perf_text_tokens += n_text_tokens; + ctx.perf_encode_ms += mel_ms + enc_ms; + ctx.perf_decode_ms += prefill_ms + decode_ms; + + Some((trimmed, n_text_tokens)) +} + +// ======================================================================== +// Segment-based splitting +// ======================================================================== + +fn find_split_point(samples: &[f32], target_sample: usize, search_sec: f32) -> usize { + let search_half = (search_sec * SAMPLE_RATE as f32) as usize; + let lo = target_sample.saturating_sub(search_half); + let hi = (target_sample + search_half).min(samples.len()); + + let win_samples = 1600; // 100ms at 16kHz + let mut best_energy = 1e30f32; + let mut best_center = target_sample; + + let mut pos = lo; + while pos + win_samples <= hi { + let end = (pos + win_samples).min(samples.len()); + let mut energy = 0.0f32; + for &s in samples.iter().take(end).skip(pos) { + energy += s * s; + } + energy /= (end - pos) as f32; + if energy < best_energy { + best_energy = energy; + best_center = pos + (end - pos) / 2; + } + pos += win_samples / 2; + } + + best_center +} + +fn should_insert_boundary_space(prev_ch: u8, next_ch: u8) -> bool { + if prev_ch == 0 || next_ch == 0 { return false; } + if (prev_ch as char).is_whitespace() { return false; } + if (next_ch as char).is_whitespace() { return false; } + if (next_ch as char).is_ascii_punctuation() { return false; } + true +} + +// ======================================================================== +// Public API +// ======================================================================== + +/// Transcribe audio samples (f32, 16 kHz, mono, range [-1, 1]). +/// +/// When `ctx.segment_sec > 0` and the audio exceeds that duration, it is +/// automatically split at low-energy boundaries. +/// Returns `None` if the tokenizer or encoder fails to initialize. +pub fn transcribe_audio(ctx: &mut QwenCtx, samples: &[f32]) -> Option { + let shared = ctx.shared.clone(); + ctx.reset_perf(); + ctx.perf_audio_ms = 1000.0 * samples.len() as f64 / SAMPLE_RATE as f64; + + let audio_samples = if ctx.skip_silence { + let compacted = audio::compact_silence(samples); + if kernels::verbose() >= 1 { + let used_pct = 100.0 * compacted.len() as f32 / samples.len().max(1) as f32; + eprintln!( + "Silence skip: used {:.1}%, skipped {:.1}% ({} -> {} samples)", + used_pct, 100.0 - used_pct, samples.len(), compacted.len() + ); + } + compacted + } else { + samples.to_vec() + }; + + if kernels::verbose() >= 2 { + eprintln!( + "Audio: {} samples ({:.1} seconds)", + audio_samples.len(), + audio_samples.len() as f32 / SAMPLE_RATE as f32 + ); + } + + let tokenizer = load_tokenizer(&shared.model_dir)?; + if !ctx.prepare_prompt_tokens(&tokenizer) { + return None; + } + + let target_samples = (ctx.segment_sec * SAMPLE_RATE as f32) as usize; + let search = ctx.search_sec.min(ctx.segment_sec / 2.0); + let margin_samples = (search * SAMPLE_RATE as f32) as usize; + + // No splitting if segment_sec is 0 or audio fits in one segment + if ctx.segment_sec <= 0.0 || audio_samples.len() <= target_samples + margin_samples { + let (text, _) = transcribe_segment(ctx, &audio_samples, &tokenizer, None)?; + return Some(text); + } + + // Build split points + let mut splits = vec![0usize]; + let mut pos = 0; + while pos + target_samples + margin_samples < audio_samples.len() { + let split = find_split_point(&audio_samples, pos + target_samples, search); + splits.push(split); + pos = split; + if splits.len() >= 127 { break; } + } + let n_splits = splits.len(); + + if kernels::verbose() >= 2 { + eprintln!("Splitting into {} segments", n_splits); + } + + let mut result = String::new(); + let min_samples = SAMPLE_RATE as usize / 2; + let use_past_text = ctx.past_text_conditioning; + + for s in 0..n_splits { + let core_start = splits[s]; + let core_end = if s + 1 < n_splits { splits[s + 1] } else { audio_samples.len() }; + let seg_start = core_start; + let seg_end = core_end; + let seg_samples = seg_end - seg_start; + + if kernels::verbose() >= 2 { + eprintln!( + "Segment {}/{}: {:.1}-{:.1}s ({} samples)", + s + 1, n_splits, + seg_start as f32 / SAMPLE_RATE as f32, + seg_end as f32 / SAMPLE_RATE as f32, + seg_samples + ); + } + + let seg_buf: Vec; + let seg_ptr = if seg_samples < min_samples { + seg_buf = { + let mut buf = vec![0.0f32; min_samples]; + buf[..seg_samples].copy_from_slice(&audio_samples[seg_start..seg_end]); + buf + }; + &seg_buf[..] + } else { + &audio_samples[seg_start..seg_end] + }; + + let past_tokens: Option> = if use_past_text && !result.is_empty() { + tokenizer.encode(&result) + } else { + None + }; + + let (seg_text, _seg_text_tokens) = match transcribe_segment( + ctx, seg_ptr, &tokenizer, past_tokens.as_deref(), + ) { + Some(r) => r, + None => continue, + }; + + if seg_text.is_empty() { continue; } + + let need_space = if !result.is_empty() { + let prev = *result.as_bytes().last().unwrap_or(&0); + let next = *seg_text.as_bytes().first().unwrap_or(&0); + should_insert_boundary_space(prev, next) + } else { + false + }; + + if need_space { + result.push(' '); + if let Some(ref cb) = ctx.token_cb { + cb(" "); + } + } + + if let Some(ref cb) = ctx.token_cb { + if ctx.past_text_conditioning { + cb(&seg_text); + } + } + + result.push_str(&seg_text); + } + + Some(result) +} + +/// Convenience wrapper: load a WAV file and transcribe it. +/// +/// Equivalent to [`audio::load_wav`](crate::audio::load_wav) followed by +/// [`transcribe_audio`]. +pub fn transcribe(ctx: &mut QwenCtx, wav_path: &str) -> Option { + let samples = audio::load_wav(wav_path)?; + transcribe_audio(ctx, &samples) +} + +/// Transcribe from stdin. +pub fn transcribe_stdin(ctx: &mut QwenCtx) -> Option { + let samples = audio::read_pcm_stdin()?; + transcribe_audio(ctx, &samples) +} + +/// Streaming transcription: processes audio in chunks, emitting tokens via +/// `ctx.token_cb` as they become stable. +/// +/// Trades throughput for lower latency compared to offline mode. If no +/// `token_cb` is set, falls back to a single offline decode of the full audio. +pub fn transcribe_stream(ctx: &mut QwenCtx, samples: &[f32]) -> Option { + let shared = ctx.shared.clone(); + let cfg = shared.config.clone(); + let dim = cfg.dec_hidden; + let chunk_samples = (ctx.stream_chunk_sec * SAMPLE_RATE as f32) as usize; + let rollback = ctx.stream_rollback; + let unfixed_chunks = ctx.stream_unfixed_chunks; + let max_new_tokens = if ctx.stream_max_new_tokens > 0 { ctx.stream_max_new_tokens } else { 32 }; + + let audio_samples = if ctx.skip_silence { + audio::compact_silence(samples) + } else { + samples.to_vec() + }; + + ctx.reset_perf(); + ctx.perf_audio_ms = 1000.0 * samples.len() as f64 / SAMPLE_RATE as f64; + + // If no token callback, fall back to offline decode + if ctx.token_cb.is_none() { + if kernels::verbose() >= 2 { + eprintln!("Streaming: no token callback, using direct final refinement"); + } + let tokenizer = load_tokenizer(&shared.model_dir)?; + ctx.prepare_prompt_tokens(&tokenizer); + let (text, _) = transcribe_segment(ctx, &audio_samples, &tokenizer, None)?; + return Some(text); + } + + let tokenizer = load_tokenizer(&shared.model_dir)?; + if !ctx.prepare_prompt_tokens(&tokenizer) { + return None; + } + + let enc_window_frames = cfg.enc_n_window_infer.clamp(100, 800); + let enc_window_samples = enc_window_frames * HOP_LENGTH; + + let tok_emb = shared.decoder.tok_embeddings_bf16; + + let mut raw_tokens: Vec = Vec::new(); + let mut stable_text_tokens: Vec = Vec::new(); + let mut result_bytes: Vec = Vec::new(); + let mut tmp_embed = vec![0.0f32; dim]; + + let mut chunk_idx = 0i32; + let mut audio_cursor = 0usize; + + // Encoder window cache + struct EncWindow { + seq_len: usize, + enc_output: Vec, + } + let mut enc_cache: Vec = Vec::new(); + let mut enc_cached_seq_total = 0usize; + + // Previous prefill embeddings for LCP reuse + let mut prev_prefill_embeds: Vec = Vec::new(); + let mut prev_prefill_len = 0usize; + + // Streaming robustness state + let mut stale_count = 0i32; + let mut prev_tail_snapshot: Vec = Vec::new(); + let mut enc_cache_base_windows = 0usize; + + while audio_cursor < audio_samples.len() { + let chunk_t0 = get_time_ms(); + audio_cursor = (audio_cursor + chunk_samples).min(audio_samples.len()); + let is_final = audio_cursor >= audio_samples.len(); + + // Encoder + let t0 = get_time_ms(); + let full_end = (audio_cursor / enc_window_samples) * enc_window_samples; + + // Cache completed windows (base offset accounts for windows cleared on re-anchor) + while (enc_cache_base_windows + enc_cache.len()) * enc_window_samples < full_end { + let ws = (enc_cache_base_windows + enc_cache.len()) * enc_window_samples; + let (mel, mel_frames) = audio::mel_spectrogram(&audio_samples[ws..ws + enc_window_samples])?; + let (win_enc, win_seq) = shared.encoder.forward(&cfg, &mel, mel_frames, Some(&mut ctx.enc_bufs))?; + enc_cached_seq_total += win_seq; + enc_cache.push(EncWindow { seq_len: win_seq, enc_output: win_enc }); + } + + // Encode partial tail + let mut partial_seq = 0; + let mut partial_enc: Vec = Vec::new(); + if full_end < audio_cursor { + let _partial_samples = audio_cursor - full_end; + if let Some((mel, mel_frames)) = audio::mel_spectrogram(&audio_samples[full_end..audio_cursor]) { + if let Some((enc, seq)) = shared.encoder.forward(&cfg, &mel, mel_frames, Some(&mut ctx.enc_bufs)) { + partial_seq = seq; + partial_enc = enc; + } + } + } + + let enc_seq_len = enc_cached_seq_total + partial_seq; + if enc_seq_len == 0 { + chunk_idx += 1; + continue; + } + + // Assemble encoder output + let mut enc_output = vec![0.0f32; enc_seq_len * dim]; + let mut enc_off = 0; + for w in &enc_cache { + enc_output[enc_off * dim..(enc_off + w.seq_len) * dim] + .copy_from_slice(&w.enc_output); + enc_off += w.seq_len; + } + if partial_seq > 0 { + enc_output[enc_off * dim..(enc_off + partial_seq) * dim] + .copy_from_slice(&partial_enc); + } + + let enc_ms = elapsed_ms(t0); + ctx.perf_encode_ms += enc_ms; + + // Prefix rollback + let n_prefix_tokens = if ctx.past_text_conditioning && chunk_idx >= unfixed_chunks && !raw_tokens.is_empty() { + (raw_tokens.len() as i32 - rollback).max(0) as usize + } else { + 0 + }; + + // Build input embeddings + let n_prompt_tokens = ctx.prompt_tokens.as_ref().map_or(0, |t| t.len()); + let n_force_prompt_tokens = ctx.force_prompt_tokens.as_ref().map_or(0, |t| t.len()); + let prefix_len = PREFIX_HEAD.len() + n_prompt_tokens + PREFIX_TAIL.len(); + let suffix_len = SUFFIX_BASE.len() + n_force_prompt_tokens; + let total_seq = prefix_len + enc_seq_len + suffix_len + n_prefix_tokens; + + let mut input_embeds = vec![0.0f32; total_seq * dim]; + let mut off = 0; + + for &tok in PREFIX_HEAD { + unsafe { tok_embed_bf16_to_f32(&mut input_embeds[off * dim..(off + 1) * dim], tok_emb, tok, dim) }; + off += 1; + } + if let Some(ref ptoks) = ctx.prompt_tokens { + for &tok in ptoks { + unsafe { tok_embed_bf16_to_f32(&mut input_embeds[off * dim..(off + 1) * dim], tok_emb, tok, dim) }; + off += 1; + } + } + for &tok in PREFIX_TAIL { + unsafe { tok_embed_bf16_to_f32(&mut input_embeds[off * dim..(off + 1) * dim], tok_emb, tok, dim) }; + off += 1; + } + + for i in 0..enc_seq_len { + input_embeds[(prefix_len + i) * dim..(prefix_len + i + 1) * dim] + .copy_from_slice(&enc_output[i * dim..(i + 1) * dim]); + } + + let suffix_off = prefix_len + enc_seq_len; + for (i, &tok) in SUFFIX_BASE.iter().enumerate() { + unsafe { tok_embed_bf16_to_f32( + &mut input_embeds[(suffix_off + i) * dim..(suffix_off + i + 1) * dim], + tok_emb, tok, dim, + ) }; + } + if let Some(ref ftoks) = ctx.force_prompt_tokens { + for (i, &tok) in ftoks.iter().enumerate() { + unsafe { tok_embed_bf16_to_f32( + &mut input_embeds[(suffix_off + SUFFIX_BASE.len() + i) * dim + ..(suffix_off + SUFFIX_BASE.len() + i + 1) * dim], + tok_emb, tok, dim, + ) }; + } + } + + let text_off = suffix_off + suffix_len; + for i in 0..n_prefix_tokens { + unsafe { tok_embed_bf16_to_f32( + &mut input_embeds[(text_off + i) * dim..(text_off + i + 1) * dim], + tok_emb, raw_tokens[i], dim, + ) }; + } + + // Decoder prefill with LCP reuse + let t0 = get_time_ms(); + let prefill_len = total_seq - 1; + + let mut reused_prefill = 0; + if !prev_prefill_embeds.is_empty() && prev_prefill_len > 0 { + let cmp_len = prefill_len.min(prev_prefill_len); + let _row_bytes = dim * std::mem::size_of::(); + while reused_prefill < cmp_len { + let a = &prev_prefill_embeds[reused_prefill * dim..(reused_prefill + 1) * dim]; + let b = &input_embeds[reused_prefill * dim..(reused_prefill + 1) * dim]; + if a != b { break; } + reused_prefill += 1; + } + } + + ctx.kv_cache.len = reused_prefill; + let delta_prefill = prefill_len - reused_prefill; + if delta_prefill > 0 { + decoder::decoder_prefill( + &shared.decoder, &cfg, &mut ctx.kv_cache, &mut ctx.rope_cache, + &mut ctx.dec_bufs, + &input_embeds[reused_prefill * dim..], + delta_prefill, + ); + } + + let last_embed = &input_embeds[prefill_len * dim..(prefill_len + 1) * dim]; + let mut token = decoder::decoder_forward( + &shared.decoder, &cfg, &mut ctx.kv_cache, &mut ctx.rope_cache, + &mut ctx.dec_bufs, last_embed, + ); + + // Save for next chunk + prev_prefill_embeds = input_embeds[..prefill_len * dim].to_vec(); + prev_prefill_len = prefill_len; + + let prefill_ms = elapsed_ms(t0); + ctx.perf_decode_ms += prefill_ms; + + // Autoregressive decode + let t0 = get_time_ms(); + let mut chunk_tokens: Vec = Vec::new(); + let mut n_generated = 0; + + while n_generated < max_new_tokens { + n_generated += 1; + if token == TOKEN_ENDOFTEXT || token == TOKEN_IM_END { break; } + chunk_tokens.push(token); + unsafe { tok_embed_bf16_to_f32(&mut tmp_embed, tok_emb, token, dim); } + token = decoder::decoder_forward( + &shared.decoder, &cfg, &mut ctx.kv_cache, &mut ctx.rope_cache, + &mut ctx.dec_bufs, &tmp_embed, + ); + } + + let decode_ms = elapsed_ms(t0); + ctx.perf_decode_ms += decode_ms; + + // Update raw token history + raw_tokens.truncate(n_prefix_tokens); + raw_tokens.extend_from_slice(&chunk_tokens); + + // Streaming degeneracy detection + if raw_tokens == prev_tail_snapshot { + stale_count += 1; + } else { + stale_count = 0; + prev_tail_snapshot = raw_tokens.clone(); + } + let (best_reps, _) = stream_tail_repeat_blocks(&raw_tokens, STREAM_DEGEN_MAX_PERIOD); + let is_degen = stale_count >= STREAM_STALE_CHUNKS + || best_reps >= STREAM_DEGEN_MIN_REPEATS; + + if is_degen { + if kernels::verbose() >= 2 { + eprintln!("[stream degen] reset at chunk {} (stale={}, reps={})", + chunk_idx, stale_count, best_reps); + } + let carry = stable_text_tokens.len().min(STREAM_RESET_CARRY_TOKENS); + let carry_start = stable_text_tokens.len() - carry; + raw_tokens.clear(); + if carry > 0 { + raw_tokens.push(TOKEN_ASR_TEXT); + raw_tokens.extend_from_slice(&stable_text_tokens[carry_start..]); + } + prev_prefill_embeds.clear(); + prev_prefill_len = 0; + stale_count = 0; + prev_tail_snapshot.clear(); + } + + // Periodic re-anchor: reset context every STREAM_RESET_INTERVAL_CHUNKS chunks + if chunk_idx > 0 && chunk_idx % STREAM_RESET_INTERVAL_CHUNKS == 0 { + if kernels::verbose() >= 2 { + eprintln!("[stream reanchor] at chunk {}", chunk_idx); + } + let carry = stable_text_tokens.len().min(STREAM_RESET_CARRY_TOKENS); + let carry_start = stable_text_tokens.len() - carry; + raw_tokens.clear(); + if carry > 0 { + raw_tokens.push(TOKEN_ASR_TEXT); + raw_tokens.extend_from_slice(&stable_text_tokens[carry_start..]); + } + prev_prefill_embeds.clear(); + prev_prefill_len = 0; + stale_count = 0; + prev_tail_snapshot.clear(); + enc_cache_base_windows += enc_cache.len(); + enc_cache.clear(); + enc_cached_seq_total = 0; + } + + // Parse text region + let text_start = if n_force_prompt_tokens == 0 { + raw_tokens.iter().position(|&t| t == TOKEN_ASR_TEXT) + .map(|p| p + 1) + .unwrap_or(0) + } else { + 0 + }; + let n_text_tokens = raw_tokens.len().saturating_sub(text_start); + + // Fixed frontier + let candidate_len = if is_final { + n_text_tokens + } else if chunk_idx >= unfixed_chunks { + (n_text_tokens as i32 - rollback).max(0) as usize + } else { + 0 + }; + + // Monotonic commit + let candidate_tokens = &raw_tokens[text_start..]; + let _lcp = stable_text_tokens.iter().zip(candidate_tokens.iter()) + .take_while(|(a, b)| a == b) + .count(); + + let emit_from = stable_text_tokens.len(); + let emit_to = candidate_len.max(emit_from); + + for i in emit_from..emit_to { + if i < candidate_tokens.len() { + if i >= stable_text_tokens.len() { + stable_text_tokens.push(candidate_tokens[i]); + } + let piece_bytes = tokenizer.decode_bytes(candidate_tokens[i]); + if let Some(ref cb) = ctx.token_cb { + cb(&String::from_utf8_lossy(piece_bytes)); + } + ctx.perf_text_tokens += 1; + result_bytes.extend_from_slice(piece_bytes); + } + } + + ctx.perf_total_ms += elapsed_ms(chunk_t0); + chunk_idx += 1; + } + + Some(String::from_utf8_lossy(&result_bytes).trim().to_string()) +}// ======================================================================== +// Incremental Streaming API +// ======================================================================== + +/// Encoder window cached output. +struct EncWindow { + seq_len: usize, + enc_output: Vec, +} + +/// Persistent state for incremental streaming transcription. +/// +/// Create once, then call [`stream_push_audio`] each time new audio +/// arrives. The state keeps encoder caches, token history, and decoder +/// prefill embeddings so that only *new* work is performed per call. +pub struct StreamState { + // Encoder + enc_cache: Vec, + enc_cached_seq_total: usize, + enc_cache_base_windows: usize, + + // Decoder token history + raw_tokens: Vec, + stable_text_tokens: Vec, + result_bytes: Vec, + + // Prefill LCP reuse + prev_prefill_embeds: Vec, + prev_prefill_len: usize, + + // Streaming robustness + prev_tail_snapshot: Vec, + stale_count: i32, + + // Lazy partial encoding: skip re-encoding every other chunk + last_partial_cursor: usize, + last_partial_enc: Vec, + last_partial_seq: usize, + + // Audio cursor + audio_cursor: usize, + chunk_idx: i32, + + // Tokenizer (loaded once) + tokenizer: Option, + prompt_prepared: bool, +} + +impl Default for StreamState { + fn default() -> Self { + Self::new() + } +} + +impl StreamState { + /// Create a new empty streaming state. + pub fn new() -> Self { + StreamState { + enc_cache: Vec::new(), + enc_cached_seq_total: 0, + enc_cache_base_windows: 0, + raw_tokens: Vec::new(), + stable_text_tokens: Vec::new(), + result_bytes: Vec::new(), + prev_prefill_embeds: Vec::new(), + prev_prefill_len: 0, + prev_tail_snapshot: Vec::new(), + stale_count: 0, + last_partial_cursor: 0, + last_partial_enc: Vec::new(), + last_partial_seq: 0, + audio_cursor: 0, + chunk_idx: 0, + tokenizer: None, + prompt_prepared: false, + } + } + + /// Reset state for a new streaming window (e.g., after 30s limit). + pub fn reset(&mut self) { + self.enc_cache.clear(); + self.enc_cached_seq_total = 0; + self.enc_cache_base_windows = 0; + self.raw_tokens.clear(); + self.stable_text_tokens.clear(); + self.result_bytes.clear(); + self.prev_prefill_embeds.clear(); + self.prev_prefill_len = 0; + self.prev_tail_snapshot.clear(); + self.stale_count = 0; + self.last_partial_cursor = 0; + self.last_partial_enc.clear(); + self.last_partial_seq = 0; + self.audio_cursor = 0; + self.chunk_idx = 0; + // Keep tokenizer and prompt_prepared + } + + /// Get the current stable transcription result. + pub fn text(&self) -> String { + String::from_utf8_lossy(&self.result_bytes).into_owned() + } + + /// Get how many samples have been processed so far. + pub fn audio_cursor(&self) -> usize { + self.audio_cursor + } +} + +/// Process all available new audio incrementally. +/// +/// `samples` is the **full** audio buffer accumulated so far (16 kHz mono f32). +/// Processes all full chunks from `state.audio_cursor` to end of `samples`. +/// When `finalize` is true, also processes any remaining partial chunk and +/// emits all rollback-buffered tokens. +/// Returns the newly emitted text delta (if any). +pub fn stream_push_audio( + ctx: &mut QwenCtx, + samples: &[f32], + state: &mut StreamState, + finalize: bool, +) -> Option { + let shared = ctx.shared.clone(); + let cfg = shared.config.clone(); + let dim = cfg.dec_hidden; + let chunk_samples = (ctx.stream_chunk_sec * SAMPLE_RATE as f32) as usize; + let rollback = ctx.stream_rollback; + let unfixed_chunks = ctx.stream_unfixed_chunks; + let max_new_tokens = if ctx.stream_max_new_tokens > 0 { ctx.stream_max_new_tokens } else { 32 }; + + // Lazy-init tokenizer + if state.tokenizer.is_none() { + state.tokenizer = load_tokenizer(&shared.model_dir); + } + let tokenizer = state.tokenizer.as_ref()?; + + if !state.prompt_prepared { + if !ctx.prepare_prompt_tokens(tokenizer) { + return None; + } + state.prompt_prepared = true; + } + + // Check if we have enough audio for at least one chunk (or finalizing) + let available = samples.len().saturating_sub(state.audio_cursor); + if available < chunk_samples && !finalize { + return Some(String::new()); + } + if available == 0 { + return Some(String::new()); + } + + let enc_window_frames = cfg.enc_n_window_infer.clamp(100, 800); + let enc_window_samples = enc_window_frames * HOP_LENGTH; + let tok_emb = shared.decoder.tok_embeddings_bf16; + let mut tmp_embed = vec![0.0f32; dim]; + let mut delta_bytes: Vec = Vec::new(); + + // ---- Process full chunks, plus remainder if finalizing ---- + while state.audio_cursor < samples.len() { + let remaining = samples.len() - state.audio_cursor; + if remaining < chunk_samples && !finalize { + break; // Wait for more audio + } + + let chunk_t0 = get_time_ms(); + state.audio_cursor = (state.audio_cursor + chunk_samples).min(samples.len()); + let is_final = finalize && state.audio_cursor >= samples.len(); + + // ---- Encoder: only encode new windows ---- + let t0 = get_time_ms(); + let full_end = (state.audio_cursor / enc_window_samples) * enc_window_samples; + + // Cache newly completed windows (base offset accounts for windows cleared on re-anchor) + while (state.enc_cache_base_windows + state.enc_cache.len()) * enc_window_samples < full_end { + let ws = (state.enc_cache_base_windows + state.enc_cache.len()) * enc_window_samples; + let (mel, mel_frames) = audio::mel_spectrogram(&samples[ws..ws + enc_window_samples])?; + let (win_enc, win_seq) = shared.encoder.forward(&cfg, &mel, mel_frames, Some(&mut ctx.enc_bufs))?; + state.enc_cached_seq_total += win_seq; + state.enc_cache.push(EncWindow { seq_len: win_seq, enc_output: win_enc }); + } + + // Encode partial tail — with lazy re-encoding for LCP optimization. + // Only re-encode when enough new audio has accumulated (every 2 chunks), + // on the first chunk, or when finalizing. On skip chunks, the reused + // encoder output gives near-perfect LCP matching, cutting prefill cost. + let enc_update_threshold = chunk_samples * 2; + let partial_age = state.audio_cursor.saturating_sub(state.last_partial_cursor); + let need_encode = state.last_partial_cursor == 0 + || partial_age >= enc_update_threshold + || is_final; + + let partial_seq; + let partial_enc; + if need_encode && full_end < state.audio_cursor { + if let Some((mel, mel_frames)) = audio::mel_spectrogram(&samples[full_end..state.audio_cursor]) { + if let Some((enc, seq)) = shared.encoder.forward(&cfg, &mel, mel_frames, Some(&mut ctx.enc_bufs)) { + partial_seq = seq; + partial_enc = enc; + state.last_partial_cursor = state.audio_cursor; + state.last_partial_enc = partial_enc.clone(); + state.last_partial_seq = partial_seq; + } else { + partial_seq = state.last_partial_seq; + partial_enc = state.last_partial_enc.clone(); + } + } else { + partial_seq = state.last_partial_seq; + partial_enc = state.last_partial_enc.clone(); + } + } else if full_end < state.audio_cursor { + // Reuse previous partial encoding (skip chunk) + partial_seq = state.last_partial_seq; + partial_enc = state.last_partial_enc.clone(); + } else { + partial_seq = 0; + partial_enc = Vec::new(); + } + + let enc_seq_len = state.enc_cached_seq_total + partial_seq; + if enc_seq_len == 0 { + state.chunk_idx += 1; + return Some(String::new()); + } + + // Assemble encoder output (cached windows + partial) + let mut enc_output = vec![0.0f32; enc_seq_len * dim]; + let mut enc_off = 0; + for w in &state.enc_cache { + enc_output[enc_off * dim..(enc_off + w.seq_len) * dim] + .copy_from_slice(&w.enc_output); + enc_off += w.seq_len; + } + if partial_seq > 0 { + enc_output[enc_off * dim..(enc_off + partial_seq) * dim] + .copy_from_slice(&partial_enc); + } + + let enc_ms = elapsed_ms(t0); + ctx.perf_encode_ms += enc_ms; + + // ---- Prefix rollback ---- + let n_prefix_tokens = if ctx.past_text_conditioning + && state.chunk_idx >= unfixed_chunks + && !state.raw_tokens.is_empty() + { + (state.raw_tokens.len() as i32 - rollback).max(0) as usize + } else { + 0 + }; + + // ---- Build input embeddings ---- + let n_prompt_tokens = ctx.prompt_tokens.as_ref().map_or(0, |t| t.len()); + let n_force_prompt_tokens = ctx.force_prompt_tokens.as_ref().map_or(0, |t| t.len()); + let prefix_len = PREFIX_HEAD.len() + n_prompt_tokens + PREFIX_TAIL.len(); + let suffix_len = SUFFIX_BASE.len() + n_force_prompt_tokens; + let total_seq = prefix_len + enc_seq_len + suffix_len + n_prefix_tokens; + + let mut input_embeds = vec![0.0f32; total_seq * dim]; + let mut off = 0; + + for &tok in PREFIX_HEAD { + unsafe { tok_embed_bf16_to_f32(&mut input_embeds[off * dim..(off + 1) * dim], tok_emb, tok, dim); } + off += 1; + } + if let Some(ref ptoks) = ctx.prompt_tokens { + for &tok in ptoks { + unsafe { tok_embed_bf16_to_f32(&mut input_embeds[off * dim..(off + 1) * dim], tok_emb, tok, dim); } + off += 1; + } + } + for &tok in PREFIX_TAIL { + unsafe { tok_embed_bf16_to_f32(&mut input_embeds[off * dim..(off + 1) * dim], tok_emb, tok, dim); } + off += 1; + } + + for i in 0..enc_seq_len { + input_embeds[(prefix_len + i) * dim..(prefix_len + i + 1) * dim] + .copy_from_slice(&enc_output[i * dim..(i + 1) * dim]); + } + + let suffix_off = prefix_len + enc_seq_len; + for (i, &tok) in SUFFIX_BASE.iter().enumerate() { + unsafe { tok_embed_bf16_to_f32( + &mut input_embeds[(suffix_off + i) * dim..(suffix_off + i + 1) * dim], + tok_emb, tok, dim, + ); } + } + if let Some(ref ftoks) = ctx.force_prompt_tokens { + for (i, &tok) in ftoks.iter().enumerate() { + unsafe { tok_embed_bf16_to_f32( + &mut input_embeds[(suffix_off + SUFFIX_BASE.len() + i) * dim + ..(suffix_off + SUFFIX_BASE.len() + i + 1) * dim], + tok_emb, tok, dim, + ); } + } + } + + let text_off = suffix_off + suffix_len; + for i in 0..n_prefix_tokens { + unsafe { tok_embed_bf16_to_f32( + &mut input_embeds[(text_off + i) * dim..(text_off + i + 1) * dim], + tok_emb, state.raw_tokens[i], dim, + ); } + } + + // ---- Decoder prefill with LCP reuse ---- + let t0 = get_time_ms(); + let prefill_len = total_seq - 1; + + let mut reused_prefill = 0; + if !state.prev_prefill_embeds.is_empty() && state.prev_prefill_len > 0 { + let cmp_len = prefill_len.min(state.prev_prefill_len); + while reused_prefill < cmp_len { + let a = &state.prev_prefill_embeds[reused_prefill * dim..(reused_prefill + 1) * dim]; + let b = &input_embeds[reused_prefill * dim..(reused_prefill + 1) * dim]; + if a != b { break; } + reused_prefill += 1; + } + } + + ctx.kv_cache.len = reused_prefill; + let delta_prefill = prefill_len - reused_prefill; + if delta_prefill > 0 { + decoder::decoder_prefill( + &shared.decoder, &cfg, &mut ctx.kv_cache, &mut ctx.rope_cache, + &mut ctx.dec_bufs, + &input_embeds[reused_prefill * dim..], + delta_prefill, + ); + } + + let last_embed = &input_embeds[prefill_len * dim..(prefill_len + 1) * dim]; + let mut token = decoder::decoder_forward( + &shared.decoder, &cfg, &mut ctx.kv_cache, &mut ctx.rope_cache, + &mut ctx.dec_bufs, last_embed, + ); + + // Save for next chunk + state.prev_prefill_embeds = input_embeds[..prefill_len * dim].to_vec(); + state.prev_prefill_len = prefill_len; + + let prefill_ms = elapsed_ms(t0); + ctx.perf_decode_ms += prefill_ms; + + if kernels::verbose() >= 2 { + eprintln!( + " [stream chunk {}] encoder: {:.0}ms, prefill: {}/{} reused ({:.0}ms, delta={})", + state.chunk_idx, enc_ms, reused_prefill, prefill_len, prefill_ms, delta_prefill + ); + } + + // ---- Autoregressive decode ---- + let t0 = get_time_ms(); + let mut chunk_tokens: Vec = Vec::new(); + let mut n_generated = 0; + + while n_generated < max_new_tokens { + n_generated += 1; + if token == TOKEN_ENDOFTEXT || token == TOKEN_IM_END { break; } + chunk_tokens.push(token); + unsafe { tok_embed_bf16_to_f32(&mut tmp_embed, tok_emb, token, dim); } + token = decoder::decoder_forward( + &shared.decoder, &cfg, &mut ctx.kv_cache, &mut ctx.rope_cache, + &mut ctx.dec_bufs, &tmp_embed, + ); + } + + let decode_ms = elapsed_ms(t0); + ctx.perf_decode_ms += decode_ms; + + // ---- Detect speech end (decoder produced EOT on silence) ---- + // When chunk_tokens is empty, the decoder saw silence/end-of-speech. + // Commit ALL remaining rollback-buffered tokens BEFORE truncation, + // since truncate will remove them. + let speech_ended = chunk_tokens.is_empty() + && !state.raw_tokens.is_empty() + && state.chunk_idx >= unfixed_chunks; + + if speech_ended { + // Emit remaining rollback tokens from current raw_tokens (before truncation) + let text_start = if n_force_prompt_tokens == 0 { + state.raw_tokens.iter().position(|&t| t == TOKEN_ASR_TEXT) + .map(|p| p + 1) + .unwrap_or(0) + } else { + 0 + }; + let candidate_tokens = &state.raw_tokens[text_start..]; + let n_text = candidate_tokens.len(); + let emit_from = state.stable_text_tokens.len(); + for i in emit_from..n_text { + if i < candidate_tokens.len() { + if i >= state.stable_text_tokens.len() { + state.stable_text_tokens.push(candidate_tokens[i]); + } + let piece_bytes = tokenizer.decode_bytes(candidate_tokens[i]); + if let Some(ref cb) = ctx.token_cb { + cb(&String::from_utf8_lossy(piece_bytes)); + } + ctx.perf_text_tokens += 1; + state.result_bytes.extend_from_slice(piece_bytes); + delta_bytes.extend_from_slice(piece_bytes); + } + } + } + + // ---- Update raw token history ---- + state.raw_tokens.truncate(n_prefix_tokens); + state.raw_tokens.extend_from_slice(&chunk_tokens); + + // ---- Streaming degeneracy detection ---- + if !speech_ended { + if state.raw_tokens == state.prev_tail_snapshot { + state.stale_count += 1; + } else { + state.stale_count = 0; + state.prev_tail_snapshot = state.raw_tokens.clone(); + } + let (best_reps, _) = stream_tail_repeat_blocks(&state.raw_tokens, STREAM_DEGEN_MAX_PERIOD); + let is_degen = state.stale_count >= STREAM_STALE_CHUNKS + || best_reps >= STREAM_DEGEN_MIN_REPEATS; + + if is_degen { + if kernels::verbose() >= 2 { + eprintln!("[stream degen] reset at chunk {} (stale={}, reps={})", + state.chunk_idx, state.stale_count, best_reps); + } + let carry = state.stable_text_tokens.len().min(STREAM_RESET_CARRY_TOKENS); + let carry_start = state.stable_text_tokens.len() - carry; + state.raw_tokens.clear(); + if carry > 0 { + state.raw_tokens.push(TOKEN_ASR_TEXT); + state.raw_tokens.extend_from_slice(&state.stable_text_tokens[carry_start..]); + } + state.prev_prefill_embeds.clear(); + state.prev_prefill_len = 0; + state.stale_count = 0; + state.prev_tail_snapshot.clear(); + if state.enc_cache.len() >= STREAM_MAX_ENC_WINDOWS { + state.enc_cache_base_windows += state.enc_cache.len(); + state.enc_cache.clear(); + state.enc_cached_seq_total = 0; + } + } + + // Periodic re-anchor: reset context every STREAM_RESET_INTERVAL_CHUNKS chunks + if state.chunk_idx > 0 && state.chunk_idx % STREAM_RESET_INTERVAL_CHUNKS == 0 { + if kernels::verbose() >= 2 { + eprintln!("[stream reanchor] at chunk {}", state.chunk_idx); + } + let carry = state.stable_text_tokens.len().min(STREAM_RESET_CARRY_TOKENS); + let carry_start = state.stable_text_tokens.len() - carry; + state.raw_tokens.clear(); + if carry > 0 { + state.raw_tokens.push(TOKEN_ASR_TEXT); + state.raw_tokens.extend_from_slice(&state.stable_text_tokens[carry_start..]); + } + state.prev_prefill_embeds.clear(); + state.prev_prefill_len = 0; + state.stale_count = 0; + state.prev_tail_snapshot.clear(); + if state.enc_cache.len() >= STREAM_MAX_ENC_WINDOWS { + state.enc_cache_base_windows += state.enc_cache.len(); + state.enc_cache.clear(); + state.enc_cached_seq_total = 0; + } + } + } + + // ---- Parse text region and emit stable tokens (non-speech-ended case) ---- + if !speech_ended { + let text_start = if n_force_prompt_tokens == 0 { + state.raw_tokens.iter().position(|&t| t == TOKEN_ASR_TEXT) + .map(|p| p + 1) + .unwrap_or(0) + } else { + 0 + }; + let n_text_tokens = state.raw_tokens.len().saturating_sub(text_start); + + let candidate_len = if is_final { + n_text_tokens + } else if state.chunk_idx >= unfixed_chunks { + (n_text_tokens as i32 - rollback).max(0) as usize + } else { + 0 + }; + + let candidate_tokens = &state.raw_tokens[text_start..]; + let emit_from = state.stable_text_tokens.len(); + let emit_to = candidate_len.max(emit_from); + + for i in emit_from..emit_to { + if i < candidate_tokens.len() { + if i >= state.stable_text_tokens.len() { + state.stable_text_tokens.push(candidate_tokens[i]); + } + let piece_bytes = tokenizer.decode_bytes(candidate_tokens[i]); + if let Some(ref cb) = ctx.token_cb { + cb(&String::from_utf8_lossy(piece_bytes)); + } + ctx.perf_text_tokens += 1; + state.result_bytes.extend_from_slice(piece_bytes); + delta_bytes.extend_from_slice(piece_bytes); + } + } + } + + ctx.perf_total_ms += elapsed_ms(chunk_t0); + state.chunk_idx += 1; + + // Stop processing after speech ends — no point encoding more silence + if speech_ended { + break; + } + } // end while loop + + Some(String::from_utf8_lossy(&delta_bytes).into_owned()) +} diff --git a/曦云系列_通用GPU_mx-smi使用手册_CN_V14.pdf b/曦云系列_通用GPU_mx-smi使用手册_CN_V14.pdf new file mode 100644 index 0000000..8b90de1 Binary files /dev/null and b/曦云系列_通用GPU_mx-smi使用手册_CN_V14.pdf differ