pdf2wiki 0.2.2__tar.gz → 0.2.3__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.3}/CHANGELOG.md +16 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/PKG-INFO +1 -1
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/pyproject.toml +1 -1
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/__init__.py +1 -1
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/cli.py +23 -2
- pdf2wiki-0.2.3/src/pdf2wiki/qa/flagged.py +42 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/tests/test_batch.py +14 -0
- pdf2wiki-0.2.3/tests/test_qa.py +94 -0
- pdf2wiki-0.2.2/tests/test_qa.py +0 -44
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/.claude-plugin/marketplace.json +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/.gitignore +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/CONTRIBUTING.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/LICENSE +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/README.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/README.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/architecture/architecture.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/explanation/design-principles.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/explanation/how-the-merge-works.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/explanation/why-dual-backend.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/convert-a-book.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/convert-in-the-cloud.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/install.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/offload-hybrid-to-a-server.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/post-process-and-split.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/qa-a-conversion.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/run-a-batch.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/set-up-remote-gpu.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/troubleshoot.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/llm-wiki/README.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/llm-wiki/explanation/why-llm-wiki.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/llm-wiki/how-to/configure-vault-location.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/llm-wiki/how-to/install.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/llm-wiki/how-to/review-a-project.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/llm-wiki/reference/skills-and-agent.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/llm-wiki/reference/vault-layout.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/llm-wiki/tutorials/query-your-first-vault.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/reference/cli.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/reference/configuration.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/reference/output-layout.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/reference/phase5-steps.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/reference/pipeline-stages.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/tutorials/convert-your-first-book.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/plugin/.claude-plugin/plugin.json +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/plugin/LICENSE +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/plugin/README.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/plugin/agents/knowledge-researcher.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/plugin/skills/knowledge-query/SKILL.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/plugin/skills/knowledge-review/SKILL.md +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/batch.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/config.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/convert/__init__.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/convert/cloud.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/convert/merge.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/executor.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/phase5/__init__.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/phase5/caption_unbleed.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/phase5/chapter_split.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/phase5/code_unescape.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/phase5/dash_normalize.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/phase5/lang_retag.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/phase5/mermaid_repair.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/qa/__init__.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/qa/review.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/qa/sample.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/render.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/scan.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/tests/test_cloud.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/tests/test_convert.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/tests/test_executor.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/tests/test_phase5.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/tests/test_render.py +0 -0
- {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/tests/test_scan.py +0 -0
|
@@ -4,6 +4,22 @@ 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.3] - 2026-07-23
|
|
8
|
+
|
|
9
|
+
QA + diagnostics from an external review of the repo. No breaking changes; +4 tests (103 → 107).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **`pdf2wiki qa flags PATHS...`** — per-book report of the code blocks where the VLM diverged from the
|
|
13
|
+
byte-clean text layer (`_code_flag`), or where hybrid indentation failed a Python ast check
|
|
14
|
+
(`_indent_flag`), read from `blocks.json`. Ranks multiple books by flagged count (which books to
|
|
15
|
+
trust least) and lists each flagged block (page / language / snippet) for a single book — the
|
|
16
|
+
highest-signal spots to spot-check.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **Batch summary rolls up `error_class`** — a partial batch now prints, e.g.,
|
|
20
|
+
`5 book(s) not done — by class: permanent×3, timeout×1, fetch×1` (plus the slug list), so a cluster
|
|
21
|
+
of same-kind failures reads as one diagnosis instead of N separate slugs. Exit code unchanged.
|
|
22
|
+
|
|
7
23
|
## [0.2.2] - 2026-07-23
|
|
8
24
|
|
|
9
25
|
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.3
|
|
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.3"
|
|
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")
|
|
@@ -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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|