| """ |
| Convierte el PDF del reglamento de Riftbound a Markdown estructurado. |
| |
| Detecta headers por tamaño de fuente relativo al body text: |
| - > 2.0x body → H1 (título principal) |
| - > 1.4x body → H2 (sección) |
| - > 1.2x body → H3 (subsección) |
| - else → párrafo |
| """ |
| import re |
|
|
| import pymupdf |
| from pathlib import Path |
| from statistics import mode |
|
|
| _RULE_BOUNDARY = re.compile(r"^\d{3,}\.") |
|
|
|
|
| def _extract_spans(doc: pymupdf.Document) -> list[dict]: |
| spans = [] |
| for page in doc: |
| blocks = page.get_text("dict")["blocks"] |
| for block in blocks: |
| if block["type"] != 0: |
| continue |
| for line in block["lines"]: |
| for span in line["spans"]: |
| text = span["text"].strip() |
| if text: |
| spans.append({"text": text, "size": round(span["size"], 1)}) |
| return spans |
|
|
|
|
| def _detect_body_size(spans: list[dict]) -> float: |
| sizes = [s["size"] for s in spans] |
| try: |
| return mode(sizes) |
| except Exception: |
| return min(sizes) |
|
|
|
|
| def _classify(size: float, body_size: float) -> str: |
| ratio = size / body_size |
| if ratio > 2.0: |
| return "h1" |
| if ratio > 1.4: |
| return "h2" |
| if ratio > 1.2: |
| return "h3" |
| return "body" |
|
|
|
|
| def _spans_to_markdown(spans: list[dict], body_size: float) -> str: |
| lines: list[str] = [] |
| current_body: list[str] = [] |
|
|
| def flush_body(): |
| if current_body: |
| lines.append(" ".join(current_body)) |
| lines.append("") |
| current_body.clear() |
|
|
| for span in spans: |
| kind = _classify(span["size"], body_size) |
| if kind == "body": |
| if current_body and _RULE_BOUNDARY.match(span["text"]): |
| flush_body() |
| current_body.append(span["text"]) |
| else: |
| flush_body() |
| prefix = {"h1": "#", "h2": "##", "h3": "###"}[kind] |
| lines.append(f"{prefix} {span['text']}") |
| lines.append("") |
|
|
| flush_body() |
|
|
| |
| result: list[str] = [] |
| prev_blank = False |
| for line in lines: |
| is_blank = line.strip() == "" |
| if is_blank and prev_blank: |
| continue |
| result.append(line) |
| prev_blank = is_blank |
|
|
| return "\n".join(result).strip() |
|
|
|
|
| def parse_rulebook(pdf_path: str | Path) -> str: |
| doc = pymupdf.open(str(pdf_path)) |
| spans = _extract_spans(doc) |
| body_size = _detect_body_size(spans) |
| return _spans_to_markdown(spans, body_size) |
|
|
|
|
| if __name__ == "__main__": |
| import sys |
|
|
| pdf = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("data/raw/rulebook.pdf") |
| output = Path("data/processed/rulebook.md") |
| output.parent.mkdir(parents=True, exist_ok=True) |
| result = parse_rulebook(pdf) |
| output.write_text(result, encoding="utf-8") |
| print(f"Generado: {output} ({len(result)} chars)") |
|
|