import csv import hashlib from collections import Counter from datetime import datetime, timezone, timedelta from pathlib import Path import fitz TZ = timezone(timedelta(hours=8)) NOW = datetime.now(TZ).strftime("%Y-%m-%dT%H:%M:%S+08:00") CASE_ID = "ANA-YS-INDUSTRY-001" RUN_ID = "RUN-ANA-YS-PDF-TABLE-RECOVERY-019" def read_csv(path): with path.open("r", encoding="utf-8-sig", newline="") as f: return list(csv.DictReader(f)) def write_csv(path, rows): path.parent.mkdir(parents=True, exist_ok=True) with path.open("w", encoding="utf-8-sig", newline="") as f: writer = csv.DictWriter(f, fieldnames=list(rows[0].keys())) writer.writeheader() writer.writerows(rows) def main(): queue_path = next(Path("ana-data/cases").glob("*/evidence/key_fact_pdf_table_review_queue_pass004.csv")) case_root = queue_path.parents[1] industry_name = queue_path.parts[2] case_dir = case_root / CASE_ID outputs_dir = case_dir / "outputs" evidence_dir = case_root / "evidence" case_evidence_dir = case_dir / "evidence" img_dir = Path("ana-data/img") / industry_name / CASE_ID / RUN_ID for path in (outputs_dir, evidence_dir, case_evidence_dir, img_dir): path.mkdir(parents=True, exist_ok=True) queue_rows = read_csv(queue_path) recovery_rows = [] for row in queue_rows: rec = dict(row) rec["recovery_run_id"] = RUN_ID rec["recovery_status"] = "" rec["recovery_image_path"] = "" rec["recovery_error"] = "" rec["manual_review_required"] = "YES" rec["formal_upgrade_status"] = "DRAFT_PAGE_IMAGE_READY_NEEDS_MANUAL_TABLE_REVIEW" rec["updated_at"] = NOW if row.get("pdf_header_ok") == "YES" and row.get("page_known") == "YES": raw = Path(row["raw_file_path"]) try: page_no = int(row["precheck_page_no"]) doc = fitz.open(str(raw)) idx = max(0, min(page_no - 1, len(doc) - 1)) page = doc.load_page(idx) pix = page.get_pixmap(matrix=fitz.Matrix(1.8, 1.8), alpha=False) out = img_dir / f"页图_{row['pdf_table_review_id']}_第{page_no}页.png" pix.save(str(out)) rec["recovery_status"] = "PDF_PAGE_IMAGE_RENDERED" rec["recovery_image_path"] = out.as_posix() rec["rendered_page_index"] = str(idx) rec["pdf_page_count"] = str(len(doc)) rec["image_sha256"] = hashlib.sha256(out.read_bytes()).hexdigest() rec["image_bytes"] = str(out.stat().st_size) except Exception as exc: rec["recovery_status"] = "RENDER_FAILED" rec["recovery_error"] = repr(exc) rec["formal_upgrade_status"] = "HELD_BY_RENDER_ERROR" elif row.get("pdf_header_ok") != "YES": rec["recovery_status"] = "HELD_BY_NOT_PDF_HEADER" rec["formal_upgrade_status"] = "HELD_BY_SOURCE_FILE_TYPE" else: rec["recovery_status"] = "HELD_BY_PAGE_UNKNOWN" rec["formal_upgrade_status"] = "HELD_BY_PAGE_UNKNOWN" recovery_rows.append(rec) recovery_path = evidence_dir / "原页表格复核恢复表_pass019.csv" write_csv(recovery_path, recovery_rows) manifest_path = case_evidence_dir / "原页表格复核恢复manifest_pass019.csv" manifest_rows = [{ "case_id": CASE_ID, "batch_id": "BATCH-003", "run_id": RUN_ID, "artifact_type": "pdf_page_table_recovery", "artifact_path": recovery_path.as_posix(), "row_count": str(len(recovery_rows)), "rendered_count": str(sum(1 for r in recovery_rows if r["recovery_status"] == "PDF_PAGE_IMAGE_RENDERED")), "held_by_page_unknown": str(sum(1 for r in recovery_rows if r["recovery_status"] == "HELD_BY_PAGE_UNKNOWN")), "held_by_not_pdf_header": str(sum(1 for r in recovery_rows if r["recovery_status"] == "HELD_BY_NOT_PDF_HEADER")), "image_dir": img_dir.as_posix(), "review_status": "DRAFT_FOR_REVIEW", "created_at": NOW, }] write_csv(manifest_path, manifest_rows) cross_path = case_root / "evidence/key_fact_external_crosscheck_pass006.csv" cross_rows = read_csv(cross_path) image_by_evidence_card = {r.get("evidence_card_id"): r for r in recovery_rows if r.get("evidence_card_id")} price_rows = [r for r in cross_rows if r.get("evidence_kind") == "PRICE_OR_MARKET_DATA"] status_counter = Counter(r.get("cross_check_status") for r in price_rows) metal_counter = Counter(r.get("metal_tags") or "未标注" for r in price_rows) lines = [ "# 原研报价格库存线索表", "", "状态:DRAFT_FOR_REVIEW", f"生成时间:{NOW}", f"关联案例:{CASE_ID}", f"关联运行:{RUN_ID}", "", "## 使用边界", "", "1. 本文件优先整理已提供 PDF/converted text 中出现的价格、库存、供需、成本和市场变量线索,用于回应“价格库存是否已在研报中提供”。", "2. 本文件不是正式价格数据库,也不是正式指标证据;正式输出前仍需核对报告日期、原始表格行、单位、口径和必要外部来源。", "3. 已生成页图的记录只表示 PDF 原页可查看,不等于表头、单位、日期和数值一致性已经人工通过。", "", "## 汇总", "", f"- 价格/库存/市场数据候选记录:{len(price_rows)} 条。", f"- 外部来源已有候选映射:{status_counter.get('PRICE_INVENTORY_SOURCE_MATCHED', 0)} 条。", f"- 仍需价格库存外部来源或日期/表格行补核:{status_counter.get('NEEDS_PRICE_INVENTORY_EXTERNAL_SOURCE', 0)} 条。", f"- 本轮 PDF 原页页图已恢复:{sum(1 for r in recovery_rows if r['recovery_status'] == 'PDF_PAGE_IMAGE_RENDERED')} 张。", "", "### 金属分布", "", "| 金属标签 | 线索条数 |", "|---|---:|", ] for metal, count in metal_counter.most_common(): lines.append(f"| {metal} | {count} |") lines.extend([ "", "### 证据状态分布", "", "| 状态 | 条数 |", "|---|---:|", ]) for status, count in status_counter.most_common(): lines.append(f"| {status or '未标注'} | {count} |") lines.extend([ "", "## 明细", "", "| 序号 | doc_id | 金属 | 主题 | 页码/位置 | PDF页图状态 | 价格库存线索 | 数值/单位候选 | 日期候选 | 后续动作 |", "|---:|---|---|---|---|---|---|---|---|---|", ]) for index, row in enumerate(price_rows, 1): rec = image_by_evidence_card.get(row.get("evidence_card_id"), {}) img_status = rec.get("recovery_status") or "未进入PASS-004页图队列" if rec.get("recovery_image_path"): img_status = f"[已生成页图]({rec['recovery_image_path']})" indicators = (row.get("matched_price_inventory_indicators") or "").replace("|", "
") values = (row.get("value_unit_candidates") or "").replace("|", "
") dates = (row.get("date_candidates") or "").replace("|", "
") loc = f"第{row.get('precheck_page_no')}页 / {row.get('precheck_location')}" action = row.get("next_action") or "" lines.append( f"| {index} | {row.get('doc_id')} | {row.get('metal_tags')} | {row.get('theme_tags')} | " f"{loc} | {img_status} | {indicators} | {values} | {dates} | {action} |" ) price_doc_path = outputs_dir / "原研报价格库存线索表.md" price_doc_path.write_text("\n".join(lines) + "\n", encoding="utf-8") summary_path = outputs_dir / "原页表格复核恢复摘要.md" summary_lines = [ "# 原页表格复核恢复摘要", "", "状态:DRAFT_FOR_REVIEW", f"生成时间:{NOW}", f"关联案例:{CASE_ID}", f"关联运行:{RUN_ID}", "", "## 恢复原因", "", "用户要求重新尝试安装 PyMuPDF。分析员确认此前失败原因是本机代理环境变量指向 `127.0.0.1:1080` 但端口无服务;临时清除代理变量后,PyMuPDF 安装和 `fitz` 导入成功。该轮属于“环境恢复且可一次性解决阻断”的例外。", "", "## 执行结果", "", f"- PASS-004 输入表格候选:{len(recovery_rows)} 条。", f"- PDF 头正常且页码已知,已生成页图:{sum(1 for r in recovery_rows if r['recovery_status'] == 'PDF_PAGE_IMAGE_RENDERED')} 条。", f"- 页码未知仍保持 HELD:{sum(1 for r in recovery_rows if r['recovery_status'] == 'HELD_BY_PAGE_UNKNOWN')} 条。", f"- 文件头不是 PDF 仍保持 HELD:{sum(1 for r in recovery_rows if r['recovery_status'] == 'HELD_BY_NOT_PDF_HEADER')} 条。", f"- 页图目录:`{img_dir.as_posix()}`。", "", "## 输出入口", "", f"1. 恢复表:`{recovery_path.as_posix()}`", f"2. manifest:`{manifest_path.as_posix()}`", f"3. 价格库存线索表:`{price_doc_path.as_posix()}`", "", "## 仍保留的边界", "", "1. 页图生成不等于正式表格证据通过;表头、单位、日期、来源和数值一致性仍需人工核验。", "2. 页码未知和非 PDF 头文件不在本轮扩大处理范围内。", "3. 本轮不输出正式行业结论、正式公司结论、正式指标、交易指令或收益承诺。", ] summary_path.write_text("\n".join(summary_lines) + "\n", encoding="utf-8") print(f"recovery_path={recovery_path}") print(f"manifest_path={manifest_path}") print(f"summary_path={summary_path}") print(f"price_doc_path={price_doc_path}") print(f"image_dir={img_dir}") print(f"rendered={sum(1 for r in recovery_rows if r['recovery_status'] == 'PDF_PAGE_IMAGE_RENDERED')}") print(f"held_page_unknown={sum(1 for r in recovery_rows if r['recovery_status'] == 'HELD_BY_PAGE_UNKNOWN')}") print(f"held_not_pdf={sum(1 for r in recovery_rows if r['recovery_status'] == 'HELD_BY_NOT_PDF_HEADER')}") print(f"price_rows={len(price_rows)}") if __name__ == "__main__": main()