Address PR review comments: rename to __main__.py, improve config examples, document save freeze

This commit is contained in:
2026-06-02 11:51:37 +02:00
parent 69eab7514f
commit dc7bb8e7eb
4 changed files with 29 additions and 19 deletions

View File

@@ -24,7 +24,7 @@ cp config/clips.example.txt config/clips.txt
# Edit config/questions.yaml to customise survey questions (optional) # Edit config/questions.yaml to customise survey questions (optional)
# 4. Run # 4. Run
uv run python -m clip_annotator.annotation_script uv run python -m clip_annotator
``` ```
## Installation ## Installation
@@ -81,9 +81,9 @@ The `clips_file` (the list of clip filenames to annotate) is always read from th
## Usage ## Usage
```sh ```sh
uv run python -m clip_annotator.annotation_script uv run python -m clip_annotator
# or, if you have the venv activated: # or, if you have the venv activated:
python -m clip_annotator.annotation_script python -m clip_annotator
``` ```
### Arguments ### Arguments
@@ -95,23 +95,23 @@ python -m clip_annotator.annotation_script
| `--out` | *(from config)* | Override `out_dir` from config | | `--out` | *(from config)* | Override `out_dir` from config |
| `--clips` | *(from config)* | Override `clips_file` 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`) | | `--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) | | `--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 | | `--no-skip` | off | Show already-annotated clips instead of skipping them |
### Typical workflows ### Typical workflows
```sh ```sh
# Annotate clips listed in config/clips.txt (default) # Annotate clips listed in config/clips.txt (default)
uv run python -m clip_annotator.annotation_script uv run python -m clip_annotator
# Use a different config file # Use a different config file
uv run python -m clip_annotator.annotation_script --config config/my_config.yaml uv run python -m clip_annotator --config config/my_config.yaml
# Override paths from the command line # Override paths from the command line
uv run python -m clip_annotator.annotation_script --data data/clips --out data/out uv run python -m clip_annotator --data data/clips --out data/out
# Annotate a single specific clip # Annotate a single specific clip
uv run python -m clip_annotator.annotation_script --clip clip_20230615T120000 uv run python -m clip_annotator --clip clip_20230615T120000
``` ```
## Configuration ## Configuration
@@ -179,7 +179,7 @@ Copy `config/clips.example.txt` as a starting point.
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`: 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 ```sh
uv run python -m clip_annotator.annotation_script --clips config/annotator_A.txt 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. Assigning non-overlapping clip lists lets each annotator work independently. Intentionally overlapping a subset of clips across annotators enables inter-annotator agreement checks.
@@ -263,7 +263,7 @@ Click **↺** below any slider to restore its default value.
| Action | How | | 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. | | 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. | | 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. | | Skip without saving | **Skip** — discards any unsaved changes and loads the next clip without writing anything to disk. |
@@ -335,7 +335,7 @@ config/
questions.yaml # Survey question definitions questions.yaml # Survey question definitions
optical_flow_config.yaml # Optical flow parameters (set enabled: false to disable Auto Segment) optical_flow_config.yaml # Optical flow parameters (set enabled: false to disable Auto Segment)
src/clip_annotator/ src/clip_annotator/
annotation_script.py # Entry point — argument parsing and app launch __main__.py # Entry point — argument parsing and app launch
annotator.py # Main QMainWindow — orchestrates all components annotator.py # Main QMainWindow — orchestrates all components
clip_selector.py # Reads the clip list and picks the next clip clip_selector.py # Reads the clip list and picks the next clip
filesystem.py # Storage backend — local passthrough or S3 via s3fs filesystem.py # Storage backend — local passthrough or S3 via s3fs

View File

@@ -1,8 +1,8 @@
storage: local # 'local' or 's3' storage: local # 'local' or 's3'
# Required: set these to your actual paths (local path or bucket/prefix for S3) # Required: set these to your actual paths (local path or bucket/prefix for S3)
data_dir: data_dir: # e.g. /data/clips or for S3: hydroscan-data/GRAMMONT/clips
out_dir: out_dir: # e.g. /data/out or for S3: hydroscan-data/annotations/<name>/ # Put your name here
# For S3 credentials, copy .env.example to .env and fill in: # For S3 credentials, copy .env.example to .env and fill in:
# S3_ACCESS_KEY, S3_SECRET_ACCESS_KEY, S3_ENDPOINT_URL # S3_ACCESS_KEY, S3_SECRET_ACCESS_KEY, S3_ENDPOINT_URL

View File

@@ -159,7 +159,7 @@ class Annotator(QMainWindow):
self.btn_prev = QPushButton("Previous") self.btn_prev = QPushButton("Previous")
self.btn_prev.setEnabled(False) self.btn_prev.setEnabled(False)
btn_next = QPushButton("Next") self.btn_next = QPushButton("Next")
btn_skip = QPushButton("Skip") btn_skip = QPushButton("Skip")
btn_clear = QPushButton("Clear") btn_clear = QPushButton("Clear")
btn_undo = QPushButton("Undo") btn_undo = QPushButton("Undo")
@@ -172,7 +172,7 @@ class Annotator(QMainWindow):
row1 = QHBoxLayout() row1 = QHBoxLayout()
for b in [ for b in [
self.btn_prev, self.btn_prev,
btn_next, self.btn_next,
btn_skip, btn_skip,
btn_load_prev_mask, btn_load_prev_mask,
btn_auto_segment, btn_auto_segment,
@@ -251,7 +251,7 @@ class Annotator(QMainWindow):
self.setCentralWidget(container) self.setCentralWidget(container)
self.btn_prev.clicked.connect(self.prev_clip) self.btn_prev.clicked.connect(self.prev_clip)
btn_next.clicked.connect(self.next_clip) self.btn_next.clicked.connect(self.next_clip)
btn_skip.clicked.connect(self.skip_clip) btn_skip.clicked.connect(self.skip_clip)
btn_clear.clicked.connect(self.mc.clear) btn_clear.clicked.connect(self.mc.clear)
btn_undo.clicked.connect(self.mc.undo) btn_undo.clicked.connect(self.mc.undo)
@@ -350,6 +350,16 @@ class Annotator(QMainWindow):
self.fs.pipe(out_path, buf.getvalue()) self.fs.pipe(out_path, buf.getvalue())
# ── actions ──────────────────────────────────────────────────── # ── 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): def save(self):
out = fsjoin(self.out_dir, fsstem(self.filename)) out = fsjoin(self.out_dir, fsstem(self.filename))
self._fs_makedirs(out) self._fs_makedirs(out)
@@ -421,7 +431,7 @@ class Annotator(QMainWindow):
def prev_clip(self): def prev_clip(self):
if self.history_pos <= 0: if self.history_pos <= 0:
return return
self.save() self._save_locked()
self.history_pos -= 1 self.history_pos -= 1
self._load_clip(path=self.history[self.history_pos]) self._load_clip(path=self.history[self.history_pos])
self._switch_ui_to_clip() self._switch_ui_to_clip()
@@ -446,13 +456,13 @@ class Annotator(QMainWindow):
msg.exec() msg.exec()
clicked = msg.clickedButton() clicked = msg.clickedButton()
if clicked == btn_replace: if clicked == btn_replace:
self.save() self._save_locked()
self._advance_clip() self._advance_clip()
elif clicked == btn_keep: elif clicked == btn_keep:
self._advance_clip() self._advance_clip()
# Cancel: do nothing # Cancel: do nothing
else: else:
self.save() self._save_locked()
self._advance_clip() self._advance_clip()
def skip_clip(self): def skip_clip(self):