"""Extract Andromeda grouped-responses PDF into structured SP JSON.""" from __future__ import annotations import json import re from pathlib import Path from pypdf import PdfReader PDF_DEFAULT = Path(r"E:\my report ai\Grouped_Responses_Restructured_DEDUPED_v2.pdf") OUT_DEFAULT = Path( r"E:\my report ai\Report-genius-ai\backend\standard_paragraphs\samples" r"\grouped_responses_full.json" ) PARENT_RE = re.compile( r"^(?P[A-J])\s*[\u2014\u2013\-]\s*(?P.+)$", re.M, ) CANON_LEAVES = {f"{letter}{n}" for letter in "ABCDEFGHIJ" for n in range(1, 20)} _SENTENCE_START = re.compile( r"^(The|We|There|From|This|It|However|Once|If|As|In|These|Their|Any|Also|" r"When|Whilst|While|Although|Our|You|I |A |An |Over |Where |Defective |" r"Because |Since |After |Before |During |With |Without |To |For |And |" r"But |Or |Not |No |Yes )\b", re.I, ) def _parent_for(parents: list[dict], pos: int) -> dict | None: cur = None for p in parents: if p["pos"] <= pos: cur = p else: break return cur def parse_responses(raw: str) -> list[dict]: lines = raw.splitlines() responses: list[dict] = [] cur_label: str | None = None cur_buf: list[str] = [] preamble = True def flush() -> None: nonlocal cur_label, cur_buf text = " ".join(x.strip() for x in cur_buf if x.strip()) text = re.sub(r"\s+", " ", text).strip() cur_buf = [] if not text or "no predefined responses" in text.lower(): cur_label = None return min_len = 15 if cur_label is not None else 40 if len(text) < min_len: cur_label = None return responses.append({"label": cur_label or "", "text": text}) cur_label = None for ln in lines: s = ln.strip() if not s: continue is_label = ( s.endswith(":") and 2 <= len(s) <= 120 and not s.startswith("||") and "Condition Rating" not in s and not _SENTENCE_START.match(s) ) if is_label and s[0].islower() and not s.lower().startswith( ("unfelt", "possibly") ): is_label = False if is_label: if preamble and cur_buf: cur_label = None flush() elif not preamble: flush() preamble = False cur_label = s[:-1].strip() cur_buf = [] else: cur_buf.append(s) flush() return responses def extract(pdf_path: Path) -> dict: reader = PdfReader(str(pdf_path)) full = "\n".join((p.extract_text() or "") for p in reader.pages) parents_raw = [] for m in PARENT_RE.finditer(full): parents_raw.append( { "pos": m.start(), "code": m.group("code"), "title": m.group("title").strip().split("\n")[0][:120], } ) seen_p: dict[str, dict] = {} for p in parents_raw: seen_p.setdefault(p["code"], p) parents = [seen_p[c] for c in sorted(seen_p)] lines = full.splitlines() offsets: list[int] = [] o = 0 for ln in lines: offsets.append(o) o += len(ln) + 1 field_blocks: list[dict] = [] for i, ln in enumerate(lines): m = re.match(r"^Field ref:\s*(\d+)\s*$", ln.strip()) if not m: continue ref = m.group(1) title = "" for j in range(i - 1, max(-1, i - 6), -1): t = lines[j].strip() if not t: continue if re.match(r"^[A-J]\s*[\u2014\u2013\-]", t): break title = t break leaf = None leaf_title = title mleaf = re.match(r"^([A-J]\d{1,2})\s+(.+)$", title) if mleaf and mleaf.group(1) in CANON_LEAVES: leaf = mleaf.group(1) leaf_title = mleaf.group(2).strip() field_blocks.append( { "pos": offsets[i], "field_ref": ref, "raw_title": title, "subsection_id": leaf, "subsection_name": leaf_title if leaf else title, } ) for i, fb in enumerate(field_blocks): end = field_blocks[i + 1]["pos"] if i + 1 < len(field_blocks) else len(full) raw = full[fb["pos"] : end] fb["raw"] = re.sub(r"^Field ref:\s*\d+\s*\n?", "", raw, count=1) by_parent = { p["code"]: { "section_id": p["code"], "section_name": p["title"], "subsections": [], } for p in parents } empty_fields = 0 total_sps = 0 for fb in field_blocks: par = _parent_for(parents, fb["pos"]) if not par: continue resps = parse_responses(fb["raw"]) if not resps: if "no predefined responses" in fb["raw"].lower(): empty_fields += 1 else: body = re.sub(r"\s+", " ", fb["raw"]).strip() if ( body and "no predefined responses" not in body.lower() and len(body) >= 15 ): resps = [{"label": "", "text": body}] sid = fb["subsection_id"] or f"field_{fb['field_ref']}" sname = fb["subsection_name"] or fb["raw_title"] or sid total_sps += len(resps) by_parent[par["code"]]["subsections"].append( { "section_id": par["code"], "subsection_id": sid, "subsection_name": sname, "field_ref": fb["field_ref"], "has_rics_code": bool(fb["subsection_id"]), "standard_paragraphs": [ {"label": r["label"], "text": r["text"]} for r in resps ], } ) return { "source": pdf_path.name, "page_count": len(reader.pages), "stats": { "sections": len(by_parent), "fields": len(field_blocks), "empty_fields": empty_fields, "standard_paragraphs": total_sps, }, "sections": [by_parent[c] for c in sorted(by_parent.keys())], } def main() -> None: payload = extract(PDF_DEFAULT) OUT_DEFAULT.parent.mkdir(parents=True, exist_ok=True) OUT_DEFAULT.write_text( json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8" ) print("wrote", OUT_DEFAULT) print("size_mb", round(OUT_DEFAULT.stat().st_size / 1e6, 2)) print("stats", payload["stats"]) for sec in payload["sections"]: nsp = sum(len(s["standard_paragraphs"]) for s in sec["subsections"]) print( f" {sec['section_id']}: {len(sec['subsections'])} fields, {nsp} SPs" ) if __name__ == "__main__": main()