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.
- {sciwrite_lint-0.2.0/sciwrite_lint.egg-info → sciwrite_lint-0.2.1}/PKG-INFO +34 -28
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/README.md +8 -8
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/pyproject.toml +26 -20
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/__init__.py +1 -1
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/__main__.py +30 -15
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/cross_section_consistency.py +3 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/full_paper_consistency.py +26 -34
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/ref_internal_checks.py +16 -49
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/cli/check.py +22 -21
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/cli/misc.py +382 -75
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/config.py +17 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/eval_claims.py +152 -77
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/llm_utils.py +99 -70
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/pipeline.py +537 -134
- sciwrite_lint-0.2.1/sciwrite_lint/py.typed +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/embedding_store.py +23 -14
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/reference_store.py +9 -23
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/workspace_db.py +220 -102
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/schemas.py +2 -2
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/usage.py +18 -42
- sciwrite_lint-0.2.1/sciwrite_lint/vision/cache.py +234 -0
- sciwrite_lint-0.2.1/sciwrite_lint/vision/describe.py +632 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/vision/image_extraction.py +31 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/vision/pipeline.py +23 -3
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/vllm/vllm_server.py +90 -11
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1/sciwrite_lint.egg-info}/PKG-INFO +34 -28
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint.egg-info/SOURCES.txt +1 -0
- sciwrite_lint-0.2.1/sciwrite_lint.egg-info/requires.txt +27 -0
- sciwrite_lint-0.2.0/sciwrite_lint/vision/cache.py +0 -140
- sciwrite_lint-0.2.0/sciwrite_lint/vision/describe.py +0 -311
- sciwrite_lint-0.2.0/sciwrite_lint.egg-info/requires.txt +0 -27
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/LICENSE +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/_network.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/api.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/__init__.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/_section_utils.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/cite_purpose.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/claim_support.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/dangling_cite.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/dangling_ref.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/reference_accuracy.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/reference_exists.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/reference_unreliable.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/registry.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/retracted_cite.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/structure_promises.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/unreferenced_figure.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/claims.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/claude_backend.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/claude_cli.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/cli/__init__.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/cli/config.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/cli/fetch.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/cli/rank.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/cli/verify.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/cross_paper.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/fulltext.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/local_pdfs.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/manuscript_store.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/models.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/pdf/__init__.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/pdf/grobid.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/pdf/pdf_download.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/prompt_safety.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/rate_limiter.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/__init__.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/citations.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/crossref.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/matching.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/metadata.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/references/retraction_watch.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/report.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/scoring/__init__.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/scoring/chain.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/scoring/contribution.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/scoring/scilint_score.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/tex_parser.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/vision/__init__.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/vllm/__init__.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/vllm/metrics.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/web.py +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint.egg-info/dependency_links.txt +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint.egg-info/entry_points.txt +0 -0
- {sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint.egg-info/top_level.txt +0 -0
- {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.
|
|
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://
|
|
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,
|
|
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 ::
|
|
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.
|
|
19
|
-
Requires-Dist: tenacity>=
|
|
20
|
-
Requires-Dist: trafilatura>=
|
|
21
|
-
Requires-Dist: aiosqlite>=0.
|
|
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>=
|
|
30
|
+
Requires-Dist: rich>=14.0
|
|
25
31
|
Requires-Dist: defusedxml>=0.7
|
|
26
|
-
Requires-Dist: pydantic>=2.
|
|
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.
|
|
30
|
-
Requires-Dist: openai>=
|
|
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>=
|
|
33
|
-
Requires-Dist: numpy>=
|
|
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.
|
|
36
|
-
Requires-Dist: transformers>=4.
|
|
37
|
-
Requires-Dist: Pillow>=
|
|
38
|
-
Requires-Dist: pypdf>=
|
|
39
|
-
Requires-Dist: pypdfium2>=
|
|
40
|
-
Requires-Dist: pdf2image>=1.
|
|
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,
|
|
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
|
-

|
|
166
|
+

|
|
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,
|
|
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
|
-

|
|
115
|
+

|
|
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.
|
|
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", "
|
|
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 ::
|
|
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.
|
|
22
|
-
"tenacity>=
|
|
23
|
-
"trafilatura>=
|
|
24
|
-
"aiosqlite>=0.
|
|
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>=
|
|
32
|
+
"rich>=14.0",
|
|
28
33
|
"defusedxml>=0.7",
|
|
29
|
-
"pydantic>=2.
|
|
34
|
+
"pydantic>=2.10",
|
|
30
35
|
"rapidfuzz>=3.0",
|
|
31
36
|
"bibtexparser>=1.4",
|
|
32
|
-
"pdfplumber>=0.
|
|
33
|
-
"openai>=
|
|
37
|
+
"pdfplumber>=0.11",
|
|
38
|
+
"openai>=2.0",
|
|
34
39
|
"grobid-tei-xml>=0.1",
|
|
35
|
-
"sentence-transformers>=
|
|
36
|
-
"numpy>=
|
|
40
|
+
"sentence-transformers>=5.0",
|
|
41
|
+
"numpy>=2.0",
|
|
37
42
|
"sqlite-vec>=0.1.6",
|
|
38
|
-
"torch>=2.
|
|
39
|
-
"transformers>=4.
|
|
40
|
-
"Pillow>=
|
|
41
|
-
"pypdf>=
|
|
42
|
-
"pypdfium2>=
|
|
43
|
-
"pdf2image>=1.
|
|
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://
|
|
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]
|
|
@@ -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=
|
|
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
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
)
|
{sciwrite_lint-0.2.0 → sciwrite_lint-0.2.1}/sciwrite_lint/checks/cross_section_consistency.py
RENAMED
|
@@ -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
|
-
#
|
|
97
|
-
|
|
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
|
|
155
|
-
|
|
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 -
|
|
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
|
-
) ->
|
|
214
|
+
) -> str | None:
|
|
217
215
|
"""Build the shared system prompt with the full paper body.
|
|
218
216
|
|
|
219
|
-
Returns
|
|
220
|
-
|
|
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 -
|
|
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
|
-
|
|
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
|
-
|
|
546
|
-
if
|
|
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
|
|
571
|
-
from sciwrite_lint.vision.describe import
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
593
|
+
describe_figures_by_source(
|
|
594
|
+
all_images, ref_image_ranges, references_dir, fresh=fresh
|
|
595
|
+
)
|
|
629
596
|
|
|
630
597
|
|
|
631
598
|
# ---------------------------------------------------------------------------
|