Files
mobea/generator/backfill.py
T
Frank Schwenk 8abd7ddb59 feat: Archiv-Backfill, responsive Bilder und Site-Meta
Ergänzt Backfill- und Hilfsskripte, erzeugt nach der Pixazo-Generierung
komprimierte srcset-Varianten, validiert leere Bilder und fügt Favicon,
Web Manifest sowie Open-Graph-/Twitter-Tags in die Templates ein.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 18:14:40 +02:00

229 lines
6.8 KiB
Python

#!/usr/bin/env python3
"""Generate missing archive editions for a date range."""
from __future__ import annotations
import argparse
import json
import os
import random
import sys
import time
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
import topics
from generate import (
THEMES,
build_archiv_entries,
build_system_prompt,
build_user_prompt,
call_openrouter,
date_human,
generate_hero_image,
load_archive,
log,
render_template,
save_json_atomic,
write_text_atomic,
)
def load_env_file(path: Path) -> None:
if not path.exists():
return
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
os.environ.setdefault(key.strip(), value.strip())
def parse_date(value: str) -> date:
return date.fromisoformat(value)
def generate_day(
day: date,
api_key: str,
model: str,
pixazo_key: str | None,
site_dir: Path,
data_dir: Path,
archive: list[dict],
*,
update_index: bool,
pixazo_pause: float,
) -> list[dict]:
date_iso = day.isoformat()
if any(entry.get("date") == date_iso for entry in archive):
log(f"skipping {date_iso}, already in archive")
return archive
selected_topics = topics.pick_topics(day)
theme = random.Random(f"theme-{date_iso}").choice(THEMES)
log(
f"generating edition for {date_iso} theme={theme} "
f"topics={[t['name'] for t in selected_topics]}"
)
llm_json = call_openrouter(api_key, model, selected_topics)
hero_image = None
if pixazo_key:
hero_image = generate_hero_image(
pixazo_key,
site_dir,
date_iso,
llm_json["image_prompt"],
llm_json["image_negative_prompt"],
)
if pixazo_pause > 0:
log(f"pausing {pixazo_pause:.0f}s after Pixazo call")
time.sleep(pixazo_pause)
else:
log("PIXAZO_API_KEY not set, skipping hero image")
human_date = date_human(day)
site_url = os.environ.get("SITE_URL", "https://mobea.de")
startup_base = {
"date": date_iso,
"date_human": human_date,
"theme": theme,
"hero_image": hero_image,
"s": llm_json,
"site_url": site_url,
}
archiv_day_html = render_template(
"startup.html.j2",
{**startup_base, "canonical_path": f"/archiv/{date_iso}.html"},
)
archiv_day_path = site_dir / "archiv" / f"{date_iso}.html"
write_text_atomic(archiv_day_path, archiv_day_html)
log(f"wrote {archiv_day_path}")
if update_index:
startup_html = render_template(
"startup.html.j2",
{**startup_base, "canonical_path": "/"},
)
write_text_atomic(site_dir / "index.html", startup_html)
log(f"wrote {site_dir / 'index.html'}")
archive_entry = {
"date": date_iso,
"date_human": human_date,
"backronym": " ".join(llm_json["backronym_words"]),
"tagline": llm_json["tagline"],
"theme": theme,
"topics": [topic["name"] for topic in selected_topics],
"has_image": hero_image is not None,
"prompts": {
"content_system": build_system_prompt(),
"content_user": build_user_prompt(selected_topics),
"image_prompt": llm_json["image_prompt"],
"image_negative_prompt": llm_json["image_negative_prompt"],
},
}
archive = [entry for entry in archive if entry.get("date") != date_iso]
archive.append(archive_entry)
save_json_atomic(data_dir / "archive.json", archive)
log(f"updated {data_dir / 'archive.json'}")
return archive
def main() -> int:
parser = argparse.ArgumentParser(description="Backfill mobea archive editions")
parser.add_argument("--from", dest="from_date", required=True, help="Start date (YYYY-MM-DD)")
parser.add_argument("--to", dest="to_date", default=None, help="End date (YYYY-MM-DD), default today UTC")
parser.add_argument("--site-dir", default=None)
parser.add_argument("--data-dir", default=None)
parser.add_argument("--env-file", default=None)
parser.add_argument(
"--pixazo-pause",
type=float,
default=30.0,
help="Seconds to wait after each Pixazo image generation (default: 30)",
)
args = parser.parse_args()
repo_root = Path(__file__).resolve().parent.parent
env_file = Path(args.env_file) if args.env_file else repo_root / ".env"
load_env_file(env_file)
api_key = os.environ.get("OPENROUTER_API_KEY")
model = os.environ.get("OPENROUTER_MODEL")
pixazo_key = os.environ.get("PIXAZO_API_KEY")
site_dir = Path(args.site_dir or os.environ.get("SITE_DIR", repo_root / "public"))
data_dir = Path(args.data_dir or os.environ.get("DATA_DIR", repo_root / "data"))
if not api_key:
log("error: OPENROUTER_API_KEY is required")
return 1
if not model:
log("error: OPENROUTER_MODEL is required")
return 1
start = parse_date(args.from_date)
end = parse_date(args.to_date) if args.to_date else datetime.now(timezone.utc).date()
if start > end:
log("error: --from must not be after --to")
return 1
site_dir.mkdir(parents=True, exist_ok=True)
data_dir.mkdir(parents=True, exist_ok=True)
archive = load_archive(data_dir)
current = start
days: list[date] = []
while current <= end:
days.append(current)
current += timedelta(days=1)
log(f"backfill {start.isoformat()} .. {end.isoformat()} ({len(days)} day(s))")
for idx, day in enumerate(days):
try:
archive = generate_day(
day,
api_key,
model,
pixazo_key,
site_dir,
data_dir,
archive,
update_index=(idx == len(days) - 1),
pixazo_pause=args.pixazo_pause,
)
except Exception as exc:
log(f"error: failed for {day.isoformat()}: {exc}")
return 1
latest_image = next(
(
f"/img/{entry['date']}.jpg"
for entry in sorted(archive, key=lambda e: e["date"], reverse=True)
if entry.get("has_image")
),
"/og-default.jpg",
)
archiv_html = render_template(
"archiv.html.j2",
{
"entries": build_archiv_entries(archive, site_dir),
"site_url": os.environ.get("SITE_URL", "https://mobea.de"),
"og_image": latest_image,
},
)
write_text_atomic(site_dir / "archiv" / "index.html", archiv_html)
log(f"wrote {site_dir / 'archiv' / 'index.html'}")
log("backfill complete")
return 0
if __name__ == "__main__":
sys.exit(main())