From dde3e12476f98743f4a6b5ba91ec0ece46dff5ef Mon Sep 17 00:00:00 2001 From: Bifang <915779419@qq.com> Date: Mon, 28 Sep 2026 17:00:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=20Qwen-Asr=20?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E4=BB=93=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 70 + .env.example | 150 + .gitignore | 48 + .pi-crg-mcp/README.md | 36 + .pi-crg-mcp/crg.cmd | 57 + .pi-crg-mcp/main.js | 16 + .pi-crg-mcp/manifest.json | 33 + .pi-crg-mcp/selfcheck.ps1 | 96 + Dockerfile.cpu | 73 + Dockerfile.gpu | 101 + Dockerfile.iluvatar | 51 + Dockerfile.metax | 55 + Dockerfile.mthreads | 40 + README.md | 610 ++ REALTIME_ASR_TROUBLESHOOTING.md | 310 ++ app/__init__.py | 6 + app/api/__init__.py | 5 + app/api/v1/__init__.py | 22 + app/api/v1/asr.py | 481 ++ app/api/v1/meeting.py | 855 +++ app/api/v1/openai_compatible.py | 692 +++ app/api/v1/websocket_asr.py | 47 + app/bootstrap.py | 58 + app/core/__init__.py | 5 + app/core/accelerator.py | 162 + app/core/accelerators/__init__.py | 16 + app/core/accelerators/base.py | 109 + app/core/accelerators/iluvatar.py | 106 + app/core/accelerators/metax.py | 174 + app/core/accelerators/mthreads.py | 114 + app/core/accelerators/nvidia.py | 85 + app/core/config.py | 536 ++ app/core/database.py | 222 + app/core/device.py | 51 + app/core/exceptions.py | 224 + app/core/executor.py | 185 + app/core/hotword_resolver.py | 216 + app/core/logging.py | 342 ++ app/core/security.py | 173 + app/core/task_store.py | 280 + app/core/text_cleanup.py | 199 + app/core/text_cleanup_lexicon.py | 64 + app/infrastructure/__init__.py | 8 + app/infrastructure/model_utils.py | 48 + app/main.py | 283 + app/models/__init__.py | 5 + app/models/asr.py | 321 ++ app/models/common.py | 64 + app/models/websocket_asr.py | 135 + app/services/__init__.py | 5 + app/services/asr/__init__.py | 5 + app/services/asr/audio_validation.py | 24 + app/services/asr/engines/__init__.py | 41 + app/services/asr/engines/base.py | 785 +++ app/services/asr/engines/global_models.py | 141 + app/services/asr/implementations/__init__.py | 5 + app/services/asr/manager.py | 215 + app/services/asr/model_capabilities.py | 230 + app/services/asr/model_plan.py | 105 + app/services/asr/model_selection.py | 93 + app/services/asr/models.json | 70 + .../asr/offline_transcription_service.py | 137 + app/services/asr/qwen3_engine.py | 693 +++ app/services/asr/qwen3_vllm.py | 527 ++ app/services/asr/qwenasr_rust.py | 564 ++ app/services/asr/runtime/__init__.py | 16 + app/services/asr/runtime/local_pool.py | 48 + app/services/asr/runtime/router.py | 241 + app/services/audio/__init__.py | 10 + app/services/audio/audio_service.py | 234 + app/services/qwen3_websocket_asr.py | 2949 ++++++++++ app/services/realtime_speaker_clusterer.py | 640 +++ app/services/speaker_registry.py | 250 + app/utils/__init__.py | 24 + app/utils/audio.py | 528 ++ app/utils/audio_filter.py | 64 + app/utils/audio_splitter.py | 439 ++ app/utils/boot_events.py | 28 + app/utils/common.py | 90 + app/utils/download_models.py | 296 + app/utils/model_loader.py | 520 ++ app/utils/speaker_diarizer.py | 697 +++ app/utils/text_processing.py | 59 + crg-mcp-plugin/README.md | 68 + crg-mcp-plugin/crg.cmd | 57 + crg-mcp-plugin/dist/crg-mcp-1.0.0.piplug | Bin 0 -> 10403 bytes crg-mcp-plugin/main.js | 18 + crg-mcp-plugin/manifest.json | 33 + crg-mcp-plugin/selfcheck.ps1 | 96 + docker-compose-cpu.yml | 35 + docker-compose-iluvatar.yml | 50 + docker-compose-metax.yml | 47 + docker-compose-mthreads.yml | 50 + docker-compose.yml | 40 + docs/README_zh.md | 606 ++ ...EALTIME_WEBSOCKET_PROCESSING_COMPARISON.md | 382 ++ docs/TODO/ENTROPY_REDUCTION_PLAN.md | 66 + docs/TODO/X86_RUST_ALIGN_OPT_PLAN.md | 457 ++ docs/deployment.md | 699 +++ docs/iluvatar_offline_deployment.md | 242 + docs/metax_offline_deployment.md | 234 + docs/mthreads_offline_deployment.md | 53 + docs/realtime_meeting_refactor_report.md | 412 ++ docs/realtime_meeting_websocket.md | 417 ++ docs/realtime_meeting_websocket_customer.md | 297 + docs/国产化兼容适配汇报.md | 95 + environments/cpu/pyproject.toml | 54 + environments/cpu/uv.lock | 2832 ++++++++++ environments/iluvatar/pyproject.toml | 41 + environments/iluvatar/requirements.txt | 24 + environments/metax/pyproject.toml | 45 + environments/metax/requirements.txt | 23 + environments/mthreads/pyproject.toml | 41 + environments/mthreads/requirements.txt | 24 + export_offline_bundle.sh | 562 ++ pyproject.toml | 61 + pyrightconfig.json | 14 + scripts/README.md | 354 ++ scripts/analyze_audio_rms.py | 328 ++ scripts/benchmark/README.md | 198 + scripts/benchmark/__init__.py | 8 + scripts/benchmark/clients/__init__.py | 6 + scripts/benchmark/clients/asr_client.py | 204 + scripts/benchmark/clients/base_client.py | 131 + scripts/benchmark/clients/tts_client.py | 251 + scripts/benchmark/config.py | 81 + scripts/benchmark/metrics/__init__.py | 11 + scripts/benchmark/metrics/models.py | 149 + scripts/benchmark/metrics/statistics.py | 168 + scripts/benchmark/qwen_rust_sensitivity.py | 313 ++ scripts/benchmark/reporters/__init__.py | 5 + .../benchmark/reporters/chart_generator.py | 250 + .../benchmark/reporters/markdown_reporter.py | 183 + scripts/benchmark/run.py | 418 ++ scripts/benchmark/utils/__init__.py | 5 + scripts/benchmark/utils/audio_utils.py | 113 + scripts/benchmark/utils/text_generator.py | 170 + scripts/build_docker.sh | 469 ++ scripts/docker/entrypoint.sh | 615 ++ scripts/docker/init_host_dirs.sh | 18 + scripts/download-models.sh | 201 + scripts/download_models_standalone.py | 287 + scripts/package_vendor_gpu_image.sh | 132 + scripts/prepare-models.sh | 74 + scripts/sync_accel_env.sh | 71 + scripts/sync_cpu_env.sh | 8 + scripts/sync_gpu_env.sh | 8 + scripts/sync_iluvatar_env.sh | 27 + scripts/sync_metax_env.sh | 92 + scripts/sync_mthreads_env.sh | 27 + start.py | 81 + test_diarization.py | 43 + test_speed.py | 34 + test_web/index.html | 4936 +++++++++++++++++ tests/test_hotword_prompt.py | 39 + tests/test_model_integrity.py | 63 + tests/test_offline_model_selection.py | 55 + tests/test_qwenasr_cpu_features.py | 41 + tests/test_text_cleanup.py | 61 + uv.lock | 4744 ++++++++++++++++ vendor/qwenasr/.cargo/config.toml | 14 + vendor/qwenasr/Cargo.lock | 875 +++ vendor/qwenasr/Cargo.toml | 17 + vendor/qwenasr/LICENSE | 21 + vendor/qwenasr/VENDORED_FROM.md | 14 + .../qwenasr/crates/qwen-asr-cli/CHANGELOG.md | 97 + vendor/qwenasr/crates/qwen-asr-cli/Cargo.toml | 23 + vendor/qwenasr/crates/qwen-asr-cli/README.md | 48 + .../crates/qwen-asr-cli/src/download.rs | 316 ++ .../crates/qwen-asr-cli/src/live_capture.rs | 496 ++ .../qwenasr/crates/qwen-asr-cli/src/main.rs | 937 ++++ vendor/qwenasr/crates/qwen-asr/CHANGELOG.md | 88 + vendor/qwenasr/crates/qwen-asr/Cargo.toml | 30 + vendor/qwenasr/crates/qwen-asr/README.md | 177 + vendor/qwenasr/crates/qwen-asr/build.rs | 18 + vendor/qwenasr/crates/qwen-asr/src/align.rs | 389 ++ vendor/qwenasr/crates/qwen-asr/src/audio.rs | 479 ++ vendor/qwenasr/crates/qwen-asr/src/c_api.rs | 386 ++ vendor/qwenasr/crates/qwen-asr/src/config.rs | 182 + vendor/qwenasr/crates/qwen-asr/src/context.rs | 309 ++ vendor/qwenasr/crates/qwen-asr/src/decoder.rs | 1064 ++++ vendor/qwenasr/crates/qwen-asr/src/encoder.rs | 359 ++ vendor/qwenasr/crates/qwen-asr/src/jni_api.rs | 179 + .../crates/qwen-asr/src/kernels/avx.rs | 957 ++++ .../crates/qwen-asr/src/kernels/generic.rs | 78 + .../crates/qwen-asr/src/kernels/mod.rs | 2878 ++++++++++ .../crates/qwen-asr/src/kernels/neon.rs | 1113 ++++ vendor/qwenasr/crates/qwen-asr/src/lib.rs | 106 + .../crates/qwen-asr/src/safetensors.rs | 595 ++ .../qwenasr/crates/qwen-asr/src/tokenizer.rs | 627 +++ .../qwenasr/crates/qwen-asr/src/transcribe.rs | 1308 +++++ 曦云系列_通用GPU_mx-smi使用手册_CN_V14.pdf | Bin 0 -> 1549583 bytes 192 files changed, 56639 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .pi-crg-mcp/README.md create mode 100644 .pi-crg-mcp/crg.cmd create mode 100644 .pi-crg-mcp/main.js create mode 100644 .pi-crg-mcp/manifest.json create mode 100644 .pi-crg-mcp/selfcheck.ps1 create mode 100644 Dockerfile.cpu create mode 100644 Dockerfile.gpu create mode 100644 Dockerfile.iluvatar create mode 100644 Dockerfile.metax create mode 100644 Dockerfile.mthreads create mode 100644 README.md create mode 100644 REALTIME_ASR_TROUBLESHOOTING.md create mode 100644 app/__init__.py create mode 100644 app/api/__init__.py create mode 100644 app/api/v1/__init__.py create mode 100644 app/api/v1/asr.py create mode 100644 app/api/v1/meeting.py create mode 100644 app/api/v1/openai_compatible.py create mode 100644 app/api/v1/websocket_asr.py create mode 100644 app/bootstrap.py create mode 100644 app/core/__init__.py create mode 100644 app/core/accelerator.py create mode 100644 app/core/accelerators/__init__.py create mode 100644 app/core/accelerators/base.py create mode 100644 app/core/accelerators/iluvatar.py create mode 100644 app/core/accelerators/metax.py create mode 100644 app/core/accelerators/mthreads.py create mode 100644 app/core/accelerators/nvidia.py create mode 100644 app/core/config.py create mode 100644 app/core/database.py create mode 100644 app/core/device.py create mode 100644 app/core/exceptions.py create mode 100644 app/core/executor.py create mode 100644 app/core/hotword_resolver.py create mode 100644 app/core/logging.py create mode 100644 app/core/security.py create mode 100644 app/core/task_store.py create mode 100644 app/core/text_cleanup.py create mode 100644 app/core/text_cleanup_lexicon.py create mode 100644 app/infrastructure/__init__.py create mode 100644 app/infrastructure/model_utils.py create mode 100644 app/main.py create mode 100644 app/models/__init__.py create mode 100644 app/models/asr.py create mode 100644 app/models/common.py create mode 100644 app/models/websocket_asr.py create mode 100644 app/services/__init__.py create mode 100644 app/services/asr/__init__.py create mode 100644 app/services/asr/audio_validation.py create mode 100644 app/services/asr/engines/__init__.py create mode 100644 app/services/asr/engines/base.py create mode 100644 app/services/asr/engines/global_models.py create mode 100644 app/services/asr/implementations/__init__.py create mode 100644 app/services/asr/manager.py create mode 100644 app/services/asr/model_capabilities.py create mode 100644 app/services/asr/model_plan.py create mode 100644 app/services/asr/model_selection.py create mode 100644 app/services/asr/models.json create mode 100644 app/services/asr/offline_transcription_service.py create mode 100644 app/services/asr/qwen3_engine.py create mode 100644 app/services/asr/qwen3_vllm.py create mode 100644 app/services/asr/qwenasr_rust.py create mode 100644 app/services/asr/runtime/__init__.py create mode 100644 app/services/asr/runtime/local_pool.py create mode 100644 app/services/asr/runtime/router.py create mode 100644 app/services/audio/__init__.py create mode 100644 app/services/audio/audio_service.py create mode 100644 app/services/qwen3_websocket_asr.py create mode 100644 app/services/realtime_speaker_clusterer.py create mode 100644 app/services/speaker_registry.py create mode 100644 app/utils/__init__.py create mode 100644 app/utils/audio.py create mode 100644 app/utils/audio_filter.py create mode 100644 app/utils/audio_splitter.py create mode 100644 app/utils/boot_events.py create mode 100644 app/utils/common.py create mode 100644 app/utils/download_models.py create mode 100644 app/utils/model_loader.py create mode 100644 app/utils/speaker_diarizer.py create mode 100644 app/utils/text_processing.py create mode 100644 crg-mcp-plugin/README.md create mode 100644 crg-mcp-plugin/crg.cmd create mode 100644 crg-mcp-plugin/dist/crg-mcp-1.0.0.piplug create mode 100644 crg-mcp-plugin/main.js create mode 100644 crg-mcp-plugin/manifest.json create mode 100644 crg-mcp-plugin/selfcheck.ps1 create mode 100644 docker-compose-cpu.yml create mode 100644 docker-compose-iluvatar.yml create mode 100644 docker-compose-metax.yml create mode 100644 docker-compose-mthreads.yml create mode 100644 docker-compose.yml create mode 100644 docs/README_zh.md create mode 100644 docs/REALTIME_WEBSOCKET_PROCESSING_COMPARISON.md create mode 100644 docs/TODO/ENTROPY_REDUCTION_PLAN.md create mode 100644 docs/TODO/X86_RUST_ALIGN_OPT_PLAN.md create mode 100644 docs/deployment.md create mode 100644 docs/iluvatar_offline_deployment.md create mode 100644 docs/metax_offline_deployment.md create mode 100644 docs/mthreads_offline_deployment.md create mode 100644 docs/realtime_meeting_refactor_report.md create mode 100644 docs/realtime_meeting_websocket.md create mode 100644 docs/realtime_meeting_websocket_customer.md create mode 100644 docs/国产化兼容适配汇报.md create mode 100644 environments/cpu/pyproject.toml create mode 100644 environments/cpu/uv.lock create mode 100644 environments/iluvatar/pyproject.toml create mode 100644 environments/iluvatar/requirements.txt create mode 100644 environments/metax/pyproject.toml create mode 100644 environments/metax/requirements.txt create mode 100644 environments/mthreads/pyproject.toml create mode 100644 environments/mthreads/requirements.txt create mode 100644 export_offline_bundle.sh create mode 100644 pyproject.toml create mode 100644 pyrightconfig.json create mode 100644 scripts/README.md create mode 100644 scripts/analyze_audio_rms.py create mode 100644 scripts/benchmark/README.md create mode 100644 scripts/benchmark/__init__.py create mode 100644 scripts/benchmark/clients/__init__.py create mode 100644 scripts/benchmark/clients/asr_client.py create mode 100644 scripts/benchmark/clients/base_client.py create mode 100644 scripts/benchmark/clients/tts_client.py create mode 100644 scripts/benchmark/config.py create mode 100644 scripts/benchmark/metrics/__init__.py create mode 100644 scripts/benchmark/metrics/models.py create mode 100644 scripts/benchmark/metrics/statistics.py create mode 100644 scripts/benchmark/qwen_rust_sensitivity.py create mode 100644 scripts/benchmark/reporters/__init__.py create mode 100644 scripts/benchmark/reporters/chart_generator.py create mode 100644 scripts/benchmark/reporters/markdown_reporter.py create mode 100644 scripts/benchmark/run.py create mode 100644 scripts/benchmark/utils/__init__.py create mode 100644 scripts/benchmark/utils/audio_utils.py create mode 100644 scripts/benchmark/utils/text_generator.py create mode 100644 scripts/build_docker.sh create mode 100644 scripts/docker/entrypoint.sh create mode 100644 scripts/docker/init_host_dirs.sh create mode 100644 scripts/download-models.sh create mode 100644 scripts/download_models_standalone.py create mode 100644 scripts/package_vendor_gpu_image.sh create mode 100644 scripts/prepare-models.sh create mode 100644 scripts/sync_accel_env.sh create mode 100644 scripts/sync_cpu_env.sh create mode 100644 scripts/sync_gpu_env.sh create mode 100644 scripts/sync_iluvatar_env.sh create mode 100644 scripts/sync_metax_env.sh create mode 100644 scripts/sync_mthreads_env.sh create mode 100644 start.py create mode 100644 test_diarization.py create mode 100644 test_speed.py create mode 100644 test_web/index.html create mode 100644 tests/test_hotword_prompt.py create mode 100644 tests/test_model_integrity.py create mode 100644 tests/test_offline_model_selection.py create mode 100644 tests/test_qwenasr_cpu_features.py create mode 100644 tests/test_text_cleanup.py create mode 100644 uv.lock create mode 100644 vendor/qwenasr/.cargo/config.toml create mode 100644 vendor/qwenasr/Cargo.lock create mode 100644 vendor/qwenasr/Cargo.toml create mode 100644 vendor/qwenasr/LICENSE create mode 100644 vendor/qwenasr/VENDORED_FROM.md create mode 100644 vendor/qwenasr/crates/qwen-asr-cli/CHANGELOG.md create mode 100644 vendor/qwenasr/crates/qwen-asr-cli/Cargo.toml create mode 100644 vendor/qwenasr/crates/qwen-asr-cli/README.md create mode 100644 vendor/qwenasr/crates/qwen-asr-cli/src/download.rs create mode 100644 vendor/qwenasr/crates/qwen-asr-cli/src/live_capture.rs create mode 100644 vendor/qwenasr/crates/qwen-asr-cli/src/main.rs create mode 100644 vendor/qwenasr/crates/qwen-asr/CHANGELOG.md create mode 100644 vendor/qwenasr/crates/qwen-asr/Cargo.toml create mode 100644 vendor/qwenasr/crates/qwen-asr/README.md create mode 100644 vendor/qwenasr/crates/qwen-asr/build.rs create mode 100644 vendor/qwenasr/crates/qwen-asr/src/align.rs create mode 100644 vendor/qwenasr/crates/qwen-asr/src/audio.rs create mode 100644 vendor/qwenasr/crates/qwen-asr/src/c_api.rs create mode 100644 vendor/qwenasr/crates/qwen-asr/src/config.rs create mode 100644 vendor/qwenasr/crates/qwen-asr/src/context.rs create mode 100644 vendor/qwenasr/crates/qwen-asr/src/decoder.rs create mode 100644 vendor/qwenasr/crates/qwen-asr/src/encoder.rs create mode 100644 vendor/qwenasr/crates/qwen-asr/src/jni_api.rs create mode 100644 vendor/qwenasr/crates/qwen-asr/src/kernels/avx.rs create mode 100644 vendor/qwenasr/crates/qwen-asr/src/kernels/generic.rs create mode 100644 vendor/qwenasr/crates/qwen-asr/src/kernels/mod.rs create mode 100644 vendor/qwenasr/crates/qwen-asr/src/kernels/neon.rs create mode 100644 vendor/qwenasr/crates/qwen-asr/src/lib.rs create mode 100644 vendor/qwenasr/crates/qwen-asr/src/safetensors.rs create mode 100644 vendor/qwenasr/crates/qwen-asr/src/tokenizer.rs create mode 100644 vendor/qwenasr/crates/qwen-asr/src/transcribe.rs create mode 100644 曦云系列_通用GPU_mx-smi使用手册_CN_V14.pdf 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 0000000000000000000000000000000000000000..5a307a4016fcf082426a9222e3cb1258ec1aa10b GIT binary patch literal 10403 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. 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 0000000000000000000000000000000000000000..8b90de11c798cb86ec9a1598bc75d0b541abacd1 GIT binary patch literal 1549583 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%0DrqXgS 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�MMg?HB}Lb))cS8I{(trNhHx)pc*igMIMhWT7pt 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°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

AiGThe2B-g92$B<4?vf9V(rh)R$flIL$# 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

pI!^i!h!?DsxLX>EDz-9;Ikzayp4?)DN_QJvsV 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

(1Y*2kO9&?%(YJW9OpTR>m@}1AquHnRX zq_yN|lWga?kKM$USnieMR}0y4|2K9L4auTux%|d{D7dQDSjylz%M(#3gj>Y67LuPi z^k7iw_G9j#Y^|2yGwU?i)>v`wXcw4OO`g}I77J~>9Cni~iJ&GphVbJDhg0pw@6Rl< zDKLZ5OJYtKB-8;?H;oyY6B<)8n#sjn20hHA4be5CFeu}tzhRVkkaTPjUoM^xi<$2S z?>tXpy#uu|yd$`mVO<_=tSb>LE$hYapLf^%Hl`_IRtLdv;RhatuFzxLzXI`u??U{l zMGYCaTBbvQg6{Lb0Zw>Wj%i=Jkxw{T`@RbUqPgo>AnXpyNs)aqAiS`wGJxAaH9b+S zE~lk=^trmvC^czzz-zsr^)`&^=Q<=NV%o82rSl zaBt0qMKaQK3LBy0N%NBYu;RxOegkj-vo4)ukc)o8bD=zgkiMBJ%!TyVPv!S)sc^9z z^SpM)RNbW4+MXV-hI7|}=d96VNYaIF+MC>u0vDt(S+#j_yszdk=~Zj?Q-O=f9NBTk z@+PAIK&@dP`mYyi&HjGVk`s)_LeD6xxl8I|UA796&akU2;Ss#b6=`s4)#q5lDz-~G zbo$Gd+;HO- zE0IABZe3^&SM+Gb0I$@(W#ouccgWK!35afZ4%&j*cUJ1z{Z!es1kEwkB%?<;3#t1VtM<21EZ#C+S~mIIW&P%Vh$k zv>@=WTBxFtzqnh7sc@J|p4F^T0aM(i8YhTevj^)hGebeo29?7VYRasg zR3*SclVSVcZ+)o0Z9c132}U&#?~hgtESJ8X4OZ?~fxwUjv>cygz|~uc0L2zr{ms5n zird-nIsHz9!)yZk2;R&J2I#>l`~+n$ZJM3*sm6VR=Poc86)AG#Ba)ESA3ACt<#za+ zbo20I$1??_!yJNS<;TmCYz{HLoH-I~%|69iKKh@hAnOXmH>?8J%JgW6JyGG8Y zgKyF}ZU}w$L@?GUcGG2{1s_~JrfU*Ewe8?paVSJ+tzOf*BT8i5td%Bdlv1Tmhg!j= zn{8I?Env88cn#mntlbvmMIdJ<<9aGbH(mD&Od6q&a-%a6WLd?nB0c3xS5{#UzX);$ z>O|v?h4feR%(u_$2)`~)pdjg}cap7U#|n3|Ne$;>-RBQZUN?+V3}M;adhag^lx5Pe zh#`W?86yzpPuB5Woc?2TUATR+GgCwTrm~YXsJQo7c4qU20oNH;f z6|_^Zyw)TdK=2n~$~Q>G5-Z%o4_N3Mot^>8N=3*Xl>3fR-~^#PY+S(a?U0ZIb-*u! z57s*Qn~@bKU@rNxKtsT`jZ3go%9)IS9?1hi9Qik>Xhe`n1;VdeUt2xWE_&FnaQ>I6 z%7W2ygKL61H1pXX)MrpT)bGGSP;ULD;K{WNwg~*#Hug$mJH;qr%xu?c10Vm)zh6jQ zB#Fsf3Pmj#cXe@ORQSBMUD=WTp|MD~_#rRP8!2A$7<^aW(*Sps=Y~TqIwogsKEuY(10J9I= zYAvqaCG4y4<5vUsG86|0{DkrW%q~hkys;(poAoZ3Ays}LkOa?|cTKW1^pWgMO~hWF zNws7H>4n@_JL#=-4Pc_tqk`73|61q>n0&T@%E>b#h%zp*2O&wpP97Gv$^^4a>IT4aq>&gwRRg{ zBZE(5R#Fz|#^!p%FrDI;$gB^+s#j1a z(}JzpVTh>zQMrC+PJ#Nhp=Nwa}5DqA~P7HDcXa>XX#EsMdb)m)f@7W+V3UPV}B zl>(w{W?udQv!zR3H5t|IfPLan!`-08L5zi%Osr9DOF~xuPJy=VuNtA!2pon9Om)$= zrA^eA_bHKF&fg=Mbz3RsvJE=YZ!SY#X4A%ZTNY_Gi^b%|#3pX$+#)M~m>HIjN>R=o zy9<4rj-A@y(8cNSd8g;)RbCL_!p7PV+@J|d&86*yKsr~_op!cUj z!4V4CEU{&bXHuniBJs(kqb@KzFt7U=*+nT>vJYMl%0D@kfr#*n$wa;ADCR{?#*nW~ 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 PkbtlV!S86B0Qdg@@{X@0 literal 0 HcmV?d00001

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πUyM~MPl5Q$_Ziwd{n5|6QR`i*l4_~B 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�zdQ1^gZ z5mv}9#1;lAxEPg@9nwzEp$^^S&hJrvD;bUc@S9~ycic(6Aq?q!5mXr8!f)oSa&>Z) zEP>Fi1IX9S_0`J^8@_C_gtBQHIlE9)7}sw?Pyal<2)>2dq%YhP}X6 z%oK~zo#F#0J(`Ydr1Z4j`CH5^edlzVnVGVMql zo4h&r#L3-$(hpohQVyCCsoQaBb?(YcOa*~fj9Qa2AWLN-Wh_7EC;qd{(OqoTqpW=RkAy^>6zCC)RhTm zkzs+hTYUVdvjali*HA2_QX24u=F`@>PKo0#4W0wI0Z4myWDL{nk+TD9uC647G#?A@ z_DQ@LN^Hxy;*H-M`Cq!iWAN_KssY=F#=ORAwggz9M?k4~EqRSPsL5oKBDXNF2)95? zid+y06E`8Xl5HJuR~314V%)g{FVtFOUn}c>2L1nqs@eqMe+;$jf2UHW4lcIKP9Hkn zHoKq|P3EyjG$&EOa0p1Ri=_|MI1k>%LGdR}?6rbr2y#Q+{n&p9GjZyaAHLz2ccGouy`mDw3P@@ zE>tQ0oR>f1!G}>{9tM5?4MA~V>{3irBws{!v>$uHjLU5?U?k0}Xs__4P=W!&a9#lK z7Uii|`7RL`U|BKkK~71acw!;Wso5KD)FpOeKmElC9}(21khw6M;S335!8h2~+r1Co zoxwQL-x?dvJ|dQoDer0AoszrjJv<-1>6C`+dq2&>^zk=RlcMA3g7VvbcQ0?Ox}q3> zKkbd}xs%P6)75HS0%VOKZi{jEn&-ssv7C@r&d8U9^>?yFgr?aO4{0CCTCdudeWt(C zrqEhxWHrN9iMeAbFv60!Tk(Dq86zVy_m|HLXXJ?6d7@h2W}Ga=hSD;Z6`jl>h`MD9l}A)6H}Xi zU^Ns&+WU0*#a7n0ly#(Ue_bYEb|H7f;B6a$ckvG~=63wSMI6>X&&{ga@a-AD{L_=JIm;E2F3=`R;HOcSaXGI=!AJ#GEND zjK)Y{i7^P}?nvGall__t@r}GOqZz>YHg9F*=o)Bj;fg__?}Ng})9?Cd|IVOFRYrKi zLE9<>|LB%&St?^G6nz($o0|rwJEqjPGxuXuJzP$qfL|Ov0r)E^`B4Gwv2sP;Q9abm z_^&^Z81t!#_?OI`I$do1TBFjF0o)-upLwgwcH)7JJPt&bYReNTjm@RdM9y0j;ww2pj&|4pJ(^ zrlF0*JW?LFaj%@8Kz!1*jb_OiJDPU+5pFzZn$9@cO&1SIdxdzX%xBh_%2hx7)B1Ct ze1HN!!D5s-NZil{VUfbv-UVYHW;!+-cL09!int^+KT*ii(Nx>SX>I{-Y^2gaE#LHc zk2s4x7T-x?AzvV0?38!FM)Z36EI~)oWQGWRYwlZTdiT7&78a3rPaGqMr*i+G5G(u` zd~U8iV@$pGlg)R&_WpX%maQ$rmS3+Ytd|u zu+Ik;A4k;Lgexc%s2Em?9osfnjmD8TG?JnNu4)o1ru{9leA?nu=~3lX15n%!Vp)<> z=E;&`omyzer(G5=Z^(jz%6JvAwXJLtSB85#c zt4U1bLxzK^pQV5G&8cbG^gz<$^#>nGOGf*~?l+aKmNyxcnKo1)#e<;(PRSs{R7e$g zc6;yXxN1-5WMC6=ha?rDK@>7q#xvJapp@g&V$D^BBgPkQiH`(l4J=l|n@ zgPfiEd#G&Mt<}}1#~k~^mQyZ;QFZo(b+SU#uRAW9W!6wO)VlL$Q`~d%b821x@@AL| zY(822Lr#ZKB}zrDub}Pp;Wgj6tjB7M>&>|60L2=CQt}kO)%&!P3{?R!p~#d812ey7 zSt0Uqx`aM;z_DX~`I68)Zx*-7&*r^vMpX$Lz#=jp1u;749u|HJai-ha$n`2i&%6gx zKm{KtdV(-(_h|L z7{baST>&p-MHCL|M3N3jN+bSj$I1SN{9_v?aC`0|w30&azjchy5vUmvow;o+cZC&B z-sGy>++ia1UPwd_!h9L4`!w?+V2CdYw;w9uV=zd*V*z=EH;H8p&;A;%!|nV!mOC+U zslTTzgRkpPjlz|sIwRo;!cly_8m#O;8^2Or$|aYL*8tnOWBT+Z3JF_0>KOO`)ewNi zsy}snrH#E$;A_+2e*))?sWg=^O6GDEAMh5#U*AEw7b=I)4P->LL^C9nw$1mAjVw^f zc&1rb61&iDZ2oOs;hzTAXPC+|s~^48VAiWDzgp-_kg*GK=?c=tXtGiNR=8 zhMyR^m}l1Qa(EgZ%WipfsCN1Zj5VQ&&5RJv_*FuA8u8)`m4+gUDL0t@n;B z3g@=|-j*&RE1wy5%&hp=%^|cy{5RzEy4VA()8xHR)o0*)A79F!d`a)3K3SYb;jc0FpRqfF&EtKt<)b$nL*C$0ZHT7+sy8H#i6? zyoE6pYp$R2rb?&IZxy`f#8sr{4mPTK!aBgs(I%p|A5+R)9}bW$*1T$i&n+YwLIVq% zXmhczj?_~_kWj^K6)%=LEh`)6Se|3afbT<<&AC0Y=pZNvu*MPjzLcbLmiiMb! zyy=@HJ=nb=1zmQg#Sm*yI0?F94p;-5d-Gar%=!9vy_c5=#a8b=UIK6_LFy!zqkA?k zfc|1IZ}nfeSOE2Ivj+$dX>m6|F}6zp6n{ox^(NrNh5kbSX~{Fn_4|V^{J_XzVJD#718{R zDMiE4hvx}Qw@7#*268)1UH&>+X+vS(Gt&lR$gd7_t^up1)eQmny!cxMvOmkuN4Q!^ zlyEoTya{u5I+a4ui6lZYk?b+}#Gzsz`yn`^-x)=l2sn4S2oW-RIWvE!*|7X+y!peB z!TComNY}BBVdy(nwHyWVZ^>n42f^)MW!dSr{l*`7zJ#1JT!)vfaE-8y`#)>Ul#M7D zW)+!Un)UOLpYaEbzlXohc-)Zavdu&=?LL{~__hRowg!nwHNY+5bZMfsCSUAaOTZ6Q zJ18(lcJQEaB`)i4E|W84!_>eGiCZVAht})F+crJE^D+sv5E>=FhkD|KWzoy2pxDDg zV?P14-=4|eyzu2xuFD1E#*bTC&QlFDsxiOf z&;Qde+Xnns3~?5cdaBtsXU>?@GY6^F_UH3KE|qhntcLRr+#dDC%i)z42m_1VTiivwF%@DB_*Tgod zomP=Er}~kSS9sw{GzC`UqzF~`Iz;7tV+FMge)71==wJJInzFwZY8_c&%0(WjttQ_; zvacyk60eb?1v1u^LwFw2A!RexBhyYDLe3Fz^Vi|oajp8NETC+z_C@a2BcX0Tvwhjs zj5!)BC0J5Qm9SW1Hoo88)J-Y%pTODhg#a=~9UP!CmEqf(;ObWJy-^i;irL4E^Qy>W zQov%=&!VX+0In<+R9r7UmYiEWCB-)TG%B%Ulz4{s&NRG3QJzmR*v;2}DA~@>ppWpQ zVxy3yq?HKRi?0$BGDJr8?Vj6(oHCsdNfK+#cZvz?F)US0hb|RJs$%sLT`p@XO9Wj3X{}nq-aes5v>)pQ4qsW_No>MXaPNxZp0!xLL zYYae(bc`HEFbgGcFP-BnJ>|$)!X+qMCL(u69j$|C^~GhXwbvzQ9OyeI6yP_7VQ){H z+runyp)4k}eRVk_LpB;lQ zIVsLR4$BF1b!PuDr8F#YMuCIde&cW=_k2_Y_svxlEV;AR49z%|$)~kr$ctGrSVxJa z!sR17cUtY`wRBao&Qfj=UrJy~iH{i5B^4^SkADWd{X3qIPiV%Z!m|0SWh6&UCFvi^ ze{b*(ecWXc3IHvYP4SuD{&;pr@p8B8o(Sv9J6=eQR(Ld{@0v5`T%fK@elCO3;C)JS zM`RFGdEDxErpjQ1p~Uk9p8pJw&toh9U#by^C#P&`VtZ=3Vk}OqSjg2l>D%dIvj-k| zu{>3u<*yDHM`aTbRW^mHhM|v;73TYVNd@{r;XAQ2m&W&o{#LYFd0F9coa_!i+`=aU z6i0ltZ}Mfa{nsNnCn&T=>EcwpV-Z%UxYP@~-=_g4pvia&ZQ0VM5Znf?rs+&ByP!`8 zI3Jfa_RlEu*|SBeWnr^#=}>(fx8A9P3z;4Av|Mhy=C?Qv6DCVa&|u)h)!= zE4N;h-n#gi;0s4_E8qm$NbBKE29$_y;JW5Dl}+NbT||bgeZolf%K&6Xr}J*fK*DivDXI(Y zFX^hFJE}*&Fuo%*ZPm|Za`O7k5IBX2c}8JWGk0xRB89m8!~=r4<9;M|+$x;k8lfOr z58OHZeb(+WOwr%u+p8%(Rie)tlnwrqQo1Gqclk|;iv`HY5$=rCQ&-%y=!RP%cfG~i z*%hUWS1*Xv#R%L!78PHWM+rmxlJ;!D`jlF5$V%dC!^X;TyJt0DE8N^pI8T&4p!vQ| z#}%}BY1-=pGAX6_>j1}_3eB$*X7{nOn#!1IV#VmYJV)bsibGk8%^y(M&W`|CpfbGq z+`shMqa&{Rp8^^%iKj|Mvw$KMMTAZ{{7ORQ@_^!VPb#Y|Y>s7*k1M8$l1wMc;)%1YD{F^LMZd)G z4PozU33CZBBTse-IIoFccuy@T8vy<=pzh|4!RJp8kdL^Yb>vp>Gs%L=Jdp@W7&`ZJ zxq|)=7ySR?i`SJRMmEg+o~CG8Z7@jP%Nb2=Vt0pQY`mcBTg^mGnSuu~GS08|&AIuA zguoi>8z88N?3wX{bEqoSr^vRX3xdHzP&UFUd9Pk?Xxj`oh_4a zfjM*E&S^n!TE%|kb1YkF6^pL9e07hGf99RZAVQ>cP#lvnUdQixb9&s+ZoQxMzw>qW z*UR__eI&p*RQZYWoHoo|Lgdk#`1p4Mn)IdpZ;Cb>}}zA1K3_osxh!Y)(qbm zS|(mYG25rI)2cXaos z*Z0+2aF%)G3NNqay}C231o~<6^a+4(f2d9^ee5Y~JnKSh2B`w|Bf#*0_UFbqvuL8R z6HKPP8mrBR7Akq=`Zf6utr_6a*lPm^b(wZYQE+oe4=)gzU41Z&5qDhYVrxHnx2y@v zv-4UgsafP-dHws}s*=N28QUg{n^4ao9cgF+cWcVvj%DFTd}e88e@w!YtZKCwr_An5MlLQAZ-=mv}(mpq5f#9w0=Y| zso>=z_m7r;@PbWMo2*XYYiQwP!-PNTB+Qu1ntOB_L!~VPx%JjbPhfP*;T?96tV>zVPVyo)%OET#*F z!Afz|?#c6d>(5j01}+7K&~vC{)xR4dUeXttP+25ZyR>|fLo?+t4UGg3lI35NDhs#9 zKZ`XAs}d)INN~Xu$zf-RCahdo9583I>Hc3MoWIHx8t1dsCC#s!>}id$I?0pqm+0uz zO5N)4@BoHKEV%`R)h+B9;Q`EJRk61Ca`LZpds_P^`2JUxc%^=$^@JM!h?;sYB#%K3 zWcNl=quZ_kVr2iJx~}CW^5~A`@O6@U;BlVwLEjNZ?#;X%SHm&z1hvF2)OR_g^y!Sy z#Va;Se)=5iQE_e{QfY*Mp}{pv>k5}})yOnJ=8S|_7GI94$iS{N#=yruprF>64Ndz| zr!6jx8xEQ76k#uzn`jE1vUDahDAFj#Q>6J&+c#OS6qpuG!My9J(#$(oJA_#mYPtHJ zx|0+g+w_wj_!$-$q3ZMIBtL@L!yTuS8)Mo}rX5%We$OZYeo;U_HbBACy4+nNQmi`g z0)?ity#f99_VWw!zY(n{ieCilFOOJLQl%>$7NxKg>UOc1W+&w) zqy7AmC73CnGP`mFNCpE$d9qCkpVZ7VEI|4k6jRb17VcqxE-wE|=w_83J~;O@;JchS7eSV2(Q+ zO&vy48-{~tkeYQx#5DJ(?gsL@;vt}_fM#emU&Cx}^t!s*dw!o(HB`O6O#~Ya1}l4!lHskLm6(bUGfjAWy15yi~B~ncUhIyY9pThb8s7aLuZIb-R)7xHxIPM z;$KYSVEj&`uqj}7e1rP(6Pj1bI|P^^g~8;OPZ{_S=JuUi=HNx3z=f*f0zwhJ3mG zVNVlK_6>HRWKK+No4j!~TkelkCoCvSXz3J8*OGJyu7%d@OSj|s6u7ALnks`Xyi620 z>@YYB$m+-8!Bj(c^mtIZNkP+KXgW-a^WPwhYnQWX< z++~;qiI)np@C2?w`08x<3a6nFeF!WYnz!}LQ?RO7>AUMm%N_gH#4d-%&#rkx}fa-eU{d_|?MLmkU*WY=4U+JBia_js+GM`;4HI4R>d2Xvs0?Fralbmoj{LNDRj&M`UrAotG zXT z)Mg4Q3%Sd(_YMOo5L-mHsZs}PeYkxhkMLBgg2WyR*vEyx^5%`SgH62>(1<1fLSf>6 zp+KmtD3Qepn=a&L^RvV8hzEj4!qlWAbcM=7GjS4VO!;XfLICIG*WeVrQgj!(JQ=7a zilbNJZ&5XWF3^hN6TVSmD0bPgPRO>GohPQ0F7b9%wCja@X-VFkW8Ry zQaMkyVF;8?BWY_VOG&ht^l>)+Rmi98c?GKI$^;*poj4D+O)if9gm^>CiF&|6Tq2x*F%Yb^5h1rJkR*B1sQzMa}3=?zwnJr}WDLw%tpF*zs44 zQU*nm1~BomG3o^ViykyNFn)#qDE4B^7?i>>=acfNi}X++$>g|Dshmb8U>^wD1WPVz z=958fM-~qR*@1Flz1jk+=Iib|bZcA=uTEdwtDCUNGdqU-kyA&0{5qGBLCMtx7q?1! ztcEZD7JqgpQ)?=7ip{5?! zlhY-@0$)f_Pi7k_Qnnpd4h(S(UIe2zMt`u8>Q~$GQ zwGYNBvOWT8X$kQuGz7ZV9Nw}Zd8;oxZ{AjEekT02imOOFPHQMj^z2|M)Sy!J$_|ok zd;>SX3+9V)+u*Em60`)H0A|M)ca-{&#!qTN`vA!$~~m#Y{*pK?!;V=wrCHLb{ozve>vj(%=JIftEzHETup z8wU?cQ!!v&vu|aazbOLlsL4}zy&vB7nEl-f6J2)a4uvdOflfgcUb+uMByr>RJy2J> zyqD8xfW}dEl1L$lQh?NjA3f@7Fyh_~1BG2Yr9c@i)nnSry(LDEXy4#GHlZ!euK*RV zms54|+9<98)s~ycdVnjYR$Hc{BPyb7BIJ0E_fsB|2uYa~uXe22ta_7MV3!Xf^V&Kp zJ5V|#bhAIQTRM9u&nIoey+p9KCBxNj7tW_8{b=^P>{O9E^}!}J z_R|O5enWO=ZxItaTgV9Fo<~CR0@R7~V#gr_z8M4;o`E4O6Rs62bSA@S-2Q5!nX;r> zS%Oec z{>f|Y)-HdjRn#Fv}``tH9#Ly4|+OioCsY!4D4i*+w zWp%#Mg5Vm_!TqiqF6HQuIW&5$vtZZS-jILV6m|IxKTkVQqfpy0b5cG396Xuzs+%1K z*gR4E`*%r7BP(O!-aDs9fEg!XGzwx|vY|ZoC%&kxd)#OqJA~KPdxeCWjk6{y7qDH5 z>J<_5kjQ1$xT%p{MnLPe18;Yl^J5oLwW1jieoLH+*)5)y3O;5$dw&ZG8+(KIC=^S< zq9I~BIiv?)Ekjhopq{(Y&qY1gpazS}(3x=1h)(<;S_rx%ZlLVJ zdi{+C6`|gnA98_177xSWOuMkF@M%YFoXvXc2JLTSpE^~-tS-~90as!t8_^xw^r}>r z5ETh$Wg0Em2BcC>=s6~?J!VmY%Y>fr?YfYVDME3Bz2#w7Y(UflS zgh>2gWr8{u1B51@jh}h|4K6@1_T*@(&}l{}tux>p>-ddFb6_3a?FNdACDGxCw#}_b z9g}Fa(_A`E7UmQbKCF3}-wG@(x@1nPH6+Gx92SU-t@Ja+p|)Ma#%k+u=*hQpXw!CQ zTgt$s{yfqJZMMOpSw-Zl^vwKCT(3HC-Y6U!-SWOVQ?==WR$p9D3Y)VB)l^wi@9)~| z)5YKZK+>_T&EBkL!4U)eJz}+1ZtHIeevP@h5Y%A;)znDI^Db$Uq3rm4|K{udXI1KV zIl?j6!&2UBmZr6jvH`-$$O7$ZDg4S-)vlWQL$7tawsi3X)iVSyP7x274_-hwW#iBy(o zFAic^sD#?d{{-b>`Uu&@HD7EWuK@iUl`L{?affOFpg^;;!yWS16Cp*sgE%xc;@GJA ziqP8PVyh~4Trnl)S$42;DzBq9bdXxIxV-s2%!#&g^Fx4BxIsV-wIde|ry?(jy7?)+ zK#%7MK3?vIT@Mx;dxDkOVSv-u@2=Od znM)hx$RK6HrJ`_S=kb$z#~8MQmGl!Yhtcifim{GI79jbxxp|@QU%c&yQ8DZ-&qb1h<=DRdj>U#hG#WdE}`#X{<7n z3NzmA3EMGFxd(tNi5~OgVGxs~vL)8Y!;GuX&qxntPSc9=K;&){I747rI{~@a9C`#? z&mx~lTc^*dC4w~wNqrsG%`c!J?l$;IO^a9*ZhQNr^~Fix4coYi!gRcNHo0x5J(^CE z&ue)A_SY6ergDIM=G#?^%1R{@Yu{p)^1_AOv@i>%4Ut9=ox>`TW%hpk9kl`3lV62o zqkfv_m_0o~l8mr?#!~6d2KCz_awVzt)ZE3zEUVEmCYKF($EJc{J>C`CNk!-w+Om|( zFQ5;hkfqNSxn|)KZ6y0LZHgeY+_7v7W!Nx+0 zyk(t-k`t$9XL{CPa@jjC+WBUyrB{$m+tEYm&ya0u7FCbt@k*+11>f$zf%2yb&ZAUa zWhEw3_luSf$zCD{o4xzhBM#d5B;TD>;_m!RLU(KO!-l`wrRCP$(ULu>Azrr+=B!$4 zETVTeg@8xD(p@~J%*^Ur!hkaV@jgbRQh<<+30}y4bUDOU`WWT4G=*Xi1Dj#k_pr<` zbX{}C^%UC5fUu*=xg1ByQfhp=x{4ytrQ6ewG3t#{G-^(l&E`^C%e&(#8&XI*5$QtT zH|r0|7Dct76Pvs-`DlMic&*bqov(2_=VIfmFZ&z3&c%3Ck_0jB7`1b4%v8-t z)nLY$?Wqpw5W2$?M!S;^w?n61I?FQz6b`p>%DPcT9lG5IB`;5i7~S|Yvlr`~3G&wr z!`vv%O8*kz(<@K-n9frF5kW2TYX^nVbDdB>%N*O^PT6sHKJNZ1^53f#1D!lEL=)K# z{TWE^C_i~QYNlDYi_n{Xkx7o>XUo5w%&-=Nhi-RMD`Fzd+cIprjhVwtdQKJ%(dN}U z_x-k2S>7<)yk^o=uPbN}Zbg>;azLL)tESJT4{4-27@S%ZGiX@mUL_>b02!|w*mymM zqaBi9C?UPY)-(-!$bu?*9ERJ-=7C<<)QLjuP0X-hs%!+uquG^fa&XCW(B!L zy=9%w^`LB}{q&ZR*#Joa2X~h|jw3It?T|oqdrY}nS2`)Q6}C2~l@j%6Xp5}7pgSz2 ztT=43w`>ne<>5>!KJCtC`so=de2t4JAjs<=Qy~CfJaqj^bpypmzqL)J+Ek0Gl%WT- zR#=y^{fC2ZmHrRPm_(3C7$Os(?e9BWOu4>SYxV+DBG0WWEDYjYjAEsr-8S98 z;uY`DCqjb<9&9E3uBMN8LBs3lmh0LY!nb>ev)87pv^%$GqZyrs@CJ>}UP?b@#Gi0w zj}saTtVA5%_JoplD?=E=Cc;Mql>9g_(%E`aj&?Ad)ZKZZ_4;J;X$f16`5HUTi>}Uw z?MyV8E(4<#BY7U`S)1(M37eY2Y_B3vy`I77G6(qa`4*stE>2NOZmV)vs`YbM2ftZ4 zCY^U1;-Xj^;)r~J#8j;L?C6)TIug#bh1|;GKB7~U-&ldE>3qR-UlYvT0+Jj<2%#*o z09zK2O%_~)7Ja>9p{6e#9lv$R3@hJ8H$2-l5O>WtI!HxUmvpjM8o#KGosfp9%A#ng z$9|PO!l{H_O=9W@81b~jO$(idvj`NMfE#61al6DrVx6$mIa41O%!ZrP*-2@8+6MRT zdc~Pk3saOXp_4u>NYGa*JIa`k8@)vjsd5f<4ir4Gasl0tB5l9N-i$r#PWN(O{jQ!1 z=Dv?w&38Zt@N9m@kW2onVyP?Jid3_f!Nr_nf5>5Bvh5KClzKA2J3RaX&29Zgap%aI zQY+`R9wr|p!mc(zk{6VKDf4s6(cxcKOkM_fhNHCR`FK0m)EbF8)w>r+o#}o1>eBFv z`%p<1)(HUq@b*g-5>?tJQL_}pux5~8)fJFe!*nl&^DY64xl_E3FCuo?nFVJx{lZla z;{L-$%?{97izlc>q1s!Yi@AJH1AtX`4IZMOvTFp)198Z7BAY%pB!V51^q4baom!1% z*rjc++*&ntYmD-&v3JJ(veP(Ymt5Ff&BMyYGQEE*bUKdO{a$@r(*s%6&f~h5M(c~r zhJL+Hd;-?hAzqkry4JjGv=tfy>D%ax@kT;EK19U1vqU9YtUu&~Oko^9=fa;C?>jG~34@ zkw5%6g{W*DI%mJOr(ldiMvMVR-Z_`gz}o-SyElAJ8W+;B?$f%C66TXiop*l>gTMH< z!OzyW_wx3bXOlsJx!X+Ms##b9qVVV&(pO?kXlD>g;dYY9*7M4Jn#RrH;4X ze1k9SBe=~N24l79j}ihWBvj=znuh%N3Y)#NB;h2b@*>O&Bmf(?KCmBs!Os1CF-$Klgr|{IhB^p8sr3=mw=HAdCk)Bsf zv62f_QtC73^y!wqxt^b%Uy#hC0vSu9RO2ha!^Hvio};xy*t*kRJZ>ViN+$IfbO!n2 z%k1ZE-h8s*Jaey=wc_PO%(GMun%wTX48FGQEhiNb^Lo~8g$~b!jac41zXuYWtt?>@ z8K!~iJTA}Ng$~IUlLyT}l=A(gIF`ywqc5YM$5zYl+$Y}!G*e~i=1sX$&sV)Lxl(JQ z5cY|RJr$^$uk~Ip8x2CXv($MG;O)raWMeSdq}f7I-(H~KYPd*hnwz&Dg!x8HSiU%+ zwk$4ptT^fv98oN#;Ggp3KX$3&%GW1RgwMT9`!nf9tT*@`!E zD|KcavaOwi&YHg9fGZCE{}Z@-Wq-$NFnB!pg;O#kZmH>Z^&lzYjDpJNF&Cbt@ zUc|bTgA(%k%#+6UgIiCc+>c4c1Gzk4V93o29UfmnDOcurS?W4I;T&;p>0})x(dino z+|unHCJ;|yY#vbCDPW%Mg;#`BSEQ`pykQ8@!NrSg=NoT|>E`06Qb`kwi^oHBB4TJ= zw3hKhhuVMI*d2p93nPF|*2Hy2cxid{y_il*MHUvFKQA+=Z7hLLsGXNqGyH;T}WG0Xm*h{8EuTLDZ zfrLd!#g%x#98858YE_jq8KF(|DT7wb|4Y0|i4y&?kF_bqy){kYIzZ7Z+hTw^%DwSI z$AXCmcg!ymc}3c>5vcWWeFuh?y)6yd0egfq2bG+?9ro-I79vGgWEu-;)}29Ggys^b zrv7CjJY(Ll>>>G!bmaA(Rs#+1VfXr5aXmYt!*hfnog8+KW{=A`+|oH^qYwJ*iAF_y zirGgKpOmKoOK%0~nH56Fc6Al&g$Kl6*=2#_clWtMf{Nc> zb8<=U=R$H(%IRWlXr>OCCresFa%clIv;#G23d?&Rw2iNm&ge363*8UuZ<9B^6bo|R zowC?31p?fs0yPFr*78w$*=4ET?emtB!BD8O42J#;SvLIh=HIwAyA#lrO&S{F#36q# zo0w3`+{!sY)*8~=-sbvEaW~3XwF`ENKGtFCN5F%K_(WT0Hh9F)uiHT1Rv{MQP`TvP z@m9=x)G>Z}y!9Ny{GaybYB|Kh1uWwDttDy|UST-lV!eeaHLGXqSKYgGXg@5q{jdy) zXZi!P>CKT}mVUgbe9?(4m=rxln(Hm=hYxg2cAlywR|BU;OTo`qjfu%Su?)@MoBdT{ zC^aq4IdF%KR0FCW9>Z{sA~1K|)W2yx(MCeGnZ~Pyni~Uejsc&1o|oIWL-zZhJ{6vf z4sbu>(gKAglC6LbE2r=0m+!#KOxFod+r?9D&yHjxV}-(0rZ|SwsrCB`&x-OAh;Nig z-xxq|eBgu$C!98`?M?^%;P&@$wcNusU9Z<=yskk}H<%qZ?g=>Apfm;&x2j|(7mqj{ z37u8B>iuSwj_~=1=ZQyP5TBDd+Ag$TM+KYi~@Dylbt3D@` zZuI9)?ug7!nKhB>iD$$U`ZN;0oJ;_KDW&ccvBKOiErVMTV68hvBjw8HQVU)u_X=plp}Blez{a z!O)Ync31_(duNk}&qIi86C-rctt=#!_cHrs;mX||kmJAHJ`eyGyKSpaZ=BVeeXOR( zlv!yRxJ1)c$|mPS4=zzOVM&+vr;r>KCu_IGYf|1eXePf{h0m}37V7joVJ{fx2UY-T zW3^Vq!Yq~5im)^IHoFlSPbaLf^=BSOCrGXquHr6yp;fqbTc7t|Mxe6*GrAg;>s>=H z_Y?=s-LWEYyKLE_!toe_cAf^xsJ9VSP(E&vPgtw8b^UaYa#(ozzz%{#pP@mCALeS@ zNz*))L~)K1o2~v;&TkE})aM6iuDkoR2P3RWI)%Ym5*eLw3sh%*O}QKnl(RC)x9lNDC)VAC%cv zt=Wz?If}BI>>oj@?g~d^ZofEh`<&n2S-qd_K&6lq^%v9xI#WLV>fzV*1mNt|!hMq>pYXYdYKXK>d!Bwe1996rmp@aXW^q>I=@BC!;p-jUp2fu|h2tcHGl-nZ#9m|n2diTt`@ z-YRTZtX_2Eo%0_0`HTdQKPv|WvtR_wl5lWuI=$~{ux2$IHia#P#Fk7NTSrg{@oM+o zi#_R~=8(D7#NcR{#8O|R8&X>y+sQ>F{_&U~6gK0kO|!L9#9Gkp8uIn=_th$);lHip z8ejO8+h1I0_)xas_H8*Yzi?bnxoY>-Gqe2(bWQVn-lGx?WB8j-Wo*+1X}|Vr_+xWlB(N-CYylYTTQu|AlXJ+tY@nwKgFx za8mdA$dOatlu$2M&MsOwBW z;cJaLTBmzx678B>wB_lO3Nrp4!!nR^eQOCFn>I1Hsx)XW`YtsWK+rK!)BzH}h2H?Fj~(WlbkKx~l2CPT37R5yXM^*-VZK?_wT`?| zRLXzOXQ;hRR6W?0we)&*h~cf+ts3L7qW;IO^Zg`5gO2R^=$p}bVE4a2%&jCE0C~K?9M%0P5m?A3XVXBM*!i6 zfZL%25F+S1XX2vf=DEB2}-N8Cpl3OP|xWb;mLE zegist)Q!N2ZH92)tVS_%8j!0dd?oz78pHkXoiBKw4;-BzVHfM2=Fv=mbQvsJd)G?H z<2pUyuk=&f!6hrb+pobiF|?sCrm1MzJsB>A0f2@?iw4>=bIn9UAxZW%cf5P4cMus1 z-uDH>_dR*-&HTIKt))rQk@AXw6o?0af6U{zt>Vg`mKBY@s92^+hOZeK<&lDEysC=B z-C!JW9*A{>^i&szq!e5SX_Bq*}6BDBK!utaFo1s9OL zvjL?7quQCP)jP1W(>z|WNE)b)@Q1CCWqD=Y<)vovX!nA1z!V1U?_5k6-eq5f5_tCQ z&eFO-ql<)MGD=NI6Z9rp(}LY8;>QCrZ<`RudtqRZGGMCxT(+JTyfk4&(6e?YcJaa_ zz#`$qZbQBA1kU5;_t}sj{qspER!A2~+RI*AiX(?6SUhNB0c%?y_yup@q3iV__R<~_ z`hm7b#NKztjJTw+2<7GIVgJl}4mB<(@_etxkPNqWnbqZ;ZwIes;-J=cT$TJQsU>!g z#mJ8H<5S7tl(EzeO|tqfzAa&&mtx!-H(&3i`J>X}AswoFb2 z#k>GmAgCGK^mI4O>tBgo{|4d?xs(HvheK%H(MI)wEb^yWnYYc&gj@FOBCaSL=FBg? z!2x6dLAy|>n%vjLK=|Q_b%;keHw(mxcU?QKTj)+eUF!_I96}JqI3kss9Aqu$a`Ip< zdDPw>w3oIoX!*v23Ue2iuzO5Z{Tb@{wb64P(~HHGxZdcIFFpC zKL*E-w~(l2>h#qF1u_EU$mhZZr@^mIVzl)2n64A6dxlj~r0y(Zm92|klxNd|AxbHl zeY_&JAVBIMFujty$RsKzM*+*s;#)b}lDYpn>Yr9a-YchJLO6uQ7Mj#zzw03r&cB|D zB1hD-?8{3;moYhicCMs6CDvZ6#{IaYgrxF zhI>403|P6+eRFD*riU+W@=AZr$RQimOCDn#ph4wbV(yj%sw<{vRxBzFegi0Lt>y6E0di!d#Vff-f7&L^jH%6F?sLm(H*zn@E46Qbe=`Z5zm-@|S>~%0^WVN(;NCy()8>pF{fFGWqI+8#T!`7c~xJ75km2LE? zu3yH}*xe$Mu2VqWGFz8>7y)B$Sr&;LBsSE09^uRE9XRBoC!Ogb%ICYp;10ZuE^2Fp zwNb~Fn*p*c;+NG1MWX{SUM_m+A5@yv1V4MZg5B=_#k#;%h`6bM?9m@gPx$c@@Xi*A z94}YsBu25Vh2vCM)~T~-+WmNMf5BRD73%Rc#|p_7b-s-}IYu$xYS!h(q~#T$!U||p z%g3qf$h!0CcMobDCLMA>vg|P2K9*Tg)!(Jzq9t6#kpym67rRf)&X%Zo8T>M9-LzTt z8AD&!wRf97b(A=@%lgy3fy`zBn|9Fzkl69Zp20FJFxgc#`t7582Y*q)-h!h+P|0^I zBadOy6?ENm!Ce%MSwcE@h0fSrf*CridaUb-UqGwIY;1 z*pDMwpgBf*A5;>nnkN?tfpUk|EWp{{n?3N4W}ItYp&N|vf>i=K6ssl#8T-Xt|2RGEsbz)cB#0Gi+hA9g81$T)88tcUo+v|7aa>F#*n-+kUm)bd8 ztxZkq*aZMvj?!aygOj7Q+b2t7Zsq@B@2k4v+M2Ed1h?P>cbCQ;g1cLA3+|1(J2W2L zg1dWgcXxMpmyoZ|dCqV6uHKuz>aoY#Yphkf=B%nY30*q^K2 zmp2ts6fxenDcj!a-KZqPuUlVFB76Yxr_3U@XRy~@cc=AE4;BPBghtdTIi9TTcIw}fTzNkHi5k){B`}-J%vy?nG{;1Ktru1`$wKlNuaPQ{RoO@xSZ2nVwZZqSK`OozDUZoEZ+U%F955`L~+b+p2NP zbzZuP7=7?Wud8IPnIb9{){miO*BD(-`i%WqDfo6d6a=OjI?HNt%ga@ypa6Xtrg3Pd z@^Ki%@^gXwgNnL<_rpgMCBs`7)SyO2JfV|gm1A$z)|?!aO*~ZNRF(x)7jpj`2-|~kW=^?qRVI{_Fb4tWM_dka(e?EVn zxUaqBqp)F7WIM!X(c)ifmcm@{v=)G0xU5~W{q@ajf~%eaWaU@>1(-#!@RUjU&r;7*)2j9&K7~i{!_562alnZheX<08Uh?F#3`CzPQnHpM z63?)7J5rO6c1$njix#Y6cYDwZrbhXGUx|3yDd=~vbLrqQmjC&oDW-UWD5@tSO~^dw zl$E?^$}Tu%?LM*N!4_xByR5Y4bMF)tR%_qSZLf1ozulCy zoC#s+TTY}Zxwb$!dTrg`&Sk*ZaSuKOn>_vH)No+%)+?)6pykvDfr;!R#=NkyCe#}x zr1gt?wcJOPOyHx(Vc1$QVs3{L=WiRt?i&*#wKK(Vox^T+E(sj9bqfE?>RylQNb@&L zdj&84nXqs--2hDR(?PD~XlVaDJ+LOjKO?d*zJ`hPB)3>{fJcYaZM6p4DWRoYRK^xw zn63o3B{-vBGo3e@;M^LfXGZG5eLoIS+wzC>tUkpx$B=)W%E2S422_ksCzPqxt)nr3dAoFU7oiq-ols4&50kTd51sf@RB zyWLOG?K-Pj-OrVJyrBD8j9|AfYo2W51&V}b97+yecG{)0hmhKY6=OJGWhS1i^Eb%$ zL}?TPtQec@jvC)Ug3pIu{ClJwz>HCkR7ATNScCPtv9N$jN#i-iIXaYxls=;}v}Lis z?Dh-N>P>q8K;a}MG4Shy_g7GIwjnD*(0PQs?)FD?*EDtaZH7kZd?%41@Y%}!LgnJa zif7cIUV+jfoF$hvW~E|F#QgKxLtrk8w_4vfQv@C>A0XtEPkWCV%E(UPsloN$T7k(k&FW-X1$rQptq22n^#tYd zBLAjfM#LOD$D_ZcpeHftBFU@r8n(C;`yG@dJts~JHa_s%++A)ZdzJNGM)1&?d4(fg zl)r51#oWYPBv9Lh@T@+(2S@HszzgS$eZwWcgw?zw*!*&A}49cOs==V~Jsf?#{og?{L)fPN~^D%nh++ZjM{lZQ^+?x4ke z9Vc}kVUL7Cqp7G)Xrc8<+mZ&n>-Mp;?$eZLX-M0NmF#7GvzbWMI@AI|#OyNGf&h=nZ=9fC$GOAL* zGBp?ep19=*4gOVJ@#>i{sLUV(#;!H$2w=co~y<_mXjVZG!j{7(A zKn&DHEP6FFY{7xVLWJ8rOxZ&K6f(Syi)w`GKN+#b8XfdMslpfiT!!z~jlA6kt3k&j zit|of`&l}$RJMhWpv||DCmjNP6sh*S-_XC5!g%F|Gr?3_5XhVZwLOr?7#*H{ogy+i@$8h##gauzi>ei-TV!XEV2)A=K>do z6-Eq`3|9Ed4=r~mV4R*x(qOYo6n`%JeP_=UzvDAsgRnJHfM73v0RTt30{S`hifk@ju17kT)wx z>REqp#wpne19JMc`Mv&$JO6!wCH=gSMPGC0v;&vDGvB=spxnMRn$Ku9Fc);$2#%w% z3+@C0nlW&BQp3(-_p0e&x%b3wU55iI(aofOkGcmU#&%Uq7?lkty3pjHrD1ysS5Cj{&<(o4L5Z zo2O9IWGEFOl-Cd|x_ zo`BIMtF2CY-MI;c1n&h>+MWN02wrWH`8Pg+tlZQEyJ8rZB7Ai&;kZjodvit<+h+US zlBk$Feb*n>NWzxzQPT{h?ybLbwtM3)C#?q+7}nHpnb~$iYr$EjuL}Q!K3nys$M$|^ zu>BitV6J7DNZDUV4QHQ7lzNHDZCIDxxZMQ}l=9K8&aNo#P&{L7`Lpc+(=67NUANNF zw+$FryWzBuKiRjN2oH)7Y4~5<1TQf-U!c4-{QPk)eKgY|nKy6~eaKUBG-n|8@_Y3> zJR+S+*`M7lN}iJQ!ZLF7-pv#hYO_G%5oe`~oU9fOQhL~jDzF_$r$`xjrg0#NvuiOo znS7q@jsQe+=i4Ug@4jMXa&Aizi#hyQXM#+zgBOb@R+m7{kZ2l^9d1svsjw*s9ee_M z(a5p5|5?%w4T_EMHeZz^q)e|*z3zmBcn3?;;Z;uDC)u{!IDEt9O-o()({9JPrfX1q zEMn7wf*|$o%xctQ=HdBm znM`E_5*HE*8(~tlfu~AnPZho5ACJe;o6C{B#-bN(j|k!sSaKxYFcII@rEFdBD~;8~ zVHXV=rG#`p-7M(Wo?F=lU|^_E1BXx8&n`bDH)AF9Y8s6Z?%p$1S#DiuC*Bg8UjVpy z3*((pGjVN&O+68c3ZGG3hDYaH1clpw#LajmSN2q)K3T1p3iFA#l}{^o^IlhSwfhwBm%GSI_+)*%HF76CZC}}=j$u3d z*nrUAlh?nRxxxxWtg4$)YwBAcNk@Zt>SLQT>cRkbLnl zm_%zUWIb6*k>1vXDM0DHbOrVi$7zi@mHoT11&EdZhV!sxsYd4D-M}QD23Bw5#74X0 zakQ!^D0&^7lU`XUHyZg4EncV1MP~FU;3dIDzO$b3;)rF+Eb!a2pj9MBGUSCE(n(B7 zO52^W3K-DRQ%&wAo1@ z;}wm-QQrOS{#$e$InGPU;bW+i8;CR_FY4Vd@slBH$Qr_D=C4=;4JXyq{35eAafG<9 z!4;@hv1Nuwhyn(Ue?{eS5DIldAR4SpKb=K~Ral3Kx$;UO8iB(LSDyF3NtHvc-n_YH ze_EnvVRQr<(nEgSkOK15;NIW|9hX}!G z`DT#>sFk~7-kTZmBvfg^UDqyzD zL{~nNw#j22e$GHV?t2GB*68)EVrSd_YO5bQT1w_@uMlD_Oda~h9Qp=W4)P8d@{@B* z`Ki+IWNeFVhWnY+$Fitf0*fA>9jR_b%5qUNMZv*9icvjt;xjSF(`HyU`bO4QQu(Tu zwNt31G_=WBe3GfY?%_PkeWgCSu{`WXeNOzPtr+aDIm`(vOvWWRtss#}TUa|S_a5o7 z_i&)AXv(WrrBR^_aCzC%>otIv1>#HKHIt62&9vfTuboC~9&07iAIhZL{c(V`*Zv;Q zeD*p=xkCsER+Nk&%{>11^4%9sM#Pg|{DK3Tb|=&8F_XyFe8K#08d7)%kkyvy=#{%I z>TybM?(qcGe-E9=OBnMRE(}fa7Y;8cO8ENxD~yR9;z#ZwCqMw3P+@2v%2mpVfgxAi z5+B#F3TlSp^{n&>#|0WBb(?veF0|)Q?JaMIff{y6t9rA72K6h*Kcz9DurNi7wYd_T zXlg4+&fQTv2*&ACjSQ)J>**HHD&vVu7N1X=PU7ekc=w{&-R5<=Euf)JoTV&y#dK#V zmh+jG-|AjKi?sT&H5$b?HH+Wa>{Ll?E0-`F9qf2W2KvA--r;)zWbwcG5T?^~-DZ|V zUt^RKghbeNkGSr43iIVTb?+Mi!SejhLj^Y@sm)FbiTpP_QYD{^INOPCg^jGnaW@HA zN6WNJ3r{2gXkKXz>mR^CU{~hA&olPoIb*bB$Owy$$hFw(huG&ss%%a@eGpIik<)$i zLvmn}XsI-kf*EbW2iML&bU}7o>xA2fSIb|j z7`A&EiX@ar=B}5kW&oUBhn`=|ilN$lCj@>Xup>n?EHyj6!FP7X*xTiIo)iV?VH-cD zNa|@A%GR5&GB_V5O3WosakyC(^$mdicLv6}!Ok}~Yc97NRt=*+3fV;7=sE`=nGs1_ z@4-%_T+P=HjOXlG9HU6tkg}y{cXhe#c3J?S(z{&7Ci$9b@2@lzr|-Db2KL>LL!Md1 zLfR7=b2o;2JP16jr)dK{Qs*~xo$ayJm${D=7=MijJ1#hlg}=1fCH^oStavxmvUs-b zNE$jp(@h)P<(BZWq%Y;D#drbd5kH|nDn_azyrstSU)Cf7i!+{gNIO0KGh%fdQ4eFr zcP+7Z%+_OWh?~v}WVGYInF-|Jbst!QElu5-J)BQjXps(ID|^3M6f>kTPn3NhnPIhd z4Qnrdenso_@PCD=3>dYRiM&g1XTgj%al@>>jn~9`ljY_Mjhag(?lp|aY+MM(_E5oF zu(R#)G;JWGO?JAsGNbos39yg(uo?X3ISNogmY!5zcR?=Wcr&Fpa~&f+yi?9TP}=A7sS6>_-X%soLAB+^oM?~RWHN)zyX-X0 z>n8GaVn#Oju^D}R0->ALpT;709|vi;`1#JrZhCP+teu9eYC8Mqp#XhaR-k(K5TVmM z@X)E^WnqwkIi%YIc8}@NqrBm)0N5svR4VmP?jiMG79Pgk6hfEiI3O6?i6F=wCgdGm zE4w0n;9 z;edRV({oL-VsN^KDQ_w07z1EiTg`i%^6j^X}3S3#)`lQCrLe-O7oGf*#}=*Iqb-r;os}<`)Y0 zYuvv#$cte44-oxk36G&c$c8p=;Dwz>6U|Hua#kU*CK~LWcVrC~!cs|l@6^fo5fa?o z*eD+sT!%Zce9WAuTg0cQHDC_5b=oTqt2{Ae!y5M*P)HCytgdH}EBiK=QCzqcc}2u~Mw z1VMIru6gCT`k%My6Aal;?(Lb%fu=(;8tpnpH+8qZu_TL)^Ryq{Z9IMYzfXAtn}!G&3uv4(CY(wZM`!sb3}HMDsxtc&^4k_kA@p zEknY(k#cKV#W&ebx$=H+_3>|%dsB#xVwPwWtH0LD^hT=*ou<_ED^>-;cskEaR4mwV z%)Ewsw1-|^rgtqO^U&mKtj!PRHVMJLv%V?DZ=L*M`;m=F(sB;Nr%BcAC!QKXg_(ep z<)s-<*Z|tQFA;I};PIzF&czZdW`{p3&*-CCd#G^EINL=PBh)~i$XonmP8)qL>$9CP z<*pl`L_2=*X(68Wo0U(3n@kf~llOaIlYs~R)+jqC)YddzFe^(Mwkc%-_lL<@Zy`| z5LaihIr!dUXd=z5Fh`(E$oX~q!0$1$q5V)jrQm!Q0YY;*LrBx`tL4IdNOax`b$K`hJ`LU;z2^I8u&Kc!EJ2PxYvr#6IY_`c+ z_gE01Pb zvdCdclk0u`^88wP{6eyoBab(B38Inl{=3xHgqerEo-+3Y=H@jG!7q1kp)b*GA+?OJ z2fB96i3uqCHm}S{0jxZqVU}tMRa$Q53D-SB8z&a+iymO=^Ut1!}UvC`8MCkTD_8RKG>=0Xbha;Q{#d~z0$`u+3x#sW*5@FLd) z(S)9UM{0*BJ0TqdY{&;zHcBkO5l3-}RyWHpcJ~Qt;l-%tZ{4^270R@g<f zd1_~Dh9Zr9^#ZeGdv=9oibkzJya#yRk#W zT`X;Thk_d(Q0A%siy2joH$r}*x3vqB{hV*H`%pt6o~4@-O{#jW2;M+9OcQ%iPCPEL zy()xgSnLTgXn0OsDAA6*-% zStt_r{2`-XV297amGE1Vkw(N`-qs0k073y>e3L&+AlaoF%N-`yqLQAQND*eoFizWX zx~Q17mXtS-9k=F_5^7NxKnPT584bFro9?!VQ3%O%6>qI)*NW$yqtM*HG5i^VK*SEX zBiN%?3kI8cr8k${K%y5T{3!kzB9K2*(jP+L(}I4_0(SCof6@|_u5|?C2PpK&e`|F+ z9B?f{8pFlasWBfcn#*Uji5^cuch3t$?pHqJOnJ-pAValCM$0-4CLT3HX_^wXu2 z7Lz5>NiM_K7w#-`r}6dAg1`Myw}uwzQ*}g4KevsKLQz(d(2y^Q7GIvjMkl zrA=ac*!|20Ixh)B3)2OY{na5+#zjjbj-N(w4muo`Zwx{&qs4p;lx@OK>v$qro-^Ke z4-?Kvz%?w1oxPpR8k|K^h&A>MK3B!T0Z$K1X7?Q<1wyoOB#25i%RTAbHs=(3X?930 z3z}X!mn)b`O{5ZE4vf=GUXOv0>rAQ@Kz%j_X}LkbYbv@6`~oaUE3y`gqCOsPXJ}0( zLsyhSBhHSH%SR}-b9&!T"M5Us39gfO~YwP^W+MAMMeh2p+i4jpv1D}$Fh*fzcw z-_o)--ejK##C1FT51{X3>iSrZ!g_pxfZn_tB?{(hPI}Qzt%_4LtwKajWoGh7XWSAd zDM_hz8*Rl5y$&ygRD%9w9&c}mtDK=$%IN5NRKBSQj{WZaH^#<@UzD(23~|bQhqK=C zI$C;rDi@nf$w3L30>Z~c>R+nPn;sE@(taKAFQokjM8u{zTp%$q7enC)vCI^c`Y|D28#c zOA=H-ojY>s;}(*ZzQ8eK;S#5-jfc9xjKreuG|M~5BdNeYk6EzsrG%XN2!5ahw zU^?N_ns|IBo`@r0n%JJlMGy$TtGI*3gf6d15-FoHQ7{lzvo>@J8)t{m z5iLQ~oQ57O%~iLVvxJ2ReRXs}>-r1l_x!o5Rc5fX$9EP_El6-WSB=vl$jy{*^9^<4 zFWp-{m0Ff>YXv|f?~%?VNNs4A;FPneRX5phDD#4LW=52dQtjY)q)4E9Jd7tko%-$M z?Kvg(Msdl(G+t$*mmshxG02QLYZsNN{Y21tmjc5gAvr$i<X;1A|_BrajH0>z%K<+P$qF3{%FQkUpkvZY9cJ zSh&a6M@e`12mHeVUJ^ouv%J?12sS>Fp}jpJy7S`Fhx9L}F7(Tg$-+mvc?A z;GaNVVq3n!tp>2nDlXZ;bPM^3{o)n^m;NI&i-tx8y(Xlq-sE7t-Wag)V31#Z_z0=q zN-0v(P-3V|%T~q}+#mwz#}-FS0}!j|8lsC2`3@rt_+z&82Z@EcubB2u2_#^im(J^^ zou>%3c=~;{ruD@nP{8p54VN*2V7$&ypC_^uQ@vwHH)Gp1$nwJ&HpaP0&zKhvt9YJ4 zepmD8hWQGt$L(l~me_|GLnNkvVy9Kp1SU-aoI|4Buh~oJ>N&%ul^!|JE@S1nTgCWn?TybiF)1>f8XI3%pN)i6s|+VPdRVWVls!)fsG|(wj6Rk z*Q#cYQ$rynn#$c9)rhQ3yT&A}Cdy5V;yTmRZw+}Ds6UMV1$)O?B6MZ;RgZrN#>SdE z9AK(j3UY)%VG6nNn1Uey)6& zzU?qpVFz{Wd5_GOBd8D!7y!$^VhPqm{rNzo{2f}Qy)=)Td)5LmQ5+B5%oWoqsm*Ht z%gc%38f-T+U6%6C_heiN&+AU*Z05a0r=*IsF(X^VDXy%)87O!>>4dy!4-#P3+)xTy z;h#c-J0W{UCWZoc{Y()&j+9nr53yjsyBb_RoxHDK;KO(J9{rP!&pt$nnBt% zLONIZCK++LDMOoZvX(J$FKu7!lP2V^muItiOLmO9eK(8e_J8>)3dW;L`U0c7_ z&ln!}3VXXWx6u1uQY4MY-Vfx9T-UKV+ApvgB*Md6C&8w@c%~KROA4@T{!^2?Ia@=_ zOePMQM2Tq)H%{m&F5f~+`^>MLr81)?7dJTktf`jnEu&-N;9JmlPw0HTzg^+BWIT0H z^K~*@n@T!7#GmVCXMpeH?vf^eC5B%!vX4$DiJP0mHYIS}@Y57D7eDtvTAOIviBj(} zCMC-_NM$W`L4MtR7kd5&o!@J;KjJ9|KPTP6Xn6!HMmZt#BGvJ-5eRxyr=o_jRIT%)C-g!X*7koK(Sk_ ziYIzEM)~U*fxaH)^ZR2d&PK&&65zb}@+SrQiga#0lY2YV$VcF~($ESnv7-RE?hDG% zU~8EthCpVVy~*m^h1 z>76Apqu1?Wo+?u&d?HHh_VYfhqo{1l{f;8Vi}ttYRNfF#_07nT$=xjcJ>8Ex=){qZ zSALaMb3-jpc6gJk5im>>2Y)s-U&5`(F$Mbu;QIEWufwtaJumR6vU$%soX?BF_{bQy zrH1Ih#BuPgByan#3ut~=B*eb$Fft9cQ*T#axk0m%s&(ZAFs-ym!W9OyUrUNB?Oy#W7y2FWy)+Z=nA$Q3_-OuJ_MduJ>KIs@J+Ip9+=`XQBN zRn}?#!~Q0(U8V3_x76BJZ7HAYl}zI(qyNTcz3Bdar#GH82SWJ!A+@jm=zz`2Wk%e; zfDl&fKR_r`jCGU=p#D;)>cAm>XBwx(({td#kkz;D5zS1>64c}SFQaf;)}}9-c4Ofa;1=|HBAT;!VUr`S6IG-9L27hi z)j0z0C%bIKI%gOxt~nq+_7 z(xwJb@xARb{7hl#ioOxwAL1@?#hS2{X>Vxt(K=HZ>y1wq6(QV<4vzUk2Yu{ISI?%_ zKaDUNG1Y~ynq@VK@j`OWPKOkJaOp{|d znnVuutq%lGznV~mT(o}o1mz*u`BGgxXwI>w4)OF=QjfT%IM3YaM!JGU*LWD1cJ9eS z&2wKzSVL^tIbUiITaveBWel}26vy>m(UW8-AQqxTgs4Qu?Tk)HFZqWiU`sScB#X9J zsVp}FehQD8N6prKX=EZ%`=O+h-80K?h64}`d;j9~x-`9ME#=?%PE@bXAUJrd>N(fQxMat+>muz|vSe6}U@cd8cU~EsZevxyhj|ZZ{>DNu z0FT$PET&sbjSFOLM$msgPcm?ss52Bp3qAhokN3-JkeoaMW%9+%Fr2o!>qsa2T%SgvF7{J6;| ze&+5ra$Xw(yyXd+E@SL~2HG+?k$0TXVoJ6yeM>GDjro}7*zze|{Jd5P$P2TF5&iy) zT-`6CsIQlIl7C2oP?4*YwKzFQqKK_lP-9`_W%UK}*K$|21Wy58fjKp;ysa6+pLJu^ zjJtUht96F@xW|28%dhI?-_i{L2o%s-Kb633tAMVE11$a(vMqg%+p)k0hqI;t&-JJu zo(GYU;g9#m7MMORjQ{7km_zu&7pI=B?&^zezPaX6mycjm zz|0^BHQ|qg9c`Vnxwc{Ib|5yR23ui+wM|#Cq7{_AP!ZTcx|k)mT71lWzR+LKke={H zD0RQnO8OKlOzb9F&l_Hq8|ik%c$KU9P9IiS81xSCs(~ainRfNtL;bc9acTDE;U*_{ zW}$!kCEGmR|Cj>?j;5_7-Y-`eY!CV)7RENl4pP2-{)N6BM|4lT6NH5wp zedrxxi#Nm)lgb{u6=FZeIu!@qU+<@{Fv|i+74adPf#r_r#l-2*KVZ7NV^zL(zCdtY z?tm?UIF2-&aY8qkrnp}FgVazQ0uUuwq1Lr;&nJo1XuI=41Vzw9B4^#Oz` zPOh_7I#PZs1xYAuNP~mdQHaj<#V|L2Y=KTy7cDNddKLaA<1)%ja)O~_jT~oPUs3m< zzNZfkc<2f!YI(Nm>m!pjixiFJ%UtPQ)o=R~HXqWeu0y%a{Hhp&NXMH2Bt)L^wY;rJ z(X^8|<294KP4rqI{t>4WYz0Ij7MI$+%+f}x&IFVXWH~rWnL1S)2!?d*ij^w3j@5)b zL#n})Xg?^(wfb+Z_e~VLw}oU$SPRJE`?&(+;nt0b9EFrmOEafan$+vZnB{&qeK}Xg zZCJv`NZH49LN`b)-t@Y+Vxd(yOa!^bDGMUgpGllDZ>tIY9HJpU`y-y2O;XHY-7UzN z;gpcj9~OAQM>MO)>Zo^bxNsG>I);p@ykK3=sPM4;CYe;)q(P3F^XP|9*X%~T#r9Oo z%#^H~M=c3Wog(RYg1MQ|cZXV%YfvH*j#hN!K zSijw{=ZcR<4l^(3b>%%5LUKhT zYcOE|&rtb8mT=A-Q)Utm;$S>a*vbO%cG!Q~D*fY43t&I!A@ zGT0+-_K`Qd@O-q0*}Hvc*Oyuy##T}jShE~kYF_u19Lml5xx7~8W;yFUyuDS{Oc zobW4o;xUP1RfOj%@)G9sNS~iH))(Y7bDVNkAqsPh=xB;iZ>mc$=oh=a)j4EfAFo;6 z$WN!qj-O>!Z_c-0dnB<^)fZvSYSj?Z{irbM5dz`0j=Q;#vW#t90a*UoP(ZrBf-~z7 z10TO=uaU(n3cSXl(m zkI{=svI4p5iOatSEa1ENLkiK6gSwD+r<$)Vfc(%_N_dokidVS3&8PFXygWUtH_`urY`W*Trc#Xb008uke3@@1s ze%Xbv6G|vC?3{3Rb{=`ZOU1B4jL4Vo$?M5$D*})JoY9D5&(ExfM?A9C!uliHF_$#N z4iK}~g`wGxr19X%e`#^u2`{Y3;U`j|h!3MA&(ey;r`WizFzxnxP(2{g*0eaWNiSEo zXBNrVF+Px}R}iORW;su%;h{h9y6_34K~9t4geDByq`~36D$?$o^kLWZ0Oc}nv1Flu z8`&Zo`b;xd=%aYb9IksT(Cp)R7SnU}m({A}^`zPx()CiU@6*%= zXk?T1J#PX8*`ciRA0C1P&K_&e(dhCS3vRe9Um`Pqsc*O@7s=<1PV|wv@)mWbom*d& zGd}kwB+bq{k9F>PW~;RUc1RMs7jT`&Oqv#-Si%DeD_JAcUw}2R=EXNN=#%o~+rJ3a zOlzfurjDc_jXL#qydw?`MMFfIZ`<`)B?qML`i`%J?NMFP-&-L0ZPx6PY~|efHyI8j z^fH_jlzWy3q`m9>pUg`47z8?Pd9l;L?ilVVBu~8BS+=U?cZb$$ItemcDKzqxD#8ZP z@x2pZN+9V;358H~)%=lo&5_PQIIWT?Bf`4+`2&gP4ffT;YN?d;SK16Ib>U3s74vg4 zc21@$$sWwe+$*%K=__u@w1tP#facU13aaJmu44}Q>}riGwr1_!XLYj!5NNyzUJX{(~V!_w&eJU2r6#+{`MIOvaawk zI+qnC8mPaDvfGCyQG?LO3Ki#;cBa*G^7(= zr+42#5<+u769*?*s%_RH?OKAjRxm7{GW9+ZC0pLQK()^#{9Dx`Bun0=VrNsp+ZU?& z%BXzxk&M$%{7W$CEZ8a*4~zcPae*zom&BPFw#O!a#xP8`=RsMuy3t{!FqW@;Wica! z6{|;sl1^`v?OUF*@MU4j0~Hobd7@a87)5)9C)hr7pwMz0^s8A+Xw(T#Z8mp;X$K*) z+A+kpbpE&Ypvxm1CyxO#Zq@cUt_By_wx!}qsVIG6%B6kMV|*2Izza+1RMg{B56J={ za?|lEK5~k*C6u7P4{n1ix_rBB&>VJYj!QfloR_iMYYX7S*Uv>Rqd-WwWZX;jyFN(9 z?h%EAODbe>f%63TK$Vg{o@g>v)Ywr~V*3_^C+Yt+gn~9cr2#khh^KKKkymF_-z%rX z?v036;g9orK6pO%s1)XDnd!i%Hc~SUo0lBsY@=Z>vJYB>=_H2@xSXp_GwdSb)9^GI zQIE!z0ZZ+e&Nk_QW*!FEit=?@QGfg$sy5Wg8hk=GX}hz6!jqR{rHQboX&m>sZx)k@Y3G#EO3=+Nn~c&0L-;&$e;;z| zeSL|qF@NHofAiuCz`~NpYYDTQAP=p1UcLxuQ}3A@RH(ca=NwZ4o;2p{JQD6*e27gAQ(Ct3?W}I<{&~Kq9p){6P2P@5=PhsEp zyu2SUd|P78@fjY~KH3)IrM?6+sbIFYeJVWp%tSiHWU3~m`;xWml2R?ltWxaxvYom- zxOFZwI6I`Aw^Zd48MH84km(&>>RVRidw|KLsESzoLfUD#xut$?W&hzF(<9Ja?M*1Y zYDWeNo5r+p_DE=R$|CAcLA_oOU3U*$u(B>~*p&hKG*VbzV<^63UvZA+Pv2cct?1N$ z%y8%G%@zr{K7XF6+G)`bg7UvV*XD3XjPi&L1bb2G-D?hhd)5d%wzOCm_>6$t=06+x{fBm#dCNaf zKIM1(;Z>kXIqQphHt6b?Okr_N4=OS_R=;fd>7eN!D73Jqc^z|nj8oQ){a{oFuVXvZ zY9g3?>4kC_YNW<%nP@`Clggfw|6w$_;*d*DV-t{00ME9aF7l{XGB*gaws`s_yWp4jvA3s+<|whB!j3GrTW zoO>W)fca{2aJDQ!!@+npglF!M?nX=`vh@X9VP}@%!z7#yM{~H`oO#5Pi|=sCVm1@G z$({GJ9q%!J#w^Mg)SNUF9;TxGBGIfjL_1{$C$&8@piRT+PmQQ_oN_`MdRV(^De)bI zk5(czlzp6i7%v_#qFUR;rtf$Y0%PP{S*P8+Ct+=0s}4^nT#>h2%O!__;kM*7e^j8V zx^BO>Q>bO-$9~Ii4LNhW{_6`8@{3qgw76C!LPCx0$4QBTfY-ta1|^0%wTPgc7b~9X zrnU{9sQvcGR|kxIb|;i`t7DY9xGni^(lBPCa@KEUxCgYEPg(kkz{3!|{bYXK&c@9b z(y5P$d9X0UH0bg7<(h*Kly=zZ0s5t+F-fpEr&@5FGlG^$Jk$$#fnRj1(I=nKebFOR z8d9n8ygHLyS8ngYZv*1{kRFAO3eRg_+{@RChc+3KcOi zraD7gTV+Z$9>2O7COM7<$5ET&qDPyIA4xMe zcIAzNAAB}KSMe7uAPfAPBCajTA8IwNT4B7zuj6c<(2rkIm$>XP7qn1b%_}JF_u)F} zL$&kNdivRaP8sj;2SVKbK0UIqnKt6cG3S!|*iNyaXFds@e1SJVEfKV1;g(kSw5u}O zZhNwI51My$ub;KpipCm8+N)J&U@$)6?}cxgsSvH63s`>?gAWW6qR{k?czYaRDWr!M zP+OReZXD2G-|8QhXZ-|j_RB_h885`?XpO8eQ^zaWKb@@SirQFPJabLmk=$f>V?wSn zX%e><(a-M+?@W+g2&|5_LQWsj(6H7VAur2V75g1jG76k#N>eCgdj$aDRtQR$0Yi+` zd$zZBcq3;5t)4@zBD`#lMb>JH(4wYzC2bO8EoZ$Y9R}W8Hf9DgOvwAq`rjN{Ig{f~ z>RTg*o*d3Nc^>TqML04FN3D}&EwEFvNt{vm+D$gZ?MgM7ByXAFv|P{JKzIN3 zqC@{-rh|E`U4nkBa+2^357T-fF~3&RwnZDUK=##(!LjM2(hXKu_>cVur z2Y<*i{?#BO~9Vk=x6?Hz__}AD}9OlY9ozRma~axiG;+0P-Ys`Nlbype0Rc7qzn*bHfDGLfbVOdzORRhdzlBsB2CKodauj;BQ} zT42@E==zLv?fj@46A?qZR9>&`{mnVmx0+B<%8oQL#cPKB*YbCB_=;m4c?PtQrdwh! zR*_fUx$dtBJ@G!sTP%HT4p%F%4Plno^DVWv6Gp72USzfc7cz3Vkm48Ka*Wp+bR7ex zU7u-&?PM|u&l*5vdHr|XVqaj@ys#ACz}U|asOmU)59VJj3*OE2h9RVYG3}79A8t{L zN|LFxKgzGQ7#qF`EF1^iLsA;R|E9l(2EKBV=ddO}NPG^%^EGxkOkzE0H`$yqSPpS3ltr;(PQ?M39#xh{0?pKFeRQ^l6Fak>+Tqj`uzPEXX+Z zc+_NEz0ac7yH3KSxV=vEDvQ}EYDL71C{=z#yyFoGt1s#%kH~w~r6yj&`lK;_r_w!9 zU>mKCd0kQj8pT|2vw~b|Dy`n+LZxl8??W4CF)}SxdCRfZk8aW%|I+KDzC|wvt^y2@U4OP zObv&9wr&zelX}kg^Vhj3OTY`4!%A?6`=0PY zdnD_WrpV|u$)m?|#QVzhn9H|VcsjDD=4w&3w~)uXCD>ZFg*#?0ANTlRp*uyZ-tYEy z%|@ajBXxjc9I8XDzp*xE&HDLW=l#MOTzRho+p05aTh%cqJI7`zeAzT@jGOy^ES*(A z99`F}1A*WW+#$FH3p%(H+}+(_aEA~exCD3iL4v!xyAJN|u4lggJ@<1ryLKmELiF=hX&++AlRNk_{ux;z4aQ@arKBh?H zrl5W!AHbi^wSL?ONALNkq)iG{45E-hnKOW7tUc+BQcv)gBI%43{x&pcZ75uMpdO(b zTvn5>K{8RPp72Vuux|Q5NPN?efPS)>!|Pm|DIvr^0^t-f_kO;Mj(;mDaqF}NUGUBZ z{29VoulSprzmzi4$kcj-_|+Bw?cqNorCjvmiavrr`&@;VIZ6mT)_3hm5H|P+oJv40 z*jDr^2!{l6ns{QDN-P~}IW3AsNMvdsi_xx*;8HR~J3?#%%ayLfo&B&4%y-UbzOJX;Cxoed&SNVRf=4^2=*7zoS9PE87Pfp0@X`bDAk{=0Z23&ISRioht|ak z1%6Q{1EQKDj1qMqi)ktMKJOBPKIv=$gewlp#Eg~S_di$miUwyi#;`fu*D%|+!YsE` zxjNwRZ6BTo@-g<$l4UzHwdZ?%U${0VgIplvw0Qt|DWZ{u~sq(YyXhjQyjMR4<>iE zEWk-1(YEO4@?lW^BjZDSI*GfS#CVMmkI{C|uQ{a(=_AOoPAl}-;Ak>|Qnt-pw1S-I z+6ba~4l|S7v^qa(Z~>#DT`y`Pk?IRwBVdBTskJ~sg}3eNOf|$9N;~H^=rV-;3@Li) zRuz0Zy!vup+5BL8hkF`pDYEw)7yKu{fz}%a@pJJUn5EC>Ae^^^e@`C?O8#MIk?nB@ zdY@YH`*cS(Fd#|M86VGe?nbq~%`5jf$)xaJ1)HO49URIp@VftG?HX@Qg4e_MJbIWu z;37y(uNrW({)NPryKqsRy^y*OV~Y}k3laPeB?+^aPGrf$wwP|)y;YkA@V>n_bi|Rd zVDTr3A03LBRT8VnX%@%Fd-~dMT*R6G?kV%@*vW_}M!20YPnzxin_c?)e2KVmoz$6* zu5Iac!y^S5Q0X8sgFR3{UY`tZVoA#;w{Lp!?9IoPAOWz2M zNPGkF(gzNaohXQ0Iwer*O2!M$SWE9)Jth{Vd<4mEXm4^hijV4pGwt}=A_xVy7Wnx* zqOE72?0Y{I@@dr*{QciQ?TsKB-it@veTao zFdomHO{QL$6ibj4_QZB*i%fjNo(9jK6qHbUzaVw^u82tBIz6qzR=R78D4wTM%U~B0K0}<9pi|eyUW^HG|!gW zz%S2dr7=A;JG!Aw6ipPjM54N)57Uo=40#m#{;lf2_0d!nC*0B|sm#JhB3{k+R#<=8 zD5P6TO{UUGoHxB=04scwZYap)h|}yJ_sR@|?#|R@OszSTVnp9YRAw3$@I}{UexR|N zsVUQl1dIGlm&ZQ5fv6aMk?Dx7Oz@+QCyu*p~8z9T!@#l{e=qu=&0$_KNQxB=D z`+R%5-Gfzzf{b%{X0oc?UrT&_yliEE^E#&3Sdnb@3zAM{`S00S_rzL!o;-l8?{nzt z0?9&_rC!eo1bj&~CQY2a@QV&P0?-k0qS0}__Eo~C6EVvl2X5RYR@7tH$?r7y1`FiP zHr+EHZvNIbivA(c8=Z1;D*&~YrnmAm)tdI$qdT9^<%aDx11N=K_Lg-(u=1UbjG(QC zjd1%qS2~@ue&=%AYFu_w36}+wKSNziW_Y1yJ;J#9ofvScx(-TacBBITz&`YD3Futy z$`%p1S)-lRe>zJI%A)&la9t&saj^#3I49^S|(3eMtS&G`I@FQ0lIW_UTGK*?95F)W?Pw^Qia4$2i| zqFFD3M^nweb6W3!u6go(nf$?V3=EPePCev0TfWGTgyo-x88wGkiQFWUz5Uf1jKXXp zFxvh$=J+|STK4@EQgRj94X1q7ZSoiIMMYJ5`=VdBH;06db!aeGT9>~fc1y(Tj$ZN4 zi1y@j1Nw4}HPHurXq!pZz~uLdl#6*JcN{j~-+VeZBJXcs?oNH4zizyRcKSpx`{#|R z$_Nzn7vrm>d0>rllEc#k1Vv4wymO81)^`qn%4CW>`R59+_OJEj6y1JZin?axiyppO zg>7%{{lKU8es9x;O7rcv5tQ}ZxW_BA3_r+;ceHN%mI{oT(K_R{sgY7 z5@F>?d{^s#$-ULQ|AG_|%SYaeCJO3_TYYQ?)Z&_vU4ulXRLjGN1k0mNc&yQ?s{>lm zF85+QfTZ=AYZautW}_a?TECZ#la6?sr^4;EJGdJ%1aCi#F4{YHxNCeS9Zbyu9=~&F z#64YQ7)#eWvKnV?{FZ9{Z?d=?*2n5pKxTB)5d4M5#YpO%39=bet0C^MjoE zZ(6jftH{nfEA~Ia1u`;CKu1n+nW0Dy+77<8zpe3YKus_33OLoxqD=hG(J9N=^ff-q zsW>1u6+iW_e~4?*c3(B_4S%j%(ng+tNC@T=+)JGixzWr?DZt8`sdaFrWE8X;j@w(I z%GLTOaevzn${>_X(mlh$Rxl`7x0l5umdgDgz*?Jr(yN=uIPzfg#Zgnot)Dkg(Bz__(?ti+y39kuY+3GV;iQxhx=%^KE{oQq-3CLR|Aeos&W zAFv&d$ekXzh3JTl$UH=7Hf$`aMZ<9NO>}RHIa4+_#zqa`gp@cUwcdShfmo4jej&+F zq@NP8ku?C44s-eKP6Yjzo6_}Vj9Q{5e%W#~utc;tYKXs_2?=cuvI zf^ai%q}Q2(K}=+<22YLg#E*#odh2_SRMybO6j(g?Um}<7wo^hNsO@me_(cnQSof^W z1JBj0!8h^K1aEW8o}^iod5IF{n_tPIpk8MM(kpIx(_|`4z|Bqv_NlFL)}(b{h`o_zLW3`d31fRAy{bz z$CA*}tRXmZ`B|-jac%8eU^(6DX+Xo>#TI-_{EWDrDiLYIer;*VV&j8^|6fciF~rZM zh_GHTJVc@8kX~D>wR7&bg%M$^Mf$sOpP_OR3}tStqvG#~Fq3K=8^>MV{&qbOZaJBK zLdL&FrV=lHE{oXX)75v~>Mot~D%6ST($CUA*w|(Px&{`c9u41AwHwGB3z;*BMmD3ne{BNp470wTCqMds{MzvOgS4nbBx$PuuUGuTq^!m2 z1qMdAiVBv-R2iQfwH{8T0k(hGFJ-K7&L>Ne0$0VxJ+sMD@t@WQihJlV!RDBQ6zmT? zDp%Eg{6bE`C@#t%TUPSDf?Q7myz2CN97;*w9877`IwbEZxI_g|fB13f(SC=I`QNqK~7eYppOz6L=_OrI0iu%?|pN?lx4 zWK~$T6_pvg|KzajIYfC@>d+|h5-p2Ar*@_(=la9y)HaSfWOVg?FzP%V&Ed8#ayd@& zo#Ku9cjbBs^s;j_NEYf3R~=4*ds@~%b(cep^cd5+V4@d;_+NA08s0+fM+RlyH5+r_ zG|Q^ntEP=Mbdl)HDg=Ylo?Z?-jECI(srnKcW{-H|;>a0SrjY80CaoUH_$C&SX=EEU zFHfx4FMA@kn9VMbkBI+lC2o^~hnzl6)@^TCz&+N$pyS&YfoGU(zvu@)C|MxxGk+a} z%S(>F7a1JI!`i07uJYezSDOsY^C<*eFB3fcnojraEc}#+^7RM0ap+G{7H+v|lQJHHzFkhTx^;YfE9go8 zLQiQ!ZAX0eV(hxr?$s4Tu6HdBVRJ;(w@erY>D=&t&&~lZG#CU0sxm&vDjQ32`aeD7Tu4LARLI68(3NGs^HZ2C- z1wYrv4a|sA8@6_CFNNfMQB?vN`s3aqy6*C9JGT1*Ziy8*rF%1UpW2v5j;L4G6V2(|bd4y2`ojho@J~*};Ld?7OoZ3sC@igZ@uq4`K_Bs#vIeQ3^r%suWu4sN4BVVSTWw zv(KTkZw!TU$7gnvCy2FEHl*rs?&uw6&|#n>G0pG%?NaQ#1i>9WB3>3;yVy+V#$Q4? z(C0_$NXF}2PbkTfb`?jmqc=w|S~GAP6koWHbNL{}p8j}AKank3Ep-1o^vs3Rd$Jj4 zZgjMpCS!SZD(D3QU>3LjjpVnF278mGx@0%>#xjsI5TL{aC|RS2&s(ezOY15m2trRi zj>C)ktP0ZH7zm_#z5E(MXxZRJZ5EdiNo)CZkR$y)g!LW7lueY)Gc2%(Bcfh-)*ueH9LYaH>w!V2T5+Rls4gZ^>wXs-9iDEw7N;csk7+05M+;;h z68r`R6TddJ^?TIQm(2ZU+k~rGdPFFJitQCFYKfmzq0nx<;EC3%T%Kjugv;coPv$pD zoIJ66Le+{}js``_6&(&O_KQFVOib8QuFG>Q>Ih0q=Mx0g#(LqrHliQ>Eaf8DwV%QE z-+~==+k=>!Uia6qzb7l%D}^n(656X)w|gt91ma1hf3D2ua*^By z*%tTSorIr`BQ~#z{H5^h#l$PBC`i6-QFFh)x+5;X*qkQsc>UV>jz?-R_+-U7Ygr>l z*&ri*>1E!Mk~caGl>-3KQKsT49Rd~ie{FMHW=iUc_2n*vSNt?T{aE0?+pgYF-@rdTszI;s+7P~rmGVOiiVGDKW~X408Mf#LV|r_-I;48w;s zr-cr6>&5bh3j+qr$#P87H9b0X)$ zB3zg{&}Ck{>?PtvV`&X%Uhhr5*h;?GoMHFANU#xEww>jwOHIrfD7iFm{Zq{QxJBaE z6p>A`HI2Byuy!x#MouNrCi*ogI*+Not|;)xPjZ~xf{ zXw_>^YKUFZ*CO7vI~o7v*r(Sz*G2So#R{p;;Xuz^ImG>r)`PKPAd=NcFjh{jz%ZHf zp5CJ=Kxg#P3ZYTs$`lSfpQZfOs&C!VMvmxSQlVJLVAsN$yPazj?turo0kvSNMKp4I znvD2pnA*N#bAkT6+}I715Ieay!TP%DT-@7E3-63+xS3$CK*(HUf}zYfq>mVAGRpUn zhk0CKd0VG>=QCg)xlF98ld>^*kbHH*@HlVQ#wPby$i?xeZm7_u*Sp{NBKyq_pmN)u zhD7S6U^mBi$h^hT>N`meM@%6^UkP@hXVnoc*m6$BDxHBo7A0vS?!o}E<=zI zLMh7qaX>gQx;^4S`q2?%qhj*_rCj|`-Pm8gx>nKC0s{W&!T>#sAa&Zt8|y<{iR!aC-TdPWTH7a&|opV zPXgcU&4{>+7>m7xkJz`L*)7U{ma@Z1rpsVICJHBstGYR&-}MJcRGOA!0Ew@%NY?~% z&20O6e9iR2{yFgq)0LeQj)ZHRL~N1Jkx4?Y(wYBZSRMM@Uyw3w-9DUYD_pyJYJOc- zN~VQ)M^q#2`s;+sE4XoY9?Zadq1V-U0)KGAS`~`g^F@RrBn{+lx@%}tUMj$Ptbx&L z@KnbLQ)zMV>GW@^Lhfo?yul{8$R~%l?5*7IN6+T)tfZ z3h)f?1a7I)&%8xZk&MhJK`V?0oXIL`n5JvW>b8gVR{L&mVwe5?J_1uY1}u*&a*oorU0Gz!eo zcfN>D{_YV0ZQ?3MAP20?tRVWRfbRs15us8N_7Ws z^ySeD>f;IirnThdM}g61A{fS@eMhrF!t0rfPdT3@gpVqgBHQI*?|9x{W|EQ0aoSud zu;45yP{96fc)w)XQR{5T7U@Nzc&%GGSb4-a7zwjUVEo7hD&$Lpr;bPaSWlPDPU0}WiipabD&RYF z{8JsX2oc9lUko6N&FTreI0&>#Xjo9gsk+zlhvEW9m>~ScfZ4Jo|G(tzmaLWqP<4Zw zb9BH*$t95{zEY}3U0=C_Cg}J@Yp=@Pw^4C#<>%oNjsI@RwlPciD?IkGu*Xo7e}-wq zf$8y)5~>#ZG;OkeU4?O*5bd#g?x0Y&Uor&>7Xd%4Gc7L7K?m!_^0JT}(rE3HcHsoA z@0;MZ3G0Xp7!2zYbqYifV@pzpv*BG@k>ua{-tNdm-dCZEvBA$U&dj$GnUhi7p~FRc zMIKWkq9N~$nvv=RE=jEBju|&oeee-%o-+l0=bGzXTUlWwg7o@AfiP37@>oPj`31vi z-PEF8R_|U*aATd-Q7J`Vm{y&sEXfcK-#c5PG6T;~5t|!H@oTqDQjsO!P;WpCFPb_% zGwk**ifILdB#srTW{t20(nr}VOs?4h2{u^1GD8?2dhIdaUA5$;l!-QbLvL0bsRyI^T`jAa%_imO!yc|T;7XDzZIR=NDMicCD7Om%r z-%EZGE}CoeGO`nHZO~amZ3U?JHd!_=J2oC`@_tj3dpg%`{>1=3$qKns4~v(juuk&N zk3O^eUS&w%H$-{8dRA`Qj(R^Q(_)e5RSmExJgaFDeIId-y5-YG^p^Wh?4sut?n}5r znlMNavOAT#YHj2|ItsbhQq#TxEd7+5yDEauE66GgOS2jeXC_*6?HCdC4frm{iOcP` za>kl%jtJ?AP-DG8yr1uUkym@_Cm8c*nKT@ysNXngc`Q2Z08*_XjF<1^Z=-Js>ZRL# zd#zHQ4Vk_=mTJ|lA&GLzx#?$^>tuGSmZyRho)RFlQ}*dy>d-n8=taflqy+`6XH2cn zjR;Hh@cVY*Y1ZlK#D;CZ(g`k-uGTph#v4=$c{0>bRbl$QHK|mkzXq}W*B24QW}X#h zhxTc$K3A16#h|BAaMpD*-&eOSat1FC0hQ90!ag>;m-89br$=kbX2c!$NR}95oH-P1 z*?7c6VC7~R1tcv(=-3Gkq{;XdN$rK@6{k~R>138Vs@!@+QB@n2^Y{a&pi<5uwAx?+ zS(rrb-f%B`7gab5rlvD6QJl{!G@Eo2?CpXZ6@Om)^T!71i5V9DDExyYfhfI2FN-?sgH&@~SE^MX2IS^$iLg3mifn6G8wQV8tVYtU7haE=ZKho- zvQq9ViMmFBk4`21Tj3ukY)FY^T(3TH_WAZ&dP;NXHYHj^K+}NJ&l0~P{J+Xf0uP=H z6IDpssJhGb@EiKoq8RE_Ep_RDB-1hd!AMYb(lXAm+*rjXv!Dup;iTJDcWaXiK83l3 zqN&uh+VPc%UwB-LNb6WiK-qd`Nm1T1SHIx z2Ud^bCpNC-4%@@f z_JRXwF)WSJ1MW6n=knJ4-1BO%!X8g1Dy|CF{ZO=Xt2PM=b>x|Gb5G@RRvq*v5ANPO zwW!~!c>BXfV%kfD{RLh9wbN@k8 z)UM^ybYPDaJ0n)l?>x0yGcguL%O}PBXJYq{6?5tufuDY1M=c>Xgr2fk46GE}`fjwV z%aPfK!ggDz0&Q9cd!|aAIow8Bp8=^qTbwUZtyb%w^J)6UToFqEK$Y{fajwRklCMMCk#NX9J&7uBk z75w~kHt)rx7{a?weBv99T^uTk83}^{wosZyT{Z{6=z_mZtsQgF>ySyjG_evoj-vZf zh^MK-T^N1uSws1LYEmrU(SADL-6jQnbp{z7+`j&w(G4sbWAy>4rdI0xkwy_*A3Ubk ziuZq@dcWj-eRdwxEqbY~bhOt>Kc8n=ya6BLs|2BiJ*cC{O%)#2FE@y|fl?@Np=9v8w%I{7Qs2QY!U+vU97>z&SOOGrP*>0-v`Z%x%KaC8BL-#P^aNY*#>g7S4{d%>1}e3EP``N z9b#(0>(TD!_;w=02;T~m;z+pB$8m!9rM?gSo^h!@ii~pg{MQLi+@mO;*>T0Yg3IGE zry+YJ)8(x`Ng!@x5Wgq=Hlq9r*n$mBoKEB8=J8a;S0|WJY-~P$%nAM}zR@H(>aRLp z!~BfM`W|Yh+L%@rLL{6)M-i`EdAAxn}O}-{yIa^I&2`(N)afg3(`Cq zROE8YJv(6y@#WMs*L~JajRPQE*PzLx8=}8X-vh%^x!Fvq7VH_Na+J!~pxXaZOF?WB z+5?Q6>3^CpU3iuRa;ly<=-V|zfv(VO`22#af zt&+An3?&bL1AY)ZFIXmQVgLyv5ed>Y{eF{PY&r-U8P&hc0{p%@&a}s77yL!F4KB^8 zg{vQ&UN~)|1A6!CPRIq4NX{vZUy3;R=kl--m6bPr$4*nV1=>NmpYMROfgfR`>IGaD z{bTBwJS2dEPAW!#Wyc80^Ga!~()BjLhdoecC!=AgA8$OQ;A7us&eI&42ui1_{9^FA z)|r#DIe6%`Qm^j*s|K1Y0?$1lHt{nD`rIv+HRJONeLlL07c{AUpKE2gI8_03+%J5; zKJf2jnf{+Bm_~~$i3I*Le4nxZIK9f6ZWRcD} z;IMyr$44JE_P1i_AUB4rqVplAZtp9$_Z@-V^GbQndXFj8h1}*FE7oWC=K~*YNE2l6 zW^*O?9#V#6B@wh$x^6bWI(A};Q_^(JmF-NhDVmjwA&CKxN7aGRQYFpBJhBu-$Dp$GtzkeYP$(Luw zMa4Nz=ksEujgo_j8fS7QR0n<$2|CV+ITCGY^Zi70u?ZdfCmkggAoqJjs{=HvIAN|{ z=1?mD(W!}jX3*sWmF?ndLl+g#cwngS^Tn`UgC`b3qT$(`N67b<%`Z0yS9Hb`NoS?>E?{;NW$~JG&HP$03?;$H?PlVZV;mdgO7_!8{+V?|Nq&p+&F5qVM(dW z?Nyr?#E_ENKY0)<1KcAB$K0J6bxuk=LpMGz3}gIaOqh7HCR5w`+nOqvpNrjjGO}*; z*d|#?u|S+z-#+Vp%6VgC_!$>y%1O9d^kdyU=qUJ=~?3Vptj z|8ag=-TtS7Q##XhMBBiZ#W}N*qakdu|{f-dWpjfOna@J!>o^V8P+`18~~;&@EpvsgPItbwaTfEsE}Lrl|9+s{DoU| zURe`FGs%jL!%HtU_v-lZE5tvV4>&tZyI)$TN_RXCkVkT?A+jYXv6Hr5{4M9G;syYo z*Lcc}eh=a2)1tOm>cQan6IP!dG90FDaf*DJj$QF3NyG61{hGB>H}7oP7HR*2mr;Ap z{Ktl;Q2Qe=aE46t&$+8V_7FCxd1G)N>7%|o;*wEkbyNrvo?4Szz-^F7 z&?;p%D$p8(_^YFXN{g+r^da`2U+zEOiOoaUivyQQ8r^{}bR^6w(7yTIgYhw(irv)> zy7RuzwdeQU9Ibom_z>Ee10p8bdTUTMD-%Fq3X z_8lmeaIrxI{G-yRz_%$MRiQ_udTE&%uAkyild8Y+QYUXJ0K?fL-nvVF!+LBtLL2qj zUXi~?jaFwB=I?mS!l=!igtdNp(@Oe6I>`1(dZ8~ePHIFvYlCuIb`ZMVt~pqO+JwW9 zhStSD@OTGZ?Dsl71s`doz>Np73i^75kxS$kNITUbwhW~_tH(M!ylxk*tvV@lNK=#q zcI%7kk&7GlXsarf{*&MOra&}J4N=`B>Zw&b0+H&K>F4C@$A4jAjw>V-l^vDP$+;N? z;2tH+$VvjAh`P+D5-gu+Z4deR5=wz)?|CJAKS)HYR)m)Pf89Y7^c}}01d}CAQ^pnY z0>x|eu83iXwDM$Jy zH7Gw{2<_rDw-V$<8B{rDzPimkA;!uaO40Wn(d>%=0*44!;a;yRuTA2PrPT<^?PtD>X1Q;9tJ* z&!9-3ZanBZUSiCN8}1(w#=TC{K)^oeEck1y`fqm8NTRW^tv{yb(ub_{wl+yiJrG`Z zgy2U|=UPupOm&OsDA_PmNc_l(EMc?NL7@Hcq$N?#HzGHr)m`CPe1$m^%GX|*{Z(7kpfqHP?5 zdo}L+IxHIqZfgB?RwLs$n_BNlKHHUMS&PrI+a6J^F>o({ns#d{Nv)WclUs4N84~jN zpz}DX)5H-H^|_KbS;|_<2c#tY^^cYXWnARd8ns)xocACtvg;g=sTT>pXcii;wJNeQ z!A=v{h)b%4@b?R%6=AMQjTk-VV*>m0!8NKqe?LSWU`D&@Rr!9VoTQvr%R`aeH%CJO zw|2lcl~8G+I=J=)7k+P|G+2uMRj620jLxlq+mSW}6CP3m3u64{teo)km+;?-g*)F> z{T>6-ok_G(_qBMt>qD0xgS!a(l*=Z0RTMx_loN5Q+$on76jP+47*?ilIisRWGO<$` z-IG_kz5OK-1mq+Iy`Q1R_qPG$(G*=3Nkp=Bp>PSr^hR+v=E0WFmfZ|xl*)r%B$<)U zP3eugdEArpaX~Se<%4^KJ^KZUKF}jO2`Ux~DTWzG?B_@FJpEZ;6gr|Cf3KPIuu(a^ zbWu62l&=20ab7DvUJ-9bNV1}3<+mrYgvRRL?YMNv7n%2WR{O5h;%`4&i86XJnvPF= zXTPDo>eYxfveYuBjGLZ)a@V*__I&k3-GJ=TlVGpQ2FA`jL(|lfduWUZ{Qk7-v&O}1 z`WeoSKxullxQW|NxnR#w!1Dw3=t8C14L^ifTw`>KVN!y?e1$pD(SjIl@vtFlEw=SW z5ppfderx{UeiI~`(a<*20UhaDa;h=BSko2|5`hI>Nn+%}sxzz@7~WnLlF|@%W{NQ%jIap+Zn)do5YQ)o={_0*|!bCI*Zu%NV;oKBq2f? z$78auF8rz7_xCxUxJ@<+M$$~S<7?e{DMBwUjYKar8tBH{++fjguKYDh>4t)>qJu}K ziQtz#P5zYDaC~XEQS4TTC8=4pUv{y_4#HTox8n` z<~myfPSuA%jdj72Hm1@nXt&{idy#!b3n@&>p!SqCOmHPnSHdmbvln&!XmF?KzopA( zEhyE7!zo;T_(pVY^vq&wqn`Dt_04ynZ6G8~9SOjK-^Em>+1T*a-5ve$6&j@!4`^8Y zvV2@sNy?)C$;}R+7`Th0Urrc;*sO3=hwkvs&<5e{wRADDh(Wq(}qH8UZiyQG<@eL;%4B1pLH6(_mDDK+yYLXH=acpMkOpI^66-b|4^P#FeFHn3flN%)!_`J!AEt7how70*=KDG0d3p0Vg#7c7=ngCv3TBQsoXSo4nS$ zT?HZ3)WBi;YggxtGiAC7NpQ-Bq@Ia2vdV#$jdasEiKH-bE{9by2OSA|sV0Iu#b`5M zRwD7M8R~~8+=B*pS14g^Z~zG4Xp9=E`u99hly~4-U_Luzt$USegmpl=iM!iws|m}0 zGtpuXd{`#?9i4k|WI=FfSw?tB8|uK%mp{h8GbS7NpO!f;v#ylHV*gV?O+qw%m%YkV ziD-e_xr8zS=J5~Iy&sdC@knZENrieh{aqhT*a>Q6Kle7DGrEmF3I*=3^OyH>GG&<8 za7Gxl;mt)MG7}A)ImgZ`?L<_qWc-Cnnr{oeMAm^^Q#~$wKTWj1Q_}!>OYat zwQI?hRWi(7T>A8}F&(q`^UY?0YmT4_3)Kr;R~d-p`~SlewYK?J9U(;SA;&Id#nXHPZ+*hOre zD{UQ^XhND);KZKTlhZ4Y(&7~yPp!(y%^MpJ|33-TiJ~BR0c~f($pc!eF!Itfj~eVk z_R(AsVvcLL(18*jP%58}OiK3Pn%1h$q4t-XRsFp2=@BoMq`=gqTwitsEex*v;b>0& z0EJ4E2Xe-w*u5z_mDu`JWO0U&b^V8hsd!*@&Hz-;=UwhyzDgsD;4-=O?B4cbOZGR1 z1AQCQzEFj0_4T**)vP|Bf|Q4#h5`M=Rc{k2A1`chvSVe?zmW=BkSh5YU!QVxUYMN* zKx4cna6mwk$l1L3X+ZX+2j;+jV9B6+gq%cRVA6SsAK1At)>TOq?XUAwWwz-d%-o_g z6_jAt7UqVuIC%9W-X<9BeA;lqK2@e9L)MzUOp>S6gFX7^CD6}QAD{7?&X9=Nqv=%pMhV;fqY?rJhI*YSq}Apa@!qHnYiEwm8dcYZ?Q!sF_NKN-HromRz5Uv?vNG8+~J_0VRT?I*Aki070FvfgQ*^V-{elMz`-R$`1i zPXMXdeY$DbgSaV%Zuo-C#`!b8o_)kGD>Q4x>~DMfo1>0*L(~T<>fn;|DN4`5Rf-}< zXJqm7PZkomTu;Bd>~KrREESAY-W^07W~}2z?Dmn5mdNOo2-7`k{_u{bfREnA)(3Vo zEj=xw){kp|BaFt@CF;u-W^@YAfvI2Q)SogM3@1mXa9tl9#rGSqxilTawT9VeJA~S? z$J6nk7Vf9*;~s|8fkA%z7ToVIs+KQE&wPAryT>@;=zfkQI+T z@D%6Wo=u!p)2)$i)Amz|;Yp%duljp><+jSdSI+9sopMAra>1di?A*ly@C0TkHkyl*37XCmR(5uZcs4$1CizuwZvswN{wQBfljlCz=Vd{#y zMq2p4I#Uaz|G3o98-f%a>aIR23^(Hw0wfbAK3H7pN1QX3$}CPsFu2THD{8W`&U(_$ zXr=khk~qgvD5ti$>iH2R&1O+aEC6{mO9I!i3MCo9%^xdSGi$OLuWvru%g^!qp!I^F zen_W=(epOen_MabYr~SQ9n%u#4!1u^GSki~GHz-lEpA17NX6&!a%1=89wkXc7X>hA z`yD!Iq4YfO%)qik#BB?RSt(3@gtJ&fTE8|hfOU@l)-Q z_BXqmR{FnGO_WoI={0Ygh&F*jXuUNMBgCQ^M%)WrDSF@>0lVRcCEAStfQK1L9otTF zZmcu0Ju`BH@@Lq3rgvYniQ6jW{v(46JVQhX0&gn*A&&{)o02d~9to{*h-ECoQwqed zt;w=iTv*g!W`^T^Ya$!mTpiT=-1aN048P-gFR{damwrBKbv?hlULw6Am_=c9Yk*2t z!6_;yR^%FNmiCD-eQCYOE`Z*Q&Km9qtF%hqZ1*+NRK{cAw)UEmpV7?5`Ok3rUJ+to`&FC1G^3!E$$u!r>8N>S|W8P%Pz^R+2{f1uosa|S+v`q+PZC13gRdPH+)7P!$#Q19 zZ7D`yOk24S)o9#`DLuSh?7ZOi6Y4rrV=3Y>HcYB!x_US}{G|u?G_yE|wIc@n{4g{u zfPQ1HaujdOj{V4EToO`Cv?_%`frv+V`JRZcE9Ki0^b2UTT|@P{<8zQtH}} zd1wFFNY+UhcwR%CFDBmFZiz}M8X4oR%t{p~@g;_L)dFyDd7NayMIARw=mLDyU|9g;)GoZAVW_e;4lH^c$1mngI6I~gz{)e7Z@#=VJ%qmEOVkvSr#iWzv2;pHg_k;JFkNI;2AF@V z%t_l$F zm_7b=pZD>siiJ~@QM80iF(PA?JsKl%a>nemj&^=?5Ui<5{ot1S%lI#JY%=h`_N+oIVeg}w*a1MULZo5kiTL&L z6Ph{QN#1(T&%e}@nqh4M`xXz@syA-a4oGg$ifoBX%b^N>fLnVc^KpT-f=SL+VJ*5R zB76q4V_n`laq!xm%;T--YfwJ05%r9S?WA-{SpCAEkxo{7 z*w%2c)Ebbz{O`g{(%)R{hKSHVOh12Iv7Pf39@N*!$#=Ni&v;e)O7yo;!WUyD|A`PIkjoi}tL;j>$u^JXuA*Aj_{-kd*1oO{GM6n;JbIsGi{#V)ef=H4oS7+Yw%UgPnvG=9c;Y0H>ww z1N(xNJsnC`w|ii*+>_KePP6lN%JsbfxX33V#$M5zKts!82LbzPAEYLB2_*E5cu z2BK$V)QtZ)SlO{3)w7=fWdc@=Yk+eB?-yVD`@5Q|e~rAB8AwN$l-g-)O9FoXVq8;N z+^0sqUwOS5#7v z7V-&E#T{O;_L+S#Lxmfn?);)ubuzOM zSiUmpLl_`3`EvMp>1MmxPM*r=k;)E==6~=}6E#|-3%e>a2PHKub#i2*gRFse4$6h& zf^V{!2WiAka0I!j?kt!`IqS7VE5sWv*0JUy2GR9Tu@UKz27;Z8IrRv0k&NL{n|^yN zV_l3Ol`_s}b~=~(v@`k@29GvjouK8_m5j94W!IT6kks}jwEKjOb7j_DkPPvk)FthE2fHMn$Cp9iNyecrrk_ID%08~uz;VsJv{!& z-P`T8G#Iucu_)P-18Gl~HE#@}oQNBxeFyTl%*GsDQCgxpTJ zQ4!E}O7Q+e1`rL0f)S%7mM@+hZ9bu1TTZ}gMG;skL@hN*)f3uVzAXv41PjGlw9`35 ze1g!5AaQgLqLkajS6(X{`{#99`BCtrJ-UG))&FDftD52px^@#hxa;6fa39>=-Gjs6 z?h-t>ySoJo?(XjH?hsr8XWs97tIj_-SEp*KX6A11?q1z%^?F1x=Y_f*&;!konJ)8C zC$QW1Ao-(`EqB<@q~z)yIXWbwtr-|ByL$vxmF?~cq(Kg*1g_A4kqef_CMbecJKZ@- z6}fmI(gGg5qaTwQ1$z9OY)DCBs$fV))Bi#;NKCJG7xs8}UO6vjfi&}kthYyNUKso1 zTKYp{7<%b_YMHOoi(Ivwk;$c622_b+g`+7~*sXygc4LQozp=QCcQb7Z^l|q|D5>hg z!`}i|%&#AKDnMMRwVC5^oL5ZMhMs8O@3b=ef@ss77Q{Vi!! zz$}Q?IbL=-5|-|J<5|*6Mgt;J~Bp!ty!LsR;*Xp+*k#+8ilxgx4@lv+3`lI=$VAA zFPJQh0{XLTVyAFgrhP|P$rw-*8P9Kxeel#FAFoU*)dk(qpC~Dp|3XkGF^=-+|F$bq z1#6)z#Y>$Y0Na(4^kCb5Y9t@WQ_o;y9PU064LW;DBoHkObgfsJpjah=eIc@^b@QH( zWDM?md}t89f;|6LAJaL*CC~0u#?k(1^|2*Ie&F&o8?cLt1@0xD#VCLpwgH)7hy z&?z%)l(R#VIrQj^eWJXXmrtOJAp#+iv-J$}FC9S$*0593s&;KT?HLHKAwGI**i%6z z+^)Rr&a?{NGE{oLw?C~3Rz#s}Wh@PLRVJ)W(kf;ZIe;?1mGZE?BnDZ@bhbzohv=IB zLbrcSa(Y!k)gu4wp`8xHDrI(9M7uPyP>9f=Ix+#p&b5PN>@J{YMhU2|q@cPMyA`S<%*-jcFmj&?H-Yg1cl6&!|k_lTC< zEQ`1#m^X#cXDItR{=}79<||}YCt9zw!!C{8d+8wC>Oh#|TWX9#cr*2r*db^`ClGbk z?AZ|&YcEtybm%jx9#LZP;r1V5Z*y?ZOu8|0?d#Nkwn>)fCZ~8O@?Y6R^vepVF@YOH zfd8d$1|v4LtK+)Z!yW9YOeIuuA76~3Q*K|RI37ZX$J$-*&yrnDDX(|j6P~CHR}0_$ z&`qS?p*oA$M*4P7;>FSuFT5k*e~~%8p2H%f*c8#~Qe^`M3Yyk>Sj5c5K7Lm#`TJ<{ z`#RoL;LZ3P-GQNerLdg&M6;$x5z_kSujI(QMZTO))+?v_&Q*2zoT_1$b54H%?d-#% zTsW*Ormwwd!X+>FH0tN_7GER8o-j^ucJqxwHnT&s<(tpA>jdL&x6Hv5-uHO*g0a&m za_F{1;)R&)0*wZ&MUqC>XLDSc!cWmXIXS!U;>jD9RbRlnqN(5_)ssbC;Clk0B}x;r00 z`niXch!w|#AZ=)qv`5Lys)I)9J>uzdQE*W1t&|+5{05_$cQZ+N>~nvd8mplPvlX$I zP-i&3CTvp9`Q|MR%`^Bs;AX<6(I*sEE*0pzuuH}O?rQGV)l6$PoW8O@!isWk^uLY9 z$mI|XB#N~5PhH)SK8-c%gLE%OpWOnC^~@z&-$gT7+al1o2qH%}T`jP9IclWd+-r5K zHLAx7G=e;2hyLS6{{!ybVDxu| zGjNu{`nhtuDg*iHMmK2PfQdD6{c~TZViP`UHG2OcP1q*Fe!rhA&G$-1LJYf;@I;a< z-nK{y^?Z$lRnwZIf4?I?Sgdp#v6UVt-5dN9%S&)DwUB;js9x3cIZLN7`lSYLov@}I zfSLMdM_<9wEQx!_3rny%9^2O}>0@-Hmp;r`YP@$MMZF%tS*LMnwfSfvK93G1Jd)J_ zz7z>0F-62M(m|Wm0L$tCh9tmsFwV4zS75|xI>E2jxa;J)#-M0hE0RTL=kl zUmI+LSbiS8MG24ji7Z1iJ8YLG!M8Xdow)_Y#kD6-c*31cCD8ua)Qe|hMgyYQ=LV<_Rks_soLbRU z`VDjEw$Nvn+f0lRQ%fzW)>;0Pws83<7T`V_D7E< z>~(54`Phv~5wWSZaEIJsGyuw)C`vi)u2u8u(h;-e%KS1Wgy|w_SSqfTj+^f^Sggmv8b6j0!5_a`I>@*fUuL&MhbHEqIMcI%1pX3 z+&^U}<~r*zA#K(FXm5V7wRiA*1{MGbha5o=^W>P%B8^?G!g=vI4Aoj7CX?PK!^%3W8r}C8e&gX5DpErN0jII!K45erEi3@ro$0yhg$W zYht+CttL&{)s_EhGj6Ht*Vjt!ws14fLAPm8VPqHwTgMm~AdeK;lnnMx0HnTSti_jf z`lE9z-?OiPowX)r&+5-4V%EjvngJI{a2rli?AkO5awZhdFL|2<_yWo__H~zb2q!Q~ zVIBPDssy&hs|tH(S0&a1cA;J?r2)B+;6MpOkMI&{IlH7A@=O|Ksivjs;yAU#`QU*Hww5mPLjwg6qPMv8qYcwdgMS^u91N8D^Iq z>jH;o`&9#3SQB|kAM}RT>?@Z)C0AO#6p*i$6<4L+^8wVwq!9X~Xb$1yaa0r{Wo?vr zn6X3908NzWuK`iZtBp^uR0UX;8N;|w8}(6IA!xzK5eY0tutbC~Dl%H_DJi1&)nfRA zJ|P_r?i!h^@X^>=>>9h!;_5_MP+FH)!+^6E>R?;jf%sf6Qyd02tkgym+#TXSK`$Ri6A z7yf@n(bfacOVZoC+3v>Di_xit_$kgwm8PB{TFClW(X-FDa>$3K^ zrNTPO+WEZjV6<-^Uh{7#ypUdW!F`t#UfP1+>nH}+_*2o848rOBlpMrvx6TZ=V+2o- z*C!4-i9};XS!w}thhJ*=1q3fAWJty)U@OfBnws}+XrcqUrL`aOBz0%1V=R3v`80Il z{-l=6ZD?r*0CGU6FzIAjct1V_vtda6TO()%BId)(A$J*-oU`MdyOn^rPQJtg0=}Lms3p1QU?dw!) z-Mg$(Z(qQ&F-X^l!hmA|z7g9DLf;I2P-8&!g5-Tg8M`kUc~2s0Iz#?Pr*z$FZ-%~; z4j(l3q2}K+g8Chi+a;ES3A$O-JLX>P+6Q;YirDSQUH12r8_p756*s|S66fyxYFJIS z9C|ln=q-9Y!*hHJ$Ty~4aOB5x5w zHK8uSG%Be>%tvMUrCteRI$QUG7$QyNgl!V(Mva;kM1{6G?rPMM2=ay9eJbwq<^Bn` zz`TAzD_xJNIi-rvBbE+mskbI*#A8uRs9graB%VU93LI5jgUeDF9bKVjsRvn0`A#cp zW<*=}9|n$I@h>;SLP87$%l4l{I3mql;U`TlLF!$q-twa*Y$l&a-3^%CJVMsbS_|1vw< zta&C_rT~|n&F*CjYyFmm#VBFbQt|$&xvm*D%J(M2bNX;-2{4MNjO+0#A}ok>DyA>) zLFK+#ZeH}d-}}^ip#G>I?^eq+IIg5HJA3zY9`uE;hVk%p9WSRIKq9~gewxB~_^khV znuI52s~6N~4cIYP=ouEUX&f64%KA0wP4v@+?*?a<^Q57|!%a{Rm*!p2_1&2^>A)y$ zF3K2F*cV9;u>4IP70oU9e^_eJ%a5$7fO;@HaO{B$>BatHWm4|Mu!ZdoJfQ~S;6a?< zs!wR*_0h_*3)5%-E()z{2NZ@DA%9!n>eLAp z+eOElR%N?mHn45j6nxv7LmGSy9U3&JAEx_HyEWkhNjlK~n<%IDKAvbC9fe>06MfS? z!TY1_tcsfKV2x^@JZlp^W0B|WY3!ReZAojfKRGQDRpq-h$~IS#mi2_s3?K2{yc4zN#W;e<^@dj$jasZaSvVR> zdwp&*G#dt{fer9pI;cwRTMaVH99*Eo7~FH73-#v@tZR$EXr6!81Ba_ap8z*6#FYYL0XR&0anV2F2iMNNE685`^LxjU2_q zSrRn`{MV3DF?U_mV(@GB{;$sPZnR+`Tvdf_)nh3Qp`Is!%j@v5x8;THWXI2sPs|oh zAoRRJ>u<9opr6e7v)n!HM4uxlRhsL|=L?SP_lRe90|5-d=K0}&1$V;PMBlR?Wknr@ ze7PX6@oF@-oQ%2`Y9+S|+sA4D6^v5S$k6kJQv_6E`??Mt`fRB*f689RB z=l8?7n_|}9@-<8~9?Exezj)iPub6gVp&U4SV^J*dVh&ywRGcdl`G+3w-tUUx9pdC% z+9Ooh;aObC*)idvunrbj=^2+6$|h6pDEV8aIBPm0uu;!RY{Os2wU>ec8BGFLs`Opp zu6++rkTSM~vhyPD{ogLT9+`m&ZYc==L~{)g)1iTKefc-Jbl{YY3A}2u%DTWgF-g{o zGX>(iQGq@?e|kOt%8Y?#rTBDOVZ%Svic_sS190WH!Z zpuy=FKD@8C+0vhJPE~=gx5-DM%i`5xwE1i|^ItqRecG=$$3N!<)WyLD0Jz`4R@`2& z#h1#q-KRMmqK*6+v4mc($BPQm$+P{oDV@D>ZnjP@a0h(DIOGRO(=um!C zyy~Q$RqnFOgENz`?lmrLG5&$C#mj0>GRa5vYYMRa#X=>jd&w!HPv&FB0IEZv3DQpi z{z*9SQ3xu60LvT%-ekyBh+ulfYF@Bk*pMUXRyk^UU98#}1zb~D*c*h~!eDp5i!{L%Sz%_Hc;n2ofZ3Sa}&k980vdzU}gw5`7LsNW8LZ< z$S_YZI^!*ac%rejKwwqwJn;FsiQU0wlcodvQj#AqCTGrBKNs_qkO(xyh~-$+29}vx zHbG~=#IFzUEq|cwLFa3TotjJn ze<;(&)XWzobCL5k`@&9=(mIG! z_;$Ao*#&P#{8T@~U*U05GiUxR%qlk<@W}YhABU*=mxVAYjE6hg|E%!Vwo-S_N?mW; z%=_gT{ZkBnn^tX332x_J1+CA}Gg^^!21XZCd8~w>YZOI!)f+mr3~GPn%>~^o^M-Y2q%uK&lP4!H8BqM|&Ez4`3 zmM?jCdfcrP<-`lQ0*qXd2VARV>^vC1K!abzFigz{Uni*jj32}Maj2_%;$Q>HFY}U4 zL3Hugf&sh@eUiE>=IcM~*Esz0d8(x`5GLoTy!ZSkYB62O=s()Jt4D1gM;EV)*S}vz zXY@-Nh(98u{{b0^4hcoVy$0oKFtSiisTJ>M&E9j}b7h#wwFhiNhs4LOs(1E z>4_HaqCdnj=yFk*OtfwHeJP}jN>ZXDiP+HZLzaKgcxj#@u%10}QL--35N%vd%rF;~ zhn*LhWx&qx{1cKz;76(`D(h2$jr<_4Brrew?olb;U$Zb{W4=evwmjS0?ejRPb$2o> z%ibU9`Ij**Z@}vT)Togdin{8?!HtbF0{WoOZfXfF z8wV06r4pmTB;iYV0WfL3Z}-@2$c!_Xg^lv>i1^fJwnnOJ&9n}+r;H0ayKdRiX(ge> zlKu{0%ZubmwEaf4CiA7oe54RM3LE4PCxV>yB;Z{}-r;qsoBjf6z&nY287+Z)lu0CP zeVR!I*T)PRTwPmxbio~+M+Qy((wlMSxexid5;RxhmX49XD$y8V6^1z?5UqyvC|u6U zW1txTC2hza8xL>-y2(54>2slvp%gj;PW$ic!bUYQM4Y%DIK`vv?_6)FW25es~fd{B{ z>+P9Et|R!TM=)8iBd?*8b7hSnnFU=tH^TB_lnk1v*DTZO#3FWl;K6&-Pdks?xeo#8 zr(t%gxea0hmgr5XJS^h2Sd@OJ+FZ8L_h@ZoH(i=w#=ng}$4b1eLZb z`1OxrUqm&!`FYQ6>)mEOz(jBL|L%8xaglg&fYZ@n=_R;S^P5%J7S9ls`z20CcjS76 zGOm9C7;!9IoHCvkdHg&eD?(uPyKlMLz+~m~4V9hVzyNONkX|i;JigRff8MVG4=e|L_5Q3XnD$c|lPD@x5Q)LMikJbw6daE0M5t2WTD9{+C*JCL<8rtK0GTJk z>}$hGCk4s76(&GhXg@SWUzei^`Lufrp?}-N|LfcANxpkQx(C#Tkde31WSm+>4reF| zDECQ9ATK+bh57)^;i9p1Z1H2p_x`F=MnjG4CzHz#Ont@HxOMm@dwwt#aP|S8TEx*) zz`Q@^*ecGZ&-)=^m(uT@db`Wp!WlHGoRAP7gREEaeY8b`6&lh>OL8BybXgvu4WLKFSf`m4FuHg}jJ zN$NH)k$iEm4U7i6XQA%2BT^@>BfNg|40sn_a7!*!j`b>s`T0ikJONe}BPBf-ukZ9P~!JA8yr zAf5&=N>z+XmiPJmDS|M`mE3k`ju)Q3Ow%M|_=V}fCYP*RWSyUz{AanN&j%XP=YHnF zAgIw%sif$8EzV5L_)szqd>VfKHzhk`H#Gk@w05(SjT_o4zF>2KGLaY17yV#a0DGUEq{l)|PNPsDtZE0ccY(1Ky6UfUX;S2^P|1+rx^p<^=4UYExdscj;$Q#m- zV#*KZAO*!W?y>I$km-V70GL?}qtf6wsGpf~Ig@~>AO zLW#T=sC-+gX>Sg!3HTH^O8X(|HEFKT|OC@&unM0dMzfyXBwCEa2a#A+TY>fk4S z?f1wH!3cDBstAgSO**@p1k(YD}0pMuL?_b_@o^r8!ca_h}PdB5K zUKTAHPsFCR&{f7_1F0g%DI>E$S22f$U&WO@*9e;O+<94lgR#-y52j7PFS&8on|Cp z7%L{>f#inmNNmq$a6#MoOXKHH=-;A%TqPn#m#B^0z(B)=WCurgiYxqjwcy|# zl+y17y`GL&yz0rf1GM5iW!`(4(b!BHNGJ32Mf>y$^2stUs)B-*U+~pR>(pG66{aOC zj!zLqcxt{`E?ucMs=tat8FOEg4ct9hv~^ua-F&1Bx=O`@GMf2cUA&vWJmn9D>2DS~ z(oCuSA$QM%Vb7bXG}r-rc9RMAkOE&O^|m11UVAkR8ZWJgTG|31AGPRr_QZW&zxq8v z57i_J-DH-7Y-z){+)I=b^%K4{S3=ha!K zGMJu+tVwtsb@B6uxZCeUD&CGVp~i@E%`z6Mn~^IV@bMo!%+e&G%8MLI3l#2aZT7~- zo(gQ+IMRwdLL>;TjJp};8#?4bE@hf!JM0&>I70vMi((QK-32fOvu3EXN*(%0Q?}E< zLmxx~Bz{s&Wq!BB5}YQrPZ6CwZgOg}ZWl$K5;i~uxlXRs__9$-$hLTW&FzZa_KCTb z^`WiGQ!n8sJ&=t08ESrD*c9>(H!AGvV3drgg_1H#t91*s!mORfv%T%v28p&aJf9gg zFdR5IqIY}1cK^dXkFt1$5HvidZE@vqU%qejL3bC!nG%Gk@Ur#t^im zi(GO_Zle?dZ0LQmp0kDHi$PYH*Lp)Z#5>EmBw5C}KM1Q}I#(EB?LD2YXO7eq>e&m> zmowHStAB;)%h8jpxk&q+d<28JU`mz)9W1Y^a}s`0Vl8~qjwsz9a5&~P#-hto)>eZq zfCHMBn3(Rg&orjlYRi^CkttM?QA)GkorCIee>DTKQi?yXeFK>@uv1C2kBFoL_VXrn=V3odKc`3r;jq#cu zw&J~I4SqS6@vw*a(IrYr+=nhcg$HcNWVij><7cs>$@=P&U&F68sceF913mFOXuD5UwnZU zvGD-oH~gd~O9PWsQJYH`iQI|AYAGWmq_!v~bq7DlR{nN&@-6g|G1ibCrOoR%)E zx+e+iSub$T$ME0NDvcHXkk_Wpjfm@CCs|QLQLJFkaqjNe8;wCR!Rds6--|4mN47+G zadkJzDO|XFzZ`G^Wr#p}W(SQ}du$H2yr0_L;7+f~lpo`W64~&9z4rZiw;bP&I0=KV zPKJ`wRQ$M7iQ{G&b#zuNk@xBUxb@Z>hB@>^8}2BelXX1y?daji5|RTUF%-O z{?Wuo0OPLGwL_0!1uAS zowntvBdz~ly2BgIVyYIe%+lxAHCK>aK++L#!@Im|*EBd0A|g&3U*^kD|BwlQo{Kah;i<*V-x!pq@&PkPfRDz!0m`13`u;Ttx!MLh5;aEwSSQ*j)|%o8NNh($}+E2u+Z zmhI#RWjvIGC5k)u5JRPLnO`K(UK$?%(Qjar$2+Mj!W3Z_V>~FL9iJuvv(tuN=GoiJ z4Vv0|7lcV|6Ue?QvZFfs$>|glneiJS)0(Gd5e~Oo)HVF91N~wdK^MFS=uyQMy}9m9 zU`H~IQYAMzgmEV9YMy(Qw050YVaCQPap14>4Z{b}c_6jX5UZy~?lkt}rUd@11dCi6kWo zY|B!!o81GkxAdc83T)vPvO@=I+O%wi85?9eq8jNH(RS@NLL$t-@tJ+cS4 zTT{jARiDvHij9X zK>$sgps^IozsNK-L&y9P?*R04Jvk)bY}Lpm&LN*g$&a~wdqc8O0|m4EYP0!sjcm%= zLsOP5N3j#(=<@jlua}Ufky>+nnfoCg9-D)U~oq^d<<1Is~$xgLKvcI+J zSby#5V1cK3Fe3SwmjzJ3CFL2VHx2kg2ZuL?vAnrg;jir-JP=S%E27z5O`*6{5z>Yejong-_IQ7RTUf)-;~TMF-yWj+ zhz5;G&yPO8Q2I0Cnzq9?K0w>OTl`ERq@X8Nnq0%yIatmEv? zBc1LrpXa{s=Wl8|S=qvjlF<$KEkI~;JRj+K6&1^|Tv6KbxcfYVl9Yajl%sVCum(aI z$G2e)8LjMdH!E*n#NqX&GUAZ7$Z%3UZ3?#M6IMS*Ytt2g4j{@iT43yE|!3edB!yNSe0!&rW--i}4M-a(4vTuT>Dn3;@jc?T?TBTs zyt#7Zk%C_MExluHr$0P#cFAdVV$`m-t&(G9cGvPnM~_ez=IA(T6sM;r{AU3m^zPa6 zn*|k%8oSp_Ok&be*`gP-)$Ur504m>L)Gf@eeO*b^0lEqd7)^SK8s0c zoq$1)G_f25i}+u5G~Y5Oq8wmQ;*T5dbXM;6T1p-gN~x#yU@lxh^) zapm_-k^4by=kRQIu7k;twEzIxIceyaOPc7_U+MR8h>yJYPtPo)K(}p{kNVI&NkAM~ zsMBn-Vlt^6zS4FS4Xh3BbBVlzo2M-qEx-af+FJJOg+iz@^BC0OrXDDjGEhm%8wV<2N#W%FJh)c_T=#CvLKe ztd?4V@8|0+9-({HBqUd1=D%zLS|&U32Z@j+Nf?g_*8taIi1~sM_VR#&d<1Ua zzy#hwz@j~;8%?>9Q1BiN`$Z*=eW1`XYLhj8k`sHv!!@yLfU_I6Ux1y`l>>T&vRH7` zaV7hM`)>&~IpOcb#zvU^IA{#YUlPZaoNxQ(-=~{bwtBOZrx*|v_18g(niUpl`LM_X!(ZnO286tO&*vIgw8d#}Nz1MbpZ(E@ zq8H`k3vQApJ-H-iryQX&h3Nda{D`9?f%z)R56kV@V88(IPnoD;!uZ}jg`ev;``jU| z@C5opI(5oufcUVZBOxTeVrWR zmD|%FXBHzJWjb(7M|s?1T~NRbmh|#yq!CI z=!uBZFWU=3{gWji9X*em85;1;$vK)E%<>R|+TcrbVuiy5$riv!D3@Nf#gkEzZ1n2bU;iXDgbDLT3nT>;=l9vYJjs zt^bJo85K92ysa!K^ebBuh_>+nS|Qq)CyY%r?EofY5?G38 z>>+y_qj_k!;w_2=7{N)&cwQjAk|{Dwm{7?voSfGXuLPUVEY}YTCG4X zK()~8YZAtr@#j|RNugRi<`3pxb8ao%UOi-su<|Zhs70&`4&EO07^+#gH{Z9u=jm** zFj3n0Sc}^&OyJ^Yy-ZR5G8fN^I2!d)NG&BlIHo(wRx17*+!ZEUy=A6aSa^6HaEHl9 ztZ6v|<>1s4ZyskinZydteFWM)bSJyV^cxOfV}0m?pKqhbpp_89DadNO(#xxw2JKx* zq?bH6>r^$ZRcyqGPK{`rda70i3E$uh?r)xCw>xv!oM~#i_n zz6Hdi>3Jt5-JxG=*pDFH8%j#s!i-*=z{H+GtM#Bf5yLznj_Lq^x!brJ8UiS*+{iAP zo4ayS;e}2HUue2MWi5@|4zB_^UC-{PU`|sCy$VE@a^fIW~bW$PK?j;FX z69tWj!_qj$UII$3eP{BOh?^JkBD>5ZJN}o7Z&EN>-gDqMw@*;DQTlLM64+Wic~il6 zrgqQ)B1UopMTfI=ONUauqPJ3?la(gDuA%6T$*v1ko z30x7Va3PPs2qU_GkaVQ2z7#?>W8s&uB|cU_Q)wP?NxF%a!_k&t{(RwXRwgImA193DzVi*D@!N=#vQPoF}V3fJ9$m0ag`b(YDK2%gY(srh67ax27!{Q$guZga`4qoISA}UuGlU5 zLl;y;$sjNFp^;1F9|4L{ZvYl*E}rrHq=K`fy>D6By`!MO4Hq>&b>VtpMQvLI^F2-& zH1THjkr8$QI^URskpPMmSe#y9v0}C z+)m}?w8X&=W(pKf8@N)c6LWWwi5lhZzA?Wnq@8a^LPPn%*(>$NgJX9QRcG4&laZIc zmJqQ~h=a?)jHNzfOdC=W{VkwY3}ugcg;8GWsh&y$P+vU=Si z5Bh-yHX+#!jlc2_-q54ci=s|~Q8Be&CO-48%6h8&8HdfWB7_2eQe9F=-s#Ucd}GoJ zg~QWTDeqgnM+cb>)jGfs+o@VyJuR0Hwf@LI&0?0*3;)GN&fo5C^b-S8!d+QdN+9f( zjFNoI6Tx1;ZvY@RIg4s0M@V8jZe4e9n1Q0OwY zuOjx(vVDm&4^Vh*s6V(9%WkSSjWzmXmlVN-kgMalbk_Zb0y6M5tslqbEPpubGX!!n-7S(JTuuM zsLK!W3pl*y5zoM8f_2Z4GU~N}C_c9Ba*d@KxT)!#X4gHtju+f>GYN}m9*E}5E!JQVF3!IszPEDmtCXhgjF+Ez#h zqd)Q{gj2k^f1>+tVvFfHE^iiCS&Y1?b?M*_uGhD)2Hh&pLGG;xnyRO%-vxi6K}3oP znGUG%Doz}HhO3@JCAo}?_2z8=RAe}@##kmGR4}bc!EfQ7mL8|EXF+A_l#CozojEiEI7CFYNOL`G>)&r`PW)e!f(nXcg6gkGuB1>@ zy+d0kYb$c9c`tZo89{v6#-m2)Godx-aM3HA(e-&+8|AYj z=#2m-8S|D#C^*fx6qa)*EukWGu0X4|Fy;+q!KvlvcUksuRsje>)8G0AQ>HNZjw8|b z-L{bNt?p=E5z+X9)YFv6U^C&)IE({8674X#I=VmF~?Rw*|!t&h`5iio z5~b`%Gm@{sId23m7m{yY;>&{z+*{qjpZ{bb2<}5^a>PPP{*=+}E3HmD^TtUIZ*(QX z9l2D=rn04f1pD#ru+&ibmT-PS{K6llAlINOVzrq1x1}Rkup=lE2QEG3s#3ToQ|;|w zK$R|A3%bBtxq%}t0)g|a)3svHgVsuy5gV@5nq4tb*PY13mr3I%`$>y0soFe!c+ABb zb|7|QuUfcDyo^>tGhDSDo%L$oeicqm!hZ z-x8;ehBNk{ny*=4cfaDuH1E{h6B4OYhD>cG-{wqgVA$+BJ}goz-3nq(!+9? zHoI@`TLISlB$>oeIG-G|r`CXvclFhW=bZkqV1{bKzqr~lHbsHIw5Fvj`h98v9i@_3 zd@F|vY({#LrT(UBA}*H64;&3-L?hNt>VJRc8fL#6oW<>81-S$aSJ43dq-dUCmkjNQw}mO#oZ3q-PZTQxV=Zv2tu!b4`Z&{00DAs^Uttc?wG zpKM&#{@)=vr8uSq>-B_)<>TCC`6^TrD45?$jO7?GV&TZYe~l-vR_9Ty_WhTlwgwK! zY0{>O|Ji9ZVqhR`#bl%{LBdg11<27p$fb(A6P{T-v9oLriYfzFHe}p+ny&&Im=8o1 zCzV@yI}Ebd0l3sw0r3n6!?a@d?XBiYg~U%~qs;{J&h^M)G5h^$wE#v(W(UJH(jYn= z*PtNk9QI6CcJzIm;NP$hAZ=KMa1wq5yr^X5w7iw084h4))PPu4loVzn;GrXwnJ;7( z+M{02dPAZzP$xSnQQFyuF-NqOr7sGZJCk${*}XJWZCW+|Vu2>b+~JM1x9%*^j*U6$ z$(C-INS({8J^E4#^S0l|l;rMwz1wd1rFZtg!&TsU|T=ES2;8kg~yV zgfVjqrD&jS)Bqn1>y;{IP%-o6adw1Ojf^i@Nf_v<@oX+8QP>T!(0OE9_^1p9gD}re z2q#(ocA{Wi-xs*%l11aerCY{D8#SFm5=tVWJYn1rwP)**t`MrwR8LV5HvW~xtdpXM ziFSg?7pG7>D`!9~oU^AE-aeK${=8vdxgf$_f;1;i33&C!EL0^;oG{JCXpL45F@#UU zK%N|ZjGJ)YU_lO-F~-;mSfNw#Oj0m3#r~kTHqWH{_dr*Z#BP5oy&&&6p-@J8MDfIE z`iC#Jz#_z5X?3_iti=(7AXd6KEVZ&wtMUcR9Tr5fEVuIu1uKR;M3^39QUigiwpSriq!lFU|I(ZjY2%^Uy6Cqa%_ zMV<`=aKlx*Nv-nwOSikBE#I(jv>hC)OPM$#&WlI*LWpea_mm4yDSj_n3lezd>DsFY zfF4GpVg4mYDKaF}<@}Ar{VL%WH%UxnkKTE>kA6YK&1!(whBPAD^$WvSm{@)7c9lXMrLmW&IaKi^zNpJWCBuqe)Ghj*&LrZg!&hxC zMuml#SK;W*HYw?{OhurcT0g1G`jA;>m$p`jDCrhqz0TX!Okz*=Al7_INYEe;HZSRPM zp1A{3a6UvV<6`;379!-sE7B-UeK3u`S&}x#>a{2c8t%rCYGDUJ6R;F@$7iB%(^d}fm zjT&c8gyMy_PNKRTm4_qc{tr#p{2zC;Hk-z_ZQIzSF&f);8a8HQ+iGk#wr!_zvaxNv z+xy=859|-;^WAginVDy%rtXRc3SL>4#L*Mam8g^7CtK;Oqf+n~wL~pPWTXX*)zug- z0yZzD5_gcM17FsaLP1^1@LQ=GDr-@MR6OyPEV*$61})V2D7xV9Hm>`^g`$WN1RHB* z0Io}5jG$ZPcTcFFmCao;l;$;sXw(&}q2V(0ryaoyp`j_k#&uU^(->BzzZ#i} zks5ykWl?)$+g@nQdj&%94$H9VOk~SvjK0Z-@`&_t_(1wzo}> z4btjn4KIr8YLW1v032xwjRu^vQb!ApgDNEuKy7qjW!kvPf1IK1bY_A9jqE`kiJ1uS zZr>sgZJL*5qru!VUaiLZc(h(pWgzKoS{(5Ww)(i6uix1oPFVre-JE~Y5kUMQLJP|; zhY=c&fV{N(9ZPxiXYW_jGaZ?!KuNr?KW3|WGQ7J5%AbOI18&HUHt4bA9Kg*@((IP! zg6_?Ln+H)L*Q^$CQ4{ttn%{@MA1cZkY)=>txQz+5FFt%BWF^zQc7SLM-U9+?i_Oq9 z*^531CWW0UT!tveHS|CW8K0$LF=OV1YSTCbmmGpq;XkRHcpMs}2boD{j1AuTh7RjE z5kgdgNprOWoxhqF&W+M{d9Z_$=)R^3={o;H8|a}4x!0_gRLn%tZ z;(EhmePhrZJsM|10#Zg$GN5YYrT?GIlJGxHmKQC<%AF&-Y3V0^b!cni)8y;j{gn8` z1i8@H8fjQN7D3V%_>$Cx`(x?7?RDqp5va9|r^bf}yjnbar(L})ED3uB5Um3Igb(B4u&^vm5 z+`D43i`%9$NHFtsrZP%v$3O42s4v8WxqsWZqeG&Llul0<)p3N5D&^mYxlS5{w6TGT4tzaYnk2#pOENRKg*R0DfC1!!o^8=czsKO`A%P zElWncLftiOTWXGbM)8opHV_yuF~f*Gx%N@5RVZ7#1l}Cw zGgz;De1#`oBp72A8~gtFXLE+wJ_NbFEd2gHnvYv7!Pd~l&K8-%k{5`QOD}NlWuGOt zj^HL>5@%sK9g4&1IrH>6;DnXRMF#DzGx3*$#X>L8=s159@gzNvSp*?j?j?tC$R7Lp zRJ?;zE^f7z0oWjU03pjy7}0|5fN=9y{8Phm$t_EroV;a=c z>ZRwJ$@w>Q6D!E|cdIuNMN`J+W+OQQiEYx;q!<#hJh1uJ-3tw#QiSn_m!6Cc!+0Lv zJBg|`Zm^XMHSA-U?W0u&hTB$II)_*0tJi{do$-FSafRnU4$FAoGL6crIzWQHuvc8x zi^uUR_B5~ngx+Jm?IcqZp69EvBk|SIhl|y)J~H4pw+rWKG{n}5Q2f-BhPER6+ze+q zjy2UMN$lOj*z5{2=!%`=(5OcWdn#WnBnuiJOiOK-NX=~FMCTDFq{C71q#-YNa$sub zAoY4ri-ip|3q+AD02roOL5jkS6HYgHcR9qN+sLsemOV{Qlc)s=$>GSQ6L39TpZoY~ ztwQcW&txUyv&f`*4Ly`sep+dywv!_DeK ztYTxH9d|v#&MUDBL?+IV?e-C=l+Csl1L%vTeQ`JWXC1Rd>`wEb${3tPN21ZWe~y!4 z)!7pf3aVJHof(~QA9GtB_FDXM93^m4U5vbbB%o$p_z=ih>mnbXXjYPG@0Ih%6k)!! zzl@Ah;7B)|E7Iy@n1$=={&mZaqBZ0K8se#mtz&MT{6-O)dBX@x%qMu{6 ziNCgSxdfX+=>|CA#3<|BjML_sAbWl&aFdj$<$u-Mcfr2@Yr{ts5ebte%G*4K&{D;W z1Y5M@h0a_GMqT|dzo$MT>=r@dM=havn@{JsKb?@Q~xJnf-W<3az&?RD(x%Q7Rz^GIEC(3Eoq1B5WIccCM z?+A;!mTs9U2X%UQ64@IJ?utkK4#+a}EQE?5;yfj&D?woMrA84CsE?i%>4U-?<&zE- z%s+=!(Zp~>kplPdFddH1*lVA!@{7Ww{+j6LcRbF;vCVhc|Liy?$`b}_Q{E}A+5p4j zKX)+ZNg2X!MAMkha*#d_MwyR3ent+Ebv({P_G6VwAyz;pO82-Q=z&SagfejzCtZ$@C<@!(!?pK&pu)Z z5IA_k_K@VR^o>!EH!(XY)N;0ICJY~jWabbQ0qS0$h)l^IF~PHkL%ob6s;u>=Sb&2e z^QLHFQytQcb)4$5jntcj&OXpEBCb->!z&@T(E|JyeSxVsLCSIH0x`zco|~1hqHD<0GA;<{GMtgQeeK%Xii zKA3O`x!;U)hh1#~_qEEx!;!z-_fLPeebTj4$}I&(z5GDF89}q>9+82X4W17#Ao>HEiwjT?Dnk0u1Q#j& zAlP2wYO*eMcr+zH!FU!!Rv&gOr-GS8m=nE{31c%~<=H(_e)M)nATD?Pj)|*KmS#ho z$@x^VQkjnzw|U|pK1hgK-=>}M?}Ky>|F3d{`5*#F1=A<6X5?tG_!>-5H`(ce1d?is z#(BI#j{d?*ix%j^^sRU|8BE!ZJIIOer5%-AHcvB98AVx>?URmOYD7YrHWLw`FCdyP zt*RyvI0H1EGgb{vPV8|t5Jany5i5Ci!zf3V^MY1-oNrx54xmerp>}cuZ)t%NKoO|= z#1?^A`VvPg)I)Pt-Q5zk*wiY~d*4H~i%!BXk(#48GxsP%G&FNlAf{^vo%nc{nH_QMZ#v|K#>)s`snZYByZ44c-tmTmTbY=^+Ursl6 zNVGOF(tSt6!)J$bxh%uA#OjG7N(|$Jn{oJqB-tk~umdpYvp--t**Qb$((Z4xtt3)3 zQrHrY022v-nH=Hj1c`u@wEyEj&`|kzc`5Quky%wKusVg_@VeU~&~JmXkAmZL_Hf%1 z@@VG?Y=~vXhR-gjkPab&#&v!0^cbI41tCl%*FE>G`4L;xcwWDeGI3KfeW(#wrprpR zWKgmqwT0%TLyWi{Vr|vJ@OCV5;zD^ zm5F0|3_nHDi`xfcIt0Zr7;{SD%fODkg$-QOU=(^j7`we2{Zrmb)J&mUt@Zf z!h(k*Bkuc^nLfTlWew0X^nt9NAdOlLv}o7%B7prD>Nd*V^e;8OrMjaHuHOfo-}_fL z;{4ToieS%piQ+k26r-P5pd0%6IIQ2I%~x?X%%_ywqKc ziGg@9EcZpPqfN&PheChe&eLjI3_6GFra`^j5~H1u8K2|}qA@tnAy~`dqGE<-*fJn zv=wg** zrmI)~$oAzi(mIV8S6~@!mUiD@MOy1u`ydhZ)3IakJ&;aji?QM0D41st+T9^Ut(}{bsPq4+V-n!sg#IqLC;2m&o>O( z8!6xN%V%#uC2f{i`?Za7m=qfaRET1gmXYsRm*U_EjviNhl$04u-q&&XmMf$;*sdu+ zP&uxS>PCMR+{l)tIS=640XWwgsD@a6+9FNXpg-titm=J@{Lr;@Wx9H z&uz?n4_HuK-OMxRjA6v5(=gRGNDV|+hVWt>KAp@5*6*1EU)H# zkkomek}EC1Q;iQ)J!O zDTeLyX2*THA@iN2r;%+uglmW(jvmv1#hhvbTy9ti)^P2}cEVtbe8*vb6S+N3NSADX zc|BI!geDzXUy>~vz^`jA)Yzbqfs7gBhU!xOgR{Y&ImcG0^Z=UP^q?$A2@+%y>*4{* z!PvNbytK*fBYRNYM24JQ02b@_8IqFumr@R+G;+>kcuw+??V?sZI;AQjH+*{frxPy0 z?ik6;iJ}rAF^7w_9n}8-G2An`%NYr^YTyt#f`y{Z_Bw)m3_5*PYF<2NL#YYFkHI^q zbJw^-{?&G;?K6IHqcg@4!Ho_tOSVY1S!0MPh`MRKuN`cIFb7Z_+(UFCP{Z(Yd4m(B zb=?$&_qdfE@{VgzgYb+Cj$&8l&SK$H5(JPPqQ&V3wJ_<2N(W>FLjDi5UTw$(#y?2p zg)cNJLZyajuun)FR+D)j(ig`Cq`Q*{0`}o}T+oQscf^8d+gyLWQ{jwy?JU7 zI8m)lRQj+Jh`=2)pJy=iA}H3CWKUA_itQ+CtpHvYEkruxapVJ5Zc`n{uG4D9$nCvnPane*uF!NHDs>6JswvB-iljqG_T9*(vQaYf8{ipTUjiM93v=uH|2);rxwG6(J_O3_V5#KOJWp7y&Q(+Bo^k| z9BuVUqo|t4TS+8@*br*dWB;|Z0&oNPfmM_@e+BRG{afz$12|hJR$SF%qfd3_=z{e8tptF z{it~HIX*QIH%><2+8lAsrsXFgwXWQoOb2RT^e=&Q6JCJVNn%4C*OyTP3}Ldz!`*w&M_HMMkZ-f zwqKlEN_2gF794{H*pc1hbKNiX>p%CgdDh-Wm0ZGCJG!#`|(u|NBH_#bg_*9fLTi+JU)o z{h*vqrtK5Vj#r{$tnGs`WQ``-p$7*+4MFmcGcjh;#V=ubblYd=^+LHCv+n#l+aMDf zsuCbE39MGXpL)t+DX-vn4^D+WhlDQPy!nU{g2;yGRU0;VZ1>qSQ{-!_NPzo(=>Ry^6I zS39_1rz-56fgX{(G@x^GOlhFIfA<{mL8pImWlI0II8lPdZb>ScH4#=j5H0@)o%zQz zo1aueeHN;Hz3eFZh(E#Id5qR^X_+y7+jke_f00J?UGiQj6A<8_1dnG%*U%q zMsFYJX7%twMQW1V;Vz>RoPXTHJi3pwtG3^6NYo^oV5=nUGPG9&je}{hf0u=C}LoOah#0DbMvIWH5}YO#5W>t_2+bWyhWT41J|XZTWDhZz6;%x zAgZ`!xG}bo74sr&@6?7Q;MINM`-9ryiB)1Jq3-O zYweowcZvTwTz8UOw!`6%Ghbj>3iQ7A?DmXQF6;j&iBMVSq#t&&L4I^|v`y4J>e8pK z9b-AsjcOVt?sPfRRO9bpq^aTg9^57`K_Zj%MO>qzVGm{jQ7%`BZhMR(n=nz!yFJ9q zYo)vZwT2(AO zj$iESRso0Di0pw64Ca&MF zLXIP_|Gwy3`d_up%OMA<@68Q<(8&IPm-Iwg5dGowzNX+9LmNp$BGsmFv1|W1V6hd? zm~2xTT0ENZ>#Rk9Au%-I-}1+s7zH1PCDRd!S3_(zo?Q+4@TMf^8&~kS_}=^iQAw6X z<8ot~!h$1O=*Mj~`HLk=ehxH*?Q*`2DnHD%13o|I`dUx6mACh=*i;9~Xw{VOND~B= zG02aYZF$bOp?1}9ceJ>O4)i051P9Bzj8edq(?5T|Otrv2&^5B7iwoh6PQ7r5hl@Za*XqO+zs6X$3qH3vYBS9GVlu0q#|RCIM3p6$YO0Ub;; zY7%K?AWj^5o&Wgz0nsf(mnWyT0)VdpB}h`H_>X!pmbOjg4~3=OZZZQKydy2N#$i^% zlb40#H0!`FSNbIHtehb)RPOYHbh_d}C0@zQYWK25>L_@+)_HYX!LC;bd6L=NNz&)x z#U@2b8{%HB#=m^F#RSw-qg{O>r{3uOG_;hKfDzxzuX^DoANsgZ0IE%_Fag?=3EGoZ zK&XX+-baY<>=&fkR~R?Doq6J`L0jwMrF=n2E9PyhKq+l~lz$%;g*X+BlaDzJ5>B|V zPx5JLgW|;Q8O>Bu+_IHhC<-lXbd?{tL%XNsnOi7Ic$T|ZSRKyl6QT`Y%$S&ew|{WQ6)qHgUWsrL4heM7 z{=^q1F7WrBso3+^KfGL*J#)nzOrW>CI4KfTT{0^AziPQ2XgjNAyh%qO`7GQLB(bsO zdJrX|jB~WdKc@w?wz9_M*X1IpK8DuH3&u0llBm-AN@ih>&n zz1t(8+e9YFwqXXc)H0APuBD94oT{nK)E5(OZE)t`pucdN)mGx@FQ=7IX48B zjdV0_oGAj09oW(mipYdww@mX5o&u4IY&3@KM{^cv!zJT?0{g6dVJJ(mdRJl@wW&by z7mv;LiYR>c))3T&5rum)dA*8Ks_BuN;}3Es3EP`;9tk>)>vLPOGJXBwMM_=Wfj$I@ zPVgjxfn3Lwq)@)I3oyx&y zUFMj$d|Am3Dq&6&Z>jlbvZbs86p7#fYLiftCzl12BIUT;)wBv}%Rjm&^241I?_L*5 z#)`@Y;#K~q$j8>4-P`*f401|MOoRL9VOHnK&bIK=>*$7bVPL5PZ%L&0g#lzGzLqSi zcP`L8DXD|h1x2=bJ>6xBTGg{GUsQ0b)!+sBPt>$$w>Ek-mapF#b-jL7IT@D@vU)b< zX8s#yTHU9k!qTo-l0j1AkjcxtHm?2ob`{7a)~^|UII%FLMf%%npY&D0o&t^9szhib3qkG$HXog<+h@E|HZT z{K4OY)dPO%zR;{?*!)s3D5@rC;yuU9f1?{>RP@ z#?WA);`M-t1uvF03yy5 z95-Y?Y-E*ECye%@%TRh+wY`7k`0LuQ4|HCxa4@modsZT4Z-*RfLR()ML$Azg*I_$S zdw&2zcl6T~>}rHD3r_K$%n7$@(4Y3qi%b#;MbB%9+?BPCg-^`cBY=l?06-hIBYD_McVs%6Z^aZUvW4W;BA(qDTA(4lZx7x#%7mrfCeUPs{y*FD zD&ddbs@^Sd2_F3jnv%HPFUlQ12<2IDp%Bj@9l+g5Cm+MQuzpamvo-(>qhFUTjXe5G_d*8J!NQxeTu`3c`^itvNrOPOu4*V zZ0#&d`Nd4Fy{L3<$R4Y`=+p=<2BI!bHZ#rEeZoGU2>f2*xQTLgsWv3ytzMQOmxW2- z9m&`O)%9ZMX>vI-Vh zIdPI-=B?YRG%;uCF-`eanzQOv#(QC{EKuHKgpm>XjtX^V`c|=;9vyuk-H6Z_bp_?r z0IOJJKjBfif$hxq*k%IpxlzKl-TL2FQ39c5Xm{%nvJGaEB8s_2!T;U?ENmCbtq5kA zUJjYBnwcH!lV6h7$Z0(tv_u`XkL3OV9s#jzLOHv)xx=*0(+pCgbE8vGG)0QrY#hx# zONM09?yY!ZOmclg|ePHi&D0^{?&?f!g0LF*~SF^DLsx*SF-<{v-mM z2&t`dNet#@F`;Y~aE>H!b1NiA{u8_3Ml0k%p)sJJl5zg8*e~xEVi~jU@S)Z z=7IbE7tix@^06&Dp%)o^^ z+r2lTloC31;MsU@d&BcQ5%&>yNAVT<#})ac^*pLatYiQ-`FDy`>UsB=>)oNm7uyHq z1Pc=uPejU0>@1L68-^fZBqy_BI9@OHf$s#1n z2RUe6%mH`003kZ{fS2O;a0CR`X-jkP8vcY-22Z(mys+EZ@q2`cuKGSjv-Qf_&*dYSXFYBP`cxX9)|~DNc^C|4h$@O|U3*NY9>?i0VOS^Qc|#ovtcl9p zF_x3$s(EBzu8K)1*WdkdHuJ_gaP9$^!&XOZ6YMbScEM#wj&r+c?~)C zg**#u9%ph^8Pge&B9Wv0`>%hZ_G$!zWK)<``$ss?6NH(y{H@u-C-va;kjrx@vn`*b@_5x0W_(84|~WzO*b4nvg|ve zt(7Sa+So^y5~!k(wzaF0-wDKobvyD}Bf0BXhH&(G&GhM_ddcDAI5yXd1Lm$WXFaaYHPQv!ldA^(L>9Uf?T z^9#kF6~?poF5b_C!kzX%cGq#ETx-cQ+nPuDXPm?3EpuJ`Hi3(x;c2Tg+ zUcM!D2OyN-E0Wt~zWVp447`xdQ86;eAv)`Q2~nB0vw#xiq=CFI_Q?D;Ek4jWehQ{~{9Xsb4FzC;yv6!$QT9#BPgC}uF*?D| z6m1!1dlO%xLC3<7c=InPTB0+#l4lg*`!VyskW|8t`U*6Zn{QxD9?Oo3qz3y&qUV+K z(fu2Ug%uFLgdB-bjY%~SG+j-)?0KIN85$AwqpOBU@BdX#qFjKL8aA5B1m5T3NBqqS z(;!zEWHGa*i<++6~A`+{>{^zwR@z}vaye67N}Qh*|Yhdl9`jv!_u?hSu;<~O9)Eik&Mx;yj4lVkz|6d88EQ` zdrpCn7`6or)!(#0#hGavxE3BCStW+TvV)hnEq5VQhr1(`s(7QR{`IU4pT3xjcl5X-5+hCsaie{mHFEb7^b_ zzXcdL9ETWNg=^cye{lMxkg-$9g;wTT^0>14`I`#imYtfFs}Ccv3k+aUS;x?l>JhCpHi) zls~FjGI%UDm)4_8n@<+dlbs~Jm&Q!(S(DjByf=(GbQ?mystUIAJ(%!Mql!9GfO5aW zuP*CD8~B(sKsznUU}XsX#WzhTA+RSez$QrC8W_5RXVvN37NZ7K!|wm&d*T=fQd9y> zaTb4PCHg4#8~p%*W_S^Mcn_vCPT=9hpwQ@Az$oJYetQV^9Ak)_KLhYc3Oo2K?xHYy zzhIY76u%e9;Dg2PYSYe!OLXd_Q+IQVy%^(4c19&Vsd)PtnV?{>hbdxkqZ2LqV{cx z?35v^qetj4GT?&?=bB*Mu;n9Ao4N~!)$RBNf5^)E{hoTXA%N~Bf`i8Fn~RLzTUe0Q z6k^e=#DGf}N#RPSgCv0NFjq|8yGTMs%kLTC^YHTNROosV@yEGxqKgJWt5$60nxDG=+I9W+(o{nW~&Ihb{T@RR#FT}56kJI>qhfrMIBIQ zO@U1aL0URyuYoDl8y@%Ic%IZsrk9zw3%cJ2g5L+k=RIaMvq|&Vai*wEJuxiZ7iEY(D;gCUT5wiFm4wdhyo*7#*~d?z=8 z_+m$>dw^JovUJG)64hQ2g>a+jB@eojaUh&@l(&7>D9fO4us$s#n7{8B)9jFu>RZIb zMaksmO5oWKn1jgsT>n*pPBNzfCqFktzfU~Bx2T5eeOrM|b_5PR?Kqz_135eRMyT}O zRiya6MUfK>CZvA+P=ez1BDO&OilP0Irc zvYRY)xMNnjKH@qPo7!!bHDHNe34M5DZoK`_K_@rNB{OJzt!p zD@Nf0c!5M$1uVhx!V`0Tr9RrI$|cnbp3fioAam^D$BG^u+N3}8e0{m@GgA% zjxRc3CyJ7m05zzhMOE}*d2?J5`t)g^K=1~UPvd9pr zE6(805yw3FAdVtH){rqOJf!U2QQjrj5q!9iMf(wG6TxI)>OYYYWB6&#aYM@-FgWYa zniVERF~jXio{HV+`2D*$%@Axqa!=Fn(k^kPZ)!`<(ntmtG4U=6f8v#b@~%1Go@<`& zzW=z)SmFij71nPW4*UF~2v{a*Aa?vV={yju9%4|07g7+q)?tT28vn$+rMHLUP8J&K zTgp(r`)tdwHL&%8@Ar-oON5U=sa!TJ+;t|9w7F5rq(As}oDO)BI`^%htw(>ZjvxTL5xe2@PWt8g{CykJ@` zJw<4g68IYr!-w5F5pm8t(HavPYzz$PM02Zuj0tiRol=*eKv1Q(>D4Jy+gPfN zDU^*h?~mNY9@CULbZ5UPPHaL$7swpnP$3^-V98>LmfUX}EDaQG5|V%O=vy$_jK5=0 zgBv01*ELT_9kt3clkQ;DM!yu6gh1KMbOc1jV^~p@@+rLh?KyNWut|5?8tH6Ka~w++ zu6ftUkZQ8hsxG2j*Ke5F?d_^=w1u@SVz?F?V%5`bEN{7|1c91TC9x(fN0z2a^Dm^0JlGZQiMTqXj_?KwaJc4O#j}@e*El#rhqS>Eu-;4LT zapr^wNG|Cj{&Em*PC7>h{#kS)q=(kDCpkA^iY+4;L`?w7gmP|QFSE*QuPt{!?(zf- zfhF6yUEPDf`Eg(On`CIu$9^lJ4H$7o4igs1!;t-R#EC0}B{rIuFLGaCnjuR84CCS@ zYyYcCxv!@h5;DoRPo6136i-lE<6sDt7ar*|M~5BHyms`G!iJbv!N>}QKiUj1{B zqG( zSRc9()kcy>*v*huDqQ`MZm#opOOA93y>(rJ@fq%vvAlD`>lc5sg9S{92u8@nL^>P; z;#Ujj^%BR;$;E9|0s{#hv0$f5u8Z~F8*n@%q_$>AF~|5u1M4sg%J!zR`;md`J@=nY;0oX+qs$#D)DsT83%56e%+@lD&GjeCri`yHv!iL0r(%u3?#bfBEd+`Lif zcueUJlc&cHL@EC7#fUU)ZUt{`lWZx=RxzKx@rKktA(@Vg;%}o8ucE$3pZ8jXaD+Ot zg|#@w%Go5})n<}7JG3xhq1YS^^6;pgzU?$dqnY5?h$6_%6ec>l@MB| zIS;CEqTJe{Jy}Pe8Az}0C%8t|YCa;vKz+`H-u+g%SaFxZe!U+oHB+{mL+g6SY5tD$ z$Ra5FQO|bm9HU7c{4))WtPI)!@0&>k?MVY=_2u>w-bJ;EStu1};zn}Jk?M_U4hM;& zdyuvjISfpVnM-6!Ad#KjbyOK#L4+2SsqJO-@X#{LQ|Gx*!Pi9d1zb--Z`6OOJRa-= z|G(6_kBAlhwBuZ#O)q)7DJl{!P`Z7CHJp#K$LU`@-_j@|?^+NnA+~e0 zE-;ldcn2F`R5&zB1qLUsR0)kU$ABj)tAtb2aV|BOlj+_NklB1S$?5$Jo-C0sw#|sE zL2=T4$S9}!>j|ukznm7ZKAa<%$6GaSQRj|FEl&oRf*?(3L0LE-$MKP}_7xM3FPEx! zn-wtwANOi;jHL5`iT^&|eW6Y(T;GPP0N0cw9`q4-JBlMAjdx4ph3xQ%`f&p(#|n~F z(#5l~@=dccm&)L|!1a!UX2G3ZSi~bmHry2lr=ZkoLBn;8-S#;C%HP9@q%)Q}tCr-= zc{o8ObOD3K>UJBRg3!~a%S)On2eZU&nv!BA9I9e;xcW>nbdlFz^Lca-&1>jHHPwrY zwcXLlp-($VUF~w7?j88hZ{fD2BHdm#1@gRAgo?}BE#3azpWng@D-|*e=S1nEk7=_2 z(a)?WUvj%l8foxCa1>(6#Rx*%Czhnok#ezBA8KF1Rv@~&c}X10?D(nain;+3$Q)O)d!kCO<-W=yIeIdnN$qvjKN0Kq$C{G+6p z=68ANS?u6$I?AoscNr3hnQJYtI9oz}+{mzYNEoe|kL{_`UkLwbz@tN!Xdv#OFf;ykE;ZqDCf8tZ~qOafor_ zbVJ9AeT_EPP&Zc>lNi7^ClK_Cx-YLlTG-67b?Hs2tzesCfbRE1%&D4D6IvbGw_=br zbVg|vEcu(O; zyv$qJF3r4g?jesjtzUHtXn=$9*zETlW2O@`#7}qsl)fkQjLn0@OpWLVBM$jZuI|Xw*z0jTU#-4f2+n5~DB}_Bq=)sTgHY?5)sn~sUIq^H zM<_c$kKYHX-y3^Zm-wR90RT9~*TEBWdv|C)AE4z7@(Znw^2K`JP`Vf5qiTl@Rk#fKj$klED_D_ z;a1{bs*}<*RP^fBuG45$>ctk??o~N?cRj@QP2L6y%f^z^)`uyI_!A)J7^Gi|0Lh?P z$|(liUQvBNdAnXh-0}H>6 z{-#hgiCwm|0Xwd9$9@wpW@5~ET6vEQw=@PQG+&PsFh!AU@1v};<2?o9c#Wagzkq%U zGKlqLEvf3wAO{$mvlDqqW3V_8r(Qug-T>S}42;VKM$Nss*}`wF7r>1x|F+vw4=#Rr z;}Gl!@F$&^PUul3xMtN&6mzoF3+j*_%Hl;;EVG%|V4h&A`xEln7QFSDO!jUbZmU|B z%SdQ_J{=J2f#ndU?Y6X_e(Ky|iFMf=tH`7ro2V}9=hGODecI4nq*996yd?%|jU;5R zrcH})lKivQUdNJCIUnNp3TX!zoRuzu=`s4HkI#xtXekzZ#pzk;GwE_1AM zI8iZk)3IBoV+o|!`Y5rp9_;uOH6ihYuJ}IDMdyIq^wl+$FQWH~BpLLAOzmKILi}qJ zk$_pi8Bx9U`#H@nu0-o01%>h=WI(6`3BLxj@-WrcJ;K)m@6V?!oBTO*4T+^l>6xFq8B?5Qpv@EJ z>mvrc`|mpdbf2{hga1It?vuobolf_muY{g>xuZL-M9h>`%B?O1lVr^Gxy#mN5MGNE z*H`KGtisB(x>kB0>nP-?!0n`I_z}{G-Q^kviGv#0*6Oo8(4y9(n%bttfhIxEl;JF* zNb@b;mbKok*VsR~ANS*t?`Olr;j;Rc)S^{)mt+)1-BbN)7oN53j{(H$lVRgzhHWgG zy*?5zEZGt7K277SLoWU8blJxBxPLScZ@?uJp~!XhieS}?IIanNLwCz+ z9XYLM<=C%NC2(?G6s@f~Q{d_FpYHC%=P$lV@z@Cf+=Xhv%c`uTwXcPyk8fAxJfc!h zw@OSW>HTyx=H>HC9xs`W%iuMGw$5!7xRe(&AhE}*Xl?Th@WjkJy?B`ErE*^?)WU0B1afs#X)bK$hO_`C^@d{AGYC%9ZRu z$Uc{?nUDHz>^F*x9ULepkyrd}i%h9V9yT~cIp)ZY_*SVNkhURJ=sO0>(-KbKJ(S(u z7q2qa$~vEeG)tdm787adG-_wKgci-AvZkq$*g3PvUBW`S`f&j_6*X^}+us618*E%| zz$vRqE;?2&fS;+xqCI2(a^&&x1q)-OHsQ(4Ewq;`%*B~8eRmDr8WQ+t+?h?H;XM9K;-P{1kFtFch^WFXzUP2_;+ch? ze`GgvK&NnIksqWVFsPS-J9xvuPj4=^bbI%+6!z;AuS>%Y?c-=R2Juu}FRPndaV)+7 zmJa1Is)Mjup|DunFA`!&XHKoYb>fgv6VUfNOY&DbiSA6zPZRBfo|QR53^E95t^pe> z{l}kPt^wT}y(WliS!vm`eAjqK@KW#4Y?hC?J+%j!HyQ$TiflWOBnIl{U0`x3BhO_~q{z!W-Muls)NB!&bB2 z;z$VbX7jf-T_;y083VPO(ouUj*!`G=6iq}#3=$(u+O9W3bwv#udc&%yWD>;*MSnN5 z-`r60wMX-B74JHo<$s444zCZ1vp%CuZtFzeCaVXqowJZmB&Qx2b87A!{vM-)m-tp` z*Q-Y7So<$Y3qja~1>4*FZ{*z>pLYbkim4dbk{P|NkYhcU`sg{?!EH2H^IStG#H7@U zxqc;ZTMaYE4wJf8UsmdlZK#dSd!n|>dV+dWilp8boOW5l)*!MD?B5fT_g^U?^J0O4 zvf|2t!Nhh`jS+}e6qr$!m3ylisF>ajf&1V<0m~@j)BTO501}Z%6ultoY4LtzOpPX$ zkW`>DY_ad8ljlFw?(X1!&v=QJeeadIHo~bPm$XpVNWcDvkFVgwj3E~Z;#gh`ta$yp zBgNb5XWz~%VB{Jj)m%r-&Fxt8xficg!HQqyW$@7tCDMDwQ}|bzNM5jO1}}|+Gkz@Q zPBgo@7kCxgnxXD5Ou6qIeitcSQL*!c3x~n*9`OOm42Am7V%AA`W3X*d4~{v=-{@Bn zb8_n)VEeBO{Mi!QM#!pfqg2Lb= zu8)R@w|9PBO4+x-~7Y85ds0`F1>)3DtFB zP?tq*pu3hs2N4LBoN*MuTO5_K*2)^nURI#)`OauOhI&Hvpn%vEEMxahh*npvv{JQ0 zZqVop;j$SXf3+pY%KLf0g!cro<(z3gABpy$1lh0Ty zR^4>_C?wzM)g=Bsiv@$qCOb}=M9XoOJD=jHo|V-sPQ%EH{Opz)kj-E8NS%*!*h`d$2a&^2nv zlx1D8@K9T(e8`#@`w$9D*d3Be)l&ei$Cz^k-G7N<@}6TIuHL!|n7^YrUmB-6@k6b0 zXO?PnTQg#o=oo#O@+S3$BfwV!fl-uw@{Tdf@Ai*%wLJ92K0Hh+asEBBAu2ZrpkxwL zk_CbU?|f3=bk2NLhkaIE4w!KAIMf&|6FB`cc{m-F$VKPd=Y#@4FbJgY8 zd*7)a!Y7M7zI#slw`lxD#7UM=ed~wVn)q2|y?2g38vAkwImTQFw~^LfBFi~A6=1;E z8uI29jO?ZlE9IgiFdMFscF#a$hq986b7^V|PQ6rivxeKNV++){tM za&zfp;^r6xx`#UlDes}GU4GAK)#i$#krm=Ylcl-Yb=c6(1>-J=40#z~yVZhgH+QI6 zKF9uJhe2BC+}Q&}q;3z`zlxxM@|%%vP|f8XZMXnI6qjz3JrsLxm)|ib^3fIW>)NMF z6-s&Qfksrm+B5R|#j5r9l*Uy%tLR_Hgi6N|4)x{+zt>>cjM}Hw%U6_1gA94ni;tag z5tnVlmCh6K8PgRTxWhn_LOp4+LGPNR?mV2&MVK;1KAl`JcLVa2_6-<^{v ztYCl!Ds}qH7nZhCCZ`^KrX?KT`Snol44rFy=H-zyziPktc0OM?F=^YNQ&MlpDm|17 zrylgP3b9aHKjzW0j%hYG0T8 zhsc;4mMVLTVR zU0rG2eAWt9v(HpJMxnu`!m{?pxbv?(G6ja{HDNrn5}Z;6<=ZB8+T~IAA)qqEU8=l4 z=)@9ynJtT*PK2@miEOc3nWE-7Z|>Gnwu3q36cm5(B!Yf}D8sgpkflBz=4eOu3+tXa z(dra|T@NHr#?6Co3cmEqHKHHuXj}u;3u{-(Yy9~bPFs_W=>nKE`PE{+)F9&8G)9I= z;hMQqf|Es~c)Fvl%wnVlmM`iedg*yzJT{K20@f?SJ+4H`W$}*z{azykGVLp}o2}5! zu9#yVTH6Y?k~}+u@I651iSdfA)l)~$OOi1F;+>+F<&NL#71XoRTBjxNH$zKN4`?c5 zfv0Mef+&1eZ};RZY0&U62IR$%`D2CIZ=EKnKMFSk6gB$ihdRLcf*7JsoHLjDj2m$` zuP?0#f4BY?b~R>!sc1H{NG>H&X`C)@*cQ5AH^2*qud;3+cem!t;q`8FAS+O5*cMg% z@}8RzLINjjXE)A{SnA1{YamiOkfjK>a5txsF@b|P>jgY-2}7JJ&>Q@II%HHVC?md4 zXkZLYx*phwMHUR(d8YJ_7X{* z!r3UeQmO1gx<_K?3U^@_@0jqLur1LV%G^`BSOl$;a?D9sDq3^hjxGxg+0Gbb$i{oN zS$ilmzL~3Its~cA`x~!9w4?^%Y*uSV)1llfH3T61*7M>y?L?Jn($H_@zrGe0zbx1h z1d36Rb8xR&lDk)Tf`#+<$Lnth1?7DT6bXLn)W8 zK&u+PX>X@$$_~hl7QV`H@oJ*ZkOxhT%X`epC~0gAJDWLTtiUixqR372f4jNbCfp}c zd1Tk=Mqa`u+hr=s0p{BS4+v|TmXGSs;@90d?zd7BrBO3LcQo8}!Q%@X8*J0z`gyEH zjdj1fLl^Vmyp_fSMDxFpm+`Qf4_i_vn2-{X zkoSrKa}Kt_+x1gHJ|DMITv~L~!hMR$fWdGw@99$6>M-aj+65En+)}43`c`r&oNqVW zh(DlxXY7o3Rx z#U}lcAX1C1GUPf=aQTUD1SsDV8iEw!7z=c4I0~=P-=KwvUOVN>O4fMiGRRAwM*`jN z9d8)nj-_2ye%MWeKW&uBkb~h&kK*v0;u?&?!+ti+uywj}4R-FU6mjFA6~30c-a~eXu{+1C6-w5!wNDk5P083l6vhj_>`$ZJW3X_uIy>v60W|KUb;tjQ?5A{@blvwKV7jw5_in7H0= zz+vv~cp=A}TRnne>nissGi6LvZDvf#{#xaRE6%FJn-~66py_fHAQfdDY#)dgQwiql z8@rRFJmn-8Y{amSQ$u|-ZI3X=rx6pA`HvtEF0gVc{sl)hX|5AFcJ(~E$iK_$VF&Qq zlA#~l?(C%f7{DoY_rM;xNX!L2>pCtYziP96T5L#<17vbCqWBWu&oLAp>G%aM(DWU& z+Ah2d^r4~5934URIK_Aef!^d$d;*s-nv-VCU8Kqou}<$82MlrnQ!S1MJsHjqecw42I#H0%=7@W3r1M321A)MD!iP` zdCuA4!@LDHEV%F?yEi#%)ScQ{DxWt;8D7LiqenwkF%A^^AB{KyJ}*8)K#H!lVc6iu zVxY2F8j1$rfhULCS0e$GsPNl=g;ppVCUz~17%-0g(#q0baXXdPJlU|sda&! zU!S|RTtoDkwnzA;>g^O#iFP6oWFRCjk#NCMXN=>3?6|W6OkmmlOIF19$DrkPjF`;? zThX)ly9CA5_0J)@-(|kl9n`rV9(dpz(LHQK^TDb7xTi8@$6y%(#=H14$qnY6rS{jM zcIBt8-!Bp7ZFsbM!~wyU)dEgCuR8^riBL8$e&0&}cY55iw4-tY4Z%J0&LVMOPM7&h z0Tg7)w4dxmO_*cyLApj4D!d13mJLG5WSX(Xq1N> zMUJ+`0-3W*z88)X6g>_~{OwQZYdvBta$U|HF>0dTQREL>1;J*a^kAkenI#5*5g&)Q zMCe*DC~^M(zyEK6|A#Gb5gtbf+WB5aHBPHK70=^cosJM8yXYc?I$DPD($6+KKR(Gm z&VvNI+n2w9max}11OPpjhtW;+|29{>=GBTs_Ke4Lsgns<;MTr8T(N(<9Qj7FVsJN| zli(-iClJwZ0ur<}FvY_8g0d!=M;SjKxH=yzCIfv^=)HJ-vstIx#X#Tiv(x2v^DW4F z|D5=EW&~%7AB%2<9#KA>A1LmAGX6dLId$64+Vee{UASQf0kpy2cKMdOw!?A+#1Jk! zov#5m>wfaC`6^kNh|n_7^|To&iO6r0+juf3%NJq4seR|H$^QFL`$nJRR{Wt5bZH%V z7$eatGbU{XBTQ_l086$+>(J!GF>K4|>Nvk_>e4;}vq|#hR>!?7>01pbTv=y0NZ$TL z`fDNge*Y-FNZ`<0_iJUbr*f0wSn%X9TL-NzDw^jG`s0sxGq0qzFfEHu)GT>I+`Z6q zws_7%VTD=?WYopA?{x7|c;(Fb)REEEod!Z&9)DEXi9zmyk9}VWt#qk+Ubcc=&E;T} z6l`8@cc14lge0tiL?JgdSnyPYY`o?uXnhs$OQuJZ{4W&Y+C@2ksNnXP*C`jWvdXLw zD{#l}?8xy3q8!SUNg3`%%JF)<4%vpuY=oI|D?oW+%eBKxaPluI!xvxfMW>tiS~v zhY2?r#gWY}BzJ`uK8oNs$$!QrUpME9g~@(Ag>M!t^yRjE<#FC5fr{HnSm|I_{$HRwo-JN{6 z(?rhNI*+UOmQG|@n8H8Lih~!oKv`cdrEpTm&si2;6Av6BIB|t!hiXes`mYR98m5&nc(pFWPkIs^sHygi3|V7p_lM;MF6n<&vdolL8ONuSFAI;o^UlS$UQ-k zzY{+e?R5rM`9Pq8%&_29*W$^2Np~bH6EJIgr&2v@0sK=8c{B7{vu-Z$(J5pz3E4NUi>C9Ah(_Fj936M#gKrojZIaRD4kuK>l-= zoERqKKW}+dDv|pmcBM9vMuoZ}*)9s+)vUP@e`Jg~SGX!BLVff*%Bw#2G~djYUZz>n<^ASF9T)4KkzXW+FsUNFN=_!>->5BB3{6%A@Z+V5^cK}xvr*qzr=iwk z?bVT?tD$J^c>nVq!xym-&uf--`Q$9evQJgd$lc5|88d2&_neEtphI8t=6Nt3VLyLt zAPB^<(xkzLL3I^ttX_rB>Fw$fC6kq4KZTmgdnT=nlCGr3<-?aaCSANWYts1SfE^Ty zg5fWBHt7}6dF_l4qfzkaa)ln) zpnc7SIA>4d?9@WYWF|jc?CBsdI}e zo@bTDsbo?PjRn(PD3QD&?IXRo)YJmj;c|kLM063AJ>~WC5YqrsIMgJZ`Ii1XpUucy2)l7uIn(buLs}uK7Mo=v6q%;=!dv90(-&SM9vi_v>HRdY# zvS|O;97;g8p5A{MWbg*7lS%ivCqf{PeFph#m|S`Oti7@Fn?@T(mGmhB=#ykhlN=Wv zIV`c1eD}Yo2rm4*E)pPMhMGh_gMahqJDBxW*|e}CX1tJ_*P0>>TU#A24alkUD;#^Q zW>e9!$=ed#7!xacs1OUeT7VuErZ_SKXqIKdo0({v%_B}en_;uaS;!h?U8T_R+LOdS zX6{6lOaK^|2|+W#BAttJGymO0shw=n+V2>wAx+4p2*;w)D|Ip>tO|S^=9pvwRRWI> zW2ypi-I+#0ex+BZ91HbF2wm*Ab2nw;VZ$VfqpqqMGUXb1^MX$T%bsBLL66H}6;>56^>a~kWl3%r1 z66Hifrb}AiL9wXg%I#{S*&Nf?BTY1>|2yhvFwJvOzBrXiEs`#&oZl8;}*qfa@EairRQFwVF304WCv*#kl2ZL_0Uy(u9c zuPd4<#ipwFMUKi1B5~`cHMN*eBN{0KJ(zrA3XIWb0^&MxdZ^X{84Z&NFg(MQAVXUl z>)lb<4R1e_>KU;$Ff?#rU|qTvQ=3sma>~HvUWYy&bG&Ls?*YXa>6=--z`pKy;)Ihh z;DEtEzIuO<3y-(IpImj_(X0J#x2nL!J+#$)L6OLx-lyO2v&lmvqm!+99V0Dif zTl$`m=}SB-qd_EM5x#DGv=gaTb8YAi_J;}(fU+CnF{JygS z4``?9jthN^WdxCSLt8TYV%@2P=f<2J1D__YPjiFs+)aU3RVVPSoGU3AO7MAyvOO0Z z=wz;(sHgYs1{gZaP~~C${g}76kyv?^R9Rhp{wA&;ae`l`O$OS(%L>GoZD7A!1lzA{ zc`4TKOCy4C>+{5zH1mxt9J#qh%|AUnfO97L5{T{Ik4 zdc8^XqDAPFyC(A{NZ;3hNU0vlRr9N?0=EEX%21;!yKlthptTJ(|` zV8W-s#%NJmgRitI9ln?#{?oX7UZ;mAy5AM6FvPm>7A2jQ9r8z01q@Gk6SiwtB&3CF zN7SXr5IeMcj_16Y@S1AEzlNhM;DestH)elKnz(3PuZ5i*qi@SkMDW&!ZrGUAZRSZr zi9@@yWJ?xS-c&FdwVRWB4L~aL4VP}tJ_WbOG*n-GGN9`McFn!s*57!j7kkA* ziDx4(SG7Fn&_r=y7};)_9+DfO=DxSM|LK^4Kcw}0#A7b%9Q$bW4lUio?NrLK%)om7 zD#>J3o!T&n+grVR+T=jd17>6_`5X;4gCH0eol5yzs(~UIst@^pHywSwRto#__JzQF z@mzX~0&JY~2f)`Q8C^!+S_OT#Xud%3GM5>oU|)>iZ!Nkn>y2p5Emo)qgF3RvT#l9C z@Zy_MR!z>pS;9Kp=CYNl)(XaRwZ@e<7QPg_MF~!>YVQYB@AoK?{Y9=;bKUa{CKLx2 zYw_h^Mi}JJEkvjp=qqQxxCSsSHBuqySwk{dIq(Mf871m+Ud76!+EtNJTZ9pZRhP(2gs9c&0>FesOyyj zbx;qEW|FP2Qf^8E-0TQtJO=$uUf!P@q?FlDkcPXw2U^?AMrzU#jqC}n5LmSloadSA zKPTY4?69u0hYp}REA)#5GN0gK_5-`p$)15H^tl?M^27TqFX-s)^Y>TGO6H2wS1b{{ll;v z%u)8f6}^=ZWwqSZ&5IsQM3tzy{4&GO>+l<1zTG?o`TC7>RhRY3se5^a)>%kwWhVVj zrS1o^$jRilqrd2YvIX-S63hr1(#y7MFLA~Q4~M6P(@nS04)5w|PSP(3Q5d(qsMb?^6HuGhAay`mNz)oUtJSl$ zL!dL~0azNji}vnxw5-*PEJZp`p-Z#U?C8uQs(EV*KThxI-5s& z7ZbUX+S?L&<3#~h(|G-h*|SQ2P!~EpA-dnbF^9{#k)SO!$yUx;z6|5qX&HILXB8Dj25~wy z%cejWx0Kk(GrP$AS0>4P)grs!u=Lq8)Yp7MUs1{DhDN!n?SB@k@F2ElH}QCUueG9* z`#p+p&Mi{X{Kp7ObDN~e5sVW|ITKtpMrNBhUx~GqZld2cPt&boHGi0nXBMjdSNuFt zLmb5dD&O>HjpM}Icz#PZzAa}axko}Bp(wLaNRgl{-bJo&)csmh`BQ~RMN)1L>T_Yc zU4&68Q&Zq+!U`7980xSXCbY^eW{#W*lH}m`l>zS0!6~Lr>=*}*)kJ8;B4!ww_VfS! zc>r?&Y${9fjCkbgavQ1~`DiEIqB{2~Cxpw0DjC!?_QsPmcyUEuMwg|SS7NnDpfI~I zY;)e1M9+nJIF4iuGu+VXSCLr_z3CF~(XlIEj}$oNy(u9wc<1-2V+Y=a+~ror|b8+UGC3e$izOme!~6M8=Z7 z%|1|p(A>yV*WLw8totsk=~bN>tot)Yy>N$UtC7u`^K|xDHCMw29CR7Ocbro6`8LqI z49l2GvMYJid4Tm0i${tJA*F~DAw1_So1YF>8JI|*4F&sKhGPc z9?9pl$AqefF|mXhIzy-RDV%}1!haORejGH#Z2zg<1;I!8ACPcG5>yzQLQ{=0T19_y zS>T{vpVG^90vmyg>PP8z9S$GS-hhrB@wd`DH@N5cDqSS`o+5Qpn~Vch42IT_-Jb_X zxv(KkvD<>V!0}$GP)}&goD#ey#<`w{A4wQD_VCj>vjtS^ca50k=ZfPiXI*w;{6Sg> zY$KH|R2JrN7(jzlRI=pR?r+TFr&47>o{5@c0E{QOmG;3~ynVkXhnRL>xq5*1&IDLR zN8jbU;#x_IxJcn*)yZX&SXCbRC&8}o zxGgD+1$WzgB&6W9a3U#cgQcP^4}k5BqPWJCbcCh2s3y^_4&~A!lHWx@*D0ZhJQrTUK2S#94$CCK0Yc!mS=>va-fUzsi zBNKg`U||5F3p3uQlo{NT(pBZo^~6hOeaji$iIC_2v1!Ee=Irjwxgr8_l>V2K1_2}= z71xZ2&f^%HmDWZWOj6lt=-o*|5zoVz@$7Sr2z1KO7*vOUUN)1U;M1-t>u5_a>W!k* z+fQTNMvv{V3{3s5nUXVw5D#L^2_~QPgJ>V@S`q>7(ms0Q;v5X83?5pyC zQmxcB;lfX7FqAAeTf}w&WAG+*!L0!Q70UtpYwf|)dBSU?m!ddr1c! zR_x88l9O&ul|GKJ`Pw25NgojJ6W{Db(h!j=p_spypwB~mId}nnVF!0X3H^OAdOu&l zxy(ZPz8thhq26o>djp!}nZSTEXv$AFvAZw%vTX740yFg=FFVh#yg6Shq4`fex*w0@ zLD%-4RWIDo0}|cT&YBt3okm7pg=amxwX$ZdpH{STjS7$Ye+RSjPLlVXDIU_v?H~yb z=AiWPmr2f#Jvh2414m4`t-%}J>^MSB(EKwm@J~*C;xS_dGgQbBwxGhleNYK=}}?xxf2{GGmjj{u27vy z+V$Vn2+Sk~-GbSMR-Lt@)>UP-w}?NfUoO1#Fjb6ic7O1t^m{0@u!N4!w=ji=*Q)|V z-F#?3Mxs&4;Y>1!6q%GSPk0>s+Yaf#5^*gvuc4JiNKCcoJh&d&q+y6Le63(}tUmD;K9Vij~vK5=eQ>CA0jZWfx8ZYi>b zo^Yj#pjgBifoO7@ZuDUfty09m8sXUn=!pWmh8v3lYi9d~&$@CCIY0r98A4%@6OBlU z&G@&_$$csMq%5vFM%uk{y*>{~uM7WLW_LN91!38Qf!|MmRHq27vQd=UR|C6-i#--! zTKab9>l-oWgD9O+!iwPrIKEu;k%cL zPuHkf1yc*M&H6-{R~mq(>NaoURX|!gpxtxS2YCO(%+?-mt;ftP=vdRmU2$B)61bSk z)MLJGxIdBI?IxozZSyoJ&rI!IRr~P%=l}1~#GEO42%3VNokX;-yu)sJ2WEsrxauHK)GSZ*w*o@>j2}@8pP<+f6Kvh_x?6pzO&Atr3RMBWhzS?yivW>CD}#4I=g;+;&nD2`OJBnmp@x8xLb3M%4opA1 z!TOrsu5DXHKVo&DhifkpZwWegn3;$WLY0L&+qT|OS++feBLhV$bI(FH^g&VPrk}*B zkYo+TJWgNLtLVQbqVx}VX`=T(;Jz#{|J|~$q3h!dDRt1!aH=drQJ2n88umTUG2`3Y zV|KUrZEF_NrPnK*Fs}>3b&kOkyGcgOZrW5-9SEJ4iuWgOqDe4=u*o%DHJ^J-^NmNCzuLpPN_2)>fsW)2q ziMVgzlTH%zZvaC?)O{ z5XCuy`y=$WILX!u{oY`~dUeH>WiipATxc^30(Gmgs*sg{(~CK-ztN25VRwV#Xo96h zx~)RFet{0s!rM8z%zK16zAru%%R0%wHEf%9|A?ow2I1-Ta3`HEDD_TZaQ(+82{6%w z($C562Qw*l@FC&HHTR0;Oc+Rxvhh-GZMJaPB)a`J0}Ry<3EzLYWSR`CH^4kU=DT9; z+O+j5j67e}kt@v*ABPu~+n@MWgbqTg&V#-1Ki_0W*KF+wVYUMVn}q?_0^xH&sYP_U z^@f+!@19(udHI2WE-!)G)AsU`7j9E+Whhe&EAe1n0S=1)3Cmqkl-GGfwJ4$@;*d}b zIK1)jy+UABkl^HhOWNEtaRtoIQy?mgE&3n7ydenF$1QY0&Pb14H({3&bHkzjP0b!W zlx(^E9(2^YU<_92RL07<0LMatja33-fD(?l}P$@FU^5-?6ER8I< zMCms}fRDG`zyyfUlfXs4;jTXf&v#Ul6@50Cic`SABxP4|t|D$U%V9i976(to5+N9X zc2;7dQ>GX^67*ycH=lAsZo@`H03eB#cPN~H7Pq;%-k$$<#erbEi4kRoO%N}0QY_FK z5Qx>`?Hi^ndbDD@Cek5plx&PofUDj}{gS6eJSr0IrM{!>-%pH*nR{4M7gT+*7`A=`MwR00?V$3UUfITP!vp3M_tkU%jt{Ki!c1S3HXw$Cm zsY#a|-K8a8oYkZwV3sq$4IHe4mWA;-Cc%NPdwPy5)jN@CiM6fWA_7zhx~Ym+Qmaum z#MJdfGC98KrPNdvR6OcGp*>3%#x)`4vs>Cy_gU7cFXUi6j7cRoQx&v>ss}5Gkhj_m zR`RU%n{9Q;52~Nz%f70 z0oAsLa2tCYo9SUHDpDW!`3>zkK$xjn5y38zgvGj>NTa-PJt*yN)pzuEnb!D0>~%o5 zBcgs^(bUxoov`_rlsOB$m{IJb{X!K99Zr>ndSWRI*vO ze^SBuC!W1EhgGI;E!+2tEEcua4V0sU_pH+L(qZ!x8gFKQjDZZnC4C`s;XVz?Ei$*Q zMt)g-;zOh|Acik2GAZb)*vr3U>(XBxQla8efq`!um@cPGQt_>e3#;6{n%$zPpC$@)4DzJMUu}hKDE>A z3MpS^bio_M(nU&(?^cIjczwwVu^X%jQ~6dLDv1hDJOJzsAW zh5$Q9VP3>NO044h449fAOtu*U--5>T$~Y~Rc5-W zojX*|G_Ge3P?_5Aq()t$1o#N|IFYRj(q;i;!v2DBo2^NnS&6)wr5&mij3jG}3Fzj2yeVhr#ZW=rN zrOGHyK=vqm7F1kH*wmoR1&f$yHhW{;32KLv?7=xK&zlJ6i0kY9MTkrV5=M`sHrfIQ znjbV{CNSh5y9Rhj4)Kx1TwFefoSqMSgZDwk$hJ=qX)KzLZS(r@eN707i?}~k*wU@y z=aRxl;D1L7Mcc+&gq$ENdXrJATsIQ@{4XF&YLa-XXRai4t^BP=niII6!{lSYoB470 z6!=L~va;9E9mJi8(Cj#nsWxYCtFgl z*CW(8a-x}@=yBPajPhs92!ct$>1{Lu4MBckDRjo__Gy6EpWCyx8=!-6DRd~zd|mNj zF;#-W3BCnwB>nSN*vF97C0J;$iQ?Q;#-gm8QP8o*{zjircj%G)rj}XAXISV9 z;z>NKf*U!miRP~&gH$;4;}5V6VF_;bSb0nj1k#~1Eojvv^(&MdrCj8T8d(swyCnhl zK2|=H8)VGzz{tIR;a_K%*u@ca24LlFlJEJ@D1-=4sSu%(lx)-#a}^TmzbShsP^&C; z8>4)O7YuC5#T{}El_<1aA5;8@#$5hLCi@c>GbE66E?L}@=!|)lK0y+2~_@iMr>WG1rEK=a?u7uB!>;zX$YOP*oV~rYwKh^kfAgu zV_%n{s#7thkYnN(6D1uA^-)=hY?Y;7xi|FL4y&Ro$lZx8gm-rRVbXj{_^$7=T%%n` zG^M;3MAt5gyUr+i+%%(T8qk1$^v<|laB|eC^l?d;rO4zmV_on?01THTfvOhzpFYMz zZ=({Bz095lM2JG&4dA}a!uktW(n@f9N zS9!TZzv45OL{OK8m?9zW#_H0X_Do5&)8vc9Nw&L$nbH~f0I*+GanUcl zthnKn)zaeEEJO3T*g;6DVHXDiYtbOy>>huFc4w#dnCR?>U*By0wNzT`PIGqT4q|}N zj8yN_W;FUCl7ija`m@6YL4{rcvGq{+%2$qfQ_jE*5>QavjTwOD4Sbcw*Hi$XY%9uT zH;8x4P(+;g*YHL2B*%4G-IsJbd$BlWw~4jrpKR6eCdSi2<&2ALIhF;(B*(j$B{}Rw zfnramcR{XanA}f!mgn;M14M-1U{O(6boC{7Ja^z^hfG+bE`u~LNxCPLQU9a4wz3_K zW5QTr>kgOg9X$OXRpYLAP8N7#GF9ukuS`j*6=7^R6l|;aeR*2WW&||uf@9czfVC@^ z7TcGnIUa7_Zp8@*nLRFJZ65!JdyToanwU2Vh9k1miDoc; ztXVizOOoe4)0b~;v^ul*@!s_sE=fiXnLISy+=7fH_z>3VW+h-fRj*e(U2i|Yq!HA2%uWcnO*;d^841QAdR3SP-oZRS(&!xXy|v4o%>wQ(O#jK$9~xvrVqECmiGXM#`h7fHoDQwg^!6o?oHTJ z+}XQ&|6n)1{eqj4QYggLvYw|8uAx-|Wp?@!PfZZ9Dy=rHV$o^{-5JA%A@}|-7mQt= zz8>cbf0;cLEHfT2ZQz>54Jy`ml^oiSA#L3N>_uXU&mSz%u=9zFtqGiF(f;QaT`Gzo z`n)fZRU+-3z=UK_*o!WNkg*14!RyT+bD8CqwWDXsK6da_^!1e_^N>3HBDT>OXb6!D z>_V@3uC|F<^feZl+9Qp7 z+A~AGy+GnD$k3V2I0-+^aR~=+RKI_yO=`(cu*fzA)DF_JEOB-%UbK7|bZZ}KG_G4j z6HUnMW3b<}r=yNxW9}-XU_;x)VMgV}WF-P&o~Rk&TpdC*r!hMx_mOfk`sY}T-)dQa zf>A?qZe0R$yzJi-EL7ZRA_bhGC{|GlN98;nvHm>=x7gwJD~ueT8u}*rThl|f7oPmR zB2%glydoK=@huRz)(m6d_8XN{#T$Hp!nNYr%)#>zAA(i#wIH*D+ehMnTzRzk9t25| z^89aOU{W$ARitVnt>N5p;t#JcDjI?LrAGQit7sic)#SWh{I9CZRnZm+Z+PtXB|(rr z3UrqQtdMeCG6m4c!;k%Q!phF-{or6fU_<<*1-k6_o2G+3ydAJHVOHSlRSa9bl`}Gn zz!o^vtV9PAKA@FQ9G|jbcocpm@E&=ctHqgD|CT50JEiC!eC9HmDNML zQkVjmKKq|M*GX=$1fkWUhZM$ z8^t)QmF)d2HOC^r3Y}{gQ3J3N{eUh^pF-{DS$|0_cILTRDE8UJnU*~~kH5@II~3Yv%n5#i-!F(f4u(mi%RLltrHMP6#ulHdq03jZ+T~Rn zvP~;jJn<#K$GTT%0JBu*(|s8{_eAYd8BXEgm#|=Fm)d)QILjWk%9I3Aax6?u7`Vlo z=RVAcfQ#e*)K=$YX_H_3JpuVqVf=}2Yhz-{?CEI{qE%(=7%9DURv!5H2Pxq1|E{G3 zi*X^UaRsokX_t}5TtU(`kHY+awQB&dj>66>{6uC}|8{ZVe@_zv2KpVf~ zKq5jdmeFX@k}>br;I<<5#?DUyeQe8NN5Zd}73F?R&(Sm(JuInn`81g(##xF=_n!kl~>?wUk`V=E0r)gjpZ|# zJVy=~Do9?T&gXOm-t<(M83(<`X0fXHwPf*=Up=q~A<1tn1)BSV;Si*3T6kNz&+GdxzNiG#BtOo2!?P)D_nqG8UTV!a3{AV&atK?ba6*Ve0@4r@c zJ=GF!%$Rhx=zFV9gzAVa=0c1PVt$fx1WUGwLyaWs8wm;Rb17EMBzqC`rJz!1RV<6SN%x((-)$Gnr;M5EqiBQpMm0B1ynlyV0q4&Sk z&TH=&2R|!FxwJu}K@LbC=}-2Tu@GX;EQco@+k#CRKCb2-D$`Ri{ieB7(<4}#!pH;# zGR=I5tEe{FCkkDvDE4%#GiA*-3DpXFHd&;%*hlG=r7})McvQpi3T?JK{mQp~u;8|F zWHeX!2jv_FatLGE){;6U4eK(-fpbmVu3&K;k1KfT^_*zVqune}d?G>E2Z`zlf*@od zH4K)&qh5L~mOJ3~S`))qUc$AjxN#lR%iK;WV(9}baUQd$!LdNwUM}XCN5WE##ur6D z7iR>oH-Fx`)!E@ea?q~C9kcqln?=BJsTQY7vAKDmPwTxJ%^E4O%{a>;DMAlH4N!9>%pS)xu?$spILHC@n)e8uoh&&_N^1e~T?;{u%~xp2!<`1n1t-M{@2 z51-=YCiUV) z`_lVP1MTHE&4P*&C&ZN4mWo}TcIS&M12X~oXc|YM>#_!p(si6W9N>m&-cM}Wrq0nq zGZVwS^S;}{@m=^1Pm)$^k(NWu=LnP^ZD)(GL_E-*;OB$ptyOa&V_=4~#51a&cb;9_ zf;z@Q*mY5~J(_VSQ)ttCY@1fxvQG-d{<+#~Bzd+|hCaya4hwYE)RC})=PS7P5MSTH zU%toP6&K`AynNHKOXUcHAPB-fHqBl|YAn}*0vR?sPj+vRxZbp}b^`SWe+pX@+KIpi zh;uA(U1F;#+&>!B4gQoL2l96coGchP!%4xF;~eOt`m``Bs!1x8;64ZGIOLAoU{oD# z1)FRkQwQQcQWv~1MWTzRN4W@z8_(+&-%u7HMS^`NgwEKF+J%VBWC$DlD=qZraefS& z%NWe#`m812B)TUGVn(^};o^QuTbwJxy-#O?Hw*q3kx2X-RT<&e0tNb)1B zubOXbipH($HMLs^Tg(vHht$X_-m?xTTch4+!lNEDhuYXV}1B&f%^Ylo5iUy zOqNm3b4<7CoC3uXly3+;WONJjaPsDK;ha=A-j5IFFxQWZa$d@xCYy~#cb$_8U09X$TsD01&i9{!r^siu>C&jgO)hG+VMPy%3!YpQC{o77K^D>pg78B)*o19{`@ ztXj_v$`yE#J=%xD7zP(GH;17CY;5DsBdk34gFTpk3x@M#goZL|b+;a84-L(g1XEM)!WwwP){<44N&}N`q`MvjJHHpxhoqv_EzXCH`JB$?AcuPlU3Mn~D{i%e*;TxH zim$%Me|?T`enhq5O7tQ=_jGlTl8!0}f*|yi4@HmlIzK2&E^p45e@W=erw(O@u;CPP zS-*d(Tfm0;GX<^p%4U`MZse^xDFD9u17u0d%V6Cz`kHfJa-z#Up zzzYu!m8eSZ*QarH5#>Q{zF0@APPs}pt{Lh>XtV@gRs?VbivcUc7Y8sthPe^y-Tut@ zQ>$35b_h0_m&M0wN7+!8owt<9r+ruZgbN%GE=Q5clC;-Fwlrs$!A^s?`<6h~y<+>B zg8keLku#>*M^5%iiF@NXa68W^%{JylVOkEgy_Q3_4Twz*tn#PLIlnRtGr`(7jbwIv zMBpOC_MPd?xA~nL#~jrQv@79tpV|s<=5cluznaC(c{&53-il3b%208S32JlDkC8si zl+hPacfB80@v7#oHQ_3~w2`VH2%Sh&PY{H69$TShwE`L~N};mpMaBvl=)>95IC}w0 z7pc8gZkn}IUQu_kMvX1(B3E&FGfOLXZp%#uoY`%=h$XEqvPBejei+U2CaoG7Sh~XJ z)GaqSP=^+Cu=PwT@i|%zZmCtFL2KLgG*%N5P%DfFX% z3WH0?&GUSIMdJ{+Y^LbpG>%l--y(1+ir}^)17*xk;p`&fyI9|L<$6{ERO`0%;_WH_g?Za!%-EL78V5?3p%^t3+e%tL8vHyVU1g!>Y-2NddxZ{Jv zozQhI3YFG4@@CuOMkB3OCcNFP=X7zC`asY))Z7hnCv*N-KhCkTQd^rA!M`&ng_w_WX;KA72{J8X65A|#v| z*dS}9)$~4E_grshVLkbOElP%~+t3R#30~^Z{*KOi2Z?J?ZLCx3E|%^&de zrap11|Ek+ZPPFigB@7K=V3_)(Qowtg=CIi$&9D?B-WftlCLRni)4-_>TwJHts?RHw z5MZvT&iy!Jw9}C#!Y2qGiRuZ0@XmXnk&U>3bkeM^7z7yX$NU7&pTgDmas4_@pP^`n z=9EJ;ozofw`F$uuv`Wr;WqW%vZ#SyvWFg@UUuzT=@}!Nq zynsumsh`|?XE8j;5*bye$>{_G)gk9r;=2smaR{iaQ|CzjJ_&tyjV@ac=2MwZ7;^=x zt4mW}H6=RqyM@YEbhS8}G9hjK#VP#aD#``SP2ko&JbaGzt@bfZU~_Db+aYr;0g$O8 z)IW87b-YzNEA)QHMBOnX{2|2-ksNijePPEri0V9n>gF9X{e($-OV|4xKT(4jKFcE| z-ZHtV=10{Eo;=0v+xYSpzW4!O-N9ys&v*O?yB$fM%7P#WLeCNKk$s7qa~E9>P^(2> zq}vf5PPcrAN8$3K6gRT%Hfure^Qm_?%F;j2zm>f9-QKbRsdh~R&9^C5+~!PoIK@BD zP_x}=zTyB$z;4Td?#VxW5cHluPuib77IiJvM@4k~eNho=m`1%Fz zZs0|Y0^~^isa@!LmWvptATScu69nNMhpQ-Z5m)y&ozoa7s@eMF27dc7u3f>x63Qcp zV%Cskc^T4CDP}rsWi2P|1qT9G-jvIe7NrV+u8HF4y_&lRECH9#d9lQ}ZE_tU?xwO? zTHFaaVEy;VTi(ZLUs`F1M7N9f`ulL@EPnkViBFzi!uSYEdG2q|I;*h7X2BsxL3g@y zR@9M2{DKNpy$c$N?Y8u_3%}VfnhCyz*dh9OXa9x3C2iaMVlxO)goPOjnl4Y_%pCsv zbNtt5*xu&MMb)lYGM4@EDpLmRc;q9JNzpluX{n5Ba`*wg^xFyG+AjlYs8WPX~pNg_orWLXua8)O2>g)^wBt|3CWNp_tw|*&`^77|Ua59{qz@t>Byc2p+UY!wS?Kd;XnDXY3A#Tpj&@A{*z1Z!g!?+avo= zZILCvE&P;()Ak^QE>sq%{p+4p*NlUg{ZX&k9@B0>X4BwVLVZciRn}-VZ9c}G+xYBj ze0Cd^n){6$AJ3V6#wyH+e<}!qAoQd^W!$J58pgz3V|i+RQ08;oODt@+DmavbhP3;# za@I(wy6@;-?Pd6lg&F3dxmhjYF;RRwh*OxjX#G$p7q26)y2ypgZ-Srj`B@?6sJ<}B^DB9 zos=%bF5B3*w9wA`3t2+R{IyZzY{-y07nJ)^E}_4G%^0`V@!cvmo9=ClyQDa8QamO> z@JLio5QKLXTV`kct5(b{wb{JL=P}%mr3HL&17|N|WC`Io=_RUl>W?@4i)o32DhPrg^oCWSt&NsZ&#pG`;03s`!`k{C(-GyA^Z~-ACGX{l??^k`$OcvwDX(b z!_vlqGJa5#H8#y7BMB%ivTqRc;Gr(z2DBjyfb~fs(WT<5lDd45q5#^=YeTxxx1XM>F%lON=J12_pmFHB06QnrL>t` z=(Xe@%n#{|nLstyw9j)N&I)6ENBXQOAbiQ6H!GrJ(ju0cq-9jjogo!?wAbu8Y7#A? z*w#Scpq)x1@L{K~y)N=Qf;oZZ7BlJTo?w5MraRV1bKTv8J$mo;`ME`qyR98fNG+C8 zEFu^a9lP1U>>|GXUQ+bFx{JHdu(+;M$@pi8U1*7hJJ22_gb*?)xHJ0vgKM*aZ)Z_! z;?Wv@ehnYpz&n>PF|HbQ4RIUIw&;O{LM~h7r4K$=rq8G<9%!D2ePVW!)ID&*tg5aZ zp+c}f`xT%wKiW0z8Stk?<}m~_B)NJx?{W%+;-9Z&fVQO1*kVtq_{j z%*mG{mGZNRn!Smi?ZNAnVI5>##G92vOXZOD;BW^oCOA>T_}@8ZNtcG(7Iq)An11Lo z73NG*5Ov#yT_%y1)wK_ct5f*rD>y%duO{);0&cBfvEki#_9G=t z1h=Mo+t7?6CmDM62q7m{B?{F_O!Xs^U}P9SxrzV#5BU8raN#;iu!doF8-6Y_E9j?-OD{&+Gt^L6L17KSY0WBYrFLWVW+q2|4rq#JR6yCa z9trx|o_}s)S_aVxshw22hpS`c~lKrqtOFOrNb9*Z9HgbGSf$ zSle{Bs0WHLowiA?>q%0O*pYS}kDuc|zs7%kA}+mJQ~j5af-}yqZ*5UAVrh#KLI@cU z64Ue83L~+hnjOq;U~wCdSFunN{rS{5CPuY0aaw^hHd=`9HS`3NDrVs6Z7P+1Q%Z9I zuc66#5#G?1W%im3`V%Jd)6d~wCad{@eu-H;8a+Qq!({WdF3b`8K=N_rx%4J-?iFZH5)t{g=qeV_u(b|yoi&9b3FAA^Hmh!r3;@=#k z!Ik}`bC|uaJ$dPv4)NWpZ<(~ky(yqjLAfkSTafD;eZHi3t=qFuD6e8|PA*)JSMxg!5W(A!Jw$CkuiF}C7*D@w9aX#^Pd z@pP_wYMki3dp&9Ep-;gDciS&qL$Hq88W!(k?U{68F}>5yBuAXY7Pt#8p;!SbFck## zo{1ftuuaqaP-2bsUb~4`lFF;aA(V7EV9z4AL7xaP2q6PR_SkYAv*J+G?jia2k5(~0 zjQ39A#T3pIF*5Ge>5}%|i=wU)9_($T^=OZ-B)chT{kF0wW??u0N|@P4qdDw_2dOB$ z9Z{5*iY2j0_mD#xCS`t<5t`g>%ILIF+s5)LYNOITTVK*k&4u#JBS>{wbwB=1Kqm$u`AR5D&A-n@Ts^Q8io^Og{&)MT`ZQ_t5 zC$I7iAwAHmM+iAFIL%UOz9rtP=kooh$8g~+Zr;GpKf*6R#-;0`K!|$K7MmfCeSH8^ z$Te3p`>R5pujC*J%sIiqyDfd^0V-=B*u`u;UsJdeo<3{}*X8>GDv4pMe?S4gQ8a%@Y1 z_%IQNnYhY2z&L^m!orct0l6%??(vM=tZ)>;gxoCotD2tFv{4AfW(anFkd4dN*6TIq)ZzC7C1G1om9e_BuOysZn+kci8l@Tx_*Wh)91*+PEtEIv6=09w*s+S*oO;Dcaxsvwzi zl_^M(Y73F7(EIgKJJK3+rdJwCsyO9O@qL!zf!jA^!%4J-L8lP1IHtM)DkI53Mb=)|OQ)3t@qZEk|;bH_$dhS9+E_x^<+bxY)JReE_yEFn# z)pc?h)>m5T>JDEgS1;&Y9ndnTS!0WSCUGW%J64LjGdwud{*u~LVv@vucMLRcI2@AT z2|MeKHj6hwuWhRrUv1&uD*pT&trp&%#+gx!l~E2+41K1Zdx*GmHg_H+iQTTp9%R!S z9mhLo@Q0e%j(3(ZU&TgS%DTp>SX_Q^%)C8v5}{X*5OQ(|yjL&O&S6%owttpOc;^QG z`8W8xUrG7RnF}b7Agse|YcyG`ex0J~89_Jw?iK20kW+0s;t7a5nxSI)M-qEni)$nn zchv0^r-Ka-^S#FF=*|;hzsic*rD2((x{Jyp%p-`S>{U0j4hI?5_`LlHtDAT*i;v#L zuRp*?H*w{H*w0cQuZ6@lRy?5jk-5HH0}KsIEX5kgS;)yDI_ep*Yd9vezFcgn-aGN? zb;Jj0O>q|{eDLc|G&I{a$2Os?shSG;Z@2K-ZISuYd8}-Sagt4nd>-wQTI70<%F+YC zXd}j?qfG`Yg*Hb4%O!n?61pJeEJ;o+qkuik1ya?m+`?_@$HF5Z8Tms*Hr5u#M+E4QUfYbi7ap9>lgTJ z4qv^%{Uyw8VzZUg$DJF%cz5Gbnh_chEVDP1Y46lUE3qhf&ADdsMuPjc_~?iE>g(1> zzCiQIdKQ3rO|M>09$1pL+f4e{l1h(RoEO#y3Ma3-eW|P4Z8AZ+rxt(DoaAyr4oyM=u;iso@ek_e|R*Tr=ToZ*zXH6hGhRfj_4e^sHOdH%B z!58!R>kE9oBwyG)e+qqhPnuTb<^{Q|%$}op?{5#``z%@rO8jL(XJTL$Zxd zQ*@(3AYQ$L$J2HDnX9EIxlKh?O`=?KPrqv2fo32TRP#odnp}rfiqljct6fQjboZ2L ziF;2vsBLS;-jA7ywWng8s+*z4emXbO#um1#c(IIX71N`5=dz~V413pX;yl5J(v{P= zvqHfqc$v0z@52_tEv@L--v0>WN4x7g{#`Y>?X?9}`7!FjD(I|`?4t0{*C}zSiB{c- zbaA!BtX){aSNHH=U*PGYMD3YWgE#2`fiXY25O%-pt>80n+Tnd`%Dn9bW2a+?Kln_G5Kx1KW`$q?x)wU;beJ8m1g|^rz@K0^t3-(Pn57|Bw%s4hk@~fQoL?=}*>0C)D-nPH zS{<`n*luEc2yaiRORwxR7Zcr2WEAS33)8-#5>TrlAjJY^hA~;jg%OMvvC+Z34JpOx z7liRgPf5aDJ#qq}SC0^K!U%S$M@bvaCh85T2@@543kO907Z+4@&~V5!1xE}mS23oH z!UUpGF=>LnDKTgE>auRFj^YT4<5C||s$^`ba5|cD7QV^=%VM%LBh4ymg`!m3kn--< znAt>q6SL3o@E%s?(Qc%}&(&Q#n#mB#QM*f0wAW+x&dqJCY)el)DHSrV)fbyEbxl_? z=`qABNn6st*RKl2@My0)d8>@Ks1dmSP zMh9c#;^HlgprRd30-fI0S_lv3@pUyc+(%^iS~|&oTT;Y!TT=9-EMaQuIQObG-IE`G zLr8j?Vdy!+))r=;%Z#-a$KxHZ~S0uK#r1Q3c71?+fmhtd0 zmKM=$^prO?{nm;!k`&v*sLEkjUGjRAX&)J-l5BV4sjZ>0jky=N^8k;ZV15bJhHtoH zw~kWL53C(`WkLucgd7j^O3q#*al*B zllb_dOm%b}&JcbT#(NiBp~$X16=@5=vYuIo z5*lb+Rk|?uw*88pr$TUYu=Z3@ofeUXRV7>4-o~SOiHcj_O68F0Y)7P7c)K}7JI%hb zdd_+S8!fa$?YuCor;e&Mbz;$hlOPu%_Y$i*tKJc4v5>MJ-vWk?ggd5^~(3SC0^Kl1OqPr7o1I)yC5~{PlA=qyKOj z7jIztG|FSzwV^HUoOVN0t{^;Ed-hn`hDpW6NTcUmf9wfhRDTcmRf3N|^WK5$!EUc} z8W`~<6Rj@ZZly+R@c@>sEAk$oY==KAo&c+_*S4|n0uR2&x8LE5Tlnst805$~^OEGK z!|P19W@aH2+aMT$8J8d522ZPKS{qdY(2586LRZKX(i_|gAy9cX8(3Yyll%DgmgMn# zbr(zPJ?#)&fgR@q_%XVvn>{3Nm&1c+_>aFy=HiF*c=skQUcuOeSPp@m30r72#L*jS zsP!uYEsZv!v$IG##;ZCmCmNG7%|bQ6f_~7s)dbB-OFGHh+daF8KYxvS9Z#3>-c4M& zipd!{W0KREDmq)}w0uJ9t36vrA`NB!K#MmP^~cV!k^Q%YBecnPDT?8czgjt(>G;}N zt~q6bSB)LMgP<%2ySPdsG&?0~RV=UJ$sE3WhOZyun+I6i%za&;zs5&&K(p&GAb@W< zX-0KFujsX@C<;R;jLQwRQ)Ou$4<6y``?9)kJ;qjDL)X*vWw#>O(VY-N$T1|X3x`}f zyM2bwuH#SlrSsX-O}uvz7tdmHOw*M*QXHk#(%R-)y1+Oy&B%$jRktENwr+K>zKPj2 z++M_YFYwhfJX`a})+SZk;=bxrtAU}SFbt(;p!znfJ{(59dY0Ko0dpf#mruDUI+!Vn z{;*ZW+%oRY;p=Dk@;UA;q1ov*auXa)ZSP%~h(YSZ5IHzEMyL&{joiC1QRiIvDqYlw zs@d-N*tk%m;^f;ZVnjqGO#|wOiv7k*J|C;P-NLsE;=x+1;lqn~`y9?qi*HX3U+tW4 z$i%kqECjvIz&%Bw4evYd{RQ=67A00v%E^?}U=R0FVvj<9jx_)jxtP&L2lrR;?~kxr z$A@cpdq(Omjtz-b=z7n)2!QaFxofEw-PRf~p*v>Lqiny>3VXyR_LpC$APYgRc1W+S z<8P}Ad5q0V$L@T(iQiz$C5Z|x6vYDFZeVd8_ZIPu-5wY4-LlNR9oxc@$)UF@iA!RR zw9^Ni9ExgJ!Y>C|N|En1=<4XY>Lwk7Vh5#;1WDVU=~4Xrv{+$h%ecLar`uR-c72%v ze6tLu?U54$y?TU@lY|e6%2|WW7VbXA$|`O@#Lph!55LFneuFD-AsUgQ8Ah$mc3Ug& z71U0{%M@jk_>zN(dI{V=A+=MAebLqY)mB-k?bFme2#g<2qOr8T@l~?_XSL~+Sgo#1 zUm3{=!nS0;eQ_86`QP~TQ#^Pg|Jm&HR@zJYrN^~9k4VxuIOzf4LdINcF_nfdoq?Cl z1pBs5O9PEdqOc~z@>R_Z+*iuG+gp2NL){Jx6+GlUN`=~m96>QV&B$|`azB`dC*>~&HT zdhe7Kf=SzeyZ85hFFB-*+{gIq22nKcYOSnTt#<;nHFCO7PKEbD7i)(zX zrZ%c31RFI@9jNxV0cxb6Kmmaqh(7=QYm3!3x9rw|GJGYpWx}56ryY;-84ay_LJ)e zW6GpqOnD_S(BX?l6iB{WXkMzCr*d%bcQ&=@_PFrh-w!w4q{T>swAjA=rPjusWoa#G zr~RWE{wcs8hEW;yrP|TZPWFi$#zqD&o4)Sg!uZBxCQ0+*f^Mo-pUiyYa;;2HwpJs? zqctqm@Nf@NEzNpcP5p$r$TRO1%u$g@%=bLKaJhC zU?)HlzV!MNKg`&L|Ar_hYEhO%fU6DsZ65#m5T8F2QA>hsoX!f9TaSIPOtLEpG;{-D zYFD~8fvy6l@$O!?83wLHN1(Q6)7Kh3RlqM#<3bs4kK&V2{MRhLT$V8OYyn0A8|;x2 z1-*KNkP}0`UzgOuD0r)>){vk1Rf%;fm2hbq=f{1ea;bcxmS?e;*1>;?*TML1y@dqD zk}BM)f3%m-UPWBf7`FXPmmgWJ+$*0(T$meZFCb7aj@5>u63W9ekYXX%0K@D%ib#E* zk;+XJubO0ctv|)%+xYxTeDS@MId!-;RB@b!Hy*8)iQWcj*3jBQyo_)W;gpCa&;-8F z#SVurod!;Uj=bY4(kR`qC4rgsIXt_EufN8px3IGA-!4^pv|>yrNn5-Bh~h|gEC(Yo zgDv~Bg{_)|PoAHUF84RaQ9PqYMTAmWizpW4%7bh^1iOR~cylY%j87>;lxSDQ4%&<8 ztVtGZv}^4g;m(j%ttn_Jo3_@EYSpndi)vfq%HKPyU119T<`y*0G}4^KAlxN~yos*C zq+t}9BaHStl_&y_~IVcw$p7vmxDX$IAUYW?Aj$_xmk86PgYUTb}8~) zC~2+cmV|(8&f(F0e0dvR+}FaJiaa<%SymFK-wzSxgb+f=|B{iV&q{aFUQD$q`?C>C z|FdR-Qx&{Bg`1C+8Wk|iq-H8HTlVY;U<=sBns-& zN4T>jQuS(veo34*raCf}X|H1PR_E_j(bULy)}&(jXE~w<;BZmcz+z{%2LTjQqv;U4-+xI z!k33LI=eFTg+^scBx+YSMGl=hl2)(L(QAnP*rLb+n$s29)(VQv8a5iTS}%>_7iTp> zuB=rV4GKDwy+LsRQCm60VJe5FX1r~pUCqe>M?4?)HM-k!zwFK+%b}#3TO`XNO50Mt z>}Y-1XXw_i0bz6vK zTxIP&={mEzBNU&VnVz}?ZkLOFQKj(_=@K$MicWwB+xUJRTdkg5InbSx^-;*lfL=X9 z$cZEf{Ez$HYDn!4R#&8r)>ktU19kNVCQqR>gyOX7MrpzpTt)j`TC6Ku9;(4+R8Nf) zG+$tK2@me!`){x?D*>lH>fI5=mO7;|Hr?n>XArKf$fbTZDc5=CJVr*PiPX@j53@9} zALH&eU@menP%TqrB> z3}1gMZkThIFghu&tWw2y&T!Mf*%do%Q5QEVuWMltH!C`AG@eT@y<2y1`+Lm3K&_@@ z4}4FB1NIX5-(C(GuF-|ehjl!Bf-k=n%j@!0=`CmFP?**XGpB0Ddwd+3lPvJ_LBy+( znDu$7L3;lQ?%c)NvTs+J_4bQnAA5VGf=G=4cn`gupBtNaHj8g>W2}I;tC%{4p$ftY z3ta${|QgFXktK5qIq4MSM{~ zqlr_~7^z5GtnyH*G$!5|7VHWQ^cinzD@AEh(PjhND-whI&78CqY&FHy>SQUK+UYql zM?CeToK58XaIF}Nn^N|}%E8{v3)2`IL8%~N#D45auD2Zav8RHr3)tF~^=%1^yETvd zc0O$T_KKNnFYbdB#vtr^E8TsxleQ6SPwJwUUaWYtONOQuHD8jeXEZ(0WpPEJUqN=5 z^$o0U;QlghFJN&ib#=R}zN~(VIhs9i$3JUl=4uu97NlroyNT127%oeMsI@5F9LRzL z_E-+-^pI@@SGomu|| zZ;`1&P-}>SxVSE5%A*oSr%;+fFf4Wats0tGeWH+d_bqjQOhL|Vq5aq-%T3?BW)<%5 z@!x;Nzy2A2`5gBiVNDyGch$9^n{=aIg=|b(rdj#4T6j8(Prt&y{ty26H+*#m^GlKhmhAXPO@BmD znVypf>Fm8N1sE6BQIAm`!N`nE1MiRsrBH?pw!Bjgc5e9TF(Plalfl|=3rqO&JN(CA z@o#^_XW!xJymYjVb0%E>2*Kp*+o&yCuVZ-~iyPQ(N<8(*BuZ1-S8Pb!9%6$OcFhM} z4sk&XgAA+CM&uipS8?ke{_Au6`=|KpH+V2B8Cv=5C3h;3^noan)V1c08|?p#v{vLljcyAl`e~5;Bt_X4WJ*+YT@1_Q+1kS0C;0PM_?OS|r*Col zDV8=w4#8d<)Q`P01b#N=i|4dE(jU%VD5tjTcPM&}MPTqZ8VMF}B@?@M@$lmzO>P>n(9gEmWoR zQy5`*1VgGV8x0k*Pydc8RMOwtHDiMq6wg-h{X8BliY}TxrXv@E)(-_IcV$V{+cM0h z8rB=4CJhZ^RP7^YM2ONDX%646pG3oeM0aw-FNn!t2mU{g@UQpq-w(xmv7)s!@~YaA zcQMG^idi8_r_)+na;=8>ZLBn;GuF@$M#d10X=^bz*iZ*v?3F{L%Ci<2bj`PdqG(o6 zR`JPW{M$YJ@gcsN!;3Ak(YjVcmN$9y24&Zn%2dj&mNeK~u1m&hsfdvg6vp%}Yil&I zE2EuqFxnAA=7Te}hU8Bx#uuCT;yM1~q5OO{i>K>Si#6AlI=ze|6+{?#r)H9KGMyU7 z`{(h&1)Q7Fjs{U`b4B9F_l-_^XM+2&!ijaNxNX7e-d3b5Rebvbe|&;}eTct2$AdL& zG`s&?)=$9%M>Z4kajN!$?XtBS!eSk(O|c_nIgBE79o97~H`d}V!LH>HYthcYEeDN^ zY_#!s4S#!%e|wBSKE*f7vZ*%X-i^o{u}vMM&w{+u+Rh5QEo?M&IW**&OJy;sio?1s zM7iaVt%8DYnwV(I(n!roIc+P*PWx~}8dyGDkyQ|6Plt4B(e@%I1Lo=xLQX0!H3aA; zjal32=^PeTaPKMR*W@tz^bE#N>6x{W7IA25)$dq0&wS)jER`Q5*}tG~mT#9zDW83V z|L@=N#aCEf6T37qzL1D3j_=jIqX;4k67f)a%{%n2tkq3yTM4|tlUXU1GdzK_7o;|b z?Xzh_^6%QWakDuT_|BQ!E#aG(V7ROPTHju+U zZPk8!QRMtsZW^p@N=N!9FK~ZWntPW=aP~BYXXKzE>PRh3xTM1!eYv$gfc**8p(XmL zk!l-rbNJJj(pK=+eF?U0x6_vIf#gLcnH_r6W@xw5N8mew)#`Zk0`n{4eOcHLb7ICW zhZ(iLj22^TYN`KSg?9q&uo$|=glg2;8M3^HzkQ2;`Lkp@SqZl~-ox#4H`66~^xH!2 zcb9kD>|kzL`XJt$!|W>B0cIvKb4qP6z3EmCM&nGoky0*V*Q8z0LpiY$lPt!@I=;Gx zfB6KTe1{h+vMscebnd0wLJ}O!e2^_MOwI4md!)ANxI2qyOL(w=^(G3#xOhsOyLN`# zozPALJIR5xZ|Gi^9q1MzO%w{+m9Q;ofAUI4(_WLktiYq513}i&A>9`0Er|_ZTo+N) zV@yxt;*6ZIEUw_MxA6b`1-Blc)|3O6Z06Wcwc2T}GoQGG5JFA{_Q~Eh-KV;!7Di5@ zpRHqQ8xL2-B@>639>?UQD$J@*IK?1x4VXJx@Vq0&SE$885_2?0(@O}@jfVq#r{Q)i z4e-`#GR$WiXc$ZkOG=9L*wzF)sOGb~;D_@A1jg6Q5M{fC?W%MW{FgiU%LB}9$SHP~ zs_0{nOdrI<8J{@Vl|vvs|K_*E2WjQ-XdT-z#)olc9K{h|Ry1mBOk3Kqxaa#~P1!UW zIT}el(qDa<4ocrFHzEwm*9Db-PE=!rz~>KRDL@r7PJLdeN9IFYuSR+FGq6t|W; zVQ<&aoRve{cdugM0%nF#nHHyAbshDZd{BBt^jaWR|LC}|(=k*g4xkq^L5D%V;E6Y(#!S_`u>B7xYFn6-+9r+E4p4<2K7Dbvq&E~#Z_Ka%uESL@8lWY5_wq?I8I56e2LZ(!jGoJ1nq^!tM+YXT^_Y$G&(Pm$j=wr-ACW{8gYWCxdMjTwKEJ zX)H}>u2D(qVN~lC4sO*N73rVQW(kNhVc8ZnQ+N78@$fzTMNS7weR;@)<6R;%&#fu_;lfPnU7WPK@pD z=c2$@@=x*wa*uBKfo_2I|NJpaM}F=tp|I@VXEYh(TlA&Z-*Uyqy^=+z^H zoM@xdN79Ab)S;stAfRzl>l>17_LniNtYT_dG^EYC#6}r4+jg8pi&rm}Q+sQsj*ZAK zD_gkz0FR$Z_M5B2=1aOIY2)AiP_+Z&b4t9zt>y1pZM>Mrx8F%H-KEo_MsHWKwJoo^ zGw!_FZPM0s)@UCe7KPjD=uejL-Ca?*T^^Dvf)S=Y5`wT_TsYYfd?m~@ywS}HJFew5 zxsTR_n46R8?bZ}uucO&g-89*#<$k5&ha0?mfojuq=m(VR7Va)KF`s-q77%J&hY3En|F0-XkqWp5V!x z*siY6ii=@Pl0Diq_632@S(Fi=eicfEeH|x}>99V4BWl%0&IP7f$ASRrC zuu{atkgTJPZAtihIE(cyk*X6#Rs^_zCWG&rm~`vdggTtXHGKaFGIefw73s&h}B1E+L2_8*__U`bqmYg+j*;MQEGakRNP zlulc$JzS~Eh@~sW;ehg)aZ*ri3-$cj@{f>$m|w}Yj@`jBo`4UN2rn9Q7#i4vl2OM zFC@-A*k#^?(^i3!DfpjFa|yW8T^XpTVR_H6PyKX7X}eS0mLbi&+YKHT5=v>j`i_iG zu3}4NY+sEuTS5-he1Q~G;p+86OrpaPzy5cE4yHp@{l55qFh^qZ{i7v6;niI4z6pkc za+`BlJR06Oug=&m4+tmKP9~n#&76_6b^ny9b9^tI5=F7RtaCmrrc=&~hF&90P(+zVMkG8y~`A z@EXu9%xVtQVU<5hul2zvUALkHFS#3-1~n;h!+GQZ_#r+RDv>cBzQ5U1CeFXxfWO*z zv@bWHQZSXz-AsEA-}gGnv|^grL%(116EQ|uU?_&PlU3Vs8`Ail0``MCW!Nm{G=3Mp?qH7l)- z^`B$=>>$IVHmPS?5EN#!{i_wkFr9n14Yq~@i1)m6jk-+n8F6i zZ)R*<+ue(#&hFn5L=vAi8 z{c#5%!@&pccL#%0^ol@&-i^;ELJCfj)bZk#W2A%kW2Kf!*gz~@Ck#Pkclg_@7o-QEd}OFN0sfLB5Ooo9)w`vuA#4hed$qL zf;r^^Ph#DZuTEv{`3u+4x+Hyc7KSE9I9hLvqleE~H*r5NSIU^7i@Ip-!G5~+ywTr@ z151zoSi6>v^Lm#Hj#sw3wNV4KC+qZq{QKpH@L@k9JgUV+Mx+kGAE6uSHS5(DlmA(1 z4Fo3tM~A`g%5+GJiUakGB-#L3WtTK&4pT3Ym=v-{xyfX&$Ia<7)Zmz_4p;m>bA|OW zBm=m-*BdCNlYUK7J{)6gpM}K^X-~y*)fq1GYU_=z zuG$pBG6`S|%OC#)q*>hxbm;|YC%F#j&9WboG##1-{N0K6we=Bf{sZRdJG(plfI6qa zw$G!W*RJ*t*dw0!5%+kz#k;Xg@#-y`CxKQn9hF2g ztISvw$9D7b-PuijFI|wCD3q1ZigKYdT)RI?kY8kK8vE8f?Gz7X#_{KkGvINB+t%!y z?38ou7mpn6MYz8Nws;l>-*%}2WU$5P&&i3`d#3bHg=_O`c}3#2R?SOVuXebqWBd+n z5C>N13)3yTZ&02=%=hAg_3NK_-m`oVFlJKDQC0&%WTMxDtv-~$a=)D6z{3H0tg$}6 zTy_FMi%x`M+x{7ecXGP^#r6pT2^`F}-!5CM;*+W&+|0bDi%4H#TMVXw$EV6}DL7J~Dib!R8k~NKt|?Z= z0l&yE4;Yf8lRv!-Y;%MPVcHP#hL#DVPzN>euVeQMJQI#<6+Zh$Y^kcy*EpOyszXkHZf7>d@t|Om2YUzK|x^|sFD*b zt=2M98zqNDFlqQA4}IA*>A#%OV9E#rbz$h(h#7rfY;%k8KZYqtC0=pFwC5-kZ@EXf z^x1%o#ebgLaZ6zz&zsEy&UcP~d2jiJZZQ9KXC5Xn$Ncke$a4+TkhSh(K-LipB ze_O{2oa>?=BRxJs@Y2*_lg&(NC*b~00v3+{UZ#7T$7}EXI*Z=Nz%QKk!G-@=zv}T` zb_W6toWfD8aUyk_09>$EO5B=VHL8NbqRn^1Th@9-V*`^**e6bdT6CObQxFN`J!GG2 z8sE9?iR4Q%Fd_@{NklHf*6OB<606uO5N5#g#XuWnO8{T{zRbwkChOmAxtAT?tvqz` z)C&B2_mgnV%tO#p;-%a(%a0JInf3&sjOzPhgG0ZNYK}(|TAgJ%*N!~?`KsQ?Z+aK} z%=6=;M9CF?*K0LkugRvb1ZXuml?HZ0o00dmumDoQ{$8?RwE8$Hb7$Tnz$L6l$1;+7 zPR2;KB5W9e&)@IsJAX=x#2h}~v>Kt%Ae=gWh7S5>0-SP;&p6ySd=j~z*vFBJSk%Px zYH%^skPzs;yG20{?7~ey@{e4whezDp zmYinyEi5GfP>!4*AHjcu!^t>`P2U@e4I0i7-R1LyTFCV`ezRMNbP8ob?Zav!N&**Q z84`r$dH%O~pAT1+-I?CfR{-pCaBgiBw9M(lZw7Mb6=65=hY%l|9FxrfS!c3Mss6}Jyfq;1<)%(8`i$w&c zu_G?nhTf~uhLdb_lmegobh)Ej3kPM;Z}cs;>q@z7q}E={d|qOG6%2F5`N>MmRimL! z7ac`oj=G&sYgn7Sm0dYp^telP7dUq6mE69d2Iv-ov`Yzjo|_NIa#(;aXqrkH4yEM# z9ytQ6>IjS0%`6J^*lB^*g=LaqdlwRqx)zeG14bI4pFJKB@7d(`mqQ*K$8(8g%LL2N z<_bM<|L@F+7U37*_l}XZ)r4Vuq}W|Nr>^Yw-VMAH;J}|AB)e zb?8WdV%aaWO*hX4%|bo3?Dxm4-MpQRq<`^I_)`9r+Eep}^w#Y?y;@3YUnz8)ctHnACu3*;RQd?(S-F4n6ZoaZuI#p z%n8Q|K_njTxmN4usTxy&j%cC{VlMdL4`BIr{VX0;xuseOoj~Tmo)Zj2nFK}CJERS) zw;agDn@}S-9RMF|50Zy{TF(NAR4lqOQ4@tt~tIlvU1&Z<8J*+ zlW?TFh1wJ8thb57KU%+^D84ZZ6z4k%L?0FjA9hw3_d`@jzZ=Ncz9$ds%jy~x?i>y| zo@f0yJGm@U>19L~q>NR%bWMAMs?kMXW*w``grU+y+!A2jO-DObNQQ}?sizSA$Kt)B z(oE_r&5t;*yLFoVo+VKAexRywS|n~tG-2ZDNk|UgMAR#wuS1w7_xQ}RKl zLBn*L)L#I^?vr=@8-Ks>>zxsV`^sTL_73xhpHNU%li}P1pqBgPefPKNsT1D~{}(Dd z;WpcwmBIz?qRxf`wBs3GC3&ioc!d3f+Im2?`Tuslgt|i;D}-2!Q_BDR!h_t{_(xD* z(UiMP$`a)gS!vVeEeew}jq)|fw`b%gxm$`vz!0g<|1UGmeOV8K1TOTNzIXN)t~jJd zd%I5fT&c`6a8JQpUSZ^jNw=%Y`6EB9IU{9dS0 z*d-^_T|VFw8P z0TtzV5N&sqn$QzKQT*(B2$K`W@AI!z5*~)sr@N?k8|6RAs-p{yt?};Bo|y7^;sh60 ze4>I?kR1V||3YOV5M2YvPL%#=GPnnFWua2Qyh`zUp&mj4r842nogLrPpRS2 zeYg^DR6}ca=N;Wj3e4)LH0YK0LOHd)N6yk4RI@hp$mS6`pP zXJSpKGn2?Zjj4>aiyr01B=Fyfy^9v9QqCK47=gWNrvOORue=jZyrpyx9W+$ucJ!ZM zD4sZ2hVs!F^%`T95o6=I>rumR_&ex(X_F=&>bSUZjx{hDT=*#$KK+rPSS9}?|GQ5O z;(cD_mE0ec3)A?6J|b{$HMqS1@~ef?U#Kr#JS=oSr2(ZMzSq{Q%zrS33YVM7UaV?Wb{nE*JkV zml78Kh%M5xp{0oF^U|q7Oc9!4$usaaO0%`5cHcJJ0vE7=$&y7oi=bL)EkQE}8s1Bo z+GFfAUO{eaoSjOXRrUAc*+#R8+iod}^^m@zga#<&mXV=)RH30w1r`4f?PhsD)M1<1 z0GYhP<|#YX5G#8jS7X5}38cIfQ@D=g$+99Orko*cfWvmt$1XwW#Pz(S8e*^QbAQHC zGGr<+E^F*6Ssp#;4rqLB-b7f^*3tqjnboTejkYs{HJzPDCq@>3v<$FLBEwW8ABaxR zWaP9FZZe@#$NS6f>=??amRh|(RO{*w4A|KWh0e}ZS!7(p^gj3 zF(2c%5rMmrbsoydf9d20B+Hw0%<2~gBRQyBYIW6`=+5}C+M|Den$GfV6H_BeYPC^_ z(exQzK`%%v4bRCc{cLq>;n9u;034Pa6IRaGTFRB%DN)UO|Hx0~xgR7+|NWH2<$JnR zg(e?k=nZGqL=27y@5R-1j{F)ybIM$WgYU+mR;MV|)HEx8g0#Ro7Ax|JydbT5bDX7E zqW@2LBU#aRnk;^^4aK<*cAp`xOwDE$V;%pI$`WDohWmF&V0$Hx)MAYm?anESe2j5< z;emM_Q&{Dx%Fje1_T{<}XUpNG)Dz1=Km$8YV_(P*PM$gfm$oqx=<%hqy1=sf7nBrb z@Pq9yMTJrX9xrVkWGw3|6^8@Uimv{ko!zma=8RRetB}8N0GxjZJ<8P!^1$ahZLx~^ z+hb%6$7>k>R~cQNxHCK7YyP*H!9jDbu`11E3DpM5(I-Qs?-K&iDk&={1Lbqrlbbua z+d(Z(_Aq<-(WMLVg5Pc*Q^Hu36HLy7Sdjn;!(8@8PQ~ z?(cpt8*CSI^fPW_2&m!ct=ys2R2-f^3rXtlG%}SmZPgnn z0Qfq=PH>jrR=4jz%?v;`v!qKM1|xg*UC24e&*-t`cTz8Qf^4`28Nq>s`|dIxFyzAc zbS9=aMS)CV(7s;82Cn@RxcGtvrqmI;uTmC<80x7xY0NA(27vD&aSq!hi%v(4nr3#l z>l`a|VLxmWw-R5hG+tNP?D=~RJnHmBfGPHfbg;Z19w?m)tJ$<>TmiF*>6`Z3lJH!U zBbN^Pz2mJN62C=cUVv~jdy2og9Of<^i3|jPfMUH1EdIEQQTP#`{NIXz+ef;9;w0O? z^h6d~kUuVtY4eypG%BB7d#`O8GS4u&kV9u8DYCcU;^gxwm%mT_TefL*=TtYCvqmsT%n0&VxsIi1qMbr}t${?_KvX2J_`Lexdb z0E0ClYqy^9Z$FUk<(?4_4if5*_$Tjgo^jbAYhDqi*qv1D%QsqxgH}{Q!mwm!-4Yx$ z=A5Kb)}MKl9Q(~NFW+=SV0OFU{9(Lg&!iAoga9)kc3@!0LLNBsb)7dPH@7cFDmvBl zanV={g|O;g1x1NAKcCCHEf!eu;QMKQAWK7ma0`}sZv`7%V2^05K17DVg?!4gk>1Tb zxs@#ixmrmrw@ZgP>j(N*p@}12L1L(gDz_>qwGh=6wWu1_gyl#$-7Nki#xq)Z7S{=% z;h_rqA0Oh=Eemo5s}_)joaa8q<{`{r2b7r@VT`!|L^ULG-yq4!v+SnJ0R}odF^N;r zZJt`ayzsyDIXJ%3NsBo1fk3qKh!s&72-|A}gc4S znte|lCzd9k&l_JyXiq-Y_U)M8WGFz=iUoMM%J6pPck$%=t)O>7#r}Ir;^u`cN^VrSA z^7`kmh%%&h4l$s%NN~f#izn>+C892n0v>n^Q_hk8Vh@Op9#)uUN}@aG!*F|nAo68E zd!Gyj`*jky+aW3)&bl<1gd92fIugDgm~^-xrx-a|ES|)=F@88+d4=Uw2cXC&7s|{?dsC{!I=X%CYKz{`?OA5^QUKAu zjhUbN>5-ceGq&sZrB2n&qgf2Ts8_eSE#%lK(%nOM&X~WY6=`-qvO?_NUNLoZ5^xanto2`yI)9jpCy{uvS zy2_gao7m4{s!!PA*o9pS3+3&B1kEf;SR3oFDcKD&kz6Q>ST%^($n6ci`d*a1d+;xJ zeExp=Uo+0GYMK}EjEFi?es z{kx~EU}w=-=$fC-|5O(shjKr)J*uYb1*@?!(9csnxxMWL7_XZ5CD3Y!VL5Ns#Q#r* zA$dEp4-?nCXiA%?7wD_c4v-=2Fw=>v=%M=e| zs!tSWZLrL)u@NSm(40NmRJX$~MNQXcYxP^%%9F7aAAhyv(Pcbbv-XF&3=W=7KBc0r znp7c&+mtpyw>#ye^@@|KnuK=DIc+)dRF5tYSIzmE|R|?EH~a8zm#SKt0CiAALGJYsB9*pLnNwAGW2Hk zK6|W`{mIEvE99@4?jw_>IxEA@HU(2|ug-zv+vh)bJDy*hL~dkDHJB^IozU@;L+h6M zQ_QmT(58MjkX13Q93p832vu;#CHH@ywtQC3U+5R$ioO2U>WqLrWn?LwMNl9&J~tm@ z)ZZC9$|90)nA+k9ilMLI?xG33Qy&8>I*)(##zsG8*FtEi=GgTi^Ajl}8DeN^ zHV}G^mG0KJ{+wHEi%YxNi&HJT9oob+MS_=tJ#>nbt-*|j?Wvl0Ha)uA%elbIhCCT( zQWoodnkquRuC?2|(GnuL5Ln0<`gPT?WmMhA@G4HKYl15BCtCeGiqhq4exZYU#$N&5 z?lP-YU&O8_4*xrJs!39!-=N(Uorj^W)T<4HfS_f{5%6$%-xn*RSATEywzMH%@G(S5 z7Zgm7O^GiD!-HM0a4IN ztK3{5lv>eQMvE!Tt{@2fbFjg$9jGhtTdKp(@HV>6NS80@etl4ac=LeR=r})3Ur5Tv zYK1Edj$LDlZlj0gv)!K%r|8_iiTnPj$91(jReF)cF*O@Qd!_m&ti{G32Im_buZ4j? zot@&+iXG?QU?bXOqOZHqbd|c|+*UveLU_WFAK3UbZb- zy^#lK{{+{PaF(n}=%Gl37m{7UlX>|qMV{NeTw@i7Le}{C{7B2P)a@oF$zRwHsi)L& z9z@blGftOkvIgcGl&#rjG*{dMN2EGlv6%G4tPloK+89)X<(UxRzG+d_VG3)x!`MWL ztF(H53D6KL3IyLf1<1EMEVvX!JbM8o?Q&yV1|{f~-EY?_9!7W{_7t4XzXsasuzcmr zww&Ly1%#*b2PUX^p1{2M>r6Twy8-HHQ_ND+bW->x2X0q={}N~=u_z||Ye>I{#B(Ca zXYm&?|0W;lYU2w*W!gN@xLh}jj;_eZ@xqkJhfSKxQ7&4cQ{7^bSJ<&g6+6&fM~ji9 zV-?qQ*j&aVsxfZ*O%F%x`pj}0h5jkPow?+!PJ@J6x|J;?)8ca4%-0jz94Oy$%X@YoM>=C!VQi6KOgNB?aMkdsyf zhc+B~+VaCo`UPVh6h1<$mKL@CHI4&Y#axNzx7Z&_*eXI+*4SLV#J*q`%vG)aQ>q0W z;|2?J?Zw|0C42FyL9bI0Y}ImGGp)(ljV^s0V!Zrx6+0h#-yz9tM0xv0Saa1#{q<-- zW#pJGrH%k~>1KIUsg#u={Ot{E$~9v;p$wuG5BH*@!3J?{&)Lx}kjlfd*vl#3;Lt!4 zp(VZ;uRbKOf_5&#^wdbHZ%<}s=`EtAL8KC{#h+_l{D#`S04=p;ge=Dznnzgv@ zk(;LAM{RV*VchD4v!C4juoZ=fWo6ue1G7?6I6kp1$o5MHZ^mVgrsecY91K9bjBq^E zS%EPV>T!Y$NnY=KM7sIri4{G(`hyczBZ>q}(puBhCyCL(boW1Tt|u&bE(!|7UYP${ zsk_B75xY6t+_LOO(k#EdQOuWf@|Qc4mu+%g5N)~joan&j*%;0nsGmR&5Y^=y@u80X zq&{`c;evS+HIIG+b-P4QfRFh;;o#3ic}o?##{gVM<99yBV49NDG*e8asQMFA8zCG$ z{2)xPh>g91gWR9Tq0+5W(hq%gz+5=A9<^r={q@;SJ>uv_;Su931Pf}7>E|5}J!VIM zp0wQClg0KD&{4`4=Z?ulFA5(gf`YOAZh(+O;#Nu7R_b9%!a+*wMO~zKs1Vjj!W-G& z9bboIWWoAij+g!t=T(`S^k=HbdV_MfzQ*NHYvUw;|h0hx-U;OVdY)j&;ybmBaH<@=1!QjJe% zuw;>_8!0@EC6j-Q4K^;tQ$Y!0*uK_gGw~rLS4V~&KXY{l%kmk*P7W>wH#V<%H8oG9 zmo$vwpNkHRYsaQ71Bh{ZZyO%Y6fS!(W#dLOj|p8rM=L}wx-ZFRX53^ooRC>tnNc*2 z^eth78=XsoJr4reg}anBQP$AY6MB;_7MY953l&IZsMWS-jgMZbvNo^^V81azhkiPJLza3A)qUrp34=AW(z#}f6HaEeK#CKrA8oYDz*_lb{zsk@9;$`v24=L(C6?^})j0t5ft&K1cK zc@x;HA#44t+NJe_TMrq_-OBY3yVjtjLy&WAPG}tPML_g^KrxsSs6cTrAYdMDd*7R# zaknv*HAdCRe7N&rsM#3<>yf54dSo|7pHm0%o!U8@&+lD-9488eMcZHpQi5;zm>gy8 zRvQ`&cQg}J_aj?Sspf@DzGdMM){$j4xsMsust=)BhQ}J-`PUzKSiLt11hUnH#h{~) zlBGxPWlQ7Xve>j_q`XXOUhc@&V4M+lkyXAxB1p3RL@q1w#~k8^XSb0T{yhg*1-|jA z=(GZdNIS2Z_KpC2q0z372E>C{N=6Jwu!8I)v266gRc&h0%Nm@G$jcYh60k2}>;kvM z$Q#dT8<&{r*>YSuq9_~yNuhh}6^_w!M+!65&Lc-Dmlp)QvEMIcj!Gkq)?O+yepXBz zpLrILG%Z*Ut3Sgznu*gDVIbfT{)QC=#_}o@M$VmCtWQFm|ME~s!jvIX{{!LWxtSOk zMk^YR2R#oM{O_^+J&<=)yss97OQ@doccW1(6iDq#3fnz!Oxk~Lsjz(Co~f|J9jror z+#4yjUIXlnw*>>96j4YE85 zi-Im&@UJ7Af0U0IPIyallTx$YH;Y|oI`m_?qq($z=og^$CwZ6=5F2!dS?oD>cPcwX z2@b9VVe;O=$QJMhv&{F3dDkWHH;~bNIM+*e7#xUGba*yXS@GjnWw|Vl@EsMbR?=Kj zQ%oMWEUF_F9Udw$Z);$)e2hBUuv0v;DWZ$Hb7xm-8lm zZ=J#GX2!Q`^aj(EBZ7KOeV~j1`WCb~+*UJW9a;i3`O~se=?J=egCJ*FxCZ2iLErXSMt=N1Hxx<AH5jEd5nKsuv z9IyR!pW!Dvf_6KC7opu6Ta<7T->f0EqWF62-C!5bW*T^%ip;NErmFZsQa(sGzs^Xh zL=VSwJ)3lhbCsaUEfhN$_-9TBe)G~)7ql^IX`TJH#IH`WLo*)?#&N8Bvs3D`9&c@2 zrcZV13~l-F1+B!$&eT(sIu>c4m3q~&Vhkj28#~YCT;ZMg$c)`~&PW%a5Rdh~A`ezC z=>2GRFk+_UF&moH#@th+{DC@1#8>YMTAAU;O1YL;1N$#eRcCty9USlvCs~{C`x50> z7hxHnwPelmLj+_qG_+~A&ni6c{j5klj$r~3o$IzjJzeqCs(u$Vk+fSVe=7Y9}| zzn@8=b=0C2!g;ggk62~CIjQKvCxyosi{8F`4ToVWC;{PH*F9v~quHwE=N;&@b_6Oc z6czNmh{HX4zJhfR9Epc|pe!U6y4i*d3N0l>MQ@IAJ6xp6UT0d!Qs~&Z;0VC1{64+iwtr0k$=zZ?Sq) zY}H1tNGdAaUvvS#6y%Nbehs)g_W70k2tdL&n~jJ+GgOU3V+%JGF5{m*zN7D?9vZ-U z-$ken2sC)bBR1&kFVrTrj(b}FZ}LZ+1ck%l3-3eNh6x|may$6`@)CC+>zV99-~*n6 zdNpi`DHh$9d&DUDdtDhB?~{K15SEyskuX(O&L#{>PAa(s8sz0ja+QHS94~Oqu?Liw z*-?%JvNxYsNNRKv5B&sLL&mT#A(+12gk4VrsZ9C#Or=M|dpaEK1(gQE8z(g0h2q5Y zb)Fx((x!~r5JnT6lo@<3EuR7Q%=pQ_1bsdhkZCjBcbu+vri9aJ_#<)}%-_czkH)B1-Hpq$kl`>N6Ug4zwt(`JjwOV6(Y7b zZu6EGt?PJ%IzzSnk0!K!Ec=P{&o*e^I{5pyhfVhC+@X?-?O7S_??qM${HzgsyV@rH z(^Li1=F|^#{1!X$^tfI_LL7`i0P7Y_x#tVHGB{Hrb%<4c0*!(`f2_i!wAXKZ*aH-e zierV_o6;3)01CoZuIw{~ZV{0QZ_x6qGAI;PKF?k_evP5a-wwy`Fm(d~EG>YZEsiAz?5LJM-Z)x_g6;dIXyS8GL}hx$@|mI3rWc z)g-qn|yY1;@8W@P4=F5p^rgx}?~T4qls5<9rCP+p>uotCnX-&mIxC=5j|e zSMM0Sg>U%BMqTU-J1I2{Mjj$)pFJ}Cc9ofI%}#J~|EiL^y|&Zpt$#zNe0H0Skz3=n z^Xt`*9hp<`QX2a_a=aY@Siv3)#e(;>^cyh)QyR8;_fn&EWtih+J5or4WY!L=6ErrJ z(;mZQHd>s05$;u7U}#C)@$KCZVoZS{!Gnzl7vu%-0^xc%*pOmBIfaH_&G-$+ILdc8 zQW@?id{{g-a|Cm3Xwsd=cd2wES$ z3Qd08e^|#U8g3+1?UjA4b*1QAIfMB+YCVh`qkZiBjIrZ6wjAkAl_EV{>k>fbBNIG= zv$%nNl=guyZzW^YRdG?4AC%-?j2WXpCA?e_X%bjLoH$`uGX?z!y58a5Ul$*4IXYZE z?MU1Nn`zOzHhpES4x?pfDMI{0W^b22YpCbOYmzPq3FR{;yPG2aM{z+*#O^PI-f=4= zBLxLK{`c;vg)k>CXVh3srTFzY-g;h9^ZOsQ_!9B5c^VEGkUxfKr)88(ihE0zBKTGC zFPHuz=>n@{kdRK)glE2K{taZc>9Z1n*ytyq^VZI9VMQRkG_&G@X8t%38<9(hy!*#)UnTY zr0^DqYUJNG54AeDBJbDQafH4P)E^&W;CDWF4=E$ZLHYSua{kp7DqaDe~G}MAmjH9<_PeWD4i6zARpdKt)HavL(X- ztt`1V9?M;ridq|MepQZB^oz}TF+afQWV>jDsQ)FL_@s2xY#n@MlhX;U^*tBZ*FE<} z((m$*#S?^LIx@X?zT)Av#%W6g*_tzfDPlDLx_EWCzw^P?r==8u$KNov(29 zxJyBfrp^vex`u%;8L~K**y>3^t0PfvK;Xs;0>lbbLjN_)Wo&f<%Zgre*+>B6rA1!k zcb}x1c)hI8cy+tKa=jUN^7yqU+KCn5z=#$f zctI2`W1|*YCXWHo92A`drK-EJvhxj*?9Ovox3Lio`|VX~jTX{+Cs?YX#OK)%N?OhhF>-hm{{E@GHzk6{F_-32YR;|D!jb z_ByW2DRu(}N=h7GKl?um#AbihgWZuWc5D0wn65569=SU=owlyeP*^z85mi^)pvU51 zygexbYgRp5ayq0uyWDL>IQe?2uN-CbGxstUlkewV$T97p+2O)8JVq7L*9V_-NgOLL zxw`6R!QD?`+dN#47bEE)A0(4-InKU!;f*o6#IjsL2_a7}#}1lpZ-&`B|A{0Ly)5C` zPEnOWi<2rikC2MifH;2<>`+GaOA^rt1k@cdhR>DD`$9@qcu?@xlDl)}Su`8=1XH1Yh2bfhU}URDJkI8e|Yk54;(EDJBCIj~DGY|Nra z12p{JXr_ZUu@;HY-Op=_xQwdJE{~6>?s@0}Xsvi2M~% zN#8gJraTs=Emcdp0JL@-krnsWx2M z7I@TsgY_%U9wJG#p90LpmFj!DPutMGcL|NjE}`|n-wA3H1=Bg8GCDsnO@+kro!xUU zP!#5GO!RZW@SFBxKRp);wc%iM*4u@HbJ6mPf=()v3@*FHCPZj2%0=(B`_CHH>pscxa}7F(fUH3g{)U#NN2>`fTbZ+Y+M0p8#MY zx81In{~W-abpRk8BL$Eqc4(%+Z-Qzf=};YV{!&qh6|YG5pQT?Z;y})uM7>f=AeLBy z0^M95tU#8{z+M;--v7G4t;>Qaam7Nm9g`Wk<;ULq<7o_)q4d;wO-z*aeTBy_x*N2N z=P_WtB+(zlQ$RqVWw^}lE7U%!$?q5_kO8PT9+36?NIwcQ^P^sX0 z4^C%OQTC#xNamOMw_BSj2?0TUmhwywW7QQiKPMFIxCq|W zlTog`Y?Kvsla^`lQ|{4J*4EjeP%OFcQUQ$4?2ijgp-}<+7?MBr#3Ja@8cpi69r!#;SsfKm*kq$ zv!0zMLi+Dtl#S)Gi5Ji6$i8umtk*`1bX>=bH4o)5!B#lQwfs2^XZP^m4dD7~om{P;Th3NZyMo4N2k}s6u@vxsZ5>=^$3SP#O(UtotURraf;?*C#IGa5$_-@ zovdQZHK&Ll)J0d2fgb}Y=!AIlSS*W|vPdjZftvDrIh@k`o2EH74|Q>k$adWGUT#@z zpW;K&+Zqf!>KQhUg~Y1^N34sD$b0`-Kx-?eF&aPspSz zO}U!^0496H@>VtmFAsYZ(PjP^Hmyvq3z#>geC4DY>+iHcOJIlv;=>zLC-4{X`PNqB z4Tda2Mq}BTj4Es6$JkzQd|sdRf<~o&?F524*>&@STu6^(DpXA@zA9~+@x%v**|*V7}fBvXi`bgIRT-}ZZ9 zIItXg53 z5zSO6{U{Ec;or|LAaMcRdt;>vM}5v>$%O{d}4MLgsFy9&qu2RZXTIArP z)^f+jWxo1}r&(&LUY>isykb8bbPYApr=@-6m3r#F6^^gZuTSzb3g!xCo%<2NVO5>) zHz4}Cdz3`;tX%D`Bcr<1Y-PUkH6@g(kTp%Aw9(;O*v&S`?HuT0)%TNJ!f_X89r$xB z)zMd)2T{O>Rtfa7D{zy{6oXGEw1GUYtt+A!8(u^;QkXtG0^Y_|tl_va3s{y=omQPS zV&&q9kj1m$bQOp$N2xL&d&0L%xUFA*R25dHZE|kO&o!f$G*jK<0P`a4_@5%JlPbu0 zPR-Qu4YnmcoKZgzccp5BOeC7#S{WZ*{oC8=>)mBi^v@lagt%mfg!G&9Ssmm?lbkfC zM73nxHd+|M8Yx@G*#j3TK^pX=8m=YJd$YnKQ4f*Q6a9xxEWBY34m$z}o5x7ayl$E9n2wbk6T_zTMZZVzsf;SQFcgC$??dZe!bQY)>Xm8aK8kR%2u0 zy!kxegok{omGWYK_I`6N{yW2YLRLt@u+*pMIJ(SwX%qBsdIk*qJ z!h;yY*dDfdk4^gxNm4y{6&hmpmn0hlLVAgoAOh^}ABhxW*VY|T3-RLJtRsWtVX(8d zg0^zWfGn&KC)d+tx%GXUwfd z5xe!h;AR-I)U<126X8Q5GH;jeGMa<#a^oFup?*{*%nv@ttFl-ed7s=i( zQ^uy0YLXR- zaO$me6Gk>M_p9T&H5L@%s)J_QZH?Fb*a!KnhUu(Lcgu#}V6(SNvo@nt>zMrg;j5cK z>qX!g0XmLu=HNA)=QA!eR~k6!@p_>=IpfW2#QzShf+rP~XQeqe3KWRe)4;sdgQY*p znJBK%5YJ=C_#x0!jJU2hG;Lumh{8+Kg?xs?SrYitwf9EH=^}};<~|h+`b%+jx2ut9 zt6FWn*}P|F=Oy1HMGIqXXXUxV@>+Jt-FOBx;P>xte(^z2$spp{~15^Sl z&YNkI{%^2xZ9Vmka{pItb?_k{t5fhWOSaH2dJAT6LaAU+@^FR6-NQ~bJ@Xyxf?k2! z<(2#);o)oZYQ`{qowXmq5O$$YKANb@3}=Vn@25%?KuOB-M`M`Dtn|#q#v7mn89G+)#df;cdoNf<>`S*Slt z`TIRDceGfXJY2%5Bo>WC?aL@Y^~s@d4_vN2;>+Cd-?X;XMgAqFP|X$F#o}tlp#? zED@_X!sjcdA|I^43Xruf%?<8G;h%FcqLZnW!_~`YAB5&%VFfzP{0>zcp=pDLBc?^# zTNPA!3mgIr>jupxdy$DZoYVosO?BheV91~E-xSJgrP?WB zk%J{g6$qr%B`MGe7k4GAIkC+z?`iGVhdewkEwv%Q4=??F>@djx`N%{<*Sl-13u~3^ zNMItYinT?kM|@Jo*1e8N+wpF)=NV(@j1x-8XXKHcS|xiz8&CA{d0B1kYn$4W5J{#G z-J7+eBe0pyT&z#jCl`^|XMsLc-wQK;RS!OG@mi1{eSD|#$bfi(tzuossR@EHUhAa#iIrEDZstC))$Tax_=>b4!rwGdsxlMM)Chm0|;*C@`P7)bANR%)g7 z0h4bLo3c1xUste(w{ki8J^Z!x3cTG_v0sm2eEmq?ZW&&kKgX4_G?O`Xy}pT1DhTKO zcPo;LI{c_?)k#>&!(^j#Z6il#x!{%?)lS0{zF01O!F0{oO?TcaV5Du}p5vC32axUmnY`yZfh zvYtP|^+K~y^_PHGp1v?>Qyq#Hdq?wujTsf69QlQ4#`!cv$3iR_niTbD-&F7O_le|Q z4of+5PU%(&ln4lihY^NU8NEl(=AmNYclzaneiwBW8zJ}bRF3Gs z=U+-@oxLHuIhuBFPb^b?p_~w#UyFBeS?{xCq#76Bcj9sIxk;0K2YIh?|6;)6KHwE$ zvF4-X9JeM;LKrYWyWHF=)O=rEc|C7Bx|e&sslT7e++gVx+1|pEliiaiu^jY9(f$XA z1M+&U%ZDmqL^xvRm1Tz<58SYe_tY>MCH1CMjya0RXpnMdR?r5-p=}EU zYxdzf{rz;^RopRu|MLts%f24PkA@y1#?XDljy88XT|KHHmJRo0mvI+t*Bm0hfchn4 z_<6Av=_N-PhR0)-ST5qz->xFQSVY8v*KD(>jk8I7+w7Ny_Cc{P8aSm+$EdGRkNZ1R zh*l{a$@B?j<{x>||D3zQgdc9=)$lE?aQiHIlP7Va5!*6->T;tPviUOHAj^}S&JLH~ zBlkbHvSDHMW|*UySjjDLG0H?#mSvgCwpQ(p7YUQF-ca@!TK|5CU>n8kIH^DtH{~Q@ zO5Up@?dFqmN1Ym9t=vxL^dVj>V1v0;%&t9JDM(wLo(Nj?G6BWES-(cAS%^+1zxp=r ztrPp1bkN917GtQfeFiSw zoSmC>$RgrG{v}0*LBEKcxkJeUD*vLYUoD{e2gn3T(jm;9;jv&}eV#$FlIIa1C+!?k zxOA5HCJz462j7(0=3F5q9`?U|*eC9o0y_ZfjexCj;fW0$bgGUb$0*3q!jqhv1s|8Y z`!~GE@3wnT!*x^`m8o9;O8waF9QQF=PT8lGM>{wCUsgD*!@}f&40fn-C5h1}7+AMf z)FdPxdV_Lo0$eTc9)DsF+Gpr%aCx2Ac)srX$KqjEcdUPYg-oWlZb}idj0PmVLqxgCjncXAs;wYdr1PVpX)Cjy+6?F z@PHi}{1(&eYM7Ow=#{NjX$a@zU}Mk;#4@YWF1sWJ1VM}#(|MX77HXq@iO-}-AwZDq#Y)6s2W zLk^=|5Vta?4ysj82CIbLB#9 z^;^3o0Qx`XLqyBt5a5L)%o|UTIa*$Jf3Z|{l}S-x3w{lyoJjIS*lCPAMa(-?Iz%XY zD7|NZWmP*KC&R&dRC!>Lma0NScK=ba_r2^7L#}PpLT2JI*`9Oqy`+C0B@y$v-Zmo? z2wi&Vb%vi%14&PQ$B1g=6RhYQ17d%lVLJ}2)|wfTEy1z2&WKQ??a+Ht@wT+9(2^Y~ zWo@?m`-gxeqD|JXDJjpm!H$K?FDydyN-YRk#)_(+s+sIw`hgpHFmEh>nWl<&EW$f_ z!kX|wYTfffEt1xB8Hs97f%-a=IHgYLX01qWy=855gl2)IQYh&Wb*r75WO-7Q9_1tg zY6eHh)*UZ;z%_5rqEFL4Av}r7j@dFrXCr38*#C8tdujtv2p2P>v0T-6d6cT9 znQN8(R>&bYeA-cd>M4NUgtk|&j^#GC;s#I^%|LO~LFb%wL*|{oXnu3{+Dhf}>O196 zr6kmwsL8rj8(G`7u=m`-)$dD5jR!M=KHgpOhcq;ebyP^_M1?Q8SI92M%>2iYOFyRd z)gz}?7*^$!<>SGgh26DG-J{*9oB3-3@uom;xe_sZh9aMYq^7K3SvLZw+JE|u=xJTG zxsBBtrh|~eOnnF==J{D?{Dk?Mun4|za2eH#NK?c3>PD?HXXB~FHz|Uj(~%upuk4CU zyhp=L{Sb$FFU$lgi6!h4Sp3P$laF2P5w#A5YbXJGtLXb~Kw(FwARBL>bSOTWjJIMRJkebyWys`h6Lwf2v?m3E^9}6RDm|{tafZ&% z))ljTmjr2!qq?VU6z`|;eUp&0+*g;x^6JUtqy@+X1j|vTWOOHUCRCAWP2312u694t z+SfANCCy#|EHW19v?{N-@UNfRMEc|8@mVqR7o}>MQKuEOoCZRzXDX{A+k{M5g6wUF zKlw~3IgjS2dbk6h2!OUw0LpwGFruw2LLTgPc%YR#x=xo!TihI9;&dCC{5wPx%gx7c zS-+!w;A^D18;>SbIf5~)OgAO+r8Fo(#$97kYWg6*K6Jk_-Nw-Pi$>|inbPfVj^5KW zFY%Qx(v3x6&vCVVvqh!6KRy`Ttbu!sXEmGa=$vgvXeU;gz(m#}56k}wF44c(p)u{|)dt|Y#H^G_Y-S>^u47>IZ z)CSE_%ZvA~sDsD%o(TK$Mz*Rbv60vu=48c!ACKgC6uWft&54WYV7|Xf=moHnR=BQ2 z2hlE8Io6o7$688L!byUb+ffGCIY+Eh*2~aMICKyNf8fFmC$H1BA!%y9%Wv?LWco^^(e#n?{=@^&W3Hn3TEYCGMTAuNd^;|o~^Be31%rc3Jm9$%%}+ITT9 zQ-zjmndR3{YiXMH?O#~CG7}v9TYe>Bkg1RikP;sjJM~s?4CKd*cL1T^e5%Da)(het zn%`LDF@p8qJ$-yVH>_P*DO5&__}qNIrZYK(%D*tpU_bhG_!V_l#Lr^|%dWYK4UsbX z+*XvA-KC^W#)xL2?aOEC`Sh&o-OiR&d;=&2TNgG`4fH_MF7~zf^|0NB#w`q<)RwH9 zcZZ%J;}yBg_aUo@tQGlEMP8%Y7o!YvL1Fk-stP6X9yMVUP)J@SsZan7xO+G_wn}>R z&h;~9#KgCUM#vY&>|PucDZJJfd?tdWgW*V^cgSZfl`>3mKmjrGz(u-9-@1wPgPsKBbCp9Ihnu7_Ds5U>$qIr6=xz3f;AyE zLR-$5VMWK9#f5->jveU`RJ&yP)YwRz;@-a_iU8^a{YKpNqi~w!tt&x0bENv~cts_7 z#lMpI!_%5de6;iwhYRse?BHwO?Oevn8srj!Y|IrJ##C29+*aQm0@Asl%GG)EjQZ(Q zvUMZr^ezu-?<@hSp(MtsCV&`w#fk{1arhO9r2ha_eHDAP)8`WZ?cuY2_2w&*uQyCy zM_g1Xi(tu$h{llce$W^;^3e^%Zi2h$TX<@f+DpqkC5V0wt^RCDnRi+QVUB%uqCx7h8h{Y%&t+ipagz^Wi zV5J?cg3ns;L#m_mhy1i(zz+fwiQb)H9F62(VEya$%tE8nBYSp{cpTKr-2atFhx9sQNX zoO-iB`3ah)?kqj*G7Y6`M%x{b+SDc)=BE>-E*O&WibF!qM8L}RBZ#U}$wy~0O?72j ziXn}Tu}#K<@vai~jA?VQ!*^Bd>(<8Y*}rbON$z`p0rbr(bIFfXpk+4gKIk2RMIdtR zQ{~eB)bT&sZ?i|J84Y`Uo|oe=Ym$>2zXAO#^Y7E5nX%r37)~W_B^JHb8E+U8+59`k ziN%W@%&xqlahMKkR8UQvt656-Ji^1nyU_9dR9~PrD}a%*`Qtq$abX-iAxo$Tzbr+( za@X_<7d+OEKMC}>fAPC)o$=>QpIa9e!DQNJf#Rd(ZY~3O?Fa<|*(`3#w8te^5=;*M zLWRQJiIr9Aimsi2f&2fOzDfI~o}SAnRLMTrHgI*M(Nx847g+(zT>oWS8CH{WD7;(4%(xs0L1oBN+;=_8rA zaAaRzr8YaC=W0)*8Gu(yI z?}XuXXrt99{Waxagy>c2+GTWpTpr~Q&%0ZX6sVpZ7iXYsy&Xt3e9g{Z>zl>u(CiDM z9J2=^Zk9`Bb0~ z=??|Lyw9xza?5rC!5JV35HN4MiFh~DC_pF6kVM2qxe_C~{9LZvTj~imcVDSmi*u|n z5>{&BDBy2}^p7vzFH+x|V^1#jBHe$r%EWq2JxGTM@zLP7haBg|qZuap-MIUglREP? zjv+HesKx~L*BnO3qN*YupU_O?4(uUB#7eUK%hI$AnewEeVa`_|cmw0cpTR8ulify=LqP&rqPp z$Vs6dg*tTU_wMYczX4-LSvJdHIZWn2NXZc!OK;z!%y`c$Iq9|+QzF&z61y(Lsw>)}$q0t>m>e{X&XMEftJUlHHxkj7^o(M5mkk zQDgr8&SX=o_9RqmlB#~HiY`zer4t;rlt7ggkGKHWU*gM5*7+h+L+Z#IzDQ8-QVsrl zPaly?RUmRgQA^ROuT13HgmpiJfsBSQ8`%#){MNCf5n|3(6QO)75LjZtA?ma#Ya6Cx z1eWYo`**W{FM7r)D}5A!IR5+8Ogbe2yEy~J@S6=&{S9FZ4DIJN+o|&*=A~*P3^yfL z9kPmt$9HgAfxqr2AN!a%`C<4UFDMZg97T(xSD>32pZl-STp*C?z{46Xq8~aBJT(5V zUAX%pQAd&Elx$Uo5@Z5G-+Mqmo7@Vr^7Z{DLVo)NRF!($I8i9 z`T*%d*3(VcfhjaGTQjUFXbLRmsbb7u#B!k=r zIX6leN)Eh4tf4e1SKMO5Oo%KFCcMwcNE*{&+fX|ELsGD$B-fLDI^$Boxg`h=zsWRsI4ue8Vn;OM+vL>DUCMb(c{WhoU`*Jh3Ye@R#Pt2TU*EpH)_bDQ<{2Zg$vQBoX2SX z+MnJ}>I-5(^WTP#F(C-u6tu7dR&obCM|aIF=&7QnO*E~Fzkb^4wdgEQADw!mOsD_d z&#u@i{GzbnSCQ$G zNZBnG^dM#(wk(FYURfAEq;|7xY(nJ1=6Umh%YHkn8+a?hl8I%?Y=;0#iXTmX&^U_{ znDaB|%1aS_B?(C{4LwPWi8ET1{o*(FgW#zzak~>PM(MyZ3j;Jfr>9Y}#S3oZHC%F% zf|>c^=SFP<);uGyPRrTxj-#uUc4BLcwMKWa)q`0u;^^DBKjcRPsW5k8!h~EJi7gz% zvJ}1Z18cw_n^g}2(U`KJ{nc{%Q_9(K2-Y-GwFXOfsEvlxot~_mRW|Fl+w>yZ+@kc& z2kvx+ov%`2D#72go4j>3)aBqzRfmjpij!aTzUinJ`$!%Wv|QsR?YyV(zkA--Je*Fx zR18r}FtA1w%=Bq7HY;1g1%3yROSl)Ui0JKq?qBofh_X;{JQjR^9#S*GrUs=ge;k>% zw>w6uFomP<+AFF+EJe}6r()QJ#g=iiQ@q8jEi73n1f#o7jBV3*c!sW^UB1|@`q1t0 zJCff`znfmzF9g2hq+axWP8jj(46RIaeB_TM65ngdmIJB}y`wqmxyd_k?1cuKn=c(D zd@EL?;04eQl>fK=9J!ZYp9Q#Va;%`%SQ|oVKh|_HNV0n}lDJvv+#AwF51N7+39ZO~ zQlw^!=%@`D0qB8Or!57_`V9_WQ*@HNkp&tf9dbwGN(ow=Q7vMPt74@E;0s!0&a4EC z4UmRVL0HNEa5%=xX8#@>g>qLw_r>OJ!tel`bF$+3jeOEpc95~SvI;iM^LFGeNjL2a z61dfP_N85Yq4Elbn{1zXOA~uMw0}+}up1qJ#-tT{h6`~HyUeBR&!ryDy%0vc7Zl~l z^CBXvx};d=V4-4z`BWBi^@B)RL}N@_83bWG0(_5J73LBF6$_;7}Pzw zSJ6y)vJ>~11p}=G4z{_WQ%KU{;yDC-@lx6NJmyaR%_j&Z%hS>V$JxqYJK@i#5qRO? zwqc&`U*`tW-MOu)e`j(9B8R`_tP6ew5&Uz3_r#G0Dl>^x%AHkKtRKF%P7j_S5_j~* zXx#s!G$)`7i+a^hE_piC!XY{`zJAXqdJ0FkVo0K$h%K;XTEValpmbB2-weqfiM>&E z29ph`ysHI2U%gV;cMkTMwLY7({vO0LwLc6car{s7@%y)*koLVfO1X8v6rO>?0A+!Q z;Uu);G+aQtsZ1U?)$8@$O6O7O|5q)QUmr_GA~JN2STiS zx}sgJQ<^-SSO#s`0xX^I3^p*8)VWZ*wf>MWyjxm^3rX$Ms$^V{v{=+*Dqi73r{i0d zutrxh*1)MR@|4GR`Xkx`({@0v=FeJMLvd&q-gs!Gms#(kmGg@SZh1MF>U7XAc?H0& zbOow}uqWqbCzQ@xn+28+c1L$g@4cQx9CPO}ML$dW$ooD%4$IHGE5a1wqweaB;*1vsC_*l z+n^`!+}IrfO@BK|;Zi(uL)O?Bk(;bN+lO~dZfAw>{t+%BWFvsl1H5rCV3}%Z(Fj2M zU&vxl9|8Y_6PIseFOEcny{$*7r6U&T#^_gTlR1oD!IcS=IqH><+G+y<`!qN z1AL$c#vZl(fxuT3cjaSLX%Hn*}Pp{$4y8aE3LElP^YfxD&BRk}BhfdY9?`>MfAv0I(If;qh! zjrRtV)9Dh8Jl_W*0Ct&#mhXFQ*O8Yn>w=*+^G_x`^$3&ghstnXW1)~t3ce*unV4uX zpYs7asr~LefXUgnt|!>HJCYW^?l;rwqt<}h#W?enKgW=>ECHfmx9WF?n>s2zGXn=x z^cSngQ1g8AX&r)k@M~&%7#rV0-Tfynhs}T&ocF0}b=82XI8v6c^}3*-S(HalH>HP7 z|LxJ{z$bH@J&|>u2>KxX!QP79=-%k2>>;_ioRGH82$@U+ zCgRTKn5DDBu@Q&#ce%WsG<8Xowy+Kslj<%Up=VK~6?+S7w#In#QV_yeL zE9M_Hq>7T7{l59vyw+-W(A&KPM7E{|YDQ<0<5Rvi=>)U`vNA)I<~v=u;4<|<8qDp{ z8I*XTCX$zjTAQG=c!A{u>=Mdc==sJr6Le}~+bhMEqcuxsq*&oUdBPP}eVNOsWHu80 zw<`6(WsVMA3@M3yx?F;xlwrUYlS>0x?dMwm0x87xUrO|#V*MlaB)4v~8@qh${xTYx zj*rVpxPxPDUktNV9G%c{sKt2Ew84A+3=u&lW>d`teV5G(Wl}t`PjjLcL<$DmqmuW{ zPw9H&gs4Q#bFjalSN~v2jwIP{J$vFbbgqcxDn2om#6N4dq@ec-rxSEN6l9^V!I~Rv zt(4xs5pVV!cP&LrQuuuiJ=i|6(Z$dWRiHC>H3LLc8d_w#Clp3*k!@~K`TwgU zm+Ds76|9@&+O4+P^20N^8QSlq^D0^IbzHp%vPx+)0~jHUZ=uMl4Xv1 z-r#!pW9D=|EPftleeKcB-IuOd&{x9v5r_v%K<|0^snmXRF8W^e_}E!qfHl%&h?$3S z1TA034LuX>$;okYTN{0_$M!cAQ6X>V4u#)8`oRm~@BwKusd^+yac@KR@}-5VwIgt! ziQ>-D8+j2QSBP#w!VLXV?0_lV=iAjg2GS8)H~nXuY+-SCnN&w zbkGRIxaSvF!U9DnATU~3?zz=`&u?X?T*ZmOHrL{It5m!wsfp=TOCJTTz zVrOla`3&`yn%3m+>t5otc|-~zFtqz)q}X{NKT>n*Ag#~a+HdX=60h}zh|_lI zp8r(}SwV><>PaBa^vIs4{pX<~a{txOcbU?drD4|0={=TYppV*`jg0BC=6dc5J z8g`+n&oEJF+XBI>YdG9|kv2qxIa5bTZTCp)#nQan0039Nhhu=C62_ZrJ*8Ee&A1`s zzlFkG6Usea5QkE?CDY8;(LgHr^^Czwd_h|yTYeBhoZZ{h@>@h2N!9%ozJM3==zCV} zDbH%&K-9o*2c;XWg7G7^r&GzLUmzoobJ@rg*jz@SLeKCwgv**@^l&GS=X4i3N%jE9 z7pe3P(o|@vEk3K5+~;xFv-S+OD2rM%>|*da_wH7la!g-I8{fVWL-qDFf2@YnVc9N9 zJAwfTl3T_@7F1F~JfNr`;p*>OA?=X50*;f9`YzZ0qDZ;%C3KVxbs9T{my)xr?@h<-*AxXxIp;msabNW}Y${sn}k~HZ9gF;)? zvZ_LvhhMH=!?kb7nO_Dq}64yrdneFnAOE@;i7}A!g=vCO#>W9fM?n zQz{4iYFE=~1s57f`02l^F8*859V|+16UV9#8B6**=oWh5+P_Vh*ZZV#IZTZ!5Hlin zH(K%J(DbvmfG*0|>ZVBLGOD!22dUX%YiOnA`p0&lcp}KPoJ@xxf!Bu4#kM|t|K}-byxYju5?wl&1p zatSQ?hzM(uXzG==*Xqs`ssaygmD?QP@9H9lB${w|O`eKP7)<01Ns??IvCPF<6Ir~U zPm;HE6;Wgw``}r(h%fM<~aL3Cq&zpfcfHradXQctH_j zqh;d#?Y}`wFAZUjtA!Vd{HAgLH+mKgL(wV{&py5J{P?y{st#%C3PzjU+NPau=Gvhv zSroTGm?dc8BTpnHkfS}uix59F@rH>rFzFO9)Gh8x5%qeOW*B0DXXZoArxlgko;hliVT|(qUN;0`M5q%Ti&O@GhL+G|o4A_+7bjFyicp(^>aR zVPdr`M3w^{@ zZXNun91_}#a&&Xyiud*)((9w3;dIKr&c~>Q+Lcm5$aKRFV{rVu-O|la#eQLn;PKDt z7|Y)`1a6cJ5+R2$OEWzkIbKoK$1u~pU((ZuHD_^QD(-&ouJFmng#6u^DHwaXj%!`G z{ElFlBO26~hGRtqP8}ONsU78?Gii6wC!s-KggK@lYXnPZ?fbByW|FC$H9|e{(JU_G7^3G`+&($!4dv59=PRjrvsSdUSy8d@@@_d+{UYC9Q0zA_6spvf zN(X?YZ5l`!l9_IC@UZq){Pnt_^(~tEj3Y;$cj##5EX{QLs7aDa0wpn@+qc81Ah6^& zg;g-skR~d7!Jh7b5FtNmywCO&d_h(w{@W=PJDDD|*PWw&+781Rfltpnf_o1`D@BOy?Jr?((ou)92{ePJy&_o zd%8E$?y`4jqOHwsy=yIZVvj0`eteOsL2Wg=A3WYJ{ZBG~2k{4n{ z$A(Wf6@BbBh6{_=(Q4Q!(WRLSuy4(A#H!6iU-brs4v0-`ydR&clW?sndV&)y(19xT zdFeHLuclu@-TeXlJy51gS8+t_SveE^20fOzI_!-ZZyab^5xfsY>i5p@58|F4=m8!d zC5N1hq}>sg;aQ24HdL5J7NqK9-g&LJ=x-b`4hGvj=>JxA>BPAF#PCX7w>5ViKK!RM z{+H%>gxlpw(&_T3SRPL3+eW9hex+NU&x*=c9+$eP=@%XL z{)4&hN2~o~_2sg}-I_i-BJedq7(xvzDMvifJq|18g8p@NdWO6^X77l3&n;ON!Xpo> zf4Ap%t4zNt5B1|U9$R+&X>7m0_P9UtiHLi|=e9_ix8_CdbbcVt`WMu$HSrS&(rau7 z?YRNg6IgiIA`}$JbI`5e5k^{&9Dx4ujp#*uyb@*b{@5SJqbBZZEY9Cbs8#1d+>hS^K6DM z95ERC;WH6$hv~FjGE?M9qw$;yEuPxzBbi0{?(Ter_*iP!H&W^7AzbnzITu9Qyt{~t z7Azn4>KIWfi*rQ9n?q8?T37GcOD=B&yIwyUti|J8j`l29&2ziLFs(R$085=6yHZkh zw$Poorn!Bo>Q1bDzr?h0OyTO}BJ3kqEmcsdsulo)JG^29mXpSi@i2n>S(J!h8CBH2 zdh~B?oyxI6M2d|G5t%?!4=cMTQ9?|<_R`;H@4TO{A_uNME_9J%T$4OF3@9sAU?&%O zRF}8~r}0)Ll?uO^ZwBuhd9hXue(Tsm6F{>Ot$JhH(advM16nW{DC?>H#1c#LN_j-~k*3xueqH0F%(3J{y zq_$=y@h&9$Q+d|R?tO6NrnY@DCS07KiY<(4`y7Y|A|su^1H27Jo7OVbq2m54^u!0;mmrkw@A>n%%HRvHdMHKp6) z>47t_$8(+Z*SlmbJYcCPt2m8MZ)`lubsdOhkvII=t&nzP?eyIgq*~OzwA+GgmWDHd-FRy6QcqYMyt1?8{CHoa6)AEF?lGJi9z3oz+3Iu?8SuH0{h^tMU zKarCw_g`Po67wf?xeBh0;jk_o8Ba$}bkVw9M!I5Gg}26o0zbfm?9U&NyV}XYixGv+ z2*(T$7s8pDd9ZFAV%V2E0#-r`)^MLk+ZnC2SyKkqqJvP;~WpwOC6BG1%W8*J2C^$T63fB9NVpvmQGuhIlQ(W@fg zK&L{CLokTeOed#<-EMLMKf(N8T^h?IhLKf~C3UG|mCVry$c&s=?CJ=tuL!<8>qyMi zsU;9zA#ZM0<};N94cfGcp*J3y`Cn?oF{M(_H70D|r{l)h*d1!D$@MwCiJ|P!-Lo_^ zrbsnbZX4fkT`}b4DA6T;6*aYnRE1VeRk!6ByX%=!$VE5GunMexw{TY*$7^)c z45-dBy&+_#pupkG;Iq}e4TJb@Z7V36^|NpMMlClEzA+)S z1&L_8kFY1iUyB!~dnc>!V70EdhVFhGGxz%v^_f>}ghpR}wv<~F<>Wz!inNB~c`xO; zcbKKs-J^Z6Js-Of-(d8Gve8ycC6B$yzj0 zYprp!w@vm*BCx6EofBcgM{LqSSGYg8zh1D5Smg#AmxV~g{x|=BOJDF~BE$GRgn>V2 zFu*^kl#T55%r-$@L&SR>i^bbkZ`L0?j;-H`Jw7-vA2?T(dVlZSyoGqUxAV14iZVTC zNJ9vSvQbaV6YjA%e`Hyo=|EBP3EoNV6Mrqv<$bHKJQ8P!D;ax*uD11t3^I~OR1Dt1 zeN)@7r^#Kab-&O{mWN(E?6XB*-`)%ya*pbyAojehjHDm(jY$O5yhZ+>kmI0coi+b9 zxw(g)h5@hFeEeAn-)pko>=|4;zC_A@=7P7US^a4X5;MM>b}1iefZ@2t)S~YpRT8-f zL_0unt#%}5yT%XbqFp>l3(C5iiC`NJR8vDZY!#nAGxk^Nm%&MeZR2`ct*0Q$jMq;e z9RBGY`n2D{nW3ZAH*Dez&AC1-5z}}A;NSj`m^*PN2ZI7jCopu9^X{5gDSW}jd2so+ znZhwHZ4s!c`z==wjP2!A5iQhVW6xv8 zRgc4Vy<;H3f_JIdutwFCDD|s~tC>kjHkb51+tT;kVJ`I65XmW?i8C%gdf@=yJRkHm zP;{}C!)57>&;TslSp|tzW9N`eBFw?yD2fQ*b!(B0Ho&PJSlU18_Kiu;2CYMciAj#W z*`T#)7X0K~A3^eOH?oT;iQY|PO}ZCr&+m4|R=UNYOzcR5^TJZ=6?_m)(M95WXtK^} zfLbDyrJ>XEqMd!9bB$a1*GP$PRyHt|r;SH(mT+o5HoT=RC!c4DkD(s7B-v=gdM=oK z%GYISips?v+&+ncKj(0)cpoY#XsNOIWmZC=cMC0zVxh@O2fmO+Y$Z@le7x7^tjM!I z_U#Fs37@-SNcxO+O8!Zv5PR6!@$>dw^jQB)WtGuuk>PCI|4Rgo-`D4wqj!3Lr220tmBzT z1>C6CzqzAS2J=asVnG*(gUCJHwoihUSkcGq@qdfKjq(j`lu2uQn&aa|)cLhB*bAHu zx%&^Es3}l0c1slG;G9*C*f^l>Y>cRQT2Pz}Oue)a3K@fq3`M1@i70Q38{A5%ou1jA z0aDhxc(YUo9)p)e!WSUQdUQ(-M|3idvOX^_1pPii$tzw1<%5X-f_krWu-Mv z#YqJcjW;J1sJ67V{&ikwhxE|2FMp_T|9?kh-?8Ps_rDMab(3iIVs^VKI)f)&O$&am zHe)V(WX?KV=>X{sm&-&yj$F3fW4fqqg$I>4J}NQSxtv50B!(DYyG?bB>82VQTJ7j` zed@9(sD!oX3?efKGr`v3)}PA1D3BZ%dnrHr@lZ0{&u@2F`Vm%5DtcsCUY$j|FT5?L z%aWGph8BP*n!hu9BqeLpNqw2yB?(>|!A~XnU7}`bk3Z+9X2Xf%Y%rtnxl(wwgfT2; z-FzC;a0wj3aNLY8BUP}1AjuJk^yLtchfHKfjV!@;+Um44CEc~2YG>5aZwy{<l{g%hsFA6ORm_M~j5s%!? zM;AoUxcOKNs-vnmRdvq_=&-h2cup7@F z%$nZ3*3&Bx!&CMoSrq9z>Nuz)?IC_Y8QS;$@FgCxick50>2Ppn;YNHRk;$>G~y1{!?}mh0;UmtubD)NxOKeCo)Qj?y z_+m7>Qk{Eo_TQOz|JB~fitwJ+@CHV$cOyo%RFGks46a;3Rv|;J0R>=*^Kv1ceUP_ZZoG4o|4 zfeQd6HPpXsanQq)7Ab}*V`i@S=}nh(H;Jz34U=r_8{jZmq-nT*2<^3H7?OfMhqdMi z%w2zkueY8&v$>57XmVpkzD>TjdZ%Vg2Fqa(;I^cMS%mS&BmEBvy_*IuabEF^GB9l5 zr^C!FS>CFeg1GYdv_t~bFuiar_^R5Rv+QZ6YG+nfE_<0LgLQ9<0v_BanZ99B@cP|7 z{965Z#~jt*W)$YFvoz`(-k?rvgkRK`sR79w3%OE>`dS2PC+O=?pEMT${4a3ue=G9; zf09ua-lKioA;e=p0n}g_a=O%9pvKD02X)&oO5>JU+JkbUTS?V3rkZNl9@(%?!lyvuC43ToI{Q>HTHfhHci6O=FsC zZ)5rZcIjKuOZ-nx68tRZYd3qIX>Mn!9}fw#;@=-aVp+H3Ikq@gErC2QR6WZCZD-+T)lQQy-?R@aF;&T^(~7CqBMk_!8w{tWo+)zj1x46)Ug$1c%S|v zfaCha^4Mv6GC@x^?g12(M&mpA5AVG@UKGOFwPM`tOUMuIWN&YLOCZ)o`G4%a)mL3h z^d*eDyByrzorAl(LvRZo^gu}9;O_43?iO5vyA#~qB_tpBcK7(*{s+FNe%Yg5_85ED zSgUHSnzQB%!h!B-+jOd$=*vcQ&Y?E631oSu4Eag$?_o+%9A8J``YTT8-5TG^Ug+a3 zB86q94fIEVHc8^V%5`rt3(Qm}M_lx638M8vRxC121htBAqOCMLlOSmVVD4z4flWU| z%0j@a)i0*5d9A^l&av=bBL7Z6T<&;T33fdCV4n&mx9?3U+Fa7gIyV6z?7p`e;WfcG z8~r5Uv5czmZLxRn-{t+^xBlPbpSAS=MDG6^{r@r>`*G&{FhRc;7WYzPlRYdi0=B5{ z6*hS?Ey`<9_NKpuYbP!m4D`FB_=RzvpNsCi zg?FA(E`bKbMT4Xe9U^$hea9~c))7}2S>WW<&Z12(+s>Se#65cX*B08ojhy!N{0^|o zoeZ7y3}Hd(@8fCMAeZWVGtN{-@pbWn^YWPW9QOKV*!yAE*{_BoXd1fzk<|Z$)N1C9 z1ICqK%Xdi&33zn#$j|LB-hHIkdT8!|+p5)SmEymBW~0PYvU@X=@ZG2MZ?Dxh$it)? zTzEi7@YnHhO;X4M0qB2j9_7jb#zVjL&?0I8w?#}1D`40u2jl%7Y1@=r`6f4?G$Xmi z6G)YandZd97B}dJuO;;OlF_{`Li;wNJOG1h*9YPHHwC$WF9%Si*??j3z;74+0uDC< z@LAZ$IeFLvyAWu>=l&wSc7f7%txm|t)1mv@A-q;1Y0Rh2$d_d`VRcNmKL6N0eEbu2 z5uMWmF_7Uem5m%7Xw($_l)vo5L=1l>RxHE#Pk?B>*YeqWdG>H$1zS34AX4>8&=k4ojU`F1wfrN9-EL1qi5N37gP#odK4n2n%Oc`qf zSU6%*`kMvHWTAfZ2e}Lw3`ul5PF6t+!}CTyZ?GVDByKKOa?KQ!B8X|zN4;zCW)B)R$eJBc$W}(Wr*AAHf5Ik0F z-&3t4U(zr72fl_|!|#FE3N6uo8gJc^IT68g4bGdsrJMSP1Q6DY)!ET|eo;k5n^JoA z!NWn*!HYk+!gBu6nE8m^xUMht@2rl_7)~CR{>EMP#Frmy3{ctoGu%3KIk^2X5t%yJ~ z=6_pv|5E=-ns?8eUBfW;-BMs@^KFG?>EkolSO-K)I*Ei{K-sl0-twGQ3C@X+D@O64(-jW}_FVux`Y9*pkR&j}U+93lY z7&3h3s_tMfKm=qKs{d_zUCD=7fH0JLL#NS0!O|r3ZzBDf;0e6i(hxhFUoCE8#n3`7 zVu{OzVU)+O?`nAQlX$an&fi^^-ozz9FW*tsWQ>GEacC{ZMz9^C6eW_CXOAg z15tTFMhG5?OQF1>?1%@P|4!YXZ;xBdGBIbu#QrMU4_m-Dk%1kQ)E!ohBnSDHZ>#nG z-^S2kR#{Q#uC-YU6d}d&=r~M|D8(?SApHnG!N_9_hOq7OzpzH_-yP9-T8`H%Y3%l( zjqQlM-J571iC+1tc<4SRcXX+=Jmi%oHtW_oN19#wmN&CwItrxZA`{eqDn$x-v8 zI39&5FRfr$0Zq6I?J0-HE(M{V9+x^|5?fX$%(qtDvW9!x^PG2vsR5iK`&`0JL%N2` zw#JX(!$nUsszS@w`XYrOADIBqfBRGTuni-ZY;uG>9!_3WvdN;%ec^b6;i|6%N4G0U zu7DV$0tje;*-D8HAM3e=yc6`i;B|)fK;l+5&7$@x7{F0vu6H{qOVwL>eEz$A|R2`9`->q`^#2dC% z4PvMI5b^e~%x~Ycav?o_h@%wj2>z+Y7=d?cJsaW9Dk)nK{YiOu7TcI)&+X)$Tv@(0 zAL;Awf#biN_ocJw@U>(yTa~ok1rE&*(Jk|8WLER0A@F>&=PJ=ODSGDN|NDVnuNOea zOqA1z84TVmAIM@;!7zw~IH8C4KkI*Z}~uK$VLLs|39v zzolMC5RY%3snFXcVyhJ|ov4KRXB9 zsQPBe(pceds8RD}o_XiD{3ui!m^><<{UmtF7?K-#pQJQ2L|;s}>}j7d>f-}Roh;ky zFj+N=HIC0Do$)o~7e4Hkhks)+CU0xm*}{^}U=~QCXhMWzu!{2Ibs!yI$mrsQQv=1n zrh?9j$b@~pMO0U=jc~^GO!{sa6_O82*K`mV3N)h%5QYf)7;p7EHiMz&7%mHTR6|jFWRk! zULKb}=S)|kZ;2cis}Z$^*lp)t9KR!`QP^ltKrKkCfX*`qUc-&v%M(&wkI@@^f`D|s zecGw8lpqPE&JObn>`4`FID~zH2WFLu6B&n7o|&~rR4gZ$h?R;b7>KLn)z>c)btVNX z&AO^t_aT}3|IX_+0Ym8fDJgg`FV<;^uR4||M5xq6H#f4px)R^(P|Iw8gimOMVB%o3>N?f@t2G2wAwAC+6eCj}Nj+;K17xOr+$^jM{LJ@ZhQT_m;WItqz!6e< zFNEz@2bp!uch-OiFZq?fjhIW5fUqdjuzEJtz)4X0$B&MX)pdR^LYo>53R$>$f1+1F zwq%*i2!%NQ)ouB*ZuYn-C@OY4IFKk08?T)2SYVK&i93Ms&?X7({Y@f_5nQfu+^^}W&A5oCj>&W#F zkz@ERi6(^3k3`E&q}boLDmVO#@7hPk#*WoaE%kzE1E>5-7FTGmv+1&0Z6bMk9< z;~!$EHnqF`s1;w%Y(h<4EW#gezx^jZ>;2@U+|CEAxzHo<1hxz(LZ|EFqYGXL~O>2D~6MQ?2jn}^Gbd={>ktb%{GU=hfm-b_f zwyu*5KBTS@ev=T1L(8Zfs-{SiBN&PFs%Cc=y33brktTvoD(rIeSJuABoKPTxiu-aE z8A?I38-pS+-xy z*m{Yzge>4E=RwHTn|g-9S14PWYD=>wF3aawX4(Uf&CBR*r17rDwdLkrvSr z9%Sp6iNHzgK8qOQuZx7liT1=Ore653^F_rGs)QjOWJUT(q=1IXpGFh7u1=6PA6&Xg zTa6Q)Or!pji>yKli4Q5|9Jfyecj<;A1mTsS`oUWlCaS=f%npVZ`teNVy-|_hwYnS} zA(>vI-%Yv$h0g0wjH*K_?y6KOP6dqIT+ZOWB~NrKxNAu6a}r=Xu6^2{XyrQ7CmX_#`xO<)E~`f~HJfo++{xi;upuh|kW0 zHZEQqWu9EY{{jFyIBoW2(%GuqyaJv^^-Ab%72O#*o4Bcc)|Q)~4X%2e0GpwoUg>ck zIql{K48qamconCf-eJ2uh?*H$jcr2L4;6Ao$e#-a#Oyq3%ZTrz8G+OQL9|?NG&Yj{ zYxZuvYV+d3k-pLhV_mp*W)@Em!cx|`NWRNDQszb%V|tIT?ig-J`f#CDxl7^0MkJRb zvS{1bvK%}}LgAor-f!&^iaLD2+~RT_p9j=|4{w0IXx+sCYarv#5GbNBYe64v4ui4rj_ePEw1j}+dqhfYBC4Qqlwb}rX{ zwpu-rB=Z}Dm|OodRC~A&*+MSx^S7t%V}f86MppV{+w+pCB6GV;>tqeW+mx&7-Rd*$ zRIbiZvm`4GqzsMmi!Arrp;c;SgZ!5@Z9u-s>gos4fA;_p1;30)y(0gRDpibE+UK_- z`ZXBl5{IMJ(%QYOpJFmA@p#Zag?lC3L=s)(#EdyMb>zC9gy5=xGo)h=7hQi-n8o!g zp=gIVSEMf9UcYGNFt+P?ln+)?GnAdD z%FjRQGwvQemD_t*h3|c4#gbILWk~j1@OOEB{m}a^n(X%`twq_}D@>zT6Uo;pm9QS; z`7uBA*w?~vD)A2k-qWL2mEm*OhND>FSRI_5>z}_uWO|SxKN{>yvh&wulC>Vk?I`B;Vz27g6M^ z(m{QCdD1b^YWY)q$C_3`Pm)B1c8`BvDY4{fjH<;n1&UJRDsz8ruL2}2=nu}J%V*Sq z)Y*A(GS8gZ+|CV;=P2XijEWeJ*^lLcAVyX7&0pNHSxesJs_&mgLU+W^SHi!(an#2; zzY|%;ztr3{0g;~S)CN|G2?)p+`s}mOB5$Rz5$8(Rczb^JJQbWcRGby%GocY5Q?@s( z{{n}*p09$_B}bz&9k=mL{xH%pCvWfdf(VSH7KdMi2-+jo(aWyOue=vgNkYSzDB5`K z-z$lQwZFc-cG$XmfQY!@XZawD5#H%hjQR^6i?$Cs5q)tewM;stR-6MoZjZ`lA z)`V@pVzL6IFt=+8W#IG?ht-82euF!pgqEWbUC8qW#N>bn;6=s*U77WScpc4n!J?;3 zxI1UWN@mx~pKWzOuvv%3{7*?*%vC%VGW133=h2hov)d|6NN2qTgzpFfl82gy*m!`^#Xo() zqt;T4KUnqGagw!-o_vd`2i&H|S1ykVlS^fbD&)@BIxhrcU{xq%7ZHh%J%^1L`5L-{ z1@1E*>jy+l#;03v#P!tulfEbV{rjjFMB1t{vM^e&W(a6oV^BYSWX}9!%Wjxjz}m{N zAcqo$Q*R=$uKNKG|FoN&>UvIdz7}hQZWK@eD$p9;?m!{SpWX4`(}$kr$k35Tr-lrH zRYPyLNjN_1`@_(6EgbXX`@1X$Cu+Rem-u}R9V8t*6-XWYB51q$9~H6;`IsQB^K0X~ zFv>py)BJ{qePPwzspHd zmj&(7jh$)Cjw>JzQMTi33PhpK(4DV|TdA+-V>vhDadzm4#gmW#!)TXvCDA3Jn_!AV zNeW@Q1UQ1y?EoTS;7|@A_?+Gni*g7w+Rvt@ExCDY9(32I+_5ui&Qv_+GZx#?E{WEWtm=we8zF`H^?S(R>=T`LSF+g;}DHz*g&+%%I| zG72l(Z<($~)ZJCVpJ&|Q90hZty*9!$J;yTuG6w@(74XYwDjyQC+yU_gSBnbx0-h>* z$CIYAe2@1Q4a?iuI%h0$V4`*v8M_NgEwj0N6V`xwsGXS0%VOt{FHL38lKjFE&=y)} z&39SWJMVeB3f-tiaK_9_^eN}^W?Ek*$Vx!+bVMypTWf5eWgR4|e(@{oGRDSnpGX&fh=m2r#QOTcM|lkwz}Shl*pAJh(4)QRIm zP<>|`0T=67lBq|>5P8k}biW;p2_PE?z>K<&=SY$VLF>cuqJ>j4zZXcZOX`15u;E7R zrYU-vvmh4S#L50R3pOnLJ0+MlYW4{|AST8V!My1l^Rf)Zvl+WP%j}*yXEBl{B|4`E zF%+w|=Cf%GA||DzpTS4L5^QnL(-;duSrl1=lanhxH8KdXB@*B@wS9}4#App6eKcYv zrh*3?$-Dss9{m6<(PFJ|gdCx5zYKU^Vpw}xxChzk@>Hf+LIC|sZ|l^#_O?zR0-!|P z5Wo!QgahGTH;;ryT}se~(KpP>*eq0cwzKgN?NAh-*fUE6myoB`St^+1StN=Xfg-Xp z?7T{oRt5b`uJY&wG8?9AR%DlS(WqV@wlPxYT2Rud5Cp26Fu&|)-H%TGuGk;IZ<)kr zoG~32F7QM-pmlm}KbxJWQ~$BoFC|KTZT>@XPDG<5ozz}(Fe^GsO3yJ-A`3POeJT(N zpEHwCRO`uz82QlE6~agSWMx&oDp4E@=i%U^E`XA9i2pdDqx*|CQENfC1$~;VH=a-g zP10pSw7M?$Lb9z1{=Qu-*uQAFfan$3+X{|K{*?di!^wtUao_%wA7ZkC^gY*vdNR-x z(&JMgT(C*LMWF;7n_ksvgC%rD$qK7JpiXSa#of}40T^nd#J`oX#Kf&oa}tFz}db6LLJ;Vpjv z@yHD#fN1ERJpfh1Nj%ZLP5eIpuFFI3+((GibN{BCL8%BK@fx4+Cejc_RL?b&!vh?^ zkby{p!~~{Hd=)yta9sIfk<=#2836iNh=>I zva*WyuD8;Qt5;;2D!?-fW@G1bN3Ox?#3Ae=vlQ78oITUyHo5VlGA0`<0HfVClOrSg zn#pVSWxi;@qF0xH`)Y==r*EG(&7WkZ_r=vN{k~3vYJaY7s+zty^PWbYThY;8+E{+B z_XaZ}rjx`ez=NTlU9UHWwPQB$gElVe5?YVY=1}4$r)o+GPAAQ3i^hI(@d&ng2vo(Z z{;TUQ6;fy8(I+p@C~g@s65`@OARA(vi;m0BrG|_oBC^OiSzV=BAm457OQ5iaJwQ-j z#}}LU5!hdiO!^JFede2|3d2J!=a9u|<7hD*qYhl8NTSUEB!fL7Q65w@w!~`I@)>)1 z7l>B5r^7~N(f8(}a1Q4z+xSn%XA@A(P~0uwal1#rIXBlc0J>2+mpptnR5NNt>`=Nx zyA3rGm+bT=Jy8YwsC~dVEtc+sdAJpo?4YoM-=3AiVZ|m#jB0n3>R-{7dh{_U=7&-J z{MTQ{X!j3hU9tSRsj03PDf!u40AIWpJQM1dMP^S7peIt__`OjS%68%O4p*)D6{IC- z^|@yDOmMl@54rS2=(;gIykX8R>T0kT!!stIE_RW}s8a>WAy3Gjw)*&bf=3T)AYshZ z4W||`EE|*0u2e7c74d;G{ihM>31>SF4F5?w2_^p@Ir`$__+Ros9TluG*4O=A;VmFI z*($5k;kw5oe-tU_0CP?fV(Ax#$NRv$R_pWN9RWe9>qN&bQ6x%qf~1T9_FWg{BTt`) zI4Cvr4@?4f+DYkp_QY-3F6Y#zl7OKvx-ZLtZZZBfvUDieoN2jt6WI^#3)~B#spxyn zsr9~WoSS}ti0f1aHFcX%q@9i63S@y-{*|abL0);eBB2lj14}Dh-4*7QmU_kxO@<6@ zKqj9UFs-=9C8neFd)Yg!Qoo zW_S6+ZxyV1!~Ah-BrQNnHCts-h0K-LMpxPU5GMbP017GdQCo#+ms7;s(c$!{^1E%+ zv{(o)m(Zb8R$!{#{f&#HP&;Mz=roU@S1^YEKgs$r%BGvs!gp_~++)DCQ+e1)?7R*) zCLj^xoV=lW0-b&zP0;ER$52)G9&AsQfNICgILlzhm|e@}nPANnOsPTT?{L7A&p?lq zbikH{;kdHJ#~kf$iB!le2cWNNZ6`CJTfmPmUro2AO_-bF5Xf+f=Lfx}zTp0*79S+D zx#R;gU?y=CKsT4NjX8I7wZoJw-j0-)rBBC8*z*MDHIeLVxY*Mwnr0{jj}aG9%V^M} zh&`&WvBXnX*VOD>tV^|cU5bx?n|08ORndrlYwtdM$vZ0*JfJtpASVY@d|pQR8apSs zR;&e<2e`Ua>pua+XV`+K^?RjKDnz^#+nOvB7!6=EidX{Gn?))tQH3>cJkkkWnL~AD zZcZ4K&@hA(YHoa+tudJ}Uom2RYqhJgl}!s`t%0S;gIuj@ediGz$Z!nGUI#L6QM$ci z%7KbHs0u@|QQLpl!_y&u$_S!}o$5~JH6q|U1_kIfwketxWm8NJk zoRq@obDOZ*4?WiCG1v30*b^!Oq3`z))7Q8=)qOu_br~Z$OqV%JYAqglYbNLDLwF^L zU`>7=w%t4$&|pHuoF$*C;EjD>IJFbx3@Qhv34R}g9#y`zC`I^&dBDT^iH0AU%p0AX zcj_91cvyh#Q-nmIXl{?&Y2$h$_+md^dFZoN5_-WmNvZV%bx^~}iVu#Ky$#=!?FTms zbD=>|k3g+CTv|rnh4{gMtJ<-of;2;hx)S+v>>nDqpRE#Jp9uD+kNJ%#%amK>DZ!WG zC>LgE0+AAE%=yDEfkAZ!X!qnwKB`=%$i<;f)bRS~6CPQH{c0q^$SxYyrU;bRR(0Q5IAyjgc6#bT-HQa>Q=wOC}7nSJc>WVj6J%pl%gd_JXW)=#IGa zAzQ4@h?TF%sm|&wHqDj=Y|F?y=u&%9+-dB>KUTL(l!Dv{;XxeTiKiKfzOz89w)@|> zoX87(aSnaSi}Zu6@OJ4=lxC3srXFjMOhTF#!Z79k`6<0O; zIjdVxo~q`{PUl&D+uV0Y44G;&9?898_d%^e0%!|xX8CcU^YDfz`LQ!vNRfKD$L->= zQL-6@|ATFOjJ%O(5U5d3X^G0dCty&OzSVyYQahHFjXyPaiRAe(1nNUYc&4ndx}Eatr7>D8V>wl+g)al( zW%57FTGv&Ke|4ddPD_aOir;l2o}c&EKq{e{`NfIJE`G(qU0@7it!!WFZ;Kq17@?MB z2)$;MCMri6W>(UbN7r56X2n#hoc^1=o?#N!|J!_k`8j`mpX>2ZE~kXI6uzuU|G3y4 z+x4Vf%DiCE?89g6YpU^-VjQW4ToK2Xs%xK~?h(+(xf~HN#=k-?QT7DM2DybhJC_1Ci!CImLKgX zA@FrJM4{+=&x$J?(48X-z0{%xw)9(6ZUlkSPJQA|?K5cWo~`rME{(e{>cu&Frmo z8hgV6^;TmOKiZ&Kz$=aB6oM0|A{E#Y#xbip%Ser`k2NNqeNWEhS6FN}yfCeui;0yt zqLJ>!9$$DZX*X^T#K7wKFj!$Qd;QMc-+4u7KbG&b8z zC*s}L^@;wVnZ5qYUxXOak~HO$$A0aNw#ecF zhR49iYYI)xW@iy2k;{QRUJ}TKUN`ayLcN3s2r4KDt?w~iRFTATae9T2bR7CNBmPMlxk9Zv&$h~4fD1WCQa)Yiw*36*&k0n)%5~G z^iwsCGYp@V8WD44p`OIBd95_&<{tOAJZG864AdCm=0l+@^EpcchD&ype&ya)A&*?O z3>x|L6sfiSd12J%Bh2A&lxuY*koeIJY_;xJeR+FKTAx7C32m-Cme#Z z(l*M0H{$)b0;{}MU}vYREzESRMMEy)epQ|9Lp&8M9X2SCn&8xk2*IuQwfBHw^Ehx5P7 zi`w7Odr$@nLb1;%4+j0rin4Sui?{C{e<&fX`mQ|~`61S^Y?P!2XD`1-EGv7RClNg1 zbveIjgnVJYE+GM9HT&GYPU&jGkfKK<^Q(lpW#j^Vj_-JyO7i(R@_8ka{||unDep6k zeC*AyN@3J&%W*^V`!506k_6s_Twrfs_$@zV=RaBekA8gLWid9w4;}Bo(zS}bs|CmX za0DKA$@n+@nCGjJm+D2yHtoZpvYD&W`QMqIiqR2jCrP>0@Xto}oLGnzmr?h5*8nr- zbFz_4=e-}lhC5RcP}^N$OQPz#(N`wf*%#j{<|<&(=V$n=pz2x(Mbty!7oe6n#*{V} zK9%@$$tzn1x*~*o&G0}UP6j&Rbe|u#!m)zSG#Y+BHHD^6R5ndZM_<+!LSnX+6+W2y z_MMXufa(>Le7QSPO&#t*qw0~{efWKguEHr)hpa6>s^Y{KwY|MK zZqoLfdX(w_NcEzETG{I^`X-*B9B`ai5t&Y2k(Ap$w`wl+-e)2v-N z?iEyXzRe z;q-lN*5E@{loUDT zM8S_f_xS=+Qj@<*M(pC0-gnWIJiaXP;WEJ2eQ+Q?A+If{1X0=&&^YnLo`|L~MV57f z`6Ys0hp2K4ZKu22ln92u*fnJd1}nxmT{anP*~4oCQ$IDI{(g%hqA~shm@ZS4dA4nG zKo1(`FgEEd!KzXZvVF;N+SQwLl35h47&7B=3`2S%Lq5M4p24w3T3H8^ZT6~OzfjBKZr}}ViiF{D~+A0ql@k_n_M{NLJZrd z&dhUcqzcV(ov?H*Aa1Ob=B&dzr8!|h; zl>HPl+e2`!%$}>bwsYzqXJXqmzhsA}+<26*wSWab|2&A{N#jZ$o4YX_d$oNJW#+~^ zr?{pgg_N{_679`xsb8frC7fP*GucJ%RCt5p?}lHRYI~2-mF`Ho{!kQP8$(0ws?C<7 z2h~`Rk7YFNK6ng2eT{)e);A&&^Bx*U?@}p;=oIeuB%djNBD|ne_Pk>mKSsqByygFD zogai8HLUNI31?hd-HLy1IKTH&tx9z8>2ftP)udq83KTa^Tv?W~a^#uj#e(adUOi@O z*NAI|U)?j|{gt8UPVP$Tiu`s{%Gmb+Z_0!NoxMEsgfgz@*UVm#;U^kX?e+;%{8N0L z7hjp1lHFo#5K&=zwpaUd)!3V(I;NZ>?t%L$LE&s3HF(mxAl1>GQ>yV-DddLPD^q$w z)^0RSUgl@-&p}eB%P2|_2rqiWidHu(ESiEz!o!RjLG#D>h=fL&!BFM8@E^- z8WJ$d*sCx}`=sXZgL9N~6mR>l5uT>z^Qd~4A4)K$iD^6m-q*+Wu~H=mbG)i;KX3O8 zVVM)9{aLPv9DnzxJVw_G(kUayZcNDP*OtQIFrB@)57{!D5%itpJ%XIPc%kO8cbWw~ z^cZrg)igs#6lRGpcF%yiY8S;p#wy%}qiwd{S_jMb-8GZ{;NRI)B{ZxPq$Q2?umHfw zh?m7OeHF?9(r?Z_6h>7cier4GYg{zSwx;>Kkn6KSz7l^W!O@a95*g=|gw06pEFXya zHTSMUu%#*@*$G9KYqnQtRF(sC+geL@^-D32ca|7Z_8^ z2Zc=f_yK=d6~M1z=|$Rx;D~tf>GS3KQ|qlcz`YAVA$ET1 z3$`-)R^pY{?UasfSB%JXE36t?`BV!-BCm8GuZr8AsL*n=Zl2~j z{B0~=gc1BHa4cyCIN{)WWl2$Ea#IkLrs;+wkXR>{zV^F4g$Yugu>7zY{EgCn6^yta zLWup#;OVbSqIg6-{35EyxK?l88H+lQuTUHsSi zGA);Hq|!^V08RufF7F zR~+_B3Mrg2#(W-~F5llv7&x_W6>3m5p6wxA4AC9QHzWyleHK6>Di%besZe2fpW!dD z+QzKQFCB6sB!M5kQ>xgTL9Ij%MskCO$(Tj^Nram5^pI*QAm=sz-OVYA{-<<$#CtR6 zra5lZYWeqg*DsCaU+2Jr_;#@qRJTp3Zlp+Ht$pmNr5X!EcT8HlI(h^)6bex!6m@1Z z?NJ4-O08?1ls-2JEB{>N#Dq79gh7D{(FzdR5YI+?q!;V%t9BL37im!`lASjWO!xm} z6$woy%!3M6J5vvdfp!ssgzf`^vU+UE*yJ9kOn>bnL#t_JD;+sk(g<5p+$6TI z*IQ>nyrJz6l$fwDKJFiok|M+(yu`6`^1#4g7@F+5ntZj$Ln#+n2iC9Zb=z2Sn=z~a zf+9WN{3wM@2xYrx&H9)WR*l`yO3b8U)JU0vN59fG_9@B=66W0#nEC`C@1LW4qUf;_ zYN+MS^k(i^T)kf__`uNbY4qf&+uX>h4pz~DR z%EUsrd0X10x)=xzl2C`3Ib{_-4$Uym3wuuF1o0o|(UTJ1x|OJ(zM#sLCsQ<<(#>iY zucbNhJAEq*D==UVJ&iYpJ;YJE>$1jOBuc(Y#)=~49gl$V9^z3EEx|?sCLf+xhHg-L zP)FJx8Z0wOY7AHX^v!gWMYBv1JomacvyPtOPqxyl$~&KjEQgurY?0xXHd^9BU)O`J z7oIm?m*YPtTkuwhy$A$RpA!uI*_7gZ+TZ{PhSiX^HRd#ydin8%h{A4bPJj1hk%H;4Gch-W$v%m z4eJ|U8iWv+9a9rE5e&LS@r>TvjO70Y%XdXcXYw~>sqSxIp{AR>YCCey=bRGKtJ46J z^R4g!F8(fJcrUXj>b}Z3x6|`)T%hkPyy1ONu&2`Dc{UCC-6}{bEJz~8f9B_iSDYRL zTD-L~$y}jPN7sj4P?Khm~e@bFNXaO zwoQ`~gW2x7WkjbUY_~#B=%q5QsnDx)MD%!APyGa9+Fgd}SbxN`ZD269XQMEyIE0HWS0)(k!A&EpR) z!NP`%Jf%CUJ19WRiC^j>3}en6>a(r>>;_y>J-fqPVO8{0jo!{Zl=HK)@2C~EV@Z#- zNVN`CYcKXRRBS^WBwU(pQ7P1gPrqNudE9>HRGNT8+wmc%ex8*@7jiDakQmgwPsT(~ zQy{$dZqCB)HGkpjY_jVgn19z^Aa!U~D&J?hf?C+U&*Ka79C-Qkka!9vD zwS+kbZ^mmyf~pyFdX;fB(gD`Ksxc%{5ioC&xD{{xU!{x{F0w59g*CrUE1jaC z6_ny@3b~ll4Zq5VMpkMqeinFSM>Hno}x_PyiHd?Zvu%Nx(lcOy`ujnodI{ z>!U^DCIg4GGVU#s?eKL8YzliWxyLZvEZn1l(4_1?bP{n1<5E<`DG95_*8nm+_Z^cg zWr)LNNjw_C%?hA8_w|!kFKKbaKY|w8HvW@5m*xA_l~a*xSS`2`Ca6&E&8iFK`N8Yg z4N-L+QI)ON@7T|?(=KKy!=9f#td%iJT9&sZV*eYLXo(Mlc&xsEaR2P?BT5H*CW>9g z=UQ<5>@MEf6ONIxH~Qtqhgf3CeNS$?Y@s|!Z3@Z^R5#GgXLfkaQ+0f!X3dins1h}{noDU?0_ zy3+NX9wGiAP7`-&9(z&%Vkml3^NBt}X=WHo@L68WWod0_k6>BXNoExGK^Xt0lpWsq z{(Otp-oCqLU?Nr=T#>F8ikBU?HU+OB^J#vZ8-IDfmdL3Ht#MQ_J9`k==X`wcFRl6` z`?S0ka>&Z4^%fp}J$cRP&}~&DoF@k4Yx>B-5j_bKPu4~uP1FucJrBK9{8Xfe4L0wojVnsje6{;V7{le zvE%utE?S~*!Hsly*eU^ukT=uP`uhgbR;IN99#?pX;_B=!?N-fdBR~n_0%>3MOFV-g z)9NS~x`m)v}H1@||iU=uyaW$Q3G^e)u} z2Cp;)wN=$lCHfv@KG!`YR-?i8&w2)^^kOT>Prtw8Qn8-=2kgAf#}p+Gn*S8|!o06j z$TFCz|PB z0-4Y@uPzyuhYmpIib1v^g65*+3<$X-NUf78VHA$E_u6(-EnIouK4AYqBd zDaOwUDg~Y2v$6Rfp&ms;|1ji5vIMKDBD{AMtGOJ5a1ORIx7gc62@Oqt`0AybM%9DT)b9AOj#7A|zf11{l&d^QbG!~M+Xc=L>>4xcFi%G7I1GQR-O5SFzY z4I0*AtAs1NdMs0C$X%h-z)c0_7q+xxcQL+p=^97Cl# zihAROEDKMuxZ{u(cDV9Pab;>~hppOFYT{D#(Dj8MN2unWco&WV?N zAM*#WY1j<3`)n{3?$18uIKxT1+s(^r*(8Y~{6oJLPBaH)@K;ww`lWMwDsQid1B3#p zLhGL(;NC{}a5`=Rb{3MztPQK?NKZfhKvq^6fBQnc8|$)mSnfbO@5!?Hx+ zyWa=$&~Dx7Cx7a~QGlTyck$!vML+#UFFR(WWzKgA8KgNiBIJTYo-Ur6ngwrC6jvMm zTCGoRnnU1uPBKFq*?o{)T%M63`|D8b)m7U@nv}Oig~k^5$Ak^f%t*G8Nk~}V81{0l zS%J9Do!T3q?_N_o7m4>rtCej$szx)D%0uO-40{>>?PbX*F&8LK=|1`)rlo8bQo3I( z!%cN9e+0z28LPh^Y+k0dY4C*@q#)ARuKwvbc4L}#FNB}57?e(x@$(bqPNXv|sBW`i zckAb|JJDF@-b^2<6_abaK0Ub)WI>7kbT9p>lMU(;kvl#R!Da57giv99eAP(0TV#;c zObPS%g2gxSQvITpSK%+6wlyN|TD#RWN5gOFa52D=&#rNe*G7CGFH^=y?9JyPV3rAW z3^11Gb!M)wbv#;!-w?W(R;y<}Zm+g7yA#!vB3k z&s8)sXdb5D*sK*IElHl|7mfZwM^$i0P1s zPGAprDWR(E-4UD2?NTAQ5}k5U8XT9Fc-0Ud6+)iCLvQvU|7d#D>TF|%A_r6JZEBG(?Vb=+@^6!p0mt;pxyT zUtkj9<4=wVdnn^Y7a5aQ=`f_3g8t6fyArcSUVmE(D3K0Jaf%#1FqNg``&nAF9f!0D4M@84#MP!meVb%3vLPsK~;GM_=0s)XCeCTa1gOi|}S zbzGKTu(jhC2ot_TUQ)i6C-F2NhgDf6oklbhk#%26Tl4MuBCF zei+%Hj?e*+FvDeuB%_hi&Za$A?u*832(m}K`;ylEok^G7mzashLd*gA1g3d*u?RBe z|7sV|rs_vM{he^RwSVgA^qR*mXfz&sv$ioKSkJ) zMSY`ucdw%|1b24OauOqidI-}MdginSF|MS;&j2ZS`8i{2cD=~Xe$q(sZ&5G9JPz=dID41~xMDE{LA^a~ zkE#oQ18~9b++VYC31!sUDO!!*VO_wD*`r*$>MOoAdxX4hVwkgL;Ds#g4tStScqP_8 zjFB?S|KWoCfy{~jJ8a3U+g9Y$%W(UIi6RIiU-q)lD-F?3*uQfq4nc}K&qN1AfBi%Z zy2mWPuyZDd|3N%@w|I-0=c*loP?v@J`0dRe>W{r7L@%y;wBM%69>7Z0NC2C#=d*ED zgt^-3qv0NVd%14GNhFQLgF0=rJa(M$9Cw&n@h|k>(J0?s z_$+he0o9Yg2pA?^V5ck&g*lORxXkh$O7;mwgrJ#}@jtOx{6VXk+&!1h5)MUfq)&l1 z|B^;)^$Xq{E$TlowV*Lf{wYu2{j)0W`y|VrP2>JOsmai#dekFD?!oa2O$U6dOfPQA zFw)xK-(@ui1XEEdDwQXwPlC7Qh79~z<|e>T*hnD--(~qw@7SIJ%9YI(t%m(YE>?_U zauN0r%_4L6!~&3PO(ONMBw=`mEDCDgwAITTtG<~|gz=|#Vg|7R^a4FC9!s{3_kj+| zjZKg9ohZWeeBX3&9^!&(Pk0km-I~wjWBim`%L9NyG=tVMSj&8fHedOONgmCStpnC3 z(STo48Gc>tf@6CR|$Wr zGcW6Tn|M$j`9OPfaGmG#(iN8%Di`c@RrUwzMIJbQ&oS}40^j^S?ZGR4fbd4q4gLyVZ8n!L~Sg-|1Y7DaXBEFkj0UZ{BbA9{Kxe zmpxU84K2+Z6Xmffq8Y)L_28j>-2EDfJ1 zLG$eUj0ah(PMt>I5G=sBcO2JmV>kz(!N3)f|NFE<@!0-x0DC4dflDSWrD`fUYI2Sf zGn&eD?&&eH$M`9d&U>e4C|)bUT-$viECHFQmZL0RRVQR&m6z|k@JSx4eC8K%`O*Im z2toJ0gt>BRef9{0@d*<|tO~bvN~r@x=MyYW3s~CwS*{K`p-n=yPT6U-sO|)w@5h9LgITjLkq6bNjOj(4t3{m1c=rOcl zlAC27u7_5?T)V2;T~m6{gkAa zADh}3j$`;FqUvL}{H*Ug!8jz?TaeAreT$n`gb5_-u!_9R7D%^VwY~Bu5oxoaO?t!1 z>F|vDE17?h?sJ_OG#78-{CWK31penV)&kR~p)l%oa=7c|t>YM1gY z1DTzjot&JE!m;{`zxazk{nI}U+%c&R`}e>9{l>xn@|VB-=YRg^@5lce)pE`@NIIi9|EV@Nb?H)UvST5!Zrk2osoSC=uK!I z%L;yItWItz|8Da{qxxl=qbiCxYH*9xiB1jcQ4OHhi^H^t!hX^^mM|G7ldcgF!aQor z>&SI4`W^+pvJ6q?lmgRCnq6T%^d@e&!eC?Bo6$^)Xzc#=`tc#v?k#h|kW0K0kJs_6 ztC$T|dvNkC!CS%>omA&lH_owx;~*cKR}aj|m*6x7!5Y_DnI(8-__c`6t8E5bmS7V{ zbDRx;JkGH={DWJkv!iZtB7xo=Q zU97HQei{Aq$apl~>#m_zqi3xjVse(7V*vXMw`hyN7OQ7T2FWf0WP5U#5 z`_s`*A)#(xHxsHcg1^r*I=&US4jVpedEtT!Kgj|;nJnHy^#@p6#Fb09dI9tIDXrfk zUXv{OgBQOQw8HbSx{SHYI5PmtMc*)WQ`%Xg0i$tvkJ{p-$A>~}^n{kCOGw{qDt#pg ze^t3_q|YpAMu^RQ)9n-AYexqy7jb6}7tSFE42}?-x6M=kj;?O<#FgIj8~dPvS)edX zGCE3zBal22Sq72c%2temuvu`_W#<`*RPEj}kM8mEed`h7y`GRJv%p9Q-Wux5D6Zi0 zEUsR`;(}38(?t7`AxKzvM~DC*3tRInDk*U=mLTWSNzzt#tEemx_U4(h#HDxZ9+mV| zoTf1`QoQ+)C*CA5{k{`Ft~_5Gf$2`By4r9NZ>yvMTBzx>O;G}fi$umAe5!~gn+fB1)u z&9is!-cNn%Q@WNG5m8#wR!H5@FMjchA7mTBwwncun~f@!%2&Vo)h~SE3z51)bzV-N zK5YfzkdAqamn%2Q)yWFvp@T@&wu{&jg$MOFMa6;KlnkU z4p%Hs*V1;hyz#~xCr_T-%Kp^PN+lgz#h0A?&_r2W#c_Siuj0l%oSnmfgPj%R3P@SZ zNDZFAVBQlM_P`s#sc+z3NF0LfQy=bRh&xKyt-gA6x<%Io5h@N+!2%J9Y0S(zGc$0s z=!aUsH%>2bKxH{dG1#VxDZDyrB`hrA%x&DfPt@IQO~by70MErn_kxO8OkFQBhUo(V zCa1tAybLhpCZsE!X=6Ruya%jAL}!wGxj_n0-KpyfICl%ShwcL1P&`twxp7hK>JT%Po| zR_gZnXgC^x%^@3-NKzB6=p2MF8bnjaL2YBrFUY4@BWlu^TBt(R?0Xi5Pf9~5sb5Pn zzYpn6qPmKeHC%dt>+@JF8N#XXo{TJ9t;16U%1G)hjheYyi!+Ln!LDkYkIh(&FPfyh z)IVz3ldlLVtHmnYG?@Gd=D;T+`WglPSERfw~1h{ ze*oDWQp)k-qfuwjy!F`4VH4}uQLn)h#5-l{r&j`Qfq}CVoiqIF5a=Qy$mJM*(<1kR z*5&deGqFk4f%WP0V@WAZd7PGyN)`7PaN{PWFQEiC)-j=2?g?2=>fBd*(*4I2JgIslV?aF zrBZYIncB}bm2+h178w2m7dLLEmFN%TC^j(kDZDuBUC=ez)5`YrpnukGnH}>6d<~@#BRH7ux=pZ+`QeU-`;cbS_(| z?r;D0Z=;i|4HrJi5p4eBP-ZHft^F>$})~i4SosLI{C~N zBfAK@SqbnwYPPnyDrV`pQgBe2-l%h0L^XqJaom(LOn6qEsn+XM#1SGGJZgjS;QWBE zasF*8f)abTL(%2^Rb0J;GjljKhj(t{`jQDt8>xR?0E_0{kO_DximE6zU*&9DKp7~# zb?K(yN0*~-r9(^dHc0RuNHSAKG-GOM$`TDx2jSWhes~RwWn5Xqp%F|EVkl3AC`z+b zP_J;;tOy1=+X}-lUBxw~qD<*of&|aK>6%a^^Jvr9P8kLg|v z=aEmE^GkUK{#EB8*;i@yyzNsNh~jkSOp$g+*-FN>@)L9@h@GTPcT=sa&fId8H?zWr zAV|IH?5j`vB&jQc3EEwYYq&a3wbFO);^aMCT1Lfd$vS#|^aj~@ZGvx~(u1#?9vCsM z&Z^K=Vac!Q>rZ-cm?p^<7%$+JVWcI-GdRD9Yb$tAWgJeajV^O<;Tb9G#Z|pno*D6d zQ`plSq+l=V);+BJ6sIp>-)_A8JU;d!o;!q0Kb3Z5g}h9!&W%Nz1HNn9+88wP1wUYF z^r!82&J-}ykUIP)JrsR2d`9zU%uT%^f3FOfrA&)ZhH4(}8kO{Z@KgNYEu6cI`31_| zlc%m>ky%S_M0!Q!1`ikN0rcqJ>D39=0D>BrZafrWoDmpZxh^S?9QPDNnm{ZU?&Igb z!2kIH{dxZZiluxEy+kFw zb64=v5xn+MiaZP598A{8>(bp%mKCDhYu#6(!JXzCPH)>IB{=*~VspN(DLzVn?lY~-QR5- znayh3U-`;c8fWU;-~RSvEkx<$@BZ%Z!WG{Q2de(*pZ=+_Gk5LUH9b8YkuCIREN!Q3 zW@g6s{b=6PrzM~N{O23rU%YrRYAaQyELT{l! zu?m;c)hB=Tt6zQUr{Dnf)4GoUX* z2yQNoa*{>1M};<1;#t1wY}T!QIs%o_gn(^PmGsT1qRnGWeuDJlS1`ATA6~`xFXGG{ zER|7qxs#F+{Ct=UgN&30LXj?&z<;fd@rI!EXEz z*PVunRX^S>V!1;5|H2X$sx+%Nl;KJSsJ0Dei*y(V$XcgEa|C4YryrDdB&QtlP;!uM z-2!fq_7TCX-a;8C>$tXzGY{~w34BVTS1Ez(^m>7w#~QFV&aoK%aznXJw)ie=mx?`( z7s}CfLiSZ%t-5B*GkG&(7Gx847~z~*U==-1rRvS^Gg+gol68IaHvaEbs_NY@p~Tal zR*;DBWu!GDcljgb%MD)j>*!C0dj_sFZVB@q}(AlHvwDICb-2WeX4ic9v;6bsot zcrv0U;%Q4<)r;ks5gDqW*xUvyfjR6R5vHQE&UXwx z%1IGOopL~}B+a+kRk%x7xsJ*M)XS}q2!4n=YTJE{R`F`6E@EvCl>wxOd383@IRDiFlX$jPgK^*6XZNzW(|xoIa16x0}D5U{Ae{7JI$DN?6Zw=habNM)5Yvx2T6q zqfI6=gz#V|0=`ykK($C76<540-D9FYBw-l;T!Bxn$o&E9M=mI#eD zX4x{R<6ota{^ot0xq?gALkEYUDeJPuK5x4g+Tvm5XIG0TK43jqW>!@_DJbTsQ}f5d zORyE5K-I2}>^Q8p3G0padNkK??R>M6#*>M93jFqO|8~PNyvHZq zhepCBv<>IW!vpByJEHFCPk;K;hRuBAjW?dw%?g*&pY9%bjLachHnl(h{_p?(gZzKP z`ycC2n!^vQ#$dOSLZQ%^n^`uoG*#f7uqdyp2TBUGI=>q?vO{5Q! z$FL~Y_M5Xxgiz<0@R^zmi>Q`}FrvIhF?(CtL(0cv=X$>s;I{@5f&zhxx@;8`=aI_s z0&U`VP?~XqG)oKWpngBI$4MSxB#=FqCc0J^aq}+D-omNdxU<^W*%X`8@;yYksG*{v zYMHng%FD=0lR8khKH&wozAeEL{HV0#aCMV(Rih!l(yH>4=+k_!C4!E~+GVI7V{->^ zjjJq0mzH1iF`CEzZcJy;tJys)B$Jd*)FgT99bL`y;@QjzqBB%=>KIo0YHbCTA`do2 z*cu3)y|XyvSKFY>jGI-Dru3d4(uqO_(&PmuSpx}cogRXEzpenOk5tFna1ST%;M!_v?{Vd?E+dwIYvjuGK(>A~tf91m9&PF+rBL*FcN)z%j?tvcP-+RA z0BpPR=<252)GZgMmSHcOg*SGc6(exQ63d5Kgy|WPdN@~_F*_>@tDDb-WYD}Ws%NMN z3_*CW_tZSO7&j3V6yr#~oQupacFrwICk1VI@hHQMe|0wAK><@H87W*=fD;FIzc+Tam)(*6h8Aky^JW1^7B^b=A6(uyLl7uoF}N^xx4h64d+{VrRvK^JFfi)B~OL8=qXAjlUZF|ZF}(viiLU_ zEgOhGQ?`(2SFvHCD|G>$D<&9WewNnabgY^ z?_s&zQUj7Yu9Vxq>NYk3njk6R;$6IzBIu5(eqM5#`@(eq#W0%!1^}ipy{BU81h0s| zay*n5ad!#l@8ZNw+*qQzh-i>(1w32PN!yn~L#uraI$Iu1o#o@cnl1+0|UWh6wD>KU9hrPXQV zOu>H(%7IYcHOaGj4y6mI65|?l$_>&iFf&+5Y0@m!Dw%~F%XsHL&Mwd(pw~JnOMEvG z*b#nEq;6ER*6O&ni2q4bx_D17dh=A$v$IUyBFvR*q*e2yJ`Q$p9C)a)Tq4#|wE=#9 z7qiRsnIgdT4e!9Hm<4GzbGQ^=buqVs6L)EE?=54nKxC74I)GpeQ1Pq=OJ1FJ5hdrd z)Nrp+<(zqbrLEiknW^c;RlQhZG0v04^UHpczNo_I@{cK=YX*THv^}9cem)Ro6wGtC zqN2AuIQ4U!_&H9!gNs*iXMvnBgn`Z@&`0C5?ekl^+wL#m&0pZ&16;a+qxW#^ICdXE zzL5}7-elF}sl(4#dY*EjivTnd#%m$e_%x%&+tC=l+1$2E^)JT)c*pCvfs4CHk-4z~V}C4oL>J z6F*AN82x;e0aCfT%2Jic&sIygb{iLN;n*=8K7_$Zay5KLQ}oM*I8EPW0)qGGm{>XL zw6f|bE#TT6oH&USC#b4-c@}pUT2@W_(2EAWV%X7cGYgvCA<4Ts!{8_2@hon=^w)88 z(^QtnT+JDJmpW+f66Wquee@S6Ne}0)QZH4_ZJGsNt{dIkragdl{gF#+c>5g9F<(}D z;4qFJ!@e2x_Os{h(TtkU)lJ)JugsB6a2Y5jMJ^XjI;suB)aV*rXDd9|+pCJEE3Bfg z8cts!JzTwmSLX53aqQcNLW-yn-o1>U{14tbNi&G|mZ+-NFewC%U#HMQzK@6fgQx0w zV(~n0c6Ro?d_s5xAwZ&Du7kT)VH}dB1Q*l=#H-@j_!#daX zkVNgD{K=nm?)3{HKKaQ{wwaFCUVBXsM5^x}X;_}DT(R|v&ik~Z+A{2q)Y6x~{N=y- zo4@%W_f`1vHk;{*ptRNY!j`A4jQW1atFOKq#^F@AEc(^Zhd-?zKDPdF`J+GjqcDZr zdaTPfK-9Kh_}bUL_JuEeK`S$#&p(pg?Fq2KHB#%-)mT{CD*a()t}iHV^-bh2$uHf7;TCDBN*!D)Pe317p6K-mFl2*ft@Eb{nsAl0zYcH2xe5R4}9g}qRc;WuK zs-DMnVuuUfzD=WwwIYqCow|b)w{hw&?W@ z|1?s&z|-yEp?kDi!>I>Ys^Q8ij;`X^C=LvvKSQ}?$`{qCi`r+@Ws7-R=!SBNNie4% zRW}{lEq=!kU%ej^SM_3fCPl0V)MRH!JsuJ+V-hBqW3xieB@N`Q8@7;jay=al1FidL zR&jk6KmHk1@N?xVlgVj#Lx;Rr2w4yWNwB8b_;a(o=&QKnNG2j=2Z8w}=~e@n8js z3?>KB-$x~0(nAF(F^GAB)@w1go@CD(wt?fTF5u#MeDBBj$Q z3V!q!e)uMpHp^A>`F5ZO?W9Hv4FK&(4dexSu&Qt>v}9XP>6M9eQ~6A`yKYN+42Z$U zMCwqjggZCz(-Zjak8t)f6-sqMi}V09>lTp`uOIM$>LxgKQTjNjoiKY3^J_?EF)@Jt zpsJ_T;+A>;ud8mV<1bwX?{!FBq__&~8aLnh8!J9)$3Y5n9CRz(Ja>lPSs=Lf{$5NE z(}^zL$LVwUkDuUwPSc&$N8Qpfxo=uJ-Z$yEB1*Yje%#v9V^yS{R<$hp(w1J_klv;- zOQ*|_@6&GC$)Y}A2=PJAb%l%B>*MD0UJhQf1WPpb_7 z-Vt%}&>1x)-}~P8)@%DiAdg#N`he?d;mcE-s^h@-UVH5|wKodzDuc&>&wJ3HoHuD?Q9x5 zy3tkOSeto0sJf$Bk&+ZaNN%vMS224Z|8*Jvbpf;Njhpyd7}h9%9rWe4rCBr`RDfmT zGw$NmW!zcA>?&4Wbfq!L$VT)#969>T;-Q&7NJ}0==LLwAD&AaL!O2_rj|(*5HD5Ah zQC0v2q^ZxN#;n%RLj&4J-L<(@BK^9)iuo$i4kq&G$p!TX7k-`I=mbMHKF@d3SdwF~ zf=WeV6a4OF{OmezuTjkyJj7-Nqk&N<;b46NOoq0rAOz0aCEPEQ9`02rq8!Q6SEH&& zedK$R#kDCBjj%cs!9Nc{w(#%$DXjG_#H#)`~cN58u6ppWPwzQumwLJZ%>I z=uN6McBatV6?7c#RPdmR>uaP9$HsUDgK2&uXww$lcVU?UP0|5dPJT8ZQKYO?dzMKC z1Cq~##8tgmo=K7G(EkK;%dOnQz3Yr%?!aFoGO${OY)ZmzN#D~0mp!m_3aK90IePlu zo5kEET)TqVn^@x~G(D&o4%pYD^w9zP4!+SCakan#mg|_kfvdx~aS69aDRob?Sryb) zQ7$u8sk1@XjO)_tnF8y<$N$gXyT`~;o%!ESRrPgddhRnGdpsWBjcqU{Kyb2&gMWV6 zO*X3yA`u%R#I9IvP&V4-k0>m`Ab2H`jf^%5Vpm%DWfQ?hA+k0JA-f6@B3amdBb*zA z00v_l-|yqOPxo|pRlVmqr|R_S>RWZst*Ynw$by-ko}N0V&Uxyb=X;*#nK%cdXYp4I z+iS!jV5U`G411`sM@U}7!yuUQ+k|r$;eEDFItFJ}LwgjWop_QiGlA(2*eW5yb3F^k zJzIVy5)f<0h`rn-oI47qj-sr4@;vs8=q}bj(I^J)1${uL15Xta)ttqo&bOnN zC*asgICTOp3;;~P+0$_HI2=EPk{-LECNGB&kE3P4D7~i2`A}Lv$&VtUDk$g9ovZm* zN;B#CWSK~Ms)AKrYZ0zJsr`rmsfN3^x|`1^-FP=OtXJQ9>n+`5^oTH^mQk!^a8%Fr zB4^xHvlOxkd(S=hg!Ez2-Q6uGiyEj@Mf{#S`}gm^_uhMj77~d>?z-!)%ty;2pxsL9XyoCu%Sko};+Bm#_5l&oyvz?go#)#pbYc`GBMz&v$XpoFSyao5y zY+G^R5}amx^9yj2OL}5oDcvk~koMOy2>bm&SxQr!Qd| z&oh(QYLQ@%{TKIZx>E=vfNp%JPNI&tjD4Nq!!>W?-8^;$PL5zd46yxm8WoFNmZufv zs-Ey0Bwf_><>?w&DB?o_;tv3TIN#W$ZRgYMnI3@1XKy?f2UH$G&Otv=AhlV3PgW>Hc z%<$rQMb8!63DRQE=C&3{B=LcI?h^dr9k_7W&As5-74e4xloA1oTCHA~I7@Y90^UA? zD%8>ASjjn?MW1z+TU<{DPVouo%VzzM#3dJN@8wZA{63bL%9}+W?i^7Go^f>oDBXMw zB9w})6{e=)y%X@tZ!umi)q>5Ln!(`#d}rch;N;7uSQOvD{yURF5$n)VJW?n6QwC-M z1UKcNeDsMcGR$i{LO|D^9k- zIC~M^Jc7UCj4^fMSOGmZWAUAVRlFh?oq#uw;bL_4Ni1Z|nl_!rAU4rSz$NweGC zn!udk>_aY&!=dA-iskZ(Sr8do?4YKowF*gWWd8bzX*hfm+7fW;BDBSEOEx{jcVEIR zl=P%15z&%>)+Bzy=rkNXjX8=lS#OU8(>6Mn=Vq}Ny8?E{C)4oG322E!XB(U!fg@+J zMY0K+TZ*(vskhwDu3fw2*Kfc5b^u#cJ+8~+-(T7~g=&^FpDL!%V&yMB^2j4QckWyc z@~6+_b=9-Lm6d$vB}k4~s>$y0u(RnhiWRw z1=T5;xjT69pq%($`qGzztm<`kb|#Za@pS`%?3M7ysAU87)L~XBc1=yfnM$Rq$BKz! ztA3tbuoOpd)WF2=eeZjV`KQ$yncy!_Pj;!Ahm&3Z^UpsY@*LNivk}(aJ(jp9qiHyF z0pbzVs}d1B5;v7aAFlA6B%+w^p8a&zL>1?abC_n((h6lGY&*EY7*3#!l6p~m$(r!y z1>6G;UdE15zEv30Qsqzs%CnH-#+!i| z$LVl*3?_1pye(ux-Ss+Zfu9kU5@sXX$c(^|3Hbjm;5}`R@j&M+%<%DVxCv>+IA?v; zVxTU_etKpab9i3l!=Nw~TodBbFb+7RXsHo+~qHICdPhibIFF8=6Ps<=r%r zddY)9oD0KZtx@Ob8T;MKBXI5lR&AAa)GS`&IMBuvyyoxRMfmj_aN-PRc8(Z4Cw|T? zyQP&$7B7UWXp|=&wDD-kl`&K|C#H&=^;UpaTWUXu6LxH&FftCme+SNAhK@EKVaAV) z<@4@%&3I(mWG2*Z!9--w!f;DwEhePPn2p$?xbyG`O+y}59ptukM zYuD%`{Q6xyFw3@8(-}N8I+-qX7aKyXrC8e8nC6$e@4j0`Gy5qBw}h~JEk~N4Kz5&a@;#P{Se`(=O0jEd5>A+#M7o4? z$}_DhiE48%ms{zXs73~}2SioOu@4g%20(&+;0BYylLTmhSnKR+y6#VWq zMsBsooh&$6(@EOtg!e`8zj+%bXW`N`oZ}H%>ciFyYIqjNI)7SWACa|Do;-L8CAzja z?virc=O8L(>7ck5c*t6eA0wVMQT{wX3FBG5Z*tuWPMP+=JFPLtl~v>oV8h^28eTgG zrzfBzfsuCTUa*`(dX9U+b_!)hcWqtZK@^S@s3H zr*Dp+xYc}R`2OAQ{KX>c#iWR6dV=|bf~$H>!8q_(98(`*);2i4CZ3Zn1z0D=172R3YvCN4l~wd zHXVL@5i_!O#Ib26AMK`lVpxMBuVYl_(S8<;&!Utyk}g<9oJZWnWCnglh})z(#~5Q7 zclkW z7S3P5TEuJ#rRnZs!mi?*agzdTg(t4b^Uk5aUN0y*L0)XxO7NUqLXC}MP}=*aoy8@% zi_1z6X?f~O?T1|6TUIL2{a_c?KynQ+H4VozaO#{>yc#s+ZWf;%l4C#|4l?)wPs{%GR0lSaZ>dSk)ZOrt}O{LGk6SW{XZX3`9tuoK05XErS|5 z#;*U;6^y)$oaC)=Wjwpe@h8!Hx-(4hSQ)nod9@$-pD?umn+5lLoSfQ|(_G^`uN-ku zjhj}c6Tm%|sJZ+3e9lR}3hHJoz+=LarM%rHPNm`4X{2zASnGC}i9v-fuJ*cq=wr#)s^2_0Btm=zo*Z=tAk1y4gsDeaeLf45CC+@i84iPXB z?p$8}s1iupa9zEH6P)Ib&xUwZ$>mdulJUkHZU1>w}^%X|g0d!kXxidbr(UiIinH3?_UaJ@=+=F}o5!1PS3vbjbVZv1;9 zgMa+vAJ;V+OY?AU-n_Y{uvc1Wb}r0wMPAEx(s;-k-sp2kqeJ%i6(b6 zS=`5+=NVsg#c6K^+|434hT!ZDM$<5vfg{{651Kb+T# zRdyBb1t2P6w`W>IGxrIw!4?G2r%tWjkg;M&=3sn;DAvp!S3N-chWSDdkSX)@Z@A%x5H&%azl?3$wk?x;cktlBzP`S)KT4%iKl;&+*ktjf zE?oSeA~rqcUtfIjV}2>MNH4zl;?bi=wMH|=RWBo|qH0lr9pS3hHIGL|RK*U;g#gwS zJ*n0yWIXubgU>wkOkKm$GpClMRHRJ5xtK?DN+W~W(xrhT6rMbsz=b_wuflAPH75_J z;Ytx~ubuX{Zg%!lykkX%{IJM58n2{ffFsDwvi(B+9w$+b3r`HYu|BpPvgC52rh~3I zb;7b1WE$uPzB4trre{K!hJoU3;`?h?wauH@742Sshss8+~O8oE)Sxr15sL(am&lSG}6sz(sTmT@kK zWr_&I5pi!JGD^;YINMorS##@(kcO%hvXk4+vljF^+rk&zpoKhG$5!&wYR>Aj-WG{? zHCt9})|RPR&hQgBH*&zk4$r^4#cHWDZ8F2=}D2d zkE6c96N*$RrQVSvN2J{KxzByB%He&rsNOtg7+VB7Iy&yX_g>}xFM%&oA`_;S4R)y% zKqm5AdbB-u$M3xJPImhm2)q!H2fD7?9G@QcMJAC*OifMsFM8)E3#jbF@$vEP+qa7_ z2$3Z7Ti^Ot&G`bw5psd;x=>U7G(aR0@kn~?p0>BQ%ZVfMa6bL?)9T1s{EWM7ZpyEN zS%tI|5&2pgKrE`PNnwx8j@Mp$?GvB)M8ki`G|dwyPWXA}?|=V$Hp|4{UB7;P^)%57 zuv)C0V%MW}%~t_Mk78Hz@78>-F^l*3|p!Ef8o;QX@*V-RTd1J>YRz*26I|bArJpb;d zW*HBfdY9`DKGp4lcft-^aF+11iHp~v9<$)~nO~xBRqpESFZH=fu+@bf{cuf zbM_iO9JN)LW1d49eo-nj)bumndh4yZTu${@e)-E^7OwWX>Lht04`hd^-uzOXSpD@% z<>-P|IjSu!zi) zW3qIDHx^hk7ufam^z^t7-USaTsu>ksybvfaMayKi;?q2a?f{3q=kNaR@3w5&!oFCX zp8VO*el{~R<1k1SrO+o3i z_-I)gt2V02ii@U{zx2{ep64=q#+7!pI6TBIkM&9Q*6rA_!!u^YU?}_OWDhAe?1R41 z7WLAkya&}Y8`(wsN>%&_(SAy#UIeYMpQ=NYQcAj*rJoQsH;d&exZ<>{4Ssu|$i3ih zNHsf@N%m~%GlV+3!sBj|mFMgbCmSr93wA@3%K~Wdx0ikx6fWMbTdNihI;->(!qyKa zguWF^4XJtr;Zi8dD%VW@VstcF^HwdS7b=5Pw4QKdv*gmbzJ5>&nTF0P%WJmXAt9=% zX|?*n3C-l26FdA5B+ro_5xyz?1y!1+75}WX#nVC4 z3Y7qC-{;8GCA&o{wet9pH*UZE_J%+I_19mo@>8|;j9~NT&1KJnA3S(aMnbf;wcUL4 z&1F4n!?R|@!(t-E(b9Z?9!XD)6_b^9Sf6=&!tyHdXR#V;ljiL&>L4U(F{XQiV{QkS@>EY`~FteH%PwX5OY{_0o1df$EbRrehW zts=a3MwJSZ$)qQeiH#iAeA%~E0uV#>5VNnk>Z)&j>szehvPGpetJL?G4XjyE)GDO@ zk8x9@y8S1Zl?OrOUG^#Lz4FQ{^HtrgVbnepA-M}Z7)p=YRLYyJDvl!8 zdZk668YZqje4&D{7dD@RTRl#%u)R`4`0OxmA_}e63hk|V0=TI8P3c}JZlf>v0u;{8 z+YtWo;Mrd zl609l(BHE~R&us^{a(`yeW07pla-5_YEIL9=eq~@%!r873bkR?b*CRRtN zV;0VlHOJI2+@fMa*|PnN0X`$dNCu5rNg zOeT|w#bUzCQ`Q;e$@};6%P&{?G_|CYq^#TM#EBE9PoEZkEA}h*-g|FZH<>JE1EH!B zH9S20?6c2mT2RsjOFhifhn}*3o&g|E>mNCCcw1@N>^S_g8=QS8{^9@WKlp{pd%<*Q~1>2(R=RhTklnj?ujdnFrAg20Wdmy#nwxIANeL_h zmK_Z2vHReI4=&ZiV!pOmPVEv=R0-Uzbfqn}DLf1z-$WST?4)$fuiIMaSlIR^%B6($ z!1&>5|7w=)Mm<{gQk6eg7&nWU1$rGah8z!}b~kCR1u&#3X^x5l0h zKKeoCe^P4c*-bVb{FnvhsS&yooAV8+dITY#6qb__LNi?>QkSY{hd!yX><3#zF~)`l zRXrzQG3@;yl3iF%twgA)>S=CLktRDQ!jl7-ItDEIRP{g>@otXOL$)hX`<@O(W1+<3c;X56T$aYOrhe=vGT0M@=f@;g7m)l+ zld^G{)i0@m!wP=F^lQR8kUd7l^)ygW*8k$6G|Mz=^0n7qTlW1jv9^3?&CtoRrx5(B zvw`1V*hSx#*;2_^y(ZXIbJw$`S8H>%xQDC|mzo7nbJ-aE!gQHk0g%W)Z1G|YNJ!d+A>yY=|4>D$P8J`;`RgWOtHI@`1 z>|#q%(+U?oU7A3~;GspVJ^2!&>8C3+Xp-<}+peP@)M3zMp-9e}+uzh^zJ6e>$eTsw z*|p_q7Wj0~pg{}SH2n}5Ow;`JuYWCkRb}gA`49f6k{}Vv345wAP&U>|jrj4OesjsG znZ#`I5z3sW&Xy;;sn0}e5?ObuGy$Ve_U_#)otW$bKK}8KS9!C?eObz;O`GZs*HZ&x zJfgz>{rg3+qO!l}Nhx{a#0mM%0|yQqJa|xkR3s8Pdh}?`80j2p=BNMsZ?r71YI>+`rNs53;pNjv7V)f3|7xEl!`{4s)1_Y_`ZGncJ10F z9%MD;K%b~-X4SfwLE!GY?-oX8UT#6@i5zB-neCF_mL^JtdJ{TB^rGfR0Jz*DohiSP=MwPvG(LN&l z&mN&bXjUEWg{En{cYkgB-B)_I)lKGR;p1T$_JfE4FpJMjS933DO*=J2EOK3D0h-n8 z=zltB#nWL~dznbpBM8sarNmjKJ#7=|Ifmfq+24c8MFR`$+T@qr50KYM3)cehk} z-h1yo|8C7HCFnf4aFb;! z&|OfN$rbmG-QUv4W;JlUl)ZcR$`Rsu)-)fir7yhj!mobyt9emXnYkMr9F!xOJqOw3 zQfJVC0|(giTE5|lC!YA?7r)q8xjSYPzWwcQfBy5IuVPE*CAw}Q`kaj@(Jc#_^O=og z(F5#BSLI}Yr{Z8;@xf9BpGRRn7<+9W`-NuPSJu~i?svbcQ1VIo)=#VjUkx1mOyYBM1c+>=pRdQffN|1Q?@Zm%vA?xS; z{O3QHZtn5%@q-5sO4p$$k)s+(#xC!kd+rH%HJ^uLP4?OUIFao6^w(Jetw{JgkS4&mK`-&woML2B?`d)e{n3y|!Yd ziAL6jAP9mW2nnfr1VIo4K@cHBjhXY5)*BukUcGv?_}Y{4%!8UOb98jHtE)@?X!q{j zGUCl6?5QcOJhf^bd+agk#haR%s>^-$2xl?-u(T+Yh^Bh_>8GWtH!u0hmLVRY=wJTj zU;g%Q|5koGHZ~^XC*~_$FOf*FAy-MtsxrDZZ{A#bi`l?f^3b|@#}lb3llQ8LQbQre zH%w_%u2kSAO?m>$bcXxMpcXyZK?k(<8+#QO$y9O;*++7Mh zp}+m#`zyQ$Zw_+hfQuxPnS0i(nOV0sRio>K#80KGrkIp4W3P7G;S_zkm(rJu?(SQK zq)mJFDuDMPiI!bE?Ol-6Q%1ycIMuivw?y|h(dUDTybxw{|BkaXLyNI#53 zit$lAf7q!mN%h*i{Eb54>TVY-y;`Y8hMB-Q{_V?Zyh~znM z74d!r`sez;dF8v+5oPm#)AsH$tIGHYmvWuIxu6XrtkQN~>J5Dmd*LmRZ_B6x)Ef#E z#*Ct%q+9_is_{`Rg$1KmFXNcG^(u>gPMe<2s+m2Uz3YD2xR6%pl-#t@N-6JNO7><= z^Gx#0UrSz0UkKn$7oRgpC-QsT4#o0VG%hkbDMWiqud&?ELyZ*W2hkuY<=!!0f9PW_ zV2a)Cw5w57sKDbMfix>nB$vz5!z-p4a2H_);i4Ar?bn*CiY3*t*gOgGzDCRZ`~KT{ zq{`U;UJ21MTmP6CN&rwPpiS{ybf|QduzpG1>v}m)_1pxJHi16pza(0J z@s$;H{r$rr)2k42^6e(`w<^u?q8gp~6{^?hLjRFa;;pG>GNoE^WeMz`K4fcBbe|Nt zP>lO>x>eB` zlQrm@(0-sw)Q*v+s=1BkvTDAOAo|ugCyV;s zg}S);`CEYocqrh60l|6dO*RG*T6`Z&H!g8{Y27cd}C# z;Gb`&CPayRKg*eOaO4C=_#HSoMVvv^&bLDmqOm{D0Jeoh3)#c=)4@3f#fFYz<+2Y< ze6uq^i)jvds!&^<_@I4X2&L}Rjq|BSPsnnWltfKSaBLf%~e+kcbp{@ zTYFB$Ai{Ac_=H;+i+1RL%jVyvaUG}3is7dHS zZzpT$E8XbSb19c2)R*k#i-bc%LyJ~8blxpN?}>oS8bq9eS$K^4?`)#Q1sG_qFp^QF zYRby>QJ<`pC7+c)9xe*EEW|9nteb-~C5 zE}j$QhX2kYP>~2v|;m5&WA(svjCcd3l;k08q;AVRLBnkHwxT1z|Sv?k`a z)D2E8z4)s#g>Tm9UYv{MQBS^ppu@1H1|_NfJE`*th@AY`V5Tj^P+%=g6KVlb8Uq6% z(>Pty<+aDK$daOY88LDB`tJRK-u&nZK{S>0uK0YUidH{*ycvR5!l5vE7BhEnV~vF9 z_Q!^TRmn^p9go(rSK%@(&Hnh_c^w#Jsl4mYKTUb4 znk)wRN`3_IKz&fu4%&aV{aUVIN8h#p$`sTa^B?_bRRQ8%pWiR~E5Feq3-?+UcaOh{ z<(&QQ_V@N~yLD5jp88r!;^F2NbbOg@GO+?$A{gz-3H3iNE1?|^+EFzWR!II0|E2QQ z10<|alTxf|whk)S3Z`jdk#bARYw^&IM?1CHNrnD^!XfczgdQ-Op{vz?qiqOu^M*=d z*YfD-s3q%ISomX#-nPNtL4%0V9jEWnmXcRZU*%Ot^=a&*_LZq;TmnmJ)E8MALP`}u z`f7Nk!?c*02=TnvBcq9wC$DjHFEb?6R#%uP2oHcq_uw&ksVd)Vl!WqCWDeyjX3sDg5+S{A>ClYAZ>^={bp z1h0@Q;eN46$D{NMbv(MUc2NqX2<`q?(+79Z`wKf?jDqzm91v$9e(pK%ECP* zU`_lpvucnk-#@R8$Wvjj1A?%Td^Y!&F8LzmkI6~ibQBqZ;Y4C4&@A=M1PhYx^yA~> zhR1I`yz8CO#a}aeOv7mn#ANk>ftm2KUDe!gqO&z0A%6Utosr?a6$m3LFEd@$eN>#e z?>ztKptLVoH5jz^n)~fu%gr)5R`N22RIM;ZloSNkgiSOCtA)HxP>A?-kKN1?iz+H? zyuGz`Yhe?vJHq9Vw%@ZKzN$w@etk*)&^k)#Iy^8B#b6%-M%Zblzq~zkcf(>oZCv}^4$pJ(!{{>(4i2aHZRwF~-Vr&Mh^zFj!-GEwi3ZRua-xE;2g656X)Or+csJInWcs