Skip to content

Commit d1615d7

Browse files
committed
Prepare v0.3.2-alpha release
1 parent bbebc7a commit d1615d7

4 files changed

Lines changed: 466 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,28 @@ Add an entry here when the project meaningfully changes for users, for example:
1414

1515
- No unreleased user-facing changes yet.
1616

17+
## v0.3.2-alpha
18+
19+
Fifth public alpha release of DeepPaperNote.
20+
21+
### Changed
22+
23+
- Strengthened `local_pdf -> enrich_metadata` so Zotero-style attachment filenames no longer dominate metadata resolution.
24+
- Added local PDF metadata hints that prefer embedded PDF title, DOI, arXiv identifiers, and first-page title signals before falling back to cleaned filenames.
25+
- Added local-PDF-only title correction so high-confidence external matches can replace noisy attachment-style titles without changing the global merge policy.
26+
- Tightened candidate scoring so published venue/DOI records are preferred over preprint-style matches when both are available.
27+
- Normalized common PDF ligatures such as `` and `` during text extraction so titles and other extracted strings are cleaner and more stable.
28+
29+
### Packaging
30+
31+
- Rebuilt the release zip from the latest `main` branch state for `v0.3.2-alpha`.
32+
33+
### Notes
34+
35+
- This is still an alpha release.
36+
- Chinese remains the only fully supported output language.
37+
- Figure replacement is still conservative and placeholder-first when image confidence is insufficient.
38+
1739
## v0.3.1-alpha
1840

1941
Fourth public alpha release of DeepPaperNote.
95.2 KB
Binary file not shown.

scripts/common.py

Lines changed: 171 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,47 @@ def normalize_title(text: str) -> str:
104104
return re.sub(r"[^a-z0-9\s]", "", normalize_whitespace(text).lower()).strip()
105105

106106

107+
LOCAL_PDF_PREFIX_PATTERN = re.compile(r"^(?:[^-]{1,120})\s+-\s+(?:19|20)\d{2}\s+-\s+")
108+
LOCAL_PDF_SUFFIX_ID_PATTERN = re.compile(r"\s*-\s*\d{4,}\s*$")
109+
PREPRINT_HINTS = ("medrxiv", "biorxiv", "preprint", "arxiv", "10.1101/", "10.21203/rs.", "preprints.org")
110+
PDF_LIGATURE_MAP = {
111+
"\u00df": "ss",
112+
"\ufb00": "ff",
113+
"\ufb01": "fi",
114+
"\ufb02": "fl",
115+
"\ufb03": "ffi",
116+
"\ufb04": "ffl",
117+
}
118+
119+
120+
def clean_local_pdf_stem(stem: str) -> str:
121+
raw = normalize_whitespace((stem or "").replace("_", " "))
122+
if not raw:
123+
return ""
124+
cleaned = LOCAL_PDF_PREFIX_PATTERN.sub("", raw)
125+
cleaned = LOCAL_PDF_SUFFIX_ID_PATTERN.sub("", cleaned)
126+
cleaned = normalize_whitespace(cleaned)
127+
return cleaned or raw
128+
129+
130+
def is_probable_local_pdf_artifact_title(title: str) -> bool:
131+
normalized = normalize_whitespace(title)
132+
if not normalized:
133+
return False
134+
if LOCAL_PDF_PREFIX_PATTERN.match(normalized):
135+
return True
136+
if LOCAL_PDF_SUFFIX_ID_PATTERN.search(normalized):
137+
return True
138+
return bool(re.search(r"\b(?:et al\.?|等)\b", normalized, flags=re.IGNORECASE) and re.search(r"\b(?:19|20)\d{2}\b", normalized))
139+
140+
141+
def normalize_pdf_text_artifacts(text: str) -> str:
142+
normalized = text or ""
143+
for original, replacement in PDF_LIGATURE_MAP.items():
144+
normalized = normalized.replace(original, replacement)
145+
return normalized
146+
147+
107148
def slugify_filename(text: str) -> str:
108149
text = normalize_whitespace(text)
109150
text = re.sub(r"[^\w\s-]", "", text, flags=re.UNICODE)
@@ -167,8 +208,9 @@ def publication_quality_score(record: dict[str, Any]) -> int:
167208
venue = normalize_whitespace(str(record.get("venue", ""))).lower()
168209
source_url = normalize_whitespace(str(record.get("source_url", ""))).lower()
169210
source = normalize_whitespace(str(record.get("source", ""))).lower()
170-
joined = " ".join([venue, source_url, source])
171-
if any(token in joined for token in ["medrxiv", "biorxiv", "preprint", "arxiv"]):
211+
doi = normalize_whitespace(str(record.get("doi", ""))).lower()
212+
joined = " ".join([venue, source_url, source, doi])
213+
if any(token in joined for token in PREPRINT_HINTS):
172214
return 0
173215
if venue or source == "crossref":
174216
return 2
@@ -181,7 +223,7 @@ def candidate_priority_score(record: dict[str, Any]) -> int:
181223
doi = normalize_whitespace(str(record.get("doi", ""))).lower()
182224
joined = " ".join([source, source_url, doi])
183225

184-
if "preprints.org" in joined or "10.20944/preprints" in joined:
226+
if "10.20944/preprints" in joined or any(token in joined for token in PREPRINT_HINTS):
185227
return 0
186228

187229
if record.get("doi") and publication_quality_score(record) >= 2:
@@ -356,8 +398,23 @@ def fetch_arxiv_entries(*, search_query: str = "", id_list: str = "", max_result
356398
"max_results": max_results,
357399
}
358400
)
359-
xml_content = http_get_text(f"https://export.arxiv.org/api/query?{params}")
360-
return parse_arxiv_xml(xml_content)
401+
try:
402+
xml_content = http_get_text(f"https://export.arxiv.org/api/query?{params}")
403+
except Exception:
404+
return []
405+
if not normalize_whitespace(xml_content):
406+
return []
407+
try:
408+
return parse_arxiv_xml(xml_content)
409+
except Exception:
410+
return []
411+
412+
413+
def safe_fetch_arxiv_entries(*, search_query: str = "", id_list: str = "", max_results: int = 10) -> list[dict[str, Any]]:
414+
try:
415+
return fetch_arxiv_entries(search_query=search_query, id_list=id_list, max_results=max_results)
416+
except Exception:
417+
return []
361418

362419

363420
def normalize_crossref_work(item: dict[str, Any]) -> dict[str, Any]:
@@ -620,24 +677,32 @@ def resolve_reference(value: str) -> dict[str, Any]:
620677
stripped = (value or "").strip()
621678
if source_type == "local_pdf":
622679
path = Path(stripped).expanduser().resolve()
623-
return {
680+
hints = extract_local_pdf_hints(path)
681+
paper = {
624682
"status": "ok",
625683
"source_type": "local_pdf",
626684
"source_url": str(path),
627685
"local_pdf_path": str(path),
628-
"title": path.stem.replace("_", " "),
686+
"title": normalize_whitespace(str(hints.get("title", ""))) or clean_local_pdf_stem(path.stem) or path.stem.replace("_", " "),
629687
"metadata_sources": ["local_pdf"],
630-
"paper_id": paper_id_for_record({"title": path.stem}),
631688
}
689+
doi = normalize_whitespace(str(hints.get("doi", "")))
690+
arxiv_id = normalize_whitespace(str(hints.get("arxiv_id", "")))
691+
if doi:
692+
paper["doi"] = doi
693+
if arxiv_id:
694+
paper["arxiv_id"] = arxiv_id
695+
paper["paper_id"] = paper_id_for_record(paper)
696+
return paper
632697
if source_type == "arxiv_id":
633-
papers = fetch_arxiv_entries(id_list=extract_arxiv_id(stripped) or "", max_results=1)
698+
papers = safe_fetch_arxiv_entries(id_list=extract_arxiv_id(stripped) or "", max_results=1)
634699
if papers:
635700
paper = papers[0]
636701
paper["paper_id"] = paper_id_for_record(paper)
637702
paper["status"] = "ok"
638703
return paper
639704
if source_type == "arxiv_url":
640-
papers = fetch_arxiv_entries(id_list=extract_arxiv_id(stripped) or "", max_results=1)
705+
papers = safe_fetch_arxiv_entries(id_list=extract_arxiv_id(stripped) or "", max_results=1)
641706
if papers:
642707
paper = papers[0]
643708
paper["paper_id"] = paper_id_for_record(paper)
@@ -691,7 +756,7 @@ def resolve_reference(value: str) -> dict[str, Any]:
691756
search_semantic_scholar(title, limit=5)
692757
+ search_crossref_by_title(title, limit=5)
693758
+ search_openalex_by_title(title, limit=5)
694-
+ fetch_arxiv_entries(search_query=f'ti:"{title}"', max_results=5)
759+
+ safe_fetch_arxiv_entries(search_query=f'ti:"{title}"', max_results=5)
695760
)
696761
best = choose_best_title_match(title, candidates)
697762
if best:
@@ -728,7 +793,7 @@ def enrich_metadata(record: dict[str, Any]) -> dict[str, Any]:
728793
candidates.append(sem)
729794

730795
if arxiv_id:
731-
arxiv = fetch_arxiv_entries(id_list=arxiv_id, max_results=1)
796+
arxiv = safe_fetch_arxiv_entries(id_list=arxiv_id, max_results=1)
732797
if arxiv:
733798
candidates.append(arxiv[0])
734799

@@ -742,7 +807,7 @@ def enrich_metadata(record: dict[str, Any]) -> dict[str, Any]:
742807
cross = choose_best_title_match(title, search_crossref_by_title(title, limit=5))
743808
if cross:
744809
candidates.append(cross)
745-
arxiv = choose_best_title_match(title, fetch_arxiv_entries(search_query=f'ti:"{title}"', max_results=5))
810+
arxiv = choose_best_title_match(title, safe_fetch_arxiv_entries(search_query=f'ti:"{title}"', max_results=5))
746811
if arxiv:
747812
candidates.append(arxiv)
748813

@@ -753,6 +818,12 @@ def enrich_metadata(record: dict[str, Any]) -> dict[str, Any]:
753818
merged["source_url"] = f"https://doi.org/{merged['doi']}"
754819
if merged.get("arxiv_id") and not merged.get("pdf_url"):
755820
merged["pdf_url"] = f"https://arxiv.org/pdf/{merged['arxiv_id']}.pdf"
821+
if merged.get("arxiv_id") and not merged.get("doi"):
822+
merged["doi"] = f"10.48550/arXiv.{merged['arxiv_id']}"
823+
if base.get("source_type") == "local_pdf":
824+
corrected_title = choose_local_pdf_corrected_title(base, candidates[1:])
825+
if corrected_title:
826+
merged["title"] = corrected_title
756827
merged["paper_id"] = paper_id_for_record(merged)
757828
return merged
758829

@@ -943,7 +1014,7 @@ def split_sentences(text: str) -> list[str]:
9431014

9441015

9451016
def clean_pdf_line(line: str) -> str:
946-
line = re.sub(r"\s+", " ", line or "").strip()
1017+
line = re.sub(r"\s+", " ", normalize_pdf_text_artifacts(line or "")).strip()
9471018
if not line:
9481019
return ""
9491020
if re.fullmatch(r"\d+", line):
@@ -1036,6 +1107,92 @@ def extract_pdf_text(pdf_path: Path, max_pages: int | None = None) -> str:
10361107
return "\n".join(texts)
10371108

10381109

1110+
def is_plausible_pdf_title_line(line: str) -> bool:
1111+
normalized = clean_pdf_line(line)
1112+
lower = normalized.lower()
1113+
if len(normalized) < 20 or len(normalized.split()) < 4:
1114+
return False
1115+
if normalized.count(",") >= 3:
1116+
return False
1117+
if any(token in lower for token in ["doi.org/", "http://", "https://", "www.", "check for updates"]):
1118+
return False
1119+
if lower in {"abstract", "article", "preprint"}:
1120+
return False
1121+
if lower.startswith("npj |") or lower.startswith("arxiv:") or lower.startswith("submitted to"):
1122+
return False
1123+
if " doi:" in lower or lower.startswith("doi:"):
1124+
return False
1125+
return True
1126+
1127+
1128+
def first_page_title_candidate(first_page_text: str) -> str:
1129+
for raw_line in (first_page_text or "").splitlines():
1130+
if is_plausible_pdf_title_line(raw_line):
1131+
return clean_pdf_line(raw_line)
1132+
return ""
1133+
1134+
1135+
def extract_local_pdf_hints(pdf_path: Path) -> dict[str, Any]:
1136+
raw_title = normalize_whitespace(pdf_path.stem.replace("_", " "))
1137+
cleaned_title = clean_local_pdf_stem(pdf_path.stem)
1138+
hints: dict[str, Any] = {"title": cleaned_title or raw_title}
1139+
if fitz is None:
1140+
return hints
1141+
1142+
metadata_title = ""
1143+
metadata_subject = ""
1144+
first_page_text = ""
1145+
try:
1146+
doc = fitz.open(pdf_path)
1147+
except Exception:
1148+
return hints
1149+
try:
1150+
metadata = doc.metadata or {}
1151+
metadata_title = normalize_whitespace(str(metadata.get("title", "")))
1152+
metadata_subject = normalize_whitespace(str(metadata.get("subject", "")))
1153+
if len(doc):
1154+
first_page_text = doc[0].get_text("text")
1155+
except Exception:
1156+
return hints
1157+
finally:
1158+
doc.close()
1159+
1160+
if metadata_title:
1161+
hints["title"] = metadata_title
1162+
else:
1163+
page_title = first_page_title_candidate(first_page_text)
1164+
if page_title:
1165+
hints["title"] = page_title
1166+
1167+
searchable = "\n".join(part for part in [metadata_subject, metadata_title, first_page_text] if part)
1168+
doi = extract_doi(searchable)
1169+
if doi:
1170+
hints["doi"] = doi
1171+
arxiv_id = extract_arxiv_id(searchable)
1172+
if arxiv_id:
1173+
hints["arxiv_id"] = arxiv_id
1174+
1175+
return hints
1176+
1177+
1178+
def choose_local_pdf_corrected_title(base: dict[str, Any], candidates: list[dict[str, Any]]) -> str:
1179+
current_title = normalize_whitespace(str(base.get("title", "")))
1180+
if not current_title or not is_probable_local_pdf_artifact_title(current_title):
1181+
return ""
1182+
titled_candidates = [candidate for candidate in candidates if normalize_whitespace(str(candidate.get("title", "")))]
1183+
best = choose_best_title_match(current_title, titled_candidates)
1184+
if not best:
1185+
return ""
1186+
candidate_title = normalize_whitespace(str(best.get("title", "")))
1187+
if not candidate_title:
1188+
return ""
1189+
if title_similarity(current_title, candidate_title) < 0.55:
1190+
return ""
1191+
if not (best.get("doi") or best.get("arxiv_id") or publication_quality_score(best) >= 2):
1192+
return ""
1193+
return candidate_title
1194+
1195+
10391196
def extract_caption_lines(pdf_text: str, kind: str) -> list[dict[str, str]]:
10401197
results: list[dict[str, str]] = []
10411198
seen = set()

0 commit comments

Comments
 (0)