structflo-cser 0.4.0__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- structflo_cser-0.4.2/.dockerignore +19 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/.gitignore +2 -0
- structflo_cser-0.4.2/.gitlab-ci.yml +34 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/CLAUDE.md +17 -5
- structflo_cser-0.4.2/Dockerfile +41 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/PKG-INFO +2 -1
- structflo_cser-0.4.2/docs/REPRODUCIBILITY_HANDOFF.md +272 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/pyproject.toml +3 -1
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/relmatch/eval_compare_all.py +17 -3
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/relmatch/prepare_det_data.py +17 -3
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/yolo/eval_compare.py +5 -1
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/yolo/train.sh +6 -2
- structflo_cser-0.4.2/scripts/repro/aggregate.py +285 -0
- structflo_cser-0.4.2/scripts/repro/aggregate_c01.py +115 -0
- structflo_cser-0.4.2/scripts/repro/aggregate_confsweep.py +115 -0
- structflo_cser-0.4.2/scripts/repro/aggregate_scale.py +155 -0
- structflo_cser-0.4.2/scripts/repro/build_release.py +314 -0
- structflo_cser-0.4.2/scripts/repro/eval_detector.py +99 -0
- structflo_cser-0.4.2/scripts/repro/eval_lps_acc.py +66 -0
- structflo_cser-0.4.2/scripts/repro/label_conf_sweep.py +132 -0
- structflo_cser-0.4.2/scripts/repro/make_synth_manifest.py +57 -0
- structflo_cser-0.4.2/scripts/repro/prepare_finetune_plus.py +135 -0
- structflo_cser-0.4.2/scripts/repro/prepare_scale_subset.py +158 -0
- structflo_cser-0.4.2/scripts/repro/run_all.sh +20 -0
- structflo_cser-0.4.2/scripts/repro/run_c01_sweep.sh +37 -0
- structflo_cser-0.4.2/scripts/repro/run_conf010.sh +36 -0
- structflo_cser-0.4.2/scripts/repro/run_confsweep_val.sh +23 -0
- structflo_cser-0.4.2/scripts/repro/run_eval.sh +103 -0
- structflo_cser-0.4.2/scripts/repro/run_finetune_plus.sh +62 -0
- structflo_cser-0.4.2/scripts/repro/run_relmatch_matched.sh +55 -0
- structflo_cser-0.4.2/scripts/repro/run_relmatch_plus.sh +45 -0
- structflo_cser-0.4.2/scripts/repro/run_scale.sh +104 -0
- structflo_cser-0.4.2/scripts/repro/run_train.sh +114 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/lps/matcher.py +10 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/pipeline/__init__.py +5 -2
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/pipeline/matcher.py +9 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/pipeline/models.py +19 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/pipeline/pipeline.py +130 -3
- structflo_cser-0.4.2/structflo/cser/pipeline/version.py +152 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/relmatch/matcher.py +10 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/training/trainer.py +25 -4
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/weights.py +83 -3
- structflo_cser-0.4.2/tests/test_page_api.py +65 -0
- structflo_cser-0.4.2/tests/test_webapp.py +76 -0
- structflo_cser-0.4.2/tests/test_weights_info.py +88 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/uv.lock +23 -0
- structflo_cser-0.4.2/webapp/__init__.py +0 -0
- structflo_cser-0.4.2/webapp/__main__.py +4 -0
- structflo_cser-0.4.2/webapp/server.py +210 -0
- structflo_cser-0.4.2/webapp/static/tokens.css +116 -0
- structflo_cser-0.4.2/webapp/templates/index.html +313 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/.github/workflows/ci.yml +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/.github/workflows/publish.yml +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/.python-version +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/Makefile +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/README.md +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/annotate/__main__.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/annotate/pdf.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/annotate/server.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/annotate/storage.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/annotate/templates/index.html +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/asset_scripts/download_chembl.sh +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/config/data.yaml +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/config/pipeline.yaml +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/docs/fine-tune.md +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/docs/images/example-1.png +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/docs/images/example-2.png +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/docs/learned_matcher_plan.md +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/docs/lps.md +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/docs/publishing-weights.md +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/main.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/01-quickstart.ipynb +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/02-LPS.ipynb +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/03-PDF.ipynb +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/notebook-data/bio-arcgive-1.png +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/notebook-data/example-annotated.pdf +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/notebook-data/example.pdf +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/notebook-data/example.pptx +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/notebook-data/screen-1.png +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/notebook-data/syn-1.jpg +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/quick.md +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/diag_e2e_decompose.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/diag_label_recall.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/diag_lps_scores.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/eval_compare.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/eval_end2end.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/eval_rejection.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/mine_fp_negatives.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/prepare_data.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/train.sh +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/relmatch/eval_compare.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/relmatch/sweep_margin.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/yolo/prepare_data.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/publish_weights.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/__init__.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/_geometry.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/config.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/data/__init__.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/data/distractor_images.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/data/smiles.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/distractors/__init__.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/distractors/charts.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/distractors/shapes.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/distractors/text_elements.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/generation/__init__.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/generation/dataset.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/generation/page.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/generation/specialty.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/generation/tabular.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/inference/__init__.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/inference/detector.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/inference/nms.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/inference/pairing.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/inference/tiling.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/lps/__init__.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/lps/dataset.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/lps/evaluate.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/lps/features.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/lps/scorer.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/lps/train.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/pipeline/cli.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/pipeline/ocr.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/pipeline/smiles_extractor.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/relmatch/__init__.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/relmatch/dataset.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/relmatch/features.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/relmatch/model.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/relmatch/train.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/rendering/__init__.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/rendering/chemistry.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/rendering/text.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/training/__init__.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/viz/__init__.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/viz/detections.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/viz/labels.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/tests/__init__.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/tests/test_config.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/tests/test_generation.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/tests/test_geometry.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/tests/test_imports.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/tests/test_inference.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/tests/test_models.py +0 -0
- {structflo_cser-0.4.0 → structflo_cser-0.4.2}/tests/test_viz.py +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.git
|
|
2
|
+
.github
|
|
3
|
+
.venv
|
|
4
|
+
.ruff_cache
|
|
5
|
+
.pytest_cache
|
|
6
|
+
__pycache__
|
|
7
|
+
*.pyc
|
|
8
|
+
|
|
9
|
+
# training data, model runs, and notebook assets — none of it belongs in the image
|
|
10
|
+
data/
|
|
11
|
+
runs/
|
|
12
|
+
detections/
|
|
13
|
+
archive/
|
|
14
|
+
notebooks/
|
|
15
|
+
docs/
|
|
16
|
+
scripts/
|
|
17
|
+
asset_scripts/
|
|
18
|
+
tests/
|
|
19
|
+
*.pt
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Lab deployment image. GitHub (structflo/structflo-cser) stays the source of
|
|
2
|
+
# truth; this mirror exists to build for the SACNet swarm registry.
|
|
3
|
+
image: docker:latest
|
|
4
|
+
|
|
5
|
+
services:
|
|
6
|
+
- docker:dind
|
|
7
|
+
|
|
8
|
+
variables:
|
|
9
|
+
DOCKER_DRIVER: overlay2
|
|
10
|
+
IMAGE_TAG: "$CI_REGISTRY/sid/structflo-cser:$CI_COMMIT_REF_NAME"
|
|
11
|
+
GIT_DEPTH: 0 # git describe needs the tag history
|
|
12
|
+
|
|
13
|
+
stages:
|
|
14
|
+
- build
|
|
15
|
+
|
|
16
|
+
# Build and push in one job on purpose: separate jobs get separate dind
|
|
17
|
+
# daemons, so an image built in one is not present in the other.
|
|
18
|
+
build:
|
|
19
|
+
stage: build
|
|
20
|
+
script:
|
|
21
|
+
- docker info
|
|
22
|
+
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
|
|
23
|
+
# VERSION feeds SETUPTOOLS_SCM_PRETEND_VERSION. The image has no .git, and
|
|
24
|
+
# weights.py refuses to load cser-relmatcher below 0.3.0 — an image built
|
|
25
|
+
# without a real version looks fine until the first upload fails.
|
|
26
|
+
- apk add --no-cache git
|
|
27
|
+
- git fetch --tags --force || true
|
|
28
|
+
- VERSION=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//')
|
|
29
|
+
- test -n "$VERSION" || { echo "no git tag reachable; cannot stamp a version"; exit 1; }
|
|
30
|
+
- echo "building $IMAGE_TAG at version $VERSION"
|
|
31
|
+
- docker build --build-arg VERSION="$VERSION" -t $IMAGE_TAG .
|
|
32
|
+
- docker push $IMAGE_TAG
|
|
33
|
+
only:
|
|
34
|
+
- main
|
|
@@ -10,9 +10,11 @@ strings and OCR text.
|
|
|
10
10
|
## Package name & layout
|
|
11
11
|
|
|
12
12
|
- **PyPI package**: `structflo-cser`
|
|
13
|
-
- **Top-level packages** (wheel): `structflo`, `annotate`
|
|
13
|
+
- **Top-level packages** (wheel): `structflo`, `annotate`, `webapp`
|
|
14
14
|
- **Source root**: `structflo/cser/` — all library code lives here
|
|
15
15
|
- **Annotate tool**: `annotate/` — Flask web app for manual bbox annotation
|
|
16
|
+
- **Extraction UI**: `webapp/` — Flask web app for running the pipeline on an upload
|
|
17
|
+
(no auth; expects a reverse proxy in front)
|
|
16
18
|
|
|
17
19
|
### Module map
|
|
18
20
|
|
|
@@ -77,6 +79,10 @@ annotate/
|
|
|
77
79
|
pdf.py PDF page rendering for annotation
|
|
78
80
|
storage.py Annotation JSON storage
|
|
79
81
|
templates/ HTML templates
|
|
82
|
+
|
|
83
|
+
webapp/
|
|
84
|
+
server.py Flask routes + CLI (upload → NDJSON stream of pairs per page)
|
|
85
|
+
templates/ Single-page UI: page overlay + structure→SMILES cards
|
|
80
86
|
```
|
|
81
87
|
|
|
82
88
|
## CLI entry points (registered in pyproject.toml)
|
|
@@ -93,6 +99,7 @@ annotate/
|
|
|
93
99
|
| `sf-annotate` | `annotate.__main__:main` | Manual annotation web tool |
|
|
94
100
|
| `sf-train-lps` | `structflo.cser.lps.train:main` | Train Learned Pair Scorer |
|
|
95
101
|
| `sf-eval-lps` | `structflo.cser.lps.evaluate:main` | Evaluate LPS model |
|
|
102
|
+
| `sf-web` | `webapp.server:main` | Upload → IDs + SMILES web UI |
|
|
96
103
|
|
|
97
104
|
## Key public API
|
|
98
105
|
|
|
@@ -144,10 +151,15 @@ ChemPipeline.to_records(pairs)
|
|
|
144
151
|
Weights are versioned independently of the package and stored on HuggingFace Hub.
|
|
145
152
|
`structflo.cser.weights.resolve_weights(model, version)` handles auto-download + caching.
|
|
146
153
|
|
|
147
|
-
| Model
|
|
148
|
-
|
|
149
|
-
| cser-detector
|
|
150
|
-
| cser-lps
|
|
154
|
+
| Model | HF Repo | Latest |
|
|
155
|
+
|-----------------|----------------------------------|--------|
|
|
156
|
+
| cser-detector | sidxz/structflo-cser-detector | v0.4 |
|
|
157
|
+
| cser-lps | sidxz/structflo-cser-lps | v0.3 |
|
|
158
|
+
| cser-relmatcher | sidxz/structflo-cser-relmatcher | v0.2 |
|
|
159
|
+
|
|
160
|
+
v0.4 / v0.3 / v0.2 were trained on the 914-doc real corpus (the original 830 + 84 newly
|
|
161
|
+
annotated docs, of which 60 complete). Earlier releases: detector v0.3 / lps v0.2 / relmatcher v0.1
|
|
162
|
+
(830-doc corpus).
|
|
151
163
|
|
|
152
164
|
Publish script: `scripts/publish_weights.py`
|
|
153
165
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
FROM python:3.12-slim
|
|
2
|
+
|
|
3
|
+
# rdkit + opencv (via ultralytics/easyocr) need libGL and glib at runtime.
|
|
4
|
+
# libxrender1/libxext6 are for rdkit.Chem.Draw, which webapp/server.py imports
|
|
5
|
+
# lazily to re-render each extracted SMILES: without them the app boots clean and
|
|
6
|
+
# then fails on the first page that actually finds a compound.
|
|
7
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
8
|
+
libgl1 libglib2.0-0 libxrender1 libxext6 \
|
|
9
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
10
|
+
|
|
11
|
+
WORKDIR /app
|
|
12
|
+
|
|
13
|
+
# The package version comes from git tags via hatch-vcs, and .git is not in the
|
|
14
|
+
# build context (see .dockerignore). Without this the build fails outright, and
|
|
15
|
+
# a wrong version silently trips the weights compatibility gate in weights.py —
|
|
16
|
+
# cser-relmatcher requires >=0.3.0. CI passes the release tag.
|
|
17
|
+
ARG VERSION=0.0.0
|
|
18
|
+
ENV SETUPTOOLS_SCM_PRETEND_VERSION=$VERSION
|
|
19
|
+
|
|
20
|
+
COPY pyproject.toml README.md ./
|
|
21
|
+
COPY structflo/ ./structflo/
|
|
22
|
+
COPY annotate/ ./annotate/
|
|
23
|
+
COPY webapp/ ./webapp/
|
|
24
|
+
RUN pip install --no-cache-dir .
|
|
25
|
+
|
|
26
|
+
# Every model this app uses is downloaded on first run: YOLO detector + LPS +
|
|
27
|
+
# relmatcher from HF Hub, DECIMER via pystow, EasyOCR's craft/english weights.
|
|
28
|
+
# That is several GB, so point them all at one directory and mount it — a bare
|
|
29
|
+
# container re-downloads the lot on every start.
|
|
30
|
+
ENV HF_HOME=/cache/huggingface \
|
|
31
|
+
PYSTOW_HOME=/cache/pystow \
|
|
32
|
+
EASYOCR_MODULE_PATH=/cache/easyocr \
|
|
33
|
+
MPLCONFIGDIR=/cache/matplotlib
|
|
34
|
+
VOLUME /cache
|
|
35
|
+
|
|
36
|
+
EXPOSE 8000
|
|
37
|
+
|
|
38
|
+
ENTRYPOINT ["sf-web"]
|
|
39
|
+
# --preload pays the model-loading cost at startup instead of on the first
|
|
40
|
+
# upload. Drop it if you would rather the container come up immediately.
|
|
41
|
+
CMD ["--host", "0.0.0.0", "--port", "8000", "--preload"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structflo-cser
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Chemical structure-label pair extraction from scientific documents.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: chembl-webresource-client>=0.10.9
|
|
@@ -11,6 +11,7 @@ Requires-Dist: huggingface-hub>=0.24.0
|
|
|
11
11
|
Requires-Dist: matplotlib>=3.10.8
|
|
12
12
|
Requires-Dist: numpy>=2.4.2
|
|
13
13
|
Requires-Dist: opencv-python-headless>=4.13.0.92
|
|
14
|
+
Requires-Dist: openpyxl>=3.1
|
|
14
15
|
Requires-Dist: packaging>=23.0
|
|
15
16
|
Requires-Dist: pandas>=2.0.0
|
|
16
17
|
Requires-Dist: pillow>=12.1.1
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Reproducibility Handoff — Retrain matchers (≥3 seeds) & re-verify all paper numbers
|
|
2
|
+
|
|
3
|
+
**Created:** 2026-06-01. **For:** a fresh session.
|
|
4
|
+
**Goal:** Remove all weight-provenance ambiguity and establish **reproducible, multi-seed
|
|
5
|
+
(≥3)** results for the structure–label matcher benchmark (and the detection/LPS numbers it
|
|
6
|
+
depends on), cleanly separated into the paper's two sections. Report **mean ± std** across
|
|
7
|
+
training seeds.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 0. Why this handoff exists (the problem to fix)
|
|
12
|
+
|
|
13
|
+
During paper review we could **not determine from artifacts** whether the existing relational
|
|
14
|
+
checkpoints were trained on synthetic-only vs real+synthetic data:
|
|
15
|
+
|
|
16
|
+
- `runs/relmatch` — **GT-box-trained** variant (clean geometry). `sf-train-relmatch`'s
|
|
17
|
+
**default `--data-dir` is `data/finetune/lps` (real+synthetic)** and its default output is
|
|
18
|
+
`runs/relmatch`, so this checkpoint was *most likely trained on real+synthetic*, **not**
|
|
19
|
+
synthetic-only. Unconfirmed.
|
|
20
|
+
- `runs/relmatch_det` — **detection-box-trained** variant. Trained on
|
|
21
|
+
`data/finetune/relmatch_det` (1,242 real + 1,834 synthetic json). This is the **PUBLISHED**
|
|
22
|
+
matcher (`cser-relmatcher` v0.1 = `runs/relmatch_det/best.pt`; the `ChemPipeline` default).
|
|
23
|
+
- Checkpoints store no `data_dir`; no run logs survive. Hence the ambiguity.
|
|
24
|
+
|
|
25
|
+
This matters because the paper is organized as:
|
|
26
|
+
- **§A "Performance on Synthetic Data"** → must use **synthetic-only** weights, evaluated on a
|
|
27
|
+
held-out **synthetic test set @1280 px**.
|
|
28
|
+
- **§B "Generalization to Internal Documents"** → must use the **real-fine-tuned / published**
|
|
29
|
+
weights (baseline = synthetic → fine-tuned improvement, on the real test set).
|
|
30
|
+
|
|
31
|
+
If `runs/relmatch` is actually real-trained, then §A's relational row currently uses the wrong
|
|
32
|
+
(real) weights, and there is **no synthetic-only relational matcher at all**. The retrain
|
|
33
|
+
resolves this by training every matcher **from scratch with an explicit `--data-dir` and an
|
|
34
|
+
explicit `--seed`**, logging everything.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 1. The principle (don't deviate)
|
|
39
|
+
|
|
40
|
+
| Section | Weights to use | Eval data |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| §A synthetic | **synthetic-only** (`--data-dir data/generated`) | synthetic **test** set, @1280 px |
|
|
43
|
+
| §B internal | **real-fine-tuned / published** (mix `data/finetune/*`) | real **test** set (100 pages), @1280 px |
|
|
44
|
+
|
|
45
|
+
- All detection/e2e evaluation at **imgsz 1280** (the deployment resolution). Do **not** rely on
|
|
46
|
+
`model.val()`'s default imgsz — it silently uses each checkpoint's *training* imgsz (base
|
|
47
|
+
detector = 2048, fine-tuned = 1280), which previously confounded a baseline-vs-finetuned
|
|
48
|
+
comparison. Always pass `imgsz=1280` explicitly. (`scripts/finetune/yolo/eval_compare.py`
|
|
49
|
+
already pins it.)
|
|
50
|
+
- §A draws **no matcher conclusions** (matchers saturate in-distribution); all matcher
|
|
51
|
+
conclusions are on real data in §B. (User decision.)
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 2. Current single-seed numbers (the baseline to reproduce)
|
|
56
|
+
|
|
57
|
+
These are what's in the paper draft now (`docs/paper_draft.md`, gitignored). The retrain should
|
|
58
|
+
reproduce them within seed variance; **mean ± std replaces the point values**.
|
|
59
|
+
|
|
60
|
+
**§A synthetic TEST set (1,000 pages, seed 1000000, WITH distractors), @1280:**
|
|
61
|
+
- *Table 1 detection (base `yolo11l_panels`):* structure 0.998/1.000/0.995/0.990 ·
|
|
62
|
+
label 0.993/0.992/0.995/0.890 · all 0.995/0.996/0.995/0.940 (P/R/mAP50/mAP50-95)
|
|
63
|
+
- *Table 2 matching (synthetic-only weights — relational here used `runs/relmatch`, TO BE
|
|
64
|
+
RE-ESTABLISHED as truly synthetic):* clean assign/reject/prec + e2e P/R/F1 —
|
|
65
|
+
Distance 99.0/89.8/99.0 · 0.971/0.974/0.973 ;
|
|
66
|
+
Relational 98.9/86.6/98.9 · 0.977/0.977/0.977 ;
|
|
67
|
+
LPS 97.0/94.1/99.5 · 0.991/0.944/0.967
|
|
68
|
+
|
|
69
|
+
**§B real TEST set (100 pages; 247 labelled, 25 unlabelled), @1280:**
|
|
70
|
+
- *Table 3 detector:* base→FT mAP50 0.691→0.871, mAP50-95 0.404→0.552, P 0.668→0.850,
|
|
71
|
+
R 0.755→0.897. Synthetic regression (synth test): mAP50 0.995→0.995, mAP50-95 0.940→0.953.
|
|
72
|
+
- *Table 3 LPS pair-classification:* real test 85.2%→90.7% ; synth test 96.7%→96.2%.
|
|
73
|
+
- *Table 4 clean matching:* Distance 99.6/96.0/99.6 ; LPS 91.1/96.0/98.3 ;
|
|
74
|
+
Relational **with published `relmatch_det`** 98.0/96.0/99.6 **(current paper value)** ;
|
|
75
|
+
Relational with GT-trained `runs/relmatch` 99.2/100.0/100.0 (alternative — unpublished variant).
|
|
76
|
+
- *Table 5 e2e:* Distance 0.815/0.818/0.816 ; LPS 0.842/0.713/0.772 ;
|
|
77
|
+
Relational (`relmatch_det`) 0.838/0.773/0.804.
|
|
78
|
+
|
|
79
|
+
> Open decision the user was mid-answering: whether §B Table 4 reports the **published
|
|
80
|
+
> `relmatch_det`** (96% rejection, drops the "relational rejects best/100%" claim — already
|
|
81
|
+
> edited into the draft + abstract + conclusions) or also shows the GT-trained variant's
|
|
82
|
+
> 99.2/100/100 as an ablation. After the retrain, re-surface this with clean numbers.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 3. Prerequisites / data inventory
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv sync --dev
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- **Synthetic train/val:** `data/generated/{train,val}/{images,ground_truth,labels}` (10k/1k).
|
|
93
|
+
- **Synthetic TEST set:** `data/generated_test/val/...` (1,000 pages). If missing, regenerate
|
|
94
|
+
EXACTLY (seed-disjoint from train/val, with distractors):
|
|
95
|
+
```bash
|
|
96
|
+
uv run sf-generate --out data/generated_test --num-train 0 --num-val 1000 \
|
|
97
|
+
--seed 1000000 --smiles data/smiles/chembl_smiles.csv --fonts-dir data/fonts \
|
|
98
|
+
--distractors-dir data/distractors --workers 0
|
|
99
|
+
```
|
|
100
|
+
- **Real (CONFIDENTIAL):** mounted at `/net-fs-ins/shared-docker-vols/structflo-cser-annotate/data`
|
|
101
|
+
(`ground_truth/`, `images/`); split manifest `data/finetune/real_split.json`
|
|
102
|
+
(test=100, val=75, train=830, seed 42). Fine-tune corpora:
|
|
103
|
+
`data/finetune/lps/{train,val,real_test}` (GT boxes, 1,660 real + 2,000 synth in train),
|
|
104
|
+
`data/finetune/relmatch_det/{train,val}` (detection boxes), `data/finetune/yolo/` (+ `data_real_test.yaml`).
|
|
105
|
+
- **Detector checkpoints (reuse, do NOT retrain unless you accept the GPU cost — see §6):**
|
|
106
|
+
base synthetic `runs/labels_detect/yolo11l_panels/weights/best.pt` (trained imgsz **2048**);
|
|
107
|
+
fine-tuned `runs/labels_detect/finetune_3way/weights/best.pt` (imgsz 1280).
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 4. Retrain plan — matchers, ≥3 seeds, explicit provenance
|
|
112
|
+
|
|
113
|
+
Use **SEEDS = {42, 43, 44}**. Put everything under `runs/repro/`. Both trainers honor `--seed`.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
for S in 42 43 44; do
|
|
117
|
+
# ---- §A: SYNTHETIC-ONLY matchers (explicit --data-dir data/generated) ----
|
|
118
|
+
uv run sf-train-lps --data-dir data/generated --seed $S \
|
|
119
|
+
--output-dir runs/repro/lps_synth_s$S
|
|
120
|
+
uv run sf-train-relmatch --data-dir data/generated --seed $S \
|
|
121
|
+
--output-dir runs/repro/relmatch_synth_s$S # GT-box, synthetic
|
|
122
|
+
|
|
123
|
+
# ---- §B: REAL-FINE-TUNED matchers ----
|
|
124
|
+
uv run sf-train-lps --finetune runs/repro/lps_synth_s$S/best.pt \
|
|
125
|
+
--data-dir data/finetune/lps --seed $S \
|
|
126
|
+
--output-dir runs/repro/lps_ft_s$S
|
|
127
|
+
uv run sf-train-relmatch --data-dir data/finetune/lps --seed $S \
|
|
128
|
+
--output-dir runs/repro/relmatch_gt_s$S # GT-box, real+synth (clean variant)
|
|
129
|
+
uv run sf-train-relmatch --det-data-dir data/finetune/relmatch_det --seed $S \
|
|
130
|
+
--output-dir runs/repro/relmatch_det_s$S # detection-box, real+synth (PUBLISHED kind)
|
|
131
|
+
done
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Notes:
|
|
135
|
+
- `sf-train-relmatch` has **no `--finetune`** (always trains from scratch); `--det-data-dir`
|
|
136
|
+
switches it to detection-box training (overrides `--data-dir`).
|
|
137
|
+
- LPS fine-tune warm-starts from the **same seed's** synthetic base for clean seed isolation.
|
|
138
|
+
- Training is cheap: LPS ~minutes, relmatch ~minutes/seed. The whole matrix is well under an hour.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 5. Eval plan — per seed, on the FIXED test sets
|
|
143
|
+
|
|
144
|
+
Create the synthetic-test manifest once:
|
|
145
|
+
```bash
|
|
146
|
+
uv run python - <<'PY'
|
|
147
|
+
import json, glob, os
|
|
148
|
+
stems = sorted(os.path.splitext(os.path.basename(p))[0]
|
|
149
|
+
for p in glob.glob('data/generated_test/val/ground_truth/*.json'))
|
|
150
|
+
json.dump({'test': stems, 'val': [], 'train': []}, open('runs/repro/synth_test_manifest.json','w'))
|
|
151
|
+
print('pages', len(stems))
|
|
152
|
+
PY
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**(a) Synthetic matching — Table 2 (per seed):** synthetic-only weights on the synthetic test set.
|
|
156
|
+
```bash
|
|
157
|
+
for S in 42 43 44; do
|
|
158
|
+
uv run python scripts/finetune/relmatch/eval_compare_all.py \
|
|
159
|
+
--src data/generated_test/val --manifest runs/repro/synth_test_manifest.json \
|
|
160
|
+
--detector runs/labels_detect/yolo11l_panels/weights/best.pt \
|
|
161
|
+
--lps runs/repro/lps_synth_s$S/best.pt \
|
|
162
|
+
--relmatch runs/repro/relmatch_synth_s$S/best.pt \
|
|
163
|
+
--imgsz 1280 --conf 0.3 | tee runs/repro/eval_synth_s$S.txt
|
|
164
|
+
done
|
|
165
|
+
# Read the [TEST] block: PART A = clean (assign/reject/prec), PART B = e2e (P/R/F1).
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**(b) Real matching — Tables 4 & 5 (per seed):** fine-tuned/published weights on the real test set.
|
|
169
|
+
```bash
|
|
170
|
+
for S in 42 43 44; do
|
|
171
|
+
uv run python scripts/finetune/relmatch/eval_compare_all.py \
|
|
172
|
+
--detector runs/labels_detect/finetune_3way/weights/best.pt \
|
|
173
|
+
--lps runs/repro/lps_ft_s$S/best.pt \
|
|
174
|
+
--relmatch runs/repro/relmatch_det_s$S/best.pt \
|
|
175
|
+
--imgsz 1280 --conf 0.3 --margin 2.0 | tee runs/repro/eval_real_s$S.txt
|
|
176
|
+
done
|
|
177
|
+
# (default --src is the network mount; default --manifest is data/finetune/real_split.json)
|
|
178
|
+
# For the §B Table 4 "GT-trained clean variant" ablation, also run with
|
|
179
|
+
# --relmatch runs/repro/relmatch_gt_s$S/best.pt and record PART A only.
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**(c) LPS pair-classification accuracy — Table 3 LPS row + synth regression (per seed):**
|
|
183
|
+
use the helper `scripts/repro/eval_lps_acc.py` (created in this handoff).
|
|
184
|
+
```bash
|
|
185
|
+
for S in 42 43 44; do
|
|
186
|
+
uv run python scripts/repro/eval_lps_acc.py \
|
|
187
|
+
--weights runs/repro/lps_synth_s$S/best.pt --data data/finetune/lps/real_test # baseline, real
|
|
188
|
+
uv run python scripts/repro/eval_lps_acc.py \
|
|
189
|
+
--weights runs/repro/lps_ft_s$S/best.pt --data data/finetune/lps/real_test # fine-tuned, real
|
|
190
|
+
uv run python scripts/repro/eval_lps_acc.py \
|
|
191
|
+
--weights runs/repro/lps_synth_s$S/best.pt --data data/generated_test/val # baseline, synth
|
|
192
|
+
uv run python scripts/repro/eval_lps_acc.py \
|
|
193
|
+
--weights runs/repro/lps_ft_s$S/best.pt --data data/generated_test/val # fine-tuned, synth
|
|
194
|
+
done
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**(d) Detection — Tables 1 & 3 (detector is single-model; see §6):**
|
|
198
|
+
```bash
|
|
199
|
+
# Synthetic Table 1 (base @1280 on synth test):
|
|
200
|
+
uv run yolo detect val model=runs/labels_detect/yolo11l_panels/weights/best.pt \
|
|
201
|
+
data=<synth_test.yaml> split=val imgsz=1280 verbose=True
|
|
202
|
+
# Real Table 3 detector (base vs FT @1280) + synth regression:
|
|
203
|
+
uv run python scripts/finetune/yolo/eval_compare.py
|
|
204
|
+
```
|
|
205
|
+
`<synth_test.yaml>`: a data.yaml with `path: .../data/generated_test`, `val: val/images`,
|
|
206
|
+
`nc: 2`, names {0: chemical_structure, 1: compound_label}.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 6. Detector seeds — cost note / decision
|
|
211
|
+
|
|
212
|
+
The detector is standard YOLO11l; the paper's **contribution is the matcher**. A 3-seed detector
|
|
213
|
+
retrain is **expensive** (base ~hours/seed at imgsz 2048; fine-tune ~45 min/seed). Recommended:
|
|
214
|
+
- **Keep the existing detector single-seed** for Tables 1 & 3 (detection is not the novelty), and
|
|
215
|
+
report matcher reproducibility via **Part A (clean), which is detector-independent**, plus e2e
|
|
216
|
+
with the fixed detector.
|
|
217
|
+
- Only do multi-seed detector if the reviewer/user explicitly wants e2e variance to include
|
|
218
|
+
detector seeds. If so, retrain base (`config/data.yaml`, imgsz 2048, 30 ep) and fine-tune
|
|
219
|
+
(`scripts/finetune/yolo/train.sh`, imgsz 1280) per seed and re-run (a)/(b)/(d) per detector seed.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 7. Aggregate & update the paper
|
|
224
|
+
|
|
225
|
+
1. Parse each `runs/repro/eval_*_s*.txt` [TEST] block → collect per-seed values.
|
|
226
|
+
2. Compute **mean ± std** across the 3 seeds for every cell of Tables 2, 4, 5 and the LPS rows.
|
|
227
|
+
3. Update `docs/paper_draft.md` Tables 1–5 to report mean ± std (keep §A "no conclusions";
|
|
228
|
+
keep §B conclusions). Note in each caption: "mean ± std over 3 training seeds (42–44);
|
|
229
|
+
synthetic-only weights for §A, fine-tuned for §B; eval @1280."
|
|
230
|
+
4. Re-confirm the §B Table 4 relational decision (published `relmatch_det` vs GT-trained
|
|
231
|
+
ablation) with the user using the fresh multi-seed numbers.
|
|
232
|
+
5. Sanity-check: fresh numbers should land within ~±1% of §2. Large drift ⇒ investigate.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 8. Publish (after numbers are locked)
|
|
237
|
+
|
|
238
|
+
Pick a canonical seed (or the median-seed checkpoint) for the released weights and bump versions:
|
|
239
|
+
```bash
|
|
240
|
+
python scripts/publish_weights.py --model cser-detector --version vX.Y --weights-file <...>
|
|
241
|
+
python scripts/publish_weights.py --model cser-lps --version vX.Y --weights-file runs/repro/lps_ft_s42/best.pt
|
|
242
|
+
python scripts/publish_weights.py --model cser-relmatcher --version vX.Y --weights-file runs/repro/relmatch_det_s42/best.pt
|
|
243
|
+
```
|
|
244
|
+
Document the seed + training `--data-dir` in the HF model card so provenance is never ambiguous again.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 9. Gotchas / lessons (don't relearn these)
|
|
249
|
+
|
|
250
|
+
- **`model.val()` imgsz**: silently uses the checkpoint's training imgsz. Always pass `imgsz=1280`.
|
|
251
|
+
- **Two relational variants**: GT-box (`relmatch`) vs detection-box (`relmatch_det`). Published =
|
|
252
|
+
`relmatch_det`. Keep them straight; record `--data-dir`/`--det-data-dir` per run.
|
|
253
|
+
- **Synthetic test set** must be **seed-disjoint** from train/val (train/val used seeds 7–11006;
|
|
254
|
+
test uses 1000000) and generated **with distractors** to match the realistic distribution.
|
|
255
|
+
- **Real data is confidential** — never copy it into git, notebooks, or the released package.
|
|
256
|
+
- **Paper draft is gitignored** (`.gitignore:48` `docs/*draft*.md`); edits live on disk only.
|
|
257
|
+
- The synthetic LPS-classification eval over 1,000 pages is slow (visual-crop extraction) — run
|
|
258
|
+
in the background; real_test (100 pages) is fast.
|
|
259
|
+
- `runs/`, `data/` are gitignored; `runs/repro/` will not be committed (that's fine — logs + the
|
|
260
|
+
aggregated table are the durable artifacts; copy the aggregated table into the paper).
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## 10. Definition of done
|
|
265
|
+
|
|
266
|
+
- [ ] `runs/repro/` holds all seeded checkpoints + `eval_*_s*.txt` logs (3 seeds).
|
|
267
|
+
- [ ] Aggregated mean ± std table (e.g. `runs/repro/SUMMARY.md`) for Tables 2, 4, 5 + LPS rows.
|
|
268
|
+
- [ ] `runs/relmatch` provenance resolved: a **confirmed synthetic-only** relational matcher
|
|
269
|
+
exists (`runs/repro/relmatch_synth_s*`) and is used in §A; published `relmatch_det`-kind in §B.
|
|
270
|
+
- [ ] Paper Tables 1–5 updated to mean ± std with provenance noted in captions.
|
|
271
|
+
- [ ] §B Table 4 relational decision re-confirmed with the user on fresh numbers.
|
|
272
|
+
- [ ] (Optional) Re-published weights with documented seed + data provenance.
|
|
@@ -11,6 +11,7 @@ dependencies = [
|
|
|
11
11
|
"matplotlib>=3.10.8",
|
|
12
12
|
"easyocr>=1.7",
|
|
13
13
|
"numpy>=2.4.2",
|
|
14
|
+
"openpyxl>=3.1",
|
|
14
15
|
"opencv-python-headless>=4.13.0.92",
|
|
15
16
|
"pillow>=12.1.1",
|
|
16
17
|
"pymupdf>=1.27.1",
|
|
@@ -36,6 +37,7 @@ sf-extract = "structflo.cser.pipeline.cli:main"
|
|
|
36
37
|
sf-train-lps = "structflo.cser.lps.train:main"
|
|
37
38
|
sf-eval-lps = "structflo.cser.lps.evaluate:main"
|
|
38
39
|
sf-train-relmatch = "structflo.cser.relmatch.train:main"
|
|
40
|
+
sf-web = "webapp.server:main"
|
|
39
41
|
|
|
40
42
|
[build-system]
|
|
41
43
|
requires = ["hatchling<1.28", "hatch-vcs"]
|
|
@@ -45,7 +47,7 @@ build-backend = "hatchling.build"
|
|
|
45
47
|
source = "vcs"
|
|
46
48
|
|
|
47
49
|
[tool.hatch.build.targets.wheel]
|
|
48
|
-
packages = ["structflo", "annotate"]
|
|
50
|
+
packages = ["structflo", "annotate", "webapp"]
|
|
49
51
|
|
|
50
52
|
[dependency-groups]
|
|
51
53
|
dev = [
|
|
@@ -60,8 +60,16 @@ def main():
|
|
|
60
60
|
ap.add_argument("--relmatch", type=Path, default=Path("runs/relmatch_det/best.pt"))
|
|
61
61
|
ap.add_argument("--margin", type=float, default=2.0, help="relational dustbin margin for Part B")
|
|
62
62
|
ap.add_argument("--conf", type=float, default=0.3)
|
|
63
|
+
ap.add_argument(
|
|
64
|
+
"--label-conf",
|
|
65
|
+
type=float,
|
|
66
|
+
default=None,
|
|
67
|
+
help="per-class conf for labels (class 1); default = --conf. Lower it to give the "
|
|
68
|
+
"matcher more label candidates (Part B / end-to-end only; Part A uses GT boxes).",
|
|
69
|
+
)
|
|
63
70
|
ap.add_argument("--imgsz", type=int, default=1280)
|
|
64
71
|
args = ap.parse_args()
|
|
72
|
+
label_conf = args.label_conf if args.label_conf is not None else args.conf
|
|
65
73
|
|
|
66
74
|
manifest = json.loads(args.manifest.read_text())
|
|
67
75
|
stem2split = {}
|
|
@@ -113,12 +121,18 @@ def main():
|
|
|
113
121
|
|
|
114
122
|
# ---- Part B inputs: detections (one YOLO pass) ----
|
|
115
123
|
img_rgb = np.array(Image.open(ip).convert("L").convert("RGB"))
|
|
116
|
-
|
|
124
|
+
# Run YOLO at the lower of the two thresholds, then keep each class by its own conf:
|
|
125
|
+
# structures >= --conf, labels >= --label-conf (per-class gating for Part B).
|
|
126
|
+
res = model(img_rgb, conf=min(args.conf, label_conf), imgsz=args.imgsz, verbose=False)[0]
|
|
117
127
|
det_dets = []
|
|
118
128
|
for box in res.boxes:
|
|
129
|
+
cls = int(box.cls[0])
|
|
130
|
+
cf = float(box.conf[0])
|
|
131
|
+
if cf < (label_conf if cls == 1 else args.conf):
|
|
132
|
+
continue
|
|
119
133
|
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
|
|
120
134
|
det_dets.append(Detection.from_dict({"bbox": [float(x1), float(y1), float(x2), float(y2)],
|
|
121
|
-
"conf":
|
|
135
|
+
"conf": cf, "class_id": cls}))
|
|
122
136
|
labelled = [e for e in entries if e.get("label_bbox") is not None]
|
|
123
137
|
|
|
124
138
|
for tgt in (split, "all"):
|
|
@@ -187,7 +201,7 @@ def main():
|
|
|
187
201
|
print(f" {n:>11} | {assign:>7.1%} {reject:>7.1%} {prec:>7.1%}")
|
|
188
202
|
|
|
189
203
|
print("\n" + "=" * 70)
|
|
190
|
-
print(f"PART B — end-to-end full@{args.imgsz} → pairing F1 (centroid; Relational margin={args.margin})")
|
|
204
|
+
print(f"PART B — end-to-end full@{args.imgsz} (struct conf {args.conf}, label conf {label_conf}) → pairing F1 (centroid; Relational margin={args.margin})")
|
|
191
205
|
print("=" * 70)
|
|
192
206
|
for s in SPLITS:
|
|
193
207
|
print(f"\n [{s.upper()}] GT pairs={gt_pairs[s]}")
|
|
@@ -44,7 +44,8 @@ def _best(box, cands, thr):
|
|
|
44
44
|
return bi
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
def build_split(model, gt_dir, img_dir, out_dir, conf, imgsz):
|
|
47
|
+
def build_split(model, gt_dir, img_dir, out_dir, conf, imgsz, label_conf=None):
|
|
48
|
+
label_conf = conf if label_conf is None else label_conf
|
|
48
49
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
49
50
|
files = sorted(gt_dir.glob("*.json"))
|
|
50
51
|
n_pages = n_fp_struct = n_det_struct = n_missed_label = n_dustbin = n_real = 0
|
|
@@ -62,7 +63,7 @@ def build_split(model, gt_dir, img_dir, out_dir, conf, imgsz):
|
|
|
62
63
|
|
|
63
64
|
img_rgb = np.array(Image.open(ip).convert("L").convert("RGB"))
|
|
64
65
|
page_h, page_w = img_rgb.shape[:2]
|
|
65
|
-
res = model(img_rgb, conf=conf, imgsz=imgsz, verbose=False)[0]
|
|
66
|
+
res = model(img_rgb, conf=min(conf, label_conf), imgsz=imgsz, verbose=False)[0]
|
|
66
67
|
|
|
67
68
|
d_struct_boxes, d_struct_conf = [], []
|
|
68
69
|
d_label_boxes, d_label_conf = [], []
|
|
@@ -70,6 +71,8 @@ def build_split(model, gt_dir, img_dir, out_dir, conf, imgsz):
|
|
|
70
71
|
x1, y1, x2, y2 = (float(v) for v in box.xyxy[0].cpu().numpy())
|
|
71
72
|
cls = int(box.cls[0])
|
|
72
73
|
cf = float(box.conf[0])
|
|
74
|
+
if cf < (label_conf if cls == 1 else conf): # per-class gating
|
|
75
|
+
continue
|
|
73
76
|
if cls == 0:
|
|
74
77
|
d_struct_boxes.append([x1, y1, x2, y2])
|
|
75
78
|
d_struct_conf.append(cf)
|
|
@@ -131,8 +134,18 @@ def main():
|
|
|
131
134
|
ap = argparse.ArgumentParser()
|
|
132
135
|
ap.add_argument("--src", type=Path, default=Path("data/finetune/lps"))
|
|
133
136
|
ap.add_argument("--out", type=Path, default=Path("data/finetune/relmatch_det"))
|
|
134
|
-
ap.add_argument(
|
|
137
|
+
ap.add_argument(
|
|
138
|
+
"--detector",
|
|
139
|
+
type=Path,
|
|
140
|
+
default=Path("runs/labels_detect/finetune_3way/weights/best.pt"),
|
|
141
|
+
)
|
|
135
142
|
ap.add_argument("--conf", type=float, default=0.3)
|
|
143
|
+
ap.add_argument(
|
|
144
|
+
"--label-conf",
|
|
145
|
+
type=float,
|
|
146
|
+
default=None,
|
|
147
|
+
help="per-class conf for labels (class 1); default = --conf",
|
|
148
|
+
)
|
|
136
149
|
ap.add_argument("--imgsz", type=int, default=1280)
|
|
137
150
|
args = ap.parse_args()
|
|
138
151
|
|
|
@@ -148,6 +161,7 @@ def main():
|
|
|
148
161
|
args.out / split,
|
|
149
162
|
args.conf,
|
|
150
163
|
args.imgsz,
|
|
164
|
+
label_conf=args.label_conf,
|
|
151
165
|
)
|
|
152
166
|
print(f"[prep] done → {args.out}")
|
|
153
167
|
|
|
@@ -39,7 +39,11 @@ def _val(weights: Path, data_yaml: Path) -> dict[str, float] | None:
|
|
|
39
39
|
return None
|
|
40
40
|
|
|
41
41
|
model = YOLO(str(weights))
|
|
42
|
-
|
|
42
|
+
# Pin inference resolution so the baseline (trained @2048) and the
|
|
43
|
+
# fine-tuned model (trained @1280) are compared at the same deployment
|
|
44
|
+
# resolution. Without this, model.val() silently uses each checkpoint's own
|
|
45
|
+
# training imgsz, confounding the fine-tuning effect with a resolution change.
|
|
46
|
+
m = model.val(data=str(data_yaml), imgsz=1280, verbose=False)
|
|
43
47
|
|
|
44
48
|
return {
|
|
45
49
|
"mAP50": m.box.map50,
|
|
@@ -10,15 +10,18 @@
|
|
|
10
10
|
set -euo pipefail
|
|
11
11
|
|
|
12
12
|
PROJECT_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
|
13
|
-
CHECKPOINT="$PROJECT_ROOT/runs/labels_detect/yolo11l_panels/weights/best.pt"
|
|
14
13
|
OUT_DIR="$PROJECT_ROOT/runs/labels_detect"
|
|
15
14
|
|
|
16
15
|
# Overridable via env vars (defaults reproduce the original trial run).
|
|
16
|
+
# CHECKPOINT = base model to warm-start from (per-seed base for seed isolation).
|
|
17
|
+
# SEED varies init/shuffle/augmentation only — data splits stay fixed.
|
|
18
|
+
CHECKPOINT="${CHECKPOINT:-$PROJECT_ROOT/runs/labels_detect/yolo11l_panels/weights/best.pt}"
|
|
17
19
|
DATA_YAML="${DATA_YAML:-$PROJECT_ROOT/data/finetune/yolo/data.yaml}"
|
|
18
20
|
RUN_NAME="${RUN_NAME:-finetune_trial}"
|
|
19
21
|
EPOCHS="${EPOCHS:-10}"
|
|
20
22
|
PATIENCE="${PATIENCE:-5}"
|
|
21
23
|
MOSAIC="${MOSAIC:-0.5}"
|
|
24
|
+
SEED="${SEED:-42}"
|
|
22
25
|
|
|
23
26
|
if [ ! -f "$DATA_YAML" ]; then
|
|
24
27
|
echo "ERROR: $DATA_YAML not found. Run prepare_data.py first."
|
|
@@ -35,6 +38,7 @@ echo " Data: $DATA_YAML"
|
|
|
35
38
|
echo " Checkpoint: $CHECKPOINT"
|
|
36
39
|
echo " Run name: $RUN_NAME"
|
|
37
40
|
echo " Epochs: $EPOCHS (patience $PATIENCE, mosaic $MOSAIC)"
|
|
41
|
+
echo " Seed: $SEED"
|
|
38
42
|
echo ""
|
|
39
43
|
|
|
40
44
|
uv run python -c "
|
|
@@ -71,7 +75,7 @@ model.train(
|
|
|
71
75
|
project='$OUT_DIR',
|
|
72
76
|
name='$RUN_NAME',
|
|
73
77
|
exist_ok=True,
|
|
74
|
-
seed
|
|
78
|
+
seed=$SEED,
|
|
75
79
|
plots=True,
|
|
76
80
|
save_period=5,
|
|
77
81
|
)
|