diff --git a/.cursorrules b/.cursorrules deleted file mode 100644 index fd4c616e5fd5a4fc09d51925cdb9f9b744513b11..0000000000000000000000000000000000000000 --- a/.cursorrules +++ /dev/null @@ -1,71 +0,0 @@ -# Cursor Engineering Ruleset - -## 1. Context First -Always request full context and constraints before proposing any decision. -- Understand the problem completely before suggesting solutions -- Ask clarifying questions when requirements are ambiguous -- Consider existing codebase patterns and conventions - -## 2. Tech Stack Principles -Recommend tech stacks using: -- Idiomatic, native patterns for the language/framework -- Simple and maintainable components -- Minimal unnecessary abstraction -- Prefer standard library over external dependencies when reasonable - -## 3. Scaffold Before Implementation -Scaffold the project structure BEFORE implementation: -- Clear domain boundaries -- Clean folder organization -- Conventional naming (language-specific conventions) -- Consistent imports/exports -- Document the structure in README - -## 4. Test-Driven Development (TDD) -Use TDD approach: -- Tests define behavior before implementation -- Define what failure looks like explicitly -- No implementation until tests exist -- Edge cases explicitly covered -- Tests should be readable as documentation - -## 5. Idempotent Functions -All core functions must be idempotent: -- Deterministic behavior (same input โ†’ same output) -- Safe to re-run multiple times -- No hidden state or side effects -- Pure functions where possible - -## 6. Simplicity First -Optimize for simplicity: -- Low cognitive load -- Readable and clean code -- Avoid cleverness and "magic" -- Avoid premature optimization -- YAGNI (You Aren't Gonna Need It) -- DRY (Don't Repeat Yourself) but not at the cost of clarity - -## 7. Idiomatic Code -Use idiomatic language patterns at all times: -- Follow language-specific style guides -- Use conventional patterns for the ecosystem -- Leverage language features appropriately -- Write code that looks familiar to other developers - ---- - -## Project-Specific Rules - -### Video Analyzer Project -- Use 100% free and open-source tools -- Prefer local processing over cloud APIs -- Keep user data private (process locally) -- Support both CLI and future web UI -- Modular architecture for easy extension - -### Python Conventions -- Type hints on all function signatures -- Docstrings for public functions -- Use pathlib for file paths -- Rich for CLI output -- Pydantic for configuration/validation diff --git a/.env.example b/.env.example deleted file mode 100644 index cdcf80707aebc768a75ad9ec9b43326fb065af1c..0000000000000000000000000000000000000000 --- a/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -# Video Analyzer - Environment Variables -# Copy this file to .env and fill in your values - -# Hugging Face API Key (optional - for faster API-based summarization) -# Get your free key at: https://huggingface.co/settings/tokens -HUGGINGFACE_API_KEY=your_token_here - -# Whisper Model Size (tiny, base, small, medium, large-v3) -VIDEO_ANALYZER_WHISPER_MODEL=base - -# Default AI Backend (ollama, huggingface, huggingface-api) -VIDEO_ANALYZER_AI_BACKEND=huggingface diff --git a/.gitignore b/.gitignore index 3fc2a7a2201e68f63008fda815693812f35c7448..bb03274bcfdbd7e4018435eb9e9533dfc78aa0d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,38 +1,39 @@ -# Environment variables (contains secrets!) -.env -.env.local - # Python __pycache__/ *.py[cod] *$py.class *.so .Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments .venv/ venv/ ENV/ -# Data directories (large files) -data/downloads/ -data/audio/ -data/chromadb/ - -# Keep transcripts and summaries (text files are small) -# data/transcripts/ -# data/summaries/ - -# Models cache -models/ - # IDE .idea/ .vscode/ *.swp *.swo -# OS -.DS_Store -Thumbs.db +# Environment +.env +.env.local -# Logs -*.log +# uv +.python-version diff --git a/DEPLOY_TO_HF_SPACES.md b/DEPLOY_TO_HF_SPACES.md deleted file mode 100644 index 06367698366d8d77c08fd892865a2f40594a39a5..0000000000000000000000000000000000000000 --- a/DEPLOY_TO_HF_SPACES.md +++ /dev/null @@ -1,134 +0,0 @@ -# Deploy to HuggingFace Spaces - -This guide will help you deploy your Real Estate Mentor to HuggingFace Spaces for free. - -## What You'll Get - -- ๐ŸŒ **Public URL** - Access from anywhere -- ๐Ÿ’พ **Persistent Storage** - Your data is saved -- ๐Ÿ†“ **100% Free** - No cost on free tier -- ๐Ÿ”’ **Private Option** - Can make it private - ---- - -## Step 1: Create a New Space - -1. Go to: https://huggingface.co/new-space - -2. Fill in: - ``` - Space name: real-estate-mentor - License: MIT - SDK: Gradio - Hardware: CPU Basic (Free) - Visibility: Public (or Private) - ``` - -3. Click **"Create Space"** - ---- - -## Step 2: Upload Files - -### Option A: Upload via Web Interface - -1. In your new Space, click **"Files"** tab -2. Click **"+ Add file"** โ†’ **"Upload files"** -3. Upload these files from the `hf_space/` folder: - - `app.py` - - `requirements.txt` - - `README.md` - -### Option B: Use Git (Recommended) - -```bash -# Clone your space -git clone https://huggingface.co/spaces/YOUR_USERNAME/real-estate-mentor -cd real-estate-mentor - -# Copy files from hf_space/ -cp /path/to/video_analyzer/hf_space/* . - -# Push to HuggingFace -git add . -git commit -m "Initial deployment" -git push -``` - ---- - -## Step 3: Wait for Build - -1. Go to your Space URL: `https://huggingface.co/spaces/YOUR_USERNAME/real-estate-mentor` -2. Watch the **"Building"** status -3. First build takes ~3-5 minutes (downloading models) -4. When ready, you'll see **"Running"** โœ… - ---- - -## Step 4: Enable Persistent Storage - -**Important:** To keep your data between restarts: - -1. Go to Space **Settings** -2. Find **"Persistent Storage"** -3. Enable it (free tier: up to 50GB) - -This ensures your indexed content survives Space restarts. - ---- - -## Step 5: Start Using It! - -1. **Upload Tab** - Add your course transcripts -2. **Search Tab** - Find content semantically -3. **Ask Tab** - Chat with your AI mentor -4. **Status Tab** - See what's indexed - ---- - -## Troubleshooting - -### Space is "Sleeping" - -Free Spaces sleep after ~15 minutes of inactivity. Just visit the URL and it will wake up (takes ~30 seconds). - -### Build Failed - -Check the **Logs** tab for errors. Common issues: -- Missing dependencies โ†’ Check `requirements.txt` -- Syntax errors โ†’ Check `app.py` - -### Data Disappeared - -Make sure **Persistent Storage** is enabled in Settings. - ---- - -## Upgrading (Optional) - -For faster performance, you can upgrade hardware: - -| Tier | Cost | Benefits | -|------|------|----------| -| CPU Basic | Free | Works fine, sleeps after 15 min | -| CPU Upgrade | $0.03/hr | Faster, no sleep | -| GPU | $0.60/hr | Much faster embeddings | - ---- - -## Files Reference - -``` -hf_space/ -โ”œโ”€โ”€ app.py # Main Gradio application -โ”œโ”€โ”€ requirements.txt # Python dependencies -โ””โ”€โ”€ README.md # Space description (shows on page) -``` - ---- - -## Need Help? - -- HuggingFace Docs: https://huggingface.co/docs/hub/spaces -- Gradio Docs: https://gradio.app/docs/ diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 72ea5f51764172c8bbdfca781beddc4f007aea79..0000000000000000000000000000000000000000 --- a/PLAN.md +++ /dev/null @@ -1,437 +0,0 @@ -# Video Analyzer - Project Plan - -## Overview -A comprehensive tool to download videos from multiple sources, transcribe them to text, summarize content, and build a searchable knowledge base. The end goal is to create a **Virtual Real Estate Mentor** from course materials. - -**๐Ÿ†“ 100% Free & Open Source - No API costs!** - ---- - -## Tech Stack (All Free & Open Source) - -| Component | Technology | License | Notes | -|-----------|------------|---------|-------| -| **Language** | Python 3.11+ | PSF | Main language | -| **Video Download** | yt-dlp | Unlicense | Supports 1000+ sites | -| **Audio Processing** | ffmpeg | LGPL/GPL | Industry standard | -| **Transcription** | Whisper.cpp / faster-whisper | MIT | Local, fast, accurate | -| **Document Parsing** | PyMuPDF, python-docx | AGPL/MIT | PDF, Word support | -| **OCR** | Tesseract | Apache 2.0 | Image text extraction | -| **Vector DB** | ChromaDB | Apache 2.0 | Local vector storage | -| **Embeddings** | sentence-transformers | Apache 2.0 | all-MiniLM-L6-v2 model | -| **LLM** | Ollama + Llama3/Mistral/Phi | Various OSS | Local AI, no API costs | -| **Web UI** | Gradio | Apache 2.0 | Simple, beautiful UI | -| **CLI** | Typer | MIT | Command-line interface | -| **Database** | SQLite | Public Domain | Metadata storage | - ---- - -## Core Features - -### 1. Multi-Source Video Downloader -- **Supported Platforms:** - - YouTube, Vimeo, Dailymotion - - Udemy (with cookies/auth) - - Teachable, Thinkific, Kajabi - - Direct video URLs (MP4, WebM, etc.) - - Google Drive, Dropbox links -- **Technology:** `yt-dlp` (free, actively maintained) -- **Features:** - - Playlist/batch downloading - - Quality selection - - Resume interrupted downloads - - Metadata extraction (title, description, chapters) - - Cookie-based authentication for paid courses - -### 2. Audio Extraction & Transcription -- **Audio Extraction:** `ffmpeg` (free) -- **Speech-to-Text:** - - **faster-whisper** - CTranslate2 optimized, 4x faster than original - - Models: tiny, base, small, medium, large-v3 - - Runs entirely local - no internet needed -- **Features:** - - Speaker diarization (with pyannote - free for research) - - Word-level timestamps - - Multiple language support (99 languages) - - Auto language detection - -### 3. Document Processing -- **Supported Formats:** - - PDF (PyMuPDF - fast, accurate) - - Word documents (python-docx) - - PowerPoint slides (python-pptx) - - Images with text (Tesseract OCR) - - Markdown, TXT, HTML -- **All libraries are free and open source** - -### 4. Local LLM for Summarization & Analysis -- **Ollama** - Run LLMs locally with simple API -- **Recommended Models (all free):** - | Model | Size | Speed | Quality | Best For | - |-------|------|-------|---------|----------| - | Phi-3 | 3.8B | โšกโšกโšก | Good | Fast summaries | - | Mistral | 7B | โšกโšก | Great | Balanced | - | Llama3 | 8B | โšกโšก | Excellent | Best quality | - | Llama3 | 70B | โšก | Outstanding | If you have GPU | - -- **Features:** - - Quick summaries - - Detailed study notes - - Key concept extraction - - Action items and strategies - - Q&A over content - -### 5. Knowledge Base & Vector Storage -- **ChromaDB** - Local vector database (free) -- **Embeddings:** sentence-transformers - - Model: `all-MiniLM-L6-v2` (fast, 384 dimensions) - - Alternative: `all-mpnet-base-v2` (better quality, slower) -- **Features:** - - Semantic search across all content - - Source attribution with timestamps - - Hybrid search (semantic + keyword) - - No cloud, all local - -### 6. Virtual Mentor Chat Interface -- **RAG (Retrieval Augmented Generation):** - - Query โ†’ Find relevant chunks โ†’ Generate response - - All runs locally with Ollama -- **Interfaces:** - - CLI chat (terminal) - - Web UI (Gradio - beautiful, easy) -- **Features:** - - Context-aware responses - - Source citations - - Conversation memory - - Export chat history - ---- - -## Architecture - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ VIDEO ANALYZER (100% Local) โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ Ingestion โ”‚ โ”‚ Processing โ”‚ โ”‚ Knowledge Base โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ -โ”‚ โ”‚ โ€ข yt-dlp โ”‚ โ”‚ โ€ข ffmpeg โ”‚ โ”‚ โ€ข ChromaDB โ”‚ โ”‚ -โ”‚ โ”‚ โ€ข Cookies โ”‚โ†’ โ”‚ โ€ข Whisper โ”‚โ†’ โ”‚ โ€ข sentence-transform โ”‚ โ”‚ -โ”‚ โ”‚ โ€ข File input โ”‚ โ”‚ โ€ข Tesseract โ”‚ โ”‚ โ€ข SQLite metadata โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”‚ -โ”‚ โ†“ โ”‚ -โ”‚ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ Virtual Mentor (Ollama + RAG) โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ -โ”‚ โ”‚ โ€ข Llama3 / Mistral / Phi-3 (your choice) โ”‚ โ”‚ -โ”‚ โ”‚ โ€ข Context retrieval from ChromaDB โ”‚ โ”‚ -โ”‚ โ”‚ โ€ข Local inference - no API calls โ”‚ โ”‚ -โ”‚ โ”‚ โ€ข Gradio web interface โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - ---- - -## System Requirements - -### Minimum (CPU only) -- **RAM:** 8GB (16GB recommended) -- **Storage:** 20GB+ for models and data -- **CPU:** Any modern x64 processor -- **Whisper:** Use "small" or "base" model -- **LLM:** Use Phi-3 (3.8B) model - -### Recommended (with GPU) -- **RAM:** 16GB+ -- **GPU:** NVIDIA with 8GB+ VRAM (RTX 3060+) -- **Whisper:** Use "medium" or "large-v3" model -- **LLM:** Use Llama3 8B or Mistral 7B - -### Optimal (power user) -- **GPU:** RTX 4090 or similar (24GB VRAM) -- **LLM:** Llama3 70B for best quality - ---- - -## Project Structure - -``` -video_analyzer/ -โ”œโ”€โ”€ src/ -โ”‚ โ”œโ”€โ”€ __init__.py -โ”‚ โ”œโ”€โ”€ main.py # Entry point -โ”‚ โ”œโ”€โ”€ config.py # Configuration -โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€ downloaders/ # Video/content downloaders -โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py -โ”‚ โ”‚ โ”œโ”€โ”€ base.py # Base downloader class -โ”‚ โ”‚ โ”œโ”€โ”€ ytdlp.py # yt-dlp wrapper -โ”‚ โ”‚ โ””โ”€โ”€ files.py # Local file handling -โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€ processors/ # Content processors -โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py -โ”‚ โ”‚ โ”œโ”€โ”€ audio.py # Audio extraction (ffmpeg) -โ”‚ โ”‚ โ”œโ”€โ”€ transcriber.py # Whisper transcription -โ”‚ โ”‚ โ”œโ”€โ”€ documents.py # PDF, Word, PPT -โ”‚ โ”‚ โ””โ”€โ”€ ocr.py # Tesseract OCR -โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€ analyzers/ # AI analysis -โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py -โ”‚ โ”‚ โ”œโ”€โ”€ summarizer.py # Ollama summarization -โ”‚ โ”‚ โ”œโ”€โ”€ extractor.py # Key info extraction -โ”‚ โ”‚ โ””โ”€โ”€ chunker.py # Text chunking -โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€ knowledge/ # Knowledge base -โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py -โ”‚ โ”‚ โ”œโ”€โ”€ vectorstore.py # ChromaDB -โ”‚ โ”‚ โ”œโ”€โ”€ embeddings.py # sentence-transformers -โ”‚ โ”‚ โ””โ”€โ”€ search.py # Semantic search -โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€ mentor/ # Virtual mentor -โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py -โ”‚ โ”‚ โ”œโ”€โ”€ rag.py # RAG pipeline -โ”‚ โ”‚ โ”œโ”€โ”€ ollama_client.py # Ollama integration -โ”‚ โ”‚ โ””โ”€โ”€ prompts.py # System prompts -โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€ ui/ # User interfaces -โ”‚ โ”œโ”€โ”€ __init__.py -โ”‚ โ”œโ”€โ”€ cli.py # Typer CLI -โ”‚ โ””โ”€โ”€ web.py # Gradio web app -โ”‚ -โ”œโ”€โ”€ data/ # Data storage -โ”‚ โ”œโ”€โ”€ downloads/ # Downloaded videos -โ”‚ โ”œโ”€โ”€ audio/ # Extracted audio -โ”‚ โ”œโ”€โ”€ transcripts/ # Transcriptions -โ”‚ โ”œโ”€โ”€ summaries/ # Summaries -โ”‚ โ””โ”€โ”€ chromadb/ # Vector database -โ”‚ -โ”œโ”€โ”€ models/ # Local model cache -โ”‚ โ””โ”€โ”€ whisper/ # Whisper models -โ”‚ -โ”œโ”€โ”€ tests/ -โ”œโ”€โ”€ requirements.txt -โ”œโ”€โ”€ install.sh # One-click setup script -โ”œโ”€โ”€ .cursorrules -โ””โ”€โ”€ README.md -``` - ---- - -## Dependencies (requirements.txt) - -``` -# Core -python-dotenv>=1.0.0 -typer[all]>=0.9.0 -rich>=13.0.0 - -# Video/Audio -yt-dlp>=2024.1.0 -ffmpeg-python>=0.2.0 - -# Transcription -faster-whisper>=1.0.0 -# or: openai-whisper>=20231117 - -# Document Processing -PyMuPDF>=1.23.0 -python-docx>=1.0.0 -python-pptx>=0.6.23 -pytesseract>=0.3.10 - -# AI/ML -sentence-transformers>=2.2.0 -chromadb>=0.4.0 -ollama>=0.1.0 - -# Web UI -gradio>=4.0.0 - -# Utilities -tqdm>=4.66.0 -pydantic>=2.0.0 -``` - ---- - -## External Dependencies (System) - -```bash -# Ubuntu/Debian -sudo apt install ffmpeg tesseract-ocr - -# macOS -brew install ffmpeg tesseract - -# Windows -# Download ffmpeg and tesseract installers - -# Ollama (all platforms) -curl -fsSL https://ollama.com/install.sh | sh -ollama pull llama3 # or mistral, phi3 -``` - ---- - -## Development Phases - -### Phase 1: Foundation (Week 1-2) -- [ ] Project setup & dependencies -- [ ] yt-dlp video downloader -- [ ] ffmpeg audio extraction -- [ ] faster-whisper transcription -- [ ] Basic CLI with Typer - -### Phase 2: Processing Pipeline (Week 3-4) -- [ ] PDF/Word/PPT processing -- [ ] OCR for images -- [ ] Text chunking strategy -- [ ] SQLite metadata storage -- [ ] Batch processing - -### Phase 3: Knowledge Base (Week 5-6) -- [ ] sentence-transformers embeddings -- [ ] ChromaDB integration -- [ ] Semantic search -- [ ] Hybrid search (semantic + keyword) -- [ ] Source attribution - -### Phase 4: Virtual Mentor (Week 7-8) -- [ ] Ollama integration -- [ ] RAG implementation -- [ ] Real estate prompts -- [ ] Conversation memory -- [ ] CLI chat interface - -### Phase 5: Polish & UI (Week 9-10) -- [ ] Gradio web interface -- [ ] Progress tracking -- [ ] Export features -- [ ] Error handling -- [ ] Documentation - ---- - -## Real Estate Mentor - Special Features - -### Domain-Specific Prompts -```python -REAL_ESTATE_SYSTEM_PROMPT = """ -You are a knowledgeable real estate mentor with expertise from -the user's course materials. Help them with: -- Deal analysis (cash flow, ROI, cap rates) -- Negotiation strategies -- Market analysis -- Legal considerations -- Financing options - -Always cite which video/document your advice comes from. -""" -``` - -### Deal Analysis Helper -- Input property details -- Get relevant strategies from course content -- Calculate key metrics -- Risk assessment based on learned material - -### Study Features -- Auto-generate flashcards -- Create quizzes from content -- Build glossary of terms -- Track learning progress - ---- - -## CLI Commands - -```bash -# Download video(s) -video-analyzer download "https://youtube.com/watch?v=..." -video-analyzer download --playlist "https://youtube.com/playlist?..." -video-analyzer download --cookies cookies.txt "https://udemy.com/course/..." - -# Process content -video-analyzer transcribe ./data/downloads/ -video-analyzer process ./documents/ # PDFs, Word, etc. - -# Build knowledge base -video-analyzer index # Index all processed content -video-analyzer search "what is cap rate" - -# Summarize -video-analyzer summarize ./data/transcripts/video1.txt -video-analyzer summarize --all # Summarize everything - -# Chat with mentor -video-analyzer chat # CLI chat -video-analyzer ui # Launch web UI - -# Utilities -video-analyzer status # Show processing status -video-analyzer export # Export all notes -``` - ---- - -## Web UI Preview - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ ๐ŸŽ“ Real Estate Mentor [โš™๏ธ] โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ ๐Ÿ“š Knowledge Base: 47 videos, 12 documents indexed โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ You: How do I calculate cash-on-cash return? โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ Mentor: Cash-on-cash return measures the annual โ”‚ โ”‚ -โ”‚ โ”‚ pre-tax cash flow relative to the total cash โ”‚ โ”‚ -โ”‚ โ”‚ invested. The formula is: โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ CoC Return = (Annual Cash Flow / Total Cash) ร— 100 โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ ๐Ÿ“Ž Source: Module 3 - Investment Analysis (12:34) โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”‚ -โ”‚ [Type your question here... ] [Send] โ”‚ -โ”‚ โ”‚ -โ”‚ [๐Ÿ“ฅ Add Content] [๐Ÿ“Š Analyze Deal] [๐Ÿ“ Study Mode] โ”‚ -โ”‚ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - ---- - -## Cost Comparison - -| Approach | Monthly Cost | Our Approach | -|----------|--------------|--------------| -| OpenAI GPT-4 | $20-100+ | **$0** (Ollama) | -| OpenAI Whisper API | $0.006/min | **$0** (local Whisper) | -| Pinecone Vector DB | $70+ | **$0** (ChromaDB) | -| Cloud transcription | $0.01-0.05/min | **$0** (local) | -| **Total** | **$100+/month** | **$0** | - -**Only costs:** Electricity to run your computer ๐Ÿ’ก - ---- - -## Next Steps - -1. โœ… Plan complete - 100% free & open source -2. **Ready to start coding!** - -Shall I begin with Phase 1? -- Set up project structure -- Install dependencies -- Build the video downloader diff --git a/README.md b/README.md index b3baba0e4eebdbd094615291770ca2b6aa646f9d..dd402758495d9150fd1fd5f2cb5e0d4e5a4099a6 100644 --- a/README.md +++ b/README.md @@ -1,192 +1,14 @@ -# Video Analyzer ๐ŸŽฌ - -**100% Free & Open Source** - No API costs, runs entirely on your machine. - -A powerful tool to download videos from multiple sources, transcribe to text, summarize content, and build a searchable knowledge base with an AI-powered virtual mentor. - -## ๐ŸŽฏ Use Case - -Turn online courses (like real estate training) into a personal AI mentor that can: -- Answer questions about course content -- Help analyze deals using learned strategies -- Provide quick access to key concepts and definitions -- **All running locally - your data stays private!** - -## ๐Ÿ†“ 100% Free Stack - -| Component | Tool | Cost | -|-----------|------|------| -| Video Download | yt-dlp | Free | -| Transcription | Whisper (local) | Free | -| Document Processing | PyMuPDF, python-docx | Free | -| OCR | Tesseract | Free | -| Summarization | Ollama (Llama3/Mistral) | Free | -| Vector Database | ChromaDB | Free | -| Web UI | Gradio | Free | - -**Total monthly cost: $0** ๐Ÿ’ฐ - -## ๐Ÿ“‹ Features - -### Phase 1 โœ… -- **YouTube video downloading** with yt-dlp -- **AI transcription** using local Whisper -- **Audio extraction** with ffmpeg - -### Phase 2 โœ… -- **Direct file/folder import** - drop files and process -- **PDF processing** with PyMuPDF -- **Word/PowerPoint processing** -- **OCR for images** with Tesseract -- **AI summarization** with Ollama (local LLM) -- **Smart text chunking** for long documents - -### Coming Soon -- **Phase 3:** Vector database + semantic search -- **Phase 4:** Virtual mentor RAG chat -- **Phase 5:** Web UI with Gradio - -## ๐Ÿ’ป Requirements - -**Minimum:** -- 8GB RAM (16GB recommended) -- Any modern CPU -- 20GB storage - -**Recommended (for faster processing):** -- NVIDIA GPU with 8GB+ VRAM -- 16GB+ RAM - -## ๐Ÿš€ Quick Start - -### 1. Install Dependencies - -```bash -# Clone and setup -git clone -cd video_analyzer - -# Install Python dependencies -pip install -r requirements.txt -``` - -### 2. Install Ollama (for AI summaries) - -```bash -# Install Ollama -curl -fsSL https://ollama.com/install.sh | sh - -# Pull a model (choose one) -ollama pull llama3 # Best quality (8B params) -ollama pull mistral # Good balance -ollama pull phi3 # Fastest (3.8B params) - -# Start Ollama server -ollama serve -``` - -### 3. Process Your Content - -```bash -# Add local files (videos, PDFs, Word docs, etc.) -./video-analyzer add /path/to/your/course/files - -# Process everything (transcribe videos, extract docs) -./video-analyzer process-all - -# Generate AI summaries -./video-analyzer summarize --all --type study_notes - -# Check status -./video-analyzer status -``` - -## ๐Ÿ“– CLI Commands - -### Content Management -```bash -./video-analyzer add PATH # Add files/folders -./video-analyzer status # Show statistics -./video-analyzer list-content # List processed content -``` - -### Processing -```bash -./video-analyzer transcribe PATH # Transcribe audio/video -./video-analyzer process-docs [PATH] # Process PDF/Word/PPT -./video-analyzer process-images [PATH] # OCR images -./video-analyzer process-all [PATH] # Process everything -``` - -### YouTube (requires cookies) -```bash -./video-analyzer download URL --cookies cookies.txt -./video-analyzer process URL --cookies cookies.txt -``` - -### AI Summarization -```bash -./video-analyzer summarize PATH # Summarize one file -./video-analyzer summarize --all # Summarize all transcripts -./video-analyzer summarize -t real_estate # Real estate focus -./video-analyzer summarize -t study_notes # Study notes format -``` - -### Summary Types - -| Type | Description | -|------|-------------| -| `quick` | 2-3 paragraph overview | -| `detailed` | Comprehensive summary with key points | -| `study_notes` | Formatted notes with concepts, definitions, action items | -| `real_estate` | Specialized for real estate content with deal analysis | - -## ๐Ÿ”ง Whisper Models - -| Model | Size | Speed | Quality | RAM | -|-------|------|-------|---------|-----| -| tiny | 39M | โšกโšกโšกโšก | Basic | 1GB | -| base | 74M | โšกโšกโšก | Good | 1GB | -| small | 244M | โšกโšก | Great | 2GB | -| medium | 769M | โšก | Excellent | 5GB | -| large-v3 | 1550M | ๐Ÿข | Best | 10GB | - -## ๐Ÿ“ Project Structure - -``` -video_analyzer/ -โ”œโ”€โ”€ src/ -โ”‚ โ”œโ”€โ”€ downloaders/ # yt-dlp, file handling -โ”‚ โ”œโ”€โ”€ processors/ # Whisper, documents, OCR -โ”‚ โ”œโ”€โ”€ analyzers/ # Ollama summarization, chunking -โ”‚ โ”œโ”€โ”€ knowledge/ # Vector DB (Phase 3) -โ”‚ โ”œโ”€โ”€ mentor/ # RAG chat (Phase 4) -โ”‚ โ””โ”€โ”€ ui/ # CLI, web interface -โ”œโ”€โ”€ data/ -โ”‚ โ”œโ”€โ”€ downloads/ # Source files -โ”‚ โ”œโ”€โ”€ audio/ # Extracted audio -โ”‚ โ”œโ”€โ”€ transcripts/ # Text content -โ”‚ โ””โ”€โ”€ summaries/ # AI summaries -โ””โ”€โ”€ video-analyzer # CLI script -``` - -## ๐Ÿ“ Supported Formats - -| Type | Formats | -|------|---------| -| Video | .mp4, .mkv, .avi, .mov, .webm, .flv | -| Audio | .mp3, .wav, .m4a, .flac, .aac, .ogg | -| Document | .pdf, .docx, .pptx, .txt, .md | -| Image (OCR) | .png, .jpg, .jpeg, .gif, .bmp | - -## ๐Ÿ› ๏ธ Development Status - -- [x] **Phase 1:** Video downloading + transcription -- [x] **Phase 2:** Document processing + AI summarization -- [ ] **Phase 3:** Knowledge base + vector search -- [ ] **Phase 4:** Virtual mentor + RAG chat -- [ ] **Phase 5:** Web UI + polish - -## ๐Ÿ“œ License - -MIT - Free for personal and commercial use +--- +title: Video Analyzer +emoji: "๐ŸŽฌ" +colorFrom: blue +colorTo: purple +sdk: gradio +sdk_version: "6.2.0" +app_file: app.py +pinned: false +--- + +# Video Analyzer + +A Gradio application. diff --git a/VOICE_COMMANDS_PLAN.md b/VOICE_COMMANDS_PLAN.md deleted file mode 100644 index 3930b4c73f851040907252960733cef87c61e755..0000000000000000000000000000000000000000 --- a/VOICE_COMMANDS_PLAN.md +++ /dev/null @@ -1,323 +0,0 @@ -# Voice Commands Plan - 100% Local & Private - -## BLUF (Bottom Line Up Front) - -**Add voice control to video_analyzer using Whisper (STT) + Piper (TTS) - both run entirely on your machine. No audio leaves your computer. No voice fingerprinting. No cloud APIs.** - ---- - -## ELI5 (Explain Like I'm 5) - -| What | How | Privacy | -|------|-----|---------| -| **You speak** | Microphone โ†’ Whisper (already in project!) | Audio never leaves your PC | -| **App understands** | Whisper converts speech โ†’ text command | All processing is local | -| **App responds** | Piper TTS converts text โ†’ speech | No voice profile created | -| **Loop** | Wake word โ†’ listen โ†’ execute โ†’ respond | 100% offline capable | - -**Why this is private:** -- Whisper runs locally - OpenAI never sees your voice -- Piper TTS runs locally - no cloud synthesis -- No internet required after initial setup -- Your voice patterns stay on YOUR machine - ---- - -## Tech Stack (All Free & Local) - -| Component | Technology | Why This One | -|-----------|------------|--------------| -| **Speech-to-Text** | Whisper (faster-whisper) | Already in project! Fast, accurate, local | -| **Text-to-Speech** | Piper TTS | Fast, natural voices, 100% local, tiny models | -| **Wake Word** | Porcupine (free tier) or OpenWakeWord | Local detection, low CPU | -| **Audio Capture** | sounddevice + numpy | Cross-platform, real-time | -| **Command Parser** | Simple pattern matching โ†’ Ollama for complex | Start simple, add AI later | - -### Alternative TTS Options - -| TTS Engine | Quality | Speed | Size | Notes | -|------------|---------|-------|------|-------| -| **Piper** โญ | Great | โšกโšกโšก | 20-60MB | Best balance, recommended | -| Coqui TTS | Excellent | โšกโšก | 200MB+ | More natural, heavier | -| espeak-ng | Basic | โšกโšกโšกโšก | 5MB | Robotic but lightweight | -| Bark | Amazing | โšก | 5GB+ | Too heavy for real-time | - ---- - -## Architecture - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ VOICE COMMAND SYSTEM (100% Local) โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ Microphone โ”‚ โ”‚ Wake Word โ”‚ โ”‚ Command Parser โ”‚ โ”‚ -โ”‚ โ”‚ (sounddev) โ”‚ โ”€โ”€โ–ถ โ”‚ (Porcupine) โ”‚ โ”€โ”€โ–ถ โ”‚ (pattern/Ollama)โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ–ผ โ–ผ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ Whisper โ”‚ โ”‚ Execute Command โ”‚ โ”‚ -โ”‚ โ”‚ (STT) โ”‚ โ”‚ (existing CLI) โ”‚ โ”‚ -โ”‚ โ”‚ LOCAL โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ -โ”‚ โ–ผ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Piper TTS โ”‚ โ”‚ -โ”‚ โ”‚ Speaker โ”‚ โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ (local) โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”‚ -โ”‚ ๐Ÿ”’ ALL PROCESSING ON LOCAL MACHINE - NOTHING SENT TO CLOUD ๐Ÿ”’ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - ---- - -## Voice Command Flow - -``` -1. IDLE STATE - โ””โ”€โ–ถ Listening for wake word ("Hey Analyzer" / "Computer") - -2. WAKE WORD DETECTED - โ””โ”€โ–ถ Play acknowledgment sound - โ””โ”€โ–ถ Start recording user speech - -3. USER SPEAKS COMMAND - โ””โ”€โ–ถ "Summarize my latest video" - โ””โ”€โ–ถ Silence detection โ†’ stop recording - -4. SPEECH-TO-TEXT (Whisper) - โ””โ”€โ–ถ Audio โ†’ "summarize my latest video" - -5. COMMAND PARSING - โ””โ”€โ–ถ Match to CLI command: `./video-analyzer summarize --latest` - โ””โ”€โ–ถ For complex queries โ†’ use Ollama to interpret - -6. EXECUTE & RESPOND - โ””โ”€โ–ถ Run command - โ””โ”€โ–ถ Get result text - โ””โ”€โ–ถ Piper TTS โ†’ Speak result - -7. RETURN TO IDLE -``` - ---- - -## Supported Voice Commands (Examples) - -| Voice Command | Maps To | Category | -|---------------|---------|----------| -| "What's my status" | `./video-analyzer status` | Info | -| "Summarize latest video" | `./video-analyzer summarize --latest` | Processing | -| "Add files from downloads" | `./video-analyzer add ~/Downloads` | Content | -| "Process all videos" | `./video-analyzer process-all` | Processing | -| "Search for cap rate" | `./video-analyzer search "cap rate"` | Knowledge | -| "Start chat mode" | `./video-analyzer chat` | Interactive | -| "What did I learn about negotiation" | RAG query via Ollama | Q&A | - ---- - -## Project Structure (New Files) - -``` -src/ -โ”œโ”€โ”€ voice/ # NEW MODULE -โ”‚ โ”œโ”€โ”€ __init__.py -โ”‚ โ”œโ”€โ”€ listener.py # Microphone capture + wake word -โ”‚ โ”œโ”€โ”€ stt.py # Whisper wrapper for real-time -โ”‚ โ”œโ”€โ”€ tts.py # Piper TTS wrapper -โ”‚ โ”œโ”€โ”€ commands.py # Command pattern matching -โ”‚ โ””โ”€โ”€ assistant.py # Main voice assistant loop -โ”‚ -โ”œโ”€โ”€ processors/ -โ”‚ โ””โ”€โ”€ transcriber.py # Already exists - reuse for STT -``` - ---- - -## Implementation Phases - -### Phase 1: Basic TTS (Speak Responses) โ€” 2-3 hours -- [ ] Install Piper TTS -- [ ] Create `src/voice/tts.py` -- [ ] Add `--speak` flag to CLI commands -- [ ] Test: `./video-analyzer status --speak` - -### Phase 2: Real-time STT (Hear Commands) โ€” 3-4 hours -- [ ] Install sounddevice for audio capture -- [ ] Create `src/voice/stt.py` (wrap existing Whisper) -- [ ] Implement silence detection (stop recording) -- [ ] Test: record โ†’ transcribe โ†’ print - -### Phase 3: Command Parsing โ€” 2-3 hours -- [ ] Create `src/voice/commands.py` -- [ ] Pattern matching for simple commands -- [ ] Ollama fallback for complex/natural queries -- [ ] Map voice โ†’ CLI commands - -### Phase 4: Wake Word Detection โ€” 2-3 hours -- [ ] Choose: Porcupine (easier) or OpenWakeWord (more private) -- [ ] Create `src/voice/listener.py` -- [ ] Continuous low-power listening -- [ ] Wake โ†’ record โ†’ process cycle - -### Phase 5: Voice Assistant Loop โ€” 2-3 hours -- [ ] Create `src/voice/assistant.py` -- [ ] Full loop: wake โ†’ listen โ†’ parse โ†’ execute โ†’ speak -- [ ] Add `./video-analyzer voice` command -- [ ] Handle errors gracefully with voice feedback - -### Phase 6: Polish โ€” 2-3 hours -- [ ] Acknowledgment sounds (beeps/chimes) -- [ ] Voice feedback for long operations ("Processing, please wait...") -- [ ] Configurable wake word -- [ ] Voice selection for TTS - ---- - -## Dependencies to Add - -```txt -# Voice Commands - requirements.txt additions - -# Audio capture -sounddevice>=0.4.6 -numpy>=1.24.0 - -# Text-to-Speech (local) -piper-tts>=1.2.0 -# Alternative: TTS>=0.22.0 # Coqui TTS - -# Wake Word Detection (choose one) -pvporcupine>=3.0.0 # Easier setup, free tier -# openwakeword>=0.5.0 # Fully open source - -# Voice Activity Detection -webrtcvad>=2.0.10 -``` - ---- - -## System Dependencies - -```bash -# Ubuntu/Debian -sudo apt install portaudio19-dev python3-pyaudio - -# For Piper TTS voices (download once) -mkdir -p ~/.local/share/piper -cd ~/.local/share/piper -wget https://github.com/rhasspy/piper/releases/download/v1.2.0/voice-en_US-lessac-medium.onnx.json -wget https://github.com/rhasspy/piper/releases/download/v1.2.0/voice-en_US-lessac-medium.onnx -``` - ---- - -## Privacy Guarantees - -### What NEVER Leaves Your Machine -- โŒ Raw audio recordings -- โŒ Voice patterns/fingerprints -- โŒ Transcribed text -- โŒ Commands you speak -- โŒ Any biometric data - -### What Stays 100% Local -- โœ… Whisper model runs locally -- โœ… Piper TTS runs locally -- โœ… Wake word detection runs locally -- โœ… All audio processing is local -- โœ… Works completely offline after setup - -### Compared to Cloud Alternatives - -| Cloud Service | What They Collect | Our Approach | -|---------------|-------------------|--------------| -| Alexa/Siri | Voice recordings, patterns | Nothing - all local | -| Google Assistant | Voice data, usage patterns | Nothing - all local | -| OpenAI Whisper API | Audio sent to cloud | Local Whisper - never sent | -| ElevenLabs | Voice for cloning | Local Piper - no upload | - ---- - -## Configuration Options - -```json -// config/voice.json -{ - "wake_word": "hey analyzer", - "stt_model": "base", // tiny/base/small/medium - "tts_voice": "en_US-lessac-medium", - "tts_speed": 1.0, - "silence_threshold": 0.5, // seconds of silence to stop - "confirmation_sounds": true, - "speak_responses": true, - "max_listen_time": 30 // seconds -} -``` - ---- - -## Example Usage - -```bash -# Start voice assistant mode -./video-analyzer voice - -# One-shot voice command -./video-analyzer voice --once - -# Status with spoken response -./video-analyzer status --speak - -# Process with voice feedback -./video-analyzer process-all --speak -``` - -### Voice Session Example - -``` -[System]: Listening for "Hey Analyzer"... -[You]: "Hey Analyzer" -[System]: *beep* "Yes?" -[You]: "What's my current status?" -[System]: "You have 12 videos transcribed, 8 documents processed, - and 47 items in your knowledge base. 3 videos are - pending transcription." -[System]: Listening for "Hey Analyzer"... -[You]: "Hey Analyzer" -[System]: *beep* "Yes?" -[You]: "Summarize the latest video about negotiation" -[System]: "Working on it... The latest video covers 5 key - negotiation tactics: anchoring, the flinch, - bracketing, nibbling, and the walk-away..." -``` - ---- - -## Why This Approach? - -| Requirement | Solution | -|-------------|----------| -| **No voice collection** | All STT via local Whisper | -| **No fingerprinting** | No cloud = no profile building | -| **Works offline** | Everything runs locally | -| **Fast response** | Piper TTS is <100ms latency | -| **Natural voices** | Piper neural voices sound great | -| **Low resources** | Base Whisper + Piper = ~500MB RAM | - ---- - -## Next Steps - -1. **Start with Phase 1** - Get TTS working first (instant gratification) -2. **Then Phase 2** - Add STT (reuse existing Whisper code) -3. **Phases 3-5** - Build up the full assistant -4. **Phase 6** - Polish and customize - -Ready to start implementing? Just say the word! ๐ŸŽค - - - diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..7f9e9b40d7b681e4026388f7652b403a2f5207b4 --- /dev/null +++ b/app.py @@ -0,0 +1,11 @@ +import gradio as gr + +demo = gr.Interface( + fn=lambda x: x, + inputs=gr.Textbox(label="Input"), + outputs=gr.Textbox(label="Output"), + title="Video Analyzer", +) + +if __name__ == "__main__": + demo.launch() diff --git a/data/audio/test_silence.wav b/data/audio/test_silence.wav deleted file mode 100644 index 6672e90f46661016eba11d7a1506cc79a3725315..0000000000000000000000000000000000000000 Binary files a/data/audio/test_silence.wav and /dev/null differ diff --git a/data/summaries/sample_real_estate_summary.md b/data/summaries/sample_real_estate_summary.md deleted file mode 100644 index 6b37e40389b8440827fb54c4f2aea1212a58c10d..0000000000000000000000000000000000000000 --- a/data/summaries/sample_real_estate_summary.md +++ /dev/null @@ -1 +0,0 @@ -Real estate investing success comes from: understanding your numbers, doing thorough due diligence, Negotiating, and avoiding common pitfalls. In the next module, we'll dive into deeper into strategies and how to structure deals for maximum returns. Real Estate Investment Fundamentals - Course Transcript is available in English and Spanish. For more information, visit the Real Estate Investing Course Transcripts website or click here for the English version. For the Spanish version, go to the Real estate Investment Course Transcript website or visit the Dutch version. \ No newline at end of file diff --git a/data/transcripts/sample_real_estate.txt b/data/transcripts/sample_real_estate.txt deleted file mode 100644 index 0fac89e1aef2df327c7b4fe36b37b03bdf75c6b3..0000000000000000000000000000000000000000 --- a/data/transcripts/sample_real_estate.txt +++ /dev/null @@ -1,98 +0,0 @@ -Real Estate Investment Fundamentals - Course Transcript - -Welcome to Module 1: Understanding Real Estate Investment Basics - -Today we're going to cover the fundamental concepts every real estate investor needs to know. Whether you're just starting out or looking to expand your portfolio, these principles will guide your decision-making. - -CASH FLOW ANALYSIS - -Cash flow is the lifeblood of any real estate investment. Simply put, it's the money left over after you've collected rent and paid all expenses. Here's the basic formula: - -Monthly Cash Flow = Gross Rent - Operating Expenses - Mortgage Payment - -Let's break this down with an example. Say you have a rental property that brings in $2,000 per month in rent. Your expenses include: -- Property taxes: $200/month -- Insurance: $100/month -- Maintenance reserve: $150/month -- Property management: $160/month (8% of rent) -- Vacancy allowance: $100/month (5%) - -Total operating expenses: $710/month -Mortgage payment: $900/month - -Cash flow = $2,000 - $710 - $900 = $390/month positive cash flow - -This is a healthy cash-flowing property! - -CAP RATE (CAPITALIZATION RATE) - -Cap rate helps you compare properties and determine value. It's calculated as: - -Cap Rate = Net Operating Income (NOI) / Property Value - -NOI is your annual income minus operating expenses (not including mortgage). Using our example: -- Annual gross rent: $24,000 -- Annual operating expenses: $8,520 -- NOI: $15,480 - -If the property is worth $200,000: -Cap Rate = $15,480 / $200,000 = 7.74% - -Generally, higher cap rates mean higher returns but often come with more risk. Markets like New York might have 4% cap rates while smaller cities might offer 8-10%. - -CASH-ON-CASH RETURN - -This metric tells you how hard your actual invested cash is working: - -Cash-on-Cash Return = Annual Cash Flow / Total Cash Invested - -If you put $50,000 down on our example property: -- Annual cash flow: $390 x 12 = $4,680 -- Cash-on-cash return: $4,680 / $50,000 = 9.36% - -That means you're earning 9.36% on your actual cash investment - much better than a savings account! - -THE 1% RULE - -A quick screening tool: the monthly rent should be at least 1% of the purchase price. For a $200,000 property, you'd want at least $2,000/month in rent. - -Our example property meets this rule: $2,000 / $200,000 = 1% - -NEGOTIATION STRATEGIES - -When making offers: -1. Always start below asking price - leave room to negotiate -2. Use inspection findings as leverage -3. Ask for seller concessions on closing costs -4. Be prepared to walk away - this is your strongest tool -5. Build rapport with the seller when possible - -DUE DILIGENCE CHECKLIST - -Before closing, verify: -- Rent rolls and actual income -- All operating expenses with documentation -- Property condition (get professional inspection) -- Comparable sales in the area -- Zoning and any restrictions -- Title search for liens or encumbrances - -COMMON MISTAKES TO AVOID - -1. Overestimating rental income -2. Underestimating repairs and maintenance -3. Not accounting for vacancy -4. Skipping proper inspections -5. Emotional decision-making -6. Over-leveraging (too much debt) - -SUMMARY - -Real estate investing success comes from: -- Understanding your numbers (cash flow, cap rate, CoC return) -- Doing thorough due diligence -- Negotiating effectively -- Avoiding common pitfalls -- Building for long-term wealth - -In the next module, we'll dive deeper into financing strategies and how to structure deals for maximum returns. diff --git a/hf_space/README.md b/hf_space/README.md deleted file mode 100644 index dfd2bcca8e73caaaf21bc4149d8e75d9dbe2f97c..0000000000000000000000000000000000000000 --- a/hf_space/README.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: Real Estate Mentor -emoji: ๐Ÿ  -colorFrom: blue -colorTo: green -sdk: gradio -sdk_version: 4.44.0 -app_file: app.py -pinned: false -license: mit ---- - -# ๐Ÿ  Real Estate Mentor - -Your AI-powered course assistant for real estate investing education. - -## Features - -- **๐Ÿ” Semantic Search** - Search your course content by meaning, not just keywords -- **๐Ÿ’ฌ Ask Questions** - Get answers based on your indexed materials -- **๐Ÿ“ค Easy Upload** - Add transcripts and notes with one click -- **๐Ÿ’พ Persistent Storage** - Your data is saved between sessions - -## How to Use - -1. **Upload Content** - Go to the Upload tab and add your course transcripts -2. **Search** - Use natural language to find relevant information -3. **Ask** - Chat with your AI mentor about the content - -## Tech Stack - -- **Gradio** - Web interface -- **ChromaDB** - Vector database for semantic search -- **Sentence Transformers** - Text embeddings -- **100% Free** - Runs entirely on HuggingFace Spaces - -## Privacy - -Your uploaded content is stored in this Space's persistent storage. No data is sent to external services. diff --git a/hf_space/app.py b/hf_space/app.py deleted file mode 100644 index 8c13562ce78f3e0bb7c2ae8e788308557043bacb..0000000000000000000000000000000000000000 --- a/hf_space/app.py +++ /dev/null @@ -1,413 +0,0 @@ -""" -Real Estate Mentor - HuggingFace Spaces App - -A semantic search and Q&A system for course content. -Upload transcripts, search by meaning, and get answers. -""" - -import os -import sys -from pathlib import Path - -# Add src to path for imports -sys.path.insert(0, str(Path(__file__).parent)) - -import gradio as gr - -# Set up persistent storage paths for HF Spaces -DATA_DIR = Path(os.getenv("PERSISTENT_DIR", "/data" if os.path.exists("/data") else "./data")) -CHROMA_DIR = DATA_DIR / "chromadb" -TRANSCRIPTS_DIR = DATA_DIR / "transcripts" - -# Ensure directories exist -for d in [CHROMA_DIR, TRANSCRIPTS_DIR]: - d.mkdir(parents=True, exist_ok=True) - -print(f"Data directory: {DATA_DIR}") -print(f"ChromaDB directory: {CHROMA_DIR}") - - -# ============== KNOWLEDGE BASE ============== - -class SimpleKnowledgeBase: - """Simplified knowledge base for HF Spaces.""" - - def __init__(self): - self._client = None - self._collection = None - self._model = None - - def _init(self): - if self._client is not None: - return - - import chromadb - from chromadb.config import Settings - from sentence_transformers import SentenceTransformer - - # Initialize ChromaDB - self._client = chromadb.PersistentClient( - path=str(CHROMA_DIR), - settings=Settings(anonymized_telemetry=False) - ) - self._collection = self._client.get_or_create_collection( - name="real_estate_mentor", - metadata={"hnsw:space": "cosine"} - ) - - # Initialize embedding model - self._model = SentenceTransformer("all-MiniLM-L6-v2") - - print(f"Knowledge base initialized: {self._collection.count()} documents") - - def add_text(self, text: str, source: str, chunk_size: int = 500): - """Add text to the knowledge base in chunks.""" - self._init() - - # Simple chunking by sentences/paragraphs - chunks = self._chunk_text(text, chunk_size) - - if not chunks: - return 0 - - # Generate embeddings - embeddings = self._model.encode(chunks).tolist() - - # Generate IDs - import hashlib - ids = [ - hashlib.md5(f"{source}:{i}:{c[:50]}".encode()).hexdigest() - for i, c in enumerate(chunks) - ] - - # Add to collection - self._collection.add( - ids=ids, - embeddings=embeddings, - documents=chunks, - metadatas=[{"source": source, "chunk_idx": i} for i in range(len(chunks))] - ) - - return len(chunks) - - def _chunk_text(self, text: str, chunk_size: int = 500) -> list[str]: - """Split text into chunks.""" - if len(text) <= chunk_size: - return [text] if text.strip() else [] - - chunks = [] - paragraphs = text.split("\n\n") - current_chunk = "" - - for para in paragraphs: - if len(current_chunk) + len(para) <= chunk_size: - current_chunk += para + "\n\n" - else: - if current_chunk.strip(): - chunks.append(current_chunk.strip()) - current_chunk = para + "\n\n" - - if current_chunk.strip(): - chunks.append(current_chunk.strip()) - - return chunks - - def search(self, query: str, n_results: int = 5) -> list[dict]: - """Search the knowledge base.""" - self._init() - - if self._collection.count() == 0: - return [] - - # Generate query embedding - query_embedding = self._model.encode(query).tolist() - - # Search - results = self._collection.query( - query_embeddings=[query_embedding], - n_results=min(n_results, self._collection.count()), - include=["documents", "metadatas", "distances"] - ) - - # Format results - output = [] - if results["documents"] and results["documents"][0]: - for i, doc in enumerate(results["documents"][0]): - meta = results["metadatas"][0][i] if results["metadatas"] else {} - dist = results["distances"][0][i] if results["distances"] else 0 - output.append({ - "text": doc, - "source": meta.get("source", "unknown"), - "score": 1 - dist # Convert distance to similarity - }) - - return output - - def count(self) -> int: - """Get document count.""" - self._init() - return self._collection.count() - - def get_sources(self) -> list[str]: - """Get all sources.""" - self._init() - results = self._collection.get(include=["metadatas"]) - sources = set() - if results["metadatas"]: - for meta in results["metadatas"]: - if "source" in meta: - sources.add(meta["source"]) - return sorted(sources) - - def clear(self): - """Clear the knowledge base.""" - self._init() - self._client.delete_collection("real_estate_mentor") - self._collection = self._client.create_collection( - name="real_estate_mentor", - metadata={"hnsw:space": "cosine"} - ) - - -# Global instance -kb = SimpleKnowledgeBase() - - -# ============== UI FUNCTIONS ============== - -def search_knowledge(query: str, n_results: int = 5) -> str: - """Search the knowledge base.""" - if not query.strip(): - return "โš ๏ธ Please enter a search query." - - try: - results = kb.search(query, n_results=int(n_results)) - - if not results: - return "๐Ÿ“ญ No results found. Upload some content first!" - - output = ["## ๐Ÿ” Search Results\n"] - for i, r in enumerate(results, 1): - source = Path(r["source"]).stem if r["source"] != "unknown" else "unknown" - score = r["score"] * 100 - text = r["text"][:400] + "..." if len(r["text"]) > 400 else r["text"] - - output.append(f"### Result {i} โ€” {score:.0f}% match") - output.append(f"๐Ÿ“„ **Source:** {source}\n") - output.append(f"```\n{text}\n```\n") - - return "\n".join(output) - - except Exception as e: - return f"โŒ Error: {str(e)}" - - -def upload_file(file, source_name: str) -> str: - """Process uploaded file.""" - if file is None: - return "โš ๏ธ Please select a file." - - try: - # Read content - with open(file.name, "r", encoding="utf-8", errors="ignore") as f: - content = f.read() - - if not content.strip(): - return "โš ๏ธ File is empty." - - # Use custom name or filename - name = source_name.strip() if source_name.strip() else Path(file.name).stem - - # Save locally - save_path = TRANSCRIPTS_DIR / f"{name}.txt" - save_path.write_text(content) - - # Index - chunks = kb.add_text(content, source=str(save_path)) - - return f"""โœ… **Successfully indexed!** - -- **Source:** {name} -- **Chunks created:** {chunks} -- **Characters:** {len(content):,} -""" - except Exception as e: - return f"โŒ Error: {str(e)}" - - -def upload_text(text: str, source_name: str) -> str: - """Process pasted text.""" - if not text.strip(): - return "โš ๏ธ Please enter some text." - if not source_name.strip(): - return "โš ๏ธ Please provide a source name." - - try: - # Save locally - save_path = TRANSCRIPTS_DIR / f"{source_name.strip()}.txt" - save_path.write_text(text) - - # Index - chunks = kb.add_text(text, source=str(save_path)) - - return f"""โœ… **Successfully indexed!** - -- **Source:** {source_name} -- **Chunks created:** {chunks} -- **Characters:** {len(text):,} -""" - except Exception as e: - return f"โŒ Error: {str(e)}" - - -def get_status() -> str: - """Get knowledge base status.""" - try: - count = kb.count() - sources = kb.get_sources() - - output = [f"## ๐Ÿ“Š Knowledge Base Status\n"] - output.append(f"**Total chunks:** {count}") - output.append(f"**Sources:** {len(sources)}\n") - - if sources: - output.append("### ๐Ÿ“ Indexed Sources:") - for s in sources[:15]: - name = Path(s).stem - output.append(f"- {name}") - if len(sources) > 15: - output.append(f"- *...and {len(sources) - 15} more*") - else: - output.append("*No content indexed yet. Upload some files to get started!*") - - return "\n".join(output) - except Exception as e: - return f"โŒ Error: {str(e)}" - - -def clear_all() -> str: - """Clear knowledge base.""" - try: - kb.clear() - return "โœ… Knowledge base cleared!" - except Exception as e: - return f"โŒ Error: {str(e)}" - - -def chat_respond(message: str, history: list) -> tuple: - """Respond to chat message using RAG.""" - if not message.strip(): - return "", history - - try: - # Search for context - results = kb.search(message, n_results=3) - - if not results: - response = "I don't have any relevant information yet. Please upload some course content first! ๐Ÿ“š" - else: - # Build response from context - sources = set() - context_parts = [] - - for r in results: - source = Path(r["source"]).stem if r["source"] != "unknown" else "unknown" - sources.add(source) - context_parts.append(r["text"]) - - context = "\n\n---\n\n".join(context_parts) - - response = f"""Based on your course materials: - -{context[:1500]}{"..." if len(context) > 1500 else ""} - ---- -๐Ÿ“š *Sources: {", ".join(sources)}*""" - - history.append((message, response)) - return "", history - - except Exception as e: - history.append((message, f"โŒ Error: {str(e)}")) - return "", history - - -# ============== BUILD APP ============== - -with gr.Blocks( - title="Real Estate Mentor", - theme=gr.themes.Soft() -) as demo: - - gr.Markdown(""" - # ๐Ÿ  Real Estate Mentor - - Your AI-powered course assistant. Upload transcripts, search semantically, and ask questions. - - --- - """) - - with gr.Tabs(): - # Search Tab - with gr.TabItem("๐Ÿ” Search"): - with gr.Row(): - with gr.Column(scale=4): - search_input = gr.Textbox( - label="Search Query", - placeholder="e.g., How do I calculate cash-on-cash return?", - lines=2 - ) - with gr.Column(scale=1): - n_results_slider = gr.Slider(1, 10, value=5, step=1, label="Results") - search_btn = gr.Button("๐Ÿ” Search", variant="primary") - search_output = gr.Markdown() - - search_btn.click(search_knowledge, [search_input, n_results_slider], search_output) - search_input.submit(search_knowledge, [search_input, n_results_slider], search_output) - - # Chat Tab - with gr.TabItem("๐Ÿ’ฌ Ask"): - chatbot = gr.Chatbot(height=400, label="Chat") - chat_input = gr.Textbox(label="Your Question", placeholder="Ask about your course content...") - chat_btn = gr.Button("๐Ÿ’ฌ Send", variant="primary") - - chat_btn.click(chat_respond, [chat_input, chatbot], [chat_input, chatbot]) - chat_input.submit(chat_respond, [chat_input, chatbot], [chat_input, chatbot]) - - # Upload Tab - with gr.TabItem("๐Ÿ“ค Upload"): - with gr.Row(): - with gr.Column(): - gr.Markdown("### Upload File") - file_input = gr.File(label="Select .txt or .md file", file_types=[".txt", ".md"]) - file_name = gr.Textbox(label="Custom Name (optional)", placeholder="e.g., Module 1") - file_btn = gr.Button("๐Ÿ“ค Upload", variant="primary") - file_output = gr.Markdown() - - file_btn.click(upload_file, [file_input, file_name], file_output) - - with gr.Column(): - gr.Markdown("### Paste Text") - text_input = gr.Textbox(label="Text Content", lines=8, placeholder="Paste transcript here...") - text_name = gr.Textbox(label="Source Name", placeholder="e.g., Video 1 Notes") - text_btn = gr.Button("๐Ÿ“ฅ Index", variant="primary") - text_output = gr.Markdown() - - text_btn.click(upload_text, [text_input, text_name], text_output) - - # Status Tab - with gr.TabItem("๐Ÿ“Š Status"): - status_output = gr.Markdown() - with gr.Row(): - refresh_btn = gr.Button("๐Ÿ”„ Refresh") - clear_btn = gr.Button("๐Ÿ—‘๏ธ Clear All", variant="stop") - - refresh_btn.click(get_status, outputs=status_output) - clear_btn.click(clear_all, outputs=status_output) - demo.load(get_status, outputs=status_output) - - gr.Markdown("---\n*Built with Gradio, ChromaDB & Sentence Transformers โ€ข 100% Free*") - - -if __name__ == "__main__": - demo.launch() diff --git a/hf_space/requirements.txt b/hf_space/requirements.txt deleted file mode 100644 index 1e59004529a3d2e1b0bab5956e48c1a19fca96ad..0000000000000000000000000000000000000000 --- a/hf_space/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -# HuggingFace Spaces Requirements -gradio>=4.0.0 -chromadb>=0.4.0 -sentence-transformers>=2.2.0 -torch>=2.0.0 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..cf4cba5d734192070918e35f8a5710965645689a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "video-analyzer" +version = "0.1.0" +description = "A Gradio application" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "gradio>=6.0.0", +] diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 95597ec54fafd303818f1d232455202c8b3cfe8f..0000000000000000000000000000000000000000 --- a/pytest.ini +++ /dev/null @@ -1,9 +0,0 @@ -[pytest] -testpaths = tests -python_files = test_*.py -python_classes = Test* -python_functions = test_* -addopts = -v --tb=short -filterwarnings = - ignore::DeprecationWarning - ignore::UserWarning diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index a86dc4ae63745bc4b504fdde982fda8ad471f00c..0000000000000000000000000000000000000000 --- a/requirements.txt +++ /dev/null @@ -1,44 +0,0 @@ -# Video Analyzer - Dependencies -# 100% Free & Open Source - -# Core -python-dotenv>=1.0.0 -typer[all]>=0.9.0 -rich>=13.0.0 -pydantic>=2.0.0 -pydantic-settings>=2.0.0 -tqdm>=4.66.0 - -# Video/Audio -yt-dlp>=2024.1.0 - -# Transcription -faster-whisper>=1.0.0 - -# Document Processing -PyMuPDF>=1.23.0 -python-docx>=1.0.0 -python-pptx>=0.6.23 - -# OCR (optional - requires system tesseract) -pytesseract>=0.3.10 -Pillow>=10.0.0 - -# AI/ML - Multiple options -transformers>=4.36.0 # Hugging Face models -torch>=2.0.0 # PyTorch backend -# ollama>=0.1.0 # Optional: Ollama client - -# Testing -pytest>=7.4.0 -pytest-cov>=4.1.0 - -# Phase 3: Knowledge Base -sentence-transformers>=2.2.0 -chromadb>=0.4.0 - -# Phase 5: Web UI -gradio>=4.0.0 - -# Web UI (Phase 5) -# gradio>=4.0.0 diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index a6a8494603dbc87eb2213b292b5bda3c53209a30..0000000000000000000000000000000000000000 --- a/src/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Video Analyzer - Download, transcribe, and learn from video content.""" - -__version__ = "0.1.0" diff --git a/src/__pycache__/__init__.cpython-312.pyc b/src/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 9a1bde4e7427bacb0b6166cc6f7e9fe7f0224c54..0000000000000000000000000000000000000000 Binary files a/src/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/src/__pycache__/config.cpython-312.pyc b/src/__pycache__/config.cpython-312.pyc deleted file mode 100644 index 964b26379403cbb65689434972468fe05a0dd600..0000000000000000000000000000000000000000 Binary files a/src/__pycache__/config.cpython-312.pyc and /dev/null differ diff --git a/src/__pycache__/main.cpython-312.pyc b/src/__pycache__/main.cpython-312.pyc deleted file mode 100644 index ba7ec41d7138aa347fd9655de1af33df38d5f77a..0000000000000000000000000000000000000000 Binary files a/src/__pycache__/main.cpython-312.pyc and /dev/null differ diff --git a/src/analyzers/__init__.py b/src/analyzers/__init__.py deleted file mode 100644 index 30a407d004e3b4ee2f5f9ce5ccf65901de87b359..0000000000000000000000000000000000000000 --- a/src/analyzers/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -"""AI analyzers for summarization and extraction.""" - -from .chunker import chunk_text, chunk_for_summarization, TextChunk -from .summarizer import Summarizer, summarize_file, Summary, OllamaClient -from .huggingface import ( - HuggingFaceLocal, - HuggingFaceAPI, - HuggingFaceTextGen, - summarize_with_huggingface, - list_recommended_models -) - -__all__ = [ - "chunk_text", - "chunk_for_summarization", - "TextChunk", - "Summarizer", - "summarize_file", - "Summary", - "OllamaClient", - "HuggingFaceLocal", - "HuggingFaceAPI", - "HuggingFaceTextGen", - "summarize_with_huggingface", - "list_recommended_models" -] diff --git a/src/analyzers/__pycache__/__init__.cpython-312.pyc b/src/analyzers/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index b9195987772881a2c73bf8c6ea209f49a040d7b4..0000000000000000000000000000000000000000 Binary files a/src/analyzers/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/src/analyzers/__pycache__/chunker.cpython-312.pyc b/src/analyzers/__pycache__/chunker.cpython-312.pyc deleted file mode 100644 index c09e040bbc84d363588ef7d4892ec14890bceb7d..0000000000000000000000000000000000000000 Binary files a/src/analyzers/__pycache__/chunker.cpython-312.pyc and /dev/null differ diff --git a/src/analyzers/__pycache__/huggingface.cpython-312.pyc b/src/analyzers/__pycache__/huggingface.cpython-312.pyc deleted file mode 100644 index d09e38050c2024520ad4eee835bdc55fa9d6f0e3..0000000000000000000000000000000000000000 Binary files a/src/analyzers/__pycache__/huggingface.cpython-312.pyc and /dev/null differ diff --git a/src/analyzers/__pycache__/summarizer.cpython-312.pyc b/src/analyzers/__pycache__/summarizer.cpython-312.pyc deleted file mode 100644 index 51a5046f325587407f3e930e1ddc9b3e14927dea..0000000000000000000000000000000000000000 Binary files a/src/analyzers/__pycache__/summarizer.cpython-312.pyc and /dev/null differ diff --git a/src/analyzers/chunker.py b/src/analyzers/chunker.py deleted file mode 100644 index 4fda44e65a34cdff5e8e679716391fb4f72fe419..0000000000000000000000000000000000000000 --- a/src/analyzers/chunker.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Text chunking for processing long documents with LLMs.""" - -from dataclasses import dataclass -from typing import Optional - - -@dataclass -class TextChunk: - """A chunk of text with metadata.""" - - text: str - index: int - start_char: int - end_char: int - - @property - def word_count(self) -> int: - return len(self.text.split()) - - -def chunk_text( - text: str, - chunk_size: int = 4000, - chunk_overlap: int = 200, - separator: str = "\n\n" -) -> list[TextChunk]: - """Split text into overlapping chunks. - - Args: - text: Text to split - chunk_size: Maximum characters per chunk - chunk_overlap: Characters to overlap between chunks - separator: Preferred split point (paragraphs, sentences, etc.) - - Returns: - List of TextChunk objects - """ - if len(text) <= chunk_size: - return [TextChunk(text=text, index=0, start_char=0, end_char=len(text))] - - chunks = [] - start = 0 - index = 0 - - while start < len(text): - # Find end of chunk - end = start + chunk_size - - if end >= len(text): - # Last chunk - chunk_text = text[start:] - chunks.append(TextChunk( - text=chunk_text, - index=index, - start_char=start, - end_char=len(text) - )) - break - - # Try to find a good break point - # Look for separator near the end of the chunk - search_start = max(start + chunk_size - 500, start) - search_end = min(start + chunk_size + 200, len(text)) - search_text = text[search_start:search_end] - - # Find last separator in search range - sep_pos = search_text.rfind(separator) - if sep_pos != -1: - end = search_start + sep_pos + len(separator) - else: - # Fall back to sentence end - for punct in [". ", "! ", "? ", "\n"]: - punct_pos = search_text.rfind(punct) - if punct_pos != -1: - end = search_start + punct_pos + len(punct) - break - - # Create chunk - chunk_text = text[start:end].strip() - if chunk_text: - chunks.append(TextChunk( - text=chunk_text, - index=index, - start_char=start, - end_char=end - )) - index += 1 - - # Move start with overlap - start = end - chunk_overlap - - return chunks - - -def chunk_for_summarization( - text: str, - max_tokens: int = 3000, - chars_per_token: float = 4.0 -) -> list[TextChunk]: - """Chunk text optimized for LLM summarization. - - Args: - text: Text to chunk - max_tokens: Maximum tokens per chunk (for LLM context) - chars_per_token: Approximate characters per token - - Returns: - List of TextChunk objects - """ - chunk_size = int(max_tokens * chars_per_token) - overlap = int(chunk_size * 0.05) # 5% overlap for context - - return chunk_text(text, chunk_size=chunk_size, chunk_overlap=overlap) - - -def estimate_tokens(text: str, chars_per_token: float = 4.0) -> int: - """Estimate number of tokens in text.""" - return int(len(text) / chars_per_token) diff --git a/src/analyzers/huggingface.py b/src/analyzers/huggingface.py deleted file mode 100644 index d8709d3ef782606ba3b4c50e34dd18a87c05aa85..0000000000000000000000000000000000000000 --- a/src/analyzers/huggingface.py +++ /dev/null @@ -1,407 +0,0 @@ -"""AI summarization using Hugging Face (local models or API).""" - -from dataclasses import dataclass -from pathlib import Path -from typing import Optional -import os - -from rich.console import Console -from rich.progress import Progress, SpinnerColumn, TextColumn - -from src.config import settings -from src.analyzers.chunker import chunk_for_summarization, estimate_tokens - -console = Console() - - -# Recommended models for different tasks -RECOMMENDED_MODELS = { - "summarization": { - "small": "facebook/bart-large-cnn", # Fast, good for news-style - "medium": "google/flan-t5-base", # Balanced - "large": "google/flan-t5-large", # Better quality - "best": "facebook/bart-large-xsum", # Abstractive summaries - }, - "text_generation": { - "small": "microsoft/phi-2", # 2.7B, very fast - "medium": "mistralai/Mistral-7B-Instruct-v0.2", # 7B, good quality - "large": "meta-llama/Llama-2-7b-chat-hf", # Requires access - } -} - - -class HuggingFaceLocal: - """Run Hugging Face models locally.""" - - def __init__( - self, - model_name: str = "facebook/bart-large-cnn", - device: str = "auto" - ): - self.model_name = model_name - self.device = device - self._model = None - self._tokenizer = None - - def _load_model(self): - """Lazy load the model.""" - if self._model is None: - console.print(f"[bold green]Loading model:[/] {self.model_name}") - console.print("[dim]This may take a few minutes on first run...[/]") - - try: - from transformers import AutoModelForSeq2SeqLM, AutoTokenizer, pipeline - import torch - except ImportError: - raise ImportError( - "Transformers not installed. Run:\n" - " pip install transformers torch" - ) - - # Determine device - if self.device == "auto": - device = 0 if torch.cuda.is_available() else -1 - else: - device = 0 if self.device == "cuda" else -1 - - # Load tokenizer and model - self._tokenizer = AutoTokenizer.from_pretrained(self.model_name) - - # Use pipeline for easier inference - self._pipeline = pipeline( - "summarization", - model=self.model_name, - tokenizer=self._tokenizer, - device=device - ) - - device_name = "GPU" if device >= 0 else "CPU" - console.print(f"[green]โœ“[/] Model loaded on {device_name}") - - def summarize( - self, - text: str, - max_length: int = 500, - min_length: int = 100 - ) -> str: - """Summarize text using local model. - - Args: - text: Text to summarize - max_length: Maximum summary length in tokens - min_length: Minimum summary length in tokens - - Returns: - Summary text - """ - self._load_model() - - # Handle long texts by chunking - tokens = estimate_tokens(text) - - if tokens > 1000: # BART/T5 have ~1024 token limit - return self._summarize_chunks(text, max_length, min_length) - - result = self._pipeline( - text, - max_length=max_length, - min_length=min_length, - do_sample=False - ) - - return result[0]["summary_text"] - - def _summarize_chunks( - self, - text: str, - max_length: int, - min_length: int - ) -> str: - """Summarize long text in chunks.""" - chunks = chunk_for_summarization(text, max_tokens=800) - console.print(f"[bold blue]Processing {len(chunks)} chunks...[/]") - - chunk_summaries = [] - - with Progress( - SpinnerColumn(), - TextColumn("[progress.description]{task.description}"), - console=console - ) as progress: - task = progress.add_task("Summarizing...", total=len(chunks)) - - for i, chunk in enumerate(chunks): - progress.update(task, description=f"Chunk {i+1}/{len(chunks)}") - - result = self._pipeline( - chunk.text, - max_length=max_length // len(chunks) + 50, - min_length=min_length // len(chunks), - do_sample=False - ) - chunk_summaries.append(result[0]["summary_text"]) - progress.advance(task) - - # Combine summaries - combined = " ".join(chunk_summaries) - - # If combined is still long, summarize again - if len(combined) > 2000: - console.print("[bold blue]Creating final summary...[/]") - result = self._pipeline( - combined, - max_length=max_length, - min_length=min_length, - do_sample=False - ) - return result[0]["summary_text"] - - return combined - - -class HuggingFaceAPI: - """Use Hugging Face Inference API (free tier available).""" - - def __init__( - self, - model_name: str = "facebook/bart-large-cnn", - api_key: Optional[str] = None - ): - self.model_name = model_name - # Check multiple sources for API key - self.api_key = ( - api_key or - os.getenv("HUGGINGFACE_API_KEY") or - os.getenv("VIDEO_ANALYZER_HUGGINGFACE_API_KEY") - ) - - # Also try loading from settings - if not self.api_key: - try: - from src.config import settings - self.api_key = settings.huggingface_api_key - except: - pass - - self.api_url = f"https://router.huggingface.co/hf-inference/models/{model_name}" - - def _check_api_key(self): - if not self.api_key: - raise ValueError( - "Hugging Face API key not found.\n" - "Set it via:\n" - " export HUGGINGFACE_API_KEY=your_key\n" - "Or get a free key at: https://huggingface.co/settings/tokens" - ) - - def summarize( - self, - text: str, - max_length: int = 500, - min_length: int = 100 - ) -> str: - """Summarize text using Hugging Face API. - - Args: - text: Text to summarize - max_length: Maximum summary length - min_length: Minimum summary length - - Returns: - Summary text - """ - self._check_api_key() - - try: - import requests - except ImportError: - raise ImportError("requests not installed. Run: pip install requests") - - headers = {"Authorization": f"Bearer {self.api_key}"} - - # Handle long texts - tokens = estimate_tokens(text) - if tokens > 1000: - return self._summarize_chunks_api(text, max_length, min_length, headers) - - payload = { - "inputs": text, - "parameters": { - "max_length": max_length, - "min_length": min_length, - "do_sample": False - } - } - - with console.status("[bold green]Calling Hugging Face API..."): - response = requests.post(self.api_url, headers=headers, json=payload) - - if response.status_code != 200: - error = response.json().get("error", response.text) - raise Exception(f"API error: {error}") - - result = response.json() - - if isinstance(result, list) and len(result) > 0: - return result[0].get("summary_text", str(result)) - - return str(result) - - def _summarize_chunks_api( - self, - text: str, - max_length: int, - min_length: int, - headers: dict - ) -> str: - """Summarize chunks via API.""" - import requests - - chunks = chunk_for_summarization(text, max_tokens=800) - console.print(f"[bold blue]Processing {len(chunks)} chunks via API...[/]") - - chunk_summaries = [] - - for i, chunk in enumerate(chunks): - console.print(f"[dim]Chunk {i+1}/{len(chunks)}...[/]") - - payload = { - "inputs": chunk.text, - "parameters": { - "max_length": max_length // len(chunks) + 50, - "min_length": min(30, min_length // len(chunks)), - "do_sample": False - } - } - - response = requests.post(self.api_url, headers=headers, json=payload) - - if response.status_code == 200: - result = response.json() - if isinstance(result, list) and len(result) > 0: - chunk_summaries.append(result[0].get("summary_text", "")) - - return " ".join(chunk_summaries) - - -class HuggingFaceTextGen: - """Use Hugging Face for text generation (like Ollama alternative).""" - - def __init__( - self, - model_name: str = "microsoft/phi-2", - device: str = "auto" - ): - self.model_name = model_name - self.device = device - self._pipeline = None - - def _load_model(self): - """Lazy load the model.""" - if self._pipeline is None: - console.print(f"[bold green]Loading model:[/] {self.model_name}") - console.print("[dim]This may download several GB on first run...[/]") - - try: - from transformers import pipeline - import torch - except ImportError: - raise ImportError( - "Transformers not installed. Run:\n" - " pip install transformers torch accelerate" - ) - - # Determine device - if self.device == "auto": - device = "cuda" if torch.cuda.is_available() else "cpu" - else: - device = self.device - - self._pipeline = pipeline( - "text-generation", - model=self.model_name, - device_map="auto" if device == "cuda" else None, - torch_dtype="auto" - ) - - console.print(f"[green]โœ“[/] Model loaded on {device}") - - def generate( - self, - prompt: str, - max_new_tokens: int = 500, - temperature: float = 0.7 - ) -> str: - """Generate text from prompt. - - Args: - prompt: Input prompt - max_new_tokens: Maximum tokens to generate - temperature: Creativity (0-1) - - Returns: - Generated text - """ - self._load_model() - - result = self._pipeline( - prompt, - max_new_tokens=max_new_tokens, - temperature=temperature, - do_sample=temperature > 0, - pad_token_id=self._pipeline.tokenizer.eos_token_id - ) - - generated = result[0]["generated_text"] - - # Remove the prompt from output - if generated.startswith(prompt): - generated = generated[len(prompt):].strip() - - return generated - - -def summarize_with_huggingface( - text: str, - model: str = "facebook/bart-large-cnn", - use_api: bool = False, - api_key: Optional[str] = None, - max_length: int = 500 -) -> str: - """Convenience function to summarize with Hugging Face. - - Args: - text: Text to summarize - model: Model name - use_api: If True, use API instead of local - api_key: API key (if using API) - max_length: Maximum summary length - - Returns: - Summary text - """ - if use_api: - client = HuggingFaceAPI(model, api_key) - else: - client = HuggingFaceLocal(model) - - return client.summarize(text, max_length=max_length) - - -def list_recommended_models(): - """Display recommended Hugging Face models.""" - from rich.table import Table - - table = Table(title="Recommended Hugging Face Models") - table.add_column("Task", style="cyan") - table.add_column("Size", style="white") - table.add_column("Model", style="green") - table.add_column("Notes", style="dim") - - table.add_row("Summarization", "Small", "facebook/bart-large-cnn", "Fast, news-style") - table.add_row("Summarization", "Medium", "google/flan-t5-base", "Balanced") - table.add_row("Summarization", "Large", "google/flan-t5-large", "Better quality") - table.add_row("Text Gen", "Small", "microsoft/phi-2", "2.7B, very fast") - table.add_row("Text Gen", "Medium", "mistralai/Mistral-7B-Instruct-v0.2", "7B, good") - - console.print(table) diff --git a/src/analyzers/summarizer.py b/src/analyzers/summarizer.py deleted file mode 100644 index 2e02dea6b486cb69c5cd22416217355f64e393e3..0000000000000000000000000000000000000000 --- a/src/analyzers/summarizer.py +++ /dev/null @@ -1,410 +0,0 @@ -"""AI-powered summarization using Ollama (local, free).""" - -import json -import subprocess -from dataclasses import dataclass -from pathlib import Path -from typing import Optional - -from rich.console import Console -from rich.progress import Progress, SpinnerColumn, TextColumn - -from src.config import settings -from src.analyzers.chunker import chunk_for_summarization, estimate_tokens - -console = Console() - - -@dataclass -class Summary: - """A generated summary.""" - - text: str - source_path: Optional[Path] - model: str - summary_type: str # quick, detailed, study_notes - original_length: int - summary_length: int - - @property - def compression_ratio(self) -> float: - """How much the text was compressed.""" - if self.original_length == 0: - return 0 - return self.summary_length / self.original_length - - def save(self, output_path: Optional[Path] = None) -> Path: - """Save summary to file.""" - if output_path is None: - stem = self.source_path.stem if self.source_path else "summary" - output_path = settings.summaries_dir / f"{stem}_{self.summary_type}.md" - - output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_text(self.text) - return output_path - - -# Prompts for different summary types -PROMPTS = { - "quick": """Summarize the following text in 2-3 paragraphs. Focus on the main points and key takeaways. - -TEXT: -{text} - -SUMMARY:""", - - "detailed": """Create a detailed summary of the following text. Include: -- Main topics covered -- Key points and concepts -- Important details and examples -- Actionable insights - -TEXT: -{text} - -DETAILED SUMMARY:""", - - "study_notes": """Create comprehensive study notes from the following text. Format as: - -## Key Concepts -- List main concepts with brief explanations - -## Important Points -- Bullet points of critical information - -## Definitions -- Any important terms defined - -## Action Items -- Practical steps or strategies mentioned - -## Summary -- Brief overall summary - -TEXT: -{text} - -STUDY NOTES:""", - - "real_estate": """You are a real estate expert. Analyze the following content and extract: - -## Key Real Estate Concepts -- Investment strategies mentioned -- Market analysis techniques -- Deal evaluation methods - -## Financial Metrics -- ROI, Cap Rate, Cash-on-Cash calculations if mentioned -- Financing strategies - -## Negotiation & Strategy -- Negotiation tactics -- Deal structuring advice - -## Action Items -- Practical steps to take - -## Critical Warnings -- Risks or pitfalls mentioned - -TEXT: -{text} - -REAL ESTATE ANALYSIS:""" -} - - -class OllamaClient: - """Client for interacting with Ollama.""" - - def __init__(self, model: str = "llama3"): - self.model = model - self._verified = False - - def is_available(self) -> bool: - """Check if Ollama is running.""" - try: - result = subprocess.run( - ["ollama", "list"], - capture_output=True, - text=True, - timeout=5 - ) - return result.returncode == 0 - except (subprocess.TimeoutExpired, FileNotFoundError): - return False - - def list_models(self) -> list[str]: - """List available Ollama models.""" - try: - result = subprocess.run( - ["ollama", "list"], - capture_output=True, - text=True, - timeout=10 - ) - if result.returncode != 0: - return [] - - models = [] - for line in result.stdout.strip().split("\n")[1:]: # Skip header - if line.strip(): - model_name = line.split()[0] - models.append(model_name) - return models - except Exception: - return [] - - def pull_model(self, model: Optional[str] = None) -> bool: - """Pull/download a model.""" - model = model or self.model - console.print(f"[bold green]Pulling model:[/] {model}") - - try: - result = subprocess.run( - ["ollama", "pull", model], - capture_output=False, - timeout=600 # 10 minutes - ) - return result.returncode == 0 - except Exception as e: - console.print(f"[red]Error pulling model:[/] {e}") - return False - - def generate( - self, - prompt: str, - system: Optional[str] = None, - temperature: float = 0.7, - max_tokens: int = 2000 - ) -> str: - """Generate text using Ollama. - - Args: - prompt: The prompt to send - system: Optional system message - temperature: Creativity (0-1) - max_tokens: Maximum response length - - Returns: - Generated text - """ - # Build the request - request = { - "model": self.model, - "prompt": prompt, - "stream": False, - "options": { - "temperature": temperature, - "num_predict": max_tokens - } - } - - if system: - request["system"] = system - - try: - # Use ollama CLI with run command - full_prompt = prompt - if system: - full_prompt = f"System: {system}\n\n{prompt}" - - result = subprocess.run( - ["ollama", "run", self.model], - input=full_prompt, - capture_output=True, - text=True, - timeout=300 # 5 minutes - ) - - if result.returncode != 0: - raise Exception(f"Ollama error: {result.stderr}") - - return result.stdout.strip() - - except subprocess.TimeoutExpired: - raise Exception("Ollama request timed out") - except FileNotFoundError: - raise Exception( - "Ollama not found. Install it:\n" - " curl -fsSL https://ollama.com/install.sh | sh\n" - " ollama pull llama3" - ) - - -class Summarizer: - """Summarize text using Ollama.""" - - def __init__(self, model: str = "llama3"): - self.client = OllamaClient(model) - self.model = model - - def summarize( - self, - text: str, - summary_type: str = "detailed", - source_path: Optional[Path] = None - ) -> Summary: - """Summarize text using Ollama. - - Args: - text: Text to summarize - summary_type: Type of summary (quick, detailed, study_notes, real_estate) - source_path: Optional path to source file - - Returns: - Summary object - """ - # Check Ollama availability - if not self.client.is_available(): - raise Exception( - "Ollama is not running. Start it with:\n" - " ollama serve\n" - "Or install: curl -fsSL https://ollama.com/install.sh | sh" - ) - - # Check if model is available - models = self.client.list_models() - if self.model not in models and f"{self.model}:latest" not in models: - console.print(f"[yellow]Model {self.model} not found. Pulling...[/]") - self.client.pull_model() - - # Get prompt template - prompt_template = PROMPTS.get(summary_type, PROMPTS["detailed"]) - - # Check if text needs chunking - tokens = estimate_tokens(text) - - if tokens > 3000: - # Process in chunks - console.print(f"[bold blue]Text is long ({tokens} tokens). Processing in chunks...[/]") - return self._summarize_chunks(text, summary_type, source_path, prompt_template) - else: - # Process directly - return self._summarize_single(text, summary_type, source_path, prompt_template) - - def _summarize_single( - self, - text: str, - summary_type: str, - source_path: Optional[Path], - prompt_template: str - ) -> Summary: - """Summarize a single chunk of text.""" - prompt = prompt_template.format(text=text) - - with Progress( - SpinnerColumn(), - TextColumn("[progress.description]{task.description}"), - console=console - ) as progress: - progress.add_task(f"Generating {summary_type} summary...", total=None) - - response = self.client.generate(prompt) - - console.print(f"[green]โœ“[/] Summary generated") - - return Summary( - text=response, - source_path=source_path, - model=self.model, - summary_type=summary_type, - original_length=len(text), - summary_length=len(response) - ) - - def _summarize_chunks( - self, - text: str, - summary_type: str, - source_path: Optional[Path], - prompt_template: str - ) -> Summary: - """Summarize text in chunks, then combine.""" - chunks = chunk_for_summarization(text) - console.print(f"[bold blue]Split into {len(chunks)} chunks[/]") - - chunk_summaries = [] - - with Progress( - SpinnerColumn(), - TextColumn("[progress.description]{task.description}"), - console=console - ) as progress: - task = progress.add_task("Processing chunks...", total=len(chunks)) - - for i, chunk in enumerate(chunks): - progress.update(task, description=f"Processing chunk {i+1}/{len(chunks)}...") - - prompt = prompt_template.format(text=chunk.text) - response = self.client.generate(prompt) - chunk_summaries.append(response) - - progress.advance(task) - - # Combine chunk summaries - if len(chunk_summaries) > 1: - console.print("[bold blue]Combining chunk summaries...[/]") - - combined_text = "\n\n---\n\n".join(chunk_summaries) - - combine_prompt = f"""Combine these summaries into one coherent {summary_type} summary. -Remove redundancy and organize the information clearly. - -SUMMARIES: -{combined_text} - -COMBINED SUMMARY:""" - - final_response = self.client.generate(combine_prompt) - else: - final_response = chunk_summaries[0] - - console.print(f"[green]โœ“[/] Summary generated") - - return Summary( - text=final_response, - source_path=source_path, - model=self.model, - summary_type=summary_type, - original_length=len(text), - summary_length=len(final_response) - ) - - -def summarize_file( - path: Path, - summary_type: str = "detailed", - model: str = "llama3", - output_dir: Optional[Path] = None -) -> Summary: - """Summarize a transcript or document file. - - Args: - path: Path to text file - summary_type: Type of summary - model: Ollama model to use - output_dir: Output directory for summary - - Returns: - Summary object - """ - path = Path(path) - output_dir = output_dir or settings.summaries_dir - - if not path.exists(): - raise FileNotFoundError(f"File not found: {path}") - - console.print(f"[bold green]Summarizing:[/] {path.name}") - - text = path.read_text(encoding="utf-8", errors="ignore") - - summarizer = Summarizer(model=model) - summary = summarizer.summarize(text, summary_type=summary_type, source_path=path) - - # Save summary - output_path = output_dir / f"{path.stem}_{summary_type}.md" - summary.save(output_path) - console.print(f"[green]โœ“[/] Saved: {output_path}") - - return summary diff --git a/src/config.py b/src/config.py deleted file mode 100644 index 979b88b4ad1a34c24061f542e62dd7d5d083a3c4..0000000000000000000000000000000000000000 --- a/src/config.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Configuration management for Video Analyzer.""" - -from pathlib import Path -from typing import Optional -from pydantic_settings import BaseSettings, SettingsConfigDict - - -class Settings(BaseSettings): - """Application settings.""" - - # Paths - base_dir: Path = Path(__file__).parent.parent - data_dir: Path = base_dir / "data" - downloads_dir: Path = data_dir / "downloads" - audio_dir: Path = data_dir / "audio" - transcripts_dir: Path = data_dir / "transcripts" - summaries_dir: Path = data_dir / "summaries" - - # yt-dlp settings - video_format: str = "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" - audio_format: str = "bestaudio[ext=m4a]/bestaudio/best" - - # Whisper settings - whisper_model: str = "base" # tiny, base, small, medium, large-v3 - whisper_device: str = "auto" # auto, cpu, cuda - - # AI settings - ai_backend: str = "huggingface" # ollama, huggingface, huggingface-api - huggingface_api_key: Optional[str] = None - ollama_model: str = "llama3" - huggingface_model: str = "facebook/bart-large-cnn" - - # Processing - max_concurrent_downloads: int = 3 - - model_config = SettingsConfigDict( - env_prefix="VIDEO_ANALYZER_", - env_file=".env", - env_file_encoding="utf-8", - extra="ignore" # Ignore extra env vars - ) - - -# Global settings instance -settings = Settings() - -# Ensure directories exist -for dir_path in [settings.downloads_dir, settings.audio_dir, - settings.transcripts_dir, settings.summaries_dir]: - dir_path.mkdir(parents=True, exist_ok=True) diff --git a/src/downloaders/__init__.py b/src/downloaders/__init__.py deleted file mode 100644 index 843cde80db68a2c16916300d6156e32ba5002cea..0000000000000000000000000000000000000000 --- a/src/downloaders/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Video downloaders and file handling.""" - -from .youtube import YouTubeDownloader -from .files import scan_files, import_files, FileInfo, get_file_type - -__all__ = ["YouTubeDownloader", "scan_files", "import_files", "FileInfo", "get_file_type"] diff --git a/src/downloaders/files.py b/src/downloaders/files.py deleted file mode 100644 index 615d941e877288c0f84201f80984e3936abc93a9..0000000000000000000000000000000000000000 --- a/src/downloaders/files.py +++ /dev/null @@ -1,177 +0,0 @@ -"""Direct file and folder processing support.""" - -import shutil -from dataclasses import dataclass -from pathlib import Path -from typing import Optional - -from rich.console import Console -from rich.table import Table - -from src.config import settings - -console = Console() - - -@dataclass -class FileInfo: - """Information about a local file.""" - - path: Path - name: str - size: int - file_type: str # video, audio, document, image - extension: str - - @property - def size_formatted(self) -> str: - """Return human-readable file size.""" - if self.size >= 1024 * 1024 * 1024: - return f"{self.size / (1024**3):.1f} GB" - elif self.size >= 1024 * 1024: - return f"{self.size / (1024**2):.1f} MB" - elif self.size >= 1024: - return f"{self.size / 1024:.1f} KB" - return f"{self.size} B" - - -# File type mappings -VIDEO_EXTENSIONS = {".mp4", ".mkv", ".avi", ".mov", ".webm", ".flv", ".wmv", ".m4v"} -AUDIO_EXTENSIONS = {".mp3", ".wav", ".m4a", ".flac", ".aac", ".ogg", ".wma"} -DOCUMENT_EXTENSIONS = {".pdf", ".docx", ".doc", ".pptx", ".ppt", ".txt", ".md", ".rtf"} -IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".webp"} - -ALL_SUPPORTED = VIDEO_EXTENSIONS | AUDIO_EXTENSIONS | DOCUMENT_EXTENSIONS | IMAGE_EXTENSIONS - - -def get_file_type(path: Path) -> str: - """Determine file type from extension.""" - ext = path.suffix.lower() - if ext in VIDEO_EXTENSIONS: - return "video" - elif ext in AUDIO_EXTENSIONS: - return "audio" - elif ext in DOCUMENT_EXTENSIONS: - return "document" - elif ext in IMAGE_EXTENSIONS: - return "image" - return "unknown" - - -def scan_files( - path: Path, - recursive: bool = True, - file_types: Optional[list[str]] = None -) -> list[FileInfo]: - """Scan a file or directory for supported files. - - Args: - path: File or directory path - recursive: If True, scan subdirectories - file_types: Filter by type - ['video', 'audio', 'document', 'image'] - - Returns: - List of FileInfo objects - """ - path = Path(path) - files = [] - - if path.is_file(): - # Single file - if path.suffix.lower() in ALL_SUPPORTED: - file_type = get_file_type(path) - if file_types is None or file_type in file_types: - files.append(FileInfo( - path=path, - name=path.name, - size=path.stat().st_size, - file_type=file_type, - extension=path.suffix.lower() - )) - elif path.is_dir(): - # Directory - pattern = "**/*" if recursive else "*" - for file_path in path.glob(pattern): - if file_path.is_file() and file_path.suffix.lower() in ALL_SUPPORTED: - file_type = get_file_type(file_path) - if file_types is None or file_type in file_types: - files.append(FileInfo( - path=file_path, - name=file_path.name, - size=file_path.stat().st_size, - file_type=file_type, - extension=file_path.suffix.lower() - )) - - # Sort by name - files.sort(key=lambda f: f.name.lower()) - return files - - -def import_files( - source: Path, - dest_dir: Optional[Path] = None, - copy: bool = True, - recursive: bool = True -) -> list[FileInfo]: - """Import files from a source location to the data directory. - - Args: - source: Source file or directory - dest_dir: Destination directory (default: data/downloads) - copy: If True, copy files. If False, move files. - recursive: If True, scan subdirectories - - Returns: - List of imported FileInfo objects - """ - source = Path(source) - dest_dir = dest_dir or settings.downloads_dir - dest_dir.mkdir(parents=True, exist_ok=True) - - files = scan_files(source, recursive=recursive) - imported = [] - - for file_info in files: - dest_path = dest_dir / file_info.name - - # Handle duplicates - if dest_path.exists(): - stem = dest_path.stem - suffix = dest_path.suffix - counter = 1 - while dest_path.exists(): - dest_path = dest_dir / f"{stem}_{counter}{suffix}" - counter += 1 - - # Copy or move - if copy: - shutil.copy2(file_info.path, dest_path) - console.print(f"[green]โœ“[/] Copied: {file_info.name}") - else: - shutil.move(file_info.path, dest_path) - console.print(f"[green]โœ“[/] Moved: {file_info.name}") - - imported.append(FileInfo( - path=dest_path, - name=dest_path.name, - size=file_info.size, - file_type=file_info.file_type, - extension=file_info.extension - )) - - return imported - - -def list_supported_formats(): - """Display all supported file formats.""" - table = Table(title="Supported File Formats") - table.add_column("Type", style="cyan") - table.add_column("Extensions", style="white") - - table.add_row("Video", ", ".join(sorted(VIDEO_EXTENSIONS))) - table.add_row("Audio", ", ".join(sorted(AUDIO_EXTENSIONS))) - table.add_row("Document", ", ".join(sorted(DOCUMENT_EXTENSIONS))) - table.add_row("Image (OCR)", ", ".join(sorted(IMAGE_EXTENSIONS))) - - console.print(table) diff --git a/src/downloaders/youtube.py b/src/downloaders/youtube.py deleted file mode 100644 index 2e5eec8ca6a71af2a149c39e654636219ccbabfd..0000000000000000000000000000000000000000 --- a/src/downloaders/youtube.py +++ /dev/null @@ -1,264 +0,0 @@ -"""YouTube video downloader using yt-dlp.""" - -import json -import subprocess -from dataclasses import dataclass, field -from pathlib import Path -from typing import Optional - -from rich.console import Console -from rich.progress import Progress, SpinnerColumn, TextColumn - -from src.config import settings - -console = Console() - -# Add local bin paths -import os -os.environ["PATH"] = os.environ.get("PATH", "") + ":/home/ubuntu/.local/bin:/home/ubuntu/.deno/bin" - - -@dataclass -class VideoInfo: - """Information about a downloaded video.""" - - id: str - title: str - description: str - duration: int # seconds - uploader: str - upload_date: str - url: str - filepath: Optional[Path] = None - audio_filepath: Optional[Path] = None - subtitles: Optional[str] = None - chapters: list = field(default_factory=list) - - @property - def duration_formatted(self) -> str: - """Return duration in HH:MM:SS format.""" - hours, remainder = divmod(self.duration, 3600) - minutes, seconds = divmod(remainder, 60) - if hours: - return f"{hours:02d}:{minutes:02d}:{seconds:02d}" - return f"{minutes:02d}:{seconds:02d}" - - -class YouTubeDownloader: - """Download videos from YouTube using yt-dlp.""" - - def __init__(self, output_dir: Optional[Path] = None, cookies_file: Optional[Path] = None): - self.output_dir = output_dir or settings.downloads_dir - self.output_dir.mkdir(parents=True, exist_ok=True) - self.cookies_file = cookies_file # Path to cookies.txt for authenticated downloads - - def get_info(self, url: str) -> VideoInfo: - """Get video information without downloading.""" - cmd = [ - "yt-dlp", - "--dump-json", - "--no-download", - ] - - # Add cookies if provided - if self.cookies_file and Path(self.cookies_file).exists(): - cmd.extend(["--cookies", str(self.cookies_file)]) - - cmd.append(url) - - result = subprocess.run(cmd, capture_output=True, text=True) - if result.returncode != 0: - error_msg = result.stderr - if "Sign in to confirm you're not a bot" in error_msg: - raise Exception( - "YouTube requires authentication. Please provide a cookies file:\n" - "1. Install a browser extension to export cookies (e.g., 'Get cookies.txt LOCALLY')\n" - "2. Export cookies from youtube.com\n" - "3. Use --cookies path/to/cookies.txt" - ) - raise Exception(f"Failed to get video info: {error_msg}") - - data = json.loads(result.stdout) - - return VideoInfo( - id=data.get("id", ""), - title=data.get("title", "Unknown"), - description=data.get("description", ""), - duration=data.get("duration", 0), - uploader=data.get("uploader", "Unknown"), - upload_date=data.get("upload_date", ""), - url=url, - chapters=data.get("chapters", []) - ) - - def download_video( - self, - url: str, - audio_only: bool = False, - get_subtitles: bool = True, - quality: str = "best" - ) -> VideoInfo: - """Download a video from YouTube. - - Args: - url: YouTube video URL - audio_only: If True, download only audio (faster for transcription) - get_subtitles: If True, download auto-generated subtitles if available - quality: Video quality - 'best', '1080p', '720p', '480p', 'audio' - - Returns: - VideoInfo with filepath set - """ - # Get video info first - with console.status("[bold green]Getting video info..."): - info = self.get_info(url) - - console.print(f"[bold blue]Title:[/] {info.title}") - console.print(f"[bold blue]Duration:[/] {info.duration_formatted}") - console.print(f"[bold blue]Uploader:[/] {info.uploader}") - - # Build output template - output_template = str(self.output_dir / "%(id)s.%(ext)s") - - # Build yt-dlp command - cmd = [ - "yt-dlp", - "--output", output_template, - "--no-playlist", # Download single video only - "--newline", # Progress on new lines - ] - - # Add cookies if provided - if self.cookies_file and Path(self.cookies_file).exists(): - cmd.extend(["--cookies", str(self.cookies_file)]) - - # Format selection - if audio_only: - cmd.extend([ - "-x", # Extract audio - "--audio-format", "mp3", - "--audio-quality", "0", # Best quality - ]) - else: - if quality == "best": - cmd.extend(["-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best"]) - elif quality == "1080p": - cmd.extend(["-f", "bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080]"]) - elif quality == "720p": - cmd.extend(["-f", "bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/best[height<=720]"]) - elif quality == "480p": - cmd.extend(["-f", "bestvideo[height<=480][ext=mp4]+bestaudio[ext=m4a]/best[height<=480]"]) - - # Subtitles - if get_subtitles: - cmd.extend([ - "--write-auto-sub", - "--sub-lang", "en", - "--sub-format", "srt/vtt/best", - "--convert-subs", "srt", - ]) - - cmd.append(url) - - # Download - console.print(f"\n[bold green]Downloading...[/]") - - result = subprocess.run(cmd, capture_output=True, text=True) - - if result.returncode != 0: - console.print(f"[red]Error:[/] {result.stderr}") - raise Exception(f"Download failed: {result.stderr}") - - # Find the downloaded file - if audio_only: - filepath = self.output_dir / f"{info.id}.mp3" - info.audio_filepath = filepath - else: - # Try common extensions - for ext in ["mp4", "webm", "mkv"]: - filepath = self.output_dir / f"{info.id}.{ext}" - if filepath.exists(): - break - info.filepath = filepath - - # Check for subtitles - subtitle_path = self.output_dir / f"{info.id}.en.srt" - if subtitle_path.exists(): - info.subtitles = subtitle_path.read_text() - console.print(f"[green]โœ“[/] Subtitles downloaded") - - console.print(f"[green]โœ“[/] Downloaded to: {filepath}") - - return info - - def download_playlist( - self, - url: str, - audio_only: bool = False, - max_videos: Optional[int] = None - ) -> list[VideoInfo]: - """Download all videos from a playlist. - - Args: - url: YouTube playlist URL - audio_only: If True, download only audio - max_videos: Maximum number of videos to download - - Returns: - List of VideoInfo objects - """ - # Get playlist info - cmd = [ - "yt-dlp", - "--dump-json", - "--flat-playlist", - url - ] - - result = subprocess.run(cmd, capture_output=True, text=True) - if result.returncode != 0: - raise Exception(f"Failed to get playlist info: {result.stderr}") - - # Parse each video entry - videos = [] - for line in result.stdout.strip().split("\n"): - if line: - data = json.loads(line) - video_url = f"https://www.youtube.com/watch?v={data['id']}" - videos.append(video_url) - - if max_videos: - videos = videos[:max_videos] - - console.print(f"[bold blue]Found {len(videos)} videos in playlist[/]") - - # Download each video - downloaded = [] - for i, video_url in enumerate(videos, 1): - console.print(f"\n[bold]Downloading {i}/{len(videos)}[/]") - try: - info = self.download_video(video_url, audio_only=audio_only) - downloaded.append(info) - except Exception as e: - console.print(f"[red]Failed to download:[/] {e}") - - return downloaded - - -def download_youtube( - url: str, - audio_only: bool = False, - output_dir: Optional[Path] = None -) -> VideoInfo: - """Convenience function to download a YouTube video. - - Args: - url: YouTube video URL - audio_only: If True, download only audio - output_dir: Output directory (default: data/downloads) - - Returns: - VideoInfo with download details - """ - downloader = YouTubeDownloader(output_dir) - return downloader.download_video(url, audio_only=audio_only) diff --git a/src/knowledge/__init__.py b/src/knowledge/__init__.py deleted file mode 100644 index 9aac8b763650a3c2ace278079262b95d38e03a82..0000000000000000000000000000000000000000 --- a/src/knowledge/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Knowledge base with vector storage.""" - -from .embeddings import EmbeddingModel, embed_text, embed_texts -from .vectorstore import KnowledgeBase, SearchResult, get_knowledge_base, search -from .indexer import index_text, index_file, index_directory, reindex_all - -__all__ = [ - "EmbeddingModel", - "embed_text", - "embed_texts", - "KnowledgeBase", - "SearchResult", - "get_knowledge_base", - "search", - "index_text", - "index_file", - "index_directory", - "reindex_all", -] diff --git a/src/knowledge/embeddings.py b/src/knowledge/embeddings.py deleted file mode 100644 index be67a1902b8bbd07ba14296fcd650573b2c1b900..0000000000000000000000000000000000000000 --- a/src/knowledge/embeddings.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Embedding generation using sentence-transformers (local, free).""" - -from typing import Optional -import numpy as np - -from rich.console import Console - -console = Console() - - -class EmbeddingModel: - """Generate embeddings using sentence-transformers.""" - - # Recommended models (all free, run locally) - MODELS = { - "fast": "all-MiniLM-L6-v2", # 384 dims, very fast - "balanced": "all-mpnet-base-v2", # 768 dims, good quality - "multilingual": "paraphrase-multilingual-MiniLM-L12-v2", # 384 dims - } - - def __init__(self, model_name: str = "all-MiniLM-L6-v2"): - """Initialize embedding model. - - Args: - model_name: Model name or key from MODELS dict - """ - # Allow shorthand names - if model_name in self.MODELS: - model_name = self.MODELS[model_name] - - self.model_name = model_name - self._model = None - - def _load_model(self): - """Lazy load the model.""" - if self._model is None: - console.print(f"[bold green]Loading embedding model:[/] {self.model_name}") - - try: - from sentence_transformers import SentenceTransformer - except ImportError: - raise ImportError( - "sentence-transformers not installed. Run:\n" - " pip install sentence-transformers" - ) - - self._model = SentenceTransformer(self.model_name) - console.print(f"[green]โœ“[/] Model loaded (dim={self._model.get_sentence_embedding_dimension()})") - - @property - def dimension(self) -> int: - """Get embedding dimension.""" - self._load_model() - return self._model.get_sentence_embedding_dimension() - - def embed(self, text: str) -> list[float]: - """Generate embedding for a single text. - - Args: - text: Text to embed - - Returns: - List of floats (embedding vector) - """ - self._load_model() - embedding = self._model.encode(text, convert_to_numpy=True) - return embedding.tolist() - - def embed_batch(self, texts: list[str], show_progress: bool = True) -> list[list[float]]: - """Generate embeddings for multiple texts. - - Args: - texts: List of texts to embed - show_progress: Show progress bar - - Returns: - List of embedding vectors - """ - self._load_model() - embeddings = self._model.encode( - texts, - convert_to_numpy=True, - show_progress_bar=show_progress - ) - return embeddings.tolist() - - -# Global instance for convenience -_default_model: Optional[EmbeddingModel] = None - - -def get_embedding_model(model_name: str = "all-MiniLM-L6-v2") -> EmbeddingModel: - """Get or create the default embedding model.""" - global _default_model - if _default_model is None or _default_model.model_name != model_name: - _default_model = EmbeddingModel(model_name) - return _default_model - - -def embed_text(text: str) -> list[float]: - """Convenience function to embed a single text.""" - return get_embedding_model().embed(text) - - -def embed_texts(texts: list[str]) -> list[list[float]]: - """Convenience function to embed multiple texts.""" - return get_embedding_model().embed_batch(texts) diff --git a/src/knowledge/indexer.py b/src/knowledge/indexer.py deleted file mode 100644 index 66fe4b300418ade8bfe39bfa0fe751226b415234..0000000000000000000000000000000000000000 --- a/src/knowledge/indexer.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Index content into the knowledge base.""" - -from pathlib import Path -from typing import Optional - -from rich.console import Console -from rich.progress import Progress, SpinnerColumn, TextColumn - -from src.config import settings -from src.analyzers.chunker import chunk_for_summarization -from src.knowledge.vectorstore import KnowledgeBase, get_knowledge_base - -console = Console() - - -def index_text( - text: str, - source: str, - kb: Optional[KnowledgeBase] = None, - chunk_size: int = 1000 -) -> int: - """Index a text into the knowledge base. - - Args: - text: Text content to index - source: Source identifier - kb: Knowledge base (uses default if None) - chunk_size: Characters per chunk - - Returns: - Number of chunks indexed - """ - kb = kb or get_knowledge_base() - - # Chunk the text - chunks = chunk_for_summarization(text, max_tokens=chunk_size // 4) - - if not chunks: - return 0 - - # Extract just the text from chunks - texts = [c.text for c in chunks] - metadatas = [{"start_char": c.start_char, "end_char": c.end_char} for c in chunks] - - # Add to knowledge base - kb.add_texts(texts, source=source, metadatas=metadatas) - - return len(chunks) - - -def index_file( - path: Path, - kb: Optional[KnowledgeBase] = None -) -> int: - """Index a file into the knowledge base. - - Args: - path: Path to text file - kb: Knowledge base (uses default if None) - - Returns: - Number of chunks indexed - """ - path = Path(path) - - if not path.exists(): - console.print(f"[red]File not found:[/] {path}") - return 0 - - text = path.read_text(encoding="utf-8", errors="ignore") - - if not text.strip(): - console.print(f"[yellow]Empty file:[/] {path.name}") - return 0 - - return index_text(text, source=str(path), kb=kb) - - -def index_directory( - path: Optional[Path] = None, - kb: Optional[KnowledgeBase] = None, - extensions: list[str] = [".txt", ".md"] -) -> dict: - """Index all text files in a directory. - - Args: - path: Directory path (defaults to transcripts_dir) - kb: Knowledge base - extensions: File extensions to index - - Returns: - Dict with stats {files: int, chunks: int} - """ - path = path or settings.transcripts_dir - path = Path(path) - kb = kb or get_knowledge_base() - - # Find all text files - files = [] - for ext in extensions: - files.extend(path.glob(f"*{ext}")) - - if not files: - console.print(f"[yellow]No files found in {path}[/]") - return {"files": 0, "chunks": 0} - - console.print(f"[bold blue]Indexing {len(files)} files...[/]") - - total_chunks = 0 - indexed_files = 0 - - for file_path in files: - try: - chunks = index_file(file_path, kb=kb) - if chunks > 0: - indexed_files += 1 - total_chunks += chunks - except Exception as e: - console.print(f"[red]Error indexing {file_path.name}:[/] {e}") - - console.print(f"[green]โœ“[/] Indexed {indexed_files} files, {total_chunks} chunks") - - return {"files": indexed_files, "chunks": total_chunks} - - -def reindex_all(kb: Optional[KnowledgeBase] = None) -> dict: - """Clear and reindex everything. - - Args: - kb: Knowledge base - - Returns: - Dict with stats - """ - kb = kb or get_knowledge_base() - - console.print("[bold yellow]Clearing existing index...[/]") - kb.clear() - - # Index transcripts - console.print("\n[bold blue]Indexing transcripts...[/]") - transcript_stats = index_directory(settings.transcripts_dir, kb=kb) - - # Index summaries - console.print("\n[bold blue]Indexing summaries...[/]") - summary_stats = index_directory(settings.summaries_dir, kb=kb, extensions=[".md", ".txt"]) - - return { - "files": transcript_stats["files"] + summary_stats["files"], - "chunks": transcript_stats["chunks"] + summary_stats["chunks"] - } diff --git a/src/knowledge/vectorstore.py b/src/knowledge/vectorstore.py deleted file mode 100644 index 1a7dbcbe988ba8b0f33270902b31c1c6aa580a52..0000000000000000000000000000000000000000 --- a/src/knowledge/vectorstore.py +++ /dev/null @@ -1,316 +0,0 @@ -"""Vector store using ChromaDB (local, free, persistent).""" - -import hashlib -import json -from dataclasses import dataclass -from pathlib import Path -from typing import Optional - -from rich.console import Console - -from src.config import settings - -console = Console() - - -@dataclass -class SearchResult: - """A search result from the knowledge base.""" - - text: str - source: str - score: float # Similarity score (higher = more similar) - metadata: dict - - @property - def source_name(self) -> str: - """Get just the filename from source path.""" - return Path(self.source).stem if self.source else "unknown" - - -class KnowledgeBase: - """Vector store for semantic search using ChromaDB.""" - - def __init__( - self, - persist_dir: Optional[Path] = None, - collection_name: str = "video_analyzer" - ): - """Initialize knowledge base. - - Args: - persist_dir: Directory for persistent storage - collection_name: Name of the ChromaDB collection - """ - self.persist_dir = persist_dir or (settings.data_dir / "chromadb") - self.collection_name = collection_name - self._client = None - self._collection = None - self._embedding_model = None - - def _init_db(self): - """Initialize ChromaDB client and collection.""" - if self._client is None: - try: - import chromadb - from chromadb.config import Settings as ChromaSettings - except ImportError: - raise ImportError( - "ChromaDB not installed. Run:\n" - " pip install chromadb" - ) - - # Create persistent client - self.persist_dir.mkdir(parents=True, exist_ok=True) - - self._client = chromadb.PersistentClient( - path=str(self.persist_dir), - settings=ChromaSettings(anonymized_telemetry=False) - ) - - # Get or create collection - self._collection = self._client.get_or_create_collection( - name=self.collection_name, - metadata={"description": "Video Analyzer Knowledge Base"} - ) - - console.print(f"[green]โœ“[/] Knowledge base loaded: {self._collection.count()} documents") - - def _get_embedding_model(self): - """Get the embedding model.""" - if self._embedding_model is None: - from src.knowledge.embeddings import EmbeddingModel - self._embedding_model = EmbeddingModel() - return self._embedding_model - - def _generate_id(self, text: str, source: str) -> str: - """Generate a unique ID for a document.""" - content = f"{source}:{text[:100]}" - return hashlib.md5(content.encode()).hexdigest() - - def add_text( - self, - text: str, - source: str, - metadata: Optional[dict] = None - ) -> str: - """Add a single text to the knowledge base. - - Args: - text: Text content - source: Source file path or identifier - metadata: Additional metadata - - Returns: - Document ID - """ - self._init_db() - - # Generate embedding - model = self._get_embedding_model() - embedding = model.embed(text) - - # Generate ID - doc_id = self._generate_id(text, source) - - # Prepare metadata - meta = metadata or {} - meta["source"] = source - meta["text_length"] = len(text) - - # Add to collection - self._collection.add( - ids=[doc_id], - embeddings=[embedding], - documents=[text], - metadatas=[meta] - ) - - return doc_id - - def add_texts( - self, - texts: list[str], - source: str, - metadatas: Optional[list[dict]] = None, - show_progress: bool = True - ) -> list[str]: - """Add multiple texts to the knowledge base. - - Args: - texts: List of text content - source: Source file path - metadatas: List of metadata dicts - show_progress: Show progress bar - - Returns: - List of document IDs - """ - self._init_db() - - if not texts: - return [] - - console.print(f"[bold blue]Indexing {len(texts)} chunks from {Path(source).name}[/]") - - # Generate embeddings in batch - model = self._get_embedding_model() - embeddings = model.embed_batch(texts, show_progress=show_progress) - - # Generate IDs and prepare metadata - ids = [] - metas = [] - for i, text in enumerate(texts): - doc_id = self._generate_id(text, f"{source}:{i}") - ids.append(doc_id) - - meta = metadatas[i] if metadatas else {} - meta["source"] = source - meta["chunk_index"] = i - meta["text_length"] = len(text) - metas.append(meta) - - # Add to collection - self._collection.add( - ids=ids, - embeddings=embeddings, - documents=texts, - metadatas=metas - ) - - console.print(f"[green]โœ“[/] Added {len(texts)} chunks to knowledge base") - - return ids - - def search( - self, - query: str, - n_results: int = 5, - filter_source: Optional[str] = None - ) -> list[SearchResult]: - """Search the knowledge base semantically. - - Args: - query: Search query - n_results: Number of results to return - filter_source: Filter by source file - - Returns: - List of SearchResult objects - """ - self._init_db() - - # Generate query embedding - model = self._get_embedding_model() - query_embedding = model.embed(query) - - # Build filter - where_filter = None - if filter_source: - where_filter = {"source": filter_source} - - # Search - results = self._collection.query( - query_embeddings=[query_embedding], - n_results=n_results, - where=where_filter, - include=["documents", "metadatas", "distances"] - ) - - # Convert to SearchResult objects - search_results = [] - if results["documents"] and results["documents"][0]: - for i, doc in enumerate(results["documents"][0]): - # Convert distance to similarity score (1 - distance for cosine) - distance = results["distances"][0][i] if results["distances"] else 0 - score = 1 - distance # Higher = more similar - - metadata = results["metadatas"][0][i] if results["metadatas"] else {} - source = metadata.pop("source", "unknown") - - search_results.append(SearchResult( - text=doc, - source=source, - score=score, - metadata=metadata - )) - - return search_results - - def count(self) -> int: - """Get total number of documents in the knowledge base.""" - self._init_db() - return self._collection.count() - - def get_sources(self) -> list[str]: - """Get list of all sources in the knowledge base.""" - self._init_db() - - # Get all metadata - results = self._collection.get(include=["metadatas"]) - - sources = set() - if results["metadatas"]: - for meta in results["metadatas"]: - if "source" in meta: - sources.add(meta["source"]) - - return sorted(sources) - - def delete_source(self, source: str) -> int: - """Delete all documents from a specific source. - - Args: - source: Source path to delete - - Returns: - Number of documents deleted - """ - self._init_db() - - # Get IDs for this source - results = self._collection.get( - where={"source": source}, - include=["metadatas"] - ) - - if not results["ids"]: - return 0 - - # Delete - count = len(results["ids"]) - self._collection.delete(ids=results["ids"]) - - console.print(f"[green]โœ“[/] Deleted {count} chunks from {source}") - - return count - - def clear(self): - """Clear all documents from the knowledge base.""" - self._init_db() - - # Delete and recreate collection - self._client.delete_collection(self.collection_name) - self._collection = self._client.create_collection( - name=self.collection_name, - metadata={"description": "Video Analyzer Knowledge Base"} - ) - - console.print("[green]โœ“[/] Knowledge base cleared") - - -# Convenience functions -_default_kb: Optional[KnowledgeBase] = None - - -def get_knowledge_base() -> KnowledgeBase: - """Get the default knowledge base instance.""" - global _default_kb - if _default_kb is None: - _default_kb = KnowledgeBase() - return _default_kb - - -def search(query: str, n_results: int = 5) -> list[SearchResult]: - """Search the knowledge base.""" - return get_knowledge_base().search(query, n_results) diff --git a/src/main.py b/src/main.py deleted file mode 100644 index 14f9f958123c3bd7db9083b6f00923f08fb50a78..0000000000000000000000000000000000000000 --- a/src/main.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Main entry point for Video Analyzer.""" - -from src.ui.cli import app - -if __name__ == "__main__": - app() diff --git a/src/mentor/__init__.py b/src/mentor/__init__.py deleted file mode 100644 index 7683312b65fc499418d34b44d7622b155a75eed5..0000000000000000000000000000000000000000 --- a/src/mentor/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Virtual mentor with RAG (Phase 4+).""" diff --git a/src/processors/__init__.py b/src/processors/__init__.py deleted file mode 100644 index ef48ad171e6fbc0189c0f0f47518fcbd06980e2d..0000000000000000000000000000000000000000 --- a/src/processors/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Content processors for audio, documents, and transcription.""" - -from .audio import extract_audio -from .transcriber import transcribe_audio, WhisperTranscriber -from .documents import extract_document, process_documents, DocumentContent -from .ocr import extract_text_from_image, process_images, OCRResult - -__all__ = [ - "extract_audio", - "transcribe_audio", - "WhisperTranscriber", - "extract_document", - "process_documents", - "DocumentContent", - "extract_text_from_image", - "process_images", - "OCRResult" -] diff --git a/src/processors/__pycache__/__init__.cpython-312.pyc b/src/processors/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 31ac78199a2e6ecf29d1952657510d7e62b8f984..0000000000000000000000000000000000000000 Binary files a/src/processors/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/src/processors/__pycache__/audio.cpython-312.pyc b/src/processors/__pycache__/audio.cpython-312.pyc deleted file mode 100644 index c11255fd99cbe5789cd1747e20c411efe739d87e..0000000000000000000000000000000000000000 Binary files a/src/processors/__pycache__/audio.cpython-312.pyc and /dev/null differ diff --git a/src/processors/__pycache__/transcriber.cpython-312.pyc b/src/processors/__pycache__/transcriber.cpython-312.pyc deleted file mode 100644 index ce32144cdd1983b17c77b62a753ab187a9eb08e9..0000000000000000000000000000000000000000 Binary files a/src/processors/__pycache__/transcriber.cpython-312.pyc and /dev/null differ diff --git a/src/processors/audio.py b/src/processors/audio.py deleted file mode 100644 index cd5d8a8cb7783eb6362bc2cb4151982f63938e30..0000000000000000000000000000000000000000 --- a/src/processors/audio.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Audio extraction from video files using ffmpeg.""" - -import subprocess -from pathlib import Path -from typing import Optional - -from rich.console import Console - -from src.config import settings - -console = Console() - - -def extract_audio( - video_path: Path, - output_path: Optional[Path] = None, - audio_format: str = "mp3", - sample_rate: int = 16000, # Whisper prefers 16kHz -) -> Path: - """Extract audio from a video file using ffmpeg. - - Args: - video_path: Path to the video file - output_path: Output path for audio file (default: data/audio/.mp3) - audio_format: Output audio format (mp3, wav, m4a) - sample_rate: Audio sample rate in Hz (16000 recommended for Whisper) - - Returns: - Path to the extracted audio file - """ - video_path = Path(video_path) - - if not video_path.exists(): - raise FileNotFoundError(f"Video not found: {video_path}") - - # Default output path - if output_path is None: - output_path = settings.audio_dir / f"{video_path.stem}.{audio_format}" - else: - output_path = Path(output_path) - - output_path.parent.mkdir(parents=True, exist_ok=True) - - console.print(f"[bold green]Extracting audio from:[/] {video_path.name}") - - # Build ffmpeg command - cmd = [ - "ffmpeg", - "-i", str(video_path), - "-vn", # No video - "-acodec", "libmp3lame" if audio_format == "mp3" else "pcm_s16le", - "-ar", str(sample_rate), # Sample rate - "-ac", "1", # Mono (better for speech recognition) - "-y", # Overwrite output - str(output_path) - ] - - result = subprocess.run(cmd, capture_output=True, text=True) - - if result.returncode != 0: - raise Exception(f"Audio extraction failed: {result.stderr}") - - console.print(f"[green]โœ“[/] Audio extracted to: {output_path}") - - return output_path - - -def get_audio_duration(audio_path: Path) -> float: - """Get the duration of an audio file in seconds.""" - cmd = [ - "ffprobe", - "-v", "error", - "-show_entries", "format=duration", - "-of", "default=noprint_wrappers=1:nokey=1", - str(audio_path) - ] - - result = subprocess.run(cmd, capture_output=True, text=True) - - if result.returncode != 0: - raise Exception(f"Failed to get audio duration: {result.stderr}") - - return float(result.stdout.strip()) diff --git a/src/processors/documents.py b/src/processors/documents.py deleted file mode 100644 index efe8f2069d2011902ccf41a1fe045f5c8680409a..0000000000000000000000000000000000000000 --- a/src/processors/documents.py +++ /dev/null @@ -1,278 +0,0 @@ -"""Document processing for PDF, Word, PowerPoint, and text files.""" - -from dataclasses import dataclass -from pathlib import Path -from typing import Optional - -from rich.console import Console - -from src.config import settings - -console = Console() - - -@dataclass -class DocumentContent: - """Extracted content from a document.""" - - text: str - title: str - pages: int - source_path: Path - doc_type: str # pdf, docx, pptx, txt, md - metadata: dict - - def save(self, output_path: Optional[Path] = None) -> Path: - """Save extracted text to file.""" - if output_path is None: - output_path = settings.transcripts_dir / f"{self.source_path.stem}.txt" - - output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_text(self.text) - return output_path - - -def extract_pdf(path: Path) -> DocumentContent: - """Extract text from a PDF file. - - Args: - path: Path to PDF file - - Returns: - DocumentContent with extracted text - """ - try: - import fitz # PyMuPDF - except ImportError: - raise ImportError("PyMuPDF not installed. Run: pip install PyMuPDF") - - path = Path(path) - console.print(f"[bold green]Extracting PDF:[/] {path.name}") - - doc = fitz.open(path) - - text_parts = [] - for page_num, page in enumerate(doc, 1): - text = page.get_text() - if text.strip(): - text_parts.append(f"--- Page {page_num} ---\n{text}") - - full_text = "\n\n".join(text_parts) - - # Extract metadata - metadata = { - "author": doc.metadata.get("author", ""), - "title": doc.metadata.get("title", ""), - "subject": doc.metadata.get("subject", ""), - "creator": doc.metadata.get("creator", ""), - } - - title = metadata.get("title") or path.stem - - doc.close() - - console.print(f"[green]โœ“[/] Extracted {len(doc)} pages, {len(full_text)} characters") - - return DocumentContent( - text=full_text, - title=title, - pages=len(doc), - source_path=path, - doc_type="pdf", - metadata=metadata - ) - - -def extract_docx(path: Path) -> DocumentContent: - """Extract text from a Word document. - - Args: - path: Path to .docx file - - Returns: - DocumentContent with extracted text - """ - try: - from docx import Document - except ImportError: - raise ImportError("python-docx not installed. Run: pip install python-docx") - - path = Path(path) - console.print(f"[bold green]Extracting Word doc:[/] {path.name}") - - doc = Document(path) - - text_parts = [] - for para in doc.paragraphs: - if para.text.strip(): - text_parts.append(para.text) - - # Also extract from tables - for table in doc.tables: - for row in table.rows: - row_text = " | ".join(cell.text.strip() for cell in row.cells if cell.text.strip()) - if row_text: - text_parts.append(row_text) - - full_text = "\n\n".join(text_parts) - - # Extract metadata - metadata = { - "author": doc.core_properties.author or "", - "title": doc.core_properties.title or "", - "subject": doc.core_properties.subject or "", - } - - title = metadata.get("title") or path.stem - - console.print(f"[green]โœ“[/] Extracted {len(text_parts)} paragraphs, {len(full_text)} characters") - - return DocumentContent( - text=full_text, - title=title, - pages=1, # Word docs don't have fixed pages - source_path=path, - doc_type="docx", - metadata=metadata - ) - - -def extract_pptx(path: Path) -> DocumentContent: - """Extract text from a PowerPoint presentation. - - Args: - path: Path to .pptx file - - Returns: - DocumentContent with extracted text - """ - try: - from pptx import Presentation - except ImportError: - raise ImportError("python-pptx not installed. Run: pip install python-pptx") - - path = Path(path) - console.print(f"[bold green]Extracting PowerPoint:[/] {path.name}") - - prs = Presentation(path) - - text_parts = [] - for slide_num, slide in enumerate(prs.slides, 1): - slide_text = [f"--- Slide {slide_num} ---"] - - for shape in slide.shapes: - if hasattr(shape, "text") and shape.text.strip(): - slide_text.append(shape.text) - - if len(slide_text) > 1: # Has content beyond header - text_parts.append("\n".join(slide_text)) - - full_text = "\n\n".join(text_parts) - - console.print(f"[green]โœ“[/] Extracted {len(prs.slides)} slides, {len(full_text)} characters") - - return DocumentContent( - text=full_text, - title=path.stem, - pages=len(prs.slides), - source_path=path, - doc_type="pptx", - metadata={} - ) - - -def extract_text_file(path: Path) -> DocumentContent: - """Extract text from plain text or markdown files. - - Args: - path: Path to .txt or .md file - - Returns: - DocumentContent with text - """ - path = Path(path) - console.print(f"[bold green]Reading text file:[/] {path.name}") - - text = path.read_text(encoding="utf-8", errors="ignore") - - console.print(f"[green]โœ“[/] Read {len(text)} characters") - - return DocumentContent( - text=text, - title=path.stem, - pages=1, - source_path=path, - doc_type=path.suffix.lstrip("."), - metadata={} - ) - - -def extract_document(path: Path) -> DocumentContent: - """Extract text from any supported document type. - - Args: - path: Path to document - - Returns: - DocumentContent with extracted text - """ - path = Path(path) - ext = path.suffix.lower() - - if ext == ".pdf": - return extract_pdf(path) - elif ext in {".docx", ".doc"}: - return extract_docx(path) - elif ext in {".pptx", ".ppt"}: - return extract_pptx(path) - elif ext in {".txt", ".md", ".rtf"}: - return extract_text_file(path) - else: - raise ValueError(f"Unsupported document type: {ext}") - - -def process_documents( - path: Path, - output_dir: Optional[Path] = None, - recursive: bool = True -) -> list[DocumentContent]: - """Process all documents in a file or directory. - - Args: - path: File or directory path - output_dir: Output directory for extracted text - recursive: If True, scan subdirectories - - Returns: - List of DocumentContent objects - """ - from src.downloaders.files import scan_files - - path = Path(path) - output_dir = output_dir or settings.transcripts_dir - - # Get document files - files = scan_files(path, recursive=recursive, file_types=["document"]) - - if not files: - console.print("[yellow]No documents found[/]") - return [] - - console.print(f"[bold blue]Found {len(files)} documents to process[/]") - - results = [] - for file_info in files: - try: - content = extract_document(file_info.path) - - # Save extracted text - output_path = output_dir / f"{file_info.path.stem}.txt" - content.save(output_path) - console.print(f"[green]โœ“[/] Saved: {output_path.name}") - - results.append(content) - - except Exception as e: - console.print(f"[red]Error processing {file_info.name}:[/] {e}") - - return results diff --git a/src/processors/ocr.py b/src/processors/ocr.py deleted file mode 100644 index f7a0a1d118b5acf83a7692847f609fda833b7029..0000000000000000000000000000000000000000 --- a/src/processors/ocr.py +++ /dev/null @@ -1,133 +0,0 @@ -"""OCR (Optical Character Recognition) for images using Tesseract.""" - -from dataclasses import dataclass -from pathlib import Path -from typing import Optional - -from rich.console import Console - -from src.config import settings - -console = Console() - - -@dataclass -class OCRResult: - """Result of OCR processing.""" - - text: str - source_path: Path - confidence: float # 0-100 - - def save(self, output_path: Optional[Path] = None) -> Path: - """Save extracted text to file.""" - if output_path is None: - output_path = settings.transcripts_dir / f"{self.source_path.stem}_ocr.txt" - - output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_text(self.text) - return output_path - - -def extract_text_from_image(path: Path, language: str = "eng") -> OCRResult: - """Extract text from an image using Tesseract OCR. - - Args: - path: Path to image file - language: Tesseract language code (eng, spa, fra, deu, etc.) - - Returns: - OCRResult with extracted text - """ - try: - import pytesseract - from PIL import Image - except ImportError: - raise ImportError( - "OCR dependencies not installed. Run:\n" - " pip install pytesseract Pillow\n" - " sudo apt install tesseract-ocr # Linux\n" - " brew install tesseract # macOS" - ) - - path = Path(path) - console.print(f"[bold green]OCR processing:[/] {path.name}") - - # Open and process image - image = Image.open(path) - - # Get OCR data with confidence scores - data = pytesseract.image_to_data(image, lang=language, output_type=pytesseract.Output.DICT) - - # Extract text and calculate average confidence - words = [] - confidences = [] - - for i, word in enumerate(data["text"]): - if word.strip(): - words.append(word) - conf = data["conf"][i] - if conf > 0: # -1 means no confidence data - confidences.append(conf) - - text = " ".join(words) - avg_confidence = sum(confidences) / len(confidences) if confidences else 0 - - console.print(f"[green]โœ“[/] Extracted {len(words)} words, confidence: {avg_confidence:.1f}%") - - return OCRResult( - text=text, - source_path=path, - confidence=avg_confidence - ) - - -def process_images( - path: Path, - output_dir: Optional[Path] = None, - language: str = "eng", - recursive: bool = True -) -> list[OCRResult]: - """Process all images in a file or directory with OCR. - - Args: - path: File or directory path - output_dir: Output directory for extracted text - language: Tesseract language code - recursive: If True, scan subdirectories - - Returns: - List of OCRResult objects - """ - from src.downloaders.files import scan_files - - path = Path(path) - output_dir = output_dir or settings.transcripts_dir - - # Get image files - files = scan_files(path, recursive=recursive, file_types=["image"]) - - if not files: - console.print("[yellow]No images found[/]") - return [] - - console.print(f"[bold blue]Found {len(files)} images to process[/]") - - results = [] - for file_info in files: - try: - result = extract_text_from_image(file_info.path, language=language) - - if result.text.strip(): - # Save extracted text - output_path = output_dir / f"{file_info.path.stem}_ocr.txt" - result.save(output_path) - console.print(f"[green]โœ“[/] Saved: {output_path.name}") - results.append(result) - else: - console.print(f"[yellow]No text found in {file_info.name}[/]") - - except Exception as e: - console.print(f"[red]Error processing {file_info.name}:[/] {e}") - - return results diff --git a/src/processors/transcriber.py b/src/processors/transcriber.py deleted file mode 100644 index f46bdd31b0d968cff8f6161e024dead4011df2f0..0000000000000000000000000000000000000000 --- a/src/processors/transcriber.py +++ /dev/null @@ -1,243 +0,0 @@ -"""Audio transcription using Whisper (local, free).""" - -import json -from dataclasses import dataclass -from pathlib import Path -from typing import Optional - -from rich.console import Console -from rich.progress import Progress, SpinnerColumn, TextColumn - -from src.config import settings - -console = Console() - - -@dataclass -class TranscriptSegment: - """A segment of transcribed text with timing.""" - - start: float # Start time in seconds - end: float # End time in seconds - text: str # Transcribed text - - @property - def start_formatted(self) -> str: - """Format start time as HH:MM:SS.""" - return self._format_time(self.start) - - @property - def end_formatted(self) -> str: - """Format end time as HH:MM:SS.""" - return self._format_time(self.end) - - def _format_time(self, seconds: float) -> str: - hours, remainder = divmod(int(seconds), 3600) - minutes, secs = divmod(remainder, 60) - return f"{hours:02d}:{minutes:02d}:{secs:02d}" - - -@dataclass -class Transcript: - """Complete transcript with segments and metadata.""" - - text: str # Full transcript text - segments: list[TranscriptSegment] # Timed segments - language: str # Detected language - duration: float # Audio duration in seconds - - def to_srt(self) -> str: - """Convert to SRT subtitle format.""" - lines = [] - for i, seg in enumerate(self.segments, 1): - start = self._format_srt_time(seg.start) - end = self._format_srt_time(seg.end) - lines.append(f"{i}") - lines.append(f"{start} --> {end}") - lines.append(seg.text.strip()) - lines.append("") - return "\n".join(lines) - - def _format_srt_time(self, seconds: float) -> str: - hours, remainder = divmod(int(seconds), 3600) - minutes, secs = divmod(remainder, 60) - ms = int((seconds % 1) * 1000) - return f"{hours:02d}:{minutes:02d}:{secs:02d},{ms:03d}" - - def save(self, output_path: Path, format: str = "txt") -> Path: - """Save transcript to file. - - Args: - output_path: Output file path - format: 'txt', 'srt', or 'json' - """ - output_path = Path(output_path) - output_path.parent.mkdir(parents=True, exist_ok=True) - - if format == "txt": - output_path.write_text(self.text) - elif format == "srt": - output_path.write_text(self.to_srt()) - elif format == "json": - data = { - "text": self.text, - "language": self.language, - "duration": self.duration, - "segments": [ - {"start": s.start, "end": s.end, "text": s.text} - for s in self.segments - ] - } - output_path.write_text(json.dumps(data, indent=2)) - - return output_path - - -class WhisperTranscriber: - """Transcribe audio using faster-whisper (local, free).""" - - def __init__( - self, - model_size: str = "base", - device: str = "auto", - compute_type: str = "auto" - ): - """Initialize the transcriber. - - Args: - model_size: Whisper model size - tiny, base, small, medium, large-v3 - device: Device to use - 'auto', 'cpu', 'cuda' - compute_type: Computation type - 'auto', 'int8', 'float16', 'float32' - """ - self.model_size = model_size - self.device = device - self.compute_type = compute_type - self._model = None - - def _load_model(self): - """Lazy load the Whisper model.""" - if self._model is None: - console.print(f"[bold green]Loading Whisper model:[/] {self.model_size}") - - from faster_whisper import WhisperModel - - # Determine device and compute type - device = self.device - compute_type = self.compute_type - - if device == "auto": - try: - import torch - device = "cuda" if torch.cuda.is_available() else "cpu" - except ImportError: - device = "cpu" - - if compute_type == "auto": - compute_type = "float16" if device == "cuda" else "int8" - - self._model = WhisperModel( - self.model_size, - device=device, - compute_type=compute_type - ) - - console.print(f"[green]โœ“[/] Model loaded on {device}") - - def transcribe( - self, - audio_path: Path, - language: Optional[str] = None, - ) -> Transcript: - """Transcribe an audio file. - - Args: - audio_path: Path to audio file - language: Language code (e.g., 'en') or None for auto-detect - - Returns: - Transcript object with full text and segments - """ - audio_path = Path(audio_path) - - if not audio_path.exists(): - raise FileNotFoundError(f"Audio file not found: {audio_path}") - - self._load_model() - - console.print(f"[bold green]Transcribing:[/] {audio_path.name}") - - # Transcribe - segments_generator, info = self._model.transcribe( - str(audio_path), - language=language, - beam_size=5, - word_timestamps=True, - vad_filter=True, # Filter out non-speech - ) - - # Collect segments - segments = [] - full_text_parts = [] - - with Progress( - SpinnerColumn(), - TextColumn("[progress.description]{task.description}"), - console=console - ) as progress: - task = progress.add_task("Processing segments...", total=None) - - for segment in segments_generator: - segments.append(TranscriptSegment( - start=segment.start, - end=segment.end, - text=segment.text - )) - full_text_parts.append(segment.text) - progress.update(task, description=f"Processed {len(segments)} segments...") - - transcript = Transcript( - text=" ".join(full_text_parts).strip(), - segments=segments, - language=info.language, - duration=info.duration - ) - - console.print(f"[green]โœ“[/] Transcription complete") - console.print(f"[bold blue]Language:[/] {info.language}") - console.print(f"[bold blue]Duration:[/] {info.duration:.1f}s") - console.print(f"[bold blue]Segments:[/] {len(segments)}") - - return transcript - - -def transcribe_audio( - audio_path: Path, - model_size: str = "base", - output_dir: Optional[Path] = None, - save_formats: list[str] = ["txt", "json"] -) -> Transcript: - """Convenience function to transcribe audio and save results. - - Args: - audio_path: Path to audio file - model_size: Whisper model size - output_dir: Output directory (default: data/transcripts) - save_formats: List of formats to save ('txt', 'srt', 'json') - - Returns: - Transcript object - """ - audio_path = Path(audio_path) - output_dir = output_dir or settings.transcripts_dir - - # Transcribe - transcriber = WhisperTranscriber(model_size=model_size) - transcript = transcriber.transcribe(audio_path) - - # Save in requested formats - for fmt in save_formats: - output_path = output_dir / f"{audio_path.stem}.{fmt}" - transcript.save(output_path, format=fmt) - console.print(f"[green]โœ“[/] Saved: {output_path}") - - return transcript diff --git a/src/ui/__init__.py b/src/ui/__init__.py deleted file mode 100644 index 43094c429c8b703f7c878ea11f5fef50bbcd810b..0000000000000000000000000000000000000000 --- a/src/ui/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""User interfaces for Video Analyzer.""" diff --git a/src/ui/__pycache__/__init__.cpython-312.pyc b/src/ui/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index b3b654d9f5d5032170ba507b27c481de14979282..0000000000000000000000000000000000000000 Binary files a/src/ui/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/src/ui/__pycache__/cli.cpython-312.pyc b/src/ui/__pycache__/cli.cpython-312.pyc deleted file mode 100644 index 951d46f60137d4440b0e3588719354b69e3eb481..0000000000000000000000000000000000000000 Binary files a/src/ui/__pycache__/cli.cpython-312.pyc and /dev/null differ diff --git a/src/ui/cli.py b/src/ui/cli.py deleted file mode 100644 index f339a0f97f3d794d97152b91004d9611b42ae352..0000000000000000000000000000000000000000 --- a/src/ui/cli.py +++ /dev/null @@ -1,644 +0,0 @@ -"""Command-line interface for Video Analyzer.""" - -from pathlib import Path -from typing import Optional - -import typer -from rich.console import Console -from rich.panel import Panel -from rich.table import Table - -app = typer.Typer( - name="video-analyzer", - help="Download, transcribe, and learn from video content.", - add_completion=False, -) - -console = Console() - - -@app.command() -def download( - url: str = typer.Argument(..., help="YouTube video or playlist URL"), - audio_only: bool = typer.Option(False, "--audio-only", "-a", help="Download audio only (faster)"), - quality: str = typer.Option("best", "--quality", "-q", help="Video quality: best, 1080p, 720p, 480p"), - subtitles: bool = typer.Option(True, "--subtitles/--no-subtitles", help="Download subtitles if available"), - output_dir: Optional[Path] = typer.Option(None, "--output", "-o", help="Output directory"), - cookies: Optional[Path] = typer.Option(None, "--cookies", "-c", help="Path to cookies.txt for YouTube authentication"), -): - """Download a video from YouTube.""" - from src.downloaders import YouTubeDownloader - - console.print(Panel.fit( - "[bold blue]Video Analyzer[/] - Download", - border_style="blue" - )) - - downloader = YouTubeDownloader(output_dir, cookies_file=cookies) - - try: - info = downloader.download_video( - url, - audio_only=audio_only, - get_subtitles=subtitles, - quality=quality - ) - - console.print("\n[bold green]Download complete![/]") - - # Show summary - table = Table(title="Video Details") - table.add_column("Property", style="cyan") - table.add_column("Value", style="white") - - table.add_row("Title", info.title) - table.add_row("Duration", info.duration_formatted) - table.add_row("Uploader", info.uploader) - if info.filepath: - table.add_row("File", str(info.filepath)) - if info.audio_filepath: - table.add_row("Audio", str(info.audio_filepath)) - if info.subtitles: - table.add_row("Subtitles", "โœ“ Downloaded") - - console.print(table) - - except Exception as e: - console.print(f"[red]Error:[/] {e}") - raise typer.Exit(1) - - -@app.command() -def transcribe( - input_path: Path = typer.Argument(..., help="Path to video/audio file or directory"), - model: str = typer.Option("base", "--model", "-m", help="Whisper model: tiny, base, small, medium, large-v3"), - output_dir: Optional[Path] = typer.Option(None, "--output", "-o", help="Output directory for transcripts"), -): - """Transcribe audio/video to text using Whisper.""" - from src.processors import extract_audio, transcribe_audio - from src.config import settings - - console.print(Panel.fit( - "[bold blue]Video Analyzer[/] - Transcribe", - border_style="blue" - )) - - input_path = Path(input_path) - output_dir = output_dir or settings.transcripts_dir - - # Collect files to process - if input_path.is_dir(): - files = list(input_path.glob("*.mp4")) + \ - list(input_path.glob("*.mp3")) + \ - list(input_path.glob("*.wav")) + \ - list(input_path.glob("*.m4a")) + \ - list(input_path.glob("*.webm")) - console.print(f"[bold blue]Found {len(files)} files to process[/]") - else: - files = [input_path] - - for file_path in files: - try: - console.print(f"\n[bold]Processing:[/] {file_path.name}") - - # Extract audio if video - if file_path.suffix.lower() in [".mp4", ".webm", ".mkv", ".avi", ".mov"]: - audio_path = extract_audio(file_path) - else: - audio_path = file_path - - # Transcribe - transcript = transcribe_audio( - audio_path, - model_size=model, - output_dir=output_dir - ) - - # Show preview - console.print(f"\n[bold blue]Transcript preview:[/]") - preview = transcript.text[:500] + "..." if len(transcript.text) > 500 else transcript.text - console.print(preview) - - except Exception as e: - console.print(f"[red]Error processing {file_path.name}:[/] {e}") - - -@app.command() -def process( - url: str = typer.Argument(..., help="YouTube video URL"), - model: str = typer.Option("base", "--model", "-m", help="Whisper model size"), - audio_only: bool = typer.Option(True, "--audio-only/--video", help="Download audio only (faster)"), - cookies: Optional[Path] = typer.Option(None, "--cookies", "-c", help="Path to cookies.txt for YouTube authentication"), -): - """Download and transcribe a YouTube video in one step.""" - from src.downloaders import YouTubeDownloader - from src.processors import extract_audio, transcribe_audio - from src.config import settings - - console.print(Panel.fit( - "[bold blue]Video Analyzer[/] - Process", - border_style="blue" - )) - - # Step 1: Download - console.print("\n[bold cyan]Step 1/2:[/] Downloading video...") - downloader = YouTubeDownloader(cookies_file=cookies) - - try: - info = downloader.download_video(url, audio_only=audio_only, get_subtitles=True) - except Exception as e: - console.print(f"[red]Download failed:[/] {e}") - raise typer.Exit(1) - - # Check if we already have subtitles (skip transcription) - if info.subtitles: - console.print("\n[bold green]โœ“ Using existing YouTube subtitles (faster)[/]") - - # Save subtitles as transcript - transcript_path = settings.transcripts_dir / f"{info.id}.txt" - transcript_path.write_text(info.subtitles) - console.print(f"[green]โœ“[/] Saved transcript: {transcript_path}") - - else: - # Step 2: Transcribe with Whisper - console.print("\n[bold cyan]Step 2/2:[/] Transcribing with Whisper...") - - audio_path = info.audio_filepath or info.filepath - - # Extract audio if needed - if info.filepath and not info.audio_filepath: - audio_path = extract_audio(info.filepath) - - try: - transcript = transcribe_audio( - audio_path, - model_size=model, - output_dir=settings.transcripts_dir - ) - except Exception as e: - console.print(f"[red]Transcription failed:[/] {e}") - raise typer.Exit(1) - - console.print("\n[bold green]โœ“ Processing complete![/]") - - # Show summary - table = Table(title="Summary") - table.add_column("Property", style="cyan") - table.add_column("Value", style="white") - table.add_row("Title", info.title) - table.add_row("Duration", info.duration_formatted) - table.add_row("Transcript", str(settings.transcripts_dir / f"{info.id}.txt")) - console.print(table) - - -@app.command() -def status(): - """Show processing status and statistics.""" - from src.config import settings - - console.print(Panel.fit( - "[bold blue]Video Analyzer[/] - Status", - border_style="blue" - )) - - # Count files in each directory - downloads = list(settings.downloads_dir.glob("*")) - audio_files = list(settings.audio_dir.glob("*")) - transcripts = list(settings.transcripts_dir.glob("*.txt")) - - table = Table(title="Content Statistics") - table.add_column("Category", style="cyan") - table.add_column("Count", style="white") - table.add_column("Location", style="dim") - - table.add_row("Downloads", str(len(downloads)), str(settings.downloads_dir)) - table.add_row("Audio Files", str(len(audio_files)), str(settings.audio_dir)) - table.add_row("Transcripts", str(len(transcripts)), str(settings.transcripts_dir)) - - console.print(table) - - -@app.command() -def list_content(): - """List all downloaded and processed content.""" - from src.config import settings - - console.print(Panel.fit( - "[bold blue]Video Analyzer[/] - Content", - border_style="blue" - )) - - # List transcripts - transcripts = list(settings.transcripts_dir.glob("*.txt")) - - if not transcripts: - console.print("[yellow]No content found. Use 'process' command to add videos.[/]") - return - - table = Table(title="Processed Content") - table.add_column("#", style="dim") - table.add_column("ID", style="cyan") - table.add_column("Size", style="white") - - for i, t in enumerate(transcripts, 1): - size = t.stat().st_size - size_str = f"{size / 1024:.1f} KB" if size > 1024 else f"{size} B" - table.add_row(str(i), t.stem, size_str) - - console.print(table) - - -@app.command() -def add( - path: Path = typer.Argument(..., help="File or folder to add"), - copy: bool = typer.Option(True, "--copy/--move", help="Copy or move files"), - recursive: bool = typer.Option(True, "--recursive/--no-recursive", help="Scan subdirectories"), - dry_run: bool = typer.Option(False, "--dry-run", "-n", help="Show what would be done without doing it"), -): - """Add local files or folders for processing.""" - from src.downloaders.files import import_files, scan_files - - console.print(Panel.fit( - "[bold blue]Video Analyzer[/] - Add Files" + (" [DRY RUN]" if dry_run else ""), - border_style="blue" - )) - - path = Path(path) - if not path.exists(): - console.print(f"[red]Error:[/] Path not found: {path}") - raise typer.Exit(1) - - # Scan first to show what will be imported - files = scan_files(path, recursive=recursive) - - if not files: - console.print("[yellow]No supported files found.[/]") - console.print("Supported formats: .mp4, .mp3, .pdf, .docx, .pptx, .png, .jpg, etc.") - return - - # Show summary by type - table = Table(title="Files Found") - table.add_column("Type", style="cyan") - table.add_column("Count", style="white") - - type_counts = {} - for f in files: - type_counts[f.file_type] = type_counts.get(f.file_type, 0) + 1 - - for file_type, count in sorted(type_counts.items()): - table.add_row(file_type, str(count)) - - console.print(table) - console.print(f"\n[bold]Total:[/] {len(files)} files") - - # Dry run - just show what would happen - if dry_run: - action = "Would copy" if copy else "Would move" - console.print(f"\n[yellow]{action} {len(files)} files to data/downloads/[/]") - console.print("[dim]Use without --dry-run to execute[/]") - return - - # Import files - action = "Copying" if copy else "Moving" - console.print(f"\n[bold green]{action} files...[/]") - - imported = import_files(path, copy=copy, recursive=recursive) - - console.print(f"\n[green]โœ“[/] Added {len(imported)} files to data/downloads/") - - -@app.command() -def process_docs( - path: Optional[Path] = typer.Argument(None, help="Path to document(s) or use data/downloads"), - recursive: bool = typer.Option(True, "--recursive/--no-recursive", help="Scan subdirectories"), -): - """Process PDF, Word, and PowerPoint documents.""" - from src.processors.documents import process_documents - from src.config import settings - - console.print(Panel.fit( - "[bold blue]Video Analyzer[/] - Process Documents", - border_style="blue" - )) - - # Default to downloads directory - if path is None: - path = settings.downloads_dir - console.print(f"[dim]Processing files in {path}[/]") - - path = Path(path) - if not path.exists(): - console.print(f"[red]Error:[/] Path not found: {path}") - raise typer.Exit(1) - - try: - results = process_documents(path, recursive=recursive) - console.print(f"\n[green]โœ“[/] Processed {len(results)} documents") - except ImportError as e: - console.print(f"[red]Missing dependency:[/] {e}") - console.print("Install with: pip install PyMuPDF python-docx python-pptx") - raise typer.Exit(1) - - -@app.command() -def process_images( - path: Optional[Path] = typer.Argument(None, help="Path to image(s) or use data/downloads"), - language: str = typer.Option("eng", "--lang", "-l", help="OCR language code"), - recursive: bool = typer.Option(True, "--recursive/--no-recursive", help="Scan subdirectories"), -): - """Extract text from images using OCR.""" - from src.processors.ocr import process_images as do_ocr - from src.config import settings - - console.print(Panel.fit( - "[bold blue]Video Analyzer[/] - OCR Images", - border_style="blue" - )) - - if path is None: - path = settings.downloads_dir - - path = Path(path) - if not path.exists(): - console.print(f"[red]Error:[/] Path not found: {path}") - raise typer.Exit(1) - - try: - results = do_ocr(path, language=language, recursive=recursive) - console.print(f"\n[green]โœ“[/] Processed {len(results)} images") - except ImportError as e: - console.print(f"[red]Missing dependency:[/] {e}") - raise typer.Exit(1) - - -@app.command() -def summarize( - path: Optional[Path] = typer.Argument(None, help="Path to transcript file or folder"), - summary_type: str = typer.Option("detailed", "--type", "-t", - help="Summary type: quick, detailed, study_notes, real_estate"), - model: str = typer.Option("llama3", "--model", "-m", help="Ollama model to use"), - all_files: bool = typer.Option(False, "--all", "-a", help="Summarize all transcripts"), - backend: str = typer.Option("ollama", "--backend", "-b", - help="AI backend: ollama, huggingface, huggingface-api"), -): - """Summarize transcripts using local AI (Ollama or Hugging Face).""" - from src.analyzers.summarizer import summarize_file, OllamaClient - from src.config import settings - - console.print(Panel.fit( - "[bold blue]Video Analyzer[/] - AI Summarize", - border_style="blue" - )) - - # Handle different backends - if backend == "huggingface": - _summarize_huggingface(path, model, all_files, use_api=False) - return - elif backend == "huggingface-api": - _summarize_huggingface(path, model, all_files, use_api=True) - return - - # Default: Ollama - client = OllamaClient(model) - if not client.is_available(): - console.print("[red]Ollama is not running![/]") - console.print("\n[bold]Options:[/]") - console.print("1. Install & start Ollama:") - console.print(" curl -fsSL https://ollama.com/install.sh | sh") - console.print(" ollama serve") - console.print(f" ollama pull {model}") - console.print("\n2. Use Hugging Face instead (no setup needed):") - console.print(" ./video-analyzer summarize --backend huggingface") - raise typer.Exit(1) - - console.print(f"[bold blue]Backend:[/] Ollama") - console.print(f"[bold blue]Model:[/] {model}") - console.print(f"[bold blue]Summary type:[/] {summary_type}") - - # Determine files to process - if all_files or path is None: - transcripts = list(settings.transcripts_dir.glob("*.txt")) - if not transcripts: - console.print("[yellow]No transcripts found. Process some content first.[/]") - return - files_to_process = transcripts - else: - path = Path(path) - if path.is_dir(): - files_to_process = list(path.glob("*.txt")) - else: - files_to_process = [path] - - console.print(f"[bold blue]Files to summarize:[/] {len(files_to_process)}") - - # Process each file - for file_path in files_to_process: - try: - console.print(f"\n[bold]Processing:[/] {file_path.name}") - summary = summarize_file(file_path, summary_type=summary_type, model=model) - console.print(f"[dim]Compression: {len(summary.text)}/{summary.original_length} chars " - f"({summary.compression_ratio:.1%})[/]") - except Exception as e: - console.print(f"[red]Error:[/] {e}") - - -def _summarize_huggingface( - path: Optional[Path], - model: str, - all_files: bool, - use_api: bool -): - """Summarize using Hugging Face.""" - from src.analyzers.huggingface import HuggingFaceLocal, HuggingFaceAPI - from src.config import settings - - # Default model for Hugging Face - if model == "llama3": # User didn't change from default - model = "facebook/bart-large-cnn" - - console.print(f"[bold blue]Backend:[/] Hugging Face {'API' if use_api else 'Local'}") - console.print(f"[bold blue]Model:[/] {model}") - - # Get files - if all_files or path is None: - transcripts = list(settings.transcripts_dir.glob("*.txt")) - if not transcripts: - console.print("[yellow]No transcripts found.[/]") - return - files_to_process = transcripts - else: - path = Path(path) - files_to_process = [path] if path.is_file() else list(path.glob("*.txt")) - - console.print(f"[bold blue]Files:[/] {len(files_to_process)}") - - # Initialize client - try: - if use_api: - client = HuggingFaceAPI(model) - else: - client = HuggingFaceLocal(model) - except ImportError as e: - console.print(f"[red]Missing dependency:[/] {e}") - console.print("Install with: pip install transformers torch") - return - - # Process files - for file_path in files_to_process: - try: - console.print(f"\n[bold]Processing:[/] {file_path.name}") - text = file_path.read_text() - - summary = client.summarize(text) - - # Save summary - output_path = settings.summaries_dir / f"{file_path.stem}_summary.md" - output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_text(summary) - - console.print(f"[green]โœ“[/] Saved: {output_path.name}") - console.print(f"[dim]Length: {len(summary)} chars[/]") - - except Exception as e: - console.print(f"[red]Error:[/] {e}") - - -@app.command() -def list_models(): - """List recommended AI models for summarization.""" - from src.analyzers.huggingface import list_recommended_models - - console.print(Panel.fit( - "[bold blue]Video Analyzer[/] - AI Models", - border_style="blue" - )) - - # Hugging Face models - list_recommended_models() - - # Ollama models - console.print("\n[bold cyan]Ollama Models[/] (run locally):") - table = Table() - table.add_column("Model", style="green") - table.add_column("Size", style="white") - table.add_column("Notes", style="dim") - - table.add_row("phi3", "3.8B", "Fastest, good for quick summaries") - table.add_row("mistral", "7B", "Balanced speed/quality") - table.add_row("llama3", "8B", "Best quality") - table.add_row("llama3:70b", "70B", "Outstanding (needs 48GB+ RAM)") - - console.print(table) - - console.print("\n[bold]Usage:[/]") - console.print(" Ollama: ./video-analyzer summarize --backend ollama -m llama3") - console.print(" HuggingFace: ./video-analyzer summarize --backend huggingface -m facebook/bart-large-cnn") - console.print(" HF API: ./video-analyzer summarize --backend huggingface-api -m facebook/bart-large-cnn") - - -@app.command() -def process_all( - path: Optional[Path] = typer.Argument(None, help="Path to files or use data/downloads"), - whisper_model: str = typer.Option("base", "--whisper", "-w", help="Whisper model size"), - skip_summarize: bool = typer.Option(False, "--skip-summarize", help="Skip AI summarization"), - summarize_model: str = typer.Option("llama3", "--llm", help="Ollama model for summaries"), - dry_run: bool = typer.Option(False, "--dry-run", "-n", help="Show what would be processed"), -): - """Process all files: transcribe videos, extract docs, summarize.""" - from src.downloaders.files import scan_files - from src.processors import extract_audio, transcribe_audio, extract_document - from src.analyzers.summarizer import summarize_file, OllamaClient - from src.config import settings - - console.print(Panel.fit( - "[bold blue]Video Analyzer[/] - Process All" + (" [DRY RUN]" if dry_run else ""), - border_style="blue" - )) - - if path is None: - path = settings.downloads_dir - - path = Path(path) - files = scan_files(path) - - if not files: - console.print("[yellow]No files found to process.[/]") - return - - # Group by type - videos = [f for f in files if f.file_type == "video"] - audios = [f for f in files if f.file_type == "audio"] - docs = [f for f in files if f.file_type == "document"] - - console.print(f"[bold blue]Found:[/] {len(videos)} videos, {len(audios)} audio, {len(docs)} documents") - - # Dry run - just show what would happen - if dry_run: - console.print("\n[yellow]Would process:[/]") - for v in videos: - console.print(f" ๐Ÿ“น {v.name} โ†’ transcribe with Whisper ({whisper_model})") - for a in audios: - console.print(f" ๐ŸŽต {a.name} โ†’ transcribe with Whisper ({whisper_model})") - for d in docs: - console.print(f" ๐Ÿ“„ {d.name} โ†’ extract text") - if not skip_summarize: - console.print(f"\n ๐Ÿ“ Generate summaries with {summarize_model}") - console.print("\n[dim]Use without --dry-run to execute[/]") - return - - transcripts_created = [] - - # Process videos - for file_info in videos: - try: - console.print(f"\n[bold cyan]Video:[/] {file_info.name}") - audio_path = extract_audio(file_info.path) - transcript = transcribe_audio(audio_path, model_size=whisper_model) - transcripts_created.append(settings.transcripts_dir / f"{file_info.path.stem}.txt") - except Exception as e: - console.print(f"[red]Error:[/] {e}") - - # Process audio - for file_info in audios: - try: - console.print(f"\n[bold cyan]Audio:[/] {file_info.name}") - transcript = transcribe_audio(file_info.path, model_size=whisper_model) - transcripts_created.append(settings.transcripts_dir / f"{file_info.path.stem}.txt") - except Exception as e: - console.print(f"[red]Error:[/] {e}") - - # Process documents - for file_info in docs: - try: - console.print(f"\n[bold cyan]Document:[/] {file_info.name}") - doc = extract_document(file_info.path) - output_path = settings.transcripts_dir / f"{file_info.path.stem}.txt" - doc.save(output_path) - transcripts_created.append(output_path) - except Exception as e: - console.print(f"[red]Error:[/] {e}") - - console.print(f"\n[green]โœ“[/] Created {len(transcripts_created)} transcripts") - - # Summarize if Ollama available - if not skip_summarize: - client = OllamaClient(summarize_model) - if client.is_available(): - console.print(f"\n[bold green]Generating summaries with {summarize_model}...[/]") - for transcript_path in transcripts_created: - try: - summarize_file(transcript_path, summary_type="study_notes", model=summarize_model) - except Exception as e: - console.print(f"[red]Summary error:[/] {e}") - else: - console.print("\n[yellow]Skipping summaries - Ollama not running[/]") - console.print("Start Ollama to enable: ollama serve") - - -def main(): - """Entry point.""" - app() - - -if __name__ == "__main__": - main() diff --git a/src/ui/web.py b/src/ui/web.py deleted file mode 100644 index 151bdaddaa9529bb88ca5df4c55c0ce596c0c908..0000000000000000000000000000000000000000 --- a/src/ui/web.py +++ /dev/null @@ -1,325 +0,0 @@ -"""Gradio Web UI for Video Analyzer - HuggingFace Spaces compatible.""" - -import os -from pathlib import Path -from typing import Optional - -# Set up paths for HF Spaces -DATA_DIR = Path(os.getenv("DATA_DIR", "/data" if os.path.exists("/data") else "./data")) -CHROMA_DIR = DATA_DIR / "chromadb" -TRANSCRIPTS_DIR = DATA_DIR / "transcripts" -SUMMARIES_DIR = DATA_DIR / "summaries" - -# Ensure directories exist -for d in [CHROMA_DIR, TRANSCRIPTS_DIR, SUMMARIES_DIR]: - d.mkdir(parents=True, exist_ok=True) - - -def create_app(): - """Create the Gradio application.""" - import gradio as gr - - # Import here to avoid issues before directories are set up - from src.knowledge.vectorstore import KnowledgeBase - from src.knowledge.indexer import index_text - from src.analyzers.chunker import chunk_for_summarization - - # Initialize knowledge base with HF Spaces persistent storage - kb = KnowledgeBase(persist_dir=CHROMA_DIR) - - # ============== SEARCH TAB ============== - def search_knowledge(query: str, n_results: int = 5) -> str: - """Search the knowledge base.""" - if not query.strip(): - return "Please enter a search query." - - try: - results = kb.search(query, n_results=n_results) - - if not results: - return "No results found. Try indexing some content first." - - output = [] - for i, r in enumerate(results, 1): - source_name = Path(r.source).stem if r.source else "unknown" - score_pct = r.score * 100 - - output.append(f"### Result {i} (Score: {score_pct:.1f}%)") - output.append(f"**Source:** {source_name}") - output.append(f"```\n{r.text[:500]}{'...' if len(r.text) > 500 else ''}\n```") - output.append("") - - return "\n".join(output) - - except Exception as e: - return f"Error searching: {str(e)}" - - # ============== UPLOAD TAB ============== - def process_upload(file, source_name: str) -> str: - """Process an uploaded file.""" - if file is None: - return "Please upload a file." - - try: - # Read file content - if hasattr(file, 'name'): - file_path = file.name - with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: - content = f.read() - - # Use provided name or file name - name = source_name.strip() if source_name.strip() else Path(file_path).stem - else: - return "Invalid file format." - - if not content.strip(): - return "File is empty." - - # Save to transcripts directory - save_path = TRANSCRIPTS_DIR / f"{name}.txt" - save_path.write_text(content) - - # Index into knowledge base - chunks = chunk_for_summarization(content) - texts = [c.text for c in chunks] - - if texts: - kb.add_texts(texts, source=str(save_path)) - - return f"โœ… Successfully indexed!\n\n- **Source:** {name}\n- **Chunks:** {len(texts)}\n- **Characters:** {len(content):,}" - - except Exception as e: - return f"โŒ Error processing file: {str(e)}" - - def process_text_input(text: str, source_name: str) -> str: - """Process pasted text.""" - if not text.strip(): - return "Please enter some text." - - if not source_name.strip(): - return "Please provide a source name." - - try: - # Save to transcripts directory - save_path = TRANSCRIPTS_DIR / f"{source_name.strip()}.txt" - save_path.write_text(text) - - # Index into knowledge base - chunks = chunk_for_summarization(text) - texts = [c.text for c in chunks] - - if texts: - kb.add_texts(texts, source=str(save_path)) - - return f"โœ… Successfully indexed!\n\n- **Source:** {source_name}\n- **Chunks:** {len(texts)}\n- **Characters:** {len(text):,}" - - except Exception as e: - return f"โŒ Error: {str(e)}" - - # ============== STATUS TAB ============== - def get_status() -> str: - """Get knowledge base status.""" - try: - count = kb.count() - sources = kb.get_sources() - - output = ["## ๐Ÿ“Š Knowledge Base Status\n"] - output.append(f"**Total chunks indexed:** {count}") - output.append(f"**Number of sources:** {len(sources)}") - - if sources: - output.append("\n### ๐Ÿ“ Sources:") - for s in sources[:20]: # Limit to 20 - name = Path(s).stem - output.append(f"- {name}") - if len(sources) > 20: - output.append(f"- ... and {len(sources) - 20} more") - - return "\n".join(output) - - except Exception as e: - return f"Error getting status: {str(e)}" - - def clear_knowledge_base() -> str: - """Clear all indexed content.""" - try: - kb.clear() - return "โœ… Knowledge base cleared!" - except Exception as e: - return f"โŒ Error: {str(e)}" - - # ============== CHAT TAB (RAG) ============== - def chat_with_mentor(message: str, history: list, n_context: int = 3) -> str: - """Chat with the AI mentor using RAG.""" - if not message.strip(): - return "" - - try: - # Search for relevant context - results = kb.search(message, n_results=n_context) - - if not results: - return "I don't have any relevant information indexed yet. Please upload some content first." - - # Build context from search results - context_parts = [] - for r in results: - source_name = Path(r.source).stem if r.source else "unknown" - context_parts.append(f"[From: {source_name}]\n{r.text}") - - context = "\n\n---\n\n".join(context_parts) - - # For now, return context with guidance - # (Full LLM integration would go here with Ollama/HuggingFace) - response = f"""Based on your course materials, here's what I found: - -{context[:2000]}{'...' if len(context) > 2000 else ''} - ---- -*Sources: {', '.join(set(Path(r.source).stem for r in results))}*""" - - return response - - except Exception as e: - return f"Error: {str(e)}" - - # ============== BUILD UI ============== - with gr.Blocks( - title="Real Estate Mentor", - theme=gr.themes.Soft(), - css=""" - .gradio-container { max-width: 1200px !important; } - """ - ) as app: - gr.Markdown(""" - # ๐Ÿ  Real Estate Mentor - - **Your AI-powered course assistant.** Upload transcripts, search your knowledge base, and get answers. - - --- - """) - - with gr.Tabs(): - # Search Tab - with gr.TabItem("๐Ÿ” Search", id="search"): - gr.Markdown("### Search Your Knowledge Base") - with gr.Row(): - with gr.Column(scale=3): - search_input = gr.Textbox( - label="Search Query", - placeholder="e.g., How do I calculate cap rate?", - lines=2 - ) - with gr.Column(scale=1): - n_results = gr.Slider( - minimum=1, maximum=10, value=5, step=1, - label="Number of Results" - ) - search_btn = gr.Button("๐Ÿ” Search", variant="primary") - search_output = gr.Markdown(label="Results") - - search_btn.click( - search_knowledge, - inputs=[search_input, n_results], - outputs=search_output - ) - - # Chat Tab - with gr.TabItem("๐Ÿ’ฌ Ask Mentor", id="chat"): - gr.Markdown("### Ask Your Real Estate Mentor") - chatbot = gr.Chatbot(height=400) - chat_input = gr.Textbox( - label="Your Question", - placeholder="Ask anything about your course content...", - lines=2 - ) - chat_btn = gr.Button("๐Ÿ’ฌ Ask", variant="primary") - - def respond(message, history): - response = chat_with_mentor(message, history) - history.append((message, response)) - return "", history - - chat_btn.click( - respond, - inputs=[chat_input, chatbot], - outputs=[chat_input, chatbot] - ) - chat_input.submit( - respond, - inputs=[chat_input, chatbot], - outputs=[chat_input, chatbot] - ) - - # Upload Tab - with gr.TabItem("๐Ÿ“ค Upload Content", id="upload"): - gr.Markdown("### Add Content to Your Knowledge Base") - - with gr.Tabs(): - with gr.TabItem("Upload File"): - file_input = gr.File( - label="Upload Text File", - file_types=[".txt", ".md"] - ) - file_source_name = gr.Textbox( - label="Source Name (optional)", - placeholder="e.g., Module 1 - Basics" - ) - upload_btn = gr.Button("๐Ÿ“ค Upload & Index", variant="primary") - upload_output = gr.Markdown() - - upload_btn.click( - process_upload, - inputs=[file_input, file_source_name], - outputs=upload_output - ) - - with gr.TabItem("Paste Text"): - text_input = gr.Textbox( - label="Paste Your Text", - placeholder="Paste transcript or notes here...", - lines=10 - ) - text_source_name = gr.Textbox( - label="Source Name", - placeholder="e.g., Video 1 Transcript" - ) - text_btn = gr.Button("๐Ÿ“ฅ Index Text", variant="primary") - text_output = gr.Markdown() - - text_btn.click( - process_text_input, - inputs=[text_input, text_source_name], - outputs=text_output - ) - - # Status Tab - with gr.TabItem("๐Ÿ“Š Status", id="status"): - gr.Markdown("### Knowledge Base Status") - status_output = gr.Markdown() - with gr.Row(): - refresh_btn = gr.Button("๐Ÿ”„ Refresh Status") - clear_btn = gr.Button("๐Ÿ—‘๏ธ Clear All", variant="stop") - - refresh_btn.click(get_status, outputs=status_output) - clear_btn.click(clear_knowledge_base, outputs=status_output) - - # Load status on page load - app.load(get_status, outputs=status_output) - - gr.Markdown(""" - --- - *Built with โค๏ธ using Gradio, ChromaDB, and Sentence Transformers* - """) - - return app - - -def launch_app(share: bool = False, port: int = 7860): - """Launch the Gradio app.""" - app = create_app() - app.launch(share=share, server_port=port) - - -if __name__ == "__main__": - launch_app() diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 959c7882958ba539a8049a316f34b2803a01b78e..0000000000000000000000000000000000000000 --- a/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Test suite for Video Analyzer.""" diff --git a/tests/test_chunker.py b/tests/test_chunker.py deleted file mode 100644 index b63cc6eb5c1a443b2fbf98f42b80193e443b3022..0000000000000000000000000000000000000000 --- a/tests/test_chunker.py +++ /dev/null @@ -1,165 +0,0 @@ -"""Tests for text chunking functionality. - -These tests verify that the chunker: -- Produces deterministic output (idempotent) -- Handles edge cases correctly -- Maintains text integrity -""" - -import pytest -from src.analyzers.chunker import chunk_text, chunk_for_summarization, estimate_tokens, TextChunk - - -class TestEstimateTokens: - """Tests for token estimation.""" - - def test_empty_string(self): - """Empty string should return 0 tokens.""" - assert estimate_tokens("") == 0 - - def test_short_text(self): - """Short text should estimate correctly.""" - # "hello" = 5 chars / 4 = 1.25 -> 1 token - assert estimate_tokens("hello") == 1 - - def test_longer_text(self): - """Longer text should scale linearly.""" - text = "a" * 100 # 100 chars / 4 = 25 tokens - assert estimate_tokens(text) == 25 - - def test_custom_chars_per_token(self): - """Custom chars_per_token should be respected.""" - text = "a" * 100 - assert estimate_tokens(text, chars_per_token=2.0) == 50 - - def test_idempotent(self): - """Same input should always produce same output.""" - text = "This is a test string for token estimation." - result1 = estimate_tokens(text) - result2 = estimate_tokens(text) - assert result1 == result2 - - -class TestChunkText: - """Tests for text chunking.""" - - def test_short_text_no_chunking(self): - """Text shorter than chunk_size should return single chunk.""" - text = "Short text" - chunks = chunk_text(text, chunk_size=100) - - assert len(chunks) == 1 - assert chunks[0].text == text - assert chunks[0].index == 0 - - def test_empty_string(self): - """Empty string should return single empty chunk.""" - chunks = chunk_text("") - assert len(chunks) == 1 - assert chunks[0].text == "" - - def test_chunk_indices_sequential(self): - """Chunk indices should be sequential starting from 0.""" - text = "a" * 1000 - chunks = chunk_text(text, chunk_size=100, chunk_overlap=10) - - for i, chunk in enumerate(chunks): - assert chunk.index == i - - def test_chunks_cover_full_text(self): - """All text should be covered by chunks.""" - text = "This is a longer text that should be split into multiple chunks for processing." - chunks = chunk_text(text, chunk_size=20, chunk_overlap=5) - - # Verify we have multiple chunks - assert len(chunks) > 1 - - # Verify start/end positions make sense - assert chunks[0].start_char == 0 - assert chunks[-1].end_char == len(text) - - def test_idempotent(self): - """Same input should always produce same output.""" - text = "This is a test. " * 50 - - chunks1 = chunk_text(text, chunk_size=100) - chunks2 = chunk_text(text, chunk_size=100) - - assert len(chunks1) == len(chunks2) - for c1, c2 in zip(chunks1, chunks2): - assert c1.text == c2.text - assert c1.index == c2.index - - def test_respects_separator(self): - """Should prefer splitting at separator.""" - text = "Paragraph one.\n\nParagraph two.\n\nParagraph three." - chunks = chunk_text(text, chunk_size=30, separator="\n\n") - - # Should split at paragraph boundaries when possible - assert len(chunks) >= 2 - - def test_chunk_dataclass_properties(self): - """TextChunk should have correct properties.""" - chunk = TextChunk(text="hello world", index=0, start_char=0, end_char=11) - - assert chunk.word_count == 2 - assert chunk.text == "hello world" - - -class TestChunkForSummarization: - """Tests for summarization-optimized chunking.""" - - def test_uses_token_based_sizing(self): - """Should calculate chunk size based on tokens.""" - text = "a" * 20000 # Long text - - chunks = chunk_for_summarization(text, max_tokens=1000, chars_per_token=4.0) - - # Each chunk should be roughly 4000 chars (1000 tokens * 4 chars) - for chunk in chunks[:-1]: # Exclude last chunk which may be smaller - assert len(chunk.text) <= 4200 # Allow some flexibility for separator search - - def test_short_text_single_chunk(self): - """Short text should not be chunked.""" - text = "Short summary text." - chunks = chunk_for_summarization(text) - - assert len(chunks) == 1 - - def test_idempotent(self): - """Same input should always produce same output.""" - text = "Test content. " * 500 - - result1 = chunk_for_summarization(text) - result2 = chunk_for_summarization(text) - - assert len(result1) == len(result2) - - -class TestEdgeCases: - """Edge case tests.""" - - def test_unicode_text(self): - """Should handle unicode correctly.""" - text = "Hello ไธ–็•Œ! ะŸั€ะธะฒะตั‚ ะผะธั€! ๐ŸŽ‰" * 10 - chunks = chunk_text(text, chunk_size=50) - - assert len(chunks) >= 1 - # Verify no corruption - combined = "".join(c.text for c in chunks) - assert "ไธ–็•Œ" in combined or "ไธ–็•Œ" in text[:50] - - def test_only_whitespace(self): - """Should handle whitespace-only text.""" - text = " \n\n \t " - chunks = chunk_text(text) - - assert len(chunks) >= 1 - - def test_very_long_word(self): - """Should handle text with no natural break points.""" - text = "a" * 500 # No spaces or separators - chunks = chunk_text(text, chunk_size=100, chunk_overlap=10) - - assert len(chunks) >= 1 - # Should still chunk even without separators diff --git a/tests/test_documents.py b/tests/test_documents.py deleted file mode 100644 index 5b3b859ad074fb430b0867c897f5dc4eda8230cf..0000000000000000000000000000000000000000 --- a/tests/test_documents.py +++ /dev/null @@ -1,170 +0,0 @@ -"""Tests for document processing. - -These tests verify document extraction: -- Text extraction from various formats -- Metadata handling -- Error handling -""" - -import pytest -from pathlib import Path -from src.processors.documents import ( - extract_text_file, - extract_document, - DocumentContent, -) - - -class TestDocumentContent: - """Tests for DocumentContent dataclass.""" - - def test_save_creates_file(self, tmp_path): - """Should save text to file.""" - doc = DocumentContent( - text="Test content", - title="Test", - pages=1, - source_path=Path("source.txt"), - doc_type="txt", - metadata={} - ) - - output_path = tmp_path / "output.txt" - result = doc.save(output_path) - - assert result == output_path - assert output_path.exists() - assert output_path.read_text() == "Test content" - - def test_save_creates_parent_dirs(self, tmp_path): - """Should create parent directories if needed.""" - doc = DocumentContent( - text="Test", - title="Test", - pages=1, - source_path=Path("source.txt"), - doc_type="txt", - metadata={} - ) - - output_path = tmp_path / "nested" / "dir" / "output.txt" - doc.save(output_path) - - assert output_path.exists() - - -class TestExtractTextFile: - """Tests for plain text extraction.""" - - def test_extract_txt(self, tmp_path): - """Should extract text from .txt file.""" - test_file = tmp_path / "test.txt" - test_file.write_text("Hello, world!") - - result = extract_text_file(test_file) - - assert result.text == "Hello, world!" - assert result.doc_type == "txt" - assert result.title == "test" - - def test_extract_markdown(self, tmp_path): - """Should extract text from .md file.""" - test_file = tmp_path / "readme.md" - test_file.write_text("# Header\n\nContent here.") - - result = extract_text_file(test_file) - - assert "# Header" in result.text - assert result.doc_type == "md" - - def test_extract_preserves_formatting(self, tmp_path): - """Should preserve newlines and formatting.""" - content = "Line 1\nLine 2\n\nParagraph 2" - test_file = tmp_path / "test.txt" - test_file.write_text(content) - - result = extract_text_file(test_file) - - assert result.text == content - - def test_handles_unicode(self, tmp_path): - """Should handle unicode content.""" - content = "Hello ไธ–็•Œ! ะŸั€ะธะฒะตั‚! ๐ŸŽ‰" - test_file = tmp_path / "unicode.txt" - test_file.write_text(content, encoding="utf-8") - - result = extract_text_file(test_file) - - assert result.text == content - - def test_empty_file(self, tmp_path): - """Should handle empty files.""" - test_file = tmp_path / "empty.txt" - test_file.write_text("") - - result = extract_text_file(test_file) - - assert result.text == "" - assert result.pages == 1 - - def test_idempotent(self, tmp_path): - """Same file should produce same output.""" - test_file = tmp_path / "test.txt" - test_file.write_text("Consistent content") - - result1 = extract_text_file(test_file) - result2 = extract_text_file(test_file) - - assert result1.text == result2.text - assert result1.title == result2.title - - -class TestExtractDocument: - """Tests for generic document extraction.""" - - def test_routes_txt_correctly(self, tmp_path): - """Should route .txt to text extractor.""" - test_file = tmp_path / "test.txt" - test_file.write_text("Text content") - - result = extract_document(test_file) - - assert result.doc_type == "txt" - - def test_routes_md_correctly(self, tmp_path): - """Should route .md to text extractor.""" - test_file = tmp_path / "test.md" - test_file.write_text("# Markdown") - - result = extract_document(test_file) - - assert result.doc_type == "md" - - def test_unsupported_format_raises(self, tmp_path): - """Should raise for unsupported formats.""" - test_file = tmp_path / "test.xyz" - test_file.write_text("Unknown format") - - with pytest.raises(ValueError, match="Unsupported document type"): - extract_document(test_file) - - def test_nonexistent_file_raises(self, tmp_path): - """Should raise for nonexistent files.""" - fake_file = tmp_path / "nonexistent.txt" - - with pytest.raises(FileNotFoundError): - extract_text_file(fake_file) - - -class TestDocumentMetadata: - """Tests for metadata extraction.""" - - def test_txt_has_minimal_metadata(self, tmp_path): - """Text files should have minimal metadata.""" - test_file = tmp_path / "test.txt" - test_file.write_text("Content") - - result = extract_text_file(test_file) - - assert result.metadata == {} - assert result.source_path == test_file diff --git a/tests/test_files.py b/tests/test_files.py deleted file mode 100644 index 54141f1eb9b00cd6ea76469d65487caf43e00e35..0000000000000000000000000000000000000000 --- a/tests/test_files.py +++ /dev/null @@ -1,219 +0,0 @@ -"""Tests for file scanning and type detection. - -These tests verify that the file scanner: -- Correctly identifies file types -- Scans directories properly -- Handles edge cases -""" - -import pytest -from pathlib import Path -from src.downloaders.files import ( - get_file_type, - scan_files, - FileInfo, - VIDEO_EXTENSIONS, - AUDIO_EXTENSIONS, - DOCUMENT_EXTENSIONS, - IMAGE_EXTENSIONS, -) - - -class TestGetFileType: - """Tests for file type detection.""" - - def test_video_extensions(self): - """Should identify video files.""" - for ext in [".mp4", ".mkv", ".avi", ".mov", ".webm"]: - path = Path(f"test{ext}") - assert get_file_type(path) == "video" - - def test_audio_extensions(self): - """Should identify audio files.""" - for ext in [".mp3", ".wav", ".m4a", ".flac", ".ogg"]: - path = Path(f"test{ext}") - assert get_file_type(path) == "audio" - - def test_document_extensions(self): - """Should identify document files.""" - for ext in [".pdf", ".docx", ".pptx", ".txt", ".md"]: - path = Path(f"test{ext}") - assert get_file_type(path) == "document" - - def test_image_extensions(self): - """Should identify image files.""" - for ext in [".png", ".jpg", ".jpeg", ".gif", ".bmp"]: - path = Path(f"test{ext}") - assert get_file_type(path) == "image" - - def test_unknown_extension(self): - """Should return 'unknown' for unsupported types.""" - path = Path("test.xyz") - assert get_file_type(path) == "unknown" - - def test_case_insensitive(self): - """Should handle uppercase extensions.""" - path = Path("test.MP4") - assert get_file_type(path) == "video" - - def test_idempotent(self): - """Same input should always produce same output.""" - path = Path("video.mp4") - result1 = get_file_type(path) - result2 = get_file_type(path) - assert result1 == result2 == "video" - - -class TestFileInfo: - """Tests for FileInfo dataclass.""" - - def test_size_formatted_bytes(self): - """Should format small sizes in bytes.""" - info = FileInfo( - path=Path("test.txt"), - name="test.txt", - size=500, - file_type="document", - extension=".txt" - ) - assert info.size_formatted == "500 B" - - def test_size_formatted_kb(self): - """Should format KB sizes.""" - info = FileInfo( - path=Path("test.txt"), - name="test.txt", - size=2048, - file_type="document", - extension=".txt" - ) - assert info.size_formatted == "2.0 KB" - - def test_size_formatted_mb(self): - """Should format MB sizes.""" - info = FileInfo( - path=Path("test.mp4"), - name="test.mp4", - size=5 * 1024 * 1024, - file_type="video", - extension=".mp4" - ) - assert info.size_formatted == "5.0 MB" - - def test_size_formatted_gb(self): - """Should format GB sizes.""" - info = FileInfo( - path=Path("test.mp4"), - name="test.mp4", - size=2 * 1024 * 1024 * 1024, - file_type="video", - extension=".mp4" - ) - assert info.size_formatted == "2.0 GB" - - -class TestScanFiles: - """Tests for directory scanning.""" - - def test_scan_nonexistent_path(self, tmp_path): - """Should return empty list for nonexistent path.""" - fake_path = tmp_path / "nonexistent" - result = scan_files(fake_path) - assert result == [] - - def test_scan_empty_directory(self, tmp_path): - """Should return empty list for empty directory.""" - result = scan_files(tmp_path) - assert result == [] - - def test_scan_single_file(self, tmp_path): - """Should scan a single supported file.""" - test_file = tmp_path / "test.mp4" - test_file.write_text("fake video content") - - result = scan_files(test_file) - - assert len(result) == 1 - assert result[0].name == "test.mp4" - assert result[0].file_type == "video" - - def test_scan_ignores_unsupported(self, tmp_path): - """Should ignore unsupported file types.""" - (tmp_path / "test.xyz").write_text("unsupported") - (tmp_path / "test.mp4").write_text("video") - - result = scan_files(tmp_path) - - assert len(result) == 1 - assert result[0].extension == ".mp4" - - def test_scan_filters_by_type(self, tmp_path): - """Should filter by file type when specified.""" - (tmp_path / "video.mp4").write_text("video") - (tmp_path / "audio.mp3").write_text("audio") - (tmp_path / "doc.pdf").write_text("document") - - videos = scan_files(tmp_path, file_types=["video"]) - audios = scan_files(tmp_path, file_types=["audio"]) - - assert len(videos) == 1 - assert videos[0].file_type == "video" - assert len(audios) == 1 - assert audios[0].file_type == "audio" - - def test_scan_recursive(self, tmp_path): - """Should scan subdirectories when recursive=True.""" - subdir = tmp_path / "subdir" - subdir.mkdir() - - (tmp_path / "root.mp4").write_text("video") - (subdir / "nested.mp4").write_text("video") - - recursive_result = scan_files(tmp_path, recursive=True) - non_recursive_result = scan_files(tmp_path, recursive=False) - - assert len(recursive_result) == 2 - assert len(non_recursive_result) == 1 - - def test_scan_sorted_by_name(self, tmp_path): - """Results should be sorted by name.""" - (tmp_path / "zebra.mp4").write_text("video") - (tmp_path / "alpha.mp4").write_text("video") - (tmp_path / "middle.mp4").write_text("video") - - result = scan_files(tmp_path) - names = [f.name for f in result] - - assert names == sorted(names, key=str.lower) - - def test_idempotent(self, tmp_path): - """Same directory should produce same results.""" - (tmp_path / "test.mp4").write_text("video") - - result1 = scan_files(tmp_path) - result2 = scan_files(tmp_path) - - assert len(result1) == len(result2) - assert result1[0].name == result2[0].name - - -class TestExtensionSets: - """Tests for extension set completeness.""" - - def test_no_overlap_between_types(self): - """Extension sets should not overlap.""" - all_sets = [VIDEO_EXTENSIONS, AUDIO_EXTENSIONS, DOCUMENT_EXTENSIONS, IMAGE_EXTENSIONS] - - for i, set1 in enumerate(all_sets): - for j, set2 in enumerate(all_sets): - if i != j: - overlap = set1 & set2 - assert len(overlap) == 0, f"Overlap found: {overlap}" - - def test_extensions_are_lowercase(self): - """All extensions should be lowercase with leading dot.""" - all_extensions = VIDEO_EXTENSIONS | AUDIO_EXTENSIONS | DOCUMENT_EXTENSIONS | IMAGE_EXTENSIONS - - for ext in all_extensions: - assert ext.startswith("."), f"Extension missing dot: {ext}" - assert ext == ext.lower(), f"Extension not lowercase: {ext}" diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000000000000000000000000000000000..4eb66139f818849589c83dc93ac8cffaf80e7267 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1171 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version < '3.12'", +] + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, +] + +[[package]] +name = "audioop-lts" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686, upload-time = "2025-08-05T16:43:17.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523, upload-time = "2025-08-05T16:42:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455, upload-time = "2025-08-05T16:42:22.283Z" }, + { url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997, upload-time = "2025-08-05T16:42:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844, upload-time = "2025-08-05T16:42:25.208Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056, upload-time = "2025-08-05T16:42:26.559Z" }, + { url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892, upload-time = "2025-08-05T16:42:27.902Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660, upload-time = "2025-08-05T16:42:28.9Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143, upload-time = "2025-08-05T16:42:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313, upload-time = "2025-08-05T16:42:30.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044, upload-time = "2025-08-05T16:42:31.959Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766, upload-time = "2025-08-05T16:42:33.302Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640, upload-time = "2025-08-05T16:42:34.854Z" }, + { url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052, upload-time = "2025-08-05T16:42:35.839Z" }, + { url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185, upload-time = "2025-08-05T16:42:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503, upload-time = "2025-08-05T16:42:38.427Z" }, + { url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173, upload-time = "2025-08-05T16:42:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096, upload-time = "2025-08-05T16:42:40.684Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748, upload-time = "2025-08-05T16:42:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329, upload-time = "2025-08-05T16:42:42.987Z" }, + { url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407, upload-time = "2025-08-05T16:42:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811, upload-time = "2025-08-05T16:42:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470, upload-time = "2025-08-05T16:42:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878, upload-time = "2025-08-05T16:42:47.576Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867, upload-time = "2025-08-05T16:42:49.003Z" }, + { url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001, upload-time = "2025-08-05T16:42:50.038Z" }, + { url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046, upload-time = "2025-08-05T16:42:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788, upload-time = "2025-08-05T16:42:52.198Z" }, + { url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472, upload-time = "2025-08-05T16:42:53.59Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279, upload-time = "2025-08-05T16:42:54.632Z" }, + { url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568, upload-time = "2025-08-05T16:42:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942, upload-time = "2025-08-05T16:42:56.674Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603, upload-time = "2025-08-05T16:42:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104, upload-time = "2025-08-05T16:42:58.518Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754, upload-time = "2025-08-05T16:43:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332, upload-time = "2025-08-05T16:43:01.666Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396, upload-time = "2025-08-05T16:43:02.991Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811, upload-time = "2025-08-05T16:43:04.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483, upload-time = "2025-08-05T16:43:05.085Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885, upload-time = "2025-08-05T16:43:06.197Z" }, + { url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899, upload-time = "2025-08-05T16:43:07.291Z" }, + { url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998, upload-time = "2025-08-05T16:43:08.335Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046, upload-time = "2025-08-05T16:43:09.367Z" }, + { url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843, upload-time = "2025-08-05T16:43:10.749Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490, upload-time = "2025-08-05T16:43:12.131Z" }, + { url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297, upload-time = "2025-08-05T16:43:13.139Z" }, + { url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331, upload-time = "2025-08-05T16:43:14.19Z" }, + { url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697, upload-time = "2025-08-05T16:43:15.193Z" }, + { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" }, +] + +[[package]] +name = "brotli" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/ef/f285668811a9e1ddb47a18cb0b437d5fc2760d537a2fe8a57875ad6f8448/brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744", size = 863110, upload-time = "2025-11-05T18:38:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/50/62/a3b77593587010c789a9d6eaa527c79e0848b7b860402cc64bc0bc28a86c/brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f", size = 445438, upload-time = "2025-11-05T18:38:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e1/7fadd47f40ce5549dc44493877db40292277db373da5053aff181656e16e/brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd", size = 1534420, upload-time = "2025-11-05T18:38:15.111Z" }, + { url = "https://files.pythonhosted.org/packages/12/8b/1ed2f64054a5a008a4ccd2f271dbba7a5fb1a3067a99f5ceadedd4c1d5a7/brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe", size = 1632619, upload-time = "2025-11-05T18:38:16.094Z" }, + { url = "https://files.pythonhosted.org/packages/89/5a/7071a621eb2d052d64efd5da2ef55ecdac7c3b0c6e4f9d519e9c66d987ef/brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a", size = 1426014, upload-time = "2025-11-05T18:38:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/26/6d/0971a8ea435af5156acaaccec1a505f981c9c80227633851f2810abd252a/brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b", size = 1489661, upload-time = "2025-11-05T18:38:18.41Z" }, + { url = "https://files.pythonhosted.org/packages/f3/75/c1baca8b4ec6c96a03ef8230fab2a785e35297632f402ebb1e78a1e39116/brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3", size = 1599150, upload-time = "2025-11-05T18:38:19.792Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1a/23fcfee1c324fd48a63d7ebf4bac3a4115bdb1b00e600f80f727d850b1ae/brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae", size = 1493505, upload-time = "2025-11-05T18:38:20.913Z" }, + { url = "https://files.pythonhosted.org/packages/36/e5/12904bbd36afeef53d45a84881a4810ae8810ad7e328a971ebbfd760a0b3/brotli-1.2.0-cp311-cp311-win32.whl", hash = "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03", size = 334451, upload-time = "2025-11-05T18:38:21.94Z" }, + { url = "https://files.pythonhosted.org/packages/02/8b/ecb5761b989629a4758c394b9301607a5880de61ee2ee5fe104b87149ebc/brotli-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24", size = 369035, upload-time = "2025-11-05T18:38:22.941Z" }, + { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" }, + { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" }, + { url = "https://files.pythonhosted.org/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362, upload-time = "2025-11-05T18:38:32.939Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115, upload-time = "2025-11-05T18:38:33.765Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" }, + { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" }, + { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" }, + { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" }, + { url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" }, + { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" }, + { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" }, + { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[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 = "fastapi" +version = "0.128.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, +] + +[[package]] +name = "ffmpy" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/d2/1c4c582d71bcc65c76fa69fab85de6257d50fdf6fd4a2317c53917e9a581/ffmpy-1.0.0.tar.gz", hash = "sha256:b12932e95435c8820f1cd041024402765f821971e4bae753b327fc02a6e12f8b", size = 5101, upload-time = "2025-11-11T06:24:23.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/56/dd3669eccebb6d8ac81e624542ebd53fe6f08e1b8f2f8d50aeb7e3b83f99/ffmpy-1.0.0-py3-none-any.whl", hash = "sha256:5640e5f0fd03fb6236d0e119b16ccf6522db1c826fdf35dcb87087b60fd7504f", size = 5614, upload-time = "2025-11-11T06:24:22.818Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/27/954057b0d1f53f086f681755207dda6de6c660ce133c829158e8e8fe7895/fsspec-2025.12.0.tar.gz", hash = "sha256:c505de011584597b1060ff778bb664c1bc022e87921b0e4f10cc9c44f9635973", size = 309748, upload-time = "2025-12-03T15:23:42.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/c7/b64cae5dba3a1b138d7123ec36bb5ccd39d39939f18454407e5468f4763f/fsspec-2025.12.0-py3-none-any.whl", hash = "sha256:8bf1fe301b7d8acfa6e8571e3b1c3d158f909666642431cc78a1b7b4dbc5ec5b", size = 201422, upload-time = "2025-12-03T15:23:41.434Z" }, +] + +[[package]] +name = "gradio" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "anyio" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, + { name = "brotli" }, + { name = "fastapi" }, + { name = "ffmpy" }, + { name = "gradio-client" }, + { name = "groovy" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "numpy" }, + { name = "orjson" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pydub" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "safehttpx" }, + { name = "semantic-version" }, + { name = "starlette" }, + { name = "tomlkit" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/db/487d9732ef2f785359a6566cb5f4c820ad01a2a37f91af2161b71bf96c3a/gradio-6.2.0.tar.gz", hash = "sha256:f4f1d7407d5179ac3917ffb2f40545c6c23146bde4257b0bd5110e7b5e862e5b", size = 37890771, upload-time = "2025-12-19T18:29:38.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/c4/4813d4fe00298c55b355b232ca6420827c82939906aff7c396f4ac8d635c/gradio-6.2.0-py3-none-any.whl", hash = "sha256:65bcd9e26e77e9be8d688abdb18351d3f537ed16418c4274a513f280894317ed", size = 22985535, upload-time = "2025-12-19T18:29:34.922Z" }, +] + +[[package]] +name = "gradio-client" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fsspec" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/37/2bab45f9e218d5411e4e83c6d99602a1aba145b7872587dbdc8954877259/gradio_client-2.0.2.tar.gz", hash = "sha256:9800a3cead74881ffb3b0d6b731ea4a8e3c52d2ba63d5ab350e30db53566d4bf", size = 54935, upload-time = "2025-12-19T18:29:49.355Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a2/a3497afea984202f481de3464b3bfbcb3de1cd83cbbf3714933d40dd7106/gradio_client-2.0.2-py3-none-any.whl", hash = "sha256:46a7f63eaa7758fe2e38be7f78f26a1fff48a7b526ebdd87141b050e08556622", size = 55566, upload-time = "2025-12-19T18:29:47.508Z" }, +] + +[[package]] +name = "groovy" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/36/bbdede67400277bef33d3ec0e6a31750da972c469f75966b4930c753218f/groovy-0.1.2.tar.gz", hash = "sha256:25c1dc09b3f9d7e292458aa762c6beb96ea037071bf5e917fc81fb78d2231083", size = 17325, upload-time = "2025-02-28T20:24:56.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/27/3d6dcadc8a3214d8522c1e7f6a19554e33659be44546d44a2f7572ac7d2a/groovy-0.1.2-py3-none-any.whl", hash = "sha256:7f7975bab18c729a257a8b1ae9dcd70b7cafb1720481beae47719af57c35fa64", size = 14090, upload-time = "2025-02-28T20:24:55.152Z" }, +] + +[[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.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +] + +[[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.2.3" +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 = "shellingham" }, + { name = "tqdm" }, + { name = "typer-slim" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/c8/9cd2fcb670ba0e708bfdf95a1177b34ca62de2d3821df0773bc30559af80/huggingface_hub-1.2.3.tar.gz", hash = "sha256:4ba57f17004fd27bb176a6b7107df579865d4cde015112db59184c51f5602ba7", size = 614605, upload-time = "2025-12-12T15:31:42.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/8d/7ca723a884d55751b70479b8710f06a317296b1fa1c1dec01d0420d13e43/huggingface_hub-1.2.3-py3-none-any.whl", hash = "sha256:c9b7a91a9eedaa2149cdc12bdd8f5a11780e10de1f1024718becf9e41e5a4642", size = 520953, upload-time = "2025-12-12T15:31:40.339Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "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 = "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/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { 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 = "numpy" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/7a/6a3d14e205d292b738db449d0de649b373a59edb0d0b4493821d0a3e8718/numpy-2.4.0.tar.gz", hash = "sha256:6e504f7b16118198f138ef31ba24d985b124c2c469fe8467007cf30fd992f934", size = 20685720, upload-time = "2025-12-20T16:18:19.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/7e/7bae7cbcc2f8132271967aa03e03954fc1e48aa1f3bf32b29ca95fbef352/numpy-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:316b2f2584682318539f0bcaca5a496ce9ca78c88066579ebd11fd06f8e4741e", size = 16940166, upload-time = "2025-12-20T16:15:43.434Z" }, + { url = "https://files.pythonhosted.org/packages/0f/27/6c13f5b46776d6246ec884ac5817452672156a506d08a1f2abb39961930a/numpy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2718c1de8504121714234b6f8241d0019450353276c88b9453c9c3d92e101db", size = 12641781, upload-time = "2025-12-20T16:15:45.701Z" }, + { url = "https://files.pythonhosted.org/packages/14/1c/83b4998d4860d15283241d9e5215f28b40ac31f497c04b12fa7f428ff370/numpy-2.4.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:21555da4ec4a0c942520ead42c3b0dc9477441e085c42b0fbdd6a084869a6f6b", size = 5470247, upload-time = "2025-12-20T16:15:47.943Z" }, + { url = "https://files.pythonhosted.org/packages/54/08/cbce72c835d937795571b0464b52069f869c9e78b0c076d416c5269d2718/numpy-2.4.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:413aa561266a4be2d06cd2b9665e89d9f54c543f418773076a76adcf2af08bc7", size = 6799807, upload-time = "2025-12-20T16:15:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/ff/be/2e647961cd8c980591d75cdcd9e8f647d69fbe05e2a25613dc0a2ea5fb1a/numpy-2.4.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0feafc9e03128074689183031181fac0897ff169692d8492066e949041096548", size = 14701992, upload-time = "2025-12-20T16:15:51.615Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/e1652fb8b6fd91ce6ed429143fe2e01ce714711e03e5b762615e7b36172c/numpy-2.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8fdfed3deaf1928fb7667d96e0567cdf58c2b370ea2ee7e586aa383ec2cb346", size = 16646871, upload-time = "2025-12-20T16:15:54.129Z" }, + { url = "https://files.pythonhosted.org/packages/62/23/d841207e63c4322842f7cd042ae981cffe715c73376dcad8235fb31debf1/numpy-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e06a922a469cae9a57100864caf4f8a97a1026513793969f8ba5b63137a35d25", size = 16487190, upload-time = "2025-12-20T16:15:56.147Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a0/6a842c8421ebfdec0a230e65f61e0dabda6edbef443d999d79b87c273965/numpy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:927ccf5cd17c48f801f4ed43a7e5673a2724bd2171460be3e3894e6e332ef83a", size = 18580762, upload-time = "2025-12-20T16:15:58.524Z" }, + { url = "https://files.pythonhosted.org/packages/0a/d1/c79e0046641186f2134dde05e6181825b911f8bdcef31b19ddd16e232847/numpy-2.4.0-cp311-cp311-win32.whl", hash = "sha256:882567b7ae57c1b1a0250208cc21a7976d8cbcc49d5a322e607e6f09c9e0bd53", size = 6233359, upload-time = "2025-12-20T16:16:00.938Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f0/74965001d231f28184d6305b8cdc1b6fcd4bf23033f6cb039cfe76c9fca7/numpy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b986403023c8f3bf8f487c2e6186afda156174d31c175f747d8934dfddf3479", size = 12601132, upload-time = "2025-12-20T16:16:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/65/32/55408d0f46dfebce38017f5bd931affa7256ad6beac1a92a012e1fbc67a7/numpy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:3f3096405acc48887458bbf9f6814d43785ac7ba2a57ea6442b581dedbc60ce6", size = 10573977, upload-time = "2025-12-20T16:16:04.77Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ff/f6400ffec95de41c74b8e73df32e3fff1830633193a7b1e409be7fb1bb8c/numpy-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a8b6bb8369abefb8bd1801b054ad50e02b3275c8614dc6e5b0373c305291037", size = 16653117, upload-time = "2025-12-20T16:16:06.709Z" }, + { url = "https://files.pythonhosted.org/packages/fd/28/6c23e97450035072e8d830a3c411bf1abd1f42c611ff9d29e3d8f55c6252/numpy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e284ca13d5a8367e43734148622caf0b261b275673823593e3e3634a6490f83", size = 12369711, upload-time = "2025-12-20T16:16:08.758Z" }, + { url = "https://files.pythonhosted.org/packages/bc/af/acbef97b630ab1bb45e6a7d01d1452e4251aa88ce680ac36e56c272120ec/numpy-2.4.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:49ff32b09f5aa0cd30a20c2b39db3e669c845589f2b7fc910365210887e39344", size = 5198355, upload-time = "2025-12-20T16:16:10.902Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c8/4e0d436b66b826f2e53330adaa6311f5cac9871a5b5c31ad773b27f25a74/numpy-2.4.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:36cbfb13c152b1c7c184ddac43765db8ad672567e7bafff2cc755a09917ed2e6", size = 6545298, upload-time = "2025-12-20T16:16:12.607Z" }, + { url = "https://files.pythonhosted.org/packages/ef/27/e1f5d144ab54eac34875e79037011d511ac57b21b220063310cb96c80fbc/numpy-2.4.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35ddc8f4914466e6fc954c76527aa91aa763682a4f6d73249ef20b418fe6effb", size = 14398387, upload-time = "2025-12-20T16:16:14.257Z" }, + { url = "https://files.pythonhosted.org/packages/67/64/4cb909dd5ab09a9a5d086eff9586e69e827b88a5585517386879474f4cf7/numpy-2.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc578891de1db95b2a35001b695451767b580bb45753717498213c5ff3c41d63", size = 16363091, upload-time = "2025-12-20T16:16:17.32Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9c/8efe24577523ec6809261859737cf117b0eb6fdb655abdfdc81b2e468ce4/numpy-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98e81648e0b36e325ab67e46b5400a7a6d4a22b8a7c8e8bbfe20e7db7906bf95", size = 16176394, upload-time = "2025-12-20T16:16:19.524Z" }, + { url = "https://files.pythonhosted.org/packages/61/f0/1687441ece7b47a62e45a1f82015352c240765c707928edd8aef875d5951/numpy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d57b5046c120561ba8fa8e4030fbb8b822f3063910fa901ffadf16e2b7128ad6", size = 18287378, upload-time = "2025-12-20T16:16:22.866Z" }, + { url = "https://files.pythonhosted.org/packages/d3/6f/f868765d44e6fc466467ed810ba9d8d6db1add7d4a748abfa2a4c99a3194/numpy-2.4.0-cp312-cp312-win32.whl", hash = "sha256:92190db305a6f48734d3982f2c60fa30d6b5ee9bff10f2887b930d7b40119f4c", size = 5955432, upload-time = "2025-12-20T16:16:25.06Z" }, + { url = "https://files.pythonhosted.org/packages/d4/b5/94c1e79fcbab38d1ca15e13777477b2914dd2d559b410f96949d6637b085/numpy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:680060061adb2d74ce352628cb798cfdec399068aa7f07ba9fb818b2b3305f98", size = 12306201, upload-time = "2025-12-20T16:16:26.979Z" }, + { url = "https://files.pythonhosted.org/packages/70/09/c39dadf0b13bb0768cd29d6a3aaff1fb7c6905ac40e9aaeca26b1c086e06/numpy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:39699233bc72dd482da1415dcb06076e32f60eddc796a796c5fb6c5efce94667", size = 10308234, upload-time = "2025-12-20T16:16:29.417Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0d/853fd96372eda07c824d24adf02e8bc92bb3731b43a9b2a39161c3667cc4/numpy-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a152d86a3ae00ba5f47b3acf3b827509fd0b6cb7d3259665e63dafbad22a75ea", size = 16649088, upload-time = "2025-12-20T16:16:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/e3/37/cc636f1f2a9f585434e20a3e6e63422f70bfe4f7f6698e941db52ea1ac9a/numpy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39b19251dec4de8ff8496cd0806cbe27bf0684f765abb1f4809554de93785f2d", size = 12364065, upload-time = "2025-12-20T16:16:33.491Z" }, + { url = "https://files.pythonhosted.org/packages/ed/69/0b78f37ca3690969beee54103ce5f6021709134e8020767e93ba691a72f1/numpy-2.4.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:009bd0ea12d3c784b6639a8457537016ce5172109e585338e11334f6a7bb88ee", size = 5192640, upload-time = "2025-12-20T16:16:35.636Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2a/08569f8252abf590294dbb09a430543ec8f8cc710383abfb3e75cc73aeda/numpy-2.4.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5fe44e277225fd3dff6882d86d3d447205d43532c3627313d17e754fb3905a0e", size = 6541556, upload-time = "2025-12-20T16:16:37.276Z" }, + { url = "https://files.pythonhosted.org/packages/93/e9/a949885a4e177493d61519377952186b6cbfdf1d6002764c664ba28349b5/numpy-2.4.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f935c4493eda9069851058fa0d9e39dbf6286be690066509305e52912714dbb2", size = 14396562, upload-time = "2025-12-20T16:16:38.953Z" }, + { url = "https://files.pythonhosted.org/packages/99/98/9d4ad53b0e9ef901c2ef1d550d2136f5ac42d3fd2988390a6def32e23e48/numpy-2.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cfa5f29a695cb7438965e6c3e8d06e0416060cf0d709c1b1c1653a939bf5c2a", size = 16351719, upload-time = "2025-12-20T16:16:41.503Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/5f3711a38341d6e8dd619f6353251a0cdd07f3d6d101a8fd46f4ef87f895/numpy-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba0cb30acd3ef11c94dc27fbfba68940652492bc107075e7ffe23057f9425681", size = 16176053, upload-time = "2025-12-20T16:16:44.552Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5b/2a3753dc43916501b4183532e7ace862e13211042bceafa253afb5c71272/numpy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60e8c196cd82cbbd4f130b5290007e13e6de3eca79f0d4d38014769d96a7c475", size = 18277859, upload-time = "2025-12-20T16:16:47.174Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c5/a18bcdd07a941db3076ef489d036ab16d2bfc2eae0cf27e5a26e29189434/numpy-2.4.0-cp313-cp313-win32.whl", hash = "sha256:5f48cb3e88fbc294dc90e215d86fbaf1c852c63dbdb6c3a3e63f45c4b57f7344", size = 5953849, upload-time = "2025-12-20T16:16:49.554Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f1/719010ff8061da6e8a26e1980cf090412d4f5f8060b31f0c45d77dd67a01/numpy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:a899699294f28f7be8992853c0c60741f16ff199205e2e6cdca155762cbaa59d", size = 12302840, upload-time = "2025-12-20T16:16:51.227Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5a/b3d259083ed8b4d335270c76966cb6cf14a5d1b69e1a608994ac57a659e6/numpy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9198f447e1dc5647d07c9a6bbe2063cc0132728cc7175b39dbc796da5b54920d", size = 10308509, upload-time = "2025-12-20T16:16:53.313Z" }, + { url = "https://files.pythonhosted.org/packages/31/01/95edcffd1bb6c0633df4e808130545c4f07383ab629ac7e316fb44fff677/numpy-2.4.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74623f2ab5cc3f7c886add4f735d1031a1d2be4a4ae63c0546cfd74e7a31ddf6", size = 12491815, upload-time = "2025-12-20T16:16:55.496Z" }, + { url = "https://files.pythonhosted.org/packages/59/ea/5644b8baa92cc1c7163b4b4458c8679852733fa74ca49c942cfa82ded4e0/numpy-2.4.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:0804a8e4ab070d1d35496e65ffd3cf8114c136a2b81f61dfab0de4b218aacfd5", size = 5320321, upload-time = "2025-12-20T16:16:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/4e/e10938106d70bc21319bd6a86ae726da37edc802ce35a3a71ecdf1fdfe7f/numpy-2.4.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:02a2038eb27f9443a8b266a66911e926566b5a6ffd1a689b588f7f35b81e7dc3", size = 6641635, upload-time = "2025-12-20T16:16:59.379Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8d/a8828e3eaf5c0b4ab116924df82f24ce3416fa38d0674d8f708ddc6c8aac/numpy-2.4.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1889b3a3f47a7b5bee16bc25a2145bd7cb91897f815ce3499db64c7458b6d91d", size = 14456053, upload-time = "2025-12-20T16:17:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/17d97609d87d4520aa5ae2dcfb32305654550ac6a35effb946d303e594ce/numpy-2.4.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85eef4cb5625c47ee6425c58a3502555e10f45ee973da878ac8248ad58c136f3", size = 16401702, upload-time = "2025-12-20T16:17:04.235Z" }, + { url = "https://files.pythonhosted.org/packages/18/32/0f13c1b2d22bea1118356b8b963195446f3af124ed7a5adfa8fdecb1b6ca/numpy-2.4.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6dc8b7e2f4eb184b37655195f421836cfae6f58197b67e3ffc501f1333d993fa", size = 16242493, upload-time = "2025-12-20T16:17:06.856Z" }, + { url = "https://files.pythonhosted.org/packages/ae/23/48f21e3d309fbc137c068a1475358cbd3a901b3987dcfc97a029ab3068e2/numpy-2.4.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:44aba2f0cafd287871a495fb3163408b0bd25bbce135c6f621534a07f4f7875c", size = 18324222, upload-time = "2025-12-20T16:17:09.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/52/41f3d71296a3dcaa4f456aaa3c6fc8e745b43d0552b6bde56571bb4b4a0f/numpy-2.4.0-cp313-cp313t-win32.whl", hash = "sha256:20c115517513831860c573996e395707aa9fb691eb179200125c250e895fcd93", size = 6076216, upload-time = "2025-12-20T16:17:11.437Z" }, + { url = "https://files.pythonhosted.org/packages/35/ff/46fbfe60ab0710d2a2b16995f708750307d30eccbb4c38371ea9e986866e/numpy-2.4.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b48e35f4ab6f6a7597c46e301126ceba4c44cd3280e3750f85db48b082624fa4", size = 12444263, upload-time = "2025-12-20T16:17:13.182Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e3/9189ab319c01d2ed556c932ccf55064c5d75bb5850d1df7a482ce0badead/numpy-2.4.0-cp313-cp313t-win_arm64.whl", hash = "sha256:4d1cfce39e511069b11e67cd0bd78ceff31443b7c9e5c04db73c7a19f572967c", size = 10378265, upload-time = "2025-12-20T16:17:15.211Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ed/52eac27de39d5e5a6c9aadabe672bc06f55e24a3d9010cd1183948055d76/numpy-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c95eb6db2884917d86cde0b4d4cf31adf485c8ec36bf8696dd66fa70de96f36b", size = 16647476, upload-time = "2025-12-20T16:17:17.671Z" }, + { url = "https://files.pythonhosted.org/packages/77/c0/990ce1b7fcd4e09aeaa574e2a0a839589e4b08b2ca68070f1acb1fea6736/numpy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:65167da969cd1ec3a1df31cb221ca3a19a8aaa25370ecb17d428415e93c1935e", size = 12374563, upload-time = "2025-12-20T16:17:20.216Z" }, + { url = "https://files.pythonhosted.org/packages/37/7c/8c5e389c6ae8f5fd2277a988600d79e9625db3fff011a2d87ac80b881a4c/numpy-2.4.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3de19cfecd1465d0dcf8a5b5ea8b3155b42ed0b639dba4b71e323d74f2a3be5e", size = 5203107, upload-time = "2025-12-20T16:17:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/e6/94/ca5b3bd6a8a70a5eec9a0b8dd7f980c1eff4b8a54970a9a7fef248ef564f/numpy-2.4.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6c05483c3136ac4c91b4e81903cb53a8707d316f488124d0398499a4f8e8ef51", size = 6538067, upload-time = "2025-12-20T16:17:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/79/43/993eb7bb5be6761dde2b3a3a594d689cec83398e3f58f4758010f3b85727/numpy-2.4.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36667db4d6c1cea79c8930ab72fadfb4060feb4bfe724141cd4bd064d2e5f8ce", size = 14411926, upload-time = "2025-12-20T16:17:25.822Z" }, + { url = "https://files.pythonhosted.org/packages/03/75/d4c43b61de473912496317a854dac54f1efec3eeb158438da6884b70bb90/numpy-2.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a818668b674047fd88c4cddada7ab8f1c298812783e8328e956b78dc4807f9f", size = 16354295, upload-time = "2025-12-20T16:17:28.308Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0a/b54615b47ee8736a6461a4bb6749128dd3435c5a759d5663f11f0e9af4ac/numpy-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ee32359fb7543b7b7bd0b2f46294db27e29e7bbdf70541e81b190836cd83ded", size = 16190242, upload-time = "2025-12-20T16:17:30.993Z" }, + { url = "https://files.pythonhosted.org/packages/98/ce/ea207769aacad6246525ec6c6bbd66a2bf56c72443dc10e2f90feed29290/numpy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e493962256a38f58283de033d8af176c5c91c084ea30f15834f7545451c42059", size = 18280875, upload-time = "2025-12-20T16:17:33.327Z" }, + { url = "https://files.pythonhosted.org/packages/17/ef/ec409437aa962ea372ed601c519a2b141701683ff028f894b7466f0ab42b/numpy-2.4.0-cp314-cp314-win32.whl", hash = "sha256:6bbaebf0d11567fa8926215ae731e1d58e6ec28a8a25235b8a47405d301332db", size = 6002530, upload-time = "2025-12-20T16:17:35.729Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4a/5cb94c787a3ed1ac65e1271b968686521169a7b3ec0b6544bb3ca32960b0/numpy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d857f55e7fdf7c38ab96c4558c95b97d1c685be6b05c249f5fdafcbd6f9899e", size = 12435890, upload-time = "2025-12-20T16:17:37.599Z" }, + { url = "https://files.pythonhosted.org/packages/48/a0/04b89db963af9de1104975e2544f30de89adbf75b9e75f7dd2599be12c79/numpy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:bb50ce5fb202a26fd5404620e7ef820ad1ab3558b444cb0b55beb7ef66cd2d63", size = 10591892, upload-time = "2025-12-20T16:17:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/53/e5/d74b5ccf6712c06c7a545025a6a71bfa03bdc7e0568b405b0d655232fd92/numpy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:355354388cba60f2132df297e2d53053d4063f79077b67b481d21276d61fc4df", size = 12494312, upload-time = "2025-12-20T16:17:41.714Z" }, + { url = "https://files.pythonhosted.org/packages/c2/08/3ca9cc2ddf54dfee7ae9a6479c071092a228c68aef08252aa08dac2af002/numpy-2.4.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:1d8f9fde5f6dc1b6fc34df8162f3b3079365468703fee7f31d4e0cc8c63baed9", size = 5322862, upload-time = "2025-12-20T16:17:44.145Z" }, + { url = "https://files.pythonhosted.org/packages/87/74/0bb63a68394c0c1e52670cfff2e309afa41edbe11b3327d9af29e4383f34/numpy-2.4.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e0434aa22c821f44eeb4c650b81c7fbdd8c0122c6c4b5a576a76d5a35625ecd9", size = 6644986, upload-time = "2025-12-20T16:17:46.203Z" }, + { url = "https://files.pythonhosted.org/packages/06/8f/9264d9bdbcf8236af2823623fe2f3981d740fc3461e2787e231d97c38c28/numpy-2.4.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40483b2f2d3ba7aad426443767ff5632ec3156ef09742b96913787d13c336471", size = 14457958, upload-time = "2025-12-20T16:17:48.017Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d9/f9a69ae564bbc7236a35aa883319364ef5fd41f72aa320cc1cbe66148fe2/numpy-2.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6a7664ddd9746e20b7325351fe1a8408d0a2bf9c63b5e898290ddc8f09544", size = 16398394, upload-time = "2025-12-20T16:17:50.409Z" }, + { url = "https://files.pythonhosted.org/packages/34/c7/39241501408dde7f885d241a98caba5421061a2c6d2b2197ac5e3aa842d8/numpy-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ecb0019d44f4cdb50b676c5d0cb4b1eae8e15d1ed3d3e6639f986fc92b2ec52c", size = 16241044, upload-time = "2025-12-20T16:17:52.661Z" }, + { url = "https://files.pythonhosted.org/packages/7c/95/cae7effd90e065a95e59fe710eeee05d7328ed169776dfdd9f789e032125/numpy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d0ffd9e2e4441c96a9c91ec1783285d80bf835b677853fc2770a89d50c1e48ac", size = 18321772, upload-time = "2025-12-20T16:17:54.947Z" }, + { url = "https://files.pythonhosted.org/packages/96/df/3c6c279accd2bfb968a76298e5b276310bd55d243df4fa8ac5816d79347d/numpy-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:77f0d13fa87036d7553bf81f0e1fe3ce68d14c9976c9851744e4d3e91127e95f", size = 6148320, upload-time = "2025-12-20T16:17:57.249Z" }, + { url = "https://files.pythonhosted.org/packages/92/8d/f23033cce252e7a75cae853d17f582e86534c46404dea1c8ee094a9d6d84/numpy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b1f5b45829ac1848893f0ddf5cb326110604d6df96cdc255b0bf9edd154104d4", size = 12623460, upload-time = "2025-12-20T16:17:58.963Z" }, + { url = "https://files.pythonhosted.org/packages/a4/4f/1f8475907d1a7c4ef9020edf7f39ea2422ec896849245f00688e4b268a71/numpy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:23a3e9d1a6f360267e8fbb38ba5db355a6a7e9be71d7fce7ab3125e88bb646c8", size = 10661799, upload-time = "2025-12-20T16:18:01.078Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ef/088e7c7342f300aaf3ee5f2c821c4b9996a1bef2aaf6a49cc8ab4883758e/numpy-2.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b54c83f1c0c0f1d748dca0af516062b8829d53d1f0c402be24b4257a9c48ada6", size = 16819003, upload-time = "2025-12-20T16:18:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ce/a53017b5443b4b84517182d463fc7bcc2adb4faa8b20813f8e5f5aeb5faa/numpy-2.4.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:aabb081ca0ec5d39591fc33018cd4b3f96e1a2dd6756282029986d00a785fba4", size = 12567105, upload-time = "2025-12-20T16:18:05.594Z" }, + { url = "https://files.pythonhosted.org/packages/77/58/5ff91b161f2ec650c88a626c3905d938c89aaadabd0431e6d9c1330c83e2/numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:8eafe7c36c8430b7794edeab3087dec7bf31d634d92f2af9949434b9d1964cba", size = 5395590, upload-time = "2025-12-20T16:18:08.031Z" }, + { url = "https://files.pythonhosted.org/packages/1d/4e/f1a084106df8c2df8132fc437e56987308e0524836aa7733721c8429d4fe/numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2f585f52b2baf07ff3356158d9268ea095e221371f1074fadea2f42544d58b4d", size = 6709947, upload-time = "2025-12-20T16:18:09.836Z" }, + { url = "https://files.pythonhosted.org/packages/63/09/3d8aeb809c0332c3f642da812ac2e3d74fc9252b3021f8c30c82e99e3f3d/numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32ed06d0fe9cae27d8fb5f400c63ccee72370599c75e683a6358dd3a4fb50aaf", size = 14535119, upload-time = "2025-12-20T16:18:12.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7f/68f0fc43a2cbdc6bb239160c754d87c922f60fbaa0fa3cd3d312b8a7f5ee/numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:57c540ed8fb1f05cb997c6761cd56db72395b0d6985e90571ff660452ade4f98", size = 16475815, upload-time = "2025-12-20T16:18:14.433Z" }, + { url = "https://files.pythonhosted.org/packages/11/73/edeacba3167b1ca66d51b1a5a14697c2c40098b5ffa01811c67b1785a5ab/numpy-2.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a39fb973a726e63223287adc6dafe444ce75af952d711e400f3bf2b36ef55a7b", size = 12489376, upload-time = "2025-12-20T16:18:16.524Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/68/6b3659daec3a81aed5ab47700adb1a577c76a5452d35b91c88efee89987f/orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8", size = 245318, upload-time = "2025-12-06T15:54:02.355Z" }, + { url = "https://files.pythonhosted.org/packages/e9/00/92db122261425f61803ccf0830699ea5567439d966cbc35856fe711bfe6b/orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc", size = 129491, upload-time = "2025-12-06T15:54:03.877Z" }, + { url = "https://files.pythonhosted.org/packages/94/4f/ffdcb18356518809d944e1e1f77589845c278a1ebbb5a8297dfefcc4b4cb/orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968", size = 132167, upload-time = "2025-12-06T15:54:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/97/c6/0a8caff96f4503f4f7dd44e40e90f4d14acf80d3b7a97cb88747bb712d3e/orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7", size = 130516, upload-time = "2025-12-06T15:54:06.274Z" }, + { url = "https://files.pythonhosted.org/packages/4d/63/43d4dc9bd9954bff7052f700fdb501067f6fb134a003ddcea2a0bb3854ed/orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd", size = 135695, upload-time = "2025-12-06T15:54:07.702Z" }, + { url = "https://files.pythonhosted.org/packages/87/6f/27e2e76d110919cb7fcb72b26166ee676480a701bcf8fc53ac5d0edce32f/orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9", size = 139664, upload-time = "2025-12-06T15:54:08.828Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/5966153a5f1be49b5fbb8ca619a529fde7bc71aa0a376f2bb83fed248bcd/orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef", size = 137289, upload-time = "2025-12-06T15:54:09.898Z" }, + { url = "https://files.pythonhosted.org/packages/a7/34/8acb12ff0299385c8bbcbb19fbe40030f23f15a6de57a9c587ebf71483fb/orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9", size = 138784, upload-time = "2025-12-06T15:54:11.022Z" }, + { url = "https://files.pythonhosted.org/packages/ee/27/910421ea6e34a527f73d8f4ee7bdffa48357ff79c7b8d6eb6f7b82dd1176/orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125", size = 141322, upload-time = "2025-12-06T15:54:12.427Z" }, + { url = "https://files.pythonhosted.org/packages/87/a3/4b703edd1a05555d4bb1753d6ce44e1a05b7a6d7c164d5b332c795c63d70/orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814", size = 413612, upload-time = "2025-12-06T15:54:13.858Z" }, + { url = "https://files.pythonhosted.org/packages/1b/36/034177f11d7eeea16d3d2c42a1883b0373978e08bc9dad387f5074c786d8/orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5", size = 150993, upload-time = "2025-12-06T15:54:15.189Z" }, + { url = "https://files.pythonhosted.org/packages/44/2f/ea8b24ee046a50a7d141c0227c4496b1180b215e728e3b640684f0ea448d/orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880", size = 141774, upload-time = "2025-12-06T15:54:16.451Z" }, + { url = "https://files.pythonhosted.org/packages/8a/12/cc440554bf8200eb23348a5744a575a342497b65261cd65ef3b28332510a/orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d", size = 135109, upload-time = "2025-12-06T15:54:17.73Z" }, + { url = "https://files.pythonhosted.org/packages/a3/83/e0c5aa06ba73a6760134b169f11fb970caa1525fa4461f94d76e692299d9/orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1", size = 133193, upload-time = "2025-12-06T15:54:19.426Z" }, + { url = "https://files.pythonhosted.org/packages/cb/35/5b77eaebc60d735e832c5b1a20b155667645d123f09d471db0a78280fb49/orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c", size = 126830, upload-time = "2025-12-06T15:54:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a4/8052a029029b096a78955eadd68ab594ce2197e24ec50e6b6d2ab3f4e33b/orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d", size = 245347, upload-time = "2025-12-06T15:54:22.061Z" }, + { url = "https://files.pythonhosted.org/packages/64/67/574a7732bd9d9d79ac620c8790b4cfe0717a3d5a6eb2b539e6e8995e24a0/orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626", size = 129435, upload-time = "2025-12-06T15:54:23.615Z" }, + { url = "https://files.pythonhosted.org/packages/52/8d/544e77d7a29d90cf4d9eecd0ae801c688e7f3d1adfa2ebae5e1e94d38ab9/orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f", size = 132074, upload-time = "2025-12-06T15:54:24.694Z" }, + { url = "https://files.pythonhosted.org/packages/6e/57/b9f5b5b6fbff9c26f77e785baf56ae8460ef74acdb3eae4931c25b8f5ba9/orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85", size = 130520, upload-time = "2025-12-06T15:54:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6d/d34970bf9eb33f9ec7c979a262cad86076814859e54eb9a059a52f6dc13d/orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9", size = 136209, upload-time = "2025-12-06T15:54:27.264Z" }, + { url = "https://files.pythonhosted.org/packages/e7/39/bc373b63cc0e117a105ea12e57280f83ae52fdee426890d57412432d63b3/orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626", size = 139837, upload-time = "2025-12-06T15:54:28.75Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/7c4818c8d7d324da220f4f1af55c343956003aa4d1ce1857bdc1d396ba69/orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa", size = 137307, upload-time = "2025-12-06T15:54:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/46/bf/0993b5a056759ba65145effe3a79dd5a939d4a070eaa5da2ee3180fbb13f/orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477", size = 139020, upload-time = "2025-12-06T15:54:31.024Z" }, + { url = "https://files.pythonhosted.org/packages/65/e8/83a6c95db3039e504eda60fc388f9faedbb4f6472f5aba7084e06552d9aa/orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e", size = 141099, upload-time = "2025-12-06T15:54:32.196Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b4/24fdc024abfce31c2f6812973b0a693688037ece5dc64b7a60c1ce69e2f2/orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69", size = 413540, upload-time = "2025-12-06T15:54:33.361Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/01c0ec95d55ed0c11e4cae3e10427e479bba40c77312b63e1f9665e0737d/orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3", size = 151530, upload-time = "2025-12-06T15:54:34.6Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d4/f9ebc57182705bb4bbe63f5bbe14af43722a2533135e1d2fb7affa0c355d/orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca", size = 141863, upload-time = "2025-12-06T15:54:35.801Z" }, + { url = "https://files.pythonhosted.org/packages/0d/04/02102b8d19fdcb009d72d622bb5781e8f3fae1646bf3e18c53d1bc8115b5/orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98", size = 135255, upload-time = "2025-12-06T15:54:37.209Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fb/f05646c43d5450492cb387de5549f6de90a71001682c17882d9f66476af5/orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875", size = 133252, upload-time = "2025-12-06T15:54:38.401Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/7b8c0b26ba18c793533ac1cd145e131e46fcf43952aa94c109b5b913c1f0/orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe", size = 126777, upload-time = "2025-12-06T15:54:39.515Z" }, + { url = "https://files.pythonhosted.org/packages/10/43/61a77040ce59f1569edf38f0b9faadc90c8cf7e9bec2e0df51d0132c6bb7/orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629", size = 245271, upload-time = "2025-12-06T15:54:40.878Z" }, + { url = "https://files.pythonhosted.org/packages/55/f9/0f79be617388227866d50edd2fd320cb8fb94dc1501184bb1620981a0aba/orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3", size = 129422, upload-time = "2025-12-06T15:54:42.403Z" }, + { url = "https://files.pythonhosted.org/packages/77/42/f1bf1549b432d4a78bfa95735b79b5dac75b65b5bb815bba86ad406ead0a/orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39", size = 132060, upload-time = "2025-12-06T15:54:43.531Z" }, + { url = "https://files.pythonhosted.org/packages/25/49/825aa6b929f1a6ed244c78acd7b22c1481fd7e5fda047dc8bf4c1a807eb6/orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f", size = 130391, upload-time = "2025-12-06T15:54:45.059Z" }, + { url = "https://files.pythonhosted.org/packages/42/ec/de55391858b49e16e1aa8f0bbbb7e5997b7345d8e984a2dec3746d13065b/orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51", size = 135964, upload-time = "2025-12-06T15:54:46.576Z" }, + { url = "https://files.pythonhosted.org/packages/1c/40/820bc63121d2d28818556a2d0a09384a9f0262407cf9fa305e091a8048df/orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8", size = 139817, upload-time = "2025-12-06T15:54:48.084Z" }, + { url = "https://files.pythonhosted.org/packages/09/c7/3a445ca9a84a0d59d26365fd8898ff52bdfcdcb825bcc6519830371d2364/orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706", size = 137336, upload-time = "2025-12-06T15:54:49.426Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b3/dc0d3771f2e5d1f13368f56b339c6782f955c6a20b50465a91acb79fe961/orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f", size = 138993, upload-time = "2025-12-06T15:54:50.939Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a2/65267e959de6abe23444659b6e19c888f242bf7725ff927e2292776f6b89/orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863", size = 141070, upload-time = "2025-12-06T15:54:52.414Z" }, + { url = "https://files.pythonhosted.org/packages/63/c9/da44a321b288727a322c6ab17e1754195708786a04f4f9d2220a5076a649/orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228", size = 413505, upload-time = "2025-12-06T15:54:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/7f/17/68dc14fa7000eefb3d4d6d7326a190c99bb65e319f02747ef3ebf2452f12/orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2", size = 151342, upload-time = "2025-12-06T15:54:55.113Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c5/ccee774b67225bed630a57478529fc026eda33d94fe4c0eac8fe58d4aa52/orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05", size = 141823, upload-time = "2025-12-06T15:54:56.331Z" }, + { url = "https://files.pythonhosted.org/packages/67/80/5d00e4155d0cd7390ae2087130637671da713959bb558db9bac5e6f6b042/orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef", size = 135236, upload-time = "2025-12-06T15:54:57.507Z" }, + { url = "https://files.pythonhosted.org/packages/95/fe/792cc06a84808dbdc20ac6eab6811c53091b42f8e51ecebf14b540e9cfe4/orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583", size = 133167, upload-time = "2025-12-06T15:54:58.71Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/d158bd8b50e3b1cfdcf406a7e463f6ffe3f0d167b99634717acdaf5e299f/orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287", size = 126712, upload-time = "2025-12-06T15:54:59.892Z" }, + { url = "https://files.pythonhosted.org/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" }, + { url = "https://files.pythonhosted.org/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" }, + { url = "https://files.pythonhosted.org/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" }, + { url = "https://files.pythonhosted.org/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" }, + { url = "https://files.pythonhosted.org/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" }, + { url = "https://files.pythonhosted.org/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" }, + { url = "https://files.pythonhosted.org/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" }, + { url = "https://files.pythonhosted.org/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" }, + { url = "https://files.pythonhosted.org/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" }, + { url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" }, + { url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" }, + { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" }, + { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" }, + { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" }, + { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" }, + { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" }, + { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydub" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[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/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { 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 = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "safehttpx" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/d1/4282284d9cf1ee873607a46442da977fc3c985059315ab23610be31d5885/safehttpx-0.1.7.tar.gz", hash = "sha256:db201c0978c41eddb8bb480f3eee59dd67304fdd91646035e9d9a720049a9d23", size = 10385, upload-time = "2025-10-24T18:30:09.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/a3/0f0b7d78e2f1eb9e8e1afbff1d2bff8d60144aee17aca51c065b516743dd/safehttpx-0.1.7-py3-none-any.whl", hash = "sha256:c4f4a162db6993464d7ca3d7cc4af0ffc6515a606dfd220b9f82c6945d869cde", size = 8959, upload-time = "2025-10-24T18:30:08.733Z" }, +] + +[[package]] +name = "semantic-version" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, +] + +[[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 = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "typer" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/30/ff9ede605e3bd086b4dd842499814e128500621f7951ca1e5ce84bbf61b1/typer-0.21.0.tar.gz", hash = "sha256:c87c0d2b6eee3b49c5c64649ec92425492c14488096dfbc8a0c2799b2f6f9c53", size = 106781, upload-time = "2025-12-25T09:54:53.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e4/5ebc1899d31d2b1601b32d21cfb4bba022ae6fce323d365f0448031b1660/typer-0.21.0-py3-none-any.whl", hash = "sha256:c79c01ca6b30af9fd48284058a7056ba0d3bf5cf10d0ff3d0c5b11b68c258ac6", size = 47109, upload-time = "2025-12-25T09:54:51.918Z" }, +] + +[[package]] +name = "typer-slim" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/3b/2f60ce16f578b1db5b8816d37d6a4d9786b33b76407fc8c13b0b86312c31/typer_slim-0.21.0.tar.gz", hash = "sha256:f2dbd150cfa0fead2242e21fa9f654dfc64773763ddf07c6be9a49ad34f79557", size = 106841, upload-time = "2025-12-25T09:54:55.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/84/e97abf10e4a699194ff07fd586ec7f4cf867d9d04bead559a65f9e7aff84/typer_slim-0.21.0-py3-none-any.whl", hash = "sha256:92aee2188ac6fc2b2924bd75bb61a340b78bd8cd51fd9735533ce5a856812c8e", size = 47174, upload-time = "2025-12-25T09:54:54.609Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[[package]] +name = "video-analyzer" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "gradio" }, +] + +[package.metadata] +requires-dist = [{ name = "gradio", specifier = ">=6.0.0" }] diff --git a/video-analyzer b/video-analyzer deleted file mode 100755 index d274cd0943dc990b84be04388a306762546717f2..0000000000000000000000000000000000000000 --- a/video-analyzer +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -# Video Analyzer CLI wrapper - -# Add local bin paths -export PATH="$PATH:$HOME/.local/bin:$HOME/.deno/bin" - -# Run the CLI -python3 -m src.main "$@"