mygramps/generate_demo_family.py
Daniel Viegas 7119aedd3d Add portrait display to timeline and tooltips, add portrait generation to demo family
- Add small portraits (24px) next to timeline events for person events
- Add larger portraits (120px) in tooltips when hovering over events
- Implement portrait loading from Gramps media_list using media_path_full
- Add portrait drawing using Cairo with circular clipping and border
- Update demo family generator to create portraits using DiceBear Avatars API
- Generate portraits considering age and gender for appropriate appearance
- Add media objects to XML with proper IDs and gallery references
- Use relative paths for media files in demo family XML
- Add helper scripts for debugging Gramps log files
- Fix deprecation warnings for XML element truth value checks
2025-11-30 11:31:25 +01:00

821 lines
30 KiB
Python

#!/usr/bin/env python3
"""
Generate a huge demo family for Gramps testing
"""
import random
import xml.etree.ElementTree as ET
import os
import urllib.request
import urllib.error
import urllib.parse
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, List, Tuple, Dict
# Set seed for deterministic event generation
random.seed(42)
# Constants
EVENT_ID_OFFSET = 10
FAMILY_ID_OFFSET = 100
EVENT_ID_START_OFFSET = 2
MIN_MONTH = 1
MAX_MONTH = 12
MIN_DAY = 1
MAX_DAY = 28
GRAMPS_XML_VERSION = "5.1.0"
GRAMPS_XML_NAMESPACE = "http://gramps-project.org/xml/1.7.1/"
GRAMPS_XML_DTD = "http://gramps-project.org/xml/1.7.1/grampsxml.dtd"
# Portrait generation constants
PORTRAITS_DIR = "portraits"
DICEBEAR_API_BASE = "https://api.dicebear.com/7.x/avataaars/svg"
# Event types to add
EVENT_TYPES = [
("Baptism", 0.7, 0, 2), # 70% chance, 0-2 years after birth
("Christening", 0.5, 0, 1), # 50% chance, 0-1 years after birth
("Education", 0.8, 5, 18), # 80% chance, 5-18 years after birth
("Graduation", 0.6, 18, 25), # 60% chance, 18-25 years after birth
("Occupation", 0.9, 18, 65), # 90% chance, 18-65 years after birth
("Military Service", 0.3, 18, 30), # 30% chance, 18-30 years after birth
("Residence", 0.7, 0, 80), # 70% chance, any time
("Emigration", 0.2, 20, 50), # 20% chance, 20-50 years after birth
("Immigration", 0.15, 20, 50), # 15% chance, 20-50 years after birth
("Retirement", 0.4, 60, 75), # 40% chance, 60-75 years after birth
("Burial", 0.6, None, None), # 60% chance if death exists, at death time
("Cremation", 0.2, None, None), # 20% chance if death exists, at death time
]
# Name lists
MALE_NAMES = [
"James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph",
"Thomas", "Charles", "Daniel", "Matthew", "Anthony", "Mark", "Donald", "Steven",
"Paul", "Andrew", "Joshua", "Kenneth", "Kevin", "Brian", "George", "Timothy",
"Ronald", "Jason", "Edward", "Jeffrey", "Ryan", "Jacob", "Gary", "Nicholas",
"Eric", "Jonathan", "Stephen", "Larry", "Justin", "Scott", "Brandon", "Benjamin"
]
FEMALE_NAMES = [
"Mary", "Patricia", "Jennifer", "Linda", "Elizabeth", "Barbara", "Susan",
"Jessica", "Sarah", "Karen", "Nancy", "Lisa", "Betty", "Margaret", "Sandra",
"Ashley", "Kimberly", "Emily", "Donna", "Michelle", "Dorothy", "Carol",
"Amanda", "Melissa", "Deborah", "Stephanie", "Rebecca", "Sharon", "Laura",
"Cynthia", "Kathleen", "Amy", "Angela", "Shirley", "Anna", "Brenda", "Pamela",
"Emma", "Nicole", "Helen", "Samantha", "Katherine", "Christine", "Debra"
]
SURNAMES = [
"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis",
"Rodriguez", "Martinez", "Hernandez", "Lopez", "Wilson", "Anderson", "Thomas",
"Taylor", "Moore", "Jackson", "Martin", "Lee", "Thompson", "White", "Harris",
"Sanchez", "Clark", "Ramirez", "Lewis", "Robinson", "Walker", "Young", "Allen",
"King", "Wright", "Scott", "Torres", "Nguyen", "Hill", "Flores", "Green", "Adams"
]
OCCUPATIONS = [
"Farmer", "Teacher", "Engineer", "Doctor", "Lawyer", "Merchant",
"Carpenter", "Blacksmith", "Sailor", "Soldier", "Clerk", "Nurse"
]
PLACES = [
"New York", "London", "Paris", "Berlin", "Rome", "Madrid", "Amsterdam",
"Vienna", "Prague", "Warsaw", "Stockholm", "Copenhagen"
]
@dataclass
class EventData:
"""Data structure for an event."""
handle: str
event_type: str
year: int
month: int
day: int
description: str
event_id: int
@dataclass
class PersonData:
"""Data structure for person information."""
handle: str
name: str
surname: str
birth: int
death: Optional[int]
gender: str
parentin: List[str]
childof: List[str]
@dataclass
class FamilyData:
"""Data structure for family information."""
handle: str
father_handle: str
mother_handle: str
children_handles: List[str]
marriage_year: int
marriage_handle: str
family_id: int
def gen_handle(prefix: str, num: int) -> str:
"""Generate unique handle."""
return f"_{prefix}{num:08d}"
def generate_portrait(person_id: int, name: str, gender: str, birth_year: int) -> Optional[Tuple[str, str]]:
"""
Generate a portrait for a person using DiceBear Avatars API.
Considers age and gender for appropriate portrait selection.
Args:
person_id: Unique person ID.
name: Person's name (used as seed for deterministic generation).
gender: Person's gender ('M' or 'F').
birth_year: Birth year (used to determine age-appropriate features).
Returns:
Optional[Tuple[str, str]]: Tuple of (media_handle, file_path) if successful, None otherwise.
"""
# Create portraits directory if it doesn't exist
if not os.path.exists(PORTRAITS_DIR):
os.makedirs(PORTRAITS_DIR)
# Calculate approximate age (use current year or a fixed reference year)
current_year = datetime.now().year
age = current_year - birth_year
# Create seed from name, person_id, and gender for deterministic generation
# Include gender in seed to get different avatars for different genders
seed = f"{name}_{person_id}_{gender}"
# Build API URL with parameters
# DiceBear Avataaars doesn't have a gender parameter, but we can use seed
# to get deterministic results. The seed with gender included will produce
# different avatars for different genders.
params = {
"seed": seed
}
# Note: DiceBear Avataaars style doesn't support style parameter
# We rely on the seed for deterministic generation based on name, ID, and gender
# Build URL with proper encoding
url = f"{DICEBEAR_API_BASE}?{urllib.parse.urlencode(params)}"
# Generate filename
filename = f"portrait_{person_id:04d}_{name.replace(' ', '_')}.svg"
file_path = os.path.join(PORTRAITS_DIR, filename)
# Download portrait
try:
urllib.request.urlretrieve(url, file_path)
media_handle = gen_handle("MEDIA", person_id)
return (media_handle, file_path)
except (urllib.error.URLError, urllib.error.HTTPError, OSError) as e:
print(f"Warning: Could not generate portrait for {name}: {e}")
return None
def create_media_element(media_handle: str, file_path: str, title: str, media_id: Optional[int] = None) -> ET.Element:
"""
Create an XML element for a media object.
Args:
media_handle: Unique handle for the media object.
file_path: Path to the media file (relative to XML file location).
title: Title/description for the media object.
media_id: Optional media ID number. If None, extracted from handle.
Returns:
ET.Element: The media XML element.
"""
media_elem = ET.Element("media")
media_elem.set("handle", media_handle)
media_elem.set("change", str(int(datetime.now().timestamp())))
# Extract ID from handle if not provided
# Handle format: _MEDIA00000001 -> ID: O0001
if media_id is None:
# Extract number from handle (e.g., "_MEDIA00000001" -> 1)
try:
media_id = int(media_handle.replace("_MEDIA", "").lstrip("0") or "0")
except (ValueError, AttributeError):
media_id = 0
media_elem.set("id", f"O{media_id:04d}")
# Use relative path for file (relative to XML file location)
# The file_path from generate_portrait is already relative (portraits/filename)
# If it's absolute, convert it to relative based on current working directory
if os.path.isabs(file_path):
# Get current working directory (where XML file will be saved)
cwd = os.getcwd()
try:
rel_path = os.path.relpath(file_path, cwd)
except ValueError:
# If paths are on different drives (Windows), keep absolute
rel_path = file_path
else:
# Already relative, use as-is
rel_path = file_path
# Normalize path separators (use forward slashes for cross-platform compatibility)
rel_path = rel_path.replace("\\", "/")
file_elem = ET.SubElement(media_elem, "file")
file_elem.set("src", rel_path)
title_elem = ET.SubElement(media_elem, "title")
title_elem.text = title
mime_elem = ET.SubElement(media_elem, "mimetype")
mime_elem.text = "image/svg+xml"
return media_elem
def create_event_element(event_data: EventData) -> ET.Element:
"""Create an XML element for an event."""
event_elem = ET.Element("event")
event_elem.set("handle", event_data.handle)
event_elem.set("change", str(int(datetime.now().timestamp())))
event_elem.set("id", f"E{event_data.event_id:04d}")
type_elem = ET.SubElement(event_elem, "type")
type_elem.text = event_data.event_type
date_elem = ET.SubElement(event_elem, "dateval")
date_elem.set("val", f"{event_data.year}-{event_data.month:02d}-{event_data.day:02d}")
if event_data.description:
desc_elem = ET.SubElement(event_elem, "description")
desc_elem.text = event_data.description
return event_elem
def gen_additional_events(
pid: int,
first_name: str,
surname: str,
birth_year: int,
death_year: Optional[int] = None
) -> List[Tuple[str, EventData]]:
"""Generate additional events for a person."""
events: List[Tuple[str, EventData]] = []
event_id_offset = pid * EVENT_ID_OFFSET + EVENT_ID_START_OFFSET
for event_type, probability, min_years, max_years in EVENT_TYPES:
if random.random() > probability:
continue
# Special handling for death-related events
if event_type in ("Burial", "Cremation"):
if not death_year:
continue
event_year = death_year
event_month = random.randint(MIN_MONTH, MAX_MONTH)
event_day = random.randint(MIN_DAY, MAX_DAY)
else:
if max_years is None:
continue
event_year = birth_year + random.randint(min_years, max_years)
if death_year and event_year > death_year:
continue
event_month = random.randint(MIN_MONTH, MAX_MONTH)
event_day = random.randint(MIN_DAY, MAX_DAY)
event_handle = gen_handle("EVENT", event_id_offset)
# Generate description based on event type
if event_type == "Education":
description = f"Education - {first_name} {surname}"
elif event_type == "Graduation":
description = f"Graduation - {first_name} {surname}"
elif event_type == "Occupation":
occupation = random.choice(OCCUPATIONS)
description = f"{occupation} - {first_name} {surname}"
elif event_type == "Military Service":
description = f"Military Service - {first_name} {surname}"
elif event_type == "Residence":
place = random.choice(PLACES)
description = f"Residence in {place} - {first_name} {surname}"
elif event_type == "Emigration":
description = f"Emigration - {first_name} {surname}"
elif event_type == "Immigration":
description = f"Immigration - {first_name} {surname}"
elif event_type == "Retirement":
description = f"Retirement - {first_name} {surname}"
else:
description = f"{event_type} of {surname}, {first_name}"
event_data = EventData(
handle=event_handle,
event_type=event_type,
year=event_year,
month=event_month,
day=event_day,
description=description,
event_id=event_id_offset
)
events.append((event_handle, event_data))
event_id_offset += 1
return events
def gen_person(
pid: int,
first_name: str,
surname: str,
birth_year: int,
death_year: Optional[int] = None,
gender: str = "M",
parentin_families: Optional[List[str]] = None,
childof_families: Optional[List[str]] = None,
reuse_additional_events: Optional[List[Tuple[str, EventData]]] = None
) -> Tuple[ET.Element, ET.Element, Optional[ET.Element], List[ET.Element], List[Tuple[str, EventData]], Optional[Tuple[str, str]]]:
"""Generate a person with all associated events and portrait."""
handle = gen_handle("PERSON", pid)
birth_handle = gen_handle("EVENT", pid * EVENT_ID_OFFSET)
death_handle = gen_handle("EVENT", pid * EVENT_ID_OFFSET + 1) if death_year else None
# Create person element
person_elem = ET.Element("person")
person_elem.set("handle", handle)
person_elem.set("change", str(int(datetime.now().timestamp())))
person_elem.set("id", f"I{pid:04d}")
gender_elem = ET.SubElement(person_elem, "gender")
gender_elem.text = gender
name_elem = ET.SubElement(person_elem, "name")
name_elem.set("type", "Birth Name")
first_elem = ET.SubElement(name_elem, "first")
first_elem.text = first_name
surname_elem = ET.SubElement(name_elem, "surname")
surname_elem.text = surname
# Generate portrait and add gallery reference
full_name = f"{first_name} {surname}"
portrait_info = generate_portrait(pid, full_name, gender, birth_year)
if portrait_info:
media_handle, file_path = portrait_info
# Add gallery section with media reference
gallery_elem = ET.SubElement(person_elem, "gallery")
media_ref = ET.SubElement(gallery_elem, "mediaobjref")
media_ref.set("hlink", media_handle)
# Birth event reference
birth_ref = ET.SubElement(person_elem, "eventref")
birth_ref.set("hlink", birth_handle)
birth_ref.set("role", "Primary")
# Death event reference
if death_handle:
death_ref = ET.SubElement(person_elem, "eventref")
death_ref.set("hlink", death_handle)
death_ref.set("role", "Primary")
# Add additional events - reuse if provided, otherwise generate new
if reuse_additional_events is not None:
additional_events = reuse_additional_events
else:
additional_events = gen_additional_events(pid, first_name, surname, birth_year, death_year)
for event_handle, _ in additional_events:
event_ref = ET.SubElement(person_elem, "eventref")
event_ref.set("hlink", event_handle)
event_ref.set("role", "Primary")
# Add parentin references
if parentin_families:
for family_handle in parentin_families:
parentin_elem = ET.SubElement(person_elem, "parentin")
parentin_elem.set("hlink", family_handle)
# Add childof references
if childof_families:
for family_handle in childof_families:
childof_elem = ET.SubElement(person_elem, "childof")
childof_elem.set("hlink", family_handle)
# Birth event
birth_month = random.randint(MIN_MONTH, MAX_MONTH)
birth_day = random.randint(MIN_DAY, MAX_DAY)
birth_event_data = EventData(
handle=birth_handle,
event_type="Birth",
year=birth_year,
month=birth_month,
day=birth_day,
description=f"Birth of {surname}, {first_name}",
event_id=pid * EVENT_ID_OFFSET
)
birth_event = create_event_element(birth_event_data)
# Death event
death_event: Optional[ET.Element] = None
if death_handle and death_year:
death_month = random.randint(MIN_MONTH, MAX_MONTH)
death_day = random.randint(MIN_DAY, MAX_DAY)
death_event_data = EventData(
handle=death_handle,
event_type="Death",
year=death_year,
month=death_month,
day=death_day,
description=f"Death of {surname}, {first_name}",
event_id=pid * EVENT_ID_OFFSET + 1
)
death_event = create_event_element(death_event_data)
# Convert additional events to XML elements
all_additional_events_xml = [create_event_element(event_data) for _, event_data in additional_events]
return person_elem, birth_event, death_event, all_additional_events_xml, additional_events, portrait_info
def gen_family(
fid: int,
father_handle: str,
mother_handle: str,
marriage_year: int,
children_handles: List[str]
) -> Tuple[ET.Element, ET.Element]:
"""Generate a family with marriage event."""
handle = gen_handle("FAMILY", fid)
marriage_handle = gen_handle("EVENT", fid * FAMILY_ID_OFFSET)
# Create family element
family_elem = ET.Element("family")
family_elem.set("handle", handle)
family_elem.set("change", str(int(datetime.now().timestamp())))
family_elem.set("id", f"F{fid:04d}")
rel_elem = ET.SubElement(family_elem, "rel")
rel_elem.set("type", "Married")
father_elem = ET.SubElement(family_elem, "father")
father_elem.set("hlink", father_handle)
mother_elem = ET.SubElement(family_elem, "mother")
mother_elem.set("hlink", mother_handle)
for child_handle in children_handles:
child_elem = ET.SubElement(family_elem, "childref")
child_elem.set("hlink", child_handle)
marriage_ref = ET.SubElement(family_elem, "eventref")
marriage_ref.set("hlink", marriage_handle)
marriage_ref.set("role", "Family")
# Marriage event
marriage_month = random.randint(MIN_MONTH, MAX_MONTH)
marriage_day = random.randint(MIN_DAY, MAX_DAY)
marriage_event_data = EventData(
handle=marriage_handle,
event_type="Marriage",
year=marriage_year,
month=marriage_month,
day=marriage_day,
description="Marriage",
event_id=fid * FAMILY_ID_OFFSET
)
marriage_event = create_event_element(marriage_event_data)
return family_elem, marriage_event
def create_gramps_xml_document(
events: List[ET.Element],
people: List[ET.Element],
families: List[ET.Element],
media: List[ET.Element]
) -> ET.ElementTree:
"""Create the complete Gramps XML document."""
# Create root element
database = ET.Element("database")
database.set("xmlns", GRAMPS_XML_NAMESPACE)
# Header
header = ET.SubElement(database, "header")
created = ET.SubElement(header, "created")
created.set("date", datetime.now().strftime('%Y-%m-%d'))
created.set("version", GRAMPS_XML_VERSION)
researcher = ET.SubElement(header, "researcher")
resname = ET.SubElement(researcher, "resname")
resname.text = "Demo Family Generator"
# Tags (empty)
ET.SubElement(database, "tags")
# Events
events_elem = ET.SubElement(database, "events")
for event in events:
events_elem.append(event)
# People
people_elem = ET.SubElement(database, "people")
for person in people:
people_elem.append(person)
# Families
families_elem = ET.SubElement(database, "families")
for family in families:
families_elem.append(family)
# Media objects
media_elem = ET.SubElement(database, "objects")
for media_obj in media:
media_elem.append(media_obj)
return ET.ElementTree(database)
def main() -> None:
"""Main function to generate the demo family."""
print("Generating huge demo family...")
# Generate main family
# Father: John Smith, born 1950, died 2010
father_id = 1
father_handle = gen_handle("PERSON", father_id)
main_family_handle = gen_handle("FAMILY", 1)
father_person, father_birth, father_death, father_additional_xml, _, father_portrait = gen_person(
father_id, "John", "Smith", 1950, 2010, "M",
parentin_families=[main_family_handle]
)
# Mother: Mary Smith, born 1952, died 2015
mother_id = 2
mother_handle = gen_handle("PERSON", mother_id)
mother_person, mother_birth, mother_death, mother_additional_xml, _, mother_portrait = gen_person(
mother_id, "Mary", "Smith", 1952, 2015, "F",
parentin_families=[main_family_handle]
)
# Collect media elements
all_media: List[ET.Element] = []
if father_portrait:
media_handle, file_path = father_portrait
media_elem = create_media_element(media_handle, file_path, "Portrait of John Smith", father_id)
all_media.append(media_elem)
if mother_portrait:
media_handle, file_path = mother_portrait
media_elem = create_media_element(media_handle, file_path, "Portrait of Mary Smith", mother_id)
all_media.append(media_elem)
all_additional_events = father_additional_xml + mother_additional_xml
all_events = [father_birth, mother_birth]
if father_death is not None:
all_events.append(father_death)
if mother_death is not None:
all_events.append(mother_death)
# Generate 15 children
children: List[ET.Element] = []
child_handles: List[str] = []
child_additional_events_map: Dict[int, List[Tuple[str, EventData]]] = {}
child_id = 3
for i in range(15):
gender = "M" if i % 2 == 0 else "F"
first_name = random.choice(MALE_NAMES if gender == "M" else FEMALE_NAMES)
birth_year = 1970 + (i * 2) # Spread births from 1970 to 1998
death_year = birth_year + random.randint(60, 90) if random.random() < 0.3 else None # 30% chance of death
child_handle = gen_handle("PERSON", child_id)
child_person, child_birth, child_death, child_additional_xml, child_additional_tuples, child_portrait = gen_person(
child_id, first_name, "Smith", birth_year, death_year, gender,
childof_families=[main_family_handle]
)
children.append(child_person)
child_handles.append(child_handle)
all_events.append(child_birth)
if child_death is not None:
all_events.append(child_death)
# Store tuples for reuse when regenerating
child_additional_events_map[child_id] = child_additional_tuples
all_additional_events.extend(child_additional_xml)
# Add portrait media if available
if child_portrait:
media_handle, file_path = child_portrait
media_elem = create_media_element(media_handle, file_path, f"Portrait of {first_name} Smith", child_id)
all_media.append(media_elem)
child_id += 1
# Generate family
family_id = 1
family_elem, marriage_event = gen_family(family_id, father_handle, mother_handle, 1969, child_handles)
all_events.append(marriage_event)
families: List[ET.Element] = [family_elem]
# Track person data for regeneration (needed for children who become parents)
person_data: Dict[int, PersonData] = {}
# Store initial person data
person_data[father_id] = PersonData(
handle=father_handle,
name="John",
surname="Smith",
birth=1950,
death=2010,
gender="M",
parentin=[main_family_handle],
childof=[]
)
person_data[mother_id] = PersonData(
handle=mother_handle,
name="Mary",
surname="Smith",
birth=1952,
death=2015,
gender="F",
parentin=[main_family_handle],
childof=[]
)
for i, child_handle in enumerate(child_handles):
child_pid = 3 + i
gender = "M" if i % 2 == 0 else "F"
# Extract name from generated child XML
name_elem = children[i].find(".//first")
first_name = name_elem.text if name_elem is not None and name_elem.text else random.choice(MALE_NAMES if gender == "M" else FEMALE_NAMES)
birth_year = 1970 + (i * 2)
# Extract death year from events if it exists
death_year = None
for event in all_events:
if event.get("id") == f"E{child_pid * EVENT_ID_OFFSET + 1:04d}":
date_elem = event.find(".//dateval")
if date_elem is not None:
date_val = date_elem.get("val", "")
if date_val:
try:
death_year = int(date_val.split("-")[0])
except (ValueError, IndexError):
pass
person_data[child_pid] = PersonData(
handle=child_handle,
name=first_name,
surname="Smith",
birth=birth_year,
death=death_year,
gender=gender,
parentin=[],
childof=[main_family_handle]
)
# Generate grandchildren (children of first 5 children)
grandchildren: List[ET.Element] = []
grandchild_id = child_id
for i in range(5): # First 5 children have children
parent_handle = child_handles[i]
parent_pid = 3 + i
parent_gender = "M" if i % 2 == 0 else "F"
spouse_gender = "F" if parent_gender == "M" else "M"
# Create spouse
spouse_name = random.choice(FEMALE_NAMES if spouse_gender == "F" else MALE_NAMES)
spouse_birth = 1970 + (i * 2) + random.randint(-2, 2)
spouse_handle = gen_handle("PERSON", grandchild_id)
child_family_handle = gen_handle("FAMILY", family_id + 1)
person_data[grandchild_id] = PersonData(
handle=spouse_handle,
name=spouse_name,
surname="Smith",
birth=spouse_birth,
death=None,
gender=spouse_gender,
parentin=[child_family_handle],
childof=[]
)
spouse_person, spouse_birth_event, spouse_death_event, spouse_additional_xml, _, spouse_portrait = gen_person(
grandchild_id, spouse_name, "Smith", spouse_birth, None, spouse_gender,
parentin_families=[child_family_handle]
)
grandchildren.append(spouse_person)
all_events.append(spouse_birth_event)
if spouse_death_event is not None:
all_events.append(spouse_death_event)
all_additional_events.extend(spouse_additional_xml)
if spouse_portrait:
media_handle, file_path = spouse_portrait
media_elem = create_media_element(media_handle, file_path, f"Portrait of {spouse_name} Smith", grandchild_id)
all_media.append(media_elem)
grandchild_id += 1
# Update parent to include parentin reference
person_data[parent_pid].parentin.append(child_family_handle)
# Create 3-5 children per couple
num_grandchildren = random.randint(3, 5)
grandchild_handles: List[str] = []
for j in range(num_grandchildren):
gchild_gender = "M" if j % 2 == 0 else "F"
gchild_name = random.choice(MALE_NAMES if gchild_gender == "M" else FEMALE_NAMES)
gchild_birth = 1995 + (i * 3) + j
gchild_handle = gen_handle("PERSON", grandchild_id)
person_data[grandchild_id] = PersonData(
handle=gchild_handle,
name=gchild_name,
surname="Smith",
birth=gchild_birth,
death=None,
gender=gchild_gender,
parentin=[],
childof=[child_family_handle]
)
gchild_person, gchild_birth_event, gchild_death_event, gchild_additional_xml, _, gchild_portrait = gen_person(
grandchild_id, gchild_name, "Smith", gchild_birth, None, gchild_gender,
childof_families=[child_family_handle]
)
grandchildren.append(gchild_person)
grandchild_handles.append(gchild_handle)
all_events.append(gchild_birth_event)
if gchild_death_event is not None:
all_events.append(gchild_death_event)
all_additional_events.extend(gchild_additional_xml)
if gchild_portrait:
media_handle, file_path = gchild_portrait
media_elem = create_media_element(media_handle, file_path, f"Portrait of {gchild_name} Smith", grandchild_id)
all_media.append(media_elem)
grandchild_id += 1
# Create family for this couple
family_id += 1
fam_elem, fam_marriage = gen_family(family_id, parent_handle, spouse_handle, 1990 + i, grandchild_handles)
families.append(fam_elem)
all_events.append(fam_marriage)
# Regenerate children XMLs with updated family references
# We need to regenerate to update family references, but reuse the same events
children = []
for i, child_handle in enumerate(child_handles):
child_pid = 3 + i
data = person_data[child_pid]
# Reuse the original additional events to ensure consistency
original_additional_events = child_additional_events_map.get(child_pid, [])
child_person, _, _, _, _, _ = gen_person(
child_pid, data.name, data.surname, data.birth, data.death, data.gender,
parentin_families=data.parentin, childof_families=data.childof,
reuse_additional_events=original_additional_events
)
children.append(child_person)
# Add all additional events to events list
all_events.extend(all_additional_events)
# Create complete XML document
people = [father_person, mother_person] + children + grandchildren
tree = create_gramps_xml_document(all_events, people, families, all_media)
# Write XML file with proper formatting
# ET.indent is only available in Python 3.9+, so we'll format manually if needed
try:
ET.indent(tree, space=" ")
except AttributeError:
# Python < 3.9 doesn't have indent, will write without indentation
pass
tree.write("demo_family.gramps", encoding="utf-8", xml_declaration=True)
# Add DOCTYPE declaration (ElementTree doesn't support this directly)
with open("demo_family.gramps", "r", encoding="utf-8") as f:
content = f.read()
# Insert DOCTYPE after XML declaration
doctype = f'<!DOCTYPE database PUBLIC "-//Gramps//DTD Gramps XML 1.7.1//EN"\n"{GRAMPS_XML_DTD}">\n'
content = content.replace('<?xml version="1.0" encoding="utf-8"?>',
f'<?xml version="1.0" encoding="UTF-8"?>\n{doctype}', 1)
with open("demo_family.gramps", "w", encoding="utf-8") as f:
f.write(content)
total_events = len(all_events)
print(f"Generated demo_family.gramps with:")
print(f" - 2 parents (John and Mary Smith)")
print(f" - 15 children")
print(f" - 5 spouses")
print(f" - ~20 grandchildren")
print(f" - Multiple families with marriage events")
print(f" - Birth and death events for all")
print(f" - {len(all_additional_events)} additional events (Baptism, Education, Occupation, etc.)")
print(f" - {len(all_media)} portraits (generated considering age and gender)")
print(f" - Total events: {total_events}")
if __name__ == "__main__":
main()