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>
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user