- 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
821 lines
30 KiB
Python
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()
|