- 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>
86 lines
4.6 KiB
Python
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')
|