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.
Files changed (72) hide show
  1. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/CHANGELOG.md +16 -0
  2. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/PKG-INFO +1 -1
  3. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/pyproject.toml +1 -1
  4. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/__init__.py +1 -1
  5. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/cli.py +23 -2
  6. pdf2wiki-0.2.3/src/pdf2wiki/qa/flagged.py +42 -0
  7. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/tests/test_batch.py +14 -0
  8. pdf2wiki-0.2.3/tests/test_qa.py +94 -0
  9. pdf2wiki-0.2.2/tests/test_qa.py +0 -44
  10. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/.claude-plugin/marketplace.json +0 -0
  11. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/.gitignore +0 -0
  12. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/CONTRIBUTING.md +0 -0
  13. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/LICENSE +0 -0
  14. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/README.md +0 -0
  15. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/README.md +0 -0
  16. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/architecture/architecture.md +0 -0
  17. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/explanation/design-principles.md +0 -0
  18. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/explanation/how-the-merge-works.md +0 -0
  19. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/explanation/why-dual-backend.md +0 -0
  20. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/convert-a-book.md +0 -0
  21. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/convert-in-the-cloud.md +0 -0
  22. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/install.md +0 -0
  23. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/offload-hybrid-to-a-server.md +0 -0
  24. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/post-process-and-split.md +0 -0
  25. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/qa-a-conversion.md +0 -0
  26. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/run-a-batch.md +0 -0
  27. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/set-up-remote-gpu.md +0 -0
  28. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/how-to/troubleshoot.md +0 -0
  29. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/llm-wiki/README.md +0 -0
  30. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/llm-wiki/explanation/why-llm-wiki.md +0 -0
  31. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/llm-wiki/how-to/configure-vault-location.md +0 -0
  32. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/llm-wiki/how-to/install.md +0 -0
  33. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/llm-wiki/how-to/review-a-project.md +0 -0
  34. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/llm-wiki/reference/skills-and-agent.md +0 -0
  35. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/llm-wiki/reference/vault-layout.md +0 -0
  36. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/llm-wiki/tutorials/query-your-first-vault.md +0 -0
  37. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/reference/cli.md +0 -0
  38. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/reference/configuration.md +0 -0
  39. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/reference/output-layout.md +0 -0
  40. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/reference/phase5-steps.md +0 -0
  41. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/reference/pipeline-stages.md +0 -0
  42. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/docs/tutorials/convert-your-first-book.md +0 -0
  43. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/plugin/.claude-plugin/plugin.json +0 -0
  44. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/plugin/LICENSE +0 -0
  45. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/plugin/README.md +0 -0
  46. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/plugin/agents/knowledge-researcher.md +0 -0
  47. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/plugin/skills/knowledge-query/SKILL.md +0 -0
  48. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/plugin/skills/knowledge-review/SKILL.md +0 -0
  49. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/batch.py +0 -0
  50. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/config.py +0 -0
  51. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/convert/__init__.py +0 -0
  52. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/convert/cloud.py +0 -0
  53. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/convert/merge.py +0 -0
  54. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/executor.py +0 -0
  55. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/phase5/__init__.py +0 -0
  56. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/phase5/caption_unbleed.py +0 -0
  57. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/phase5/chapter_split.py +0 -0
  58. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/phase5/code_unescape.py +0 -0
  59. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/phase5/dash_normalize.py +0 -0
  60. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/phase5/lang_retag.py +0 -0
  61. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/phase5/mermaid_repair.py +0 -0
  62. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/qa/__init__.py +0 -0
  63. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/qa/review.py +0 -0
  64. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/qa/sample.py +0 -0
  65. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/render.py +0 -0
  66. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/src/pdf2wiki/scan.py +0 -0
  67. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/tests/test_cloud.py +0 -0
  68. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/tests/test_convert.py +0 -0
  69. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/tests/test_executor.py +0 -0
  70. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/tests/test_phase5.py +0 -0
  71. {pdf2wiki-0.2.2 → pdf2wiki-0.2.3}/tests/test_render.py +0 -0
  72. {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.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.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"
@@ -11,4 +11,4 @@ Pipeline stages:
11
11
  4. batch — manifest-driven multi-book runs, resumable, with optional SSH remote execution.
12
12
  """
13
13
 
14
- __version__ = "0.2.2"
14
+ __version__ = "0.2.3"
@@ -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
- print(f"batch: {len(failed)} book(s) not done: {failed}", file=sys.stderr)
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
@@ -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