Files
jobtrackingapp/docs/cv-integration-qa/benchmarks/generate.py
T

156 lines
6.3 KiB
Python

"""Generate a separate synthetic QA corpus; never replace the original import benchmark."""
import argparse
from hashlib import sha256
import json
from pathlib import Path
import re
from docx import Document
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from reportlab import __file__ as reportlab_file
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen import canvas
FONT = "CvQaSynthetic"
FONT_PATH = Path(reportlab_file).parent / "fonts/Vera.ttf"
def load_corpus(path):
cases = json.loads(path.read_text(encoding="utf-8"))
ids = [case["id"] for case in cases]
if len(ids) != len(set(ids)):
raise ValueError("Duplicate logical fixture ID.")
for case in cases:
if not re.fullmatch(r"[a-z0-9-]+", case["id"]) or case["split"] not in {"development", "frozen"}:
raise ValueError("Invalid fixture ID or split.")
if not isinstance(case["expected"], dict) or not case["blocks"]:
raise ValueError("Every case requires independent expectations and source blocks.")
return cases
def fragments(case):
"""Source units for literal text recovery, before interpretation or field assignment."""
result = []
for block in case["blocks"]:
if block.get("type") == "table":
result.extend(block["headers"])
result.extend(cell for row in block["rows"] for cell in row if cell)
elif block.get("type") != "page_break":
result.append(block["text"])
return result
def generate_case(case, output):
identifier = case["id"]
if not re.fullmatch(r"[a-z0-9-]+", identifier):
raise ValueError("Unsafe fixture ID.")
paths = {kind: output / f"{identifier}.{kind}" for kind in ("pdf", "docx")}
if any(path.exists() for path in paths.values()):
raise FileExistsError("Select fresh fixture output; historical bytes are immutable.")
output.mkdir(parents=True, exist_ok=True)
pdfmetrics.registerFont(TTFont(FONT, str(FONT_PATH)))
pdf = canvas.Canvas(str(paths["pdf"]), pagesize=(595, 842), invariant=1)
pdf.setTitle("Synthetic CV integration QA: " + identifier)
pdf.setAuthor("Jobjakt synthetic fixture")
word = Document()
y = 800
def page():
nonlocal y
pdf.showPage()
y = 800
def wrap(text, width, size=10):
lines, line = [], ""
for token in text.split():
proposed = (line + " " + token).strip()
if line and pdfmetrics.stringWidth(proposed, FONT, size) > width:
lines.append(line)
line = token
else:
line = proposed
return lines + [line]
for block in case["blocks"]:
kind = block.get("type", "paragraph")
if kind == "page_break":
word.add_page_break()
page()
continue
if kind == "table":
headers, rows = block["headers"], block["rows"]
if not headers or any(len(row) != len(headers) for row in rows):
raise ValueError("Table rows must match actual header columns.")
table = word.add_table(rows=1, cols=len(headers))
table.style = "Table Grid"
for cell, text in zip(table.rows[0].cells, headers):
cell.text = text
repeat_header = OxmlElement("w:tblHeader")
table.rows[0]._tr.get_or_add_trPr().append(repeat_header)
for row in rows:
for cell, text in zip(table.add_row().cells, row):
cell.text = text
width = 515 / len(headers)
for row in [headers] + rows:
wrapped = [wrap(cell, width - 10) for cell in row]
height = max(len(lines) for lines in wrapped) * 14 + 10
if y - height < 40:
page()
pdf.setFont(FONT, 10)
for column, lines in enumerate(wrapped):
x = 40 + column * width
pdf.rect(x, y - height, width, height)
for i, line in enumerate(lines):
pdf.drawString(x + 5, y - 15 - 14 * i, line)
y -= height
y -= 18
continue
if kind not in {"paragraph", "heading", "list"}:
raise ValueError("Unsupported fixture block kind.")
text = block["text"]
paragraph = word.add_paragraph(text)
if kind == "heading":
paragraph.style = word.styles["Heading 2"]
elif kind == "list":
paragraph.style = word.styles["List Bullet"]
# PDF lists use a visible explicit marker; DOCX lists use actual paragraph list styling.
size = 12 if kind == "heading" else 10
lines = wrap(("- " if kind == "list" else "") + text, 515, size)
height = len(lines) * 15 + (14 if kind == "heading" else 9)
if y - height < 40:
page()
pdf.setFont(FONT, size)
for line in lines:
pdf.drawString(40, y, line)
y -= 15
y -= 14 if kind == "heading" else 9
pdf.save()
word.save(paths["docx"])
return paths
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--corpus", type=Path, default=Path(__file__).with_name("corpus.json"))
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
if args.output.exists():
raise FileExistsError("Select a fresh directory; do not overwrite a frozen manifest.")
cases = load_corpus(args.corpus)
manifest = {"corpusSha256": sha256(args.corpus.read_bytes()).hexdigest(), "logicalCases": len(cases),
"generatorSha256": sha256(Path(__file__).read_bytes()).hexdigest(), "documents": []}
for case in cases:
for kind, path in generate_case(case, args.output).items():
manifest["documents"].append({"caseId": case["id"], "split": case["split"], "format": kind,
"file": path.name, "sha256": sha256(path.read_bytes()).hexdigest(),
"bytes": path.stat().st_size})
(args.output / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
print(json.dumps({"logicalCases": len(cases), "documents": len(manifest["documents"]), "output": str(args.output)}))
if __name__ == "__main__":
main()