본문 바로가기
C.W.K.
Stream
Lesson 02 of 04 · published

이미지 보내기

~20 min · vision, images

Level 0Downloader
0 XP0/41 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

Ollama 이미지 형식은 OpenAI 형식 아님

이게 Ollama 두 번째로 흔한 footgun (NDJSON-vs-SSE 다음). Ollama는 이미지를 raw base64 문자열로, message에 붙은 images array에 받아. data:image/png;base64, prefix는 붙이지 마. image_url 객체를 담은 content array 모양도 안 통하고.

  • Ollama: {"role": "user", "content": "describe this", "images": ["iVBORw0KGgoA..."]}
  • OpenAI: {"role": "user", "content": [{"type": "text", "text": "describe"}, {"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBORw0KGgoA..."}}]}

OpenAI 모양을 그대로 Ollama의 native /api/chat에 복사해 넣으면 조용히 텍스트만 가지고 답하는 게 아니라 HTTP 400이 요란하게 떨어져. json: cannot unmarshal array into Go struct field Alias.content of type string. Ollama는 Message.Content를 그냥 문자열로 선언해 놔서 content array는 아예 해독이 안 돼. data: prefix도 똑같이 요란하게 떨어지고. images 항목은 raw base64 바이트로 해독되니까 data:image/png;base64,…는 illegal base64 data at input byte 4가 나와.

두 endpoint가 원하는 모양이 정반대고, 서로 상대방 모양을 거절해:

Endpoint원하는 모양반대 모양을 주면
/api/chat (native)images: ["<맨 base64>"]400 — cannot unmarshal array into … content of type string
/v1/chat/completions (OpenAI 호환)image_url.url = "data:image/png;base64,…"를 담은 content array400 — 맨 base64 문자열엔 invalid image input

이 footgun의 좋은 소식이 그거야. 어느 쪽으로 틀리든 에러 본문이 어느 필드가 터졌는지 말해 줘. 찍지 말고 그걸 읽어.

보내기 전에 resize

Local vision 모델은 보통 max input 차원이 정해져 있어 (~896에서 ~1568 px 사이, 모델마다 달라). Encoding 전에 long edge를 ~1024 px로 줄여. 그보다 큰 이미지는 내부에서 downscale돼서 encode를 낭비하거나, context limit에 걸려.

Code

Vision 모델에 single 이미지 보내기·python
import httpx, base64
from pathlib import Path

def encode_image(path: str | Path) -> str:
    """파일 읽어서 base64 문자열 반환 (data URI prefix 없음)."""
    return base64.b64encode(Path(path).read_bytes()).decode("utf-8")

resp = httpx.post(
    "http://localhost:11434/api/chat",
    json={
        "model": "gemma3:12b",
        "messages": [{
            "role": "user",
            "content": "Describe what you see in this image. Be specific about objects and any text.",
            "images": [encode_image("screenshot.png")],
        }],
        "stream": False,
    },
    timeout=300.0,  # vision 모델은 느려
)
print(resp.json()["message"]["content"])
먼저 resize해서 빠르게 유지·python
from PIL import Image
import io, base64

def encode_resized(path: str, max_edge: int = 1024) -> str:
    img = Image.open(path)
    if img.mode != "RGB":
        img = img.convert("RGB")
    img.thumbnail((max_edge, max_edge), Image.LANCZOS)
    buf = io.BytesIO()
    img.save(buf, format="JPEG", quality=88)
    return base64.b64encode(buf.getvalue()).decode("utf-8")

# 1024px JPEG q88이면 품질은 지키면서 보통 폰 사진의 encode + transfer + decode 시간이 4배쯤 줄어

External links

Exercise

컴퓨터의 진짜 screenshot을 max edge 1024px로 encode. Gemma 3 12B에 이미지 진짜 봐야 답할 수 있는 질문 (예: '브라우저의 탭 제목이 뭐?')과 함께 보내. 400이 뜨면 에러 본문부터 읽어. 필드 이름이 거기 있어. 200인데도 답이 두루뭉술하면 이미지는 해독됐지만 도움이 안 된 거야. 모델의 vision capability랑 얼마나 줄였는지를 봐.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.