feat(backend): wire exclude_recipe_ids, verify MealPlan votes schema, add image generation service

- api/meal_plans.py: /regenerate now passes exclude_recipe_ids into generate_meal_plan
- planner/generate.py: filter recipe_dicts by exclude_recipe_ids set
- image_generation.py: OpenAI gpt-image-1 client with prompt building, b64_json handling
- main.py: StaticFiles mount at /static for generated images
- admin.py: POST /api/admin/trigger-images endpoint for batch generation
- scripts/generate_images.py: CLI for batch image generation
- docker-compose.yml + nginx: volume mounts for static/images persistence
- Verify MealPlanItem.votes ↔ MealPlanVote relationship is correct; no model bug exists
This commit is contained in:
2026-05-24 19:31:26 -07:00
parent dcdb88595e
commit ae32e650ce
17 changed files with 348 additions and 3 deletions
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Batch-generate recipe images using OpenAI DALL-E 3.
Usage:
python scripts/generate_images.py --limit 10 --missing-only
python scripts/generate_images.py --recipe-id <uuid>
"""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
# Ensure imports resolve regardless of cwd.
_ = sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from app.database import get_db as _get_db
from app.services.image_generation import generate_images_batch
def main() -> int:
parser = argparse.ArgumentParser(description="Generate recipe images via DALL-E 3")
parser.add_argument("--limit", type=int, default=10, help="Max recipes to process")
parser.add_argument("--missing-only", action="store_true", default=True, help="Only recipes without images")
parser.add_argument("--force", action="store_true", default=False, help="Overwrite existing images")
parser.add_argument("--recipe-id", type=str, default=None, help="Generate for a single recipe")
args = parser.parse_args()
# FastAPI DB dependency is a generator; consume it.
# Ensure project-root .env is discoverable before importing config.
project_root = Path(__file__).resolve().parent.parent.parent
os.environ.setdefault("DATABASE_URL", os.environ.get("DATABASE_URL", ""))
os.environ.setdefault("AI_IMAGE_ENABLED", os.environ.get("AI_IMAGE_ENABLED", "true"))
os.environ.setdefault("AI_IMAGE_API_KEY", os.environ.get("AI_IMAGE_API_KEY", ""))
os.environ.setdefault("OPENAI_API_KEY", os.environ.get("OPENAI_API_KEY", ""))
db_gen = _get_db()
db = next(db_gen)
recipe_ids = [args.recipe_id] if args.recipe_id else None
result = generate_images_batch(
db,
recipe_ids=recipe_ids,
missing_only=args.missing_only,
force=args.force,
limit=args.limit,
)
print(f"Total: {result['total']}")
print(f"Succeeded: {result['succeeded']}")
print(f"Failed: {result['failed']}")
print(f"Skipped: {result['skipped']}")
for rid, info in result["results"].items():
print(f" {rid}: {info['status']} {info['url'] or ''}")
return 0
if __name__ == "__main__":
raise SystemExit(main())