"""AI 커버 아트 생성 — DALL·E 3 / gpt-image-1 + 그라데이션 폴백.""" import base64 import logging import os from io import BytesIO import httpx from PIL import Image from . import storage from .gradient import make_gradient_with_title logger = logging.getLogger("music-lab.cover") DALLE_TIMEOUT_S = 90 def _get_api_key() -> str: return os.getenv("OPENAI_API_KEY", "") def _get_model() -> str: return os.getenv("OPENAI_IMAGE_MODEL", "gpt-image-1") async def generate(*, pipeline_id: int, genre: str, prompt_template: str, mood: str = "", track_title: str = "", feedback: str = "") -> dict: """커버 아트 생성. 성공 시 jpg 저장 + URL 반환. 실패 시 그라데이션 폴백. 반환: {"url": str, "used_fallback": bool, "error": str | None} """ out_path = os.path.join(storage.pipeline_dir(pipeline_id), "cover.jpg") used_fallback = False error = None api_key = _get_api_key() model = _get_model() if api_key: try: await _generate_with_dalle(prompt_template, mood, feedback, out_path, api_key=api_key, model=model) except (httpx.HTTPError, httpx.TimeoutException, KeyError, ValueError, OSError) as e: logger.warning("DALL·E 실패 — 폴백: %s", e) error = str(e) used_fallback = True make_gradient_with_title(genre, track_title, out_path) else: used_fallback = True error = "OPENAI_API_KEY 미설정" make_gradient_with_title(genre, track_title, out_path) return { "url": storage.media_url(pipeline_id, "cover.jpg"), "used_fallback": used_fallback, "error": error, } async def _generate_with_dalle(prompt_template: str, mood: str, feedback: str, out_path: str, *, api_key: str, model: str) -> None: prompt = prompt_template if mood: prompt = f"{prompt}, {mood} mood" if feedback: prompt = f"{prompt}. 추가 지시: {feedback}" prompt = f"{prompt}, no text, high quality" async with httpx.AsyncClient(timeout=DALLE_TIMEOUT_S) as client: resp = await client.post( "https://api.openai.com/v1/images/generations", headers={"Authorization": f"Bearer {api_key}"}, json={"model": model, "prompt": prompt, "size": "1024x1024", "n": 1}, ) resp.raise_for_status() data = resp.json()["data"][0] if "url" in data: img_resp = await client.get(data["url"]) img_resp.raise_for_status() img_bytes = img_resp.content elif "b64_json" in data: img_bytes = base64.b64decode(data["b64_json"]) else: raise ValueError("DALL·E response has neither url nor b64_json") # PNG → JPG 변환 with Image.open(BytesIO(img_bytes)) as src: img = src.convert("RGB") img.save(out_path, "JPEG", quality=92)