paper_reader_artifact.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. """阅读产物管理 —— 导读摘要、术语表等 AI 生成内容的持久化."""
  2. from __future__ import annotations
  3. import json
  4. import os
  5. import time
  6. from typing import Any
  7. from .paper_reader_structure import parse_pdf_merged_text_to_json
  8. ARTIFACT_VERSION = 1
  9. ARTIFACT_DIR = "reader_artifacts"
  10. def reader_artifact_path(db_path: str, paper_id: int) -> str:
  11. data_root = os.path.dirname(os.path.abspath(db_path or "."))
  12. return os.path.join(data_root, ARTIFACT_DIR, f"paper_{int(paper_id)}.json")
  13. def _pdf_stat(pdf_abspath: str | None) -> dict[str, int]:
  14. if not pdf_abspath or not os.path.isfile(pdf_abspath):
  15. return {}
  16. try:
  17. st = os.stat(pdf_abspath)
  18. return {"mtime": int(st.st_mtime), "size": int(st.st_size)}
  19. except Exception:
  20. return {}
  21. def _paper_meta(paper: Any) -> dict[str, Any]:
  22. authors = [
  23. (getattr(a, "name", None) or "").strip()
  24. for a in (getattr(paper, "authors", None) or [])
  25. if (getattr(a, "name", None) or "").strip()
  26. ]
  27. return {
  28. "id": getattr(paper, "id", None),
  29. "title": (getattr(paper, "title", None) or "").strip(),
  30. "authors": authors,
  31. "year": getattr(paper, "year", None),
  32. "venue": (getattr(paper, "journal", None) or "").strip(),
  33. "doi": (getattr(paper, "doi", None) or "").strip(),
  34. "arxiv_id": (getattr(paper, "arxiv_id", None) or "").strip(),
  35. "abstract": (getattr(paper, "abstract", None) or "").strip(),
  36. "keywords": [str(x) for x in (getattr(paper, "keywords", None) or [])[:32] if str(x).strip()],
  37. }
  38. def load_reader_artifact(db_path: str, paper_id: int, pdf_abspath: str | None = None) -> dict[str, Any] | None:
  39. path = reader_artifact_path(db_path, paper_id)
  40. if not os.path.isfile(path):
  41. return None
  42. try:
  43. with open(path, "r", encoding="utf-8") as f:
  44. obj = json.load(f)
  45. except Exception:
  46. return None
  47. if int(obj.get("version") or 0) != ARTIFACT_VERSION:
  48. return None
  49. current = _pdf_stat(pdf_abspath)
  50. saved = obj.get("pdf") or {}
  51. if current and (int(saved.get("mtime") or 0) != current["mtime"] or int(saved.get("size") or 0) != current["size"]):
  52. return None
  53. return obj
  54. def build_reader_artifact(
  55. db_path: str,
  56. paper_id: int,
  57. paper: Any,
  58. pdf_text: str,
  59. pdf_abspath: str | None = None,
  60. ) -> dict[str, Any] | None:
  61. text = (pdf_text or "").strip()
  62. if len(text) < 200:
  63. return None
  64. parsed = parse_pdf_merged_text_to_json(text, max_chapter_chars=9000, max_chapters=40, max_ref_entries=100)
  65. artifact = {
  66. "version": ARTIFACT_VERSION,
  67. "generated_at": int(time.time()),
  68. "paper": _paper_meta(paper),
  69. "pdf": _pdf_stat(pdf_abspath),
  70. "structure": parsed,
  71. }
  72. path = reader_artifact_path(db_path, paper_id)
  73. try:
  74. os.makedirs(os.path.dirname(path), exist_ok=True)
  75. tmp = path + ".tmp"
  76. with open(tmp, "w", encoding="utf-8") as f:
  77. json.dump(artifact, f, ensure_ascii=False, indent=2)
  78. os.replace(tmp, path)
  79. except Exception:
  80. return None
  81. return artifact
  82. def ensure_reader_artifact(
  83. db_path: str,
  84. paper_id: int,
  85. paper: Any,
  86. pdf_text: str,
  87. pdf_abspath: str | None = None,
  88. ) -> dict[str, Any] | None:
  89. cached = load_reader_artifact(db_path, paper_id, pdf_abspath)
  90. if cached:
  91. return cached
  92. return build_reader_artifact(db_path, paper_id, paper, pdf_text, pdf_abspath)
  93. def format_reader_artifact_block(artifact: dict[str, Any] | None, *, max_chars: int = 9000) -> str:
  94. if not artifact:
  95. return ""
  96. paper = artifact.get("paper") or {}
  97. structure = artifact.get("structure") or {}
  98. chapters = structure.get("chapters") or []
  99. refs = (structure.get("references") or {}).get("entries") or []
  100. lines = [
  101. "【结构化阅读档案(由 PDF 自动解析生成;阅读助手优先依据此档案回答)】",
  102. f"标题:{paper.get('title') or '—'}",
  103. f"摘要:{paper.get('abstract') or '(无摘要)'}",
  104. "章节:",
  105. ]
  106. for i, ch in enumerate(chapters[:14], start=1):
  107. heading = (ch.get("heading") or f"Section {i}").strip()
  108. text = " ".join(str(ch.get("text") or "").split())
  109. snippet = text[:900]
  110. tail = "..." if len(text) > 900 else ""
  111. lines.append(f"{i}. {heading}\n{snippet}{tail}")
  112. if refs:
  113. lines.append("参考文献条目:")
  114. for i, ref in enumerate(refs[:30], start=1):
  115. lines.append(f"[{i}] {str(ref)[:420]}")
  116. block = "\n".join(lines).strip()
  117. return block[:max_chars]