| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136 |
- """阅读产物管理 —— 导读摘要、术语表等 AI 生成内容的持久化."""
- from __future__ import annotations
- import json
- import os
- import time
- from typing import Any
- from .paper_reader_structure import parse_pdf_merged_text_to_json
- ARTIFACT_VERSION = 1
- ARTIFACT_DIR = "reader_artifacts"
- def reader_artifact_path(db_path: str, paper_id: int) -> str:
- data_root = os.path.dirname(os.path.abspath(db_path or "."))
- return os.path.join(data_root, ARTIFACT_DIR, f"paper_{int(paper_id)}.json")
- def _pdf_stat(pdf_abspath: str | None) -> dict[str, int]:
- if not pdf_abspath or not os.path.isfile(pdf_abspath):
- return {}
- try:
- st = os.stat(pdf_abspath)
- return {"mtime": int(st.st_mtime), "size": int(st.st_size)}
- except Exception:
- return {}
- def _paper_meta(paper: Any) -> dict[str, Any]:
- authors = [
- (getattr(a, "name", None) or "").strip()
- for a in (getattr(paper, "authors", None) or [])
- if (getattr(a, "name", None) or "").strip()
- ]
- return {
- "id": getattr(paper, "id", None),
- "title": (getattr(paper, "title", None) or "").strip(),
- "authors": authors,
- "year": getattr(paper, "year", None),
- "venue": (getattr(paper, "journal", None) or "").strip(),
- "doi": (getattr(paper, "doi", None) or "").strip(),
- "arxiv_id": (getattr(paper, "arxiv_id", None) or "").strip(),
- "abstract": (getattr(paper, "abstract", None) or "").strip(),
- "keywords": [str(x) for x in (getattr(paper, "keywords", None) or [])[:32] if str(x).strip()],
- }
- def load_reader_artifact(db_path: str, paper_id: int, pdf_abspath: str | None = None) -> dict[str, Any] | None:
- path = reader_artifact_path(db_path, paper_id)
- if not os.path.isfile(path):
- return None
- try:
- with open(path, "r", encoding="utf-8") as f:
- obj = json.load(f)
- except Exception:
- return None
- if int(obj.get("version") or 0) != ARTIFACT_VERSION:
- return None
- current = _pdf_stat(pdf_abspath)
- saved = obj.get("pdf") or {}
- if current and (int(saved.get("mtime") or 0) != current["mtime"] or int(saved.get("size") or 0) != current["size"]):
- return None
- return obj
- def build_reader_artifact(
- db_path: str,
- paper_id: int,
- paper: Any,
- pdf_text: str,
- pdf_abspath: str | None = None,
- ) -> dict[str, Any] | None:
- text = (pdf_text or "").strip()
- if len(text) < 200:
- return None
- parsed = parse_pdf_merged_text_to_json(text, max_chapter_chars=9000, max_chapters=40, max_ref_entries=100)
- artifact = {
- "version": ARTIFACT_VERSION,
- "generated_at": int(time.time()),
- "paper": _paper_meta(paper),
- "pdf": _pdf_stat(pdf_abspath),
- "structure": parsed,
- }
- path = reader_artifact_path(db_path, paper_id)
- try:
- os.makedirs(os.path.dirname(path), exist_ok=True)
- tmp = path + ".tmp"
- with open(tmp, "w", encoding="utf-8") as f:
- json.dump(artifact, f, ensure_ascii=False, indent=2)
- os.replace(tmp, path)
- except Exception:
- return None
- return artifact
- def ensure_reader_artifact(
- db_path: str,
- paper_id: int,
- paper: Any,
- pdf_text: str,
- pdf_abspath: str | None = None,
- ) -> dict[str, Any] | None:
- cached = load_reader_artifact(db_path, paper_id, pdf_abspath)
- if cached:
- return cached
- return build_reader_artifact(db_path, paper_id, paper, pdf_text, pdf_abspath)
- def format_reader_artifact_block(artifact: dict[str, Any] | None, *, max_chars: int = 9000) -> str:
- if not artifact:
- return ""
- paper = artifact.get("paper") or {}
- structure = artifact.get("structure") or {}
- chapters = structure.get("chapters") or []
- refs = (structure.get("references") or {}).get("entries") or []
- lines = [
- "【结构化阅读档案(由 PDF 自动解析生成;阅读助手优先依据此档案回答)】",
- f"标题:{paper.get('title') or '—'}",
- f"摘要:{paper.get('abstract') or '(无摘要)'}",
- "章节:",
- ]
- for i, ch in enumerate(chapters[:14], start=1):
- heading = (ch.get("heading") or f"Section {i}").strip()
- text = " ".join(str(ch.get("text") or "").split())
- snippet = text[:900]
- tail = "..." if len(text) > 900 else ""
- lines.append(f"{i}. {heading}\n{snippet}{tail}")
- if refs:
- lines.append("参考文献条目:")
- for i, ref in enumerate(refs[:30], start=1):
- lines.append(f"[{i}] {str(ref)[:420]}")
- block = "\n".join(lines).strip()
- return block[:max_chars]
|