sciwrite-lint 0.2.0__tar.gz → 0.2.1__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 (85) hide show
  1. {sciwrite_lint-0.2.0/sciwrite_lint.egg-info → sciwrite_lint-0.2.1}/PKG-INFO +34 -28
  2. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/README.md +8 -8
  3. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/pyproject.toml +26 -20
  4. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/__init__.py +1 -1
  5. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/__main__.py +30 -15
  6. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/cross_section_consistency.py +3 -0
  7. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/full_paper_consistency.py +26 -34
  8. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/ref_internal_checks.py +16 -49
  9. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/cli/check.py +22 -21
  10. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/cli/misc.py +382 -75
  11. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/config.py +17 -0
  12. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/eval_claims.py +152 -77
  13. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/llm_utils.py +99 -70
  14. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/pipeline.py +537 -134
  15. sciwrite_lint-0.2.1/sciwrite_lint/py.typed +0 -0
  16. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/embedding_store.py +23 -14
  17. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/reference_store.py +9 -23
  18. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/workspace_db.py +220 -102
  19. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/schemas.py +2 -2
  20. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/usage.py +18 -42
  21. sciwrite_lint-0.2.1/sciwrite_lint/vision/cache.py +234 -0
  22. sciwrite_lint-0.2.1/sciwrite_lint/vision/describe.py +632 -0
  23. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/vision/image_extraction.py +31 -0
  24. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/vision/pipeline.py +23 -3
  25. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/vllm/vllm_server.py +90 -11
  26. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1/sciwrite_lint.egg-info}/PKG-INFO +34 -28
  27. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint.egg-info/SOURCES.txt +1 -0
  28. sciwrite_lint-0.2.1/sciwrite_lint.egg-info/requires.txt +27 -0
  29. sciwrite_lint-0.2.0/sciwrite_lint/vision/cache.py +0 -140
  30. sciwrite_lint-0.2.0/sciwrite_lint/vision/describe.py +0 -311
  31. sciwrite_lint-0.2.0/sciwrite_lint.egg-info/requires.txt +0 -27
  32. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/LICENSE +0 -0
  33. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/_network.py +0 -0
  34. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/api.py +0 -0
  35. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/__init__.py +0 -0
  36. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/_section_utils.py +0 -0
  37. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/cite_purpose.py +0 -0
  38. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/claim_support.py +0 -0
  39. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/dangling_cite.py +0 -0
  40. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/dangling_ref.py +0 -0
  41. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/reference_accuracy.py +0 -0
  42. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/reference_exists.py +0 -0
  43. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/reference_unreliable.py +0 -0
  44. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/registry.py +0 -0
  45. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/retracted_cite.py +0 -0
  46. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/structure_promises.py +0 -0
  47. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/unreferenced_figure.py +0 -0
  48. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/claims.py +0 -0
  49. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/claude_backend.py +0 -0
  50. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/claude_cli.py +0 -0
  51. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/cli/__init__.py +0 -0
  52. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/cli/config.py +0 -0
  53. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/cli/fetch.py +0 -0
  54. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/cli/rank.py +0 -0
  55. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/cli/verify.py +0 -0
  56. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/cross_paper.py +0 -0
  57. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/fulltext.py +0 -0
  58. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/local_pdfs.py +0 -0
  59. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/manuscript_store.py +0 -0
  60. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/models.py +0 -0
  61. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/pdf/__init__.py +0 -0
  62. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/pdf/grobid.py +0 -0
  63. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/pdf/pdf_download.py +0 -0
  64. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/prompt_safety.py +0 -0
  65. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/rate_limiter.py +0 -0
  66. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/__init__.py +0 -0
  67. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/citations.py +0 -0
  68. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/crossref.py +0 -0
  69. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/matching.py +0 -0
  70. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/metadata.py +0 -0
  71. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/retraction_watch.py +0 -0
  72. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/report.py +0 -0
  73. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/scoring/__init__.py +0 -0
  74. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/scoring/chain.py +0 -0
  75. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/scoring/contribution.py +0 -0
  76. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/scoring/scilint_score.py +0 -0
  77. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/tex_parser.py +0 -0
  78. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/vision/__init__.py +0 -0
  79. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/vllm/__init__.py +0 -0
  80. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/vllm/metrics.py +0 -0
  81. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/web.py +0 -0
  82. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint.egg-info/dependency_links.txt +0 -0
  83. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint.egg-info/entry_points.txt +0 -0
  84. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint.egg-info/top_level.txt +0 -0
  85. {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/setup.cfg +0 -0
@@ -1,43 +1,49 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sciwrite-lint
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A linter for scientific manuscripts — reference verification, consistency checking, and structural validation.
5
5
  Author: Sergey Samsonau
6
+ Maintainer-email: Authentic Research Partners LLC <info@arpconnect.com>
6
7
  License-Expression: MIT
7
- Project-URL: Homepage, https://github.com/authentic-research-partners/sciwrite-lint
8
+ Project-URL: Homepage, https://arpconnect.com
8
9
  Project-URL: Repository, https://github.com/authentic-research-partners/sciwrite-lint
10
+ Project-URL: Documentation, https://github.com/authentic-research-partners/sciwrite-lint#readme
9
11
  Project-URL: Issues, https://github.com/authentic-research-partners/sciwrite-lint/issues
10
- Keywords: linter,scientific-writing,citation-verification,latex
12
+ Keywords: linter,latex,bibtex,pdf,scientific-writing,academic-writing,citation-checker,reference-verification,claim-verification,manuscript,peer-review,arxiv,vllm,qwen
11
13
  Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
12
15
  Classifier: Intended Audience :: Science/Research
13
- Classifier: Topic :: Text Processing :: Markup :: LaTeX
16
+ Classifier: Topic :: Scientific/Engineering
17
+ Classifier: Topic :: Software Development :: Quality Assurance
18
+ Classifier: Programming Language :: Python :: 3
14
19
  Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Typing :: Typed
15
21
  Requires-Python: <3.14,>=3.13
16
22
  Description-Content-Type: text/markdown
17
23
  License-File: LICENSE
18
- Requires-Dist: httpx>=0.27
19
- Requires-Dist: tenacity>=8.0
20
- Requires-Dist: trafilatura>=1.8
21
- Requires-Dist: aiosqlite>=0.20
24
+ Requires-Dist: httpx>=0.28
25
+ Requires-Dist: tenacity>=9.0
26
+ Requires-Dist: trafilatura>=2.0
27
+ Requires-Dist: aiosqlite>=0.22
22
28
  Requires-Dist: loguru>=0.7
23
29
  Requires-Dist: anyascii>=0.3
24
- Requires-Dist: rich>=13.0
30
+ Requires-Dist: rich>=14.0
25
31
  Requires-Dist: defusedxml>=0.7
26
- Requires-Dist: pydantic>=2.0
32
+ Requires-Dist: pydantic>=2.10
27
33
  Requires-Dist: rapidfuzz>=3.0
28
34
  Requires-Dist: bibtexparser>=1.4
29
- Requires-Dist: pdfplumber>=0.10
30
- Requires-Dist: openai>=1.0
35
+ Requires-Dist: pdfplumber>=0.11
36
+ Requires-Dist: openai>=2.0
31
37
  Requires-Dist: grobid-tei-xml>=0.1
32
- Requires-Dist: sentence-transformers>=3.0
33
- Requires-Dist: numpy>=1.24
38
+ Requires-Dist: sentence-transformers>=5.0
39
+ Requires-Dist: numpy>=2.0
34
40
  Requires-Dist: sqlite-vec>=0.1.6
35
- Requires-Dist: torch>=2.0
36
- Requires-Dist: transformers>=4.45
37
- Requires-Dist: Pillow>=10.0
38
- Requires-Dist: pypdf>=4.0
39
- Requires-Dist: pypdfium2>=4.0
40
- Requires-Dist: pdf2image>=1.16
41
+ Requires-Dist: torch>=2.5
42
+ Requires-Dist: transformers>=4.50
43
+ Requires-Dist: Pillow>=12.0
44
+ Requires-Dist: pypdf>=6.0
45
+ Requires-Dist: pypdfium2>=5.0
46
+ Requires-Dist: pdf2image>=1.17
41
47
  Provides-Extra: dev
42
48
  Requires-Dist: pytest>=8.0; extra == "dev"
43
49
  Requires-Dist: pytest-xdist>=3.0; extra == "dev"
@@ -123,7 +129,7 @@ sciwrite-lint contributions paper.pdf --format json
123
129
 
124
130
  ### Optimizations
125
131
 
126
- Three models — a vision model (Qwen3-VL-2B), an embedding model (Arctic Embed), and an 8B reasoning LLM (Qwen3 via vLLM) — share a single consumer GPU. Models run in separate pipeline stages; on WSL2, CUDA memory virtualization pages idle allocations to system RAM, letting all three share physical VRAM. FP8 weights and KV cache (Ada Lovelace+) and per-paper SQLite caching with hash-based invalidation are baseline. On top of that:
132
+ Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-backend vllm` for +15% accuracy), an embedding model (Arctic Embed), and an 8B reasoning LLM (Qwen3 via vLLM) — share a single consumer GPU. Models run in separate pipeline stages; on WSL2, CUDA memory virtualization pages idle allocations to system RAM, letting all three share physical VRAM. FP8 weights and KV cache (Ada Lovelace+) and per-paper SQLite caching with hash-based invalidation are baseline. On top of that:
127
133
 
128
134
  - **Semantic section filtering** — embedding-based KNN retrieval sends only the ~5 most relevant sections per claim to the LLM, reducing LLM calls ~4x
129
135
  - **Prefix-first prompt structure** — shared context placed before variable content in all prompts, maximizing vLLM's automatic prefix caching
@@ -134,11 +140,11 @@ Three models — a vision model (Qwen3-VL-2B), an embedding model (Arctic Embed)
134
140
  - **8-source full-text cascade** — early exit on first successful download across arXiv, Semantic Scholar, OpenAlex, PubMed Central, Europe PMC, Unpaywall, bioRxiv, CORE
135
141
  - **Live monitoring** *(advanced)* — `sciwrite-lint containers monitor` shows service health, VRAM usage, and KV cache utilization in a terminal dashboard
136
142
 
137
- Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns. On native Linux, embedding and vision default to CPU (tens of times slower) due to limited VRAM on consumer GPUs and no transparent paging of inactive vLLM container allocations; force GPU via config if VRAM permits.
143
+ Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns. On native Linux, the pipeline automatically swaps vLLM containers to free GPU for embedding and vision stages (~50x faster than CPU). *(Native Linux GPU swap is preliminary tested on WSL2 only; expected to work, may need minor fixes.)*
138
144
 
139
145
  ## Install
140
146
 
141
- **Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM). Developed and tested on Windows (WSL2). Native Linux is likely to work with GPU memory allocation tuning (see [docs/services.md](docs/services.md#embedding-device)). Not tested on macOS.
147
+ **Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM). Developed and tested on Windows (WSL2). Native Linux is likely to work with GPU memory allocation tuning (see [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md#embedding-device)). Not tested on macOS.
142
148
 
143
149
  Requires [uv](https://docs.astral.sh/uv/getting-started/installation/), a container runtime (podman or docker), CUDA drivers, and [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html).
144
150
 
@@ -157,7 +163,7 @@ sciwrite-lint containers start # start GROBID + vLLM (needs GPU for vLLM
157
163
  sciwrite-lint containers monitor # live dashboard: service health, VRAM, KV cache
158
164
  ```
159
165
 
160
- ![Monitor dashboard](docs/monitor.png)
166
+ ![Monitor dashboard](https://github.com/authentic-research-partners/sciwrite-lint/raw/main/docs/monitor.png)
161
167
 
162
168
  `init` detects `.tex` files and their `.bib` references and generates a `.sciwrite-lint.toml` config. Review to confirm the right files were detected.
163
169
 
@@ -170,7 +176,7 @@ sciwrite-lint config show # see what's configured
170
176
  sciwrite-lint config set-key semantic-scholar YOUR_KEY # dedicated rate limit
171
177
  ```
172
178
 
173
- See [docs/services.md](docs/services.md) for GPU requirements, all external APIs, and API key details.
179
+ See [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md) for GPU requirements, all external APIs, and API key details.
174
180
 
175
181
  ## Usage
176
182
 
@@ -256,12 +262,12 @@ Output formats: terminal (default) or `--format json`.
256
262
 
257
263
  - `sciwrite-lint checks` — list all checks
258
264
  - `sciwrite-lint <command> --help` — detailed usage for any command
259
- - [docs/services.md](docs/services.md) — GROBID, vLLM, external APIs, configuration
265
+ - [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md) — GROBID, vLLM, external APIs, configuration
260
266
 
261
267
  For contributors and advanced users:
262
268
 
263
- - [docs/evals.md](docs/evals.md) — detection evaluation framework, adding test cases
264
- - [docs/calibration.md](docs/calibration.md) — SciLint Score calibration against ground-truth papers
269
+ - [docs/evals.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/evals.md) — detection evaluation framework, adding test cases
270
+ - [docs/calibration.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/calibration.md) — SciLint Score calibration against ground-truth papers
265
271
 
266
272
  ## License
267
273
 
@@ -78,7 +78,7 @@ sciwrite-lint contributions paper.pdf --format json
78
78
 
79
79
  ### Optimizations
80
80
 
81
- Three models — a vision model (Qwen3-VL-2B), an embedding model (Arctic Embed), and an 8B reasoning LLM (Qwen3 via vLLM) — share a single consumer GPU. Models run in separate pipeline stages; on WSL2, CUDA memory virtualization pages idle allocations to system RAM, letting all three share physical VRAM. FP8 weights and KV cache (Ada Lovelace+) and per-paper SQLite caching with hash-based invalidation are baseline. On top of that:
81
+ Three models — a vision model (Qwen3-VL-2B default, or 8B FP8 via `--vision-backend vllm` for +15% accuracy), an embedding model (Arctic Embed), and an 8B reasoning LLM (Qwen3 via vLLM) — share a single consumer GPU. Models run in separate pipeline stages; on WSL2, CUDA memory virtualization pages idle allocations to system RAM, letting all three share physical VRAM. FP8 weights and KV cache (Ada Lovelace+) and per-paper SQLite caching with hash-based invalidation are baseline. On top of that:
82
82
 
83
83
  - **Semantic section filtering** — embedding-based KNN retrieval sends only the ~5 most relevant sections per claim to the LLM, reducing LLM calls ~4x
84
84
  - **Prefix-first prompt structure** — shared context placed before variable content in all prompts, maximizing vLLM's automatic prefix caching
@@ -89,11 +89,11 @@ Three models — a vision model (Qwen3-VL-2B), an embedding model (Arctic Embed)
89
89
  - **8-source full-text cascade** — early exit on first successful download across arXiv, Semantic Scholar, OpenAlex, PubMed Central, Europe PMC, Unpaywall, bioRxiv, CORE
90
90
  - **Live monitoring** *(advanced)* — `sciwrite-lint containers monitor` shows service health, VRAM usage, and KV cache utilization in a terminal dashboard
91
91
 
92
- Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns. On native Linux, embedding and vision default to CPU (tens of times slower) due to limited VRAM on consumer GPUs and no transparent paging of inactive vLLM container allocations; force GPU via config if VRAM permits.
92
+ Full pipeline on a 50-reference paper: ~30 minutes initial (dominated by downloads and claim verification), minutes on cached reruns. On native Linux, the pipeline automatically swaps vLLM containers to free GPU for embedding and vision stages (~50x faster than CPU). *(Native Linux GPU swap is preliminary tested on WSL2 only; expected to work, may need minor fixes.)*
93
93
 
94
94
  ## Install
95
95
 
96
- **Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM). Developed and tested on Windows (WSL2). Native Linux is likely to work with GPU memory allocation tuning (see [docs/services.md](docs/services.md#embedding-device)). Not tested on macOS.
96
+ **Assumed setup:** A workstation with an NVIDIA GPU (16+ GB VRAM). Developed and tested on Windows (WSL2). Native Linux is likely to work with GPU memory allocation tuning (see [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md#embedding-device)). Not tested on macOS.
97
97
 
98
98
  Requires [uv](https://docs.astral.sh/uv/getting-started/installation/), a container runtime (podman or docker), CUDA drivers, and [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html).
99
99
 
@@ -112,7 +112,7 @@ sciwrite-lint containers start # start GROBID + vLLM (needs GPU for vLLM
112
112
  sciwrite-lint containers monitor # live dashboard: service health, VRAM, KV cache
113
113
  ```
114
114
 
115
- ![Monitor dashboard](docs/monitor.png)
115
+ ![Monitor dashboard](https://github.com/authentic-research-partners/sciwrite-lint/raw/main/docs/monitor.png)
116
116
 
117
117
  `init` detects `.tex` files and their `.bib` references and generates a `.sciwrite-lint.toml` config. Review to confirm the right files were detected.
118
118
 
@@ -125,7 +125,7 @@ sciwrite-lint config show # see what's configured
125
125
  sciwrite-lint config set-key semantic-scholar YOUR_KEY # dedicated rate limit
126
126
  ```
127
127
 
128
- See [docs/services.md](docs/services.md) for GPU requirements, all external APIs, and API key details.
128
+ See [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md) for GPU requirements, all external APIs, and API key details.
129
129
 
130
130
  ## Usage
131
131
 
@@ -211,12 +211,12 @@ Output formats: terminal (default) or `--format json`.
211
211
 
212
212
  - `sciwrite-lint checks` — list all checks
213
213
  - `sciwrite-lint <command> --help` — detailed usage for any command
214
- - [docs/services.md](docs/services.md) — GROBID, vLLM, external APIs, configuration
214
+ - [docs/services.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/services.md) — GROBID, vLLM, external APIs, configuration
215
215
 
216
216
  For contributors and advanced users:
217
217
 
218
- - [docs/evals.md](docs/evals.md) — detection evaluation framework, adding test cases
219
- - [docs/calibration.md](docs/calibration.md) — SciLint Score calibration against ground-truth papers
218
+ - [docs/evals.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/evals.md) — detection evaluation framework, adding test cases
219
+ - [docs/calibration.md](https://github.com/authentic-research-partners/sciwrite-lint/blob/main/docs/calibration.md) — SciLint Score calibration against ground-truth papers
220
220
 
221
221
  ## License
222
222
 
@@ -4,48 +4,54 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sciwrite-lint"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "A linter for scientific manuscripts — reference verification, consistency checking, and structural validation."
9
9
  requires-python = ">=3.13,<3.14"
10
10
  license = "MIT"
11
11
  authors = [{ name = "Sergey Samsonau" }]
12
+ maintainers = [{ name = "Authentic Research Partners LLC", email = "info@arpconnect.com" }]
12
13
  readme = "README.md"
13
- keywords = ["linter", "scientific-writing", "citation-verification", "latex"]
14
+ keywords = ["linter", "latex", "bibtex", "pdf", "scientific-writing", "academic-writing", "citation-checker", "reference-verification", "claim-verification", "manuscript", "peer-review", "arxiv", "vllm", "qwen"]
14
15
  classifiers = [
15
16
  "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
16
18
  "Intended Audience :: Science/Research",
17
- "Topic :: Text Processing :: Markup :: LaTeX",
19
+ "Topic :: Scientific/Engineering",
20
+ "Topic :: Software Development :: Quality Assurance",
21
+ "Programming Language :: Python :: 3",
18
22
  "Programming Language :: Python :: 3.13",
23
+ "Typing :: Typed",
19
24
  ]
20
25
  dependencies = [
21
- "httpx>=0.27",
22
- "tenacity>=8.0",
23
- "trafilatura>=1.8",
24
- "aiosqlite>=0.20",
26
+ "httpx>=0.28",
27
+ "tenacity>=9.0",
28
+ "trafilatura>=2.0",
29
+ "aiosqlite>=0.22",
25
30
  "loguru>=0.7",
26
31
  "anyascii>=0.3",
27
- "rich>=13.0",
32
+ "rich>=14.0",
28
33
  "defusedxml>=0.7",
29
- "pydantic>=2.0",
34
+ "pydantic>=2.10",
30
35
  "rapidfuzz>=3.0",
31
36
  "bibtexparser>=1.4",
32
- "pdfplumber>=0.10",
33
- "openai>=1.0",
37
+ "pdfplumber>=0.11",
38
+ "openai>=2.0",
34
39
  "grobid-tei-xml>=0.1",
35
- "sentence-transformers>=3.0",
36
- "numpy>=1.24",
40
+ "sentence-transformers>=5.0",
41
+ "numpy>=2.0",
37
42
  "sqlite-vec>=0.1.6",
38
- "torch>=2.0",
39
- "transformers>=4.45",
40
- "Pillow>=10.0",
41
- "pypdf>=4.0",
42
- "pypdfium2>=4.0",
43
- "pdf2image>=1.16",
43
+ "torch>=2.5",
44
+ "transformers>=4.50",
45
+ "Pillow>=12.0",
46
+ "pypdf>=6.0",
47
+ "pypdfium2>=5.0",
48
+ "pdf2image>=1.17",
44
49
  ]
45
50
 
46
51
  [project.urls]
47
- Homepage = "https://github.com/authentic-research-partners/sciwrite-lint"
52
+ Homepage = "https://arpconnect.com"
48
53
  Repository = "https://github.com/authentic-research-partners/sciwrite-lint"
54
+ Documentation = "https://github.com/authentic-research-partners/sciwrite-lint#readme"
49
55
  Issues = "https://github.com/authentic-research-partners/sciwrite-lint/issues"
50
56
 
51
57
  [project.optional-dependencies]
@@ -1,3 +1,3 @@
1
1
  """sciwrite-lint: a linter for scientific manuscripts."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.1"
@@ -18,6 +18,10 @@ from loguru import logger
18
18
 
19
19
  from sciwrite_lint import __version__
20
20
  from sciwrite_lint.config import LintConfig, PaperConfig, load_config
21
+ from sciwrite_lint.vllm.vllm_server import MODELS as _VLLM_MODELS
22
+
23
+ _ALL_MODELS = sorted(_VLLM_MODELS.keys())
24
+ _TEXT_MODELS = [k for k, v in _VLLM_MODELS.items() if v["kind"] == "text"]
21
25
 
22
26
 
23
27
  # ---------------------------------------------------------------------------
@@ -179,6 +183,12 @@ def main(argv: list[str] | None = None) -> int:
179
183
  "limit. Above 4, the live monitor may fail to track progress and "
180
184
  "vLLM/API throughput plateaus.",
181
185
  )
186
+ p_check.add_argument(
187
+ "--vision-backend",
188
+ choices=["transformers", "vllm"],
189
+ default=None,
190
+ help="Vision backend: transformers (2B, default) or vllm (8B FP8 on port 5002)",
191
+ )
182
192
  p_check.set_defaults(func=run_check)
183
193
 
184
194
  # --- checks ---
@@ -296,7 +306,7 @@ def main(argv: list[str] | None = None) -> int:
296
306
  )
297
307
  p_vc.add_argument(
298
308
  "--model",
299
- choices=["qwen3", "gemma3"],
309
+ choices=_TEXT_MODELS,
300
310
  default="",
301
311
  help="vLLM model preset (default: qwen3)",
302
312
  )
@@ -381,14 +391,20 @@ def main(argv: list[str] | None = None) -> int:
381
391
  # --- vision ---
382
392
  p_vision = sub.add_parser(
383
393
  "vision",
384
- help="Extract and describe manuscript figures via Qwen3-VL-2B (also runs automatically in check pipeline).",
394
+ help="Extract and describe manuscript figures (transformers 2B or vLLM 8B FP8).",
385
395
  )
386
396
  p_vision.add_argument("--paper", required=True, help="Paper name from [[papers]]")
387
397
  p_vision.add_argument(
388
398
  "--device",
389
399
  choices=["auto", "cpu", "cuda"],
390
- default="auto",
391
- help="Device for VL inference (default: auto — CUDA if available)",
400
+ default=None,
401
+ help="Device for VL inference (default: auto — CUDA if available, transformers only)",
402
+ )
403
+ p_vision.add_argument(
404
+ "--backend",
405
+ choices=["transformers", "vllm"],
406
+ default=None,
407
+ help="Vision backend: transformers (2B, default) or vllm (8B FP8 on port 5002)",
392
408
  )
393
409
  p_vision.add_argument(
394
410
  "--fresh",
@@ -407,9 +423,7 @@ def main(argv: list[str] | None = None) -> int:
407
423
  choices=["start", "stop", "restart", "status", "monitor"],
408
424
  help="start/stop/restart/status/monitor of all containers",
409
425
  )
410
- p_containers.add_argument(
411
- "--model", choices=["qwen3", "gemma3"], help="vLLM model to use"
412
- )
426
+ p_containers.add_argument("--model", choices=_ALL_MODELS, help="vLLM model to use")
413
427
  p_containers.add_argument(
414
428
  "--update",
415
429
  action="store_true",
@@ -427,6 +441,11 @@ def main(argv: list[str] | None = None) -> int:
427
441
  action="store_true",
428
442
  help="Remove and recreate containers (applies config changes like memory limits)",
429
443
  )
444
+ p_containers.add_argument(
445
+ "--vision",
446
+ action="store_true",
447
+ help="Also start vision vLLM (qwen3-vl-8b-fp8 on port 5002)",
448
+ )
430
449
  p_containers.add_argument("--config", help="Path to .sciwrite-lint.toml")
431
450
  p_containers.set_defaults(func=run_containers)
432
451
 
@@ -452,9 +471,7 @@ def main(argv: list[str] | None = None) -> int:
452
471
  p_vllm_status.set_defaults(func=run_vllm)
453
472
 
454
473
  p_vllm_start = vllm_sub.add_parser("start", help="Start vLLM server")
455
- p_vllm_start.add_argument(
456
- "--model", choices=["qwen3", "gemma3"], help="Model to serve"
457
- )
474
+ p_vllm_start.add_argument("--model", choices=_ALL_MODELS, help="Model to serve")
458
475
  p_vllm_start.add_argument(
459
476
  "--update",
460
477
  action="store_true",
@@ -464,13 +481,11 @@ def main(argv: list[str] | None = None) -> int:
464
481
  p_vllm_start.set_defaults(func=run_vllm)
465
482
 
466
483
  p_vllm_stop = vllm_sub.add_parser("stop", help="Stop vLLM server")
467
- p_vllm_stop.add_argument(
468
- "--model", choices=["qwen3", "gemma3"], help="Model to stop"
469
- )
484
+ p_vllm_stop.add_argument("--model", choices=_ALL_MODELS, help="Model to stop")
470
485
  p_vllm_stop.set_defaults(func=run_vllm)
471
486
 
472
487
  p_vllm_logs = vllm_sub.add_parser("logs", help="Show vLLM container logs")
473
- p_vllm_logs.add_argument("--model", choices=["qwen3", "gemma3"])
488
+ p_vllm_logs.add_argument("--model", choices=_ALL_MODELS)
474
489
  p_vllm_logs.add_argument(
475
490
  "-f", "--follow", action="store_true", help="Follow log output"
476
491
  )
@@ -478,7 +493,7 @@ def main(argv: list[str] | None = None) -> int:
478
493
  p_vllm_logs.set_defaults(func=run_vllm)
479
494
 
480
495
  p_vllm_rm = vllm_sub.add_parser("rm", help="Remove vLLM container")
481
- p_vllm_rm.add_argument("--model", choices=["qwen3", "gemma3"])
496
+ p_vllm_rm.add_argument("--model", choices=_ALL_MODELS)
482
497
  p_vllm_rm.add_argument(
483
498
  "--force", action="store_true", help="Force remove even if running"
484
499
  )
@@ -44,6 +44,9 @@ For each item, set is_genuine to true ONLY if the values or claims \
44
44
  CONFLICT. Set is_genuine to false if the passages agree or discuss \
45
45
  different topics.
46
46
 
47
+ Report at most 4 contradictions. If you find more, return only the 4 \
48
+ most clear-cut cases.
49
+
47
50
  Reply with JSON: {"contradictions": [\
48
51
  {"type": "number|claim|framing", "section_a_says": "...", \
49
52
  "section_b_says": "...", "explanation": "...", "is_genuine": true/false}]}
@@ -73,6 +73,8 @@ using established terminology loosely).
73
73
  - Set is_genuine to true ONLY for clear, unambiguous factual errors. \
74
74
  When in doubt, set is_genuine to false.
75
75
  - Prefer returning {{"issues": []}} over flagging borderline cases.
76
+ - Report at most 5 issues. If you find more than 5 genuine issues, \
77
+ return only the 5 most important ones.
76
78
 
77
79
  Return ONLY valid JSON: {{"issues": [{{"description": "...", "evidence": "...", \
78
80
  "location": "section or paragraph where the issue appears", \
@@ -93,12 +95,21 @@ Return {{"issues": []}} if no genuine issues found.\
93
95
  # question prompt) plus 2K safety padding for token estimation error.
94
96
  _OVERHEAD_TOKENS = 2500
95
97
 
96
- # Minimum output budget: enough for a few issues.
97
- _MIN_OUTPUT_TOKENS = 1024
98
+ # Output budget reserved for the JSON response. Matches the vLLM model
99
+ # default (VLLM_MODELS["qwen3"]["max_tokens"]). FullPaperIssueList caps
100
+ # the issues list at 5, which fits comfortably in this budget.
101
+ _OUTPUT_RESERVE_TOKENS = 2048
102
+
103
+ # Worst-case thinking budget (medium preset, see THINKING_PRESETS).
104
+ _THINKING_RESERVE_TOKENS = 1024
98
105
 
99
106
  # Rough chars-to-tokens ratio (conservative: overestimates tokens).
100
107
  _CHARS_PER_TOKEN = 3.5
101
108
 
109
+ # Total tokens reserved for overhead + thinking + output. Whatever is left
110
+ # in max_model_len is the budget for paper body + figure descriptions.
111
+ _RESERVED_TOKENS = _OVERHEAD_TOKENS + _OUTPUT_RESERVE_TOKENS + _THINKING_RESERVE_TOKENS
112
+
102
113
 
103
114
  def _estimate_tokens(text: str) -> int:
104
115
  """Estimate token count from character length (conservative)."""
@@ -151,8 +162,10 @@ def _get_max_model_len(config: "LintConfig") -> int:
151
162
  def _get_paper_body(tex_path: Path, config: "LintConfig") -> tuple[str, int] | None:
152
163
  """Return (paper_body, token_estimate) or *None* if too large.
153
164
 
154
- Checks the paper body against ``max_model_len`` minus fixed overhead,
155
- ensuring enough room for thinking + output.
165
+ Checks the paper body against ``max_model_len`` minus the fixed reserve
166
+ for prompt overhead, thinking budget, and output. The output budget is
167
+ constant — large papers are rejected rather than squeezed into a smaller
168
+ output window, because findings scale with paper size.
156
169
  """
157
170
  from sciwrite_lint.manuscript_store import get_or_create_manuscript_context
158
171
 
@@ -165,7 +178,7 @@ def _get_paper_body(tex_path: Path, config: "LintConfig") -> tuple[str, int] | N
165
178
  body_tokens = _estimate_tokens(body)
166
179
 
167
180
  max_model_len = _get_max_model_len(config)
168
- max_body_tokens = max_model_len - _OVERHEAD_TOKENS - _MIN_OUTPUT_TOKENS
181
+ max_body_tokens = max_model_len - _RESERVED_TOKENS
169
182
  if body_tokens > max_body_tokens:
170
183
  logger.info(
171
184
  "Paper body ~{}K tokens (limit ~{}K from max_model_len={}) "
@@ -180,21 +193,6 @@ def _get_paper_body(tex_path: Path, config: "LintConfig") -> tuple[str, int] | N
180
193
  return body, body_tokens
181
194
 
182
195
 
183
- def _compute_max_tokens(body_tokens: int, config: "LintConfig") -> int:
184
- """Compute max_tokens for output based on remaining context budget.
185
-
186
- ``max_tokens`` only caps the non-thinking output (JSON). Thinking
187
- budget is separate (set via ``extra_body.thinking.budget``). We give
188
- the output as much room as possible after the input fills the context.
189
- """
190
- max_model_len = _get_max_model_len(config)
191
- # Available = total context - input (body + overhead) - thinking budget
192
- # Thinking budget for medium=1024, low=200. Use 1024 as worst case.
193
- available = max_model_len - body_tokens - _OVERHEAD_TOKENS - 1024
194
- # Clamp to reasonable range
195
- return max(512, min(available, 4096))
196
-
197
-
198
196
  def _load_figure_descriptions(config: "LintConfig") -> str:
199
197
  """Load cached figure descriptions from the vision cache, if available."""
200
198
  if not config.current_paper:
@@ -213,11 +211,12 @@ def _build_system_prompt(
213
211
  tex_path: Path,
214
212
  config: "LintConfig",
215
213
  figure_descriptions: str = "",
216
- ) -> tuple[str, int] | None:
214
+ ) -> str | None:
217
215
  """Build the shared system prompt with the full paper body.
218
216
 
219
- Returns ``(system_prompt, max_tokens)`` or *None* if too large.
220
- ``max_tokens`` is computed dynamically based on remaining context.
217
+ Returns the system prompt string, or *None* if the paper body plus
218
+ figure descriptions would overflow the context window after reserving
219
+ the constant output + thinking + overhead budget.
221
220
 
222
221
  If ``figure_descriptions`` is not provided, attempts to load them
223
222
  from the vision cache in the paper workspace.
@@ -237,7 +236,7 @@ def _build_system_prompt(
237
236
  total_input_tokens = body_tokens + fig_tokens
238
237
 
239
238
  max_model_len = _get_max_model_len(config)
240
- max_input = max_model_len - _OVERHEAD_TOKENS - _MIN_OUTPUT_TOKENS
239
+ max_input = max_model_len - _RESERVED_TOKENS
241
240
  if total_input_tokens > max_input:
242
241
  logger.info(
243
242
  "Paper body + figures ~{}K tokens (limit ~{}K) — too large",
@@ -246,13 +245,10 @@ def _build_system_prompt(
246
245
  )
247
246
  return None
248
247
 
249
- system = _SYSTEM_TEMPLATE.format(
248
+ return _SYSTEM_TEMPLATE.format(
250
249
  paper_body=body,
251
250
  figure_section=figure_section,
252
251
  )
253
- max_tokens = _compute_max_tokens(total_input_tokens, config)
254
-
255
- return system, max_tokens
256
252
 
257
253
 
258
254
  # ---------------------------------------------------------------------------
@@ -542,12 +538,10 @@ def _make_check_fns(
542
538
  def _build_queries(
543
539
  tex_path: Path, config: "LintConfig"
544
540
  ) -> list[tuple[str, str, dict, str]]:
545
- result = _build_system_prompt(tex_path, config)
546
- if result is None:
541
+ system = _build_system_prompt(tex_path, config)
542
+ if system is None:
547
543
  _build_queries._state = None # type: ignore[attr-defined]
548
- _build_queries._max_tokens = None # type: ignore[attr-defined]
549
544
  return []
550
- system, max_tokens = result
551
545
 
552
546
  # Skip figure checks when no figure descriptions are available
553
547
  if (
@@ -555,11 +549,9 @@ def _make_check_fns(
555
549
  and "Not available." in system.split("</figure_descriptions>")[0]
556
550
  ):
557
551
  _build_queries._state = None # type: ignore[attr-defined]
558
- _build_queries._max_tokens = None # type: ignore[attr-defined]
559
552
  return []
560
553
 
561
554
  _build_queries._state = tex_path # type: ignore[attr-defined]
562
- _build_queries._max_tokens = max_tokens # type: ignore[attr-defined]
563
555
  return [(system, question, _ISSUE_SCHEMA, "FullPaperIssue")]
564
556
 
565
557
  def _process_results(results: list[dict | None]) -> list[Finding]:
@@ -538,49 +538,24 @@ def _describe_cited_figures(
538
538
  Returns {ref_key: formatted_figure_descriptions}.
539
539
  """
540
540
  from sciwrite_lint.vision.cache import format_descriptions_from_db
541
- from sciwrite_lint.vision.image_extraction import (
542
- ExtractedImage,
543
- extract_images_from_pdf,
544
- )
545
-
546
- # Collect images from all cited PDFs
547
- all_images: list[ExtractedImage] = []
548
- ref_image_ranges: dict[str, tuple[int, int]] = {} # key → (start, end) index
549
-
550
- output_dir = references_dir / "parsed" / "ref_figures"
551
- output_dir.mkdir(parents=True, exist_ok=True)
552
-
553
- for key in ref_keys:
554
- # Find the PDF for this key (may have suffix like _core, _arxiv)
555
- candidates = sorted(references_dir.glob(f"{key}*.pdf"))
556
- if not candidates:
557
- continue
558
- pdf_path = candidates[0]
559
-
560
- ref_output = output_dir / key
561
- start = len(all_images)
562
- images = extract_images_from_pdf(pdf_path, ref_output)
563
- all_images.extend(images)
564
- if images:
565
- ref_image_ranges[key] = (start, len(all_images))
541
+ from sciwrite_lint.vision.image_extraction import collect_cited_images
566
542
 
543
+ all_images, ref_image_ranges = collect_cited_images(ref_keys, references_dir)
567
544
  if not all_images:
568
545
  return {}
569
546
 
570
- # Run VL inference on all images at once (batched)
571
- from sciwrite_lint.vision.describe import describe_figures
547
+ # Run VL inference per-ref so each gets tagged with its source key.
548
+ from sciwrite_lint.vision.describe import describe_figures_by_source
572
549
 
573
- describe_figures(
574
- all_images,
575
- references_dir=references_dir,
576
- fresh=fresh,
550
+ describe_figures_by_source(
551
+ all_images, ref_image_ranges, references_dir, fresh=fresh
577
552
  )
578
553
 
579
554
  # Build per-ref description strings
580
555
  result: dict[str, str] = {}
581
556
  for key, (start, end) in ref_image_ranges.items():
582
557
  ref_images = all_images[start:end]
583
- desc = format_descriptions_from_db(ref_images, references_dir)
558
+ desc = format_descriptions_from_db(ref_images, references_dir, source=key)
584
559
  if desc:
585
560
  result[key] = desc
586
561
 
@@ -598,6 +573,9 @@ def _describe_cited_figures_vl(references_dir_str: str, fresh: bool = False) ->
598
573
 
599
574
  Extracts images, runs VL model, caches results in workspace.db.
600
575
  Called by _stage_cited_vision() in a subprocess for CUDA isolation.
576
+
577
+ Each ref's images are described separately so the cache entries get
578
+ tagged with the correct ``source=ref_key``.
601
579
  """
602
580
  from pathlib import Path
603
581
 
@@ -607,25 +585,14 @@ def _describe_cited_figures_vl(references_dir_str: str, fresh: bool = False) ->
607
585
  if not keys:
608
586
  return
609
587
 
610
- from sciwrite_lint.vision.describe import describe_figures
611
- from sciwrite_lint.vision.image_extraction import (
612
- ExtractedImage,
613
- extract_images_from_pdf,
614
- )
615
-
616
- all_images: list[ExtractedImage] = []
617
- output_dir = references_dir / "parsed" / "ref_figures"
618
- output_dir.mkdir(parents=True, exist_ok=True)
619
-
620
- for key in keys:
621
- candidates = sorted(references_dir.glob(f"{key}*.pdf"))
622
- if not candidates:
623
- continue
624
- images = extract_images_from_pdf(candidates[0], output_dir / key)
625
- all_images.extend(images)
588
+ from sciwrite_lint.vision.describe import describe_figures_by_source
589
+ from sciwrite_lint.vision.image_extraction import collect_cited_images
626
590
 
591
+ all_images, ref_image_ranges = collect_cited_images(keys, references_dir)
627
592
  if all_images:
628
- describe_figures(all_images, references_dir=references_dir, fresh=fresh)
593
+ describe_figures_by_source(
594
+ all_images, ref_image_ranges, references_dir, fresh=fresh
595
+ )
629
596
 
630
597
 
631
598
  # ---------------------------------------------------------------------------