8abd7ddb59
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>
229 lines
6.8 KiB
Python
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())
|