From 18efab6996a4cfc1df1d15f6bc5a7d00fed68275 Mon Sep 17 00:00:00 2001 From: Gulum Date: Mon, 22 Jun 2026 14:49:38 +0200 Subject: [PATCH] fix(import): Foto-Generations-Shift beim Stammbaum-Import beheben MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fotos liegen in den Stammbaum-xlsx je nach Datei links, rechts oder auf der Namenszelle. Die alte Seiten-Erkennung hing an festen Spalten (col 1 / col 4): Blätter ohne solche Anker wurden als Rechts-Layout fehlgedeutet, wodurch jedes Foto eine Generation zum Probanden hin verrutschte. Folge: Kazuya trug das Foto seines Vaters (Wilbur), Wilbur das seines Großvaters (Elay), Elay hatte keins. Fix (extract._attach_photos): beide Interpretationen (links/rechts) durchrechnen und die wählen, die jedes Foto am nächsten an die Namensspalte seines Tiers legt (minimale horizontale Fehlausrichtung). Diagnose über alle 42 Dateien: keine ist echt rechts-seitig; ~21 waren fehlklassifiziert. Selbstjustierend statt fester Spalten-Annahme. Regressionstest in test_extract.py: Kazuya ohne Foto, Wilbur/Elay mit ihren eigenen (gated auf vorhandene Quelldatei). Co-Authored-By: Claude Opus 4.8 --- tools/import/extract.py | 54 ++++++++++++++++++---------- tools/import/output/review-report.md | 2 +- tools/import/test_extract.py | 21 +++++++++++ 3 files changed, 57 insertions(+), 20 deletions(-) diff --git a/tools/import/extract.py b/tools/import/extract.py index c856d7a..3ed3064 100644 --- a/tools/import/extract.py +++ b/tools/import/extract.py @@ -341,14 +341,22 @@ def _attach_photos(z, sheets, animals, fname): if not anchors: return - # Detect left_style: whether photo is to the left or to the right of the name cell - a_count = sum(1 for a in anchors if a[1] == 1) - d_count = sum(1 for a in anchors if a[1] == 4) - has_proband_in_d = any(a[1] == 4 and 50 <= a[2] <= 70 for a in anchors) - left_style = a_count > 0 or (d_count > 0 and not has_proband_in_d) + by_gen = {} + for a in animals: + by_gen.setdefault(a["_gen"], []).append(a) + media_dir = os.path.join(OUT, "photos") + col_offset = 0 if any(a["_col"] == 2 for a in animals) else 3 - def get_anchor_gen(colnum, offset=0): - effective_col = colnum - offset + # A photo sits one column either side of (or on) its animal's name cell; + # the generation is read from the photo's column. Whether photos are LEFT or + # RIGHT of the name varies, and the old fixed-column heuristic (col 1 / col 4) + # misclassified sheets that have neither, shifting every photo one generation + # toward the proband (e.g. „Stammbaum von Kazuya“ / „Picus Son“: Kazuya wore + # his father's photo, the father his grandfather's). Instead, try BOTH + # interpretations and keep the one that places photos closest to their + # assigned animal's name column (minimal horizontal misalignment). + def get_anchor_gen(colnum, left_style): + effective_col = colnum - col_offset if left_style: if effective_col <= 3: return 0 if effective_col <= 6: return 1 @@ -364,18 +372,26 @@ def _attach_photos(z, sheets, animals, fname): if effective_col <= 16: return 4 return 5 - by_gen = {} - for a in animals: - by_gen.setdefault(a["_gen"], []).append(a) - media_dir = os.path.join(OUT, "photos") - col_offset = 0 if any(a["_col"] == 2 for a in animals) else 3 - for i, (sp, col, row, media) in enumerate(anchors): - g = get_anchor_gen(col, col_offset) - cands = by_gen.get(g, []) - if not cands: - # fall back to nearest animal by row across all gens - cands = animals - target = min(cands, key=lambda a: abs((a["_row"] - row) - 10)) if cands else None + def targets_for(left_style): + out = [] + for (sp, col, row, media) in anchors: + cands = by_gen.get(get_anchor_gen(col, left_style), []) or animals + out.append(min(cands, key=lambda a: abs((a["_row"] - row) - 10)) if cands else None) + return out + + def mean_col_offset(targets): + vals = [abs(col - t["_col"]) for (sp, col, row, m), t in zip(anchors, targets) if t] + return sum(vals) / len(vals) if vals else 0.0 + + left_targets = targets_for(True) + right_targets = targets_for(False) + targets = ( + left_targets + if mean_col_offset(left_targets) <= mean_col_offset(right_targets) + else right_targets + ) + + for (sp, col, row, media), target in zip(anchors, targets): if not target: continue ext = os.path.splitext(media)[1] or ".img" diff --git a/tools/import/output/review-report.md b/tools/import/output/review-report.md index 774d115..b11e321 100644 --- a/tools/import/output/review-report.md +++ b/tools/import/output/review-report.md @@ -10,7 +10,7 @@ _Automatisch erzeugt von `tools/import/extract.py` — **noch nichts in die Date - in mehreren Dateien gefunden (Dubletten zusammengeführt): 476 - Konflikte zur Klärung: **7** - Mehrdeutige / unvollständige Einträge (ohne Name+Datum): **380** -- Fotos zugeordnet: **441** +- Fotos zugeordnet: **435** - Würfe aus der Wurfchronik: **752** - Tiere mit Wurf verknüpft: **270** (davon über Geburtsdatum **und** Eltern: 168, nur über Geburtsdatum: 102; mehrdeutig: 17) - Würfe mit Datenqualitäts-Hinweisen: 113 (+ 138 Zeilen mit abweichendem Spaltenschema) diff --git a/tools/import/test_extract.py b/tools/import/test_extract.py index 5ad68fb..a1a12c8 100644 --- a/tools/import/test_extract.py +++ b/tools/import/test_extract.py @@ -396,6 +396,27 @@ if os.path.exists(danako_path): else: print("\nWarning: Danako stammbaum file not found, skipping integration checks.") +# --- Stammbaum von Kazuya: photo generation-shift regression (PHOTO-LEFT-STYLE) --- +# This sheet has neither col-1 nor col-4 image anchors, so the old fixed-column +# heuristic misread it as right-style and shifted every photo one generation +# toward the proband: Kazuya wore his father's (Wilbur's) photo, Wilbur wore the +# grandfather's (Elay's). The fix picks the layout that places photos closest to +# their animal's name column → photos land on the correct generation. +kazuya_path = r"C:\Users\gulum\dev\Sttammbäume\Stammbaum von Kazuya.xlsx" +if os.path.exists(kazuya_path): + print(f"\nFound Kazuya stammbaum, running photo generation-shift validation...") + kz = {a["name"]: a for a in e.extract_stammbaum(kazuya_path)} + if "Kazuya" in kz: + check("Kazuya (proband) has NO photo of his own", kz["Kazuya"]["photos"] == []) + if "Wilbur" in kz: + check("Wilbur (father) gets his own photo (image7), not the grandfather's", + kz["Wilbur"]["photos"] == ["photos/wilbur-19032017/image7.jpeg"]) + if "Elay" in kz: + check("Elay (grandfather) gets his own photo (image2)", + kz["Elay"]["photos"] == ["photos/elay-16032016/image2.jpeg"]) +else: + print("\nWarning: Kazuya stammbaum file not found, skipping photo-shift checks.") + if failed: print(f"\n{failed} test(s) FAILED") sys.exit(1)