Merge branch 'refactor-for-aimsight' into 'main'

Refactor for AimSight: modular architecture, S3 storage, and new annotation tools

See merge request industry/aimsight/river-annotation-tool!1
This commit is contained in:
2026-06-02 09:53:59 +00:00
32 changed files with 3779 additions and 840 deletions

View File

@@ -1,8 +0,0 @@
{
"permissions": {
"allow": [
"Bash(Get-ChildItem -Recurse -Depth 2)",
"Bash(Select-Object FullName)"
]
}
}

5
.env.example Normal file
View File

@@ -0,0 +1,5 @@
S3_ACCESS_KEY=your-access-key-here
S3_SECRET_ACCESS_KEY=your-secret-key-here
S3_ENDPOINT_URL=https://os.zhdk.cloud.switch.ch
AWS_REQUEST_CHECKSUM_CALCULATION="when_required"
AWS_RESPONSE_CHECKSUM_VALIDATION="when_required"

View File

@@ -1,38 +0,0 @@
name: Run tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12"]
env:
PIP_ROOT_USER_ACTION: ignore
UV_LINK_MODE: copy
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install uv
uv sync --locked
- name: Check format with ruff
run: |
uv run ruff format --check
- name: Check code linting with ruff
run: |
uv run ruff check

12
.gitignore vendored
View File

@@ -2,6 +2,8 @@
*.pyc *.pyc
.ipynb_checkpoints/ .ipynb_checkpoints/
*.egg-info/ *.egg-info/
.claude/
.github/
# IDE settings # IDE settings
.vscode/ .vscode/
@@ -11,4 +13,12 @@
.DS_Store .DS_Store
# Data # Data
data/** data/**
# User-specific config (copy from *.example.* files)
config/config.yaml
config/clips.txt
.env
# Notebooks
notebooks/

363
README.md
View File

@@ -1,97 +1,360 @@
# River Annotation Tool # Video Annotation Tool
A desktop application for manually annotating river video clips as part of the [HydroScan](https://github.com/HydroScan) project. It lets annotators draw pixel-level masks over river regions of interest and answer structured survey questions about flow conditions, lighting, and scene quality. A desktop GUI application for manually annotating video clips. Annotators draw pixel-level segmentation masks over footage and answer structured survey questions defined in a config file.
## Features
- Load river video clips from ZIP archives (containing MP4s)
- Draw and erase masks with an adjustable brush on video frames
- Cycle through all frames with auto-playback at native FPS
- Answer structured questions across three categories: **River**, **Scene**, and **Weather**
- Resume saved annotation sessions; exports masks, metadata, and overlay GIFs
## Requirements ## Requirements
- Python 3.12 - Python 3.12
- [uv](https://docs.astral.sh/uv/) (recommended) or pip - [uv](https://docs.astral.sh/uv/) (recommended) or pip
## Quick start
```sh
# 1. Clone and install
git clone https://gitlab.datascience.ch/industry/aimsight/river-annotation-tool
cd river-annotation-tool
uv sync
# 2. Create config and clip list from examples
cp config/config.example.yaml config/config.yaml
cp config/clips.example.txt config/clips.txt
# 3. Edit config/config.yaml (set data_dir and out_dir)
# Edit config/clips.txt (list clips to annotate)
# Edit config/questions.yaml to customise survey questions (optional)
# 4. Run
uv run python -m clip_annotator
```
## Installation ## Installation
```sh ```sh
# Clone the repository # Install with uv (creates the virtual environment automatically)
git clone <repo-url>
cd river-annotation-tool
# Install dependencies (creates a virtual environment automatically with uv)
uv sync uv sync
# Or with pip # Or with pip
python -m venv .venv python -m venv .venv
.venv\Scripts\activate # Windows .venv\Scripts\activate # Windows
# source .venv/bin/activate # macOS/Linux source .venv/bin/activate # macOS/Linux
pip install -e . pip install -e .
``` ```
## Setup
Before running, create your config and clip list from the provided examples:
```sh
cp config/config.example.yaml config/config.yaml
cp config/clips.example.txt config/clips.txt
```
Edit `config/config.yaml` to set your `data_dir` and `out_dir`, then edit `config/clips.txt` to list the clips you want to annotate. Survey questions are defined in `config/questions.yaml` (committed to the repo; edit to customise). See the [Configuration](#configuration) section for all available options.
### S3 storage (optional)
By default the tool reads clips from and writes annotations to the local filesystem (`storage: local`). To use an S3-compatible object store instead, set `storage: s3` in `config/config.yaml` and give `data_dir` / `out_dir` as `bucket/prefix` paths:
```yaml
storage: s3
data_dir: my-bucket/clips
out_dir: my-bucket/annotation_results
```
Copy `.env.example` to `.env` and fill in your credentials — the app loads this file automatically at startup:
```sh
cp .env.example .env
# edit .env with your credentials
```
| Variable | Description |
|---|---|
| `S3_ACCESS_KEY` | Access key ID |
| `S3_SECRET_ACCESS_KEY` | Secret access key |
| `S3_ENDPOINT_URL` | Endpoint URL (defaults to `https://os.zhdk.cloud.switch.ch` if not set) |
| `AWS_REQUEST_CHECKSUM_CALCULATION` | Set to `when_required` to avoid checksum errors on SwitchEngines/Ceph |
| `AWS_RESPONSE_CHECKSUM_VALIDATION` | Set to `when_required` to avoid checksum errors on SwitchEngines/Ceph |
The `clips_file` (the list of clip filenames to annotate) is always read from the local filesystem even when `storage: s3`.
## Usage ## Usage
```sh ```sh
python -m river_annotation_tool.annotation_script \ uv run python -m clip_annotator
--data <path/to/zip/files> \ # or, if you have the venv activated:
--out <path/to/output/dir> \ python -m clip_annotator
[--clip <clip_name>]
``` ```
### Arguments
| Argument | Default | Description | | Argument | Default | Description |
|---|---|---| |---|---|---|
| `--data` | `../torrent-flow/data/examples_for_annotations/` | Directory containing ZIP files | | `--config` | `config/config.yaml` | Path to the config YAML file |
| `--out` | `data/annotation_results/` | Output directory for saved annotations | | `--data` | *(from config)* | Override `data_dir` from config |
| `--clip` | *(first clip)* | Specific clip to open (e.g. `left_20230501`) | | `--out` | *(from config)* | Override `out_dir` from config |
| `--clips` | *(from config)* | Override `clips_file` from config |
| `--clip` | *(first unannotated in list)* | Open a specific clip by its stem name (filename without extension, e.g. `clip_20230501T120000`) |
| `--extras` | off | Also save GIFs and extra PNGs (see Output section). **Note: GIF encoding is slow — saving will freeze the UI for several seconds per clip.** |
| `--no-skip` | off | Show already-annotated clips instead of skipping them |
### Controls ### Typical workflows
```sh
# Annotate clips listed in config/clips.txt (default)
uv run python -m clip_annotator
# Use a different config file
uv run python -m clip_annotator --config config/my_config.yaml
# Override paths from the command line
uv run python -m clip_annotator --data data/clips --out data/out
# Annotate a single specific clip
uv run python -m clip_annotator --clip clip_20230615T120000
```
## Configuration
Main settings live in `config/config.yaml`. Copy `config/config.example.yaml` to get started.
```yaml
storage: local # required: 'local' or 's3'
data_dir: # required: read-only source of ZIP archives (local path or bucket/prefix for S3)
out_dir: # required: write destination for annotations (can be same bucket as data_dir with a different prefix, or a separate location)
clips_file: config/clips.txt
optical_flow_config_file: config/optical_flow_config.yaml
questions_config_file: config/questions.yaml
display_max: 720 # longest side in pixels for display
fps_fallback: 25 # FPS to use if the video header is missing
max_frames: 100 # max frames to extract per clip
# Override input filenames only if your ZIP archives differ from the defaults
filenames:
video_in_zip: left.mp4
video_tmp_suffix: .mp4
zip_extension: .zip
```
Output filenames (`mask.png`, `metadata.json`, etc.) have sensible defaults and can be overridden in the `filenames:` block — see [`config.py`](src/clip_annotator/config.py) for the full list.
### Survey questions
Survey questions are defined in `config/questions.yaml` (committed to the repo). Add, remove, or reorder sections and items — the UI rebuilds automatically. `key` is what gets saved in `metadata.json`; `default` selects the pre-checked option (omit or set to `null` to leave unselected).
### Optical flow segmentation
`config/optical_flow_config.yaml` controls the **Auto Segment** button. When pressed, the tool computes a segmentation mask from the loaded frames and replaces the current mask (undoable). The segmentation combines two criteria:
- **Optical flow magnitude** — pixels where the temporal median of frame-to-frame flow (scaled by FPS) exceeds a fraction of the maximum are considered moving.
- **Brightness** — pixels outside a brightness window are excluded (removes sky, saturated glare, etc.).
```yaml
# config/optical_flow_config.yaml
enabled: true
norm_squared_threshold: 0.06 # fraction of max flow² that counts as moving
gaussian_kernel: [5, 5] # blur kernel applied to the reference frame before brightness check
brightness_range: [2, 253] # [min, max] greyscale brightness to keep
```
`enabled: false` disables the button without removing the config file.
## Clip list file
`config/clips.txt` lists bare clip filenames (not full paths) to annotate, one per line — the tool prepends `data_dir` automatically. Each entry must be the exact filename of the ZIP archive as it appears in `data_dir` (e.g. `clip_20230501T120000.zip`). Lines starting with `#` are ignored. Clips are processed in order; a clip is considered already-annotated when `<out_dir>/<clip_stem>/mask.png` exists and is skipped automatically. Pass `--no-skip` to include already-annotated clips. When the last clip is reached, a dialog appears and the app exits.
```
# Example clips.txt
clip_20230501T120000.zip
clip_20230502T120000.zip
```
Copy `config/clips.example.txt` as a starting point.
## Multi-annotator setup
To distribute clips across multiple annotators, create one clips file per annotator (e.g. `config/annotator_A.txt`) listing their assigned clip filenames, then pass it via `--clips`:
```sh
uv run python -m clip_annotator --clips config/annotator_A.txt
```
Assigning non-overlapping clip lists lets each annotator work independently. Intentionally overlapping a subset of clips across annotators enables inter-annotator agreement checks.
## Controls
The window is split into two panels: the **video canvas** on the left (~70% of the width) and the **survey panel** on the right. The video auto-plays as a looping preview. Drawing tools and mask controls are arranged above and beside the canvas; navigation buttons (**Previous / Next / Skip**) sit at the top.
### Tool modes
Three drawing tools are available in the tool row. The active tool is highlighted in blue.
| Tool | How to activate | Description |
|---|---|---|
| **Brush** | Click **Brush** | Click and drag to paint the mask with a circular brush (default) |
| **Polygon** | Click **Polygon** | Click to place vertices and build closed shapes; use **Fill** mode to commit them |
| **Fill** | Click **Fill** | Click inside a closed polygon to fill it onto the mask |
### Brush tool
| Action | How | | Action | How |
|---|---| |---|---|
| Draw mask | Click and drag on the canvas | | Draw mask | Click and drag on the video |
| Erase mask | Toggle **Eraser** button, then drag | | Erase mask | Toggle **Eraser** button (turns orange when active), then drag |
| Undo last stroke | **Undo** button | | Brush preview | A white circle follows the cursor showing the current brush size |
| Play/pause frames | **Play / Pause** button | | Adjust brush size | **Brush size** slider (250 px, default 5); click **↺** to reset |
| Save annotation | **Save** button |
| Change brush size | Slider in the toolbar | ### Polygon tool
Polygons are drawn as overlays and do not affect the mask until you use **Fill** mode.
| Action | How |
|---|---|
| Add vertex | Left-click on the canvas |
| Remove last vertex | Right-click |
| Close a shape | Left-click near the first vertex (red dot) when ≥ 3 vertices are placed; completed shapes turn bold cyan |
| Draw multiple shapes | Each closed shape is kept independently; draw as many as needed |
| Cancel in-progress polygon | **Cancel Current Poly** — discards the unfinished polygon, keeps completed shapes |
| Delete last completed shape | **Del Shape** |
### Fill tool
| Action | How |
|---|---|
| Fill a shape | Left-click anywhere inside a closed polygon; that shape's interior is painted onto the mask |
| Nested shapes | If a closed polygon lies entirely inside the target, its interior is left unfilled (acts as a hole) |
| Innermost shape | Clicking inside nested shapes always fills the innermost (smallest) polygon containing the click |
| Undo fill | **Undo** — each fill is a single undoable step |
### Mask editing
| Action | How |
|---|---|
| Undo last action | **Undo** |
| Undo 10 actions | **Undo×10** |
| Redo | **Redo** |
| Clear entire mask | **Clear** |
| Toggle mask overlay | **Hide Mask / Show Mask** — button turns red when hidden; does not affect mask data |
| Mask transparency | **Mask Alpha** slider (0100%, default 15%); click **↺** to reset |
### Starting-point shortcuts
| Action | How |
|---|---|
| Load mask from previous clip | **Load Prev Mask** — copies the saved mask of the previous clip onto the current one; undoable |
| Optical flow first guess | **Auto Segment** — replaces the current mask with an automatic segmentation based on motion and brightness; undoable. Disabled when `enabled: false` in `config/optical_flow_config.yaml`. |
### Image display adjustments
Three vertical sliders sit to the left of the video and affect display only — they do not change what is saved.
| Slider | Effect | Range |
|---|---|---|
| Brightness | Shifts all pixel values up or down | 100 to +100 |
| Contrast | Scales pixel values around the midpoint | 100 to +100 |
| Gamma | Applies a power-law correction (higher = brighter) | 0.1× to 3.0× |
Click **↺** below any slider to restore its default value.
### Navigation
| Action | How |
|---|---|
| Save and continue | **Next** — saves current clip and loads the next one. If the clip already has a saved annotation a dialog asks whether to replace it or keep the existing save. The UI freezes during saving; this is normal — wait for it to complete before clicking again. |
| Go back | **Previous** — saves current clip and returns to the previously viewed clip. Disabled on the first clip. |
| Skip without saving | **Skip** — discards any unsaved changes and loads the next clip without writing anything to disk. |
## Output ## Output
Each clip is saved to `<output_dir>/<clip_stem>/`: Each annotated clip produces a folder `<out_dir>/<clip_stem>/` with:
``` ```
mask.png # Binary mask at full resolution mask.png # Binary segmentation mask at full source resolution (always)
metadata.json # Survey answers metadata.json # Survey answers as JSON (always)
frame.png # Key frame frame.png # Middle frame of the clip (always)
mask_vis.png # Mask visualisation overlay.png # That frame with the mask blended in green (always)
overlay.png # Frame + mask overlay
video_original_hires.gif # Only with --extras:
video_original_lowres.gif mask_vis.png # Mask rendered as a greyscale PNG
video_overlay_hires.gif video_original_hires.gif # All frames at display resolution
video_overlay_lowres.gif video_original_lowres.gif # All frames at 50% of display resolution
video_overlay_hires.gif # Overlay GIF at display resolution
video_overlay_lowres.gif # Overlay GIF at 50% of display resolution
``` ```
## Repository Structure All output filenames can be overridden via the `filenames:` section in `config/config.yaml`.
### Survey answers (`metadata.json`)
Keys and values are determined by `config/questions.yaml`. With the default questions:
```json
{
"flow": "Turbulent | Laminar | Uncertain",
"shadows": "Yes | No | Uncertain",
"artifacts": "Yes | No | Uncertain",
"lighting": "Day | Night | Uncertain",
"exposure": "Overexposed | Underexposed | Both | Normal | Uncertain",
"snowing": "Yes | No | Uncertain",
"snow_on_ground":"Yes | No | Uncertain"
}
```
## Internals
### Clip format
Each clip is a ZIP archive containing a video file. The video inside the archive must be named `left.mp4` by default; if your archives use a different internal name, set `filenames.video_in_zip` in `config.yaml`. The ZIP filename becomes the output folder name (e.g. `clip_20230501T120000.zip``<out_dir>/clip_20230501T120000/`).
### Frame loading
Up to `max_frames` frames are extracted from the video and scaled so the longest side is `display_max` px. This display-resolution copy is what the annotator works on; the full-resolution dimensions are remembered separately so the saved mask is upscaled back to the original size on export.
### Mask drawing
The mask is a binary array at display resolution. **Brush** strokes stamp a filled circle (draw or erase). **Polygon** shapes are stored as overlays and don't touch the mask until a **Fill** click rasterises them — the innermost polygon containing the click is filled, and any polygon whose centroid falls inside it is punched out as a hole.
Every mask-changing operation is pushed onto an undo stack before it executes. On save, the mask is upscaled to the original video resolution and written as an 8-bit PNG (0 or 255).
### Resuming
When a clip is loaded that already has a saved `mask.png` and `metadata.json`, the mask is restored at display resolution and the survey answers are pre-filled.
## Repository structure
``` ```
src/river_annotation_tool/ .env.example # S3 credential template (copy to .env and fill in)
annotation_script.py # Main GUI application config/
__init__.py # Package version config.yaml # Your local config (git-ignored, copy from example)
pyproject.toml # Project metadata and dependencies config.example.yaml # Example config to copy and edit
requirements.txt # Pinned dependencies (generated) clips.txt # Your clip list (git-ignored, copy from example)
clips.example.txt # Example clip list
questions.yaml # Survey question definitions
optical_flow_config.yaml # Optical flow parameters (set enabled: false to disable Auto Segment)
src/clip_annotator/
__main__.py # Entry point — argument parsing and app launch
annotator.py # Main QMainWindow — orchestrates all components
clip_selector.py # Reads the clip list and picks the next clip
filesystem.py # Storage backend — local passthrough or S3 via s3fs
mask_canvas.py # Drawing widget — brush, undo, erase, mouse events
video_loader.py # ZIP extraction and frame resizing
compute_optical_flow.py # Optical flow segmentation (Auto Segment button)
config.py # AppConfig dataclass and YAML loader
__init__.py # Package version
pyproject.toml # Project metadata and dependencies
``` ```
## Development ## Development
```sh ```sh
# Install pre-commit hooks # Install pre-commit hooks
pre-commit install uv run pre-commit install
pre-commit run --all-files # Run hooks manually once uv run pre-commit run --all-files # Run manually once
# Add a dependency # Add a dependency
uv add <package> uv add <package>
uv add --dev <package> # Development-only uv add --dev <package> # Development-only
``` ```

94
config/annotator_A.txt Normal file
View File

@@ -0,0 +1,94 @@
GRAMMONT_2025-11-17T11:31:38.546953+00:00.zip
GRAMMONT_2025-11-17T12:31:39.650554+00:00.zip
GRAMMONT_2025-11-17T15:32:07.184007+00:00.zip
GRAMMONT_2025-11-17T15:47:10.070449+00:00.zip
GRAMMONT_2025-11-17T16:02:09.881377+00:00.zip
GRAMMONT_2025-11-17T16:17:07.937820+00:00.zip
GRAMMONT_2025-11-17T16:32:06.019806+00:00.zip
GRAMMONT_2025-11-17T16:47:05.241264+00:00.zip
GRAMMONT_2025-11-17T17:02:05.056396+00:00.zip
GRAMMONT_2025-11-17T17:17:05.186394+00:00.zip
GRAMMONT_2025-11-17T19:47:09.762766+00:00.zip
GRAMMONT_2025-11-17T20:02:05.552868+00:00.zip
GRAMMONT_2025-11-17T23:02:09.394251+00:00.zip
GRAMMONT_2025-12-03T09:32:01.515556+00:00.zip
GRAMMONT_2025-12-03T11:17:03.118822+00:00.zip
GRAMMONT_2025-12-03T15:17:03.043807+00:00.zip
GRAMMONT_2025-12-03T15:32:04.085232+00:00.zip
GRAMMONT_2025-12-03T15:47:04.864411+00:00.zip
GRAMMONT_2025-12-03T16:02:09.554901+00:00.zip
GRAMMONT_2025-12-03T16:17:07.984380+00:00.zip
GRAMMONT_2025-12-03T16:32:06.042132+00:00.zip
GRAMMONT_2025-12-03T16:47:05.425453+00:00.zip
GRAMMONT_2025-12-03T17:02:05.867456+00:00.zip
GRAMMONT_2025-12-03T19:17:08.208705+00:00.zip
GRAMMONT_2025-12-03T19:47:07.924056+00:00.zip
GRAMMONT_2025-12-03T20:17:04.802945+00:00.zip
GRAMMONT_2025-12-03T23:17:04.368047+00:00.zip
GRAMMONT_2026-01-01T00:01:38.617204+00:00.zip
GRAMMONT_2026-01-01T01:01:28.406165+00:00.zip
GRAMMONT_2026-01-01T01:16:27.492561+00:00.zip
GRAMMONT_2026-01-01T04:31:27.576814+00:00.zip
GRAMMONT_2026-01-01T06:01:28.565510+00:00.zip
GRAMMONT_2026-01-01T06:16:27.160500+00:00.zip
GRAMMONT_2026-01-01T06:31:27.071465+00:00.zip
GRAMMONT_2026-01-01T06:46:26.490815+00:00.zip
GRAMMONT_2026-01-01T07:01:27.248968+00:00.zip
GRAMMONT_2026-01-01T07:16:24.975576+00:00.zip
GRAMMONT_2026-01-01T07:31:22.112561+00:00.zip
GRAMMONT_2026-01-01T07:46:21.433475+00:00.zip
GRAMMONT_2026-01-01T08:01:18.720307+00:00.zip
GRAMMONT_2026-01-01T09:31:19.111375+00:00.zip
GRAMMONT_2026-01-01T09:46:17.316758+00:00.zip
GRAMMONT_2026-01-01T10:01:17.837711+00:00.zip
GRAMMONT_2026-01-01T10:16:18.968668+00:00.zip
GRAMMONT_2026-01-01T11:16:20.885047+00:00.zip
GRAMMONT_2026-01-01T11:31:19.000253+00:00.zip
GRAMMONT_2026-01-01T15:31:21.006934+00:00.zip
GRAMMONT_2026-01-01T15:46:23.298079+00:00.zip
GRAMMONT_2026-01-01T16:01:26.959862+00:00.zip
GRAMMONT_2026-01-01T16:16:26.753510+00:00.zip
GRAMMONT_2026-01-01T16:31:27.166783+00:00.zip
GRAMMONT_2026-01-01T16:46:27.263665+00:00.zip
GRAMMONT_2026-01-01T17:01:27.154872+00:00.zip
GRAMMONT_2026-01-01T17:46:27.751280+00:00.zip
GRAMMONT_2026-01-01T21:31:28.388140+00:00.zip
GRAMMONT_2026-01-09T00:01:37.412545+00:00.zip
GRAMMONT_2026-01-09T05:31:25.188144+00:00.zip
GRAMMONT_2026-01-09T07:31:19.934715+00:00.zip
GRAMMONT_2026-01-09T07:46:17.007279+00:00.zip
GRAMMONT_2026-01-09T08:16:17.338747+00:00.zip
GRAMMONT_2026-01-09T10:31:15.867851+00:00.zip
GRAMMONT_2026-01-09T18:16:25.202124+00:00.zip
GRAMMONT_2026-01-09T19:16:26.688542+00:00.zip
GRAMMONT_2026-01-09T20:01:22.748919+00:00.zip
GRAMMONT_2026-01-09T23:31:26.962588+00:00.zip
GRAMMONT_2026-02-11T00:01:38.048234+00:00.zip
GRAMMONT_2026-02-11T01:01:25.496751+00:00.zip
GRAMMONT_2026-02-11T04:31:25.749592+00:00.zip
GRAMMONT_2026-02-11T04:46:26.124855+00:00.zip
GRAMMONT_2026-02-11T05:31:27.076000+00:00.zip
GRAMMONT_2026-02-11T05:46:26.426312+00:00.zip
GRAMMONT_2026-02-11T06:01:26.452689+00:00.zip
GRAMMONT_2026-02-11T06:16:26.767560+00:00.zip
GRAMMONT_2026-02-11T06:31:24.624797+00:00.zip
GRAMMONT_2026-02-11T06:46:23.455019+00:00.zip
GRAMMONT_2026-02-11T07:01:21.080045+00:00.zip
GRAMMONT_2026-02-11T07:16:20.533988+00:00.zip
GRAMMONT_2026-02-11T07:31:19.697932+00:00.zip
GRAMMONT_2026-02-11T08:01:19.085138+00:00.zip
GRAMMONT_2026-02-11T09:16:17.805079+00:00.zip
GRAMMONT_2026-02-11T09:31:17.747859+00:00.zip
GRAMMONT_2026-02-11T12:31:20.771768+00:00.zip
GRAMMONT_2026-02-11T15:46:19.819366+00:00.zip
GRAMMONT_2026-02-11T16:31:20.659090+00:00.zip
GRAMMONT_2026-02-11T16:46:19.780502+00:00.zip
GRAMMONT_2026-02-11T17:01:25.098410+00:00.zip
GRAMMONT_2026-02-11T17:16:25.979320+00:00.zip
GRAMMONT_2026-02-11T17:31:27.522773+00:00.zip
GRAMMONT_2026-02-11T17:46:26.400124+00:00.zip
GRAMMONT_2026-02-11T18:01:25.365899+00:00.zip
GRAMMONT_2026-02-11T18:16:25.442042+00:00.zip
GRAMMONT_2026-02-11T21:01:25.956306+00:00.zip
GRAMMONT_2026-02-11T21:31:26.462852+00:00.zip
GRAMMONT_2026-02-11T22:31:26.629631+00:00.zip

128
config/annotator_B.txt Normal file
View File

@@ -0,0 +1,128 @@
GRAMMONT_2025-11-18T00:02:16.622295+00:00.zip
GRAMMONT_2025-11-18T01:02:05.553357+00:00.zip
GRAMMONT_2025-11-18T01:17:10.391972+00:00.zip
GRAMMONT_2025-11-18T04:02:09.847002+00:00.zip
GRAMMONT_2025-11-18T05:47:07.350141+00:00.zip
GRAMMONT_2025-11-18T06:02:06.960352+00:00.zip
GRAMMONT_2025-11-18T06:17:11.438801+00:00.zip
GRAMMONT_2025-11-18T06:32:10.134718+00:00.zip
GRAMMONT_2025-11-18T06:47:05.176210+00:00.zip
GRAMMONT_2025-11-18T07:02:05.016401+00:00.zip
GRAMMONT_2025-11-18T07:17:04.505510+00:00.zip
GRAMMONT_2025-11-18T07:32:02.052621+00:00.zip
GRAMMONT_2025-11-18T10:47:04.410566+00:00.zip
GRAMMONT_2025-11-18T14:47:02.368668+00:00.zip
GRAMMONT_2025-11-18T15:32:07.687051+00:00.zip
GRAMMONT_2025-11-18T15:47:04.708858+00:00.zip
GRAMMONT_2025-11-18T16:02:08.642632+00:00.zip
GRAMMONT_2025-11-18T16:17:08.155540+00:00.zip
GRAMMONT_2025-11-18T16:32:08.782723+00:00.zip
GRAMMONT_2025-11-18T16:47:09.340365+00:00.zip
GRAMMONT_2025-11-18T17:02:04.224910+00:00.zip
GRAMMONT_2025-11-18T17:17:06.866509+00:00.zip
GRAMMONT_2025-11-18T18:32:07.533041+00:00.zip
GRAMMONT_2025-12-05T00:02:14.666551+00:00.zip
GRAMMONT_2025-12-05T01:02:05.794759+00:00.zip
GRAMMONT_2025-12-05T01:17:05.892742+00:00.zip
GRAMMONT_2025-12-05T04:02:05.727427+00:00.zip
GRAMMONT_2025-12-05T06:17:08.164092+00:00.zip
GRAMMONT_2025-12-05T06:32:04.336644+00:00.zip
GRAMMONT_2025-12-05T06:47:08.004420+00:00.zip
GRAMMONT_2025-12-05T07:02:05.120746+00:00.zip
GRAMMONT_2025-12-05T07:17:05.647473+00:00.zip
GRAMMONT_2025-12-05T07:32:01.392138+00:00.zip
GRAMMONT_2025-12-05T07:47:01.666820+00:00.zip
GRAMMONT_2025-12-05T08:02:03.470785+00:00.zip
GRAMMONT_2025-12-05T08:31:58.765660+00:00.zip
GRAMMONT_2025-12-05T10:17:01.321795+00:00.zip
GRAMMONT_2025-12-05T12:01:57.048658+00:00.zip
GRAMMONT_2025-12-05T15:17:05.187833+00:00.zip
GRAMMONT_2025-12-05T15:32:03.494960+00:00.zip
GRAMMONT_2025-12-05T15:47:03.966183+00:00.zip
GRAMMONT_2025-12-05T16:02:08.393449+00:00.zip
GRAMMONT_2025-12-05T16:17:04.961643+00:00.zip
GRAMMONT_2025-12-05T16:32:07.906681+00:00.zip
GRAMMONT_2025-12-05T16:47:06.887564+00:00.zip
GRAMMONT_2025-12-05T17:02:06.990948+00:00.zip
GRAMMONT_2025-12-05T18:02:04.071706+00:00.zip
GRAMMONT_2025-12-05T18:32:05.013636+00:00.zip
GRAMMONT_2026-01-06T00:01:37.202493+00:00.zip
GRAMMONT_2026-01-06T01:01:27.022847+00:00.zip
GRAMMONT_2026-01-06T01:16:27.094115+00:00.zip
GRAMMONT_2026-01-06T04:31:26.976344+00:00.zip
GRAMMONT_2026-01-06T06:16:26.327723+00:00.zip
GRAMMONT_2026-01-06T06:31:24.587899+00:00.zip
GRAMMONT_2026-01-06T06:46:27.517903+00:00.zip
GRAMMONT_2026-01-06T07:01:26.908449+00:00.zip
GRAMMONT_2026-01-06T07:16:24.967227+00:00.zip
GRAMMONT_2026-01-06T07:31:21.683307+00:00.zip
GRAMMONT_2026-01-06T07:46:19.343980+00:00.zip
GRAMMONT_2026-01-06T08:01:19.342405+00:00.zip
GRAMMONT_2026-01-06T08:16:18.724209+00:00.zip
GRAMMONT_2026-01-06T09:31:17.414283+00:00.zip
GRAMMONT_2026-01-06T09:46:18.170203+00:00.zip
GRAMMONT_2026-01-06T10:01:18.539805+00:00.zip
GRAMMONT_2026-01-06T10:16:18.307980+00:00.zip
GRAMMONT_2026-01-06T10:31:19.164363+00:00.zip
GRAMMONT_2026-01-06T10:46:19.104227+00:00.zip
GRAMMONT_2026-01-06T11:16:18.209654+00:00.zip
GRAMMONT_2026-01-06T11:31:16.324483+00:00.zip
GRAMMONT_2026-01-06T11:46:17.515591+00:00.zip
GRAMMONT_2026-01-06T15:46:18.998119+00:00.zip
GRAMMONT_2026-01-06T16:01:21.931831+00:00.zip
GRAMMONT_2026-01-06T16:16:24.878420+00:00.zip
GRAMMONT_2026-01-06T16:31:28.169775+00:00.zip
GRAMMONT_2026-01-06T16:46:26.848151+00:00.zip
GRAMMONT_2026-01-06T17:01:27.977881+00:00.zip
GRAMMONT_2026-01-06T17:16:26.719117+00:00.zip
GRAMMONT_2026-01-06T17:31:26.921538+00:00.zip
GRAMMONT_2026-01-06T17:46:26.350830+00:00.zip
GRAMMONT_2026-01-06T21:31:27.184279+00:00.zip
GRAMMONT_2026-02-12T00:01:39.500750+00:00.zip
GRAMMONT_2026-02-12T04:46:27.068804+00:00.zip
GRAMMONT_2026-02-12T05:31:26.143011+00:00.zip
GRAMMONT_2026-02-12T05:46:25.853849+00:00.zip
GRAMMONT_2026-02-12T06:01:25.057912+00:00.zip
GRAMMONT_2026-02-12T06:16:25.744052+00:00.zip
GRAMMONT_2026-02-12T06:31:24.481263+00:00.zip
GRAMMONT_2026-02-12T06:46:24.932963+00:00.zip
GRAMMONT_2026-02-12T07:01:21.846587+00:00.zip
GRAMMONT_2026-02-12T07:16:21.265797+00:00.zip
GRAMMONT_2026-02-12T07:31:18.457424+00:00.zip
GRAMMONT_2026-02-12T07:46:20.250231+00:00.zip
GRAMMONT_2026-02-12T12:31:19.009888+00:00.zip
GRAMMONT_2026-02-12T15:01:19.953447+00:00.zip
GRAMMONT_2026-02-12T15:46:19.735032+00:00.zip
GRAMMONT_2026-02-12T16:31:19.052209+00:00.zip
GRAMMONT_2026-02-12T16:46:21.632499+00:00.zip
GRAMMONT_2026-02-12T17:01:25.223457+00:00.zip
GRAMMONT_2026-02-12T17:16:26.740464+00:00.zip
GRAMMONT_2026-02-12T17:31:25.908204+00:00.zip
GRAMMONT_2026-02-12T17:46:26.495323+00:00.zip
GRAMMONT_2026-02-12T18:01:26.530360+00:00.zip
GRAMMONT_2026-02-12T21:01:27.495845+00:00.zip
GRAMMONT_2026-02-12T22:31:26.172360+00:00.zip
GRAMMONT_2026-03-02T00:02:00.235433+00:00.zip
GRAMMONT_2026-03-02T01:16:49.553391+00:00.zip
GRAMMONT_2026-03-02T03:16:48.829809+00:00.zip
GRAMMONT_2026-03-02T05:01:49.656932+00:00.zip
GRAMMONT_2026-03-02T05:16:48.603914+00:00.zip
GRAMMONT_2026-03-02T05:31:48.072998+00:00.zip
GRAMMONT_2026-03-02T05:46:48.743481+00:00.zip
GRAMMONT_2026-03-02T06:01:46.583618+00:00.zip
GRAMMONT_2026-03-02T06:16:45.405628+00:00.zip
GRAMMONT_2026-03-02T06:31:43.415579+00:00.zip
GRAMMONT_2026-03-02T06:46:41.697932+00:00.zip
GRAMMONT_2026-03-02T07:01:41.696734+00:00.zip
GRAMMONT_2026-03-02T12:16:42.701851+00:00.zip
GRAMMONT_2026-03-02T15:31:41.098916+00:00.zip
GRAMMONT_2026-03-02T16:01:39.517501+00:00.zip
GRAMMONT_2026-03-02T16:46:42.561834+00:00.zip
GRAMMONT_2026-03-02T17:01:41.958941+00:00.zip
GRAMMONT_2026-03-02T17:16:46.511805+00:00.zip
GRAMMONT_2026-03-02T17:31:48.499316+00:00.zip
GRAMMONT_2026-03-02T17:46:48.055562+00:00.zip
GRAMMONT_2026-03-02T18:01:47.851974+00:00.zip
GRAMMONT_2026-03-02T18:16:48.002798+00:00.zip
GRAMMONT_2026-03-02T18:31:48.973226+00:00.zip
GRAMMONT_2026-03-02T21:16:48.406731+00:00.zip

146
config/annotator_C.txt Normal file
View File

@@ -0,0 +1,146 @@
GRAMMONT_2025-11-22T00:02:20.203920+00:00.zip
GRAMMONT_2025-11-22T01:02:09.028313+00:00.zip
GRAMMONT_2025-11-22T01:17:07.434190+00:00.zip
GRAMMONT_2025-11-22T04:02:07.766345+00:00.zip
GRAMMONT_2025-11-22T05:32:06.253178+00:00.zip
GRAMMONT_2025-11-22T05:47:05.799143+00:00.zip
GRAMMONT_2025-11-22T06:02:04.909260+00:00.zip
GRAMMONT_2025-11-22T06:17:06.901461+00:00.zip
GRAMMONT_2025-11-22T06:31:15.263893+00:00.zip
GRAMMONT_2025-11-22T06:47:02.531210+00:00.zip
GRAMMONT_2025-11-22T07:02:01.143506+00:00.zip
GRAMMONT_2025-11-22T07:16:59.906656+00:00.zip
GRAMMONT_2025-11-22T10:47:02.705611+00:00.zip
GRAMMONT_2025-11-22T14:47:00.096714+00:00.zip
GRAMMONT_2025-11-22T15:32:01.015469+00:00.zip
GRAMMONT_2025-11-22T15:47:02.337459+00:00.zip
GRAMMONT_2025-11-22T16:02:04.420357+00:00.zip
GRAMMONT_2025-11-22T16:17:04.468696+00:00.zip
GRAMMONT_2025-11-22T16:32:07.616206+00:00.zip
GRAMMONT_2025-11-22T16:47:04.224377+00:00.zip
GRAMMONT_2025-11-22T17:02:08.264697+00:00.zip
GRAMMONT_2025-11-22T17:17:06.243128+00:00.zip
GRAMMONT_2025-11-22T18:32:05.405485+00:00.zip
GRAMMONT_2025-12-12T00:02:16.601750+00:00.zip
GRAMMONT_2025-12-12T01:02:06.959165+00:00.zip
GRAMMONT_2025-12-12T01:17:05.555142+00:00.zip
GRAMMONT_2025-12-12T04:02:05.356836+00:00.zip
GRAMMONT_2025-12-12T06:02:09.414012+00:00.zip
GRAMMONT_2025-12-12T06:17:06.397863+00:00.zip
GRAMMONT_2025-12-12T06:32:06.114230+00:00.zip
GRAMMONT_2025-12-12T06:47:07.413290+00:00.zip
GRAMMONT_2025-12-12T07:02:05.817199+00:00.zip
GRAMMONT_2025-12-12T07:17:05.572966+00:00.zip
GRAMMONT_2025-12-12T07:32:07.077808+00:00.zip
GRAMMONT_2025-12-12T07:47:02.952666+00:00.zip
GRAMMONT_2025-12-12T08:02:01.468729+00:00.zip
GRAMMONT_2025-12-12T09:32:00.091785+00:00.zip
GRAMMONT_2025-12-12T10:47:03.611372+00:00.zip
GRAMMONT_2025-12-12T15:01:24.078486+00:00.zip
GRAMMONT_2025-12-12T15:16:25.132035+00:00.zip
GRAMMONT_2025-12-12T15:31:27.076379+00:00.zip
GRAMMONT_2025-12-12T15:46:29.057871+00:00.zip
GRAMMONT_2025-12-12T16:01:29.207153+00:00.zip
GRAMMONT_2025-12-12T16:16:28.383856+00:00.zip
GRAMMONT_2025-12-12T16:31:29.989159+00:00.zip
GRAMMONT_2025-12-12T16:46:27.655685+00:00.zip
GRAMMONT_2025-12-12T17:01:26.768672+00:00.zip
GRAMMONT_2025-12-12T18:01:28.364073+00:00.zip
GRAMMONT_2025-12-12T18:31:28.885962+00:00.zip
GRAMMONT_2026-01-07T00:01:36.564462+00:00.zip
GRAMMONT_2026-01-07T01:01:25.835453+00:00.zip
GRAMMONT_2026-01-07T05:31:25.879211+00:00.zip
GRAMMONT_2026-01-07T06:01:27.118806+00:00.zip
GRAMMONT_2026-01-07T06:16:26.590606+00:00.zip
GRAMMONT_2026-01-07T06:31:11.284211+00:00.zip
GRAMMONT_2026-01-07T06:46:28.122544+00:00.zip
GRAMMONT_2026-01-07T07:01:26.812905+00:00.zip
GRAMMONT_2026-01-07T07:16:21.745008+00:00.zip
GRAMMONT_2026-01-07T07:31:20.991098+00:00.zip
GRAMMONT_2026-01-07T07:46:18.360296+00:00.zip
GRAMMONT_2026-01-07T08:01:18.000617+00:00.zip
GRAMMONT_2026-01-07T09:31:17.177515+00:00.zip
GRAMMONT_2026-01-07T09:46:16.591998+00:00.zip
GRAMMONT_2026-01-07T10:01:17.593665+00:00.zip
GRAMMONT_2026-01-07T10:16:17.565584+00:00.zip
GRAMMONT_2026-01-07T10:46:18.688218+00:00.zip
GRAMMONT_2026-01-07T15:46:20.163005+00:00.zip
GRAMMONT_2026-01-07T16:01:19.900623+00:00.zip
GRAMMONT_2026-01-07T16:16:25.483667+00:00.zip
GRAMMONT_2026-01-07T16:31:26.102407+00:00.zip
GRAMMONT_2026-01-07T16:46:27.893362+00:00.zip
GRAMMONT_2026-01-07T17:01:26.885062+00:00.zip
GRAMMONT_2026-01-07T17:16:26.349277+00:00.zip
GRAMMONT_2026-01-07T18:31:27.062101+00:00.zip
GRAMMONT_2026-01-07T18:46:27.409224+00:00.zip
GRAMMONT_2026-01-07T19:16:27.442506+00:00.zip
GRAMMONT_2026-01-07T19:31:27.148138+00:00.zip
GRAMMONT_2026-01-07T19:46:27.178939+00:00.zip
GRAMMONT_2026-01-07T20:01:26.118404+00:00.zip
GRAMMONT_2026-01-07T20:16:26.814555+00:00.zip
GRAMMONT_2026-01-07T20:31:26.777464+00:00.zip
GRAMMONT_2026-01-07T21:01:28.269021+00:00.zip
GRAMMONT_2026-01-07T21:16:26.280773+00:00.zip
GRAMMONT_2026-01-07T22:01:25.272933+00:00.zip
GRAMMONT_2026-01-07T22:46:25.828656+00:00.zip
GRAMMONT_2026-02-16T00:01:49.506731+00:00.zip
GRAMMONT_2026-02-16T02:01:38.207676+00:00.zip
GRAMMONT_2026-02-16T05:31:38.138921+00:00.zip
GRAMMONT_2026-02-16T05:46:39.421285+00:00.zip
GRAMMONT_2026-02-16T06:01:41.482751+00:00.zip
GRAMMONT_2026-02-16T06:16:38.604849+00:00.zip
GRAMMONT_2026-02-16T06:31:38.007478+00:00.zip
GRAMMONT_2026-02-16T06:46:38.680991+00:00.zip
GRAMMONT_2026-02-16T07:01:36.215933+00:00.zip
GRAMMONT_2026-02-16T07:16:32.902087+00:00.zip
GRAMMONT_2026-02-16T07:31:32.860647+00:00.zip
GRAMMONT_2026-02-16T07:46:32.874607+00:00.zip
GRAMMONT_2026-02-16T08:31:30.794666+00:00.zip
GRAMMONT_2026-02-16T09:16:29.781662+00:00.zip
GRAMMONT_2026-02-16T09:31:32.218357+00:00.zip
GRAMMONT_2026-02-16T09:46:28.532516+00:00.zip
GRAMMONT_2026-02-16T10:01:29.027530+00:00.zip
GRAMMONT_2026-02-16T10:16:30.952271+00:00.zip
GRAMMONT_2026-02-16T10:31:30.118460+00:00.zip
GRAMMONT_2026-02-16T10:46:30.728758+00:00.zip
GRAMMONT_2026-02-16T11:01:29.613915+00:00.zip
GRAMMONT_2026-02-16T11:16:27.770965+00:00.zip
GRAMMONT_2026-02-16T11:31:28.625626+00:00.zip
GRAMMONT_2026-02-16T11:46:27.599574+00:00.zip
GRAMMONT_2026-02-16T12:01:28.213671+00:00.zip
GRAMMONT_2026-02-16T12:16:28.302803+00:00.zip
GRAMMONT_2026-02-16T12:31:28.683376+00:00.zip
GRAMMONT_2026-02-16T12:46:29.904051+00:00.zip
GRAMMONT_2026-02-16T13:01:28.903040+00:00.zip
GRAMMONT_2026-02-16T13:16:31.554237+00:00.zip
GRAMMONT_2026-02-16T14:01:29.428221+00:00.zip
GRAMMONT_2026-02-16T14:16:28.134941+00:00.zip
GRAMMONT_2026-02-16T14:31:28.642884+00:00.zip
GRAMMONT_2026-02-16T14:46:31.050308+00:00.zip
GRAMMONT_2026-02-16T15:01:36.440533+00:00.zip
GRAMMONT_2026-02-16T18:31:37.721767+00:00.zip
GRAMMONT_2026-03-03T00:02:00.115993+00:00.zip
GRAMMONT_2026-03-03T01:16:47.545369+00:00.zip
GRAMMONT_2026-03-03T03:16:47.017876+00:00.zip
GRAMMONT_2026-03-03T05:01:48.172478+00:00.zip
GRAMMONT_2026-03-03T05:16:48.425996+00:00.zip
GRAMMONT_2026-03-03T05:31:47.535047+00:00.zip
GRAMMONT_2026-03-03T05:46:49.230361+00:00.zip
GRAMMONT_2026-03-03T06:01:48.032458+00:00.zip
GRAMMONT_2026-03-03T06:16:43.444057+00:00.zip
GRAMMONT_2026-03-03T06:31:43.572406+00:00.zip
GRAMMONT_2026-03-03T06:46:42.094886+00:00.zip
GRAMMONT_2026-03-03T07:01:40.412911+00:00.zip
GRAMMONT_2026-03-03T10:46:41.016664+00:00.zip
GRAMMONT_2026-03-03T12:16:43.885012+00:00.zip
GRAMMONT_2026-03-03T15:46:40.689643+00:00.zip
GRAMMONT_2026-03-03T16:16:40.142917+00:00.zip
GRAMMONT_2026-03-03T16:46:42.950814+00:00.zip
GRAMMONT_2026-03-03T17:01:42.774269+00:00.zip
GRAMMONT_2026-03-03T17:16:45.482827+00:00.zip
GRAMMONT_2026-03-03T17:31:48.702675+00:00.zip
GRAMMONT_2026-03-03T17:46:48.793191+00:00.zip
GRAMMONT_2026-03-03T18:01:48.301590+00:00.zip
GRAMMONT_2026-03-03T18:16:47.700676+00:00.zip
GRAMMONT_2026-03-03T18:31:48.737829+00:00.zip
GRAMMONT_2026-03-03T21:16:48.709909+00:00.zip

102
config/annotator_D.txt Normal file
View File

@@ -0,0 +1,102 @@
GRAMMONT_2025-11-18T00:02:16.622295+00:00.zip
GRAMMONT_2025-11-18T01:02:05.553357+00:00.zip
GRAMMONT_2025-11-18T01:17:10.391972+00:00.zip
GRAMMONT_2025-11-18T04:02:09.847002+00:00.zip
GRAMMONT_2025-11-18T05:47:07.350141+00:00.zip
GRAMMONT_2025-11-18T06:02:06.960352+00:00.zip
GRAMMONT_2025-11-18T06:17:11.438801+00:00.zip
GRAMMONT_2025-11-18T06:32:10.134718+00:00.zip
GRAMMONT_2025-11-18T06:47:05.176210+00:00.zip
GRAMMONT_2025-11-18T07:02:05.016401+00:00.zip
GRAMMONT_2025-11-18T07:17:04.505510+00:00.zip
GRAMMONT_2025-11-18T07:32:02.052621+00:00.zip
GRAMMONT_2025-11-18T10:47:04.410566+00:00.zip
GRAMMONT_2025-11-18T14:47:02.368668+00:00.zip
GRAMMONT_2025-11-18T15:32:07.687051+00:00.zip
GRAMMONT_2025-11-18T15:47:04.708858+00:00.zip
GRAMMONT_2025-11-18T16:02:08.642632+00:00.zip
GRAMMONT_2025-11-18T16:17:08.155540+00:00.zip
GRAMMONT_2025-11-18T16:32:08.782723+00:00.zip
GRAMMONT_2025-11-18T16:47:09.340365+00:00.zip
GRAMMONT_2025-11-18T17:02:04.224910+00:00.zip
GRAMMONT_2025-11-18T17:17:06.866509+00:00.zip
GRAMMONT_2025-11-18T18:32:07.533041+00:00.zip
GRAMMONT_2025-11-24T00:02:17.220289+00:00.zip
GRAMMONT_2025-11-24T01:17:06.672762+00:00.zip
GRAMMONT_2025-11-24T05:32:04.261510+00:00.zip
GRAMMONT_2025-11-24T05:47:09.391030+00:00.zip
GRAMMONT_2025-11-24T06:02:08.824582+00:00.zip
GRAMMONT_2025-11-24T06:17:07.446905+00:00.zip
GRAMMONT_2025-11-24T06:32:00.494092+00:00.zip
GRAMMONT_2025-11-24T06:47:07.344360+00:00.zip
GRAMMONT_2025-11-24T07:02:04.529073+00:00.zip
GRAMMONT_2025-11-24T07:17:05.544853+00:00.zip
GRAMMONT_2025-11-24T07:32:01.891046+00:00.zip
GRAMMONT_2025-11-24T07:47:00.405018+00:00.zip
GRAMMONT_2025-11-24T10:01:59.266718+00:00.zip
GRAMMONT_2025-11-24T14:47:01.734141+00:00.zip
GRAMMONT_2025-11-24T15:02:01.452351+00:00.zip
GRAMMONT_2025-11-24T15:17:01.907373+00:00.zip
GRAMMONT_2025-11-24T15:32:04.746603+00:00.zip
GRAMMONT_2025-11-24T15:47:05.257663+00:00.zip
GRAMMONT_2025-11-24T16:02:03.851786+00:00.zip
GRAMMONT_2025-11-24T16:17:08.370715+00:00.zip
GRAMMONT_2025-11-24T16:32:06.312360+00:00.zip
GRAMMONT_2025-11-24T18:02:05.411421+00:00.zip
GRAMMONT_2025-11-24T19:02:04.658144+00:00.zip
GRAMMONT_2025-12-16T00:01:41.461226+00:00.zip
GRAMMONT_2025-12-16T01:16:28.567684+00:00.zip
GRAMMONT_2025-12-16T06:31:29.022557+00:00.zip
GRAMMONT_2025-12-16T06:46:29.500063+00:00.zip
GRAMMONT_2025-12-16T07:01:31.526934+00:00.zip
GRAMMONT_2025-12-16T07:16:30.177120+00:00.zip
GRAMMONT_2025-12-16T07:31:27.010355+00:00.zip
GRAMMONT_2025-12-16T07:46:26.162426+00:00.zip
GRAMMONT_2025-12-16T08:16:25.350003+00:00.zip
GRAMMONT_2025-12-16T10:46:24.286299+00:00.zip
GRAMMONT_2025-12-16T11:16:24.959519+00:00.zip
GRAMMONT_2025-12-16T15:01:24.622331+00:00.zip
GRAMMONT_2025-12-16T15:16:25.305782+00:00.zip
GRAMMONT_2025-12-16T15:31:26.923661+00:00.zip
GRAMMONT_2025-12-16T15:46:30.091662+00:00.zip
GRAMMONT_2025-12-16T16:01:30.164684+00:00.zip
GRAMMONT_2025-12-16T16:16:26.912740+00:00.zip
GRAMMONT_2025-12-16T16:31:27.805273+00:00.zip
GRAMMONT_2025-12-16T16:46:28.631751+00:00.zip
GRAMMONT_2025-12-16T20:16:27.452196+00:00.zip
GRAMMONT_2025-12-16T21:01:28.663624+00:00.zip
GRAMMONT_2025-12-16T23:01:30.036410+00:00.zip
GRAMMONT_2026-01-08T00:01:38.571453+00:00.zip
GRAMMONT_2026-01-08T01:01:25.664393+00:00.zip
GRAMMONT_2026-01-08T02:31:25.943832+00:00.zip
GRAMMONT_2026-01-08T08:16:17.392212+00:00.zip
GRAMMONT_2026-01-08T08:46:16.720207+00:00.zip
GRAMMONT_2026-01-08T09:31:15.801732+00:00.zip
GRAMMONT_2026-01-08T18:16:24.859999+00:00.zip
GRAMMONT_2026-01-08T19:16:25.609118+00:00.zip
GRAMMONT_2026-01-08T20:01:24.809889+00:00.zip
GRAMMONT_2026-01-08T23:31:24.989446+00:00.zip
GRAMMONT_2026-03-02T00:02:00.235433+00:00.zip
GRAMMONT_2026-03-02T01:16:49.553391+00:00.zip
GRAMMONT_2026-03-02T03:16:48.829809+00:00.zip
GRAMMONT_2026-03-02T05:01:49.656932+00:00.zip
GRAMMONT_2026-03-02T05:16:48.603914+00:00.zip
GRAMMONT_2026-03-02T05:31:48.072998+00:00.zip
GRAMMONT_2026-03-02T05:46:48.743481+00:00.zip
GRAMMONT_2026-03-02T06:01:46.583618+00:00.zip
GRAMMONT_2026-03-02T06:16:45.405628+00:00.zip
GRAMMONT_2026-03-02T06:31:43.415579+00:00.zip
GRAMMONT_2026-03-02T06:46:41.697932+00:00.zip
GRAMMONT_2026-03-02T07:01:41.696734+00:00.zip
GRAMMONT_2026-03-02T12:16:42.701851+00:00.zip
GRAMMONT_2026-03-02T15:31:41.098916+00:00.zip
GRAMMONT_2026-03-02T16:01:39.517501+00:00.zip
GRAMMONT_2026-03-02T16:46:42.561834+00:00.zip
GRAMMONT_2026-03-02T17:01:41.958941+00:00.zip
GRAMMONT_2026-03-02T17:16:46.511805+00:00.zip
GRAMMONT_2026-03-02T17:31:48.499316+00:00.zip
GRAMMONT_2026-03-02T17:46:48.055562+00:00.zip
GRAMMONT_2026-03-02T18:01:47.851974+00:00.zip
GRAMMONT_2026-03-02T18:16:48.002798+00:00.zip
GRAMMONT_2026-03-02T18:31:48.973226+00:00.zip
GRAMMONT_2026-03-02T21:16:48.406731+00:00.zip

80
config/annotator_E.txt Normal file
View File

@@ -0,0 +1,80 @@
GRAMMONT_2025-11-25T00:02:17.876729+00:00.zip
GRAMMONT_2025-11-25T01:02:05.646001+00:00.zip
GRAMMONT_2025-11-25T01:17:09.894834+00:00.zip
GRAMMONT_2025-11-25T04:02:04.433980+00:00.zip
GRAMMONT_2025-11-25T05:47:07.666696+00:00.zip
GRAMMONT_2025-11-25T06:02:11.511745+00:00.zip
GRAMMONT_2025-11-25T06:17:04.055068+00:00.zip
GRAMMONT_2025-11-25T06:32:09.079202+00:00.zip
GRAMMONT_2025-11-25T06:46:29.266484+00:00.zip
GRAMMONT_2025-11-25T07:02:05.006324+00:00.zip
GRAMMONT_2025-11-25T07:17:04.431043+00:00.zip
GRAMMONT_2025-11-25T07:32:00.875367+00:00.zip
GRAMMONT_2025-11-25T08:32:01.921526+00:00.zip
GRAMMONT_2025-11-25T14:02:00.055854+00:00.zip
GRAMMONT_2025-11-25T15:02:00.674240+00:00.zip
GRAMMONT_2025-11-25T15:32:04.667759+00:00.zip
GRAMMONT_2025-11-25T15:47:03.201793+00:00.zip
GRAMMONT_2025-11-25T16:02:06.850816+00:00.zip
GRAMMONT_2025-11-25T16:17:04.360406+00:00.zip
GRAMMONT_2025-11-25T16:32:09.562770+00:00.zip
GRAMMONT_2025-11-25T16:47:04.872758+00:00.zip
GRAMMONT_2025-11-25T17:02:08.046657+00:00.zip
GRAMMONT_2025-12-03T09:32:01.515556+00:00.zip
GRAMMONT_2025-12-03T11:17:03.118822+00:00.zip
GRAMMONT_2025-12-03T15:17:03.043807+00:00.zip
GRAMMONT_2025-12-03T15:32:04.085232+00:00.zip
GRAMMONT_2025-12-03T15:47:04.864411+00:00.zip
GRAMMONT_2025-12-03T16:02:09.554901+00:00.zip
GRAMMONT_2025-12-03T16:17:07.984380+00:00.zip
GRAMMONT_2025-12-03T16:32:06.042132+00:00.zip
GRAMMONT_2025-12-03T16:47:05.425453+00:00.zip
GRAMMONT_2025-12-03T17:02:05.867456+00:00.zip
GRAMMONT_2025-12-03T19:17:08.208705+00:00.zip
GRAMMONT_2025-12-03T19:47:07.924056+00:00.zip
GRAMMONT_2025-12-03T20:17:04.802945+00:00.zip
GRAMMONT_2025-12-03T23:17:04.368047+00:00.zip
GRAMMONT_2026-01-09T00:01:37.412545+00:00.zip
GRAMMONT_2026-01-09T05:31:25.188144+00:00.zip
GRAMMONT_2026-01-09T07:31:19.934715+00:00.zip
GRAMMONT_2026-01-09T07:46:17.007279+00:00.zip
GRAMMONT_2026-01-09T08:16:17.338747+00:00.zip
GRAMMONT_2026-01-09T10:31:15.867851+00:00.zip
GRAMMONT_2026-01-09T18:16:25.202124+00:00.zip
GRAMMONT_2026-01-09T19:16:26.688542+00:00.zip
GRAMMONT_2026-01-09T20:01:22.748919+00:00.zip
GRAMMONT_2026-01-09T23:31:26.962588+00:00.zip
GRAMMONT_2026-01-12T01:01:26.080207+00:00.zip
GRAMMONT_2026-01-12T04:01:26.663453+00:00.zip
GRAMMONT_2026-01-12T08:16:16.249789+00:00.zip
GRAMMONT_2026-01-12T09:31:16.196442+00:00.zip
GRAMMONT_2026-01-12T18:16:26.514693+00:00.zip
GRAMMONT_2026-01-12T18:31:25.430227+00:00.zip
GRAMMONT_2026-01-12T19:16:26.610193+00:00.zip
GRAMMONT_2026-01-12T20:01:26.503739+00:00.zip
GRAMMONT_2026-01-12T23:31:26.591525+00:00.zip
GRAMMONT_2026-03-03T00:02:00.115993+00:00.zip
GRAMMONT_2026-03-03T01:16:47.545369+00:00.zip
GRAMMONT_2026-03-03T03:16:47.017876+00:00.zip
GRAMMONT_2026-03-03T05:01:48.172478+00:00.zip
GRAMMONT_2026-03-03T05:16:48.425996+00:00.zip
GRAMMONT_2026-03-03T05:31:47.535047+00:00.zip
GRAMMONT_2026-03-03T05:46:49.230361+00:00.zip
GRAMMONT_2026-03-03T06:01:48.032458+00:00.zip
GRAMMONT_2026-03-03T06:16:43.444057+00:00.zip
GRAMMONT_2026-03-03T06:31:43.572406+00:00.zip
GRAMMONT_2026-03-03T06:46:42.094886+00:00.zip
GRAMMONT_2026-03-03T07:01:40.412911+00:00.zip
GRAMMONT_2026-03-03T10:46:41.016664+00:00.zip
GRAMMONT_2026-03-03T12:16:43.885012+00:00.zip
GRAMMONT_2026-03-03T15:46:40.689643+00:00.zip
GRAMMONT_2026-03-03T16:16:40.142917+00:00.zip
GRAMMONT_2026-03-03T16:46:42.950814+00:00.zip
GRAMMONT_2026-03-03T17:01:42.774269+00:00.zip
GRAMMONT_2026-03-03T17:16:45.482827+00:00.zip
GRAMMONT_2026-03-03T17:31:48.702675+00:00.zip
GRAMMONT_2026-03-03T17:46:48.793191+00:00.zip
GRAMMONT_2026-03-03T18:01:48.301590+00:00.zip
GRAMMONT_2026-03-03T18:16:47.700676+00:00.zip
GRAMMONT_2026-03-03T18:31:48.737829+00:00.zip
GRAMMONT_2026-03-03T21:16:48.709909+00:00.zip

93
config/annotator_F.txt Normal file
View File

@@ -0,0 +1,93 @@
GRAMMONT_2025-11-25T00:02:17.876729+00:00.zip
GRAMMONT_2025-11-25T01:02:05.646001+00:00.zip
GRAMMONT_2025-11-25T01:17:09.894834+00:00.zip
GRAMMONT_2025-11-25T04:02:04.433980+00:00.zip
GRAMMONT_2025-11-25T05:47:07.666696+00:00.zip
GRAMMONT_2025-11-25T06:02:11.511745+00:00.zip
GRAMMONT_2025-11-25T06:17:04.055068+00:00.zip
GRAMMONT_2025-11-25T06:32:09.079202+00:00.zip
GRAMMONT_2025-11-25T06:46:29.266484+00:00.zip
GRAMMONT_2025-11-25T07:02:05.006324+00:00.zip
GRAMMONT_2025-11-25T07:17:04.431043+00:00.zip
GRAMMONT_2025-11-25T07:32:00.875367+00:00.zip
GRAMMONT_2025-11-25T08:32:01.921526+00:00.zip
GRAMMONT_2025-11-25T14:02:00.055854+00:00.zip
GRAMMONT_2025-11-25T15:02:00.674240+00:00.zip
GRAMMONT_2025-11-25T15:32:04.667759+00:00.zip
GRAMMONT_2025-11-25T15:47:03.201793+00:00.zip
GRAMMONT_2025-11-25T16:02:06.850816+00:00.zip
GRAMMONT_2025-11-25T16:17:04.360406+00:00.zip
GRAMMONT_2025-11-25T16:32:09.562770+00:00.zip
GRAMMONT_2025-11-25T16:47:04.872758+00:00.zip
GRAMMONT_2025-11-25T17:02:08.046657+00:00.zip
GRAMMONT_2025-12-16T00:01:41.461226+00:00.zip
GRAMMONT_2025-12-16T01:16:28.567684+00:00.zip
GRAMMONT_2025-12-16T06:31:29.022557+00:00.zip
GRAMMONT_2025-12-16T06:46:29.500063+00:00.zip
GRAMMONT_2025-12-16T07:01:31.526934+00:00.zip
GRAMMONT_2025-12-16T07:16:30.177120+00:00.zip
GRAMMONT_2025-12-16T07:31:27.010355+00:00.zip
GRAMMONT_2025-12-16T07:46:26.162426+00:00.zip
GRAMMONT_2025-12-16T08:16:25.350003+00:00.zip
GRAMMONT_2025-12-16T10:46:24.286299+00:00.zip
GRAMMONT_2025-12-16T11:16:24.959519+00:00.zip
GRAMMONT_2025-12-16T15:01:24.622331+00:00.zip
GRAMMONT_2025-12-16T15:16:25.305782+00:00.zip
GRAMMONT_2025-12-16T15:31:26.923661+00:00.zip
GRAMMONT_2025-12-16T15:46:30.091662+00:00.zip
GRAMMONT_2025-12-16T16:01:30.164684+00:00.zip
GRAMMONT_2025-12-16T16:16:26.912740+00:00.zip
GRAMMONT_2025-12-16T16:31:27.805273+00:00.zip
GRAMMONT_2025-12-16T16:46:28.631751+00:00.zip
GRAMMONT_2025-12-16T20:16:27.452196+00:00.zip
GRAMMONT_2025-12-16T21:01:28.663624+00:00.zip
GRAMMONT_2025-12-16T23:01:30.036410+00:00.zip
GRAMMONT_2026-01-10T00:01:36.641915+00:00.zip
GRAMMONT_2026-01-10T01:01:28.284170+00:00.zip
GRAMMONT_2026-01-10T04:31:29.222060+00:00.zip
GRAMMONT_2026-01-10T08:16:15.986472+00:00.zip
GRAMMONT_2026-01-10T09:31:17.407612+00:00.zip
GRAMMONT_2026-01-10T18:16:28.897049+00:00.zip
GRAMMONT_2026-01-10T19:16:30.647866+00:00.zip
GRAMMONT_2026-01-10T20:01:30.348871+00:00.zip
GRAMMONT_2026-01-10T21:31:29.784010+00:00.zip
GRAMMONT_2026-01-10T23:31:28.564388+00:00.zip
GRAMMONT_2026-01-13T00:01:38.910412+00:00.zip
GRAMMONT_2026-01-13T01:16:26.259757+00:00.zip
GRAMMONT_2026-01-13T13:31:19.055589+00:00.zip
GRAMMONT_2026-01-13T15:01:18.349499+00:00.zip
GRAMMONT_2026-01-13T15:46:18.863149+00:00.zip
GRAMMONT_2026-01-13T16:01:19.328714+00:00.zip
GRAMMONT_2026-01-13T16:16:24.076563+00:00.zip
GRAMMONT_2026-01-13T16:31:26.963254+00:00.zip
GRAMMONT_2026-01-13T16:46:27.052033+00:00.zip
GRAMMONT_2026-01-13T17:01:27.425383+00:00.zip
GRAMMONT_2026-01-13T17:16:27.033257+00:00.zip
GRAMMONT_2026-01-13T17:31:26.566384+00:00.zip
GRAMMONT_2026-01-13T20:16:26.283507+00:00.zip
GRAMMONT_2026-01-13T23:16:26.953217+00:00.zip
GRAMMONT_2026-03-11T00:02:00.572803+00:00.zip
GRAMMONT_2026-03-11T01:16:49.764464+00:00.zip
GRAMMONT_2026-03-11T03:16:48.638128+00:00.zip
GRAMMONT_2026-03-11T04:46:48.636321+00:00.zip
GRAMMONT_2026-03-11T05:01:50.264589+00:00.zip
GRAMMONT_2026-03-11T05:16:49.061370+00:00.zip
GRAMMONT_2026-03-11T05:31:50.354757+00:00.zip
GRAMMONT_2026-03-11T05:46:47.683406+00:00.zip
GRAMMONT_2026-03-11T06:01:45.221459+00:00.zip
GRAMMONT_2026-03-11T06:16:44.072001+00:00.zip
GRAMMONT_2026-03-11T06:31:42.765049+00:00.zip
GRAMMONT_2026-03-11T06:46:42.012177+00:00.zip
GRAMMONT_2026-03-11T11:16:40.882550+00:00.zip
GRAMMONT_2026-03-11T12:01:41.385581+00:00.zip
GRAMMONT_2026-03-11T14:16:41.864888+00:00.zip
GRAMMONT_2026-03-11T15:46:40.760608+00:00.zip
GRAMMONT_2026-03-11T16:46:44.235006+00:00.zip
GRAMMONT_2026-03-11T17:01:44.107636+00:00.zip
GRAMMONT_2026-03-11T17:16:47.593339+00:00.zip
GRAMMONT_2026-03-11T17:31:49.616627+00:00.zip
GRAMMONT_2026-03-11T17:46:49.957863+00:00.zip
GRAMMONT_2026-03-11T18:01:48.458727+00:00.zip
GRAMMONT_2026-03-11T18:16:49.530207+00:00.zip
GRAMMONT_2026-03-11T18:31:48.654295+00:00.zip
GRAMMONT_2026-03-11T23:01:49.104435+00:00.zip

110
config/annotator_G.txt Normal file
View File

@@ -0,0 +1,110 @@
GRAMMONT_2025-11-22T00:02:20.203920+00:00.zip
GRAMMONT_2025-11-22T01:02:09.028313+00:00.zip
GRAMMONT_2025-11-22T01:17:07.434190+00:00.zip
GRAMMONT_2025-11-22T04:02:07.766345+00:00.zip
GRAMMONT_2025-11-22T05:32:06.253178+00:00.zip
GRAMMONT_2025-11-22T05:47:05.799143+00:00.zip
GRAMMONT_2025-11-22T06:02:04.909260+00:00.zip
GRAMMONT_2025-11-22T06:17:06.901461+00:00.zip
GRAMMONT_2025-11-22T06:31:15.263893+00:00.zip
GRAMMONT_2025-11-22T06:47:02.531210+00:00.zip
GRAMMONT_2025-11-22T07:02:01.143506+00:00.zip
GRAMMONT_2025-11-22T07:16:59.906656+00:00.zip
GRAMMONT_2025-11-22T10:47:02.705611+00:00.zip
GRAMMONT_2025-11-22T14:47:00.096714+00:00.zip
GRAMMONT_2025-11-22T15:32:01.015469+00:00.zip
GRAMMONT_2025-11-22T15:47:02.337459+00:00.zip
GRAMMONT_2025-11-22T16:02:04.420357+00:00.zip
GRAMMONT_2025-11-22T16:17:04.468696+00:00.zip
GRAMMONT_2025-11-22T16:32:07.616206+00:00.zip
GRAMMONT_2025-11-22T16:47:04.224377+00:00.zip
GRAMMONT_2025-11-22T17:02:08.264697+00:00.zip
GRAMMONT_2025-11-22T17:17:06.243128+00:00.zip
GRAMMONT_2025-11-22T18:32:05.405485+00:00.zip
GRAMMONT_2025-12-05T00:02:14.666551+00:00.zip
GRAMMONT_2025-12-05T01:02:05.794759+00:00.zip
GRAMMONT_2025-12-05T01:17:05.892742+00:00.zip
GRAMMONT_2025-12-05T04:02:05.727427+00:00.zip
GRAMMONT_2025-12-05T06:17:08.164092+00:00.zip
GRAMMONT_2025-12-05T06:32:04.336644+00:00.zip
GRAMMONT_2025-12-05T06:47:08.004420+00:00.zip
GRAMMONT_2025-12-05T07:02:05.120746+00:00.zip
GRAMMONT_2025-12-05T07:17:05.647473+00:00.zip
GRAMMONT_2025-12-05T07:32:01.392138+00:00.zip
GRAMMONT_2025-12-05T07:47:01.666820+00:00.zip
GRAMMONT_2025-12-05T08:02:03.470785+00:00.zip
GRAMMONT_2025-12-05T08:31:58.765660+00:00.zip
GRAMMONT_2025-12-05T10:17:01.321795+00:00.zip
GRAMMONT_2025-12-05T12:01:57.048658+00:00.zip
GRAMMONT_2025-12-05T15:17:05.187833+00:00.zip
GRAMMONT_2025-12-05T15:32:03.494960+00:00.zip
GRAMMONT_2025-12-05T15:47:03.966183+00:00.zip
GRAMMONT_2025-12-05T16:02:08.393449+00:00.zip
GRAMMONT_2025-12-05T16:17:04.961643+00:00.zip
GRAMMONT_2025-12-05T16:32:07.906681+00:00.zip
GRAMMONT_2025-12-05T16:47:06.887564+00:00.zip
GRAMMONT_2025-12-05T17:02:06.990948+00:00.zip
GRAMMONT_2025-12-05T18:02:04.071706+00:00.zip
GRAMMONT_2025-12-05T18:32:05.013636+00:00.zip
GRAMMONT_2026-01-12T01:01:26.080207+00:00.zip
GRAMMONT_2026-01-12T04:01:26.663453+00:00.zip
GRAMMONT_2026-01-12T08:16:16.249789+00:00.zip
GRAMMONT_2026-01-12T09:31:16.196442+00:00.zip
GRAMMONT_2026-01-12T18:16:26.514693+00:00.zip
GRAMMONT_2026-01-12T18:31:25.430227+00:00.zip
GRAMMONT_2026-01-12T19:16:26.610193+00:00.zip
GRAMMONT_2026-01-12T20:01:26.503739+00:00.zip
GRAMMONT_2026-01-12T23:31:26.591525+00:00.zip
GRAMMONT_2026-02-11T00:01:38.048234+00:00.zip
GRAMMONT_2026-02-11T01:01:25.496751+00:00.zip
GRAMMONT_2026-02-11T04:31:25.749592+00:00.zip
GRAMMONT_2026-02-11T04:46:26.124855+00:00.zip
GRAMMONT_2026-02-11T05:31:27.076000+00:00.zip
GRAMMONT_2026-02-11T05:46:26.426312+00:00.zip
GRAMMONT_2026-02-11T06:01:26.452689+00:00.zip
GRAMMONT_2026-02-11T06:16:26.767560+00:00.zip
GRAMMONT_2026-02-11T06:31:24.624797+00:00.zip
GRAMMONT_2026-02-11T06:46:23.455019+00:00.zip
GRAMMONT_2026-02-11T07:01:21.080045+00:00.zip
GRAMMONT_2026-02-11T07:16:20.533988+00:00.zip
GRAMMONT_2026-02-11T07:31:19.697932+00:00.zip
GRAMMONT_2026-02-11T08:01:19.085138+00:00.zip
GRAMMONT_2026-02-11T09:16:17.805079+00:00.zip
GRAMMONT_2026-02-11T09:31:17.747859+00:00.zip
GRAMMONT_2026-02-11T12:31:20.771768+00:00.zip
GRAMMONT_2026-02-11T15:46:19.819366+00:00.zip
GRAMMONT_2026-02-11T16:31:20.659090+00:00.zip
GRAMMONT_2026-02-11T16:46:19.780502+00:00.zip
GRAMMONT_2026-02-11T17:01:25.098410+00:00.zip
GRAMMONT_2026-02-11T17:16:25.979320+00:00.zip
GRAMMONT_2026-02-11T17:31:27.522773+00:00.zip
GRAMMONT_2026-02-11T17:46:26.400124+00:00.zip
GRAMMONT_2026-02-11T18:01:25.365899+00:00.zip
GRAMMONT_2026-02-11T18:16:25.442042+00:00.zip
GRAMMONT_2026-02-11T21:01:25.956306+00:00.zip
GRAMMONT_2026-02-11T21:31:26.462852+00:00.zip
GRAMMONT_2026-02-11T22:31:26.629631+00:00.zip
GRAMMONT_2026-03-12T00:02:00.756259+00:00.zip
GRAMMONT_2026-03-12T01:16:49.289447+00:00.zip
GRAMMONT_2026-03-12T05:01:48.035876+00:00.zip
GRAMMONT_2026-03-12T05:16:48.883482+00:00.zip
GRAMMONT_2026-03-12T05:31:49.793476+00:00.zip
GRAMMONT_2026-03-12T05:46:49.641480+00:00.zip
GRAMMONT_2026-03-12T06:01:46.587244+00:00.zip
GRAMMONT_2026-03-12T06:16:45.028717+00:00.zip
GRAMMONT_2026-03-12T06:31:43.245102+00:00.zip
GRAMMONT_2026-03-12T06:46:42.052574+00:00.zip
GRAMMONT_2026-03-12T10:46:43.935297+00:00.zip
GRAMMONT_2026-03-12T12:31:42.025147+00:00.zip
GRAMMONT_2026-03-12T15:46:41.830340+00:00.zip
GRAMMONT_2026-03-12T16:16:40.453007+00:00.zip
GRAMMONT_2026-03-12T16:46:42.917700+00:00.zip
GRAMMONT_2026-03-12T17:01:43.627300+00:00.zip
GRAMMONT_2026-03-12T17:16:45.055691+00:00.zip
GRAMMONT_2026-03-12T17:31:48.034432+00:00.zip
GRAMMONT_2026-03-12T17:46:31.239038+00:00.zip
GRAMMONT_2026-03-12T18:01:49.569079+00:00.zip
GRAMMONT_2026-03-12T18:16:47.537836+00:00.zip
GRAMMONT_2026-03-12T18:31:49.125861+00:00.zip
GRAMMONT_2026-03-12T18:46:48.535395+00:00.zip
GRAMMONT_2026-03-12T22:01:49.336660+00:00.zip

10
config/clips.example.txt Normal file
View File

@@ -0,0 +1,10 @@
# List the clip filenames (without path) to annotate, one per line.
# Lines starting with # are ignored. Order is preserved.
GRAMMONT_2025-11-17T11:31:38.546953+00:00.zip
GRAMMONT_2025-11-17T12:31:39.650554+00:00.zip
GRAMMONT_2025-11-17T15:32:07.184007+00:00.zip
GRAMMONT_2025-11-17T15:32:07.184007+00:00.zip
GRAMMONT_2025-11-17T15:47:10.070449+00:00.zip
GRAMMONT_2025-11-22T10:47:02.705611+00:00.zip
GRAMMONT_2025-11-22T14:47:00.096714+00:00.zip
GRAMMONT_2025-11-22T15:32:01.015469+00:00.zip

View File

@@ -0,0 +1,21 @@
storage: local # 'local' or 's3'
# Required: set these to your actual paths (local path or bucket/prefix for S3)
data_dir: # e.g. /data/clips or for S3: hydroscan-data/GRAMMONT/clips
out_dir: # e.g. /data/out or for S3: hydroscan-data/annotations/<name>/ # Put your name here
# For S3 credentials, copy .env.example to .env and fill in:
# S3_ACCESS_KEY, S3_SECRET_ACCESS_KEY, S3_ENDPOINT_URL
clips_file: config/clips.txt
optical_flow_config_file: config/optical_flow_config.yaml
questions_config_file: config/questions.yaml
display_max: 720
fps_fallback: 25
max_frames: 100
# Input filenames (override if your ZIP archives differ)
filenames:
video_in_zip: left.mp4
video_tmp_suffix: .mp4
zip_extension: .zip

View File

@@ -0,0 +1,4 @@
enabled: true
norm_squared_threshold: 0.06
gaussian_kernel: [5, 5]
brightness_range: [2, 253]

34
config/questions.yaml Normal file
View File

@@ -0,0 +1,34 @@
- section: River
items:
- key: flow
label: Flow Regime
options: [Turbulent, Laminar, Uncertain]
default: Laminar
- key: shadows
label: Strong Shadows
options: [Yes, No, Uncertain]
default: No
- key: artifacts
label: Artifacts on River
options: [Yes, No, Uncertain]
default: No
- section: Scene
items:
- key: lighting
label: Lighting
options: [Day, Night, Uncertain]
default: Day
- key: exposure
label: Exposure
options: [Overexposed, Underexposed, Both, Normal, Uncertain]
default: Normal
- section: Weather
items:
- key: snowing
label: Snowing
options: [Yes, No, Uncertain]
default: No
- key: snow_on_ground
label: Snow on Ground
options: [Yes, No, Uncertain]
default: No

View File

View File

View File

@@ -1,100 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "fe0521db",
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"import json\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from PIL import Image\n",
"from IPython.display import display, Image as IPImage"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c6d7ebbf",
"metadata": {},
"outputs": [],
"source": [
"out_dir = Path(\"../data/annotation_results/\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "75efc15a",
"metadata": {},
"outputs": [],
"source": [
"def show_result(folder):\n",
" folder = Path(folder)\n",
"\n",
" with open(folder / \"metadata.json\") as f:\n",
" metadata = json.load(f)\n",
"\n",
" frame = np.array(Image.open(folder / \"frame.png\"))\n",
" mask = np.array(Image.open(folder / \"mask_vis.png\"))\n",
" overlay = np.array(Image.open(folder / \"overlay.png\"))\n",
"\n",
" title = \" | \".join(f\"{k}: {v}\" for k, v in metadata.items())\n",
" fig, axs = plt.subplots(1, 3, figsize=(15, 5))\n",
" axs[0].imshow(frame)\n",
" axs[0].set_title(\"Frame\")\n",
" axs[0].axis(\"off\")\n",
" axs[1].imshow(mask, cmap=\"gray\")\n",
" axs[1].set_title(\"Mask\")\n",
" axs[1].axis(\"off\")\n",
" axs[2].imshow(overlay)\n",
" axs[2].set_title(\"Overlay\")\n",
" axs[2].axis(\"off\")\n",
" plt.suptitle(f\"{folder.name}\\n{title}\", fontsize=9)\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
" for gif_name in [\"video_original_lowres.gif\", \"video_overlay_lowres.gif\"]:\n",
" gif_path = folder / gif_name\n",
" if gif_path.exists():\n",
" display(IPImage(filename=str(gif_path)))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "02ff1ae9",
"metadata": {},
"outputs": [],
"source": [
"for folder in sorted(out_dir.iterdir()):\n",
" if folder.is_dir() and (folder / \"metadata.json\").exists():\n",
" show_result(folder)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "river-annotation-tool",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -3,7 +3,7 @@ requires = ["setuptools>=61.0.0"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "river_annotation_tool" name = "clip_annotator"
authors = [ authors = [
# TODO configure authors # TODO configure authors
# { name = "Jane Smith", email = "jane.smith@example.com" }, # { name = "Jane Smith", email = "jane.smith@example.com" },
@@ -19,6 +19,9 @@ dependencies = [
"matplotlib-inline>=0.2.1", "matplotlib-inline>=0.2.1",
"pillow>=12.2.0", "pillow>=12.2.0",
"pyside6>=6.11.0", "pyside6>=6.11.0",
"python-dotenv>=1.0",
"pyyaml>=6.0",
"s3fs>=2024.0",
] ]
dynamic = ["version"] dynamic = ["version"]
@@ -30,7 +33,7 @@ dev = [
] ]
[tool.setuptools.dynamic] [tool.setuptools.dynamic]
version = {attr = "river_annotation_tool.__version__"} version = {attr = "clip_annotator.__version__"}
[tool.ruff] [tool.ruff]
target-version = "py312" target-version = "py312"

View File

@@ -1 +1,971 @@
# This file will be autogenerated from pyproject.toml # This file was autogenerated by uv via the following command:
# uv export --frozen --output-file=requirements.txt
-e .
aiobotocore==3.7.0 \
--hash=sha256:680bde7c64679a821a9312641b759d9497f790ba8b2e88c6959e6273ee765b8e \
--hash=sha256:c64d871ed5491a6571948dd48eabd185b46c6c23b64e3afd0c059fc7593ada30
# via s3fs
aiohappyeyeballs==2.6.1 \
--hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \
--hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8
# via aiohttp
aiohttp==3.13.5 \
--hash=sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9 \
--hash=sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b \
--hash=sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1 \
--hash=sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416 \
--hash=sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe \
--hash=sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9 \
--hash=sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286 \
--hash=sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9 \
--hash=sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88 \
--hash=sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14 \
--hash=sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3 \
--hash=sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1 \
--hash=sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4 \
--hash=sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2 \
--hash=sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1 \
--hash=sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e \
--hash=sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5 \
--hash=sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3
# via
# aiobotocore
# s3fs
aioitertools==0.13.0 \
--hash=sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be \
--hash=sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c
# via aiobotocore
aiosignal==1.4.0 \
--hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \
--hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7
# via aiohttp
anyio==4.13.0 \
--hash=sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708 \
--hash=sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc
# via
# httpx
# jupyter-server
appnope==0.1.4 ; sys_platform == 'darwin' \
--hash=sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee \
--hash=sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c
# via ipykernel
argon2-cffi==25.1.0 \
--hash=sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1 \
--hash=sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741
# via jupyter-server
argon2-cffi-bindings==25.1.0 \
--hash=sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99 \
--hash=sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6 \
--hash=sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44 \
--hash=sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2 \
--hash=sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0 \
--hash=sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98 \
--hash=sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500 \
--hash=sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94 \
--hash=sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d \
--hash=sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d \
--hash=sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a
# via argon2-cffi
arrow==1.4.0 \
--hash=sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205 \
--hash=sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7
# via isoduration
asttokens==3.0.1 \
--hash=sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a \
--hash=sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7
# via stack-data
async-lru==2.3.0 \
--hash=sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6 \
--hash=sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315
# via jupyterlab
attrs==26.1.0 \
--hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \
--hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32
# via
# aiohttp
# jsonschema
# referencing
babel==2.18.0 \
--hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \
--hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35
# via jupyterlab-server
beautifulsoup4==4.14.3 \
--hash=sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb \
--hash=sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86
# via nbconvert
bleach==6.3.0 \
--hash=sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22 \
--hash=sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6
# via nbconvert
botocore==1.43.0 \
--hash=sha256:cc5b15eaec3c6eac05d8012cb5ef17ebe891beb88a16ca13c374bfaece1241e6 \
--hash=sha256:e933b31a2d644253e1d029d7d39e99ba41b87e29300534f189744cc438cdf928
# via aiobotocore
certifi==2026.4.22 \
--hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \
--hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580
# via
# httpcore
# httpx
# requests
cffi==2.0.0 \
--hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \
--hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \
--hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \
--hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \
--hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \
--hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \
--hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \
--hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \
--hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \
--hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \
--hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \
--hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \
--hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18
# via
# argon2-cffi-bindings
# pyzmq
cfgv==3.5.0 \
--hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \
--hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132
# via pre-commit
charset-normalizer==3.4.7 \
--hash=sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5 \
--hash=sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb \
--hash=sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c \
--hash=sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1 \
--hash=sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d \
--hash=sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d \
--hash=sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116 \
--hash=sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d \
--hash=sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6 \
--hash=sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2 \
--hash=sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15 \
--hash=sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5 \
--hash=sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7 \
--hash=sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49 \
--hash=sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b \
--hash=sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46 \
--hash=sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a \
--hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464
# via requests
colorama==0.4.6 ; sys_platform == 'win32' \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
# via ipython
comm==0.2.3 \
--hash=sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971 \
--hash=sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417
# via ipykernel
contourpy==1.3.3 \
--hash=sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69 \
--hash=sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc \
--hash=sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880 \
--hash=sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7 \
--hash=sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411 \
--hash=sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1 \
--hash=sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6 \
--hash=sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea \
--hash=sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b \
--hash=sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7 \
--hash=sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb \
--hash=sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8
# via matplotlib
cycler==0.12.1 \
--hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \
--hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c
# via matplotlib
debugpy==1.8.20 \
--hash=sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390 \
--hash=sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d \
--hash=sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33 \
--hash=sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7 \
--hash=sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b \
--hash=sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3
# via ipykernel
decorator==5.2.1 \
--hash=sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360 \
--hash=sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a
# via ipython
defusedxml==0.7.1 \
--hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \
--hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61
# via nbconvert
distlib==0.4.0 \
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
# via virtualenv
executing==2.2.1 \
--hash=sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4 \
--hash=sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017
# via stack-data
fastjsonschema==2.21.2 \
--hash=sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463 \
--hash=sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de
# via nbformat
filelock==3.29.0 \
--hash=sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90 \
--hash=sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258
# via
# python-discovery
# virtualenv
fonttools==4.62.1 \
--hash=sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04 \
--hash=sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9 \
--hash=sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392 \
--hash=sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d \
--hash=sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd \
--hash=sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974 \
--hash=sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936 \
--hash=sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42 \
--hash=sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c \
--hash=sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d
# via matplotlib
fqdn==1.5.1 \
--hash=sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f \
--hash=sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014
# via jsonschema
frozenlist==1.8.0 \
--hash=sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d \
--hash=sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b \
--hash=sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143 \
--hash=sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746 \
--hash=sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8 \
--hash=sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad \
--hash=sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29 \
--hash=sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf \
--hash=sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383 \
--hash=sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608 \
--hash=sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa \
--hash=sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1 \
--hash=sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3 \
--hash=sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4 \
--hash=sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b \
--hash=sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52 \
--hash=sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4 \
--hash=sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd
# via
# aiohttp
# aiosignal
fsspec==2026.4.0 \
--hash=sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2 \
--hash=sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4
# via s3fs
h11==0.16.0 \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
# via httpcore
httpcore==1.0.9 \
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
# via httpx
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via jupyterlab
identify==2.6.19 \
--hash=sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a \
--hash=sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842
# via pre-commit
idna==3.13 \
--hash=sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242 \
--hash=sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3
# via
# anyio
# httpx
# jsonschema
# requests
# yarl
ipykernel==7.2.0 \
--hash=sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e \
--hash=sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661
# via jupyterlab
ipython==9.13.0 \
--hash=sha256:57f9d4639e20818d328d287c7b549af3d05f12486ea8f2e7f73e52a36ec4d201 \
--hash=sha256:7e834b6afc99f020e3f05966ced34792f40267d64cb1ea9043886dab0dde5967
# via ipykernel
ipython-pygments-lexers==1.1.1 \
--hash=sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81 \
--hash=sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c
# via ipython
isoduration==20.11.0 \
--hash=sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9 \
--hash=sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042
# via jsonschema
jedi==0.19.2 \
--hash=sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0 \
--hash=sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9
# via ipython
jinja2==3.1.6 \
--hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
--hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
# via
# jupyter-server
# jupyterlab
# jupyterlab-server
# nbconvert
jmespath==1.1.0 \
--hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \
--hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64
# via
# aiobotocore
# botocore
json5==0.14.0 \
--hash=sha256:56cf861bab076b1178eb8c92e1311d273a9b9acea2ccc82c276abf839ebaef3a \
--hash=sha256:b3f492fad9f6cdbced8b7d40b28b9b1c9701c5f561bef0d33b81c2ff433fefcb
# via jupyterlab-server
jsonpointer==3.1.1 \
--hash=sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900 \
--hash=sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca
# via jsonschema
jsonschema==4.26.0 \
--hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \
--hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce
# via
# jupyter-events
# jupyterlab-server
# nbformat
jsonschema-specifications==2025.9.1 \
--hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \
--hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d
# via jsonschema
jupyter-client==8.8.0 \
--hash=sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e \
--hash=sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a
# via
# ipykernel
# jupyter-server
# nbclient
jupyter-core==5.9.1 \
--hash=sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508 \
--hash=sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407
# via
# ipykernel
# jupyter-client
# jupyter-server
# jupyterlab
# nbclient
# nbconvert
# nbformat
jupyter-events==0.12.1 \
--hash=sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf \
--hash=sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3
# via jupyter-server
jupyter-lsp==2.3.1 \
--hash=sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81 \
--hash=sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6
# via jupyterlab
jupyter-server==2.17.0 \
--hash=sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5 \
--hash=sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f
# via
# jupyter-lsp
# jupyterlab
# jupyterlab-server
# notebook
# notebook-shim
jupyter-server-terminals==0.5.4 \
--hash=sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14 \
--hash=sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5
# via jupyter-server
jupyterlab==4.5.7 \
--hash=sha256:55a9822c4754da305f41e113452c68383e214dcf96de760146af89ce5d5117b0 \
--hash=sha256:fba4cb0e2c44a52859669d8c98b45de029d5e515f8407bf8534d2a8fc5f0964d
# via notebook
jupyterlab-pygments==0.3.0 \
--hash=sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d \
--hash=sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780
# via nbconvert
jupyterlab-server==2.28.0 \
--hash=sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c \
--hash=sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968
# via
# jupyterlab
# notebook
kiwisolver==1.5.0 \
--hash=sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15 \
--hash=sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9 \
--hash=sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57 \
--hash=sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9 \
--hash=sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314 \
--hash=sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797 \
--hash=sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083 \
--hash=sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588 \
--hash=sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1 \
--hash=sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d \
--hash=sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf \
--hash=sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f \
--hash=sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7 \
--hash=sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6 \
--hash=sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a \
--hash=sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0 \
--hash=sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819 \
--hash=sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384 \
--hash=sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203 \
--hash=sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7
# via matplotlib
lark==1.3.1 \
--hash=sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905 \
--hash=sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12
# via rfc3987-syntax
markupsafe==3.0.3 \
--hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \
--hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \
--hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \
--hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \
--hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \
--hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \
--hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \
--hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \
--hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \
--hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \
--hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \
--hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d
# via
# jinja2
# nbconvert
matplotlib==3.10.9 \
--hash=sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42 \
--hash=sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320 \
--hash=sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2 \
--hash=sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285 \
--hash=sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6 \
--hash=sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf \
--hash=sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1 \
--hash=sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358
# via clip-annotator
matplotlib-inline==0.2.1 \
--hash=sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76 \
--hash=sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe
# via
# clip-annotator
# ipykernel
# ipython
mistune==3.2.0 \
--hash=sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a \
--hash=sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1
# via nbconvert
multidict==6.7.1 \
--hash=sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511 \
--hash=sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582 \
--hash=sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e \
--hash=sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a \
--hash=sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd \
--hash=sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56 \
--hash=sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733 \
--hash=sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6 \
--hash=sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172 \
--hash=sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7 \
--hash=sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf \
--hash=sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961 \
--hash=sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3 \
--hash=sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b \
--hash=sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53 \
--hash=sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a \
--hash=sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75 \
--hash=sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d \
--hash=sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba \
--hash=sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19
# via
# aiobotocore
# aiohttp
# yarl
nbclient==0.10.4 \
--hash=sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9 \
--hash=sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440
# via nbconvert
nbconvert==7.17.1 \
--hash=sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2 \
--hash=sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8
# via jupyter-server
nbformat==5.10.4 \
--hash=sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a \
--hash=sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b
# via
# jupyter-server
# nbclient
# nbconvert
nest-asyncio==1.6.0 \
--hash=sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe \
--hash=sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c
# via ipykernel
nodeenv==1.10.0 \
--hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \
--hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb
# via pre-commit
notebook==7.5.6 \
--hash=sha256:4dde3f8fb55fa8fb7946d58c6e869ce9baf46d00fc070664f62604569d0faca0 \
--hash=sha256:621174aade80108f0020b0f00738000b215f75fa3cd90771ad7aa0f24536a4e1
notebook-shim==0.2.4 \
--hash=sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef \
--hash=sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb
# via
# jupyterlab
# notebook
numpy==2.2.6 \
--hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \
--hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \
--hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \
--hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \
--hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \
--hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \
--hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \
--hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \
--hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \
--hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \
--hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de
# via
# contourpy
# matplotlib
# opencv-contrib-python-headless
# pandas
opencv-contrib-python-headless==4.12.0.88 \
--hash=sha256:3d8a7b23a5faba4ad34e13f51668c56be791e57ab02d68d9016200fed3c12c77 \
--hash=sha256:85b520e527052a85a682f09cdc12e5f156f56d8c277261b4b65b48431abae96f \
--hash=sha256:902888b4e1b4826c721840d9107e91d32f146a2c3bc8cb728f0088bf44204e4b \
--hash=sha256:a17ebb914f309afe72447c33b9187ff02f23f1483faa5c0ffde7aadc88711e2a \
--hash=sha256:b183e2322468c9d3bd9cac4ba44b272d828ec22842395bcfa51df31765224c0a \
--hash=sha256:c57e32812fea2a542bb220088fb3ce8a210fe114c9454d1c9e8cd162e1a1fde8 \
--hash=sha256:d60a12b915c55a50468c013fcd839e941b49ccc1f37b914b62543382c36bf81d
# via clip-annotator
packaging==26.2 \
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
--hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
# via
# ipykernel
# jupyter-events
# jupyter-server
# jupyterlab
# jupyterlab-server
# matplotlib
# nbconvert
pandas==3.0.2 \
--hash=sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd \
--hash=sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18 \
--hash=sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d \
--hash=sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14 \
--hash=sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4 \
--hash=sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d \
--hash=sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f \
--hash=sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043 \
--hash=sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab
# via clip-annotator
pandocfilters==1.5.1 \
--hash=sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e \
--hash=sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc
# via nbconvert
parso==0.8.6 \
--hash=sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd \
--hash=sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff
# via jedi
pexpect==4.9.0 ; sys_platform != 'emscripten' and sys_platform != 'win32' \
--hash=sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523 \
--hash=sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f
# via ipython
pillow==12.2.0 \
--hash=sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5 \
--hash=sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987 \
--hash=sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5 \
--hash=sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940 \
--hash=sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780 \
--hash=sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005 \
--hash=sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5 \
--hash=sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5 \
--hash=sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414 \
--hash=sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76 \
--hash=sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421 \
--hash=sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5
# via
# clip-annotator
# matplotlib
platformdirs==4.9.6 \
--hash=sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a \
--hash=sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917
# via
# jupyter-core
# python-discovery
# virtualenv
pre-commit==4.6.0 \
--hash=sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9 \
--hash=sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b
prometheus-client==0.25.0 \
--hash=sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28 \
--hash=sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1
# via jupyter-server
prompt-toolkit==3.0.52 \
--hash=sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855 \
--hash=sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955
# via ipython
propcache==0.5.2 \
--hash=sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427 \
--hash=sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42 \
--hash=sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33 \
--hash=sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84 \
--hash=sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64 \
--hash=sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba \
--hash=sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144 \
--hash=sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476 \
--hash=sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a \
--hash=sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba \
--hash=sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9 \
--hash=sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031 \
--hash=sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913 \
--hash=sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe \
--hash=sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1 \
--hash=sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a \
--hash=sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42 \
--hash=sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a \
--hash=sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf
# via
# aiohttp
# yarl
psutil==7.2.2 \
--hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \
--hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \
--hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \
--hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \
--hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \
--hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \
--hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \
--hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \
--hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8
# via
# ipykernel
# ipython
ptyprocess==0.7.0 ; os_name != 'nt' or (sys_platform != 'emscripten' and sys_platform != 'win32') \
--hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \
--hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220
# via
# pexpect
# terminado
pure-eval==0.2.3 \
--hash=sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0 \
--hash=sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42
# via stack-data
pycparser==3.0 ; implementation_name != 'PyPy' \
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
# via cffi
pygments==2.20.0 \
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
# via
# ipython
# ipython-pygments-lexers
# nbconvert
pyparsing==3.3.2 \
--hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \
--hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc
# via matplotlib
pyside6==6.11.0 \
--hash=sha256:1f2735dc4f2bd4ec452ae50502c8a22128bba0aced35358a2bbc58384b820c6f \
--hash=sha256:267b344c73580ac938ca63c611881fb42a3922ebfe043e271005f4f06c372c4e \
--hash=sha256:9092cb002ca43c64006afb2e0d0f6f51aef17aa737c33a45e502326a081ddcbc \
--hash=sha256:b15f39acc2b8f46251a630acad0d97f9a0a0461f2baffcd66d7adfada8eb641e \
--hash=sha256:c642e2d25704ca746fd37f56feacf25c5aecc4cd40bef23d18eec81f87d9dc00
# via clip-annotator
pyside6-addons==6.11.0 \
--hash=sha256:413e6121c24f5ffdce376298059eddecff74aa6d638e94e0f6015b33d29b889e \
--hash=sha256:8ffb40222456078930816ebcac2f2511716d2acbc11716dd5acc5c365179a753 \
--hash=sha256:aaaee83385977a0fe134b2f4fbfb92b45a880d5b656e4d90a708eef10b1b6de8 \
--hash=sha256:ac6fe3d4ef4497dde3efc5e896b0acd53ff6c93be4bf485f045690f919419f35 \
--hash=sha256:d5eaa4643302e3a0fa94c5766234bee4073d7d5ab9c2b7fd222692a176faf182
# via pyside6
pyside6-essentials==6.11.0 \
--hash=sha256:3b3362882ad9389357a80504e600180006a957731fec05786fced7b038461fdf \
--hash=sha256:4854cb0a1b061e7a576d8fb7bb7cf9f49540d558b1acb7df0742a7afefe61e4e \
--hash=sha256:81ca603dbf21bc39f89bb42db215c25ebe0c879a1a4c387625c321d2730ec187 \
--hash=sha256:85d6ca87ef35fa6565d385ede72ae48420dd3f63113929d10fc800f6b0360e01 \
--hash=sha256:dc20e7afd5fc6fe51297db91cef997ce60844be578f7a49fc61b7ab9657a8849
# via
# pyside6
# pyside6-addons
python-dateutil==2.9.0.post0 \
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
# via
# aiobotocore
# arrow
# botocore
# jupyter-client
# matplotlib
# pandas
python-discovery==1.2.2 \
--hash=sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb \
--hash=sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a
# via virtualenv
python-dotenv==1.2.2 \
--hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \
--hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3
# via clip-annotator
python-json-logger==4.1.0 \
--hash=sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2 \
--hash=sha256:b396b9e3ed782b09ff9d6e4f1683d46c83ad0d35d2e407c09a9ebbf038f88195
# via jupyter-events
pywinpty==3.0.3 ; os_name == 'nt' \
--hash=sha256:15e79d870e18b678fb8a5a6105fd38496b55697c66e6fc0378236026bc4d59e9 \
--hash=sha256:523441dc34d231fb361b4b00f8c99d3f16de02f5005fd544a0183112bcc22412 \
--hash=sha256:c9081df0e49ffa86d15db4a6ba61530630e48707f987df42c9d3313537e81fc0
# via
# jupyter-server
# jupyter-server-terminals
# terminado
pyyaml==6.0.3 \
--hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
--hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
--hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
--hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
--hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
--hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
--hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
--hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
--hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
--hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
# via
# clip-annotator
# jupyter-events
# pre-commit
pyzmq==27.1.0 \
--hash=sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28 \
--hash=sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113 \
--hash=sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd \
--hash=sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233 \
--hash=sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31 \
--hash=sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc \
--hash=sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f \
--hash=sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf \
--hash=sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540 \
--hash=sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856 \
--hash=sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496
# via
# ipykernel
# jupyter-client
# jupyter-server
referencing==0.37.0 \
--hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \
--hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8
# via
# jsonschema
# jsonschema-specifications
# jupyter-events
requests==2.33.1 \
--hash=sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517 \
--hash=sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a
# via jupyterlab-server
rfc3339-validator==0.1.4 \
--hash=sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b \
--hash=sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa
# via
# jsonschema
# jupyter-events
rfc3986-validator==0.1.1 \
--hash=sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9 \
--hash=sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055
# via
# jsonschema
# jupyter-events
rfc3987-syntax==1.1.0 \
--hash=sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f \
--hash=sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d
# via jsonschema
rpds-py==0.30.0 \
--hash=sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf \
--hash=sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6 \
--hash=sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23 \
--hash=sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e \
--hash=sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e \
--hash=sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05 \
--hash=sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5 \
--hash=sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394 \
--hash=sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b \
--hash=sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd \
--hash=sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad \
--hash=sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51 \
--hash=sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28 \
--hash=sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1 \
--hash=sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84 \
--hash=sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f
# via
# jsonschema
# referencing
ruff==0.15.0 \
--hash=sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3 \
--hash=sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662 \
--hash=sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621 \
--hash=sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a \
--hash=sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3 \
--hash=sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179 \
--hash=sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d \
--hash=sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16 \
--hash=sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78 \
--hash=sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e \
--hash=sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9 \
--hash=sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455 \
--hash=sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1 \
--hash=sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4 \
--hash=sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a \
--hash=sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce \
--hash=sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d \
--hash=sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18
s3fs==2026.4.0 \
--hash=sha256:5bdce0abb00b0435ee150807a45fea727451dbc22de4cbc116464f8504ab9d37 \
--hash=sha256:de0d2a1f33cdf03831fd2382d278c6e4e31fe57c3bf2f703c61f8aec6b703e2a
# via clip-annotator
send2trash==2.1.0 \
--hash=sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c \
--hash=sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459
# via jupyter-server
setuptools==82.0.1 \
--hash=sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9 \
--hash=sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb
# via jupyterlab
shiboken6==6.11.0 \
--hash=sha256:3bd76cf56105ab2d62ecaff630366f11264f69b88d488f10f048da9a065781f4 \
--hash=sha256:483ff78a73c7b3189ca924abc694318084f078bcfeaffa68e32024ff2d025ee1 \
--hash=sha256:a10dc7718104ea2dc15d5b0b96909b77162ce1c76fcc6968e6df692b947a00e9 \
--hash=sha256:ad54e64f8192ddbdff0c54ac82b89edcd62ed623f502ea21c960541d19514053 \
--hash=sha256:d88e8a1eb705f2b9ad21db08a61ae1dc0c773e5cd86a069de0754c4cf1f9b43b
# via
# pyside6
# pyside6-addons
# pyside6-essentials
six==1.17.0 \
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
# via
# python-dateutil
# rfc3339-validator
soupsieve==2.8.3 \
--hash=sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349 \
--hash=sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95
# via beautifulsoup4
stack-data==0.6.3 \
--hash=sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9 \
--hash=sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695
# via ipython
terminado==0.18.1 \
--hash=sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0 \
--hash=sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e
# via
# jupyter-server
# jupyter-server-terminals
tinycss2==1.4.0 \
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \
--hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289
# via bleach
tornado==6.5.5 \
--hash=sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9 \
--hash=sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6 \
--hash=sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca \
--hash=sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e \
--hash=sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07 \
--hash=sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa \
--hash=sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b \
--hash=sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521 \
--hash=sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7 \
--hash=sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5
# via
# ipykernel
# jupyter-client
# jupyter-server
# jupyterlab
# notebook
# terminado
traitlets==5.14.3 \
--hash=sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7 \
--hash=sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f
# via
# ipykernel
# ipython
# jupyter-client
# jupyter-core
# jupyter-events
# jupyter-server
# jupyterlab
# matplotlib-inline
# nbclient
# nbconvert
# nbformat
typing-extensions==4.15.0 \
--hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
# via
# aiosignal
# anyio
# beautifulsoup4
# referencing
tzdata==2026.2 \
--hash=sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10 \
--hash=sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7
# via
# arrow
# pandas
uri-template==1.3.0 \
--hash=sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7 \
--hash=sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363
# via jsonschema
urllib3==2.6.3 \
--hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \
--hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4
# via
# botocore
# requests
virtualenv==21.3.0 \
--hash=sha256:4d28ee41f6d9ec8f1f00cd472b9ffbcedda1b3d3b9a575b5c94a2d004fd51bd7 \
--hash=sha256:733750db978ec95c2d8eb4feadaa57091002bce404cb39ba69899cf7bd28944e
# via pre-commit
wcwidth==0.6.0 \
--hash=sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad \
--hash=sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159
# via prompt-toolkit
webcolors==25.10.0 \
--hash=sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d \
--hash=sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf
# via jsonschema
webencodings==0.5.1 \
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
--hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
# via
# bleach
# tinycss2
websocket-client==1.9.0 \
--hash=sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98 \
--hash=sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef
# via jupyter-server
wrapt==2.1.2 \
--hash=sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63 \
--hash=sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e \
--hash=sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748 \
--hash=sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1 \
--hash=sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8 \
--hash=sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2 \
--hash=sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8 \
--hash=sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e \
--hash=sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf \
--hash=sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c \
--hash=sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0 \
--hash=sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c \
--hash=sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9
# via aiobotocore
yarl==1.24.2 \
--hash=sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30 \
--hash=sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9 \
--hash=sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db \
--hash=sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2 \
--hash=sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d \
--hash=sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035 \
--hash=sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7 \
--hash=sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad \
--hash=sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c \
--hash=sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d \
--hash=sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996 \
--hash=sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8 \
--hash=sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b \
--hash=sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8 \
--hash=sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576 \
--hash=sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712 \
--hash=sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1 \
--hash=sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c \
--hash=sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8
# via aiohttp

View File

@@ -0,0 +1,76 @@
import argparse
import sys
from pathlib import Path
from matplotlib import use
use("QtAgg")
from PySide6.QtWidgets import QApplication, QMessageBox
from .annotator import Annotator
from .config import load_config
from .filesystem import make_fs
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
"--config",
default="config/config.yaml",
help="Path to config YAML file (default: config/config.yaml)",
)
parser.add_argument("--data", default=None, help="Override data_dir from config")
parser.add_argument("--out", default=None, help="Override out_dir from config")
parser.add_argument("--clips", default=None, help="Override clips_file from config")
parser.add_argument(
"--clip", default=None, help="Stem name of a specific clip to load"
)
parser.add_argument(
"--extras",
action="store_true",
help="Also save GIFs, frame PNG, overlay PNG, and mask_vis PNG alongside the mask.",
)
parser.add_argument(
"--no-skip",
action="store_true",
help="Show already-annotated clips instead of skipping them.",
)
return parser.parse_args()
if __name__ == "__main__":
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
args = parse_args()
cfg = load_config(Path(args.config))
if args.data:
cfg.data_dir = args.data
if args.out:
cfg.out_dir = args.out
if args.clips:
cfg.clips_file = args.clips
fs = make_fs(cfg.storage)
app = QApplication([])
try:
win = Annotator(
cfg,
clip=args.clip,
extras=args.extras,
skip_annotated=not args.no_skip,
fs=fs,
)
except RuntimeError as e:
QMessageBox.information(None, "No clips", str(e))
sys.exit(0)
win.show()
app.exec()

View File

@@ -0,0 +1,506 @@
import io
import json
from pathlib import Path
import cv2
import numpy as np
from PIL import Image
from PySide6.QtCore import Qt, QTimer
from PySide6.QtWidgets import (
QApplication,
QButtonGroup,
QGroupBox,
QHBoxLayout,
QLabel,
QMainWindow,
QMessageBox,
QPushButton,
QRadioButton,
QVBoxLayout,
QWidget,
)
from .clip_selector import ClipSelector
from .compute_optical_flow import compute_optical_flow_mask
from .config import AppConfig, load_optical_flow_config
from .filesystem import fsjoin, fsname, fsstem
from .mask_canvas import MaskCanvas
from .video_loader import load_frames
class Annotator(QMainWindow):
def __init__(
self,
config: AppConfig,
clip: str = None,
extras: bool = False,
skip_annotated: bool = True,
fs=None,
):
super().__init__()
self.cfg = config
self.fs = fs
self.out_dir = config.out_dir
self.extras = extras
self.of_cfg = load_optical_flow_config(Path(config.optical_flow_config_file))
self.selector = ClipSelector(
data_dir=config.data_dir,
out_dir=self.out_dir,
clips_file=Path(config.clips_file),
mask_filename=config.filenames.mask,
zip_extension=config.filenames.zip_extension,
skip_annotated=skip_annotated,
fs=fs,
)
self.history: list[str] = []
self.history_pos: int = -1
self.setWindowTitle("Clip Annotator")
self._load_clip(specific=clip)
self._history_push()
self._init_ui()
self._init_timer()
# ── filesystem helpers ─────────────────────────────────────────
def _out_path(self, *parts: str) -> str:
return fsjoin(self.out_dir, fsstem(self.filename), *parts)
def _fs_exists(self, path: str) -> bool:
if self.fs is None:
return Path(path).exists()
return self.fs.exists(path)
def _fs_makedirs(self, path: str):
if self.fs is None:
Path(path).mkdir(parents=True, exist_ok=True)
else:
self.fs.makedirs(path, exist_ok=True)
def _pil_open(self, path: str) -> Image.Image:
if self.fs is None:
return Image.open(path)
with self.fs.open(path, "rb") as f:
return Image.open(io.BytesIO(f.read()))
def _pil_save(self, img: Image.Image, path: str):
if self.fs is None:
img.save(path)
else:
ext = str(path).rsplit(".", 1)[-1].lower()
fmt = "JPEG" if ext in ("jpg", "jpeg") else ext.upper()
buf = io.BytesIO()
img.save(buf, format=fmt)
self.fs.pipe(path, buf.getvalue())
def _json_read(self, path: str):
if self.fs is None:
with open(path) as f:
return json.load(f)
with self.fs.open(path, "r") as f:
return json.load(f)
def _json_write(self, data, path: str):
if self.fs is None:
with open(path, "w") as f:
json.dump(data, f, indent=2)
else:
self.fs.pipe(path, json.dumps(data, indent=2).encode())
# ── clip loading ───────────────────────────────────────────────
def _load_clip(self, specific: str = None, path: str = None):
if path is not None:
self.filename = path
else:
self.filename = self.selector.next(specific=specific)
self.frames, self.fps, self.dh, self.dw, self.h, self.w = load_frames(
self.filename,
self.cfg.max_frames,
self.cfg.display_max,
self.cfg.fps_fallback,
self.cfg.filenames.video_in_zip,
self.cfg.filenames.video_tmp_suffix,
fs=self.fs,
)
self._pending_answers = self._read_saved_answers()
def _history_push(self):
del self.history[self.history_pos + 1 :]
self.history.append(self.filename)
self.history_pos = len(self.history) - 1
def _read_saved_mask(self):
mask_path = self._out_path(self.cfg.filenames.mask)
if not self._fs_exists(mask_path):
return None
mask_full = np.array(self._pil_open(mask_path).convert("L"))
return cv2.resize(
(mask_full > 127).astype(np.uint8),
(self.dw, self.dh),
interpolation=cv2.INTER_NEAREST,
)
def _read_saved_answers(self):
meta_path = self._out_path(self.cfg.filenames.metadata)
if not self._fs_exists(meta_path):
return None
return self._json_read(meta_path)
# ── UI setup ───────────────────────────────────────────────────
def _init_ui(self):
self.mc = MaskCanvas(self.frames, self.dh, self.dw)
self.mc.set_title(fsname(self.filename))
self.mc.reset(self._read_saved_mask())
self.q_widgets = {}
question_panel = self._build_question_panel()
self.btn_prev = QPushButton("Previous")
self.btn_prev.setEnabled(False)
self.btn_next = QPushButton("Next")
btn_skip = QPushButton("Skip")
btn_clear = QPushButton("Clear")
btn_undo = QPushButton("Undo")
btn_undo10 = QPushButton("Undo×10")
btn_redo = QPushButton("Redo")
btn_load_prev_mask = QPushButton("Load Prev Mask")
btn_auto_segment = QPushButton("Auto Segment")
btn_auto_segment.setEnabled(self.of_cfg.enabled)
row1 = QHBoxLayout()
for b in [
self.btn_prev,
self.btn_next,
btn_skip,
btn_load_prev_mask,
btn_auto_segment,
]:
row1.addWidget(b)
row_tools = QHBoxLayout()
for b in [
self.mc.btn_brush,
self.mc.btn_polygon,
self.mc.btn_fill,
self.mc.btn_del_shape,
self.mc.btn_cancel_poly,
]:
row_tools.addWidget(b)
row2 = QHBoxLayout()
for b in [
btn_clear,
self.mc.btn_erase,
btn_undo,
btn_undo10,
btn_redo,
self.mc.btn_mask,
]:
row2.addWidget(b)
row3 = QHBoxLayout()
row3.addWidget(QLabel("Brush size"))
row3.addWidget(self.mc.brush_slider)
row3.addWidget(self.mc.brush_reset)
row4 = QHBoxLayout()
row4.addWidget(QLabel("Mask Alpha"))
row4.addWidget(self.mc.alpha_slider)
row4.addWidget(self.mc.alpha_reset)
vert_panel = QHBoxLayout()
vert_panel.setContentsMargins(0, 0, 4, 0)
for label_text, slider, reset_btn in [
("Brightness", self.mc.brightness_slider, self.mc.brightness_reset),
("Contrast", self.mc.contrast_slider, self.mc.contrast_reset),
("Gamma", self.mc.gamma_slider, self.mc.gamma_reset),
]:
col = QVBoxLayout()
lbl = QLabel(label_text)
lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
col.addWidget(lbl)
col.addWidget(slider, 1)
col.addWidget(reset_btn)
vert_panel.addLayout(col)
canvas_row = QHBoxLayout()
canvas_row.addLayout(vert_panel)
canvas_row.addWidget(self.mc.canvas, 1)
left = QVBoxLayout()
left.addLayout(canvas_row)
left.addLayout(row1)
left.addLayout(row_tools)
left.addLayout(row2)
left.addLayout(row3)
left.addLayout(row4)
left_widget = QWidget()
left_widget.setLayout(left)
right_widget = QWidget()
right_widget.setLayout(question_panel)
main = QHBoxLayout()
main.addWidget(left_widget, 3)
main.addWidget(right_widget, 1)
container = QWidget()
container.setLayout(main)
self.setCentralWidget(container)
self.btn_prev.clicked.connect(self.prev_clip)
self.btn_next.clicked.connect(self.next_clip)
btn_skip.clicked.connect(self.skip_clip)
btn_clear.clicked.connect(self.mc.clear)
btn_undo.clicked.connect(self.mc.undo)
btn_undo10.clicked.connect(self.mc.undo10)
btn_redo.clicked.connect(self.mc.redo)
btn_load_prev_mask.clicked.connect(self.load_prev_mask)
btn_auto_segment.clicked.connect(self.run_optical_flow)
if self._pending_answers:
self._set_answers(self._pending_answers)
self._pending_answers = None
def _build_question_panel(self) -> QVBoxLayout:
vbox = QVBoxLayout()
for section, qs in self.cfg.get_questions():
group = QGroupBox(section)
gvbox = QVBoxLayout()
for key, label, options, default in qs:
gvbox.addWidget(QLabel(label))
btn_group = QButtonGroup(self)
row = QHBoxLayout()
buttons = []
for opt in options:
btn = QRadioButton(opt)
btn_group.addButton(btn)
row.addWidget(btn)
buttons.append(btn)
if default == opt:
btn.setChecked(True)
if default is None and buttons:
buttons[-1].setChecked(True)
self.q_widgets[key] = (btn_group, buttons, options)
gvbox.addLayout(row)
group.setLayout(gvbox)
vbox.addWidget(group)
return vbox
def _set_answers(self, answers: dict):
for key, value in answers.items():
if key not in self.q_widgets:
continue
_, buttons, options = self.q_widgets[key]
for i, btn in enumerate(buttons):
btn.setChecked(options[i] == value)
def _init_timer(self):
self.frame_i = 0
self.timer = QTimer()
self.timer.timeout.connect(self._tick)
self.timer.start(int(1000 / self.fps))
def _tick(self):
self.frame_i = (self.frame_i + 1) % len(self.frames)
self.mc.set_frame(self.frames[self.frame_i])
# ── answers ────────────────────────────────────────────────────
def get_answers(self) -> dict:
out = {}
for key, (_, buttons, options) in self.q_widgets.items():
for i, btn in enumerate(buttons):
if btn.isChecked():
out[key] = options[i]
return out
# ── save helpers ───────────────────────────────────────────────
def _make_overlay(self, frame, alpha=0.4):
overlay = frame.copy()
green = np.zeros_like(frame)
green[..., 1] = 255
m = self.mc.mask.astype(bool)
overlay[m] = (1 - alpha) * overlay[m] + alpha * green[m]
return overlay.astype(np.uint8)
def _save_gif(self, frames, out_path: str, scale=1.0):
h, w = frames[0].shape[:2]
nh, nw = max(1, int(h * scale)), max(1, int(w * scale))
pil_frames = [Image.fromarray(cv2.resize(f, (nw, nh))) for f in frames]
if self.fs is None:
pil_frames[0].save(
out_path,
save_all=True,
append_images=pil_frames[1:],
duration=int(1000 / self.fps),
loop=0,
)
else:
buf = io.BytesIO()
pil_frames[0].save(
buf,
format="GIF",
save_all=True,
append_images=pil_frames[1:],
duration=int(1000 / self.fps),
loop=0,
)
self.fs.pipe(out_path, buf.getvalue())
# ── actions ────────────────────────────────────────────────────
def _save_locked(self):
self.btn_next.setEnabled(False)
self.btn_prev.setEnabled(False)
QApplication.processEvents()
try:
self.save()
finally:
self.btn_next.setEnabled(True)
self.btn_prev.setEnabled(self.history_pos > 0)
def save(self):
out = fsjoin(self.out_dir, fsstem(self.filename))
self._fs_makedirs(out)
mask_full = cv2.resize(
self.mc.mask.astype(np.uint8),
(self.w, self.h),
interpolation=cv2.INTER_NEAREST,
)
fn = self.cfg.filenames
self._pil_save(Image.fromarray(mask_full * 255), fsjoin(out, fn.mask))
self._json_write(self.get_answers(), fsjoin(out, fn.metadata))
mid = len(self.frames) // 2
frame = self.frames[mid]
self._pil_save(Image.fromarray(frame), fsjoin(out, fn.frame))
self._pil_save(
Image.fromarray(self._make_overlay(frame)), fsjoin(out, fn.overlay)
)
if self.extras:
self._pil_save(
Image.fromarray((self.mc.mask * 255).astype(np.uint8)),
fsjoin(out, fn.mask_vis),
)
overlay_frames = [self._make_overlay(f) for f in self.frames]
self._save_gif(self.frames, fsjoin(out, fn.gif_original_hires), scale=1.0)
self._save_gif(self.frames, fsjoin(out, fn.gif_original_lowres), scale=0.5)
self._save_gif(overlay_frames, fsjoin(out, fn.gif_overlay_hires), scale=1.0)
self._save_gif(
overlay_frames, fsjoin(out, fn.gif_overlay_lowres), scale=0.5
)
print("Saved:", out)
def _switch_ui_to_clip(self):
self.frame_i = 0
self.mc.load_clip(
self.frames,
self.dh,
self.dw,
mask=self._read_saved_mask(),
title=fsname(self.filename),
)
if self._pending_answers:
self._set_answers(self._pending_answers)
self._pending_answers = None
self.btn_prev.setEnabled(self.history_pos > 0)
def _advance_clip(self):
if self.history_pos < len(self.history) - 1:
self.history_pos += 1
self._load_clip(path=self.history[self.history_pos])
self._switch_ui_to_clip()
return
try:
self._load_clip()
except RuntimeError:
msg = QMessageBox(self)
msg.setWindowTitle("All done!")
msg.setText("You have reached the end of all clips.")
msg.setStandardButtons(QMessageBox.StandardButton.Ok)
msg.exec()
QApplication.instance().quit()
return
self._history_push()
self._switch_ui_to_clip()
def prev_clip(self):
if self.history_pos <= 0:
return
self._save_locked()
self.history_pos -= 1
self._load_clip(path=self.history[self.history_pos])
self._switch_ui_to_clip()
def next_clip(self):
mask_path = self._out_path(self.cfg.filenames.mask)
if self._fs_exists(mask_path):
msg = QMessageBox(self)
msg.setWindowTitle("Existing annotation found")
msg.setText(
f"'{fsstem(self.filename)}' already has a saved annotation.\n"
"Replace it with your current work, or keep the existing save?"
)
btn_replace = msg.addButton(
"Replace & Continue", QMessageBox.ButtonRole.AcceptRole
)
btn_keep = msg.addButton(
"Keep Existing & Continue", QMessageBox.ButtonRole.AcceptRole
)
msg.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
msg.setDefaultButton(btn_replace)
msg.exec()
clicked = msg.clickedButton()
if clicked == btn_replace:
self._save_locked()
self._advance_clip()
elif clicked == btn_keep:
self._advance_clip()
# Cancel: do nothing
else:
self._save_locked()
self._advance_clip()
def skip_clip(self):
self._advance_clip()
def load_prev_mask(self):
try:
idx = self.selector.clips.index(self.filename)
except ValueError:
return
if idx == 0:
QMessageBox.information(
self, "No previous clip", "This is the first clip in the list."
)
return
prev_clip = self.selector.clips[idx - 1]
mask_path = fsjoin(self.out_dir, fsstem(prev_clip), self.cfg.filenames.mask)
if not self._fs_exists(mask_path):
QMessageBox.information(
self,
"No mask found",
f"No saved mask found for '{fsstem(prev_clip)}'.",
)
return
mask_full = np.array(self._pil_open(mask_path).convert("L"))
mask = cv2.resize(
(mask_full > 127).astype(np.uint8),
(self.dw, self.dh),
interpolation=cv2.INTER_NEAREST,
)
self.mc.set_mask(mask)
def run_optical_flow(self):
mask = compute_optical_flow_mask(
self.frames,
self.fps,
self.of_cfg.norm_squared_threshold,
self.of_cfg.gaussian_kernel,
self.of_cfg.brightness_range,
)
self.mc.set_mask(mask)

View File

@@ -0,0 +1,75 @@
from pathlib import Path
from .filesystem import fsjoin, fsstem
class ClipSelector:
def __init__(
self,
data_dir,
out_dir,
clips_file: Path,
mask_filename: str = "mask.png",
zip_extension: str = ".zip",
skip_annotated: bool = True,
fs=None,
):
self.data_dir = str(data_dir)
self.out_dir = str(out_dir)
self.mask_filename = mask_filename
self.zip_extension = zip_extension
self.skip_annotated = skip_annotated
self.fs = fs
self.clips = self._load_clips(clips_file)
self.index = 0
def _load_clips(self, clips_file: Path) -> list:
lines = clips_file.read_text().splitlines()
return [
fsjoin(self.data_dir, name.strip())
for name in lines
if name.strip() and not name.strip().startswith("#")
]
def is_annotated(self, path) -> bool:
mask_path = fsjoin(self.out_dir, fsstem(path), self.mask_filename)
if self.fs is None:
return Path(mask_path).exists()
return self.fs.exists(mask_path)
def next(self, specific: str = None) -> str:
if specific:
return self._resolve_specific(specific)
return self._pick_next()
def _resolve_specific(self, specific: str) -> str:
if self.fs is None:
data_dir = Path(self.data_dir)
matches = list(data_dir.glob(f"{specific}{self.zip_extension}"))
if not matches:
p = data_dir / specific
matches = [p] if p.exists() else []
if not matches:
raise FileNotFoundError(
f"Clip '{specific}' not found in {self.data_dir}"
)
return str(matches[0])
else:
pattern = fsjoin(self.data_dir, f"{specific}{self.zip_extension}")
matches = self.fs.glob(pattern)
if not matches:
p = fsjoin(self.data_dir, specific)
matches = [p] if self.fs.exists(p) else []
if not matches:
raise FileNotFoundError(
f"Clip '{specific}' not found in {self.data_dir}"
)
return matches[0]
def _pick_next(self) -> str:
while self.index < len(self.clips):
clip = self.clips[self.index]
self.index += 1
if not self.skip_annotated or not self.is_annotated(clip):
return clip
raise RuntimeError("No remaining clips to annotate")

View File

@@ -0,0 +1,49 @@
import cv2
import numpy as np
def compute_optical_flow_mask(
frames: list[np.ndarray],
fps: float,
norm_squared_threshold: float,
gaussian_kernel: tuple[int, int],
brightness_range: tuple[int, int],
) -> np.ndarray:
"""Return a binary mask (uint8, values 0/1) from optical flow + brightness."""
if len(frames) < 2:
return np.zeros(frames[0].shape[:2], dtype=np.uint8)
frames_arr = np.stack(frames).astype(np.float64)
frames_sub_mean = frames_arr - np.mean(frames_arr, axis=0)
mn, mx = frames_sub_mean.min(), frames_sub_mean.max()
if mx > mn:
standardized = ((frames_sub_mean - mn) / (mx - mn) * 255).astype(np.uint8)
else:
standardized = np.zeros_like(frames_arr, dtype=np.uint8)
N = len(standardized)
gray = np.stack([cv2.cvtColor(f, cv2.COLOR_RGB2GRAY) for f in standardized])
flow_data = np.zeros((N - 1,) + gray.shape[1:] + (2,))
for i in range(N - 1):
flow_data[i] = fps * cv2.optflow.calcOpticalFlowSparseToDense(
gray[i], gray[i + 1]
)
optical_flow = np.median(flow_data, axis=0)
flow_norm_sq = np.sum(optical_flow**2, axis=-1)
max_norm = np.max(flow_norm_sq)
if max_norm > 0:
flow_mask = flow_norm_sq >= max_norm * norm_squared_threshold**2
else:
flow_mask = np.zeros(flow_norm_sq.shape, dtype=bool)
reference_frame = frames[len(frames) // 2]
smoothed = cv2.GaussianBlur(reference_frame, gaussian_kernel, 0)
gray_ref = cv2.cvtColor(smoothed, cv2.COLOR_RGB2GRAY)
brightness_mask = (gray_ref > brightness_range[0]) & (
gray_ref < brightness_range[1]
)
return np.logical_and(brightness_mask, flow_mask).astype(np.uint8)

View File

@@ -0,0 +1,94 @@
from dataclasses import dataclass, field
from pathlib import Path
import yaml
@dataclass
class FilenameConfig:
video_in_zip: str = "left.mp4"
video_tmp_suffix: str = ".mp4"
zip_extension: str = ".zip"
mask: str = "mask.png"
metadata: str = "metadata.json"
frame: str = "frame.png"
overlay: str = "overlay.png"
mask_vis: str = "mask_vis.png"
gif_original_hires: str = "video_original_hires.gif"
gif_original_lowres: str = "video_original_lowres.gif"
gif_overlay_hires: str = "video_overlay_hires.gif"
gif_overlay_lowres: str = "video_overlay_lowres.gif"
@dataclass
class AppConfig:
storage: str
data_dir: str
out_dir: str
optical_flow_config_file: str
questions_config_file: str
display_max: int = 480
fps_fallback: int = 25
max_frames: int = 100
clips_file: str = "config/clips.txt"
filenames: FilenameConfig = field(default_factory=FilenameConfig)
questions: list = field(default_factory=list, init=False)
def get_questions(self):
return [
(
s["section"],
[
(
item["key"],
item["label"],
[str(o) for o in item["options"]],
str(item["default"])
if item.get("default") is not None
else None,
)
for item in s["items"]
],
)
for s in self.questions
]
@dataclass
class OpticalFlowConfig:
enabled: bool = False
norm_squared_threshold: float = 0.3
gaussian_kernel: tuple[int, int] = (5, 5)
brightness_range: tuple[int, int] = (20, 235)
def load_optical_flow_config(path: Path) -> OpticalFlowConfig:
with open(path) as f:
data = yaml.safe_load(f)
data["gaussian_kernel"] = tuple(data["gaussian_kernel"])
data["brightness_range"] = tuple(data["brightness_range"])
return OpticalFlowConfig(**data)
def load_questions_config(path: Path) -> list:
with open(path) as f:
return yaml.safe_load(f)
def load_config(path: Path) -> AppConfig:
with open(path) as f:
data = yaml.safe_load(f)
for required in (
"storage",
"data_dir",
"out_dir",
"optical_flow_config_file",
"questions_config_file",
):
if not data.get(required):
raise ValueError(f"{path}: missing required field '{required}'.")
fn_data = data.pop("filenames", {})
cfg = AppConfig(**data)
cfg.filenames = FilenameConfig(**fn_data)
cfg.questions = load_questions_config(Path(cfg.questions_config_file))
return cfg

View File

@@ -0,0 +1,35 @@
import os
_DEFAULT_ENDPOINT = "https://os.zhdk.cloud.switch.ch"
def make_fs(storage: str):
"""Return an S3FileSystem for storage='s3', or None for local."""
if storage != "s3":
return None
import s3fs
return s3fs.S3FileSystem(
key=os.environ["S3_ACCESS_KEY"],
secret=os.environ["S3_SECRET_ACCESS_KEY"],
client_kwargs={
"endpoint_url": os.environ.get("S3_ENDPOINT_URL", _DEFAULT_ENDPOINT)
},
)
def fsjoin(base, *parts: str) -> str:
"""Join path segments with forward slashes (works for both local and S3)."""
return "/".join([str(base).rstrip("/"), *[str(p).strip("/") for p in parts if p]])
def fsstem(path) -> str:
"""Filename stem (no extension) for local Path or S3 string."""
name = str(path).replace("\\", "/").split("/")[-1]
return name.rsplit(".", 1)[0] if "." in name else name
def fsname(path) -> str:
"""Filename component (with extension) for local Path or S3 string."""
return str(path).replace("\\", "/").split("/")[-1]

View File

@@ -0,0 +1,452 @@
import cv2
import numpy as np
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from matplotlib.patches import Circle
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QPushButton, QSlider
class MaskCanvas:
"""Matplotlib canvas with brush/polygon mask drawing, undo/redo, and erase."""
_BRUSH_DEFAULT = 5
_ALPHA_DEFAULT = 15
_BRIGHTNESS_DEFAULT = 0
_CONTRAST_DEFAULT = 0
_GAMMA_DEFAULT = 100
_CLOSE_THRESHOLD = 15 # image-pixel distance to first vertex that closes a polygon
def __init__(self, frames, dh: int, dw: int):
self.dh = dh
self.dw = dw
self.mask = np.zeros((dh, dw), dtype=np.uint8)
self.history: list[np.ndarray] = []
self.redo_stack: list[np.ndarray] = []
self.erase_mode = False
self.drawing = False
self.mask_visible = True
self._current_frame = frames[0]
self.tool_mode = "brush"
self._shapes: list[list[tuple]] = []
self._current_poly: list[tuple] = []
self._poly_artists: list = []
self._mouse_pos: tuple | None = None
self._build_figure(frames)
self._build_controls()
self._connect_events()
def _build_figure(self, frames):
self.fig = Figure(figsize=(self.dw / 80, self.dh / 80))
self.canvas = FigureCanvas(self.fig)
self.ax = self.fig.add_subplot(111)
self.ax.axis("off")
self.img_artist = self.ax.imshow(frames[0])
self.mask_artist = self.ax.imshow(np.zeros((self.dh, self.dw, 4)))
self.title_text = self.ax.set_title("", fontsize=10, pad=4)
self.brush_circle = Circle(
(0, 0), radius=5, fill=False, color="white", linewidth=1.5, visible=False
)
self.ax.add_patch(self.brush_circle)
self.ax.autoscale(False) # prevent polygon plot() calls from expanding the view
def _build_controls(self):
self.btn_erase = QPushButton("Eraser")
self.btn_mask = QPushButton("Hide Mask")
self.btn_brush = QPushButton("Brush")
self.btn_brush.setStyleSheet("background-color: #4488ff; color: white;")
self.btn_polygon = QPushButton("Polygon")
self.btn_fill = QPushButton("Fill")
self.btn_fill.setEnabled(False)
self.btn_del_shape = QPushButton("Del Shape")
self.btn_del_shape.setEnabled(False)
self.btn_cancel_poly = QPushButton("Cancel Current Poly")
self.brush_slider = QSlider(Qt.Horizontal)
self.brush_slider.setRange(2, 50)
self.brush_slider.setValue(self._BRUSH_DEFAULT)
self.brush_reset = QPushButton("")
self.brush_reset.setFixedWidth(28)
self.alpha_slider = QSlider(Qt.Horizontal)
self.alpha_slider.setRange(0, 100)
self.alpha_slider.setValue(self._ALPHA_DEFAULT)
self.alpha_reset = QPushButton("")
self.alpha_reset.setFixedWidth(28)
self.brightness_slider = QSlider(Qt.Vertical)
self.brightness_slider.setRange(-100, 100)
self.brightness_slider.setValue(self._BRIGHTNESS_DEFAULT)
self.brightness_reset = QPushButton("")
self.brightness_reset.setFixedWidth(28)
self.contrast_slider = QSlider(Qt.Vertical)
self.contrast_slider.setRange(-100, 100)
self.contrast_slider.setValue(self._CONTRAST_DEFAULT)
self.contrast_reset = QPushButton("")
self.contrast_reset.setFixedWidth(28)
self.gamma_slider = QSlider(Qt.Vertical)
self.gamma_slider.setRange(10, 300)
self.gamma_slider.setValue(self._GAMMA_DEFAULT)
self.gamma_reset = QPushButton("")
self.gamma_reset.setFixedWidth(28)
def _connect_events(self):
self.canvas.mpl_connect("button_press_event", self._on_press)
self.canvas.mpl_connect("motion_notify_event", self._on_move)
self.canvas.mpl_connect("button_release_event", self._on_release)
self.canvas.mpl_connect("axes_leave_event", self._on_axes_leave)
self.btn_erase.clicked.connect(self.toggle_erase)
self.btn_mask.clicked.connect(self.toggle_mask)
self.btn_brush.clicked.connect(lambda: self.set_tool_mode("brush"))
self.btn_polygon.clicked.connect(lambda: self.set_tool_mode("polygon"))
self.btn_fill.clicked.connect(lambda: self.set_tool_mode("fill"))
self.btn_del_shape.clicked.connect(self.delete_last_shape)
self.btn_cancel_poly.clicked.connect(self.cancel_polygon)
self.alpha_slider.valueChanged.connect(self.redraw)
self.brightness_slider.valueChanged.connect(self._refresh_frame)
self.contrast_slider.valueChanged.connect(self._refresh_frame)
self.gamma_slider.valueChanged.connect(self._refresh_frame)
self.brush_reset.clicked.connect(
lambda: self.brush_slider.setValue(self._BRUSH_DEFAULT)
)
self.alpha_reset.clicked.connect(
lambda: self.alpha_slider.setValue(self._ALPHA_DEFAULT)
)
self.brightness_reset.clicked.connect(
lambda: self.brightness_slider.setValue(self._BRIGHTNESS_DEFAULT)
)
self.contrast_reset.clicked.connect(
lambda: self.contrast_slider.setValue(self._CONTRAST_DEFAULT)
)
self.gamma_reset.clicked.connect(
lambda: self.gamma_slider.setValue(self._GAMMA_DEFAULT)
)
# ── clip transition ────────────────────────────────────────────
def load_clip(self, frames, dh: int, dw: int, mask=None, title: str = ""):
self.dh = dh
self.dw = dw
self.mask = mask if mask is not None else np.zeros((dh, dw), dtype=np.uint8)
self.history = []
self.redo_stack = []
self._current_frame = frames[0]
self._clear_poly_state()
self.img_artist.set_data(self._apply_image_adjustments(frames[0]))
self.ax.set_xlim(-0.5, dw - 0.5)
self.ax.set_ylim(dh - 0.5, -0.5)
self.set_title(title)
self.redraw()
def _clear_poly_state(self):
self._shapes = []
self._current_poly = []
self._mouse_pos = None
for a in self._poly_artists:
a.remove()
self._poly_artists = []
self._update_poly_buttons()
# ── frame / title ──────────────────────────────────────────────
def set_frame(self, frame):
self._current_frame = frame
self.img_artist.set_data(self._apply_image_adjustments(frame))
self.canvas.draw_idle()
# ── image adjustments ──────────────────────────────────────────
def _apply_image_adjustments(self, frame):
img = frame.astype(np.float32)
img += self.brightness_slider.value()
c = self.contrast_slider.value() / 100.0
img = (1.0 + c) * (img - 128.0) + 128.0
np.clip(img, 0, 255, out=img)
g = self.gamma_slider.value() / 100.0
img = (img / 255.0) ** (1.0 / g) * 255.0
return np.clip(img, 0, 255).astype(np.uint8)
def _refresh_frame(self):
if self._current_frame is not None:
self.img_artist.set_data(self._apply_image_adjustments(self._current_frame))
self.canvas.draw_idle()
def set_title(self, text: str):
self.title_text.set_text(text)
# ── mask ops ───────────────────────────────────────────────────
def reset(self, mask=None):
self.mask = (
mask if mask is not None else np.zeros((self.dh, self.dw), dtype=np.uint8)
)
self.history = []
self.redo_stack = []
self.redraw()
def set_mask(self, mask):
"""Replace the mask and push the previous state onto the undo stack."""
self.history.append(self.mask.copy())
self.redo_stack.clear()
self.mask = mask
self.redraw()
def redraw(self):
if self.mask_visible:
alpha = self.alpha_slider.value() / 100.0
rgba = np.zeros((self.dh, self.dw, 4))
rgba[..., 1] = self.mask * 0.7
rgba[..., 3] = self.mask * alpha
else:
rgba = np.zeros((self.dh, self.dw, 4))
self.mask_artist.set_data(rgba)
self.canvas.draw_idle()
def clear(self):
self.mask[:] = 0
self.redraw()
def undo(self):
if self.history:
self.redo_stack.append(self.mask.copy())
self.mask = self.history.pop()
self.redraw()
def undo10(self):
for _ in range(10):
if not self.history:
break
self.redo_stack.append(self.mask.copy())
self.mask = self.history.pop()
self.redraw()
def redo(self):
if self.redo_stack:
self.history.append(self.mask.copy())
self.mask = self.redo_stack.pop()
self.redraw()
def toggle_erase(self):
self.erase_mode = not self.erase_mode
if self.erase_mode:
self.btn_erase.setText("Eraser ON")
self.btn_erase.setStyleSheet("background-color: orange; color: black;")
else:
self.btn_erase.setText("Eraser")
self.btn_erase.setStyleSheet("")
def toggle_mask(self):
self.mask_visible = not self.mask_visible
if self.mask_visible:
self.btn_mask.setText("Hide Mask")
self.btn_mask.setStyleSheet("")
else:
self.btn_mask.setText("Show Mask")
self.btn_mask.setStyleSheet("background-color: red; color: white;")
self.redraw()
def stamp(self, x, y):
if x is None or y is None:
return
self.history.append(self.mask.copy())
self.redo_stack.clear()
r = self.brush_slider.value()
ix, iy = int(x), int(y)
y0, y1 = max(0, iy - r), min(self.dh, iy + r + 1)
x0, x1 = max(0, ix - r), min(self.dw, ix + r + 1)
Y, X = np.ogrid[y0:y1, x0:x1]
circle = (X - ix) ** 2 + (Y - iy) ** 2 <= r**2
self.mask[y0:y1, x0:x1][circle] = 0 if self.erase_mode else 1
self.redraw()
# ── tool mode ──────────────────────────────────────────────────
def set_tool_mode(self, mode: str):
self.tool_mode = mode
active = "background-color: #4488ff; color: white;"
self.btn_brush.setStyleSheet(active if mode == "brush" else "")
self.btn_polygon.setStyleSheet(active if mode == "polygon" else "")
self.btn_fill.setStyleSheet(active if mode == "fill" else "")
if mode != "brush":
self.brush_circle.set_visible(False)
self.canvas.draw_idle()
# ── polygon ops ────────────────────────────────────────────────
def _near_first(self, x: float, y: float) -> bool:
if not self._current_poly:
return False
fx, fy = self._current_poly[0]
return (x - fx) ** 2 + (y - fy) ** 2 <= self._CLOSE_THRESHOLD**2
def _update_poly_buttons(self):
has = bool(self._shapes)
self.btn_fill.setEnabled(has)
self.btn_del_shape.setEnabled(has)
def _draw_polygon_overlay(self, mouse_pos=None):
for a in self._poly_artists:
a.remove()
self._poly_artists.clear()
# Completed shapes — thick closed outline
for shape in self._shapes:
xs = [p[0] for p in shape] + [shape[0][0]]
ys = [p[1] for p in shape] + [shape[0][1]]
(line,) = self.ax.plot(xs, ys, color="cyan", linewidth=3, zorder=5)
(dots,) = self.ax.plot(
[p[0] for p in shape],
[p[1] for p in shape],
"o",
color="cyan",
markersize=4,
zorder=6,
)
self._poly_artists.extend([line, dots])
# In-progress polygon
if self._current_poly:
xs = [p[0] for p in self._current_poly]
ys = [p[1] for p in self._current_poly]
if len(self._current_poly) > 1:
(edge,) = self.ax.plot(xs, ys, color="yellow", linewidth=1.5, zorder=5)
self._poly_artists.append(edge)
(verts,) = self.ax.plot(xs, ys, "o", color="yellow", markersize=5, zorder=6)
# Red dot on first vertex as close-target indicator
(first,) = self.ax.plot(
[xs[0]], [ys[0]], "o", color="red", markersize=8, zorder=7
)
self._poly_artists.extend([verts, first])
# Rubber-band line from last vertex to cursor
if mouse_pos:
mx, my = mouse_pos
near = len(self._current_poly) >= 3 and self._near_first(mx, my)
clr = "lime" if near else "yellow"
(rband,) = self.ax.plot(
[xs[-1], mx], [ys[-1], my], "--", color=clr, linewidth=1, zorder=5
)
self._poly_artists.append(rband)
self.canvas.draw_idle()
def cancel_polygon(self):
self._current_poly = []
self._draw_polygon_overlay(mouse_pos=self._mouse_pos)
def delete_last_shape(self):
if self._shapes:
self._shapes.pop()
self._update_poly_buttons()
self._draw_polygon_overlay(mouse_pos=self._mouse_pos)
def _fill_shape_at(self, x: float, y: float):
if not self._shapes:
return
polys = [
np.array(
[(int(round(px)), int(round(py))) for px, py in shape], dtype=np.int32
)
for shape in self._shapes
]
# Find all shapes that contain the click point
containing = []
for i, poly in enumerate(polys):
poly_f32 = poly.reshape(-1, 1, 2).astype(np.float32)
if cv2.pointPolygonTest(poly_f32, (x, y), False) >= 0:
containing.append((i, poly))
if not containing:
return # click was outside all shapes
# Pick the innermost (smallest area) shape that contains the click
containing.sort(key=lambda t: cv2.contourArea(t[1]))
target_idx, target_poly = containing[0]
self.history.append(self.mask.copy())
self.redo_stack.clear()
temp = np.zeros((self.dh, self.dw), dtype=np.uint8)
cv2.fillPoly(temp, [target_poly], 1)
# Punch holes for any shapes completely inside the target
target_f32 = target_poly.reshape(-1, 1, 2).astype(np.float32)
for i, poly in enumerate(polys):
if i == target_idx:
continue
cx = float(np.mean(poly[:, 0]))
cy = float(np.mean(poly[:, 1]))
if cv2.pointPolygonTest(target_f32, (cx, cy), False) > 0:
cv2.fillPoly(temp, [poly], 0)
self.mask |= temp
self.redraw()
# ── brush preview ──────────────────────────────────────────────
def _update_brush_preview(self, e):
if e.inaxes == self.ax and e.xdata is not None:
self.brush_circle.center = (e.xdata, e.ydata)
self.brush_circle.set_radius(self.brush_slider.value())
self.brush_circle.set_visible(True)
else:
self.brush_circle.set_visible(False)
self.canvas.draw_idle()
def _on_axes_leave(self, _):
self.brush_circle.set_visible(False)
if self.tool_mode == "polygon":
self._mouse_pos = None
self._draw_polygon_overlay()
else:
self.canvas.draw_idle()
# ── mouse events ───────────────────────────────────────────────
def _on_press(self, e):
if e.xdata is None:
return
if self.tool_mode == "brush":
self.drawing = True
self.stamp(e.xdata, e.ydata)
elif self.tool_mode == "polygon":
self._handle_polygon_click(e)
elif self.tool_mode == "fill" and e.button == 1:
self._fill_shape_at(e.xdata, e.ydata)
def _handle_polygon_click(self, e):
if e.button == 3: # right-click: remove last vertex
if self._current_poly:
self._current_poly.pop()
self._draw_polygon_overlay(mouse_pos=self._mouse_pos)
return
if e.button != 1:
return
x, y = e.xdata, e.ydata
if len(self._current_poly) >= 3 and self._near_first(x, y):
self._shapes.append(list(self._current_poly))
self._current_poly = []
self._update_poly_buttons()
self._draw_polygon_overlay(mouse_pos=self._mouse_pos)
else:
self._current_poly.append((x, y))
self._draw_polygon_overlay(mouse_pos=self._mouse_pos)
def _on_move(self, e):
if self.tool_mode == "brush":
self._update_brush_preview(e)
if self.drawing:
self.stamp(e.xdata, e.ydata)
elif self.tool_mode == "polygon":
self.brush_circle.set_visible(False)
if e.inaxes == self.ax and e.xdata is not None:
self._mouse_pos = (e.xdata, e.ydata)
self._draw_polygon_overlay(mouse_pos=self._mouse_pos)
else:
self._mouse_pos = None
self._draw_polygon_overlay()
def _on_release(self, _):
self.drawing = False

View File

@@ -0,0 +1,56 @@
import io
import os
import tempfile
import zipfile
import cv2
def load_frames(
zip_path,
max_frames: int,
display_max: int,
fps_fallback: int,
video_in_zip: str = "left.mp4",
video_tmp_suffix: str = ".mp4",
fs=None,
):
if fs is None:
video_bytes = zipfile.ZipFile(zip_path).read(video_in_zip)
else:
with fs.open(str(zip_path), "rb") as f:
video_bytes = zipfile.ZipFile(io.BytesIO(f.read())).read(video_in_zip)
with tempfile.NamedTemporaryFile(suffix=video_tmp_suffix, delete=False) as f:
f.write(video_bytes)
tmp_path = f.name
cap = cv2.VideoCapture(tmp_path)
fps = cap.get(cv2.CAP_PROP_FPS) or fps_fallback
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
step = max(1, total // max_frames)
frames = []
i = 0
while len(frames) < max_frames:
cap.set(cv2.CAP_PROP_POS_FRAMES, i)
ok, frame = cap.read()
if not ok:
break
frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
i += step
cap.release()
os.unlink(tmp_path)
if not frames:
raise RuntimeError(f"No frames found in {zip_path}")
h, w = frames[0].shape[:2]
scale = display_max / max(h, w)
dh, dw = int(h * scale), int(w * scale)
frames = [cv2.resize(f, (dw, dh)) for f in frames]
return frames, fps, dh, dw, h, w

View File

@@ -1,604 +0,0 @@
import os
import zipfile
import tempfile
import json
import argparse
from pathlib import Path
import cv2
import numpy as np
import pandas as pd
from PIL import Image
from matplotlib import use
use("QtAgg")
from PySide6.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QPushButton,
QVBoxLayout,
QHBoxLayout,
QLabel,
QRadioButton,
QButtonGroup,
QGroupBox,
QSlider,
)
from PySide6.QtCore import Qt, QTimer
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
# ─────────────────────────────────────────────
# CONFIG
# ─────────────────────────────────────────────
class Config:
DISPLAY_MAX = 480
FPS_FALLBACK = 25
MAX_FRAMES = 100
# ─────────────────────────────────────────────
# QUESTIONS
# ─────────────────────────────────────────────
QUESTIONS = [
(
"River",
[
("flow", "Flow Regime", ["Turbulent", "Laminar", "Uncertain"]),
("shadows", "Strong Shadows", ["Yes", "No", "Uncertain"]),
("artifacts", "Artifacts on River", ["Yes", "No", "Uncertain"]),
],
),
(
"Scene",
[
("lighting", "Lighting", ["Day", "Night", "Uncertain"]),
(
"exposure",
"Exposure",
["Overexposed", "Underexposed", "Both", "Normal", "Uncertain"],
),
],
),
(
"Weather",
[
("snowing", "Snowing", ["Yes", "No", "Uncertain"]),
("snow_on_ground", "Snow on Ground", ["Yes", "No", "Uncertain"]),
],
),
]
# ─────────────────────────────────────────────
# DEFAULTS
# ─────────────────────────────────────────────
DEFAULTS = {
"flow": "Laminar",
"shadows": "No",
"artifacts": "No",
"lighting": "Day",
"exposure": "Normal",
"snowing": "No",
"snow_on_ground": "No",
}
# ─────────────────────────────────────────────
# VIDEO LOADING
# ─────────────────────────────────────────────
def load_frames(zip_path: Path, max_frames: int):
video_bytes = zipfile.ZipFile(zip_path).read("left.mp4")
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f:
f.write(video_bytes)
tmp_path = f.name
cap = cv2.VideoCapture(tmp_path)
fps = cap.get(cv2.CAP_PROP_FPS) or Config.FPS_FALLBACK
frames = []
while len(frames) < max_frames:
ok, frame = cap.read()
if not ok:
break
frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
cap.release()
os.unlink(tmp_path)
if not frames:
raise RuntimeError(f"No frames found in {zip_path}")
h, w = frames[0].shape[:2]
scale = Config.DISPLAY_MAX / max(h, w)
dh, dw = int(h * scale), int(w * scale)
frames = [cv2.resize(f, (dw, dh)) for f in frames]
return frames, fps, dh, dw, h, w
# ─────────────────────────────────────────────
# MAIN APP
# ─────────────────────────────────────────────
class Annotator(QMainWindow):
def __init__(self, data_dir: Path, out_dir: Path, clip: str = None, target_time: str = None, daily: bool = False, extras: bool = False, skip_existing_day: bool = False):
super().__init__()
self.data_dir = Path(data_dir)
self.out_dir = Path(out_dir)
self.target_time = target_time
self.daily = daily
self.extras = extras
self.skip_existing_day = skip_existing_day
self.current_date = None
self.history = []
self.erase_mode = False
self.frame_i = 0
self.drawing = False
self._pending_answers = None
self.setWindowTitle("River Annotator")
self.df = self._load_dataset()
self._load_clip(specific=clip)
self._init_canvas()
self._init_ui()
self._init_timer()
# ─────────────────────────────
# DATA
# ─────────────────────────────
def _load_dataset(self):
files = list(self.data_dir.glob("*.zip"))
if not files:
raise FileNotFoundError(f"No zip files in {self.data_dir}")
df = pd.DataFrame({"filename": files})
df["datetime"] = df["filename"].apply(
lambda x: pd.to_datetime(x.stem.split("_")[1], errors="coerce")
)
# sort by datetime
df = df.sort_values("datetime").reset_index(drop=True)
return df
def _load_clip(self, specific: str = None, next_day: bool = False):
if specific is not None:
matches = list(self.data_dir.glob(f"{specific}.zip"))
if not matches:
p = self.data_dir / specific
matches = [p] if p.exists() else []
if not matches:
raise FileNotFoundError(f"Clip '{specific}' not found in {self.data_dir}")
self.filename = matches[0]
else:
remaining = [
f
for f in self.df["filename"]
if not (self.out_dir / f.stem / "mask.png").exists()
]
if not remaining:
raise RuntimeError("No remaining clips to annotate")
if self.target_time or self.daily:
# Parse target time (format: HH:MM)
if self.target_time:
target_hour, target_minute = map(int, self.target_time.split(":"))
else:
target_hour, target_minute = 12, 0 # Default to noon
target_seconds = target_hour * 3600 + target_minute * 60
# Get datetimes for remaining files
remaining_datetimes = [
self.df[self.df["filename"] == f]["datetime"].values[0]
for f in remaining
]
# Group by day
df_remaining = pd.DataFrame({
"filename": remaining,
"datetime": remaining_datetimes
})
df_remaining["date"] = df_remaining["datetime"].dt.date
# In daily mode, filter to next day if needed
if self.daily and next_day and self.current_date is not None:
import datetime
next_date = self.current_date + datetime.timedelta(days=1)
df_remaining = df_remaining[df_remaining["date"] >= next_date]
# In daily mode, skip entire days that already have any annotated clip
if self.daily and self.skip_existing_day:
annotated_dates = set()
for f in self.df["filename"]:
if (self.out_dir / f.stem / "mask.png").exists():
dt = self.df[self.df["filename"] == f]["datetime"].values[0]
annotated_dates.add(pd.Timestamp(dt).date())
df_remaining = df_remaining[~df_remaining["date"].isin(annotated_dates)]
if df_remaining.empty:
raise RuntimeError("No remaining clips to annotate")
# For each day, find the clip closest to target time
closest_clips = []
dates_list = []
for date, group in df_remaining.groupby("date"):
group = group.copy()
group["time_seconds"] = group["datetime"].dt.hour * 3600 + group["datetime"].dt.minute * 60
group["time_diff"] = (group["time_seconds"] - target_seconds).abs()
closest = group.loc[group["time_diff"].idxmin()]
closest_clips.append(closest["filename"])
dates_list.append(date)
# In daily mode, take only the first day's clip
if self.daily:
self.filename = closest_clips[0]
self.current_date = dates_list[0]
else:
# Take the first one (earliest by date/time)
self.filename = closest_clips[0]
self.current_date = dates_list[0]
else:
# take the earliest one (after sorting by datetime)
self.filename = remaining[0]
# Extract date from filename
import datetime
dt = self.df[self.df["filename"] == self.filename]["datetime"].values[0]
self.current_date = pd.Timestamp(dt).date()
self.frames, self.fps, self.dh, self.dw, self.h, self.w = load_frames(
self.filename, Config.MAX_FRAMES
)
self.history = []
self.mask = np.zeros((self.dh, self.dw), dtype=np.uint8)
self._pending_answers = None
out = self.out_dir / self.filename.stem
mask_path = out / "mask.png"
meta_path = out / "metadata.json"
if mask_path.exists():
mask_full = np.array(Image.open(mask_path).convert("L"))
self.mask = cv2.resize(
(mask_full > 127).astype(np.uint8),
(self.dw, self.dh),
interpolation=cv2.INTER_NEAREST,
)
if meta_path.exists():
with open(meta_path) as f:
self._pending_answers = json.load(f)
def _set_answers(self, answers: dict):
for key, value in answers.items():
if key not in self.q_widgets:
continue
_, buttons, options = self.q_widgets[key]
for i, btn in enumerate(buttons):
btn.setChecked(options[i] == value)
# ─────────────────────────────
# UI
# ─────────────────────────────
def _init_canvas(self):
self.fig = Figure()
self.canvas = FigureCanvas(self.fig)
self.ax = self.fig.add_subplot(111)
self.ax.axis("off")
self.img = self.ax.imshow(self.frames[0])
self.mask_img = self.ax.imshow(np.zeros((self.dh, self.dw, 4)))
self.title_text = self.ax.set_title(self.filename.name, fontsize=10, pad=4)
def _init_ui(self):
self.q_widgets = {}
question_box = QVBoxLayout()
for section, qs in QUESTIONS:
group = QGroupBox(section)
vbox = QVBoxLayout()
for key, label, options in qs:
vbox.addWidget(QLabel(label))
btn_group = QButtonGroup(self)
row = QHBoxLayout()
buttons = []
default_value = DEFAULTS.get(key)
for opt in options:
btn = QRadioButton(opt)
btn_group.addButton(btn)
row.addWidget(btn)
buttons.append(btn)
if default_value == opt:
btn.setChecked(True)
if default_value is None and buttons:
buttons[-1].setChecked(True)
self.q_widgets[key] = (btn_group, buttons, options)
vbox.addLayout(row)
group.setLayout(vbox)
question_box.addWidget(group)
# Controls
self.btn_save = QPushButton("Save")
self.btn_next = QPushButton("Next")
self.btn_skip = QPushButton("Skip")
self.btn_clear = QPushButton("Clear")
self.btn_erase = QPushButton("Eraser")
self.btn_undo = QPushButton("Undo")
self.btn_reload = QPushButton("Reload Saved")
self.brush_slider = QSlider(Qt.Horizontal)
self.brush_slider.setRange(2, 50)
self.brush_slider.setValue(5)
row1 = QHBoxLayout()
for b in [self.btn_save, self.btn_next, self.btn_skip]:
row1.addWidget(b)
row2 = QHBoxLayout()
for b in [self.btn_clear, self.btn_erase, self.btn_undo, self.btn_reload]:
row2.addWidget(b)
row2.addWidget(QLabel("Brush"))
row2.addWidget(self.brush_slider)
left = QVBoxLayout()
left.addWidget(self.canvas)
left.addLayout(row1)
left.addLayout(row2)
main = QHBoxLayout()
left_widget = QWidget()
left_widget.setLayout(left)
right_widget = QWidget()
right_widget.setLayout(question_box)
main.addWidget(left_widget, 3)
main.addWidget(right_widget, 2)
container = QWidget()
container.setLayout(main)
self.setCentralWidget(container)
# events
self.btn_save.clicked.connect(self.save)
self.btn_next.clicked.connect(self.next_clip)
self.btn_skip.clicked.connect(self.skip_clip)
self.btn_clear.clicked.connect(self.clear_mask)
self.btn_erase.clicked.connect(self.toggle_eraser)
self.btn_undo.clicked.connect(self.undo)
self.btn_reload.clicked.connect(self.reload_saved)
self.canvas.mpl_connect("button_press_event", self.on_press)
self.canvas.mpl_connect("motion_notify_event", self.on_move)
self.canvas.mpl_connect("button_release_event", self.on_release)
if self._pending_answers:
self._set_answers(self._pending_answers)
self._pending_answers = None
def _init_timer(self):
self.timer = QTimer()
self.timer.timeout.connect(self.update_frame)
self.timer.start(int(1000 / self.fps))
# ─────────────────────────────
# ANNOTATION
# ─────────────────────────────
def get_answers(self):
out = {}
for key, (group, buttons, options) in self.q_widgets.items():
for i, btn in enumerate(buttons):
if btn.isChecked():
out[key] = options[i]
return out
def stamp(self, x, y):
if x is None or y is None:
return
self.history.append(self.mask.copy())
r = self.brush_slider.value()
ix, iy = int(x), int(y)
y0, y1 = max(0, iy - r), min(self.dh, iy + r + 1)
x0, x1 = max(0, ix - r), min(self.dw, ix + r + 1)
Y, X = np.ogrid[y0:y1, x0:x1]
circle = (X - ix) ** 2 + (Y - iy) ** 2 <= r**2
self.mask[y0:y1, x0:x1][circle] = 0 if self.erase_mode else 1
self.redraw_mask()
def redraw_mask(self):
rgba = np.zeros((self.dh, self.dw, 4))
rgba[..., 1] = self.mask * 0.7
rgba[..., 3] = self.mask * 0.4
self.mask_img.set_data(rgba)
self.canvas.draw_idle()
# ─────────────────────────────
# EVENTS
# ─────────────────────────────
def on_press(self, e):
if e.xdata is None:
return
self.drawing = True
self.stamp(e.xdata, e.ydata)
def on_move(self, e):
if self.drawing:
self.stamp(e.xdata, e.ydata)
def on_release(self, _):
self.drawing = False
def update_frame(self):
self.frame_i = (self.frame_i + 1) % len(self.frames)
self.img.set_data(self.frames[self.frame_i])
self.canvas.draw_idle()
# ─────────────────────────────
# HELPERS
# ─────────────────────────────
def _make_overlay(self, frame, alpha=0.4):
overlay = frame.copy()
green = np.zeros_like(frame)
green[..., 1] = 255
m = self.mask.astype(bool)
overlay[m] = (1 - alpha) * overlay[m] + alpha * green[m]
return overlay.astype(np.uint8)
def _save_gif(self, frames, out_path, scale=1.0):
h, w = frames[0].shape[:2]
nh, nw = max(1, int(h * scale)), max(1, int(w * scale))
pil_frames = [Image.fromarray(cv2.resize(f, (nw, nh))) for f in frames]
pil_frames[0].save(
out_path,
save_all=True,
append_images=pil_frames[1:],
duration=int(1000 / self.fps),
loop=0,
)
# ─────────────────────────────
# ACTIONS
# ─────────────────────────────
def reload_saved(self):
out = self.out_dir / self.filename.stem
mask_path = out / "mask.png"
meta_path = out / "metadata.json"
if not mask_path.exists():
return
mask_full = np.array(Image.open(mask_path).convert("L"))
self.mask = cv2.resize(
(mask_full > 127).astype(np.uint8),
(self.dw, self.dh),
interpolation=cv2.INTER_NEAREST,
)
self.history = []
self.redraw_mask()
if meta_path.exists():
with open(meta_path) as f:
self._set_answers(json.load(f))
def clear_mask(self):
self.mask[:] = 0
self.redraw_mask()
def undo(self):
if self.history:
self.mask = self.history.pop()
self.redraw_mask()
def toggle_eraser(self):
self.erase_mode = not self.erase_mode
self.btn_erase.setText("Eraser ON" if self.erase_mode else "Eraser")
def save(self):
out = self.out_dir / self.filename.stem
out.mkdir(parents=True, exist_ok=True)
mask_full = cv2.resize(
self.mask.astype(np.uint8),
(self.w, self.h),
interpolation=cv2.INTER_NEAREST,
)
Image.fromarray(mask_full * 255).save(out / "mask.png")
with open(out / "metadata.json", "w") as f:
json.dump(self.get_answers(), f, indent=2)
mid = len(self.frames) // 2
frame = self.frames[mid]
overlay_frame = self._make_overlay(frame)
Image.fromarray(frame).save(out / "frame.png")
Image.fromarray(overlay_frame).save(out / "overlay.png")
if self.extras:
Image.fromarray((self.mask * 255).astype(np.uint8)).save(out / "mask_vis.png")
overlay_frames = [self._make_overlay(f) for f in self.frames]
self._save_gif(self.frames, out / "video_original_hires.gif", scale=1.0)
self._save_gif(self.frames, out / "video_original_lowres.gif", scale=0.5)
self._save_gif(overlay_frames, out / "video_overlay_hires.gif", scale=1.0)
self._save_gif(overlay_frames, out / "video_overlay_lowres.gif", scale=0.5)
print("Saved:", out)
def next_clip(self):
self.save()
self._load_clip(next_day=self.daily)
self.frame_i = 0
self.img.set_data(self.frames[0])
self.title_text.set_text(self.filename.name)
self.redraw_mask()
if self._pending_answers:
self._set_answers(self._pending_answers)
self._pending_answers = None
def skip_clip(self):
self._load_clip(next_day=self.daily)
self.frame_i = 0
self.img.set_data(self.frames[0])
self.title_text.set_text(self.filename.name)
self.redraw_mask()
if self._pending_answers:
self._set_answers(self._pending_answers)
self._pending_answers = None
# ─────────────────────────────────────────────
# ENTRY POINT
# ─────────────────────────────────────────────
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--data", default=r"C:\Users\sieverin\HydroScan\Code\river-annotation-tool\data\filtered_data")
parser.add_argument("--out", default="data/annotation_results/")
parser.add_argument("--clip", default=None, help="Stem name of a specific clip to load (e.g. 'left_20230501')")
parser.add_argument("--time", default=None, help="Target time to filter clips by day (format: HH:MM, e.g. '14:30'). Selects the closest clip to this time for each day.")
parser.add_argument("--daily", action="store_true", help="Load only 1 clip per day at the specified time (requires --time).")
parser.add_argument("--extras", action="store_true", help="Also save GIFs, frame PNG, overlay PNG, and mask_vis PNG alongside the mask.")
parser.add_argument("--skip-existing-day", action="store_true", help="In --daily mode, skip days that already have any annotated clip.")
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
app = QApplication([])
win = Annotator(Path(args.data), Path(args.out), clip=args.clip, target_time=args.time, daily=args.daily, extras=args.extras, skip_existing_day=args.skip_existing_day)
win.show()
app.exec()

345
uv.lock generated
View File

@@ -7,6 +7,89 @@ resolution-markers = [
"sys_platform != 'emscripten' and sys_platform != 'win32'", "sys_platform != 'emscripten' and sys_platform != 'win32'",
] ]
[[package]]
name = "aiobotocore"
version = "3.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "aioitertools" },
{ name = "botocore" },
{ name = "jmespath" },
{ name = "multidict" },
{ name = "python-dateutil" },
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e7/75/42cce839c2ec263ff74b10b650fe36b066fbb124cbee6f247eac0983e1ab/aiobotocore-3.7.0.tar.gz", hash = "sha256:c64d871ed5491a6571948dd48eabd185b46c6c23b64e3afd0c059fc7593ada30", size = 127054, upload-time = "2026-05-09T10:02:52.332Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/90/5f/85535dfb3cfd6442d66d1df1694062c5d6df02f895329e7e120b2a3d2b8b/aiobotocore-3.7.0-py3-none-any.whl", hash = "sha256:680bde7c64679a821a9312641b759d9497f790ba8b2e88c6959e6273ee765b8e", size = 89539, upload-time = "2026-05-09T10:02:50.389Z" },
]
[[package]]
name = "aiohappyeyeballs"
version = "2.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" },
]
[[package]]
name = "aiohttp"
version = "3.13.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
{ name = "aiosignal" },
{ name = "attrs" },
{ name = "frozenlist" },
{ name = "multidict" },
{ name = "propcache" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" },
{ url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" },
{ url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" },
{ url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" },
{ url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" },
{ url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" },
{ url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" },
{ url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" },
{ url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" },
{ url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" },
{ url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" },
{ url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" },
{ url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" },
{ url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" },
{ url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" },
{ url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" },
]
[[package]]
name = "aioitertools"
version = "0.13.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322, upload-time = "2025-11-06T22:17:07.609Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182, upload-time = "2025-11-06T22:17:06.502Z" },
]
[[package]]
name = "aiosignal"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "frozenlist" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
]
[[package]] [[package]]
name = "anyio" name = "anyio"
version = "4.13.0" version = "4.13.0"
@@ -141,6 +224,20 @@ css = [
{ name = "tinycss2" }, { name = "tinycss2" },
] ]
[[package]]
name = "botocore"
version = "1.43.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/28/79/2f4be1896db3db7ccf44504253a175d56b6bd6b669619edc5147d1aa21ea/botocore-1.43.0.tar.gz", hash = "sha256:e933b31a2d644253e1d029d7d39e99ba41b87e29300534f189744cc438cdf928", size = 15286817, upload-time = "2026-04-29T22:07:31.723Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/4b/afc1fef8a43bafb139f57f73bbd70df82807af5934321e8112ae50668827/botocore-1.43.0-py3-none-any.whl", hash = "sha256:cc5b15eaec3c6eac05d8012cb5ef17ebe891beb88a16ca13c374bfaece1241e6", size = 14970102, upload-time = "2026-04-29T22:07:27Z" },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2026.4.22" version = "2026.4.22"
@@ -207,6 +304,48 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
] ]
[[package]]
name = "clip-annotator"
source = { editable = "." }
dependencies = [
{ name = "matplotlib" },
{ name = "matplotlib-inline" },
{ name = "opencv-contrib-python-headless" },
{ name = "pandas" },
{ name = "pillow" },
{ name = "pyside6" },
{ name = "python-dotenv" },
{ name = "pyyaml" },
{ name = "s3fs" },
]
[package.dev-dependencies]
dev = [
{ name = "notebook" },
{ name = "pre-commit" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "matplotlib", specifier = ">=3.10.8" },
{ name = "matplotlib-inline", specifier = ">=0.2.1" },
{ name = "opencv-contrib-python-headless", specifier = "==4.12.0.88" },
{ name = "pandas", specifier = ">=2.3.3" },
{ name = "pillow", specifier = ">=12.2.0" },
{ name = "pyside6", specifier = ">=6.11.0" },
{ name = "python-dotenv", specifier = ">=1.0" },
{ name = "pyyaml", specifier = ">=6.0" },
{ name = "s3fs", specifier = ">=2024.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "notebook", specifier = "~=7.5" },
{ name = "pre-commit", specifier = "~=4.5" },
{ name = "ruff", specifier = "==0.15.0" },
]
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
@@ -349,6 +488,40 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" },
] ]
[[package]]
name = "frozenlist"
version = "1.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" },
{ url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" },
{ url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" },
{ url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" },
{ url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" },
{ url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" },
{ url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" },
{ url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" },
{ url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" },
{ url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" },
{ url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" },
{ url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" },
{ url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" },
{ url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" },
{ url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" },
{ url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" },
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
]
[[package]]
name = "fsspec"
version = "2026.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" },
]
[[package]] [[package]]
name = "h11" name = "h11"
version = "0.16.0" version = "0.16.0"
@@ -498,6 +671,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
] ]
[[package]]
name = "jmespath"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
]
[[package]] [[package]]
name = "json5" name = "json5"
version = "0.14.0" version = "0.14.0"
@@ -811,6 +993,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" },
] ]
[[package]]
name = "multidict"
version = "6.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" },
{ url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" },
{ url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" },
{ url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" },
{ url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" },
{ url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" },
{ url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" },
{ url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" },
{ url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" },
{ url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" },
{ url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" },
{ url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" },
{ url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" },
{ url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" },
{ url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" },
{ url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" },
{ url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" },
{ url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" },
{ url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
]
[[package]] [[package]]
name = "nbclient" name = "nbclient"
version = "0.10.4" version = "0.10.4"
@@ -1072,6 +1281,32 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
] ]
[[package]]
name = "propcache"
version = "0.5.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" },
{ url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" },
{ url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" },
{ url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" },
{ url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" },
{ url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" },
{ url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" },
{ url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" },
{ url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" },
{ url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" },
{ url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" },
{ url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" },
{ url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" },
{ url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" },
{ url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" },
{ url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" },
{ url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" },
{ url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" },
]
[[package]] [[package]]
name = "psutil" name = "psutil"
version = "7.2.2" version = "7.2.2"
@@ -1206,6 +1441,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" },
] ]
[[package]]
name = "python-dotenv"
version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
]
[[package]] [[package]]
name = "python-json-logger" name = "python-json-logger"
version = "4.1.0" version = "4.1.0"
@@ -1326,42 +1570,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" },
] ]
[[package]]
name = "river-annotation-tool"
source = { editable = "." }
dependencies = [
{ name = "matplotlib" },
{ name = "matplotlib-inline" },
{ name = "opencv-contrib-python-headless" },
{ name = "pandas" },
{ name = "pillow" },
{ name = "pyside6" },
]
[package.dev-dependencies]
dev = [
{ name = "notebook" },
{ name = "pre-commit" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "matplotlib", specifier = ">=3.10.8" },
{ name = "matplotlib-inline", specifier = ">=0.2.1" },
{ name = "opencv-contrib-python-headless", specifier = "==4.12.0.88" },
{ name = "pandas", specifier = ">=2.3.3" },
{ name = "pillow", specifier = ">=12.2.0" },
{ name = "pyside6", specifier = ">=6.11.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "notebook", specifier = "~=7.5" },
{ name = "pre-commit", specifier = "~=4.5" },
{ name = "ruff", specifier = "==0.15.0" },
]
[[package]] [[package]]
name = "rpds-py" name = "rpds-py"
version = "0.30.0" version = "0.30.0"
@@ -1410,6 +1618,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" },
] ]
[[package]]
name = "s3fs"
version = "2026.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiobotocore" },
{ name = "aiohttp" },
{ name = "fsspec" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/d8/76f3dc1558bdf4494b117a9f7a9cc0a5d9d34edadc9e5d7ceabc5a6a7c37/s3fs-2026.4.0.tar.gz", hash = "sha256:5bdce0abb00b0435ee150807a45fea727451dbc22de4cbc116464f8504ab9d37", size = 85986, upload-time = "2026-04-29T20:52:51.748Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/a4/9d1ea10ebc9e028a289a72fec84da170689549a8102c8aacfcad26bc5035/s3fs-2026.4.0-py3-none-any.whl", hash = "sha256:de0d2a1f33cdf03831fd2382d278c6e4e31fe57c3bf2f703c61f8aec6b703e2a", size = 32392, upload-time = "2026-04-29T20:52:50.295Z" },
]
[[package]] [[package]]
name = "send2trash" name = "send2trash"
version = "2.1.0" version = "2.1.0"
@@ -1610,3 +1832,54 @@ sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" },
] ]
[[package]]
name = "wrapt"
version = "2.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" },
{ url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" },
{ url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" },
{ url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" },
{ url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" },
{ url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" },
{ url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" },
{ url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" },
{ url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" },
{ url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" },
{ url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" },
{ url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" },
]
[[package]]
name = "yarl"
version = "1.24.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "multidict" },
{ name = "propcache" },
]
sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" },
{ url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" },
{ url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" },
{ url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" },
{ url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" },
{ url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" },
{ url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" },
{ url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" },
{ url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" },
{ url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" },
{ url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" },
{ url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" },
{ url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" },
{ url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" },
{ url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" },
{ url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" },
{ url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" },
{ url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" },
]