diff --git a/services/image-render/providers/_media.py b/services/image-render/providers/_media.py new file mode 100644 index 0000000..c6ca94d --- /dev/null +++ b/services/image-render/providers/_media.py @@ -0,0 +1,18 @@ +"""b64 이미지 → NAS SMB 경로 저장 → /media/image URL 반환.""" +from __future__ import annotations + +import base64 +import os +import uuid + +IMAGE_MEDIA_ROOT = os.getenv("IMAGE_MEDIA_ROOT", "/mnt/nas/webpage/data/image") +IMAGE_MEDIA_URL_PREFIX = os.getenv("IMAGE_MEDIA_URL_PREFIX", "/media/image") + + +def save_b64_png(task_id: str, b64_data: str) -> str: + os.makedirs(IMAGE_MEDIA_ROOT, exist_ok=True) + fname = f"{task_id}-{uuid.uuid4().hex[:8]}.png" + path = os.path.join(IMAGE_MEDIA_ROOT, fname) + with open(path, "wb") as f: + f.write(base64.b64decode(b64_data)) + return f"{IMAGE_MEDIA_URL_PREFIX}/{fname}" diff --git a/services/image-render/providers/gpt_image.py b/services/image-render/providers/gpt_image.py new file mode 100644 index 0000000..2c13b55 --- /dev/null +++ b/services/image-render/providers/gpt_image.py @@ -0,0 +1,47 @@ +"""GPT Image 2.0 — OpenAI Images API. + +POST https://api.openai.com/v1/images/generations +body {model:"gpt-image-1", prompt, size, n:1} → data[0].b64_json +""" +from __future__ import annotations + +import logging +import os + +import requests + +from nas_client import webhook_update_task +from providers._media import save_b64_png + +logger = logging.getLogger(__name__) +OPENAI_URL = "https://api.openai.com/v1/images/generations" +DEFAULT_MODEL = "gpt-image-1" + + +def run_gpt_image_generation(task_id: str, params: dict) -> None: + try: + if not os.getenv("OPENAI_API_KEY"): + webhook_update_task(task_id, "failed", 0, "", error="OPENAI_API_KEY 미설정 (Windows .env)") + return + webhook_update_task(task_id, "processing", 10, "GPT Image 호출 중...") + body = { + "model": params.get("model") or DEFAULT_MODEL, + "prompt": params["prompt"], + "size": params.get("size") or "1024x1024", + "n": 1, + } + resp = requests.post( + OPENAI_URL, + headers={"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}", "Content-Type": "application/json"}, + json=body, + timeout=120, + ) + if resp.status_code != 200: + webhook_update_task(task_id, "failed", 0, "", error=f"OpenAI {resp.status_code}: {resp.text[:200]}") + return + b64 = resp.json()["data"][0]["b64_json"] + url = save_b64_png(task_id, b64) + webhook_update_task(task_id, "succeeded", 100, "완료", image_url=url) + except Exception as e: + logger.exception("gpt_image task=%s 실패", task_id) + webhook_update_task(task_id, "failed", 0, "", error=str(e)) diff --git a/services/image-render/tests/test_gpt_image.py b/services/image-render/tests/test_gpt_image.py new file mode 100644 index 0000000..2acf30d --- /dev/null +++ b/services/image-render/tests/test_gpt_image.py @@ -0,0 +1,32 @@ +import providers.gpt_image as gi + + +def test_missing_key_reports_failed(monkeypatch): + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + calls = [] + monkeypatch.setattr(gi, "webhook_update_task", lambda *a, **k: calls.append((a, k))) + gi.run_gpt_image_generation("t1", {"prompt": "a cat"}) + # 마지막 호출이 failed + assert calls[-1][0][1] == "failed" + + +def test_success_saves_and_reports_url(monkeypatch): + monkeypatch.setenv("OPENAI_API_KEY", "sk-test") + calls = [] + monkeypatch.setattr(gi, "webhook_update_task", lambda *a, **k: calls.append((a, k))) + monkeypatch.setattr(gi, "save_b64_png", lambda tid, b64: "/media/image/t1.png") + + class FakeResp: + status_code = 200 + + def json(self): + return {"data": [{"b64_json": "ZmFrZQ=="}]} + + def raise_for_status(self): + pass + + monkeypatch.setattr(gi.requests, "post", lambda *a, **k: FakeResp()) + + gi.run_gpt_image_generation("t1", {"prompt": "a cat"}) + succeeded = [c for c in calls if c[0][1] == "succeeded"] + assert succeeded and succeeded[-1][1]["image_url"] == "/media/image/t1.png"