
하나의 API 키로 이미지 생성 모델을 비교하는 구성과 프롬프트에서 '품질 단어'를 제거한 이야기
요약
OpenAI 호환 통합 엔드포인트를 활용하여 여러 이미지 생성 모델을 하나의 API 키로 비교할 수 있는 구성 방법을 소개합니다. 이미지 생성 API의 비동기 처리 방식과 프롬프트에서 품질 관련 단어가 이미지 질감에 미치는 영향을 분석합니다.
핵심 포인트
- OpenAI 호환 엔드포인트를 통해 모델 전환을 간소화하는 구성법
- 이미지 생성 API의 비동기 태스크 ID 기반 폴링(Polling) 처리 방식
- 품질 관련 단어(8k, ultra-realistic)가 이미지 질감을 저해하는 현상
- 프롬프트에서 품질 단어를 제거할 때 실제 질감이 살아나는 역상관 관계
고민했던 점
이미지 생성 모델을 비교하고 싶을 뿐인데, 준비 과정이 가장 무겁다.
모델마다 계정을 만들고, API 키를 발급하고, 각각의 SDK를 설치하고,
응답 형식(Response format)의 차이를 흡수하는 코드를 작성한다. "이 프롬프트, 다른
모델에서는 어떻게 나올까"를 확인하기까지 30분이 걸린다. 본론은 그게 아니다.
결국, OpenAI 호환 통합 엔드포인트(Unified endpoint)를 경유하는 방식으로 통일했다. 모델 전환이
문자열 하나로 끝날 수 있게 되었으므로, 그 구성과 그곳에서 검증하며 알게 된
프롬프트에 관한 이야기를 쓰겠다.
구성: 이미지 생성은 비동기이므로 2단계로 진행
이 부분이 첫 번째 함정이었다. 이미지 계열은 POST /v1/images/generations가
태스크 ID(Task ID)만 반환하고 즉시 종료된다. 생성이 완료될 때까지 기다려주지 않으므로,
GET /v1/tasks/{task_id}를 폴링(Polling)해야 한다.
텍스트의 chat/completions와
같은 감각으로 client.images.generate()를
호출하면, URL이 아니라 태스크 ID가 반환되어 막히게 된다.
import os
import time
import requests
...
status는 pending / processing / completed / failed의 4가지다.
성공 시에는 results에 이미지 URL 배열이 들어간다. 실패 시 이유가 들어가는 것은
error 객체(code / message / type) 쪽이며, 이 부분을
fail_reason 같은 별칭으로 읽으려고 하면 원인이 전부 unknown으로
뭉뚱그려지므로 주의해야 한다. 실제로 반환된 것은 이것이었다.
{
"status": "failed",
"results": [],
...
즉, 혼잡으로 인한 서버 측의 실패도 이 경로로 들어온다. 재시도(Retry)할지 아니면 다른
모델로 넘길지는 error.code를 보고 결정하게 된다.
이로써 비교는 모델 이름만 교체하면 된다.
PROMPT = "expired 35mm film, pushed two stops, heavy coarse grain..."
for model in ["qwen-image-2.0", "doubao-seedream-5-0", "gpt-image-2"]:
print(model, generate(model, PROMPT))
환경 변수 하나, 의존성은 requests뿐. 준비는 여기까지.
검증: 유통기한이 지난 필름의 질감
구성이 완료되었으니 본론으로. 결론이 직관과 반대였기에 공유한다.
먼저 작동했던 프롬프트.
expired 35mm film, pushed two stops, heavy coarse grain across the entire
frame including sky and shadows, strong halation bleeding around every
highlight, milky blown-out sky, lifted blacks, no true black anywhere,
...
품질 단어를 지우면 질감이 살아난다
ultra-realistic과 8k를 넣고 있는 동안에는, 어떤 모델에서도
"노이즈를 섞은 깨끗한 디지털 이미지"밖에 나오지 않았다.
이유는 생각해보면 당연한데, 품질 단어는 샤프니스(Sharpness)와 마이크로
콘트라스트(Micro contrast)를 최대화하라는 지시가 된다. 유통기한이 지난 필름은 그 두 가지가
없는 상태 그 자체이므로, 정면으로 서로를 상쇄하고 있었다.
지우는 순간 나타났다. 품질 단어를 추가할수록 렌더링(Rendering) 느낌이 강해진다는
역상관 관계였다.
효과를 주는 것은 입자가 아니라 할레이션(Halation)
눈에 띄는 것은 입자(Grain)이지만, 필름처럼 느껴지게 만드는 것은
하이라이트 주변의 할레이션이었다. strong halation bleeding around every highlight를
빼면, 입자가 남아있더라도 필터 느낌으로 떨어진다.
깎아낼 후보를 찾고 있다면, 이 부분은 마지막까지 남겨두는 것이 좋다.
inpaint에서 전체 색상 지정를 남기면 배경이 움직인다
손만 고치고 싶은데 배경 색상이 변하는 현상을 겪었다.
inpaint 프롬프트에 hazy faded sepia-green cast를
남겨두었던 것이
원인이 되어, 모델이 전체적인 톤을 재계산하러 갔던 것이다.
마스크(mask)한 범위의 내용만 적으면 해결된다.
모델을 교체할 때 고려하는 점
질감(texture) 평가는 주관이 개입되므로, 여기서는 실제로 구동하며 확인할 수 있었던 동작만을 기술한다.
동일한 코드 상태에서 qwen-image-2.0과 doubao-seedream-5-0은 둘 다 수용되었다. 다만 이번에는 doubao-seedream-5-0이 2회 연속으로 Service busy로 실패했고, qwen-image-2.0은 한 번에 통과했다. 혼잡도는 시간대에 따라 달라질 것이므로 고정된 우열은 아니지만, "한쪽이 막혀 있을 때 다른 쪽으로 문자열 하나로 우회할 수 있다"는 점이 실무적인 이점이 더 컸다. 위의 루프에서 qwen-image-2.0을 먼저 배치한 이유도 그것이다.
"너무 정돈되지 않은" 것이 요건이 되는 질감에서는 평가 축이 평소와 반대가 된다.
어떤 모델이 맞는지 결국은 돌려보는 수밖에 없으므로, 돌리는 비용을 낮추는 방향으로 맞추는 것이 빠르다.
환경에 대하여
과금은 종량제를 선택했다. 이런 종류의 질감 구현은 버리는 컷이 대량으로 발생하기 때문에, 월정액을 소비하며 "어떤 단어를 지울 것인가"를 찾는 것은 정신적으로 힘들다.
이번에는 Velokey를 통해 1 요청(request)씩 지불하는 방식으로 했다. 국내 발행 카드로 결제 수단이 그대로 통하는 것도 사소하지만 번거로움을 줄여준다.
과금액은 응답(response)의 usage.credits_reserved에 그대로 나타난다. 이번에는 qwen-image-2.0이 1장당 $0.028, doubao-seedream-5-0이 $0.035였다.
20장을 버려도 $0.6 전후이므로, "어떤 단어를 지울 것인가"에 대한 탐색을 횟수로 밀어붙일 수 있다.
요약
- 이미지 계열은 비동기(asynchronous) 방식이다.
images/generations는 태스크 ID(task ID)를 반환할 뿐이며,GET /v1/tasks/{task_id}를 통한 폴링(polling)이 필요하다.- POST 응답에서 태스크 ID가 들어있는 키는
id이다. status는pending/processing/completed/failed이며, URL은results배열에, 실패 이유는error.message에 들어있다.- 혼잡으로 인한
Service busy도failed로 오기 때문에, 재시도(retry)나 다른 모델로의 전환을 상정해 두어야 한다.
- 필름 질감에서는 품질 워드(
8k,ultra-realistic)가 역효과를 낸다. - 효과가 있는 것은 입자(grain)보다 할레이션(halation)이다.
- inpaint에서는 전체 색상 지정(color specification)을 제외한다.
프롬프트의 세부 사항에 대해 질문이 있다면 댓글로 남겨달라.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기