"""FastAPI entrypoint for image-lab. POST /api/image/generate — provider + prompt → Redis push → task_id GET /api/image/tasks/{id} — DB 조회 GET /api/image/providers — 3 provider 메타 """ from __future__ import annotations import json import logging import os import uuid from datetime import datetime, timedelta, timezone from typing import Any, Dict, Optional import redis.asyncio as aioredis from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field from . import db from .internal_router import router as internal_router logger = logging.getLogger(__name__) CORS_ALLOW_ORIGINS = os.getenv("CORS_ALLOW_ORIGINS", "http://localhost:3007,http://localhost:8080") REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379") redis_client = aioredis.from_url(REDIS_URL, decode_responses=False) SUPPORTED_PROVIDERS = {"gpt_image", "nano_banana", "flux"} app = FastAPI() app.include_router(internal_router) app.add_middleware( CORSMiddleware, allow_origins=[o.strip() for o in CORS_ALLOW_ORIGINS.split(",")], allow_credentials=False, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"], allow_headers=["Content-Type"], ) @app.on_event("startup") def on_startup(): db.init_db() @app.get("/health") def health(): return {"ok": True, "service": "image-lab"} @app.get("/api/image/providers") def list_providers(): """3 provider 항상 노출 (key 누락은 worker가 failed 보고).""" return {"providers": [ {"id": "gpt_image", "name": "GPT Image 2.0", "models": ["gpt-image-1"], "sizes": ["1024x1024", "1024x1536", "1536x1024"]}, {"id": "nano_banana", "name": "Nano Banana (Gemini)", "models": ["gemini-2.5-flash-image"], "sizes": ["1024x1024"]}, {"id": "flux", "name": "FLUX (local)", "models": ["flux-schnell", "flux-dev"], "sizes": ["1024x1024", "832x1216", "1216x832"]}, ]} class GenerateRequest(BaseModel): provider: str = Field(..., description="gpt_image|nano_banana|flux") model: Optional[str] = None prompt: str size: Optional[str] = None negative_prompt: Optional[str] = None # Provider 별 추가 키는 extra 허용 extra: Optional[Dict[str, Any]] = None class Config: extra = "allow" async def _push_render_job(task_id: str, job_type: str, params: dict) -> None: """Redis queue:image-render에 push.""" kst = timezone(timedelta(hours=9)) payload = { "task_id": task_id, "kind": "image", "job_type": job_type, "params": params, "submitted_at": datetime.now(kst).isoformat(), } await redis_client.rpush("queue:image-render", json.dumps(payload)) @app.post("/api/image/generate") async def generate_image(req: GenerateRequest): """이미지 생성 — Redis 큐로 Windows image-render에 위임.""" if req.provider not in SUPPORTED_PROVIDERS: raise HTTPException(400, f"지원하지 않는 provider: {req.provider} (supported: {sorted(SUPPORTED_PROVIDERS)})") task_id = str(uuid.uuid4()) params = req.model_dump(exclude_none=True) db.create_task(task_id, req.provider, params) job_type = f"{req.provider}_generation" # gpt_image_generation, nano_banana_generation, flux_generation await _push_render_job(task_id, job_type, params) return {"task_id": task_id, "provider": req.provider} @app.get("/api/image/tasks/{task_id}") def get_task_status(task_id: str): t = db.get_task(task_id) if not t: raise HTTPException(404, "task not found") return t