feat: add skill-seekers video --setup for GPU auto-detection and dependency installation
Auto-detects NVIDIA (CUDA), AMD (ROCm), or CPU-only GPU and installs the correct PyTorch variant + easyocr + all visual extraction dependencies. Removes easyocr from video-full pip extras to avoid pulling ~2GB of wrong CUDA packages on non-NVIDIA systems. New files: - video_setup.py (835 lines): GPU detection, PyTorch install, ROCm config, venv checks, system dep validation, module selection, verification - test_video_setup.py (60 tests): Full coverage of detection, install, verify Updated docs: CHANGELOG, AGENTS.md, CLAUDE.md, README.md, CLI_REFERENCE, FAQ, TROUBLESHOOTING, installation guide, video dependency plan All 2523 tests passing (15 skipped). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
75
AGENTS.md
75
AGENTS.md
@@ -1,12 +1,12 @@
|
|||||||
# AGENTS.md - Skill Seekers
|
# AGENTS.md - Skill Seekers
|
||||||
|
|
||||||
This file provides essential guidance for AI coding agents working with the Skill Seekers codebase.
|
Essential guidance for AI coding agents working with the Skill Seekers codebase.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
**Skill Seekers** is a Python CLI tool that converts documentation websites, GitHub repositories, and PDF files into AI-ready skills for LLM platforms and RAG (Retrieval-Augmented Generation) pipelines. It serves as the universal preprocessing layer for AI systems.
|
**Skill Seekers** is a Python CLI tool that converts documentation websites, GitHub repositories, PDF files, and videos into AI-ready skills for LLM platforms and RAG (Retrieval-Augmented Generation) pipelines. It serves as the universal preprocessing layer for AI systems.
|
||||||
|
|
||||||
### Key Facts
|
### Key Facts
|
||||||
|
|
||||||
@@ -16,8 +16,8 @@ This file provides essential guidance for AI coding agents working with the Skil
|
|||||||
| **Python Version** | 3.10+ (tested on 3.10, 3.11, 3.12, 3.13) |
|
| **Python Version** | 3.10+ (tested on 3.10, 3.11, 3.12, 3.13) |
|
||||||
| **License** | MIT |
|
| **License** | MIT |
|
||||||
| **Package Name** | `skill-seekers` (PyPI) |
|
| **Package Name** | `skill-seekers` (PyPI) |
|
||||||
| **Source Files** | 169 Python files |
|
| **Source Files** | 182 Python files |
|
||||||
| **Test Files** | 101 test files |
|
| **Test Files** | 105+ test files |
|
||||||
| **Website** | https://skillseekersweb.com/ |
|
| **Website** | https://skillseekersweb.com/ |
|
||||||
| **Repository** | https://github.com/yusufkaraaslan/Skill_Seekers |
|
| **Repository** | https://github.com/yusufkaraaslan/Skill_Seekers |
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ This file provides essential guidance for AI coding agents working with the Skil
|
|||||||
|
|
||||||
### Core Workflow
|
### Core Workflow
|
||||||
|
|
||||||
1. **Scrape Phase** - Crawl documentation/GitHub/PDF sources
|
1. **Scrape Phase** - Crawl documentation/GitHub/PDF/video sources
|
||||||
2. **Build Phase** - Organize content into categorized references
|
2. **Build Phase** - Organize content into categorized references
|
||||||
3. **Enhancement Phase** - AI-powered quality improvements (optional)
|
3. **Enhancement Phase** - AI-powered quality improvements (optional)
|
||||||
4. **Package Phase** - Create platform-specific packages
|
4. **Package Phase** - Create platform-specific packages
|
||||||
@@ -73,12 +73,18 @@ This file provides essential guidance for AI coding agents working with the Skil
|
|||||||
│ │ │ ├── weaviate.py # Weaviate vector DB adaptor
|
│ │ │ ├── weaviate.py # Weaviate vector DB adaptor
|
||||||
│ │ │ └── streaming_adaptor.py # Streaming output adaptor
|
│ │ │ └── streaming_adaptor.py # Streaming output adaptor
|
||||||
│ │ ├── arguments/ # CLI argument definitions
|
│ │ ├── arguments/ # CLI argument definitions
|
||||||
|
│ │ ├── parsers/ # Argument parsers
|
||||||
|
│ │ │ └── extractors/ # Content extractors
|
||||||
│ │ ├── presets/ # Preset configuration management
|
│ │ ├── presets/ # Preset configuration management
|
||||||
|
│ │ ├── storage/ # Cloud storage adaptors
|
||||||
│ │ ├── main.py # Unified CLI entry point
|
│ │ ├── main.py # Unified CLI entry point
|
||||||
│ │ ├── create_command.py # Unified create command
|
│ │ ├── create_command.py # Unified create command
|
||||||
│ │ ├── doc_scraper.py # Documentation scraper
|
│ │ ├── doc_scraper.py # Documentation scraper
|
||||||
│ │ ├── github_scraper.py # GitHub repository scraper
|
│ │ ├── github_scraper.py # GitHub repository scraper
|
||||||
│ │ ├── pdf_scraper.py # PDF extraction
|
│ │ ├── pdf_scraper.py # PDF extraction
|
||||||
|
│ │ ├── word_scraper.py # Word document scraper
|
||||||
|
│ │ ├── video_scraper.py # Video extraction
|
||||||
|
│ │ ├── video_setup.py # GPU detection & dependency installation
|
||||||
│ │ ├── unified_scraper.py # Multi-source scraping
|
│ │ ├── unified_scraper.py # Multi-source scraping
|
||||||
│ │ ├── codebase_scraper.py # Local codebase analysis
|
│ │ ├── codebase_scraper.py # Local codebase analysis
|
||||||
│ │ ├── enhance_command.py # AI enhancement command
|
│ │ ├── enhance_command.py # AI enhancement command
|
||||||
@@ -118,10 +124,10 @@ This file provides essential guidance for AI coding agents working with the Skil
|
|||||||
│ │ ├── generator.py # Embedding generation
|
│ │ ├── generator.py # Embedding generation
|
||||||
│ │ ├── cache.py # Embedding cache
|
│ │ ├── cache.py # Embedding cache
|
||||||
│ │ └── models.py # Embedding models
|
│ │ └── models.py # Embedding models
|
||||||
│ ├── workflows/ # YAML workflow presets
|
│ ├── workflows/ # YAML workflow presets (66 presets)
|
||||||
│ ├── _version.py # Version information (reads from pyproject.toml)
|
│ ├── _version.py # Version information (reads from pyproject.toml)
|
||||||
│ └── __init__.py # Package init
|
│ └── __init__.py # Package init
|
||||||
├── tests/ # Test suite (101 test files)
|
├── tests/ # Test suite (105+ test files)
|
||||||
├── configs/ # Preset configuration files
|
├── configs/ # Preset configuration files
|
||||||
├── docs/ # Documentation (80+ markdown files)
|
├── docs/ # Documentation (80+ markdown files)
|
||||||
│ ├── integrations/ # Platform integration guides
|
│ ├── integrations/ # Platform integration guides
|
||||||
@@ -245,9 +251,8 @@ pytest tests/ -v -m "not slow and not integration"
|
|||||||
|
|
||||||
### Test Architecture
|
### Test Architecture
|
||||||
|
|
||||||
- **101 test files** covering all features
|
- **105+ test files** covering all features
|
||||||
- **1880+ tests** passing
|
- **CI Matrix:** Ubuntu + macOS, Python 3.10-3.12
|
||||||
- CI Matrix: Ubuntu + macOS, Python 3.10-3.12
|
|
||||||
- Test markers defined in `pyproject.toml`:
|
- Test markers defined in `pyproject.toml`:
|
||||||
|
|
||||||
| Marker | Description |
|
| Marker | Description |
|
||||||
@@ -376,6 +381,8 @@ The CLI uses subcommands that delegate to existing modules:
|
|||||||
- `scrape` - Documentation scraping
|
- `scrape` - Documentation scraping
|
||||||
- `github` - GitHub repository scraping
|
- `github` - GitHub repository scraping
|
||||||
- `pdf` - PDF extraction
|
- `pdf` - PDF extraction
|
||||||
|
- `word` - Word document extraction
|
||||||
|
- `video` - Video extraction (YouTube or local). Use `--setup` to auto-detect GPU and install visual deps.
|
||||||
- `unified` - Multi-source scraping
|
- `unified` - Multi-source scraping
|
||||||
- `analyze` / `codebase` - Local codebase analysis
|
- `analyze` / `codebase` - Local codebase analysis
|
||||||
- `enhance` - AI enhancement
|
- `enhance` - AI enhancement
|
||||||
@@ -402,7 +409,7 @@ Two implementations:
|
|||||||
|
|
||||||
Tools are organized by category:
|
Tools are organized by category:
|
||||||
- Config tools (3 tools): generate_config, list_configs, validate_config
|
- Config tools (3 tools): generate_config, list_configs, validate_config
|
||||||
- Scraping tools (9 tools): estimate_pages, scrape_docs, scrape_github, scrape_pdf, scrape_codebase, detect_patterns, extract_test_examples, build_how_to_guides, extract_config_patterns
|
- Scraping tools (10 tools): estimate_pages, scrape_docs, scrape_github, scrape_pdf, scrape_video (supports `setup` parameter for GPU detection and visual dep installation), scrape_codebase, detect_patterns, extract_test_examples, build_how_to_guides, extract_config_patterns
|
||||||
- Packaging tools (4 tools): package_skill, upload_skill, enhance_skill, install_skill
|
- Packaging tools (4 tools): package_skill, upload_skill, enhance_skill, install_skill
|
||||||
- Source tools (5 tools): fetch_config, submit_config, add_config_source, list_config_sources, remove_config_source
|
- Source tools (5 tools): fetch_config, submit_config, add_config_source, list_config_sources, remove_config_source
|
||||||
- Splitting tools (2 tools): split_config, generate_router
|
- Splitting tools (2 tools): split_config, generate_router
|
||||||
@@ -619,7 +626,7 @@ export ANTHROPIC_BASE_URL=https://custom-endpoint.com/v1
|
|||||||
|
|
||||||
**Reference (technical details):**
|
**Reference (technical details):**
|
||||||
- `docs/reference/CLI_REFERENCE.md` - Complete command reference (20 commands)
|
- `docs/reference/CLI_REFERENCE.md` - Complete command reference (20 commands)
|
||||||
- `docs/reference/MCP_REFERENCE.md` - MCP tools reference (26 tools)
|
- `docs/reference/MCP_REFERENCE.md` - MCP tools reference (33 tools)
|
||||||
- `docs/reference/CONFIG_FORMAT.md` - JSON configuration specification
|
- `docs/reference/CONFIG_FORMAT.md` - JSON configuration specification
|
||||||
- `docs/reference/ENVIRONMENT_VARIABLES.md` - All environment variables
|
- `docs/reference/ENVIRONMENT_VARIABLES.md` - All environment variables
|
||||||
|
|
||||||
@@ -629,20 +636,16 @@ export ANTHROPIC_BASE_URL=https://custom-endpoint.com/v1
|
|||||||
- `docs/advanced/custom-workflows.md` - Creating custom workflows
|
- `docs/advanced/custom-workflows.md` - Creating custom workflows
|
||||||
- `docs/advanced/multi-source.md` - Multi-source scraping
|
- `docs/advanced/multi-source.md` - Multi-source scraping
|
||||||
|
|
||||||
**Legacy (being phased out):**
|
|
||||||
- `QUICKSTART.md` - Old quick start (see docs/getting-started/)
|
|
||||||
- `docs/guides/USAGE.md` - Old usage guide (see docs/user-guide/)
|
|
||||||
- `docs/QUICK_REFERENCE.md` - Old reference (see docs/reference/)
|
|
||||||
|
|
||||||
### Configuration Documentation
|
### Configuration Documentation
|
||||||
|
|
||||||
Preset configs are in `configs/` directory:
|
Preset configs are in `configs/` directory:
|
||||||
- `godot.json` - Godot Engine
|
- `godot.json` / `godot_unified.json` - Godot Engine
|
||||||
- `blender.json` / `blender-unified.json` - Blender Engine
|
- `blender.json` / `blender-unified.json` - Blender Engine
|
||||||
- `claude-code.json` - Claude Code
|
- `claude-code.json` - Claude Code
|
||||||
- `httpx_comprehensive.json` - HTTPX library
|
- `httpx_comprehensive.json` - HTTPX library
|
||||||
- `medusa-mercurjs.json` - Medusa/MercurJS
|
- `medusa-mercurjs.json` - Medusa/MercurJS
|
||||||
- `astrovalley_unified.json` - Astrovalley
|
- `astrovalley_unified.json` - Astrovalley
|
||||||
|
- `react.json` - React documentation
|
||||||
- `configs/integrations/` - Integration-specific configs
|
- `configs/integrations/` - Integration-specific configs
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -685,8 +688,13 @@ Preset configs are in `configs/` directory:
|
|||||||
| AWS S3 | `boto3>=1.34.0` | `pip install -e ".[s3]"` |
|
| AWS S3 | `boto3>=1.34.0` | `pip install -e ".[s3]"` |
|
||||||
| Google Cloud Storage | `google-cloud-storage>=2.10.0` | `pip install -e ".[gcs]"` |
|
| Google Cloud Storage | `google-cloud-storage>=2.10.0` | `pip install -e ".[gcs]"` |
|
||||||
| Azure Blob Storage | `azure-storage-blob>=12.19.0` | `pip install -e ".[azure]"` |
|
| Azure Blob Storage | `azure-storage-blob>=12.19.0` | `pip install -e ".[azure]"` |
|
||||||
|
| Word Documents | `mammoth>=1.6.0`, `python-docx>=1.1.0` | `pip install -e ".[docx]"` |
|
||||||
|
| Video (lightweight) | `yt-dlp>=2024.12.0`, `youtube-transcript-api>=1.2.0` | `pip install -e ".[video]"` |
|
||||||
|
| Video (full) | +`faster-whisper`, `scenedetect`, `opencv-python-headless` (`easyocr` now installed via `--setup`) | `pip install -e ".[video-full]"` |
|
||||||
|
| Video (GPU setup) | Auto-detects GPU, installs PyTorch + easyocr + all visual deps | `skill-seekers video --setup` |
|
||||||
| Chroma DB | `chromadb>=0.4.0` | `pip install -e ".[chroma]"` |
|
| Chroma DB | `chromadb>=0.4.0` | `pip install -e ".[chroma]"` |
|
||||||
| Weaviate | `weaviate-client>=3.25.0` | `pip install -e ".[weaviate]"` |
|
| Weaviate | `weaviate-client>=3.25.0` | `pip install -e ".[weaviate]"` |
|
||||||
|
| Pinecone | `pinecone>=5.0.0` | `pip install -e ".[pinecone]"` |
|
||||||
| Embedding Server | `fastapi>=0.109.0`, `uvicorn>=0.27.0`, `sentence-transformers>=2.3.0` | `pip install -e ".[embedding]"` |
|
| Embedding Server | `fastapi>=0.109.0`, `uvicorn>=0.27.0`, `sentence-transformers>=2.3.0` | `pip install -e ".[embedding]"` |
|
||||||
|
|
||||||
### Dev Dependencies (in dependency-groups)
|
### Dev Dependencies (in dependency-groups)
|
||||||
@@ -702,6 +710,7 @@ Preset configs are in `configs/` directory:
|
|||||||
| `psutil` | >=5.9.0 | Process utilities for testing |
|
| `psutil` | >=5.9.0 | Process utilities for testing |
|
||||||
| `numpy` | >=1.24.0 | Numerical operations |
|
| `numpy` | >=1.24.0 | Numerical operations |
|
||||||
| `starlette` | >=0.31.0 | HTTP transport testing |
|
| `starlette` | >=0.31.0 | HTTP transport testing |
|
||||||
|
| `httpx` | >=0.24.0 | HTTP client for testing |
|
||||||
| `boto3` | >=1.26.0 | AWS S3 testing |
|
| `boto3` | >=1.26.0 | AWS S3 testing |
|
||||||
| `google-cloud-storage` | >=2.10.0 | GCS testing |
|
| `google-cloud-storage` | >=2.10.0 | GCS testing |
|
||||||
| `azure-storage-blob` | >=12.17.0 | Azure testing |
|
| `azure-storage-blob` | >=12.17.0 | Azure testing |
|
||||||
@@ -824,6 +833,34 @@ Skill Seekers uses JSON configuration files to define scraping targets. Example
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Workflow Presets
|
||||||
|
|
||||||
|
Skill Seekers includes 66 YAML workflow presets for AI enhancement in `src/skill_seekers/workflows/`:
|
||||||
|
|
||||||
|
**Built-in presets:**
|
||||||
|
- `default.yaml` - Standard enhancement workflow
|
||||||
|
- `minimal.yaml` - Fast, minimal enhancement
|
||||||
|
- `security-focus.yaml` - Security-focused review
|
||||||
|
- `architecture-comprehensive.yaml` - Deep architecture analysis
|
||||||
|
- `api-documentation.yaml` - API documentation focus
|
||||||
|
- And 61 more specialized presets...
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
# Apply a preset
|
||||||
|
skill-seekers create ./my-project --enhance-workflow security-focus
|
||||||
|
|
||||||
|
# Chain multiple presets
|
||||||
|
skill-seekers create ./my-project --enhance-workflow security-focus --enhance-workflow minimal
|
||||||
|
|
||||||
|
# Manage presets
|
||||||
|
skill-seekers workflows list
|
||||||
|
skill-seekers workflows show security-focus
|
||||||
|
skill-seekers workflows copy security-focus
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
*This document is maintained for AI coding agents. For human contributors, see README.md and CONTRIBUTING.md.*
|
*This document is maintained for AI coding agents. For human contributors, see README.md and CONTRIBUTING.md.*
|
||||||
|
|
||||||
*Last updated: 2026-02-24*
|
*Last updated: 2026-03-01*
|
||||||
|
|||||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### 🎬 Video `--setup`: GPU Auto-Detection & Dependency Installation
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **`skill-seekers video --setup`** — One-command GPU auto-detection and dependency installation for the video scraper pipeline
|
||||||
|
- `video_setup.py` (~835 lines) — New module with complete setup orchestration
|
||||||
|
- **GPU auto-detection** — Detects NVIDIA (nvidia-smi → CUDA version), AMD (rocminfo → ROCm version), or CPU-only without requiring PyTorch
|
||||||
|
- **Correct PyTorch variant** — Installs from the right index URL: `cu124`/`cu121`/`cu118` for NVIDIA, `rocm6.3`/`rocm6.2.4` for AMD, `cpu` for CPU-only
|
||||||
|
- **ROCm configuration** — Sets `MIOPEN_FIND_MODE=FAST` and `HSA_OVERRIDE_GFX_VERSION` for AMD GPUs (fixes MIOpen workspace allocation issues)
|
||||||
|
- **Virtual environment detection** — Warns users outside a venv with opt-in `--force` override
|
||||||
|
- **System dependency checks** — Validates `tesseract` and `ffmpeg` binaries, provides OS-specific install instructions
|
||||||
|
- **Module selection** — `SetupModules` dataclass for optional component selection (easyocr, opencv, tesseract, scenedetect, whisper)
|
||||||
|
- **Base video deps always included** — `yt-dlp` and `youtube-transcript-api` installed automatically so video pipeline is fully ready after setup
|
||||||
|
- **Verification step** — Post-install import checks for all deps including `torch.cuda.is_available()` and `torch.version.hip`
|
||||||
|
- **Non-interactive mode** — `run_setup(interactive=False)` for MCP server and CI/CD use
|
||||||
|
- **`--setup` flag** in `arguments/video.py` — Added to `VIDEO_ARGUMENTS` dict
|
||||||
|
- **Early-exit in `video_scraper.py`** — `--setup` runs before source validation (no `--url` required)
|
||||||
|
- **MCP `scrape_video` setup parameter** — `setup: bool = False` param in `server_fastmcp.py` and `scraping_tools.py`
|
||||||
|
- **`create` command routing** — `create_command.py` forwards `--setup` to video scraper
|
||||||
|
- **`tests/test_video_setup.py`** (60 tests) — GPU detection, CUDA/ROCm version mapping, installation, verification, venv checks, system deps, module selection, argument parsing
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **`easyocr` removed from `video-full` optional deps** — Was pulling ~2GB of NVIDIA CUDA packages regardless of GPU vendor. Now installed via `--setup` with correct PyTorch variant.
|
||||||
|
- **Video dependency error messages** — `video_scraper.py` and `video_visual.py` now suggest `skill-seekers video --setup` as the primary fix
|
||||||
|
- **Multi-engine OCR** — `video_visual.py` uses EasyOCR + pytesseract ensemble for code frames (per-line confidence merge with code-token preference), EasyOCR only for non-code frames
|
||||||
|
- **Tesseract circuit breaker** — `_tesseract_broken` flag disables pytesseract for the session after first failure, avoiding repeated subprocess errors
|
||||||
|
- **`video_models.py`** — Added `SetupModules` dataclass for granular dependency control
|
||||||
|
- **`video_segmenter.py`** — Updated dependency check messages to reference `--setup`
|
||||||
|
|
||||||
### 📄 B2: Microsoft Word (.docx) Support & Stage 1 Quality Improvements
|
### 📄 B2: Microsoft Word (.docx) Support & Stage 1 Quality Improvements
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
13
CLAUDE.md
13
CLAUDE.md
@@ -341,6 +341,9 @@ skill-seekers how-to-guides output/test_examples.json --output output/guides/
|
|||||||
# Test enhancement status monitoring
|
# Test enhancement status monitoring
|
||||||
skill-seekers enhance-status output/react/ --watch
|
skill-seekers enhance-status output/react/ --watch
|
||||||
|
|
||||||
|
# Video setup (auto-detect GPU and install deps)
|
||||||
|
skill-seekers video --setup
|
||||||
|
|
||||||
# Test multi-platform packaging
|
# Test multi-platform packaging
|
||||||
skill-seekers package output/react/ --target gemini --dry-run
|
skill-seekers package output/react/ --target gemini --dry-run
|
||||||
|
|
||||||
@@ -750,6 +753,7 @@ skill-seekers-install-agent = "skill_seekers.cli.install_agent:main"
|
|||||||
skill-seekers-patterns = "skill_seekers.cli.pattern_recognizer:main" # C3.1 Pattern detection
|
skill-seekers-patterns = "skill_seekers.cli.pattern_recognizer:main" # C3.1 Pattern detection
|
||||||
skill-seekers-how-to-guides = "skill_seekers.cli.how_to_guide_builder:main" # C3.3 Guide generation
|
skill-seekers-how-to-guides = "skill_seekers.cli.how_to_guide_builder:main" # C3.3 Guide generation
|
||||||
skill-seekers-workflows = "skill_seekers.cli.workflows_command:main" # NEW: Workflow preset management
|
skill-seekers-workflows = "skill_seekers.cli.workflows_command:main" # NEW: Workflow preset management
|
||||||
|
skill-seekers-video = "skill_seekers.cli.video_scraper:main" # Video scraping pipeline (use --setup to install deps)
|
||||||
|
|
||||||
# New v3.0.0 Entry Points
|
# New v3.0.0 Entry Points
|
||||||
skill-seekers-setup = "skill_seekers.cli.setup_wizard:main" # NEW: v3.0.0 Setup wizard
|
skill-seekers-setup = "skill_seekers.cli.setup_wizard:main" # NEW: v3.0.0 Setup wizard
|
||||||
@@ -771,6 +775,8 @@ skill-seekers-quality = "skill_seekers.cli.quality_metrics:main" # N
|
|||||||
- Install with: `pip install -e .` (installs only core deps)
|
- Install with: `pip install -e .` (installs only core deps)
|
||||||
- Install dev deps: See CI workflow or manually install pytest, ruff, mypy
|
- Install dev deps: See CI workflow or manually install pytest, ruff, mypy
|
||||||
|
|
||||||
|
**Note on video dependencies:** `easyocr` and GPU-specific PyTorch builds are **not** included in the `video-full` optional dependency group. They are installed at runtime by `skill-seekers video --setup`, which auto-detects the GPU (CUDA/ROCm/MPS/CPU) and installs the correct builds.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
gemini = ["google-generativeai>=0.8.0"]
|
gemini = ["google-generativeai>=0.8.0"]
|
||||||
@@ -1985,6 +1991,13 @@ UNIVERSAL_ARGUMENTS = {
|
|||||||
- Profile creation
|
- Profile creation
|
||||||
- First-time setup
|
- First-time setup
|
||||||
|
|
||||||
|
**Video Scraper** (`src/skill_seekers/cli/`):
|
||||||
|
- `video_scraper.py` - Main video scraping pipeline CLI
|
||||||
|
- `video_setup.py` - GPU auto-detection, PyTorch installation, visual dependency setup (~835 lines)
|
||||||
|
- Detects CUDA/ROCm/MPS/CPU and installs matching PyTorch build
|
||||||
|
- Installs `easyocr` and other visual processing deps at runtime via `--setup`
|
||||||
|
- Run `skill-seekers video --setup` before first use
|
||||||
|
|
||||||
## 🎯 Project-Specific Best Practices
|
## 🎯 Project-Specific Best Practices
|
||||||
|
|
||||||
1. **Prefer the unified `create` command** - Use `skill-seekers create <source>` over legacy commands for consistency
|
1. **Prefer the unified `create` command** - Use `skill-seekers create <source>` over legacy commands for consistency
|
||||||
|
|||||||
34
README.md
34
README.md
@@ -92,6 +92,11 @@ skill-seekers create ./my-project
|
|||||||
|
|
||||||
# PDF document
|
# PDF document
|
||||||
skill-seekers create manual.pdf
|
skill-seekers create manual.pdf
|
||||||
|
|
||||||
|
# Video (YouTube, Vimeo, or local file — requires skill-seekers[video])
|
||||||
|
skill-seekers video --url https://www.youtube.com/watch?v=... --name mytutorial
|
||||||
|
# First time? Auto-install GPU-aware visual deps:
|
||||||
|
skill-seekers video --setup
|
||||||
```
|
```
|
||||||
|
|
||||||
### Export Everywhere
|
### Export Everywhere
|
||||||
@@ -593,8 +598,14 @@ skill-seekers-setup
|
|||||||
| `pip install skill-seekers[openai]` | + OpenAI ChatGPT support |
|
| `pip install skill-seekers[openai]` | + OpenAI ChatGPT support |
|
||||||
| `pip install skill-seekers[all-llms]` | + All LLM platforms |
|
| `pip install skill-seekers[all-llms]` | + All LLM platforms |
|
||||||
| `pip install skill-seekers[mcp]` | + MCP server for Claude Code, Cursor, etc. |
|
| `pip install skill-seekers[mcp]` | + MCP server for Claude Code, Cursor, etc. |
|
||||||
|
| `pip install skill-seekers[video]` | + YouTube/Vimeo transcript & metadata extraction |
|
||||||
|
| `pip install skill-seekers[video-full]` | + Whisper transcription & visual frame extraction |
|
||||||
| `pip install skill-seekers[all]` | Everything enabled |
|
| `pip install skill-seekers[all]` | Everything enabled |
|
||||||
|
|
||||||
|
> **Video visual deps (GPU-aware):** After installing `skill-seekers[video-full]`, run
|
||||||
|
> `skill-seekers video --setup` to auto-detect your GPU and install the correct PyTorch
|
||||||
|
> variant + easyocr. This is the recommended way to install visual extraction dependencies.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 One-Command Install Workflow
|
## 🚀 One-Command Install Workflow
|
||||||
@@ -683,6 +694,29 @@ skill-seekers pdf --pdf docs/manual.pdf --name myskill \
|
|||||||
skill-seekers pdf --pdf docs/scanned.pdf --name myskill --ocr
|
skill-seekers pdf --pdf docs/scanned.pdf --name myskill --ocr
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Video Extraction
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install video support
|
||||||
|
pip install skill-seekers[video] # Transcripts + metadata
|
||||||
|
pip install skill-seekers[video-full] # + Whisper + visual frame extraction
|
||||||
|
|
||||||
|
# Auto-detect GPU and install visual deps (PyTorch + easyocr)
|
||||||
|
skill-seekers video --setup
|
||||||
|
|
||||||
|
# Extract from YouTube video
|
||||||
|
skill-seekers video --url https://www.youtube.com/watch?v=dQw4w9WgXcQ --name mytutorial
|
||||||
|
|
||||||
|
# Extract from a YouTube playlist
|
||||||
|
skill-seekers video --playlist https://www.youtube.com/playlist?list=... --name myplaylist
|
||||||
|
|
||||||
|
# Extract from a local video file
|
||||||
|
skill-seekers video --video-file recording.mp4 --name myrecording
|
||||||
|
|
||||||
|
# Extract with visual frame analysis (requires video-full deps)
|
||||||
|
skill-seekers video --url https://www.youtube.com/watch?v=... --name mytutorial --visual
|
||||||
|
```
|
||||||
|
|
||||||
### GitHub Repository Analysis
|
### GitHub Repository Analysis
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
18
docs/FAQ.md
18
docs/FAQ.md
@@ -59,6 +59,24 @@ Each platform has a dedicated adaptor for optimal formatting and upload.
|
|||||||
|
|
||||||
**Recommendation:** Use LOCAL mode for free AI enhancement or skip enhancement entirely.
|
**Recommendation:** Use LOCAL mode for free AI enhancement or skip enhancement entirely.
|
||||||
|
|
||||||
|
### How do I set up video extraction?
|
||||||
|
|
||||||
|
**Quick setup:**
|
||||||
|
```bash
|
||||||
|
# 1. Install video support
|
||||||
|
pip install skill-seekers[video-full]
|
||||||
|
|
||||||
|
# 2. Auto-detect GPU and install visual deps
|
||||||
|
skill-seekers video --setup
|
||||||
|
```
|
||||||
|
|
||||||
|
The `--setup` command auto-detects your GPU vendor (NVIDIA CUDA, AMD ROCm, or CPU-only) and installs the correct PyTorch variant along with easyocr and other visual extraction dependencies. This avoids the ~2GB NVIDIA CUDA download that would happen if easyocr were installed via pip on non-NVIDIA systems.
|
||||||
|
|
||||||
|
**What it detects:**
|
||||||
|
- **NVIDIA:** Uses `nvidia-smi` to find CUDA version → installs matching `cu124`/`cu121`/`cu118` PyTorch
|
||||||
|
- **AMD:** Uses `rocminfo` to find ROCm version → installs matching ROCm PyTorch
|
||||||
|
- **CPU-only:** Installs lightweight CPU-only PyTorch
|
||||||
|
|
||||||
### How long does it take to create a skill?
|
### How long does it take to create a skill?
|
||||||
|
|
||||||
**Typical Times:**
|
**Typical Times:**
|
||||||
|
|||||||
@@ -90,6 +90,35 @@ pyenv install 3.12
|
|||||||
pyenv global 3.12
|
pyenv global 3.12
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Issue: Video Visual Dependencies Missing
|
||||||
|
|
||||||
|
**Symptoms:**
|
||||||
|
```
|
||||||
|
Missing video dependencies: easyocr
|
||||||
|
RuntimeError: Required video visual dependencies not installed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run the GPU-aware setup command
|
||||||
|
skill-seekers video --setup
|
||||||
|
|
||||||
|
# This auto-detects your GPU and installs:
|
||||||
|
# - PyTorch (correct CUDA/ROCm/CPU variant)
|
||||||
|
# - easyocr, opencv, pytesseract, scenedetect, faster-whisper
|
||||||
|
# - yt-dlp, youtube-transcript-api
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
python -c "import torch; print(f'PyTorch: {torch.__version__}, CUDA: {torch.cuda.is_available()}')"
|
||||||
|
python -c "import easyocr; print('easyocr OK')"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common issues:**
|
||||||
|
- Running outside a virtual environment → `--setup` will warn you; create a venv first
|
||||||
|
- Missing system packages → Install `tesseract-ocr` and `ffmpeg` for your OS
|
||||||
|
- AMD GPU without ROCm → Install ROCm first, then re-run `--setup`
|
||||||
|
|
||||||
## Configuration Issues
|
## Configuration Issues
|
||||||
|
|
||||||
### Issue: API Keys Not Recognized
|
### Issue: API Keys Not Recognized
|
||||||
|
|||||||
@@ -124,10 +124,14 @@ pip install skill-seekers[dev]
|
|||||||
| `gcs` | Google Cloud Storage | `pip install skill-seekers[gcs]` |
|
| `gcs` | Google Cloud Storage | `pip install skill-seekers[gcs]` |
|
||||||
| `azure` | Azure Blob Storage | `pip install skill-seekers[azure]` |
|
| `azure` | Azure Blob Storage | `pip install skill-seekers[azure]` |
|
||||||
| `embedding` | Embedding server | `pip install skill-seekers[embedding]` |
|
| `embedding` | Embedding server | `pip install skill-seekers[embedding]` |
|
||||||
|
| `video` | YouTube/video transcript extraction | `pip install skill-seekers[video]` |
|
||||||
|
| `video-full` | + Whisper transcription, scene detection | `pip install skill-seekers[video-full]` |
|
||||||
| `all-llms` | All LLM platforms | `pip install skill-seekers[all-llms]` |
|
| `all-llms` | All LLM platforms | `pip install skill-seekers[all-llms]` |
|
||||||
| `all` | Everything | `pip install skill-seekers[all]` |
|
| `all` | Everything | `pip install skill-seekers[all]` |
|
||||||
| `dev` | Development tools | `pip install skill-seekers[dev]` |
|
| `dev` | Development tools | `pip install skill-seekers[dev]` |
|
||||||
|
|
||||||
|
> **Video visual deps:** After installing `skill-seekers[video-full]`, run `skill-seekers video --setup` to auto-detect your GPU (NVIDIA/AMD/CPU) and install the correct PyTorch variant + easyocr.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Post-Installation Setup
|
## Post-Installation Setup
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ All languages supported by:
|
|||||||
| [`04_VIDEO_INTEGRATION.md`](./04_VIDEO_INTEGRATION.md) | CLI, config, source detection, unified scraper integration |
|
| [`04_VIDEO_INTEGRATION.md`](./04_VIDEO_INTEGRATION.md) | CLI, config, source detection, unified scraper integration |
|
||||||
| [`05_VIDEO_OUTPUT.md`](./05_VIDEO_OUTPUT.md) | Output structure, SKILL.md integration, reference file format |
|
| [`05_VIDEO_OUTPUT.md`](./05_VIDEO_OUTPUT.md) | Output structure, SKILL.md integration, reference file format |
|
||||||
| [`06_VIDEO_TESTING.md`](./06_VIDEO_TESTING.md) | Test strategy, mocking, fixtures, CI considerations |
|
| [`06_VIDEO_TESTING.md`](./06_VIDEO_TESTING.md) | Test strategy, mocking, fixtures, CI considerations |
|
||||||
| [`07_VIDEO_DEPENDENCIES.md`](./07_VIDEO_DEPENDENCIES.md) | Dependency tiers, optional installs, system requirements |
|
| [`07_VIDEO_DEPENDENCIES.md`](./07_VIDEO_DEPENDENCIES.md) | Dependency tiers, optional installs, system requirements — **IMPLEMENTED** (`video_setup.py`, GPU auto-detection, `--setup`) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,15 @@
|
|||||||
**Document:** 07 of 07
|
**Document:** 07 of 07
|
||||||
**Status:** Planning
|
**Status:** Planning
|
||||||
|
|
||||||
|
> **Status: IMPLEMENTED** — `skill-seekers video --setup` (see `video_setup.py`, 835 lines, 60 tests)
|
||||||
|
> - GPU auto-detection: NVIDIA (nvidia-smi/CUDA), AMD (rocminfo/ROCm), CPU fallback
|
||||||
|
> - Correct PyTorch index URL selection per GPU vendor
|
||||||
|
> - EasyOCR removed from pip extras, installed at runtime via --setup
|
||||||
|
> - ROCm configuration (MIOPEN_FIND_MODE, HSA_OVERRIDE_GFX_VERSION)
|
||||||
|
> - Virtual environment detection with --force override
|
||||||
|
> - System dependency checks (tesseract, ffmpeg)
|
||||||
|
> - Non-interactive mode for MCP/CI usage
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
- [unified](#unified) - Multi-source scraping
|
- [unified](#unified) - Multi-source scraping
|
||||||
- [update](#update) - Incremental updates
|
- [update](#update) - Incremental updates
|
||||||
- [upload](#upload) - Upload to platform
|
- [upload](#upload) - Upload to platform
|
||||||
|
- [video](#video) - Video extraction & setup
|
||||||
- [workflows](#workflows) - Manage workflow presets
|
- [workflows](#workflows) - Manage workflow presets
|
||||||
- [Common Workflows](#common-workflows)
|
- [Common Workflows](#common-workflows)
|
||||||
- [Exit Codes](#exit-codes)
|
- [Exit Codes](#exit-codes)
|
||||||
@@ -1035,6 +1036,44 @@ skill-seekers upload output/react-weaviate.zip --target weaviate \
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### video
|
||||||
|
|
||||||
|
Extract skills from video tutorials (YouTube, Vimeo, or local files).
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Setup (first time — auto-detects GPU, installs PyTorch + visual deps)
|
||||||
|
skill-seekers video --setup
|
||||||
|
|
||||||
|
# Extract from YouTube
|
||||||
|
skill-seekers video --url https://www.youtube.com/watch?v=VIDEO_ID --name my-skill
|
||||||
|
|
||||||
|
# With visual frame extraction (requires --setup first)
|
||||||
|
skill-seekers video --url VIDEO_URL --name my-skill --visual
|
||||||
|
|
||||||
|
# Local video file
|
||||||
|
skill-seekers video --url /path/to/video.mp4 --name my-skill
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Flags
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--setup` | Auto-detect GPU and install visual extraction dependencies |
|
||||||
|
| `--url URL` | Video URL (YouTube, Vimeo) or local file path |
|
||||||
|
| `--name NAME` | Skill name for output |
|
||||||
|
| `--visual` | Enable visual frame extraction (OCR on keyframes) |
|
||||||
|
| `--vision-api` | Use Claude Vision API as OCR fallback for low-confidence frames |
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- `--setup` detects NVIDIA (CUDA), AMD (ROCm), or CPU-only and installs the correct PyTorch variant
|
||||||
|
- Requires `pip install skill-seekers[video]` (transcripts) or `skill-seekers[video-full]` (+ whisper + scene detection)
|
||||||
|
- EasyOCR is NOT included in pip extras — it is installed by `--setup` with the correct GPU backend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### workflows
|
### workflows
|
||||||
|
|
||||||
Manage enhancement workflow presets.
|
Manage enhancement workflow presets.
|
||||||
|
|||||||
@@ -121,12 +121,13 @@ video = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Video processing (full: + Whisper + visual extraction)
|
# Video processing (full: + Whisper + visual extraction)
|
||||||
|
# NOTE: easyocr removed — it pulls torch with the wrong GPU variant.
|
||||||
|
# Use: skill-seekers video --setup (auto-detects GPU, installs correct PyTorch + easyocr)
|
||||||
video-full = [
|
video-full = [
|
||||||
"yt-dlp>=2024.12.0",
|
"yt-dlp>=2024.12.0",
|
||||||
"youtube-transcript-api>=1.2.0",
|
"youtube-transcript-api>=1.2.0",
|
||||||
"faster-whisper>=1.0.0",
|
"faster-whisper>=1.0.0",
|
||||||
"scenedetect[opencv]>=0.6.4",
|
"scenedetect[opencv]>=0.6.4",
|
||||||
"easyocr>=1.7.0",
|
|
||||||
"opencv-python-headless>=4.9.0",
|
"opencv-python-headless>=4.9.0",
|
||||||
"pytesseract>=0.3.13",
|
"pytesseract>=0.3.13",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -495,6 +495,24 @@ VIDEO_ARGUMENTS: dict[str, dict[str, Any]] = {
|
|||||||
"help": "Use Claude Vision API as fallback for low-confidence code frames (requires ANTHROPIC_API_KEY, ~$0.004/frame)",
|
"help": "Use Claude Vision API as fallback for low-confidence code frames (requires ANTHROPIC_API_KEY, ~$0.004/frame)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"start_time": {
|
||||||
|
"flags": ("--start-time",),
|
||||||
|
"kwargs": {
|
||||||
|
"type": str,
|
||||||
|
"default": None,
|
||||||
|
"metavar": "TIME",
|
||||||
|
"help": "Start time for extraction (seconds, MM:SS, or HH:MM:SS). Single video only.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"end_time": {
|
||||||
|
"flags": ("--end-time",),
|
||||||
|
"kwargs": {
|
||||||
|
"type": str,
|
||||||
|
"default": None,
|
||||||
|
"metavar": "TIME",
|
||||||
|
"help": "End time for extraction (seconds, MM:SS, or HH:MM:SS). Single video only.",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Multi-source config specific (from unified_scraper.py)
|
# Multi-source config specific (from unified_scraper.py)
|
||||||
|
|||||||
@@ -109,6 +109,31 @@ VIDEO_ARGUMENTS: dict[str, dict[str, Any]] = {
|
|||||||
"help": "Use Claude Vision API as fallback for low-confidence code frames (requires ANTHROPIC_API_KEY, ~$0.004/frame)",
|
"help": "Use Claude Vision API as fallback for low-confidence code frames (requires ANTHROPIC_API_KEY, ~$0.004/frame)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"start_time": {
|
||||||
|
"flags": ("--start-time",),
|
||||||
|
"kwargs": {
|
||||||
|
"type": str,
|
||||||
|
"default": None,
|
||||||
|
"metavar": "TIME",
|
||||||
|
"help": "Start time for extraction (seconds, MM:SS, or HH:MM:SS). Single video only.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"end_time": {
|
||||||
|
"flags": ("--end-time",),
|
||||||
|
"kwargs": {
|
||||||
|
"type": str,
|
||||||
|
"default": None,
|
||||||
|
"metavar": "TIME",
|
||||||
|
"help": "End time for extraction (seconds, MM:SS, or HH:MM:SS). Single video only.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"flags": ("--setup",),
|
||||||
|
"kwargs": {
|
||||||
|
"action": "store_true",
|
||||||
|
"help": "Auto-detect GPU and install visual extraction deps (PyTorch, easyocr, etc.)",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -398,6 +398,12 @@ class CreateCommand:
|
|||||||
vs = getattr(self.args, "visual_similarity", None)
|
vs = getattr(self.args, "visual_similarity", None)
|
||||||
if vs is not None and vs != 3.0:
|
if vs is not None and vs != 3.0:
|
||||||
argv.extend(["--visual-similarity", str(vs)])
|
argv.extend(["--visual-similarity", str(vs)])
|
||||||
|
st = getattr(self.args, "start_time", None)
|
||||||
|
if st is not None:
|
||||||
|
argv.extend(["--start-time", str(st)])
|
||||||
|
et = getattr(self.args, "end_time", None)
|
||||||
|
if et is not None:
|
||||||
|
argv.extend(["--end-time", str(et)])
|
||||||
|
|
||||||
# Call video_scraper with modified argv
|
# Call video_scraper with modified argv
|
||||||
logger.debug(f"Calling video_scraper with argv: {argv}")
|
logger.debug(f"Calling video_scraper with argv: {argv}")
|
||||||
|
|||||||
@@ -621,6 +621,11 @@ class VideoInfo:
|
|||||||
transcript_confidence: float = 0.0
|
transcript_confidence: float = 0.0
|
||||||
content_richness_score: float = 0.0
|
content_richness_score: float = 0.0
|
||||||
|
|
||||||
|
# Time-clipping metadata (None when full video is used)
|
||||||
|
original_duration: float | None = None
|
||||||
|
clip_start: float | None = None
|
||||||
|
clip_end: float | None = None
|
||||||
|
|
||||||
# Consensus-based text tracking (Phase A-D)
|
# Consensus-based text tracking (Phase A-D)
|
||||||
text_group_timeline: TextGroupTimeline | None = None
|
text_group_timeline: TextGroupTimeline | None = None
|
||||||
audio_visual_alignments: list[AudioVisualAlignment] = field(default_factory=list)
|
audio_visual_alignments: list[AudioVisualAlignment] = field(default_factory=list)
|
||||||
@@ -657,6 +662,9 @@ class VideoInfo:
|
|||||||
"extracted_at": self.extracted_at,
|
"extracted_at": self.extracted_at,
|
||||||
"transcript_confidence": self.transcript_confidence,
|
"transcript_confidence": self.transcript_confidence,
|
||||||
"content_richness_score": self.content_richness_score,
|
"content_richness_score": self.content_richness_score,
|
||||||
|
"original_duration": self.original_duration,
|
||||||
|
"clip_start": self.clip_start,
|
||||||
|
"clip_end": self.clip_end,
|
||||||
"text_group_timeline": self.text_group_timeline.to_dict()
|
"text_group_timeline": self.text_group_timeline.to_dict()
|
||||||
if self.text_group_timeline
|
if self.text_group_timeline
|
||||||
else None,
|
else None,
|
||||||
@@ -698,6 +706,9 @@ class VideoInfo:
|
|||||||
extracted_at=data.get("extracted_at", ""),
|
extracted_at=data.get("extracted_at", ""),
|
||||||
transcript_confidence=data.get("transcript_confidence", 0.0),
|
transcript_confidence=data.get("transcript_confidence", 0.0),
|
||||||
content_richness_score=data.get("content_richness_score", 0.0),
|
content_richness_score=data.get("content_richness_score", 0.0),
|
||||||
|
original_duration=data.get("original_duration"),
|
||||||
|
clip_start=data.get("clip_start"),
|
||||||
|
clip_end=data.get("clip_end"),
|
||||||
text_group_timeline=timeline,
|
text_group_timeline=timeline,
|
||||||
audio_visual_alignments=[
|
audio_visual_alignments=[
|
||||||
AudioVisualAlignment.from_dict(a) for a in data.get("audio_visual_alignments", [])
|
AudioVisualAlignment.from_dict(a) for a in data.get("audio_visual_alignments", [])
|
||||||
@@ -739,6 +750,10 @@ class VideoSourceConfig:
|
|||||||
# Subtitle files
|
# Subtitle files
|
||||||
subtitle_patterns: list[str] | None = None
|
subtitle_patterns: list[str] | None = None
|
||||||
|
|
||||||
|
# Time-clipping (single video only)
|
||||||
|
clip_start: float | None = None
|
||||||
|
clip_end: float | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict) -> VideoSourceConfig:
|
def from_dict(cls, data: dict) -> VideoSourceConfig:
|
||||||
return cls(
|
return cls(
|
||||||
@@ -758,6 +773,8 @@ class VideoSourceConfig:
|
|||||||
max_segment_duration=data.get("max_segment_duration", 600.0),
|
max_segment_duration=data.get("max_segment_duration", 600.0),
|
||||||
categories=data.get("categories"),
|
categories=data.get("categories"),
|
||||||
subtitle_patterns=data.get("subtitle_patterns"),
|
subtitle_patterns=data.get("subtitle_patterns"),
|
||||||
|
clip_start=data.get("clip_start"),
|
||||||
|
clip_end=data.get("clip_end"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate(self) -> list[str]:
|
def validate(self) -> list[str]:
|
||||||
@@ -774,6 +791,23 @@ class VideoSourceConfig:
|
|||||||
)
|
)
|
||||||
if sources_set > 1:
|
if sources_set > 1:
|
||||||
errors.append("Video source must specify exactly one source type")
|
errors.append("Video source must specify exactly one source type")
|
||||||
|
|
||||||
|
# Clip range validation
|
||||||
|
has_clip = self.clip_start is not None or self.clip_end is not None
|
||||||
|
if has_clip and self.playlist is not None:
|
||||||
|
errors.append(
|
||||||
|
"--start-time/--end-time cannot be used with --playlist. "
|
||||||
|
"Clip range is for single videos only."
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
self.clip_start is not None
|
||||||
|
and self.clip_end is not None
|
||||||
|
and self.clip_start >= self.clip_end
|
||||||
|
):
|
||||||
|
errors.append(
|
||||||
|
f"--start-time ({self.clip_start}s) must be before --end-time ({self.clip_end}s)"
|
||||||
|
)
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -83,9 +83,15 @@ def check_video_dependencies(require_full: bool = False) -> None:
|
|||||||
if missing:
|
if missing:
|
||||||
deps = ", ".join(missing)
|
deps = ", ".join(missing)
|
||||||
extra = "[video-full]" if require_full else "[video]"
|
extra = "[video-full]" if require_full else "[video]"
|
||||||
|
setup_hint = (
|
||||||
|
"\nFor visual deps (GPU-aware): skill-seekers video --setup"
|
||||||
|
if require_full
|
||||||
|
else ""
|
||||||
|
)
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Missing video dependencies: {deps}\n"
|
f"Missing video dependencies: {deps}\n"
|
||||||
f'Install with: pip install "skill-seekers{extra}"\n'
|
f'Install with: pip install "skill-seekers{extra}"'
|
||||||
|
f"{setup_hint}\n"
|
||||||
f"Or: pip install {' '.join(missing)}"
|
f"Or: pip install {' '.join(missing)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -105,6 +111,45 @@ def _sanitize_filename(title: str, max_length: int = 60) -> str:
|
|||||||
return name[:max_length]
|
return name[:max_length]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_time_to_seconds(time_str: str) -> float:
|
||||||
|
"""Parse a time string into seconds.
|
||||||
|
|
||||||
|
Accepted formats:
|
||||||
|
- Plain seconds: ``"330"`` or ``"330.5"``
|
||||||
|
- MM:SS: ``"5:30"``
|
||||||
|
- HH:MM:SS: ``"00:05:30"``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
time_str: Time string in one of the accepted formats.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Time in seconds as a float.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If *time_str* cannot be parsed.
|
||||||
|
"""
|
||||||
|
time_str = time_str.strip()
|
||||||
|
if not time_str:
|
||||||
|
raise ValueError("Empty time string")
|
||||||
|
|
||||||
|
parts = time_str.split(":")
|
||||||
|
try:
|
||||||
|
if len(parts) == 1:
|
||||||
|
return float(parts[0])
|
||||||
|
if len(parts) == 2:
|
||||||
|
minutes, seconds = float(parts[0]), float(parts[1])
|
||||||
|
return minutes * 60 + seconds
|
||||||
|
if len(parts) == 3:
|
||||||
|
hours, minutes, seconds = float(parts[0]), float(parts[1]), float(parts[2])
|
||||||
|
return hours * 3600 + minutes * 60 + seconds
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid time format: '{time_str}'. "
|
||||||
|
"Use seconds (330), MM:SS (5:30), or HH:MM:SS (00:05:30)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _format_duration(seconds: float) -> str:
|
def _format_duration(seconds: float) -> str:
|
||||||
"""Format seconds as HH:MM:SS or MM:SS."""
|
"""Format seconds as HH:MM:SS or MM:SS."""
|
||||||
total = int(seconds)
|
total = int(seconds)
|
||||||
@@ -221,6 +266,10 @@ class VideoToSkillConverter:
|
|||||||
self.visual_similarity = config.get("visual_similarity", 3.0)
|
self.visual_similarity = config.get("visual_similarity", 3.0)
|
||||||
self.vision_ocr = config.get("vision_ocr", False)
|
self.vision_ocr = config.get("vision_ocr", False)
|
||||||
|
|
||||||
|
# Time-clipping (seconds, None = full video)
|
||||||
|
self.start_time: float | None = config.get("start_time")
|
||||||
|
self.end_time: float | None = config.get("end_time")
|
||||||
|
|
||||||
# Paths
|
# Paths
|
||||||
self.skill_dir = config.get("output") or f"output/{self.name}"
|
self.skill_dir = config.get("output") or f"output/{self.name}"
|
||||||
self.data_file = f"output/{self.name}_video_extracted.json"
|
self.data_file = f"output/{self.name}_video_extracted.json"
|
||||||
@@ -265,6 +314,8 @@ class VideoToSkillConverter:
|
|||||||
languages=self.languages,
|
languages=self.languages,
|
||||||
visual_extraction=self.visual,
|
visual_extraction=self.visual,
|
||||||
whisper_model=self.whisper_model,
|
whisper_model=self.whisper_model,
|
||||||
|
clip_start=self.start_time,
|
||||||
|
clip_end=self.end_time,
|
||||||
)
|
)
|
||||||
|
|
||||||
videos: list[VideoInfo] = []
|
videos: list[VideoInfo] = []
|
||||||
@@ -317,6 +368,37 @@ class VideoToSkillConverter:
|
|||||||
if transcript_source == TranscriptSource.YOUTUBE_AUTO:
|
if transcript_source == TranscriptSource.YOUTUBE_AUTO:
|
||||||
video_info.transcript_confidence *= 0.8
|
video_info.transcript_confidence *= 0.8
|
||||||
|
|
||||||
|
# Apply time clipping to transcript and chapters
|
||||||
|
clip_start = self.start_time
|
||||||
|
clip_end = self.end_time
|
||||||
|
if clip_start is not None or clip_end is not None:
|
||||||
|
cs = clip_start or 0.0
|
||||||
|
ce = clip_end or float("inf")
|
||||||
|
|
||||||
|
# Store original duration before clipping
|
||||||
|
video_info.original_duration = video_info.duration
|
||||||
|
video_info.clip_start = cs
|
||||||
|
video_info.clip_end = clip_end # keep None if not set
|
||||||
|
|
||||||
|
# Filter transcript segments to clip range
|
||||||
|
original_count = len(transcript_segments)
|
||||||
|
transcript_segments = [
|
||||||
|
seg for seg in transcript_segments if seg.end > cs and seg.start < ce
|
||||||
|
]
|
||||||
|
video_info.raw_transcript = transcript_segments
|
||||||
|
logger.info(
|
||||||
|
f" Clipped transcript: {len(transcript_segments)}/{original_count} "
|
||||||
|
f"segments in range {_format_duration(cs)}-{_format_duration(ce) if clip_end else 'end'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter chapters to clip range
|
||||||
|
if video_info.chapters:
|
||||||
|
video_info.chapters = [
|
||||||
|
ch
|
||||||
|
for ch in video_info.chapters
|
||||||
|
if ch.end_time > cs and ch.start_time < ce
|
||||||
|
]
|
||||||
|
|
||||||
# Segment video
|
# Segment video
|
||||||
segments = segment_video(video_info, transcript_segments, source_config)
|
segments = segment_video(video_info, transcript_segments, source_config)
|
||||||
video_info.segments = segments
|
video_info.segments = segments
|
||||||
@@ -336,7 +418,12 @@ class VideoToSkillConverter:
|
|||||||
import tempfile as _tmpmod
|
import tempfile as _tmpmod
|
||||||
|
|
||||||
temp_video_dir = _tmpmod.mkdtemp(prefix="ss_video_")
|
temp_video_dir = _tmpmod.mkdtemp(prefix="ss_video_")
|
||||||
video_path = download_video(source, temp_video_dir)
|
video_path = download_video(
|
||||||
|
source,
|
||||||
|
temp_video_dir,
|
||||||
|
clip_start=self.start_time,
|
||||||
|
clip_end=self.end_time,
|
||||||
|
)
|
||||||
|
|
||||||
if video_path and os.path.exists(video_path):
|
if video_path and os.path.exists(video_path):
|
||||||
keyframes, code_blocks, timeline = extract_visual_data(
|
keyframes, code_blocks, timeline = extract_visual_data(
|
||||||
@@ -347,6 +434,8 @@ class VideoToSkillConverter:
|
|||||||
min_gap=self.visual_min_gap,
|
min_gap=self.visual_min_gap,
|
||||||
similarity_threshold=self.visual_similarity,
|
similarity_threshold=self.visual_similarity,
|
||||||
use_vision_api=self.vision_ocr,
|
use_vision_api=self.vision_ocr,
|
||||||
|
clip_start=self.start_time,
|
||||||
|
clip_end=self.end_time,
|
||||||
)
|
)
|
||||||
# Attach keyframes to segments
|
# Attach keyframes to segments
|
||||||
for kf in keyframes:
|
for kf in keyframes:
|
||||||
@@ -510,7 +599,13 @@ class VideoToSkillConverter:
|
|||||||
else:
|
else:
|
||||||
meta_parts.append(f"**Source:** {video.channel_name}")
|
meta_parts.append(f"**Source:** {video.channel_name}")
|
||||||
if video.duration > 0:
|
if video.duration > 0:
|
||||||
meta_parts.append(f"**Duration:** {_format_duration(video.duration)}")
|
dur_str = _format_duration(video.duration)
|
||||||
|
if video.clip_start is not None or video.clip_end is not None:
|
||||||
|
orig = _format_duration(video.original_duration) if video.original_duration else "?"
|
||||||
|
cs = _format_duration(video.clip_start) if video.clip_start is not None else "0:00"
|
||||||
|
ce = _format_duration(video.clip_end) if video.clip_end is not None else orig
|
||||||
|
dur_str = f"{cs} - {ce} (of {orig})"
|
||||||
|
meta_parts.append(f"**Duration:** {dur_str}")
|
||||||
if video.upload_date:
|
if video.upload_date:
|
||||||
meta_parts.append(f"**Published:** {video.upload_date}")
|
meta_parts.append(f"**Published:** {video.upload_date}")
|
||||||
|
|
||||||
@@ -737,7 +832,21 @@ class VideoToSkillConverter:
|
|||||||
else:
|
else:
|
||||||
meta.append(video.channel_name)
|
meta.append(video.channel_name)
|
||||||
if video.duration > 0:
|
if video.duration > 0:
|
||||||
meta.append(_format_duration(video.duration))
|
dur_str = _format_duration(video.duration)
|
||||||
|
if video.clip_start is not None or video.clip_end is not None:
|
||||||
|
orig = (
|
||||||
|
_format_duration(video.original_duration)
|
||||||
|
if video.original_duration
|
||||||
|
else "?"
|
||||||
|
)
|
||||||
|
cs = (
|
||||||
|
_format_duration(video.clip_start)
|
||||||
|
if video.clip_start is not None
|
||||||
|
else "0:00"
|
||||||
|
)
|
||||||
|
ce = _format_duration(video.clip_end) if video.clip_end is not None else orig
|
||||||
|
dur_str = f"Clip {cs}-{ce} (of {orig})"
|
||||||
|
meta.append(dur_str)
|
||||||
if video.view_count is not None:
|
if video.view_count is not None:
|
||||||
meta.append(f"{_format_count(video.view_count)} views")
|
meta.append(f"{_format_count(video.view_count)} views")
|
||||||
if meta:
|
if meta:
|
||||||
@@ -817,6 +926,12 @@ Examples:
|
|||||||
add_video_arguments(parser)
|
add_video_arguments(parser)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# --setup: run GPU detection + dependency installation, then exit
|
||||||
|
if getattr(args, "setup", False):
|
||||||
|
from skill_seekers.cli.video_setup import run_setup
|
||||||
|
|
||||||
|
return run_setup(interactive=True)
|
||||||
|
|
||||||
# Setup logging
|
# Setup logging
|
||||||
log_level = logging.DEBUG if args.verbose else (logging.WARNING if args.quiet else logging.INFO)
|
log_level = logging.DEBUG if args.verbose else (logging.WARNING if args.quiet else logging.INFO)
|
||||||
logging.basicConfig(level=log_level, format="%(levelname)s: %(message)s")
|
logging.basicConfig(level=log_level, format="%(levelname)s: %(message)s")
|
||||||
@@ -834,6 +949,29 @@ Examples:
|
|||||||
if not has_source and not has_json:
|
if not has_source and not has_json:
|
||||||
parser.error("Must specify --url, --video-file, --playlist, or --from-json")
|
parser.error("Must specify --url, --video-file, --playlist, or --from-json")
|
||||||
|
|
||||||
|
# Parse and validate time clipping
|
||||||
|
raw_start = getattr(args, "start_time", None)
|
||||||
|
raw_end = getattr(args, "end_time", None)
|
||||||
|
clip_start: float | None = None
|
||||||
|
clip_end: float | None = None
|
||||||
|
|
||||||
|
if raw_start is not None:
|
||||||
|
try:
|
||||||
|
clip_start = parse_time_to_seconds(raw_start)
|
||||||
|
except ValueError as exc:
|
||||||
|
parser.error(f"--start-time: {exc}")
|
||||||
|
if raw_end is not None:
|
||||||
|
try:
|
||||||
|
clip_end = parse_time_to_seconds(raw_end)
|
||||||
|
except ValueError as exc:
|
||||||
|
parser.error(f"--end-time: {exc}")
|
||||||
|
|
||||||
|
if clip_start is not None or clip_end is not None:
|
||||||
|
if getattr(args, "playlist", None):
|
||||||
|
parser.error("--start-time/--end-time cannot be used with --playlist")
|
||||||
|
if clip_start is not None and clip_end is not None and clip_start >= clip_end:
|
||||||
|
parser.error(f"--start-time ({clip_start}s) must be before --end-time ({clip_end}s)")
|
||||||
|
|
||||||
# Build config
|
# Build config
|
||||||
config = {
|
config = {
|
||||||
"name": args.name or "video_skill",
|
"name": args.name or "video_skill",
|
||||||
@@ -849,6 +987,8 @@ Examples:
|
|||||||
"visual_min_gap": getattr(args, "visual_min_gap", 0.5),
|
"visual_min_gap": getattr(args, "visual_min_gap", 0.5),
|
||||||
"visual_similarity": getattr(args, "visual_similarity", 3.0),
|
"visual_similarity": getattr(args, "visual_similarity", 3.0),
|
||||||
"vision_ocr": getattr(args, "vision_ocr", False),
|
"vision_ocr": getattr(args, "vision_ocr", False),
|
||||||
|
"start_time": clip_start,
|
||||||
|
"end_time": clip_end,
|
||||||
}
|
}
|
||||||
|
|
||||||
converter = VideoToSkillConverter(config)
|
converter = VideoToSkillConverter(config)
|
||||||
@@ -862,6 +1002,10 @@ Examples:
|
|||||||
logger.info(f" name: {config['name']}")
|
logger.info(f" name: {config['name']}")
|
||||||
logger.info(f" languages: {config['languages']}")
|
logger.info(f" languages: {config['languages']}")
|
||||||
logger.info(f" visual: {config['visual']}")
|
logger.info(f" visual: {config['visual']}")
|
||||||
|
if clip_start is not None or clip_end is not None:
|
||||||
|
start_str = _format_duration(clip_start) if clip_start is not None else "start"
|
||||||
|
end_str = _format_duration(clip_end) if clip_end is not None else "end"
|
||||||
|
logger.info(f" clip range: {start_str} - {end_str}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# Workflow 1: Build from JSON
|
# Workflow 1: Build from JSON
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ def segment_by_time_window(
|
|||||||
video_info: VideoInfo,
|
video_info: VideoInfo,
|
||||||
transcript_segments: list[TranscriptSegment],
|
transcript_segments: list[TranscriptSegment],
|
||||||
window_seconds: float = 120.0,
|
window_seconds: float = 120.0,
|
||||||
|
start_offset: float = 0.0,
|
||||||
|
end_limit: float | None = None,
|
||||||
) -> list[VideoSegment]:
|
) -> list[VideoSegment]:
|
||||||
"""Segment video using fixed time windows.
|
"""Segment video using fixed time windows.
|
||||||
|
|
||||||
@@ -139,6 +141,8 @@ def segment_by_time_window(
|
|||||||
video_info: Video metadata.
|
video_info: Video metadata.
|
||||||
transcript_segments: Raw transcript segments.
|
transcript_segments: Raw transcript segments.
|
||||||
window_seconds: Duration of each window in seconds.
|
window_seconds: Duration of each window in seconds.
|
||||||
|
start_offset: Start segmentation at this time (seconds).
|
||||||
|
end_limit: Stop segmentation at this time (seconds). None = full duration.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of VideoSegment objects.
|
List of VideoSegment objects.
|
||||||
@@ -149,10 +153,13 @@ def segment_by_time_window(
|
|||||||
if duration <= 0 and transcript_segments:
|
if duration <= 0 and transcript_segments:
|
||||||
duration = max(seg.end for seg in transcript_segments)
|
duration = max(seg.end for seg in transcript_segments)
|
||||||
|
|
||||||
|
if end_limit is not None:
|
||||||
|
duration = min(duration, end_limit)
|
||||||
|
|
||||||
if duration <= 0:
|
if duration <= 0:
|
||||||
return segments
|
return segments
|
||||||
|
|
||||||
current_time = 0.0
|
current_time = start_offset
|
||||||
index = 0
|
index = 0
|
||||||
|
|
||||||
while current_time < duration:
|
while current_time < duration:
|
||||||
@@ -215,4 +222,10 @@ def segment_video(
|
|||||||
# Fallback to time-window
|
# Fallback to time-window
|
||||||
window = config.time_window_seconds
|
window = config.time_window_seconds
|
||||||
logger.info(f"Using time-window segmentation ({window}s windows)")
|
logger.info(f"Using time-window segmentation ({window}s windows)")
|
||||||
return segment_by_time_window(video_info, transcript_segments, window)
|
return segment_by_time_window(
|
||||||
|
video_info,
|
||||||
|
transcript_segments,
|
||||||
|
window,
|
||||||
|
start_offset=config.clip_start or 0.0,
|
||||||
|
end_limit=config.clip_end,
|
||||||
|
)
|
||||||
|
|||||||
835
src/skill_seekers/cli/video_setup.py
Normal file
835
src/skill_seekers/cli/video_setup.py
Normal file
@@ -0,0 +1,835 @@
|
|||||||
|
"""GPU auto-detection and video dependency installation.
|
||||||
|
|
||||||
|
Detects NVIDIA (CUDA) or AMD (ROCm) GPUs using system tools (without
|
||||||
|
requiring torch to be installed) and installs the correct PyTorch variant
|
||||||
|
plus all visual extraction dependencies (easyocr, opencv, etc.).
|
||||||
|
|
||||||
|
Also handles:
|
||||||
|
- Virtual environment creation (if not already in one)
|
||||||
|
- System dependency checks (tesseract binary)
|
||||||
|
- ROCm environment variable configuration (MIOPEN_FIND_MODE)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
skill-seekers video --setup # Interactive (all modules)
|
||||||
|
skill-seekers video --setup # Interactive, choose modules
|
||||||
|
From MCP: run_setup(interactive=False)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import venv
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Data Structures
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class GPUVendor(Enum):
|
||||||
|
"""Detected GPU hardware vendor."""
|
||||||
|
|
||||||
|
NVIDIA = "nvidia"
|
||||||
|
AMD = "amd"
|
||||||
|
NONE = "none"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GPUInfo:
|
||||||
|
"""Result of GPU auto-detection."""
|
||||||
|
|
||||||
|
vendor: GPUVendor
|
||||||
|
name: str = ""
|
||||||
|
compute_version: str = ""
|
||||||
|
index_url: str = ""
|
||||||
|
details: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SetupModules:
|
||||||
|
"""Which modules to install during setup."""
|
||||||
|
|
||||||
|
torch: bool = True
|
||||||
|
easyocr: bool = True
|
||||||
|
opencv: bool = True
|
||||||
|
tesseract: bool = True
|
||||||
|
scenedetect: bool = True
|
||||||
|
whisper: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PyTorch Index URL Mapping
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
_PYTORCH_BASE = "https://download.pytorch.org/whl"
|
||||||
|
|
||||||
|
|
||||||
|
def _cuda_version_to_index_url(version: str) -> str:
|
||||||
|
"""Map a CUDA version string to the correct PyTorch index URL."""
|
||||||
|
try:
|
||||||
|
parts = version.split(".")
|
||||||
|
major = int(parts[0])
|
||||||
|
minor = int(parts[1]) if len(parts) > 1 else 0
|
||||||
|
ver = major + minor / 10.0
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return f"{_PYTORCH_BASE}/cpu"
|
||||||
|
|
||||||
|
if ver >= 12.4:
|
||||||
|
return f"{_PYTORCH_BASE}/cu124"
|
||||||
|
if ver >= 12.1:
|
||||||
|
return f"{_PYTORCH_BASE}/cu121"
|
||||||
|
if ver >= 11.8:
|
||||||
|
return f"{_PYTORCH_BASE}/cu118"
|
||||||
|
return f"{_PYTORCH_BASE}/cpu"
|
||||||
|
|
||||||
|
|
||||||
|
def _rocm_version_to_index_url(version: str) -> str:
|
||||||
|
"""Map a ROCm version string to the correct PyTorch index URL."""
|
||||||
|
try:
|
||||||
|
parts = version.split(".")
|
||||||
|
major = int(parts[0])
|
||||||
|
minor = int(parts[1]) if len(parts) > 1 else 0
|
||||||
|
ver = major + minor / 10.0
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return f"{_PYTORCH_BASE}/cpu"
|
||||||
|
|
||||||
|
if ver >= 6.3:
|
||||||
|
return f"{_PYTORCH_BASE}/rocm6.3"
|
||||||
|
if ver >= 6.0:
|
||||||
|
return f"{_PYTORCH_BASE}/rocm6.2.4"
|
||||||
|
return f"{_PYTORCH_BASE}/cpu"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# GPU Detection (without torch)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def detect_gpu() -> GPUInfo:
|
||||||
|
"""Detect GPU vendor and compute version using system tools.
|
||||||
|
|
||||||
|
Detection order:
|
||||||
|
1. nvidia-smi -> NVIDIA + CUDA version
|
||||||
|
2. rocminfo -> AMD + ROCm version
|
||||||
|
3. lspci -> AMD GPU present but no ROCm (warn)
|
||||||
|
4. Fallback -> CPU-only
|
||||||
|
"""
|
||||||
|
# 1. Check NVIDIA
|
||||||
|
nvidia = _check_nvidia()
|
||||||
|
if nvidia is not None:
|
||||||
|
return nvidia
|
||||||
|
|
||||||
|
# 2. Check AMD ROCm
|
||||||
|
amd = _check_amd_rocm()
|
||||||
|
if amd is not None:
|
||||||
|
return amd
|
||||||
|
|
||||||
|
# 3. Check if AMD GPU exists but ROCm isn't installed
|
||||||
|
amd_no_rocm = _check_amd_lspci()
|
||||||
|
if amd_no_rocm is not None:
|
||||||
|
return amd_no_rocm
|
||||||
|
|
||||||
|
# 4. CPU fallback
|
||||||
|
return GPUInfo(
|
||||||
|
vendor=GPUVendor.NONE,
|
||||||
|
name="CPU-only",
|
||||||
|
index_url=f"{_PYTORCH_BASE}/cpu",
|
||||||
|
details=["No GPU detected, will use CPU-only PyTorch"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_nvidia() -> GPUInfo | None:
|
||||||
|
"""Detect NVIDIA GPU via nvidia-smi."""
|
||||||
|
if not shutil.which("nvidia-smi"):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["nvidia-smi"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
output = result.stdout
|
||||||
|
# Parse CUDA version from "CUDA Version: X.Y"
|
||||||
|
cuda_match = re.search(r"CUDA Version:\s*(\d+\.\d+)", output)
|
||||||
|
cuda_ver = cuda_match.group(1) if cuda_match else ""
|
||||||
|
|
||||||
|
# Parse GPU name from the table row (e.g., "NVIDIA GeForce RTX 4090")
|
||||||
|
gpu_name = ""
|
||||||
|
name_match = re.search(r"\|\s+(NVIDIA[^\|]+?)\s+(?:On|Off)\s+\|", output)
|
||||||
|
if name_match:
|
||||||
|
gpu_name = name_match.group(1).strip()
|
||||||
|
|
||||||
|
index_url = _cuda_version_to_index_url(cuda_ver) if cuda_ver else f"{_PYTORCH_BASE}/cpu"
|
||||||
|
|
||||||
|
return GPUInfo(
|
||||||
|
vendor=GPUVendor.NVIDIA,
|
||||||
|
name=gpu_name or "NVIDIA GPU",
|
||||||
|
compute_version=cuda_ver,
|
||||||
|
index_url=index_url,
|
||||||
|
details=[f"CUDA {cuda_ver}" if cuda_ver else "CUDA version unknown"],
|
||||||
|
)
|
||||||
|
except (subprocess.TimeoutExpired, OSError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _check_amd_rocm() -> GPUInfo | None:
|
||||||
|
"""Detect AMD GPU via rocminfo."""
|
||||||
|
if not shutil.which("rocminfo"):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["rocminfo"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
output = result.stdout
|
||||||
|
# Parse GPU name from "Name: gfx..." or "Marketing Name: ..."
|
||||||
|
gpu_name = ""
|
||||||
|
marketing_match = re.search(r"Marketing Name:\s*(.+)", output)
|
||||||
|
if marketing_match:
|
||||||
|
gpu_name = marketing_match.group(1).strip()
|
||||||
|
|
||||||
|
# Get ROCm version from /opt/rocm/.info/version
|
||||||
|
rocm_ver = _read_rocm_version()
|
||||||
|
|
||||||
|
index_url = _rocm_version_to_index_url(rocm_ver) if rocm_ver else f"{_PYTORCH_BASE}/cpu"
|
||||||
|
|
||||||
|
return GPUInfo(
|
||||||
|
vendor=GPUVendor.AMD,
|
||||||
|
name=gpu_name or "AMD GPU",
|
||||||
|
compute_version=rocm_ver,
|
||||||
|
index_url=index_url,
|
||||||
|
details=[f"ROCm {rocm_ver}" if rocm_ver else "ROCm version unknown"],
|
||||||
|
)
|
||||||
|
except (subprocess.TimeoutExpired, OSError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _read_rocm_version() -> str:
|
||||||
|
"""Read ROCm version from /opt/rocm/.info/version."""
|
||||||
|
try:
|
||||||
|
with open("/opt/rocm/.info/version") as f:
|
||||||
|
return f.read().strip().split("-")[0]
|
||||||
|
except (OSError, IOError):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _check_amd_lspci() -> GPUInfo | None:
|
||||||
|
"""Detect AMD GPU via lspci when ROCm isn't installed."""
|
||||||
|
if not shutil.which("lspci"):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["lspci"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Look for AMD/ATI VGA or Display controllers
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
if ("VGA" in line or "Display" in line) and ("AMD" in line or "ATI" in line):
|
||||||
|
return GPUInfo(
|
||||||
|
vendor=GPUVendor.AMD,
|
||||||
|
name=line.split(":")[-1].strip() if ":" in line else "AMD GPU",
|
||||||
|
compute_version="",
|
||||||
|
index_url=f"{_PYTORCH_BASE}/cpu",
|
||||||
|
details=[
|
||||||
|
"AMD GPU detected but ROCm is not installed",
|
||||||
|
"Install ROCm first for GPU acceleration: https://rocm.docs.amd.com/",
|
||||||
|
"Falling back to CPU-only PyTorch",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
except (subprocess.TimeoutExpired, OSError):
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Virtual Environment
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def is_in_venv() -> bool:
|
||||||
|
"""Check if the current Python process is running inside a venv."""
|
||||||
|
return sys.prefix != sys.base_prefix
|
||||||
|
|
||||||
|
|
||||||
|
def create_venv(venv_path: str = ".venv") -> bool:
|
||||||
|
"""Create a virtual environment and return True on success."""
|
||||||
|
path = Path(venv_path).resolve()
|
||||||
|
if path.exists():
|
||||||
|
logger.info(f"Venv already exists at {path}")
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
venv.create(str(path), with_pip=True)
|
||||||
|
return True
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
logger.error(f"Failed to create venv: {exc}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_venv_python(venv_path: str = ".venv") -> str:
|
||||||
|
"""Return the python executable path inside a venv."""
|
||||||
|
path = Path(venv_path).resolve()
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
return str(path / "Scripts" / "python.exe")
|
||||||
|
return str(path / "bin" / "python")
|
||||||
|
|
||||||
|
|
||||||
|
def get_venv_activate_cmd(venv_path: str = ".venv") -> str:
|
||||||
|
"""Return the shell command to activate the venv."""
|
||||||
|
path = Path(venv_path).resolve()
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
return str(path / "Scripts" / "activate")
|
||||||
|
return f"source {path}/bin/activate"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# System Dependency Checks
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_distro() -> str:
|
||||||
|
"""Detect Linux distro family for install command suggestions."""
|
||||||
|
try:
|
||||||
|
with open("/etc/os-release") as f:
|
||||||
|
content = f.read().lower()
|
||||||
|
if "arch" in content or "manjaro" in content or "endeavour" in content:
|
||||||
|
return "arch"
|
||||||
|
if "debian" in content or "ubuntu" in content or "mint" in content or "pop" in content:
|
||||||
|
return "debian"
|
||||||
|
if "fedora" in content or "rhel" in content or "centos" in content or "rocky" in content:
|
||||||
|
return "fedora"
|
||||||
|
if "opensuse" in content or "suse" in content:
|
||||||
|
return "suse"
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tesseract_install_cmd() -> str:
|
||||||
|
"""Return distro-specific command to install tesseract."""
|
||||||
|
distro = _detect_distro()
|
||||||
|
cmds = {
|
||||||
|
"arch": "sudo pacman -S tesseract tesseract-data-eng",
|
||||||
|
"debian": "sudo apt install tesseract-ocr tesseract-ocr-eng",
|
||||||
|
"fedora": "sudo dnf install tesseract tesseract-langpack-eng",
|
||||||
|
"suse": "sudo zypper install tesseract-ocr tesseract-ocr-traineddata-english",
|
||||||
|
}
|
||||||
|
return cmds.get(distro, "Install tesseract-ocr with your package manager")
|
||||||
|
|
||||||
|
|
||||||
|
def check_tesseract() -> dict[str, bool | str]:
|
||||||
|
"""Check if tesseract binary is installed and has English data.
|
||||||
|
|
||||||
|
Returns dict with keys: installed, has_eng, install_cmd, version.
|
||||||
|
"""
|
||||||
|
result: dict[str, bool | str] = {
|
||||||
|
"installed": False,
|
||||||
|
"has_eng": False,
|
||||||
|
"install_cmd": _get_tesseract_install_cmd(),
|
||||||
|
"version": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
tess_bin = shutil.which("tesseract")
|
||||||
|
if not tess_bin:
|
||||||
|
return result
|
||||||
|
|
||||||
|
result["installed"] = True
|
||||||
|
|
||||||
|
# Get version
|
||||||
|
try:
|
||||||
|
ver = subprocess.run(
|
||||||
|
["tesseract", "--version"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
first_line = (ver.stdout or ver.stderr).split("\n")[0]
|
||||||
|
result["version"] = first_line.strip()
|
||||||
|
except (subprocess.TimeoutExpired, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check for eng language data
|
||||||
|
try:
|
||||||
|
langs = subprocess.run(
|
||||||
|
["tesseract", "--list-langs"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
output = langs.stdout + langs.stderr
|
||||||
|
result["has_eng"] = "eng" in output.split()
|
||||||
|
except (subprocess.TimeoutExpired, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ROCm Environment Configuration
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def configure_rocm_env() -> list[str]:
|
||||||
|
"""Set environment variables for ROCm/MIOpen to work correctly.
|
||||||
|
|
||||||
|
Returns list of env vars that were set.
|
||||||
|
"""
|
||||||
|
changes: list[str] = []
|
||||||
|
|
||||||
|
# MIOPEN_FIND_MODE=FAST avoids the workspace allocation issue
|
||||||
|
# where MIOpen requires huge workspace but allocates 0 bytes
|
||||||
|
if "MIOPEN_FIND_MODE" not in os.environ:
|
||||||
|
os.environ["MIOPEN_FIND_MODE"] = "FAST"
|
||||||
|
changes.append("MIOPEN_FIND_MODE=FAST")
|
||||||
|
|
||||||
|
# Ensure MIOpen user DB has a writable location
|
||||||
|
if "MIOPEN_USER_DB_PATH" not in os.environ:
|
||||||
|
db_path = os.path.expanduser("~/.config/miopen")
|
||||||
|
os.makedirs(db_path, exist_ok=True)
|
||||||
|
os.environ["MIOPEN_USER_DB_PATH"] = db_path
|
||||||
|
changes.append(f"MIOPEN_USER_DB_PATH={db_path}")
|
||||||
|
|
||||||
|
return changes
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Installation
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
_BASE_VIDEO_DEPS = ["yt-dlp", "youtube-transcript-api"]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_visual_deps(modules: SetupModules) -> list[str]:
|
||||||
|
"""Build the list of pip packages based on selected modules."""
|
||||||
|
# Base video deps are always included — setup must leave video fully ready
|
||||||
|
deps: list[str] = list(_BASE_VIDEO_DEPS)
|
||||||
|
if modules.easyocr:
|
||||||
|
deps.append("easyocr")
|
||||||
|
if modules.opencv:
|
||||||
|
deps.append("opencv-python-headless")
|
||||||
|
if modules.tesseract:
|
||||||
|
deps.append("pytesseract")
|
||||||
|
if modules.scenedetect:
|
||||||
|
deps.append("scenedetect[opencv]")
|
||||||
|
if modules.whisper:
|
||||||
|
deps.append("faster-whisper")
|
||||||
|
return deps
|
||||||
|
|
||||||
|
|
||||||
|
def install_torch(gpu_info: GPUInfo, python_exe: str | None = None) -> bool:
|
||||||
|
"""Install PyTorch with the correct GPU variant.
|
||||||
|
|
||||||
|
Returns True on success, False on failure.
|
||||||
|
"""
|
||||||
|
exe = python_exe or sys.executable
|
||||||
|
cmd = [exe, "-m", "pip", "install", "torch", "torchvision", "--index-url", gpu_info.index_url]
|
||||||
|
logger.info(f"Installing PyTorch from {gpu_info.index_url}")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, timeout=600, capture_output=True, text=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
logger.error(f"PyTorch install failed:\n{result.stderr[-500:]}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.error("PyTorch installation timed out (10 min)")
|
||||||
|
return False
|
||||||
|
except OSError as exc:
|
||||||
|
logger.error(f"PyTorch installation error: {exc}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def install_visual_deps(
|
||||||
|
modules: SetupModules | None = None, python_exe: str | None = None
|
||||||
|
) -> bool:
|
||||||
|
"""Install visual extraction dependencies.
|
||||||
|
|
||||||
|
Returns True on success, False on failure.
|
||||||
|
"""
|
||||||
|
mods = modules or SetupModules()
|
||||||
|
deps = _build_visual_deps(mods)
|
||||||
|
if not deps:
|
||||||
|
return True
|
||||||
|
|
||||||
|
exe = python_exe or sys.executable
|
||||||
|
cmd = [exe, "-m", "pip", "install"] + deps
|
||||||
|
logger.info(f"Installing visual deps: {', '.join(deps)}")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, timeout=600, capture_output=True, text=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
logger.error(f"Visual deps install failed:\n{result.stderr[-500:]}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.error("Visual deps installation timed out (10 min)")
|
||||||
|
return False
|
||||||
|
except OSError as exc:
|
||||||
|
logger.error(f"Visual deps installation error: {exc}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def install_skill_seekers(python_exe: str) -> bool:
|
||||||
|
"""Install skill-seekers into the target python environment."""
|
||||||
|
cmd = [python_exe, "-m", "pip", "install", "skill-seekers"]
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, timeout=300, capture_output=True, text=True)
|
||||||
|
return result.returncode == 0
|
||||||
|
except (subprocess.TimeoutExpired, OSError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Verification
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def verify_installation() -> dict[str, bool]:
|
||||||
|
"""Verify that all video deps are importable.
|
||||||
|
|
||||||
|
Returns a dict mapping package name to import success.
|
||||||
|
"""
|
||||||
|
results: dict[str, bool] = {}
|
||||||
|
|
||||||
|
# Base video deps
|
||||||
|
try:
|
||||||
|
import yt_dlp # noqa: F401
|
||||||
|
|
||||||
|
results["yt-dlp"] = True
|
||||||
|
except ImportError:
|
||||||
|
results["yt-dlp"] = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
import youtube_transcript_api # noqa: F401
|
||||||
|
|
||||||
|
results["youtube-transcript-api"] = True
|
||||||
|
except ImportError:
|
||||||
|
results["youtube-transcript-api"] = False
|
||||||
|
|
||||||
|
# torch
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
|
||||||
|
results["torch"] = True
|
||||||
|
results["torch.cuda"] = torch.cuda.is_available()
|
||||||
|
results["torch.rocm"] = hasattr(torch.version, "hip") and torch.version.hip is not None
|
||||||
|
except ImportError:
|
||||||
|
results["torch"] = False
|
||||||
|
results["torch.cuda"] = False
|
||||||
|
results["torch.rocm"] = False
|
||||||
|
|
||||||
|
# easyocr
|
||||||
|
try:
|
||||||
|
import easyocr # noqa: F401
|
||||||
|
|
||||||
|
results["easyocr"] = True
|
||||||
|
except ImportError:
|
||||||
|
results["easyocr"] = False
|
||||||
|
|
||||||
|
# opencv
|
||||||
|
try:
|
||||||
|
import cv2 # noqa: F401
|
||||||
|
|
||||||
|
results["opencv"] = True
|
||||||
|
except ImportError:
|
||||||
|
results["opencv"] = False
|
||||||
|
|
||||||
|
# pytesseract
|
||||||
|
try:
|
||||||
|
import pytesseract # noqa: F401
|
||||||
|
|
||||||
|
results["pytesseract"] = True
|
||||||
|
except ImportError:
|
||||||
|
results["pytesseract"] = False
|
||||||
|
|
||||||
|
# scenedetect
|
||||||
|
try:
|
||||||
|
import scenedetect # noqa: F401
|
||||||
|
|
||||||
|
results["scenedetect"] = True
|
||||||
|
except ImportError:
|
||||||
|
results["scenedetect"] = False
|
||||||
|
|
||||||
|
# faster-whisper
|
||||||
|
try:
|
||||||
|
import faster_whisper # noqa: F401
|
||||||
|
|
||||||
|
results["faster-whisper"] = True
|
||||||
|
except ImportError:
|
||||||
|
results["faster-whisper"] = False
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Module Selection (Interactive)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def _ask_modules(interactive: bool) -> SetupModules:
|
||||||
|
"""Ask the user which modules to install. Returns all if non-interactive."""
|
||||||
|
if not interactive:
|
||||||
|
return SetupModules()
|
||||||
|
|
||||||
|
print("Which modules do you want to install?")
|
||||||
|
print(" [a] All (default)")
|
||||||
|
print(" [c] Choose individually")
|
||||||
|
try:
|
||||||
|
choice = input(" > ").strip().lower()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print()
|
||||||
|
return SetupModules()
|
||||||
|
|
||||||
|
if choice not in ("c", "choose"):
|
||||||
|
return SetupModules()
|
||||||
|
|
||||||
|
modules = SetupModules()
|
||||||
|
_ask = _interactive_yn
|
||||||
|
|
||||||
|
modules.torch = _ask("PyTorch (required for easyocr GPU)", default=True)
|
||||||
|
modules.easyocr = _ask("EasyOCR (text extraction from video frames)", default=True)
|
||||||
|
modules.opencv = _ask("OpenCV (frame extraction and image processing)", default=True)
|
||||||
|
modules.tesseract = _ask("pytesseract (secondary OCR engine)", default=True)
|
||||||
|
modules.scenedetect = _ask("scenedetect (scene change detection)", default=True)
|
||||||
|
modules.whisper = _ask("faster-whisper (local audio transcription)", default=True)
|
||||||
|
|
||||||
|
return modules
|
||||||
|
|
||||||
|
|
||||||
|
def _interactive_yn(prompt: str, default: bool = True) -> bool:
|
||||||
|
"""Ask a yes/no question, return bool."""
|
||||||
|
suffix = "[Y/n]" if default else "[y/N]"
|
||||||
|
try:
|
||||||
|
answer = input(f" {prompt}? {suffix} ").strip().lower()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
return default
|
||||||
|
if not answer:
|
||||||
|
return default
|
||||||
|
return answer in ("y", "yes")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Orchestrator
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def run_setup(interactive: bool = True) -> int:
|
||||||
|
"""Auto-detect GPU and install all visual extraction dependencies.
|
||||||
|
|
||||||
|
Handles:
|
||||||
|
1. Venv creation (if not in one)
|
||||||
|
2. GPU detection
|
||||||
|
3. Module selection (optional — interactive only)
|
||||||
|
4. System dep checks (tesseract binary)
|
||||||
|
5. ROCm env var configuration
|
||||||
|
6. PyTorch installation (correct GPU variant)
|
||||||
|
7. Visual deps installation
|
||||||
|
8. Verification
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interactive: If True, prompt user for confirmation before installing.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
0 on success, 1 on failure.
|
||||||
|
"""
|
||||||
|
print("=" * 60)
|
||||||
|
print(" Video Visual Extraction Setup")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
total_steps = 7
|
||||||
|
|
||||||
|
# ── Step 1: Venv check ──
|
||||||
|
print(f"[1/{total_steps}] Checking environment...")
|
||||||
|
if is_in_venv():
|
||||||
|
print(f" Already in venv: {sys.prefix}")
|
||||||
|
python_exe = sys.executable
|
||||||
|
else:
|
||||||
|
print(" Not in a virtual environment.")
|
||||||
|
venv_path = ".venv"
|
||||||
|
if interactive:
|
||||||
|
try:
|
||||||
|
answer = input(
|
||||||
|
f" Create venv at ./{venv_path}? [Y/n] "
|
||||||
|
).strip().lower()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nSetup cancelled.")
|
||||||
|
return 1
|
||||||
|
if answer and answer not in ("y", "yes"):
|
||||||
|
print(" Continuing without venv (installing to system Python).")
|
||||||
|
python_exe = sys.executable
|
||||||
|
else:
|
||||||
|
if not create_venv(venv_path):
|
||||||
|
print(" FAILED: Could not create venv.")
|
||||||
|
return 1
|
||||||
|
python_exe = get_venv_python(venv_path)
|
||||||
|
activate_cmd = get_venv_activate_cmd(venv_path)
|
||||||
|
print(f" Venv created at ./{venv_path}")
|
||||||
|
print(f" Installing skill-seekers into venv...")
|
||||||
|
if not install_skill_seekers(python_exe):
|
||||||
|
print(" FAILED: Could not install skill-seekers into venv.")
|
||||||
|
return 1
|
||||||
|
print(f" After setup completes, activate with:")
|
||||||
|
print(f" {activate_cmd}")
|
||||||
|
else:
|
||||||
|
# Non-interactive: use current python
|
||||||
|
python_exe = sys.executable
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── Step 2: GPU detection ──
|
||||||
|
print(f"[2/{total_steps}] Detecting GPU...")
|
||||||
|
gpu_info = detect_gpu()
|
||||||
|
|
||||||
|
vendor_label = {
|
||||||
|
GPUVendor.NVIDIA: "NVIDIA (CUDA)",
|
||||||
|
GPUVendor.AMD: "AMD (ROCm)",
|
||||||
|
GPUVendor.NONE: "CPU-only",
|
||||||
|
}
|
||||||
|
print(f" GPU: {gpu_info.name}")
|
||||||
|
print(f" Vendor: {vendor_label.get(gpu_info.vendor, gpu_info.vendor.value)}")
|
||||||
|
if gpu_info.compute_version:
|
||||||
|
print(f" Version: {gpu_info.compute_version}")
|
||||||
|
for detail in gpu_info.details:
|
||||||
|
print(f" {detail}")
|
||||||
|
print(f" PyTorch index: {gpu_info.index_url}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── Step 3: Module selection ──
|
||||||
|
print(f"[3/{total_steps}] Selecting modules...")
|
||||||
|
modules = _ask_modules(interactive)
|
||||||
|
deps = _build_visual_deps(modules)
|
||||||
|
print(f" Selected: {', '.join(deps) if deps else '(none)'}")
|
||||||
|
if modules.torch:
|
||||||
|
print(f" + PyTorch + torchvision")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── Step 4: System dependency check ──
|
||||||
|
print(f"[4/{total_steps}] Checking system dependencies...")
|
||||||
|
if modules.tesseract:
|
||||||
|
tess = check_tesseract()
|
||||||
|
if not tess["installed"]:
|
||||||
|
print(f" WARNING: tesseract binary not found!")
|
||||||
|
print(f" The pytesseract Python package needs the tesseract binary installed.")
|
||||||
|
print(f" Install it with: {tess['install_cmd']}")
|
||||||
|
print()
|
||||||
|
elif not tess["has_eng"]:
|
||||||
|
print(f" WARNING: tesseract installed ({tess['version']}) but English data missing!")
|
||||||
|
print(f" Install with: {tess['install_cmd']}")
|
||||||
|
print()
|
||||||
|
else:
|
||||||
|
print(f" tesseract: {tess['version']} (eng data OK)")
|
||||||
|
else:
|
||||||
|
print(" tesseract: skipped (not selected)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── Step 5: ROCm configuration ──
|
||||||
|
print(f"[5/{total_steps}] Configuring GPU environment...")
|
||||||
|
if gpu_info.vendor == GPUVendor.AMD:
|
||||||
|
changes = configure_rocm_env()
|
||||||
|
if changes:
|
||||||
|
print(" Set ROCm environment variables:")
|
||||||
|
for c in changes:
|
||||||
|
print(f" {c}")
|
||||||
|
print(" (These fix MIOpen workspace allocation issues)")
|
||||||
|
else:
|
||||||
|
print(" ROCm env vars already configured.")
|
||||||
|
elif gpu_info.vendor == GPUVendor.NVIDIA:
|
||||||
|
print(" NVIDIA: no extra configuration needed.")
|
||||||
|
else:
|
||||||
|
print(" CPU-only: no GPU configuration needed.")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── Step 6: Confirm and install ──
|
||||||
|
if interactive:
|
||||||
|
print("Ready to install. Summary:")
|
||||||
|
if modules.torch:
|
||||||
|
print(f" - PyTorch + torchvision (from {gpu_info.index_url})")
|
||||||
|
for dep in deps:
|
||||||
|
print(f" - {dep}")
|
||||||
|
print()
|
||||||
|
try:
|
||||||
|
answer = input("Proceed? [Y/n] ").strip().lower()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nSetup cancelled.")
|
||||||
|
return 1
|
||||||
|
if answer and answer not in ("y", "yes"):
|
||||||
|
print("Setup cancelled.")
|
||||||
|
return 1
|
||||||
|
print()
|
||||||
|
|
||||||
|
print(f"[6/{total_steps}] Installing packages...")
|
||||||
|
if modules.torch:
|
||||||
|
print(" Installing PyTorch...")
|
||||||
|
if not install_torch(gpu_info, python_exe):
|
||||||
|
print(" FAILED: PyTorch installation failed.")
|
||||||
|
print(f" Try: {python_exe} -m pip install torch torchvision --index-url {gpu_info.index_url}")
|
||||||
|
return 1
|
||||||
|
print(" PyTorch installed.")
|
||||||
|
|
||||||
|
if deps:
|
||||||
|
print(" Installing visual packages...")
|
||||||
|
if not install_visual_deps(modules, python_exe):
|
||||||
|
print(" FAILED: Visual packages installation failed.")
|
||||||
|
print(f" Try: {python_exe} -m pip install {' '.join(deps)}")
|
||||||
|
return 1
|
||||||
|
print(" Visual packages installed.")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── Step 7: Verify ──
|
||||||
|
print(f"[7/{total_steps}] Verifying installation...")
|
||||||
|
results = verify_installation()
|
||||||
|
all_ok = True
|
||||||
|
for pkg, ok in results.items():
|
||||||
|
status = "OK" if ok else "MISSING"
|
||||||
|
print(f" {pkg}: {status}")
|
||||||
|
# torch.cuda / torch.rocm are informational, not required
|
||||||
|
if not ok and pkg not in ("torch.cuda", "torch.rocm"):
|
||||||
|
# Only count as failure if the module was selected
|
||||||
|
if pkg == "torch" and modules.torch:
|
||||||
|
all_ok = False
|
||||||
|
elif pkg == "easyocr" and modules.easyocr:
|
||||||
|
all_ok = False
|
||||||
|
elif pkg == "opencv" and modules.opencv:
|
||||||
|
all_ok = False
|
||||||
|
elif pkg == "pytesseract" and modules.tesseract:
|
||||||
|
all_ok = False
|
||||||
|
elif pkg == "scenedetect" and modules.scenedetect:
|
||||||
|
all_ok = False
|
||||||
|
elif pkg == "faster-whisper" and modules.whisper:
|
||||||
|
all_ok = False
|
||||||
|
|
||||||
|
print()
|
||||||
|
if all_ok:
|
||||||
|
print("Setup complete! You can now use: skill-seekers video --url <URL> --visual")
|
||||||
|
if not is_in_venv() and python_exe != sys.executable:
|
||||||
|
activate_cmd = get_venv_activate_cmd()
|
||||||
|
print(f"\nDon't forget to activate the venv first:")
|
||||||
|
print(f" {activate_cmd}")
|
||||||
|
else:
|
||||||
|
print("Some packages failed to install. Check the output above.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import difflib
|
import difflib
|
||||||
|
import gc
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -32,6 +33,18 @@ from skill_seekers.cli.video_models import (
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Set ROCm/MIOpen env vars BEFORE importing torch (via easyocr).
|
||||||
|
# Without MIOPEN_FIND_MODE=FAST, MIOpen tries to allocate huge workspace
|
||||||
|
# buffers (300MB+), gets 0 bytes, and silently falls back to CPU kernels.
|
||||||
|
import os as _os
|
||||||
|
|
||||||
|
if "MIOPEN_FIND_MODE" not in _os.environ:
|
||||||
|
_os.environ["MIOPEN_FIND_MODE"] = "FAST"
|
||||||
|
if "MIOPEN_USER_DB_PATH" not in _os.environ:
|
||||||
|
_miopen_db = _os.path.expanduser("~/.config/miopen")
|
||||||
|
_os.makedirs(_miopen_db, exist_ok=True)
|
||||||
|
_os.environ["MIOPEN_USER_DB_PATH"] = _miopen_db
|
||||||
|
|
||||||
# Tier 2 dependency flags
|
# Tier 2 dependency flags
|
||||||
try:
|
try:
|
||||||
import cv2
|
import cv2
|
||||||
@@ -65,23 +78,46 @@ except ImportError:
|
|||||||
pytesseract = None # type: ignore[assignment]
|
pytesseract = None # type: ignore[assignment]
|
||||||
HAS_PYTESSERACT = False
|
HAS_PYTESSERACT = False
|
||||||
|
|
||||||
|
# Circuit breaker: after first tesseract failure, disable it for the session.
|
||||||
|
# Prevents wasting time spawning subprocesses that always fail.
|
||||||
|
_tesseract_broken = False
|
||||||
|
|
||||||
|
|
||||||
_INSTALL_MSG = (
|
_INSTALL_MSG = (
|
||||||
"Visual extraction requires additional dependencies.\n"
|
"Visual extraction requires additional dependencies.\n"
|
||||||
'Install with: pip install "skill-seekers[video-full]"\n'
|
"Recommended: skill-seekers video --setup (auto-detects GPU, installs correct PyTorch)\n"
|
||||||
"Or: pip install opencv-python-headless scenedetect easyocr"
|
'Alternative: pip install "skill-seekers[video-full]" (may install wrong PyTorch variant)'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Lazy-initialized EasyOCR reader (heavy, only load once)
|
# Lazy-initialized EasyOCR reader (heavy, only load once)
|
||||||
_ocr_reader = None
|
_ocr_reader = None
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_gpu() -> bool:
|
||||||
|
"""Check if a CUDA or ROCm GPU is available for EasyOCR/PyTorch."""
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
return True
|
||||||
|
# ROCm exposes GPU via torch.version.hip
|
||||||
|
if hasattr(torch.version, "hip") and torch.version.hip is not None:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _get_ocr_reader():
|
def _get_ocr_reader():
|
||||||
"""Get or create the EasyOCR reader (lazy singleton)."""
|
"""Get or create the EasyOCR reader (lazy singleton)."""
|
||||||
global _ocr_reader
|
global _ocr_reader
|
||||||
if _ocr_reader is None:
|
if _ocr_reader is None:
|
||||||
logger.info("Initializing OCR engine (first run may download models)...")
|
use_gpu = _detect_gpu()
|
||||||
_ocr_reader = easyocr.Reader(["en"], gpu=False)
|
logger.info(
|
||||||
|
f"Initializing OCR engine ({'GPU' if use_gpu else 'CPU'} mode, "
|
||||||
|
"first run may download models)..."
|
||||||
|
)
|
||||||
|
_ocr_reader = easyocr.Reader(["en"], gpu=use_gpu)
|
||||||
return _ocr_reader
|
return _ocr_reader
|
||||||
|
|
||||||
|
|
||||||
@@ -296,11 +332,15 @@ def _run_tesseract_ocr(preprocessed_path: str, frame_type: FrameType) -> list[tu
|
|||||||
Returns results in the same format as EasyOCR: list of (bbox, text, confidence).
|
Returns results in the same format as EasyOCR: list of (bbox, text, confidence).
|
||||||
Groups words into lines by y-coordinate.
|
Groups words into lines by y-coordinate.
|
||||||
|
|
||||||
|
Uses a circuit breaker: if tesseract fails once, it's disabled for the
|
||||||
|
rest of the session to avoid wasting time on repeated subprocess failures.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
preprocessed_path: Path to the preprocessed grayscale image.
|
preprocessed_path: Path to the preprocessed grayscale image.
|
||||||
frame_type: Frame classification (reserved for future per-type tuning).
|
frame_type: Frame classification (reserved for future per-type tuning).
|
||||||
"""
|
"""
|
||||||
if not HAS_PYTESSERACT:
|
global _tesseract_broken
|
||||||
|
if not HAS_PYTESSERACT or _tesseract_broken:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Produce clean binary for Tesseract
|
# Produce clean binary for Tesseract
|
||||||
@@ -312,7 +352,11 @@ def _run_tesseract_ocr(preprocessed_path: str, frame_type: FrameType) -> list[tu
|
|||||||
output_type=pytesseract.Output.DICT,
|
output_type=pytesseract.Output.DICT,
|
||||||
)
|
)
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
logger.debug("pytesseract failed, returning empty results")
|
_tesseract_broken = True
|
||||||
|
logger.warning(
|
||||||
|
"pytesseract failed — disabling for this session. "
|
||||||
|
"Install tesseract binary: skill-seekers video --setup"
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
finally:
|
finally:
|
||||||
if binary_path != preprocessed_path and os.path.exists(binary_path):
|
if binary_path != preprocessed_path and os.path.exists(binary_path):
|
||||||
@@ -897,6 +941,25 @@ def _crop_code_region(frame_path: str, bbox: tuple[int, int, int, int], suffix:
|
|||||||
return cropped_path
|
return cropped_path
|
||||||
|
|
||||||
|
|
||||||
|
def _frame_type_from_regions(
|
||||||
|
regions: list[tuple[int, int, int, int, FrameType]],
|
||||||
|
) -> FrameType:
|
||||||
|
"""Derive the dominant frame type from pre-computed regions.
|
||||||
|
|
||||||
|
Same logic as ``classify_frame`` but avoids re-loading the image.
|
||||||
|
"""
|
||||||
|
for _x1, _y1, _x2, _y2, ft in regions:
|
||||||
|
if ft == FrameType.TERMINAL:
|
||||||
|
return FrameType.TERMINAL
|
||||||
|
if ft == FrameType.CODE_EDITOR:
|
||||||
|
return FrameType.CODE_EDITOR
|
||||||
|
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
type_counts = Counter(ft for _, _, _, _, ft in regions)
|
||||||
|
return type_counts.most_common(1)[0][0] if type_counts else FrameType.OTHER
|
||||||
|
|
||||||
|
|
||||||
def classify_frame(frame_path: str) -> FrameType:
|
def classify_frame(frame_path: str) -> FrameType:
|
||||||
"""Classify a video frame by its visual content.
|
"""Classify a video frame by its visual content.
|
||||||
|
|
||||||
@@ -1114,6 +1177,8 @@ def _compute_frame_timestamps(
|
|||||||
duration: float,
|
duration: float,
|
||||||
sample_interval: float = 0.7,
|
sample_interval: float = 0.7,
|
||||||
min_gap: float = 0.5,
|
min_gap: float = 0.5,
|
||||||
|
start_offset: float = 0.0,
|
||||||
|
end_limit: float | None = None,
|
||||||
) -> list[float]:
|
) -> list[float]:
|
||||||
"""Build a deduplicated list of timestamps to extract frames at.
|
"""Build a deduplicated list of timestamps to extract frames at.
|
||||||
|
|
||||||
@@ -1126,10 +1191,13 @@ def _compute_frame_timestamps(
|
|||||||
duration: Total video duration in seconds.
|
duration: Total video duration in seconds.
|
||||||
sample_interval: Seconds between interval samples.
|
sample_interval: Seconds between interval samples.
|
||||||
min_gap: Minimum gap between kept timestamps.
|
min_gap: Minimum gap between kept timestamps.
|
||||||
|
start_offset: Start sampling at this time (seconds).
|
||||||
|
end_limit: Stop sampling at this time (seconds). None = full duration.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Sorted, deduplicated list of timestamps (seconds).
|
Sorted, deduplicated list of timestamps (seconds).
|
||||||
"""
|
"""
|
||||||
|
effective_end = end_limit if end_limit is not None else duration
|
||||||
timestamps: set[float] = set()
|
timestamps: set[float] = set()
|
||||||
|
|
||||||
# 1. Scene detection — catches cuts, slide transitions, editor switches
|
# 1. Scene detection — catches cuts, slide transitions, editor switches
|
||||||
@@ -1138,19 +1206,21 @@ def _compute_frame_timestamps(
|
|||||||
scenes = detect_scenes(video_path)
|
scenes = detect_scenes(video_path)
|
||||||
for start, _end in scenes:
|
for start, _end in scenes:
|
||||||
# Take frame 0.5s after the scene starts (avoids transition blur)
|
# Take frame 0.5s after the scene starts (avoids transition blur)
|
||||||
timestamps.add(round(start + 0.5, 1))
|
ts = round(start + 0.5, 1)
|
||||||
|
if ts >= start_offset and ts < effective_end:
|
||||||
|
timestamps.add(ts)
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
logger.warning(f"Scene detection failed, falling back to interval: {exc}")
|
logger.warning(f"Scene detection failed, falling back to interval: {exc}")
|
||||||
|
|
||||||
# 2. Regular interval sampling — fills gaps between scene cuts
|
# 2. Regular interval sampling — fills gaps between scene cuts
|
||||||
t = 0.5 # start slightly after 0 to avoid black intro frames
|
t = max(0.5, start_offset)
|
||||||
while t < duration:
|
while t < effective_end:
|
||||||
timestamps.add(round(t, 1))
|
timestamps.add(round(t, 1))
|
||||||
t += sample_interval
|
t += sample_interval
|
||||||
|
|
||||||
# Always include near the end
|
# Always include near the end
|
||||||
if duration > 2.0:
|
if effective_end > 2.0:
|
||||||
timestamps.add(round(duration - 1.0, 1))
|
timestamps.add(round(effective_end - 1.0, 1))
|
||||||
|
|
||||||
# 3. Sort and deduplicate (merge timestamps closer than min_gap)
|
# 3. Sort and deduplicate (merge timestamps closer than min_gap)
|
||||||
sorted_ts = sorted(timestamps)
|
sorted_ts = sorted(timestamps)
|
||||||
@@ -1876,6 +1946,8 @@ def extract_visual_data(
|
|||||||
min_gap: float = 0.5,
|
min_gap: float = 0.5,
|
||||||
similarity_threshold: float = 3.0,
|
similarity_threshold: float = 3.0,
|
||||||
use_vision_api: bool = False,
|
use_vision_api: bool = False,
|
||||||
|
clip_start: float | None = None,
|
||||||
|
clip_end: float | None = None,
|
||||||
) -> tuple[list[KeyFrame], list[CodeBlock], TextGroupTimeline | None]:
|
) -> tuple[list[KeyFrame], list[CodeBlock], TextGroupTimeline | None]:
|
||||||
"""Run continuous visual extraction on a video.
|
"""Run continuous visual extraction on a video.
|
||||||
|
|
||||||
@@ -1899,6 +1971,8 @@ def extract_visual_data(
|
|||||||
similarity_threshold: Pixel-diff threshold for duplicate detection (default 3.0).
|
similarity_threshold: Pixel-diff threshold for duplicate detection (default 3.0).
|
||||||
use_vision_api: If True, use Claude Vision API as fallback for low-confidence
|
use_vision_api: If True, use Claude Vision API as fallback for low-confidence
|
||||||
code frames (requires ANTHROPIC_API_KEY).
|
code frames (requires ANTHROPIC_API_KEY).
|
||||||
|
clip_start: Start of clip range in seconds (None = beginning).
|
||||||
|
clip_end: End of clip range in seconds (None = full duration).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (keyframes, code_blocks, text_group_timeline).
|
Tuple of (keyframes, code_blocks, text_group_timeline).
|
||||||
@@ -1937,7 +2011,12 @@ def extract_visual_data(
|
|||||||
|
|
||||||
# Build candidate timestamps
|
# Build candidate timestamps
|
||||||
timestamps = _compute_frame_timestamps(
|
timestamps = _compute_frame_timestamps(
|
||||||
video_path, duration, sample_interval=sample_interval, min_gap=min_gap
|
video_path,
|
||||||
|
duration,
|
||||||
|
sample_interval=sample_interval,
|
||||||
|
min_gap=min_gap,
|
||||||
|
start_offset=clip_start or 0.0,
|
||||||
|
end_limit=clip_end,
|
||||||
)
|
)
|
||||||
logger.info(f" {len(timestamps)} candidate timestamps after dedup")
|
logger.info(f" {len(timestamps)} candidate timestamps after dedup")
|
||||||
|
|
||||||
@@ -1961,17 +2040,21 @@ def extract_visual_data(
|
|||||||
skipped_similar += 1
|
skipped_similar += 1
|
||||||
continue
|
continue
|
||||||
prev_frame = frame.copy()
|
prev_frame = frame.copy()
|
||||||
|
frame_h, frame_w = frame.shape[:2]
|
||||||
|
|
||||||
# Save frame
|
# Save frame
|
||||||
idx = len(keyframes)
|
idx = len(keyframes)
|
||||||
frame_filename = f"frame_{idx:03d}_{ts:.0f}s.jpg"
|
frame_filename = f"frame_{idx:03d}_{ts:.0f}s.jpg"
|
||||||
frame_path = os.path.join(frames_dir, frame_filename)
|
frame_path = os.path.join(frames_dir, frame_filename)
|
||||||
cv2.imwrite(frame_path, frame)
|
cv2.imwrite(frame_path, frame)
|
||||||
|
del frame # Free the numpy array early — saved to disk
|
||||||
|
|
||||||
# Classify using region-based panel detection
|
# Classify using region-based panel detection
|
||||||
regions = classify_frame_regions(frame_path)
|
regions = classify_frame_regions(frame_path)
|
||||||
code_panels = _get_code_panels(regions)
|
code_panels = _get_code_panels(regions)
|
||||||
frame_type = classify_frame(frame_path) # dominant type for metadata
|
# Derive frame_type from already-computed regions (avoids loading
|
||||||
|
# the image a second time — classify_frame() would repeat the work).
|
||||||
|
frame_type = _frame_type_from_regions(regions)
|
||||||
is_code_frame = frame_type in (FrameType.CODE_EDITOR, FrameType.TERMINAL)
|
is_code_frame = frame_type in (FrameType.CODE_EDITOR, FrameType.TERMINAL)
|
||||||
|
|
||||||
# Per-panel OCR: each code/terminal panel is OCR'd independently
|
# Per-panel OCR: each code/terminal panel is OCR'd independently
|
||||||
@@ -1982,11 +2065,13 @@ def extract_visual_data(
|
|||||||
ocr_confidence = 0.0
|
ocr_confidence = 0.0
|
||||||
|
|
||||||
if is_code_frame and code_panels and (HAS_EASYOCR or HAS_PYTESSERACT):
|
if is_code_frame and code_panels and (HAS_EASYOCR or HAS_PYTESSERACT):
|
||||||
full_area = frame.shape[0] * frame.shape[1]
|
full_area = frame_h * frame_w
|
||||||
|
|
||||||
if len(code_panels) > 1:
|
if len(code_panels) > 1:
|
||||||
# Parallel OCR — each panel is independent
|
# Parallel OCR — each panel is independent
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=len(code_panels)) as pool:
|
with concurrent.futures.ThreadPoolExecutor(
|
||||||
|
max_workers=min(2, len(code_panels))
|
||||||
|
) as pool:
|
||||||
futures = {
|
futures = {
|
||||||
pool.submit(
|
pool.submit(
|
||||||
_ocr_single_panel,
|
_ocr_single_panel,
|
||||||
@@ -2084,8 +2169,8 @@ def extract_visual_data(
|
|||||||
ocr_text=ocr_text,
|
ocr_text=ocr_text,
|
||||||
ocr_regions=ocr_regions,
|
ocr_regions=ocr_regions,
|
||||||
ocr_confidence=ocr_confidence,
|
ocr_confidence=ocr_confidence,
|
||||||
width=frame.shape[1],
|
width=frame_w,
|
||||||
height=frame.shape[0],
|
height=frame_h,
|
||||||
sub_sections=sub_sections,
|
sub_sections=sub_sections,
|
||||||
)
|
)
|
||||||
keyframes.append(kf)
|
keyframes.append(kf)
|
||||||
@@ -2101,6 +2186,10 @@ def extract_visual_data(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Periodically collect to free PyTorch/numpy memory
|
||||||
|
if idx % 10 == 9:
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
cap.release()
|
cap.release()
|
||||||
|
|
||||||
# Finalize text tracking and extract code blocks
|
# Finalize text tracking and extract code blocks
|
||||||
@@ -2131,7 +2220,12 @@ def extract_visual_data(
|
|||||||
return keyframes, code_blocks, timeline
|
return keyframes, code_blocks, timeline
|
||||||
|
|
||||||
|
|
||||||
def download_video(url: str, output_dir: str) -> str | None:
|
def download_video(
|
||||||
|
url: str,
|
||||||
|
output_dir: str,
|
||||||
|
clip_start: float | None = None,
|
||||||
|
clip_end: float | None = None,
|
||||||
|
) -> str | None:
|
||||||
"""Download a video using yt-dlp for visual processing.
|
"""Download a video using yt-dlp for visual processing.
|
||||||
|
|
||||||
Downloads the best quality up to 1080p. Uses separate video+audio streams
|
Downloads the best quality up to 1080p. Uses separate video+audio streams
|
||||||
@@ -2142,6 +2236,8 @@ def download_video(url: str, output_dir: str) -> str | None:
|
|||||||
Args:
|
Args:
|
||||||
url: Video URL.
|
url: Video URL.
|
||||||
output_dir: Directory to save the downloaded file.
|
output_dir: Directory to save the downloaded file.
|
||||||
|
clip_start: Download from this time (seconds). None = beginning.
|
||||||
|
clip_end: Download until this time (seconds). None = full video.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Path to downloaded video file, or None on failure.
|
Path to downloaded video file, or None on failure.
|
||||||
@@ -2156,13 +2252,30 @@ def download_video(url: str, output_dir: str) -> str | None:
|
|||||||
output_template = os.path.join(output_dir, "video.%(ext)s")
|
output_template = os.path.join(output_dir, "video.%(ext)s")
|
||||||
|
|
||||||
opts = {
|
opts = {
|
||||||
"format": "bestvideo[height<=1080]+bestaudio/best[height<=1080]",
|
"format": (
|
||||||
|
"bestvideo[height<=1080][vcodec^=avc1]+bestaudio/best[height<=1080][vcodec^=avc1]/"
|
||||||
|
"bestvideo[height<=1080][vcodec^=h264]+bestaudio/best[height<=1080][vcodec^=h264]/"
|
||||||
|
"bestvideo[height<=1080]+bestaudio/best[height<=1080]"
|
||||||
|
),
|
||||||
"merge_output_format": "mp4",
|
"merge_output_format": "mp4",
|
||||||
"outtmpl": output_template,
|
"outtmpl": output_template,
|
||||||
"quiet": True,
|
"quiet": True,
|
||||||
"no_warnings": True,
|
"no_warnings": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Apply download_ranges for clip support (yt-dlp 2023.01.02+)
|
||||||
|
if clip_start is not None or clip_end is not None:
|
||||||
|
try:
|
||||||
|
from yt_dlp.utils import download_range_func
|
||||||
|
|
||||||
|
ranges = [(clip_start or 0, clip_end or float("inf"))]
|
||||||
|
opts["download_ranges"] = download_range_func(None, ranges)
|
||||||
|
except (ImportError, TypeError):
|
||||||
|
logger.warning(
|
||||||
|
"yt-dlp version does not support download_ranges; "
|
||||||
|
"downloading full video and relying on frame timestamp filtering"
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(f"Downloading video for visual extraction...")
|
logger.info(f"Downloading video for visual extraction...")
|
||||||
try:
|
try:
|
||||||
with yt_dlp.YoutubeDL(opts) as ydl:
|
with yt_dlp.YoutubeDL(opts) as ydl:
|
||||||
|
|||||||
@@ -440,6 +440,9 @@ async def scrape_video(
|
|||||||
visual_min_gap: float | None = None,
|
visual_min_gap: float | None = None,
|
||||||
visual_similarity: float | None = None,
|
visual_similarity: float | None = None,
|
||||||
vision_ocr: bool = False,
|
vision_ocr: bool = False,
|
||||||
|
start_time: str | None = None,
|
||||||
|
end_time: str | None = None,
|
||||||
|
setup: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Scrape video content and build Claude skill.
|
Scrape video content and build Claude skill.
|
||||||
@@ -458,10 +461,19 @@ async def scrape_video(
|
|||||||
visual_min_gap: Minimum seconds between kept frames (default: 2.0)
|
visual_min_gap: Minimum seconds between kept frames (default: 2.0)
|
||||||
visual_similarity: Similarity threshold to skip duplicate frames 0.0-1.0 (default: 0.95)
|
visual_similarity: Similarity threshold to skip duplicate frames 0.0-1.0 (default: 0.95)
|
||||||
vision_ocr: Use vision model for OCR on extracted frames
|
vision_ocr: Use vision model for OCR on extracted frames
|
||||||
|
start_time: Start time for extraction (seconds, MM:SS, or HH:MM:SS). Single video only.
|
||||||
|
end_time: End time for extraction (seconds, MM:SS, or HH:MM:SS). Single video only.
|
||||||
|
setup: Auto-detect GPU and install visual extraction deps (PyTorch, easyocr, etc.)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Video scraping results with file paths.
|
Video scraping results with file paths.
|
||||||
"""
|
"""
|
||||||
|
if setup:
|
||||||
|
from skill_seekers.cli.video_setup import run_setup
|
||||||
|
|
||||||
|
rc = run_setup(interactive=False)
|
||||||
|
return "Setup completed successfully." if rc == 0 else "Setup failed. Check logs."
|
||||||
|
|
||||||
args = {}
|
args = {}
|
||||||
if url:
|
if url:
|
||||||
args["url"] = url
|
args["url"] = url
|
||||||
@@ -477,6 +489,10 @@ async def scrape_video(
|
|||||||
args["languages"] = languages
|
args["languages"] = languages
|
||||||
if from_json:
|
if from_json:
|
||||||
args["from_json"] = from_json
|
args["from_json"] = from_json
|
||||||
|
if start_time:
|
||||||
|
args["start_time"] = start_time
|
||||||
|
if end_time:
|
||||||
|
args["end_time"] = end_time
|
||||||
if visual:
|
if visual:
|
||||||
args["visual"] = visual
|
args["visual"] = visual
|
||||||
if whisper_model:
|
if whisper_model:
|
||||||
|
|||||||
@@ -378,10 +378,21 @@ async def scrape_video_tool(args: dict) -> list[TextContent]:
|
|||||||
- visual_min_gap (float, optional): Minimum seconds between kept frames (default: 2.0)
|
- visual_min_gap (float, optional): Minimum seconds between kept frames (default: 2.0)
|
||||||
- visual_similarity (float, optional): Similarity threshold to skip duplicate frames (default: 0.95)
|
- visual_similarity (float, optional): Similarity threshold to skip duplicate frames (default: 0.95)
|
||||||
- vision_ocr (bool, optional): Use vision model for OCR on frames (default: False)
|
- vision_ocr (bool, optional): Use vision model for OCR on frames (default: False)
|
||||||
|
- start_time (str, optional): Start time for extraction (seconds, MM:SS, or HH:MM:SS)
|
||||||
|
- end_time (str, optional): End time for extraction (seconds, MM:SS, or HH:MM:SS)
|
||||||
|
- setup (bool, optional): Auto-detect GPU and install visual extraction deps
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[TextContent]: Tool execution results
|
List[TextContent]: Tool execution results
|
||||||
"""
|
"""
|
||||||
|
# Handle --setup early exit
|
||||||
|
if args.get("setup", False):
|
||||||
|
from skill_seekers.cli.video_setup import run_setup
|
||||||
|
|
||||||
|
rc = run_setup(interactive=False)
|
||||||
|
msg = "Setup completed successfully." if rc == 0 else "Setup failed. Check logs."
|
||||||
|
return [TextContent(type="text", text=msg)]
|
||||||
|
|
||||||
url = args.get("url")
|
url = args.get("url")
|
||||||
video_file = args.get("video_file")
|
video_file = args.get("video_file")
|
||||||
playlist = args.get("playlist")
|
playlist = args.get("playlist")
|
||||||
@@ -395,6 +406,8 @@ async def scrape_video_tool(args: dict) -> list[TextContent]:
|
|||||||
visual_min_gap = args.get("visual_min_gap")
|
visual_min_gap = args.get("visual_min_gap")
|
||||||
visual_similarity = args.get("visual_similarity")
|
visual_similarity = args.get("visual_similarity")
|
||||||
vision_ocr = args.get("vision_ocr", False)
|
vision_ocr = args.get("vision_ocr", False)
|
||||||
|
start_time = args.get("start_time")
|
||||||
|
end_time = args.get("end_time")
|
||||||
|
|
||||||
# Build command
|
# Build command
|
||||||
cmd = [sys.executable, str(CLI_DIR / "video_scraper.py")]
|
cmd = [sys.executable, str(CLI_DIR / "video_scraper.py")]
|
||||||
@@ -440,6 +453,10 @@ async def scrape_video_tool(args: dict) -> list[TextContent]:
|
|||||||
cmd.extend(["--visual-similarity", str(visual_similarity)])
|
cmd.extend(["--visual-similarity", str(visual_similarity)])
|
||||||
if vision_ocr:
|
if vision_ocr:
|
||||||
cmd.append("--vision-ocr")
|
cmd.append("--vision-ocr")
|
||||||
|
if start_time:
|
||||||
|
cmd.extend(["--start-time", str(start_time)])
|
||||||
|
if end_time:
|
||||||
|
cmd.extend(["--end-time", str(end_time)])
|
||||||
|
|
||||||
# Run video_scraper.py with streaming
|
# Run video_scraper.py with streaming
|
||||||
timeout = 600 # 10 minutes for video extraction
|
timeout = 600 # 10 minutes for video extraction
|
||||||
|
|||||||
@@ -3115,5 +3115,286 @@ class TestVideoWorkflowAutoInjection(unittest.TestCase):
|
|||||||
self.assertTrue(yaml_path.exists(), "video-tutorial.yaml not found")
|
self.assertTrue(yaml_path.exists(), "video-tutorial.yaml not found")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Test: Time Clipping (--start-time / --end-time)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimeClipping(unittest.TestCase):
|
||||||
|
"""Test --start-time / --end-time clipping support."""
|
||||||
|
|
||||||
|
# ---- parse_time_to_seconds() ----
|
||||||
|
|
||||||
|
def test_parse_time_seconds_integer(self):
|
||||||
|
from skill_seekers.cli.video_scraper import parse_time_to_seconds
|
||||||
|
|
||||||
|
self.assertEqual(parse_time_to_seconds("330"), 330.0)
|
||||||
|
|
||||||
|
def test_parse_time_seconds_float(self):
|
||||||
|
from skill_seekers.cli.video_scraper import parse_time_to_seconds
|
||||||
|
|
||||||
|
self.assertAlmostEqual(parse_time_to_seconds("90.5"), 90.5)
|
||||||
|
|
||||||
|
def test_parse_time_mmss(self):
|
||||||
|
from skill_seekers.cli.video_scraper import parse_time_to_seconds
|
||||||
|
|
||||||
|
self.assertEqual(parse_time_to_seconds("5:30"), 330.0)
|
||||||
|
|
||||||
|
def test_parse_time_hhmmss(self):
|
||||||
|
from skill_seekers.cli.video_scraper import parse_time_to_seconds
|
||||||
|
|
||||||
|
self.assertEqual(parse_time_to_seconds("1:05:30"), 3930.0)
|
||||||
|
|
||||||
|
def test_parse_time_zero(self):
|
||||||
|
from skill_seekers.cli.video_scraper import parse_time_to_seconds
|
||||||
|
|
||||||
|
self.assertEqual(parse_time_to_seconds("0"), 0.0)
|
||||||
|
self.assertEqual(parse_time_to_seconds("0:00"), 0.0)
|
||||||
|
self.assertEqual(parse_time_to_seconds("0:00:00"), 0.0)
|
||||||
|
|
||||||
|
def test_parse_time_decimal_mmss(self):
|
||||||
|
from skill_seekers.cli.video_scraper import parse_time_to_seconds
|
||||||
|
|
||||||
|
self.assertAlmostEqual(parse_time_to_seconds("1:30.5"), 90.5)
|
||||||
|
|
||||||
|
def test_parse_time_invalid_raises(self):
|
||||||
|
from skill_seekers.cli.video_scraper import parse_time_to_seconds
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
parse_time_to_seconds("abc")
|
||||||
|
|
||||||
|
def test_parse_time_empty_raises(self):
|
||||||
|
from skill_seekers.cli.video_scraper import parse_time_to_seconds
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
parse_time_to_seconds("")
|
||||||
|
|
||||||
|
def test_parse_time_too_many_colons_raises(self):
|
||||||
|
from skill_seekers.cli.video_scraper import parse_time_to_seconds
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
parse_time_to_seconds("1:2:3:4")
|
||||||
|
|
||||||
|
# ---- Argument registration ----
|
||||||
|
|
||||||
|
def test_video_arguments_include_start_end_time(self):
|
||||||
|
from skill_seekers.cli.arguments.video import VIDEO_ARGUMENTS
|
||||||
|
|
||||||
|
self.assertIn("start_time", VIDEO_ARGUMENTS)
|
||||||
|
self.assertIn("end_time", VIDEO_ARGUMENTS)
|
||||||
|
|
||||||
|
def test_create_arguments_include_start_end_time(self):
|
||||||
|
from skill_seekers.cli.arguments.create import VIDEO_ARGUMENTS
|
||||||
|
|
||||||
|
self.assertIn("start_time", VIDEO_ARGUMENTS)
|
||||||
|
self.assertIn("end_time", VIDEO_ARGUMENTS)
|
||||||
|
|
||||||
|
def test_argument_parsing_defaults_none(self):
|
||||||
|
import argparse
|
||||||
|
from skill_seekers.cli.arguments.video import add_video_arguments
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
add_video_arguments(parser)
|
||||||
|
args = parser.parse_args(["--url", "https://example.com"])
|
||||||
|
self.assertIsNone(args.start_time)
|
||||||
|
self.assertIsNone(args.end_time)
|
||||||
|
|
||||||
|
def test_argument_parsing_with_values(self):
|
||||||
|
import argparse
|
||||||
|
from skill_seekers.cli.arguments.video import add_video_arguments
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
add_video_arguments(parser)
|
||||||
|
args = parser.parse_args(
|
||||||
|
["--url", "https://example.com", "--start-time", "2:00", "--end-time", "5:00"]
|
||||||
|
)
|
||||||
|
self.assertEqual(args.start_time, "2:00")
|
||||||
|
self.assertEqual(args.end_time, "5:00")
|
||||||
|
|
||||||
|
# ---- Transcript filtering ----
|
||||||
|
|
||||||
|
def test_transcript_clip_filters_segments(self):
|
||||||
|
"""Verify transcript segments are filtered to clip range."""
|
||||||
|
from skill_seekers.cli.video_models import TranscriptSegment
|
||||||
|
|
||||||
|
segments = [
|
||||||
|
TranscriptSegment(text="intro", start=0.0, end=30.0),
|
||||||
|
TranscriptSegment(text="part1", start=30.0, end=90.0),
|
||||||
|
TranscriptSegment(text="part2", start=90.0, end=150.0),
|
||||||
|
TranscriptSegment(text="outro", start=150.0, end=200.0),
|
||||||
|
]
|
||||||
|
|
||||||
|
clip_start, clip_end = 60.0, 120.0
|
||||||
|
filtered = [s for s in segments if s.end > clip_start and s.start < clip_end]
|
||||||
|
# part1 (30-90) overlaps with 60-120, part2 (90-150) overlaps with 60-120
|
||||||
|
self.assertEqual(len(filtered), 2)
|
||||||
|
self.assertEqual(filtered[0].text, "part1")
|
||||||
|
self.assertEqual(filtered[1].text, "part2")
|
||||||
|
|
||||||
|
def test_transcript_clip_start_only(self):
|
||||||
|
"""Verify only clip_start filters correctly."""
|
||||||
|
from skill_seekers.cli.video_models import TranscriptSegment
|
||||||
|
|
||||||
|
segments = [
|
||||||
|
TranscriptSegment(text="before", start=0.0, end=50.0),
|
||||||
|
TranscriptSegment(text="after", start=50.0, end=100.0),
|
||||||
|
]
|
||||||
|
clip_start = 50.0
|
||||||
|
clip_end = float("inf")
|
||||||
|
filtered = [s for s in segments if s.end > clip_start and s.start < clip_end]
|
||||||
|
self.assertEqual(len(filtered), 1)
|
||||||
|
self.assertEqual(filtered[0].text, "after")
|
||||||
|
|
||||||
|
# ---- Validation ----
|
||||||
|
|
||||||
|
def test_playlist_plus_clip_rejected(self):
|
||||||
|
from skill_seekers.cli.video_models import VideoSourceConfig
|
||||||
|
|
||||||
|
config = VideoSourceConfig(
|
||||||
|
playlist="https://youtube.com/playlist?list=x",
|
||||||
|
clip_start=60.0,
|
||||||
|
)
|
||||||
|
errors = config.validate()
|
||||||
|
self.assertTrue(any("--start-time" in e for e in errors))
|
||||||
|
|
||||||
|
def test_start_gte_end_rejected(self):
|
||||||
|
from skill_seekers.cli.video_models import VideoSourceConfig
|
||||||
|
|
||||||
|
config = VideoSourceConfig(
|
||||||
|
url="https://youtube.com/watch?v=x", clip_start=300.0, clip_end=120.0
|
||||||
|
)
|
||||||
|
errors = config.validate()
|
||||||
|
self.assertTrue(any("must be before" in e for e in errors))
|
||||||
|
|
||||||
|
def test_valid_clip_no_errors(self):
|
||||||
|
from skill_seekers.cli.video_models import VideoSourceConfig
|
||||||
|
|
||||||
|
config = VideoSourceConfig(
|
||||||
|
url="https://youtube.com/watch?v=x", clip_start=60.0, clip_end=300.0
|
||||||
|
)
|
||||||
|
errors = config.validate()
|
||||||
|
self.assertEqual(errors, [])
|
||||||
|
|
||||||
|
# ---- VideoInfo clip metadata serialization ----
|
||||||
|
|
||||||
|
def test_video_info_clip_roundtrip(self):
|
||||||
|
from skill_seekers.cli.video_models import VideoInfo, VideoSourceType
|
||||||
|
|
||||||
|
info = VideoInfo(
|
||||||
|
video_id="test",
|
||||||
|
source_type=VideoSourceType.YOUTUBE,
|
||||||
|
duration=300.0,
|
||||||
|
original_duration=600.0,
|
||||||
|
clip_start=120.0,
|
||||||
|
clip_end=420.0,
|
||||||
|
)
|
||||||
|
data = info.to_dict()
|
||||||
|
self.assertEqual(data["original_duration"], 600.0)
|
||||||
|
self.assertEqual(data["clip_start"], 120.0)
|
||||||
|
self.assertEqual(data["clip_end"], 420.0)
|
||||||
|
|
||||||
|
restored = VideoInfo.from_dict(data)
|
||||||
|
self.assertEqual(restored.original_duration, 600.0)
|
||||||
|
self.assertEqual(restored.clip_start, 120.0)
|
||||||
|
self.assertEqual(restored.clip_end, 420.0)
|
||||||
|
|
||||||
|
def test_video_info_no_clip_roundtrip(self):
|
||||||
|
from skill_seekers.cli.video_models import VideoInfo, VideoSourceType
|
||||||
|
|
||||||
|
info = VideoInfo(video_id="test", source_type=VideoSourceType.YOUTUBE)
|
||||||
|
data = info.to_dict()
|
||||||
|
self.assertIsNone(data["original_duration"])
|
||||||
|
self.assertIsNone(data["clip_start"])
|
||||||
|
self.assertIsNone(data["clip_end"])
|
||||||
|
|
||||||
|
restored = VideoInfo.from_dict(data)
|
||||||
|
self.assertIsNone(restored.original_duration)
|
||||||
|
self.assertIsNone(restored.clip_start)
|
||||||
|
|
||||||
|
# ---- VideoSourceConfig clip fields ----
|
||||||
|
|
||||||
|
def test_source_config_clip_fields(self):
|
||||||
|
from skill_seekers.cli.video_models import VideoSourceConfig
|
||||||
|
|
||||||
|
config = VideoSourceConfig.from_dict(
|
||||||
|
{
|
||||||
|
"url": "https://example.com",
|
||||||
|
"clip_start": 10.0,
|
||||||
|
"clip_end": 60.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(config.clip_start, 10.0)
|
||||||
|
self.assertEqual(config.clip_end, 60.0)
|
||||||
|
|
||||||
|
def test_source_config_clip_defaults_none(self):
|
||||||
|
from skill_seekers.cli.video_models import VideoSourceConfig
|
||||||
|
|
||||||
|
config = VideoSourceConfig.from_dict({"url": "https://example.com"})
|
||||||
|
self.assertIsNone(config.clip_start)
|
||||||
|
self.assertIsNone(config.clip_end)
|
||||||
|
|
||||||
|
# ---- Converter init ----
|
||||||
|
|
||||||
|
def test_converter_init_with_clip_times(self):
|
||||||
|
from skill_seekers.cli.video_scraper import VideoToSkillConverter
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"name": "test",
|
||||||
|
"url": "https://youtube.com/watch?v=x",
|
||||||
|
"start_time": 120.0,
|
||||||
|
"end_time": 300.0,
|
||||||
|
}
|
||||||
|
converter = VideoToSkillConverter(config)
|
||||||
|
self.assertEqual(converter.start_time, 120.0)
|
||||||
|
self.assertEqual(converter.end_time, 300.0)
|
||||||
|
|
||||||
|
def test_converter_init_without_clip_times(self):
|
||||||
|
from skill_seekers.cli.video_scraper import VideoToSkillConverter
|
||||||
|
|
||||||
|
config = {"name": "test", "url": "https://youtube.com/watch?v=x"}
|
||||||
|
converter = VideoToSkillConverter(config)
|
||||||
|
self.assertIsNone(converter.start_time)
|
||||||
|
self.assertIsNone(converter.end_time)
|
||||||
|
|
||||||
|
# ---- Segmenter start_offset / end_limit ----
|
||||||
|
|
||||||
|
def test_segmenter_time_window_with_offset(self):
|
||||||
|
from skill_seekers.cli.video_segmenter import segment_by_time_window
|
||||||
|
from skill_seekers.cli.video_models import VideoInfo, VideoSourceType
|
||||||
|
|
||||||
|
info = VideoInfo(video_id="test", source_type=VideoSourceType.YOUTUBE, duration=600.0)
|
||||||
|
# Use 120s windows starting at 120s, ending at 360s
|
||||||
|
segments = segment_by_time_window(
|
||||||
|
info, [], window_seconds=120.0, start_offset=120.0, end_limit=360.0
|
||||||
|
)
|
||||||
|
# No transcript segments so no segments generated, but verify no crash
|
||||||
|
self.assertEqual(len(segments), 0)
|
||||||
|
|
||||||
|
def test_segmenter_time_window_offset_with_transcript(self):
|
||||||
|
from skill_seekers.cli.video_segmenter import segment_by_time_window
|
||||||
|
from skill_seekers.cli.video_models import (
|
||||||
|
VideoInfo,
|
||||||
|
VideoSourceType,
|
||||||
|
TranscriptSegment,
|
||||||
|
)
|
||||||
|
|
||||||
|
info = VideoInfo(video_id="test", source_type=VideoSourceType.YOUTUBE, duration=600.0)
|
||||||
|
transcript = [
|
||||||
|
TranscriptSegment(text="before clip", start=0.0, end=60.0),
|
||||||
|
TranscriptSegment(text="in clip part1", start=120.0, end=180.0),
|
||||||
|
TranscriptSegment(text="in clip part2", start=200.0, end=300.0),
|
||||||
|
TranscriptSegment(text="after clip", start=400.0, end=500.0),
|
||||||
|
]
|
||||||
|
segments = segment_by_time_window(
|
||||||
|
info, transcript, window_seconds=120.0, start_offset=120.0, end_limit=360.0
|
||||||
|
)
|
||||||
|
# Should have segments starting at 120, 240
|
||||||
|
self.assertTrue(len(segments) >= 1)
|
||||||
|
# All segments should be within clip range
|
||||||
|
for seg in segments:
|
||||||
|
self.assertGreaterEqual(seg.start_time, 120.0)
|
||||||
|
self.assertLessEqual(seg.end_time, 360.0)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
679
tests/test_video_setup.py
Normal file
679
tests/test_video_setup.py
Normal file
@@ -0,0 +1,679 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Tests for Video Setup (cli/video_setup.py) and video_visual.py resilience.
|
||||||
|
|
||||||
|
Tests cover:
|
||||||
|
- GPU detection (NVIDIA, AMD ROCm, AMD without ROCm, CPU fallback)
|
||||||
|
- CUDA / ROCm version → index URL mapping
|
||||||
|
- PyTorch installation (mocked subprocess)
|
||||||
|
- Visual deps installation (mocked subprocess)
|
||||||
|
- Installation verification
|
||||||
|
- run_setup orchestrator
|
||||||
|
- Venv detection and creation
|
||||||
|
- System dep checks (tesseract binary)
|
||||||
|
- ROCm env var configuration
|
||||||
|
- Module selection (SetupModules)
|
||||||
|
- Tesseract circuit breaker (video_visual.py)
|
||||||
|
- --setup flag in VIDEO_ARGUMENTS and early-exit in video_scraper
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from skill_seekers.cli.video_setup import (
|
||||||
|
_BASE_VIDEO_DEPS,
|
||||||
|
GPUInfo,
|
||||||
|
GPUVendor,
|
||||||
|
SetupModules,
|
||||||
|
_build_visual_deps,
|
||||||
|
_cuda_version_to_index_url,
|
||||||
|
_detect_distro,
|
||||||
|
_PYTORCH_BASE,
|
||||||
|
_rocm_version_to_index_url,
|
||||||
|
check_tesseract,
|
||||||
|
configure_rocm_env,
|
||||||
|
create_venv,
|
||||||
|
detect_gpu,
|
||||||
|
get_venv_activate_cmd,
|
||||||
|
get_venv_python,
|
||||||
|
install_torch,
|
||||||
|
install_visual_deps,
|
||||||
|
is_in_venv,
|
||||||
|
run_setup,
|
||||||
|
verify_installation,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# GPU Detection Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestGPUDetection(unittest.TestCase):
|
||||||
|
"""Tests for detect_gpu() and its helpers."""
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.shutil.which")
|
||||||
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
||||||
|
def test_nvidia_detected(self, mock_run, mock_which):
|
||||||
|
"""nvidia-smi present → GPUVendor.NVIDIA."""
|
||||||
|
mock_which.side_effect = lambda cmd: "/usr/bin/nvidia-smi" if cmd == "nvidia-smi" else None
|
||||||
|
mock_run.return_value = MagicMock(
|
||||||
|
returncode=0,
|
||||||
|
stdout=(
|
||||||
|
"+-------------------------+\n"
|
||||||
|
"| NVIDIA GeForce RTX 4090 On |\n"
|
||||||
|
"| CUDA Version: 12.4 |\n"
|
||||||
|
"+-------------------------+\n"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
gpu = detect_gpu()
|
||||||
|
assert gpu.vendor == GPUVendor.NVIDIA
|
||||||
|
assert "12.4" in gpu.compute_version
|
||||||
|
assert "cu124" in gpu.index_url
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.shutil.which")
|
||||||
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
||||||
|
@patch("skill_seekers.cli.video_setup._read_rocm_version", return_value="6.3.1")
|
||||||
|
def test_amd_rocm_detected(self, mock_rocm_ver, mock_run, mock_which):
|
||||||
|
"""rocminfo present → GPUVendor.AMD."""
|
||||||
|
|
||||||
|
def which_side(cmd):
|
||||||
|
if cmd == "nvidia-smi":
|
||||||
|
return None
|
||||||
|
if cmd == "rocminfo":
|
||||||
|
return "/usr/bin/rocminfo"
|
||||||
|
return None
|
||||||
|
|
||||||
|
mock_which.side_effect = which_side
|
||||||
|
mock_run.return_value = MagicMock(
|
||||||
|
returncode=0,
|
||||||
|
stdout="Marketing Name: AMD Radeon RX 7900 XTX\n",
|
||||||
|
)
|
||||||
|
gpu = detect_gpu()
|
||||||
|
assert gpu.vendor == GPUVendor.AMD
|
||||||
|
assert "rocm6.3" in gpu.index_url
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.shutil.which")
|
||||||
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
||||||
|
def test_amd_no_rocm_fallback(self, mock_run, mock_which):
|
||||||
|
"""AMD GPU in lspci but no ROCm → AMD vendor, CPU index URL."""
|
||||||
|
|
||||||
|
def which_side(cmd):
|
||||||
|
if cmd == "lspci":
|
||||||
|
return "/usr/bin/lspci"
|
||||||
|
return None
|
||||||
|
|
||||||
|
mock_which.side_effect = which_side
|
||||||
|
|
||||||
|
mock_run.return_value = MagicMock(
|
||||||
|
returncode=0,
|
||||||
|
stdout="06:00.0 VGA compatible controller: AMD/ATI Navi 31 [Radeon RX 7900 XTX]\n",
|
||||||
|
)
|
||||||
|
gpu = detect_gpu()
|
||||||
|
assert gpu.vendor == GPUVendor.AMD
|
||||||
|
assert "cpu" in gpu.index_url
|
||||||
|
assert any("ROCm is not installed" in d for d in gpu.details)
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.shutil.which", return_value=None)
|
||||||
|
def test_cpu_fallback(self, mock_which):
|
||||||
|
"""No GPU tools found → GPUVendor.NONE."""
|
||||||
|
gpu = detect_gpu()
|
||||||
|
assert gpu.vendor == GPUVendor.NONE
|
||||||
|
assert "cpu" in gpu.index_url
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.shutil.which")
|
||||||
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
||||||
|
def test_nvidia_smi_error(self, mock_run, mock_which):
|
||||||
|
"""nvidia-smi returns non-zero → skip to next check."""
|
||||||
|
mock_which.side_effect = lambda cmd: (
|
||||||
|
"/usr/bin/nvidia-smi" if cmd == "nvidia-smi" else None
|
||||||
|
)
|
||||||
|
mock_run.return_value = MagicMock(returncode=1, stdout="")
|
||||||
|
gpu = detect_gpu()
|
||||||
|
assert gpu.vendor == GPUVendor.NONE
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.shutil.which")
|
||||||
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
||||||
|
def test_nvidia_smi_timeout(self, mock_run, mock_which):
|
||||||
|
"""nvidia-smi times out → skip to next check."""
|
||||||
|
mock_which.side_effect = lambda cmd: (
|
||||||
|
"/usr/bin/nvidia-smi" if cmd == "nvidia-smi" else None
|
||||||
|
)
|
||||||
|
mock_run.side_effect = subprocess.TimeoutExpired(cmd="nvidia-smi", timeout=10)
|
||||||
|
gpu = detect_gpu()
|
||||||
|
assert gpu.vendor == GPUVendor.NONE
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.shutil.which")
|
||||||
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
||||||
|
def test_rocminfo_error(self, mock_run, mock_which):
|
||||||
|
"""rocminfo returns non-zero → skip to next check."""
|
||||||
|
|
||||||
|
def which_side(cmd):
|
||||||
|
if cmd == "nvidia-smi":
|
||||||
|
return None
|
||||||
|
if cmd == "rocminfo":
|
||||||
|
return "/usr/bin/rocminfo"
|
||||||
|
return None
|
||||||
|
|
||||||
|
mock_which.side_effect = which_side
|
||||||
|
mock_run.return_value = MagicMock(returncode=1, stdout="")
|
||||||
|
gpu = detect_gpu()
|
||||||
|
assert gpu.vendor == GPUVendor.NONE
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Version Mapping Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestVersionMapping(unittest.TestCase):
|
||||||
|
"""Tests for CUDA/ROCm version → index URL mapping."""
|
||||||
|
|
||||||
|
def test_cuda_124(self):
|
||||||
|
assert _cuda_version_to_index_url("12.4") == f"{_PYTORCH_BASE}/cu124"
|
||||||
|
|
||||||
|
def test_cuda_126(self):
|
||||||
|
assert _cuda_version_to_index_url("12.6") == f"{_PYTORCH_BASE}/cu124"
|
||||||
|
|
||||||
|
def test_cuda_121(self):
|
||||||
|
assert _cuda_version_to_index_url("12.1") == f"{_PYTORCH_BASE}/cu121"
|
||||||
|
|
||||||
|
def test_cuda_118(self):
|
||||||
|
assert _cuda_version_to_index_url("11.8") == f"{_PYTORCH_BASE}/cu118"
|
||||||
|
|
||||||
|
def test_cuda_old_falls_to_cpu(self):
|
||||||
|
assert _cuda_version_to_index_url("10.2") == f"{_PYTORCH_BASE}/cpu"
|
||||||
|
|
||||||
|
def test_cuda_invalid_string(self):
|
||||||
|
assert _cuda_version_to_index_url("garbage") == f"{_PYTORCH_BASE}/cpu"
|
||||||
|
|
||||||
|
def test_rocm_63(self):
|
||||||
|
assert _rocm_version_to_index_url("6.3.1") == f"{_PYTORCH_BASE}/rocm6.3"
|
||||||
|
|
||||||
|
def test_rocm_60(self):
|
||||||
|
assert _rocm_version_to_index_url("6.0") == f"{_PYTORCH_BASE}/rocm6.2.4"
|
||||||
|
|
||||||
|
def test_rocm_old_falls_to_cpu(self):
|
||||||
|
assert _rocm_version_to_index_url("5.4") == f"{_PYTORCH_BASE}/cpu"
|
||||||
|
|
||||||
|
def test_rocm_invalid(self):
|
||||||
|
assert _rocm_version_to_index_url("bad") == f"{_PYTORCH_BASE}/cpu"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Venv Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestVenv(unittest.TestCase):
|
||||||
|
"""Tests for venv detection and creation."""
|
||||||
|
|
||||||
|
def test_is_in_venv_returns_bool(self):
|
||||||
|
result = is_in_venv()
|
||||||
|
assert isinstance(result, bool)
|
||||||
|
|
||||||
|
def test_is_in_venv_detects_prefix_mismatch(self):
|
||||||
|
# If sys.prefix != sys.base_prefix, we're in a venv
|
||||||
|
with patch.object(sys, "prefix", "/some/venv"), \
|
||||||
|
patch.object(sys, "base_prefix", "/usr"):
|
||||||
|
assert is_in_venv() is True
|
||||||
|
|
||||||
|
def test_is_in_venv_detects_no_venv(self):
|
||||||
|
with patch.object(sys, "prefix", "/usr"), \
|
||||||
|
patch.object(sys, "base_prefix", "/usr"):
|
||||||
|
assert is_in_venv() is False
|
||||||
|
|
||||||
|
def test_create_venv_in_tempdir(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
venv_path = os.path.join(tmpdir, "test_venv")
|
||||||
|
result = create_venv(venv_path)
|
||||||
|
assert result is True
|
||||||
|
assert os.path.isdir(venv_path)
|
||||||
|
|
||||||
|
def test_create_venv_already_exists(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
# Create it once
|
||||||
|
create_venv(tmpdir)
|
||||||
|
# Creating again should succeed (already exists)
|
||||||
|
assert create_venv(tmpdir) is True
|
||||||
|
|
||||||
|
def test_get_venv_python_linux(self):
|
||||||
|
with patch("skill_seekers.cli.video_setup.platform.system", return_value="Linux"):
|
||||||
|
path = get_venv_python("/path/.venv")
|
||||||
|
assert path.endswith("bin/python")
|
||||||
|
|
||||||
|
def test_get_venv_activate_cmd_linux(self):
|
||||||
|
with patch("skill_seekers.cli.video_setup.platform.system", return_value="Linux"):
|
||||||
|
cmd = get_venv_activate_cmd("/path/.venv")
|
||||||
|
assert "source" in cmd
|
||||||
|
assert "bin/activate" in cmd
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# System Dep Check Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestSystemDeps(unittest.TestCase):
|
||||||
|
"""Tests for system dependency checks."""
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.shutil.which", return_value=None)
|
||||||
|
def test_tesseract_not_installed(self, mock_which):
|
||||||
|
result = check_tesseract()
|
||||||
|
assert result["installed"] is False
|
||||||
|
assert result["has_eng"] is False
|
||||||
|
assert isinstance(result["install_cmd"], str)
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
||||||
|
@patch("skill_seekers.cli.video_setup.shutil.which", return_value="/usr/bin/tesseract")
|
||||||
|
def test_tesseract_installed_with_eng(self, mock_which, mock_run):
|
||||||
|
mock_run.side_effect = [
|
||||||
|
# --version call
|
||||||
|
MagicMock(returncode=0, stdout="tesseract 5.3.0\n", stderr=""),
|
||||||
|
# --list-langs call
|
||||||
|
MagicMock(returncode=0, stdout="List of available languages:\neng\nosd\n", stderr=""),
|
||||||
|
]
|
||||||
|
result = check_tesseract()
|
||||||
|
assert result["installed"] is True
|
||||||
|
assert result["has_eng"] is True
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
||||||
|
@patch("skill_seekers.cli.video_setup.shutil.which", return_value="/usr/bin/tesseract")
|
||||||
|
def test_tesseract_installed_no_eng(self, mock_which, mock_run):
|
||||||
|
mock_run.side_effect = [
|
||||||
|
MagicMock(returncode=0, stdout="tesseract 5.3.0\n", stderr=""),
|
||||||
|
MagicMock(returncode=0, stdout="List of available languages:\nosd\n", stderr=""),
|
||||||
|
]
|
||||||
|
result = check_tesseract()
|
||||||
|
assert result["installed"] is True
|
||||||
|
assert result["has_eng"] is False
|
||||||
|
|
||||||
|
def test_detect_distro_returns_string(self):
|
||||||
|
result = _detect_distro()
|
||||||
|
assert isinstance(result, str)
|
||||||
|
|
||||||
|
@patch("builtins.open", side_effect=OSError)
|
||||||
|
def test_detect_distro_no_os_release(self, mock_open):
|
||||||
|
assert _detect_distro() == "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ROCm Configuration Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestROCmConfig(unittest.TestCase):
|
||||||
|
"""Tests for configure_rocm_env()."""
|
||||||
|
|
||||||
|
def test_sets_miopen_find_mode(self):
|
||||||
|
env_backup = os.environ.get("MIOPEN_FIND_MODE")
|
||||||
|
try:
|
||||||
|
os.environ.pop("MIOPEN_FIND_MODE", None)
|
||||||
|
changes = configure_rocm_env()
|
||||||
|
assert "MIOPEN_FIND_MODE=FAST" in changes
|
||||||
|
assert os.environ["MIOPEN_FIND_MODE"] == "FAST"
|
||||||
|
finally:
|
||||||
|
if env_backup is not None:
|
||||||
|
os.environ["MIOPEN_FIND_MODE"] = env_backup
|
||||||
|
|
||||||
|
def test_does_not_override_existing(self):
|
||||||
|
env_backup = os.environ.get("MIOPEN_FIND_MODE")
|
||||||
|
try:
|
||||||
|
os.environ["MIOPEN_FIND_MODE"] = "NORMAL"
|
||||||
|
changes = configure_rocm_env()
|
||||||
|
miopen_changes = [c for c in changes if "MIOPEN_FIND_MODE" in c]
|
||||||
|
assert len(miopen_changes) == 0
|
||||||
|
assert os.environ["MIOPEN_FIND_MODE"] == "NORMAL"
|
||||||
|
finally:
|
||||||
|
if env_backup is not None:
|
||||||
|
os.environ["MIOPEN_FIND_MODE"] = env_backup
|
||||||
|
else:
|
||||||
|
os.environ.pop("MIOPEN_FIND_MODE", None)
|
||||||
|
|
||||||
|
def test_sets_miopen_user_db_path(self):
|
||||||
|
env_backup = os.environ.get("MIOPEN_USER_DB_PATH")
|
||||||
|
try:
|
||||||
|
os.environ.pop("MIOPEN_USER_DB_PATH", None)
|
||||||
|
changes = configure_rocm_env()
|
||||||
|
db_changes = [c for c in changes if "MIOPEN_USER_DB_PATH" in c]
|
||||||
|
assert len(db_changes) == 1
|
||||||
|
finally:
|
||||||
|
if env_backup is not None:
|
||||||
|
os.environ["MIOPEN_USER_DB_PATH"] = env_backup
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Module Selection Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestModuleSelection(unittest.TestCase):
|
||||||
|
"""Tests for SetupModules and _build_visual_deps."""
|
||||||
|
|
||||||
|
def test_default_modules_all_true(self):
|
||||||
|
m = SetupModules()
|
||||||
|
assert m.torch is True
|
||||||
|
assert m.easyocr is True
|
||||||
|
assert m.opencv is True
|
||||||
|
assert m.tesseract is True
|
||||||
|
assert m.scenedetect is True
|
||||||
|
assert m.whisper is True
|
||||||
|
|
||||||
|
def test_build_all_deps(self):
|
||||||
|
deps = _build_visual_deps(SetupModules())
|
||||||
|
assert "yt-dlp" in deps
|
||||||
|
assert "youtube-transcript-api" in deps
|
||||||
|
assert "easyocr" in deps
|
||||||
|
assert "opencv-python-headless" in deps
|
||||||
|
assert "pytesseract" in deps
|
||||||
|
assert "scenedetect[opencv]" in deps
|
||||||
|
assert "faster-whisper" in deps
|
||||||
|
|
||||||
|
def test_build_no_optional_deps(self):
|
||||||
|
"""Even with all optional modules off, base video deps are included."""
|
||||||
|
m = SetupModules(
|
||||||
|
torch=False, easyocr=False, opencv=False,
|
||||||
|
tesseract=False, scenedetect=False, whisper=False,
|
||||||
|
)
|
||||||
|
deps = _build_visual_deps(m)
|
||||||
|
assert deps == list(_BASE_VIDEO_DEPS)
|
||||||
|
|
||||||
|
def test_build_partial_deps(self):
|
||||||
|
m = SetupModules(easyocr=True, opencv=True, tesseract=False, scenedetect=False, whisper=False)
|
||||||
|
deps = _build_visual_deps(m)
|
||||||
|
assert "yt-dlp" in deps
|
||||||
|
assert "youtube-transcript-api" in deps
|
||||||
|
assert "easyocr" in deps
|
||||||
|
assert "opencv-python-headless" in deps
|
||||||
|
assert "pytesseract" not in deps
|
||||||
|
assert "faster-whisper" not in deps
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Installation Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestInstallation(unittest.TestCase):
|
||||||
|
"""Tests for install_torch() and install_visual_deps()."""
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
||||||
|
def test_install_torch_success(self, mock_run):
|
||||||
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
||||||
|
gpu = GPUInfo(vendor=GPUVendor.NVIDIA, index_url=f"{_PYTORCH_BASE}/cu124")
|
||||||
|
assert install_torch(gpu) is True
|
||||||
|
call_args = mock_run.call_args[0][0]
|
||||||
|
assert "torch" in call_args
|
||||||
|
assert "--index-url" in call_args
|
||||||
|
assert f"{_PYTORCH_BASE}/cu124" in call_args
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
||||||
|
def test_install_torch_cpu(self, mock_run):
|
||||||
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
||||||
|
gpu = GPUInfo(vendor=GPUVendor.NONE, index_url=f"{_PYTORCH_BASE}/cpu")
|
||||||
|
assert install_torch(gpu) is True
|
||||||
|
call_args = mock_run.call_args[0][0]
|
||||||
|
assert f"{_PYTORCH_BASE}/cpu" in call_args
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
||||||
|
def test_install_torch_failure(self, mock_run):
|
||||||
|
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="error msg")
|
||||||
|
gpu = GPUInfo(vendor=GPUVendor.NVIDIA, index_url=f"{_PYTORCH_BASE}/cu124")
|
||||||
|
assert install_torch(gpu) is False
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
||||||
|
def test_install_torch_timeout(self, mock_run):
|
||||||
|
mock_run.side_effect = subprocess.TimeoutExpired(cmd="pip", timeout=600)
|
||||||
|
gpu = GPUInfo(vendor=GPUVendor.NVIDIA, index_url=f"{_PYTORCH_BASE}/cu124")
|
||||||
|
assert install_torch(gpu) is False
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
||||||
|
def test_install_torch_custom_python(self, mock_run):
|
||||||
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
||||||
|
gpu = GPUInfo(vendor=GPUVendor.NONE, index_url=f"{_PYTORCH_BASE}/cpu")
|
||||||
|
install_torch(gpu, python_exe="/custom/python")
|
||||||
|
call_args = mock_run.call_args[0][0]
|
||||||
|
assert call_args[0] == "/custom/python"
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
||||||
|
def test_install_visual_deps_success(self, mock_run):
|
||||||
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
||||||
|
assert install_visual_deps() is True
|
||||||
|
call_args = mock_run.call_args[0][0]
|
||||||
|
assert "easyocr" in call_args
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
||||||
|
def test_install_visual_deps_failure(self, mock_run):
|
||||||
|
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="error")
|
||||||
|
assert install_visual_deps() is False
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
||||||
|
def test_install_visual_deps_partial_modules(self, mock_run):
|
||||||
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
||||||
|
modules = SetupModules(easyocr=True, opencv=False, tesseract=False, scenedetect=False, whisper=False)
|
||||||
|
install_visual_deps(modules)
|
||||||
|
call_args = mock_run.call_args[0][0]
|
||||||
|
assert "easyocr" in call_args
|
||||||
|
assert "opencv-python-headless" not in call_args
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
||||||
|
def test_install_visual_deps_base_only(self, mock_run):
|
||||||
|
"""Even with all optional modules off, base video deps get installed."""
|
||||||
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
||||||
|
modules = SetupModules(easyocr=False, opencv=False, tesseract=False, scenedetect=False, whisper=False)
|
||||||
|
result = install_visual_deps(modules)
|
||||||
|
assert result is True
|
||||||
|
call_args = mock_run.call_args[0][0]
|
||||||
|
assert "yt-dlp" in call_args
|
||||||
|
assert "youtube-transcript-api" in call_args
|
||||||
|
assert "easyocr" not in call_args
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Verification Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestVerification(unittest.TestCase):
|
||||||
|
"""Tests for verify_installation()."""
|
||||||
|
|
||||||
|
@patch.dict("sys.modules", {"torch": None, "easyocr": None, "cv2": None})
|
||||||
|
def test_returns_dict(self):
|
||||||
|
results = verify_installation()
|
||||||
|
assert isinstance(results, dict)
|
||||||
|
|
||||||
|
def test_expected_keys(self):
|
||||||
|
results = verify_installation()
|
||||||
|
for key in ("yt-dlp", "youtube-transcript-api", "torch", "torch.cuda", "torch.rocm", "easyocr", "opencv"):
|
||||||
|
assert key in results, f"Missing key: {key}"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Orchestrator Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestRunSetup(unittest.TestCase):
|
||||||
|
"""Tests for run_setup() orchestrator."""
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.verify_installation")
|
||||||
|
@patch("skill_seekers.cli.video_setup.install_visual_deps", return_value=True)
|
||||||
|
@patch("skill_seekers.cli.video_setup.install_torch", return_value=True)
|
||||||
|
@patch("skill_seekers.cli.video_setup.check_tesseract")
|
||||||
|
@patch("skill_seekers.cli.video_setup.detect_gpu")
|
||||||
|
def test_non_interactive_success(self, mock_detect, mock_tess, mock_torch, mock_deps, mock_verify):
|
||||||
|
mock_detect.return_value = GPUInfo(
|
||||||
|
vendor=GPUVendor.NONE, name="CPU-only", index_url=f"{_PYTORCH_BASE}/cpu",
|
||||||
|
)
|
||||||
|
mock_tess.return_value = {"installed": True, "has_eng": True, "install_cmd": "", "version": "5.3.0"}
|
||||||
|
mock_verify.return_value = {
|
||||||
|
"torch": True, "torch.cuda": False, "torch.rocm": False,
|
||||||
|
"easyocr": True, "opencv": True, "pytesseract": True,
|
||||||
|
"scenedetect": True, "faster-whisper": True,
|
||||||
|
}
|
||||||
|
rc = run_setup(interactive=False)
|
||||||
|
assert rc == 0
|
||||||
|
mock_torch.assert_called_once()
|
||||||
|
mock_deps.assert_called_once()
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.install_torch", return_value=False)
|
||||||
|
@patch("skill_seekers.cli.video_setup.check_tesseract")
|
||||||
|
@patch("skill_seekers.cli.video_setup.detect_gpu")
|
||||||
|
def test_failure_returns_nonzero(self, mock_detect, mock_tess, mock_torch):
|
||||||
|
mock_detect.return_value = GPUInfo(
|
||||||
|
vendor=GPUVendor.NONE, name="CPU-only", index_url=f"{_PYTORCH_BASE}/cpu",
|
||||||
|
)
|
||||||
|
mock_tess.return_value = {"installed": True, "has_eng": True, "install_cmd": "", "version": "5.3.0"}
|
||||||
|
rc = run_setup(interactive=False)
|
||||||
|
assert rc == 1
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.install_torch", return_value=True)
|
||||||
|
@patch("skill_seekers.cli.video_setup.install_visual_deps", return_value=False)
|
||||||
|
@patch("skill_seekers.cli.video_setup.check_tesseract")
|
||||||
|
@patch("skill_seekers.cli.video_setup.detect_gpu")
|
||||||
|
def test_visual_deps_failure(self, mock_detect, mock_tess, mock_deps, mock_torch):
|
||||||
|
mock_detect.return_value = GPUInfo(
|
||||||
|
vendor=GPUVendor.NONE, name="CPU-only", index_url=f"{_PYTORCH_BASE}/cpu",
|
||||||
|
)
|
||||||
|
mock_tess.return_value = {"installed": True, "has_eng": True, "install_cmd": "", "version": "5.3.0"}
|
||||||
|
rc = run_setup(interactive=False)
|
||||||
|
assert rc == 1
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.verify_installation")
|
||||||
|
@patch("skill_seekers.cli.video_setup.install_visual_deps", return_value=True)
|
||||||
|
@patch("skill_seekers.cli.video_setup.install_torch", return_value=True)
|
||||||
|
@patch("skill_seekers.cli.video_setup.check_tesseract")
|
||||||
|
@patch("skill_seekers.cli.video_setup.detect_gpu")
|
||||||
|
def test_rocm_configures_env(self, mock_detect, mock_tess, mock_torch, mock_deps, mock_verify):
|
||||||
|
"""AMD GPU → configure_rocm_env called and env vars set."""
|
||||||
|
mock_detect.return_value = GPUInfo(
|
||||||
|
vendor=GPUVendor.AMD, name="RX 7900", index_url=f"{_PYTORCH_BASE}/rocm6.3",
|
||||||
|
)
|
||||||
|
mock_tess.return_value = {"installed": True, "has_eng": True, "install_cmd": "", "version": "5.3.0"}
|
||||||
|
mock_verify.return_value = {
|
||||||
|
"torch": True, "torch.cuda": False, "torch.rocm": True,
|
||||||
|
"easyocr": True, "opencv": True, "pytesseract": True,
|
||||||
|
"scenedetect": True, "faster-whisper": True,
|
||||||
|
}
|
||||||
|
rc = run_setup(interactive=False)
|
||||||
|
assert rc == 0
|
||||||
|
assert os.environ.get("MIOPEN_FIND_MODE") is not None
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Tesseract Circuit Breaker Tests (video_visual.py)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestTesseractCircuitBreaker(unittest.TestCase):
|
||||||
|
"""Tests for _tesseract_broken flag in video_visual.py."""
|
||||||
|
|
||||||
|
def test_circuit_breaker_flag_exists(self):
|
||||||
|
import skill_seekers.cli.video_visual as vv
|
||||||
|
assert hasattr(vv, "_tesseract_broken")
|
||||||
|
|
||||||
|
def test_circuit_breaker_skips_after_failure(self):
|
||||||
|
import skill_seekers.cli.video_visual as vv
|
||||||
|
from skill_seekers.cli.video_models import FrameType
|
||||||
|
|
||||||
|
# Save and set broken state
|
||||||
|
original = vv._tesseract_broken
|
||||||
|
try:
|
||||||
|
vv._tesseract_broken = True
|
||||||
|
result = vv._run_tesseract_ocr("/nonexistent/path.png", FrameType.CODE_EDITOR)
|
||||||
|
assert result == []
|
||||||
|
finally:
|
||||||
|
vv._tesseract_broken = original
|
||||||
|
|
||||||
|
def test_circuit_breaker_allows_when_not_broken(self):
|
||||||
|
import skill_seekers.cli.video_visual as vv
|
||||||
|
from skill_seekers.cli.video_models import FrameType
|
||||||
|
|
||||||
|
original = vv._tesseract_broken
|
||||||
|
try:
|
||||||
|
vv._tesseract_broken = False
|
||||||
|
if not vv.HAS_PYTESSERACT:
|
||||||
|
# pytesseract not installed → returns [] immediately
|
||||||
|
result = vv._run_tesseract_ocr("/nonexistent/path.png", FrameType.CODE_EDITOR)
|
||||||
|
assert result == []
|
||||||
|
# If pytesseract IS installed, it would try to run and potentially fail
|
||||||
|
# on our fake path — that's fine, the circuit breaker would trigger
|
||||||
|
finally:
|
||||||
|
vv._tesseract_broken = original
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MIOPEN Env Var Tests (video_visual.py)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestMIOPENEnvVars(unittest.TestCase):
|
||||||
|
"""Tests that video_visual.py sets MIOPEN env vars at import time."""
|
||||||
|
|
||||||
|
def test_miopen_find_mode_set(self):
|
||||||
|
# video_visual.py sets this at module level before torch import
|
||||||
|
assert "MIOPEN_FIND_MODE" in os.environ
|
||||||
|
|
||||||
|
def test_miopen_user_db_path_set(self):
|
||||||
|
assert "MIOPEN_USER_DB_PATH" in os.environ
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Argument & Early-Exit Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestVideoArgumentSetup(unittest.TestCase):
|
||||||
|
"""Tests for --setup flag in VIDEO_ARGUMENTS."""
|
||||||
|
|
||||||
|
def test_setup_in_video_arguments(self):
|
||||||
|
from skill_seekers.cli.arguments.video import VIDEO_ARGUMENTS
|
||||||
|
|
||||||
|
assert "setup" in VIDEO_ARGUMENTS
|
||||||
|
assert VIDEO_ARGUMENTS["setup"]["kwargs"]["action"] == "store_true"
|
||||||
|
|
||||||
|
def test_parser_accepts_setup(self):
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from skill_seekers.cli.arguments.video import add_video_arguments
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
add_video_arguments(parser)
|
||||||
|
args = parser.parse_args(["--setup"])
|
||||||
|
assert args.setup is True
|
||||||
|
|
||||||
|
def test_parser_default_false(self):
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from skill_seekers.cli.arguments.video import add_video_arguments
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
add_video_arguments(parser)
|
||||||
|
args = parser.parse_args(["--url", "https://example.com"])
|
||||||
|
assert args.setup is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestVideoScraperSetupEarlyExit(unittest.TestCase):
|
||||||
|
"""Test that --setup exits before source validation."""
|
||||||
|
|
||||||
|
@patch("skill_seekers.cli.video_setup.run_setup", return_value=0)
|
||||||
|
def test_setup_skips_source_validation(self, mock_setup):
|
||||||
|
"""--setup without --url should NOT error about missing source."""
|
||||||
|
from skill_seekers.cli.video_scraper import main
|
||||||
|
|
||||||
|
old_argv = sys.argv
|
||||||
|
try:
|
||||||
|
sys.argv = ["video_scraper", "--setup"]
|
||||||
|
rc = main()
|
||||||
|
assert rc == 0
|
||||||
|
mock_setup.assert_called_once_with(interactive=True)
|
||||||
|
finally:
|
||||||
|
sys.argv = old_argv
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
462
uv.lock
generated
462
uv.lock
generated
@@ -1078,31 +1078,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" },
|
{ url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "easyocr"
|
|
||||||
version = "1.7.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "ninja" },
|
|
||||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
||||||
{ name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
||||||
{ name = "opencv-python-headless" },
|
|
||||||
{ name = "pillow" },
|
|
||||||
{ name = "pyclipper" },
|
|
||||||
{ name = "python-bidi" },
|
|
||||||
{ name = "pyyaml" },
|
|
||||||
{ name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
||||||
{ name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
||||||
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
||||||
{ name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
||||||
{ name = "shapely" },
|
|
||||||
{ name = "torch" },
|
|
||||||
{ name = "torchvision" },
|
|
||||||
]
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/84/4a2cab0e6adde6a85e7ba543862e5fc0250c51f3ac721a078a55cdcff250/easyocr-1.7.2-py3-none-any.whl", hash = "sha256:5be12f9b0e595d443c9c3d10b0542074b50f0ec2d98b141a109cd961fd1c177c", size = 2870178, upload-time = "2024-09-24T11:34:43.554Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "exceptiongroup"
|
name = "exceptiongroup"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -1898,20 +1873,6 @@ 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" },
|
{ 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 = "imageio"
|
|
||||||
version = "2.37.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
||||||
{ name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
||||||
{ name = "pillow" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "importlib-metadata"
|
name = "importlib-metadata"
|
||||||
version = "8.7.1"
|
version = "8.7.1"
|
||||||
@@ -2267,18 +2228,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e6/8e/063e09c5e8a3dcd77e2a8f0bff3f71c1c52a9d238da1bcafd2df3281da17/langsmith-0.6.9-py3-none-any.whl", hash = "sha256:86ba521e042397f6fbb79d63991df9d5f7b6a6dd6a6323d4f92131291478dcff", size = 319228, upload-time = "2026-02-05T20:10:54.248Z" },
|
{ url = "https://files.pythonhosted.org/packages/e6/8e/063e09c5e8a3dcd77e2a8f0bff3f71c1c52a9d238da1bcafd2df3281da17/langsmith-0.6.9-py3-none-any.whl", hash = "sha256:86ba521e042397f6fbb79d63991df9d5f7b6a6dd6a6323d4f92131291478dcff", size = 319228, upload-time = "2026-02-05T20:10:54.248Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lazy-loader"
|
|
||||||
version = "0.4"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "packaging" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "librt"
|
name = "librt"
|
||||||
version = "0.7.8"
|
version = "0.7.8"
|
||||||
@@ -3196,32 +3145,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
|
{ url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ninja"
|
|
||||||
version = "1.13.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/43/73/79a0b22fc731989c708068427579e840a6cf4e937fe7ae5c5d0b7356ac22/ninja-1.13.0.tar.gz", hash = "sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978", size = 242558, upload-time = "2025-08-11T15:10:19.421Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/74/d02409ed2aa865e051b7edda22ad416a39d81a84980f544f8de717cab133/ninja-1.13.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1", size = 310125, upload-time = "2025-08-11T15:09:50.971Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/de/6e1cd6b84b412ac1ef327b76f0641aeb5dcc01e9d3f9eee0286d0c34fd93/ninja-1.13.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630", size = 177467, upload-time = "2025-08-11T15:09:52.767Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c8/83/49320fb6e58ae3c079381e333575fdbcf1cca3506ee160a2dcce775046fa/ninja-1.13.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c", size = 187834, upload-time = "2025-08-11T15:09:54.115Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/c7/ba22748fb59f7f896b609cd3e568d28a0a367a6d953c24c461fe04fc4433/ninja-1.13.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e", size = 202736, upload-time = "2025-08-11T15:09:55.745Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/22/d1de07632b78ac8e6b785f41fa9aad7a978ec8c0a1bf15772def36d77aac/ninja-1.13.0-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1c97223cdda0417f414bf864cfb73b72d8777e57ebb279c5f6de368de0062988", size = 179034, upload-time = "2025-08-11T15:09:57.394Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/de/0e6edf44d6a04dabd0318a519125ed0415ce437ad5a1ec9b9be03d9048cf/ninja-1.13.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa", size = 180716, upload-time = "2025-08-11T15:09:58.696Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/28/938b562f9057aaa4d6bfbeaa05e81899a47aebb3ba6751e36c027a7f5ff7/ninja-1.13.0-py3-none-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4be9c1b082d244b1ad7ef41eb8ab088aae8c109a9f3f0b3e56a252d3e00f42c1", size = 146843, upload-time = "2025-08-11T15:10:00.046Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/fb/d06a3838de4f8ab866e44ee52a797b5491df823901c54943b2adb0389fbb/ninja-1.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6739d3352073341ad284246f81339a384eec091d9851a886dfa5b00a6d48b3e2", size = 154402, upload-time = "2025-08-11T15:10:01.657Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/bf/0d7808af695ceddc763cf251b84a9892cd7f51622dc8b4c89d5012779f06/ninja-1.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11be2d22027bde06f14c343f01d31446747dbb51e72d00decca2eb99be911e2f", size = 552388, upload-time = "2025-08-11T15:10:03.349Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/70/c99d0c2c809f992752453cce312848abb3b1607e56d4cd1b6cded317351a/ninja-1.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aa45b4037b313c2f698bc13306239b8b93b4680eb47e287773156ac9e9304714", size = 472501, upload-time = "2025-08-11T15:10:04.735Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/43/c217b1153f0e499652f5e0766da8523ce3480f0a951039c7af115e224d55/ninja-1.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f8e1e8a1a30835eeb51db05cf5a67151ad37542f5a4af2a438e9490915e5b72", size = 638280, upload-time = "2025-08-11T15:10:06.512Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8c/45/9151bba2c8d0ae2b6260f71696330590de5850e5574b7b5694dce6023e20/ninja-1.13.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:3d7d7779d12cb20c6d054c61b702139fd23a7a964ec8f2c823f1ab1b084150db", size = 642420, upload-time = "2025-08-11T15:10:08.35Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/fb/95752eb635bb8ad27d101d71bef15bc63049de23f299e312878fc21cb2da/ninja-1.13.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:d741a5e6754e0bda767e3274a0f0deeef4807f1fec6c0d7921a0244018926ae5", size = 585106, upload-time = "2025-08-11T15:10:09.818Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/31/aa56a1a286703800c0cbe39fb4e82811c277772dc8cd084f442dd8e2938a/ninja-1.13.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:e8bad11f8a00b64137e9b315b137d8bb6cbf3086fbdc43bf1f90fd33324d2e96", size = 707138, upload-time = "2025-08-11T15:10:11.366Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/34/6f/5f5a54a1041af945130abdb2b8529cbef0cdcbbf9bcf3f4195378319d29a/ninja-1.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b4f2a072db3c0f944c32793e91532d8948d20d9ab83da9c0c7c15b5768072200", size = 581758, upload-time = "2025-08-11T15:10:13.295Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/97/51359c77527d45943fe7a94d00a3843b81162e6c4244b3579fe8fc54cb9c/ninja-1.13.0-py3-none-win32.whl", hash = "sha256:8cfbb80b4a53456ae8a39f90ae3d7a2129f45ea164f43fadfa15dc38c4aef1c9", size = 267201, upload-time = "2025-08-11T15:10:15.158Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/45/c0adfbfb0b5895aa18cec400c535b4f7ff3e52536e0403602fc1a23f7de9/ninja-1.13.0-py3-none-win_amd64.whl", hash = "sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e", size = 309975, upload-time = "2025-08-11T15:10:16.697Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/93/a7b983643d1253bb223234b5b226e69de6cda02b76cdca7770f684b795f5/ninja-1.13.0-py3-none-win_arm64.whl", hash = "sha256:3c0b40b1f0bba764644385319028650087b4c1b18cdfa6f45cb39a3669b81aa9", size = 290806, upload-time = "2025-08-11T15:10:18.018Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nltk"
|
name = "nltk"
|
||||||
version = "3.9.2"
|
version = "3.9.2"
|
||||||
@@ -4464,49 +4387,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ee/2b/e18ee7c5ee508a82897f021c1981533eca2940b5f072fc6ed0906c03a7a7/pybase64-1.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:debf737e09b8bf832ba86f5ecc3d3dbd0e3021d6cd86ba4abe962d6a5a77adb3", size = 36134, upload-time = "2025-12-06T13:26:47.35Z" },
|
{ url = "https://files.pythonhosted.org/packages/ee/2b/e18ee7c5ee508a82897f021c1981533eca2940b5f072fc6ed0906c03a7a7/pybase64-1.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:debf737e09b8bf832ba86f5ecc3d3dbd0e3021d6cd86ba4abe962d6a5a77adb3", size = 36134, upload-time = "2025-12-06T13:26:47.35Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pyclipper"
|
|
||||||
version = "1.4.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/21/3c06205bb407e1f79b73b7b4dfb3950bd9537c4f625a68ab5cc41177f5bc/pyclipper-1.4.0.tar.gz", hash = "sha256:9882bd889f27da78add4dd6f881d25697efc740bf840274e749988d25496c8e1", size = 54489, upload-time = "2025-12-01T13:15:35.015Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/9f/a10173d32ecc2ce19a04d018163f3ca22a04c0c6ad03b464dcd32f9152a8/pyclipper-1.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bafad70d2679c187120e8c44e1f9a8b06150bad8c0aecf612ad7dfbfa9510f73", size = 264510, upload-time = "2025-12-01T13:14:46.551Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/c2/5490ddc4a1f7ceeaa0258f4266397e720c02db515b2ca5bc69b85676f697/pyclipper-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b74a9dd44b22a7fd35d65fb1ceeba57f3817f34a97a28c3255556362e491447", size = 139498, upload-time = "2025-12-01T13:14:48.31Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3b/0a/bea9102d1d75634b1a5702b0e92982451a1eafca73c4845d3dbe27eba13d/pyclipper-1.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a4d2736fb3c42e8eb1d38bf27a720d1015526c11e476bded55138a977c17d9d", size = 970974, upload-time = "2025-12-01T13:14:49.799Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/1b/097f8776d5b3a10eb7b443b632221f4ed825d892e79e05682f4b10a1a59c/pyclipper-1.4.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3b3630051b53ad2564cb079e088b112dd576e3d91038338ad1cc7915e0f14dc", size = 943315, upload-time = "2025-12-01T13:14:51.266Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/4d/17d6a3f1abf0f368d58f2309e80ee3761afb1fd1342f7780ab32ba4f0b1d/pyclipper-1.4.0-cp310-cp310-win32.whl", hash = "sha256:8d42b07a2f6cfe2d9b87daf345443583f00a14e856927782fde52f3a255e305a", size = 95286, upload-time = "2025-12-01T13:14:52.922Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/53/ca/b30138427ed122ec9b47980b943164974a2ec606fa3f71597033b9a9f9a6/pyclipper-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:6a97b961f182b92d899ca88c1bb3632faea2e00ce18d07c5f789666ebb021ca4", size = 104227, upload-time = "2025-12-01T13:14:54.013Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/e3/64cf7794319b088c288706087141e53ac259c7959728303276d18adc665d/pyclipper-1.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:adcb7ca33c5bdc33cd775e8b3eadad54873c802a6d909067a57348bcb96e7a2d", size = 264281, upload-time = "2025-12-01T13:14:55.47Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/34/cd/44ec0da0306fa4231e76f1c2cb1fa394d7bde8db490a2b24d55b39865f69/pyclipper-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd24849d2b94ec749ceac7c34c9f01010d23b6e9d9216cf2238b8481160e703d", size = 139426, upload-time = "2025-12-01T13:14:56.683Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/88/d8f6c6763ea622fe35e19c75d8b39ed6c55191ddc82d65e06bc46b26cb8e/pyclipper-1.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b6c8d75ba20c6433c9ea8f1a0feb7e4d3ac06a09ad1fd6d571afc1ddf89b869", size = 989649, upload-time = "2025-12-01T13:14:58.28Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/e9/ea7d68c8c4af3842d6515bedcf06418610ad75f111e64c92c1d4785a1513/pyclipper-1.4.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58e29d7443d7cc0e83ee9daf43927730386629786d00c63b04fe3b53ac01462c", size = 962842, upload-time = "2025-12-01T13:15:00.044Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/b7/0b4a272d8726e51ab05e2b933d8cc47f29757fb8212e38b619e170e6015c/pyclipper-1.4.0-cp311-cp311-win32.whl", hash = "sha256:a8d2b5fb75ebe57e21ce61e79a9131edec2622ff23cc665e4d1d1f201bc1a801", size = 95098, upload-time = "2025-12-01T13:15:01.359Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3a/76/4901de2919198bb2bd3d989f86d4a1dff363962425bb2d63e24e6c990042/pyclipper-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:e9b973467d9c5fa9bc30bb6ac95f9f4d7c3d9fc25f6cf2d1cc972088e5955c01", size = 104362, upload-time = "2025-12-01T13:15:02.439Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/1b/7a07b68e0842324d46c03e512d8eefa9cb92ba2a792b3b4ebf939dafcac3/pyclipper-1.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:222ac96c8b8281b53d695b9c4fedc674f56d6d4320ad23f1bdbd168f4e316140", size = 265676, upload-time = "2025-12-01T13:15:04.15Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/dd/8bd622521c05d04963420ae6664093f154343ed044c53ea260a310c8bb4d/pyclipper-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f3672dbafbb458f1b96e1ee3e610d174acb5ace5bd2ed5d1252603bb797f2fc6", size = 140458, upload-time = "2025-12-01T13:15:05.76Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7a/06/6e3e241882bf7d6ab23d9c69ba4e85f1ec47397cbbeee948a16cf75e21ed/pyclipper-1.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1f807e2b4760a8e5c6d6b4e8c1d71ef52b7fe1946ff088f4fa41e16a881a5ca", size = 978235, upload-time = "2025-12-01T13:15:06.993Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/f4/3418c1cd5eea640a9fa2501d4bc0b3655fa8d40145d1a4f484b987990a75/pyclipper-1.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce1f83c9a4e10ea3de1959f0ae79e9a5bd41346dff648fee6228ba9eaf8b3872", size = 961388, upload-time = "2025-12-01T13:15:08.467Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/94/c85401d24be634af529c962dd5d781f3cb62a67cd769534df2cb3feee97a/pyclipper-1.4.0-cp312-cp312-win32.whl", hash = "sha256:3ef44b64666ebf1cb521a08a60c3e639d21b8c50bfbe846ba7c52a0415e936f4", size = 95169, upload-time = "2025-12-01T13:15:10.098Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/77/dfea08e3b230b82ee22543c30c35d33d42f846a77f96caf7c504dd54fab1/pyclipper-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:d1e5498d883b706a4ce636247f0d830c6eb34a25b843a1b78e2c969754ca9037", size = 104619, upload-time = "2025-12-01T13:15:11.592Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/d0/cbce7d47de1e6458f66a4d999b091640134deb8f2c7351eab993b70d2e10/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d49df13cbb2627ccb13a1046f3ea6ebf7177b5504ec61bdef87d6a704046fd6e", size = 264342, upload-time = "2025-12-01T13:15:12.697Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/cc/742b9d69d96c58ac156947e1b56d0f81cbacbccf869e2ac7229f2f86dc4e/pyclipper-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37bfec361e174110cdddffd5ecd070a8064015c99383d95eb692c253951eee8a", size = 139839, upload-time = "2025-12-01T13:15:13.911Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/db/48/dd301d62c1529efdd721b47b9e5fb52120fcdac5f4d3405cfc0d2f391414/pyclipper-1.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:14c8bdb5a72004b721c4e6f448d2c2262d74a7f0c9e3076aeff41e564a92389f", size = 972142, upload-time = "2025-12-01T13:15:15.477Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/bf/d493fd1b33bb090fa64e28c1009374d5d72fa705f9331cd56517c35e381e/pyclipper-1.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2a50c22c3a78cb4e48347ecf06930f61ce98cf9252f2e292aa025471e9d75b1", size = 952789, upload-time = "2025-12-01T13:15:17.042Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/88/b95ea8ea21ddca34aa14b123226a81526dd2faaa993f9aabd3ed21231604/pyclipper-1.4.0-cp313-cp313-win32.whl", hash = "sha256:c9a3faa416ff536cee93417a72bfb690d9dea136dc39a39dbbe1e5dadf108c9c", size = 94817, upload-time = "2025-12-01T13:15:18.724Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/42/0a1920d276a0e1ca21dc0d13ee9e3ba10a9a8aa3abac76cd5e5a9f503306/pyclipper-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:d4b2d7c41086f1927d14947c563dfc7beed2f6c0d9af13c42fe3dcdc20d35832", size = 104007, upload-time = "2025-12-01T13:15:19.763Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/20/04d58c70f3ccd404f179f8dd81d16722a05a3bf1ab61445ee64e8218c1f8/pyclipper-1.4.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7c87480fc91a5af4c1ba310bdb7de2f089a3eeef5fe351a3cedc37da1fcced1c", size = 265167, upload-time = "2025-12-01T13:15:20.844Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/2e/a570c1abe69b7260ca0caab4236ce6ea3661193ebf8d1bd7f78ccce537a5/pyclipper-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81d8bb2d1fb9d66dc7ea4373b176bb4b02443a7e328b3b603a73faec088b952e", size = 139966, upload-time = "2025-12-01T13:15:22.036Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e8/3b/e0859e54adabdde8a24a29d3f525ebb31c71ddf2e8d93edce83a3c212ffc/pyclipper-1.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:773c0e06b683214dcfc6711be230c83b03cddebe8a57eae053d4603dd63582f9", size = 968216, upload-time = "2025-12-01T13:15:23.18Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/6b/e3c4febf0a35ae643ee579b09988dd931602b5bf311020535fd9e5b7e715/pyclipper-1.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bc45f2463d997848450dbed91c950ca37c6cf27f84a49a5cad4affc0b469e39", size = 954198, upload-time = "2025-12-01T13:15:24.522Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fc/74/728efcee02e12acb486ce9d56fa037120c9bf5b77c54bbdbaa441c14a9d9/pyclipper-1.4.0-cp314-cp314-win32.whl", hash = "sha256:0b8c2105b3b3c44dbe1a266f64309407fe30bf372cf39a94dc8aaa97df00da5b", size = 96951, upload-time = "2025-12-01T13:15:25.79Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/d7/7f4354e69f10a917e5c7d5d72a499ef2e10945312f5e72c414a0a08d2ae4/pyclipper-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:6c317e182590c88ec0194149995e3d71a979cfef3b246383f4e035f9d4a11826", size = 106782, upload-time = "2025-12-01T13:15:26.945Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/60/fc32c7a3d7f61a970511ec2857ecd09693d8ac80d560ee7b8e67a6d268c9/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f160a2c6ba036f7eaf09f1f10f4fbfa734234af9112fb5187877efed78df9303", size = 269880, upload-time = "2025-12-01T13:15:28.117Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/df/c4a72d3f62f0ba03ec440c4fff56cd2d674a4334d23c5064cbf41c9583f6/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a9f11ad133257c52c40d50de7a0ca3370a0cdd8e3d11eec0604ad3c34ba549e9", size = 141706, upload-time = "2025-12-01T13:15:30.134Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c5/0b/cf55df03e2175e1e2da9db585241401e0bc98f76bee3791bed39d0313449/pyclipper-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:bbc827b77442c99deaeee26e0e7f172355ddb097a5e126aea206d447d3b26286", size = 105308, upload-time = "2025-12-01T13:15:31.225Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/dc/53df8b6931d47080b4fe4ee8450d42e660ee1c5c1556c7ab73359182b769/pyclipper-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29dae3e0296dff8502eeb7639fcfee794b0eec8590ba3563aee28db269da6b04", size = 117608, upload-time = "2025-12-01T13:15:32.69Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/59/81050abdc9e5b90ffc2c765738c5e40e9abd8e44864aaa737b600f16c562/pyclipper-1.4.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98b2a40f98e1fc1b29e8a6094072e7e0c7dfe901e573bf6cfc6eb7ce84a7ae87", size = 126495, upload-time = "2025-12-01T13:15:33.743Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "2.23"
|
version = "2.23"
|
||||||
@@ -4853,90 +4733,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
|
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "python-bidi"
|
|
||||||
version = "0.6.7"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/e3/c0c8bf6fca79ac946a28d57f116e3b9e5b10a4469b6f70bf73f3744c49bf/python_bidi-0.6.7.tar.gz", hash = "sha256:c10065081c0e137975de5d9ba2ff2306286dbf5e0c586d4d5aec87c856239b41", size = 45503, upload-time = "2025-10-22T09:52:49.624Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/c3/cdbece686fab47d4d04f2c15d372b3d3f3308da2e535657bf4bbd5afef50/python_bidi-0.6.7-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:94dbfd6a6ec0ae64b5262290bf014d6063f9ac8688bda9ec668dc175378d2c80", size = 274857, upload-time = "2025-10-22T09:51:57.298Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/19/1cd52f04345717613eafe8b23dd1ce8799116f7cc54b23aaefa27db298d6/python_bidi-0.6.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8274ff02d447cca026ba00f56070ba15f95e184b2d028ee0e4b6c9813d2aaf9", size = 264682, upload-time = "2025-10-22T09:51:48.203Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/39/f46dae8bd298ffecaf169ea8871c1e63c6116e1b0178ca4eab2cb99d1c13/python_bidi-0.6.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24afff65c581a5d6f658a9ec027d6719d19a1d8a4401000fdb22d2eeb677b8e3", size = 293680, upload-time = "2025-10-22T09:50:57.091Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/ed/c4e2c684bf8f226de4d0070780073fc7f3f97def3ad06f11b4c021bfa965/python_bidi-0.6.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8678c2272e7bd60a75f781409e900c9ddb9f01f55c625d83ae0d49dfc6a2674f", size = 302625, upload-time = "2025-10-22T09:51:05.378Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/83/fa/3b5be9187515a4c28ad358c2f2785f968d4de090389f08a11c826ae1c17f/python_bidi-0.6.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4cd82e65b5aeb31bd73534e61ece1cab625f4bcbdc13bc4ddc5f8cbfb37c24a", size = 441183, upload-time = "2025-10-22T09:51:14.014Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/c7/023028ca45e674b67abee29a049fb3b7aac74873181940a1d34ad27e23cd/python_bidi-0.6.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dde1c3f3edb1f0095dcbf79cf8a0bb768f9539e809d0ad010d78200eea97d42a", size = 326788, upload-time = "2025-10-22T09:51:22.58Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/30/0753601fdad405e806c89cfa9603ff75241f8c7196cfe2cb37c43e34cdbd/python_bidi-0.6.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c463ae15e94b1c6a8a50bd671d6166b0b0d779fd1e56cbf46d8a4a84c9aa2d0", size = 302036, upload-time = "2025-10-22T09:51:40.341Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c6/38/e83901206c7161e4fa14f52d1244eb54bad2b9a959be62af7b472cded20a/python_bidi-0.6.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f9fa1257e075eeeed67d21f95e411036b7ca2b5c78f757d4ac66485c191720a", size = 315484, upload-time = "2025-10-22T09:51:32.285Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/89/cd73185ad92990261b050a30753a693ad22a72ad5dc61b4e3845c58eff75/python_bidi-0.6.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9adeec7cab0f2c2c291bd7faf9fa3fa233365fd0bf1c1c27a6ddd6cc563d4b32", size = 474003, upload-time = "2025-10-22T09:52:06.535Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/38/03fd74c68cae08d08a32a4bc2031300a882a7ceab39b7e7fc5a5e37f5b7c/python_bidi-0.6.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3b96744e4709f4445788a3645cea7ef8d7520ccd4fa8bbbfb3b650702e12c1e6", size = 567114, upload-time = "2025-10-22T09:52:17.534Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/44/e196002ba8317d48ebab4750092a61287574195a3f685232059aa776edf4/python_bidi-0.6.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8860d67dc04dc530b8b4f588f38b7341a76f2ec44a45685a2d54e9dcffa5d15a", size = 493810, upload-time = "2025-10-22T09:52:28.683Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e8/e2/1d495515d3fea0ecdd8bbb50e573282826ba074bceb2c0430206f94cde68/python_bidi-0.6.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a4319f478ab1b90bbbe9921606ecb7baa0ebf0b332e821d41c3abdf1a30f0c35", size = 465208, upload-time = "2025-10-22T09:52:39.411Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/c7/fc5b25d017677793435c415c7884f9c60ce7705bd35565280cca3be69fa9/python_bidi-0.6.7-cp310-cp310-win32.whl", hash = "sha256:8d4e621caadfdbc73d36eabdb2f392da850d28c58b020738411d09dda6208509", size = 157426, upload-time = "2025-10-22T09:52:58.114Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/85/be/bd323950b98d40ab45f97630c3bfb5ed3a7416b2f71c250bcc1ed1267eb0/python_bidi-0.6.7-cp310-cp310-win_amd64.whl", hash = "sha256:fd87d112eda1f0528074e1f7c0312881816cb75854133021124269a27c6c48dc", size = 161038, upload-time = "2025-10-22T09:52:50.44Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/de/c30a13ad95239507af472a5fc2cadd2e5e172055068f12ac39b37922c7f8/python_bidi-0.6.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a8892a7da0f617135fe9c92dc7070d13a0f96ab3081f9db7ff5b172a3905bd78", size = 274420, upload-time = "2025-10-22T09:51:58.262Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/9f/be5efef7eea5f1e2a6415c4052a988f594dcf5a11a15103f2718d324a35b/python_bidi-0.6.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:06650a164e63e94dc8a291cc9d415b4027cb1cce125bc9b02dac0f34d535ed47", size = 264586, upload-time = "2025-10-22T09:51:49.255Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/ec/2c374b6de35870817ffb3512c0666ea8c3794ef923b5586c69451e0e5395/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6df7be07af867ec1d121c92ea827efad4d77b25457c06eeab477b601e82b2340", size = 293672, upload-time = "2025-10-22T09:50:58.504Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/1a/722d7d7128bdc9a530351a0d2fdf2ff5f4af66a865a6bca925f99832e2cc/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73a88dc333efc42281bd800d5182c8625c6e11d109fc183fe3d7a11d48ab1150", size = 302643, upload-time = "2025-10-22T09:51:06.419Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/24/d7/5b9b593dd58fc745233d8476e9f4e0edd437547c78c58340619868470349/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f24189dc3aea3a0a94391a047076e1014306b39ba17d7a38ebab510553cd1a97", size = 441692, upload-time = "2025-10-22T09:51:15.39Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/08/b9/16e7a1db5f022da6654e89875d231ec2e044d42ef7b635feeff61cee564c/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a507fe6928a27a308e04ebf2065719b7850d1bf9ff1924f4e601ef77758812bd", size = 326933, upload-time = "2025-10-22T09:51:23.631Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/a6/45aaec301292c6a07a9cc3168f5d1a92c8adc2ef36a3cd1f227b9caa980c/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbbffb948a32f9783d1a28bc0c53616f0a76736ed1e7c1d62e3e99a8dfaab869", size = 302034, upload-time = "2025-10-22T09:51:41.347Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/a3/7e42cce6e153c21b4e5cc96d429a5910909823f6fedd174b64ff67bc76a7/python_bidi-0.6.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7e507e1e798ebca77ddc9774fd405107833315ad802cfdaa1ab07b6d9154fc8", size = 315738, upload-time = "2025-10-22T09:51:33.409Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/7c/a5e4c0acc8e6ca61953b4add0576f0483f63b809b5389154e5da13927b0b/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:849a57d39feaf897955d0b19bbf4796bea53d1bcdf83b82e0a7b059167eb2049", size = 473968, upload-time = "2025-10-22T09:52:07.624Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/aa/a18bc3cbab7a0e598cbe7b89f2c0913aedcc66dcafce9a4c357465c87859/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5ebc19f24e65a1f5c472e26d88e78b9d316e293bc6f205f32de4c4e99276336e", size = 567038, upload-time = "2025-10-22T09:52:18.594Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/46/fc6c54a8b5bfbee50e650f885ddef4f8c4f92880467ea0bc2bf133747048/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:24388c77cb00b8aa0f9c84beb7e3e523a3dac4f786ece64a1d8175a07b24da72", size = 493970, upload-time = "2025-10-22T09:52:29.815Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/f1/2c15f5b938b2e087e4e950cc14dcead5bedbaabfc6c576dac15739bc0c91/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:19737d217088ef27014f98eac1827c5913e6fb1dea96332ed84ede61791070d9", size = 465161, upload-time = "2025-10-22T09:52:40.517Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/d7/73a70a1fb819152485521b8dfe627e14ba9d3d5a65213244ab099adf3600/python_bidi-0.6.7-cp311-cp311-win32.whl", hash = "sha256:95c9de7ebc55ffb777548f2ecaf4b96b0fa0c92f42bf4d897b9f4cd164ec7394", size = 157033, upload-time = "2025-10-22T09:52:59.228Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/84/06999dc54ea047fe33209af7150df4202ab7ad52deeb66b2c2040ac07884/python_bidi-0.6.7-cp311-cp311-win_amd64.whl", hash = "sha256:898db0ea3e4aaa95b7fecba02a7560dfbf368f9d85053f2875f6d610c4d4ec2c", size = 161282, upload-time = "2025-10-22T09:52:51.467Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/03/5b2f3e73501d0f41ebc2b075b49473047c6cdfc3465cf890263fc69e3915/python_bidi-0.6.7-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:11c51579e01f768446a7e13a0059fea1530936a707abcbeaad9467a55cb16073", size = 272536, upload-time = "2025-10-22T09:51:59.721Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/77/c6048e938a73e5a7c6fa3d5e3627a5961109daa728c2e7d050567cecdc26/python_bidi-0.6.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47deaada8949af3a790f2cd73b613f9bfa153b4c9450f91c44a60c3109a81f73", size = 263258, upload-time = "2025-10-22T09:51:50.328Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/56/ed4dc501cab7de70ce35cd435c86278e4eb1caf238c80bc72297767c9219/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b38ddfab41d10e780edb431edc30aec89bee4ce43d718e3896e99f33dae5c1d3", size = 292700, upload-time = "2025-10-22T09:50:59.628Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/6a/1bf06d7544c940ffddd97cd0e02c55348a92163c5495fa18e34217dfbebe/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a93b0394cc684d64356b0475858c116f1e335ffbaba388db93bf47307deadfa", size = 300881, upload-time = "2025-10-22T09:51:07.507Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/1d/ce7577a8f50291c06e94f651ac5de0d1678fc2642af26a5dad9901a0244f/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec1694134961b71ac05241ac989b49ccf08e232b5834d5fc46f8a7c3bb1c13a9", size = 439125, upload-time = "2025-10-22T09:51:16.559Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/87/4cf6dcd58e22f0fd904e7a161c6b73a5f9d17d4d49073fcb089ba62f1469/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8047c33b85f7790474a1f488bef95689f049976a4e1c6f213a8d075d180a93e4", size = 325816, upload-time = "2025-10-22T09:51:25.12Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/0a/4028a088e29ce8f1673e85ec9f64204fc368355c3207e6a71619c2b4579a/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d9de35eb5987da27dd81e371c52142dd8e924bd61c1006003071ea05a735587", size = 300550, upload-time = "2025-10-22T09:51:42.739Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/05/cac15eba462d5a2407ac4ef1c792c45a948652b00c6bd81eaab3834a62d2/python_bidi-0.6.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a99d898ad1a399d9c8cab5561b3667fd24f4385820ac90c3340aa637aa5adfc9", size = 313017, upload-time = "2025-10-22T09:51:34.905Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/b1/3ba91b9ea60fa54a9aa730a5fe432bd73095d55be371244584fc6818eae1/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5debaab33562fdfc79ffdbd8d9c51cf07b8529de0e889d8cd145d78137aab21e", size = 472798, upload-time = "2025-10-22T09:52:09.079Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/40/4bf5fb7255e35c218174f322a4d4c80b63b2604d73adc6e32f843e700824/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c11c62a3cdb9d1426b1536de9e3446cb09c7d025bd4df125275cae221f214899", size = 565234, upload-time = "2025-10-22T09:52:19.703Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/81/ad23fb85bff69d0a25729cd3834254b87c3c7caa93d657c8f8edcbed08f6/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6c051f2d28ca542092d01da8b5fe110fb6191ff58d298a54a93dc183bece63bf", size = 491844, upload-time = "2025-10-22T09:52:31.216Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/85/103baaf142b2838f583b71904a2454fa31bd2a912ff505c25874f45d6c3e/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95867a07c5dee0ea2340fe1d0e4f6d9f5c5687d473193b6ee6f86fa44aac45d1", size = 463753, upload-time = "2025-10-22T09:52:41.943Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/c3/6a5c3b9f42a6b188430c83a7e70a76bc7c0db3354302fce7c8ed94a0c062/python_bidi-0.6.7-cp312-cp312-win32.whl", hash = "sha256:4c73cd980d45bb967799c7f0fc98ea93ae3d65b21ef2ba6abef6a057720bf483", size = 155820, upload-time = "2025-10-22T09:53:00.254Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/c4/683216398ee3abf6b9bb0f26ae15c696fabbe36468ba26d5271f0c11b343/python_bidi-0.6.7-cp312-cp312-win_amd64.whl", hash = "sha256:d524a4ba765bae9b950706472a77a887a525ed21144fe4b41f6190f6e57caa2c", size = 159966, upload-time = "2025-10-22T09:52:52.547Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/25/a5/8ad0a448d42fd5d01dd127c1dc5ab974a8ea6e20305ac89a3356dacd3bdf/python_bidi-0.6.7-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c061207212cd1db27bf6140b96dcd0536246f1e13e99bb5d03f4632f8e2ad7f", size = 272129, upload-time = "2025-10-22T09:52:00.761Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/c0/a13981fc0427a0d35e96fc4e31fbb0f981b28d0ce08416f98f42d51ea3bc/python_bidi-0.6.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2eb8fca918c7381531035c3aae31c29a1c1300ab8a63cad1ec3a71331096c78", size = 263174, upload-time = "2025-10-22T09:51:51.401Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/32/74034239d0bca32c315cac5c3ec07ef8eb44fa0e8cea1585cad85f5b8651/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:414004fe9cba33d288ff4a04e1c9afe6a737f440595d01b5bbed00d750296bbd", size = 292496, upload-time = "2025-10-22T09:51:00.708Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/83/fa/d6c853ed2668b1c12d66e71d4f843d0710d1ccaecc17ce09b35d2b1382a7/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5013ba963e9da606c4c03958cc737ebd5f8b9b8404bd71ab0d580048c746f875", size = 300727, upload-time = "2025-10-22T09:51:09.152Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/8d/55685bddfc1fbfa6e28e1c0be7df4023e504de7d2ac1355a3fa610836bc1/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad5f0847da00687f52d2b81828e8d887bdea9eb8686a9841024ea7a0e153028e", size = 438823, upload-time = "2025-10-22T09:51:17.844Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/54/db9e70443f89e3ec6fa70dcd16809c3656d1efe7946076dcd59832f722df/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26a8fe0d532b966708fc5f8aea0602107fde4745a8a5ae961edd3cf02e807d07", size = 325721, upload-time = "2025-10-22T09:51:26.132Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/c5/98ac9c00f17240f9114c756791f0cd9ba59a5d4b5d84fd1a6d0d50604e82/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6323e943c7672b271ad9575a2232508f17e87e81a78d7d10d6e93040e210eddf", size = 300493, upload-time = "2025-10-22T09:51:43.783Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/cb/382538dd7c656eb50408802b9a9466dbd3432bea059410e65a6c14bc79f9/python_bidi-0.6.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:349b89c3110bd25aa56d79418239ca4785d4bcc7a596e63bb996a9696fc6a907", size = 312889, upload-time = "2025-10-22T09:51:36.011Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/8d/dbc784cecd9b2950ba99c8fef0387ae588837e4e2bfd543be191d18bf9f6/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e7cad66317f12f0fd755fe41ee7c6b06531d2189a9048a8f37addb5109f7e3e3", size = 472798, upload-time = "2025-10-22T09:52:10.446Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/83/e6/398d59075265717d2950622ede1d366aff88ffcaa67a30b85709dea72206/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49639743f1230648fd4fb47547f8a48ada9c5ca1426b17ac08e3be607c65394c", size = 564974, upload-time = "2025-10-22T09:52:22.416Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/8e/2b939be0651bc2b69c234dc700723a26b93611d5bdd06b253d67d9da3557/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4636d572b357ab9f313c5340915c1cf51e3e54dd069351e02b6b76577fd1a854", size = 491711, upload-time = "2025-10-22T09:52:32.322Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/05/f53739ab2ce2eee0c855479a31b64933f6ff6164f3ddc611d04e4b79d922/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d7310312a68fdb1a8249cf114acb5435aa6b6a958b15810f053c1df5f98476e4", size = 463536, upload-time = "2025-10-22T09:52:43.142Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/c6/800899e2764f723c2ea9172eabcc1a31ffb8b4bb71ea5869158fd83bd437/python_bidi-0.6.7-cp313-cp313-win32.whl", hash = "sha256:ec985386bc3cd54155f2ef0434fccbfd743617ed6fc1a84dae2ab1de6062e0c6", size = 155786, upload-time = "2025-10-22T09:53:01.357Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/ba/a811c12c1a4b8fa7c0c0963d92c042284c2049b1586615af6b1774b786d9/python_bidi-0.6.7-cp313-cp313-win_amd64.whl", hash = "sha256:f57726b5a90d818625e6996f5116971b7a4ceb888832337d0e2cf43d1c362a90", size = 159863, upload-time = "2025-10-22T09:52:53.537Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/a5/cda302126e878be162bf183eb0bd6dc47ca3e680fb52111e49c62a8ea1eb/python_bidi-0.6.7-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b0bee27fb596a0f518369c275a965d0448c39a0730e53a030b311bb10562d4d5", size = 271899, upload-time = "2025-10-22T09:52:01.758Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/4b/9c15ca0fe795a5c55a39daa391524ac74e26d9187493632d455257771023/python_bidi-0.6.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c19ab378fefb1f09623f583fcfa12ed42369a998ddfbd39c40908397243c56b", size = 262235, upload-time = "2025-10-22T09:51:52.379Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/5e/25b25be64bff05272aa28d8bef2fbbad8415db3159a41703eb2e63dc9824/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:630cee960ba9e3016f95a8e6f725a621ddeff6fd287839f5693ccfab3f3a9b5c", size = 471983, upload-time = "2025-10-22T09:52:12.182Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/78/a9363f5da1b10d9211514b96ea47ecc95c797ed5ac566684bfece0666082/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0dbb4bbae212cca5bcf6e522fe8f572aff7d62544557734c2f810ded844d9eea", size = 565016, upload-time = "2025-10-22T09:52:23.515Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0d/ed/37dcb7d3dc250ecdff8120b026c37fcdbeada4111e4d7148c053180bcf54/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1dd0a5ec0d8710905cebb4c9e5018aa8464395a33cb32a3a6c2a951bf1984fe5", size = 491180, upload-time = "2025-10-22T09:52:33.505Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/a3/50d1f6060a7a500768768f5f8735cb68deba36391248dbf13d5d2c9c0885/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4ea928c31c7364098f853f122868f6f2155d6840661f7ea8b2ccfdf6084eb9f4", size = 463126, upload-time = "2025-10-22T09:52:44.28Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/47/712cd7d1068795c57fdf6c4acca00716688aa8b4e353b30de2ed8f599fd6/python_bidi-0.6.7-cp314-cp314-win32.whl", hash = "sha256:f7c055a50d068b3a924bd33a327646346839f55bcb762a26ec3fde8ea5d40564", size = 155793, upload-time = "2025-10-22T09:53:02.7Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/e8/1f86bf699b20220578351f9b7b635ed8b6e84dd51ad3cca08b89513ae971/python_bidi-0.6.7-cp314-cp314-win_amd64.whl", hash = "sha256:8a17631e3e691eec4ae6a370f7b035cf0a5767f4457bd615d11728c23df72e43", size = 159821, upload-time = "2025-10-22T09:52:54.95Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/4e/6135798d84b62eea70c0f9435301c2a4ba854e87be93a3fcd1d935266d24/python_bidi-0.6.7-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c9a679b24f5c6f366a0dec75745e1abeae2f597f033d0d54c74cbe62e7e6ae28", size = 276275, upload-time = "2025-10-22T09:52:05.078Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/83/2123596d43e552af9e2806e361646fa579f34a1d1e9e2c1707a0ab6a02dd/python_bidi-0.6.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:05fe5971110013610f0db40505d0b204edc756e92eafac1372a464f8b9162b11", size = 266951, upload-time = "2025-10-22T09:51:56.216Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/8c/8d1e1501717227a6d52fc7b9c47a3de61486b024fbdd4821bfad724c0699/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17572944e6d8fb616d111fc702c759da2bf7cedab85a3e4fa2af0c9eb95ed438", size = 295745, upload-time = "2025-10-22T09:51:04.438Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/ff/ef04e7f9067c2c5d862b9f8d9a192486c500c8aa295f0fb756c25ab47fc8/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3b63d19f3f56ff7f99bce5ca9ef8c811dbf0f509d8e84c1bc06105ed26a49528", size = 304123, upload-time = "2025-10-22T09:51:12.559Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/72/b973895e257a7d4cc8365ab094612f6ee885df863a4964d8865b9f534b67/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1350033431d75be749273236dcfc808e54404cd6ece6204cdb1bc4ccc163455", size = 442484, upload-time = "2025-10-22T09:51:21.575Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/1a/68ca9d10bc309828e8cdb2d57a30dd7e5753ac8520c8d7a0322daeb9eef7/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c5fb99f774748de283fadf915106f130b74be1bade934b7f73a7a8488b95da1", size = 329149, upload-time = "2025-10-22T09:51:31.232Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/40/ab450c06167a7de596d99b1ba5cee2c605b3ff184baccf08210ede706b1b/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d28e2bdcadf5b6161bb4ee9313ce41eac746ba57e744168bf723a415a11af05", size = 303529, upload-time = "2025-10-22T09:51:46.997Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/c5/585b5c413e3b77a32500fb877ea30aa23c45a6064dbd7fe77d87b72cd90b/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3777ae3e088e94df854fbcbd8d59f9239b74aac036cb6bbd19f8035c8e42478", size = 317753, upload-time = "2025-10-22T09:51:39.272Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/05/b7b4b447890d614ccb40633f4d65f334bcf9fe3ad13be33aaa54dcbc34f3/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:77bb4cbadf4121db395189065c58c9dd5d1950257cc1983004e6df4a3e2f97ad", size = 476054, upload-time = "2025-10-22T09:52:15.856Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/94/64f6d2c09c4426918345b54ca8902f94b663eadd744c9dd89070f546c9bc/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:f1fe71c203f66bc169a393964d5702f9251cfd4d70279cb6453fdd42bd2e675f", size = 568365, upload-time = "2025-10-22T09:52:27.556Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fc/d2/c39a6b82aa0fcedac7cbe6078b78bb9089b43d903f8e00859e42b504bb8e/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:d87ed09e5c9b6d2648e8856a4e556147b9d3cd4d63905fa664dd6706bc414256", size = 495292, upload-time = "2025-10-22T09:52:38.306Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0a/8d/a80f37ab92118e305d7b574306553599f81534c50b4eb23ef34ebe09c09c/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:766d5f5a686eb99b53168a7bdfb338035931a609bdbbcb537cef9e050a86f359", size = 467159, upload-time = "2025-10-22T09:52:48.603Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.9.0.post0"
|
version = "2.9.0.post0"
|
||||||
@@ -5491,120 +5287,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/20/a7/84c96b61fd13205f2cafbe263cdb2745965974bdf3e0078f121dfeca5f02/schedule-1.2.2-py3-none-any.whl", hash = "sha256:5bef4a2a0183abf44046ae0d164cadcac21b1db011bdd8102e4a0c1e91e06a7d", size = 12220, upload-time = "2024-05-25T18:41:59.121Z" },
|
{ url = "https://files.pythonhosted.org/packages/20/a7/84c96b61fd13205f2cafbe263cdb2745965974bdf3e0078f121dfeca5f02/schedule-1.2.2-py3-none-any.whl", hash = "sha256:5bef4a2a0183abf44046ae0d164cadcac21b1db011bdd8102e4a0c1e91e06a7d", size = 12220, upload-time = "2024-05-25T18:41:59.121Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scikit-image"
|
|
||||||
version = "0.25.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
resolution-markers = [
|
|
||||||
"python_full_version < '3.11'",
|
|
||||||
]
|
|
||||||
dependencies = [
|
|
||||||
{ name = "imageio", marker = "python_full_version < '3.11'" },
|
|
||||||
{ name = "lazy-loader", marker = "python_full_version < '3.11'" },
|
|
||||||
{ name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
||||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
||||||
{ name = "packaging", marker = "python_full_version < '3.11'" },
|
|
||||||
{ name = "pillow", marker = "python_full_version < '3.11'" },
|
|
||||||
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
||||||
{ name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/cb/016c63f16065c2d333c8ed0337e18a5cdf9bc32d402e4f26b0db362eb0e2/scikit_image-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3278f586793176599df6a4cf48cb6beadae35c31e58dc01a98023af3dc31c78", size = 13988922, upload-time = "2025-02-18T18:04:11.069Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/ca/ff4731289cbed63c94a0c9a5b672976603118de78ed21910d9060c82e859/scikit_image-0.25.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5c311069899ce757d7dbf1d03e32acb38bb06153236ae77fcd820fd62044c063", size = 13192698, upload-time = "2025-02-18T18:04:15.362Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/6d/a2aadb1be6d8e149199bb9b540ccde9e9622826e1ab42fe01de4c35ab918/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be455aa7039a6afa54e84f9e38293733a2622b8c2fb3362b822d459cc5605e99", size = 14153634, upload-time = "2025-02-18T18:04:18.496Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/08/916e7d9ee4721031b2f625db54b11d8379bd51707afaa3e5a29aecf10bc4/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c464b90e978d137330be433df4e76d92ad3c5f46a22f159520ce0fdbea8a09", size = 14767545, upload-time = "2025-02-18T18:04:22.556Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/ee/c53a009e3997dda9d285402f19226fbd17b5b3cb215da391c4ed084a1424/scikit_image-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:60516257c5a2d2f74387c502aa2f15a0ef3498fbeaa749f730ab18f0a40fd054", size = 12812908, upload-time = "2025-02-18T18:04:26.364Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/97/3051c68b782ee3f1fb7f8f5bb7d535cf8cb92e8aae18fa9c1cdf7e15150d/scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17", size = 14003057, upload-time = "2025-02-18T18:04:30.395Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/23/257fc696c562639826065514d551b7b9b969520bd902c3a8e2fcff5b9e17/scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0", size = 13180335, upload-time = "2025-02-18T18:04:33.449Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ef/14/0c4a02cb27ca8b1e836886b9ec7c9149de03053650e9e2ed0625f248dd92/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173", size = 14144783, upload-time = "2025-02-18T18:04:36.594Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/9b/9fb556463a34d9842491d72a421942c8baff4281025859c84fcdb5e7e602/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641", size = 14785376, upload-time = "2025-02-18T18:04:39.856Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/ec/b57c500ee85885df5f2188f8bb70398481393a69de44a00d6f1d055f103c/scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b", size = 12791698, upload-time = "2025-02-18T18:04:42.868Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/35/8c/5df82881284459f6eec796a5ac2a0a304bb3384eec2e73f35cfdfcfbf20c/scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb", size = 13986000, upload-time = "2025-02-18T18:04:47.156Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/e6/93bebe1abcdce9513ffec01d8af02528b4c41fb3c1e46336d70b9ed4ef0d/scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed", size = 13235893, upload-time = "2025-02-18T18:04:51.049Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/53/4b/eda616e33f67129e5979a9eb33c710013caa3aa8a921991e6cc0b22cea33/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d", size = 14178389, upload-time = "2025-02-18T18:04:54.245Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/b5/b75527c0f9532dd8a93e8e7cd8e62e547b9f207d4c11e24f0006e8646b36/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824", size = 15003435, upload-time = "2025-02-18T18:04:57.586Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/34/e3/49beb08ebccda3c21e871b607c1cb2f258c3fa0d2f609fed0a5ba741b92d/scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2", size = 12899474, upload-time = "2025-02-18T18:05:01.166Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841, upload-time = "2025-02-18T18:05:03.963Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862, upload-time = "2025-02-18T18:05:06.986Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785, upload-time = "2025-02-18T18:05:10.69Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119, upload-time = "2025-02-18T18:05:13.871Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116, upload-time = "2025-02-18T18:05:17.844Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801, upload-time = "2025-02-18T18:05:20.783Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scikit-image"
|
|
||||||
version = "0.26.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
resolution-markers = [
|
|
||||||
"python_full_version >= '3.14'",
|
|
||||||
"python_full_version == '3.13.*'",
|
|
||||||
"python_full_version == '3.12.*'",
|
|
||||||
"python_full_version == '3.11.*'",
|
|
||||||
]
|
|
||||||
dependencies = [
|
|
||||||
{ name = "imageio", marker = "python_full_version >= '3.11'" },
|
|
||||||
{ name = "lazy-loader", marker = "python_full_version >= '3.11'" },
|
|
||||||
{ name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
||||||
{ name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
||||||
{ name = "packaging", marker = "python_full_version >= '3.11'" },
|
|
||||||
{ name = "pillow", marker = "python_full_version >= '3.11'" },
|
|
||||||
{ name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
||||||
{ name = "tifffile", version = "2026.2.24", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/b4/2528bb43c67d48053a7a649a9666432dc307d66ba02e3a6d5c40f46655df/scikit_image-0.26.0.tar.gz", hash = "sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa", size = 22729739, upload-time = "2025-12-20T17:12:21.824Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/16/8a407688b607f86f81f8c649bf0d68a2a6d67375f18c2d660aba20f5b648/scikit_image-0.26.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b1ede33a0fb3731457eaf53af6361e73dd510f449dac437ab54573b26788baf0", size = 12355510, upload-time = "2025-12-20T17:10:31.628Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/f9/7efc088ececb6f6868fd4475e16cfafc11f242ce9ab5fc3557d78b5da0d4/scikit_image-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7af7aa331c6846bd03fa28b164c18d0c3fd419dbb888fb05e958ac4257a78fdd", size = 12056334, upload-time = "2025-12-20T17:10:34.559Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/1e/bc7fb91fb5ff65ef42346c8b7ee8b09b04eabf89235ab7dbfdfd96cbd1ea/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ea6207d9e9d21c3f464efe733121c0504e494dbdc7728649ff3e23c3c5a4953", size = 13297768, upload-time = "2025-12-20T17:10:37.733Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/2a/e71c1a7d90e70da67b88ccc609bd6ae54798d5847369b15d3a8052232f9d/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74aa5518ccea28121f57a95374581d3b979839adc25bb03f289b1bc9b99c58af", size = 13711217, upload-time = "2025-12-20T17:10:40.935Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/59/9637ee12c23726266b91296791465218973ce1ad3e4c56fc81e4d8e7d6e1/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d5c244656de905e195a904e36dbc18585e06ecf67d90f0482cbde63d7f9ad59d", size = 14337782, upload-time = "2025-12-20T17:10:43.452Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e7/5c/a3e1e0860f9294663f540c117e4bf83d55e5b47c281d475cc06227e88411/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21a818ee6ca2f2131b9e04d8eb7637b5c18773ebe7b399ad23dcc5afaa226d2d", size = 14805997, upload-time = "2025-12-20T17:10:45.93Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/c6/2eeacf173da041a9e388975f54e5c49df750757fcfc3ee293cdbbae1ea0a/scikit_image-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:9490360c8d3f9a7e85c8de87daf7c0c66507960cf4947bb9610d1751928721c7", size = 11878486, upload-time = "2025-12-20T17:10:48.246Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/a4/a852c4949b9058d585e762a66bf7e9a2cd3be4795cd940413dfbfbb0ce79/scikit_image-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:0baa0108d2d027f34d748e84e592b78acc23e965a5de0e4bb03cf371de5c0581", size = 11346518, upload-time = "2025-12-20T17:10:50.575Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/e8/e13757982264b33a1621628f86b587e9a73a13f5256dad49b19ba7dc9083/scikit_image-0.26.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d454b93a6fa770ac5ae2d33570f8e7a321bb80d29511ce4b6b78058ebe176e8c", size = 12376452, upload-time = "2025-12-20T17:10:52.796Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/be/f8dd17d0510f9911f9f17ba301f7455328bf13dae416560126d428de9568/scikit_image-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3409e89d66eff5734cd2b672d1c48d2759360057e714e1d92a11df82c87cba37", size = 12061567, upload-time = "2025-12-20T17:10:55.207Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b3/2b/c70120a6880579fb42b91567ad79feb4772f7be72e8d52fec403a3dde0c6/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c717490cec9e276afb0438dd165b7c3072d6c416709cc0f9f5a4c1070d23a44", size = 13084214, upload-time = "2025-12-20T17:10:57.468Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/a2/70401a107d6d7466d64b466927e6b96fcefa99d57494b972608e2f8be50f/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df650e79031634ac90b11e64a9eedaf5a5e06fcd09bcd03a34be01745744466", size = 13561683, upload-time = "2025-12-20T17:10:59.49Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/a5/48bdfd92794c5002d664e0910a349d0a1504671ef5ad358150f21643c79a/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cefd85033e66d4ea35b525bb0937d7f42d4cdcfed2d1888e1570d5ce450d3932", size = 14112147, upload-time = "2025-12-20T17:11:02.083Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/b5/ac71694da92f5def5953ca99f18a10fe98eac2dd0a34079389b70b4d0394/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3f5bf622d7c0435884e1e141ebbe4b2804e16b2dd23ae4c6183e2ea99233be70", size = 14661625, upload-time = "2025-12-20T17:11:04.528Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/4d/a3cc1e96f080e253dad2251bfae7587cf2b7912bcd76fd43fd366ff35a87/scikit_image-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:abed017474593cd3056ae0fe948d07d0747b27a085e92df5474f4955dd65aec0", size = 11911059, upload-time = "2025-12-20T17:11:06.61Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/35/8a/d1b8055f584acc937478abf4550d122936f420352422a1a625eef2c605d8/scikit_image-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d57e39ef67a95d26860c8caf9b14b8fb130f83b34c6656a77f191fa6d1d04d8", size = 11348740, upload-time = "2025-12-20T17:11:09.118Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/48/02357ffb2cca35640f33f2cfe054a4d6d5d7a229b88880a64f1e45c11f4e/scikit_image-0.26.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a2e852eccf41d2d322b8e60144e124802873a92b8d43a6f96331aa42888491c7", size = 12346329, upload-time = "2025-12-20T17:11:11.599Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/b9/b792c577cea2c1e94cda83b135a656924fc57c428e8a6d302cd69aac1b60/scikit_image-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:98329aab3bc87db352b9887f64ce8cdb8e75f7c2daa19927f2e121b797b678d5", size = 12031726, upload-time = "2025-12-20T17:11:13.871Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/a9/9564250dfd65cb20404a611016db52afc6268b2b371cd19c7538ea47580f/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:915bb3ba66455cf8adac00dc8fdf18a4cd29656aec7ddd38cb4dda90289a6f21", size = 13094910, upload-time = "2025-12-20T17:11:16.2Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/b8/0d8eeb5a9fd7d34ba84f8a55753a0a3e2b5b51b2a5a0ade648a8db4a62f7/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b36ab5e778bf50af5ff386c3ac508027dc3aaeccf2161bdf96bde6848f44d21b", size = 13660939, upload-time = "2025-12-20T17:11:18.464Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2f/d6/91d8973584d4793d4c1a847d388e34ef1218d835eeddecfc9108d735b467/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:09bad6a5d5949c7896c8347424c4cca899f1d11668030e5548813ab9c2865dcb", size = 14138938, upload-time = "2025-12-20T17:11:20.919Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/9a/7e15d8dc10d6bbf212195fb39bdeb7f226c46dd53f9c63c312e111e2e175/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aeb14db1ed09ad4bee4ceb9e635547a8d5f3549be67fc6c768c7f923e027e6cd", size = 14752243, upload-time = "2025-12-20T17:11:23.347Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac529eb9dbd5954f9aaa2e3fe9a3fd9661bfe24e134c688587d811a0233127f1", size = 11906770, upload-time = "2025-12-20T17:11:25.297Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/ec/96941474a18a04b69b6f6562a5bd79bd68049fa3728d3b350976eccb8b93/scikit_image-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a2d211bc355f59725efdcae699b93b30348a19416cc9e017f7b2fb599faf7219", size = 11342506, upload-time = "2025-12-20T17:11:27.399Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/e5/c1a9962b0cf1952f42d32b4a2e48eed520320dbc4d2ff0b981c6fa508b6b/scikit_image-0.26.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9eefb4adad066da408a7601c4c24b07af3b472d90e08c3e7483d4e9e829d8c49", size = 12663278, upload-time = "2025-12-20T17:11:29.358Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/97/c1a276a59ce8e4e24482d65c1a3940d69c6b3873279193b7ebd04e5ee56b/scikit_image-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6caec76e16c970c528d15d1c757363334d5cb3069f9cea93d2bead31820511f3", size = 12405142, upload-time = "2025-12-20T17:11:31.282Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/4a/f1cbd1357caef6c7993f7efd514d6e53d8fd6f7fe01c4714d51614c53289/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a07200fe09b9d99fcdab959859fe0f7db8df6333d6204344425d476850ce3604", size = 12942086, upload-time = "2025-12-20T17:11:33.683Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5b/6f/74d9fb87c5655bd64cf00b0c44dc3d6206d9002e5f6ba1c9aeb13236f6bf/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92242351bccf391fc5df2d1529d15470019496d2498d615beb68da85fe7fdf37", size = 13265667, upload-time = "2025-12-20T17:11:36.11Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/73/faddc2413ae98d863f6fa2e3e14da4467dd38e788e1c23346cf1a2b06b97/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:52c496f75a7e45844d951557f13c08c81487c6a1da2e3c9c8a39fcde958e02cc", size = 14001966, upload-time = "2025-12-20T17:11:38.55Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/94/9f46966fa042b5d57c8cd641045372b4e0df0047dd400e77ea9952674110/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20ef4a155e2e78b8ab973998e04d8a361d49d719e65412405f4dadd9155a61d9", size = 14359526, upload-time = "2025-12-20T17:11:41.087Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/b4/2840fe38f10057f40b1c9f8fb98a187a370936bf144a4ac23452c5ef1baf/scikit_image-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c9087cf7d0e7f33ab5c46d2068d86d785e70b05400a891f73a13400f1e1faf6a", size = 12287629, upload-time = "2025-12-20T17:11:43.11Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/ba/73b6ca70796e71f83ab222690e35a79612f0117e5aaf167151b7d46f5f2c/scikit_image-0.26.0-cp313-cp313t-win_arm64.whl", hash = "sha256:27d58bc8b2acd351f972c6508c1b557cfed80299826080a4d803dd29c51b707e", size = 11647755, upload-time = "2025-12-20T17:11:45.279Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/44/6b744f92b37ae2833fd423cce8f806d2368859ec325a699dc30389e090b9/scikit_image-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:63af3d3a26125f796f01052052f86806da5b5e54c6abef152edb752683075a9c", size = 12365810, upload-time = "2025-12-20T17:11:47.357Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/f5/83590d9355191f86ac663420fec741b82cc547a4afe7c4c1d986bf46e4db/scikit_image-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ce00600cd70d4562ed59f80523e18cdcc1fae0e10676498a01f73c255774aefd", size = 12075717, upload-time = "2025-12-20T17:11:49.483Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/72/48/253e7cf5aee6190459fe136c614e2cbccc562deceb4af96e0863f1b8ee29/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6381edf972b32e4f54085449afde64365a57316637496c1325a736987083e2ab", size = 13161520, upload-time = "2025-12-20T17:11:51.58Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/c3/cec6a3cbaadfdcc02bd6ff02f3abfe09eaa7f4d4e0a525a1e3a3f4bce49c/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6624a76c6085218248154cc7e1500e6b488edcd9499004dd0d35040607d7505", size = 13684340, upload-time = "2025-12-20T17:11:53.708Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/0d/39a776f675d24164b3a267aa0db9f677a4cb20127660d8bf4fd7fef66817/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f775f0e420faac9c2aa6757135f4eb468fb7b70e0b67fa77a5e79be3c30ee331", size = 14203839, upload-time = "2025-12-20T17:11:55.89Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/25/2514df226bbcedfe9b2caafa1ba7bc87231a0c339066981b182b08340e06/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede4d6d255cc5da9faeb2f9ba7fedbc990abbc652db429f40a16b22e770bb578", size = 14770021, upload-time = "2025-12-20T17:11:58.014Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/5b/0671dc91c0c79340c3fe202f0549c7d3681eb7640fe34ab68a5f090a7c7f/scikit_image-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:0660b83968c15293fd9135e8d860053ee19500d52bf55ca4fb09de595a1af650", size = 12023490, upload-time = "2025-12-20T17:12:00.013Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/08/7c4cb59f91721f3de07719085212a0b3962e3e3f2d1818cbac4eeb1ea53e/scikit_image-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:b8d14d3181c21c11170477a42542c1addc7072a90b986675a71266ad17abc37f", size = 11473782, upload-time = "2025-12-20T17:12:01.983Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/41/65c4258137acef3d73cb561ac55512eacd7b30bb4f4a11474cad526bc5db/scikit_image-0.26.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cde0bbd57e6795eba83cb10f71a677f7239271121dc950bc060482834a668ad1", size = 12686060, upload-time = "2025-12-20T17:12:03.886Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e7/32/76971f8727b87f1420a962406388a50e26667c31756126444baf6668f559/scikit_image-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:163e9afb5b879562b9aeda0dd45208a35316f26cc7a3aed54fd601604e5cf46f", size = 12422628, upload-time = "2025-12-20T17:12:05.921Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/0d/996febd39f757c40ee7b01cdb861867327e5c8e5f595a634e8201462d958/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724f79fd9b6cb6f4a37864fe09f81f9f5d5b9646b6868109e1b100d1a7019e59", size = 12962369, upload-time = "2025-12-20T17:12:07.912Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/b4/612d354f946c9600e7dea012723c11d47e8d455384e530f6daaaeb9bf62c/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3268f13310e6857508bd87202620df996199a016a1d281b309441d227c822394", size = 13272431, upload-time = "2025-12-20T17:12:10.255Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0a/6e/26c00b466e06055a086de2c6e2145fe189ccdc9a1d11ccc7de020f2591ad/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fac96a1f9b06cd771cbbb3cd96c5332f36d4efd839b1d8b053f79e5887acde62", size = 14016362, upload-time = "2025-12-20T17:12:12.793Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/88/00a90402e1775634043c2a0af8a3c76ad450866d9fa444efcc43b553ba2d/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c1e7bd342f43e7a97e571b3f03ba4c1293ea1a35c3f13f41efdc8a81c1dc8f2", size = 14364151, upload-time = "2025-12-20T17:12:14.909Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/ca/918d8d306bd43beacff3b835c6d96fac0ae64c0857092f068b88db531a7c/scikit_image-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b702c3bb115e1dcf4abf5297429b5c90f2189655888cbed14921f3d26f81d3a4", size = 12413484, upload-time = "2025-12-20T17:12:17.046Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/cd/4da01329b5a8d47ff7ec3c99a2b02465a8017b186027590dc7425cee0b56/scikit_image-0.26.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0608aa4a9ec39e0843de10d60edb2785a30c1c47819b67866dd223ebd149acaf", size = 11769501, upload-time = "2025-12-20T17:12:19.339Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scikit-learn"
|
name = "scikit-learn"
|
||||||
version = "1.7.2"
|
version = "1.7.2"
|
||||||
@@ -5875,74 +5557,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" },
|
{ url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "shapely"
|
|
||||||
version = "2.1.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
||||||
{ name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/89/c3548aa9b9812a5d143986764dededfa48d817714e947398bdda87c77a72/shapely-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7ae48c236c0324b4e139bea88a306a04ca630f49be66741b340729d380d8f52f", size = 1825959, upload-time = "2025-09-24T13:50:00.682Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/8a/7ebc947080442edd614ceebe0ce2cdbd00c25e832c240e1d1de61d0e6b38/shapely-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eba6710407f1daa8e7602c347dfc94adc02205ec27ed956346190d66579eb9ea", size = 1629196, upload-time = "2025-09-24T13:50:03.447Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c8/86/c9c27881c20d00fc409e7e059de569d5ed0abfcec9c49548b124ebddea51/shapely-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef4a456cc8b7b3d50ccec29642aa4aeda959e9da2fe9540a92754770d5f0cf1f", size = 2951065, upload-time = "2025-09-24T13:50:05.266Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/8a/0ab1f7433a2a85d9e9aea5b1fbb333f3b09b309e7817309250b4b7b2cc7a/shapely-2.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e38a190442aacc67ff9f75ce60aec04893041f16f97d242209106d502486a142", size = 3058666, upload-time = "2025-09-24T13:50:06.872Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/c6/5a30ffac9c4f3ffd5b7113a7f5299ccec4713acd5ee44039778a7698224e/shapely-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:40d784101f5d06a1fd30b55fc11ea58a61be23f930d934d86f19a180909908a4", size = 3966905, upload-time = "2025-09-24T13:50:09.417Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/72/e92f3035ba43e53959007f928315a68fbcf2eeb4e5ededb6f0dc7ff1ecc3/shapely-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f6f6cd5819c50d9bcf921882784586aab34a4bd53e7553e175dece6db513a6f0", size = 4129260, upload-time = "2025-09-24T13:50:11.183Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/42/24/605901b73a3d9f65fa958e63c9211f4be23d584da8a1a7487382fac7fdc5/shapely-2.1.2-cp310-cp310-win32.whl", hash = "sha256:fe9627c39c59e553c90f5bc3128252cb85dc3b3be8189710666d2f8bc3a5503e", size = 1544301, upload-time = "2025-09-24T13:50:12.521Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e1/89/6db795b8dd3919851856bd2ddd13ce434a748072f6fdee42ff30cbd3afa3/shapely-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:1d0bfb4b8f661b3b4ec3565fa36c340bfb1cda82087199711f86a88647d26b2f", size = 1722074, upload-time = "2025-09-24T13:50:13.909Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842, upload-time = "2025-09-24T13:50:21.77Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316, upload-time = "2025-09-24T13:50:23.626Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586, upload-time = "2025-09-24T13:50:25.443Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961, upload-time = "2025-09-24T13:50:26.968Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856, upload-time = "2025-09-24T13:50:28.497Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shellingham"
|
name = "shellingham"
|
||||||
version = "1.5.4"
|
version = "1.5.4"
|
||||||
@@ -6080,7 +5694,6 @@ video = [
|
|||||||
{ name = "yt-dlp" },
|
{ name = "yt-dlp" },
|
||||||
]
|
]
|
||||||
video-full = [
|
video-full = [
|
||||||
{ name = "easyocr" },
|
|
||||||
{ name = "faster-whisper" },
|
{ name = "faster-whisper" },
|
||||||
{ name = "opencv-python-headless" },
|
{ name = "opencv-python-headless" },
|
||||||
{ name = "pytesseract" },
|
{ name = "pytesseract" },
|
||||||
@@ -6124,7 +5737,6 @@ requires-dist = [
|
|||||||
{ name = "chromadb", marker = "extra == 'chroma'", specifier = ">=0.4.0" },
|
{ name = "chromadb", marker = "extra == 'chroma'", specifier = ">=0.4.0" },
|
||||||
{ name = "chromadb", marker = "extra == 'rag-upload'", specifier = ">=0.4.0" },
|
{ name = "chromadb", marker = "extra == 'rag-upload'", specifier = ">=0.4.0" },
|
||||||
{ name = "click", specifier = ">=8.3.0" },
|
{ name = "click", specifier = ">=8.3.0" },
|
||||||
{ name = "easyocr", marker = "extra == 'video-full'", specifier = ">=1.7.0" },
|
|
||||||
{ name = "fastapi", marker = "extra == 'all'", specifier = ">=0.109.0" },
|
{ name = "fastapi", marker = "extra == 'all'", specifier = ">=0.109.0" },
|
||||||
{ name = "fastapi", marker = "extra == 'embedding'", specifier = ">=0.109.0" },
|
{ name = "fastapi", marker = "extra == 'embedding'", specifier = ">=0.109.0" },
|
||||||
{ name = "faster-whisper", marker = "extra == 'video-full'", specifier = ">=1.0.0" },
|
{ name = "faster-whisper", marker = "extra == 'video-full'", specifier = ">=1.0.0" },
|
||||||
@@ -6368,39 +5980,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
|
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tifffile"
|
|
||||||
version = "2025.5.10"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
resolution-markers = [
|
|
||||||
"python_full_version < '3.11'",
|
|
||||||
]
|
|
||||||
dependencies = [
|
|
||||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/44/d0/18fed0fc0916578a4463f775b0fbd9c5fed2392152d039df2fb533bfdd5d/tifffile-2025.5.10.tar.gz", hash = "sha256:018335d34283aa3fd8c263bae5c3c2b661ebc45548fde31504016fcae7bf1103", size = 365290, upload-time = "2025-05-10T19:22:34.386Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/06/bd0a6097da704a7a7c34a94cfd771c3ea3c2f405dd214e790d22c93f6be1/tifffile-2025.5.10-py3-none-any.whl", hash = "sha256:e37147123c0542d67bc37ba5cdd67e12ea6fbe6e86c52bee037a9eb6a064e5ad", size = 226533, upload-time = "2025-05-10T19:22:27.279Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tifffile"
|
|
||||||
version = "2026.2.24"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
resolution-markers = [
|
|
||||||
"python_full_version >= '3.14'",
|
|
||||||
"python_full_version == '3.13.*'",
|
|
||||||
"python_full_version == '3.12.*'",
|
|
||||||
"python_full_version == '3.11.*'",
|
|
||||||
]
|
|
||||||
dependencies = [
|
|
||||||
{ name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6e/1c/19fc653e2b05ec0defae511b03b330ca60c95f2c47fcaaf21c52c6e84aa8/tifffile-2026.2.24.tar.gz", hash = "sha256:d73cfa6d7a8f5775a1e3c9f3bfca77c992946639fb41a5bbe888878cb6964dc6", size = 387373, upload-time = "2026-02-24T23:59:11.706Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/fe/80250dc06cd4a3a5afe7059875a8d53e97a78528c5dd9ea8c3f981fb897a/tifffile-2026.2.24-py3-none-any.whl", hash = "sha256:38ef6258c2bd8dd3551c7480c6d75a36c041616262e6cd55a50dd16046b71863", size = 243223, upload-time = "2026-02-24T23:59:10.131Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tiktoken"
|
name = "tiktoken"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
@@ -6616,47 +6195,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" },
|
{ url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "torchvision"
|
|
||||||
version = "0.25.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
||||||
{ name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
||||||
{ name = "pillow" },
|
|
||||||
{ name = "torch" },
|
|
||||||
]
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/ae/cbf727421eb73f1cf907fbe5788326a08f111b3f6b6ddca15426b53fec9a/torchvision-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a95c47abb817d4e90ea1a8e57bd0d728e3e6b533b3495ae77d84d883c4d11f56", size = 1874919, upload-time = "2026-01-21T16:27:47.617Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/68/dc7a224f606d53ea09f9a85196a3921ec3a801b0b1d17e84c73392f0c029/torchvision-0.25.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:acc339aba4a858192998c2b91f635827e40d9c469d9cf1455bafdda6e4c28ea4", size = 2343220, upload-time = "2026-01-21T16:27:44.26Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/fa/8cce5ca7ffd4da95193232493703d20aa06303f37b119fd23a65df4f239a/torchvision-0.25.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0d9a3f925a081dd2ebb0b791249b687c2ef2c2717d027946654607494b9b64b6", size = 8068106, upload-time = "2026-01-21T16:27:37.805Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/b9/a53bcf8f78f2cd89215e9ded70041765d50ef13bf301f9884ec6041a9421/torchvision-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:b57430fbe9e9b697418a395041bb615124d9c007710a2712fda6e35fb310f264", size = 3697295, upload-time = "2026-01-21T16:27:36.574Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/be/c704bceaf11c4f6b19d64337a34a877fcdfe3bd68160a8c9ae9bea4a35a3/torchvision-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db74a551946b75d19f9996c419a799ffdf6a223ecf17c656f90da011f1d75b20", size = 1874923, upload-time = "2026-01-21T16:27:46.574Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/e9/f143cd71232430de1f547ceab840f68c55e127d72558b1061a71d0b193cd/torchvision-0.25.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f49964f96644dbac2506dffe1a0a7ec0f2bf8cf7a588c3319fed26e6329ffdf3", size = 2344808, upload-time = "2026-01-21T16:27:43.191Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/ae/ad5d6165797de234c9658752acb4fce65b78a6a18d82efdf8367c940d8da/torchvision-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:153c0d2cbc34b7cf2da19d73450f24ba36d2b75ec9211b9962b5022fb9e4ecee", size = 8070752, upload-time = "2026-01-21T16:27:33.748Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/19/55b28aecdc7f38df57b8eb55eb0b14a62b470ed8efeb22cdc74224df1d6a/torchvision-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:ea580ffd6094cc01914ad32f8c8118174f18974629af905cea08cb6d5d48c7b7", size = 4038722, upload-time = "2026-01-21T16:27:41.355Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/3a/6ea0d73f49a9bef38a1b3a92e8dd455cea58470985d25635beab93841748/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2abe430c90b1d5e552680037d68da4eb80a5852ebb1c811b2b89d299b10573b", size = 1874920, upload-time = "2026-01-21T16:27:45.348Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/f8/c0e1ef27c66e15406fece94930e7d6feee4cb6374bbc02d945a630d6426e/torchvision-0.25.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b75deafa2dfea3e2c2a525559b04783515e3463f6e830cb71de0fb7ea36fe233", size = 2344556, upload-time = "2026-01-21T16:27:40.125Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/2f/f24b039169db474e8688f649377de082a965fbf85daf4e46c44412f1d15a/torchvision-0.25.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f25aa9e380865b11ea6e9d99d84df86b9cc959f1a007cd966fc6f1ab2ed0e248", size = 8072351, upload-time = "2026-01-21T16:27:21.074Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/16/8f650c2e288977cf0f8f85184b90ee56ed170a4919347fc74ee99286ed6f/torchvision-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9c55ae8d673ab493325d1267cbd285bb94d56f99626c00ac4644de32a59ede3", size = 4303059, upload-time = "2026-01-21T16:27:11.08Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/5b/1562a04a6a5a4cf8cf40016a0cdeda91ede75d6962cff7f809a85ae966a5/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:24e11199e4d84ba9c5ee7825ebdf1cd37ce8deec225117f10243cae984ced3ec", size = 1874918, upload-time = "2026-01-21T16:27:39.02Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/b1/3d6c42f62c272ce34fcce609bb8939bdf873dab5f1b798fd4e880255f129/torchvision-0.25.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f271136d2d2c0b7a24c5671795c6e4fd8da4e0ea98aeb1041f62bc04c4370ef", size = 2309106, upload-time = "2026-01-21T16:27:30.624Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/60/59bb9c8b67cce356daeed4cb96a717caa4f69c9822f72e223a0eae7a9bd9/torchvision-0.25.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:855c0dc6d37f462482da7531c6788518baedca1e0847f3df42a911713acdfe52", size = 8071522, upload-time = "2026-01-21T16:27:29.392Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/32/a5/9a9b1de0720f884ea50dbf9acb22cbe5312e51d7b8c4ac6ba9b51efd9bba/torchvision-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:cef0196be31be421f6f462d1e9da1101be7332d91984caa6f8022e6c78a5877f", size = 4321911, upload-time = "2026-01-21T16:27:35.195Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/99/dca81ed21ebaeff2b67cc9f815a20fdaa418b69f5f9ea4c6ed71721470db/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a8f8061284395ce31bcd460f2169013382ccf411148ceb2ee38e718e9860f5a7", size = 1896209, upload-time = "2026-01-21T16:27:32.159Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/cc/2103149761fdb4eaed58a53e8437b2d716d48f05174fab1d9fcf1e2a2244/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:146d02c9876858420adf41f3189fe90e3d6a409cbfa65454c09f25fb33bf7266", size = 2310735, upload-time = "2026-01-21T16:27:22.327Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/ad/f4c985ad52ddd3b22711c588501be1b330adaeaf6850317f66751711b78c/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c4d395cb2c4a2712f6eb93a34476cdf7aae74bb6ea2ea1917f858e96344b00aa", size = 8089557, upload-time = "2026-01-21T16:27:27.666Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/cc/0ea68b5802e5e3c31f44b307e74947bad5a38cc655231d845534ed50ddb8/torchvision-0.25.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5e6b449e9fa7d642142c0e27c41e5a43b508d57ed8e79b7c0a0c28652da8678c", size = 4344260, upload-time = "2026-01-21T16:27:17.018Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/1f/fa839532660e2602b7e704d65010787c5bb296258b44fa8b9c1cd6175e7d/torchvision-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:620a236288d594dcec7634c754484542dc0a5c1b0e0b83a34bda5e91e9b7c3a1", size = 1896193, upload-time = "2026-01-21T16:27:24.785Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/ed/d51889da7ceaf5ff7a0574fb28f9b6b223df19667265395891f81b364ab3/torchvision-0.25.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b5e7f50002a8145a98c5694a018e738c50e2972608310c7e88e1bd4c058f6ce", size = 2309331, upload-time = "2026-01-21T16:27:19.97Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/a5/f93fcffaddd8f12f9e812256830ec9c9ca65abbf1bc369379f9c364d1ff4/torchvision-0.25.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:632db02300e83793812eee4f61ae6a2686dab10b4cfd628b620dc47747aa9d03", size = 8088713, upload-time = "2026-01-21T16:27:15.281Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/eb/d0096eed5690d962853213f2ee00d91478dfcb586b62dbbb449fb8abc3a6/torchvision-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:d1abd5ed030c708f5dbf4812ad5f6fbe9384b63c40d6bd79f8df41a4a759a917", size = 4325058, upload-time = "2026-01-21T16:27:26.165Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/36/96374a4c7ab50dea9787ce987815614ccfe988a42e10ac1a2e3e5b60319a/torchvision-0.25.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad9a8a5877782944d99186e4502a614770fe906626d76e9cd32446a0ac3075f2", size = 1896207, upload-time = "2026-01-21T16:27:23.383Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/e2/7abb10a867db79b226b41da419b63b69c0bd5b82438c4a4ed50e084c552f/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:40a122c3cf4d14b651f095e0f672b688dde78632783fc5cd3d4d5e4f6a828563", size = 2310741, upload-time = "2026-01-21T16:27:18.712Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/08/e6/0927784e6ffc340b6676befde1c60260bd51641c9c574b9298d791a9cda4/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:846890161b825b38aa85fc37fb3ba5eea74e7091ff28bab378287111483b6443", size = 8089772, upload-time = "2026-01-21T16:27:14.048Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/37/e7ca4ec820d434c0f23f824eb29f0676a0c3e7a118f1514f5b949c3356da/torchvision-0.25.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f07f01d27375ad89d72aa2b3f2180f07da95dd9d2e4c758e015c0acb2da72977", size = 4425879, upload-time = "2026-01-21T16:27:12.579Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tqdm"
|
name = "tqdm"
|
||||||
version = "4.67.1"
|
version = "4.67.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user