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.
Files changed (143) hide show
  1. structflo_cser-0.4.2/.dockerignore +19 -0
  2. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/.gitignore +2 -0
  3. structflo_cser-0.4.2/.gitlab-ci.yml +34 -0
  4. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/CLAUDE.md +17 -5
  5. structflo_cser-0.4.2/Dockerfile +41 -0
  6. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/PKG-INFO +2 -1
  7. structflo_cser-0.4.2/docs/REPRODUCIBILITY_HANDOFF.md +272 -0
  8. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/pyproject.toml +3 -1
  9. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/relmatch/eval_compare_all.py +17 -3
  10. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/relmatch/prepare_det_data.py +17 -3
  11. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/yolo/eval_compare.py +5 -1
  12. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/yolo/train.sh +6 -2
  13. structflo_cser-0.4.2/scripts/repro/aggregate.py +285 -0
  14. structflo_cser-0.4.2/scripts/repro/aggregate_c01.py +115 -0
  15. structflo_cser-0.4.2/scripts/repro/aggregate_confsweep.py +115 -0
  16. structflo_cser-0.4.2/scripts/repro/aggregate_scale.py +155 -0
  17. structflo_cser-0.4.2/scripts/repro/build_release.py +314 -0
  18. structflo_cser-0.4.2/scripts/repro/eval_detector.py +99 -0
  19. structflo_cser-0.4.2/scripts/repro/eval_lps_acc.py +66 -0
  20. structflo_cser-0.4.2/scripts/repro/label_conf_sweep.py +132 -0
  21. structflo_cser-0.4.2/scripts/repro/make_synth_manifest.py +57 -0
  22. structflo_cser-0.4.2/scripts/repro/prepare_finetune_plus.py +135 -0
  23. structflo_cser-0.4.2/scripts/repro/prepare_scale_subset.py +158 -0
  24. structflo_cser-0.4.2/scripts/repro/run_all.sh +20 -0
  25. structflo_cser-0.4.2/scripts/repro/run_c01_sweep.sh +37 -0
  26. structflo_cser-0.4.2/scripts/repro/run_conf010.sh +36 -0
  27. structflo_cser-0.4.2/scripts/repro/run_confsweep_val.sh +23 -0
  28. structflo_cser-0.4.2/scripts/repro/run_eval.sh +103 -0
  29. structflo_cser-0.4.2/scripts/repro/run_finetune_plus.sh +62 -0
  30. structflo_cser-0.4.2/scripts/repro/run_relmatch_matched.sh +55 -0
  31. structflo_cser-0.4.2/scripts/repro/run_relmatch_plus.sh +45 -0
  32. structflo_cser-0.4.2/scripts/repro/run_scale.sh +104 -0
  33. structflo_cser-0.4.2/scripts/repro/run_train.sh +114 -0
  34. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/lps/matcher.py +10 -0
  35. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/pipeline/__init__.py +5 -2
  36. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/pipeline/matcher.py +9 -0
  37. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/pipeline/models.py +19 -0
  38. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/pipeline/pipeline.py +130 -3
  39. structflo_cser-0.4.2/structflo/cser/pipeline/version.py +152 -0
  40. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/relmatch/matcher.py +10 -0
  41. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/training/trainer.py +25 -4
  42. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/weights.py +83 -3
  43. structflo_cser-0.4.2/tests/test_page_api.py +65 -0
  44. structflo_cser-0.4.2/tests/test_webapp.py +76 -0
  45. structflo_cser-0.4.2/tests/test_weights_info.py +88 -0
  46. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/uv.lock +23 -0
  47. structflo_cser-0.4.2/webapp/__init__.py +0 -0
  48. structflo_cser-0.4.2/webapp/__main__.py +4 -0
  49. structflo_cser-0.4.2/webapp/server.py +210 -0
  50. structflo_cser-0.4.2/webapp/static/tokens.css +116 -0
  51. structflo_cser-0.4.2/webapp/templates/index.html +313 -0
  52. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/.github/workflows/ci.yml +0 -0
  53. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/.github/workflows/publish.yml +0 -0
  54. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/.python-version +0 -0
  55. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/Makefile +0 -0
  56. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/README.md +0 -0
  57. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/annotate/__main__.py +0 -0
  58. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/annotate/pdf.py +0 -0
  59. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/annotate/server.py +0 -0
  60. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/annotate/storage.py +0 -0
  61. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/annotate/templates/index.html +0 -0
  62. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/asset_scripts/download_chembl.sh +0 -0
  63. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/config/data.yaml +0 -0
  64. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/config/pipeline.yaml +0 -0
  65. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/docs/fine-tune.md +0 -0
  66. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/docs/images/example-1.png +0 -0
  67. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/docs/images/example-2.png +0 -0
  68. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/docs/learned_matcher_plan.md +0 -0
  69. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/docs/lps.md +0 -0
  70. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/docs/publishing-weights.md +0 -0
  71. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/main.py +0 -0
  72. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/01-quickstart.ipynb +0 -0
  73. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/02-LPS.ipynb +0 -0
  74. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/03-PDF.ipynb +0 -0
  75. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/notebook-data/bio-arcgive-1.png +0 -0
  76. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/notebook-data/example-annotated.pdf +0 -0
  77. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/notebook-data/example.pdf +0 -0
  78. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/notebook-data/example.pptx +0 -0
  79. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/notebook-data/screen-1.png +0 -0
  80. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/notebooks/notebook-data/syn-1.jpg +0 -0
  81. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/quick.md +0 -0
  82. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/diag_e2e_decompose.py +0 -0
  83. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/diag_label_recall.py +0 -0
  84. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/diag_lps_scores.py +0 -0
  85. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/eval_compare.py +0 -0
  86. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/eval_end2end.py +0 -0
  87. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/eval_rejection.py +0 -0
  88. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/mine_fp_negatives.py +0 -0
  89. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/prepare_data.py +0 -0
  90. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/lps/train.sh +0 -0
  91. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/relmatch/eval_compare.py +0 -0
  92. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/relmatch/sweep_margin.py +0 -0
  93. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/finetune/yolo/prepare_data.py +0 -0
  94. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/scripts/publish_weights.py +0 -0
  95. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/__init__.py +0 -0
  96. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/_geometry.py +0 -0
  97. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/config.py +0 -0
  98. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/data/__init__.py +0 -0
  99. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/data/distractor_images.py +0 -0
  100. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/data/smiles.py +0 -0
  101. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/distractors/__init__.py +0 -0
  102. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/distractors/charts.py +0 -0
  103. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/distractors/shapes.py +0 -0
  104. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/distractors/text_elements.py +0 -0
  105. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/generation/__init__.py +0 -0
  106. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/generation/dataset.py +0 -0
  107. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/generation/page.py +0 -0
  108. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/generation/specialty.py +0 -0
  109. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/generation/tabular.py +0 -0
  110. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/inference/__init__.py +0 -0
  111. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/inference/detector.py +0 -0
  112. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/inference/nms.py +0 -0
  113. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/inference/pairing.py +0 -0
  114. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/inference/tiling.py +0 -0
  115. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/lps/__init__.py +0 -0
  116. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/lps/dataset.py +0 -0
  117. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/lps/evaluate.py +0 -0
  118. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/lps/features.py +0 -0
  119. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/lps/scorer.py +0 -0
  120. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/lps/train.py +0 -0
  121. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/pipeline/cli.py +0 -0
  122. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/pipeline/ocr.py +0 -0
  123. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/pipeline/smiles_extractor.py +0 -0
  124. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/relmatch/__init__.py +0 -0
  125. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/relmatch/dataset.py +0 -0
  126. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/relmatch/features.py +0 -0
  127. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/relmatch/model.py +0 -0
  128. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/relmatch/train.py +0 -0
  129. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/rendering/__init__.py +0 -0
  130. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/rendering/chemistry.py +0 -0
  131. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/rendering/text.py +0 -0
  132. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/training/__init__.py +0 -0
  133. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/viz/__init__.py +0 -0
  134. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/viz/detections.py +0 -0
  135. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/structflo/cser/viz/labels.py +0 -0
  136. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/tests/__init__.py +0 -0
  137. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/tests/test_config.py +0 -0
  138. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/tests/test_generation.py +0 -0
  139. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/tests/test_geometry.py +0 -0
  140. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/tests/test_imports.py +0 -0
  141. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/tests/test_inference.py +0 -0
  142. {structflo_cser-0.4.0 → structflo_cser-0.4.2}/tests/test_models.py +0 -0
  143. {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
@@ -40,6 +40,8 @@ Thumbs.db
40
40
  runs/
41
41
  detections/
42
42
  archive/
43
+ # Zenodo reproducibility bundle (weights + synth test set; upload, don't commit)
44
+ repro_release/
43
45
 
44
46
  # ── Project: datasets & generated data ───────────────────────────────────────
45
47
  /data/
@@ -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 | HF Repo | Latest |
148
- |----------------|----------------------------------|--------|
149
- | cser-detector | sidxz/structflo-cser-detector | v0.2 |
150
- | cser-lps | sidxz/structflo-cser-lps | v0.1 |
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.0
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
- res = model(img_rgb, conf=args.conf, imgsz=args.imgsz, verbose=False)[0]
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": float(box.conf[0]), "class_id": int(box.cls[0])}))
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("--detector", type=Path, default=Path("runs/labels_detect/finetune_3way/weights/best.pt"))
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
- m = model.val(data=str(data_yaml), verbose=False)
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=42,
78
+ seed=$SEED,
75
79
  plots=True,
76
80
  save_period=5,
77
81
  )