#!/usr/bin/env python3 """ Offline variant of generate-model-descriptions.py: no database connection required. Reads a list of popular (brand, model) combos from scripts/popular-models.json, generates Dutch copy via Claude Haiku 4.5, writes results to scripts/generated-model-content.json. Use this when the local DB isn't up. Import later with import-model-content.py. Usage: python scripts/generate-model-descriptions-offline.py # full run python scripts/generate-model-descriptions-offline.py --limit 3 # test a few python scripts/generate-model-descriptions-offline.py --brand nike # filter Output format (array of rows): [ { "brand_slug": "nike", "model_slug": "air-max-90", "model_name": "Air Max 90", "pdp_body": "...", "opener_variants": ["...", "...", "..."], "closer_variants": ["...", "...", "..."], "style_tags": ["retro"] }, ... ] """ import os import sys import json import re import time import argparse from pathlib import Path from dotenv import load_dotenv from anthropic import Anthropic, APIError load_dotenv() MODEL = "claude-haiku-4-5" MAX_TOKENS = 1200 HERE = Path(__file__).parent HERITAGE = HERE / "brand-heritage.json" MODELS_LIST = HERE / "popular-models.json" OUTPUT = HERE / "generated-model-content.json" def slugify(text: str) -> str: return re.sub(r"(^-|-$)", "", re.sub(r"[^a-zA-Z0-9]+", "-", text.strip())).lower() def load_json(path: Path): with open(path, "r", encoding="utf-8") as f: return json.load(f) def save_output(rows): with open(OUTPUT, "w", encoding="utf-8") as f: json.dump(rows, f, ensure_ascii=False, indent=2) def load_existing_output(): if not OUTPUT.exists(): return [] try: return load_json(OUTPUT) except json.JSONDecodeError: return [] def build_prompt(brand_name: str, brand_heritage: str, model_name: str) -> str: heritage_block = brand_heritage or f"{brand_name} is een bekend sneakermerk." return f"""Je bent copywriter voor SneakerPicks, een Nederlandse sneaker-prijsvergelijker. Schrijf in natuurlijk Nederlands, informeel maar professioneel. Geen cliches, geen uitroeptekens. Merk: {brand_name} Merk-achtergrond: {heritage_block} Model: {model_name} Genereer EXACT dit JSON-object, geen extra tekst: {{ "pdp_body": "", "opener_variants": [ "", "", "" ], "closer_variants": [ "", "", "" ], "style_tags": [""] }} Belangrijk: - Noem NOOIT concrete prijzen, maten, kleuren of specifieke shops. - Alleen Nederlands. Geen Engelse zinnen. - Zorg dat elke variant duidelijk anders voelt. - Output uitsluitend geldig JSON, niets eromheen. """ def extract_json(text: str): text = text.strip() if text.startswith("```"): text = text.strip("`") if text.startswith("json"): text = text[4:] start = text.find("{") end = text.rfind("}") if start < 0 or end <= start: return None try: return json.loads(text[start : end + 1]) except json.JSONDecodeError: return None def call_ai(client: Anthropic, prompt: str, retries: int = 2): for attempt in range(retries + 1): try: response = client.messages.create( model=MODEL, max_tokens=MAX_TOKENS, messages=[{"role": "user", "content": prompt}], ) text = "".join( block.text for block in response.content if getattr(block, "type", "") == "text" ) parsed = extract_json(text) if parsed: usage = getattr(response, "usage", None) if usage: parsed["_usage"] = { "input_tokens": usage.input_tokens, "output_tokens": usage.output_tokens, } return parsed print(f" JSON parse failed on attempt {attempt + 1}, retrying...", file=sys.stderr) except APIError as e: wait = 2 ** attempt print(f" API error: {e}. Retrying in {wait}s...", file=sys.stderr) time.sleep(wait) return None def validate(payload): if not isinstance(payload.get("pdp_body"), str) or len(payload["pdp_body"]) < 60: return False for key in ("opener_variants", "closer_variants"): arr = payload.get(key) if not isinstance(arr, list) or len(arr) < 2: return False if not all(isinstance(s, str) and len(s) > 5 for s in arr): return False return True def cost_estimate(usage): if not usage: return 0.0 in_cost = usage.get("input_tokens", 0) / 1_000_000 * 1.0 out_cost = usage.get("output_tokens", 0) / 1_000_000 * 5.0 return in_cost + out_cost def main(): parser = argparse.ArgumentParser() parser.add_argument("--limit", type=int, default=None) parser.add_argument("--brand", type=str, default=None) parser.add_argument("--force", action="store_true", help="Regenerate entries already in output file") args = parser.parse_args() heritage = load_json(HERITAGE) models_by_brand = load_json(MODELS_LIST) existing = load_existing_output() done = {(r["brand_slug"], r["model_slug"]) for r in existing} if not args.force else set() # Flatten to list of tasks tasks = [] for brand_slug, model_names in models_by_brand.items(): if args.brand and args.brand != brand_slug: continue brand_name = brand_slug.replace("-", " ").title() if brand_slug == "asics": brand_name = "ASICS" elif brand_slug == "new-balance": brand_name = "New Balance" for model_name in model_names: model_slug = slugify(model_name) if (brand_slug, model_slug) in done: continue tasks.append((brand_slug, brand_name, model_slug, model_name)) if args.limit: tasks = tasks[: args.limit] print(f"Processing {len(tasks)} models (already done: {len(done)})") print(f"Output: {OUTPUT}") print() client = Anthropic() results = list(existing) total_cost = 0.0 ok = 0 fail = 0 for idx, (bslug, bname, mslug, mname) in enumerate(tasks, 1): print(f"[{idx}/{len(tasks)}] {bname} / {mname}") prompt = build_prompt(bname, heritage.get(bslug, ""), mname) payload = call_ai(client, prompt) if not payload or not validate(payload): print(" FAIL") fail += 1 continue usage = payload.pop("_usage", None) cost = cost_estimate(usage) total_cost += cost # Remove any previous entry for same key, then append fresh results = [r for r in results if not (r["brand_slug"] == bslug and r["model_slug"] == mslug)] results.append({ "brand_slug": bslug, "model_slug": mslug, "model_name": mname, "pdp_body": payload["pdp_body"].strip(), "opener_variants": payload["opener_variants"], "closer_variants": payload["closer_variants"], "style_tags": payload.get("style_tags") or [], }) save_output(results) ok += 1 print(f" OK (est. ${cost:.4f}, total ${total_cost:.4f})") time.sleep(0.3) print(f"\nDone. ok={ok} fail={fail} total_est_cost=${total_cost:.2f}") print(f"Wrote {len(results)} rows to {OUTPUT}") if __name__ == "__main__": main()