Skip to content

Commit 9bab933

Browse files
committed
De-duplicate repeated content sections (0.9.19)
Sparse or hand-drawn posters can make the model loop and emit the same section several times (e.g. three identical untitled "than you might think!" sections on the Turing Way sketchnote poster), and the raw-text recovery step could re-add an already-present fragment. Post-processing now collapses sections with identical normalized content to one entry, keeping the first occurrence and preferring a titled copy. Distinct sections are unaffected.
1 parent 838f949 commit 9bab933

4 files changed

Lines changed: 68 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.9.19] - 2026-06-10
9+
10+
### Fixed
11+
12+
- **Duplicate content sections are now de-duplicated.** On sparse or hand-drawn posters the model can loop and emit the same section several times (observed as three identical untitled "than you might think!" sections on the Turing Way sketchnote poster), and the raw-text recovery step could re-add a fragment that already appeared. Post-processing now collapses sections with identical normalized content (case- and whitespace-insensitive) to a single entry, keeping the first occurrence and preferring a copy that carries a title. Distinct sections are unaffected.
13+
814
## [0.9.18] - 2026-06-10
915

1016
### Fixed

poster2json/extract.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2349,6 +2349,26 @@ def _postprocess_json(
23492349
"sectionContent": blob,
23502350
})
23512351

2352+
# Drop duplicate sections. The model can repeat a section verbatim
2353+
# (a generation loop, common on sparse or hand-drawn posters), and
2354+
# the raw-text recovery above can re-add a fragment that already
2355+
# appears several times. De-dupe by normalized content, keeping the
2356+
# first occurrence and preferring a copy that carries a title.
2357+
deduped = []
2358+
seen_by_content = {}
2359+
for sec in cleaned_sections:
2360+
ckey = " ".join(sec.get("sectionContent", "").lower().split())
2361+
if not ckey:
2362+
continue
2363+
if ckey in seen_by_content:
2364+
idx = seen_by_content[ckey]
2365+
if not deduped[idx].get("sectionTitle") and sec.get("sectionTitle"):
2366+
deduped[idx] = sec
2367+
continue
2368+
seen_by_content[ckey] = len(deduped)
2369+
deduped.append(sec)
2370+
cleaned_sections = deduped
2371+
23522372
result["content"]["sections"] = cleaned_sections
23532373

23542374
# Build sections from raw text when LLM produced none

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[tool.poetry]
22

33
name = "poster2json"
4-
version = "0.9.18"
4+
version = "0.9.19"
55
description = "Convert scientific posters (PDF/images) to structured JSON metadata using Large Language Models"
66

77
packages = [{ include = "poster2json" }]

tests/test_normalize.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,47 @@ def test_creator_nametype_personal_vs_organizational():
478478
assert out["creators"][1]["nameType"] == "Organizational"
479479

480480

481+
def test_postprocess_dedupes_repeated_sections():
482+
from poster2json.extract import _postprocess_json
483+
484+
# The model looped and emitted the same fragment as three untitled sections
485+
# (the TTW sketchnote-poster case): they must collapse to one.
486+
data = {"content": {"sections": [
487+
{"sectionTitle": "", "sectionContent": "than you might think!"},
488+
{"sectionTitle": "", "sectionContent": "than you might think!"},
489+
{"sectionTitle": "", "sectionContent": "than you might think!"},
490+
]}}
491+
out = _postprocess_json(data, raw_text="")
492+
secs = out["content"]["sections"]
493+
assert len(secs) == 1
494+
assert secs[0]["sectionContent"] == "than you might think!"
495+
496+
497+
def test_postprocess_dedup_prefers_titled_section():
498+
from poster2json.extract import _postprocess_json
499+
500+
# Same content appears untitled then titled; keep the titled copy.
501+
data = {"content": {"sections": [
502+
{"sectionContent": "shared body text content goes here"},
503+
{"sectionTitle": "Collaboration", "sectionContent": "shared body text content goes here"},
504+
]}}
505+
out = _postprocess_json(data, raw_text="")
506+
secs = out["content"]["sections"]
507+
assert len(secs) == 1
508+
assert secs[0].get("sectionTitle") == "Collaboration"
509+
510+
511+
def test_postprocess_keeps_distinct_sections():
512+
from poster2json.extract import _postprocess_json
513+
514+
data = {"content": {"sections": [
515+
{"sectionTitle": "Intro", "sectionContent": "first distinct section body"},
516+
{"sectionTitle": "Methods", "sectionContent": "second distinct section body"},
517+
]}}
518+
out = _postprocess_json(data, raw_text="")
519+
assert len(out["content"]["sections"]) == 2
520+
521+
481522
def test_orcid_enrichment_uses_affiliation_resolver():
482523
from poster2json.orcid import enrich_creators_orcid
483524

0 commit comments

Comments
 (0)