htmlsift 0.1.0__tar.gz → 0.2.0__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 (58) hide show
  1. htmlsift-0.2.0/.github/workflows/publish.yml +39 -0
  2. {htmlsift-0.1.0 → htmlsift-0.2.0}/.gitignore +0 -2
  3. htmlsift-0.2.0/CHANGELOG.md +72 -0
  4. htmlsift-0.2.0/PKG-INFO +148 -0
  5. {htmlsift-0.1.0 → htmlsift-0.2.0}/README.md +97 -15
  6. htmlsift-0.2.0/README.pypi.md +116 -0
  7. htmlsift-0.2.0/docs/assets/annotation-example.svg +109 -0
  8. {htmlsift-0.1.0 → htmlsift-0.2.0}/docs/assets/architecture.svg +136 -136
  9. {htmlsift-0.1.0 → htmlsift-0.2.0}/docs/assets/concept.svg +89 -89
  10. {htmlsift-0.1.0 → htmlsift-0.2.0}/docs/assets/cpu_speed_accuracy.svg +66 -66
  11. {htmlsift-0.1.0 → htmlsift-0.2.0}/docs/assets/data-efficiency.svg +120 -118
  12. {htmlsift-0.1.0 → htmlsift-0.2.0}/docs/assets/gpu_speed_accuracy_lite.svg +30 -30
  13. {htmlsift-0.1.0 → htmlsift-0.2.0}/docs/assets/signals.svg +75 -75
  14. {htmlsift-0.1.0 → htmlsift-0.2.0}/pyproject.toml +4 -0
  15. {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/__init__.py +1 -1
  16. htmlsift-0.2.0/src/htmlsift/_decode.py +45 -0
  17. htmlsift-0.2.0/src/htmlsift/_links.py +52 -0
  18. htmlsift-0.2.0/src/htmlsift/cli.py +135 -0
  19. {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/prune.py +5 -4
  20. {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/extractor.py +26 -12
  21. htmlsift-0.2.0/tests/conftest.py +16 -0
  22. htmlsift-0.2.0/tests/data/article_fi.html +32 -0
  23. htmlsift-0.2.0/tests/data/article_ja.html +29 -0
  24. {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/data/sample.extracted.html +14 -14
  25. {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/data/sample.extracted.md +19 -19
  26. {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/data/sample.extracted.txt +14 -14
  27. {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/data/sample.rendered.txt +24 -24
  28. {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/test_api.py +32 -0
  29. htmlsift-0.2.0/tests/test_cli.py +167 -0
  30. htmlsift-0.2.0/tests/test_decode.py +153 -0
  31. {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/test_extract_golden.py +29 -0
  32. htmlsift-0.2.0/tests/test_links.py +163 -0
  33. {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/test_prune.py +20 -0
  34. htmlsift-0.1.0/PKG-INFO +0 -89
  35. htmlsift-0.1.0/README.pypi.md +0 -61
  36. htmlsift-0.1.0/src/htmlsift/cli.py +0 -29
  37. {htmlsift-0.1.0 → htmlsift-0.2.0}/.gitattributes +0 -0
  38. {htmlsift-0.1.0 → htmlsift-0.2.0}/.github/workflows/ci.yml +0 -0
  39. {htmlsift-0.1.0 → htmlsift-0.2.0}/AGENTS.md +0 -0
  40. {htmlsift-0.1.0 → htmlsift-0.2.0}/LICENSE +0 -0
  41. {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/_artifacts.py +0 -0
  42. {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/__init__.py +0 -0
  43. {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/band.py +0 -0
  44. {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/features.py +0 -0
  45. {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/onnx_infer.py +0 -0
  46. {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/prep_page.py +0 -0
  47. {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/render.py +0 -0
  48. {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/tokenizer.py +0 -0
  49. {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/torch_infer.py +0 -0
  50. {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/loader.py +0 -0
  51. {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/py.typed +0 -0
  52. {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/__init__.py +0 -0
  53. {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/data/sample.html +0 -0
  54. {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/test_features.py +0 -0
  55. {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/test_manifest.py +0 -0
  56. {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/test_prep_page.py +0 -0
  57. {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/test_render_golden.py +0 -0
  58. {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/test_render_links.py +0 -0
@@ -0,0 +1,39 @@
1
+ name: Publish
2
+
3
+ # Publishes to PyPI via Trusted Publishing (OIDC) -- no API token or secret.
4
+ # Fires when a GitHub release is published; the release tag should match the
5
+ # version in src/htmlsift/__init__.py (e.g. tag v0.1.1 for version 0.1.1).
6
+
7
+ on:
8
+ release:
9
+ types: [published]
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.13"
19
+ - name: Build sdist and wheel
20
+ run: |
21
+ python -m pip install --upgrade build
22
+ python -m build
23
+ - uses: actions/upload-artifact@v4
24
+ with:
25
+ name: dist
26
+ path: dist/
27
+
28
+ publish:
29
+ needs: build
30
+ runs-on: ubuntu-latest
31
+ environment: pypi
32
+ permissions:
33
+ id-token: write # required: lets PyPI mint a short-lived OIDC token
34
+ steps:
35
+ - uses: actions/download-artifact@v4
36
+ with:
37
+ name: dist
38
+ path: dist/
39
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -44,5 +44,3 @@ desktop.ini
44
44
  htmlcov/
45
45
  .tox/
46
46
  .nox/
47
-
48
- .env
@@ -0,0 +1,72 @@
1
+ # Changelog
2
+
3
+ All notable changes to htmlsift are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.0] - 2026-11-02
10
+
11
+ ### Added
12
+ - `extract()` accepts `bytes` and decodes them: byte order mark, then `encoding=`, then the
13
+ page's `<meta>` declaration, then utf-8, falling back to cp1252. Legacy charset labels map
14
+ the way browsers read them (`latin1` → cp1252, `shift_jis` → cp932, `gb2312` → gb18030).
15
+ - Keyword-only `encoding=` on `Extractor.extract` and `extract`, for a charset known from
16
+ an HTTP or WARC header.
17
+ - Dependency: `w3lib` (BSD-3-Clause) for charset sniffing.
18
+ - Keyword-only `base_url=` on `Extractor.extract` and `extract`: links and image URLs in
19
+ `html` and `markdown` output are made absolute against it. The page's first
20
+ `<base href>` is resolved against `base_url`; malformed links are left as they are.
21
+ `text` output and `with_blocks` are unaffected.
22
+ - `htmlsift extract` command: files, quoted globs or stdin in; the content of a single
23
+ input, or one JSON line per file (`--jsonl` to force), out. Flags `--output`,
24
+ `--mode`, `--device`, `--base-url`, `--encoding`. Failed files are reported as
25
+ records and the run exits with 1.
26
+
27
+ ### Changed
28
+ - Input other than `str` or `bytes` raises `TypeError`; `extract(None)` previously
29
+ returned `""`.
30
+ - `encoding=` with `str` input raises `TypeError`, since a `str` is already decoded.
31
+ - A page with an absolute `<base href>` gets absolute links in `html` and `markdown` output
32
+ even without `base_url`. Previously `<base>` was dropped with `<head>`, leaving its links
33
+ relative.
34
+
35
+ ### Fixed
36
+ - Non-utf-8 `bytes` input crashed with `UnicodeDecodeError`.
37
+ - `html` and `markdown` output could drop images and other inline content inside a kept
38
+ block, varying from run to run on the same page. `text` output was unaffected.
39
+
40
+ ## [0.1.1] - 2026-09-23
41
+
42
+ Documentation and packaging metadata only — no code or model changes since 0.1.0.
43
+
44
+ ### Added
45
+ - Project URLs in package metadata (research repo, Hugging Face weights, issue tracker).
46
+ - PyPI Trusted Publishing workflow (`.github/workflows/publish.yml`).
47
+ - Cross-benchmark accuracy table (`base`/`mini` vs trafilatura, readability, resiliparse on
48
+ WMB, WCXB, DAnIEL) and an "Annotation is a policy" section in the README.
49
+ - GPU-on-Windows install note (CUDA torch from the PyTorch index).
50
+
51
+ ### Changed
52
+ - Stated the model-weight license (Apache-2.0) and added WebMainBench attribution.
53
+ - Cited the WebMainBench source paper (Liu et al., *Dripper*, arXiv:2511.23119).
54
+ - Clarified model shorthand and tightened copy in the README and the PyPI description.
55
+
56
+ ## [0.1.0] - 2026-09-22
57
+
58
+ ### Added
59
+ - Initial release.
60
+ - `mini` model (int8 ONNX, runs on CPU) and `base` model (311M encoder, GPU-capable),
61
+ installed via `pip install htmlsift` and `pip install htmlsift[full]` respectively.
62
+ - `Extractor(mode, *, bundle_dir, threads, device)` with
63
+ `.extract(html, output=..., with_blocks=...)`, plus the module-level `extract(...)`.
64
+ - Output formats: plain text, HTML, and markdown.
65
+ - Device selection for `base`: `device="cuda"` / `"cpu"`, auto-detecting a GPU and
66
+ warning on CPU fallback.
67
+ - Model bundles fetched from the Hugging Face Hub, pinned to a fixed commit revision.
68
+
69
+ [Unreleased]: https://github.com/koivualeksi/htmlsift/compare/v0.2.0...HEAD
70
+ [0.2.0]: https://github.com/koivualeksi/htmlsift/compare/v0.1.1...v0.2.0
71
+ [0.1.1]: https://github.com/koivualeksi/htmlsift/compare/v0.1.0...v0.1.1
72
+ [0.1.0]: https://github.com/koivualeksi/htmlsift/releases/tag/v0.1.0
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.5
2
+ Name: htmlsift
3
+ Version: 0.2.0
4
+ Summary: Main-content extraction for web pages
5
+ Project-URL: Repository, https://github.com/koivualeksi/htmlsift
6
+ Project-URL: Research, https://github.com/koivualeksi/htmlsift-research
7
+ Project-URL: Model weights, https://huggingface.co/koivualeksi/htmlsift-release
8
+ Project-URL: Issues, https://github.com/koivualeksi/htmlsift/issues
9
+ Author-email: Aleksi Koivu <koivu.aleksi@gmail.com>
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: boilerplate-removal,content-extraction,html,main-content,web-scraping
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Text Processing :: Markup :: HTML
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: huggingface-hub
23
+ Requires-Dist: lxml
24
+ Requires-Dist: numpy
25
+ Requires-Dist: onnxruntime
26
+ Requires-Dist: tokenizers
27
+ Requires-Dist: w3lib<3,>=2.5
28
+ Provides-Extra: full
29
+ Requires-Dist: torch; extra == 'full'
30
+ Requires-Dist: transformers; extra == 'full'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # htmlsift
34
+
35
+ Web main-content extraction: HTML in, clean text / HTML / markdown out. Trained on
36
+ [WebMainBench](https://github.com/opendatalab/WebMainBench), scores each rendered line of a
37
+ page as main content or boilerplate (nav, ads, cookie banners, and footers) and returns just
38
+ the content.
39
+
40
+ Apache-2.0. Full documentation, diagrams, and benchmarks:
41
+ https://github.com/koivualeksi/htmlsift
42
+
43
+ ## Results
44
+
45
+ | model | desc | WMB test F1 | seeds | CPU (pages/s) | GPU (pages/s) | install |
46
+ |---|---|---|---|---|---|---|
47
+ | `base` | 311M, 10 layers | **0.9311** | 3 | 0.4 | 24.6 | `htmlsift[full]` |
48
+ | unpublished | 97M, 6 layers, int8 QAT | 0.9256 | 3 | 0.9 | 30.2 | — |
49
+ | `mini` | int8 embedding table | 0.9010 | 3 | **27.7** | — | `htmlsift` |
50
+
51
+ GPU = RTX 4090, CPU = Ryzen 5 3600 single thread; WMB test = 544 pages (one of the 545 has
52
+ no gold reference). On WMB (in-domain), `mini` scores ~15 F1 points above trafilatura at
53
+ comparable CPU speed.
54
+
55
+ ### Cross-benchmark accuracy (zero-shot)
56
+
57
+ Trained on WMB, scored on benchmarks the model never saw. Each column uses that benchmark's own metric.
58
+
59
+ | model | WMB (ROUGE-5) | WCXB (word-F1) | DAnIEL (ROUGE-L) |
60
+ |---|---|---|---|
61
+ | `base` | 0.9311 | 0.8633 | 0.9175 |
62
+ | `mini` | 0.9010 | 0.8474 | 0.8797 |
63
+ | trafilatura | 0.7525 | 0.8584 | 0.8265 |
64
+ | readability | 0.8016 | 0.7653 | 0.8925 |
65
+ | resiliparse | 0.7135 | 0.7909 | 0.7094 |
66
+
67
+ Model rows are 3-seed test means; heuristics are single deterministic runs. "Main content" is a labeling policy: htmlsift leads in-domain and stays competitive off-policy — retrain on your own labels if your definition differs. Details in the [research repo](https://github.com/koivualeksi/htmlsift-research).
68
+
69
+ ## Install
70
+
71
+ ```bash
72
+ pip install htmlsift # default: the mini model (CPU, ONNX)
73
+ pip install htmlsift[full] # adds the base 311M model (torch; GPU-capable)
74
+ ```
75
+
76
+ - `htmlsift` — the `mini` model only; few dependencies (onnxruntime + tokenizers + lxml +
77
+ numpy + huggingface-hub + w3lib). First use downloads ~230 MB of model files.
78
+ - `htmlsift[full]` — adds `torch` + `transformers` to run `base`. First use of `base`
79
+ downloads ~1 GB.
80
+
81
+ Requires Python 3.10+. Model files download from the Hugging Face Hub on first use and are
82
+ cached; run `htmlsift download <mode>` to prefetch, or set `HF_HUB_OFFLINE=1` to run from
83
+ cache only.
84
+
85
+ ## Usage
86
+
87
+ ```python
88
+ from htmlsift import Extractor
89
+
90
+ ex = Extractor() # defaults to "mini"
91
+ ex.extract(html) # -> str (plain text)
92
+ ex.extract(html, output="html") # structure, links, images kept
93
+ ex.extract(html, output="markdown") # clean markdown: [text](url), ![alt](src)
94
+ ex.extract(html, with_blocks=True) # -> (blocks, probs)
95
+
96
+ import htmlsift
97
+ htmlsift.extract(html) # module-level convenience
98
+ ```
99
+
100
+ Raw bytes are decoded for you (byte order mark, then `encoding=`, then `<meta charset>`,
101
+ then utf-8 / cp1252):
102
+
103
+ ```python
104
+ ex.extract(response.content) # charset from the page itself
105
+ ex.extract(payload, encoding="windows-1251") # charset from the HTTP / WARC header
106
+ ```
107
+
108
+ Make links and image URLs absolute in `html` / `markdown` output (the page's `<base href>`
109
+ is honoured):
110
+
111
+ ```python
112
+ ex.extract(html, output="markdown", base_url="https://example.com/blog/post")
113
+ ```
114
+
115
+ The larger model, from `htmlsift[full]`:
116
+
117
+ ```python
118
+ ex = Extractor("base") # 311M encoder; GPU when present, else CPU (with a warning)
119
+ ex = Extractor("base", device="cuda") # require the GPU
120
+ ```
121
+
122
+ `mini` is CPU-only; only `base` runs on the GPU.
123
+
124
+ ## Output modes
125
+
126
+ | `output` | what you get |
127
+ |---|---|
128
+ | `text` (default) | selected content as plain text |
129
+ | `html` | selected DOM, with structure / links / images kept |
130
+ | `markdown` | clean markdown, links and images preserved |
131
+
132
+ ## Command line
133
+
134
+ ```bash
135
+ htmlsift extract page.html # main content of one page
136
+ curl -s https://example.com/a | htmlsift extract - --base-url https://example.com/a
137
+ htmlsift extract "crawl/*.html" --output markdown > out.jsonl # one JSON line per file
138
+ ```
139
+
140
+ Flags: `--output`, `--mode`, `--device`, `--base-url`, `--encoding`, `--jsonl` (records
141
+ even for a single file). Failed files become `{"path", "error"}` records and the exit code
142
+ is 1. Output is utf-8.
143
+
144
+ ## License
145
+
146
+ Apache-2.0. Model weights are distributed from the Hugging Face Hub, also under Apache-2.0, and
147
+ are trained on [WebMainBench](https://github.com/opendatalab/WebMainBench) (Apache-2.0); please
148
+ attribute WebMainBench when you use them.
@@ -7,7 +7,7 @@ Main-content extraction for web pages: hand it a raw HTML string and get back th
7
7
  Two models shipped, both trained on [WebMainBench][wmb] (WMB) — 7,825 labelled pages, custom split
8
8
  6,548 train / 732 val / 545 test:
9
9
 
10
- - **`mini`** (default) - a small int8-ONNX model that runs on CPU. `pip install htmlsift`
10
+ - **`mini`** (default) - a fast int8-ONNX model that runs on CPU. `pip install htmlsift`
11
11
  - **`base`** - a 311M encoder, most accurate and GPU-capable. `pip install htmlsift[full]`
12
12
 
13
13
  Each model is fully re-trainable with your own annotated data and the whole research suite is available at [htmlsift-research](https://github.com/koivualeksi/htmlsift-research)
@@ -18,12 +18,13 @@ Each model is fully re-trainable with your own annotated data and the whole rese
18
18
 
19
19
  | model | desc | WMB test F1 | seeds | CPU (pages/s) | GPU (pages/s) | install |
20
20
  |---|---|---|---|---|---|---|
21
- | `base` | 311m-10 | **0.9311** | 3 | 0.4 | 24.6 | `htmlsift[full]` |
22
- | unpublished | 97m-6, int8 QAT | 0.9256 | 3 | 0.9 | 30.2 | — |
23
- | `mini` | int8 table | 0.9010 | 3 | **27.7** | — | `htmlsift` |
21
+ | `base` | 311M, 10 layers | **0.9311** | 3 | 0.4 | 24.6 | `htmlsift[full]` |
22
+ | unpublished | 97M, 6 layers, int8 QAT | 0.9256 | 3 | 0.9 | 30.2 | — |
23
+ | `mini` | int8 embedding table | 0.9010 | 3 | **27.7** | — | `htmlsift` |
24
24
 
25
25
  * GPU = RTX 4090
26
26
  * CPU = Ryzen 5 3600 single thread
27
+ * WMB test = 544 pages: one of the 545 test pages has no gold reference and is skipped in scoring.
27
28
 
28
29
  ### On CPU
29
30
 
@@ -36,21 +37,25 @@ per-page time (36 vs 31 ms median) and scores ~15 F1 points higher. readability
36
37
 
37
38
  ![htmlsift base sustains 21x the throughput of MinerU-HTML at matched F1 on an RTX 4090](docs/assets/gpu_speed_accuracy_lite.svg)
38
39
 
39
- RTX 4090, WMB test (544 pages). `base` with band attention sustains 24.6 pages/s against
40
+ RTX 4090, WMB test (544 pages). `base` sustains 24.6 pages/s against
40
41
  MinerU-HTML v1.1's 1.16. A 21x gap at matched accuracy (F1 0.9311 vs 0.9306).
41
42
 
42
43
  ### Cross benchmark accuracy
43
44
 
44
45
  The shipped models are trained on WMB, so WMB is in-domain. The other two columns are
45
46
  **zero-shot**, the model never saw WCXB or the multilingual DAnIEL in training. Each column is
46
- scored with that benchmark's own metric.
47
+ scored with that benchmark's own metric. `base` leads every heuristic here; `mini` lands just
48
+ behind the best on each board — see **Annotation is a policy** under Concept.
47
49
 
48
50
  | model | WMB (ROUGE-5) | WCXB (word-F1, zero-shot) | DAnIEL (ROUGE-L, zero-shot) |
49
51
  |---|---|---|---|
50
- | `base` (311m-10) | 0.9311 | 0.8633 | 0.9175 |
51
- | `mini` (int8) | 0.9010 | 0.8474 | 0.8797 |
52
+ | `base` | 0.9311 | 0.8633 | 0.9175 |
53
+ | `mini` | 0.9010 | 0.8474 | 0.8797 |
54
+ | trafilatura | 0.7525 | 0.8584 | 0.8265 |
55
+ | readability | 0.8016 | 0.7653 | 0.8925 |
56
+ | resiliparse | 0.7135 | 0.7909 | 0.7094 |
52
57
 
53
- All results are 3-seed test means.
58
+ Model rows are 3-seed test means; heuristics are single deterministic runs.
54
59
 
55
60
  ## Installation
56
61
 
@@ -59,8 +64,8 @@ pip install htmlsift # default: the mini model (CPU, ONNX)
59
64
  pip install htmlsift[full] # adds the base 311M model (torch; GPU-capable)
60
65
  ```
61
66
 
62
- - **`htmlsift`** — the `mini` model only; a small install (onnxruntime + tokenizers + lxml +
63
- numpy + huggingface-hub). First use downloads ~230 MB of model files.
67
+ - **`htmlsift`** — the `mini` model only; few dependencies (onnxruntime + tokenizers + lxml +
68
+ numpy + huggingface-hub + w3lib). First use downloads ~230 MB of model files.
64
69
  - **`htmlsift[full]`** — adds `torch` + `transformers` to run `base`. First use of `base`
65
70
  downloads ~1 GB.
66
71
 
@@ -87,6 +92,36 @@ import htmlsift
87
92
  htmlsift.extract(html) # default mini
88
93
  ```
89
94
 
95
+ ### Bytes and encodings
96
+
97
+ Pass raw bytes from a file, an HTTP response or a WARC record, and htmlsift decodes them:
98
+
99
+ ```python
100
+ ex.extract(response.content) # charset from the page itself
101
+ ex.extract(payload, encoding="windows-1251") # charset from the HTTP / WARC header
102
+ ```
103
+
104
+ The charset is taken from, in order: a byte order mark, `encoding=`, the page's
105
+ `<meta charset>`; an undeclared page is read as utf-8, falling back to cp1252. Unknown
106
+ labels are skipped and undecodable bytes become `�`. A `str` counts as already decoded, so
107
+ `encoding=` with a `str` raises `TypeError`. Prefer passing bytes over a `str` decoded
108
+ upstream: `requests`' `response.text`, for one, guesses ISO-8859-1 for pages without a
109
+ charset header.
110
+
111
+ ### Absolute links
112
+
113
+ Pass the page's URL to make links and image URLs absolute in `html` and `markdown` output:
114
+
115
+ ```python
116
+ ex.extract(html, output="markdown", base_url="https://example.com/blog/post")
117
+ # [the report](report) -> [the report](https://example.com/blog/report)
118
+ ```
119
+
120
+ The page's first `<base href>` is resolved against `base_url`, as a browser does. Without
121
+ `base_url`, an absolute `<base href>` is used on its own; with neither, links stay as they
122
+ are. Malformed links are left unchanged. `text` output has no links, so it ignores
123
+ `base_url`.
124
+
90
125
  The larger model, from the `[full]` install:
91
126
 
92
127
  ```python
@@ -131,6 +166,35 @@ Composite image showing the global distribution of photosynthesis, including bot
131
166
  | `markdown` | clean markdown, links and images preserved |
132
167
 
133
168
 
169
+ ## Command line
170
+
171
+ ```bash
172
+ htmlsift extract page.html # main content of one page
173
+ htmlsift extract page.html --output markdown --base-url https://example.com/page
174
+ curl -s https://example.com/a | htmlsift extract - --base-url https://example.com/a
175
+ htmlsift extract "crawl/*.html" > out.jsonl # one JSON record per file
176
+ htmlsift extract "crawl/**/*.html" --mode base --device cuda > out.jsonl
177
+ ```
178
+
179
+ One file or `-` (stdin) prints the content itself. Several files or a glob print one JSON
180
+ line per file, `{"path": ..., "content": ...}`, or `{"path": ..., "error": ...}` for a file
181
+ that failed; the run continues and exits with 1 at the end. Pass `--jsonl` to get records
182
+ for a single file too, which scripts should do: bash expands an unquoted `*.html` before
183
+ htmlsift sees it. Quoted globs are expanded by htmlsift itself (`**` recurses), so they
184
+ work the same in every shell; directories are not walked.
185
+
186
+ | flag | |
187
+ |---|---|
188
+ | `--output text\|html\|markdown` | as `output=` above (default `text`) |
189
+ | `--mode mini\|base`, `--device` | model and torch device, as for `Extractor` |
190
+ | `--base-url URL` | make links absolute; applies to every input |
191
+ | `--encoding ENC` | charset of the input bytes; applies to every input |
192
+ | `--jsonl` | always print JSON records |
193
+
194
+ Files are read as bytes, so the charset is detected as described above. Output is utf-8.
195
+ Exit codes: 0 ok, 1 a file or the model failed, 2 a usage error. In Windows PowerShell 5.1,
196
+ `>` re-encodes a program's output; redirect from bash, cmd or PowerShell 7 instead.
197
+
134
198
  ## Concept
135
199
 
136
200
  <details>
@@ -161,6 +225,19 @@ Both tiers share the render, the per-line pooling, and the BiGRU head. They diff
161
225
  - base uses granite r2 311m embedder using only first 10 layers requiring GPU for efficient usage
162
226
 
163
227
 
228
+ </details>
229
+
230
+ <details>
231
+ <summary><b>Annotation is a policy</b></summary>
232
+
233
+ ![The same page kept differently under different benchmark annotation policies](docs/assets/annotation-example.svg)
234
+
235
+ htmlsift learns what counts as main content from a benchmark's labels, so it's most accurate on pages that follow that policy. On WebMainBench (the benchmark the shipped models train on) it beats the common heuristic extractors by a wide margin. Zero-shot on benchmarks with different policies, the gap narrows: the 311M `base` still edges every heuristic tested (trafilatura, readability, resiliparse) on both WCXB and DAnIEL, and `mini` lands just behind the strongest heuristic on each board, ahead of the rest.
236
+
237
+ Train htmlsift on a different policy and it turns around. Fit it to WCXB instead of WMB and it tops WCXB but loses points on other benchmarks. For example when training `base` model on WCXB data, WMB falls to 0.8479, DAnIEL to 0.8826 while WCXB climbs up to 0.9209. Whichever policy you train on, htmlsift leads there and pays for it elsewhere.
238
+
239
+ For more information, review the [htmlsift-research](https://github.com/koivualeksi/htmlsift-research) readme.
240
+
164
241
  </details>
165
242
 
166
243
  <details>
@@ -183,20 +260,25 @@ and its `docs/REPRODUCE.md`.
183
260
  A standalone hero image that is its own block renders to no text line, so it can't be selected
184
261
  today. Inline images inside a kept block are preserved in `html` and `markdown`.
185
262
 
263
+ `base_url` does not rewrite `srcset`, so responsive images in `html` output keep relative
264
+ candidates (`src` is rewritten; `markdown` uses only `src`).
265
+
186
266
  </details>
187
267
 
188
268
  ## License
189
269
 
190
270
  Apache-2.0. See [LICENSE](LICENSE).
191
271
 
192
- htmlsift does **not** depend on `html2text`, directly or transitively — rendering and
193
- serialization are lxml-only, which is what keeps the licence clean. Model weights are
194
- distributed separately, from the Hugging Face Hub.
272
+ Model weights are distributed from the Hugging Face Hub, also under Apache-2.0, and are
273
+ trained on [WebMainBench](https://github.com/opendatalab/WebMainBench) (Apache-2.0); please
274
+ attribute WebMainBench when you use them.
195
275
 
196
276
  ## References
197
277
 
198
278
  - **WMB — WebMainBench.** The training benchmark; ROUGE-5 main-content metric.
199
- [opendatalab/WebMainBench](https://github.com/opendatalab/WebMainBench)
279
+ Liu et al., *Dripper: Token-Efficient Main HTML Extraction with a Lightweight LM*.
280
+ [arXiv:2511.23119](https://arxiv.org/abs/2511.23119) ·
281
+ [repo](https://github.com/opendatalab/WebMainBench)
200
282
  - **WCXB — Web Content Extraction Benchmark.** Foley, *WCXB: A Multi-Type Web Content Extraction
201
283
  Benchmark*. [arXiv:2605.21097](https://arxiv.org/abs/2605.21097) ·
202
284
  [repo](https://github.com/Murrough-Foley/web-content-extraction-benchmark)
@@ -0,0 +1,116 @@
1
+ # htmlsift
2
+
3
+ Web main-content extraction: HTML in, clean text / HTML / markdown out. Trained on
4
+ [WebMainBench](https://github.com/opendatalab/WebMainBench), scores each rendered line of a
5
+ page as main content or boilerplate (nav, ads, cookie banners, and footers) and returns just
6
+ the content.
7
+
8
+ Apache-2.0. Full documentation, diagrams, and benchmarks:
9
+ https://github.com/koivualeksi/htmlsift
10
+
11
+ ## Results
12
+
13
+ | model | desc | WMB test F1 | seeds | CPU (pages/s) | GPU (pages/s) | install |
14
+ |---|---|---|---|---|---|---|
15
+ | `base` | 311M, 10 layers | **0.9311** | 3 | 0.4 | 24.6 | `htmlsift[full]` |
16
+ | unpublished | 97M, 6 layers, int8 QAT | 0.9256 | 3 | 0.9 | 30.2 | — |
17
+ | `mini` | int8 embedding table | 0.9010 | 3 | **27.7** | — | `htmlsift` |
18
+
19
+ GPU = RTX 4090, CPU = Ryzen 5 3600 single thread; WMB test = 544 pages (one of the 545 has
20
+ no gold reference). On WMB (in-domain), `mini` scores ~15 F1 points above trafilatura at
21
+ comparable CPU speed.
22
+
23
+ ### Cross-benchmark accuracy (zero-shot)
24
+
25
+ Trained on WMB, scored on benchmarks the model never saw. Each column uses that benchmark's own metric.
26
+
27
+ | model | WMB (ROUGE-5) | WCXB (word-F1) | DAnIEL (ROUGE-L) |
28
+ |---|---|---|---|
29
+ | `base` | 0.9311 | 0.8633 | 0.9175 |
30
+ | `mini` | 0.9010 | 0.8474 | 0.8797 |
31
+ | trafilatura | 0.7525 | 0.8584 | 0.8265 |
32
+ | readability | 0.8016 | 0.7653 | 0.8925 |
33
+ | resiliparse | 0.7135 | 0.7909 | 0.7094 |
34
+
35
+ Model rows are 3-seed test means; heuristics are single deterministic runs. "Main content" is a labeling policy: htmlsift leads in-domain and stays competitive off-policy — retrain on your own labels if your definition differs. Details in the [research repo](https://github.com/koivualeksi/htmlsift-research).
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install htmlsift # default: the mini model (CPU, ONNX)
41
+ pip install htmlsift[full] # adds the base 311M model (torch; GPU-capable)
42
+ ```
43
+
44
+ - `htmlsift` — the `mini` model only; few dependencies (onnxruntime + tokenizers + lxml +
45
+ numpy + huggingface-hub + w3lib). First use downloads ~230 MB of model files.
46
+ - `htmlsift[full]` — adds `torch` + `transformers` to run `base`. First use of `base`
47
+ downloads ~1 GB.
48
+
49
+ Requires Python 3.10+. Model files download from the Hugging Face Hub on first use and are
50
+ cached; run `htmlsift download <mode>` to prefetch, or set `HF_HUB_OFFLINE=1` to run from
51
+ cache only.
52
+
53
+ ## Usage
54
+
55
+ ```python
56
+ from htmlsift import Extractor
57
+
58
+ ex = Extractor() # defaults to "mini"
59
+ ex.extract(html) # -> str (plain text)
60
+ ex.extract(html, output="html") # structure, links, images kept
61
+ ex.extract(html, output="markdown") # clean markdown: [text](url), ![alt](src)
62
+ ex.extract(html, with_blocks=True) # -> (blocks, probs)
63
+
64
+ import htmlsift
65
+ htmlsift.extract(html) # module-level convenience
66
+ ```
67
+
68
+ Raw bytes are decoded for you (byte order mark, then `encoding=`, then `<meta charset>`,
69
+ then utf-8 / cp1252):
70
+
71
+ ```python
72
+ ex.extract(response.content) # charset from the page itself
73
+ ex.extract(payload, encoding="windows-1251") # charset from the HTTP / WARC header
74
+ ```
75
+
76
+ Make links and image URLs absolute in `html` / `markdown` output (the page's `<base href>`
77
+ is honoured):
78
+
79
+ ```python
80
+ ex.extract(html, output="markdown", base_url="https://example.com/blog/post")
81
+ ```
82
+
83
+ The larger model, from `htmlsift[full]`:
84
+
85
+ ```python
86
+ ex = Extractor("base") # 311M encoder; GPU when present, else CPU (with a warning)
87
+ ex = Extractor("base", device="cuda") # require the GPU
88
+ ```
89
+
90
+ `mini` is CPU-only; only `base` runs on the GPU.
91
+
92
+ ## Output modes
93
+
94
+ | `output` | what you get |
95
+ |---|---|
96
+ | `text` (default) | selected content as plain text |
97
+ | `html` | selected DOM, with structure / links / images kept |
98
+ | `markdown` | clean markdown, links and images preserved |
99
+
100
+ ## Command line
101
+
102
+ ```bash
103
+ htmlsift extract page.html # main content of one page
104
+ curl -s https://example.com/a | htmlsift extract - --base-url https://example.com/a
105
+ htmlsift extract "crawl/*.html" --output markdown > out.jsonl # one JSON line per file
106
+ ```
107
+
108
+ Flags: `--output`, `--mode`, `--device`, `--base-url`, `--encoding`, `--jsonl` (records
109
+ even for a single file). Failed files become `{"path", "error"}` records and the exit code
110
+ is 1. Output is utf-8.
111
+
112
+ ## License
113
+
114
+ Apache-2.0. Model weights are distributed from the Hugging Face Hub, also under Apache-2.0, and
115
+ are trained on [WebMainBench](https://github.com/opendatalab/WebMainBench) (Apache-2.0); please
116
+ attribute WebMainBench when you use them.