fix(baseline): space block boundaries in the article tier (#896) - #905
Open
taro0915 wants to merge 1 commit into
Open
fix(baseline): space block boundaries in the article tier (#896)#905taro0915 wants to merge 1 commit into
taro0915 wants to merge 1 commit into
Conversation
taro0915
force-pushed
the
fix/article-tier-block-spacing
branch
from
August 13, 2026 23:18
6942fb1 to
0de4f7d
Compare
The <article> tier called text_content() on the whole subtree, so text ran together across block boundaries: <h1>Notice Title</h1><p>This ... came out as "Notice TitleThis ...", corrupting the words themselves. The same failure mode is already handled in html2txt(), which pads block boundaries via _BLOCK_ELEMS before reading text -- the article tier was the only caller that skipped it. Extract that pass into _space_block_boundaries() and call it from both sites. The article tier goes through _spaced_text_content(), which applies it to a deepcopy: the pass writes .text/.tail, and baseline() shares one tree across all its strategies. Scope is the fusion only. The missing Markdown heading reported in adbar#896 originates earlier, in the stage 2 comparison, and is not addressed here. Measured on the bundled evaluation corpus (990 documents): F1 unchanged to four decimal places on both runners (fast 0.9184, fallback 0.9243); MIN_EXTRACTED_SIZE crossed by 0 documents in either direction; extract() output changed in 4 documents, all of them short pages on the baseline rescue path this issue reports.
taro0915
force-pushed
the
fix/article-tier-block-spacing
branch
from
August 14, 2026 00:05
0de4f7d to
2d7b2f9
Compare
Owner
|
Thanks, I may have a better fix for this after all, I'll need to check. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #896 (the fusion half; see Scope below).
Problem
The
<article>tier calledtext_content()on the whole subtree, so text ran together across block boundaries.<h1>Notice Title</h1><p>This ...came out asNotice TitleThis ..., corrupting the words themselves rather than merely dropping formatting.The same failure mode is already handled a few lines down in the same file:
html2txt()pads block boundaries via_BLOCK_ELEMSbefore reading text, with a comment naming this exact case. The other baseline tiers avoid it structurally — the paragraph tier callstext_content()per element and joins with"\n", the default tier usesitertext(). Only the<article>tier calls it on a whole subtree, so every block boundary inside the article fuses: h2 / p / li / td alike, not just the h1 in the report.Change
Extract the spacing pass into
_space_block_boundaries()and call it from both sites, so the loop body exists once rather than being duplicated.The article tier goes through
_spaced_text_content(), which applies it to adeepcopy: the pass writes.text/.tail, andbaseline()shares one tree across all its strategies.html2txt()'s tree is freshly parsed or already copied, so it spaces the body in place and pays no copy cost.Per your guidance in #896 this reuses the existing pass rather than restructuring the tier to build the body per block element.
Scope
The fusion only. The missing Markdown heading also reported in #896 originates earlier, in the stage 2 comparison as @ebarkhordar showed, and is not addressed here.
Threshold and benchmark impact
You asked for the evaluation data on the cutoff question. Measured on the bundled corpus (990 documents), comparing 2ba8f62 against the same tree with this change applied. Python 3.12, Windows,
pip install -e ".[all]".baseline()output lengthDeltas over the 312 changed documents: min −1, median +8, max +908.
The single decrease is
anglerboard.de-rute.html(4312 → 4311): the article now goes throughremove_control_characters, which drops one U+200B that rawtext_content()kept. That is the existing guard doing its job, not lost content.Strategy selection — no document changed tier. Identical before and after: article 527, paragraph 369, json 77, default 17.
Threshold crossings
_MIN_CONTENT_LENGTH = 100cutoff = max/5MIN_EXTRACTED_SIZE = 250The four newly admitted candidates (100→102, 98→101, 100→101, 100→101) are small related-content blocks in pages carrying a much larger dominant article, so the
max/5cutoff drops them again. The selected article set is unchanged in all 990 documents.End-to-end
extract(), comparing SHA-1 of the output: 4 / 990 documents changed, the same four under both runners.extract()All four are short pages where the main extractor falls under
MIN_EXTRACTED_SIZEand stage 3 hands over tobaseline()— the path #896 reports. They gain only the separators that were missing at block boundaries. The remaining 986 are byte-identical.Quality gate (
tests/eval_gate.py)F1 is unchanged to four decimal places on both runners, and the gate exits 0 in both states. The before/after runs were taken by stashing only
trafilatura/baseline.py; re-running after restoring reproduced the "after" numbers exactly.One caveat rather than leave it implicit:
fallbackmeasures 0.9243 against a pinned floor of 0.9245 — 0.0002 under, inside the gate'sEPSILON = 0.0005band, so it passes. That is equally true before the change, so it looks like a property of this measurement environment rather than something this PR introduces.Tests
Three tests in
tests/baseline_tests.py. Full suite: 339 passed, 0 skipped.Stashing the implementation and re-running turns 5 of them red, which is how the "h2/p/li/td alike" claim above was checked rather than assumed:
test_baseline_article_block_boundaries— h1/p fusion, and that inline runs inside a block stay joinedtest_baseline_article_spacing_covers_all_block_elements[h2_p|li_li|td_td]— all three reproduce the fusion without the fixtest_spaced_text_content_does_not_mutate_input— removing thedeepcopyalone is enough to turn this one redA note on that last one. An earlier version asserted non-mutation through
baseline()instead, and it passed with thedeepcopyremoved — the later tiers runtrim()per element, which absorbs the injected spaces. It was guarding nothing. Calling_spaced_text_contentdirectly and comparingtostring()is what makes it fail on the mutation it is meant to catch.ruff check,ruff format --checkandmypy -p trafilaturashow nothing new; the pre-existingzstandardnote inutils.py:38is unrelated.Disclosure
Prepared with AI assistance. The design decisions, the scope boundary and every number above are mine — the measurements were run locally on the bundled corpus, and the before/after comparisons were taken by stashing only
trafilatura/baseline.py.