fix: Naho Eltern + Foto-Layout-Erkennung + Eltern-Fallback-Lookup
Some checks failed
CI / Backend Tests (.NET) (push) Successful in 1m2s
CI / Frontend Tests (Node/Vite) (push) Successful in 9m34s
CI / Docker Build & Push (push) Failing after 9s

This commit is contained in:
2026-06-21 22:55:50 +02:00
parent 41ff973f9a
commit cb2ee03a14
4 changed files with 81 additions and 4 deletions

View File

@@ -257,6 +257,14 @@
"father": "Wilbur von den Kleinen Chaoten",
"mother": "Naho von den Kleinen Chaoten",
"source": "Stammbaum von Kazuya.xlsx"
},
{
"name": "Naho von den Kleinen Chaoten",
"dob": "20.07.2017",
"decision": "father = Osamu von den Kleinen Chaoten, mother = Montana v.d. Kleinen Chaoten (from Stammbaum von Kazuya.xlsx)",
"father": "Osamu von den Kleinen Chaoten",
"mother": "Montana v.d. Kleinen Chaoten",
"source": "Stammbaum von Kazuya.xlsx"
}
]
}

View File

@@ -340,13 +340,37 @@ def _attach_photos(z, sheets, animals, fname):
anchors = [a for a in xu.image_anchors(z)]
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)
def get_anchor_gen(colnum, offset=0):
effective_col = colnum - offset
if left_style:
if effective_col <= 3: return 0
if effective_col <= 6: return 1
if effective_col <= 9: return 2
if effective_col <= 12: return 3
if effective_col <= 15: return 4
return 5
else:
if effective_col <= 4: return 0
if effective_col <= 7: return 1
if effective_col <= 10: return 2
if effective_col <= 13: return 3
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 = gen_of(col, col_offset)
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
@@ -362,7 +386,8 @@ def _attach_photos(z, sheets, animals, fname):
try:
with z.open(media) as src, open(os.path.join(OUT, rel), "wb") as dst:
shutil.copyfileobj(src, dst)
target["photos"].append(rel)
if rel not in target["photos"]:
target["photos"].append(rel)
except KeyError:
pass

View File

@@ -10,7 +10,7 @@ _Automatisch erzeugt von `tools/import/extract.py` — **noch nichts in die Date
- in mehreren Dateien gefunden (Dubletten zusammengeführt): 462
- Konflikte zur Klärung: **4**
- Mehrdeutige / unvollständige Einträge (ohne Name+Datum): **342**
- Fotos zugeordnet: **416**
- Fotos zugeordnet: **422**
- Würfe aus der Wurfchronik: **752**
- Tiere mit Wurf verknüpft: **269** (davon über Geburtsdatum **und** Eltern: 165, nur über Geburtsdatum: 104; mehrdeutig: 16)
- Würfe mit Datenqualitäts-Hinweisen: 113 (+ 138 Zeilen mit abweichendem Spaltenschema)
@@ -19,6 +19,17 @@ _Automatisch erzeugt von `tools/import/extract.py` — **noch nichts in die Date
Tiere wurden zusammengeführt über **normalisierter Rufname + Geburtsdatum**, mit der **Zucht als Unterscheidungsmerkmal** (Julians Regel: die `[Klammern]` in der Wurfchronik und das `of/von <Linie>`-Suffix der Stammbäume bezeichnen beide die Zucht und werden zusammengeführt — z. B. `[ZdkC]``von den Kleinen Chaoten`). Namensvarianten (z. B. `v.d.``von den`, `gen.`-Spitznamen) werden als `nameVariants` erhalten.
### Erweiterte Zusammenführungsregel: Gleicher Name + gleiche Eltern
Wenn zwei Einträge denselben Rufnamen **und** dieselben Eltern (Vater + Mutter) tragen, werden sie als dasselbe Tier betrachtet — auch wenn das Geburtsdatum abweicht. Das DOB des Eintrags, in dem das Tier Proband ist (`_gen == 0`), hat Priorität. Diese Regel greift als Sicherheitsnetz für Datenfehler beim Geburtsdatum.
**Im aktuellen Datensatz ausgelöst für:**
| Tier | DOB (falsch) | DOB (korrekt) | Vater | Mutter | Lösung |
|---|---|---|---|---|---|
| Kazuya von den Kleinen Chaoten | 22.08.2018 | 14.07.2019 | Wilbur von den Kleinen Chaoten | Naho von den Kleinen Chaoten | `correctDob`-Eintrag in conflict-decisions.json → DOB vor Dedup remapped |
### Gleicher Name + Geburtsdatum, aber unterschiedliche Zucht (NICHT zusammengeführt — bitte prüfen)
| Tier | Geburtsdatum | Zuchten | Dateien |

View File

@@ -111,10 +111,15 @@ def main():
# Index animals for fast lookup by slug ID and by normalized name+dob
animal_by_id = {a["id"]: a for a in animals}
animal_by_name_dob = {}
animal_by_name = {}
for a in animals:
key = (ex.norm_name(a["name"]), ex.norm_dob(a["dob"]))
if key[0] and key[1]:
animal_by_name_dob.setdefault(key, []).append(a)
nk = ex.norm_name(a["name"])
if nk:
animal_by_name.setdefault(nk, []).append(a)
# 2. Extract and resolve unique Contacts (Breeders & Zuchten)
print("Extracting unique contacts...")
@@ -163,8 +168,36 @@ def main():
l_parents = litter_parent_map.setdefault(l_id, [None, None]) # [father, mother]
for p_ref in a.get("parentRefs", []):
p_key = (ex.norm_name(p_ref["name"]), ex.norm_dob(p_ref["dob"]))
p_dob = ex.norm_dob(p_ref.get("dob", ""))
p_key = (ex.norm_name(p_ref["name"]), p_dob)
p_candidates = animal_by_name_dob.get(p_key, [])
if not p_candidates:
# Fallback to name-only lookup when parent DOB is empty/not found
p_norm = ex.norm_name(p_ref["name"])
candidates = animal_by_name.get(p_norm, [])
if candidates:
offspring_dob_str = parse_date_only(a["dob"])
if offspring_dob_str:
try:
o_dob = datetime.strptime(offspring_dob_str, "%Y-%m-%d")
valid_candidates = []
for cand in candidates:
cand_dob_str = parse_date_only(cand["dob"])
if cand_dob_str:
try:
c_dob = datetime.strptime(cand_dob_str, "%Y-%m-%d")
if c_dob < o_dob:
valid_candidates.append(cand)
except ValueError:
valid_candidates.append(cand)
else:
valid_candidates.append(cand)
if valid_candidates:
p_candidates = [valid_candidates[0]]
except ValueError:
p_candidates = [candidates[0]]
else:
p_candidates = [candidates[0]]
if p_candidates:
p_guid = animal_guid_map[p_candidates[0]["id"]]
if p_ref["roleGuess"] == "father":