516 lines
44 KiB
Markdown
516 lines
44 KiB
Markdown
# Article-Cropping Strategy & Learnings (self-improving loop)
|
||
|
||
Living doc for the geometric article-cropping in `smart_extractor.py`
|
||
(`cluster_all_article_blocks` → `crop_all_article_blocks`). Updated each loop iteration.
|
||
|
||
## Validation rules (NEVER violate)
|
||
- **Never run `smart_extractor.py`** — it calls the paid Claude API. Validate ONLY with the
|
||
geometric replay scripts: `_replay_all.py` (AJ), `_replay_namaste.py` (NT Siddipet),
|
||
`_replay_jangaon.py` (NT Jangaon). These run the full cluster chain with NO API.
|
||
- **Measure read-only BEFORE editing** (blast-radius of any new rule), then **re-validate after**.
|
||
- **Baselines** (block counts) — keep stable unless an improvement is intentional & documented:
|
||
- AJ: 12 / 18 / 14 / **21** (pg4 20→21: the ';'-delimiter fix recovered a real గజ్వేల్ dateline R12)
|
||
- NT Siddipet: **11 / 23 / 20 / 25** (pg2 22→23: అంగన్వాడీ R127 split off the ప్రజావాణి section R110;
|
||
pg3 19→20: పర్యావరణ R60 split off; both are the Type-2 paragraph_title-anchor split)
|
||
- NT Jangaon: pg1 → **6**, pg2 → **18** (pg1 7→6: orphan photo R25 bound into its article;
|
||
pg2 Type-2 splits: R65→3 కలెక్టర్/సుందరయ్య/పోస్టుమార్టం, R64→2 జాతీయరహదారి/ఉపసర్పంచుల, R97→2
|
||
గుండెచికిత్స/మృతుడిపరామర్శ; + L89 court notice, F44 furniture)
|
||
- Sakshi Jangaon: pg1 → **22**, pg2 → **20** (new paper, no line separators; all orphan PHOTOS
|
||
bound to articles — 0 image orphans remain)
|
||
|
||
## Layout model (from the line-pattern study)
|
||
- **NT (Namaste) = boxed grid.** ~139 horizontal rules/4pp, many ~2-column wide; long vertical
|
||
column dividers. ~50% of articles are ruled on 3–4 sides. Boundaries are largely *drawn*.
|
||
- **AJ (Andhra Jyothi) = whitespace layout.** Few rules (~69/4pp, mostly ≤0.6-col caption
|
||
underlines). Most articles ruled on 0–2 sides. Boundaries come from column gutters +
|
||
grow-right-to-dead-end + headline/dateline structure. Lines are only *confirmation* in AJ.
|
||
- **Rule width classifies horizontal lines:** ≤0.6 col = caption underline (NEVER a boundary);
|
||
~1 col = single-col article edge / box edge; ~2 col = article boundary (NT workhorse);
|
||
≥2.5 col = section/banner divider.
|
||
- **Vertical rules:** long (≥0.5 page H) = column divider / article wall; short = quote-box edge.
|
||
|
||
## Why earlier runs failed (root causes found)
|
||
1. **Over-merge from clustering + containment-merge.** Multi-column articles whose datelines sit
|
||
only in the left column get over-extended upward; a separate dated article then falls *inside*
|
||
the box and is absorbed → 2 articles in one crop (JG R28, R64). Fixed by **header-split**.
|
||
2. **Banner over multiple sub-stories** not cropped as one rectangle, or its sub-stories
|
||
double-cropped → **section-banner rectangle** + **containment-merge**.
|
||
3. **L-shaped articles** (one column longer): rectangular crop's empty corner = a *different*
|
||
article masked white → looks like white space (JG H28). Generic ragged-bottom fix REJECTED —
|
||
it would also move AJ R48/R53 (currently fine). Still open; needs a tighter signal.
|
||
4. **Top-of-column banner with no anchor** falls into the article below → page-tall crop (JG R2).
|
||
Fixed by **ceiling-trim**.
|
||
5. **Body-less header strips** (doc_title or quote-box paragraph_title) cropped alone, split from
|
||
body → **body-less header merge** (H and now P).
|
||
|
||
## Strategies implemented (all gated + measured to touch only defective blocks)
|
||
- **Banner rectangle** (H & P, ≥1.6-col header, ≥2 cols, sure bottom boundary): crop whole rect.
|
||
- **Section-banner rectangle** (doc_title ≥2.5 col with ≥2 headed sub-stories): crop banner-top →
|
||
next spanning full-width separator; absorb sub-blocks. (Only JG R67 across all runs.)
|
||
- **Containment-merge** (block ≥90% inside a larger one → fold in). Fixes split columns,
|
||
roundup sub-stories, orphan tails.
|
||
- **Header-split** (H block with a dateline >20px ABOVE its headline → split at headline).
|
||
Only JG R28, R64.
|
||
- **Ceiling-trim** (DL block, leading furniture >300px above the dateline → drop). Only JG R2.
|
||
- **Body-less header merge** (H or P, no text, sits ≤90px above a text block, ≥60% overlap → fold).
|
||
H: NT R65→R73. P (iter-1): NT R133→R132, JG R52→R4.
|
||
- **Right-edge → page** (rightmost-column block → extend right edge to page-content edge).
|
||
Touches only rightmost blocks (AJ 4/6/3/4, NT 2/3/3/8), ≤15px; interior untouched.
|
||
- **Drop classifieds** (`drop_classifieds_regions`, runs right after `drop_masthead_regions`).
|
||
Classifieds = the "Classified Ads / phone-number listing" columns = page FURNITURE, not news.
|
||
Left in, a neighbour article's column-fold grows down into them (JG R65 swallowed the whole
|
||
left column to page-bottom). A region is classifieds when (only TALL regions ≥400px are
|
||
OCR-scanned) its OCR text carries a classifieds keyword (`classified`/`contact`) **OR** ≥5
|
||
phone-like `\d{6,}` runs (a news article rarely has >3). OCR is **half-res `eng`-only** — the
|
||
phone-runs + English keyword are the real signals, so this is ~2× faster than full-res tel+eng
|
||
and gives the IDENTICAL drop set on the corpus. Court/legal notices are intentionally NOT
|
||
dropped (user: classifieds ONLY) — they are carved into their own section instead (next).
|
||
- **Carve court/legal notices** (`tag_legal_notices` + a carve pass in `cluster_all_article_blocks`).
|
||
Court summons / name-change & matrimonial petitions / "by order of the court" notices are page
|
||
furniture too — but, per the brief, KEPT and cropped as their OWN section (kind **`L`**), not
|
||
dropped and not absorbed into a neighbour (JG R65 swallowed a court summons in its left column).
|
||
`tag_legal_notices` half-res-`eng`-OCRs single-column text/paragraph_title regions and tags
|
||
`r['_legal']='core'|'support'` on a legal-boilerplate keyword (CORE: hon'ble, in the court of,
|
||
by order of, plaintiff/defendant, petitioner/respondent, summons, o.s.no, civil judge … ;
|
||
SUPPORT: advocate, whereas, vakalat). The clusterer then groups adjacent tagged regions and
|
||
carves a cluster into its own LOCKED `L` block **only when** (1) ≥2 tagged regions incl. ≥1
|
||
CORE, (2) they all sit in ONE host block, and (3) that host is WIDE (>1100px → masked). Gate 3
|
||
is the safety rule: a wide masked host whites the carved area out (no duplication), whereas a
|
||
NARROW host is cropped verbatim, so carving out of it would duplicate the notice — those are
|
||
left alone (e.g. NT pg3's petition notice straddling the narrow locked #75 + open #62 is NOT
|
||
carved, so the NT baseline is untouched). The carved `L` block shows in BOTH the per-article
|
||
crops and the debug boundary map (the carve lives inside `cluster_all_article_blocks`, which
|
||
every consumer re-runs).
|
||
- **Type-2 split** (split pass in `cluster_all_article_blocks`, after the legal carve). TWO
|
||
layouts look like one big box but need OPPOSITE treatment (AJ/Namaste = ruled papers):
|
||
- **Type 1** — sub-stories under ONE header with a COMMON BOTTOM rule → the rule + wide banner
|
||
already made it a `_raw_rect` section (banner/section pass) → crop as ONE rectangle. NEVER split.
|
||
- **Type 2** — independent articles stacked one below another, EACH with its own doc_title +
|
||
its own dateline, NO common bottom rule (JG R65: కలెక్టర్/సుందరయ్య/పోస్టుమార్టం) → over-merge →
|
||
SPLIT into one article per headline (members assigned to the nearest headline at/above them).
|
||
Discriminator = the `_raw_rect` flag (set ⇒ sure bottom rule ⇒ Type 1). The split anchors are
|
||
headlines that **OWN** a dateline (each dateline assigned to the nearest headline above it) — a
|
||
single article with a headline + a sub-headline sharing ONE dateline has only ONE owning
|
||
headline, so it is NOT split (this fixed the AJ R50/R57 & R35/R79 false splits). Anchors must be
|
||
vertically stacked ≥120px apart (not two columns headed side-by-side). NOTE for line-less papers
|
||
(Sakshi): the `_raw_rect` bottom-rule signal won't exist — a different bottom cue would be needed.
|
||
|
||
## Iteration log
|
||
- **iter-1:** body-less header merge extended to `paragraph_title` → NT R133→R132, JG R52→R4
|
||
(title-only fragments fixed). AJ untouched; NT pg4 26→25, JG pg1→9.
|
||
- **iter-2:** drop graphic-label orphans (pure-text O block ≥80% inside a FOREIGN image) →
|
||
JG R34 dropped (label inside banner image). AJ/NT 0 drops; JG pg1 9→8.
|
||
- **iter-3:** orphan-TEXT absorb (fold pure-text orphan UP into clear above-article, gap≤80,
|
||
no down-folds) — TRIED & **REVERTED**. Only match was NT pg4 R75→R132, a CLASSIFIEDS
|
||
"change of name" notice; could not verify R75 belongs to that ad vs a separate notice →
|
||
merging distinct classifieds is a regression risk. Deferred. (AJ/JG had 0 matches anyway.)
|
||
|
||
## Assessment after iter-3 — safe heuristics largely exhausted
|
||
The remaining backlog items are **risk-bearing or need human judgment**, not safe heuristics:
|
||
- Orphan PHOTOS (JG R25, AJ R12/R26): binding a standalone photo to "the right" article is
|
||
ambiguous (far gaps, multiple candidates).
|
||
- Orphan TEXT in classifieds/notices (NT R75, etc.): can't tell same-ad vs separate-ad.
|
||
- JG front-page over-merges (R64/R65): dense multi-headline roundups; further splitting is
|
||
complex and could fragment legit roundups.
|
||
- JG H28 L-shape white space: a generic ragged-bottom split also moves AJ R48/R53 (fine now).
|
||
Recommendation: pause autonomous loop; these need a human call per case. Resume with a
|
||
specific directive if desired.
|
||
|
||
## Open defect backlog (prioritized for next iterations)
|
||
1. **Orphan photo/text blocks (kind O)** — REMAINING: AJ pg1 R12 (photo+cap+text), pg1 R76
|
||
(text), pg3 R32 (text), pg3 R26 (img+text); NT pg3 R7 (text tail, 36px below H98),
|
||
pg4 R75 (text, 14px below P132); JG pg1 R25 (img-only photo). [R34 done in iter-2.]
|
||
Next safest: fold orphan TEXT into the article directly above/below within a small gap
|
||
(NT R75→P132 gap14, R7→H98 gap36) — measure first (risk: wrong-article merge).
|
||
Harder: bind orphan IMAGE (R25) to caption+nearest article.
|
||
2. **JG front-page over-merges** R65 (4DL), R64 (residual 2DL below headline) — dense section
|
||
roundups; may need below-headline multi-headline split.
|
||
3. **JG H28 L-shape white space** — needs a signal that distinguishes it from AJ R48/R53 (which
|
||
are fine). Candidate: only when the empty corner is masked-out *foreign* article AND the
|
||
block's tail is a single-column sidebar with its own sub-head/byline → split the sidebar.
|
||
|
||
## Hard limitation: undetected headlines (OCR gap, not a cropping bug)
|
||
On some pages PaddleOCR detects **no doc_title** for the top articles (JG pg1: only 3 doc_titles
|
||
total, ALL in the bottom half; top-half headlines `భానుడి భగభగ` / Indiramma / కరీమాబాద్ were
|
||
missed entirely). A geometric crop cannot include a headline that has no region. Symptoms:
|
||
DL-anchored crops with the heading missing (JG a01_DL3, a03_DL4) and lead photos orphaned from
|
||
their headless article (JG a02_O25). The REAL pipeline recovers these via the paid Claude
|
||
"catch by eye" pass (pipeline log: "N article(s) Claude must catch by EYE — no headline box").
|
||
The geometric `all_article_crops` (real run AND replay) will lack them. The only geometric fix
|
||
is a NEW pixel-level headline-band detector (find the large-type band above a headless dateline)
|
||
— substantial + risky for AJ/NT baselines; deferred unless explicitly requested.
|
||
|
||
## Replay validation cheatsheet
|
||
```
|
||
python3 _replay_all.py # AJ → all_article_crops_final
|
||
python3 _replay_namaste.py # NT Siddipet → all_article_crops_final
|
||
python3 _replay_jangaon.py # NT Jangaon → all_article_crops_review
|
||
```
|
||
Read-only block inspection: replicate the chain (dedup → merge_stacked → drop_masthead →
|
||
promote_paragraph_titles → mark_bullet_titles → find_article_starts_by_dateline →
|
||
[probe cluster → recover_orphan_text_headlines → re-detect] → cluster_all_article_blocks).
|
||
|
||
## Partition passes added (post iter-2) — all gated + validated
|
||
- **Seed/lock confident cells** — a block is `_locked` (confident) when it's a single complete
|
||
article (exactly 1 dateline + a headline) OR fully boxed (≥3 ruled sides). Over-merges stay open.
|
||
- **Step 4 — grow OPEN cells into gaps** (bounded by walls/cells/frame; locked never move;
|
||
strict guard: never increase overlap with a locked cell). AJ/NT: 0 open∩locked overlaps.
|
||
- **Step 5 — containing-assignment** — every unassigned region inside a cell's box joins it
|
||
(no box growth). Cleared NT pg4's 11 section-absorbed regions; counts unchanged.
|
||
- **No-overlap reconciliation** — trims a 0-member over-reaching box back to its members.
|
||
- **Lead-photo rebind (caption-anchored)** — a PHOTO→CAPTION→HEADLINE stack = lead photo; move
|
||
the photo+caption down into the headline's block. Fixes JG a06_P37/maize R29 + AJ pg4/NT lead
|
||
photos. The caption requirement excludes the H93 false positive (photo at an article's bottom).
|
||
- **Horizontal-line LENGTH FLOOR (`_lines.separator_barriers`)** — a horizontal rule counts as an
|
||
article separator only if width ≥ **0.6× column** (median text width). Drops short
|
||
caption/decorative underlines. NOTE: ≥1.0× is TOO aggressive (a single-column article's own
|
||
boundary is <1 col → ≥1.0 merged NT pg4 R121); 0.6× merges ZERO articles. Caption-vs-separator
|
||
is still decided primarily by "what's directly above the line" (photo/caption) — length is a
|
||
secondary floor. A rule below a caption ≈ caption width = caption underline (dropped); only a
|
||
rule much WIDER than the caption above it (≥1.5×) is kept as a boundary.
|
||
|
||
## Tried & REVERTED (kept for the record)
|
||
- **Orphan-TEXT absorb** — only match was a NT classifieds box (R75/R132); couldn't verify
|
||
same-ad vs separate-ad → regression risk. Deferred.
|
||
- **Ragged-bottom sidebar split** — landed for JG H28 (split the `రాత్రికి రాత్రికే` sidebar so the
|
||
L-shape white space cleared); later the lead-photo/grow cascade made H28 tile both quote boxes
|
||
with no white space anyway. (Gate: main part must stay multi-column → AJ R53 never split.)
|
||
- **Below-divider cut** (cut content sitting below a vertical divider's END out of the right-column
|
||
article above — JG DL2 holding the maize wide photo R30). Detection is SAFE (straddle gate ⇒
|
||
only JG R2; AJ/NT untouched), but the repair destabilised JG (merged into wrong neighbour R49;
|
||
emptied DL2 regrew). Deferred — needs correct merge-target (the dateline governing the band) +
|
||
a "don't regrow a just-cut block" guard.
|
||
|
||
## RUN-WIDE VALIDATION (all 7 output runs, geometric, no API) — 2026-06-09
|
||
Ran the full cluster chain on every run in `output/` and checked counts + 2+dateline over-merges.
|
||
- **AJ Siddipet ×3** (incl. 2 never-loaded runs): **12 / 18 / 14 / 20 on all three, 0 over-merges.**
|
||
- **NT Siddipet ×3** (incl. 2 never-loaded runs): **11 / 22 / 19 / 25 on all three.**
|
||
- **NT Jangaon**: 7 / 12.
|
||
- **Every `2+dateline` block is an intended ROUNDUP** (verified): NT pg2 [110,127], pg3 [54,60,62],
|
||
pg4 [67,121] (R121 = a `raw_rect` section: headline + 2 dated sub-stories + photos, same accepted
|
||
pattern as R67), JG [64,65] (front-page section roundups).
|
||
- **New runs match the baselines exactly** ⇒ rules generalise (not overfit). No crashes, no new
|
||
over-merges, no merged articles from the length floor.
|
||
Conclusion: the horizontal-line + caption handling and the partition/lead-photo passes are
|
||
validated across the whole corpus.
|
||
|
||
## Classifieds-drop validation (end-to-end replay, no API) — 2026-06-09
|
||
Wired `drop_classifieds_regions` into the pipeline (after `drop_masthead_regions`) + all 3 replay
|
||
scripts, then re-ran every replay:
|
||
- **AJ: 12 / 18 / 14 / 20** — baseline EXACT, **0 classifieds dropped** (no false positives).
|
||
- **NT Siddipet: 11 / 22 / 19 / 25** — baseline EXACT; roundups [110,127] / [54,60,62] /
|
||
[67,121] all intact; 0 classifieds dropped.
|
||
- **NT Jangaon 130635: pg1 = 7, pg2 = 13** (was 12). The only intended change: R65 shrank from
|
||
`[113,1958,1977,6279]` (page bottom, 4 datelines, swallowing the phone-listing classifieds) to
|
||
`[113,1958,1977,5113]` (3-DL front-page roundup). Classifieds R87 (`y5143–6267`, phones=8)
|
||
dropped; the freed left-column content below becomes its own block (12→13). a06_H65 renders as
|
||
a bounded roundup, no longer page-tall over the classifieds.
|
||
- **OCR cost addressed:** detector now OCRs each tall (≥400px) region at **half-res `eng`-only**
|
||
(was full-res `tel+eng`) — ~2× faster per call, verified to produce the IDENTICAL drop set
|
||
(R87 still phones=8/kw=True; 6 sampled news bodies still 0/False).
|
||
- NOTE: R65 still includes a *court/legal* notice stacked in its left column. Per the user's
|
||
"classifieds ONLY" directive this is intentionally left; separating court notices is deferred.
|
||
|
||
## Court/legal-notice carve validation (end-to-end replay, no API) — 2026-06-09
|
||
Follow-up directive: "include court/legal notices as box/section in populating the crops + debug
|
||
img." Added `tag_legal_notices` (pipeline + 3 replays, after classifieds) + the carve pass in
|
||
`cluster_all_article_blocks`, then re-ran every replay:
|
||
- **JG 130635 pg2: 13 → 14 crops.** The court summons (R51/R89/R101/R108/R93) was carved out of
|
||
R65 into a new **`a08_L89`** block `[118,2887,595,4080]` (5 regs) — a clean, complete notice
|
||
("IN THE COURT OF THE HONBLE JUNIOR CIVIL JUDGE … BY ORDER OF THE HON'BLE COURT … ADVOCATE").
|
||
R65 dropped 30→25 members and **whites the carved area out** (verified in `a06_H65` — no
|
||
duplication). The debug boundary map shows it as **LOCK #89 [L] (5)**, nested in #65. pg1 = 7.
|
||
- **AJ: 12 / 18 / 14 / 20** — baseline EXACT, 0 legal carves.
|
||
- **NT Siddipet: 11 / 22 / 19 / 25** — baseline EXACT, 0 legal carves; roundups intact. NT pg3 has
|
||
a real petition notice (R47/R79/R89) but it straddles the NARROW locked #75 + open #62, so Gate 3
|
||
(wide-host-only) correctly SKIPS it → baseline untouched.
|
||
- Detection sweep (read-only) across the corpus: legal keywords flagged ONLY the JG court notice
|
||
and the NT pg3 petition; **0 false positives on any AJ/NT news region.**
|
||
- COST: `tag_legal_notices` OCRs ~40–70 single-column text regions/page (the expensive part).
|
||
Acceptable for the paid pipeline (Claude vision dominates), but a candidate for a cheaper
|
||
pre-gate (e.g. skip dateline-anchored regions) if it becomes a bottleneck.
|
||
- DEFERRED (harder, needs the seed-before-clustering model): a legal notice that straddles a
|
||
NARROW straight-cropped article (NT pg3) — to carve it cleanly the host article's box must be
|
||
shrunk away from the notice, which can't be done by masking alone.
|
||
|
||
## Type-2 split validation (end-to-end replay, no API) — 2026-06-09
|
||
Directive: Type-1 (shared header + common bottom rule) = ONE rectangle (fine); Type-2 (stacked
|
||
articles, own headline+dateline each, no common rule) = SEPARATE articles. Added the split pass.
|
||
- **JG 130635 pg2: 14 → 16 crops.** R65 (the კალెక్టర్ over-merge) split into 3 clean articles:
|
||
R65 కలెక్టర్ `[113,1958,1975,2859]`, R86 సుందరయ్య `[123,2894,1977,4292]`, R112 పోస్టుమార్టం
|
||
`[133,4325,1975,5113]`. Court notice R89 [L] still its own section. pg1 = 7.
|
||
- **AJ: 12 / 18 / 14 / 20** — baseline EXACT (after the dateline-OWNERSHIP fix; the first cut wrongly
|
||
split AJ pg1 R50/R57 and pg3 R35/R79 — headline+subhead sharing one dateline — 13/15, now fixed).
|
||
- **NT Siddipet: 11 / 22 / 19 / 25** — baseline EXACT. The 2+dl roundups [110,127]/[54,60,62] are
|
||
single-header-multi-dateline (Type 1) so they have <2 dateline-owning headlines → NOT split;
|
||
[67,121] are `_raw_rect` → excluded. All correctly preserved.
|
||
- KNOWN minor issue: R65's left column held a furniture stack (court notice + classifieds logo +
|
||
a small job ad + a గమనిక notice). The court notice is carved (L89) and the main classifieds
|
||
(R87) dropped, but the leftover logo (R78 image) bleeds into R86's crop and the job ad + గమనిక
|
||
bleed into R112's crop (they're <400px / images, below the classifieds-drop gate). Secondary
|
||
cleanup — extend furniture detection if needed. [RESOLVED below: furniture section.]
|
||
|
||
## Furniture section (Type-2 split) — column-aware member assignment — 2026-06-09
|
||
Directive: when splitting a Type-2 stack, LEAVE classifieds / notices / unrelated content OUT of
|
||
the article crops, but KEEP that section visible on the debug empty-canvas map. Implemented
|
||
**geometrically (no OCR)** inside the split: each member is assigned to the nearest headline
|
||
at/above it ONLY IF it also sits within that headline's COLUMN x-span (±0.3 col). A member that
|
||
lands in a headline's y-band but a DIFFERENT column is unrelated content sharing the page — it
|
||
falls out as LEFTOVER and is grouped into a `_furniture` section block. Furniture blocks are:
|
||
(1) EXCLUDED from article crops — `crop_all_article_blocks` skips saving them, and because each
|
||
article sub-block's bbox is rebuilt from its in-column members only, the furniture column isn't
|
||
even inside the article rectangle; (2) DRAWN on the debug boundary map as a hatched GREY
|
||
"FURNITURE #id" box; (3) NEVER saved as a crop. The split fires only on JG R65, so this touches
|
||
ONLY that page — AJ/NT untouched.
|
||
- **JG pg2:** R65 → R65 కలెక్టర్ + R86 సుందరయ్య (x682, was x123 w/ the classifieds logo) + R112
|
||
పోస్టుమార్టం (x682, clean) + R89 [L] court notice + **F44 furniture** [123,4139,626,5113]
|
||
(classifieds logo + job ad + గమనిక — debug-only, not cropped). 16 saved crops; boundary map shows
|
||
the FURNITURE box. a09_H86 & a13_H112 verified clean (no furniture bleed).
|
||
- **AJ 12/18/14/20** and **NT 11/22/19/25** — baselines EXACT.
|
||
|
||
## Type-2 split — paragraph_title anchors + ';' dateline + section discriminator — 2026-06-09
|
||
Two more JG pg2 over-merges were reported (R97 = గుండె చికిత్స + మృతుడి పరామర్శ; R64 = జాతీయ రహదారి +
|
||
ఉపసర్పంచుల చెక్ పవర్). Three root causes, three fixes:
|
||
1. **2nd headline detected as `paragraph_title`, not `doc_title`** (R58, R84) → my split's anchors
|
||
were doc_title-only. FIX: anchors = `doc_title` OR `paragraph_title`, with COLUMN-AWARE dateline
|
||
ownership (each dateline → nearest headline above WHOSE x-span covers it; a caption in another
|
||
column can't steal it). Captions/sub-heads still own no dateline ⇒ never an anchor.
|
||
2. **2nd dateline's colon OCR'd as a semicolon** (R97: 'చిల్పూరు, మే 19 ;') → `_dateline_in_text`
|
||
required ':'. FIX: accept ';' too (guards: digit + short prefix + no sentence punctuation).
|
||
3. **R64 was `_raw_rect`** (banner-rectangle extended the wide headline to the next banner, NO
|
||
spanning rule) so the split skipped it. FIX: the split's Type-1 exclusion is now the `_section`
|
||
flag (set ONLY by the section-banner pass, which requires a real spanning bottom rule), not the
|
||
coarse `_raw_rect` flag. R64 (no rule) → splits; R67/R121 (real section) → stay one rectangle.
|
||
- **JG pg2 16→18:** R64→R64+R84, R97→R97+R58 (verified: each crop is one complete article).
|
||
- Side effects, all VERIFIED correct (same rule, genuinely distinct articles — not fragmentation):
|
||
NT pg2 22→23 (అంగన్వాడీ R127 off the ప్రజావాణి section), NT pg3 19→20 (పర్యావరణ R60 off), AJ pg4
|
||
20→21 (గజ్వేల్ R12 — a real dateline the ';' fix recovered; an existing pass then split it).
|
||
R110 (ప్రజావాణి) stayed whole as a `_section` ⇒ the discriminator protects real sections.
|
||
|
||
## Orphan-PHOTO binding — an image may never stand alone — 2026-06-09
|
||
RULE (user): an article may be orphan, but an IMAGE may not — every photo must belong to some
|
||
article. A final pass in `cluster_all_article_blocks` binds each ORPHAN-PHOTO block (no
|
||
doc_title/paragraph_title headline AND no dateline; image-only, OR image + caption only, OR
|
||
image-area ≥50%) to the best NEWS article (furniture/legal blocks are excluded as targets): the
|
||
article whose top is just BELOW the photo in the same column (lead photo), else just above, else
|
||
the nearest column-overlapping article. A TEXT/body orphan is LEFT ALONE.
|
||
- **Sakshi pg1 23→22, pg2 27→20:** every orphan photo bound (R28→R78 lead photo verified — the
|
||
'collector inspecting BL-1' photo + caption now sit in R78; pg2 R38/R42/R48/R52/R60/R63 all
|
||
bound). **0 image orphans remain** on either page. The text orphan R18 (pg1) correctly stays.
|
||
- **JG pg1 7→6:** orphan photo R25 bound into its article.
|
||
- **AJ 12/18/14/21** and **NT 11/23/20/25** — UNCHANGED (no qualifying image orphans on those).
|
||
- NOTE: Sakshi itself has rough article structure (headlines/datelines under-detected, no rules) —
|
||
some bound photos land on a column-overlapping article that may need per-case review; but the
|
||
rule (no floating image) now holds. Deeper Sakshi clustering is a separate task.
|
||
- **Respect horizontal separators in the photo bind (refinement):** a photo binds to an ADJACENT
|
||
article (gap ≤200px) even if a rule sits between them — that rule is the article's own headline
|
||
border and the photo is its lead photo (JG R28→R78, 7px gap). But a FAR fallback bind is REJECTED
|
||
if a horizontal rule LONGER than a caption (>0.6 col) lies between the photo and the article —
|
||
the user's cross-paper line rule. This kept the Sakshi pg2 "student photo" R52 (top of page) from
|
||
binding down through two FULL-WIDTH rules + a page of white space into R118 (the భక్తులకు/
|
||
Kaleshwaram story); R52 now binds to R127 in its own band, and `a08_H118` is clean. Verified:
|
||
Sakshi pg1 22 / pg2 20 (0 image orphans); AJ 12/18/14/21, NT 11/23/20/25, JG 6/18 all unchanged.
|
||
|
||
## Sakshi page-1 deep fix round — 2026-06-10 (visual review of all 22 pg1 crops → 14 BAD)
|
||
A crop-by-crop visual audit of Sakshi JG pg1 found 14/22 BAD crops. Root causes & fixes, ALL
|
||
Sakshi-gated (AJ 12/18/14/21, NT 11/23/20/25, JG 6/18 re-validated EXACT after every change):
|
||
- **Masthead drop for Sakshi** (`drop_masthead_regions`, `paper=="sakshi"` branch, every page):
|
||
drop every region that ends ABOVE the topmost headline-type region (sanity: that line must be
|
||
in the top 30%). pg1: weather box R64, photo-brief R62/R59, nameplate R63, and the page-date
|
||
strip R71 — which OCR'd as a DATELINE and seeded a fake DL71 article. pg2: R68/R71/R84/R104.
|
||
- **`detect_sakshi_headline_bands`** (called after `promote_paragraph_titles`, pipeline + replay)
|
||
— the big one. Sakshi's display headlines are often NOT boxed by PaddleOCR at all. Two passes:
|
||
1. **WIDEN**: a doc_title grows sideways over adjacent UNCOVERED large-ink columns — its
|
||
un-boxed kicker half (R52 'అనూహ్య స్పందన' + red kicker 'రిబేట్ ఆఫర్కు'). Widest titles first;
|
||
extensions join the coverage map so a narrow neighbour can't steal a wide headline's kicker.
|
||
Ink within 25px of a foreign region's edge is the neighbour's own glyph overflow — ignored.
|
||
2. **SYNTHESIZE**: an uncovered big-type band (55–280px tall, ≥0.75 col wide, ink 5–60%, text
|
||
following below, never above the first real headline) becomes a synthetic `doc_title`
|
||
(`_synth_band`) anchor. Found BOTH missing pg1 banners (lift-1 'జూలై 31...' R99 and SIR
|
||
'మ్యాపింగ్లో జిల్లాకు అగ్రస్థానం' R100) and pg2's గమనిక box R139. CRITICAL detail: two
|
||
headlines can share pixel rows with only a ~50px gutter (same size as a word gap) — a gap
|
||
splits a band run ONLY if it contains a known text-column LEFT edge (≥35px wide); plain
|
||
gap-size segmentation merges both banners into one anchor (measured: pg1 12 crops, 45-member
|
||
monster).
|
||
- **Box-edge rule disarm** (`_is_box_edge` in `_barrier_between`, Sakshi only): a horizontal rule
|
||
with a PARALLEL partner 80–400px away (≥70% overlap) is a quote/bullet-box border, never a story
|
||
separator. Without it the lift-1 bullet-box borders fenced the article's own columns off.
|
||
- **Photo-bottom border disarm** (`_in_photo`, Sakshi only): a rule within 14px BELOW an image,
|
||
horizontally inside the photo span ±25px, is the photo's frame/caption rule (PaddleOCR's box
|
||
ends a few px above the printed border, so the interior test missed it). Without it R61/R46/R23
|
||
(lift-1 caption + col-3 body) fell into the story below.
|
||
- **Caption never a headline line** (headline-stack recovery, all papers — baselines unaffected):
|
||
a figure_title directly under an image (≤90px) is a CAPTION; stop the stack there. Without it
|
||
the synth SIR headline pulled the Indiramma photo's caption R69 down as a "wrapped line".
|
||
- **No lead-photo rebind into a `_synth_band` headline**: a synth band's lead photo is BELOW it by
|
||
construction; a photo+caption ABOVE it belongs to the story above (R36/R69 stay with R77).
|
||
- **Continuation-column fold** (Sakshi only, end of clusterer): a TEXT-only orphan block directly
|
||
beside an article block with the SAME vertical band (top ±90px, ≥80% overlap, gap ≤80px, no
|
||
vertical rule between) is the other column of the same box → merge (pg1 R18 → R79; the text even
|
||
continues mid-word 'విద్యా సంస్థ|లకు' across the two).
|
||
- **Stray-image re-home** (Sakshi only): an image member vertically DETACHED from the rest of its
|
||
block but fully inside ANOTHER article's box moves there. A 3-column synth banner column-overlaps
|
||
everything below it, so R50's tiny సాక్షి-ఎఫెక్ట్ clipping R44 (whose own narrow headline does
|
||
NOT column-overlap it) had fallen to the banner 1100px above.
|
||
- **Narrow-crop masking** (`crop_all_article_blocks`, all papers): narrow (≤1100px) crops now white
|
||
out regions OWNED BY OTHER blocks (gpad=2) before the ink-trim — kills the sliced-glyph slivers
|
||
at crop edges. Un-owned ink is left alone (it may be the crop's own un-boxed headline). Members
|
||
are repasted, so nothing of the article itself can be erased. Counts unchanged on all papers.
|
||
- **Review overlays**: `crop_all_article_blocks(..., overlay=True)` (used by `_replay_sakshi.py`)
|
||
draws member boxes on the review crops — blue=anchor, magenta=synth band, green=member.
|
||
- **RESULT (Sakshi JG pg1): 22 → 16 crops, every reduction a genuine reunification.** Visual
|
||
re-audit of all 16: 14 GOOD / 2 MINOR / 0 BAD (was 8 GOOD / 14 BAD). The rebate feature package
|
||
(was shredded into P94+H52+DL71+DL11 with sliced letters) is ONE crop; lift-1 and SIR-mapping
|
||
(whose headlines have NO region) are complete articles anchored by synth headlines; H77 is
|
||
exactly the Indiramma story; H54/H78/H55 no longer hold neighbours' content. pg2: 20 → 17
|
||
(top photo-gallery orphans folded into their stories; not yet visually audited).
|
||
- **NEW Sakshi baselines: pg1 → 16, pg2 → 17.**
|
||
- Known remaining (MINOR): a01_H55 loses the unboxed last line of its మ్యూజియాలజీ sub-brief
|
||
(no region covers it; the next block's white-out grazes it); the EAPCET ad image R33 sits
|
||
mid-article in H55 (it IS physically inside the article band; ad detection deferred).
|
||
- Known remaining (pg2, PRE-EXISTING): the Type-2 split's column-aware leftover pass marks the
|
||
విద్యార్థిని ఆత్మహత్య article as FURNITURE (R4/R10 F) → it is NOT cropped at all. Present in the
|
||
pre-fix baseline too. Needs its own round. [RESOLVED in the pg2 round below.]
|
||
|
||
## Sakshi page-2 round — 2026-06-10 (visual audit of all pg2 crops; user-reported defects)
|
||
Fixes, validated after each change (AJ 12/18/14/21, NT 11/23/20/25 incl. 2+dl=[110]-only on pg2,
|
||
JG 6/18 — all EXACT):
|
||
- **a04_H118 body truncated at the column boundary** (col5 ends mid-word 'నిర్మా-', col6 holds
|
||
'ణం…'): the col6 body R20 was stolen by the headless dateline R14's GROW-UP, which climbed PAST
|
||
the R14 story's own subhead R121 because `_is_caption` lets a 197px two-paragraph text through
|
||
(≤200px + a photo 82px above). FIX (all papers, baselines unchanged): grow-up STOPS after
|
||
claiming a `paragraph_title` — the heading IS the article's top; nothing above it is ours.
|
||
- **విద్యార్థిని ఆత్మహత్య never cropped** (whole story = furniture): its heading R88 IS detected
|
||
(paragraph_title) but the R41 Type-2 split's anchors don't span col4 → all 8 regions fell out
|
||
as LEFTOVER → furniture. FIX (all papers; gates keep AJ/NT/JG exact): a leftover group that
|
||
LEADS with its own COLUMN-WIDE heading (≥0.6 of the group width — a narrow WANTED label stays
|
||
furniture) and has body text → emit as an ARTICLE (kind P), not furniture.
|
||
- **a13_H93/'విషాదయాత్ర' cut mid-sentence, its tail + tributes in the girl's crop**: the col4
|
||
leftover zone holds TWO stacked items (girl's story y3658-4643, శ్రీనివాస్ tail y4723-5938) with
|
||
a separator rule at y4679 between them. FIX (Sakshi only): (1) leftover clustering never merges
|
||
across a separator rule; (2) a heading-less leftover group whose band MATCHES an adjacent-column
|
||
split article (top ±90px, ≥80% overlap, gap ≤80px) is that article's column CONTINUATION → its
|
||
members merge there (the tail's text literally resumes H93's "...అయితే వైద్యులు" mid-sentence).
|
||
ORDER MATTERS: the wide-heading→ARTICLE check runs FIRST, else the girl's story (gap-close to
|
||
H99's band) band-attaches into H99. Both passes are Sakshi-gated: un-gated they re-shuffled an
|
||
NT pg2 leftover into R105 (2+dl-blocks [110]→[110,105]) — caught by the 2+dl indicator and
|
||
re-gated.
|
||
- **a03_P129/a05_H91: dead farmer's portrait R47 + caption R115 stuck at the briefs column's
|
||
bottom, white hole in his own story's crop**: the stray-image re-home pass couldn't see R47 as
|
||
detached because its caption R115 sat below it inside the same block. FIX: re-home treats an
|
||
image + its caption (figure_title ≤90px below, ≥40% overlap) as ONE UNIT — excluded from the
|
||
envelope test and moved together (→ H91, inside whose box they sit).
|
||
- **గమనిక notice box synthesized as a headline** (R139, its own bogus crop): a filled red header
|
||
measures ink 0.51; real Sakshi display headlines measure 0.30-0.35. FIX: synth band ink ceiling
|
||
0.60 → 0.45; the notice text returns to the classifieds-column furniture (not cropped).
|
||
- **RESULT (Sakshi JG pg2): 20 → 17 crops.** Visual audit of all 17: 11 GOOD / 4 MINOR / 2 BAD →
|
||
after the H93/P88 boundary fix the BAD pair re-audited GOOD. The H118 two-column body, the
|
||
విద్యార్థిని article (new crop P88), H91's portrait, and the P129 briefs column all verified.
|
||
- Known remaining (pg2, magazine-style kids/summer feature band only): H82/H127 share the gallery
|
||
collage and H82's story conclusion ('–దుగ్గొండి' sign-off + 'నీతి' moral box) renders inside
|
||
H127's crop — a collage-page attribution problem, low priority for the news pipeline. The
|
||
classifieds crops (P136/P111) bundle neighbouring ads and clip one ref number (MINOR).
|
||
|
||
## POLITICAL crops now BLOCK-SNAP to the geometric blocks — 2026-06-10
|
||
`crop_political_articles` used to re-derive each Claude-selected article's rectangle from the raw
|
||
member_region_ids union plus ~400 lines of parallel ad-hoc geometry (banner walls, foreign-headline
|
||
clips, floors, grow-right) — none of which carried the geometric-crop fixes. Now it BLOCK-SNAPS:
|
||
- It clusters the page once (`cluster_all_article_blocks`) and renders every block crop via
|
||
`crop_all_article_blocks` into a temp dir — the exact validated pipeline (furniture drop,
|
||
un-boxed headline recovery, neighbour masking, box-edge rules, continuation folds).
|
||
- Each political article is mapped to the geometric block owning the MAJORITY (≥50%) of its
|
||
member regions; that block's crop is shipped as `article.png` (`method:
|
||
"smart_extractor_blocksnap"`, info.json carries the block's full member list).
|
||
- A block already claimed by an earlier article on the page is not re-used (no duplicate crops);
|
||
any article that fails to map falls back to the legacy path unchanged.
|
||
- The pipeline call passes `dateline_starts` + `sep_lines` through so the clustering matches the
|
||
all_article_crops output exactly.
|
||
- VALIDATION (no API — `_replay_political.py` replays saved `page_NNN.political_result.json`
|
||
member lists through the new path): Sakshi JG pg1 2/2 snapped (the rebate package + DL5, both
|
||
verified visually as the block-quality crops); AJ Siddipet pg2/3/4 4/4 snapped (10-13 of 12-13
|
||
member votes, incl. the R48/R33 banner blocks); NT Jangaon pg1 2/2 snapped (13/13, 4/4). ZERO
|
||
legacy fallbacks needed on the corpus.
|
||
- NOTE: replayed member lists come from OLD paid runs (stale region understanding) — e.g. the
|
||
Sakshi 'Transfers delayed!' entry maps to the rebate block because its old members [66,29] sit
|
||
there now. On the next paid run Claude selects members against the CURRENT regions (incl. the
|
||
synthesized Sakshi headlines, which are in regions.json and the prompt's region list), so the
|
||
member→block vote will be consistent end-to-end.
|
||
|
||
## New-run defects round (Sakshi_Jangaon District_20260513, first PAID run on new code) — 2026-06-10
|
||
User-reported bad crops on the fresh paper; three more Sakshi-gated passes (validated: old-run
|
||
Sakshi pg1 16 / pg2 17 UNCHANGED; AJ 12/18/14/21, NT 11/23/20/25 [110], JG 6/18 all EXACT):
|
||
- **pg2 a02_H106 was a 695×5055 column strip** (statue story + 4000px of masked white):
|
||
TWO causes. (a) The dance-photo caption R104 (page top) fell down-column into H106 —
|
||
its photo R44 lived in another block. FIX: **stray caption re-home** — a figure_title member
|
||
moves to the block that owns the image it abuts (±90px above/below, with **-30px overlap
|
||
tolerance**: PaddleOCR's photo box overlaps the caption strip by a few px, R104 vs R44 = -11px,
|
||
which defeated a 0-gap test on the first attempt). Donor block shrinks back to its members.
|
||
(b) The aviation story's heading is a `paragraph_title` 1200px below the statue story's
|
||
doc_title in-column → never promoted → whole stack merged. FIX: **stacked-story heading
|
||
promotion** — a pt ≥0.55 col wide with content directly beneath (≤150px) and the nearest
|
||
in-column base anchor **>900px** above becomes an anchor. The gate was first 600px and broke
|
||
the old-run rebate package (its sub-head R94 sits 614px under the widened R52 headline and
|
||
re-split, whiting R88's box over the stats infographic — caught by re-validating the old run);
|
||
900px keeps feature sub-heads folded and still promotes genuinely stacked stories.
|
||
- **pg2 a08_H100 stretched ~900px up with a foreign fragment above its own headline** (the NPDCL
|
||
story's tail folded down-column into H100). FIX: **stray text re-home** — a plain-text member
|
||
of an H block lying entirely ABOVE the anchor headline's top is never that article's content
|
||
(kickers/decks are title-typed); move it to the smallest other article box containing it.
|
||
- Verified visually: new-run a10_H106 (639×1326, exactly the statue story), a09_H100 (complete,
|
||
no stray top), old-run a02_H52 (infographic numbers 15,941/5.79/11,376/3.72/1,00,20,087 intact).
|
||
- New-run pages have NOT had a full visual audit (only the reported crops + block maps checked);
|
||
pg1 = 14 crops / pg2 = 16 on replay.
|
||
|
||
## NT Adilabad flood-special front page — NT rule split + late structural cut — 2026-06-10
|
||
NamastheTelangana_Adilabad_20260528 pg1 produced 5 page-scale OVERLAPPING mega-blocks. This page
|
||
is DETECTION-LIMITED at the source: 65 regions only, 4 doc_titles for ~8 stories, and BOTH big
|
||
feature headlines ('రెక్కల కష్టం.. నీళ్లపాలు', 'వడ్ల తరలింపు ఎక్కడిదాకా?') are INSIDE PaddleOCR
|
||
image boxes — invisible to every geometric pass incl. the synth-band detector (coverage test).
|
||
With anchors missing, the image-must-belong bind + grow/merge built page-tall blocks. Fixes:
|
||
- **NT horizontal-rule split enabled** (was Sakshi-only): same machinery, but only at rules
|
||
≥1.8 col (per the line study a ≥2-col rule is an article boundary on NT; the boxed grid's
|
||
~1-col caption/box rules must not cut). _replay_adilabad.py added (NTA_RUN env overridable).
|
||
- **NT LATE structural cut** (end of clusterer, after grow/merge/photo binds): the mid-pipeline
|
||
split runs BEFORE the passes that built the mega-block, so DL5 [668,901,4084,6302] was never
|
||
re-checked against the 4.4-col rule at y=4764 (and at mid-split time the bottom band's R30
|
||
headline wasn't yet a member, so the new-start gate failed). Final blocks (not locked/raw_rect/
|
||
section/furniture) are re-cut at ≥1.8-col rules with the underline guard; a rule ≥2.2 col cuts
|
||
even with no detected start below. DL5 → DL56 (the whole flood feature, verified coherent) +
|
||
H30 ('తడిసిన వడ్లు కొనాలి' bottom band, verified clean). pg1 5 → 6 crops.
|
||
- VALIDATION: NT Siddipet 11/23/20/25 EXACT (2+dl [110],[54,62],[67,121]); NT Jangaon 6/18 EXACT;
|
||
Adilabad pg2 31 crops unchanged (no shredding). AJ/Sakshi untouched (gate).
|
||
- HONEST REMAINDER: H44 still spans the page top (left story + photo row + nameplate share the
|
||
band; the 60%-block-span guard correctly refuses partial rules on a page-wide block) and the
|
||
H59/DL9 boxes still overlap neighbours. Only a better layout model (headline bands inside
|
||
photo boxes) or the paid pipeline's Claude-vision pass can fully fix this page. NT masthead
|
||
drop never fired here (nameplate is INSIDE image R46, no header region) — benign, the strip
|
||
rides atop the photo-row crop.
|
||
|
||
## ⚠ REPLAY-TOOLING BUG (found 2026-06-10, fixed): _ACTIVE_PAPER was never set for NT
|
||
`_replay_jangaon.py` / `_replay_namaste.py` never set `se._ACTIVE_PAPER`, so every prior "NT
|
||
validated EXACT" run measured with ALL paper-gated clustering passes OFF while the real pipeline
|
||
runs them ON. (AJ has no gated passes, Sakshi's replay always set it — only NT was affected.)
|
||
Both scripts now set it; `_replay_jangaon.py` takes JG_RUN env (default = the 130635 run; the
|
||
user-facing run of the same paper is ..._20260608_124720). FIRST true gated-NT measurement:
|
||
NT Siddipet 11/23/20/25 + 2+dl [110],[54,62],[67,121] EXACT (gated passes are no-ops there ✓),
|
||
JG both runs 6/18, Adilabad 6/31.
|
||
|
||
## NT Jangaon pg1 (user-reported: a01_DL3 white holes, a04_H28 over-merge, a05_DL2 foreign photo)
|
||
Root cause is the same family as Sakshi/Adilabad: THREE un-detected display headlines (brush-font
|
||
'భానుడి భగభగ', script 'నమ్మిండు..', DL2's heading). The heat feature's lower content (vendor +
|
||
ice-cream photos, captions, continuation column) folded DOWN to the 'ఆలస్యంపై రైతుల ఆగ్రహం'
|
||
headline 2200px below (nearest-anchor metric); the white holes in a01_DL3 were those stolen
|
||
regions masked out. Fixes (gated `in ("sakshi","namaste_telangana")` — AJ untouched):
|
||
- **Re-home family enabled for NT** (stray image+caption unit, stray caption, above-own-headline
|
||
members). The above-headline re-home is GENERALISED from text-only to text/image/figure_title
|
||
(title-typed members stay — kickers/decks are legitimate above a headline).
|
||
- **Synth headline bands enabled for NT — FRONT PAGE ONLY** (`n == 1`): inner NT pages are dense
|
||
and well-detected; un-gated synth split the protected Siddipet pg4 roundups (25→28, caught).
|
||
Band height cap 280→400 (tall display/script faces; ink-fraction still rejects photos).
|
||
- RESULT (JG 124720 pg1, counts unchanged at 6): DL3 = the COMPLETE heat feature (mem 8→13, no
|
||
white holes, verified visually); H28 starts AT its own headline [y3089, was 838] = exactly the
|
||
ఆగ్రహం article (verified); DL2 = clean single-column committee article, its foreign tractor
|
||
photo+caption re-homed to R29's procurement story (verified).
|
||
- Residual: DL2 remains heading-less (its display heading has no region and the synth band gates
|
||
didn't admit it) — hard-limitation class; the heading pixels sit above the crop.
|