| """ |
| Parser for YourEpi DNA-Analysis PDFs. |
| Extracts GPS scores, genotypes and section results. |
| """ |
|
|
| import re |
| import fitz |
|
|
|
|
| |
| |
| |
|
|
| RESULT_MAP = { |
| |
| "gute kontrolle": ("sattigung_hunger", "Gute Kontrolle"), |
| "schlechte kontrolle": ("sattigung_hunger", "Schlechte Kontrolle"), |
|
|
| "geringes risiko": { |
| "milchprodukte": ("milchprodukte", "Geringes Risiko"), |
| }, |
| "vermindertes risiko": ("glutensensitivitaet", "Vermindertes Risiko"), |
| "erhöhtes risiko": ("getreidesensitivitaet", "Erhöhtes Risiko"), |
| "normale sensitivität": ("histamin", "Normale Sensitivität"), |
|
|
| "apo-ε3/ε3": ("apoe", "Apo-ε3/ε3"), |
| "apo-ε3/ε4": ("apoe", "Apo-ε3/ε4"), |
| "apo-ε4/ε4": ("apoe", "Apo-ε4/ε4"), |
|
|
| "normale reaktion": ("gesaettigte_fette", "Normale Reaktion"), |
| "gute reaktion": ("eufs", "Gute Reaktion"), |
| "geringeres entzündungsrisiko": ("mufs", "Geringeres Entzündungsrisiko"), |
| "höheres entzündungsrisiko": ("mufs", "Höheres Entzündungsrisiko"), |
|
|
| "normaler bedarf": None, |
| "erhöhter bedarf": None, |
| "verminderter bedarf": None, |
|
|
| "ungünstige reaktion": ("kohlenhydrate", "Ungünstige Reaktion"), |
| "sensitiver": ("insulin", "Sensitiver Insulinstoffwechsel"), |
| "geringerer proteinbedarf": ("protein", "Geringerer Bedarf"), |
| |
| "hoher stoffwechsel": ("stoffwechsel", "Hoher Stoffwechsel"), |
| "langsamer stoffwechsel": ("stoffwechsel", "Langsamer Stoffwechsel"), |
| "schneller koffeinstoffwechsel": ("koffein", "Schneller Stoffwechsel"), |
| "langsamer koffeinstoffwechsel": ("koffein", "Langsamer Stoffwechsel"), |
| "abendtyp": ("chronotyp", "Abendtyp"), |
| "morgentyp": ("chronotyp", "Morgentyp"), |
| "kürzere schlafdauer": ("schlafdauer", "Kürzere Schlafdauer"), |
| "längere schlafdauer": ("schlafdauer", "Längere Schlafdauer"), |
| "geringe schlafqualität": ("schlafqualitaet", "Geringe Schlafqualität"), |
| "gute schlafqualität": ("schlafqualitaet", "Gute Schlafqualität"), |
|
|
| |
| "übermethylierer": ("methylierung", "Übermethylierer"), |
| "untermethylierer": ("methylierung", "Untermethylierer"), |
| "normale entgiftungseffizienz": ("phase1_detox", "Normale Effizienz"), |
| "normale acetylierung": ("acetylierung", "Normale Acetylierung"), |
| "normales glutathionprofil": ("glutathion", "Normales Profil"), |
| "höhere mitochondrieneffizienz": ("mitochondrien", "Höhere Effizienz"), |
| "hohe sensitivität": ("umweltgifte", "Hohe Sensitivität"), |
| "effiziente verarbeitung von schwermetallen": ("schwermetalle", "Effiziente Verarbeitung"), |
|
|
| |
| "normale schilddrüsenfunktion": ("schilddruese", "Normale Funktion"), |
| "geringere produktion": ("cortisol", "Geringere Produktion"), |
| "geschützter östrogenstoffwechsel": ("oestrogen", "Geschützter Stoffwechsel"), |
| "normaler testosteronspiegel": ("testosteron", "Normaler Spiegel"), |
|
|
| |
| "optimales vo2max-potenzial": ("vo2max", "Optimales Potenzial"), |
| "normale muskelkraft": ("kraft", "Normale Muskelkraft"), |
| "normales potenzial": ("hypertrophie", "Normales Potenzial"), |
| "normale veranlagung": ("ausdauer", "Normale Veranlagung"), |
| "höhere veranlagung für schnellkraft": ("schnellkraft", "Höhere Schnellkraft-Veranlagung"), |
| "normale erholung": ("erholung", "Normale Erholung"), |
| } |
|
|
| |
| NUTRIENT_SECTIONS = { |
| "vitamin b1": "vitamin_b1", |
| "vitamin b4": "vitamin_b4", |
| "vitamin b6": "vitamin_b6", |
| "vitamin b9": "vitamin_b9", |
| "vitamin b12": "vitamin_b12", |
| "vitamin c": "vitamin_c", |
| "vitamin a": "vitamin_a", |
| "vitamin e": "vitamin_e", |
| "vitamin d": "vitamin_d", |
| "vitamin k": "vitamin_k", |
| "selen": "selen", |
| "magnesium": "magnesium", |
| "kupfer": "kupfer", |
| "zink": "zink", |
| "eisen": "eisen", |
| "stickstoffmonoxid": "stickstoffmonoxid", |
| "natrium": "natrium", |
| } |
|
|
| NUTRIENT_RESULT_WORDS = { |
| "erhöhter bedarf": "erhoehter_bedarf", |
| "verminderter bedarf": "verminderter_bedarf", |
| "normaler bedarf": "normaler_bedarf", |
| "weniger effiziente umwandlung": "verminderte_umwandlung", |
| "geringe umwandlung": "verminderte_umwandlung", |
| "höherer spiegel": "hoeherer_spiegel", |
| "erhöhter spiegel": "hoeherer_spiegel", |
| } |
|
|
|
|
| def extract_text(pdf_path: str) -> list[str]: |
| """Return list of page texts from the PDF (PyMuPDF, with fallbacks).""" |
| doc = fitz.open(pdf_path) |
| print(f"[extract_text] {len(doc)} Seiten, encrypted={doc.is_encrypted}", flush=True) |
|
|
| texts = [] |
| image_pages = 0 |
| for i, page in enumerate(doc): |
| t = "" |
|
|
| |
| try: |
| t = page.get_text("text", sort=True) |
| except Exception: |
| pass |
|
|
| |
| if not t.strip(): |
| try: |
| t = page.get_text("text") |
| except Exception: |
| pass |
|
|
| |
| if not t.strip(): |
| try: |
| d = page.get_text("rawdict") |
| parts = [] |
| for block in d.get("blocks", []): |
| for line in block.get("lines", []): |
| for span in line.get("spans", []): |
| txt = span.get("text", "") |
| if txt.strip(): |
| parts.append(txt) |
| t = " ".join(parts) |
| except Exception: |
| pass |
|
|
| |
| if not t.strip(): |
| try: |
| blocks = page.get_text("blocks") |
| t = "\n".join( |
| b[4] for b in blocks |
| if len(b) > 4 and isinstance(b[4], str) and b[4].strip() |
| ) |
| except Exception: |
| pass |
|
|
| imgs = len(page.get_images()) |
| if not t.strip() and imgs > 0: |
| image_pages += 1 |
| texts.append(t or "") |
| print( |
| f"[extract_text] Seite {i}: {len(t.strip())} Zeichen, {imgs} Bilder" |
| f" | {t[:80]!r}", |
| flush=True, |
| ) |
|
|
| doc.close() |
|
|
| |
| if not any(t.strip() for t in texts): |
| empty_pages = sum(1 for t in texts if not t.strip()) |
| if image_pages >= empty_pages * 0.5: |
| print( |
| f"[extract_text] {image_pages}/{empty_pages} leere Seiten mit Bildern" |
| f" — bildbasiertes PDF erkannt, versuche OCR", |
| flush=True, |
| ) |
| |
| |
| |
| texts = ["__OCR_NEEDED__"] * len(texts) |
| else: |
| try: |
| import pdfplumber |
| print("[extract_text] fitz fand keinen Text — versuche pdfplumber", flush=True) |
| texts = [] |
| with pdfplumber.open(pdf_path) as plumb: |
| for i, page in enumerate(plumb.pages): |
| t = page.extract_text() or "" |
| texts.append(t) |
| if t.strip(): |
| print(f"[extract_text] pdfplumber Seite {i}: {len(t)} Zeichen", flush=True) |
| if any(t.strip() for t in texts): |
| print("[extract_text] pdfplumber erfolgreich", flush=True) |
| else: |
| print("[extract_text] pdfplumber: ebenfalls kein Text", flush=True) |
| except ImportError: |
| print("[extract_text] pdfplumber nicht installiert", flush=True) |
| except Exception as e: |
| print(f"[extract_text] pdfplumber Fehler: {e}", flush=True) |
|
|
| return texts |
|
|
|
|
| def ocr_pages_batched(pdf_path: str, page_count: int, batch_size: int = 5): |
| """Generator: OCR in Batches. Yieldet (pages_done, texts) nach jedem Batch. |
| Ermöglicht Fortschrittsanzeige in der Gradio-Generator-Funktion ohne Timeout. |
| """ |
| try: |
| import pytesseract |
| from PIL import Image |
| except ImportError: |
| print("[ocr] pytesseract oder Pillow nicht installiert", flush=True) |
| yield page_count, [""] * page_count |
| return |
|
|
| |
| |
| |
| _cfg = "--oem 3 --psm 3" |
|
|
| |
| try: |
| _available = pytesseract.get_languages() |
| _lang = "deu+eng" if "deu" in _available else "eng" |
| except Exception: |
| _lang = "deu+eng" |
| print(f"[ocr] Sprache: {_lang}, Config: {_cfg}", flush=True) |
|
|
| doc = fitz.open(pdf_path) |
| texts = [""] * page_count |
| _errors = 0 |
|
|
| for batch_start in range(0, page_count, batch_size): |
| batch_end = min(batch_start + batch_size, page_count) |
| for i in range(batch_start, batch_end): |
| try: |
| pix = doc[i].get_pixmap(dpi=200) |
| img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples) |
| t = pytesseract.image_to_string(img, lang=_lang, config=_cfg, timeout=30) |
| texts[i] = t |
| if t.strip(): |
| print(f"[ocr] Seite {i}: {len(t.strip())} Zeichen", flush=True) |
| else: |
| print(f"[ocr] Seite {i}: leer", flush=True) |
| except Exception as e: |
| _errors += 1 |
| print(f"[ocr] Seite {i} Fehler: {e}", flush=True) |
| texts[i] = "" |
| yield batch_end, texts |
|
|
| doc.close() |
| ocr_chars = sum(len(t.strip()) for t in texts) |
| print(f"[ocr] Fertig: {ocr_chars} Zeichen aus {page_count} Seiten, {_errors} Fehler", flush=True) |
|
|
|
|
|
|
|
|
| def clean(text: str) -> str: |
| """Remove font encoding artifacts and normalise whitespace.""" |
| |
| text = re.sub(r'\(cid:\d+\)', '', text) |
| |
| text = re.sub(r'[ \t]+', ' ', text) |
| return text.strip() |
|
|
|
|
| def _page_header(page_text: str, n_paragraphs: int = 3) -> str: |
| """ |
| Return the first N paragraphs of a page (separated by blank lines). |
| This covers the breadcrumb + section title area and avoids matching |
| keywords that appear only in the body description text. |
| """ |
| paras = [p.strip() for p in re.split(r'\n{2,}', page_text) if p.strip()] |
| return clean(" ".join(paras[:n_paragraphs])).lower() |
|
|
|
|
| def parse_genetic_test(pdf_path: str) -> dict: |
| """ |
| Parse a YourEpi PDF and return a structured dict. |
| Wenn die PDF bildbasiert ist, enthält das Result _ocr_needed=True |
| — der Aufrufer soll dann ocr_pages_batched() verwenden und parse_pages() aufrufen. |
| """ |
| pages = extract_text(pdf_path) |
|
|
| |
| if pages and pages[0] == "__OCR_NEEDED__": |
| return { |
| "client_name": "Kunde/Kundin", |
| "results": {}, |
| "nutrients": {}, |
| "genotypes": [], |
| "raw_pages": len(pages), |
| "diag": ["Bildbasierte PDF — OCR erforderlich"], |
| "_ocr_needed": True, |
| } |
|
|
| return parse_pages(pages) |
|
|
|
|
| def parse_pages(pages: list) -> dict: |
| """Parst bereits extrahierte Seiten-Texte (nach fitz oder OCR).""" |
| result = { |
| "client_name": "Kunde/Kundin", |
| "results": {}, |
| "nutrients": {}, |
| "genotypes": [], |
| "raw_pages": len(pages), |
| "diag": [], |
| } |
|
|
| |
| pages_with_text = sum(1 for t in pages if len(clean(t)) > 30) |
| result["diag"].append(f"{pages_with_text}/{len(pages)} Seiten mit Text") |
| print(f"[parse] {pages_with_text}/{len(pages)} Seiten mit verwertbarem Text", flush=True) |
|
|
| |
| |
| name_word = r'[A-ZÄÖÜ][a-zäöüß\-]+' |
| name_full_inline = rf'{name_word}(?:[ ]+{name_word})*' |
| NON_NAME = { |
| 'seite', 'page', 'score', 'genomic', 'yourepi', 'epigenetik', |
| 'analyse', 'ergebnis', 'potential', 'dna', 'test', 'ihren', |
| 'mehr', 'wir', 'hier', 'bitte', 'alle', 'diese', |
| } |
|
|
| for i, page_text in enumerate(pages): |
| if not page_text.strip(): |
| continue |
|
|
| |
| name_match = ( |
| re.search(rf'Liebe\(r\)\s+({name_full_inline})', page_text) |
| or re.search(rf'Liebe[rs]?\s+({name_full_inline})', page_text) |
| ) |
| if name_match: |
| candidate = name_match.group(1).strip() |
| |
| words = candidate.split() |
| if not any(w.lower() in NON_NAME for w in words): |
| result["client_name"] = candidate |
| print(f"[parse] Name (Anrede) Seite {i}: {candidate!r}", flush=True) |
| break |
|
|
| |
| footer_lines = [l.strip() for l in page_text.strip().split('\n')[-6:] if l.strip()] |
| for line in reversed(footer_lines): |
| m = re.match(rf'^({name_word}(?:[ ]+{name_word}){{1,2}})$', line) |
| if m: |
| candidate = m.group(1) |
| if not any(w.lower() in NON_NAME for w in candidate.split()): |
| result["client_name"] = candidate |
| print(f"[parse] Name (Footer) Seite {i}: {candidate!r}", flush=True) |
| break |
| if result["client_name"] != "Kunde/Kundin": |
| break |
|
|
| |
| |
| |
| |
|
|
| PAGE_SECTION_MAP = [ |
| |
| |
| ("wahrnehmung von sättigung", "sattigung_hunger", |
| ["Gute Kontrolle", "Schlechte Kontrolle"]), |
| ("milchprodukte", "milchprodukte", |
| ["Geringes Risiko", "Erhöhtes Risiko"]), |
| ("glutensensitivität", "glutensensitivitaet", |
| ["Vermindertes Risiko", "Geringes Risiko", "Erhöhtes Risiko"]), |
| ("getreidesensitivität", "getreidesensitivitaet", |
| ["Erhöhtes Risiko", "Geringes Risiko", "Vermindertes Risiko"]), |
| ("histamin", "histamin", |
| ["Normale Sensitivität", "Erhöhte Sensitivität"]), |
| ("apoe status", "apoe", |
| ["Apo-ε3/ε3 Typ", "Apo-ε3/ε4 Typ", "Apo-ε4/ε4 Typ", |
| "apo-ε3/ε3", "apo-ε3/ε4", "apo-ε4/ε4"]), |
| ("gesättigte fette", "gesaettigte_fette", |
| ["Normale Reaktion", "Erhöhte Reaktion"]), |
| ("einfach ungesättigte fettsäuren", "eufs", |
| ["Gute Reaktion", "Normale Reaktion"]), |
| ("mehrfach ungesättigte fettsäuren", "mufs", |
| ["Geringeres Entzündungsrisiko", "Höheres Entzündungsrisiko"]), |
| ("omega-3", "omega3", |
| ["Normaler Bedarf", "Erhöhter Bedarf"]), |
| ("kohlenhydrate", "kohlenhydrate", |
| ["Ungünstige Reaktion", "Normale Reaktion"]), |
| ("insulin", "insulin", |
| ["Sensitiver Insulinstoffwechsel", "Sensitiver", "Normaler Insulinstoffwechsel"]), |
| ("proteinzufuhr", "protein", |
| ["Geringerer Bedarf", "Normaler Bedarf", "Erhöhter Bedarf"]), |
| |
| ("vitamin b1", "vit_b1_page", |
| ["Erhöhter Bedarf", "Normaler Bedarf", "Verminderter Bedarf"]), |
| ("vitamin b4", "vit_b4_page", |
| ["Verminderter Bedarf", "Normaler Bedarf", "Erhöhter Bedarf"]), |
| ("vitamin b6", "vit_b6_page", |
| ["Normaler Bedarf", "Erhöhter Bedarf", "Verminderter Bedarf"]), |
| ("vitamin b9", "vit_b9_page", |
| ["Verminderter Bedarf", "Normaler Bedarf", "Erhöhter Bedarf"]), |
| ("vitamin b12", "vit_b12_page", |
| ["Normaler Bedarf", "Erhöhter Bedarf", "Verminderter Bedarf"]), |
| |
| ("chronotypen", "chronotyp", |
| ["Abendtyp", "Morgentyp"]), |
| ("schlafdauer", "schlafdauer", |
| ["Kürzere Schlafdauer", "Längere Schlafdauer", "Normale Schlafdauer"]), |
| ("schlafstörungen", "schlafqualitaet", |
| ["Geringe Schlafqualität", "Gute Schlafqualität"]), |
| ("stoffwechsel", "stoffwechsel", |
| ["Hoher Stoffwechsel", "Langsamer Stoffwechsel", "Normaler Stoffwechsel"]), |
| |
| ("methylierung", "methylierung", |
| ["Übermethylierer", "Untermethylierer", "Normale Methylierung"]), |
| ("glutathion", "glutathion", |
| ["Normales Glutathionprofil", "Normales Profil", "Vermindertes Profil"]), |
| ("mitochondrien", "mitochondrien", |
| ["Höhere Mitochondrieneffizienz", "Höhere Effizienz", "Normale Effizienz"]), |
| ("umweltgifte", "umweltgifte", |
| ["Hohe Sensitivität", "Normale Sensitivität", "Niedrige Sensitivität"]), |
| ("schwermetalle", "schwermetalle", |
| ["Effiziente Verarbeitung von Schwermetallen", "Effiziente Verarbeitung", |
| "Eingeschränkte Verarbeitung"]), |
| ("phase-1-detox", "phase1_detox", |
| ["Normale Entgiftungseffizienz", "Normale Effizienz", |
| "Verringerte Entgiftungseffizienz"]), |
| ("acetylierung", "acetylierung", |
| ["Normale Acetylierung", "Schnelle Acetylierung", "Langsame Acetylierung"]), |
| |
| ("schilddrüsenhormon", "schilddruese", |
| ["Normale Schilddrüsenfunktion", "Normale Funktion", |
| "Eingeschränkte Schilddrüsenfunktion"]), |
| ("cortisol", "cortisol", |
| ["Geringere Produktion", "Normale Produktion", "Erhöhte Produktion"]), |
| ("östrogen", "oestrogen", |
| ["Geschützter Östrogenstoffwechsel", "Geschützter Stoffwechsel", |
| "Ungünstiger Stoffwechsel"]), |
| ("testosteron", "testosteron", |
| ["Normaler Testosteronspiegel", "Normaler Spiegel", |
| "Niedriger Testosteronspiegel"]), |
| |
| ("vo2max", "vo2max", |
| ["Optimales VO2max-Potenzial", "Optimales Potenzial", "Normales Potenzial"]), |
| ("muskelkraft", "kraft", |
| ["Normale Muskelkraft", "Höhere Muskelkraft"]), |
| ("muskelhypertrophie", "hypertrophie", |
| ["Normales Potenzial", "Höheres Potenzial"]), |
| ("ausdauer", "ausdauer", |
| ["Normale Veranlagung", "Höhere Veranlagung"]), |
| ("schnellkraft", "schnellkraft", |
| ["Höhere Veranlagung für Schnellkraft", "Normale Veranlagung"]), |
| ("erholung", "erholung", |
| ["Normale Erholung", "Verbesserte Erholung"]), |
| ] |
|
|
| full_text = "\n".join(pages) |
| full_lower = clean(full_text).lower() |
|
|
| for page_text in pages: |
| if not page_text.strip(): |
| continue |
|
|
| page_lower = clean(page_text).lower() |
| header_lower = _page_header(page_text, n_paragraphs=2) |
|
|
| for (keyword, result_key, possible_results) in PAGE_SECTION_MAP: |
| kw_lower = keyword.lower() |
|
|
| |
| |
| if kw_lower not in header_lower: |
| continue |
|
|
| |
| for label in possible_results: |
| if label.lower() in page_lower: |
| |
| if result_key not in result["results"]: |
| _set_result(result, result_key, label) |
| print( |
| f"[parse] GPS '{result_key}' = '{label}' " |
| f"(kw: {keyword!r})", |
| flush=True, |
| ) |
| break |
|
|
| |
| _normalise = { |
| "glutathion": {"Normales Glutathionprofil": "Normales Profil"}, |
| "mitochondrien": {"Höhere Mitochondrieneffizienz": "Höhere Effizienz"}, |
| "schwermetalle": {"Effiziente Verarbeitung von Schwermetallen": "Effiziente Verarbeitung"}, |
| "schilddruese": {"Normale Schilddrüsenfunktion": "Normale Funktion"}, |
| "oestrogen": {"Geschützter Östrogenstoffwechsel": "Geschützter Stoffwechsel"}, |
| "testosteron": {"Normaler Testosteronspiegel": "Normaler Spiegel"}, |
| "apoe": {"Apo-ε3/ε3 Typ": "Apo-ε3/ε3", |
| "Apo-ε3/ε4 Typ": "Apo-ε3/ε4", |
| "Apo-ε4/ε4 Typ": "Apo-ε4/ε4"}, |
| "vo2max": {"Optimales VO2max-Potenzial": "Optimales Potenzial"}, |
| } |
| for key, replacements in _normalise.items(): |
| if key in result["results"] and result["results"][key] in replacements: |
| result["results"][key] = replacements[result["results"][key]] |
|
|
| |
| global_patterns = [ |
| |
| |
| (r'apo-ε3/ε3', "apoe", "Apo-ε3/ε3"), |
| (r'apo-ε3/ε4', "apoe", "Apo-ε3/ε4"), |
| (r'apo-ε4/ε4', "apoe", "Apo-ε4/ε4"), |
| ] |
|
|
| for pattern, key, fixed_val in global_patterns: |
| if key not in result["results"]: |
| m = re.search(pattern, full_lower, re.IGNORECASE | re.DOTALL) |
| if m: |
| val = fixed_val if fixed_val else m.group(0) |
| _set_result(result, key, val.strip()) |
|
|
| |
| |
| |
| _gps_to_nut = { |
| "vit_b1_page": "vitamin_b1", |
| "vit_b4_page": "vitamin_b4", |
| "vit_b6_page": "vitamin_b6", |
| "vit_b9_page": "vitamin_b9", |
| "vit_b12_page": "vitamin_b12", |
| } |
| for gps_key, nut_key in _gps_to_nut.items(): |
| if gps_key in result["results"]: |
| label = result["results"].pop(gps_key) |
| mapped = next( |
| (v for k, v in NUTRIENT_RESULT_WORDS.items() if k in label.lower()), |
| "normaler_bedarf", |
| ) |
| result["nutrients"].setdefault(nut_key, mapped) |
|
|
| |
| |
| |
| for nutrient, key in NUTRIENT_SECTIONS.items(): |
| if key in result["nutrients"]: |
| continue |
| pattern = ( |
| rf'{re.escape(nutrient)}\s*\n' |
| rf'.*?' |
| rf'(erhöhter bedarf|verminderter bedarf|normaler bedarf' |
| rf'|weniger eff.{{0,6}}iziente umwandlung' |
| rf'|geringe umwandlung' |
| rf'|höherer spiegel|erhöhter spiegel)' |
| ) |
| m = re.search(pattern, full_lower, re.IGNORECASE | re.DOTALL) |
| if m: |
| result_text = m.group(1).strip() |
| mapped = next( |
| (v for k, v in NUTRIENT_RESULT_WORDS.items() if k in result_text), |
| "normaler_bedarf", |
| ) |
| result["nutrients"][key] = mapped |
|
|
| |
| |
| geno_pattern = r'([A-Z][A-Z0-9]+(?:\d+)?)\s+(rs\d+)\s+([ACGT]+)\s+([ACGT]+)\s+([ACGT]{2})' |
| for m in re.finditer(geno_pattern, full_text): |
| result["genotypes"].append({ |
| "gene": m.group(1), |
| "snp": m.group(2), |
| "wildtype": m.group(3), |
| "variant": m.group(4), |
| "genotype": m.group(5), |
| }) |
|
|
| |
| cat_entries = [g for g in result["genotypes"] if g["gene"] == "CAT"] |
| for cat in cat_entries: |
| if cat["genotype"] != cat["wildtype"] * 2: |
| result["results"]["cat_variant"] = True |
|
|
| |
| result["diag"].append(f"GPS gefunden: {len(result['results'])}") |
| result["diag"].append(f"Nährstoffe: {len(result['nutrients'])}") |
| result["diag"].append(f"Genotypen: {len(result['genotypes'])}") |
| print(f"[parse] Fertig: {len(result['results'])} GPS, " |
| f"{len(result['nutrients'])} Nährstoffe, " |
| f"{len(result['genotypes'])} Genotypen", flush=True) |
|
|
| return result |
|
|
|
|
| def _set_result(result_dict: dict, key: str, value: str): |
| result_dict["results"][key] = value |
|
|
|
|
| def build_recommendations(parsed: dict) -> dict: |
| """ |
| Erstellt Empfehlungen aus dem geparsten Gentest. |
| Primär SNP-basiert (genetic_analyzer), ergänzt durch GPS-Befunde. |
| Gibt dict mit keys: supplements, lab_tests, genetic_analysis, parsed zurück. |
| """ |
| from supplement_db import SUPPLEMENTS, LAB_TESTS, BASIS_SUPPLEMENTS |
| from genetic_analyzer import analyze as snp_analyze |
|
|
| |
| genetic_analysis = snp_analyze(parsed.get("genotypes", [])) |
| snp_supplement_keys = set(genetic_analysis["supplements"]) |
|
|
| |
| from supplement_db import GPS_TO_SUPPLEMENTS |
| results = parsed["results"] |
| nutrients = parsed["nutrients"] |
|
|
| gps_supplement_map = { |
| "getreidesensitivitaet:Erhöhtes Risiko": "getreidesensitivitaet_erhoehtes_risiko", |
| "kohlenhydrate:Ungünstige Reaktion": "kohlenhydrate_ungueinstige_reaktion", |
| "methylierung:Übermethylierer": "uebermethylierung", |
| "umweltgifte:Hohe Sensitivität": "umweltgifte_hohe_sensitivitaet", |
| "mitochondrien:Höhere Effizienz": "mitochondrien_hoehere_effizienz", |
| "schlafqualitaet:Geringe Schlafqualität": "schlafqualitaet_gering", |
| "chronotyp:Abendtyp": "abendtyp", |
| "cortisol:Geringere Produktion": "cortisol_geringere_produktion", |
| } |
| for section, result_val in results.items(): |
| lookup = f"{section}:{result_val}" |
| if lookup in gps_supplement_map: |
| snp_supplement_keys.update( |
| GPS_TO_SUPPLEMENTS.get(gps_supplement_map[lookup], [])) |
|
|
| nutrient_supplement_map = { |
| "selen:erhoehter_bedarf": "selen_erhoehter_bedarf", |
| "eisen:erhoehter_bedarf": "eisen_erhoehter_bedarf", |
| "vitamin_b1:erhoehter_bedarf": "vitamin_b1_erhoehter_bedarf", |
| "vitamin_b9:erhoehter_bedarf": "vitamin_b9_erhoehter_bedarf", |
| "vitamin_a:verminderte_umwandlung": "vitamin_a_umwandlung_vermindert", |
| } |
| for nut_key, nut_result in nutrients.items(): |
| lookup = f"{nut_key}:{nut_result}" |
| if lookup in nutrient_supplement_map: |
| snp_supplement_keys.update( |
| GPS_TO_SUPPLEMENTS.get(nutrient_supplement_map[lookup], [])) |
|
|
| if results.get("cat_variant"): |
| snp_supplement_keys.add("catalase") |
|
|
| |
| |
| |
| to_remove: set = set() |
| all_active = snp_supplement_keys | set(BASIS_SUPPLEMENTS) |
|
|
| |
| for key in all_active: |
| supp = SUPPLEMENTS.get(key, {}) |
| for covered_key in supp.get("supersedes", []): |
| if covered_key in all_active: |
| print(f"[recs] '{covered_key}' entfernt — Inhalt bereits in '{key}'", |
| flush=True) |
| to_remove.add(covered_key) |
|
|
| |
| for key in all_active: |
| supp = SUPPLEMENTS.get(key, {}) |
| for sb in supp.get("superseded_by", []): |
| if sb in all_active: |
| print(f"[recs] '{key}' entfernt — superseded_by '{sb}'", flush=True) |
| to_remove.add(key) |
|
|
| snp_supplement_keys -= to_remove |
| |
| basis_supplements_active = [k for k in BASIS_SUPPLEMENTS if k not in to_remove] |
|
|
| |
| priority_order = ["SEHR HOCH", "HOCH", "MITTEL", "STANDARD", "NIEDRIG", "SITUATIV"] |
|
|
| basis_list = [] |
| for key in basis_supplements_active: |
| if key in SUPPLEMENTS: |
| s = dict(SUPPLEMENTS[key]) |
| s["key"] = key |
| basis_list.append(s) |
|
|
| gezielt_list = [] |
| for key in snp_supplement_keys: |
| if key in SUPPLEMENTS and key not in BASIS_SUPPLEMENTS: |
| s = dict(SUPPLEMENTS[key]) |
| s["key"] = key |
| gezielt_list.append(s) |
|
|
| gezielt_list.sort(key=lambda x: priority_order.index(x.get("priority", "STANDARD"))) |
| supplements = basis_list + gezielt_list |
|
|
| |
| lab_sections = ["basis", "mikro", "detox_oxidativer_stress"] |
| categories_with_risk = {f["category"] for f in genetic_analysis["findings"] |
| if f["risk"] in ("hoch", "moderat")} |
|
|
| if "stress" in categories_with_risk or results.get("cortisol"): |
| lab_sections.append("hormone") |
| if "schlaf" in categories_with_risk or results.get("schlafqualitaet") == "Geringe Schlafqualität": |
| lab_sections.append("schlaf_neuro") |
| if results.get("getreidesensitivitaet") == "Erhöhtes Risiko": |
| lab_sections.append("entzuendung_darm") |
|
|
| selected_labs = {k: LAB_TESTS[k] for k in lab_sections if k in LAB_TESTS} |
|
|
| return { |
| "supplements": supplements, |
| "lab_tests": selected_labs, |
| "genetic_analysis": genetic_analysis, |
| "parsed": parsed, |
| } |
|
|