update
This commit is contained in:
@@ -370,27 +370,60 @@ def _pick_conversion(md: str | None, plain: str | None) -> tuple[str, str] | Non
|
||||
return plain, "pdftotext"
|
||||
|
||||
|
||||
# Small-Caps-/Math-Italic-Artefakte der PDF-Extraktion: LaTeX-\textsc/Kerning liest sich als
|
||||
# Binnen-Leerzeichen („H ITTING S ET", „N P") und erzeugt in der QA Phantom-Konzepte, die kein
|
||||
# Block je ankern kann. Regel A: Einzelgroßbuchstabe + GROSSLAUF(≥2) mergen — außer der Lauf
|
||||
# wird klein fortgesetzt („L NP-vollständig": L ist Variable, NP gehört zum Kompositum).
|
||||
_PDF_CAPS_SPLIT = re.compile(r"(?<![A-Za-zÄÖÜäöüß])([A-ZÄÖÜ]) ([A-ZÄÖÜ]{2,})(?!-?[a-zäöüß])")
|
||||
# Regel B: Einzelbuchstaben-PAAR („N P") — nur mergen, wenn das Ergebnis im selben Dokument
|
||||
# mehrfach ungespalten vorkommt (Frequenz-Beleg statt Domänenliste: „NP" ja, „C Y" nein).
|
||||
_PDF_LETTER_PAIR = re.compile(r"(?<![A-Za-zÄÖÜäöüß])([A-ZÄÖÜ]) ([A-ZÄÖÜ])(?![A-Za-zÄÖÜäöüß-])")
|
||||
_PDF_NORM_VERSION = 1 # bump → nächster Lauf re-konvertiert alle PDFs (Marker .pdf-txt-norm)
|
||||
|
||||
|
||||
def _entzerre_pdf_woerter(text: str) -> str:
|
||||
"""Gespaltene Wörter aus der PDF-Konvertierung zusammenfügen (nur Merges, kein Umbau).
|
||||
Bewusste Restlücken: Zeilenumbruch-Splits und Mehrfach-Splits ohne Großlauf — dafür
|
||||
sind Lücken-Research + Freispruch das Netz."""
|
||||
prev = None
|
||||
while prev != text: # Ketten: „V ERTEX C OVER" braucht zwei Durchgänge pro Segment
|
||||
prev = text
|
||||
text = _PDF_CAPS_SPLIT.sub(r"\1\2", text)
|
||||
|
||||
def _belegt(m: re.Match) -> str:
|
||||
merged = m.group(1) + m.group(2)
|
||||
n = len(re.findall(rf"(?<![A-Za-zÄÖÜäöüß]){merged}(?![A-Za-zÄÖÜäöüß])", text))
|
||||
return merged if n >= 3 else m.group(0)
|
||||
|
||||
return _PDF_LETTER_PAIR.sub(_belegt, text)
|
||||
|
||||
|
||||
def _convert_pdfs(project: Path) -> None:
|
||||
"""Convert PDFs in the project to .txt — agents read text instead of page images.
|
||||
|
||||
Called before every project generation; converts only if the .txt is missing or
|
||||
older than the PDF. Both converters run; the fidelity guard picks the better result
|
||||
per file. Neither available → hard error instead of an unreliable direct-read mode
|
||||
(MiniMax image limit, vision cost)."""
|
||||
(MiniMax image limit, vision cost). Ein Versions-Marker (.pdf-txt-norm) erzwingt nach
|
||||
Änderungen an der Wort-Entzerrung einmalig die Re-Konvertierung trotz mtime-Cache."""
|
||||
pdfs = list(project.rglob("*.pdf"))
|
||||
if not pdfs:
|
||||
return
|
||||
marker = project / ".pdf-txt-norm"
|
||||
aktuell = marker.exists() and marker.read_text(encoding="utf-8").strip() == str(_PDF_NORM_VERSION)
|
||||
for pdf in pdfs:
|
||||
txt = pdf.with_suffix(".txt")
|
||||
if txt.exists() and txt.stat().st_mtime >= pdf.stat().st_mtime:
|
||||
if aktuell and txt.exists() and txt.stat().st_mtime >= pdf.stat().st_mtime:
|
||||
continue
|
||||
picked = _pick_conversion(_pdf_markdown(pdf), _pdf_plaintext(pdf))
|
||||
if picked is None:
|
||||
raise RuntimeError(f"PDF conversion failed ({pdf.name}): weder pymupdf4llm noch "
|
||||
"pdftotext verfügbar/erfolgreich (pip install pymupdf4llm oder poppler-utils)")
|
||||
text, tool = picked
|
||||
txt.write_text(text, encoding="utf-8")
|
||||
txt.write_text(_entzerre_pdf_woerter(text), encoding="utf-8")
|
||||
_log(project.name, f"PDF konvertiert ({tool}): {pdf.name} → {txt.name}")
|
||||
if not aktuell:
|
||||
marker.write_text(str(_PDF_NORM_VERSION), encoding="utf-8")
|
||||
|
||||
|
||||
_SOURCE_TEMPLATE = {"projekt": "Blocks-Source-Projekt", "uni": "Blocks-Source-Uni", "link": "Blocks-Source-Link"}
|
||||
@@ -1350,28 +1383,6 @@ def _completion_schema(data, n_umbrellas: int, ids: set[int]):
|
||||
return out
|
||||
|
||||
|
||||
# Deterministic backstop to the grouping judge's TEST 1 (type gate): an umbrella may bundle ONLY
|
||||
# constituent sub-definitions of ONE definition. If a member title carries a standalone-unit signal
|
||||
# (a named algorithm / problem / reduction / theorem), the umbrella is dissolved — those stay their own
|
||||
# blocks. Kept narrow so real definition-parts (Konfiguration, Übergangsfunktion δ, Literale, Makespan,
|
||||
# m Maschinen) never match; checked against the aak over-merge (member „Greedy-Algorithmus GA" hits).
|
||||
# Suffix-anchored head nouns (German compounds are head-final: „Approximations+algorithmus" has NO word
|
||||
# boundary before „algorithmus", so \bAlgorithmus\b misses it → the MAX-SAT over-merge). \w* absorbs the
|
||||
# modifier; the head noun stays the discriminator. FP-safe: no real TM/KNF definition-part ends in these
|
||||
# heads (Berechnung is deliberately NOT a head → „Akzeptierende Berechnung" stays a valid member).
|
||||
_GROUP_STANDALONE = re.compile(
|
||||
r'\w*algorithm(?:us|en)\b|\w*problem(?:e|s|en)?\b|\w*reduktion(?:en)?\b|\bscheduling\b|[≤⪯]'
|
||||
r'|^\s*(?:Satz|Lemma|Korollar|Theorem|Bemerkung|Beobachtung)\s*\d'
|
||||
# atomicity: a named COMPLEXITY CLASS / a "…-Vollständigkeit(completeness)" / a "…Transformation" is a
|
||||
# self-contained concept (learning-object / atomic-KC), never a sub-definition — a bundle of ≥1 such
|
||||
# member is siblings, not one model → dissolve (catches the P/NP/NP-Vollständigkeit over-merge that NO
|
||||
# cosine floor separates). Head-final compounds (\w*klasse absorbs "Komplexitäts+klasse"); FP-safe —
|
||||
# no real TM/KNF/TSP/Scheduling definition-part carries these heads.
|
||||
r'|\w*vollständigkeit\b|\w*completeness\b|\w*transformation(?:en)?\b|\w*klasse[nr]?\b', re.I)
|
||||
|
||||
|
||||
|
||||
|
||||
# --- Outline (blocks artifact: chapter structure, only read by the guide) ---
|
||||
|
||||
def _outline_review_schema(data, valid: set[int], n_chapters: int, n_blocks: int):
|
||||
@@ -1640,10 +1651,10 @@ async def _guide_ebene(topic: str, instructions: str, provider: str, is_cancelle
|
||||
|
||||
async def _reparieren():
|
||||
set_p("Befunde beheben (Guide)…")
|
||||
await guide_board.repair_karten(topic, fmt) # befundtragende Karten → pruefer
|
||||
betroffen = await guide_board.repair_karten(topic, fmt) # Karten → pruefer/fix
|
||||
await guide.generate_guide(topic=topic, format_name=fmt, guide_id=guide_id,
|
||||
instructions=instructions, provider=provider) # resumt offene Karten
|
||||
return await _messen()
|
||||
return await _messen(), len(betroffen) > 0
|
||||
|
||||
res = await auto_repair_loop("Guide", erst, _reparieren)
|
||||
if res["grund"] != "fertig":
|
||||
|
||||
Reference in New Issue
Block a user