pdf2wiki 0.2.2__tar.gz → 0.2.4__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.
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/CHANGELOG.md +26 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/PKG-INFO +1 -1
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/pyproject.toml +1 -1
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/__init__.py +1 -1
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/cli.py +23 -2
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/convert/merge.py +6 -1
- pdf2wiki-0.2.4/src/pdf2wiki/qa/flagged.py +42 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/tests/test_batch.py +14 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/tests/test_convert.py +33 -0
- pdf2wiki-0.2.4/tests/test_qa.py +94 -0
- pdf2wiki-0.2.2/tests/test_qa.py +0 -44
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/.claude-plugin/marketplace.json +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/.gitignore +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/CONTRIBUTING.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/LICENSE +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/README.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/README.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/architecture/architecture.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/explanation/design-principles.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/explanation/how-the-merge-works.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/explanation/why-dual-backend.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/how-to/convert-a-book.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/how-to/convert-in-the-cloud.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/how-to/install.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/how-to/offload-hybrid-to-a-server.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/how-to/post-process-and-split.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/how-to/qa-a-conversion.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/how-to/run-a-batch.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/how-to/set-up-remote-gpu.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/how-to/troubleshoot.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/llm-wiki/README.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/llm-wiki/explanation/why-llm-wiki.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/llm-wiki/how-to/configure-vault-location.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/llm-wiki/how-to/install.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/llm-wiki/how-to/review-a-project.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/llm-wiki/reference/skills-and-agent.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/llm-wiki/reference/vault-layout.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/llm-wiki/tutorials/query-your-first-vault.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/reference/cli.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/reference/configuration.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/reference/output-layout.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/reference/phase5-steps.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/reference/pipeline-stages.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/docs/tutorials/convert-your-first-book.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/plugin/.claude-plugin/plugin.json +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/plugin/LICENSE +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/plugin/README.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/plugin/agents/knowledge-researcher.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/plugin/skills/knowledge-query/SKILL.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/plugin/skills/knowledge-review/SKILL.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/batch.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/config.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/convert/__init__.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/convert/cloud.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/executor.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/phase5/__init__.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/phase5/caption_unbleed.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/phase5/chapter_split.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/phase5/code_unescape.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/phase5/dash_normalize.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/phase5/lang_retag.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/phase5/mermaid_repair.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/qa/__init__.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/qa/review.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/qa/sample.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/render.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/src/pdf2wiki/scan.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/tests/test_cloud.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/tests/test_executor.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/tests/test_phase5.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/tests/test_render.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.4}/tests/test_scan.py +0 -0
|
@@ -4,6 +4,32 @@ All notable changes to this project are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.2.4] - 2026-07-23
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Remote convert produced no output** (`no content_list.json`). `run_mineru` handed MinerU a
|
|
11
|
+
*relative* `-o`/`-p`, but MinerU runs with a different working directory (the stdlib-shadow-safe
|
|
12
|
+
`clean_cwd`), so its output landed where pdf2wiki couldn't find it. Only surfaced in `--remote` mode,
|
|
13
|
+
where `--out` is passed home-relative. Now the paths handed to MinerU are absolutized
|
|
14
|
+
(`os.path.abspath`) — idempotent for the already-absolute local case. Found by the first real
|
|
15
|
+
end-to-end remote run; +1 regression test reproducing the cwd divergence.
|
|
16
|
+
|
|
17
|
+
## [0.2.3] - 2026-07-23
|
|
18
|
+
|
|
19
|
+
QA + diagnostics from an external review of the repo. No breaking changes; +4 tests (103 → 107).
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **`pdf2wiki qa flags PATHS...`** — per-book report of the code blocks where the VLM diverged from the
|
|
23
|
+
byte-clean text layer (`_code_flag`), or where hybrid indentation failed a Python ast check
|
|
24
|
+
(`_indent_flag`), read from `blocks.json`. Ranks multiple books by flagged count (which books to
|
|
25
|
+
trust least) and lists each flagged block (page / language / snippet) for a single book — the
|
|
26
|
+
highest-signal spots to spot-check.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- **Batch summary rolls up `error_class`** — a partial batch now prints, e.g.,
|
|
30
|
+
`5 book(s) not done — by class: permanent×3, timeout×1, fetch×1` (plus the slug list), so a cluster
|
|
31
|
+
of same-kind failures reads as one diagnosis instead of N separate slugs. Exit code unchanged.
|
|
32
|
+
|
|
7
33
|
## [0.2.2] - 2026-07-23
|
|
8
34
|
|
|
9
35
|
Resilience + security hardening from a book-grounded review (Tech-Books vault: Backoff-Retries,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pdf2wiki
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Convert technical books (native-text PDFs) into clean, chapter-split, LLM-ready Markdown using a dual-pass MinerU pipeline plus fidelity-preserving post-processing.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Sevthered/pdf2wiki
|
|
6
6
|
Project-URL: Repository, https://github.com/Sevthered/pdf2wiki
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pdf2wiki"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
description = "Convert technical books (native-text PDFs) into clean, chapter-split, LLM-ready Markdown using a dual-pass MinerU pipeline plus fidelity-preserving post-processing."
|
|
5
5
|
authors = [{ name = "Sevthered" }]
|
|
6
6
|
readme = "README.md"
|
|
@@ -99,6 +99,19 @@ def _cmd_qa_review(a, cfg):
|
|
|
99
99
|
return 0
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
def _cmd_qa_flagged(a, cfg):
|
|
103
|
+
from .qa.flagged import flagged_report
|
|
104
|
+
reports = sorted((flagged_report(p) for p in a.blocks), key=lambda r: r["flagged"], reverse=True)
|
|
105
|
+
print(f"{'flagged':>7} {'diverged':>8} {'indent':>6} {'code':>5} book")
|
|
106
|
+
for r in reports:
|
|
107
|
+
print(f"{r['flagged']:>7} {r['diverged']:>8} {r['indent_suspect']:>6} {r['code_blocks']:>5} {r['name']}")
|
|
108
|
+
if len(reports) == 1: # single book -> also list the blocks to eyeball
|
|
109
|
+
print()
|
|
110
|
+
for e in reports[0]["blocks"]:
|
|
111
|
+
print(f" p{e['page']:<4} [{e['lang'] or '?'}] {e['flag']}: {e['snippet']}")
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
|
|
102
115
|
def _cmd_scan(a, cfg):
|
|
103
116
|
from .scan import scan_dir
|
|
104
117
|
print(json.dumps(scan_dir(a.directory), indent=1))
|
|
@@ -109,9 +122,13 @@ def _cmd_batch(a, cfg):
|
|
|
109
122
|
from .batch import run_batch
|
|
110
123
|
manifest = run_batch(a.books, cfg, a.stage, remote=a.remote or (cfg.remote.host or None),
|
|
111
124
|
max_books=a.max_books, only=a.only, vault=a.vault or (cfg.output.vault or None))
|
|
112
|
-
failed = [s for s, e in manifest.items() if e.get("status") != "done"]
|
|
125
|
+
failed = [(s, e) for s, e in manifest.items() if e.get("status") != "done"]
|
|
113
126
|
if failed: # non-zero exit so CI/automation can detect a partial batch
|
|
114
|
-
|
|
127
|
+
from collections import Counter
|
|
128
|
+
by_class = Counter(e.get("error_class", "unknown") for _, e in failed)
|
|
129
|
+
rollup = ", ".join(f"{k}×{v}" for k, v in by_class.most_common())
|
|
130
|
+
print(f"batch: {len(failed)} book(s) not done — by class: {rollup}", file=sys.stderr)
|
|
131
|
+
print(f" not done: {[s for s, _ in failed]}", file=sys.stderr)
|
|
115
132
|
return 1
|
|
116
133
|
return 0
|
|
117
134
|
|
|
@@ -166,6 +183,10 @@ def main(argv=None):
|
|
|
166
183
|
p.add_argument("name")
|
|
167
184
|
p.add_argument("--blocks", default=None, help="explicit blocks.json path")
|
|
168
185
|
p.set_defaults(fn=_cmd_qa_review)
|
|
186
|
+
p = qsub.add_parser("flags", help="report code blocks the VLM diverged on (from blocks.json), "
|
|
187
|
+
"ranked across books — the highest-signal QA sample")
|
|
188
|
+
p.add_argument("blocks", nargs="+", help="one or more converted-book blocks.json paths")
|
|
189
|
+
p.set_defaults(fn=_cmd_qa_flagged)
|
|
169
190
|
|
|
170
191
|
p = sub.add_parser("scan", help="scan a directory of PDFs -> title/year guesses (JSON)")
|
|
171
192
|
p.add_argument("directory")
|
|
@@ -179,7 +179,12 @@ def run_mineru(mineru_bin, pdf, a, b, backend, extra, outdir, clean_cwd, env,
|
|
|
179
179
|
cl = glob.glob(f"{outdir}/*/*/*content_list.json")
|
|
180
180
|
if not (os.path.exists(done) and cl):
|
|
181
181
|
os.makedirs(outdir, exist_ok=True)
|
|
182
|
-
|
|
182
|
+
# MinerU runs with cwd=clean_cwd (stdlib-shadow-safe), NOT pdf2wiki's cwd — so any relative path
|
|
183
|
+
# handed to it resolves against the wrong directory and its output lands where our glob can't see
|
|
184
|
+
# it (remote mode passes a home-relative --out). Absolutize the paths MinerU receives; abspath is
|
|
185
|
+
# idempotent for already-absolute (local) paths. See bug-pdf2wiki-remote-relative-outdir.
|
|
186
|
+
cmd = ([mineru_bin, "-p", os.path.abspath(pdf), "-o", os.path.abspath(outdir),
|
|
187
|
+
"-b", backend, "-s", str(a), "-e", str(b)] + extra)
|
|
183
188
|
print(" run:", " ".join(cmd[3:]))
|
|
184
189
|
with open(f"{outdir}.log", "w", encoding="utf-8") as log:
|
|
185
190
|
# start_new_session=True puts MinerU in its own process group so a timeout can SIGKILL the
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Per-book flagged-block report: which code blocks the VLM diverged on — the highest-signal QA
|
|
2
|
+
sample. Pure read over a converted book's blocks.json (no converter or schema change). The flags are
|
|
3
|
+
set during merge in convert/merge.py: `_code_flag` = the hybrid/VLM code diverged from the byte-clean
|
|
4
|
+
text layer (output shows the pipeline tokens); `_indent_flag` = tokens agreed but the hybrid
|
|
5
|
+
indentation failed a Python ast sanity check. These are exactly the places worth eyeballing."""
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def flagged_report(blocks_path: str, name: str | None = None) -> dict:
|
|
11
|
+
"""Summarize the flagged code blocks in one book's blocks.json.
|
|
12
|
+
|
|
13
|
+
Returns {name, code_blocks, flagged, diverged, indent_suspect, blocks} where `blocks` is the
|
|
14
|
+
page-sorted list of flagged entries {page, lang, flag, snippet}; `page` is 1-based for display.
|
|
15
|
+
"""
|
|
16
|
+
path = os.path.expanduser(blocks_path)
|
|
17
|
+
with open(path, encoding="utf-8") as f:
|
|
18
|
+
blocks = json.load(f)
|
|
19
|
+
name = name or os.path.basename(os.path.dirname(os.path.abspath(path)))
|
|
20
|
+
|
|
21
|
+
code_blocks = 0
|
|
22
|
+
flagged = []
|
|
23
|
+
for b in blocks:
|
|
24
|
+
if b.get("type") != "code":
|
|
25
|
+
continue
|
|
26
|
+
code_blocks += 1
|
|
27
|
+
if b.get("_code_flag"):
|
|
28
|
+
flag = "diverged"
|
|
29
|
+
elif b.get("_indent_flag"):
|
|
30
|
+
flag = "indent"
|
|
31
|
+
else:
|
|
32
|
+
continue
|
|
33
|
+
body = str(b.get("code_body") or "")
|
|
34
|
+
snippet = next((ln.strip() for ln in body.splitlines() if ln.strip()), "")[:80]
|
|
35
|
+
flagged.append({"page": int(b.get("abs_page", 0)) + 1, "lang": b.get("sub_type") or "",
|
|
36
|
+
"flag": flag, "snippet": snippet})
|
|
37
|
+
|
|
38
|
+
flagged.sort(key=lambda e: e["page"])
|
|
39
|
+
return {"name": name, "code_blocks": code_blocks, "flagged": len(flagged),
|
|
40
|
+
"diverged": sum(1 for e in flagged if e["flag"] == "diverged"),
|
|
41
|
+
"indent_suspect": sum(1 for e in flagged if e["flag"] == "indent"),
|
|
42
|
+
"blocks": flagged}
|
|
@@ -125,3 +125,17 @@ def test_no_breaker_when_executor_stays_healthy(tmp_path, monkeypatch):
|
|
|
125
125
|
failed = [s for s, e in manifest.items() if e["status"] == "convert_failed"]
|
|
126
126
|
assert len(failed) == 5 # all attempted, no premature abort
|
|
127
127
|
assert manifest["book-0"]["error_class"] == "permanent"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_cmd_batch_rolls_up_error_class(monkeypatch, capsys):
|
|
131
|
+
# a partial batch must aggregate error_class so a cluster of same-kind failures reads as one line.
|
|
132
|
+
monkeypatch.setattr(batch, "run_batch",
|
|
133
|
+
lambda *a, **k: {"book-a": {"status": "done"},
|
|
134
|
+
"book-b": {"status": "convert_failed", "error_class": "permanent"},
|
|
135
|
+
"book-c": {"status": "convert_failed", "error_class": "permanent"},
|
|
136
|
+
"book-d": {"status": "convert_failed", "error_class": "TimeoutExpired"}})
|
|
137
|
+
assert cli.main(["batch", "books.toml"]) == 1
|
|
138
|
+
err = capsys.readouterr().err
|
|
139
|
+
assert "3 book(s) not done" in err
|
|
140
|
+
assert "permanent×2" in err and "TimeoutExpired×1" in err
|
|
141
|
+
assert "not done:" in err # slug detail retained
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Unit tests for the converter's pure functions (no MinerU / GPU needed)."""
|
|
2
|
+
import json
|
|
2
3
|
import os
|
|
3
4
|
import subprocess
|
|
4
5
|
import sys
|
|
@@ -346,3 +347,35 @@ def test_render_emits_code_verify_flag():
|
|
|
346
347
|
out = render(flagged)
|
|
347
348
|
assert out.startswith("<!-- code-verify:")
|
|
348
349
|
assert "private_key" in out
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def test_run_mineru_passes_absolute_paths_to_mineru(tmp_path, monkeypatch):
|
|
353
|
+
# MinerU runs in clean_cwd, NOT pdf2wiki's cwd. If run_mineru hands it a relative -o/-p, MinerU's
|
|
354
|
+
# output lands under clean_cwd and pdf2wiki's glob (against its own cwd) misses it -> the remote-mode
|
|
355
|
+
# "no content_list.json" bug. run_mineru must absolutize the paths it hands MinerU.
|
|
356
|
+
proj = tmp_path / "proj"; proj.mkdir()
|
|
357
|
+
clean = tmp_path / "clean"; clean.mkdir() # a DIFFERENT cwd, as clean_cwd would be on the box
|
|
358
|
+
monkeypatch.chdir(proj) # pdf2wiki's cwd
|
|
359
|
+
(proj / "in.pdf").write_bytes(b"%PDF-1.4")
|
|
360
|
+
captured = {}
|
|
361
|
+
|
|
362
|
+
class FakeProc:
|
|
363
|
+
pid = 4321
|
|
364
|
+
def __init__(self, cmd, cwd):
|
|
365
|
+
captured["cmd"] = cmd
|
|
366
|
+
odir = cmd[cmd.index("-o") + 1]
|
|
367
|
+
# simulate MinerU resolving its -o against ITS cwd (clean_cwd) when relative
|
|
368
|
+
base = odir if os.path.isabs(odir) else os.path.join(cwd, odir)
|
|
369
|
+
dst = os.path.join(base, "in", "txt"); os.makedirs(dst, exist_ok=True)
|
|
370
|
+
with open(os.path.join(dst, "in_content_list.json"), "w") as f:
|
|
371
|
+
json.dump([{"type": "text", "page_idx": 0, "bbox": [0, 0, 1, 1], "text": "x"}], f)
|
|
372
|
+
def wait(self, timeout=None): return 0
|
|
373
|
+
|
|
374
|
+
monkeypatch.setattr(merge_mod.subprocess, "Popen",
|
|
375
|
+
lambda cmd, **k: FakeProc(cmd, k.get("cwd")))
|
|
376
|
+
blocks, _ = run_mineru("mineru", "in.pdf", 0, 1, "pipeline", ["-m", "txt"],
|
|
377
|
+
"reldir", str(clean), {}, label="pipeline 0-1", timeout=60)
|
|
378
|
+
oi, pi = captured["cmd"].index("-o"), captured["cmd"].index("-p")
|
|
379
|
+
assert os.path.isabs(captured["cmd"][oi + 1]), "MinerU -o must be absolute"
|
|
380
|
+
assert os.path.isabs(captured["cmd"][pi + 1]), "MinerU -p must be absolute"
|
|
381
|
+
assert blocks and blocks[0]["text"] == "x" # found the content_list MinerU 'wrote'
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Tests for qa.sample page-window math on short books (no GPU/MinerU needed).
|
|
2
|
+
|
|
3
|
+
Regression: `random.sample(range(lo, hi), n)` raised `ValueError: Sample larger than
|
|
4
|
+
population` on any book whose 5..95% window was smaller than the requested sample size.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
10
|
+
|
|
11
|
+
import pymupdf # runtime dependency
|
|
12
|
+
|
|
13
|
+
from pdf2wiki.qa.sample import sample_pages
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _make_pdf(path, pages):
|
|
17
|
+
d = pymupdf.open()
|
|
18
|
+
for _ in range(pages):
|
|
19
|
+
d.new_page(width=200, height=200)
|
|
20
|
+
d.save(path)
|
|
21
|
+
d.close()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_sample_short_book_clamps_instead_of_crashing(tmp_path):
|
|
25
|
+
pdf = str(tmp_path / "short.pdf")
|
|
26
|
+
_make_pdf(pdf, 5) # window 0..4 < default n=20
|
|
27
|
+
r = sample_pages(pdf, "short", str(tmp_path / "qa"), n=20)
|
|
28
|
+
assert 0 < len(r["pages"]) <= 4 # clamped to the window, no ValueError
|
|
29
|
+
assert len(set(r["pages"])) == len(r["pages"]) # still distinct
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_sample_single_page_book(tmp_path):
|
|
33
|
+
pdf = str(tmp_path / "one.pdf")
|
|
34
|
+
_make_pdf(pdf, 1) # 5..95% window empty -> falls back to whole book
|
|
35
|
+
r = sample_pages(pdf, "one", str(tmp_path / "qa"), n=20)
|
|
36
|
+
assert r["pages"] == [0]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_sample_normal_book_unchanged(tmp_path):
|
|
40
|
+
pdf = str(tmp_path / "big.pdf")
|
|
41
|
+
_make_pdf(pdf, 100) # window 5..95 holds 90 pages
|
|
42
|
+
r = sample_pages(pdf, "big", str(tmp_path / "qa"), n=20)
|
|
43
|
+
assert len(r["pages"]) == 20
|
|
44
|
+
assert all(5 <= p < 95 for p in r["pages"])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------- qa.flagged: per-book flagged-block report (T3) ----------
|
|
48
|
+
import json
|
|
49
|
+
|
|
50
|
+
import pdf2wiki.cli as cli
|
|
51
|
+
from pdf2wiki.qa.flagged import flagged_report
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _write_blocks(dirpath, specs):
|
|
55
|
+
"""specs: list of (type, flag) where flag in {None, '_code_flag', '_indent_flag'}."""
|
|
56
|
+
os.makedirs(dirpath, exist_ok=True)
|
|
57
|
+
blocks = []
|
|
58
|
+
for i, (t, flag) in enumerate(specs):
|
|
59
|
+
b = {"type": t, "abs_page": i, "sub_type": "python", "code_body": f"line_{i} = {i}\nmore"}
|
|
60
|
+
if flag:
|
|
61
|
+
b[flag] = True
|
|
62
|
+
blocks.append(b)
|
|
63
|
+
p = os.path.join(dirpath, "blocks.json")
|
|
64
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
65
|
+
json.dump(blocks, f)
|
|
66
|
+
return p
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_flagged_report_counts(tmp_path):
|
|
70
|
+
p = _write_blocks(str(tmp_path / "mybook"), [
|
|
71
|
+
("code", None), ("code", "_code_flag"), ("code", "_indent_flag"),
|
|
72
|
+
("code", "_code_flag"), ("text", None)])
|
|
73
|
+
r = flagged_report(p)
|
|
74
|
+
assert r["name"] == "mybook"
|
|
75
|
+
assert r["code_blocks"] == 4 # 4 code blocks, the text block ignored
|
|
76
|
+
assert r["diverged"] == 2 and r["indent_suspect"] == 1 and r["flagged"] == 3
|
|
77
|
+
assert [e["flag"] for e in r["blocks"]] == ["diverged", "indent", "diverged"] # page-sorted
|
|
78
|
+
assert r["blocks"][0]["page"] == 2 and r["blocks"][0]["lang"] == "python" # abs_page 1 -> pg 2
|
|
79
|
+
assert r["blocks"][0]["snippet"].startswith("line_1")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_flagged_report_empty(tmp_path):
|
|
83
|
+
p = _write_blocks(str(tmp_path / "clean"), [("code", None), ("code", None), ("text", None)])
|
|
84
|
+
r = flagged_report(p)
|
|
85
|
+
assert r["flagged"] == 0 and r["blocks"] == [] and r["code_blocks"] == 2
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_cmd_qa_flags_ranks_books(tmp_path, capsys):
|
|
89
|
+
a = _write_blocks(str(tmp_path / "alpha"), [("code", "_code_flag"), ("code", None)]) # 1 flag
|
|
90
|
+
b = _write_blocks(str(tmp_path / "beta"),
|
|
91
|
+
[("code", "_code_flag"), ("code", "_code_flag"), ("code", "_indent_flag")]) # 3 flags
|
|
92
|
+
assert cli.main(["qa", "flags", a, b]) == 0
|
|
93
|
+
out = capsys.readouterr().out
|
|
94
|
+
assert out.index("beta") < out.index("alpha") # most-flagged book ranked first
|
pdf2wiki-0.2.2/tests/test_qa.py
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
"""Tests for qa.sample page-window math on short books (no GPU/MinerU needed).
|
|
2
|
-
|
|
3
|
-
Regression: `random.sample(range(lo, hi), n)` raised `ValueError: Sample larger than
|
|
4
|
-
population` on any book whose 5..95% window was smaller than the requested sample size.
|
|
5
|
-
"""
|
|
6
|
-
import os
|
|
7
|
-
import sys
|
|
8
|
-
|
|
9
|
-
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
10
|
-
|
|
11
|
-
import pymupdf # runtime dependency
|
|
12
|
-
|
|
13
|
-
from pdf2wiki.qa.sample import sample_pages
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _make_pdf(path, pages):
|
|
17
|
-
d = pymupdf.open()
|
|
18
|
-
for _ in range(pages):
|
|
19
|
-
d.new_page(width=200, height=200)
|
|
20
|
-
d.save(path)
|
|
21
|
-
d.close()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_sample_short_book_clamps_instead_of_crashing(tmp_path):
|
|
25
|
-
pdf = str(tmp_path / "short.pdf")
|
|
26
|
-
_make_pdf(pdf, 5) # window 0..4 < default n=20
|
|
27
|
-
r = sample_pages(pdf, "short", str(tmp_path / "qa"), n=20)
|
|
28
|
-
assert 0 < len(r["pages"]) <= 4 # clamped to the window, no ValueError
|
|
29
|
-
assert len(set(r["pages"])) == len(r["pages"]) # still distinct
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def test_sample_single_page_book(tmp_path):
|
|
33
|
-
pdf = str(tmp_path / "one.pdf")
|
|
34
|
-
_make_pdf(pdf, 1) # 5..95% window empty -> falls back to whole book
|
|
35
|
-
r = sample_pages(pdf, "one", str(tmp_path / "qa"), n=20)
|
|
36
|
-
assert r["pages"] == [0]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_sample_normal_book_unchanged(tmp_path):
|
|
40
|
-
pdf = str(tmp_path / "big.pdf")
|
|
41
|
-
_make_pdf(pdf, 100) # window 5..95 holds 90 pages
|
|
42
|
-
r = sample_pages(pdf, "big", str(tmp_path / "qa"), n=20)
|
|
43
|
-
assert len(r["pages"]) == 20
|
|
44
|
-
assert all(5 <= p < 95 for p in r["pages"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|