feat(cv): extend templates and extraction

This commit is contained in:
cesnimda
2026-08-27 15:27:48 +02:00
parent cc76f86482
commit ccd0af908c
15 changed files with 586 additions and 54 deletions
+32 -4
View File
@@ -1045,8 +1045,20 @@ async def purge_cache():
def _normalize_text(value: str) -> str:
value = value.replace("\x00", " ")
return re.sub(r"\s+", " ", value).strip()
"""Normalize extraction noise without destroying section, bullet, or table boundaries."""
value = value.replace("\x00", " ").replace("\r\n", "\n").replace("\r", "\n")
lines = []
blank = False
for raw_line in value.split("\n"):
line = re.sub(r"[\t\f\v]+", " ", raw_line).strip()
if not line:
if lines and not blank:
lines.append("")
blank = True
continue
lines.append(line)
blank = False
return "\n".join(lines).strip()
def _ocr_image(image: Image.Image) -> str:
@@ -1063,7 +1075,10 @@ def _extract_pdf_text(data: bytes) -> tuple[str, bool, int]:
reader = PdfReader(io.BytesIO(data))
page_count = len(reader.pages)
for page in reader.pages:
extracted_pages.append(page.extract_text() or "")
try:
extracted_pages.append(page.extract_text(extraction_mode="layout") or "")
except TypeError:
extracted_pages.append(page.extract_text() or "")
except Exception:
extracted_pages = []
@@ -1084,7 +1099,20 @@ def _extract_pdf_text(data: bytes) -> tuple[str, bool, int]:
def _extract_docx_text(data: bytes) -> str:
document = Document(io.BytesIO(data))
parts = [p.text.strip() for p in document.paragraphs if p.text and p.text.strip()]
parts = []
blocks = document.iter_inner_content() if hasattr(document, "iter_inner_content") else document.paragraphs
for block in blocks:
if hasattr(block, "rows"):
for row in block.rows:
cells = [cell.text.strip() for cell in row.cells if cell.text and cell.text.strip()]
if cells:
parts.append(" | ".join(cells))
elif getattr(block, "text", "").strip():
text = block.text.strip()
style = (getattr(getattr(block, "style", None), "name", "") or "").lower()
if "role" in style and parts:
parts.append("")
parts.append(f"- {text}" if "bullet" in style else text)
return _normalize_text("\n".join(parts))
+28
View File
@@ -1,4 +1,5 @@
import importlib
import io
import json
import sys
from pathlib import Path
@@ -522,6 +523,33 @@ def test_extract_text_rejects_oversized_upload_before_parsing(monkeypatch):
assert "too large" in response.json()["detail"].lower()
def test_extraction_normalization_preserves_sections_and_bullets(monkeypatch):
module = load_app_module(monkeypatch)
normalized = module._normalize_text("PROFILE\n\nEngineer\n• Built APIs\n• Shipped services")
assert normalized.splitlines() == ["PROFILE", "", "Engineer", "• Built APIs", "• Shipped services"]
def test_docx_extraction_preserves_paragraph_and_table_boundaries(monkeypatch):
module = load_app_module(monkeypatch)
document = module.Document()
document.add_heading("Technical Skills", level=1)
table = document.add_table(rows=2, cols=2)
table.cell(0, 0).text = "Backend"
table.cell(0, 1).text = "C#, .NET"
table.cell(1, 0).text = "DevOps"
table.cell(1, 1).text = "Docker, Linux"
payload = io.BytesIO()
document.save(payload)
extracted = module._extract_docx_text(payload.getvalue())
assert "Technical Skills" in extracted
assert "Backend | C#, .NET" in extracted
assert "DevOps | Docker, Linux" in extracted
def test_cache_purge_requires_service_token_and_clears_content(monkeypatch):
module = load_app_module(monkeypatch, service_token="s3cret")
module.cache["synthetic-key"] = "synthetic-summary"