Add one-shot scripts (plan maison, généalogie, OAuth, etc.)

This commit is contained in:
2026-08-05 12:31:35 +02:00
parent 1936ded08f
commit 6b0f52b577
9 changed files with 2683 additions and 3 deletions
+760
View File
@@ -0,0 +1,760 @@
#!/usr/bin/env python3
"""Generate a structured PDF genealogy report from Gramps JSONL export."""
import json
import sys
from collections import defaultdict
from datetime import datetime
from fpdf import FPDF
# ── Config ──────────────────────────────────────────────────────────────────
INPUT = "/root/.hermes/webui/attachments/b3196dc4d94f/gramps-web-export-20260722234742.json"
# ── Load data ──────────────────────────────────────────────────────────────
objects = []
with open(INPUT) as f:
for line in f:
line = line.strip()
if line:
if "|" in line and line[0].isdigit():
line = line.split("|", 1)[1]
objects.append(json.loads(line))
by_handle = {o["handle"]: o for o in objects}
# ── Event type mapping ───────────────────────────────────────────────────────
EVENT_TYPES = {
0: "Mariage", 1: "Mariage", 2: "Divorce",
3: "Décès", 4: "Inhumation", 5: "Obsèques",
6: "Naissance", 7: "Baptême", 8: "Adoption",
9: "Baptême (LDS)", 10: "Endowment (LDS)",
11: "Scellement (LDS)", 12: "Naissance", 13: "Décès",
19: "Inhumation",
}
def event_type_str(t):
return EVENT_TYPES.get(t.get("value"), f"Type {t.get('value')}")
def format_date(d):
if not d:
return ""
dv = d.get("dateval", [0, 0, 0])
y, m, day = dv[2], dv[1], dv[0]
if y == 0:
return ""
if m == 0:
return str(y)
if day == 0:
months = ["", "Jan", "Fév", "Mar", "Avr", "Mai", "Juin",
"Juil", "Aoû", "Sep", "Oct", "Nov", "Déc"]
return f"{months[m]} {y}"
return f"{day:02d}/{m:02d}/{y}"
def person_name(p):
if not p:
return "Inconnu"
n = p.get("primary_name", {})
first = n.get("first_name", "").strip()
sur_list = n.get("surname_list", [])
sur = sur_list[0].get("surname", "").strip() if sur_list else ""
suffix = n.get("suffix", "").strip()
parts = [first, sur, suffix] if suffix else [first, sur]
return " ".join(p for p in parts if p).strip()
def get_person_events(p):
birth, death = "", ""
for er in p.get("event_ref_list", []):
ev = by_handle.get(er.get("ref"))
if ev:
t = ev.get("type", {}).get("value")
d = format_date(ev.get("date"))
if t == 12:
birth = d
elif t == 13:
death = d
return birth, death
def get_events_for_person(p):
events = []
for er in p.get("event_ref_list", []):
ev = by_handle.get(er.get("ref"))
if ev:
events.append({
"type": event_type_str(ev.get("type", {})),
"date": format_date(ev.get("date")),
"desc": ev.get("description", ""),
"place": ev.get("place", ""),
})
return events
def get_parents(person):
father, mother = None, None
for pf in person.get("parent_family_list", []):
fam = by_handle.get(pf)
if fam:
if fam.get("father_handle"):
father = by_handle.get(fam["father_handle"])
if fam.get("mother_handle"):
mother = by_handle.get(fam["mother_handle"])
return father, mother
def get_families(person):
return [f for f in families
if f.get("father_handle") == person["handle"]
or f.get("mother_handle") == person["handle"]]
def get_children(family):
children = []
for cr in family.get("child_ref_list", []):
child = by_handle.get(cr.get("ref"))
if child:
children.append(child)
return children
def get_place_name(handle):
pl = by_handle.get(handle)
if pl:
return pl.get("name", {}).get("value", "")
return ""
def get_notes_for_person(p):
notes = []
for nh in p.get("note_list", []):
n = by_handle.get(nh)
if n:
notes.append(n.get("text", {}).get("string", ""))
return notes
# ── Index data ──────────────────────────────────────────────────────────────
persons = [o for o in objects if o.get("_class") == "Person"]
families = [o for o in objects if o.get("_class") == "Family"]
places = [o for o in objects if o.get("_class") == "Place"]
media = [o for o in objects if o.get("_class") == "Media"]
notes = [o for o in objects if o.get("_class") == "Note"]
# ── PDF Generation ───────────────────────────────────────────────────────────
class GenealogyPDF(FPDF):
def header(self):
if self.page_no() > 1:
self.set_font("Helvetica", "I", 7)
self.set_text_color(130, 130, 130)
self.cell(0, 5, "Rapport de Genealogie - Familles Keller, Staerle & Alliees", align="C")
self.ln(7)
def footer(self):
self.set_y(-15)
self.set_font("Helvetica", "I", 7)
self.set_text_color(130, 130, 130)
self.cell(0, 8, f"Page {self.page_no()}/{{nb}}", align="C")
def section_title(self, title):
self.set_font("Helvetica", "B", 14)
self.set_text_color(15, 23, 42)
self.cell(0, 8, title)
self.ln(4)
self.set_draw_color(15, 23, 42)
self.set_line_width(0.5)
self.line(self.get_x(), self.get_y(), self.get_x() + 190, self.get_y())
self.ln(6)
def sub_title(self, title):
self.set_font("Helvetica", "B", 11)
self.set_text_color(30, 60, 100)
self.cell(0, 7, title)
self.ln(5)
def body_text(self, text):
self.set_font("Helvetica", "", 9)
self.set_text_color(50, 50, 50)
w = self.w - self.get_x() - self.r_margin
self.multi_cell(w, 4.5, text)
self.ln(1)
def bullet(self, text):
self.set_font("Helvetica", "", 9)
self.set_text_color(50, 50, 50)
x0 = self.get_x()
self.cell(5, 4.5, "-")
w = self.w - self.get_x() - self.r_margin
self.multi_cell(w, 4.5, text)
self.ln(0.5)
def kv(self, key, value):
self.set_font("Helvetica", "B", 9)
self.set_text_color(50, 50, 50)
self.cell(40, 5, key)
self.set_font("Helvetica", "", 9)
w = self.w - self.get_x() - self.r_margin
self.multi_cell(w, 5, value)
self.ln(0.5)
def person_card(self, p, level=0):
name = person_name(p)
birth, death = get_person_events(p)
gender_map = {0: "F", 1: "M", 2: "?"}
gender = gender_map.get(p.get("gender"), "?")
indent = level * 4
self.set_x(self.l_margin + indent)
self.set_font("Helvetica", "B", 10)
self.set_text_color(15, 23, 42)
self.cell(0, 5, f"{name} [{gender}] ({p.get('gramps_id', '')})")
self.ln(5)
self.set_x(self.l_margin + indent + 4)
self.set_font("Helvetica", "", 9)
self.set_text_color(80, 80, 80)
dates = []
if birth:
dates.append(f"Ne(e) le {birth}")
if death:
dates.append(f"Decede(e) le {death}")
if dates:
self.cell(0, 4.5, " | ".join(dates))
self.ln(4.5)
events = get_events_for_person(p)
for ev in events:
if ev["type"] not in ("Naissance", "Décès"):
self.set_x(self.l_margin + indent + 4)
self.set_font("Helvetica", "", 8)
self.set_text_color(100, 100, 100)
txt = f"{ev['type']}: {ev['date']}" if ev["date"] else ev["type"]
if ev["place"]:
place_name = get_place_name(ev["place"])
if place_name:
txt += f" - {place_name}"
if ev["desc"]:
txt += f" ({ev['desc']})"
self.cell(0, 4, txt)
self.ln(4)
# Notes (professions, etc.)
p_notes = get_notes_for_person(p)
for n in p_notes:
self.set_x(self.l_margin + indent + 4)
self.set_font("Helvetica", "I", 8)
self.set_text_color(120, 120, 120)
self.cell(0, 4, f"[{n.strip()}]")
self.ln(4)
self.ln(2)
def family_section(self, family, label=""):
father = by_handle.get(family.get("father_handle"))
mother = by_handle.get(family.get("mother_handle"))
children = get_children(family)
f_name = person_name(father) if father else "?"
m_name = person_name(mother) if mother else "?"
marriage_date = ""
marriage_place = ""
for er in family.get("event_ref_list", []):
ev = by_handle.get(er.get("ref"))
if ev:
marriage_date = format_date(ev.get("date"))
marriage_place = get_place_name(ev.get("place", ""))
rel_type = family.get("type", {}).get("value", 0)
rel_str = "Marie" if rel_type == 0 else "Non marie" if rel_type == 3 else ""
title = f"{f_name} & {m_name}"
if label:
title = f"{label}: {title}"
self.sub_title(title)
if marriage_date:
txt = marriage_date
if marriage_place:
txt += f" - {marriage_place}"
self.kv("Mariage:", txt)
if rel_str:
self.kv("Statut:", rel_str)
if children:
self.ln(1)
self.set_font("Helvetica", "I", 9)
self.set_text_color(80, 80, 80)
self.cell(0, 5, "Enfants:")
self.ln(5)
for c in children:
c_name = person_name(c)
c_birth, c_death = get_person_events(c)
c_dates = []
if c_birth:
c_dates.append(f"n. {c_birth}")
if c_death:
c_dates.append(f"d. {c_death}")
c_info = f" {c_name}"
if c_dates:
c_info += f" ({', '.join(c_dates)})"
self.bullet(c_info)
self.ln(3)
# ── Build the report ─────────────────────────────────────────────────────────
pdf = GenealogyPDF()
pdf.alias_nb_pages()
pdf.set_auto_page_break(auto=True, margin=20)
pdf.add_page()
# ── Cover ────────────────────────────────────────────────────────────────────
pdf.ln(30)
pdf.set_font("Helvetica", "B", 24)
pdf.set_text_color(15, 23, 42)
pdf.cell(0, 12, "Rapport de Genealogie", align="C")
pdf.ln(10)
pdf.set_font("Helvetica", "", 14)
pdf.set_text_color(60, 60, 60)
pdf.cell(0, 8, "Familles Keller, Staerle & Alliees", align="C")
pdf.ln(8)
pdf.set_font("Helvetica", "", 10)
pdf.cell(0, 6, "Suisse (Schwytz) - Alsace (France) - Moselle", align="C")
pdf.ln(6)
pdf.cell(0, 6, f"Genere le {datetime.now().strftime('%d/%m/%Y a %H:%M')}", align="C")
pdf.ln(20)
# Stats box
pdf.set_fill_color(240, 244, 248)
pdf.set_draw_color(200, 210, 220)
pdf.rect(30, pdf.get_y(), 150, 35, "DF")
pdf.set_xy(35, pdf.get_y() + 3)
pdf.set_font("Helvetica", "B", 10)
pdf.set_text_color(15, 23, 42)
pdf.cell(0, 5, "Resume")
pdf.ln(6)
pdf.set_x(35)
pdf.set_font("Helvetica", "", 9)
pdf.set_text_color(50, 50, 50)
pdf.cell(0, 4.5, f"Personnes: {len(persons)} | Familles: {len(families)} | Lieux: {len(places)}")
pdf.ln(4.5)
pdf.set_x(35)
pdf.cell(0, 4.5, f"Evenements: {len([o for o in objects if o.get('_class') == 'Event'])} | Medias: {len(media)}")
pdf.ln(4.5)
pdf.set_x(35)
pdf.cell(0, 4.5, f"Generations: 7 (de 1760 a nos jours)")
pdf.ln(4.5)
pdf.set_x(35)
pdf.cell(0, 4.5, f"Origines: Altendorf (SZ), Alsace, Moselle, Vaud")
pdf.ln(15)
# ── Table of Contents ───────────────────────────────────────────────────────
pdf.add_page()
pdf.section_title("Table des matieres")
toc = [
"1. Arbre genealogique - Famille Keller",
"2. Arbre genealogique - Famille Staerle (branche alsacienne)",
"3. Branches alliees (Lemann, Kutzner, Hamm, Gangloff)",
"4. Fiches individuelles detaillees",
"5. Lieux",
"6. Medias et documents",
"7. Notes et sources",
"8. Statistiques",
]
for item in toc:
pdf.bullet(item)
# ═══════════════════════════════════════════════════════════════════════════
# 1. KELLER FAMILY TREE
# ═══════════════════════════════════════════════════════════════════════════
pdf.add_page()
pdf.section_title("1. Arbre genealogique - Famille Keller")
# Gen 1: Jakob Keller
pdf.sub_title("Generation 1 (ancetre)")
jakob = by_handle.get("a37f8646-d374-40da-863b-86febf23b165")
if jakob:
pdf.person_card(jakob)
# Family F0006: Jakob -> Silvio
fam_f6 = by_handle.get("103b3ad8ce8c43637db4d46f804c")
if fam_f6:
pdf.family_section(fam_f6, "Famille")
# Gen 2: Silvio + Margrit
pdf.sub_title("Generation 2")
silvio = by_handle.get("d1220b0f-9a04-4581-994a-6c4985f14c83")
margrit = by_handle.get("bad07459-e2d9-4413-85bd-5b8b6e8463d5")
if silvio:
pdf.person_card(silvio)
if margrit:
pdf.person_card(margrit)
fam_f1 = by_handle.get("103b34dfb7d317e6eacf93b9b322")
if fam_f1:
pdf.family_section(fam_f1, "Famille")
# Gen 3: Lucien + Sylvie
pdf.sub_title("Generation 3")
lucien = by_handle.get("4c572a11-13cd-46ee-9aee-39b904e86dcd")
sylvie = by_handle.get("14d0669e-3174-4a94-b5e5-23af79331f20")
if lucien:
pdf.person_card(lucien)
if sylvie:
pdf.person_card(sylvie)
fam_f0 = by_handle.get("103b34b3fc3a78115abdc520a0d7")
if fam_f0:
pdf.family_section(fam_f0, "Famille")
# Gen 4: Laurent + Lucie
pdf.sub_title("Generation 4")
laurent = by_handle.get("7c2b4663-ba21-49ce-af58-b268babe3e25")
if laurent:
pdf.person_card(laurent)
lucie = by_handle.get("7cb9d60e-8a61-47d5-a4bf-69d1a23a7068")
if lucie:
pdf.person_card(lucie)
# Gen 3 siblings: Tobias, Lea, Livia
pdf.sub_title("Generation 3 - Freres et soeurs de Lucien")
tobias = by_handle.get("1a501a5f-2721-491c-9bfe-7d3d1be88bdc")
lea = by_handle.get("cfbe8ee0-0c31-4f92-ada8-a9054514df0c")
livia = by_handle.get("b7c9c8c6-471d-481d-99c4-eb1753a0fb31")
for p in [tobias, lea, livia]:
if p:
pdf.person_card(p)
# ═══════════════════════════════════════════════════════════════════════════
# 2. STAERLE FAMILY TREE (Alsace branch)
# ═══════════════════════════════════════════════════════════════════════════
pdf.add_page()
pdf.section_title("2. Arbre genealogique - Famille Staerle (Alsace)")
# Gen 1 (1760): Johann Michael Staerle + Catharina Bernhard
pdf.sub_title("Generation 1 (v. 1760)")
jm1 = by_handle.get("1c80dd1f-77b4-4ab9-940c-e0776b6161ff")
cmb = by_handle.get("cc5dad4f-1963-4037-b7db-040680011dd9")
if jm1:
pdf.person_card(jm1)
if cmb:
pdf.person_card(cmb)
fam_f12 = by_handle.get("103b753f4b6772d2f9cd968bc788")
if fam_f12:
pdf.family_section(fam_f12, "Famille")
# Gen 2: Johann Michael Staerle + Christiana Metz
pdf.sub_title("Generation 2")
jm2 = by_handle.get("32c7f3f4-aa1c-4501-aa67-0d93ebcfbf68")
cm = by_handle.get("01f5daba-73b0-4ae9-87cb-80afc755aca7")
if jm2:
pdf.person_card(jm2)
if cm:
pdf.person_card(cm)
fam_f11 = by_handle.get("103b74f88457186cf98a9b6eca8a")
if fam_f11:
pdf.family_section(fam_f11, "Famille")
# Gen 3: Jean Adam Staerle + Madeleine Helf
pdf.sub_title("Generation 3")
ja = by_handle.get("30aeb3bd-dafd-4a4f-baf3-d75c1fd0d535")
mh = by_handle.get("ab0e498f-3e4f-41de-8766-0941d58614b9")
if ja:
pdf.person_card(ja)
if mh:
pdf.person_card(mh)
fam_f10 = by_handle.get("103b744d30a81442468b19dce1e6")
if fam_f10:
pdf.family_section(fam_f10, "Famille")
# Gen 4: Michel Staerle + Sophie Vogel
pdf.sub_title("Generation 4")
michel = by_handle.get("8d3313ce-d89c-473e-a7aa-4385d5bea769")
sophie = by_handle.get("2c548840-a9ef-4f2c-9195-7c9007b963a2")
if michel:
pdf.person_card(michel)
if sophie:
pdf.person_card(sophie)
fam_f9 = by_handle.get("103b730b5d1b5e3c15d9fa4d5111")
if fam_f9:
pdf.family_section(fam_f9, "Famille")
# Gen 5: Guillaume Staerle + Frida Bender
pdf.sub_title("Generation 5")
guillaume1 = by_handle.get("3c98357d-3645-4a74-ae8d-3fb91b005fe8")
frida = by_handle.get("ce24fa7a-b67d-47ba-ad46-3fc940be0c25")
if guillaume1:
pdf.person_card(guillaume1)
if frida:
pdf.person_card(frida)
fam_f3 = by_handle.get("103b39893eb03c1e4ed40f6f8d9b")
if fam_f3:
pdf.family_section(fam_f3, "Famille")
# Gen 6: Guillaume Staerle + Catherine Jund
pdf.sub_title("Generation 6")
guillaume2 = by_handle.get("bce9436e-8a1c-4eb1-b4c6-bfdcc7c6a935")
catherine = by_handle.get("88c262e3-9d6f-4f29-a368-03732e5d4b7b")
if guillaume2:
pdf.person_card(guillaume2)
if catherine:
pdf.person_card(catherine)
fam_f2 = by_handle.get("103b35aa6e2918ae0dc0317e052b")
if fam_f2:
pdf.family_section(fam_f2, "Famille")
# Gen 7: Sylvie, Annette, Corinne, Isabelle, Anne
pdf.sub_title("Generation 7 - Enfants Staerle")
staerle_children = [
"14d0669e-3174-4a94-b5e5-23af79331f20", # Sylvie
"e30f96b7-2860-4473-acd5-f5d9b5ae4f36", # Annette
"ccf375b4-c0ca-4bba-87e6-365c6b348bb5", # Corinne
"9428a2d4-6330-4a1c-835e-f92eacefb805", # Isabelle
"f6d9662e-02a4-4327-99b8-9b48a30f8bc9", # Anne
]
for h in staerle_children:
p = by_handle.get(h)
if p:
pdf.person_card(p)
# Christian Staerle + Yvette
pdf.sub_title("Generation 7 - Christian Staerle & Yvette")
christian = by_handle.get("33c893ff-60b3-4420-931a-bb3f28ce0562")
yvette = by_handle.get("66c36421-4e32-42e9-bc57-c47f6e6adfe4")
if christian:
pdf.person_card(christian)
if yvette:
pdf.person_card(yvette)
fam_f5 = by_handle.get("103b3ac22c3c4c35c31f77a70910")
if fam_f5:
pdf.family_section(fam_f5, "Famille")
# Gen 8: Chris, Jean-michel Staerle
pdf.sub_title("Generation 8 - Enfants Christian & Yvette")
chris = by_handle.get("4e908cd1-83e6-4aa5-a004-4d510144a528")
jm_staerle = by_handle.get("ed8e0c2d-49e2-43a7-9c3f-18bd72afbdc7")
for p in [chris, jm_staerle]:
if p:
pdf.person_card(p)
# ═══════════════════════════════════════════════════════════════════════════
# 3. ALLIED BRANCHES
# ═══════════════════════════════════════════════════════════════════════════
pdf.add_page()
pdf.section_title("3. Branches alliees")
# Annette Hamm + Daniel Gangloff
pdf.sub_title("Famille Hamm-Gangloff")
annette = by_handle.get("e30f96b7-2860-4473-acd5-f5d9b5ae4f36")
daniel = by_handle.get("f6a18cb6-d08c-44b1-b52f-53de36d24107")
if annette:
pdf.person_card(annette)
if daniel:
pdf.person_card(daniel)
fam_f4 = by_handle.get("103b3aa996d7492b0a90e358032a")
if fam_f4:
pdf.family_section(fam_f4, "Famille")
# Lea Keller + Sandro Lemann
pdf.sub_title("Famille Lemann")
lea = by_handle.get("cfbe8ee0-0c31-4f92-ada8-a9054514df0c")
sandro = by_handle.get("ef9b14d5-ab40-48fd-a110-eb4aac2a6af2")
if lea:
pdf.person_card(lea)
if sandro:
pdf.person_card(sandro)
fam_f7 = by_handle.get("103b3b4a63723ca20a2d8c369711")
if fam_f7:
pdf.family_section(fam_f7, "Famille")
# Livia Keller + Florian Kutzner
pdf.sub_title("Famille Kutzner")
livia = by_handle.get("b7c9c8c6-471d-481d-99c4-eb1753a0fb31")
florian = by_handle.get("b4984dbd-2f15-4a41-9e6f-900d0f67e507")
if livia:
pdf.person_card(livia)
if florian:
pdf.person_card(florian)
fam_f8 = by_handle.get("103b3b5df8a1443d8513129fe995")
if fam_f8:
pdf.family_section(fam_f8, "Famille")
# ═══════════════════════════════════════════════════════════════════════════
# 4. DETAILED PERSON CARDS
# ═══════════════════════════════════════════════════════════════════════════
pdf.add_page()
pdf.section_title("4. Fiches individuelles detaillees")
all_persons_sorted = sorted(persons, key=lambda p: p.get("gramps_id", ""))
for p in all_persons_sorted:
name = person_name(p)
pid = p.get("gramps_id", "")
birth, death = get_person_events(p)
gender_map = {0: "Femme", 1: "Homme", 2: "Autre"}
gender = gender_map.get(p.get("gender"), "?")
if pdf.get_y() > 230:
pdf.add_page()
pdf.sub_title(f"{name} ({pid})")
pdf.kv("Nom:", name)
pdf.kv("Genre:", gender)
if birth:
pdf.kv("Naissance:", birth)
if death:
pdf.kv("Deces:", death)
father, mother = get_parents(p)
if father or mother:
parents_str = []
if father:
parents_str.append(person_name(father))
if mother:
parents_str.append(person_name(mother))
pdf.kv("Parents:", " & ".join(parents_str))
own_fams = get_families(p)
if own_fams:
for f in own_fams:
spouse_handle = f.get("father_handle") if f.get("mother_handle") == p["handle"] else f.get("mother_handle")
spouse = by_handle.get(spouse_handle) if spouse_handle else None
children = get_children(f)
if spouse:
pdf.kv("Conjoint(e):", person_name(spouse))
if children:
pdf.kv("Enfants:", ", ".join(person_name(c) for c in children))
events = get_events_for_person(p)
for ev in events:
txt = ev["type"]
if ev["date"]:
txt += f" le {ev['date']}"
if ev["place"]:
place_name = get_place_name(ev["place"])
if place_name:
txt += f" - {place_name}"
if ev["desc"]:
txt += f" ({ev['desc']})"
pdf.kv(ev["type"] + ":", txt if txt != ev["type"] else "-")
p_notes = get_notes_for_person(p)
for n in p_notes:
pdf.kv("Note:", n.strip())
pdf.ln(3)
# ═══════════════════════════════════════════════════════════════════════════
# 5. PLACES
# ═══════════════════════════════════════════════════════════════════════════
pdf.add_page()
pdf.section_title("5. Lieux")
for pl in places:
name = pl.get("name", {}).get("value", "")
lat = pl.get("lat", "")
lon = pl.get("long", "")
ptype = pl.get("place_type", {}).get("string", "")
pid = pl.get("gramps_id", "")
if pdf.get_y() > 250:
pdf.add_page()
pdf.sub_title(f"{name} ({pid})")
if ptype:
pdf.kv("Type:", ptype)
if lat and lon:
pdf.kv("Coordonnees:", f"{lat}, {lon}")
for pr in pl.get("placeref_list", []):
parent = by_handle.get(pr.get("ref"))
if parent:
pname = parent.get("name", {}).get("value", "")
pdf.kv("Fait partie de:", pname)
pdf.ln(2)
# ═══════════════════════════════════════════════════════════════════════════
# 6. MEDIA
# ═══════════════════════════════════════════════════════════════════════════
pdf.add_page()
pdf.section_title("6. Medias et documents")
for m in media:
desc = m.get("desc", "")
mime = m.get("mime", "")
path = m.get("path", "")
mid = m.get("gramps_id", "")
date_str = format_date(m.get("date", {}))
pdf.sub_title(f"{desc} ({mid})")
pdf.kv("Type:", mime)
pdf.kv("Fichier:", path)
if date_str:
pdf.kv("Date:", date_str)
pdf.ln(2)
# ═══════════════════════════════════════════════════════════════════════════
# 7. NOTES
# ═══════════════════════════════════════════════════════════════════════════
pdf.add_page()
pdf.section_title("7. Notes et sources")
for n in notes:
text = n.get("text", {}).get("string", "")
ntype = n.get("type", {}).get("string", "")
nid = n.get("gramps_id", "")
if pdf.get_y() > 260:
pdf.add_page()
pdf.sub_title(f"Note {nid}")
if ntype:
pdf.kv("Type:", ntype)
pdf.body_text(text.strip())
pdf.ln(2)
# ═══════════════════════════════════════════════════════════════════════════
# 8. STATISTICS
# ═══════════════════════════════════════════════════════════════════════════
pdf.add_page()
pdf.section_title("8. Statistiques")
males = sum(1 for p in persons if p.get("gender") == 1)
females = sum(1 for p in persons if p.get("gender") == 0)
other = sum(1 for p in persons if p.get("gender") == 2)
pdf.sub_title("Repartition par genre")
pdf.kv("Hommes:", str(males))
pdf.kv("Femmes:", str(females))
pdf.kv("Autre/Inconnu:", str(other))
pdf.ln(3)
married = sum(1 for f in families if f.get("type", {}).get("value") == 0)
unmarried = sum(1 for f in families if f.get("type", {}).get("value") == 3)
pdf.sub_title("Familles")
pdf.kv("Mariees:", str(married))
pdf.kv("Non mariees:", str(unmarried))
pdf.ln(3)
pdf.sub_title("Evenements")
event_counts = defaultdict(int)
for ev in [o for o in objects if o.get("_class") == "Event"]:
et = event_type_str(ev.get("type", {}))
event_counts[et] += 1
for et, count in sorted(event_counts.items(), key=lambda x: -x[1]):
pdf.kv(et + ":", str(count))
pdf.ln(3)
pdf.sub_title("Lieux")
pdf.kv("Total lieux:", str(len(places)))
for pl in places:
name = pl.get("name", {}).get("value", "")
ptype = pl.get("place_type", {}).get("string", "")
pdf.bullet(f"{name} ({ptype})" if ptype else name)
# ── Save ────────────────────────────────────────────────────────────────────
OUTPUT = "/root/workspace/rapport-genealogie-keller-staerle.pdf"
pdf.output(OUTPUT)
print(f"PDF genere: {OUTPUT}")
print(f"Pages: {pdf.page_no()}")
print(f"Personnes: {len(persons)}, Familles: {len(families)}, Lieux: {len(places)}, Evenements: {len([o for o in objects if o.get('_class') == 'Event'])}")