diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index e52b9a5..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(Get-ChildItem -Recurse -Depth 2)", - "Bash(Select-Object FullName)" - ] - } -} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..49be4b5 --- /dev/null +++ b/.env.example @@ -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" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index aa9a7e4..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -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 diff --git a/.gitignore b/.gitignore index 588b3eb..2662552 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ *.pyc .ipynb_checkpoints/ *.egg-info/ +.claude/ +.github/ # IDE settings .vscode/ @@ -11,4 +13,12 @@ .DS_Store # Data -data/** \ No newline at end of file +data/** + +# User-specific config (copy from *.example.* files) +config/config.yaml +config/clips.txt +.env + +# Notebooks +notebooks/ diff --git a/README.md b/README.md index 4af6ddd..b6bff51 100644 --- a/README.md +++ b/README.md @@ -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. - -## 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 +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. ## Requirements - Python 3.12 - [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 ```sh -# Clone the repository -git clone -cd river-annotation-tool - -# Install dependencies (creates a virtual environment automatically with uv) +# Install with uv (creates the virtual environment automatically) uv sync # Or with pip python -m venv .venv -.venv\Scripts\activate # Windows -# source .venv/bin/activate # macOS/Linux +.venv\Scripts\activate # Windows +source .venv/bin/activate # macOS/Linux 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 ```sh -python -m river_annotation_tool.annotation_script \ - --data \ - --out \ - [--clip ] +uv run python -m clip_annotator +# or, if you have the venv activated: +python -m clip_annotator ``` +### Arguments + | Argument | Default | Description | |---|---|---| -| `--data` | `../torrent-flow/data/examples_for_annotations/` | Directory containing ZIP files | -| `--out` | `data/annotation_results/` | Output directory for saved annotations | -| `--clip` | *(first clip)* | Specific clip to open (e.g. `left_20230501`) | +| `--config` | `config/config.yaml` | Path to the config YAML file | +| `--data` | *(from config)* | Override `data_dir` from config | +| `--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 `//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 | |---|---| -| Draw mask | Click and drag on the canvas | -| Erase mask | Toggle **Eraser** button, then drag | -| Undo last stroke | **Undo** button | -| Play/pause frames | **Play / Pause** button | -| Save annotation | **Save** button | -| Change brush size | Slider in the toolbar | +| Draw mask | Click and drag on the video | +| Erase mask | Toggle **Eraser** button (turns orange when active), then drag | +| Brush preview | A white circle follows the cursor showing the current brush size | +| Adjust brush size | **Brush size** slider (2–50 px, default 5); click **↺** to reset | + +### 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 (0–100%, 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 -Each clip is saved to `//`: +Each annotated clip produces a folder `//` with: ``` -mask.png # Binary mask at full resolution -metadata.json # Survey answers -frame.png # Key frame -mask_vis.png # Mask visualisation -overlay.png # Frame + mask overlay -video_original_hires.gif -video_original_lowres.gif -video_overlay_hires.gif -video_overlay_lowres.gif +mask.png # Binary segmentation mask at full source resolution (always) +metadata.json # Survey answers as JSON (always) +frame.png # Middle frame of the clip (always) +overlay.png # That frame with the mask blended in green (always) + +# Only with --extras: +mask_vis.png # Mask rendered as a greyscale PNG +video_original_hires.gif # All frames at display resolution +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` → `/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/ - annotation_script.py # Main GUI application - __init__.py # Package version -pyproject.toml # Project metadata and dependencies -requirements.txt # Pinned dependencies (generated) +.env.example # S3 credential template (copy to .env and fill in) +config/ + config.yaml # Your local config (git-ignored, copy from example) + config.example.yaml # Example config to copy and edit + 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 ```sh # Install pre-commit hooks -pre-commit install -pre-commit run --all-files # Run hooks manually once +uv run pre-commit install +uv run pre-commit run --all-files # Run manually once # Add a dependency uv add -uv add --dev # Development-only +uv add --dev # Development-only ``` diff --git a/config/annotator_A.txt b/config/annotator_A.txt new file mode 100644 index 0000000..984b0a3 --- /dev/null +++ b/config/annotator_A.txt @@ -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 diff --git a/config/annotator_B.txt b/config/annotator_B.txt new file mode 100644 index 0000000..e073659 --- /dev/null +++ b/config/annotator_B.txt @@ -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 diff --git a/config/annotator_C.txt b/config/annotator_C.txt new file mode 100644 index 0000000..e0ffda5 --- /dev/null +++ b/config/annotator_C.txt @@ -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 diff --git a/config/annotator_D.txt b/config/annotator_D.txt new file mode 100644 index 0000000..093c4af --- /dev/null +++ b/config/annotator_D.txt @@ -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 diff --git a/config/annotator_E.txt b/config/annotator_E.txt new file mode 100644 index 0000000..00a66ab --- /dev/null +++ b/config/annotator_E.txt @@ -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 diff --git a/config/annotator_F.txt b/config/annotator_F.txt new file mode 100644 index 0000000..6423d0f --- /dev/null +++ b/config/annotator_F.txt @@ -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 diff --git a/config/annotator_G.txt b/config/annotator_G.txt new file mode 100644 index 0000000..b164101 --- /dev/null +++ b/config/annotator_G.txt @@ -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 diff --git a/config/clips.example.txt b/config/clips.example.txt new file mode 100644 index 0000000..4491270 --- /dev/null +++ b/config/clips.example.txt @@ -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 diff --git a/config/config.example.yaml b/config/config.example.yaml new file mode 100644 index 0000000..c197db8 --- /dev/null +++ b/config/config.example.yaml @@ -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// # 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 diff --git a/config/optical_flow_config.yaml b/config/optical_flow_config.yaml new file mode 100644 index 0000000..3049af6 --- /dev/null +++ b/config/optical_flow_config.yaml @@ -0,0 +1,4 @@ +enabled: true +norm_squared_threshold: 0.06 +gaussian_kernel: [5, 5] +brightness_range: [2, 253] diff --git a/config/questions.yaml b/config/questions.yaml new file mode 100644 index 0000000..4a5740c --- /dev/null +++ b/config/questions.yaml @@ -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 diff --git a/data/.gitkeep b/data/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/notebooks/.gitkeep b/notebooks/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/notebooks/annotation_segmentation.ipynb b/notebooks/annotation_segmentation.ipynb deleted file mode 100644 index bbb4814..0000000 --- a/notebooks/annotation_segmentation.ipynb +++ /dev/null @@ -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 -} diff --git a/pyproject.toml b/pyproject.toml index 4153de8..83d953d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=61.0.0"] build-backend = "setuptools.build_meta" [project] -name = "river_annotation_tool" +name = "clip_annotator" authors = [ # TODO configure authors # { name = "Jane Smith", email = "jane.smith@example.com" }, @@ -19,6 +19,9 @@ dependencies = [ "matplotlib-inline>=0.2.1", "pillow>=12.2.0", "pyside6>=6.11.0", + "python-dotenv>=1.0", + "pyyaml>=6.0", + "s3fs>=2024.0", ] dynamic = ["version"] @@ -30,7 +33,7 @@ dev = [ ] [tool.setuptools.dynamic] -version = {attr = "river_annotation_tool.__version__"} +version = {attr = "clip_annotator.__version__"} [tool.ruff] target-version = "py312" diff --git a/requirements.txt b/requirements.txt index de6f95a..014f5d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/src/river_annotation_tool/__init__.py b/src/clip_annotator/__init__.py similarity index 100% rename from src/river_annotation_tool/__init__.py rename to src/clip_annotator/__init__.py diff --git a/src/clip_annotator/__main__.py b/src/clip_annotator/__main__.py new file mode 100644 index 0000000..68c0cf4 --- /dev/null +++ b/src/clip_annotator/__main__.py @@ -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() diff --git a/src/clip_annotator/annotator.py b/src/clip_annotator/annotator.py new file mode 100644 index 0000000..c6adcee --- /dev/null +++ b/src/clip_annotator/annotator.py @@ -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) diff --git a/src/clip_annotator/clip_selector.py b/src/clip_annotator/clip_selector.py new file mode 100644 index 0000000..a73b71f --- /dev/null +++ b/src/clip_annotator/clip_selector.py @@ -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") diff --git a/src/clip_annotator/compute_optical_flow.py b/src/clip_annotator/compute_optical_flow.py new file mode 100644 index 0000000..308fe7f --- /dev/null +++ b/src/clip_annotator/compute_optical_flow.py @@ -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) diff --git a/src/clip_annotator/config.py b/src/clip_annotator/config.py new file mode 100644 index 0000000..7c4ce5c --- /dev/null +++ b/src/clip_annotator/config.py @@ -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 diff --git a/src/clip_annotator/filesystem.py b/src/clip_annotator/filesystem.py new file mode 100644 index 0000000..b63ba16 --- /dev/null +++ b/src/clip_annotator/filesystem.py @@ -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] diff --git a/src/clip_annotator/mask_canvas.py b/src/clip_annotator/mask_canvas.py new file mode 100644 index 0000000..c52e167 --- /dev/null +++ b/src/clip_annotator/mask_canvas.py @@ -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 diff --git a/src/clip_annotator/video_loader.py b/src/clip_annotator/video_loader.py new file mode 100644 index 0000000..8c866cb --- /dev/null +++ b/src/clip_annotator/video_loader.py @@ -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 diff --git a/src/river_annotation_tool/annotation_script.py b/src/river_annotation_tool/annotation_script.py deleted file mode 100644 index 7c3f385..0000000 --- a/src/river_annotation_tool/annotation_script.py +++ /dev/null @@ -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() diff --git a/uv.lock b/uv.lock index f5ece01..ff8edcc 100644 --- a/uv.lock +++ b/uv.lock @@ -7,6 +7,89 @@ resolution-markers = [ "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]] name = "anyio" version = "4.13.0" @@ -141,6 +224,20 @@ css = [ { 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]] name = "certifi" 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" }, ] +[[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]] name = "colorama" 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" }, ] +[[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]] name = "h11" 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" }, ] +[[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]] name = "json5" 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" }, ] +[[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]] name = "nbclient" 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" }, ] +[[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]] name = "psutil" 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" }, ] +[[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]] name = "python-json-logger" 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" }, ] -[[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]] name = "rpds-py" 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" }, ] +[[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]] name = "send2trash" version = "2.1.0" @@ -1610,3 +1832,54 @@ sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c 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" }, ] + +[[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" }, +]