thats-me/dev/tools/lifeline-images/anchors.py
Kevin Adametz 10251e4f41 Doku und Werkzeuge: Tickets, Audit, Logos, Playwright-MCP
- dev/: Ticketliste, Audit und Vorschläge vom 17.09., Logo-SVGs,
  Lifeline-Bildwerkzeuge.
- AGENTS.md auf den aktuellen Stand (syncQueue statt syncService, kein
  legacy-Ordner mehr).
- .codex/ und __pycache__/ ignoriert; Playwright-MCP in .mcp.json.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 07:00:35 +00:00

86 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""Generate the age anchor portraits for every character of a lifeline.
Base portrait: text-only (children optionally with the parents as references).
Each further anchor: references = base portrait + previous anchor, so identity
stays pinned to the base while aging progresses step by step.
Existing files are kept; delete a file to regenerate it.
"""
import json, os, subprocess, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from gemini import generate, ROOT
lifeline = sys.argv[1]
bible = json.load(open(f'{ROOT}/backend/database/data/lifelines/characters/{lifeline}.json', encoding='utf-8'))
out_root = f'{ROOT}/backend/storage/app/private/lifeline-images/characters/{lifeline}'
PORTRAIT = bible.get('portrait', "Head-and-shoulders portrait, looking straight into the camera, neutral bright daylight, plain light wall background, plain clothing, natural skin, no retouching, no text.")
def era_for(year):
for span, desc in bible['era'].items():
a, b = map(int, span.split('-'))
if a <= year <= b: return desc
return ''
def preview(png):
subprocess.run(['php', '-r', '$im=imagecreatefromstring(file_get_contents($argv[1])); imagejpeg(imagescale($im,500),$argv[2],80);', png, png.replace('.png', '.preview.jpg')])
def run(key, c):
portrait = c.get('portrait', PORTRAIT)
d = f'{out_root}/{key}'; os.makedirs(d, exist_ok=True)
base_age = c.get('base', c['anchors'][0])
base = f'{d}/{base_age}.png'
if not os.path.exists(base):
desc = c.get('baseDescription', c['description'])
year = c['birthYear'] + base_age
prompt = f"Photo from {year} ({era_for(year)}): {portrait} {c.get('ageLabels', {}).get(str(base_age), f'A {base_age}-year-old')} named {c['name']}: {desc}"
generate(prompt, [], base, aspect='3:4'); preview(base)
print(f'{key} {base_age} (base) ok', flush=True)
prev = base
# anchors older than base, ascending; then anchors younger than base, descending
older = [a for a in c['anchors'] if a > base_age]
younger = sorted([a for a in c['anchors'] if a < base_age], reverse=True)
for seq in (older, younger):
prev = base
for age in seq:
png = f'{d}/{age}.png'
if os.path.exists(png):
prev = png; continue
year = c['birthYear'] + age
diff = age - (int(os.path.basename(prev)[:-4]))
direction = 'older' if diff > 0 else 'younger'
change = c['aging'].get(str(age), '')
if age == 0 and c.get('parents'):
parents = c.get('parents', [])
shown = 'the child of the person shown' if len(parents) == 1 else 'the child of the two people shown' if parents else ''
prompt = (f"Photo from {year}: a newborn baby a few days old, {shown}. {change}. "
f"Close-up of the baby's face wrapped in a white blanket, soft hospital light, {bible['style']}")
refs = []
for pk in parents:
pc = bible['characters'][pk]
page = year - pc['birthYear']
refs.append(f"{out_root}/{pk}/{min(pc['anchors'], key=lambda a: abs(a - page))}.png")
else:
label = c.get('ageLabels', {}).get(str(age), f"{age} years old")
prompt = (f"Photo from {year} ({era_for(year)}): {portrait} The exact same individual as in the reference photos, {c['name']}, "
f"now {label}, {abs(diff)} years {direction} than in the second reference photo. Keep every identifying feature "
f"unchanged: face shape, eyes, nose, mouth, ears and any moles, freckles or markings ({c['description']}). Age-related changes only: {change}.")
# From `adultBase` on, the identity reference is that adult anchor
# instead of the (teenage) base portrait.
ident = base
if c.get('adultBase') and age > c['adultBase'] and os.path.exists(f"{d}/{c['adultBase']}.png"):
ident = f"{d}/{c['adultBase']}.png"
refs = [ident, prev] if prev != ident else [ident]
generate(prompt, refs, png, aspect='3:4'); preview(png)
print(f'{key} {age} ok', flush=True)
prev = png
failed = []
for key, c in bible['characters'].items():
if not c.get('anchors'):
continue
try:
run(key, c)
except Exception as ex:
print(f'{key} FAILED, skipping remaining anchors of this character: {ex}', flush=True)
failed.append(key)
print('failed: ' + ', '.join(failed) if failed else 'all anchors ok')