로컬 AI 인프라 노트 (13/15) — Nano Banana 2로 블로그 이미지 자동 생성

Gemini 3.1 Flash Image(Nano Banana 2)를 Blogger API와 연동해 이미지를 자동 생성·삽입하는 파이프라인 구현 기록


핵심 요약

  • Nano Banana 2(Gemini 3.1 Flash Image)는 generate_content API로 이미지를 생성하며, Imagen 4와 호출 구조가 다르다
  • 블로그 HTML을 파싱해 섹션별 이미지 삽입 위치를 자동 결정하는 파이프라인을 구축했다
  • thinking 토큰 포함 실측 비용은 이미지당 약 $0.08이며, 공식 문서의 기준가($0.067)보다 높다

배경

Blogger API 자동 발행 파이프라인이 이미 갖춰진 상황에서, 이미지 생성 자동화를 추가하는 것이 이번 작업의 목표였습니다. 텍스트 전용 블로그 글은 가독성이 떨어지고, 수동으로 이미지를 제작하는 것은 규모가 커질수록 비효율적입니다. 이미 갖춰진 API 파이프라인에 이미지 생성 단계를 끼워 넣는 방식으로 설계했습니다.


본문

1. 모델 선택 — Imagen 4 vs Nano Banana 2

초기에는 Imagen 4(imagen-4.0-generate-001)를 검토했습니다. generate_images API를 사용하며 이미지당 약 $0.02로 저렴합니다. 그러나 텍스트가 포함된 이미지에서 글자가 깨지는 문제가 빈번하게 발생했고, 다이어그램 표현력도 제한적이었습니다.

Nano Banana 2(Gemini 3.1 Flash Image, gemini-3.1-flash-image-preview)는 generate_content API를 사용합니다. Imagen 계열과 다른 SDK 호출 구조를 갖추고 있으며, thinking 토큰을 함께 소비하는 점이 비용 구조에 영향을 미칩니다.

항목 Imagen 4 Nano Banana 2
모델 ID imagen-4.0-generate-001 gemini-3.1-flash-image-preview
API generate_images generate_content
기준가(공식 문서) ~$0.02/img ~$0.067/img
thinking 포함 실측가 ~$0.08/img
텍스트 렌더링 깨짐 빈번 영문 텍스트 깔끔
이미지 품질 단순 일러스트 정교한 다이어그램

비용 메모: 공식 문서의 $0.067은 thinking 토큰을 반영하지 않은 수치입니다. thinking 토큰 오버헤드가 추가되어 실측치는 $0.08/img입니다. 대량 생성 시 예산 계획에는 $0.08 기준을 사용하는 것이 안전합니다.


2. API 호출 구조

Nano Banana 2는 generate_content를 사용하며, 응답 구조도 Imagen 4와 다릅니다.

import google.generativeai as genai
from PIL import Image
import io, base64

genai.configure(api_key=API_KEY)
model = genai.GenerativeModel("gemini-3.1-flash-image-preview")

response = model.generate_content(
    contents=prompt,
    generation_config=genai.types.GenerationConfig(
        response_modalities=["IMAGE", "TEXT"]
    )
)

for part in response.candidates[0].content.parts:
    if hasattr(part, "inline_data") and part.inline_data:
        img_bytes = base64.b64decode(part.inline_data.data)
        img = Image.open(io.BytesIO(img_bytes))

Imagen 4의 generate_imagesresponse.generated_images에서 이미지를 꺼내는 반면, Nano Banana 2는 candidates[0].content.parts를 순회하며 inline_data가 있는 파트를 추출합니다. 두 모델을 혼용할 경우 이미지 추출 로직을 별도로 작성해야 합니다.


3. 프롬프트 엔지니어링 — 한글 프롬프트의 함정

한국어 블로그 제목을 그대로 프롬프트에 넣으면 이미지 내부에 한글이 깨진 형태로 렌더링됩니다. CSS 코드처럼 의미 없는 문자열이 이미지에 등장하는 경우도 있었습니다.

해결 전략:

  1. translate_title_to_concept 함수로 한국어 기술 용어를 영어 시각 개념으로 매핑
  2. "에이전트" → "AI agent"
  3. "데이터 흐름" → "data flow"
  4. "온톨로지" → "ontology"

  5. strip_korean 함수로 매핑 후에도 남은 한글 강제 제거

  6. 프롬프트에 언어 제약 명시: All text must be in English only. No Korean, no Japanese, no Chinese characters.

이 두 단계를 거치면 이미지에 한글이 등장할 확률이 사실상 0으로 수렴합니다.

시각 테마 자동 감지:

섹션 내용에서 키워드를 추출해 시각 힌트를 프롬프트에 삽입합니다.

def build_visual_hint(section_text: str) -> str:
    if "flow" in section_text or "파이프라인" in section_text:
        return "Connected nodes with data flowing between components"
    if "architecture" in section_text or "아키텍처" in section_text:
        return "Modular system architecture with interconnected blocks"
    return "Clean technical diagram on white background"

4. 글 구조 분석 → 이미지 삽입 위치 자동 결정

단순히 글 상단에 이미지를 붙이는 것이 아니라, HTML 구조를 분석해 맥락에 맞는 위치를 선택합니다.

파싱 과정:

from bs4 import BeautifulSoup

def extract_sections(html: str) -> list[dict]:
    soup = BeautifulSoup(html, "html.parser")
    sections = []
    for tag in soup.find_all(["h2", "h3"]):
        content = []
        for sib in tag.next_siblings:
            if sib.name in ["h2", "h3"]:
                break
            content.append(sib.get_text())
        sections.append({
            "title": tag.get_text(),
            "body": " ".join(content),
            "length": len(" ".join(content))
        })
    return sections

배치 규칙:

  • 헤더 이미지: 글 최상단에 항상 1장
  • 섹션 이미지: 본문 200자 이상 + 기술 키워드(구조, 파이프라인, 아키텍처, 흐름 등) 포함 섹션 우선 선택, 최대 2장
  • 총 이미지 수: 글당 최대 3장으로 제한

5. Rate Limit 관리

Nano Banana 2의 Tier 1 제한:

제한 항목 수치
RPM (분당 요청) 100
TPM (분당 토큰) 200,000
RPD (일당 요청) 1,000

이미지 1장당 약 1,300토큰(프롬프트 + 출력 + thinking)이므로 TPM 기준 분당 약 153장이 상한선입니다. 실제 병목은 RPM 100입니다. 안전 마진을 고려해 이미지 간 2초, 포스트 간 3초 간격을 설정했습니다.

RPD 1,000 이내라면 하루 안에 대량 처리가 가능합니다.


6. Blogger API 삽입 연동

생성된 이미지는 base64로 인코딩해 Blogger HTML에 직접 삽입합니다. 외부 이미지 호스팅이 필요 없고, Blogger의 이미지 업로드 제한을 우회합니다.

def insert_image_to_html(html: str, img_bytes: bytes, position: str) -> str:
    b64 = base64.b64encode(img_bytes).decode()
    img_tag = f'<img src="data:image/png;base64,{b64}" style="width:100%;max-width:800px;" />'
    soup = BeautifulSoup(html, "html.parser")
    if position == "header":
        soup.body.insert(0, BeautifulSoup(img_tag, "html.parser"))
    else:
        target = soup.find("h2", string=position)
        if target:
            target.insert_after(BeautifulSoup(img_tag, "html.parser"))
    return str(soup)

Blogger API로 업데이트할 때는 배치 처리 시 요청 간 10초 간격을 반드시 유지해야 Rate Limit 오류를 피할 수 있습니다.


시행착오

Imagen 3 deprecated: 초기에 Imagen 3로 시도했으나 이미 종료된 모델이었습니다. 모델 선택 시 최신 API 문서를 확인하는 것이 필수입니다.

generate_images vs generate_content 혼동: 같은 Google AI SDK 내에서도 Imagen 계열과 Gemini 계열의 호출 구조가 다릅니다. 응답 파싱 로직도 모델별로 분기해야 합니다.

한글 잔여 문제: translate_title_to_concept의 매핑 테이블에 없는 한국어가 프롬프트에 남으면 이미지에 깨진 텍스트가 나타납니다. strip_korean 함수를 추가해 2단계 처리로 해결했습니다.

thinking 토큰 비용 과소평가: 공식 문서 기준가($0.067)만 보고 예산을 잡으면 실제 비용($0.08)이 초과됩니다. thinking 토큰 오버헤드를 예산에 반영해야 합니다.


마무리

Nano Banana 2를 활용한 이미지 자동 생성 파이프라인의 핵심은 세 가지입니다.

  1. API 구조 이해generate_contentgenerate_images는 호출 방식과 응답 파싱이 다르다
  2. 프롬프트 엔지니어링 — 한글 제거 2단계 처리와 시각 테마 자동 감지로 이미지 품질을 제어한다
  3. 비용 계획 — thinking 토큰 포함 실측가($0.08/img)를 기준으로 예산을 산정한다

이 파이프라인은 새 글 발행 시에도 동일하게 적용할 수 있으며, 섹션 분석 로직과 프롬프트 매핑 테이블을 확장해 다양한 글 유형에 대응할 수 있습니다.

시리즈 전체 안내: 시리즈 목차

댓글

이 블로그의 인기 게시물

"LLM 핵심 학습 (1/6) — 기본: 토큰화·임베딩·어텐션·위치 인코딩"

"LLM 핵심 학습 (2/6) — 파인튜닝: LoRA·QLoRA·증류·Adapter"

"ML 기초 학습 (1/9) — 머신러닝과 sklearn: 학습의 좌표계"