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)
# 4. Run
uv run python -m clip_annotator.annotation_script
uv run python -m clip_annotator
```
## Installation
@@ -81,9 +81,9 @@ The `clips_file` (the list of clip filenames to annotate) is always read from th
## Usage
```sh
uv run python -m clip_annotator.annotation_script
uv run python -m clip_annotator
# or, if you have the venv activated:
python -m clip_annotator.annotation_script
python -m clip_annotator
```
### Arguments
@@ -95,23 +95,23 @@ python -m clip_annotator.annotation_script
| `--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) |
| `--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 |
### Typical workflows
```sh
# 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
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
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
uv run python -m clip_annotator.annotation_script --clip clip_20230615T120000
uv run python -m clip_annotator --clip clip_20230615T120000
```
## 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`:
```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.
@@ -263,7 +263,7 @@ Click **↺** below any slider to restore its default value.
| 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. |
| 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
optical_flow_config.yaml # Optical flow parameters (set enabled: false to disable Auto Segment)
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
clip_selector.py # Reads the clip list and picks the next clip
filesystem.py # Storage backend — local passthrough or S3 via s3fs

View File

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

View File

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