From b24dd72ae00188024288db061aa1c4e363c06a39 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 19:48:13 -0400 Subject: [PATCH] Remove redundant cursor styling --- another_todo.txt | 4 + clio-ai/real_time/.gitignore | 10 + clio-ai/real_time/.python-version | 1 + clio-ai/real_time/README.md | 0 clio-ai/real_time/collect_samples.sh | 154 +++ clio-ai/real_time/flake.lock | 61 + clio-ai/real_time/flake.nix | 60 + clio-ai/real_time/listener.py | 562 ++++++++ clio-ai/real_time/main.py | 6 + clio-ai/real_time/pyproject.toml | 16 + clio-ai/real_time/run_listener.sh | 3 + clio-ai/real_time/test_corsair_mic.sh | 71 + clio-ai/real_time/train_classifier.py | 191 +++ clio-ai/real_time/uv.lock | 1104 ++++++++++++++++ clio-ai/transcription/.gitignore | 10 + clio-ai/transcription/.python-version | 1 + clio-ai/transcription/README.md | 0 clio-ai/transcription/flake.lock | 61 + clio-ai/transcription/flake.nix | 46 + clio-ai/transcription/main.py | 6 + clio-ai/transcription/pyproject.toml | 10 + clio-ai/transcription/transcribe.py | 187 +++ clio-ai/transcription/uv.lock | 1146 +++++++++++++++++ clio-android/.claude/CLAUDE.md | 87 ++ clio-android/.gitignore | 1 + clio-android/app/build.gradle.kts | 61 + clio-android/app/proguard-rules.pro | 7 + clio-android/app/src/main/AndroidManifest.xml | 74 ++ .../com/example/phonerecorder/BootReceiver.kt | 33 + .../com/example/phonerecorder/MainActivity.kt | 540 ++++++++ .../example/phonerecorder/RecordingService.kt | 378 ++++++ .../example/phonerecorder/StorageHelper.kt | 222 ++++ .../phonerecorder/api/RecordingsApi.kt | 229 ++++ .../phonerecorder/crypto/CryptoHelper.kt | 124 ++ .../phonerecorder/crypto/KeyManager.kt | 77 ++ .../phonerecorder/sync/NetworkHelper.kt | 99 ++ .../example/phonerecorder/sync/SyncManager.kt | 189 +++ .../phonerecorder/sync/SyncSettings.kt | 86 ++ .../example/phonerecorder/sync/SyncTracker.kt | 82 ++ .../example/phonerecorder/sync/SyncWorker.kt | 104 ++ .../res/drawable/ic_launcher_foreground.xml | 17 + .../src/main/res/drawable/status_circle.xml | 5 + .../res/drawable/status_circle_active.xml | 5 + .../app/src/main/res/layout/activity_main.xml | 232 ++++ .../src/main/res/mipmap-hdpi/ic_launcher.xml | 5 + .../res/mipmap-hdpi/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-mdpi/ic_launcher.xml | 5 + .../res/mipmap-mdpi/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-xhdpi/ic_launcher.xml | 5 + .../res/mipmap-xhdpi/ic_launcher_round.xml | 5 + .../main/res/mipmap-xxhdpi/ic_launcher.xml | 5 + .../res/mipmap-xxhdpi/ic_launcher_round.xml | 5 + .../main/res/mipmap-xxxhdpi/ic_launcher.xml | 5 + .../res/mipmap-xxxhdpi/ic_launcher_round.xml | 5 + .../app/src/main/res/values/colors.xml | 9 + .../app/src/main/res/values/strings.xml | 58 + .../app/src/main/res/values/themes.xml | 11 + .../app/src/main/res/xml/file_paths.xml | 6 + clio-android/build.gradle.kts | 4 + clio-android/description.md | 136 ++ clio-android/flake.lock | 61 + clio-android/flake.nix | 52 + clio-android/gradle.properties | 4 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + clio-android/gradlew | 251 ++++ clio-android/gradlew.bat | 94 ++ clio-android/local.properties | 8 + clio-android/notes.txt | 14 + clio-android/settings.gradle.kts | 18 + clio-android/todo.md | 117 ++ clio-api/.gitignore | 10 + clio-api/.python-version | 1 + clio-api/README.md | 0 clio-api/deploy.sh | 74 ++ clio-api/nixos/Caddyfile | 46 + clio-api/nixos/phone-recorder.nix | 182 +++ clio-api/nixos/phone-recorder.service | 33 + clio-api/requirements.txt | 8 + clio-infra/ARCHITECTURE.md | 126 ++ clio-infra/TODO.md | 153 +++ clio-infra/nixos/Caddyfile | 46 + clio-infra/nixos/phone-recorder.nix | 182 +++ clio-infra/nixos/phone-recorder.service | 33 + clio-infra/scripts/deploy-api-server.sh | 74 ++ clio-infra/scripts/deploy-api.sh | 15 + clio-infra/scripts/deploy.sh | 78 ++ clio-pi/README.md | 102 ++ clio-pi/bin/gen_recording_status.py | 352 +++++ clio-pi/bin/record.sh | 31 + clio-pi/setup.sh | 238 ++++ clio-pi/systemd/record.service | 11 + clio-ui/components/RecordingDetail.jsx | 2 +- 93 files changed, 9058 insertions(+), 1 deletion(-) create mode 100644 another_todo.txt create mode 100644 clio-ai/real_time/.gitignore create mode 100644 clio-ai/real_time/.python-version create mode 100644 clio-ai/real_time/README.md create mode 100755 clio-ai/real_time/collect_samples.sh create mode 100644 clio-ai/real_time/flake.lock create mode 100644 clio-ai/real_time/flake.nix create mode 100755 clio-ai/real_time/listener.py create mode 100644 clio-ai/real_time/main.py create mode 100644 clio-ai/real_time/pyproject.toml create mode 100755 clio-ai/real_time/run_listener.sh create mode 100755 clio-ai/real_time/test_corsair_mic.sh create mode 100644 clio-ai/real_time/train_classifier.py create mode 100644 clio-ai/real_time/uv.lock create mode 100644 clio-ai/transcription/.gitignore create mode 100644 clio-ai/transcription/.python-version create mode 100644 clio-ai/transcription/README.md create mode 100644 clio-ai/transcription/flake.lock create mode 100644 clio-ai/transcription/flake.nix create mode 100644 clio-ai/transcription/main.py create mode 100644 clio-ai/transcription/pyproject.toml create mode 100644 clio-ai/transcription/transcribe.py create mode 100644 clio-ai/transcription/uv.lock create mode 100644 clio-android/.claude/CLAUDE.md create mode 100644 clio-android/.gitignore create mode 100644 clio-android/app/build.gradle.kts create mode 100644 clio-android/app/proguard-rules.pro create mode 100644 clio-android/app/src/main/AndroidManifest.xml create mode 100644 clio-android/app/src/main/java/com/example/phonerecorder/BootReceiver.kt create mode 100644 clio-android/app/src/main/java/com/example/phonerecorder/MainActivity.kt create mode 100644 clio-android/app/src/main/java/com/example/phonerecorder/RecordingService.kt create mode 100644 clio-android/app/src/main/java/com/example/phonerecorder/StorageHelper.kt create mode 100644 clio-android/app/src/main/java/com/example/phonerecorder/api/RecordingsApi.kt create mode 100644 clio-android/app/src/main/java/com/example/phonerecorder/crypto/CryptoHelper.kt create mode 100644 clio-android/app/src/main/java/com/example/phonerecorder/crypto/KeyManager.kt create mode 100644 clio-android/app/src/main/java/com/example/phonerecorder/sync/NetworkHelper.kt create mode 100644 clio-android/app/src/main/java/com/example/phonerecorder/sync/SyncManager.kt create mode 100644 clio-android/app/src/main/java/com/example/phonerecorder/sync/SyncSettings.kt create mode 100644 clio-android/app/src/main/java/com/example/phonerecorder/sync/SyncTracker.kt create mode 100644 clio-android/app/src/main/java/com/example/phonerecorder/sync/SyncWorker.kt create mode 100644 clio-android/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 clio-android/app/src/main/res/drawable/status_circle.xml create mode 100644 clio-android/app/src/main/res/drawable/status_circle_active.xml create mode 100644 clio-android/app/src/main/res/layout/activity_main.xml create mode 100644 clio-android/app/src/main/res/mipmap-hdpi/ic_launcher.xml create mode 100644 clio-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.xml create mode 100644 clio-android/app/src/main/res/mipmap-mdpi/ic_launcher.xml create mode 100644 clio-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.xml create mode 100644 clio-android/app/src/main/res/mipmap-xhdpi/ic_launcher.xml create mode 100644 clio-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.xml create mode 100644 clio-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.xml create mode 100644 clio-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.xml create mode 100644 clio-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.xml create mode 100644 clio-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.xml create mode 100644 clio-android/app/src/main/res/values/colors.xml create mode 100644 clio-android/app/src/main/res/values/strings.xml create mode 100644 clio-android/app/src/main/res/values/themes.xml create mode 100644 clio-android/app/src/main/res/xml/file_paths.xml create mode 100644 clio-android/build.gradle.kts create mode 100644 clio-android/description.md create mode 100644 clio-android/flake.lock create mode 100644 clio-android/flake.nix create mode 100644 clio-android/gradle.properties create mode 100644 clio-android/gradle/wrapper/gradle-wrapper.jar create mode 100644 clio-android/gradle/wrapper/gradle-wrapper.properties create mode 100755 clio-android/gradlew create mode 100644 clio-android/gradlew.bat create mode 100644 clio-android/local.properties create mode 100644 clio-android/notes.txt create mode 100644 clio-android/settings.gradle.kts create mode 100644 clio-android/todo.md create mode 100644 clio-api/.gitignore create mode 100644 clio-api/.python-version create mode 100644 clio-api/README.md create mode 100755 clio-api/deploy.sh create mode 100644 clio-api/nixos/Caddyfile create mode 100644 clio-api/nixos/phone-recorder.nix create mode 100644 clio-api/nixos/phone-recorder.service create mode 100644 clio-api/requirements.txt create mode 100644 clio-infra/ARCHITECTURE.md create mode 100644 clio-infra/TODO.md create mode 100644 clio-infra/nixos/Caddyfile create mode 100644 clio-infra/nixos/phone-recorder.nix create mode 100644 clio-infra/nixos/phone-recorder.service create mode 100755 clio-infra/scripts/deploy-api-server.sh create mode 100755 clio-infra/scripts/deploy-api.sh create mode 100755 clio-infra/scripts/deploy.sh create mode 100644 clio-pi/README.md create mode 100755 clio-pi/bin/gen_recording_status.py create mode 100755 clio-pi/bin/record.sh create mode 100755 clio-pi/setup.sh create mode 100644 clio-pi/systemd/record.service diff --git a/another_todo.txt b/another_todo.txt new file mode 100644 index 0000000..e945fb7 --- /dev/null +++ b/another_todo.txt @@ -0,0 +1,4 @@ + +add echo service for testing +add a food diary +google drive plugin diff --git a/clio-ai/real_time/.gitignore b/clio-ai/real_time/.gitignore new file mode 100644 index 0000000..505a3b1 --- /dev/null +++ b/clio-ai/real_time/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/clio-ai/real_time/.python-version b/clio-ai/real_time/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/clio-ai/real_time/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/clio-ai/real_time/README.md b/clio-ai/real_time/README.md new file mode 100644 index 0000000..e69de29 diff --git a/clio-ai/real_time/collect_samples.sh b/clio-ai/real_time/collect_samples.sh new file mode 100755 index 0000000..254b87c --- /dev/null +++ b/clio-ai/real_time/collect_samples.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash + +# Training sample collection script for voice command classification +# Creates organized recordings for wake word and category phrases + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RECORDINGS_DIR="$SCRIPT_DIR/recordings" +SAMPLE_RATE=16000 +DURATION=4 + +# Categories +declare -A CATEGORIES=( + ["wake_word"]="ok, note this" + ["pressure_below_15"]="eye pressure is less than 15" + ["pressure_15_to_25"]="eye pressure is 15 to 25" + ["pressure_25_to_35"]="eye pressure is 25 to 35" + ["pressure_above_35"]="eye pressure is greater than 35" + ["drop_l"]="L drop" + ["drop_p"]="P drop" + ["drop_d"]="D drop" + ["both_eyes"]="both eyes" +) + +# Get Corsair mic source +get_mic_source() { + wpctl status | grep -i "corsair" | grep -i "mono" | grep -oE '[0-9]+\.' | head -1 | tr -d '.' +} + +# Create directory structure +setup_dirs() { + for category in "${!CATEGORIES[@]}"; do + mkdir -p "$RECORDINGS_DIR/$category" + done + echo "Created recording directories in: $RECORDINGS_DIR" +} + +# Count existing samples for a category +count_samples() { + local category=$1 + find "$RECORDINGS_DIR/$category" -name "*.wav" 2>/dev/null | wc -l +} + +# Record a single sample +record_sample() { + local category=$1 + local voice_name=$2 + local source_id=$3 + + local count=$(count_samples "$category") + local filename="${voice_name}_$(printf "%03d" $((count + 1))).wav" + local filepath="$RECORDINGS_DIR/$category/$filename" + + echo "" + echo "Recording: $category" + echo "Say: \"${CATEGORIES[$category]}\"" + echo "" + echo "Press ENTER to start recording (${DURATION}s)..." + read + + echo ">>> RECORDING NOW - SPEAK! <<<" + pw-record --target "$source_id" --rate "$SAMPLE_RATE" --channels 1 --format s16 "$filepath" & + local pid=$! + sleep "$DURATION" + kill "$pid" 2>/dev/null + wait "$pid" 2>/dev/null + + # Check audio level + local max_vol=$(ffmpeg -i "$filepath" -af "volumedetect" -f null /dev/null 2>&1 | grep "max_volume" | sed 's/.*max_volume: \([-0-9.]*\).*/\1/') + + echo "Saved: $filename (max volume: ${max_vol} dB)" + + if [ "${max_vol%.*}" -lt -50 ] 2>/dev/null; then + echo "WARNING: Recording seems quiet. Keep? [Y/n]" + read -r keep + if [[ "$keep" =~ ^[Nn] ]]; then + rm "$filepath" + echo "Deleted. Try again." + return 1 + fi + fi + return 0 +} + +# Interactive recording session +record_session() { + local source_id=$(get_mic_source) + + if [ -z "$source_id" ]; then + echo "ERROR: Corsair mic not found" + exit 1 + fi + + echo "=== Voice Command Sample Collection ===" + echo "Mic source: $source_id" + echo "" + + echo "Enter your name/voice identifier (e.g., 'john', 'sarah'):" + read -r voice_name + voice_name=${voice_name:-default} + + while true; do + echo "" + echo "=== Current sample counts ===" + for category in "${!CATEGORIES[@]}"; do + local count=$(count_samples "$category") + echo " $category: $count samples" + done + + echo "" + echo "Select category to record:" + echo " 1) wake_word - \"ok, note this\"" + echo " 2) pressure_below_15 - \"eye pressure is less than 15\"" + echo " 3) pressure_15_to_25 - \"eye pressure is 15 to 25\"" + echo " 4) pressure_25_to_35 - \"eye pressure is 25 to 35\"" + echo " 5) pressure_above_35 - \"eye pressure is greater than 35\"" + echo " 6) drop_l - \"L drop\"" + echo " 7) drop_p - \"P drop\"" + echo " 8) drop_d - \"D drop\"" + echo " 9) both_eyes - \"both eyes\"" + echo " a) Record ALL categories (one each)" + echo " q) Quit" + echo "" + read -r choice + + case $choice in + 1) record_sample "wake_word" "$voice_name" "$source_id" ;; + 2) record_sample "pressure_below_15" "$voice_name" "$source_id" ;; + 3) record_sample "pressure_15_to_25" "$voice_name" "$source_id" ;; + 4) record_sample "pressure_25_to_35" "$voice_name" "$source_id" ;; + 5) record_sample "pressure_above_35" "$voice_name" "$source_id" ;; + 6) record_sample "drop_l" "$voice_name" "$source_id" ;; + 7) record_sample "drop_p" "$voice_name" "$source_id" ;; + 8) record_sample "drop_d" "$voice_name" "$source_id" ;; + 9) record_sample "both_eyes" "$voice_name" "$source_id" ;; + a|A) + for category in wake_word pressure_below_15 pressure_15_to_25 pressure_25_to_35 pressure_above_35 drop_l drop_p drop_d both_eyes; do + record_sample "$category" "$voice_name" "$source_id" + done + ;; + q|Q) + echo "Done. Total samples collected:" + for category in "${!CATEGORIES[@]}"; do + echo " $category: $(count_samples "$category")" + done + exit 0 + ;; + *) echo "Invalid choice" ;; + esac + done +} + +# Main +setup_dirs +record_session diff --git a/clio-ai/real_time/flake.lock b/clio-ai/real_time/flake.lock new file mode 100644 index 0000000..cd56566 --- /dev/null +++ b/clio-ai/real_time/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1778443072, + "narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/clio-ai/real_time/flake.nix b/clio-ai/real_time/flake.nix new file mode 100644 index 0000000..50aa65c --- /dev/null +++ b/clio-ai/real_time/flake.nix @@ -0,0 +1,60 @@ +{ + description = "Voice-activated eye pressure classifier"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + config.cudaSupport = true; + }; + in + { + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + # Python and uv + python313 + uv + + # Audio + portaudio + pipewire + alsa-lib + ffmpeg + + # CUDA (for faster-whisper) + cudaPackages.cudatoolkit + cudaPackages.cudnn + + # SSL certificates + cacert + + # Text-to-speech + espeak-ng + ]; + + shellHook = '' + export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [ + pkgs.portaudio + pkgs.pipewire + pkgs.alsa-lib + pkgs.cudaPackages.cudatoolkit + pkgs.cudaPackages.cudnn + ]}:/run/opengl-driver/lib:$LD_LIBRARY_PATH" + + export SSL_CERT_FILE="${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" + export REQUESTS_CA_BUNDLE="$SSL_CERT_FILE" + + echo "Voice classifier dev environment" + echo "Run: uv run python listener.py" + ''; + }; + } + ); +} diff --git a/clio-ai/real_time/listener.py b/clio-ai/real_time/listener.py new file mode 100755 index 0000000..05d57f9 --- /dev/null +++ b/clio-ai/real_time/listener.py @@ -0,0 +1,562 @@ +#!/usr/bin/env python3 +""" +Voice-activated eye pressure classifier. + +Listens for wake word "ok, note this" then classifies the following speech +into eye pressure categories. + +Uses two classification methods: +1. Transcription + regex pattern matching (fallback) +2. Audio embeddings + trained classifier (primary, if available) + +Usage: + uv run python listener.py [--model large-v3] [--device cuda] +""" + +import argparse +import collections +import pickle +import queue +import re +import subprocess +import sys +import time +from pathlib import Path + +import numpy as np +import sounddevice as sd +import torch +import webrtcvad + +# Lazy imports for faster startup feedback +whisper_model = None +embedding_model = None +classifier_data = None + +SCRIPT_DIR = Path(__file__).parent +CLASSIFIER_PATH = SCRIPT_DIR / "classifier.pkl" + +# Categories with regex patterns for matching +CATEGORIES = { + "pressure_below_15": [ + r"(less\s+than|under|below)\s*15", + r"<\s*15", + r"15.*less", + ], + "pressure_15_to_25": [ + r"15\s*(to|through|-)\s*25", + r"between\s*15\s*(and|to)\s*25", + ], + "pressure_25_to_35": [ + r"25\s*(to|through|-)\s*35", + r"between\s*25\s*(and|to)\s*35", + ], + "pressure_above_35": [ + r"(greater\s+than|over|above|more\s+than)\s*35", + r">\s*35", + r"35.*more", + ], + "drop_l": [ + r"\bl\s*drop", + r"drop\s*l\b", + r"\bell\s*drop", + ], + "drop_p": [ + r"\bp\s*drop", + r"drop\s*p\b", + r"\bpee\s*drop", + ], + "drop_d": [ + r"\bd\s*drop", + r"drop\s*d\b", + r"\bdee\s*drop", + ], + "both_eyes": [ + r"both\s*eyes", + r"both\s*i", + ], +} + +WAKE_PHRASES = [ + r"ok[,.]?\s*note\s+this", + r"okay[,.]?\s*note\s+this", + r"o\.?k\.?\s*note\s+this", + # Common mis-transcriptions + r"ok[,.]?\s*notice", + r"okay[,.]?\s*notice", + r"ok[,.]?\s*noted", + r"okay[,.]?\s*noted", + r"ok[,.]?\s*no\s+this", + r"okay[,.]?\s*no\s+this", + r"ok[,.]?\s*not\s+this", + r"okay[,.]?\s*not\s+this", +] + +CONFIRM_YES = [ + r"\byes\b", r"\byeah\b", r"\byep\b", r"\byup\b", + r"\bcorrect\b", r"\bright\b", r"\bconfirm\b", + r"\bsave\b", r"\bok\b", r"\bokay\b", +] + +CONFIRM_NO = [ + r"\bno\b", r"\bnope\b", r"\bnah\b", + r"\bwrong\b", r"\bcancel\b", r"\bdelete\b", + r"\bdiscard\b", r"\bredo\b", +] + +# Spoken responses for each category +CATEGORY_RESPONSES = { + "pressure_below_15": "Eye pressure below 15", + "pressure_15_to_25": "Eye pressure 15 to 25", + "pressure_25_to_35": "Eye pressure 25 to 35", + "pressure_above_35": "Eye pressure above 35", + "drop_l": "L drop", + "drop_p": "P drop", + "drop_d": "D drop", + "both_eyes": "Both eyes", +} + +SAMPLE_RATE = 16000 +FRAME_DURATION_MS = 30 # webrtcvad frame size +FRAME_SIZE = int(SAMPLE_RATE * FRAME_DURATION_MS / 1000) +SPEECH_PADDING_MS = 300 # padding around speech +NUM_PADDING_FRAMES = SPEECH_PADDING_MS // FRAME_DURATION_MS + + +def speak(text: str): + """Speak text using available TTS.""" + # Try espeak-ng, then espeak, then piper + for cmd in [ + ["espeak-ng", "-v", "en-us", "-s", "150", text], + ["espeak", "-v", "en-us", "-s", "150", text], + ]: + try: + result = subprocess.run(cmd, capture_output=True, timeout=10) + if result.returncode == 0: + return + except FileNotFoundError: + continue + except Exception as e: + print(f" TTS error: {e}") + return + + # Fallback: use ffmpeg to generate speech-like notification + print(f" (TTS unavailable - install espeak-ng)") + + +def beep(): + """Play a short beep tone.""" + try: + # Generate a 100ms 800Hz beep + subprocess.run( + ["ffplay", "-nodisp", "-autoexit", "-f", "lavfi", "-i", + "sine=frequency=800:duration=0.1", "-loglevel", "quiet"], + capture_output=True, + timeout=2 + ) + except Exception: + pass # Silent fail + + +def get_corsair_device(): + """Find Corsair microphone device index.""" + devices = sd.query_devices() + for i, dev in enumerate(devices): + if "corsair" in dev["name"].lower() and dev["max_input_channels"] > 0: + return i + return None + + +def load_whisper(model_name: str, device: str): + """Load faster-whisper model.""" + global whisper_model + if whisper_model is None: + from faster_whisper import WhisperModel + print(f"Loading Whisper model '{model_name}' on {device}...") + compute_type = "float16" if device == "cuda" else "int8" + whisper_model = WhisperModel(model_name, device=device, compute_type=compute_type) + print(" Whisper loaded.") + return whisper_model + + +def load_embedding_model(device: str): + """Load wav2vec2 model for embeddings.""" + global embedding_model + if embedding_model is None: + from torchaudio.pipelines import WAV2VEC2_BASE + print(f"Loading wav2vec2 on {device}...") + bundle = WAV2VEC2_BASE + embedding_model = bundle.get_model().to(device) + embedding_model.eval() + print(" wav2vec2 loaded.") + return embedding_model + + +def load_classifier(): + """Load trained classifier if available.""" + global classifier_data + if classifier_data is None and CLASSIFIER_PATH.exists(): + print(f"Loading classifier from {CLASSIFIER_PATH}...") + with open(CLASSIFIER_PATH, 'rb') as f: + classifier_data = pickle.load(f) + print(f" Classifier loaded. Categories: {list(classifier_data['label_encoder'].classes_)}") + return classifier_data + + +def transcribe(audio: np.ndarray, model) -> str: + """Transcribe audio to text.""" + segments, _ = model.transcribe(audio, language="en", beam_size=5) + return " ".join(seg.text for seg in segments).strip().lower() + + +def extract_embedding(audio: np.ndarray, model, device: str) -> np.ndarray: + """Extract embedding from audio using wav2vec2.""" + with torch.no_grad(): + # Convert to torch tensor + waveform = torch.from_numpy(audio).unsqueeze(0).to(device) + features, _ = model.extract_features(waveform) + # Use the last layer's features, averaged over time + embedding = features[-1].mean(dim=1).cpu().numpy() + return embedding.flatten() + + +def classify_with_embedding(audio: np.ndarray, emb_model, classifier, device: str) -> tuple[str, float]: + """Classify audio using embedding + trained classifier.""" + embedding = extract_embedding(audio, emb_model, device) + embedding = embedding.reshape(1, -1) + + probs = classifier['classifier'].predict_proba(embedding)[0] + pred_idx = probs.argmax() + confidence = probs[pred_idx] + category = classifier['label_encoder'].inverse_transform([pred_idx])[0] + + return category, confidence + + +def check_wake_word(text: str) -> bool: + """Check if text contains wake phrase.""" + for pattern in WAKE_PHRASES: + if re.search(pattern, text, re.IGNORECASE): + return True + return False + + +def check_confirmation(text: str) -> str | None: + """Check if text is a yes/no confirmation. Returns 'yes', 'no', or None.""" + for pattern in CONFIRM_YES: + if re.search(pattern, text, re.IGNORECASE): + return "yes" + for pattern in CONFIRM_NO: + if re.search(pattern, text, re.IGNORECASE): + return "no" + return None + + +def log_recording(log_file: str, category: str, timestamp: str): + """Append confirmed recording to log file.""" + import csv + import os + + file_exists = os.path.exists(log_file) + with open(log_file, 'a', newline='') as f: + writer = csv.writer(f) + if not file_exists: + writer.writerow(['timestamp', 'category']) + writer.writerow([timestamp, category]) + + +def classify_category(text: str) -> str | None: + """Classify text into a category. Returns None if no match.""" + for category, patterns in CATEGORIES.items(): + for pattern in patterns: + if re.search(pattern, text, re.IGNORECASE): + return category + return None + + +def classify_text(text: str) -> tuple[str | None, str | None]: + """ + Check for wake word and classify category from transcription. + Returns (category, text) or (None, None) if no wake word. + """ + if not check_wake_word(text): + return None, None + + category = classify_category(text) + return category if category else "unknown", text + + +class VoiceActivityDetector: + """VAD-based speech segmentation.""" + + def __init__(self, aggressiveness: int = 2): + self.vad = webrtcvad.Vad(aggressiveness) + self.ring_buffer = collections.deque(maxlen=NUM_PADDING_FRAMES) + self.triggered = False + self.voiced_frames = [] + + def process_frame(self, frame: bytes) -> np.ndarray | None: + """ + Process a frame of audio. + Returns complete utterance when speech ends, None otherwise. + """ + is_speech = self.vad.is_speech(frame, SAMPLE_RATE) + + if not self.triggered: + self.ring_buffer.append((frame, is_speech)) + num_voiced = sum(1 for _, speech in self.ring_buffer if speech) + + if num_voiced > 0.9 * self.ring_buffer.maxlen: + self.triggered = True + self.voiced_frames = [f for f, _ in self.ring_buffer] + self.ring_buffer.clear() + else: + self.voiced_frames.append(frame) + self.ring_buffer.append((frame, is_speech)) + num_unvoiced = sum(1 for _, speech in self.ring_buffer if not speech) + + if num_unvoiced > 0.9 * self.ring_buffer.maxlen: + self.triggered = False + audio_bytes = b"".join(self.voiced_frames) + self.voiced_frames = [] + self.ring_buffer.clear() + + # Convert to numpy array + audio = np.frombuffer(audio_bytes, dtype=np.int16).astype(np.float32) / 32768.0 + return audio + + return None + + +def audio_callback(indata, frames, time_info, status, audio_queue): + """Callback for audio stream.""" + if status: + print(f"Audio status: {status}", file=sys.stderr) + audio_queue.put(bytes(indata)) + + +def main(): + parser = argparse.ArgumentParser(description="Voice-activated eye pressure classifier") + parser.add_argument("--model", default="base.en", help="Whisper model name") + parser.add_argument("--device", default="cuda", choices=["cuda", "cpu"], help="Compute device") + parser.add_argument("--list-devices", action="store_true", help="List audio devices and exit") + parser.add_argument("--no-classifier", action="store_true", help="Disable embedding classifier") + parser.add_argument("--confidence-threshold", type=float, default=0.6, + help="Minimum confidence for embedding classifier (default: 0.6)") + parser.add_argument("--wake-timeout", type=float, default=5.0, + help="Seconds to wait for command after wake word (default: 5.0)") + parser.add_argument("--confirm", action="store_true", + help="Require yes/no confirmation before saving") + parser.add_argument("--log-file", type=str, default="recordings_log.csv", + help="File to log confirmed recordings (default: recordings_log.csv)") + args = parser.parse_args() + + if args.list_devices: + print(sd.query_devices()) + return + + # Find microphone + mic_device = get_corsair_device() + if mic_device is None: + print("Corsair microphone not found. Available devices:") + print(sd.query_devices()) + sys.exit(1) + + print(f"Using microphone: {sd.query_devices(mic_device)['name']}") + print() + + # Load models + whisper = load_whisper(args.model, args.device) + + # Load embedding classifier if available + classifier = None + emb_model = None + if not args.no_classifier: + classifier = load_classifier() + if classifier: + emb_model = load_embedding_model(args.device) + else: + print(" No classifier found. Run train_classifier.py after collecting samples.") + + print() + + # Setup VAD and audio queue + vad = VoiceActivityDetector(aggressiveness=2) + audio_queue = queue.Queue() + + print("=" * 60) + print("Listening for: \"Ok, note this\"") + print("Then say your command (can pause between wake word and command)") + print("Commands: pressure <15 / 15-25 / 25-35 / >35, L/P/D drop, both eyes") + if classifier: + print(f"Using embedding classifier (confidence threshold: {args.confidence_threshold})") + else: + print("Using transcription-only mode") + if args.confirm: + print("Confirmation mode: say 'yes' to save, 'no' to discard") + print("Press Ctrl+C to stop") + print("=" * 60) + print() + + # State machine: idle -> awaiting_command -> (awaiting_confirmation) -> idle + state = "idle" + state_time = 0 + pending_category = None + + def do_classification(utterance, text, use_embedding=True): + """Classify utterance. Returns (category, method) or (None, None).""" + final_category = None + method_used = None + + # Try embedding classifier first + if use_embedding and classifier and emb_model: + start = time.time() + emb_category, confidence = classify_with_embedding( + utterance, emb_model, classifier, args.device + ) + emb_time = time.time() - start + + print(f" Embedding: {emb_category} ({confidence:.1%}) [{emb_time:.2f}s]") + + if confidence >= args.confidence_threshold: + final_category = emb_category + method_used = f"embedding ({confidence:.1%})" + else: + print(f" Low confidence, trying transcription") + + # Fall back to transcription + if final_category is None: + text_category = classify_category(text) + if text_category: + final_category = text_category + method_used = "transcription" + + return final_category, method_used + + def announce_classification(category, method, ask_confirm=False): + """Announce the classification result.""" + response = CATEGORY_RESPONSES.get(category, category) + print() + print(f" >>> {response} <<< (via {method})") + print() + if ask_confirm: + speak(f"{response}, correct?") + else: + speak(response) + return response + + def save_recording(category): + """Save the confirmed recording.""" + from datetime import datetime + timestamp = datetime.now().isoformat() + log_recording(args.log_file, category, timestamp) + print(f" Saved to {args.log_file}") + beep() + + try: + with sd.RawInputStream( + samplerate=SAMPLE_RATE, + blocksize=FRAME_SIZE, + device=mic_device, + dtype="int16", + channels=1, + callback=lambda *a: audio_callback(*a, audio_queue), + ): + while True: + frame = audio_queue.get() + + # Check for timeouts + if state != "idle" and (time.time() - state_time) > args.wake_timeout: + print(" (timeout)") + print() + state = "idle" + pending_category = None + + utterance = vad.process_frame(frame) + + if utterance is not None and len(utterance) > SAMPLE_RATE * 0.5: # At least 0.5s + print("Processing speech...", end=" ", flush=True) + + # Transcribe + start = time.time() + text = transcribe(utterance, whisper) + transcribe_time = time.time() - start + + print(f"({transcribe_time:.2f}s)") + print(f" Heard: \"{text}\"") + + # State: awaiting confirmation + if state == "awaiting_confirmation": + confirmation = check_confirmation(text) + if confirmation == "yes": + print(" Confirmed!") + save_recording(pending_category) + elif confirmation == "no": + print(" Discarded.") + beep() + else: + print(" Say 'yes' to save or 'no' to discard") + state_time = time.time() # Reset timeout + print() + continue + state = "idle" + pending_category = None + print() + continue + + # State: awaiting command + if state == "awaiting_command": + category, method = do_classification(utterance, text) + if category: + announce_classification(category, method, ask_confirm=args.confirm) + if args.confirm: + print(" Say 'yes' to save, 'no' to discard") + state = "awaiting_confirmation" + state_time = time.time() + pending_category = category + else: + save_recording(category) + state = "idle" + else: + print(" Could not classify. Try again.") + state_time = time.time() # Reset timeout + print() + continue + + # State: idle - check for wake word + has_wake = check_wake_word(text) + + if not has_wake: + print(" (no wake word detected)") + print() + continue + + print(" Wake word detected!") + + # Try to classify in same utterance + category, method = do_classification(utterance, text) + if category: + announce_classification(category, method, ask_confirm=args.confirm) + if args.confirm: + print(" Say 'yes' to save, 'no' to discard") + state = "awaiting_confirmation" + state_time = time.time() + pending_category = category + else: + save_recording(category) + print() + continue + + # No category found - wait for next utterance + print(" Listening for command...") + beep() + state = "awaiting_command" + state_time = time.time() + print() + + except KeyboardInterrupt: + print("\nStopped.") + + +if __name__ == "__main__": + main() diff --git a/clio-ai/real_time/main.py b/clio-ai/real_time/main.py new file mode 100644 index 0000000..9d606d6 --- /dev/null +++ b/clio-ai/real_time/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from real-time!") + + +if __name__ == "__main__": + main() diff --git a/clio-ai/real_time/pyproject.toml b/clio-ai/real_time/pyproject.toml new file mode 100644 index 0000000..93ccc5e --- /dev/null +++ b/clio-ai/real_time/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "real-time" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "faster-whisper>=1.2.1", + "numpy>=2.4.4", + "scikit-learn>=1.8.0", + "scipy>=1.17.1", + "sounddevice>=0.5.5", + "torch>=2.12.0", + "torchaudio>=2.11.0", + "webrtcvad>=2.0.10", +] diff --git a/clio-ai/real_time/run_listener.sh b/clio-ai/real_time/run_listener.sh new file mode 100755 index 0000000..a78bf60 --- /dev/null +++ b/clio-ai/real_time/run_listener.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +cd "$(dirname "$0")" +exec nix develop --command uv run python listener.py "$@" diff --git a/clio-ai/real_time/test_corsair_mic.sh b/clio-ai/real_time/test_corsair_mic.sh new file mode 100755 index 0000000..fa4a09a --- /dev/null +++ b/clio-ai/real_time/test_corsair_mic.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +# Test script for Corsair VOID ELITE Wireless microphone +# Uses PipeWire for audio capture + +OUTPUT_FILE="test_recording.wav" +DURATION=5 + +echo "=== Corsair VOID ELITE Microphone Test ===" +echo "" + +# Get the Corsair source ID +CORSAIR_SOURCE=$(wpctl status | grep -i "corsair" | grep -i "mono" | grep -oE '[0-9]+\.' | head -1 | tr -d '.') + +if [ -z "$CORSAIR_SOURCE" ]; then + echo "ERROR: Corsair microphone not found in PipeWire sources" + wpctl status | grep -A5 "Sources:" + exit 1 +fi + +echo "Found Corsair mic: Source ID $CORSAIR_SOURCE" +echo "Duration: ${DURATION} seconds" +echo "" +echo "Speak into your Corsair headset microphone..." +echo "" + +# Record using pw-record +pw-record --target "$CORSAIR_SOURCE" --rate 48000 --channels 1 --format s16 "$OUTPUT_FILE" & +PW_PID=$! + +sleep "$DURATION" +kill "$PW_PID" 2>/dev/null +wait "$PW_PID" 2>/dev/null + +if [ -f "$OUTPUT_FILE" ]; then + echo "" + echo "=== Recording complete ===" + echo "Saved to: $OUTPUT_FILE" + echo "" + + # Show file info + echo "File info:" + ffprobe -hide_banner "$OUTPUT_FILE" 2>&1 | grep -E "Duration|Stream" + + # Check if there's actual audio (not silence) + echo "" + echo "Audio levels:" + ffmpeg -i "$OUTPUT_FILE" -af "volumedetect" -f null /dev/null 2>&1 | grep -E "max_volume|mean_volume" + + MAX_VOL=$(ffmpeg -i "$OUTPUT_FILE" -af "volumedetect" -f null /dev/null 2>&1 | grep "max_volume" | sed 's/.*max_volume: \([-0-9.]*\).*/\1/') + + echo "" + # Check if max volume is greater than -50 dB (audible) + if [ "$(echo "$MAX_VOL" | cut -d. -f1)" -gt -50 ] 2>/dev/null; then + echo "SUCCESS: Audio detected! Max volume: ${MAX_VOL} dB" + else + echo "WARNING: Very quiet or silent recording (${MAX_VOL} dB)" + echo "" + echo "Possible causes:" + echo " - Mic mute button on headset is ON (flip the switch!)" + echo " - Headset is not being worn / mic boom is retracted" + echo " - Volume too low in system settings" + fi + + echo "" + echo "To play back: ffplay -nodisp -autoexit $OUTPUT_FILE" +else + echo "" + echo "=== Recording FAILED ===" + exit 1 +fi diff --git a/clio-ai/real_time/train_classifier.py b/clio-ai/real_time/train_classifier.py new file mode 100644 index 0000000..064e366 --- /dev/null +++ b/clio-ai/real_time/train_classifier.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +""" +Train an audio embedding classifier on recorded samples. + +Uses wav2vec2 to extract embeddings, then trains a simple classifier. + +Usage: + uv run python train_classifier.py +""" + +import pickle +from pathlib import Path + +import numpy as np +import torch +from scipy.io import wavfile +from torchaudio.pipelines import WAV2VEC2_BASE + +RECORDINGS_DIR = Path(__file__).parent / "recordings" +MODEL_PATH = Path(__file__).parent / "classifier.pkl" +SAMPLE_RATE = 16000 + +# Categories (excluding wake_word which is handled separately) +CATEGORIES = [ + "pressure_below_15", + "pressure_15_to_25", + "pressure_25_to_35", + "pressure_above_35", + "drop_l", + "drop_p", + "drop_d", + "both_eyes", +] + + +def load_audio(path: Path) -> torch.Tensor: + """Load and preprocess audio file.""" + sr, data = wavfile.read(path) + + # Convert to float32 + if data.dtype == np.int16: + data = data.astype(np.float32) / 32768.0 + elif data.dtype == np.int32: + data = data.astype(np.float32) / 2147483648.0 + elif data.dtype != np.float32: + data = data.astype(np.float32) + + # Convert to mono if stereo + if len(data.shape) > 1: + data = data.mean(axis=1) + + # Resample if needed + if sr != SAMPLE_RATE: + from scipy import signal + num_samples = int(len(data) * SAMPLE_RATE / sr) + data = signal.resample(data, num_samples) + + # Convert to torch tensor with batch dimension + waveform = torch.from_numpy(data).unsqueeze(0) + return waveform + + +def extract_embedding(waveform: torch.Tensor, model, device: str) -> np.ndarray: + """Extract embedding from audio using wav2vec2.""" + with torch.no_grad(): + waveform = waveform.to(device) + features, _ = model.extract_features(waveform) + # Use the last layer's features, averaged over time + embedding = features[-1].mean(dim=1).cpu().numpy() + return embedding.flatten() + + +def load_samples(): + """Load all training samples.""" + samples = [] + labels = [] + + for category in CATEGORIES: + category_dir = RECORDINGS_DIR / category + if not category_dir.exists(): + print(f"Warning: {category_dir} does not exist") + continue + + wav_files = list(category_dir.glob("*.wav")) + print(f" {category}: {len(wav_files)} samples") + + for wav_file in wav_files: + samples.append(wav_file) + labels.append(category) + + return samples, labels + + +def main(): + print("=== Training Audio Classifier ===\n") + + # Check for samples + if not RECORDINGS_DIR.exists(): + print(f"Error: {RECORDINGS_DIR} does not exist") + print("Run ./collect_samples.sh first to record training samples") + return + + print("Loading samples...") + sample_paths, labels = load_samples() + + if len(sample_paths) == 0: + print("\nNo samples found. Run ./collect_samples.sh first.") + return + + if len(set(labels)) < 2: + print(f"\nNeed samples from at least 2 categories. Found: {set(labels)}") + return + + print(f"\nTotal: {len(sample_paths)} samples across {len(set(labels))} categories") + + # Check minimum samples per category + from collections import Counter + counts = Counter(labels) + min_samples = min(counts.values()) + if min_samples < 3: + print(f"\nWarning: Some categories have < 3 samples. More samples = better accuracy.") + + # Load model + device = "cuda" if torch.cuda.is_available() else "cpu" + print(f"\nLoading wav2vec2 on {device}...") + bundle = WAV2VEC2_BASE + model = bundle.get_model().to(device) + model.eval() + + # Extract embeddings + print("Extracting embeddings...") + embeddings = [] + valid_labels = [] + + for i, (path, label) in enumerate(zip(sample_paths, labels)): + try: + waveform = load_audio(path) + embedding = extract_embedding(waveform, model, device) + embeddings.append(embedding) + valid_labels.append(label) + print(f" [{i+1}/{len(sample_paths)}] {path.name}") + except Exception as e: + print(f" [{i+1}/{len(sample_paths)}] {path.name} - ERROR: {e}") + + if len(embeddings) < 2: + print("\nNot enough valid samples to train.") + return + + X = np.array(embeddings) + y = np.array(valid_labels) + + print(f"\nEmbedding shape: {X.shape}") + + # Train classifier + print("\nTraining classifier...") + from sklearn.preprocessing import LabelEncoder + from sklearn.svm import SVC + from sklearn.model_selection import cross_val_score + + label_encoder = LabelEncoder() + y_encoded = label_encoder.fit_transform(y) + + # Use SVM with probability estimates + classifier = SVC(kernel='rbf', probability=True, C=1.0) + + # Cross-validation if enough samples + if len(X) >= 4: + n_splits = min(5, len(X)) + scores = cross_val_score(classifier, X, y_encoded, cv=n_splits) + print(f"Cross-validation accuracy: {scores.mean():.2%} (+/- {scores.std()*2:.2%})") + + # Train on all data + classifier.fit(X, y_encoded) + + # Save model + model_data = { + 'classifier': classifier, + 'label_encoder': label_encoder, + 'categories': CATEGORIES, + } + + with open(MODEL_PATH, 'wb') as f: + pickle.dump(model_data, f) + + print(f"\nClassifier saved to: {MODEL_PATH}") + print(f"Categories: {list(label_encoder.classes_)}") + print("\nDone! The listener will now use this classifier.") + + +if __name__ == "__main__": + main() diff --git a/clio-ai/real_time/uv.lock b/clio-ai/real_time/uv.lock new file mode 100644 index 0000000..9b18203 --- /dev/null +++ b/clio-ai/real_time/uv.lock @@ -0,0 +1,1104 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[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 = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +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 = "av" +version = "17.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/f0/8c8dca97ae0cf00e8e2a53bb5cb9aca5fd484f585ef3e9b412200aff3ebd/av-17.0.1.tar.gz", hash = "sha256:fbcbd4aa43bca6a8691816283112d1659a27f407bbeb66d1397023691339f5d4", size = 4411938, upload-time = "2026-04-18T17:12:34.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/82/e7007dcef7bd2d2c377e2e85977701384f42d19fc808c2ccb3a99eaf58f2/av-17.0.1-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:987f4f46ceae4da6c614dcbd2b8149be9dbf680c3bb7a6841c58af9cff4d9230", size = 23238802, upload-time = "2026-04-18T17:11:51.166Z" }, + { url = "https://files.pythonhosted.org/packages/6b/aa/858b09a08ea6f83f91be44b5a5adad13ae8d9ac8b80fda27e73c24bfb160/av-17.0.1-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:d97f54e55b18a74912f479c1978aadd1341d38d892dee95bb5c2f2dccfa72f32", size = 18709338, upload-time = "2026-04-18T17:11:53.286Z" }, + { url = "https://files.pythonhosted.org/packages/a8/8b/8de3fd21c4b0b74d44337421abeab0e71462337fb6a28fff888e0c356cbd/av-17.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e6eee84afa48d0e9321047cd3e4facd44b401493f6bdc753e2e1d1e7c9e6d13e", size = 34007351, upload-time = "2026-04-18T17:11:56.116Z" }, + { url = "https://files.pythonhosted.org/packages/02/28/167b291356c2cc315a2d62a95b0ceace72b5b0bf547de30b89313110f032/av-17.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c58c71bffd9383908c85695ac61d3184c668accb04a5bd1b262e0fb8d09f60a5", size = 36345295, upload-time = "2026-04-18T17:11:59.125Z" }, + { url = "https://files.pythonhosted.org/packages/04/fa/aae56f2ff2c204c408641e1120f5ca5ce9c3390cf5362245c6f1158704b5/av-17.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:42d6745d30a410ec9b22aef79a52a7ab5a001eb8f5adfd952946606a30983318", size = 35183754, upload-time = "2026-04-18T17:12:01.697Z" }, + { url = "https://files.pythonhosted.org/packages/ba/bd/776046f27093aef80155a204ca7d82a887ae4ee72ba4ef8411b46ea7898c/av-17.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3ed6bcd7021fe55832f95b8ef78dd01a4cb21faf3cd71f1e1bf4f20bf100b278", size = 37430809, upload-time = "2026-04-18T17:12:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d5/3261bd2c6b7f6c0aa8379fc970d1ecf496330990b992ad28607785074268/av-17.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:9af524e8632a54032e361d6b88895bd3e7c6212ca560de60f5ccc525323c764c", size = 28889649, upload-time = "2026-04-18T17:12:07.04Z" }, + { url = "https://files.pythonhosted.org/packages/98/39/381104e427a0c7231d2ec0d25d538d58fc20fc0458846b95860d3ef8073b/av-17.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:50e58a473d65ea29b645e45c9fd8518a6783737135683ecc40571a91592bdfe4", size = 21918412, upload-time = "2026-04-18T17:12:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8c/bb1498f031abb6157b30b7fc2379359176953821b6ba59fbd89dbb56f61f/av-17.0.1-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:1d33871742d1e71562db3c8e752cacc5a62766d7efc3ae408bff1c3e26ebb46e", size = 23484157, upload-time = "2026-04-18T17:12:11.67Z" }, + { url = "https://files.pythonhosted.org/packages/1a/58/dedaef187b797243cd5762722e376c69c5ad95ab23db44127f09afc2cd66/av-17.0.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:1229e879f4b6431bc00f69d7f8891fe9a683b0a6e0e009e6c98eb7e449f0383d", size = 18920872, upload-time = "2026-04-18T17:12:14.826Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/5c550231651d6285e6a5c4f6f4a0e67459bfe2b622a7c9352be8cca8c819/av-17.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4744837f4116964280bcc72285e3cdd51361e98a696205aadd924203440ef511", size = 37471077, upload-time = "2026-04-18T17:12:17.349Z" }, + { url = "https://files.pythonhosted.org/packages/59/e4/9807b89a9d775c6f015677996c48bce48aaff70b5d95885adf39e59832a2/av-17.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3d0a7d45d9599bf9df9f8249827113d4f36df1cd6b5356227b997f0552dbc98e", size = 39566981, upload-time = "2026-04-18T17:12:19.942Z" }, + { url = "https://files.pythonhosted.org/packages/5c/72/a22a657abc3de652f5b4f46cbbebdf7cba629752112791b81f05d340991d/av-17.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9acd0b6a6e02af2b37f63d97a03ee2c47936d58e82425c3cd075a95245937c59", size = 38397369, upload-time = "2026-04-18T17:12:22.909Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b2/f4e83e41c1e3c186f34b7df506779d0cd7e40499e2e19519c7ece148cd20/av-17.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3d3a36204cb1f1e7691e6446afa8d6b7097b09946dae732c71c5d05ce09e506e", size = 40582445, upload-time = "2026-04-18T17:12:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/c8/59/8676188b72eed09d48ce6cfaf0f22b0bb9f3cfd74d388ee2b7fdf960536d/av-17.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:b87b98afe971cde123953073bc9c95ab0b7efd2ecc082dd2dbd11f9d9abf190e", size = 29217136, upload-time = "2026-04-18T17:12:29.189Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/0a6e1d2a845988039f6c197fa7269b5e9abbe17354fb41cc9d75bb260fcb/av-17.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a87a42c36e29f75e7dff7281944f2a6876a2c8875e225ccbf6c1ae62748b4caa", size = 22072676, upload-time = "2026-04-18T17:12:31.836Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[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/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[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 = "ctranslate2" +version = "4.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pyyaml" }, + { name = "setuptools" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/6d/eb49ba05db286b4ea9d5d3fcf5f5cd0a9a5e218d46349618d5041001e303/ctranslate2-4.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b2abf2929756e3ec6246057b56df379995661560a2d776af05f9d97f63afcf5", size = 1256960, upload-time = "2026-02-04T06:11:47.487Z" }, + { url = "https://files.pythonhosted.org/packages/45/5a/b9cce7b00d89fc6fdeaf27587aa52d0597b465058563e93ff50910553bdd/ctranslate2-4.7.1-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:857ef3959d6b1c40dc227c715a36db33db2d097164996d6c75b6db8e30828f52", size = 11918645, upload-time = "2026-02-04T06:11:49.599Z" }, + { url = "https://files.pythonhosted.org/packages/ea/03/c0db0a5276599fb44ceafa2f2cb1afd5628808ec406fe036060a39693680/ctranslate2-4.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:393a9e7e989034660526a2c0e8bb65d1924f43d9a5c77d336494a353d16ba2a4", size = 16860452, upload-time = "2026-02-04T06:11:52.276Z" }, + { url = "https://files.pythonhosted.org/packages/0b/03/4e3728ce29d192ee75ed9a2d8589bf4f19edafe5bed3845187de51b179a3/ctranslate2-4.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a3d0682f2b9082e31c73d75b45f16cde77355ab76d7e8356a24c3cb2480a6d3", size = 38995174, upload-time = "2026-02-04T06:11:55.477Z" }, + { url = "https://files.pythonhosted.org/packages/9b/15/6e8e87c6a201d69803a79ac2e29623ce7c2cc9cd1df9db99810cca714373/ctranslate2-4.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:baa6d2b10f57933d8c11791e8522659217918722d07bbef2389a443801125fe7", size = 18844953, upload-time = "2026-02-04T06:11:58.519Z" }, + { url = "https://files.pythonhosted.org/packages/fd/73/8a6b7ba18cad0c8667ee221ddab8c361cb70926440e5b8dd0e81924c28ac/ctranslate2-4.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d5dfb076566551f4959dfd0706f94c923c1931def9b7bb249a2caa6ab23353a0", size = 1257560, upload-time = "2026-02-04T06:12:00.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/c2/8817ca5d6c1b175b23a12f7c8b91484652f8718a76353317e5919b038733/ctranslate2-4.7.1-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:eecdb4ed934b384f16e8c01b185b082d6b5ffc7dcbb0b6a6eb48cd465282d957", size = 11918995, upload-time = "2026-02-04T06:12:02.875Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/b8eb3acc67bbca4d9872fc9ff94db78e6167a7ba5cd932f585d1560effc7/ctranslate2-4.7.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1aa6796edcc3c8d163c9e39c429d50076d266d68980fed9d1b2443f617c67e9e", size = 16844162, upload-time = "2026-02-04T06:12:05.099Z" }, + { url = "https://files.pythonhosted.org/packages/80/11/6474893b07121057035069a0a483fe1cd8c47878213f282afb4c0c6fc275/ctranslate2-4.7.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24c0482c51726430fb83724451921c0e539d769c8618dcfd46b1645e7f75960d", size = 38966728, upload-time = "2026-02-04T06:12:07.923Z" }, + { url = "https://files.pythonhosted.org/packages/94/88/8fc7ff435c5e783e5fad9586d839d463e023988dbbbad949d442092d01f1/ctranslate2-4.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:76db234c0446a23d20dd8eeaa7a789cc87d1d05283f48bf3152bae9fa0a69844", size = 19100788, upload-time = "2026-02-04T06:12:10.592Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b3/f100013a76a98d64e67c721bd4559ea4eeb54be3e4ac45f4d801769899af/ctranslate2-4.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:058c9db2277dc8b19ecc86c7937628f69022f341844b9081d2ab642965d88fc6", size = 1280179, upload-time = "2026-02-04T06:12:12.596Z" }, + { url = "https://files.pythonhosted.org/packages/39/22/b77f748015667a5e2ca54a5ee080d7016fce34314f0e8cf904784549305a/ctranslate2-4.7.1-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:5abcf885062c7f28a3f9a46be8d185795e8706ac6230ad086cae0bc82917df31", size = 11940166, upload-time = "2026-02-04T06:12:14.054Z" }, + { url = "https://files.pythonhosted.org/packages/7d/78/6d7fd52f646c6ba3343f71277a9bbef33734632949d1651231948b0f0359/ctranslate2-4.7.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9950acb04a002d5c60ae90a1ddceead1a803af1f00cadd9b1a1dc76e1f017481", size = 16849483, upload-time = "2026-02-04T06:12:17.082Z" }, + { url = "https://files.pythonhosted.org/packages/40/27/58769ff15ac31b44205bd7a8aeca80cf7357c657ea5df1b94ce0f5c83771/ctranslate2-4.7.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1dcc734e92e3f1ceeaa0c42bbfd009352857be179ecd4a7ed6cccc086a202f58", size = 38949393, upload-time = "2026-02-04T06:12:21.302Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5c/9fa0ad6462b62efd0fb5ac1100eee47bc96ecc198ff4e237c731e5473616/ctranslate2-4.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:dfb7657bdb7b8211c8f9ecb6f3b70bc0db0e0384d01a8b1808cb66fe7199df59", size = 19123451, upload-time = "2026-02-04T06:12:24.115Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" }, + { url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" }, + { url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/d0/c177e29701cf1d3008d7d2b16b5fc626592ce13bd535f8795c5f57187e0e/cuda_pathfinder-1.5.4-py3-none-any.whl", hash = "sha256:9563d3175ce1828531acf4b94e1c1c7d67208c347ca002493e2654878b26f4b7", size = 51657, upload-time = "2026-04-27T22:42:07.712Z" }, +] + +[[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] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[package]] +name = "faster-whisper" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "av" }, + { name = "ctranslate2" }, + { name = "huggingface-hub" }, + { name = "onnxruntime" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/99/49ee85903dee060d9f08297b4a342e5e0bcfca2f027a07b4ee0a38ab13f9/faster_whisper-1.2.1-py3-none-any.whl", hash = "sha256:79a66ad50688c0b794dd501dc340a736992a6342f7f95e5811be60b5224a26a7", size = 1118909, upload-time = "2025-10-31T11:35:47.794Z" }, +] + +[[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 = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, +] + +[[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 = "hf-xet" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, + { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, + { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, + { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, + { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, + { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +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 = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +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 = "huggingface-hub" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/40/43109e943fd718b0ccd0cd61eb4f1c347df22bf81f5874c6f22adf44bcff/huggingface_hub-1.14.0.tar.gz", hash = "sha256:d6d2c9cd6be1d02ae9ec6672d5587d10a427f377db688e82528f426a041622c2", size = 782365, upload-time = "2026-05-06T14:14:34.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/a5/33b49ba7bea7c41bb37f74ec0f8beea0831e052330196633fe2c77516ea6/huggingface_hub-1.14.0-py3-none-any.whl", hash = "sha256:efe075535c62e130b30e836b138e13785f6f043d1f0539e0a39aa411a99e90b8", size = 661479, upload-time = "2026-05-06T14:14:32.029Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +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 = "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 = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[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/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[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 = "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 = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +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 = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +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/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, +] + +[[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.20.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +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" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, +] +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" }, +] +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.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.29.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, +] + +[[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 = "onnxruntime" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/a2/c801242685e0ce48a4ca51dfafbb588765e0446397e123be53ba5598f3f5/onnxruntime-1.26.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccce19c5f771b8268902f77d9fed9e88f9499465d6780808faa6611a789d33f0", size = 18016563, upload-time = "2026-05-08T19:07:28.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/64/0492c0b1db04e29b2630c87cfa36f9d6872b1ca8614b90c5cad58fac7d76/onnxruntime-1.26.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdbed8cf3b672b66acb032f33a253bc27f42bce6ece48ae3fab4fa483a5e96e0", size = 16052634, upload-time = "2026-05-08T19:07:16.885Z" }, + { url = "https://files.pythonhosted.org/packages/3d/26/4d09ddc755a84fc8d5e192991626b0e0680e8f6c5d58f4f1d05c42bc48cf/onnxruntime-1.26.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c07af6fc6d5557835f2b6ee7a96d8b3235d0c57a8e230efdedaee106a8a3cbc6", size = 18185632, upload-time = "2026-05-08T19:07:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/77/89/3e52249aa08fa301e217ecba07b5246a8338fa2b401e109326e3fc5be0f9/onnxruntime-1.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:61bec80655efa460591c2bc655392d57d2650ce85533a6b9b3b7a790d7ea7916", size = 13026751, upload-time = "2026-05-08T19:08:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/06/b3/c1c8782b14af6797c303de132d6eef26a9fb80dfacd3750ce57911d11c6b/onnxruntime-1.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a6677545ff451e3539a02746d2f207d8c5baa4a0a818886bb9d6a6eb9511ee89", size = 12796807, upload-time = "2026-05-08T19:07:54.879Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f5/47b0676408abec652c14b84d7173e389837832d850c24f87184277313e8d/onnxruntime-1.26.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e016edc15d3c19f36807e1c6b10be5b27807688c32720f91b5ae480a95215d0", size = 16057265, upload-time = "2026-05-08T19:07:19.603Z" }, + { url = "https://files.pythonhosted.org/packages/3b/45/33ab6deeef010ca844c877dd618cebc079590bbe52d2a3678e7223b1b908/onnxruntime-1.26.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5fc48a91a046a6a5c9b147f83fb41d65d24d24923373b222cdd248f0f4f4aac", size = 18197590, upload-time = "2026-05-08T19:07:41.422Z" }, + { url = "https://files.pythonhosted.org/packages/40/89/17546c1c20f6bfc3ae41c22152378a26edfea918af3129e2139dcd7c99f3/onnxruntime-1.26.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:33a791f31432a3af1a96db5e54818b37aba5e5eefc2e6af5794c10a9118a9993", size = 18019724, upload-time = "2026-05-08T19:07:30.723Z" }, + { url = "https://files.pythonhosted.org/packages/bb/24/89457a35f6af29538a76647f2c18c3a28277e6c19234c847e7b4b7c19860/onnxruntime-1.26.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e90c00732c4553618103149d93f688e8c3063017938f8983e21a71d9f3b6d22e", size = 16054821, upload-time = "2026-05-08T19:07:22.348Z" }, + { url = "https://files.pythonhosted.org/packages/12/f9/15b2e1815cf570d238e0135529f80d2dce64e8e8818a1489cae83823c5c6/onnxruntime-1.26.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01498e80ba8988428d08c2d51b1338f89e3de2a93e6ffe555f79c68f26a5c06b", size = 18185815, upload-time = "2026-05-08T19:07:44.179Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/2e11055faf015e4b07f45b513fa49b391baf2e19d92d77d73ebee13c1004/onnxruntime-1.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:7ead61450d8405167c87dd3a31d8da1d576b490a57dab1aa8b82a7da6825f5aa", size = 13349887, upload-time = "2026-05-08T19:08:08.671Z" }, + { url = "https://files.pythonhosted.org/packages/19/e4/0f9d1a5718b1781c610c1e354765a3820597081754277a6a9a2b50705702/onnxruntime-1.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:31d71a53490e46910877d0902b5ad99c69a5955e5c7ea6c82863519410e1ba7c", size = 13140121, upload-time = "2026-05-08T19:07:57.804Z" }, + { url = "https://files.pythonhosted.org/packages/1c/42/3b8e635f067d06d9f45bede470b8d539d101a4166c272213158dfd08b6ce/onnxruntime-1.26.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b6d258fb78fdfcf049795bcfaa74dcb90ae7baa277afd21e6fd28b83f2c496", size = 16057240, upload-time = "2026-05-08T19:07:25.163Z" }, + { url = "https://files.pythonhosted.org/packages/93/99/f2be40a31b908d96b861ae0ce98582fa376c18a7f816b9d5eb4cd6aa0a4c/onnxruntime-1.26.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4eefd386a45202aefb7a5132b94f32df9d506c9edcc7faf2fc60d65183f4b183", size = 18197382, upload-time = "2026-05-08T19:07:46.965Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[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 = "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 = "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 = "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/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "real-time" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "faster-whisper" }, + { name = "numpy" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "sounddevice" }, + { name = "torch" }, + { name = "torchaudio" }, + { name = "webrtcvad" }, +] + +[package.metadata] +requires-dist = [ + { name = "faster-whisper", specifier = ">=1.2.1" }, + { name = "numpy", specifier = ">=2.4.4" }, + { name = "scikit-learn", specifier = ">=1.8.0" }, + { name = "scipy", specifier = ">=1.17.1" }, + { name = "sounddevice", specifier = ">=0.5.5" }, + { name = "torch", specifier = ">=2.12.0" }, + { name = "torchaudio", specifier = ">=2.11.0" }, + { name = "webrtcvad", specifier = ">=2.0.10" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +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/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, +] + +[[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 = "sounddevice" +version = "0.5.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/f9/2592608737553638fca98e21e54bfec40bf577bb98a61b2770c912aab25e/sounddevice-0.5.5.tar.gz", hash = "sha256:22487b65198cb5bf2208755105b524f78ad173e5ab6b445bdab1c989f6698df3", size = 143191, upload-time = "2026-01-23T18:36:43.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/0a/478e441fd049002cf308520c0d62dd8333e7c6cc8d997f0dda07b9fbcc46/sounddevice-0.5.5-py3-none-any.whl", hash = "sha256:30ff99f6c107f49d25ad16a45cacd8d91c25a1bcdd3e81a206b921a3a6405b1f", size = 32807, upload-time = "2026-01-23T18:36:35.649Z" }, + { url = "https://files.pythonhosted.org/packages/56/f9/c037c35f6d0b6bc3bc7bfb314f1d6f1f9a341328ef47cd63fc4f850a7b27/sounddevice-0.5.5-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:05eb9fd6c54c38d67741441c19164c0dae8ce80453af2d8c4ad2e7823d15b722", size = 108557, upload-time = "2026-01-23T18:36:37.41Z" }, + { url = "https://files.pythonhosted.org/packages/88/a1/d19dd9889cd4bce2e233c4fac007cd8daaf5b9fe6e6a5d432cf17be0b807/sounddevice-0.5.5-py3-none-win32.whl", hash = "sha256:1234cc9b4c9df97b6cbe748146ae0ec64dd7d6e44739e8e42eaa5b595313a103", size = 317765, upload-time = "2026-01-23T18:36:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0e/002ed7c4c1c2ab69031f78989d3b789fee3a7fba9e586eb2b81688bf4961/sounddevice-0.5.5-py3-none-win_amd64.whl", hash = "sha256:cfc6b2c49fb7f555591c78cb8ecf48d6a637fd5b6e1db5fec6ed9365d64b3519", size = 365324, upload-time = "2026-01-23T18:36:40.496Z" }, + { url = "https://files.pythonhosted.org/packages/4e/39/a61d4b83a7746b70d23d9173be688c0c6bfc7173772344b7442c2c155497/sounddevice-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3861901ddd8230d2e0e8ae62ac320cdd4c688d81df89da036dcb812f757bb3e6", size = 317115, upload-time = "2026-01-23T18:36:42.235Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +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 = "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.23.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748, upload-time = "2026-04-27T14:43:25.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732, upload-time = "2026-04-27T14:43:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954, upload-time = "2026-04-27T14:43:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081, upload-time = "2026-04-27T14:43:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641, upload-time = "2026-04-27T14:43:03.856Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624, upload-time = "2026-04-27T14:43:10.664Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062, upload-time = "2026-04-27T14:43:05.643Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098, upload-time = "2026-04-27T14:43:08.854Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235, upload-time = "2026-04-27T14:43:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398, upload-time = "2026-04-27T14:43:07.264Z" }, + { url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279, upload-time = "2026-04-27T14:43:17.222Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986, upload-time = "2026-04-27T14:43:19.45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181, upload-time = "2026-04-27T14:43:21.463Z" }, + { url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853, upload-time = "2026-04-27T14:43:23.617Z" }, + { url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263, upload-time = "2026-04-27T14:43:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223, upload-time = "2026-04-27T14:43:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" }, +] + +[[package]] +name = "torch" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, + { 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" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/ca/01896c80ba921676aa45886b2c5b8d774912de2a1f719de48169c6f755cd/torch-2.12.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:90dd587a5f61bfe1307148b581e2084fc5bc4a06e2b90a20e9a36b81087ff16b", size = 88009511, upload-time = "2026-05-13T14:54:47.411Z" }, + { url = "https://files.pythonhosted.org/packages/a5/04/52bdaf4787eab6ac7d7f5851dff934e4def0bc8ead9c8fd2b69b3e529699/torch-2.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:864392c73b7654f4d2b3ae712f607937d0dbb1101c4555fbb41848106b297f39", size = 426383231, upload-time = "2026-05-13T14:53:32.129Z" }, + { url = "https://files.pythonhosted.org/packages/49/8a/94bdecd13f5aaa90d45920b89789d9fe7c6f4af8c3cdd7ce01fcb59908fc/torch-2.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5d6b560dfa7d56291c07d615c3bb73e8d9943d9b6d87f76cd0d9d570c4797fa6", size = 532269288, upload-time = "2026-05-13T14:53:49.423Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2f/bdbaaa267de519ef1b73054bf590d8c93c37a266c9a4e24a01bd38b6918f/torch-2.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:3fee918902090ade827643e758e98363278815de583c75d111fdd665ebffde9f", size = 122987706, upload-time = "2026-05-13T14:54:00.335Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ad/e95e822f3538171e22640a7fbe839a1fdb666600bf6487025de2ff03b11a/torch-2.12.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:10ee1448a9f304d3b987eb4656f664ba6e4d7b410ca7a5a7c642199777a2cf88", size = 88319556, upload-time = "2026-05-13T14:54:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/b7/07/055d06d985b445d67422d25b033c11cf55bbb81785d4c4e68e28bca5820e/torch-2.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af68dbf403439cae9ceaeaaf92f8352b460787dcd27b92aa05c40dd4a19c0f1e", size = 426397656, upload-time = "2026-05-13T14:52:38.84Z" }, + { url = "https://files.pythonhosted.org/packages/43/94/b0b4fdc3014122e0a7302fb90086d352aa48f2576f0b252561ebb38c01a8/torch-2.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a6a2eebb237d3b1d9ad3b378e86d9b9e0782afdea8b1e0eba6a13646b9b49c07", size = 532183124, upload-time = "2026-05-13T14:53:16.178Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c8/052405e6ad05d3237bfe5a4df78f917773956f8e17813a2d44c059068b74/torch-2.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2140e373e9a51a3e22ef62e8d14366d0b470d18f0adf19fdc757368077133a34", size = 123232462, upload-time = "2026-05-13T14:52:27.26Z" }, + { url = "https://files.pythonhosted.org/packages/67/dc/ac069f8d6e8be701535921141055293b0d4819d3d7f224a4612cf157c7f9/torch-2.12.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7dfae4a519197dfa050e98d8e36378a0fb5899625a875c2b54445005a2e404e", size = 88027282, upload-time = "2026-05-13T14:53:05.258Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/1c1eb00e34555b536dddf792676026a988d710ed36981aa00499b36b0620/torch-2.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:891c769072637c74e9a5a77a3bc782894696d8ffec83b938df8536dee7f0ba78", size = 426386961, upload-time = "2026-05-13T14:51:28.406Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d4/7e730dba0c7032a4154dc9056b76cf9625515e030e269cfbf8098fcfee7d/torch-2.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e2ad3eb85d39c3cab62dfa93ed5a73516e6a53c6713cb97d004004fe089f0f1f", size = 532272265, upload-time = "2026-05-13T14:51:59.308Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b4/92c80d1bbfee1c0036c06d1d2155a3065bd2423134c83bf8a47e65cd6b9b/torch-2.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:c66696857e987efb8bc1777a37357ec4f60ab5e8af6250b83d6034437fa2d8f3", size = 122987138, upload-time = "2026-05-13T14:51:45.942Z" }, + { url = "https://files.pythonhosted.org/packages/7b/78/2e12b37ce50a19a037d7bc62d652a5a8f27385a7b05859d6bc9204f20cfe/torch-2.12.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b4556715c8572758625d62b6e0ae3b1f76c440221913a6fb5e100f321fb4fb02", size = 88320100, upload-time = "2026-05-13T14:51:39.955Z" }, + { url = "https://files.pythonhosted.org/packages/56/5e/83c450ec7b0bb40a7b74611c1b5440f9260e33c54c90d556fd4a1f0fd955/torch-2.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a43ac605a5e13116c72b64c359644cce0229f213dde48d2ae0ae5eb5becf7feb", size = 426391871, upload-time = "2026-05-13T14:52:14.989Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e9/1a0b575d98d0afedd8f157d23fa3d2759421483660448e60d0a4b10b6daa/torch-2.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6a7512adfdd7f6732e40de1c620831e3c75b39b98cef60b11d0c5f0a76473ec5", size = 532192241, upload-time = "2026-05-13T14:51:07.795Z" }, + { url = "https://files.pythonhosted.org/packages/88/21/afadd25ecd81b3cea1e11c73cf1ab41a983a50271548c3ec7ec3b9efc3e9/torch-2.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f96b63f8287f66a005dd1b5a6abba2920f11156c5e5c4d815f3e2050fd1aa16", size = 123231092, upload-time = "2026-05-13T14:51:18.854Z" }, +] + +[[package]] +name = "torchaudio" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/9e/f76fcd9877c8c78f258ee34e0fb8291fdb91e6218d582d9ca66b1e4bd4ae/torchaudio-2.11.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e3f9696a9ef1d49acc452159b052370c636406d072e9d8f10895fda87b591ea9", size = 679904, upload-time = "2026-03-23T18:13:28.329Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/249c1498ebdad3e7752866635ec0855fc0dcf898beccda5a9d2b9df8e4d0/torchaudio-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b034d7672f1c415434f48ef17807f2cce47f29e8795338c751d4e596c9fbe8b5", size = 1618523, upload-time = "2026-03-23T18:13:15.703Z" }, + { url = "https://files.pythonhosted.org/packages/4f/98/be13fe35d9aa5c26381c0e453c828a789d15c007f8f7d08c95341d19974d/torchaudio-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1c1101c1243ef0e4063ec63298977e2d3655c15cf88d9eb0a1bd4fe2db9f47ea", size = 1771992, upload-time = "2026-03-23T18:13:35.343Z" }, + { url = "https://files.pythonhosted.org/packages/e2/8b/2bbb3dca6ff28cba0de250874d5ef4fc2822c47a934b59b3974cff3219ef/torchaudio-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:986f4df5ed17b003dc52489468601720090e65f964f8bebccf90eb45bba75744", size = 328662, upload-time = "2026-03-23T18:13:18.308Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ce/52c652d30af7d6e96c8f1735d26131e94708e3f38d852b8fa97958804dd8/torchaudio-2.11.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:bda09ea630ae7207384fb0f28c35e4f8c0d82dd6eba020b6b335ad0caa9fed49", size = 680814, upload-time = "2026-03-23T18:13:17.08Z" }, + { url = "https://files.pythonhosted.org/packages/06/95/1ad1507482e7263e556709a3f5f87fecd375a0742cdaf238806c8e72eaad/torchaudio-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:9fe3083c62e035646483a14e180d33561bdc2eed436c9ab1259c137fb7120b4a", size = 1618546, upload-time = "2026-03-23T18:13:29.686Z" }, + { url = "https://files.pythonhosted.org/packages/98/4c/480328ba07487eb9890406720304d0d460dd7a6a64098614f5aa53b662ca/torchaudio-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:13cff988697ccbad539987599f9dc672f40c417bed67570b365e4e5002bbd096", size = 1771991, upload-time = "2026-03-23T18:13:30.843Z" }, + { url = "https://files.pythonhosted.org/packages/3e/98/5d4790e2d6548768999acd34999d5aeefce8bcc23a07afaa5f03e723f557/torchaudio-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ed404c4399ad7f172c86a47c1b25293d322d1d58e26b10b0456a86cf67d37d84", size = 328661, upload-time = "2026-03-23T18:13:34.359Z" }, + { url = "https://files.pythonhosted.org/packages/39/fe/ffa618b4f0d9732d7df7a2fa2bd48657d896599bc224e5af3c70d46c546b/torchaudio-2.11.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:cc09cd1f6015b8549e7fe255fb1be5346b57e7fee06541d3f3dbb012d8c4715f", size = 679901, upload-time = "2026-03-23T18:13:25.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/54/f414d7b92dd0b3094a2409c95a97bd6c49aa0620da722a0e55462f9bd9cb/torchaudio-2.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:79fb3cb99169fd41bd9719647261402a164da0d105a4d81f42a3260844ec5e79", size = 1618527, upload-time = "2026-03-23T18:13:26.68Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a8/bf2e1f6ce24c990192400ae49b4acc1a0d0295b6c6a06bceecdc46ce08de/torchaudio-2.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:00e9f71ab9c656f0abdb40c515bd65d4658ab0ad380dee27a2efd7d51dabd3d6", size = 1771995, upload-time = "2026-03-23T18:13:23.373Z" }, + { url = "https://files.pythonhosted.org/packages/83/6f/b0efb44e0bfe8dd4d78d76ae3be280354e1fb5c8631c782785d74cd8a7b1/torchaudio-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:1424638adb8bb40087bc7b6eb103e8e4fe398210f09076f33b7b5e61501b5d66", size = 328662, upload-time = "2026-03-23T18:13:32.243Z" }, + { url = "https://files.pythonhosted.org/packages/60/84/1c792b0b700eac9a96772cfd9f96c097b17bca3234a2fde3c64b8063660d/torchaudio-2.11.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:da2725e250866da42a12934c9a6552f65a18b7187fd7a6221387f0e605fb3b96", size = 679926, upload-time = "2026-03-23T18:13:24.452Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a0/62a5842062f739239691f2e57523e0570dd06704ad987755f7644a3afa23/torchaudio-2.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:1be3767064364ae82705bdf2b15c1e8b41fea82c4cd04d47428a8684b634b6ed", size = 1618552, upload-time = "2026-03-23T18:13:21.09Z" }, + { url = "https://files.pythonhosted.org/packages/6d/89/c293d818f9f899db93bf291b42401c05ae29acfb2e53d5341c30ea703e62/torchaudio-2.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:67f6edac29ed004652c11db5c19d9debb5d835695930574f564efc8bdd061bba", size = 1771986, upload-time = "2026-03-23T18:13:22.153Z" }, + { url = "https://files.pythonhosted.org/packages/93/f7/ee5da8c03f1a3c7662c6c6a119f24a4b3e646da94be56dce3201e3a6ee9b/torchaudio-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:88fb5e29f670a33d9bac6aabb1d2734460cf6e461bde5cdc352826035851b16d", size = 328661, upload-time = "2026-03-23T18:13:20.1Z" }, +] + +[[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 = "triton" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/e1/a59a583de59b8f62c495d67c80ee3ea97d09e91ac80c4c6e76456ed8d8ac/triton-3.7.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abdf6beaa89b1bcfb9a43cd990536ce66091a997841a4814b260b7bee4c88c3c", size = 188503209, upload-time = "2026-05-07T19:05:17.935Z" }, + { url = "https://files.pythonhosted.org/packages/30/b1/b7507bb9815d403927c8dd51d4158ed2e11751a92dbc118a044f247b6848/triton-3.7.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a35d7afe3f3f058e7ec49fcce09794049e0ffc5c59019ac25ec3413741b8c4e7", size = 201453566, upload-time = "2026-05-07T18:46:20.427Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8f/0bea7a6a0c989315c9135a1d7fb37e41905cfb3a17cbc1f10044ebd4cc3a/triton-3.7.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc1d61c172d257db80ddf42595131fb196ad2e9bdd751e90fe2ef13531734e8b", size = 188612899, upload-time = "2026-05-07T19:05:24.955Z" }, + { url = "https://files.pythonhosted.org/packages/e1/02/d96f57828d0912aec733b9bc7e0e7dbfd2c6f079a8fa433ac25cb93d1a30/triton-3.7.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70fb9bbdc9f400afc54bbf6eb2670af28829a6ae3996863317964783141daf56", size = 201553816, upload-time = "2026-05-07T18:46:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/40/fb/82a802dac4689f2a2fb2e69302e6a138eecc3e175bbe976ba3cfc717683a/triton-3.7.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a44a8476d0d3571eac4e4d1048e1ff75aad81a09ff4602ccfc56c6dea1672e", size = 188507879, upload-time = "2026-05-07T19:05:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/8f/af/9904ec6d3c93d9b24e5ec360445bbdf758b7f00bfbeedb89cb0eb64eb8bb/triton-3.7.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9b85e72968a9d8bba5ddb24e9b64aaabaf48affb042f2755cb7cfa92b7531ce", size = 201460637, upload-time = "2026-05-07T18:46:34.749Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f9/4835a8ea746b88727d8899f4e3ccce4f9cacb38abfc3bb0a638266c53111/triton-3.7.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18a160de426fd99f92b0baf509045360afbd3bfaa0b4a5171dde800ec9f09684", size = 188608706, upload-time = "2026-05-07T19:05:39.218Z" }, + { url = "https://files.pythonhosted.org/packages/c1/68/fa86e5a39608000f645535b2c124920126327ab731f8c4fafd5b07ff8d4b/triton-3.7.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce061073102714b725f3660ec6939d94a1da7984b3aa99c921417cae273672f5", size = 201546766, upload-time = "2026-05-07T18:46:42.088Z" }, +] + +[[package]] +name = "typer" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, +] + +[[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 = "webrtcvad" +version = "2.0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/34/e2de2d97f3288512b9ea56f92e7452f8207eb5a0096500badf9dfd48f5e6/webrtcvad-2.0.10.tar.gz", hash = "sha256:f1bed2fb25b63fb7b1a55d64090c993c9c9167b28485ae0bcdd81cf6ede96aea", size = 66156, upload-time = "2017-01-07T23:05:18.732Z" } diff --git a/clio-ai/transcription/.gitignore b/clio-ai/transcription/.gitignore new file mode 100644 index 0000000..505a3b1 --- /dev/null +++ b/clio-ai/transcription/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/clio-ai/transcription/.python-version b/clio-ai/transcription/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/clio-ai/transcription/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/clio-ai/transcription/README.md b/clio-ai/transcription/README.md new file mode 100644 index 0000000..e69de29 diff --git a/clio-ai/transcription/flake.lock b/clio-ai/transcription/flake.lock new file mode 100644 index 0000000..ae76b78 --- /dev/null +++ b/clio-ai/transcription/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1777954456, + "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/clio-ai/transcription/flake.nix b/clio-ai/transcription/flake.nix new file mode 100644 index 0000000..8062528 --- /dev/null +++ b/clio-ai/transcription/flake.nix @@ -0,0 +1,46 @@ +{ + description = "Audio transcription with faster-whisper (local models only)"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + + pythonEnv = pkgs.python313.withPackages (ps: with ps; [ + pip + ]); + in + { + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + pythonEnv + uv + ffmpeg + zlib + stdenv.cc.cc.lib + ]; + + shellHook = '' + export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [ + pkgs.zlib + pkgs.stdenv.cc.cc.lib + pkgs.ffmpeg + ]}:$LD_LIBRARY_PATH" + + # Ensure venv exists + if [ ! -d .venv ]; then + uv venv + fi + + echo "Transcription environment ready." + echo "Run: uv run python transcribe.py /path/to/recordings" + ''; + }; + } + ); +} diff --git a/clio-ai/transcription/main.py b/clio-ai/transcription/main.py new file mode 100644 index 0000000..9b9b0e7 --- /dev/null +++ b/clio-ai/transcription/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from view-recordings!") + + +if __name__ == "__main__": + main() diff --git a/clio-ai/transcription/pyproject.toml b/clio-ai/transcription/pyproject.toml new file mode 100644 index 0000000..e526327 --- /dev/null +++ b/clio-ai/transcription/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "view-recordings" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "faster-whisper>=1.2.1", + "openai-whisper>=20250625", +] diff --git a/clio-ai/transcription/transcribe.py b/clio-ai/transcription/transcribe.py new file mode 100644 index 0000000..7920846 --- /dev/null +++ b/clio-ai/transcription/transcribe.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +""" +Transcribe audio recordings using Whisper models via faster-whisper. + +For Chromebook (CPU-only): Uses 'tiny' or 'base' models which run reasonably fast. +For GPU (3090): Can use 'large-v3' for best quality. +""" + +import argparse +import json +import os +import sys +from datetime import timedelta +from pathlib import Path + + +def format_timestamp(seconds: float) -> str: + """Convert seconds to HH:MM:SS.mmm format.""" + td = timedelta(seconds=seconds) + hours, remainder = divmod(td.seconds, 3600) + minutes, secs = divmod(remainder, 60) + return f"{hours:02d}:{minutes:02d}:{secs:02d}.{int(td.microseconds/1000):03d}" + + +def transcribe_file(model, audio_path: Path, output_dir: Path, verbose: bool = False): + """Transcribe a single audio file and save results.""" + print(f"\nTranscribing: {audio_path.name}") + + segments, info = model.transcribe( + str(audio_path), + beam_size=5, + vad_filter=True, # Filter out non-speech sections + vad_parameters=dict( + min_silence_duration_ms=500, + speech_pad_ms=200, + ), + ) + + print(f" Detected language: {info.language} (probability: {info.language_probability:.2f})") + print(f" Duration: {info.duration:.1f}s") + + # Collect all segments + results = [] + full_text = [] + + for segment in segments: + results.append({ + "start": segment.start, + "end": segment.end, + "text": segment.text.strip(), + }) + full_text.append(segment.text.strip()) + + if verbose: + print(f" [{format_timestamp(segment.start)} -> {format_timestamp(segment.end)}] {segment.text.strip()}") + + # Save outputs + stem = audio_path.stem + + # Save JSON with timestamps + json_path = output_dir / f"{stem}.json" + with open(json_path, "w") as f: + json.dump({ + "source_file": str(audio_path), + "language": info.language, + "language_probability": info.language_probability, + "duration": info.duration, + "segments": results, + }, f, indent=2) + + # Save plain text + txt_path = output_dir / f"{stem}.txt" + with open(txt_path, "w") as f: + f.write("\n".join(full_text)) + + print(f" Saved: {json_path.name}, {txt_path.name}") + print(f" Found {len(results)} speech segments") + + return results + + +def main(): + parser = argparse.ArgumentParser(description="Transcribe audio files using Whisper") + parser.add_argument( + "input_dir", + type=Path, + nargs="?", + default=None, + help="Directory containing audio files", + ) + parser.add_argument( + "-o", "--output-dir", + type=Path, + default=None, + help="Output directory for transcriptions (default: input_dir/transcriptions)", + ) + parser.add_argument( + "-m", "--model", + default=os.getenv("WHISPER_MODEL", "large-v3"), + choices=["tiny", "base", "small", "medium", "large-v3"], + help="Whisper model size (default: large-v3, or WHISPER_MODEL env var)", + ) + parser.add_argument( + "--device", + default="auto", + choices=["auto", "cpu", "cuda"], + help="Device to use (auto detects GPU)", + ) + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Print each segment as it's transcribed", + ) + parser.add_argument( + "--file", + type=Path, + default=None, + help="Transcribe a single file instead of directory", + ) + + args = parser.parse_args() + + # Import here to show helpful error if not installed + try: + from faster_whisper import WhisperModel + except ImportError: + print("Error: faster-whisper not installed. Install with:") + print(" uv add faster-whisper") + print(" # or: pip install faster-whisper") + sys.exit(1) + + # Determine device + if args.device == "auto": + import torch + device = "cuda" if torch.cuda.is_available() else "cpu" + compute_type = "float16" if device == "cuda" else "int8" + elif args.device == "cuda": + device = "cuda" + compute_type = "float16" + else: + device = "cpu" + compute_type = "int8" + + print(f"Using device: {device}, compute type: {compute_type}") + print(f"Loading model: {args.model}") + + model = WhisperModel(args.model, device=device, compute_type=compute_type) + + # Handle single file or directory + if args.file: + audio_files = [args.file] + output_dir = args.output_dir or args.file.parent / "transcriptions" + elif args.input_dir: + input_dir = args.input_dir + if not input_dir.exists(): + print(f"Error: Input directory does not exist: {input_dir}") + sys.exit(1) + + audio_files = sorted( + p for p in input_dir.iterdir() + if p.suffix.lower() in {".ogg", ".mp3", ".wav", ".m4a", ".flac", ".webm"} + ) + output_dir = args.output_dir or input_dir / "transcriptions" + else: + print("Error: Must specify either input_dir or --file") + sys.exit(1) + + if not audio_files: + print("No audio files found") + sys.exit(1) + + output_dir.mkdir(parents=True, exist_ok=True) + + print(f"Found {len(audio_files)} audio file(s)") + print(f"Output directory: {output_dir}") + + for audio_path in audio_files: + try: + transcribe_file(model, audio_path, output_dir, verbose=args.verbose) + except Exception as e: + print(f" Error transcribing {audio_path.name}: {e}") + + print("\nDone!") + + +if __name__ == "__main__": + main() diff --git a/clio-ai/transcription/uv.lock b/clio-ai/transcription/uv.lock new file mode 100644 index 0000000..5f5529f --- /dev/null +++ b/clio-ai/transcription/uv.lock @@ -0,0 +1,1146 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[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 = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +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 = "av" +version = "17.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/f0/8c8dca97ae0cf00e8e2a53bb5cb9aca5fd484f585ef3e9b412200aff3ebd/av-17.0.1.tar.gz", hash = "sha256:fbcbd4aa43bca6a8691816283112d1659a27f407bbeb66d1397023691339f5d4", size = 4411938, upload-time = "2026-04-18T17:12:34.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/82/e7007dcef7bd2d2c377e2e85977701384f42d19fc808c2ccb3a99eaf58f2/av-17.0.1-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:987f4f46ceae4da6c614dcbd2b8149be9dbf680c3bb7a6841c58af9cff4d9230", size = 23238802, upload-time = "2026-04-18T17:11:51.166Z" }, + { url = "https://files.pythonhosted.org/packages/6b/aa/858b09a08ea6f83f91be44b5a5adad13ae8d9ac8b80fda27e73c24bfb160/av-17.0.1-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:d97f54e55b18a74912f479c1978aadd1341d38d892dee95bb5c2f2dccfa72f32", size = 18709338, upload-time = "2026-04-18T17:11:53.286Z" }, + { url = "https://files.pythonhosted.org/packages/a8/8b/8de3fd21c4b0b74d44337421abeab0e71462337fb6a28fff888e0c356cbd/av-17.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e6eee84afa48d0e9321047cd3e4facd44b401493f6bdc753e2e1d1e7c9e6d13e", size = 34007351, upload-time = "2026-04-18T17:11:56.116Z" }, + { url = "https://files.pythonhosted.org/packages/02/28/167b291356c2cc315a2d62a95b0ceace72b5b0bf547de30b89313110f032/av-17.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c58c71bffd9383908c85695ac61d3184c668accb04a5bd1b262e0fb8d09f60a5", size = 36345295, upload-time = "2026-04-18T17:11:59.125Z" }, + { url = "https://files.pythonhosted.org/packages/04/fa/aae56f2ff2c204c408641e1120f5ca5ce9c3390cf5362245c6f1158704b5/av-17.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:42d6745d30a410ec9b22aef79a52a7ab5a001eb8f5adfd952946606a30983318", size = 35183754, upload-time = "2026-04-18T17:12:01.697Z" }, + { url = "https://files.pythonhosted.org/packages/ba/bd/776046f27093aef80155a204ca7d82a887ae4ee72ba4ef8411b46ea7898c/av-17.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3ed6bcd7021fe55832f95b8ef78dd01a4cb21faf3cd71f1e1bf4f20bf100b278", size = 37430809, upload-time = "2026-04-18T17:12:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d5/3261bd2c6b7f6c0aa8379fc970d1ecf496330990b992ad28607785074268/av-17.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:9af524e8632a54032e361d6b88895bd3e7c6212ca560de60f5ccc525323c764c", size = 28889649, upload-time = "2026-04-18T17:12:07.04Z" }, + { url = "https://files.pythonhosted.org/packages/98/39/381104e427a0c7231d2ec0d25d538d58fc20fc0458846b95860d3ef8073b/av-17.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:50e58a473d65ea29b645e45c9fd8518a6783737135683ecc40571a91592bdfe4", size = 21918412, upload-time = "2026-04-18T17:12:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8c/bb1498f031abb6157b30b7fc2379359176953821b6ba59fbd89dbb56f61f/av-17.0.1-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:1d33871742d1e71562db3c8e752cacc5a62766d7efc3ae408bff1c3e26ebb46e", size = 23484157, upload-time = "2026-04-18T17:12:11.67Z" }, + { url = "https://files.pythonhosted.org/packages/1a/58/dedaef187b797243cd5762722e376c69c5ad95ab23db44127f09afc2cd66/av-17.0.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:1229e879f4b6431bc00f69d7f8891fe9a683b0a6e0e009e6c98eb7e449f0383d", size = 18920872, upload-time = "2026-04-18T17:12:14.826Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/5c550231651d6285e6a5c4f6f4a0e67459bfe2b622a7c9352be8cca8c819/av-17.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4744837f4116964280bcc72285e3cdd51361e98a696205aadd924203440ef511", size = 37471077, upload-time = "2026-04-18T17:12:17.349Z" }, + { url = "https://files.pythonhosted.org/packages/59/e4/9807b89a9d775c6f015677996c48bce48aaff70b5d95885adf39e59832a2/av-17.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3d0a7d45d9599bf9df9f8249827113d4f36df1cd6b5356227b997f0552dbc98e", size = 39566981, upload-time = "2026-04-18T17:12:19.942Z" }, + { url = "https://files.pythonhosted.org/packages/5c/72/a22a657abc3de652f5b4f46cbbebdf7cba629752112791b81f05d340991d/av-17.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9acd0b6a6e02af2b37f63d97a03ee2c47936d58e82425c3cd075a95245937c59", size = 38397369, upload-time = "2026-04-18T17:12:22.909Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b2/f4e83e41c1e3c186f34b7df506779d0cd7e40499e2e19519c7ece148cd20/av-17.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3d3a36204cb1f1e7691e6446afa8d6b7097b09946dae732c71c5d05ce09e506e", size = 40582445, upload-time = "2026-04-18T17:12:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/c8/59/8676188b72eed09d48ce6cfaf0f22b0bb9f3cfd74d388ee2b7fdf960536d/av-17.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:b87b98afe971cde123953073bc9c95ab0b7efd2ecc082dd2dbd11f9d9abf190e", size = 29217136, upload-time = "2026-04-18T17:12:29.189Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/0a6e1d2a845988039f6c197fa7269b5e9abbe17354fb41cc9d75bb260fcb/av-17.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a87a42c36e29f75e7dff7281944f2a6876a2c8875e225ccbf6c1ae62748b4caa", size = 22072676, upload-time = "2026-04-18T17:12:31.836Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[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/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { 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.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[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 = "ctranslate2" +version = "4.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pyyaml" }, + { name = "setuptools" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/6d/eb49ba05db286b4ea9d5d3fcf5f5cd0a9a5e218d46349618d5041001e303/ctranslate2-4.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b2abf2929756e3ec6246057b56df379995661560a2d776af05f9d97f63afcf5", size = 1256960, upload-time = "2026-02-04T06:11:47.487Z" }, + { url = "https://files.pythonhosted.org/packages/45/5a/b9cce7b00d89fc6fdeaf27587aa52d0597b465058563e93ff50910553bdd/ctranslate2-4.7.1-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:857ef3959d6b1c40dc227c715a36db33db2d097164996d6c75b6db8e30828f52", size = 11918645, upload-time = "2026-02-04T06:11:49.599Z" }, + { url = "https://files.pythonhosted.org/packages/ea/03/c0db0a5276599fb44ceafa2f2cb1afd5628808ec406fe036060a39693680/ctranslate2-4.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:393a9e7e989034660526a2c0e8bb65d1924f43d9a5c77d336494a353d16ba2a4", size = 16860452, upload-time = "2026-02-04T06:11:52.276Z" }, + { url = "https://files.pythonhosted.org/packages/0b/03/4e3728ce29d192ee75ed9a2d8589bf4f19edafe5bed3845187de51b179a3/ctranslate2-4.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a3d0682f2b9082e31c73d75b45f16cde77355ab76d7e8356a24c3cb2480a6d3", size = 38995174, upload-time = "2026-02-04T06:11:55.477Z" }, + { url = "https://files.pythonhosted.org/packages/9b/15/6e8e87c6a201d69803a79ac2e29623ce7c2cc9cd1df9db99810cca714373/ctranslate2-4.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:baa6d2b10f57933d8c11791e8522659217918722d07bbef2389a443801125fe7", size = 18844953, upload-time = "2026-02-04T06:11:58.519Z" }, + { url = "https://files.pythonhosted.org/packages/fd/73/8a6b7ba18cad0c8667ee221ddab8c361cb70926440e5b8dd0e81924c28ac/ctranslate2-4.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d5dfb076566551f4959dfd0706f94c923c1931def9b7bb249a2caa6ab23353a0", size = 1257560, upload-time = "2026-02-04T06:12:00.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/c2/8817ca5d6c1b175b23a12f7c8b91484652f8718a76353317e5919b038733/ctranslate2-4.7.1-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:eecdb4ed934b384f16e8c01b185b082d6b5ffc7dcbb0b6a6eb48cd465282d957", size = 11918995, upload-time = "2026-02-04T06:12:02.875Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/b8eb3acc67bbca4d9872fc9ff94db78e6167a7ba5cd932f585d1560effc7/ctranslate2-4.7.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1aa6796edcc3c8d163c9e39c429d50076d266d68980fed9d1b2443f617c67e9e", size = 16844162, upload-time = "2026-02-04T06:12:05.099Z" }, + { url = "https://files.pythonhosted.org/packages/80/11/6474893b07121057035069a0a483fe1cd8c47878213f282afb4c0c6fc275/ctranslate2-4.7.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24c0482c51726430fb83724451921c0e539d769c8618dcfd46b1645e7f75960d", size = 38966728, upload-time = "2026-02-04T06:12:07.923Z" }, + { url = "https://files.pythonhosted.org/packages/94/88/8fc7ff435c5e783e5fad9586d839d463e023988dbbbad949d442092d01f1/ctranslate2-4.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:76db234c0446a23d20dd8eeaa7a789cc87d1d05283f48bf3152bae9fa0a69844", size = 19100788, upload-time = "2026-02-04T06:12:10.592Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b3/f100013a76a98d64e67c721bd4559ea4eeb54be3e4ac45f4d801769899af/ctranslate2-4.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:058c9db2277dc8b19ecc86c7937628f69022f341844b9081d2ab642965d88fc6", size = 1280179, upload-time = "2026-02-04T06:12:12.596Z" }, + { url = "https://files.pythonhosted.org/packages/39/22/b77f748015667a5e2ca54a5ee080d7016fce34314f0e8cf904784549305a/ctranslate2-4.7.1-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:5abcf885062c7f28a3f9a46be8d185795e8706ac6230ad086cae0bc82917df31", size = 11940166, upload-time = "2026-02-04T06:12:14.054Z" }, + { url = "https://files.pythonhosted.org/packages/7d/78/6d7fd52f646c6ba3343f71277a9bbef33734632949d1651231948b0f0359/ctranslate2-4.7.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9950acb04a002d5c60ae90a1ddceead1a803af1f00cadd9b1a1dc76e1f017481", size = 16849483, upload-time = "2026-02-04T06:12:17.082Z" }, + { url = "https://files.pythonhosted.org/packages/40/27/58769ff15ac31b44205bd7a8aeca80cf7357c657ea5df1b94ce0f5c83771/ctranslate2-4.7.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1dcc734e92e3f1ceeaa0c42bbfd009352857be179ecd4a7ed6cccc086a202f58", size = 38949393, upload-time = "2026-02-04T06:12:21.302Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5c/9fa0ad6462b62efd0fb5ac1100eee47bc96ecc198ff4e237c731e5473616/ctranslate2-4.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:dfb7657bdb7b8211c8f9ecb6f3b70bc0db0e0384d01a8b1808cb66fe7199df59", size = 19123451, upload-time = "2026-02-04T06:12:24.115Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" }, + { url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" }, + { url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/d0/c177e29701cf1d3008d7d2b16b5fc626592ce13bd535f8795c5f57187e0e/cuda_pathfinder-1.5.4-py3-none-any.whl", hash = "sha256:9563d3175ce1828531acf4b94e1c1c7d67208c347ca002493e2654878b26f4b7", size = 51657, upload-time = "2026-04-27T22:42:07.712Z" }, +] + +[[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 = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[package]] +name = "faster-whisper" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "av" }, + { name = "ctranslate2" }, + { name = "huggingface-hub" }, + { name = "onnxruntime" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/99/49ee85903dee060d9f08297b4a342e5e0bcfca2f027a07b4ee0a38ab13f9/faster_whisper-1.2.1-py3-none-any.whl", hash = "sha256:79a66ad50688c0b794dd501dc340a736992a6342f7f95e5811be60b5224a26a7", size = 1118909, upload-time = "2025-10-31T11:35:47.794Z" }, +] + +[[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 = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, +] + +[[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 = "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/72/43/724d307b34e353da0abd476e02f72f735cdd2bc86082dee1b32ea0bfee1d/hf_xet-1.4.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7551659ba4f1e1074e9623996f28c3873682530aee0a846b7f2f066239228144", size = 3800935, upload-time = "2026-03-31T22:39:49.618Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d2/8bee5996b699262edb87dbb54118d287c0e1b2fc78af7cdc41857ba5e3c4/hf_xet-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bee693ada985e7045997f05f081d0e12c4c08bd7626dc397f8a7c487e6c04f7f", size = 3558942, upload-time = "2026-03-31T22:39:47.938Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a1/e993d09cbe251196fb60812b09a58901c468127b7259d2bf0f68bf6088eb/hf_xet-1.4.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21644b404bb0100fe3857892f752c4d09642586fd988e61501c95bbf44b393a3", size = 4207657, upload-time = "2026-03-31T22:39:39.69Z" }, + { url = "https://files.pythonhosted.org/packages/64/44/9eb6d21e5c34c63e5e399803a6932fa983cabdf47c0ecbcfe7ea97684b8c/hf_xet-1.4.3-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:987f09cfe418237812896a6736b81b1af02a3a6dcb4b4944425c4c4fca7a7cf8", size = 3986765, upload-time = "2026-03-31T22:39:37.936Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/8ad6f16fdb82f5f7284a34b5ec48645bd575bdcd2f6f0d1644775909c486/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:60cf7fc43a99da0a853345cf86d23738c03983ee5249613a6305d3e57a5dca74", size = 4188162, upload-time = "2026-03-31T22:39:58.382Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c4/39d6e136cbeea9ca5a23aad4b33024319222adbdc059ebcda5fc7d9d5ff4/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2815a49a7a59f3e2edf0cf113ae88e8cb2ca2a221bf353fb60c609584f4884d4", size = 4424525, upload-time = "2026-03-31T22:40:00.225Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/adc32dae6bdbc367853118b9878139ac869419a4ae7ba07185dc31251b76/hf_xet-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:42ee323265f1e6a81b0e11094564fb7f7e0ec75b5105ffd91ae63f403a11931b", size = 3671610, upload-time = "2026-03-31T22:40:10.42Z" }, + { url = "https://files.pythonhosted.org/packages/e2/19/25d897dcc3f81953e0c2cde9ec186c7a0fee413eb0c9a7a9130d87d94d3a/hf_xet-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:27c976ba60079fb8217f485b9c5c7fcd21c90b0367753805f87cb9f3cdc4418a", size = 3528529, upload-time = "2026-03-31T22:40:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/ec/36/3e8f85ca9fe09b8de2b2e10c63b3b3353d7dda88a0b3d426dffbe7b8313b/hf_xet-1.4.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5251d5ece3a81815bae9abab41cf7ddb7bcb8f56411bce0827f4a3071c92fdc6", size = 3801019, upload-time = "2026-03-31T22:39:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9c/defb6cb1de28bccb7bd8d95f6e60f72a3d3fa4cb3d0329c26fb9a488bfe7/hf_xet-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1feb0f3abeacee143367c326a128a2e2b60868ec12a36c225afb1d6c5a05e6d2", size = 3558746, upload-time = "2026-03-31T22:39:54.766Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/8d001191893178ff8e826e46ad5299446e62b93cd164e17b0ffea08832ec/hf_xet-1.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b301fc150290ca90b4fccd079829b84bb4786747584ae08b94b4577d82fb791", size = 4207692, upload-time = "2026-03-31T22:39:46.246Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/6790b402803250e9936435613d3a78b9aaeee7973439f0918848dde58309/hf_xet-1.4.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d972fbe95ddc0d3c0fc49b31a8a69f47db35c1e3699bf316421705741aab6653", size = 3986281, upload-time = "2026-03-31T22:39:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/51/56/ea62552fe53db652a9099eda600b032d75554d0e86c12a73824bfedef88b/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c5b48db1ee344a805a1b9bd2cda9b6b65fe77ed3787bd6e87ad5521141d317cd", size = 4187414, upload-time = "2026-03-31T22:40:04.951Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f5/bc1456d4638061bea997e6d2db60a1a613d7b200e0755965ec312dc1ef79/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:22bdc1f5fb8b15bf2831440b91d1c9bbceeb7e10c81a12e8d75889996a5c9da8", size = 4424368, upload-time = "2026-03-31T22:40:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/ab597bae87e1f06d18d3ecb8ed7f0d3c9a37037fc32ce76233d369273c64/hf_xet-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0392c79b7cf48418cd61478c1a925246cf10639f4cd9d94368d8ca1e8df9ea07", size = 3672280, upload-time = "2026-03-31T22:40:16.401Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/2e462d34e23a09a74d73785dbed71cc5dbad82a72eee2ad60a72a554155d/hf_xet-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:681c92a07796325778a79d76c67011764ecc9042a8c3579332b61b63ae512075", size = 3528945, upload-time = "2026-03-31T22:40:14.995Z" }, + { 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" }, + { name = "h11" }, +] +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 = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +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 = "huggingface-hub" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/ff/ec7ed2eb43bd7ce8bb2233d109cc235c3e807ffe5e469dc09db261fac05e/huggingface_hub-1.13.0.tar.gz", hash = "sha256:f6df2dac5abe82ce2fe05873d10d5ff47bc677d616a2f521f4ee26db9415d9d0", size = 781788, upload-time = "2026-04-30T11:57:33.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/db/4b1cdae9460ae1f3ca020cd767f013430ce23eb1d9c890ae3a0609b38d26/huggingface_hub-1.13.0-py3-none-any.whl", hash = "sha256:e942cb50d6a08dd5306688b1ac05bda157fd2fcc88b63dae405f7bd0d3234005", size = 660643, upload-time = "2026-04-30T11:57:31.802Z" }, +] + +[[package]] +name = "idna" +version = "3.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +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 = "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/77/6f/4615353e016799f80fa52ccb270a843c413b22361fadda2589b2922fb9b0/llvmlite-0.47.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a3c6a735d4e1041808434f9d440faa3d78d9b4af2ee64d05a66f351883b6ceec", size = 37232771, upload-time = "2026-03-31T18:29:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/31/b8/69f5565f1a280d032525878a86511eebed0645818492feeb169dfb20ae8e/llvmlite-0.47.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2699a74321189e812d476a43d6d7f652f51811e7b5aad9d9bba842a1c7927acb", size = 56275178, upload-time = "2026-03-31T18:29:05.748Z" }, + { url = "https://files.pythonhosted.org/packages/d6/da/b32cafcb926fb0ce2aa25553bf32cb8764af31438f40e2481df08884c947/llvmlite-0.47.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c6951e2b29930227963e53ee152441f0e14be92e9d4231852102d986c761e40", size = 55128632, upload-time = "2026-03-31T18:29:11.235Z" }, + { url = "https://files.pythonhosted.org/packages/46/9f/4898b44e4042c60fafcb1162dfb7014f6f15b1ec19bf29cfea6bf26df90d/llvmlite-0.47.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2e9adf8698d813a9a5efb2d4370caf344dbc1e145019851fee6a6f319ba760e", size = 38138695, upload-time = "2026-03-31T18:29:15.43Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d4/33c8af00f0bf6f552d74f3a054f648af2c5bc6bece97972f3bfadce4f5ec/llvmlite-0.47.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:de966c626c35c9dff5ae7bf12db25637738d0df83fc370cf793bc94d43d92d14", size = 37232773, upload-time = "2026-03-31T18:29:19.453Z" }, + { url = "https://files.pythonhosted.org/packages/64/1d/a760e993e0c0ba6db38d46b9f48f6c7dceb8ac838824997fb9e25f97bc04/llvmlite-0.47.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ddbccff2aeaff8670368340a158abefc032fe9b3ccf7d9c496639263d00151aa", size = 56275176, upload-time = "2026-03-31T18:29:24.149Z" }, + { url = "https://files.pythonhosted.org/packages/84/3b/e679bc3b29127182a7f4aa2d2e9e5bea42adb93fb840484147d59c236299/llvmlite-0.47.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a7b778a2e144fc64468fb9bf509ac1226c9813a00b4d7afea5d988c4e22fca", size = 55128631, upload-time = "2026-03-31T18:29:29.536Z" }, + { url = "https://files.pythonhosted.org/packages/be/f7/19e2a09c62809c9e63bbd14ce71fb92c6ff7b7b3045741bb00c781efc3c9/llvmlite-0.47.0-cp314-cp314-win_amd64.whl", hash = "sha256:694e3c2cdc472ed2bd8bd4555ca002eec4310961dd58ef791d508f57b5cc4c94", size = 39153826, upload-time = "2026-03-31T18:29:33.681Z" }, + { url = "https://files.pythonhosted.org/packages/40/a1/581a8c707b5e80efdbbe1dd94527404d33fe50bceb71f39d5a7e11bd57b7/llvmlite-0.47.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:92ec8a169a20b473c1c54d4695e371bde36489fc1efa3688e11e99beba0abf9c", size = 37232772, upload-time = "2026-03-31T18:29:37.952Z" }, + { url = "https://files.pythonhosted.org/packages/11/03/16090dd6f74ba2b8b922276047f15962fbeea0a75d5601607edb301ba945/llvmlite-0.47.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa1cbd800edd3b20bc141521f7fd45a6185a5b84109aa6855134e81397ffe72b", size = 56275178, upload-time = "2026-03-31T18:29:42.58Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cb/0abf1dd4c5286a95ffe0c1d8c67aec06b515894a0dd2ac97f5e27b82ab0b/llvmlite-0.47.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6725179b89f03b17dabe236ff3422cb8291b4c1bf40af152826dfd34e350ae8", size = 55128632, upload-time = "2026-03-31T18:29:46.939Z" }, + { url = "https://files.pythonhosted.org/packages/4f/79/d3bbab197e86e0ff4f9c07122895b66a3e0d024247fcff7f12c473cb36d9/llvmlite-0.47.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6842cf6f707ec4be3d985a385ad03f72b2d724439e118fcbe99b2929964f0453", size = 39153839, upload-time = "2026-03-31T18:29:51.004Z" }, +] + +[[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/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[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 = "more-itertools" +version = "11.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804", size = 144659, upload-time = "2026-04-09T15:01:33.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4", size = 71939, upload-time = "2026-04-09T15:01:32.21Z" }, +] + +[[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 = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +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.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/c5/db2ac3685833d626c0dcae6bd2330cd68433e1fd248d15f70998160d3ad7/numba-0.65.1.tar.gz", hash = "sha256:19357146c32fe9ed25059ab915e8465fb13951cf6b0aace3826b76886373ab23", size = 2765600, upload-time = "2026-04-24T02:02:56.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/37/14a4579049c1eb673afd0de0cb4842982acd55b9ce2643e763db858bcea0/numba-0.65.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:1735c15c1134a5108b4d6a5c77fc0947924ea066a738dc09a52008c13df9cad3", size = 2681344, upload-time = "2026-04-24T02:02:33.65Z" }, + { url = "https://files.pythonhosted.org/packages/a0/22/b8d873f6466b20aa563fc9b33acd48dec89a07803ddaa2f1c8ca1cd33126/numba-0.65.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c09f49117ef255e1f1c6dad0c7a1ed39868243862a73be5706793241a3755f1b", size = 3810619, upload-time = "2026-04-24T02:02:36.041Z" }, + { url = "https://files.pythonhosted.org/packages/62/08/e16a8b5d9a018962ebb5c66be662317cde32b9f5dab08441f90bed5522fb/numba-0.65.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:594a8680b3fadac99e97e489b1fd89007177e5336713745c3b769528c635a464", size = 3509783, upload-time = "2026-04-24T02:02:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a5/03c970d57f4c1741354837353ce39fb5206952ae1dba8922d29c86f64805/numba-0.65.1-cp313-cp313-win_amd64.whl", hash = "sha256:85be74c0d036842699a30058f82fb88fc5ffdc59f7615cab5792ea92914c9b62", size = 2750534, upload-time = "2026-04-24T02:02:39.903Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2e/8aed9b726d9ba5f11ad287645fd479e88278db3060a25cb1225d730eb2b7/numba-0.65.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:33f5eb68eb1c843511615d14663ce60258525d6a4c65ab040e2c2b0c4cf17450", size = 2681554, upload-time = "2026-04-24T02:02:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/87/96/f3eb235fafa82a34e2ab5dd7dc9ffff998ebf5f0bbc23fa56a96aeb44da6/numba-0.65.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71e73029bf53a62cc6afcf96be4bd942290d8b4c55f0a454fb536158115790f7", size = 3779602, upload-time = "2026-04-24T02:02:43.726Z" }, + { url = "https://files.pythonhosted.org/packages/09/90/b0f09b48752d23640b8284f22aa597737e8adaddc7fbfacc4708b7f73a4c/numba-0.65.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a07635e0be926b9bdbffb09137c230fb13f6ec0e564914ba937cee12ce3eb35", size = 3479532, upload-time = "2026-04-24T02:02:45.427Z" }, + { url = "https://files.pythonhosted.org/packages/56/46/3f7fc04fb853559e74b210e0b62c19974ec844cefec611f9e535f4da3761/numba-0.65.1-cp314-cp314-win_amd64.whl", hash = "sha256:2a20fcdabdefbdacf88d85caf70c3b18c4bcb7ebb8f82e6a19486383dd26ab63", size = 2752637, upload-time = "2026-04-24T02:02:47.664Z" }, + { url = "https://files.pythonhosted.org/packages/81/7b/c1a341a9067367778f4152a5f01061cf281fb09582c92c510ec4918cabf6/numba-0.65.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:548dd4b3a4508d5062768d1514b2cd7b015f9a25ec7af651c50dee243965e652", size = 2684600, upload-time = "2026-04-24T02:02:49.653Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/98ddbcf3e4f04a6dd07e1c67249955920579ba4af6bb6868e3088f4ed282/numba-0.65.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:78abc28feff2c2ff8307fff3975b6438352759c9acb797ecd6b1fb6e7e39e31d", size = 3817198, upload-time = "2026-04-24T02:02:51.266Z" }, + { url = "https://files.pythonhosted.org/packages/a3/83/0dad21057ece5a835599f5d24099b091703995e23dbbf894f259e91c010b/numba-0.65.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7676cb389555805f9b9a1840cbcd1ea6c8bd5376ab6918e3a29c5ea1dbda20", size = 3533862, upload-time = "2026-04-24T02:02:52.987Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/8be7118ffd4c8440881046eac3d0982cc5ab42909508cf5d67024d62a2e4/numba-0.65.1-cp314-cp314t-win_amd64.whl", hash = "sha256:20609346e3bd75204950dcbbfe383a8d7dbf4902f442aedbf00f97fef4aa8f38", size = 2758237, upload-time = "2026-04-24T02:02:54.612Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +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/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, +] + +[[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" }, +] +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-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +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" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, +] +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" }, +] +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-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 = "onnxruntime" +version = "1.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/ee/db3ac55ef770347a926ac0f1317df0ab42c8bc604350833b30c7356bf936/onnxruntime-1.25.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e9d9b3b1694196bc3c5bc66f760a237a5e27d7688aaa2e2c9c0f66abd0486699", size = 17770761, upload-time = "2026-04-27T21:59:54.853Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9a/33225481a94a59906fce44e27ab12fc3bddd2aaecdc6160bd73341ca1aba/onnxruntime-1.25.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:311d29b943e46a55ca72ca1ea48d7815c993122bfc359f68215fddeb9583fff4", size = 15871542, upload-time = "2026-04-27T21:59:41.881Z" }, + { url = "https://files.pythonhosted.org/packages/8b/09/f20aac60f6fcf840543be54d4e9252cfeb7e8c2bb6d22477aaeb180e763e/onnxruntime-1.25.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98016a038b31160db23208706139fa3b99cd60bc1c5ffdade77aafd6a37a92ad", size = 18036960, upload-time = "2026-04-27T22:00:10.739Z" }, + { url = "https://files.pythonhosted.org/packages/50/83/47964ac7e2f7e2f9e83c69ec466642c6835466252cc2ef0561eafeb56b66/onnxruntime-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:08717d6eee2820807ba60b1b17032af207bd7aaca5b6c4abaee71f83feae877b", size = 12904886, upload-time = "2026-04-27T22:00:39.878Z" }, + { url = "https://files.pythonhosted.org/packages/d4/6c/a6c5aea47dc95fca7728f8a5af67c184ec9e7d4e7882125c7062e4bba8dd/onnxruntime-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:84f8963d70e00167bae273ab7e80e9795bfc5eb94f6b23236a99c5c11af00844", size = 12634117, upload-time = "2026-04-27T22:00:29.15Z" }, + { url = "https://files.pythonhosted.org/packages/a8/8a/3b65e7911eec86c125e3d6f43d690a6f68671500543c0390ecd6eb59b771/onnxruntime-1.25.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03e800b3a4b48d9f3a2d23aacc4fa95486a3b406b14e51d1a9b8b6981d9adf9c", size = 15882935, upload-time = "2026-04-27T21:59:44.912Z" }, + { url = "https://files.pythonhosted.org/packages/3c/bb/410a760694f8ae7bbfc5fa81ccbeb7da241e6d520ee02a333a439cf462a2/onnxruntime-1.25.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd83ef5c10cfc051a1cb465db692d57b996a1bc75a2a97b161398e29cdbc47ff", size = 18021727, upload-time = "2026-04-27T22:00:13.846Z" }, + { url = "https://files.pythonhosted.org/packages/fb/aa/04530bd38e31e26970fa1212346d76cf81705dc16a8ee5e6f4fb24634c11/onnxruntime-1.25.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:395eb662c437fa2407f44266e4778b75bff261b17c2a6fef042421f9069f871d", size = 17773721, upload-time = "2026-04-27T21:59:59.24Z" }, + { url = "https://files.pythonhosted.org/packages/ef/7f/ec79ab5cece6a688c944a7fa214a8511d548b9d5142a15d1a3d730b705f1/onnxruntime-1.25.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ae85395f41b291ae3e61780ec5092640181d369ef6c268aa8141c478b509e69", size = 15875954, upload-time = "2026-04-27T21:59:49.394Z" }, + { url = "https://files.pythonhosted.org/packages/67/fe/20428215d822099ea2c1e3cf35c295cf1a58f467bf18b6c607597a39c18a/onnxruntime-1.25.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:828e1b12710fbedb6dfab5e7bae6f11563617cddf3c2e7e8d84c64de566a4a3a", size = 18038703, upload-time = "2026-04-27T22:00:16.199Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b1/b15db965e6a68bc47ca7eb584de4e6b3d2d2f484d46cc57f715b596f6528/onnxruntime-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:2affc9d2fd9ab013b9c9637464e649a0cca870d57ae18bfef74180eee65c3369", size = 13218513, upload-time = "2026-04-27T22:00:42.506Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f9/25cd2d1b29cdc8140eee4afbb6fb930b69125526632b1d579bc747975306/onnxruntime-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:3387d75d1a815b4b2495b4e47a05ef1b3bcb64a817ddc68587e0bfcb9702bcf6", size = 12969835, upload-time = "2026-04-27T22:00:31.504Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0e/6c507d1e65b2421fb44e241cbba577c7276792279485024fb1752b43f5c5/onnxruntime-1.25.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06280b06604660595037f783c6d24bc70cbe5c6093975f194cd1482e77d450de", size = 15883298, upload-time = "2026-04-27T21:59:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/df/4e/1c9df57496409dc86b320bd38f29ad7a34b7115e4f35b8fca44a827568a7/onnxruntime-1.25.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e79fd5ce7db10ebcc24e020e2ed0159476e69e2326b9b7828e5aadcf6184212", size = 18021249, upload-time = "2026-04-27T22:00:18.954Z" }, +] + +[[package]] +name = "openai-whisper" +version = "20250625" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, + { name = "numba" }, + { name = "numpy" }, + { name = "tiktoken" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "triton", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'linux2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/8e/d36f8880bcf18ec026a55807d02fe4c7357da9f25aebd92f85178000c0dc/openai_whisper-20250625.tar.gz", hash = "sha256:37a91a3921809d9f44748ffc73c0a55c9f366c85a3ef5c2ae0cc09540432eb96", size = 803191, upload-time = "2025-06-26T01:06:13.34Z" } + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[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 = "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 = "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/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[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/9d/83/c4373bc5f31f2cf4b66f9b7c31005bd87fe66f0dce17701f7db4ee79ee29/regex-2026.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62f5519042c101762509b1d717b45a69c0139d60414b3c604b81328c01bd1943", size = 490273, upload-time = "2026-04-03T20:54:11.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/f8/fe62afbcc3cf4ad4ac9adeaafd98aa747869ae12d3e8e2ac293d0593c435/regex-2026.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3790ba9fb5dd76715a7afe34dbe603ba03f8820764b1dc929dd08106214ed031", size = 291954, upload-time = "2026-04-03T20:54:13.412Z" }, + { url = "https://files.pythonhosted.org/packages/5a/92/4712b9fe6a33d232eeb1c189484b80c6c4b8422b90e766e1195d6e758207/regex-2026.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fae3c6e795d7678963f2170152b0d892cf6aee9ee8afc8c45e6be38d5107fe7", size = 289487, upload-time = "2026-04-03T20:54:15.824Z" }, + { url = "https://files.pythonhosted.org/packages/88/2c/f83b93f85e01168f1070f045a42d4c937b69fdb8dd7ae82d307253f7e36e/regex-2026.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:298c3ec2d53225b3bf91142eb9691025bab610e0c0c51592dde149db679b3d17", size = 796646, upload-time = "2026-04-03T20:54:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/df/55/61a2e17bf0c4dc57e11caf8dd11771280d8aaa361785f9e3bc40d653f4a7/regex-2026.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9638791082eaf5b3ac112c587518ee78e083a11c4b28012d8fe2a0f536dfb17", size = 865904, upload-time = "2026-04-03T20:54:20.019Z" }, + { url = "https://files.pythonhosted.org/packages/45/32/1ac8ed1b5a346b5993a3d256abe0a0f03b0b73c8cc88d928537368ac65b6/regex-2026.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae3e764bd4c5ff55035dc82a8d49acceb42a5298edf6eb2fc4d328ee5dd7afae", size = 912304, upload-time = "2026-04-03T20:54:22.403Z" }, + { url = "https://files.pythonhosted.org/packages/26/47/2ee5c613ab546f0eddebf9905d23e07beb933416b1246c2d8791d01979b4/regex-2026.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa81f81b80047ba89a3c69ae6a0f78d06f4a42ce5126b0eb2a0a10ad44e0b2e", size = 801126, upload-time = "2026-04-03T20:54:24.308Z" }, + { url = "https://files.pythonhosted.org/packages/75/cd/41dacd129ca9fd20bd7d02f83e0fad83e034ac8a084ec369c90f55ef37e2/regex-2026.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f56ebf9d70305307a707911b88469213630aba821e77de7d603f9d2f0730687d", size = 776772, upload-time = "2026-04-03T20:54:26.319Z" }, + { url = "https://files.pythonhosted.org/packages/89/6d/5af0b588174cb5f46041fa7dd64d3fd5cd2fe51f18766703d1edc387f324/regex-2026.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:773d1dfd652bbffb09336abf890bfd64785c7463716bf766d0eb3bc19c8b7f27", size = 785228, upload-time = "2026-04-03T20:54:28.387Z" }, + { url = "https://files.pythonhosted.org/packages/b7/3b/f5a72b7045bd59575fc33bf1345f156fcfd5a8484aea6ad84b12c5a82114/regex-2026.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d51d20befd5275d092cdffba57ded05f3c436317ee56466c8928ac32d960edaf", size = 860032, upload-time = "2026-04-03T20:54:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/39/a4/72a317003d6fcd7a573584a85f59f525dfe8f67e355ca74eb6b53d66a5e2/regex-2026.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0a51cdb3c1e9161154f976cb2bef9894bc063ac82f31b733087ffb8e880137d0", size = 765714, upload-time = "2026-04-03T20:54:32.789Z" }, + { url = "https://files.pythonhosted.org/packages/25/1e/5672e16f34dbbcb2560cc7e6a2fbb26dfa8b270711e730101da4423d3973/regex-2026.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae5266a82596114e41fb5302140e9630204c1b5f325c770bec654b95dd54b0aa", size = 852078, upload-time = "2026-04-03T20:54:34.546Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/c813f0af7c6cc7ed7b9558bac2e5120b60ad0fa48f813e4d4bd55446f214/regex-2026.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c882cd92ec68585e9c1cf36c447ec846c0d94edd706fe59e0c198e65822fd23b", size = 789181, upload-time = "2026-04-03T20:54:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/a344608d1adbd2a95090ddd906cec09a11be0e6517e878d02a5123e0917f/regex-2026.4.4-cp313-cp313-win32.whl", hash = "sha256:05568c4fbf3cb4fa9e28e3af198c40d3237cf6041608a9022285fe567ec3ad62", size = 266690, upload-time = "2026-04-03T20:54:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/54049f89b46235ca6f45cd6c88668a7050e77d4a15555e47dd40fde75263/regex-2026.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:3384df51ed52db0bea967e21458ab0a414f67cdddfd94401688274e55147bb81", size = 277733, upload-time = "2026-04-03T20:54:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0e/21/61366a8e20f4d43fb597708cac7f0e2baadb491ecc9549b4980b2be27d16/regex-2026.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:acd38177bd2c8e69a411d6521760806042e244d0ef94e2dd03ecdaa8a3c99427", size = 270565, upload-time = "2026-04-03T20:54:41.883Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1e/3a2b9672433bef02f5d39aa1143ca2c08f311c1d041c464a42be9ae648dc/regex-2026.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f94a11a9d05afcfcfa640e096319720a19cc0c9f7768e1a61fceee6a3afc6c7c", size = 494126, upload-time = "2026-04-03T20:54:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/4e/4b/c132a4f4fe18ad3340d89fcb56235132b69559136036b845be3c073142ed/regex-2026.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:36bcb9d6d1307ab629edc553775baada2aefa5c50ccc0215fbfd2afcfff43141", size = 293882, upload-time = "2026-04-03T20:54:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5f/eaa38092ce7a023656280f2341dbbd4ad5f05d780a70abba7bb4f4bea54c/regex-2026.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261c015b3e2ed0919157046d768774ecde57f03d8fa4ba78d29793447f70e717", size = 292334, upload-time = "2026-04-03T20:54:47.051Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f6/dd38146af1392dac33db7074ab331cec23cced3759167735c42c5460a243/regex-2026.4.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c228cf65b4a54583763645dcd73819b3b381ca8b4bb1b349dee1c135f4112c07", size = 811691, upload-time = "2026-04-03T20:54:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f0/dc54c2e69f5eeec50601054998ec3690d5344277e782bd717e49867c1d29/regex-2026.4.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd2630faeb6876fb0c287f664d93ddce4d50cd46c6e88e60378c05c9047e08ca", size = 871227, upload-time = "2026-04-03T20:54:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/a1/af/cb16bd5dc61621e27df919a4449bbb7e5a1034c34d307e0a706e9cc0f3e3/regex-2026.4.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a50ab11b7779b849472337191f3a043e27e17f71555f98d0092fa6d73364520", size = 917435, upload-time = "2026-04-03T20:54:52.994Z" }, + { url = "https://files.pythonhosted.org/packages/5c/71/8b260897f22996b666edd9402861668f45a2ca259f665ac029e6104a2d7d/regex-2026.4.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0734f63afe785138549fbe822a8cfeaccd1bae814c5057cc0ed5b9f2de4fc883", size = 816358, upload-time = "2026-04-03T20:54:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/1c/60/775f7f72a510ef238254906c2f3d737fc80b16ca85f07d20e318d2eea894/regex-2026.4.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4ee50606cb1967db7e523224e05f32089101945f859928e65657a2cbb3d278b", size = 785549, upload-time = "2026-04-03T20:54:57.01Z" }, + { url = "https://files.pythonhosted.org/packages/58/42/34d289b3627c03cf381e44da534a0021664188fa49ba41513da0b4ec6776/regex-2026.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6c1818f37be3ca02dcb76d63f2c7aaba4b0dc171b579796c6fbe00148dfec6b1", size = 801364, upload-time = "2026-04-03T20:54:58.981Z" }, + { url = "https://files.pythonhosted.org/packages/fc/20/f6ecf319b382a8f1ab529e898b222c3f30600fcede7834733c26279e7465/regex-2026.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f5bfc2741d150d0be3e4a0401a5c22b06e60acb9aa4daa46d9e79a6dcd0f135b", size = 866221, upload-time = "2026-04-03T20:55:00.88Z" }, + { url = "https://files.pythonhosted.org/packages/92/6a/9f16d3609d549bd96d7a0b2aee1625d7512ba6a03efc01652149ef88e74d/regex-2026.4.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:504ffa8a03609a087cad81277a629b6ce884b51a24bd388a7980ad61748618ff", size = 772530, upload-time = "2026-04-03T20:55:03.213Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f6/aa9768bc96a4c361ac96419fbaf2dcdc33970bb813df3ba9b09d5d7b6d96/regex-2026.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70aadc6ff12e4b444586e57fc30771f86253f9f0045b29016b9605b4be5f7dfb", size = 856989, upload-time = "2026-04-03T20:55:05.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b4/c671db3556be2473ae3e4bb7a297c518d281452871501221251ea4ecba57/regex-2026.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f4f83781191007b6ef43b03debc35435f10cad9b96e16d147efe84a1d48bdde4", size = 803241, upload-time = "2026-04-03T20:55:07.162Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5c/83e3b1d89fa4f6e5a1bc97b4abd4a9a97b3c1ac7854164f694f5f0ba98a0/regex-2026.4.4-cp313-cp313t-win32.whl", hash = "sha256:e014a797de43d1847df957c0a2a8e861d1c17547ee08467d1db2c370b7568baa", size = 269921, upload-time = "2026-04-03T20:55:09.62Z" }, + { url = "https://files.pythonhosted.org/packages/28/07/077c387121f42cdb4d92b1301133c0d93b5709d096d1669ab847dda9fe2e/regex-2026.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b15b88b0d52b179712632832c1d6e58e5774f93717849a41096880442da41ab0", size = 281240, upload-time = "2026-04-03T20:55:11.521Z" }, + { url = "https://files.pythonhosted.org/packages/9d/22/ead4a4abc7c59a4d882662aa292ca02c8b617f30b6e163bc1728879e9353/regex-2026.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:586b89cdadf7d67bf86ae3342a4dcd2b8d70a832d90c18a0ae955105caf34dbe", size = 272440, upload-time = "2026-04-03T20:55:13.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f5/ed97c2dc47b5fbd4b73c0d7d75f9ebc8eca139f2bbef476bba35f28c0a77/regex-2026.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2da82d643fa698e5e5210e54af90181603d5853cf469f5eedf9bfc8f59b4b8c7", size = 490343, upload-time = "2026-04-03T20:55:15.241Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/de4828a7385ec166d673a5790ad06ac48cdaa98bc0960108dd4b9cc1aef7/regex-2026.4.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:54a1189ad9d9357760557c91103d5e421f0a2dabe68a5cdf9103d0dcf4e00752", size = 291909, upload-time = "2026-04-03T20:55:17.558Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d6/5cfbfc97f3201a4d24b596a77957e092030dcc4205894bc035cedcfce62f/regex-2026.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:76d67d5afb1fe402d10a6403bae668d000441e2ab115191a804287d53b772951", size = 289692, upload-time = "2026-04-03T20:55:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/f2212d9fd56fe897e36d0110ba30ba2d247bd6410c5bd98499c7e5a1e1f2/regex-2026.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7cd3e4ee8d80447a83bbc9ab0c8459781fa77087f856c3e740d7763be0df27f", size = 796979, upload-time = "2026-04-03T20:55:22.56Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e3/a016c12675fbac988a60c7e1c16e67823ff0bc016beb27bd7a001dbdabc6/regex-2026.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e19e18c568d2866d8b6a6dfad823db86193503f90823a8f66689315ba28fbe8", size = 866744, upload-time = "2026-04-03T20:55:24.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/0b90ca4cf17adc3cb43de80ec71018c37c88ad64987e8d0d481a95ca60b5/regex-2026.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7698a6f38730fd1385d390d1ed07bb13dce39aa616aca6a6d89bea178464b9a4", size = 911613, upload-time = "2026-04-03T20:55:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3b/2b3dac0b82d41ab43aa87c6ecde63d71189d03fe8854b8ca455a315edac3/regex-2026.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:173a66f3651cdb761018078e2d9487f4cf971232c990035ec0eb1cdc6bf929a9", size = 800551, upload-time = "2026-04-03T20:55:29.532Z" }, + { url = "https://files.pythonhosted.org/packages/25/fe/5365eb7aa0e753c4b5957815c321519ecab033c279c60e1b1ae2367fa810/regex-2026.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa7922bbb2cc84fa062d37723f199d4c0cd200245ce269c05db82d904db66b83", size = 776911, upload-time = "2026-04-03T20:55:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b3/7fb0072156bba065e3b778a7bc7b0a6328212be5dd6a86fd207e0c4f2dab/regex-2026.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59f67cd0a0acaf0e564c20bbd7f767286f23e91e2572c5703bf3e56ea7557edb", size = 785751, upload-time = "2026-04-03T20:55:33.797Z" }, + { url = "https://files.pythonhosted.org/packages/02/1a/9f83677eb699273e56e858f7bd95acdbee376d42f59e8bfca2fd80d79df3/regex-2026.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:475e50f3f73f73614f7cba5524d6de49dee269df00272a1b85e3d19f6d498465", size = 860484, upload-time = "2026-04-03T20:55:35.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7a/93937507b61cfcff8b4c5857f1b452852b09f741daa9acae15c971d8554e/regex-2026.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a1c0c7d67b64d85ac2e1879923bad2f08a08f3004055f2f406ef73c850114bd4", size = 765939, upload-time = "2026-04-03T20:55:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/86/ea/81a7f968a351c6552b1670ead861e2a385be730ee28402233020c67f9e0f/regex-2026.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1371c2ccbb744d66ee63631cc9ca12aa233d5749972626b68fe1a649dd98e566", size = 851417, upload-time = "2026-04-03T20:55:39.92Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7e/323c18ce4b5b8f44517a36342961a0306e931e499febbd876bb149d900f0/regex-2026.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59968142787042db793348a3f5b918cf24ced1f23247328530e063f89c128a95", size = 789056, upload-time = "2026-04-03T20:55:42.303Z" }, + { url = "https://files.pythonhosted.org/packages/c0/af/e7510f9b11b1913b0cd44eddb784b2d650b2af6515bfce4cffcc5bfd1d38/regex-2026.4.4-cp314-cp314-win32.whl", hash = "sha256:59efe72d37fd5a91e373e5146f187f921f365f4abc1249a5ab446a60f30dd5f8", size = 272130, upload-time = "2026-04-03T20:55:44.995Z" }, + { url = "https://files.pythonhosted.org/packages/9a/51/57dae534c915e2d3a21490e88836fa2ae79dde3b66255ecc0c0a155d2c10/regex-2026.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e0aab3ff447845049d676827d2ff714aab4f73f340e155b7de7458cf53baa5a4", size = 280992, upload-time = "2026-04-03T20:55:47.316Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5e/abaf9f4c3792e34edb1434f06717fae2b07888d85cb5cec29f9204931bf8/regex-2026.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:a7a5bb6aa0cf62208bb4fa079b0c756734f8ad0e333b425732e8609bd51ee22f", size = 273563, upload-time = "2026-04-03T20:55:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/ff/06/35da85f9f217b9538b99cbb170738993bcc3b23784322decb77619f11502/regex-2026.4.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:97850d0638391bdc7d35dc1c1039974dcb921eaafa8cc935ae4d7f272b1d60b3", size = 494191, upload-time = "2026-04-03T20:55:51.258Z" }, + { url = "https://files.pythonhosted.org/packages/54/5b/1bc35f479eef8285c4baf88d8c002023efdeebb7b44a8735b36195486ae7/regex-2026.4.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ee7337f88f2a580679f7bbfe69dc86c043954f9f9c541012f49abc554a962f2e", size = 293877, upload-time = "2026-04-03T20:55:53.214Z" }, + { url = "https://files.pythonhosted.org/packages/39/5b/f53b9ad17480b3ddd14c90da04bfb55ac6894b129e5dea87bcaf7d00e336/regex-2026.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7429f4e6192c11d659900c0648ba8776243bf396ab95558b8c51a345afeddde6", size = 292410, upload-time = "2026-04-03T20:55:55.736Z" }, + { url = "https://files.pythonhosted.org/packages/bb/56/52377f59f60a7c51aa4161eecf0b6032c20b461805aca051250da435ffc9/regex-2026.4.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4f10fbd5dd13dcf4265b4cc07d69ca70280742870c97ae10093e3d66000359", size = 811831, upload-time = "2026-04-03T20:55:57.802Z" }, + { url = "https://files.pythonhosted.org/packages/dd/63/8026310bf066f702a9c361f83a8c9658f3fe4edb349f9c1e5d5273b7c40c/regex-2026.4.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a152560af4f9742b96f3827090f866eeec5becd4765c8e0d3473d9d280e76a5a", size = 871199, upload-time = "2026-04-03T20:56:00.333Z" }, + { url = "https://files.pythonhosted.org/packages/20/9f/a514bbb00a466dbb506d43f187a04047f7be1505f10a9a15615ead5080ee/regex-2026.4.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54170b3e95339f415d54651f97df3bff7434a663912f9358237941bbf9143f55", size = 917649, upload-time = "2026-04-03T20:56:02.445Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6b/8399f68dd41a2030218839b9b18360d79b86d22b9fab5ef477c7f23ca67c/regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07f190d65f5a72dcb9cf7106bfc3d21e7a49dd2879eda2207b683f32165e4d99", size = 816388, upload-time = "2026-04-03T20:56:04.595Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9c/103963f47c24339a483b05edd568594c2be486188f688c0170fd504b2948/regex-2026.4.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a2741ce5a29d3c84b0b94261ba630ab459a1b847a0d6beca7d62d188175c790", size = 785746, upload-time = "2026-04-03T20:56:07.13Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/7f6054c0dec0cee3463c304405e4ff42e27cff05bf36fcb34be549ab17bd/regex-2026.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b26c30df3a28fd9793113dac7385a4deb7294a06c0f760dd2b008bd49a9139bc", size = 801483, upload-time = "2026-04-03T20:56:09.365Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/51d3d941cf6070dc00c3338ecf138615fc3cce0421c3df6abe97a08af61a/regex-2026.4.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:421439d1bee44b19f4583ccf42670ca464ffb90e9fdc38d37f39d1ddd1e44f1f", size = 866331, upload-time = "2026-04-03T20:56:12.039Z" }, + { url = "https://files.pythonhosted.org/packages/16/e8/76d50dcc122ac33927d939f350eebcfe3dbcbda96913e03433fc36de5e63/regex-2026.4.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b40379b53ecbc747fd9bdf4a0ea14eb8188ca1bd0f54f78893a39024b28f4863", size = 772673, upload-time = "2026-04-03T20:56:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6e/5f6bf75e20ea6873d05ba4ec78378c375cbe08cdec571c83fbb01606e563/regex-2026.4.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:08c55c13d2eef54f73eeadc33146fb0baaa49e7335eb1aff6ae1324bf0ddbe4a", size = 857146, upload-time = "2026-04-03T20:56:16.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/33/3c76d9962949e487ebba353a18e89399f292287204ac8f2f4cfc3a51c233/regex-2026.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9776b85f510062f5a75ef112afe5f494ef1635607bf1cc220c1391e9ac2f5e81", size = 803463, upload-time = "2026-04-03T20:56:18.923Z" }, + { url = "https://files.pythonhosted.org/packages/19/eb/ef32dcd2cb69b69bc0c3e55205bce94a7def48d495358946bc42186dcccc/regex-2026.4.4-cp314-cp314t-win32.whl", hash = "sha256:385edaebde5db5be103577afc8699fea73a0e36a734ba24870be7ffa61119d74", size = 275709, upload-time = "2026-04-03T20:56:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/a0/86/c291bf740945acbf35ed7dbebf8e2eea2f3f78041f6bd7cdab80cb274dc0/regex-2026.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5d354b18839328927832e2fa5f7c95b7a3ccc39e7a681529e1685898e6436d45", size = 285622, upload-time = "2026-04-03T20:56:23.641Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e7/ec846d560ae6a597115153c02ca6138a7877a1748b2072d9521c10a93e58/regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d", size = 275773, upload-time = "2026-04-03T20:56:26.07Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, +] + +[[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 = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +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 = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +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/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748, upload-time = "2026-04-27T14:43:25.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732, upload-time = "2026-04-27T14:43:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954, upload-time = "2026-04-27T14:43:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081, upload-time = "2026-04-27T14:43:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641, upload-time = "2026-04-27T14:43:03.856Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624, upload-time = "2026-04-27T14:43:10.664Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062, upload-time = "2026-04-27T14:43:05.643Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098, upload-time = "2026-04-27T14:43:08.854Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235, upload-time = "2026-04-27T14:43:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398, upload-time = "2026-04-27T14:43:07.264Z" }, + { url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279, upload-time = "2026-04-27T14:43:17.222Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986, upload-time = "2026-04-27T14:43:19.45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181, upload-time = "2026-04-27T14:43:21.463Z" }, + { url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853, upload-time = "2026-04-27T14:43:23.617Z" }, + { url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263, upload-time = "2026-04-27T14:43:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223, upload-time = "2026-04-27T14:43:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" }, +] + +[[package]] +name = "torch" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +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" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { 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" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6debd97ccd3205bbb37eb806a9d8219e1139d15419982c09e23ef7d4369d18", size = 80606801, upload-time = "2026-03-23T18:10:18.649Z" }, + { url = "https://files.pythonhosted.org/packages/32/d1/8ed2173589cbfe744ed54e5a73efc107c0085ba5777ee93a5f4c1ab90553/torch-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:63a68fa59de8f87acc7e85a5478bb2dddbb3392b7593ec3e78827c793c4b73fd", size = 419732382, upload-time = "2026-03-23T18:08:30.835Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e1/b73f7c575a4b8f87a5928f50a1e35416b5e27295d8be9397d5293e7e8d4c/torch-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cc89b9b173d9adfab59fd227f0ab5e5516d9a52b658ae41d64e59d2e55a418db", size = 530711509, upload-time = "2026-03-23T18:08:47.213Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/3e3fcdd388fbe54e29fd3f991f36846ff4ac90b0d0181e9c8f7236565f82/torch-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:4dda3b3f52d121063a731ddb835f010dc137b920d7fec2778e52f60d8e4bf0cd", size = 114555842, upload-time = "2026-03-23T18:09:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/db/38/8ac78069621b8c2b4979c2f96dc8409ef5e9c4189f6aac629189a78677ca/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8b394322f49af4362d4f80e424bcaca7efcd049619af03a4cf4501520bdf0fb4", size = 80959574, upload-time = "2026-03-23T18:10:14.214Z" }, + { url = "https://files.pythonhosted.org/packages/6d/6c/56bfb37073e7136e6dd86bfc6af7339946dd684e0ecf2155ac0eee687ae1/torch-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2658f34ce7e2dabf4ec73b45e2ca68aedad7a5be87ea756ad656eaf32bf1e1ea", size = 419732324, upload-time = "2026-03-23T18:09:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/07/f4/1b666b6d61d3394cca306ea543ed03a64aad0a201b6cd159f1d41010aeb1/torch-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:98bb213c3084cfe176302949bdc360074b18a9da7ab59ef2edc9d9f742504778", size = 530596026, upload-time = "2026-03-23T18:09:20.842Z" }, + { url = "https://files.pythonhosted.org/packages/48/6b/30d1459fa7e4b67e9e3fe1685ca1d8bb4ce7c62ef436c3a615963c6c866c/torch-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a97b94bbf62992949b4730c6cd2cc9aee7b335921ee8dc207d930f2ed09ae2db", size = 114793702, upload-time = "2026-03-23T18:09:47.304Z" }, + { url = "https://files.pythonhosted.org/packages/26/0d/8603382f61abd0db35841148ddc1ffd607bf3100b11c6e1dab6d2fc44e72/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01018087326984a33b64e04c8cb5c2795f9120e0d775ada1f6638840227b04d7", size = 80573442, upload-time = "2026-03-23T18:09:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/c7/86/7cd7c66cb9cec6be330fff36db5bd0eef386d80c031b581ec81be1d4b26c/torch-2.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:2bb3cc54bd0dea126b0060bb1ec9de0f9c7f7342d93d436646516b0330cd5be7", size = 419749385, upload-time = "2026-03-23T18:07:33.77Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/b98ca2d39b2e0e4730c0ee52537e488e7008025bc77ca89552ff91021f7c/torch-2.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4dc8b3809469b6c30b411bb8c4cad3828efd26236153d9beb6a3ec500f211a60", size = 530716756, upload-time = "2026-03-23T18:07:50.02Z" }, + { url = "https://files.pythonhosted.org/packages/78/88/d4a4cda8362f8a30d1ed428564878c3cafb0d87971fbd3947d4c84552095/torch-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b4e811728bd0cc58fb2b0948fe939a1ee2bf1422f6025be2fca4c7bd9d79718", size = 114552300, upload-time = "2026-03-23T18:09:05.617Z" }, + { url = "https://files.pythonhosted.org/packages/bf/46/4419098ed6d801750f26567b478fc185c3432e11e2cad712bc6b4c2ab0d0/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8245477871c3700d4370352ffec94b103cfcb737229445cf9946cddb7b2ca7cd", size = 80959460, upload-time = "2026-03-23T18:09:00.818Z" }, + { url = "https://files.pythonhosted.org/packages/fd/66/54a56a4a6ceaffb567231994a9745821d3af922a854ed33b0b3a278e0a99/torch-2.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ab9a8482f475f9ba20e12db84b0e55e2f58784bdca43a854a6ccd3fd4b9f75e6", size = 419735835, upload-time = "2026-03-23T18:07:18.974Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e7/0b6665f533aa9e337662dc190425abc0af1fe3234088f4454c52393ded61/torch-2.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:563ed3d25542d7e7bbc5b235ccfacfeb97fb470c7fee257eae599adb8005c8a2", size = 530613405, upload-time = "2026-03-23T18:08:07.014Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bf/c8d12a2c86dbfd7f40fb2f56fbf5a505ccf2d9ce131eb559dfc7c51e1a04/torch-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b2a43985ff5ef6ddd923bbcf99943e5f58059805787c5c9a2622bf05ca2965b0", size = 114792991, upload-time = "2026-03-23T18:08:19.216Z" }, +] + +[[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 = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/12/34d71b350e89a204c2c7777a9bba0dcf2f19a5bfdd70b57c4dbc5ffd7154/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd", size = 176133521, upload-time = "2026-01-20T16:16:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4e/41b0c8033b503fd3cfcd12392cdd256945026a91ff02452bef40ec34bee7/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6", size = 176276087, upload-time = "2026-01-20T16:16:18.989Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/49/55/5ecf0dcaa0f2fbbd4420f7ef227ee3cb172e91e5fede9d0ecaddc43363b4/triton-3.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43", size = 176138577, upload-time = "2026-01-20T16:16:25.426Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/db/56ee649cab5eaff4757541325aca81f52d02d4a7cd3506776cad2451e060/triton-3.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d", size = 176274804, upload-time = "2026-01-20T16:16:31.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, +] + +[[package]] +name = "typer" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, +] + +[[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 = "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 = "view-recordings" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "faster-whisper" }, + { name = "openai-whisper" }, +] + +[package.metadata] +requires-dist = [ + { name = "faster-whisper", specifier = ">=1.2.1" }, + { name = "openai-whisper", specifier = ">=20250625" }, +] diff --git a/clio-android/.claude/CLAUDE.md b/clio-android/.claude/CLAUDE.md new file mode 100644 index 0000000..83a6661 --- /dev/null +++ b/clio-android/.claude/CLAUDE.md @@ -0,0 +1,87 @@ +# Phone Recorder Server + +Encrypted audio recording server with transcript viewing capabilities. + +## Quick Start + +```bash +cd server +nix develop # or activate venv +uv run python main.py +``` + +Server runs on port 8000 by default. + +## Project Structure + +``` +. +├── app/ # Android app (Kotlin, Android Studio) +├── server/ # FastAPI server (Python) +│ ├── main.py # API endpoints +│ ├── crypto.py # PyNaCl decryption, keypair management +│ ├── database.py # SQLite for recordings metadata +│ ├── importer.py # Import transcriptions from batch processing +│ ├── cli.py # CLI tools +│ ├── config.py # Paths, ports +│ ├── keys/ # Server keypair (auto-generated) +│ └── recordings/ # Decrypted audio files +└── test_recordings/ # Test data +``` + +## Key Commands + +```bash +# Run server +cd server && uv run python main.py + +# Run tests +cd server && python test_crypto.py +cd server && python test_integration.py + +# CLI tools +cd server && uv run python cli.py --help +``` + +## API Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/health` | GET | Health check | +| `/api/v1/public-key` | GET | Get server public key (Base64) | +| `/api/v1/recordings/upload` | POST | Upload encrypted file from phone | +| `/api/v1/recordings` | GET | List recordings with transcripts | +| `/api/v1/recordings/{id}` | GET | Get recording details | +| `/api/v1/recordings/{id}/audio` | GET | Stream audio file | +| `/api/v1/import/transcription` | POST | Import transcription JSON | +| `/api/v1/highlights` | GET/POST | Manage highlights | + +## Security Model + +- Phone encrypts with server's public key (sealed box / X25519 + XSalsa20-Poly1305) +- Server decrypts with private key on upload +- Phone never stores unencrypted audio +- Files use SREC format (chunked encryption for large files) + +## Frontend + +The frontend is a separate static React app in `/work/projects/view_recordings/designer_export/`: + +```bash +cd /work/projects/view_recordings/designer_export +python -m http.server 3000 +# Open http://localhost:3000/RecordingsViewer.html +``` + +## Dependencies + +- Python 3.x with uv +- PyNaCl (libsodium bindings) +- FastAPI + uvicorn +- SQLite (via database.py) + +## Important Files + +- `description.md` - Detailed architecture documentation +- `todo.md` - Future features and known limitations +- `server/keys/` - Contains server keypair (do not commit private key) diff --git a/clio-android/.gitignore b/clio-android/.gitignore new file mode 100644 index 0000000..8abaf13 --- /dev/null +++ b/clio-android/.gitignore @@ -0,0 +1 @@ +android_studio_install_path \ No newline at end of file diff --git a/clio-android/app/build.gradle.kts b/clio-android/app/build.gradle.kts new file mode 100644 index 0000000..00c96e4 --- /dev/null +++ b/clio-android/app/build.gradle.kts @@ -0,0 +1,61 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.example.phonerecorder" + compileSdk = 34 + + defaultConfig { + applicationId = "com.example.phonerecorder" + minSdk = 29 + targetSdk = 33 + versionCode = 2 + versionName = "1.1.0" + + // Build timestamp for version display + buildConfigField("long", "BUILD_TIMESTAMP", "${System.currentTimeMillis()}L") + } + + buildFeatures { + buildConfig = true + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.10.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0") + + // Encryption + implementation("com.goterl:lazysodium-android:5.1.0@aar") + implementation("net.java.dev.jna:jna:5.14.0@aar") + + // Background sync + implementation("androidx.work:work-runtime-ktx:2.9.0") + + // HTTP client + implementation("com.squareup.okhttp3:okhttp:4.12.0") +} diff --git a/clio-android/app/proguard-rules.pro b/clio-android/app/proguard-rules.pro new file mode 100644 index 0000000..ec71fae --- /dev/null +++ b/clio-android/app/proguard-rules.pro @@ -0,0 +1,7 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /sdk/tools/proguard/proguard-android.txt + +# Keep service and receiver classes +-keep class com.example.phonerecorder.RecordingService { *; } +-keep class com.example.phonerecorder.BootReceiver { *; } diff --git a/clio-android/app/src/main/AndroidManifest.xml b/clio-android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3838e6c --- /dev/null +++ b/clio-android/app/src/main/AndroidManifest.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clio-android/app/src/main/java/com/example/phonerecorder/BootReceiver.kt b/clio-android/app/src/main/java/com/example/phonerecorder/BootReceiver.kt new file mode 100644 index 0000000..b5ed012 --- /dev/null +++ b/clio-android/app/src/main/java/com/example/phonerecorder/BootReceiver.kt @@ -0,0 +1,33 @@ +package com.example.phonerecorder + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.core.content.ContextCompat + +class BootReceiver : BroadcastReceiver() { + + companion object { + private const val TAG = "BootReceiver" + private const val PREFS_NAME = "phone_recorder_prefs" + private const val PREF_AUTO_START = "auto_start_enabled" + } + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Intent.ACTION_BOOT_COMPLETED) { + Log.i(TAG, "Boot completed received") + + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val autoStartEnabled = prefs.getBoolean(PREF_AUTO_START, false) + + if (autoStartEnabled) { + Log.i(TAG, "Auto-start enabled, starting recording service") + val serviceIntent = Intent(context, RecordingService::class.java) + ContextCompat.startForegroundService(context, serviceIntent) + } else { + Log.i(TAG, "Auto-start disabled, not starting service") + } + } + } +} diff --git a/clio-android/app/src/main/java/com/example/phonerecorder/MainActivity.kt b/clio-android/app/src/main/java/com/example/phonerecorder/MainActivity.kt new file mode 100644 index 0000000..57100a4 --- /dev/null +++ b/clio-android/app/src/main/java/com/example/phonerecorder/MainActivity.kt @@ -0,0 +1,540 @@ +package com.example.phonerecorder + +import android.Manifest +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.ServiceConnection +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.net.Uri +import android.os.BatteryManager +import android.os.Build +import android.os.Bundle +import android.os.IBinder +import android.os.PowerManager +import android.provider.Settings +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.EditText +import android.widget.Spinner +import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.example.phonerecorder.crypto.KeyManager +import com.example.phonerecorder.sync.NetworkHelper +import com.example.phonerecorder.sync.SyncManager +import com.example.phonerecorder.sync.SyncMode +import com.example.phonerecorder.sync.SyncSettings +import com.example.phonerecorder.sync.SyncStatus +import com.google.android.material.button.MaterialButton +import com.google.android.material.switchmaterial.SwitchMaterial +import java.util.Locale + +class MainActivity : AppCompatActivity() { + + companion object { + private const val PREFS_NAME = "phone_recorder_prefs" + private const val PREF_AUTO_START = "auto_start_enabled" + } + + private lateinit var statusIndicator: android.view.View + private lateinit var statusText: TextView + private lateinit var elapsedTimeText: TextView + private lateinit var currentFileText: TextView + private lateinit var storageLocationText: TextView + private lateinit var storageInfoText: TextView + private lateinit var batteryInfoText: TextView + private lateinit var toggleButton: MaterialButton + private lateinit var autoStartSwitch: SwitchMaterial + private lateinit var batteryOptimizationHint: TextView + private lateinit var syncModeSpinner: Spinner + private lateinit var keyStatusText: TextView + private lateinit var importKeyButton: MaterialButton + private lateinit var syncStatusText: TextView + private lateinit var networkInfoText: TextView + private lateinit var configureLanButton: MaterialButton + private lateinit var versionText: TextView + + private lateinit var storageHelper: StorageHelper + private lateinit var prefs: SharedPreferences + private lateinit var keyManager: KeyManager + private lateinit var syncManager: SyncManager + private lateinit var networkHelper: NetworkHelper + private lateinit var syncSettings: SyncSettings + + private var recordingService: RecordingService? = null + private var isBound = false + + private val serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + val binder = service as RecordingService.RecordingBinder + recordingService = binder.getService() + isBound = true + updateUI() + } + + override fun onServiceDisconnected(name: ComponentName?) { + recordingService = null + isBound = false + } + } + + private val recordingReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + RecordingService.ACTION_RECORDING_STARTED, + RecordingService.ACTION_RECORDING_STOPPED, + RecordingService.ACTION_RECORDING_UPDATE -> updateUI() + RecordingService.ACTION_LOW_STORAGE -> { + Toast.makeText( + this@MainActivity, + R.string.storage_warning, + Toast.LENGTH_LONG + ).show() + updateUI() + updateSyncStatus() + } + } + } + } + + private val permissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + val allGranted = permissions.all { it.value } + if (allGranted) { + startRecordingService() + } else { + Toast.makeText(this, R.string.permission_required, Toast.LENGTH_LONG).show() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + storageHelper = StorageHelper(this) + prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + keyManager = KeyManager(this) + syncManager = SyncManager.getInstance(this) + networkHelper = NetworkHelper(this) + syncSettings = SyncSettings(this) + + initViews() + setupListeners() + setupSyncUI() + checkBatteryOptimization() + + // Start periodic sync based on settings + syncManager.startPeriodicSync() + } + + override fun onStart() { + super.onStart() + bindRecordingService() + registerReceivers() + updateUI() + updateNetworkInfo() + updateSyncStatus() + } + + override fun onStop() { + super.onStop() + unregisterReceivers() + unbindRecordingService() + } + + private fun initViews() { + statusIndicator = findViewById(R.id.statusIndicator) + statusText = findViewById(R.id.statusText) + elapsedTimeText = findViewById(R.id.elapsedTimeText) + currentFileText = findViewById(R.id.currentFileText) + storageLocationText = findViewById(R.id.storageLocationText) + storageInfoText = findViewById(R.id.storageInfoText) + batteryInfoText = findViewById(R.id.batteryInfoText) + toggleButton = findViewById(R.id.toggleButton) + autoStartSwitch = findViewById(R.id.autoStartSwitch) + batteryOptimizationHint = findViewById(R.id.batteryOptimizationHint) + + // Initialize auto-start switch state + autoStartSwitch.isChecked = prefs.getBoolean(PREF_AUTO_START, false) + updateAutoStartText() + + // Sync UI elements + syncModeSpinner = findViewById(R.id.syncModeSpinner) + keyStatusText = findViewById(R.id.keyStatusText) + importKeyButton = findViewById(R.id.importKeyButton) + syncStatusText = findViewById(R.id.syncStatusText) + networkInfoText = findViewById(R.id.networkInfoText) + configureLanButton = findViewById(R.id.configureLanButton) + versionText = findViewById(R.id.versionText) + + // Set version info + val buildDate = java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.US) + .format(java.util.Date(BuildConfig.BUILD_TIMESTAMP)) + versionText.text = "v${BuildConfig.VERSION_NAME} (build $buildDate)" + } + + private fun setupListeners() { + toggleButton.setOnClickListener { + if (recordingService?.isRecording() == true) { + stopRecordingService() + } else { + checkPermissionsAndStart() + } + } + + autoStartSwitch.setOnCheckedChangeListener { _, isChecked -> + prefs.edit().putBoolean(PREF_AUTO_START, isChecked).apply() + updateAutoStartText() + } + + batteryOptimizationHint.setOnClickListener { + openBatteryOptimizationSettings() + } + + importKeyButton.setOnClickListener { + showImportKeyDialog() + } + + configureLanButton.setOnClickListener { + showLanConfigDialog() + } + } + + private fun setupSyncUI() { + // Setup sync mode spinner + val syncModes = arrayOf( + getString(R.string.sync_mode_never), + getString(R.string.sync_mode_lan_only), + getString(R.string.sync_mode_wifi_only), + getString(R.string.sync_mode_any_network) + ) + + val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, syncModes) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + syncModeSpinner.adapter = adapter + + // Set current selection + syncModeSpinner.setSelection(syncManager.getSyncMode().ordinal) + + // Handle selection changes + syncModeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + val mode = SyncMode.entries[position] + syncManager.setSyncMode(mode) + updateSyncStatus() + } + + override fun onNothingSelected(parent: AdapterView<*>?) {} + } + + updateKeyStatus() + updateSyncStatus() + updateNetworkInfo() + } + + private fun updateNetworkInfo() { + val ipAddress = networkHelper.getCurrentIpAddress() + networkInfoText.text = when { + ipAddress != null && networkHelper.isOnWifi() -> + getString(R.string.network_wifi, ipAddress) + networkHelper.isNetworkAvailable() -> + getString(R.string.network_mobile) + else -> + getString(R.string.network_not_connected) + } + } + + private fun showLanConfigDialog() { + val currentIp = networkHelper.getCurrentIpAddress() ?: "Not connected" + val currentServerUrl = syncSettings.lanServerUrl + val currentPrefix = syncSettings.lanSubnetPrefix + val currentApiKey = syncSettings.apiKey + + // Create a layout with fields + val layout = android.widget.LinearLayout(this).apply { + orientation = android.widget.LinearLayout.VERTICAL + setPadding(48, 24, 48, 0) + } + + val serverLabel = TextView(this).apply { + text = "LAN Server URL:" + setTextColor(ContextCompat.getColor(this@MainActivity, android.R.color.darker_gray)) + } + layout.addView(serverLabel) + + val serverEdit = EditText(this).apply { + hint = "http://10.0.0.45:8000" + setText(currentServerUrl) + isSingleLine = true + inputType = android.text.InputType.TYPE_TEXT_VARIATION_URI + } + layout.addView(serverEdit) + + val prefixLabel = TextView(this).apply { + text = "\nLAN Subnet Prefix:" + setTextColor(ContextCompat.getColor(this@MainActivity, android.R.color.darker_gray)) + } + layout.addView(prefixLabel) + + val prefixEdit = EditText(this).apply { + hint = getString(R.string.lan_subnet_hint) + setText(currentPrefix) + isSingleLine = true + } + layout.addView(prefixEdit) + + val apiKeyLabel = TextView(this).apply { + text = "\nAPI Key (for upload auth):" + setTextColor(ContextCompat.getColor(this@MainActivity, android.R.color.darker_gray)) + } + layout.addView(apiKeyLabel) + + val apiKeyEdit = EditText(this).apply { + hint = "Leave empty if not required" + setText(currentApiKey) + isSingleLine = true + inputType = android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD + } + layout.addView(apiKeyEdit) + + val infoText = TextView(this).apply { + text = "\nYour IP: $currentIp" + setTextColor(ContextCompat.getColor(this@MainActivity, android.R.color.darker_gray)) + textSize = 12f + } + layout.addView(infoText) + + AlertDialog.Builder(this) + .setTitle(R.string.lan_config_title) + .setView(layout) + .setPositiveButton(android.R.string.ok) { _, _ -> + // Save server URL + val serverUrl = serverEdit.text.toString().trim() + if (serverUrl.isNotEmpty()) { + syncSettings.lanServerUrl = serverUrl + } + + // Save subnet prefix + var prefix = prefixEdit.text.toString().trim() + if (prefix.isNotEmpty() && !prefix.endsWith(".")) { + prefix = "$prefix." + } + if (prefix.isNotEmpty()) { + syncSettings.lanSubnetPrefix = prefix + } + + // Save API key + syncSettings.apiKey = apiKeyEdit.text.toString().trim() + + Toast.makeText(this, "Settings saved", Toast.LENGTH_SHORT).show() + updateNetworkInfo() + updateSyncStatus() + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun showImportKeyDialog() { + val editText = EditText(this).apply { + hint = getString(R.string.import_key_hint) + isSingleLine = false + minLines = 2 + maxLines = 4 + } + + AlertDialog.Builder(this) + .setTitle(R.string.import_key_title) + .setView(editText) + .setPositiveButton(android.R.string.ok) { _, _ -> + val key = editText.text.toString().trim() + if (keyManager.setPublicKey(key)) { + Toast.makeText(this, R.string.import_key_success, Toast.LENGTH_SHORT).show() + updateKeyStatus() + updateSyncStatus() + } else { + Toast.makeText(this, R.string.import_key_error, Toast.LENGTH_LONG).show() + } + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun updateKeyStatus() { + keyStatusText.text = if (keyManager.hasPublicKey()) { + getString(R.string.key_status_configured) + } else { + getString(R.string.key_status_not_configured) + } + } + + private fun updateSyncStatus() { + val status = syncManager.getSyncStatus(keyManager.hasPublicKey()) + syncStatusText.text = when (status) { + is SyncStatus.Disabled -> getString(R.string.sync_status_disabled) + is SyncStatus.NoKey -> getString(R.string.sync_status_no_key) + is SyncStatus.NotOnWifi -> getString(R.string.sync_status_not_on_wifi) + is SyncStatus.NotOnLan -> getString(R.string.sync_status_not_on_lan) + is SyncStatus.NoNetwork -> getString(R.string.sync_status_no_network) + is SyncStatus.Ready -> getString(R.string.sync_status_pending, status.pending) + is SyncStatus.Synced -> getString(R.string.sync_status_synced) + } + } + + private fun updateAutoStartText() { + autoStartSwitch.text = if (autoStartSwitch.isChecked) { + getString(R.string.auto_start_enabled) + } else { + getString(R.string.auto_start_disabled) + } + } + + private fun checkPermissionsAndStart() { + val permissions = mutableListOf(Manifest.permission.RECORD_AUDIO) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + permissions.add(Manifest.permission.POST_NOTIFICATIONS) + } + + val notGranted = permissions.filter { + ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED + } + + if (notGranted.isEmpty()) { + startRecordingService() + } else { + permissionLauncher.launch(notGranted.toTypedArray()) + } + } + + private fun startRecordingService() { + val intent = Intent(this, RecordingService::class.java) + ContextCompat.startForegroundService(this, intent) + bindRecordingService() + } + + private fun stopRecordingService() { + recordingService?.stopRecording() + stopService(Intent(this, RecordingService::class.java)) + } + + private fun bindRecordingService() { + val intent = Intent(this, RecordingService::class.java) + bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) + } + + private fun unbindRecordingService() { + if (isBound) { + unbindService(serviceConnection) + isBound = false + } + } + + private fun registerReceivers() { + val filter = IntentFilter().apply { + addAction(RecordingService.ACTION_RECORDING_STARTED) + addAction(RecordingService.ACTION_RECORDING_STOPPED) + addAction(RecordingService.ACTION_RECORDING_UPDATE) + addAction(RecordingService.ACTION_LOW_STORAGE) + } + LocalBroadcastManager.getInstance(this).registerReceiver(recordingReceiver, filter) + } + + private fun unregisterReceivers() { + LocalBroadcastManager.getInstance(this).unregisterReceiver(recordingReceiver) + } + + private fun updateUI() { + val isRecording = recordingService?.isRecording() == true + + // Status indicator + statusIndicator.setBackgroundResource( + if (isRecording) R.drawable.status_circle_active else R.drawable.status_circle + ) + statusText.text = getString( + if (isRecording) R.string.status_recording else R.string.status_stopped + ) + + // Elapsed time + val elapsed = recordingService?.getElapsedTime() ?: 0L + elapsedTimeText.text = formatElapsedTime(elapsed) + + // Current file + val filename = recordingService?.getCurrentFilename() + currentFileText.text = if (filename != null) { + getString(R.string.current_file, filename) + } else { + getString(R.string.current_file, getString(R.string.no_file)) + } + + // Storage info + val storageInfo = storageHelper.getStorageInfo() + storageLocationText.text = getString( + if (storageInfo.isSDCard) R.string.storage_location_sd else R.string.storage_location_internal + ) + storageInfoText.text = getString( + R.string.storage_info, + storageHelper.formatBytes(storageInfo.usedBytes), + storageHelper.formatBytes(storageInfo.availableBytes) + ) + + // Battery info + val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager + val batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) + batteryInfoText.text = getString(R.string.battery_info, batteryLevel) + + // Toggle button + toggleButton.text = getString( + if (isRecording) R.string.stop_recording else R.string.start_recording + ) + } + + private fun formatElapsedTime(millis: Long): String { + val seconds = (millis / 1000) % 60 + val minutes = (millis / (1000 * 60)) % 60 + val hours = millis / (1000 * 60 * 60) + return String.format(Locale.US, "%02d:%02d:%02d", hours, minutes, seconds) + } + + private fun checkBatteryOptimization() { + val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager + if (!powerManager.isIgnoringBatteryOptimizations(packageName)) { + showBatteryOptimizationDialog() + } else { + batteryOptimizationHint.visibility = android.view.View.GONE + } + } + + private fun showBatteryOptimizationDialog() { + AlertDialog.Builder(this) + .setTitle(R.string.battery_optimization_title) + .setMessage(R.string.battery_optimization_message) + .setPositiveButton(R.string.battery_optimization_button) { _, _ -> + openBatteryOptimizationSettings() + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun openBatteryOptimizationSettings() { + try { + val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { + data = Uri.parse("package:$packageName") + } + startActivity(intent) + } catch (e: Exception) { + // Fall back to general battery settings + val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) + startActivity(intent) + } + } +} diff --git a/clio-android/app/src/main/java/com/example/phonerecorder/RecordingService.kt b/clio-android/app/src/main/java/com/example/phonerecorder/RecordingService.kt new file mode 100644 index 0000000..df262c2 --- /dev/null +++ b/clio-android/app/src/main/java/com/example/phonerecorder/RecordingService.kt @@ -0,0 +1,378 @@ +package com.example.phonerecorder + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.media.MediaRecorder +import android.os.Binder +import android.os.Build +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.os.PowerManager +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.example.phonerecorder.crypto.CryptoHelper +import com.example.phonerecorder.crypto.KeyManager +import com.example.phonerecorder.sync.SyncManager +import java.io.File +import java.util.Locale + +class RecordingService : Service() { + + companion object { + private const val TAG = "RecordingService" + private const val NOTIFICATION_ID = 1 + private const val CHANNEL_ID = "recording_channel" + + // Segment rotation: 2 hours in milliseconds + private const val SEGMENT_DURATION_MS = 2 * 60 * 60 * 1000L + + // Storage check interval: 5 minutes + private const val STORAGE_CHECK_INTERVAL_MS = 5 * 60 * 1000L + + // Notification update interval: 1 second + private const val NOTIFICATION_UPDATE_INTERVAL_MS = 1000L + + // Broadcast actions + const val ACTION_RECORDING_STARTED = "com.example.phonerecorder.RECORDING_STARTED" + const val ACTION_RECORDING_STOPPED = "com.example.phonerecorder.RECORDING_STOPPED" + const val ACTION_RECORDING_UPDATE = "com.example.phonerecorder.RECORDING_UPDATE" + const val ACTION_LOW_STORAGE = "com.example.phonerecorder.LOW_STORAGE" + + // Intent extras + const val EXTRA_FILENAME = "filename" + const val EXTRA_ELAPSED_TIME = "elapsed_time" + } + + private val binder = RecordingBinder() + private var mediaRecorder: MediaRecorder? = null + private var wakeLock: PowerManager.WakeLock? = null + private lateinit var storageHelper: StorageHelper + private lateinit var notificationManager: NotificationManager + private lateinit var keyManager: KeyManager + private lateinit var cryptoHelper: CryptoHelper + private var syncManager: SyncManager? = null + + private var isRecording = false + private var currentTempFile: File? = null + private var currentFile: File? = null + private var segmentStartTime: Long = 0 + private var totalElapsedTime: Long = 0 + + private val handler = Handler(Looper.getMainLooper()) + + private val segmentRotationRunnable = Runnable { rotateSegment() } + private val storageCheckRunnable = object : Runnable { + override fun run() { + checkStorage() + if (isRecording) { + handler.postDelayed(this, STORAGE_CHECK_INTERVAL_MS) + } + } + } + private val notificationUpdateRunnable = object : Runnable { + override fun run() { + updateNotification() + broadcastUpdate() + if (isRecording) { + handler.postDelayed(this, NOTIFICATION_UPDATE_INTERVAL_MS) + } + } + } + + inner class RecordingBinder : Binder() { + fun getService(): RecordingService = this@RecordingService + } + + override fun onCreate() { + super.onCreate() + storageHelper = StorageHelper(this) + notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + keyManager = KeyManager(this) + cryptoHelper = CryptoHelper(keyManager) + syncManager = SyncManager.getInstance(this) + createNotificationChannel() + } + + override fun onBind(intent: Intent?): IBinder = binder + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (!isRecording) { + startRecording() + } + return START_STICKY + } + + override fun onDestroy() { + stopRecording() + super.onDestroy() + } + + fun isRecording(): Boolean = isRecording + + fun getCurrentFilename(): String? = currentFile?.name + + fun getElapsedTime(): Long { + return if (isRecording) { + totalElapsedTime + (System.currentTimeMillis() - segmentStartTime) + } else { + totalElapsedTime + } + } + + fun startRecording() { + if (isRecording) return + + if (storageHelper.isStorageLow()) { + broadcastLowStorage() + return + } + + try { + acquireWakeLock() + startNewSegment() + isRecording = true + totalElapsedTime = 0 + + // Schedule segment rotation + handler.postDelayed(segmentRotationRunnable, SEGMENT_DURATION_MS) + + // Start storage checks + handler.postDelayed(storageCheckRunnable, STORAGE_CHECK_INTERVAL_MS) + + // Start notification updates + handler.post(notificationUpdateRunnable) + + // Start foreground service + startForeground(NOTIFICATION_ID, createNotification()) + + broadcastRecordingStarted() + Log.i(TAG, "Recording started: ${currentFile?.name}") + } catch (e: Exception) { + Log.e(TAG, "Failed to start recording", e) + stopRecording() + } + } + + fun stopRecording() { + if (!isRecording) return + + isRecording = false + totalElapsedTime += System.currentTimeMillis() - segmentStartTime + + handler.removeCallbacks(segmentRotationRunnable) + handler.removeCallbacks(storageCheckRunnable) + handler.removeCallbacks(notificationUpdateRunnable) + + releaseMediaRecorder() + releaseWakeLock() + + stopForeground(STOP_FOREGROUND_REMOVE) + broadcastRecordingStopped() + + Log.i(TAG, "Recording stopped") + } + + private fun startNewSegment() { + releaseMediaRecorder() + + // Record to temp file first (will be encrypted after recording stops) + currentTempFile = storageHelper.getNewTempRecordingFile() + segmentStartTime = System.currentTimeMillis() + + mediaRecorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + MediaRecorder(this) + } else { + @Suppress("DEPRECATION") + MediaRecorder() + } + + mediaRecorder?.apply { + setAudioSource(MediaRecorder.AudioSource.MIC) + setOutputFormat(MediaRecorder.OutputFormat.OGG) + setAudioEncoder(MediaRecorder.AudioEncoder.OPUS) + setAudioEncodingBitRate(64000) + setAudioSamplingRate(48000) + setAudioChannels(1) + setOutputFile(currentTempFile?.absolutePath) + prepare() + start() + } + + // Update currentFile to show the expected encrypted filename + currentFile = currentTempFile?.let { storageHelper.getEncryptedFileForTemp(it) } + + Log.i(TAG, "New segment started: ${currentTempFile?.name}") + } + + private fun rotateSegment() { + if (!isRecording) return + + Log.i(TAG, "Rotating segment...") + totalElapsedTime += System.currentTimeMillis() - segmentStartTime + + try { + startNewSegment() + handler.postDelayed(segmentRotationRunnable, SEGMENT_DURATION_MS) + } catch (e: Exception) { + Log.e(TAG, "Failed to rotate segment", e) + stopRecording() + } + } + + private fun checkStorage() { + if (storageHelper.isStorageLow()) { + Log.w(TAG, "Low storage detected, stopping recording") + stopRecording() + broadcastLowStorage() + } + } + + private fun releaseMediaRecorder() { + val tempFile = currentTempFile + try { + mediaRecorder?.apply { + stop() + release() + } + } catch (e: Exception) { + Log.e(TAG, "Error releasing MediaRecorder", e) + } + mediaRecorder = null + + // Encrypt the completed recording + if (tempFile != null && tempFile.exists()) { + encryptCompletedRecording(tempFile) + } + currentTempFile = null + } + + /** + * Encrypts a completed recording and deletes the temp file. + * If no public key is available, the recording is NOT stored (security requirement). + */ + private fun encryptCompletedRecording(tempFile: File) { + if (!cryptoHelper.hasPublicKey()) { + Log.w(TAG, "No public key configured - deleting unencrypted recording for security") + tempFile.delete() + return + } + + val encryptedFile = storageHelper.getEncryptedFileForTemp(tempFile) + + try { + val success = cryptoHelper.encryptFile(tempFile, encryptedFile) + if (success) { + // Delete temp file after successful encryption + tempFile.delete() + currentFile = encryptedFile + Log.i(TAG, "Recording encrypted: ${encryptedFile.name}") + + // Trigger immediate sync + syncManager?.scheduleImmediateSync() + } else { + Log.e(TAG, "Failed to encrypt recording - deleting temp file for security") + tempFile.delete() + encryptedFile.delete() // Clean up partial encrypted file + } + } catch (e: Exception) { + Log.e(TAG, "Error during encryption - deleting files for security", e) + tempFile.delete() + encryptedFile.delete() + } + } + + private fun acquireWakeLock() { + val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager + wakeLock = powerManager.newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, + "PhoneRecorder::RecordingWakeLock" + ).apply { + acquire() + } + } + + private fun releaseWakeLock() { + wakeLock?.let { + if (it.isHeld) { + it.release() + } + } + wakeLock = null + } + + private fun createNotificationChannel() { + val channel = NotificationChannel( + CHANNEL_ID, + getString(R.string.notification_channel_name), + NotificationManager.IMPORTANCE_LOW + ).apply { + description = getString(R.string.notification_channel_description) + setShowBadge(false) + } + notificationManager.createNotificationChannel(channel) + } + + private fun createNotification(): Notification { + val intent = Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + } + val pendingIntent = PendingIntent.getActivity( + this, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val elapsedFormatted = formatElapsedTime(getElapsedTime()) + + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(getString(R.string.notification_title)) + .setContentText(getString(R.string.notification_text, elapsedFormatted)) + .setSmallIcon(android.R.drawable.ic_btn_speak_now) + .setContentIntent(pendingIntent) + .setOngoing(true) + .setSilent(true) + .build() + } + + private fun updateNotification() { + if (isRecording) { + notificationManager.notify(NOTIFICATION_ID, createNotification()) + } + } + + private fun formatElapsedTime(millis: Long): String { + val seconds = (millis / 1000) % 60 + val minutes = (millis / (1000 * 60)) % 60 + val hours = millis / (1000 * 60 * 60) + return String.format(Locale.US, "%02d:%02d:%02d", hours, minutes, seconds) + } + + private fun broadcastRecordingStarted() { + val intent = Intent(ACTION_RECORDING_STARTED).apply { + putExtra(EXTRA_FILENAME, currentFile?.name) + } + LocalBroadcastManager.getInstance(this).sendBroadcast(intent) + } + + private fun broadcastRecordingStopped() { + LocalBroadcastManager.getInstance(this).sendBroadcast(Intent(ACTION_RECORDING_STOPPED)) + } + + private fun broadcastUpdate() { + val intent = Intent(ACTION_RECORDING_UPDATE).apply { + putExtra(EXTRA_FILENAME, currentFile?.name) + putExtra(EXTRA_ELAPSED_TIME, getElapsedTime()) + } + LocalBroadcastManager.getInstance(this).sendBroadcast(intent) + } + + private fun broadcastLowStorage() { + LocalBroadcastManager.getInstance(this).sendBroadcast(Intent(ACTION_LOW_STORAGE)) + } +} diff --git a/clio-android/app/src/main/java/com/example/phonerecorder/StorageHelper.kt b/clio-android/app/src/main/java/com/example/phonerecorder/StorageHelper.kt new file mode 100644 index 0000000..bea07ef --- /dev/null +++ b/clio-android/app/src/main/java/com/example/phonerecorder/StorageHelper.kt @@ -0,0 +1,222 @@ +package com.example.phonerecorder + +import android.content.Context +import android.os.StatFs +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class StorageHelper(private val context: Context) { + + companion object { + private const val RECORDINGS_FOLDER = "recordings" + private const val TEMP_FOLDER = "temp" + private const val LOW_STORAGE_THRESHOLD_MB = 500L + private const val BYTES_PER_MB = 1024L * 1024L + private const val BYTES_PER_GB = 1024L * 1024L * 1024L + } + + data class StorageInfo( + val usedBytes: Long, + val availableBytes: Long, + // this should be a storage type, not an isSDCard + val isSDCard: Boolean, + val recordingsDir: File + ) + + /** + * Gets the best available storage location. + * Prefers SD card, falls back to internal storage. + */ + fun getRecordingsDirectory(): File { + val externalDirs = context.getExternalFilesDirs(null) + + // Find SD card (second external storage, if available) + val sdCardDir = externalDirs.getOrNull(1) + if (sdCardDir != null && isStorageAvailable(sdCardDir)) { + return ensureRecordingsFolder(sdCardDir) + } + + // Fall back to primary external storage + val primaryDir = externalDirs.getOrNull(0) + if (primaryDir != null && isStorageAvailable(primaryDir)) { + return ensureRecordingsFolder(primaryDir) + } + + // Last resort: internal files directory + return ensureRecordingsFolder(context.filesDir) + } + + /** + * Checks if we're using SD card storage. + */ + fun isUsingSDCard(): Boolean { + val externalDirs = context.getExternalFilesDirs(null) + val sdCardDir = externalDirs.getOrNull(1) + return sdCardDir != null && isStorageAvailable(sdCardDir) + } + + /** + * Gets storage information for the recordings directory. + */ + fun getStorageInfo(): StorageInfo { + val recordingsDir = getRecordingsDirectory() + val parentDir = recordingsDir.parentFile ?: recordingsDir + + val statFs = StatFs(parentDir.absolutePath) + val availableBytes = statFs.availableBytes + val usedBytes = calculateUsedStorage(recordingsDir) + + return StorageInfo( + usedBytes = usedBytes, + availableBytes = availableBytes, + isSDCard = isUsingSDCard(), + recordingsDir = recordingsDir + ) + } + + /** + * Checks if storage is below the low threshold. + */ + fun isStorageLow(): Boolean { + val info = getStorageInfo() + return info.availableBytes < LOW_STORAGE_THRESHOLD_MB * BYTES_PER_MB + } + + /** + * Gets the temp directory for unencrypted recordings during recording. + */ + fun getTempDirectory(): File { + val baseDir = getBaseStorageDirectory() + return ensureFolder(baseDir, TEMP_FOLDER) + } + + /** + * Gets the base storage directory (parent of recordings and temp). + */ + private fun getBaseStorageDirectory(): File { + val externalDirs = context.getExternalFilesDirs(null) + + // Find SD card (second external storage, if available) + val sdCardDir = externalDirs.getOrNull(1) + if (sdCardDir != null && isStorageAvailable(sdCardDir)) { + return sdCardDir + } + + // Fall back to primary external storage + val primaryDir = externalDirs.getOrNull(0) + if (primaryDir != null && isStorageAvailable(primaryDir)) { + return primaryDir + } + + // Last resort: internal files directory + return context.filesDir + } + + /** + * Generates a new recording filename with current timestamp (for encrypted files). + */ + fun generateFilename(): String { + val dateFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US) + val timestamp = dateFormat.format(Date()) + return "recording_$timestamp.ogg.enc" + } + + /** + * Generates a temp filename (unencrypted, used during recording). + */ + fun generateTempFilename(): String { + val dateFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US) + val timestamp = dateFormat.format(Date()) + return "recording_$timestamp.ogg" + } + + /** + * Gets the full path for a new temp recording file (unencrypted during recording). + */ + fun getNewTempRecordingFile(): File { + val dir = getTempDirectory() + return File(dir, generateTempFilename()) + } + + /** + * Gets the full path for a new encrypted recording file. + */ + fun getNewRecordingFile(): File { + val dir = getRecordingsDirectory() + return File(dir, generateFilename()) + } + + /** + * Gets the encrypted file path corresponding to a temp file. + */ + fun getEncryptedFileForTemp(tempFile: File): File { + val dir = getRecordingsDirectory() + val encryptedName = tempFile.name + ".enc" + return File(dir, encryptedName) + } + + /** + * Lists all unsynced encrypted recording files. + */ + fun listUnsyncedRecordings(syncedFiles: Set): List { + val dir = getRecordingsDirectory() + return dir.listFiles { file -> file.name.endsWith(".ogg.enc") } + ?.filter { !syncedFiles.contains(it.name) } + ?.sortedBy { it.lastModified() } + ?: emptyList() + } + + /** + * Formats bytes as human-readable string (e.g., "2.5 GB"). + */ + fun formatBytes(bytes: Long): String { + return when { + bytes >= BYTES_PER_GB -> String.format(Locale.US, "%.1f GB", bytes.toDouble() / BYTES_PER_GB) + bytes >= BYTES_PER_MB -> String.format(Locale.US, "%.1f MB", bytes.toDouble() / BYTES_PER_MB) + else -> String.format(Locale.US, "%d KB", bytes / 1024) + } + } + + /** + * Lists all recording files sorted by date (newest first). + * Includes both encrypted (.ogg.enc) and unencrypted (.ogg) files. + */ + fun listRecordings(): List { + val dir = getRecordingsDirectory() + return dir.listFiles { file -> + file.name.endsWith(".ogg") || file.name.endsWith(".ogg.enc") + } + ?.sortedByDescending { it.lastModified() } + ?: emptyList() + } + + private fun isStorageAvailable(dir: File): Boolean { + return try { + if (!dir.exists()) { + dir.mkdirs() + } + dir.canWrite() + } catch (e: Exception) { + false + } + } + + private fun ensureRecordingsFolder(baseDir: File): File { + return ensureFolder(baseDir, RECORDINGS_FOLDER) + } + + private fun ensureFolder(baseDir: File, folderName: String): File { + val dir = File(baseDir, folderName) + if (!dir.exists()) { + dir.mkdirs() + } + return dir + } + + private fun calculateUsedStorage(dir: File): Long { + if (!dir.exists()) return 0L + return dir.listFiles()?.sumOf { it.length() } ?: 0L + } +} diff --git a/clio-android/app/src/main/java/com/example/phonerecorder/api/RecordingsApi.kt b/clio-android/app/src/main/java/com/example/phonerecorder/api/RecordingsApi.kt new file mode 100644 index 0000000..4f9f901 --- /dev/null +++ b/clio-android/app/src/main/java/com/example/phonerecorder/api/RecordingsApi.kt @@ -0,0 +1,229 @@ +package com.example.phonerecorder.api + +import android.util.Log +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.asRequestBody +import org.json.JSONObject +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.security.MessageDigest +import java.util.concurrent.TimeUnit + +class RecordingsApi { + + companion object { + private const val TAG = "RecordingsApi" + private const val CONNECT_TIMEOUT_SECONDS = 30L + private const val READ_TIMEOUT_SECONDS = 120L + private const val WRITE_TIMEOUT_SECONDS = 120L + } + + private val client = OkHttpClient.Builder() + .connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .writeTimeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .build() + + /** + * Checks if the server is healthy and reachable. + * + * @param serverUrl The base server URL + * @return true if server responds with status "ok" + */ + fun checkHealth(serverUrl: String): Boolean { + val request = Request.Builder() + .url("$serverUrl/health") + .get() + .build() + + return try { + client.newCall(request).execute().use { response -> + if (response.isSuccessful) { + val body = response.body?.string() + val json = JSONObject(body ?: "{}") + json.optString("status") == "ok" + } else { + Log.w(TAG, "Health check failed: ${response.code}") + false + } + } + } catch (e: IOException) { + Log.e(TAG, "Health check error", e) + false + } + } + + /** + * Fetches the server's public key for encryption. + * + * @param serverUrl The base server URL + * @return Base64-encoded public key, or null on failure + */ + fun getPublicKey(serverUrl: String): String? { + val request = Request.Builder() + .url("$serverUrl/api/v1/public-key") + .get() + .build() + + return try { + client.newCall(request).execute().use { response -> + if (response.isSuccessful) { + val body = response.body?.string() + val json = JSONObject(body ?: "{}") + json.optString("public_key", null) + } else { + Log.w(TAG, "Get public key failed: ${response.code}") + null + } + } + } catch (e: IOException) { + Log.e(TAG, "Get public key error", e) + null + } + } + + /** + * Uploads an encrypted recording file to the server. + * Verifies the server received the correct data by comparing SHA-256 hashes. + * + * @param serverUrl The base server URL + * @param file The encrypted file to upload + * @param apiKey Optional API key for authentication + * @return UploadResult indicating success or failure with message + */ + fun uploadRecording(serverUrl: String, file: File, apiKey: String? = null): UploadResult { + // Compute hash of file before upload for verification + val localHash = computeFileHash(file) + if (localHash == null) { + Log.e(TAG, "Failed to compute hash for: ${file.name}") + return UploadResult(false, "Failed to compute file hash") + } + + val mediaType = "application/octet-stream".toMediaType() + val requestBody = MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart( + "file", + file.name, + file.asRequestBody(mediaType) + ) + .build() + + val requestBuilder = Request.Builder() + .url("$serverUrl/api/v1/recordings/upload") + .post(requestBody) + + if (!apiKey.isNullOrEmpty()) { + requestBuilder.addHeader("X-API-Key", apiKey) + } + + val request = requestBuilder.build() + + return try { + client.newCall(request).execute().use { response -> + val body = response.body?.string() + if (response.isSuccessful) { + // Verify server received correct data + val json = JSONObject(body ?: "{}") + val serverHash = json.optString("received_hash", "") + val verified = json.optBoolean("verified", false) + + if (serverHash.isNotEmpty() && serverHash == localHash && verified) { + Log.i(TAG, "Upload verified: ${file.name} (hash: ${localHash.take(8)}...)") + UploadResult(true, "Upload verified", verified = true) + } else if (serverHash.isEmpty()) { + // Legacy server without hash verification + Log.w(TAG, "Upload successful but unverified (no hash): ${file.name}") + UploadResult(true, "Upload successful (unverified)", verified = false) + } else { + Log.e(TAG, "Hash mismatch! Local: $localHash, Server: $serverHash") + UploadResult(false, "Data corruption detected - hash mismatch") + } + } else { + val errorMessage = try { + val json = JSONObject(body ?: "{}") + json.optString("detail", "Upload failed: ${response.code}") + } catch (e: Exception) { + "Upload failed: ${response.code}" + } + Log.w(TAG, "Upload failed: $errorMessage") + UploadResult(false, errorMessage) + } + } + } catch (e: IOException) { + Log.e(TAG, "Upload error", e) + UploadResult(false, "Network error: ${e.message}") + } + } + + /** + * Computes SHA-256 hash of a file. + */ + private fun computeFileHash(file: File): String? { + return try { + val digest = MessageDigest.getInstance("SHA-256") + FileInputStream(file).use { fis -> + val buffer = ByteArray(8192) + var bytesRead: Int + while (fis.read(buffer).also { bytesRead = it } != -1) { + digest.update(buffer, 0, bytesRead) + } + } + digest.digest().joinToString("") { "%02x".format(it) } + } catch (e: Exception) { + Log.e(TAG, "Hash computation failed", e) + null + } + } + + /** + * Lists recordings on the server, optionally filtered by date. + * + * @param serverUrl The base server URL + * @param date Optional date filter in YYYY-MM-DD format + * @return List of recording filenames, or null on failure + */ + fun listRecordings(serverUrl: String, date: String? = null): List? { + val url = if (date != null) { + "$serverUrl/api/v1/recordings?date=$date" + } else { + "$serverUrl/api/v1/recordings" + } + + val request = Request.Builder() + .url(url) + .get() + .build() + + return try { + client.newCall(request).execute().use { response -> + if (response.isSuccessful) { + val body = response.body?.string() + val json = JSONObject(body ?: "{}") + val recordings = json.optJSONArray("recordings") + if (recordings != null) { + (0 until recordings.length()).map { recordings.getString(it) } + } else { + emptyList() + } + } else { + Log.w(TAG, "List recordings failed: ${response.code}") + null + } + } + } catch (e: IOException) { + Log.e(TAG, "List recordings error", e) + null + } + } + + data class UploadResult( + val success: Boolean, + val message: String, + val verified: Boolean = false + ) +} diff --git a/clio-android/app/src/main/java/com/example/phonerecorder/crypto/CryptoHelper.kt b/clio-android/app/src/main/java/com/example/phonerecorder/crypto/CryptoHelper.kt new file mode 100644 index 0000000..28dfb35 --- /dev/null +++ b/clio-android/app/src/main/java/com/example/phonerecorder/crypto/CryptoHelper.kt @@ -0,0 +1,124 @@ +package com.example.phonerecorder.crypto + +import android.util.Log +import com.goterl.lazysodium.LazySodiumAndroid +import com.goterl.lazysodium.SodiumAndroid +import com.goterl.lazysodium.interfaces.Box +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.ByteOrder + +class CryptoHelper(private val keyManager: KeyManager) { + + companion object { + private const val TAG = "CryptoHelper" + + // File format constants + private val MAGIC_BYTES = byteArrayOf('S'.code.toByte(), 'R'.code.toByte(), 'E'.code.toByte(), 'C'.code.toByte()) + private const val VERSION: Byte = 1 + private const val CHUNK_SIZE = 64 * 1024 // 64KB chunks for streaming encryption + } + + private val lazySodium: LazySodiumAndroid = LazySodiumAndroid(SodiumAndroid()) + + /** + * Encrypts a file using libsodium sealed box encryption. + * + * File format: + * [4 bytes: "SREC" magic] + * [1 byte: version (1)] + * [4 bytes: chunk count (big-endian)] + * For each chunk: + * [4 bytes: encrypted length (big-endian)] + * [N bytes: sealed box encrypted data] + * + * @param inputFile The unencrypted source file + * @param outputFile The destination for the encrypted file + * @return true if encryption succeeded, false otherwise + */ + fun encryptFile(inputFile: File, outputFile: File): Boolean { + val publicKey = keyManager.getPublicKey() + if (publicKey == null) { + Log.e(TAG, "No public key available for encryption") + return false + } + + try { + FileInputStream(inputFile).use { input -> + FileOutputStream(outputFile).use { output -> + // Calculate number of chunks + val fileSize = inputFile.length() + val chunkCount = ((fileSize + CHUNK_SIZE - 1) / CHUNK_SIZE).toInt() + + // Write header + output.write(MAGIC_BYTES) + output.write(VERSION.toInt()) + output.write(intToBytes(chunkCount)) + + // Encrypt and write each chunk + val buffer = ByteArray(CHUNK_SIZE) + var bytesRead: Int + + while (input.read(buffer).also { bytesRead = it } != -1) { + val chunk = if (bytesRead < CHUNK_SIZE) { + buffer.copyOf(bytesRead) + } else { + buffer + } + + val encrypted = encryptChunk(chunk, publicKey) + if (encrypted == null) { + Log.e(TAG, "Failed to encrypt chunk") + return false + } + + // Write encrypted chunk length and data + output.write(intToBytes(encrypted.size)) + output.write(encrypted) + } + } + } + + Log.i(TAG, "Successfully encrypted file: ${inputFile.name} -> ${outputFile.name}") + return true + + } catch (e: IOException) { + Log.e(TAG, "IO error during encryption", e) + return false + } catch (e: Exception) { + Log.e(TAG, "Unexpected error during encryption", e) + return false + } + } + + /** + * Encrypts a single chunk using sealed box encryption. + * Sealed box provides anonymous sender encryption using X25519 + XSalsa20-Poly1305. + */ + private fun encryptChunk(data: ByteArray, publicKey: ByteArray): ByteArray? { + return try { + // Sealed box overhead: crypto_box_SEALBYTES (48 bytes) + val ciphertext = ByteArray(data.size + Box.SEALBYTES) + val success = lazySodium.cryptoBoxSeal(ciphertext, data, data.size.toLong(), publicKey) + if (success) ciphertext else null + } catch (e: Exception) { + Log.e(TAG, "Encryption failed", e) + null + } + } + + private fun intToBytes(value: Int): ByteArray { + return ByteBuffer.allocate(4) + .order(ByteOrder.BIG_ENDIAN) + .putInt(value) + .array() + } + + /** + * Checks if a public key is available for encryption. + */ + fun hasPublicKey(): Boolean = keyManager.getPublicKey() != null +} diff --git a/clio-android/app/src/main/java/com/example/phonerecorder/crypto/KeyManager.kt b/clio-android/app/src/main/java/com/example/phonerecorder/crypto/KeyManager.kt new file mode 100644 index 0000000..46ce64a --- /dev/null +++ b/clio-android/app/src/main/java/com/example/phonerecorder/crypto/KeyManager.kt @@ -0,0 +1,77 @@ +package com.example.phonerecorder.crypto + +import android.content.Context +import android.content.SharedPreferences +import android.util.Base64 +import android.util.Log +import com.goterl.lazysodium.interfaces.Box + +class KeyManager(context: Context) { + + companion object { + private const val TAG = "KeyManager" + private const val PREFS_NAME = "crypto_prefs" + private const val KEY_PUBLIC_KEY = "server_public_key" + private const val PUBLIC_KEY_LENGTH = Box.PUBLICKEYBYTES // 32 bytes for X25519 + } + + private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + /** + * Stores the server's public key from a Base64-encoded string. + * + * @param base64Key The Base64-encoded public key + * @return true if the key was valid and stored, false otherwise + */ + fun setPublicKey(base64Key: String): Boolean { + return try { + val keyBytes = Base64.decode(base64Key.trim(), Base64.DEFAULT) + if (keyBytes.size != PUBLIC_KEY_LENGTH) { + Log.e(TAG, "Invalid public key length: ${keyBytes.size}, expected $PUBLIC_KEY_LENGTH") + return false + } + prefs.edit().putString(KEY_PUBLIC_KEY, base64Key.trim()).apply() + Log.i(TAG, "Server public key stored successfully") + true + } catch (e: IllegalArgumentException) { + Log.e(TAG, "Invalid Base64 encoding for public key", e) + false + } + } + + /** + * Retrieves the stored server public key as raw bytes. + * + * @return The public key bytes, or null if not set + */ + fun getPublicKey(): ByteArray? { + val base64Key = prefs.getString(KEY_PUBLIC_KEY, null) ?: return null + return try { + val keyBytes = Base64.decode(base64Key, Base64.DEFAULT) + if (keyBytes.size == PUBLIC_KEY_LENGTH) keyBytes else null + } catch (e: IllegalArgumentException) { + Log.e(TAG, "Failed to decode stored public key", e) + null + } + } + + /** + * Returns the stored public key as a Base64 string for display. + */ + fun getPublicKeyBase64(): String? { + return prefs.getString(KEY_PUBLIC_KEY, null) + } + + /** + * Checks if a public key has been configured. + */ + fun hasPublicKey(): Boolean = getPublicKey() != null + + /** + * Clears the stored public key. + */ + fun clearPublicKey() { + prefs.edit().remove(KEY_PUBLIC_KEY).apply() + Log.i(TAG, "Server public key cleared") + } +} diff --git a/clio-android/app/src/main/java/com/example/phonerecorder/sync/NetworkHelper.kt b/clio-android/app/src/main/java/com/example/phonerecorder/sync/NetworkHelper.kt new file mode 100644 index 0000000..0353a14 --- /dev/null +++ b/clio-android/app/src/main/java/com/example/phonerecorder/sync/NetworkHelper.kt @@ -0,0 +1,99 @@ +package com.example.phonerecorder.sync + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.wifi.WifiManager +import android.util.Log +import java.net.Inet4Address +import java.net.NetworkInterface + +class NetworkHelper(private val context: Context) { + + companion object { + private const val TAG = "NetworkHelper" + } + + private val connectivityManager: ConnectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + private val syncSettings = SyncSettings(context) + + /** + * Checks if any network is available. + */ + fun isNetworkAvailable(): Boolean { + val network = connectivityManager.activeNetwork ?: return false + val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false + return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } + + /** + * Checks if currently connected to WiFi. + */ + fun isOnWifi(): Boolean { + val network = connectivityManager.activeNetwork ?: return false + val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false + return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + } + + /** + * Checks if currently on the configured LAN subnet. + * The subnet prefix is configurable in SyncSettings (default: 10.0.0.) + */ + fun isOnLan(): Boolean { + if (!isOnWifi()) return false + + try { + val ipAddress = getLocalIpAddress() + val lanPrefix = syncSettings.lanSubnetPrefix + val isLan = ipAddress?.startsWith(lanPrefix) == true + Log.d(TAG, "Local IP: $ipAddress, LAN prefix: $lanPrefix, isOnLan: $isLan") + return isLan + } catch (e: Exception) { + Log.e(TAG, "Error checking LAN status", e) + return false + } + } + + /** + * Returns the current local IP address (for display/debugging). + */ + fun getCurrentIpAddress(): String? = getLocalIpAddress() + + /** + * Gets the local IPv4 address. + */ + private fun getLocalIpAddress(): String? { + try { + val interfaces = NetworkInterface.getNetworkInterfaces() + while (interfaces.hasMoreElements()) { + val networkInterface = interfaces.nextElement() + if (networkInterface.isLoopback || !networkInterface.isUp) continue + + val addresses = networkInterface.inetAddresses + while (addresses.hasMoreElements()) { + val address = addresses.nextElement() + if (address is Inet4Address && !address.isLoopbackAddress) { + return address.hostAddress + } + } + } + } catch (e: Exception) { + Log.e(TAG, "Error getting local IP address", e) + } + return null + } + + /** + * Checks if sync should proceed based on current network state and sync mode. + */ + fun shouldSync(syncMode: SyncMode): Boolean { + return when (syncMode) { + SyncMode.NEVER -> false + SyncMode.LAN_ONLY -> isOnLan() + SyncMode.WIFI_ONLY -> isOnWifi() + SyncMode.ANY_NETWORK -> isNetworkAvailable() + } + } +} diff --git a/clio-android/app/src/main/java/com/example/phonerecorder/sync/SyncManager.kt b/clio-android/app/src/main/java/com/example/phonerecorder/sync/SyncManager.kt new file mode 100644 index 0000000..cdb3691 --- /dev/null +++ b/clio-android/app/src/main/java/com/example/phonerecorder/sync/SyncManager.kt @@ -0,0 +1,189 @@ +package com.example.phonerecorder.sync + +import android.content.Context +import android.util.Log +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import com.example.phonerecorder.StorageHelper +import java.util.concurrent.TimeUnit + +class SyncManager private constructor(private val context: Context) { + + companion object { + private const val TAG = "SyncManager" + private const val PERIODIC_SYNC_INTERVAL_MINUTES = 15L + + @Volatile + private var instance: SyncManager? = null + + fun getInstance(context: Context): SyncManager { + return instance ?: synchronized(this) { + instance ?: SyncManager(context.applicationContext).also { instance = it } + } + } + } + + private val workManager = WorkManager.getInstance(context) + private val syncSettings = SyncSettings(context) + private val syncTracker = SyncTracker(context) + private val storageHelper = StorageHelper(context) + private val networkHelper = NetworkHelper(context) + + /** + * Starts or updates the periodic sync schedule based on current settings. + */ + fun startPeriodicSync() { + val syncMode = syncSettings.syncMode + if (syncMode == SyncMode.NEVER) { + Log.i(TAG, "Sync disabled, cancelling periodic work") + workManager.cancelUniqueWork(SyncWorker.WORK_NAME_PERIODIC) + return + } + + val constraints = buildConstraints(syncMode) + + val periodicWorkRequest = PeriodicWorkRequestBuilder( + PERIODIC_SYNC_INTERVAL_MINUTES, TimeUnit.MINUTES + ) + .setConstraints(constraints) + .build() + + workManager.enqueueUniquePeriodicWork( + SyncWorker.WORK_NAME_PERIODIC, + ExistingPeriodicWorkPolicy.UPDATE, + periodicWorkRequest + ) + + Log.i(TAG, "Periodic sync scheduled for mode: $syncMode") + } + + /** + * Stops the periodic sync. + */ + fun stopPeriodicSync() { + workManager.cancelUniqueWork(SyncWorker.WORK_NAME_PERIODIC) + Log.i(TAG, "Periodic sync stopped") + } + + /** + * Triggers an immediate sync attempt. + */ + fun scheduleImmediateSync() { + val syncMode = syncSettings.syncMode + if (syncMode == SyncMode.NEVER) { + Log.i(TAG, "Sync disabled, skipping immediate sync") + return + } + + val constraints = buildConstraints(syncMode) + + val immediateWorkRequest = OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .build() + + workManager.enqueueUniqueWork( + SyncWorker.WORK_NAME_IMMEDIATE, + ExistingWorkPolicy.REPLACE, + immediateWorkRequest + ) + + Log.i(TAG, "Immediate sync scheduled") + } + + /** + * Returns the count of files pending sync. + */ + fun getPendingSyncCount(): Int { + val syncedFiles = syncTracker.getSyncedFiles() + val allFiles = storageHelper.listUnsyncedRecordings(emptySet()) + return allFiles.count { !syncedFiles.contains(it.name) } + } + + /** + * Returns the detailed sync status based on current conditions. + * @param hasKey Whether the server public key is configured + */ + fun getSyncStatus(hasKey: Boolean): SyncStatus { + val syncMode = syncSettings.syncMode + + // Check sync mode first + if (syncMode == SyncMode.NEVER) { + return SyncStatus.Disabled + } + + // Check key + if (!hasKey) { + return SyncStatus.NoKey + } + + // Check network conditions based on mode + when (syncMode) { + SyncMode.LAN_ONLY -> { + if (!networkHelper.isOnWifi()) { + return SyncStatus.NotOnWifi + } + if (!networkHelper.isOnLan()) { + return SyncStatus.NotOnLan + } + } + SyncMode.WIFI_ONLY -> { + if (!networkHelper.isOnWifi()) { + return SyncStatus.NotOnWifi + } + } + SyncMode.ANY_NETWORK -> { + if (!networkHelper.isNetworkAvailable()) { + return SyncStatus.NoNetwork + } + } + SyncMode.NEVER -> { /* Already handled above */ } + } + + // Network conditions are met, check pending count + val pending = getPendingSyncCount() + return if (pending > 0) { + SyncStatus.Ready(pending) + } else { + SyncStatus.Synced + } + } + + /** + * Returns the current sync mode. + */ + fun getSyncMode(): SyncMode = syncSettings.syncMode + + /** + * Updates the sync mode and restarts periodic sync if needed. + */ + fun setSyncMode(mode: SyncMode) { + syncSettings.syncMode = mode + startPeriodicSync() + } + + private fun buildConstraints(syncMode: SyncMode): Constraints { + val builder = Constraints.Builder() + + when (syncMode) { + SyncMode.NEVER -> { + // Should not reach here, but set impossible constraint + builder.setRequiredNetworkType(NetworkType.NOT_REQUIRED) + } + SyncMode.LAN_ONLY, SyncMode.WIFI_ONLY -> { + // Require unmetered (WiFi) connection + builder.setRequiredNetworkType(NetworkType.UNMETERED) + } + SyncMode.ANY_NETWORK -> { + // Any connected network + builder.setRequiredNetworkType(NetworkType.CONNECTED) + } + } + + return builder.build() + } +} diff --git a/clio-android/app/src/main/java/com/example/phonerecorder/sync/SyncSettings.kt b/clio-android/app/src/main/java/com/example/phonerecorder/sync/SyncSettings.kt new file mode 100644 index 0000000..82dd612 --- /dev/null +++ b/clio-android/app/src/main/java/com/example/phonerecorder/sync/SyncSettings.kt @@ -0,0 +1,86 @@ +package com.example.phonerecorder.sync + +import android.content.Context +import android.content.SharedPreferences + +enum class SyncMode { + NEVER, // Never sync + LAN_ONLY, // Only sync when on LAN (10.0.0.x subnet) + WIFI_ONLY, // Only sync when on WiFi (any network) + ANY_NETWORK // Sync on any network (including mobile data) +} + +sealed class SyncStatus { + object Disabled : SyncStatus() + object NoKey : SyncStatus() + object NotOnWifi : SyncStatus() + object NotOnLan : SyncStatus() + object NoNetwork : SyncStatus() + data class Ready(val pending: Int) : SyncStatus() + object Synced : SyncStatus() +} + +class SyncSettings(context: Context) { + + companion object { + private const val PREFS_NAME = "sync_settings" + private const val KEY_SYNC_MODE = "sync_mode" + private const val KEY_LAN_SERVER_URL = "lan_server_url" + private const val KEY_REMOTE_SERVER_URL = "remote_server_url" + private const val KEY_LAN_SUBNET_PREFIX = "lan_subnet_prefix" + private const val KEY_API_KEY = "api_key" + + const val DEFAULT_LAN_SERVER_URL = "http://10.0.0.45:8000" + const val DEFAULT_REMOTE_SERVER_URL = "https://recordings.hallocks.xyz" + const val DEFAULT_LAN_SUBNET_PREFIX = "10.0.0." // Change this to match your network + } + + private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + var syncMode: SyncMode + get() { + val ordinal = prefs.getInt(KEY_SYNC_MODE, SyncMode.NEVER.ordinal) + return SyncMode.entries.getOrElse(ordinal) { SyncMode.NEVER } + } + set(value) { + prefs.edit().putInt(KEY_SYNC_MODE, value.ordinal).apply() + } + + var lanServerUrl: String + get() = prefs.getString(KEY_LAN_SERVER_URL, DEFAULT_LAN_SERVER_URL) ?: DEFAULT_LAN_SERVER_URL + set(value) { + prefs.edit().putString(KEY_LAN_SERVER_URL, value).apply() + } + + var remoteServerUrl: String + get() = prefs.getString(KEY_REMOTE_SERVER_URL, DEFAULT_REMOTE_SERVER_URL) ?: DEFAULT_REMOTE_SERVER_URL + set(value) { + prefs.edit().putString(KEY_REMOTE_SERVER_URL, value).apply() + } + + /** + * The subnet prefix used to detect LAN (e.g., "192.168.1." or "10.0.0."). + * Your phone must be on this subnet for LAN mode to work. + */ + var lanSubnetPrefix: String + get() = prefs.getString(KEY_LAN_SUBNET_PREFIX, DEFAULT_LAN_SUBNET_PREFIX) ?: DEFAULT_LAN_SUBNET_PREFIX + set(value) { + prefs.edit().putString(KEY_LAN_SUBNET_PREFIX, value).apply() + } + + /** + * API key for upload authentication. + */ + var apiKey: String + get() = prefs.getString(KEY_API_KEY, "") ?: "" + set(value) { + prefs.edit().putString(KEY_API_KEY, value).apply() + } + + /** + * Returns the appropriate server URL based on whether we're on LAN. + */ + fun getServerUrl(isOnLan: Boolean): String { + return if (isOnLan) lanServerUrl else remoteServerUrl + } +} diff --git a/clio-android/app/src/main/java/com/example/phonerecorder/sync/SyncTracker.kt b/clio-android/app/src/main/java/com/example/phonerecorder/sync/SyncTracker.kt new file mode 100644 index 0000000..10524af --- /dev/null +++ b/clio-android/app/src/main/java/com/example/phonerecorder/sync/SyncTracker.kt @@ -0,0 +1,82 @@ +package com.example.phonerecorder.sync + +import android.content.Context +import android.content.SharedPreferences + +class SyncTracker(context: Context) { + + companion object { + private const val PREFS_NAME = "sync_tracker" + private const val KEY_SYNCED_FILES = "synced_files" + private const val SEPARATOR = "|" + } + + private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + /** + * Returns the set of filenames that have been successfully synced. + */ + fun getSyncedFiles(): Set { + val stored = prefs.getString(KEY_SYNCED_FILES, "") ?: "" + return if (stored.isEmpty()) { + emptySet() + } else { + stored.split(SEPARATOR).toSet() + } + } + + /** + * Marks a file as successfully synced. + */ + fun markSynced(filename: String) { + val current = getSyncedFiles().toMutableSet() + current.add(filename) + saveSyncedFiles(current) + } + + /** + * Marks multiple files as successfully synced. + */ + fun markSynced(filenames: Collection) { + val current = getSyncedFiles().toMutableSet() + current.addAll(filenames) + saveSyncedFiles(current) + } + + /** + * Checks if a specific file has been synced. + */ + fun isSynced(filename: String): Boolean { + return getSyncedFiles().contains(filename) + } + + /** + * Returns the count of files pending sync. + */ + fun getPendingCount(allFiles: List): Int { + val synced = getSyncedFiles() + return allFiles.count { !synced.contains(it) } + } + + /** + * Clears the sync history for a specific file. + */ + fun clearSyncStatus(filename: String) { + val current = getSyncedFiles().toMutableSet() + current.remove(filename) + saveSyncedFiles(current) + } + + /** + * Clears all sync history. + */ + fun clearAll() { + prefs.edit().remove(KEY_SYNCED_FILES).apply() + } + + private fun saveSyncedFiles(files: Set) { + prefs.edit() + .putString(KEY_SYNCED_FILES, files.joinToString(SEPARATOR)) + .apply() + } +} diff --git a/clio-android/app/src/main/java/com/example/phonerecorder/sync/SyncWorker.kt b/clio-android/app/src/main/java/com/example/phonerecorder/sync/SyncWorker.kt new file mode 100644 index 0000000..36ccd91 --- /dev/null +++ b/clio-android/app/src/main/java/com/example/phonerecorder/sync/SyncWorker.kt @@ -0,0 +1,104 @@ +package com.example.phonerecorder.sync + +import android.content.Context +import android.util.Log +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.example.phonerecorder.StorageHelper +import com.example.phonerecorder.api.RecordingsApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class SyncWorker( + context: Context, + params: WorkerParameters +) : CoroutineWorker(context, params) { + + companion object { + private const val TAG = "SyncWorker" + const val WORK_NAME_PERIODIC = "sync_worker_periodic" + const val WORK_NAME_IMMEDIATE = "sync_worker_immediate" + } + + private val syncSettings = SyncSettings(applicationContext) + private val networkHelper = NetworkHelper(applicationContext) + private val syncTracker = SyncTracker(applicationContext) + private val storageHelper = StorageHelper(applicationContext) + private val recordingsApi = RecordingsApi() + + override suspend fun doWork(): Result = withContext(Dispatchers.IO) { + Log.i(TAG, "Starting sync work") + + // Check if sync is enabled and conditions are met + val syncMode = syncSettings.syncMode + if (syncMode == SyncMode.NEVER) { + Log.i(TAG, "Sync disabled") + return@withContext Result.success() + } + + if (!networkHelper.shouldSync(syncMode)) { + Log.i(TAG, "Network conditions not met for sync mode: $syncMode") + return@withContext Result.retry() + } + + // Determine server URL based on sync mode + // LAN_ONLY mode uses LAN URL, other modes use remote URL + val serverUrl = if (syncMode == SyncMode.LAN_ONLY) { + syncSettings.lanServerUrl + } else { + syncSettings.remoteServerUrl + } + Log.i(TAG, "Using server: $serverUrl (mode: $syncMode)") + + // Check server health + if (!recordingsApi.checkHealth(serverUrl)) { + Log.w(TAG, "Server health check failed") + return@withContext Result.retry() + } + + // Get unsynced files + val syncedFiles = syncTracker.getSyncedFiles() + val unsyncedFiles = storageHelper.listUnsyncedRecordings(syncedFiles) + + if (unsyncedFiles.isEmpty()) { + Log.i(TAG, "No files to sync") + return@withContext Result.success() + } + + Log.i(TAG, "Found ${unsyncedFiles.size} files to sync") + + var successCount = 0 + var failCount = 0 + + for (file in unsyncedFiles) { + // Re-check network conditions before each upload + if (!networkHelper.shouldSync(syncMode)) { + Log.w(TAG, "Network conditions changed during sync") + break + } + + val result = recordingsApi.uploadRecording(serverUrl, file, syncSettings.apiKey) + if (result.success && result.verified) { + // Only mark as synced if server verified the hash + syncTracker.markSynced(file.name) + successCount++ + Log.i(TAG, "Synced and verified: ${file.name}") + } else if (result.success && !result.verified) { + // Upload succeeded but no verification - don't mark as synced, retry later + Log.w(TAG, "Upload succeeded but unverified, will retry: ${file.name}") + failCount++ + } else { + failCount++ + Log.w(TAG, "Failed to sync ${file.name}: ${result.message}") + } + } + + Log.i(TAG, "Sync completed: $successCount succeeded, $failCount failed") + + if (failCount > 0 && successCount == 0) { + return@withContext Result.retry() + } + + return@withContext Result.success() + } +} diff --git a/clio-android/app/src/main/res/drawable/ic_launcher_foreground.xml b/clio-android/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..947fed3 --- /dev/null +++ b/clio-android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/clio-android/app/src/main/res/drawable/status_circle.xml b/clio-android/app/src/main/res/drawable/status_circle.xml new file mode 100644 index 0000000..fe892f1 --- /dev/null +++ b/clio-android/app/src/main/res/drawable/status_circle.xml @@ -0,0 +1,5 @@ + + + + diff --git a/clio-android/app/src/main/res/drawable/status_circle_active.xml b/clio-android/app/src/main/res/drawable/status_circle_active.xml new file mode 100644 index 0000000..ea62a74 --- /dev/null +++ b/clio-android/app/src/main/res/drawable/status_circle_active.xml @@ -0,0 +1,5 @@ + + + + diff --git a/clio-android/app/src/main/res/layout/activity_main.xml b/clio-android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..298fec6 --- /dev/null +++ b/clio-android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clio-android/app/src/main/res/mipmap-hdpi/ic_launcher.xml b/clio-android/app/src/main/res/mipmap-hdpi/ic_launcher.xml new file mode 100644 index 0000000..a8b92c6 --- /dev/null +++ b/clio-android/app/src/main/res/mipmap-hdpi/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/clio-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.xml b/clio-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.xml new file mode 100644 index 0000000..a8b92c6 --- /dev/null +++ b/clio-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/clio-android/app/src/main/res/mipmap-mdpi/ic_launcher.xml b/clio-android/app/src/main/res/mipmap-mdpi/ic_launcher.xml new file mode 100644 index 0000000..a8b92c6 --- /dev/null +++ b/clio-android/app/src/main/res/mipmap-mdpi/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/clio-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.xml b/clio-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.xml new file mode 100644 index 0000000..a8b92c6 --- /dev/null +++ b/clio-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/clio-android/app/src/main/res/mipmap-xhdpi/ic_launcher.xml b/clio-android/app/src/main/res/mipmap-xhdpi/ic_launcher.xml new file mode 100644 index 0000000..a8b92c6 --- /dev/null +++ b/clio-android/app/src/main/res/mipmap-xhdpi/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/clio-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.xml b/clio-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.xml new file mode 100644 index 0000000..a8b92c6 --- /dev/null +++ b/clio-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/clio-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.xml b/clio-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.xml new file mode 100644 index 0000000..a8b92c6 --- /dev/null +++ b/clio-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/clio-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.xml b/clio-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.xml new file mode 100644 index 0000000..a8b92c6 --- /dev/null +++ b/clio-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/clio-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.xml b/clio-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.xml new file mode 100644 index 0000000..a8b92c6 --- /dev/null +++ b/clio-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/clio-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.xml b/clio-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.xml new file mode 100644 index 0000000..a8b92c6 --- /dev/null +++ b/clio-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/clio-android/app/src/main/res/values/colors.xml b/clio-android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..fcc78ce --- /dev/null +++ b/clio-android/app/src/main/res/values/colors.xml @@ -0,0 +1,9 @@ + + + #D32F2F + #B71C1C + #FF5252 + #4CAF50 + #9E9E9E + #FF9800 + diff --git a/clio-android/app/src/main/res/values/strings.xml b/clio-android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..592eeea --- /dev/null +++ b/clio-android/app/src/main/res/values/strings.xml @@ -0,0 +1,58 @@ + + + Phone Recorder + Recording + Stopped + Start Recording + Stop Recording + Storage: %1$s used / %2$s available + Battery: %1$d%% + Current file: %1$s + No file + Recording + Shows when recording is active + Recording in progress + Elapsed: %1$s + Low storage! Recording stopped. + Storage: SD Card + Storage: Internal (SD card not available) + Auto-start on boot: Enabled + Auto-start on boot: Disabled + Battery Optimization + For reliable background recording, disable battery optimization for this app. + Open Settings + Tip: Disable battery optimization for best reliability + Microphone permission is required for recording + + + Sync Mode + Never + LAN Only + WiFi Only + Any Network + Sync: %1$d pending + Sync: Disabled + Sync: No server key configured + Sync: Not on WiFi + Sync: Not on configured LAN + Sync: No network connection + Sync: All synced + Import Server Key + Import Server Public Key + Paste Base64 public key + Server key imported successfully + Invalid key format + Key: Configured + Key: Not configured + + + Network: Not connected + Network: WiFi (%1$s) + Network: Mobile data + LAN Subnet + e.g., 192.168.1. or 10.0.0. + Configure + LAN Subnet Configuration + Enter the IP prefix for your home network.\n\nYour current IP: %1$s\n\nCommon examples:\n• 192.168.1.\n• 192.168.0.\n• 10.0.0. + LAN subnet saved: %1$s + diff --git a/clio-android/app/src/main/res/values/themes.xml b/clio-android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..631ab1b --- /dev/null +++ b/clio-android/app/src/main/res/values/themes.xml @@ -0,0 +1,11 @@ + + + + diff --git a/clio-android/app/src/main/res/xml/file_paths.xml b/clio-android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..7e9631f --- /dev/null +++ b/clio-android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,6 @@ + + + + diff --git a/clio-android/build.gradle.kts b/clio-android/build.gradle.kts new file mode 100644 index 0000000..3a6ec8d --- /dev/null +++ b/clio-android/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + id("com.android.application") version "8.2.0" apply false + id("org.jetbrains.kotlin.android") version "1.9.20" apply false +} diff --git a/clio-android/description.md b/clio-android/description.md new file mode 100644 index 0000000..bfe16ab --- /dev/null +++ b/clio-android/description.md @@ -0,0 +1,136 @@ +# Phone Recorder - Architecture Overview + +## Purpose + +Android app that records audio continuously, encrypts recordings immediately (never stored unencrypted), and syncs them to a server over LAN or internet. + +## Security Model + +``` +Phone (has: server public key) Server (has: private key) +───────────────────────────── ──────────────────────── +Record audio → temp.ogg +Encrypt with public key → + recording_*.ogg.enc + │ + └──── HTTP POST ────────→ Receive encrypted file + Decrypt with private key + Store as .ogg in recordings/ +``` + +- **Sealed box encryption**: X25519 key exchange + XSalsa20-Poly1305 +- **Forward secrecy**: Each encryption uses ephemeral keys +- **Phone never stores unencrypted**: Temp file deleted immediately after encryption +- **If no public key configured**: Recordings are deleted (security-first design) + +## File Format (SREC) + +Encrypted files use a chunked format for streaming large files: + +``` +[4 bytes: "SREC" magic] +[1 byte: version (1)] +[4 bytes: chunk count, big-endian] +For each chunk (64KB max): + [4 bytes: encrypted length, big-endian] + [N bytes: sealed box ciphertext] +``` + +## Android App Structure + +### Main Files + +| File | Purpose | +|------|---------| +| `MainActivity.kt` | UI, settings, sync controls | +| `RecordingService.kt` | Foreground service for recording, encryption on stop | +| `StorageHelper.kt` | File paths, temp vs recordings directories | + +### Crypto Package (`crypto/`) + +| File | Purpose | +|------|---------| +| `CryptoHelper.kt` | SREC format encryption using lazysodium | +| `KeyManager.kt` | Store/retrieve server public key (SharedPreferences) | + +### Sync Package (`sync/`) + +| File | Purpose | +|------|---------| +| `SyncSettings.kt` | Sync mode enum, server URLs, LAN subnet prefix | +| `NetworkHelper.kt` | Detect WiFi, LAN (by IP prefix), network availability | +| `SyncTracker.kt` | Track which files synced successfully (SharedPreferences) | +| `SyncWorker.kt` | WorkManager background job for uploads | +| `SyncManager.kt` | Schedule periodic (15min) and immediate syncs | + +### API Package (`api/`) + +| File | Purpose | +|------|---------| +| `RecordingsApi.kt` | OkHttp client, upload with SHA-256 verification | + +## Sync Modes + +| Mode | Behavior | +|------|----------| +| Never | No sync, files stay on phone | +| LAN Only | Sync only when IP matches configured subnet (e.g., `10.0.0.`) | +| WiFi Only | Sync on any WiFi network | +| Any Network | Sync on WiFi or mobile data | + +## Server Structure (Python/FastAPI) + +| File | Purpose | +|------|---------| +| `main.py` | FastAPI endpoints | +| `crypto.py` | PyNaCl decryption, keypair management | +| `config.py` | Paths, ports | + +### Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/health` | GET | Returns `{"status": "ok"}` | +| `/api/v1/public-key` | GET | Returns Base64 public key | +| `/api/v1/recordings/upload` | POST | Upload encrypted file, returns hash for verification | +| `/api/v1/recordings` | GET | List decrypted recordings | + +## Data Flow + +1. **Recording starts**: `RecordingService` writes to `temp/recording_TIMESTAMP.ogg` +2. **Recording stops**: + - `CryptoHelper.encryptFile()` reads temp, writes `recordings/recording_TIMESTAMP.ogg.enc` + - Temp file deleted + - `SyncManager.scheduleImmediateSync()` triggered +3. **Sync runs**: + - `SyncWorker` checks network conditions match `SyncMode` + - Computes SHA-256 of encrypted file + - Uploads to server + - Server decrypts, returns hash + - If hashes match, marks file as synced +4. **Server storage**: `recordings/YYYY/MM/DD/recording_TIMESTAMP.ogg` + +## Key Dependencies + +### Android +- `lazysodium-android` - libsodium bindings +- `androidx.work` - Background job scheduling +- `okhttp3` - HTTP client + +### Server +- `fastapi` + `uvicorn` - Web framework +- `pynacl` - libsodium bindings +- `aiofiles` - Async file I/O + +## Configuration + +### Android (in-app settings) +- Sync mode (Never/LAN/WiFi/Any) +- LAN subnet prefix (default: `10.0.0.`) +- Server URLs (LAN and remote) +- Server public key (Base64) + +### Server (config.py or env vars) +- `HOST` - Bind address (default: 0.0.0.0) +- `PORT` - Listen port (default: 8000) +- `BASE_DIR` - Base directory for keys and recordings diff --git a/clio-android/flake.lock b/clio-android/flake.lock new file mode 100644 index 0000000..20db77f --- /dev/null +++ b/clio-android/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1776877367, + "narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0726a0ecb6d4e08f6adced58726b95db924cef57", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/clio-android/flake.nix b/clio-android/flake.nix new file mode 100644 index 0000000..75120f3 --- /dev/null +++ b/clio-android/flake.nix @@ -0,0 +1,52 @@ +{ + description = "Phone Recorder Android app development environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + config = { + allowUnfree = true; + android_sdk.accept_license = true; + }; + }; + + androidComposition = pkgs.androidenv.composeAndroidPackages { + buildToolsVersions = [ "34.0.0" ]; + platformVersions = [ "34" "33" ]; + abiVersions = [ "arm64-v8a" "x86_64" ]; + includeEmulator = false; + includeSystemImages = false; + includeSources = false; + includeNDK = false; + }; + + androidSdk = androidComposition.androidsdk; + in + { + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + android-studio + android-tools + jdk17 + androidSdk + ]; + + ANDROID_HOME = "${androidSdk}/libexec/android-sdk"; + ANDROID_SDK_ROOT = "${androidSdk}/libexec/android-sdk"; + + shellHook = '' + echo "Phone Recorder development environment" + echo "Run 'android-studio' to launch Android Studio" + echo "ANDROID_HOME=$ANDROID_HOME" + ''; + }; + } + ); +} diff --git a/clio-android/gradle.properties b/clio-android/gradle.properties new file mode 100644 index 0000000..f0a2e55 --- /dev/null +++ b/clio-android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +kotlin.code.style=official +android.nonTransitiveRClass=true diff --git a/clio-android/gradle/wrapper/gradle-wrapper.jar b/clio-android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/clio-android/gradlew.bat b/clio-android/gradlew.bat new file mode 100644 index 0000000..5eed7ee --- /dev/null +++ b/clio-android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/clio-android/local.properties b/clio-android/local.properties new file mode 100644 index 0000000..f3ee35f --- /dev/null +++ b/clio-android/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Sun Apr 26 22:24:29 GMT 2026 +sdk.dir=/work/projects/phone_recorder/claude/android_studio_install_path diff --git a/clio-android/notes.txt b/clio-android/notes.txt new file mode 100644 index 0000000..a0278ce --- /dev/null +++ b/clio-android/notes.txt @@ -0,0 +1,14 @@ +./gradlew assembleDebug + + 2. Connect your phone (USB debugging enabled) and verify connection: + ./android_studio_install_path/platform-tools/adb devices + + 3. Install the APK: + ./android_studio_install_path/platform-tools/adb install app/build/outputs/apk/debug/app-debug.apk + + Or you can combine build + install in one step: + ./gradlew installDebug + + This requires your phone to be connected with USB debugging enabled in Developer Options. + +❯ can you add it to the flake? diff --git a/clio-android/settings.gradle.kts b/clio-android/settings.gradle.kts new file mode 100644 index 0000000..fe32700 --- /dev/null +++ b/clio-android/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "PhoneRecorder" +include(":app") diff --git a/clio-android/todo.md b/clio-android/todo.md new file mode 100644 index 0000000..a4dd418 --- /dev/null +++ b/clio-android/todo.md @@ -0,0 +1,117 @@ +# Phone Recorder - Future Ideas / TODO + +## Questions to Address + +### Key Rotation +- What happens when server keypair changes? +- Current behavior: Old encrypted files can't be decrypted with new key +- Options: + 1. Keep old private key archived for decrypting old files + 2. Sync all files before rotating keys + 3. Store key version/ID in encrypted file header + +### Manual Decryption +- Document steps to decrypt files copied via USB from phone +- Need: Python script using server's private key +- Consider: CLI tool in server/ directory + +### No-Key Behavior +- Current: Deletes recordings if no public key configured (security-first) +- Alternatives to consider: + 1. Store unencrypted with warning (less secure) + 2. Queue recordings until key configured (could fill storage) + 3. Current behavior is probably correct for security model + +--- + +## Possible Features + +### High Priority (if needed) +- [ ] QR code for key import (avoid email/typing) +- [ ] Server URL configuration in app UI (currently requires code change for non-defaults) +- [ ] Manual sync trigger button +- [ ] View sync history/status in app +- [ ] **Display last successful sync time on phone** (e.g., "Last sync: 2 hours ago") +- [ ] **Server status page** - HTML dashboard showing: + - When phone last synced + - Total recordings count + - Recent uploads + - Disk space usage + - Could be at `/status` or `/dashboard` + +### Medium Priority +- [ ] Configurable recording quality (bitrate, sample rate) +- [ ] Configurable segment duration (currently 2 hours) +- [ ] Delete synced files automatically (with configurable retention) +- [ ] Server: Authentication for upload endpoint +- [ ] Server: HTTPS with Let's Encrypt + +### Audio Analysis & Intelligence +- [ ] **Speech detection bar** - Visual indicator showing when speech is detected vs silence + - Display as additional bar in recording detail view alongside waveform + - Data comes from batch processing script +- [ ] **Sleeping detection** - Detect periods of sleep/silence for overnight recordings + - Could help skip to interesting parts + - Visual indicator in timeline +- [ ] **Auto voice recognition / Speaker identification** + - Identify who is talking (requires training on voice samples) + - Label segments by speaker + - Not text-based (transcripts are unreliable) +- [ ] **Auto highlight generation** + - Audio-based detection (not text search since transcripts are bad) + - Example: detect phrase patterns like "Ok, note taken" acoustically + - Auto-create highlights with tags like "note_taken" + - Use audio fingerprinting or keyword spotting models + +### Viewer UX +- [ ] **Background audio playback** - Keep playing current audio while browsing other recordings + - Mini player that persists across views + - Allows reviewing recordings list while listening +- [ ] **Highlight duration** - Currently highlights mark only a point; consider adding end time/duration +- [ ] **Auto-scroll timeline to recordings** - If recordings are only at end of day, auto-scroll so user doesn't have to scroll down to see them + +### Low Priority / Nice to Have +- [ ] Multiple server destinations +- [ ] Compression before encryption +- [ ] Resume interrupted uploads +- [ ] Server: Web UI to browse/play recordings +- [ ] Server: Transcription integration + +### Infrastructure +- [ ] Systemd service file for server +- [ ] Docker container for server +- [ ] NixOS module for server +- [ ] Backup strategy for server private key + +--- + +## Known Limitations + +1. **No streaming encryption**: Entire temp file must complete before encryption +2. **No partial sync**: If upload fails, whole file retries +3. **Single server**: Can only sync to one destination at a time +4. **No authentication**: Anyone who can reach server can upload (relies on encryption for confidentiality) + +--- + +## Testing Notes + +### Unit Tests (server/) +```bash +python test_crypto.py +``` + +### Integration Tests (server/) +```bash +# Start server first +python main.py & +python test_integration.py +``` + +### Manual Testing Checklist +- [ ] Record short clip, verify .ogg.enc created +- [ ] Verify temp file deleted after encryption +- [ ] Check sync triggers after recording stops +- [ ] Verify hash verification (corrupt a file, see it retry) +- [ ] Test LAN detection with different subnets +- [ ] Test sync mode transitions diff --git a/clio-api/.gitignore b/clio-api/.gitignore new file mode 100644 index 0000000..505a3b1 --- /dev/null +++ b/clio-api/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/clio-api/.python-version b/clio-api/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/clio-api/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/clio-api/README.md b/clio-api/README.md new file mode 100644 index 0000000..e69de29 diff --git a/clio-api/deploy.sh b/clio-api/deploy.sh new file mode 100755 index 0000000..28a7308 --- /dev/null +++ b/clio-api/deploy.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Deployment script for phone-recorder server +# Target: thallock@10.0.0.45 (SSH port 2252) +# +# Paths on server: +# Code: /work/hosting/recordings +# Data (db): /work/hosting/recordings/data +# Audio: /media/external/Hosting/Recordings + +SERVER_HOST="10.0.0.45" +SERVER_USER="thallock" +SERVER_PORT="2252" +CODE_DIR="/work/hosting/recordings" +DATA_DIR="/work/hosting/recordings/data" +AUDIO_DIR="/media/external/Hosting/Recordings" + +SSH_OPTS="-p ${SERVER_PORT}" +SSH_CMD="ssh ${SSH_OPTS} ${SERVER_USER}@${SERVER_HOST}" +RSYNC_SSH="ssh -p ${SERVER_PORT}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "=== Deploying phone-recorder server to ${SERVER_USER}@${SERVER_HOST} ===" + +# Sync server code (excluding local data) +echo "Syncing server code..." +rsync -avz --delete \ + --exclude '.venv' \ + --exclude '__pycache__' \ + --exclude '*.pyc' \ + --exclude 'recordings/' \ + --exclude 'recordings.db' \ + --exclude 'keys/' \ + --exclude 'data/' \ + --exclude '.git' \ + --exclude 'nixos/' \ + -e "${RSYNC_SSH}" \ + "${SCRIPT_DIR}/" \ + "${SERVER_USER}@${SERVER_HOST}:${CODE_DIR}/" + +echo "Setting up on remote server..." +${SSH_CMD} bash << REMOTE_SCRIPT +set -euo pipefail + +# Create directories +mkdir -p ${DATA_DIR}/keys +mkdir -p ${AUDIO_DIR} + +cd ${CODE_DIR} + +# Sync dependencies +echo "Syncing Python dependencies..." +uv sync + +echo "" +echo "=== Server code deployed ===" +echo "Code: ${CODE_DIR}" +echo "Data: ${DATA_DIR}" +echo "Audio: ${AUDIO_DIR}" +echo "" +echo "To install/update systemd service:" +echo " sudo cp ${CODE_DIR}/nixos/phone-recorder.service /etc/systemd/system/" +echo " sudo systemctl daemon-reload" +echo " sudo systemctl enable --now phone-recorder" +echo "" +echo "To check status:" +echo " sudo systemctl status phone-recorder" +echo " curl http://localhost:8000/health" +REMOTE_SCRIPT + +echo "" +echo "=== Deployment complete ===" diff --git a/clio-api/nixos/Caddyfile b/clio-api/nixos/Caddyfile new file mode 100644 index 0000000..0046ae7 --- /dev/null +++ b/clio-api/nixos/Caddyfile @@ -0,0 +1,46 @@ +# Caddyfile snippet for phone-recorder +# Add this to your global /etc/caddy/Caddyfile +# +# Caddy will automatically obtain and renew HTTPS certificates + +recordings.hallocks.xyz { + # Proxy everything to FastAPI + reverse_proxy localhost:8000 + + # Security headers + header { + X-Content-Type-Options nosniff + X-Frame-Options SAMEORIGIN + Referrer-Policy strict-origin-when-cross-origin + -Server + } + + # Logging for fail2ban + log { + output file /var/log/caddy/recordings.log { + roll_size 10mb + roll_keep 5 + } + format json + } +} + +# ============================================================ +# Other services (for reference when setting them back up): +# ============================================================ + +# todo.hallocks.xyz { +# reverse_proxy localhost:3456 # Vikunja +# log { +# output file /var/log/caddy/todo.log +# format json +# } +# } + +# passwords.hallocks.xyz { +# reverse_proxy localhost:8080 # Vaultwarden +# log { +# output file /var/log/caddy/passwords.log +# format json +# } +# } diff --git a/clio-api/nixos/phone-recorder.nix b/clio-api/nixos/phone-recorder.nix new file mode 100644 index 0000000..6dc0f55 --- /dev/null +++ b/clio-api/nixos/phone-recorder.nix @@ -0,0 +1,182 @@ +# NixOS module for phone-recorder server +# Add to your configuration.nix imports or use as a standalone module +# +# Usage in configuration.nix: +# imports = [ ./phone-recorder.nix ]; +# services.phone-recorder.enable = true; + +{ config, lib, pkgs, ... }: + +let + cfg = config.services.phone-recorder; +in +{ + options.services.phone-recorder = { + enable = lib.mkEnableOption "Phone Recorder server"; + + port = lib.mkOption { + type = lib.types.port; + default = 8000; + description = "Port for the API server"; + }; + + dataDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/phone-recorder"; + description = "Directory for recordings and database"; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "phone-recorder"; + description = "User to run the service as"; + }; + + domain = lib.mkOption { + type = lib.types.str; + default = "recordings.hallocks.xyz"; + description = "Domain for HTTPS access"; + }; + + enableCaddy = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable Caddy reverse proxy with automatic HTTPS"; + }; + + enableFail2ban = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable fail2ban protection"; + }; + + sourceDir = lib.mkOption { + type = lib.types.path; + default = /home/thallock/phone-recorder; + description = "Path to the server source code"; + }; + }; + + config = lib.mkIf cfg.enable { + # Create user and group + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.user; + home = cfg.dataDir; + createHome = true; + }; + users.groups.${cfg.user} = {}; + + # Systemd service + systemd.services.phone-recorder = { + description = "Phone Recorder API Server"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment = { + HOST = "127.0.0.1"; # Only listen locally, Caddy handles external + PORT = toString cfg.port; + BASE_DIR = toString cfg.sourceDir; + RECORDINGS_DIR = "${cfg.dataDir}/recordings"; + DATABASE_PATH = "${cfg.dataDir}/recordings.db"; + }; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.user; + WorkingDirectory = cfg.sourceDir; + ExecStart = "${pkgs.uv}/bin/uv run python main.py"; + Restart = "always"; + RestartSec = 5; + + # Hardening + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = "read-only"; + PrivateTmp = true; + ReadWritePaths = [ cfg.dataDir ]; + }; + + preStart = '' + mkdir -p ${cfg.dataDir}/recordings + mkdir -p ${cfg.dataDir}/keys + ''; + }; + + # Caddy reverse proxy + services.caddy = lib.mkIf cfg.enableCaddy { + enable = true; + virtualHosts = { + "${cfg.domain}" = { + extraConfig = '' + # API endpoints + handle /api/* { + reverse_proxy localhost:${toString cfg.port} + } + handle /health { + reverse_proxy localhost:${toString cfg.port} + } + + # Static frontend (if you have one) + handle { + root * /var/www/recordings-viewer + file_server + try_files {path} /index.html + } + + # Security headers + header { + X-Content-Type-Options nosniff + X-Frame-Options DENY + Referrer-Policy strict-origin-when-cross-origin + } + + # Rate limiting for uploads + @upload path /api/v1/recordings/upload + handle @upload { + rate_limit {remote.ip} 10r/m + reverse_proxy localhost:${toString cfg.port} + } + + # Logging + log { + output file /var/log/caddy/recordings.log + format json + } + ''; + }; + }; + }; + + # Open firewall + networking.firewall.allowedTCPPorts = [ 80 443 ] + ++ lib.optional (!cfg.enableCaddy) cfg.port; + + # Fail2ban + services.fail2ban = lib.mkIf cfg.enableFail2ban { + enable = true; + maxretry = 5; + bantime = "1h"; + + jails = { + caddy-auth = lib.mkIf cfg.enableCaddy '' + enabled = true + filter = caddy-auth + logpath = /var/log/caddy/recordings.log + maxretry = 5 + bantime = 3600 + ''; + }; + }; + + # Fail2ban filter for Caddy + environment.etc."fail2ban/filter.d/caddy-auth.conf" = lib.mkIf (cfg.enableFail2ban && cfg.enableCaddy) { + text = '' + [Definition] + failregex = ^.*"remote_ip":"".*"status":40[13].*$ + ignoreregex = + ''; + }; + }; +} diff --git a/clio-api/nixos/phone-recorder.service b/clio-api/nixos/phone-recorder.service new file mode 100644 index 0000000..d150ed5 --- /dev/null +++ b/clio-api/nixos/phone-recorder.service @@ -0,0 +1,33 @@ +# Systemd service file for phone-recorder +# Copy to /etc/systemd/system/phone-recorder.service +# Then: systemctl daemon-reload && systemctl enable --now phone-recorder + +[Unit] +Description=Phone Recorder API Server +After=network.target local-fs.target +Wants=network-online.target + +[Service] +Type=simple +User=thallock +Group=thallock +WorkingDirectory=/work/hosting/recordings + +# Paths +Environment="HOST=0.0.0.0" +Environment="PORT=8000" +Environment="BASE_DIR=/work/hosting/recordings" +Environment="RECORDINGS_DIR=/media/external/Hosting/Recordings" +Environment="DATABASE_PATH=/work/hosting/recordings/data/recordings.db" +Environment="KEYS_DIR=/work/hosting/recordings/data/keys" + +ExecStart=/home/thallock/.local/bin/uv run python main.py +Restart=always +RestartSec=5 + +# Hardening +NoNewPrivileges=true +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/clio-api/requirements.txt b/clio-api/requirements.txt new file mode 100644 index 0000000..4e09cd8 --- /dev/null +++ b/clio-api/requirements.txt @@ -0,0 +1,8 @@ +fastapi>=0.109.0 +uvicorn[standard]>=0.27.0 +pynacl>=1.5.0 +aiofiles>=23.2.1 +python-multipart>=0.0.6 + +# For testing +requests>=2.31.0 diff --git a/clio-infra/ARCHITECTURE.md b/clio-infra/ARCHITECTURE.md new file mode 100644 index 0000000..026c7c4 --- /dev/null +++ b/clio-infra/ARCHITECTURE.md @@ -0,0 +1,126 @@ +# Clio - Architecture Overview + +## Purpose + +Multi-device audio recording system with end-to-end encryption. Records continuously on phone and portable devices, encrypts immediately, syncs to central server, transcribes with GPU, and provides a web interface for search and highlights. + +## Components + +| Repository | Purpose | Deployment Target | +|------------|---------|-------------------| +| clio-api | FastAPI server, recordings DB, highlights | Server (10.0.0.45) | +| clio-android | Android recording app | Phone (manual APK) | +| clio-ui | React viewer frontend | Server (10.0.0.45) | +| clio-ai | Whisper transcription, wake word detection | Desktop (10.0.0.130) | +| clio-infra | Deployment configs, docs, NixOS modules | All | + +## Security Model + +``` +Phone (has: server public key) Server (has: private key) +───────────────────────────── ──────────────────────── +Record audio → temp.ogg +Encrypt with public key → + recording_*.ogg.enc + │ + └──── HTTP POST ────────→ Receive encrypted file + Decrypt with private key + Store as .ogg in recordings/ +``` + +- **Sealed box encryption**: X25519 key exchange + XSalsa20-Poly1305 +- **Forward secrecy**: Each encryption uses ephemeral keys +- **Phone never stores unencrypted**: Temp file deleted immediately after encryption +- **If no public key configured**: Recordings are deleted (security-first design) + +## File Format (SREC) + +Encrypted files use a chunked format for streaming large files: + +``` +[4 bytes: "SREC" magic] +[1 byte: version (1)] +[4 bytes: chunk count, big-endian] +For each chunk (64KB max): + [4 bytes: encrypted length, big-endian] + [N bytes: sealed box ciphertext] +``` + +## Data Flow + +``` +1. Recording + Phone/Pi → temp.ogg → encrypt → recording_*.ogg.enc + +2. Sync + Phone/Pi → HTTP POST → Server decrypts → recordings/YYYY/MM/DD/*.ogg + +3. Transcription (async, on desktop) + Desktop pulls recordings → Whisper → JSON segments → POST to server + +4. Viewing + Browser → clio-ui → clio-api → recordings + transcripts + highlights +``` + +## API Endpoints (clio-api) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/health` | GET | Health check | +| `/api/v1/public-key` | GET | Server public key (Base64) | +| `/api/v1/recordings/upload` | POST | Upload encrypted file | +| `/api/v1/recordings` | GET | List recordings with transcripts | +| `/api/v1/recordings/{id}` | GET | Recording details | +| `/api/v1/recordings/{id}/audio` | GET | Stream audio file | +| `/api/v1/import/transcription` | POST | Import transcription JSON | +| `/api/v1/highlights` | GET/POST | Manage highlights | +| `/api/v1/sources` | GET | List recording sources | + +## Database Schema + +**recordings**: id, source_name, audio_path, duration, language, recorded_at, transcribed_at +**segments**: id, recording_id, start_time, end_time, text, confidence +**highlights**: id, recording_id, start_seconds, end_seconds, tag, note, created_at + +## Sync Modes (Android) + +| Mode | Behavior | +|------|----------| +| Never | No sync, files stay on phone | +| LAN Only | Sync only when IP matches subnet (e.g., `10.0.0.`) | +| WiFi Only | Sync on any WiFi network | +| Any Network | Sync on WiFi or mobile data | + +## Hosts & Paths + +### Server (10.0.0.45) +- Code: `/work/hosting/recordings` +- Audio: `/media/external/Hosting/Recordings` +- Database: `/work/hosting/recordings/data/recordings.db` +- Frontend: `/var/www/recordings-viewer` +- Domain: `recordings.hallocks.xyz` + +### Desktop (10.0.0.130) +- Transcription scripts +- Real-time wake word detection +- GPU: RTX 3090 + +## Dependencies + +### clio-api (Python) +- fastapi, uvicorn +- pynacl (libsodium) +- aiofiles +- sqlite3 + +### clio-android (Kotlin) +- lazysodium-android +- androidx.work +- okhttp3 + +### clio-ai (Python) +- faster-whisper +- torch, torchaudio +- sounddevice +- webrtcvad +- scikit-learn diff --git a/clio-infra/TODO.md b/clio-infra/TODO.md new file mode 100644 index 0000000..9f81269 --- /dev/null +++ b/clio-infra/TODO.md @@ -0,0 +1,153 @@ +# Clio - Consolidated TODO + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ TARGET ARCHITECTURE │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────┐ ┌──────────────────────────────────────────────┐ │ +│ │ Phone │───────▶│ Server (10.0.0.45) │ │ +│ │ (Android)│ push │ - Storage hub (recordings) │ │ +│ └──────────┘ │ - clio-api (FastAPI) │ │ +│ │ - clio-ui (React frontend) │ │ +│ ┌──────────┐ │ - Database (highlights, transcripts) │ │ +│ │ Pi │───────▶│ - Forgejo (code.hallocks.xyz) │ │ +│ │(portable)│ push └──────────────────────────────────────────────┘ │ +│ └──────────┘ │ │ +│ │ pulls recordings │ +│ ▼ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ Desktop (10.0.0.130) │ │ +│ │ - GPU: RTX 3090 │ │ +│ │ - clio-ai/transcription (Whisper) │ │ +│ │ - clio-ai/real_time (wake word detection) │ │ +│ │ - Off at night (non-critical) │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Immediate Tasks + +### Infrastructure Setup +- [x] Import Pi recorder code (systemd setup) → clio-pi/ +- [ ] Migrate ~100s GB of recordings from Pi to server storage +- [ ] Pi: Add sync script to push recordings to server +- [ ] Pi: Auto-detect audio device (currently hardcoded to hw:1) +- [ ] Set up Forgejo repos for all clio-* projects +- [ ] Configure Forgejo Actions for server deployments (10.0.0.45) +- [ ] Create backup strategy for viewer, highlights, and database + +### Deployment Pipeline +- [ ] clio-api: Forgejo Actions → deploy to 10.0.0.45 +- [ ] clio-ui: Forgejo Actions → deploy to 10.0.0.45:/var/www/recordings-viewer +- [ ] clio-ai: Manual deploy to desktop (10.0.0.130) - not via Actions + +--- + +## Feature Backlog + +### High Priority +- [ ] QR code for key import (avoid email/typing) +- [ ] Server URL configuration in app UI +- [ ] Manual sync trigger button +- [ ] View sync history/status in app +- [ ] Display last successful sync time on phone +- [ ] Server status page/dashboard: + - When phone/pi last synced + - Total recordings count + - Recent uploads + - Disk space usage + +### Wake Word / Voice Recognition (clio-ai) +- [ ] Manage wake words in UI (currently hardcoded) +- [ ] Centralize voice recognition training data +- [ ] Real-time detection improvements (currently sucks even on desktop) +- [ ] Future: Real-time detection on phone/pi (low priority) + +### Audio Analysis & Intelligence +- [ ] Speech detection bar in recording detail view +- [ ] Sleeping/silence detection for overnight recordings +- [ ] Auto voice recognition / speaker identification +- [ ] Auto highlight generation (audio-based, not text search) + +### Viewer UX +- [ ] Background audio playback (mini player across views) +- [ ] Highlight duration (currently point-only, add end time) +- [ ] Auto-scroll timeline to recordings + +### Medium Priority +- [ ] Configurable recording quality (bitrate, sample rate) +- [ ] Configurable segment duration (currently 2 hours) +- [ ] Delete synced files automatically (with retention) +- [ ] Server authentication for upload endpoint +- [ ] Server HTTPS with Let's Encrypt (partially done via Caddy) + +### Low Priority / Nice to Have +- [ ] Multiple server destinations +- [ ] Compression before encryption +- [ ] Resume interrupted uploads +- [ ] Docker container for server + +--- + +## Questions to Resolve + +### Key Rotation +- What happens when server keypair changes? +- Options: + 1. Keep old private key archived + 2. Sync all files before rotating + 3. Store key version in encrypted file header + +### Manual Decryption +- Document steps to decrypt files copied via USB +- CLI tool needed + +### Desktop Worker Architecture +- How does desktop pull recordings from server? +- How does it push transcriptions back? +- Queue system needed? + +--- + +## Known Limitations + +1. **No streaming encryption**: Entire temp file must complete before encryption +2. **No partial sync**: If upload fails, whole file retries +3. **Single server**: Can only sync to one destination at a time +4. **No authentication**: Relies on encryption for confidentiality +5. **Desktop off at night**: Real-time detection unavailable + +--- + +## Repository Structure + +``` +/work/projects/clio/ +├── clio-api/ # FastAPI server (recordings, highlights, transcripts) +├── clio-android/ # Android recording app +├── clio-ui/ # React frontend (viewer) +├── clio-ai/ # ML/AI components +│ ├── real_time/ # Wake word detection, voice classifier +│ └── transcription/ # Whisper transcription scripts +├── clio-pi/ # Raspberry Pi recorder +│ ├── bin/ # Recording scripts +│ ├── systemd/ # Service units +│ └── setup.sh # Pi setup script +└── clio-infra/ # Deployment, configs, docs + ├── nixos/ # NixOS modules + ├── scripts/ # Deploy scripts + └── TODO.md # This file +``` + +## Hosts + +| Host | IP | Role | +|------|-----|------| +| Server | 10.0.0.45 | Storage, API, UI, Forgejo | +| Desktop | 10.0.0.130 | GPU transcription, real-time detection | +| Phone | (mobile) | Recording source | +| Pi | (TBD) | Portable recording source | diff --git a/clio-infra/nixos/Caddyfile b/clio-infra/nixos/Caddyfile new file mode 100644 index 0000000..0046ae7 --- /dev/null +++ b/clio-infra/nixos/Caddyfile @@ -0,0 +1,46 @@ +# Caddyfile snippet for phone-recorder +# Add this to your global /etc/caddy/Caddyfile +# +# Caddy will automatically obtain and renew HTTPS certificates + +recordings.hallocks.xyz { + # Proxy everything to FastAPI + reverse_proxy localhost:8000 + + # Security headers + header { + X-Content-Type-Options nosniff + X-Frame-Options SAMEORIGIN + Referrer-Policy strict-origin-when-cross-origin + -Server + } + + # Logging for fail2ban + log { + output file /var/log/caddy/recordings.log { + roll_size 10mb + roll_keep 5 + } + format json + } +} + +# ============================================================ +# Other services (for reference when setting them back up): +# ============================================================ + +# todo.hallocks.xyz { +# reverse_proxy localhost:3456 # Vikunja +# log { +# output file /var/log/caddy/todo.log +# format json +# } +# } + +# passwords.hallocks.xyz { +# reverse_proxy localhost:8080 # Vaultwarden +# log { +# output file /var/log/caddy/passwords.log +# format json +# } +# } diff --git a/clio-infra/nixos/phone-recorder.nix b/clio-infra/nixos/phone-recorder.nix new file mode 100644 index 0000000..6dc0f55 --- /dev/null +++ b/clio-infra/nixos/phone-recorder.nix @@ -0,0 +1,182 @@ +# NixOS module for phone-recorder server +# Add to your configuration.nix imports or use as a standalone module +# +# Usage in configuration.nix: +# imports = [ ./phone-recorder.nix ]; +# services.phone-recorder.enable = true; + +{ config, lib, pkgs, ... }: + +let + cfg = config.services.phone-recorder; +in +{ + options.services.phone-recorder = { + enable = lib.mkEnableOption "Phone Recorder server"; + + port = lib.mkOption { + type = lib.types.port; + default = 8000; + description = "Port for the API server"; + }; + + dataDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/phone-recorder"; + description = "Directory for recordings and database"; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "phone-recorder"; + description = "User to run the service as"; + }; + + domain = lib.mkOption { + type = lib.types.str; + default = "recordings.hallocks.xyz"; + description = "Domain for HTTPS access"; + }; + + enableCaddy = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable Caddy reverse proxy with automatic HTTPS"; + }; + + enableFail2ban = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable fail2ban protection"; + }; + + sourceDir = lib.mkOption { + type = lib.types.path; + default = /home/thallock/phone-recorder; + description = "Path to the server source code"; + }; + }; + + config = lib.mkIf cfg.enable { + # Create user and group + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.user; + home = cfg.dataDir; + createHome = true; + }; + users.groups.${cfg.user} = {}; + + # Systemd service + systemd.services.phone-recorder = { + description = "Phone Recorder API Server"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment = { + HOST = "127.0.0.1"; # Only listen locally, Caddy handles external + PORT = toString cfg.port; + BASE_DIR = toString cfg.sourceDir; + RECORDINGS_DIR = "${cfg.dataDir}/recordings"; + DATABASE_PATH = "${cfg.dataDir}/recordings.db"; + }; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.user; + WorkingDirectory = cfg.sourceDir; + ExecStart = "${pkgs.uv}/bin/uv run python main.py"; + Restart = "always"; + RestartSec = 5; + + # Hardening + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = "read-only"; + PrivateTmp = true; + ReadWritePaths = [ cfg.dataDir ]; + }; + + preStart = '' + mkdir -p ${cfg.dataDir}/recordings + mkdir -p ${cfg.dataDir}/keys + ''; + }; + + # Caddy reverse proxy + services.caddy = lib.mkIf cfg.enableCaddy { + enable = true; + virtualHosts = { + "${cfg.domain}" = { + extraConfig = '' + # API endpoints + handle /api/* { + reverse_proxy localhost:${toString cfg.port} + } + handle /health { + reverse_proxy localhost:${toString cfg.port} + } + + # Static frontend (if you have one) + handle { + root * /var/www/recordings-viewer + file_server + try_files {path} /index.html + } + + # Security headers + header { + X-Content-Type-Options nosniff + X-Frame-Options DENY + Referrer-Policy strict-origin-when-cross-origin + } + + # Rate limiting for uploads + @upload path /api/v1/recordings/upload + handle @upload { + rate_limit {remote.ip} 10r/m + reverse_proxy localhost:${toString cfg.port} + } + + # Logging + log { + output file /var/log/caddy/recordings.log + format json + } + ''; + }; + }; + }; + + # Open firewall + networking.firewall.allowedTCPPorts = [ 80 443 ] + ++ lib.optional (!cfg.enableCaddy) cfg.port; + + # Fail2ban + services.fail2ban = lib.mkIf cfg.enableFail2ban { + enable = true; + maxretry = 5; + bantime = "1h"; + + jails = { + caddy-auth = lib.mkIf cfg.enableCaddy '' + enabled = true + filter = caddy-auth + logpath = /var/log/caddy/recordings.log + maxretry = 5 + bantime = 3600 + ''; + }; + }; + + # Fail2ban filter for Caddy + environment.etc."fail2ban/filter.d/caddy-auth.conf" = lib.mkIf (cfg.enableFail2ban && cfg.enableCaddy) { + text = '' + [Definition] + failregex = ^.*"remote_ip":"".*"status":40[13].*$ + ignoreregex = + ''; + }; + }; +} diff --git a/clio-infra/nixos/phone-recorder.service b/clio-infra/nixos/phone-recorder.service new file mode 100644 index 0000000..d150ed5 --- /dev/null +++ b/clio-infra/nixos/phone-recorder.service @@ -0,0 +1,33 @@ +# Systemd service file for phone-recorder +# Copy to /etc/systemd/system/phone-recorder.service +# Then: systemctl daemon-reload && systemctl enable --now phone-recorder + +[Unit] +Description=Phone Recorder API Server +After=network.target local-fs.target +Wants=network-online.target + +[Service] +Type=simple +User=thallock +Group=thallock +WorkingDirectory=/work/hosting/recordings + +# Paths +Environment="HOST=0.0.0.0" +Environment="PORT=8000" +Environment="BASE_DIR=/work/hosting/recordings" +Environment="RECORDINGS_DIR=/media/external/Hosting/Recordings" +Environment="DATABASE_PATH=/work/hosting/recordings/data/recordings.db" +Environment="KEYS_DIR=/work/hosting/recordings/data/keys" + +ExecStart=/home/thallock/.local/bin/uv run python main.py +Restart=always +RestartSec=5 + +# Hardening +NoNewPrivileges=true +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/clio-infra/scripts/deploy-api-server.sh b/clio-infra/scripts/deploy-api-server.sh new file mode 100755 index 0000000..28a7308 --- /dev/null +++ b/clio-infra/scripts/deploy-api-server.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Deployment script for phone-recorder server +# Target: thallock@10.0.0.45 (SSH port 2252) +# +# Paths on server: +# Code: /work/hosting/recordings +# Data (db): /work/hosting/recordings/data +# Audio: /media/external/Hosting/Recordings + +SERVER_HOST="10.0.0.45" +SERVER_USER="thallock" +SERVER_PORT="2252" +CODE_DIR="/work/hosting/recordings" +DATA_DIR="/work/hosting/recordings/data" +AUDIO_DIR="/media/external/Hosting/Recordings" + +SSH_OPTS="-p ${SERVER_PORT}" +SSH_CMD="ssh ${SSH_OPTS} ${SERVER_USER}@${SERVER_HOST}" +RSYNC_SSH="ssh -p ${SERVER_PORT}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "=== Deploying phone-recorder server to ${SERVER_USER}@${SERVER_HOST} ===" + +# Sync server code (excluding local data) +echo "Syncing server code..." +rsync -avz --delete \ + --exclude '.venv' \ + --exclude '__pycache__' \ + --exclude '*.pyc' \ + --exclude 'recordings/' \ + --exclude 'recordings.db' \ + --exclude 'keys/' \ + --exclude 'data/' \ + --exclude '.git' \ + --exclude 'nixos/' \ + -e "${RSYNC_SSH}" \ + "${SCRIPT_DIR}/" \ + "${SERVER_USER}@${SERVER_HOST}:${CODE_DIR}/" + +echo "Setting up on remote server..." +${SSH_CMD} bash << REMOTE_SCRIPT +set -euo pipefail + +# Create directories +mkdir -p ${DATA_DIR}/keys +mkdir -p ${AUDIO_DIR} + +cd ${CODE_DIR} + +# Sync dependencies +echo "Syncing Python dependencies..." +uv sync + +echo "" +echo "=== Server code deployed ===" +echo "Code: ${CODE_DIR}" +echo "Data: ${DATA_DIR}" +echo "Audio: ${AUDIO_DIR}" +echo "" +echo "To install/update systemd service:" +echo " sudo cp ${CODE_DIR}/nixos/phone-recorder.service /etc/systemd/system/" +echo " sudo systemctl daemon-reload" +echo " sudo systemctl enable --now phone-recorder" +echo "" +echo "To check status:" +echo " sudo systemctl status phone-recorder" +echo " curl http://localhost:8000/health" +REMOTE_SCRIPT + +echo "" +echo "=== Deployment complete ===" diff --git a/clio-infra/scripts/deploy-api.sh b/clio-infra/scripts/deploy-api.sh new file mode 100755 index 0000000..917117c --- /dev/null +++ b/clio-infra/scripts/deploy-api.sh @@ -0,0 +1,15 @@ +#!/run/current-system/sw/bin/fish + +rsync -avz --delete \ + --exclude '.venv' --exclude '__pycache__' --exclude '*.pyc' \ + --exclude 'recordings/' --exclude 'recordings.db' --exclude 'keys/' \ + --exclude 'data/' --exclude '.git' \ + -e 'ssh -p 2252' \ + /work/projects/phone_recorder/claude/server/ \ + thallock@10.0.0.45:/work/hosting/recordings/ + + # Deploy React frontend +rsync -avz --delete \ + -e 'ssh -p 2252' \ + /work/projects/view_recordings/designer_export/ \ + thallock@10.0.0.45:/work/hosting/recordings-viewer/ diff --git a/clio-infra/scripts/deploy.sh b/clio-infra/scripts/deploy.sh new file mode 100755 index 0000000..b423445 --- /dev/null +++ b/clio-infra/scripts/deploy.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Clio deployment script +# Deploys clio-api and clio-ui to server (10.0.0.45) +# +# Usage: ./deploy.sh [api|ui|all] + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CLIO_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" + +SERVER_HOST="10.0.0.45" +SERVER_USER="thallock" +SERVER_PORT="2252" + +# Server paths +API_CODE_DIR="/work/hosting/recordings" +UI_DIR="/work/hosting/recordings-viewer" + +SSH_OPTS="-p ${SERVER_PORT}" +SSH_CMD="ssh ${SSH_OPTS} ${SERVER_USER}@${SERVER_HOST}" +RSYNC_SSH="ssh -p ${SERVER_PORT}" + +deploy_api() { + echo "=== Deploying clio-api ===" + rsync -avz --delete \ + --exclude '.venv' \ + --exclude '__pycache__' \ + --exclude '*.pyc' \ + --exclude 'recordings/' \ + --exclude 'recordings.db' \ + --exclude 'keys/' \ + --exclude 'data/' \ + --exclude '.git' \ + --exclude 'nixos/' \ + -e "${RSYNC_SSH}" \ + "${CLIO_ROOT}/clio-api/" \ + "${SERVER_USER}@${SERVER_HOST}:${API_CODE_DIR}/" + + echo "Running uv sync on server..." + ${SSH_CMD} "cd ${API_CODE_DIR} && uv sync" + echo "clio-api deployed." +} + +deploy_ui() { + echo "=== Deploying clio-ui ===" + rsync -avz --delete \ + -e "${RSYNC_SSH}" \ + "${CLIO_ROOT}/clio-ui/" \ + "${SERVER_USER}@${SERVER_HOST}:${UI_DIR}/" + echo "clio-ui deployed." +} + +case "${1:-all}" in + api) + deploy_api + ;; + ui) + deploy_ui + ;; + all) + deploy_api + deploy_ui + ;; + *) + echo "Usage: $0 [api|ui|all]" + exit 1 + ;; +esac + +echo "" +echo "=== Deployment complete ===" +echo "" +echo "To restart the API service:" +echo " ssh -p ${SERVER_PORT} ${SERVER_USER}@${SERVER_HOST} 'sudo systemctl restart phone-recorder'" +echo "" +echo "To check status:" +echo " curl https://recordings.hallocks.xyz/health" diff --git a/clio-pi/README.md b/clio-pi/README.md new file mode 100644 index 0000000..fbaf5bb --- /dev/null +++ b/clio-pi/README.md @@ -0,0 +1,102 @@ +# Clio Pi + +Raspberry Pi recording station for the Clio audio system. + +## Features + +- Continuous audio recording with ffmpeg (opus codec, 2hr segments) +- Systemd service for auto-start on boot +- HTML status page showing recording coverage +- NTP time sync via chrony +- SSH key-only access (hardened) + +## Quick Setup + +1. Flash Raspberry Pi OS Lite to SD card using Raspberry Pi Imager +2. Configure in Imager: hostname, user/pass, WiFi, enable SSH +3. Boot and SSH into Pi +4. Copy this directory to the Pi: + ```bash + rsync -avz /work/projects/clio/clio-pi/ pi@:~/clio-pi/ + ``` +5. Run setup: + ```bash + ssh pi@ + cd ~/clio-pi + sudo ./setup.sh + ``` +6. Add your SSH key before rebooting: + ```bash + ssh-copy-id pi@ + ``` +7. Start recording: + ```bash + sudo systemctl start record.service + ``` + +## Files + +``` +clio-pi/ +├── setup.sh # Main setup script (run with sudo) +├── bin/ +│ ├── record.sh # Recording loop (ffmpeg) +│ └── gen_recording_status.py # Status page generator +├── systemd/ +│ └── record.service # Systemd service unit +└── README.md +``` + +## Configuration + +Edit `~/bin/record.sh` environment variables: + +| Variable | Default | Description | +|----------|---------|-------------| +| `AUDIO_DEVICE` | `hw:1` | ALSA device (run `arecord -l` to list) | +| `SEGMENT_SECONDS` | `7200` | Segment length (2 hours) | +| `RECORDINGS_DIR` | `~/recordings` | Output directory | +| `SOURCE_NAME` | `stationary` | Filename prefix | + +## Services + +| Service | Description | +|---------|-------------| +| `record.service` | Recording daemon | +| `chrony` | NTP time sync | +| `nginx` | Status page web server | + +## Commands + +```bash +# Recording service +sudo systemctl start record.service +sudo systemctl stop record.service +sudo systemctl status record.service +journalctl -u record.service -f + +# Check time sync +chronyc tracking +timedatectl + +# View status page +curl http://localhost/ +``` + +## Syncing Recordings to Server + +TODO: Add rsync/sync script to push recordings to server (10.0.0.45) + +## Audio Device Setup + +List available devices: +```bash +arecord -l +``` + +Example output: +``` +card 1: Device [USB Audio Device], device 0: USB Audio [USB Audio] +``` + +This would be `hw:1,0` or just `hw:1`. diff --git a/clio-pi/bin/gen_recording_status.py b/clio-pi/bin/gen_recording_status.py new file mode 100755 index 0000000..4179a32 --- /dev/null +++ b/clio-pi/bin/gen_recording_status.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python3 +import argparse +import datetime as dt +import html +import os +import re +import shutil +import statistics +from pathlib import Path +from typing import List, Tuple, Optional + +FNAME_RE = re.compile(r"^stationary-(\d{4}-\d{2}-\d{2})_(\d{2}-\d{2}-\d{2})\.opus$") + +def parse_start(path: Path, tz) -> Optional[dt.datetime]: + m = FNAME_RE.match(path.name) + if not m: + return None + date_str, time_str = m.groups() + start = dt.datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H-%M-%S") + return start.replace(tzinfo=tz) + +def get_duration_seconds(path: Path) -> Optional[float]: + try: + from mutagen.oggopus import OggOpus # optional + audio = OggOpus(path) + return float(getattr(audio.info, "length", 0.0)) or None + except Exception: + return None + +def human_delta(delta: dt.timedelta) -> str: + secs = int(abs(delta).total_seconds()) + d, rem = divmod(secs, 86400) + h, rem = divmod(rem, 3600) + m, s = divmod(rem, 60) + parts = [] + if d: parts.append(f"{d}d") + if h: parts.append(f"{h}h") + if m: parts.append(f"{m}m") + if not parts: parts.append(f"{s}s") + return " ".join(parts) + +def merge_intervals(intervals: List[Tuple[dt.datetime, dt.datetime]]) -> List[Tuple[dt.datetime, dt.datetime]]: + if not intervals: return [] + intervals.sort(key=lambda x: x[0]) + merged = [intervals[0]] + for s, e in intervals[1:]: + ls, le = merged[-1] + if s <= le: + merged[-1] = (ls, max(le, e)) + else: + merged.append((s, e)) + return merged + +def day_bounds(d: dt.date, tz): + start = dt.datetime(d.year, d.month, d.day, tzinfo=tz) + return start, start + dt.timedelta(days=1) + +def intersect(a: Tuple[dt.datetime, dt.datetime], b: Tuple[dt.datetime, dt.datetime]) -> Optional[Tuple[dt.datetime, dt.datetime]]: + s1, e1 = a; s2, e2 = b + s = max(s1, s2); e = min(e1, e2) + return (s, e) if s < e else None + +def pct(x, total): + return 0 if total <= 0 else max(0, min(100, (x/total)*100)) + +def fmt_ampm(t: dt.datetime) -> str: + s = t.strftime("%I:%M%p") # 01:05PM + return s.lstrip("0") + +def build(args): + tz = dt.datetime.now().astimezone().tzinfo + rec_dir = Path(args.dir).expanduser().resolve() + files = [p for p in rec_dir.glob("*.opus") if p.is_file()] + files.sort() + + # Gather metadata + items = [] # (path, start_dt, size_bytes, duration_opt) + for p in files: + s = parse_start(p, tz) + if not s: + continue + dur = get_duration_seconds(p) if args.read_durations else None + try: + size = p.stat().st_size + except Exception: + size = 0 + items.append((p, s, size, dur)) + items.sort(key=lambda x: x[1]) + + starts = [s for _, s, _, _ in items] + deltas = [(b - a).total_seconds() for a, b in zip(starts, starts[1:])] + + # Duration policy (defaults: NO inference, assume 2h) + if args.infer: + sane = [d for d in deltas if args.infer_min_sec <= d <= args.infer_max_sec] + inferred_default = (statistics.median(sane) if sane else args.assume_duration_sec or args.infer_default_sec) + inferred_default = max(args.infer_min_sec, min(args.infer_max_sec, inferred_default)) + else: + inferred_default = args.assume_duration_sec or args.infer_default_sec + + # Build intervals + intervals: List[Tuple[dt.datetime, dt.datetime]] = [] + for idx, (p, s, _size, dur) in enumerate(items): + if dur is None or dur <= 0: + dur_use = float(inferred_default) + else: + dur_use = dur + if idx < len(items)-1 and args.guard_gap_sec > 0: + to_next = (items[idx+1][1] - s).total_seconds() + dur_use = min(dur_use, max(1.0, to_next - args.guard_gap_sec)) + e = s + dt.timedelta(seconds=max(1.0, dur_use)) + intervals.append((s, e)) + + gen_time = dt.datetime.now(tz) + last_start = starts[-1] if starts else None + + # FS usage for recordings partition + try: + total_b, used_b, free_b = shutil.disk_usage(str(rec_dir)) + except Exception: + total_b = used_b = free_b = 0 + + # Window + today = gen_time.date() + start_date = today - dt.timedelta(days=args.days - 1) + + big_gap = dt.timedelta(minutes=args.gap_yellow_min) + green_gap = dt.timedelta(minutes=args.gap_green_min) + + rows = [] + window_total_bytes = 0 + for i in range(args.days): + d = start_date + dt.timedelta(days=i) + d_start, d_end = day_bounds(d, tz) + + # Per-day file count and bytes (count files that START this day) + day_files = [(p, s, size) for (p, s, size, _du) in [(p, s, size, du) for (p, s, size, du) in items] if d_start <= s < d_end] + day_bytes = sum(size for (_p, _s, size) in day_files) + window_total_bytes += day_bytes + + # Intervals overlapping this day + day_intervals = [] + for (s, e) in intervals: + inter = intersect((s, e), (d_start, d_end)) + if inter: + day_intervals.append(inter) + + merged = merge_intervals(day_intervals) + covered = sum((e - s).total_seconds() for s, e in merged) + day_seconds = 86400.0 + coverage_pct = pct(covered, day_seconds) + + # Max gap includes edges + if not merged: + max_gap = dt.timedelta(days=1) + else: + gaps = [merged[0][0] - d_start] + gaps += [b[0] - a[1] for a, b in zip(merged, merged[1:])] + gaps.append(d_end - merged[-1][1]) + max_gap = max(gaps) if gaps else dt.timedelta(0) + + # Status + count = len(day_files) + if count == 0: + status = "red" + elif max_gap <= green_gap and coverage_pct >= args.green_cover_pct: + status = "green" + elif max_gap >= big_gap or coverage_pct < args.yellow_cover_pct: + status = "yellow" + else: + status = "yellow" + + # Mini timeline segments + segs = [] + for s, e in merged: + left = pct((s - d_start).total_seconds(), day_seconds) + width = pct((e - s).total_seconds(), day_seconds) + if width > 0: + segs.append((left, width)) + + # Details list: show ALL spans; AM/PM times + details_txt = ", ".join(f"{fmt_ampm(s)}–{fmt_ampm(e)}" for s, e in merged) + + rows.append({ + "date": d.isoformat(), + "count": count, + "bytes": day_bytes, + "max_gap": max_gap, + "coverage_pct": coverage_pct, + "status": status, + "segments": segs, + "details": details_txt or "—", + }) + + # HTML + def hhmm(td: dt.timedelta) -> str: + total = int(td.total_seconds()) + h = total // 3600 + m = (total % 3600) // 60 + return f"{h:02d}:{m:02d}" + + def fmt_bytes(n: int) -> str: + units = ("B", "KiB", "MiB", "GiB", "TiB", "PiB") + x = float(n) + for u in units: + if x < 1024.0 or u == units[-1]: + return f"{int(x)} {u}" if u == "B" else f"{x:.1f} {u}" + x /= 1024.0 + + + title = args.title + last_str = "—" + if last_start: + last_str = f"{last_start.strftime('%Y-%m-%d %I:%M:%S %p %Z')} ({human_delta(gen_time - last_start)} ago)" + + # FS usage strings + total_str = fmt_bytes(total_b) + used_str = fmt_bytes(used_b) + free_str = fmt_bytes(free_b) + window_bytes_str = fmt_bytes(window_total_bytes) + + html_out = f""" + + + + + +{html.escape(title)} + + + +

{html.escape(title)}

+
Generated at {gen_time.strftime('%Y-%m-%d %I:%M:%S %p %Z')}
+
+ Last recording started: {html.escape(last_str)} + Window: {args.days} days + Dir: {html.escape(str(rec_dir))} +
+
+ Data in window: {window_bytes_str} + Filesystem usage (recordings partition): used {used_str} • free {free_str} • total {total_str} +
+ + + + + + + + + + + + + + +""" + for r in reversed(rows): + cls = f"row-{r['status']}" + cov = int(round(r["coverage_pct"])) + seg_divs = "".join( + f'
' + for left, width in r["segments"] + ) + data_str = fmt_bytes(r["bytes"]) + html_out += f""" + + + + + + + + +""" + html_out += """ +
Date# RecsDataMax gapCoverageSpansStatus
{r['date']}{r['count']}{data_str}{hhmm(r['max_gap'])} +
{seg_divs}
+
{cov}%
+
{html.escape(r['details'])}
{r['status']}
+

Legend: green = nearly continuous • yellow = gaps or low coverage • red = no recordings

+ + +""" + + out_path = Path(args.out).resolve() + out_path.parent.mkdir(parents=True, exist_ok=True) + tmp = out_path.with_suffix(out_path.suffix + ".tmp") + with open(tmp, "w", encoding="utf-8") as f: + f.write(html_out) + os.replace(tmp, out_path) + +def main(): + p = argparse.ArgumentParser(description="Generate recordings status HTML") + p.add_argument("--dir", default=str(Path.home() / "recordings"), + help="Directory containing .opus files (default: ~/recordings)") + p.add_argument("--out", default="/var/www/html/index.html", + help="HTML output path (default: /var/www/html/index.html)") + # Defaults tuned so cron needs no flags: + p.add_argument("--days", type=int, default=365, help="How many days to show (default: 365)") + p.add_argument("--gap-yellow-min", type=int, default=120, help="Gap (minutes) that marks a day as yellow (default: 120)") + p.add_argument("--gap-green-min", type=int, default=15, help="Max gap (minutes) to still be green (default: 15)") + p.add_argument("--read-durations", action="store_true", + help="Use mutagen to read exact file durations (optional; default off).") + p.add_argument("--green-cover-pct", type=float, default=90.0, + help="Min coverage%% for green (default: 90)") + p.add_argument("--yellow-cover-pct", type=float, default=10.0, + help="Coverage%% below which day is yellow (default: 10)") + # Duration policy: default is NO inference, assume 2h + p.add_argument("--assume-duration-sec", type=int, default=7200, + help="Assumed duration (s) when real duration is unavailable. Default: 7200 (2h).") + p.add_argument("--infer", action="store_true", + help="Enable inferring duration from median start deltas (disabled by default).") + p.add_argument("--infer-min-sec", type=int, default=60, + help="Ignore deltas smaller than this when inferring (default: 60s)") + p.add_argument("--infer-max-sec", type=int, default=6*3600, + help="Ignore deltas larger than this when inferring (default: 6h)") + p.add_argument("--infer-default-sec", type=int, default=20*60, + help="Fallback segment length if nothing else is available (default: 1200s)") + p.add_argument("--guard-gap-sec", type=int, default=0, + help="Don’t let a segment extend within this many seconds of next start (default: 0)") + p.add_argument("--title", default="Portable Recorder Status", help="Page title") + args = p.parse_args() + build(args) + +if __name__ == "__main__": + main() diff --git a/clio-pi/bin/record.sh b/clio-pi/bin/record.sh new file mode 100755 index 0000000..6e86c46 --- /dev/null +++ b/clio-pi/bin/record.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -euo pipefail + +# Configuration +AUDIO_DEVICE="${AUDIO_DEVICE:-hw:1}" # ALSA device (run 'arecord -l' to list) +SEGMENT_SECONDS="${SEGMENT_SECONDS:-7200}" # 2 hours per file +RECORDINGS_DIR="${RECORDINGS_DIR:-$HOME/recordings}" +SOURCE_NAME="${SOURCE_NAME:-stationary}" # Prefix for filenames + +mkdir -p "${RECORDINGS_DIR}" + +echo "Starting recording..." +echo " Device: ${AUDIO_DEVICE}" +echo " Segment: ${SEGMENT_SECONDS}s" +echo " Output: ${RECORDINGS_DIR}/${SOURCE_NAME}-*.opus" + +while true; do + ffmpeg -f alsa -ac 1 -i "${AUDIO_DEVICE}" \ + -c:a libopus \ + -b:a 32k \ + -f segment \ + -segment_time "${SEGMENT_SECONDS}" \ + -strftime 1 \ + -reset_timestamps 1 \ + "${RECORDINGS_DIR}/${SOURCE_NAME}-%Y-%m-%d_%H-%M-%S.opus" \ + 2>&1 | while read line; do echo "[ffmpeg] $line"; done + + echo "Recording stopped, restarting in 5s..." + sleep 5 +done + diff --git a/clio-pi/setup.sh b/clio-pi/setup.sh new file mode 100755 index 0000000..3760de2 --- /dev/null +++ b/clio-pi/setup.sh @@ -0,0 +1,238 @@ +#!/bin/bash +set -euo pipefail + +# Clio Pi Setup Script +# Run this after a fresh Raspberry Pi OS install +# +# Prerequisites: +# 1. Flash Raspberry Pi OS Lite to SD card +# 2. Use Raspberry Pi Imager to pre-configure: +# - Hostname (e.g., clio-pi) +# - Username/password +# - WiFi credentials +# - Enable SSH +# 3. Boot Pi and SSH in +# 4. Copy this repo to /home/pi/clio-pi +# 5. Run: sudo ./setup.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PI_USER="${PI_USER:-pi}" +PI_HOME="/home/${PI_USER}" + +echo "=== Clio Pi Setup ===" +echo "User: ${PI_USER}" +echo "Home: ${PI_HOME}" +echo "" + +# ============================================================ +# 1. System updates +# ============================================================ +echo ">>> Updating system packages..." +apt-get update +apt-get upgrade -y + +# ============================================================ +# 2. Install required packages +# ============================================================ +echo ">>> Installing required packages..." +apt-get install -y \ + ffmpeg \ + python3 \ + python3-pip \ + nginx-light \ + chrony \ + rsync + +# Optional: for reading audio durations +pip3 install --break-system-packages mutagen || true + +# ============================================================ +# 3. Configure NTP/time sync (chrony) +# ============================================================ +echo ">>> Configuring time sync (chrony)..." +cat > /etc/chrony/chrony.conf << 'EOF' +# Use default NTP pools +pool 2.debian.pool.ntp.org iburst + +# Allow the system clock to be stepped in the first 3 updates +# if its offset is larger than 1 second +makestep 1.0 3 + +# Enable kernel synchronization of the real-time clock (RTC) +rtcsync + +# Serve time to the local subnet (optional) +#allow 10.0.0.0/24 + +# Log tracking, measurements, statistics +logdir /var/log/chrony +EOF + +systemctl enable chrony +systemctl restart chrony + +# Set timezone (adjust as needed) +timedatectl set-timezone America/New_York + +echo ">>> Time sync configured. Current time:" +timedatectl + +# ============================================================ +# 4. Configure SSH key-only access +# ============================================================ +echo ">>> Configuring SSH for key-only access..." + +# Ensure .ssh directory exists +mkdir -p "${PI_HOME}/.ssh" +chmod 700 "${PI_HOME}/.ssh" +touch "${PI_HOME}/.ssh/authorized_keys" +chmod 600 "${PI_HOME}/.ssh/authorized_keys" +chown -R "${PI_USER}:${PI_USER}" "${PI_HOME}/.ssh" + +# Harden sshd_config +SSHD_CONFIG="/etc/ssh/sshd_config" + +# Backup original +cp "${SSHD_CONFIG}" "${SSHD_CONFIG}.bak" + +# Disable password authentication +sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' "${SSHD_CONFIG}" +sed -i 's/^#*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication no/' "${SSHD_CONFIG}" +sed -i 's/^#*UsePAM.*/UsePAM no/' "${SSHD_CONFIG}" +sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' "${SSHD_CONFIG}" + +# Ensure pubkey auth is enabled +grep -q "^PubkeyAuthentication" "${SSHD_CONFIG}" || echo "PubkeyAuthentication yes" >> "${SSHD_CONFIG}" + +echo "" +echo "!!! IMPORTANT !!!" +echo "Before rebooting, add your SSH public key to:" +echo " ${PI_HOME}/.ssh/authorized_keys" +echo "" +echo "From your local machine, run:" +echo " ssh-copy-id ${PI_USER}@" +echo "" +echo "Or manually append your ~/.ssh/id_ed25519.pub (or id_rsa.pub)" +echo "" + +# Don't restart SSH yet - user needs to add their key first + +# ============================================================ +# 5. Set up recording directories +# ============================================================ +echo ">>> Setting up recording directories..." +mkdir -p "${PI_HOME}/recordings" +mkdir -p "${PI_HOME}/bin" +chown -R "${PI_USER}:${PI_USER}" "${PI_HOME}/recordings" +chown -R "${PI_USER}:${PI_USER}" "${PI_HOME}/bin" + +# ============================================================ +# 6. Install recording scripts +# ============================================================ +echo ">>> Installing recording scripts..." +cp "${SCRIPT_DIR}/bin/record.sh" "${PI_HOME}/bin/" +cp "${SCRIPT_DIR}/bin/gen_recording_status.py" "${PI_HOME}/bin/" +chmod +x "${PI_HOME}/bin/record.sh" +chmod +x "${PI_HOME}/bin/gen_recording_status.py" +chown -R "${PI_USER}:${PI_USER}" "${PI_HOME}/bin" + +# ============================================================ +# 7. Install systemd service +# ============================================================ +echo ">>> Installing systemd service..." +cp "${SCRIPT_DIR}/systemd/record.service" /etc/systemd/system/ +sed -i "s|/home/pi|${PI_HOME}|g" /etc/systemd/system/record.service +sed -i "s|User=pi|User=${PI_USER}|g" /etc/systemd/system/record.service + +systemctl daemon-reload +systemctl enable record.service +# Don't start yet - user may need to configure audio device + +# ============================================================ +# 8. Set up cron for status page +# ============================================================ +echo ">>> Setting up cron job for status page..." +CRON_LINE="*/10 * * * * python3 ${PI_HOME}/bin/gen_recording_status.py --dir ${PI_HOME}/recordings" + +# Install cron job for pi user +(crontab -u "${PI_USER}" -l 2>/dev/null | grep -v gen_recording_status; echo "${CRON_LINE}") | crontab -u "${PI_USER}" - + +# ============================================================ +# 9. Configure nginx for status page +# ============================================================ +echo ">>> Configuring nginx for status page..." +cat > /etc/nginx/sites-available/default << 'EOF' +server { + listen 80 default_server; + listen [::]:80 default_server; + + root /var/www/html; + index index.html; + + server_name _; + + location / { + try_files $uri $uri/ =404; + } +} +EOF + +# Ensure www-data can read the status file +mkdir -p /var/www/html +chown -R www-data:www-data /var/www/html + +# Allow pi user to write to /var/www/html +usermod -a -G www-data "${PI_USER}" || true +chmod 775 /var/www/html + +systemctl enable nginx +systemctl restart nginx + +# Generate initial status page +sudo -u "${PI_USER}" python3 "${PI_HOME}/bin/gen_recording_status.py" --dir "${PI_HOME}/recordings" || true + +# ============================================================ +# 10. Print audio device info +# ============================================================ +echo "" +echo "=== Audio Devices ===" +arecord -l || echo "(no audio devices found yet)" +echo "" +echo "The recording script uses hw:1 by default." +echo "Edit ${PI_HOME}/bin/record.sh if your device is different." + +# ============================================================ +# Summary +# ============================================================ +echo "" +echo "============================================================" +echo "=== Setup Complete ===" +echo "============================================================" +echo "" +echo "Next steps:" +echo "" +echo "1. Add your SSH public key:" +echo " From your computer: ssh-copy-id ${PI_USER}@$(hostname -I | awk '{print $1}')" +echo "" +echo "2. Test SSH key login works, then restart SSH:" +echo " sudo systemctl restart sshd" +echo "" +echo "3. Check/configure audio device:" +echo " arecord -l" +echo " Edit ~/bin/record.sh if needed (currently uses hw:1)" +echo "" +echo "4. Start recording service:" +echo " sudo systemctl start record.service" +echo " sudo systemctl status record.service" +echo "" +echo "5. View status page:" +echo " http://$(hostname -I | awk '{print $1}')/" +echo "" +echo "Services installed:" +echo " - chrony (NTP time sync) - RUNNING" +echo " - nginx (status page) - RUNNING" +echo " - record.service - ENABLED (not started)" +echo "" +echo "Cron job installed:" +echo " - gen_recording_status.py runs every 10 minutes" +echo "" diff --git a/clio-pi/systemd/record.service b/clio-pi/systemd/record.service new file mode 100644 index 0000000..9e8240a --- /dev/null +++ b/clio-pi/systemd/record.service @@ -0,0 +1,11 @@ +[Unit] +Description=Record + +[Service] +ExecStart=/home/pi/bin/record.sh +User=pi +Type=simple +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/clio-ui/components/RecordingDetail.jsx b/clio-ui/components/RecordingDetail.jsx index 9c939c4..ac5a5b6 100644 --- a/clio-ui/components/RecordingDetail.jsx +++ b/clio-ui/components/RecordingDetail.jsx @@ -510,7 +510,7 @@ function RecordingDetail({ recording: r, data, onBack, allHighlights, setAllHigh style={{ fontFamily: 'var(--font-mono)', fontSize: 10, padding: '3px 10px', background: 'var(--bg4)', border: '1px solid var(--border)', borderRadius: 4, - color: 'var(--amber)', cursor: 'pointer', + color: 'var(--amber)', }} > ↓ Go to {formatTime(currentTime)}