04. 이미지 워크플로 API 직접 호출 v2
이 노트북은 생성, 평가, 재생성, multi-turn refinement를 하나의 흐름으로 연결합니다. 각 단계의 결과를 다음 API 호출의 입력으로 넘기는 방식을 코드에서 바로 확인할 수 있게 구성했습니다.
# Colab 또는 로컬 노트북 실행 환경을 구분하기 위해 sys를 가져옵니다.
import sys
# 패키지 설치 명령을 현재 Python 커널에서 실행하기 위해 subprocess를 가져옵니다.
import subprocess
# 패키지 설치 여부를 확인하기 위해 importlib.util을 가져옵니다.
import importlib.util
# 환경 변수에서 API 키를 읽고 설정하기 위해 os를 가져옵니다.
import os
# API 키를 화면에 노출하지 않고 입력받기 위해 getpass를 가져옵니다.
import getpass
# .env 파일 위치를 다루기 위해 pathlib의 Path를 가져옵니다.
from pathlib import Path
# google.colab 모듈이 있으면 현재 런타임이 Google Colab이라고 판단합니다.
IN_COLAB = "google.colab" in sys.modules
# 노트북에서 사용하는 import 이름과 pip 패키지 이름을 짝지어 둡니다.
REQUIRED_PACKAGES = {
"openai": "openai>=2.26.0",
"dotenv": "python-dotenv>=1.2.2",
"pydantic": "pydantic>=2.11.0",
"PIL": "pillow>=12.1.1",
"requests": "requests>=2.32.5",
"numpy": "numpy>=2.3.3",
"websockets": "websockets>=15.0.1",
"websocket": "websocket-client>=1.8.0",
"nest_asyncio": "nest-asyncio>=1.6.0",
}
# 필요한 패키지가 없을 때만 현재 커널에 조용히 설치하는 함수를 정의합니다.
def ensure_package(import_name: str, package_name: str) -> None:
# import 가능한 상태면 다시 설치하지 않습니다.
if importlib.util.find_spec(import_name) is not None:
# 이미 설치되어 있으므로 함수를 바로 끝냅니다.
return
# 현재 Python 커널 기준으로 필요한 패키지를 설치합니다.
subprocess.check_call(
# sys.executable을 사용해 현재 노트북 커널과 같은 환경에 설치합니다.
[sys.executable, "-m", "pip", "install", "-q", package_name]
)
# Colab에서는 필요한 패키지를 실행 전에 한 번 점검합니다.
if IN_COLAB:
# import 이름과 패키지 이름 쌍을 하나씩 확인합니다.
for import_name, package_name in REQUIRED_PACKAGES.items():
# 빠진 패키지만 설치합니다.
ensure_package(import_name, package_name)
# 환경 변수 로딩을 위해 load_dotenv를 가져옵니다.
from dotenv import load_dotenv
# 현재 작업 디렉터리의 .env 파일을 먼저 읽습니다.
load_dotenv(Path.cwd() / ".env")
# OPENAI_API_KEY가 없으면 Colab Secrets 또는 수동 입력으로 보완합니다.
if not os.getenv("OPENAI_API_KEY"):
# 우선 비어 있는 기본값으로 시작합니다.
secret_key = None
# Colab 환경이면 Secrets에서 키를 읽어 봅니다.
try:
from google.colab import userdata
# Colab Secrets에 저장된 OPENAI_API_KEY를 읽습니다.
secret_key = userdata.get("OPENAI_API_KEY")
# Colab이 아니거나 Secrets 접근에 실패해도 아래 fallback으로 진행합니다.
except Exception:
# Secrets를 읽지 못했으므로 None을 유지합니다.
secret_key = None
# Secrets에서 키를 얻었으면 환경 변수에 반영합니다.
if secret_key:
os.environ["OPENAI_API_KEY"] = secret_key
# Secrets가 없으면 사용자가 직접 입력할 수 있게 합니다.
else:
# 입력 과정에서 앞뒤 공백을 제거합니다.
entered_key = getpass.getpass("OPENAI_API_KEY를 입력하세요: ").strip()
# 사용자가 값을 입력했을 때만 환경 변수에 저장합니다.
if entered_key:
os.environ["OPENAI_API_KEY"] = entered_key
# 여기까지도 키가 없으면 실습을 더 진행하지 않고 중단합니다.
if not os.getenv("OPENAI_API_KEY"):
raise RuntimeError(
"OPENAI_API_KEY가 없습니다. Colab Secrets, 수동 입력, 또는 .env 파일로 설정해 주세요."
)
# 현재 실행 환경을 확인용으로 출력합니다.
print(f"개발환경: {'Colab' if IN_COLAB else '로컬'}")
# API 키 준비가 끝났음을 출력합니다.
print("OPENAI_API_KEY 준비 완료")
개발환경: 로컬
OPENAI_API_KEY 준비 완료
실습 전에 보는 핵심 개념과 API
핵심 개념
- 워크플로는 한 번의 생성으로 끝내지 않고, 이전 결과를 다시 입력으로 삼아 품질을 올리는 반복 구조입니다.
- 분석 결과를 다음 프롬프트에 반영하면 단순 생성보다 더 의도적인 개선이 가능합니다.
- previous_response_id를 사용하면 이전 응답 맥락을 이어 받아 멀티턴 수정 과정을 만들 수 있습니다.
- 스트리밍 이미지 생성은 최종 결과가 완성되기 전에 중간 진행 상태를 먼저 확인할 수 있게 해 줍니다.
API 소개
- client.responses.create(...): 생성, 재생성, 멀티턴 refinement(정교화)를 모두 한 인터페이스에서 다루는 중심 API입니다.
- stream=True와 부분 이미지 이벤트는 이미지 생성 진행 상황을 단계적으로 보여줍니다.
- 이 노트북은 생성, 평가, 재생성, 멀티턴 refinement를 하나의 실험 흐름으로 묶습니다.
API 호출부와 주요 매개변수
response = client.responses.create(
model=MODEL_NAME,
input=prompt,
tools=[{"type": "image_generation"}],
)
followup = client.responses.create(
model=MODEL_NAME,
previous_response_id=response.id,
input="배경을 더 따뜻하게 수정해 주세요.",
tools=[{"type": "image_generation"}],
)
stream = client.responses.create(
model=MODEL_NAME,
input=prompt,
tools=[{"type": "image_generation"}],
stream=True,
partial_images=1,
)
- model
- 보통 gpt-5, gpt-5-mini, gpt-5.5처럼 Responses API에서 이미지 도구를 호출할 수 있는 모델을 넣습니다.
- 기본값: 실질적인 기본값이 없으므로 명시하는 것이 안전합니다.
- input
- 현재 턴에서 모델에게 줄 프롬프트 또는 메시지입니다.
- 초안 생성, 평가 지시, 후속 수정 요청 같은 텍스트가 들어갈 수 있습니다.
- 기본값: 없습니다.
- tools
- [{"type": "image_generation"}]처럼 이미지 생성 도구를 허용합니다.
- Responses API에서는 이 도구를 통해 대화 안에서 이미지 생성과 수정 흐름을 이어갈 수 있습니다.
- 기본값: 생략하면 도구를 사용하지 않는 일반 응답으로 처리됩니다.
- previous_response_id
- 이전 응답의 문맥을 이어받아 후속 요청을 보낼 때 사용합니다.
- 같은 초안을 점진적으로 다듬는 멀티턴 워크플로의 핵심 연결값입니다.
- 기본값: 없습니다. 첫 턴에서는 넣지 않고, 후속 턴에서만 사용합니다.
- stream
- 가능한 값은 true, false입니다.
- true이면 최종 응답을 기다리지 않고 이벤트를 순차적으로 받을 수 있습니다.
- 기본값: false로 생각하면 됩니다.
- partial_images
- 중간 이미지 진행 결과를 몇 개까지 받을지 정하는 정수 옵션입니다.
- 실습에서는 1처럼 작게 두어 이벤트 구조를 확인하는 용도로 자주 사용합니다.
- 기본값: 생략하면 부분 이미지 이벤트를 사용하지 않는 흐름으로 보면 됩니다.
요청/응답 필드에서 볼 것
- 워크플로 실습에서는 요청마다 현재 턴의 input과 이전 턴의 previous_response_id를 같이 읽고, 지금 단계가 초안 생성인지 수정 지시인지 먼저 구분해 보세요.
- 일반 응답에서는 response.id를 바로 기록해 두는 습관이 중요합니다. 이 값이 있어야 다음 셀이나 다음 턴에서 같은 흐름을 이어갈 수 있습니다.
- 스트리밍 응답을 볼 때는 event.type을 먼저 출력해 어떤 이벤트가 오는지 순서를 파악하고, 부분 이미지 이벤트가 오면 즉시 저장해 최종 결과와 비교해 보는 것이 좋습니다.
- 최종 이미지 데이터는 마지막 image_generation 결과 필드에서 꺼내며, 초안 이미지와 수정 이미지의 파일 경로를 나란히 남겨 두면 변화 비교가 쉬워집니다.
- 반복 생성 품질을 비교하려면 각 턴의 프롬프트, 응답 ID, 출력 파일, usage를 한 JSON에 함께 남겨 두는 방식이 가장 실습 친화적입니다.
1단계. 실행 환경 준비
워크플로는 앞 단계의 출력이 다음 단계 입력이 되므로 파일 경로와 응답 ID를 추적해야 합니다. 준비 셀에서 저장 위치와 직접 파싱 함수를 마련합니다.
# 운영체제 환경 변수를 읽기 위해 os 모듈을 가져옵니다.
import os
# JSON 메타데이터를 저장하고 보기 좋게 출력하기 위해 json 모듈을 가져옵니다.
import json
# 이미지의 base64 문자열을 파일 바이트로 바꾸기 위해 base64 모듈을 가져옵니다.
import base64
# API 호출 시간을 측정하기 위해 time 모듈을 가져옵니다.
import time
# 파일 경로를 운영체제에 맞게 안전하게 다루기 위해 Path를 가져옵니다.
from pathlib import Path
# 타입 힌트에서 응답 객체의 유연한 구조를 표현하기 위해 Any를 가져옵니다.
from typing import Any
# .env 파일에 저장된 OPENAI_API_KEY를 현재 프로세스 환경 변수로 올리기 위해 load_dotenv를 가져옵니다.
from dotenv import load_dotenv
# OpenAI API를 직접 호출하기 위해 공식 SDK의 OpenAI 클라이언트를 가져옵니다.
from openai import OpenAI
# 실습용 기준 폴더를 현재 실행 위치로 저장합니다.
CURRENT_DIR = Path.cwd()
# 루트에서 실행할 때 사용할 이미지 트랙 폴더를 찾습니다.
if (CURRENT_DIR / "05.image").exists():
# 워크스페이스 루트에서 실행 중이면 05.image 폴더를 실습 루트로 사용합니다.
IMAGE_ROOT = CURRENT_DIR / "05.image"
# 하위 실습 폴더에서 실행할 때를 처리합니다.
elif "05.image" in [part for part in CURRENT_DIR.parts]:
# 현재 경로 조각 중 05.image까지를 찾아 실습 루트로 복원합니다.
IMAGE_ROOT = Path(*CURRENT_DIR.parts[: CURRENT_DIR.parts.index("05.image") + 1])
# 예상 밖 위치에서 실행할 때도 상대 경로로 실습 루트를 구성합니다.
else:
# 현재 위치 기준의 05.image 폴더를 실습 루트로 사용합니다.
IMAGE_ROOT = CURRENT_DIR / "05.image"
# .env 파일이 있는 워크스페이스 루트는 이미지 트랙 폴더의 상위입니다.
WORKSPACE_ROOT = IMAGE_ROOT.parent
# 생성물 저장 폴더를 정합니다.
OUTPUT_DIR = IMAGE_ROOT / "output" / "v2"
# 생성물 저장 폴더가 없으면 새로 만듭니다.
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# 워크스페이스 루트의 .env 파일을 읽습니다.
load_dotenv(WORKSPACE_ROOT / ".env")
# API 키가 없으면 뒤쪽 API 호출이 모호하게 실패하므로 여기서 명확히 중단합니다.
if not os.getenv("OPENAI_API_KEY"):
# 교육생이 바로 해결할 수 있도록 필요한 환경 변수 이름을 메시지에 포함합니다.
raise RuntimeError("OPENAI_API_KEY가 없습니다. 워크스페이스 루트의 .env 파일을 확인해 주세요.")
# OpenAI API 클라이언트를 생성합니다.
client = OpenAI()
# Responses API에서 이미지 생성과 이미지 이해를 함께 실습할 모델명을 읽습니다.
RESPONSES_MODEL = os.getenv("OPENAI_RESPONSES_MODEL", "gpt-5-mini")
# Images API에서 이미지 생성과 편집을 실습할 모델명을 읽습니다.
IMAGE_MODEL = os.getenv("OPENAI_IMAGE_API_MODEL", "gpt-image-1-mini")
# 워크스페이스 기준 상대 경로를 보여 주는 작은 함수를 정의합니다.
def workspace_path(path: Path) -> str:
# 절대 경로를 워크스페이스 루트 기준의 / 구분 경로로 바꿉니다.
return path.resolve().relative_to(WORKSPACE_ROOT.resolve()).as_posix()
# base64 이미지 문자열을 PNG 파일로 저장하는 작은 함수를 정의합니다.
def save_base64_png(image_base64: str, path: Path) -> Path:
# base64 문자열을 실제 이미지 바이트로 디코딩합니다.
image_bytes = base64.b64decode(image_base64)
# 디코딩한 바이트를 지정한 파일에 씁니다.
path.write_bytes(image_bytes)
# 저장된 파일 경로를 호출한 쪽에 반환합니다.
return path
# 응답 객체에서 이미지 생성 결과의 base64 문자열을 직접 찾아내는 함수를 정의합니다.
def first_image_base64(response: Any) -> str:
# Responses API의 output 배열을 하나씩 확인합니다.
for item in getattr(response, "output", []) or []:
# 이미지 생성 도구 호출에는 result 또는 result 배열에 base64가 들어올 수 있습니다.
result = getattr(item, "result", None)
# result가 문자열이면 바로 이미지 base64로 사용합니다.
if isinstance(result, str) and result:
return result
# result가 리스트이면 첫 번째 문자열 값을 찾습니다.
if isinstance(result, list):
# 리스트 안의 각 후보를 확인합니다.
for candidate in result:
# 후보가 문자열이면 이미지 base64로 반환합니다.
if isinstance(candidate, str) and candidate:
return candidate
# SDK 버전에 따라 b64_json이라는 이름으로 들어오는 경우도 확인합니다.
b64_json = getattr(item, "b64_json", None)
# b64_json이 문자열이면 반환합니다.
if isinstance(b64_json, str) and b64_json:
return b64_json
# 어떤 위치에서도 찾지 못하면 원인 파악을 위해 상태를 포함해 중단합니다.
raise RuntimeError(f"이미지 base64 결과를 찾지 못했습니다. status={getattr(response, 'status', None)}")
# 환경과 저장 위치를 출력합니다.
print(f"이미지 실습 폴더: {IMAGE_ROOT}")
# 결과 저장 위치를 출력합니다.
print(f"결과 저장 폴더: {OUTPUT_DIR}")
# 사용할 모델명을 출력합니다.
print(f"Responses 모델: {RESPONSES_MODEL}")
# 사용할 이미지 모델명을 출력합니다.
print(f"Images 모델: {IMAGE_MODEL}")
이미지 실습 폴더: c:\Users\USER\Desktop\AS.1.1\d02\05.image
결과 저장 폴더: c:\Users\USER\Desktop\AS.1.1\d02\05.image\output\v2
Responses 모델: gpt-5-mini
Images 모델: gpt-image-1-mini
2단계. 첫 이미지 생성과 텍스트 리뷰
먼저 이미지 생성 도구로 초안을 만들고, 같은 Responses API를 다시 호출해 개선 지시문을 얻습니다. 이렇게 하면 사람이 직접 프롬프트를 다시 쓰는 과정을 코드로 자동화할 수 있습니다.
# 초안 이미지 프롬프트를 작성합니다.
draft_prompt = "OpenAI API 수업 첫 화면에 사용할 친근한 데이터 학습 포스터 이미지를 만들어 주세요."
# Images API 호출 시작 시간을 기록합니다.
started = time.perf_counter()
# Images API를 직접 호출해 초안 이미지를 생성합니다.
draft_response = client.images.generate(
# 이미지 생성 전용 모델을 지정합니다.
model=IMAGE_MODEL,
# 초안 이미지 프롬프트를 전달합니다.
prompt=draft_prompt,
# 한 장만 생성합니다.
n=1,
# 워크숍 자료에 쓰기 쉬운 정사각형 크기를 지정합니다.
size="1024x1024",
# 품질과 비용의 균형을 위해 medium을 사용합니다.
quality="medium",
)
# API 호출 시간을 계산합니다.
elapsed = time.perf_counter() - started
# 초안 이미지 base64를 읽습니다.
draft_base64 = draft_response.data[0].b64_json
# 초안 이미지 저장 경로를 정합니다.
draft_path = OUTPUT_DIR / "04-02_workflow_draft.png"
# 초안 이미지를 저장합니다.
save_base64_png(draft_base64, draft_path)
# 초안 이미지를 data URL로 변환합니다.
draft_data_url = "data:image/png;base64," + draft_base64
# 초안 리뷰를 Responses API에 요청합니다.
review_response = client.responses.create(
# 이미지 이해 모델을 지정합니다.
model=RESPONSES_MODEL,
# 리뷰어 역할을 지시합니다.
instructions=(
"당신은 교육 콘텐츠 아트디렉터입니다. "
"이미지를 보고 다음 생성 프롬프트에 반영할 개선점을 제안하세요."
),
# 초안 이미지와 리뷰 요청을 함께 전달합니다.
input=[
{
"role": "user",
"content": [
{
"type": "input_text",
"text": (
"이 이미지를 더 선명한 수업 대표 이미지로 만들기 위한 "
"개선 프롬프트를 작성해 주세요. 5문장 이내의 한국어 문단으로 답하세요."
),
},
{
"type": "input_image",
"image_url": draft_data_url,
"detail": "high",
},
],
}
],
# 리뷰 문장은 유지하되 과도한 추론 토큰 사용을 줄이기 위해 low 강도를 사용합니다.
reasoning={"effort": "low"},
# 짧은 개선 지시문을 안정적으로 받기 위해 출력 토큰 한도를 조금 넉넉히 둡니다.
max_output_tokens=1200,
)
# 리뷰 텍스트를 읽습니다.
review_text = (review_response.output_text or "").strip()
# 텍스트가 비어 있으면 바로 원인을 파악할 수 있게 중단합니다.
if not review_text:
raise RuntimeError(
"Responses API 응답에서 review_response.output_text가 비어 있습니다. max_output_tokens 또는 reasoning 설정을 확인해 주세요."
)
# 리뷰 텍스트를 파일로 저장합니다.
(OUTPUT_DIR / "04-02_workflow_review.txt").write_text(
review_text + "\n",
encoding="utf-8",
)
# 초안과 리뷰 위치를 출력합니다.
print(f"초안 이미지: {workspace_path(draft_path)}")
# 리뷰 미리보기를 출력합니다.
print(review_text[:500])
초안 이미지: 05.image/output/v2/04-02_workflow_draft.png
타이틀을 더 읽기 쉽도록 굵고 산세리프 계열의 고대비 폰트로 크게 배치하고, 부제목(예: “기초부터 실무까지”)을 작은 글씨로 추가해 정보 계층을 명확히 해주세요. 배경은 단색이나 부드러운 그라데이션으로 단순화하고, 아이콘(원형 차트, 막대그래프, 데이터베이스 등)은 선 굵기와 색상을 통일해 가독성을 높이며 과도한 디테일을 줄여주세요. 중앙 인물은 약간 축소해 노트북과 그래프 아이콘에 여백을 확보하고, 주요 포인트에는 강조색(예: 주황/청록 계열)으로 시각적 포커스를 주십시오. 고해상도(최소 3000px 긴변) 벡터 스타일로 작업하고, 색약 접근성을 고려한 색상 대비 확인을 포함해주세요. 전체적으로 깔끔한 레이아웃, 일관된 아이콘 스타일, 명확한 타이포그래피로 교육용 대표 이미지임을 즉시 전달하도록 요청하십시오.
3단계. 리뷰 기반 재생성과 multi-turn refinement
두 번째 호출은 리뷰 내용을 프롬프트에 합쳐 이미지를 다시 생성합니다. 이어서 추가 개선 지시를 프롬프트에 덧붙여 한 번 더 이미지를 다듬습니다.
# 리뷰를 반영한 재생성 프롬프트를 만듭니다.
regeneration_prompt = (
"다음 리뷰를 반영해 더 좋은 교육용 대표 이미지를 생성해 주세요.\n"
+ review_text
)
# Images API로 리뷰 기반 재생성 요청을 보냅니다.
regen_response = client.images.generate(
# 이미지 생성 모델을 지정합니다.
model=IMAGE_MODEL,
# 리뷰가 포함된 재생성 프롬프트입니다.
prompt=regeneration_prompt,
# 한 장만 생성합니다.
n=1,
# 정사각형 출력 크기를 지정합니다.
size="1024x1024",
# 중간 품질을 사용합니다.
quality="medium",
)
# 재생성 이미지 base64를 추출합니다.
regen_base64 = regen_response.data[0].b64_json
# 재생성 이미지 저장 경로를 정합니다.
regen_path = OUTPUT_DIR / "04-03_workflow_regenerated.png"
# 재생성 이미지를 저장합니다.
save_base64_png(regen_base64, regen_path)
# 추가 개선 프롬프트를 만듭니다.
refine_prompt = (
regeneration_prompt + "\n이미지에서 모든 텍스트를 제거한 깔끔한 버전으로 한 번 더 개선해 주세요."
)
# Images API로 추가 개선 이미지를 생성합니다.
refine_response = client.images.generate(
# 같은 이미지 모델을 사용합니다.
model=IMAGE_MODEL,
# 추가 개선 프롬프트를 전달합니다.
prompt=refine_prompt,
# 한 장만 생성합니다.
n=1,
# 정사각형 출력 크기를 지정합니다.
size="1024x1024",
# 중간 품질을 사용합니다.
quality="medium",
)
# 개선 이미지 base64를 추출합니다.
refine_base64 = refine_response.data[0].b64_json
# 개선 이미지 저장 경로를 정합니다.
refine_path = OUTPUT_DIR / "04-03_workflow_refined.png"
# 개선 이미지를 저장합니다.
save_base64_png(refine_base64, refine_path)
# 결과 위치를 출력합니다.
print(f"재생성 이미지: {workspace_path(regen_path)}")
# 개선 결과 위치를 출력합니다.
print(f"추가 개선 이미지: {workspace_path(refine_path)}")
재생성 이미지: 05.image/output/v2/04-03_workflow_regenerated.png
추가 개선 이미지: 05.image/output/v2/04-03_workflow_refined.png
4단계. Responses API 스트리밍 이벤트 관찰
이미지 생성은 전용 Images API로 안정적으로 실행했고, 마지막에는 앞 단계에서 만든 초안 이미지와 최종 이미지를 다시 입력으로 넣어 Responses API의 스트리밍 구조를 관찰합니다. 스트리밍은 결과가 한 번에 오지 않고 여러 이벤트로 나뉘어 오므로, 이벤트 타입을 기록하면서 이전 단계 산출물을 어떻게 해설에 재사용하는지도 함께 확인합니다.
# 스트리밍 이벤트 타입을 저장할 리스트를 만듭니다.
event_types = []
# 스트리밍으로 받은 텍스트 조각을 저장할 리스트를 만듭니다.
text_deltas = []
# 줄바꿈 직후 첫 공백은 건너뛰기 위한 상태를 둡니다.
skip_leading_space = False
# 초안 이미지를 Responses API 입력용 data URL로 준비합니다.
draft_stream_image_url = "data:image/png;base64," + draft_base64
# 최종 개선 이미지를 Responses API 입력용 data URL로 준비합니다.
refine_stream_image_url = "data:image/png;base64," + refine_base64
# Responses API 스트리밍 요청을 시작합니다.
stream = client.responses.create(
# 텍스트 스트리밍을 수행할 모델입니다.
model=RESPONSES_MODEL,
# 특정 답을 유도하지 않고 실제로 보이는 차이만 설명하도록 역할을 지정합니다.
instructions=(
"당신은 교육용 이미지 품질 리뷰어입니다. "
"반드시 두 이미지를 실제로 비교해 눈에 보이는 차이만 설명하세요. "
"가장 두드러진 차이부터 설명하되, 확실하지 않은 내용은 추정하지 마세요. "
"사용자 프롬프트를 다시 요약하지 말고 텍스트에 없는 내용을 상상하지도 마세요."
),
# 앞 단계의 초안 이미지와 최종 이미지를 비교 입력으로 전달합니다.
input=[
{
"role": "user",
"content": [
{
"type": "input_text",
"text": (
"첫 번째 이미지는 초안이고 두 번째 이미지는 최종본입니다. "
"두 이미지를 실제로 비교해 눈에 보이는 개선점만 3문장으로 한국어 요약해 주세요. "
"먼저 가장 두드러진 차이 1가지를 말하고, 그다음 색감, 구도, 가독성, 요소 정리, 강조된 대상처럼 이미지에서 실제로 확인 가능한 변화만 설명하세요. "
"실제로 보이는 것만 언급하고, 확실하지 않으면 추정하지 마세요."
),
},
{
"type": "input_image",
"image_url": draft_stream_image_url,
"detail": "high",
},
{
"type": "input_image",
"image_url": refine_stream_image_url,
"detail": "high",
},
],
}
],
# 스트리밍 모드를 켭니다.
stream=True,
# 텍스트 설명은 low reasoning으로도 충분하므로 비용과 지연을 줄입니다.
reasoning={"effort": "low"},
# 짧은 요약이 끊기지 않도록 출력 토큰 한도를 둡니다.
max_output_tokens=800,
)
# 스트림 이벤트를 하나씩 순회합니다.
for event in stream:
# 이벤트 타입 문자열을 읽습니다.
event_type = getattr(event, "type", "unknown")
# 이벤트 타입을 기록합니다.
event_types.append(event_type)
# 텍스트 delta 이벤트이면 조각을 저장합니다.
if event_type == "response.output_text.delta":
# delta 텍스트를 리스트에 추가합니다.
text_deltas.append(event.delta)
# delta가 도착할 때마다 바로 출력하되, 마침표를 만나면 줄을 바꿉니다.
for char in event.delta:
# 줄바꿈 직후 첫 공백이면 보기 좋게 건너뜁니다.
if skip_leading_space and char == " ":
continue
# 실제 문자를 만나면 공백 건너뛰기 상태를 해제합니다.
skip_leading_space = False
# 현재 문자를 즉시 출력해 타이핑처럼 보이게 합니다.
print(char, end="", flush=True)
# 마침표를 출력했으면 다음 문장은 새 줄에서 시작하게 합니다.
if char == ".":
print(flush=True)
# 줄바꿈 직후 첫 공백은 생략하도록 상태를 켭니다.
skip_leading_space = True
# 텍스트 조각을 하나의 문자열로 합칩니다.
# 스트리밍 출력이 끝난 뒤 구분용 줄바꿈을 한 번 더 넣습니다.
print()
streamed_text = "".join(text_deltas)
# 이벤트 기록과 텍스트를 JSON으로 저장합니다.
(OUTPUT_DIR / "04-04_workflow_stream_events.json").write_text(
json.dumps(
{"event_types": event_types, "text": streamed_text},
ensure_ascii=False,
indent=2,
)
+ "\n",
encoding="utf-8",
)
# 위에서 실시간으로 출력했으므로 여기서는 빈 줄만 정리한 최종 텍스트를 유지합니다.
# 일부 이벤트 타입을 출력합니다.
print(event_types[:10])
가장 두드러진 차이는 초안 상단의 큰 텍스트("DATA LEARNING")와 주변의 작은 장식 요소들이 최종본에서 제거되어 인물과 노트북이 중앙에 더 크게 배치된 점입니다.
색감은 초안에서 주황색 셔츠와 어두운 청색 노트북을 썼던 것에서 최종본은 티얼색 상의와 흰색 노트북으로 바뀌어 전체적으로 더 차분한 톤이 되었습니다.
구성과 가독성면에서는 아이콘 수가 줄고 배치가 간소화되어 주변 여백이 늘었고, 아이콘 선이 굵고 단순화되어 요소들이 더 명확하게 정리되어 보입니다.
['response.created', 'response.in_progress', 'response.output_item.added', 'response.output_item.done', 'response.output_item.added', 'response.content_part.added', 'response.output_text.delta', 'response.output_text.delta', 'response.output_text.delta', 'response.output_text.delta']'AI System > OpenAI API와 바이브 코딩으로 배우는 AI 서비스 개발' 카테고리의 다른 글
| d02 - 06. Audio - 02.tts (0) | 2026.06.02 |
|---|---|
| d02 - 06. Audio - 01. stt (0) | 2026.06.02 |
| d02 - 05. Image - 03. analysis (0) | 2026.06.02 |
| d02 - 05. Image - 02. editing (0) | 2026.06.02 |
| d02 - 05. Image - 01. generation (0) | 2026.06.02 |