Files
web-page-backend/docs/superpowers/specs/2026-05-17-insta-design-importer-design.md

13 KiB
Raw Blame History

insta-lab Design Importer — Claude Vision으로 이미지 디자인 → Jinja HTML 자동 생성

작성일: 2026-05-17 상태: 사용자 승인 대기 → writing-plans 진입 예정 연관 문서: 2026-05-15-insta-agent-design.md, 2026-05-16-insta-trends-design.md, feedback_external_data_sources.md


1. 목적·배경

insta-lab의 카드 렌더는 현재 templates/default/card.html.j2 한 골격만 사용 (단순 그라데이션 + Noto Sans KR). 사용자가 직접 디자인한 10장 카드 이미지(templates/minimal/pages/insta_card_*.png)를 이미 NAS에 배포한 상태인데, 이 이미지들이 카드 렌더에 반영되지 않음.

이 spec은 사용자가 만든 디자인 이미지를 카드 렌더 파이프라인에 통합하는 메커니즘을 정의한다. 핵심은 Claude Vision으로 10장 PNG를 분석해 페이지별 텍스트 영역·색·폰트·레이아웃을 도출하고, 이를 그대로 모방한 단일 Jinja2 HTML 파일을 자동 생성하는 것이다. 생성된 HTML은 동적 카피(headline, body, cta)를 사용자 디자인 위에 layer로 얹어 일관된 시각 + 동적 텍스트를 동시에 확보한다.


2. 스코프

포함

  • 신규 백엔드 모듈 insta-lab/app/design_importer.py — 10장 PNG → Claude Sonnet Vision → card.html.j2 생성
  • CLI 진입점 python -m app.design_importer <theme_name> (운영자가 한 번씩 실행)
  • 환경변수 INSTA_DEFAULT_THEME 신규 (default="default") — 모든 슬레이트가 이 theme 사용
  • card_renderer.render_slate에 theme 전달 (기존 template 인자 활용, 호출자만 변경)
  • pytest: Vision 호출 mock + 출력 HTML 파싱 검증

제외 (후속)

  • API endpoint POST /api/insta/templates/import — UI에서 트리거 가능
  • card_slates.theme 컬럼 — 슬레이트별 다른 theme 선택
  • 다중 theme 비교/A·B 테스트 UI
  • 자동 theme 추천 (트렌드 카테고리별 다른 theme)

3. 데이터·디렉토리 구조

insta-lab/app/templates/
├── default/                          # 기존 — 폴백 / 초기 골격
│   ├── card.html.j2
│   └── .gitkeep
└── <theme_name>/                     # 사용자 디자인 1세트 (반복 가능)
    ├── pages/                        # 사용자가 git commit으로 업로드
    │   ├── insta_card_start.png      # 의미 있는 이름 권장 (Claude가 페이지 의도 파악에 활용)
    │   ├── insta_card_keyword.png
    │   ├── ... (총 10장)
    │   └── README.md (선택, 디자인 의도 메모)
    └── card.html.j2                  # design_importer가 자동 생성

파일명 컨벤션:

  • 페이지 번호 매핑은 사용자가 제공하지 않음. design_importer가 다음 순서로 자동 매핑:
    1. 파일명에 cover > start > intro 키워드 포함 (우선순위 순서) → page 1 (커버). 여러 파일이 매치되면 가장 앞 키워드를 가진 파일만 선택, 나머지는 본문 풀로
    2. 파일명에 cta > outro > finish > end 키워드 포함 (우선순위 순서) → page 10. 동일하게 첫 매치만 page 10, 나머지는 본문 풀로
    3. 남은 8장은 알파벳 정렬 순으로 page 2~9 (본문)
  • 현재 운영 케이스: insta_card_start.png(start=1순위) → page 1, insta_card_cta.png(cta=1순위) → page 10, insta_card_finish.png는 finish=3순위인데 cta가 이미 page 10이므로 본문 풀로 떨어져 알파벳 순에 따라 page 2~9 어딘가 배치됨
  • 사용자가 매핑을 override하려면 pages/_order.json 파일에 {"insta_card_start.png": 1, "insta_card_finish.png": 10, ...} 명시 가능 (충돌·의도 명시 시 강력 권장)
  • 매핑이 의도와 어긋나면 importer 실행 결과 dict의 page_mapping 필드로 확인 후 _order.json 추가하고 재실행

4. 핵심 모듈 design_importer.py

4-1. Public API

def import_design_theme(theme_name: str) -> dict:
    """templates/<theme>/pages/*.png 10장 → Claude Sonnet Vision → card.html.j2 생성.

    Returns:
        {
            "theme_name": str,
            "html_path": str,
            "page_mapping": {filename: page_no, ...},
            "analysis_summary": str,  # Claude가 도출한 디자인 분석 짧은 요약
            "tokens_used": int,
        }

    Raises:
        ValueError: pages/ 폴더에 PNG 10장 미만이거나 매핑 실패
        anthropic.APIError: Vision 호출 실패 (retry 1회 후)
    """

4-2. 처리 흐름

  1. templates/<theme>/pages/ 폴더 스캔 → PNG 10장 검증 (10장 정확히)
  2. 파일명 → 페이지 매핑 결정 (3장 규칙 + 선택적 _order.json override)
  3. 각 PNG base64 인코딩
  4. Claude Sonnet(claude-sonnet-4-6) Vision 호출 1회:
    • 시스템 프롬프트: 디자이너 역할 + 출력 형식 명세
    • 사용자 메시지: 10장 이미지 + 페이지 매핑 정보 + 변수 명세 (page_no, headline, body, cta)
    • 출력 요청: 단일 Jinja2 HTML 파일 (page_no 분기 + 텍스트 영역 절대 위치 CSS + background-image: url('pages/{{filename}}'))
  5. 응답 HTML 파싱 + Jinja Environment로 sanity render 1회 (분기·문법 검증)
  6. templates/<theme>/card.html.j2에 저장
  7. dict 반환

4-3. Vision 프롬프트 스킴

시스템 프롬프트 (요약):

너는 인스타그램 카드 뉴스 디자인을 모방하는 프론트엔드 디자이너다.
입력: 10장의 카드 디자인 이미지 (각 1080×1350) + 페이지 번호 매핑.
출력: 단일 Jinja2 HTML 파일.

요구사항:
- 컨테이너 width 1080px, height 1350px
- background-image로 해당 페이지 PNG를 url('pages/{{filename}}')로 로드
- 그 위에 텍스트 layer (headline, body, cta) — 각 페이지의 원본 디자인에서
  텍스트가 있던 위치·크기·색을 그대로 모방. 비어 있는 디자인 영역은 layer 위치 추정
- page_no 1~10 분기: {% if page_no == N %}...{% endif %} 구조
- 폰트는 Noto Sans KR (기존 default 템플릿과 동일)
- 출력은 HTML 본문만 (```html 코드펜스 금지)

사용자 메시지에 각 이미지 + filename + page_no 매핑 포함.

4-4. 캐시 / 재실행 정책

  • 이미 card.html.j2가 존재하면 덮어쓰기 (사용자 명시적 재import 의도)
  • 백업: 기존 HTML이 있으면 card.html.j2.bak.YYYYMMDD-HHMMSS로 rename 후 새 파일 작성
  • 분석 결과 캐시 X (재실행할 때마다 최신 결과)

5. CLI 진입점

# 컨테이너 내부에서 실행
docker exec insta-lab python -m app.design_importer <theme_name>

# 결과 stdout (예시)
{
  "theme_name": "minimal",
  "html_path": "/app/app/templates/minimal/card.html.j2",
  "page_mapping": {
    "insta_card_start.png": 1,
    "insta_card_keyword.png": 2,
    ...
    "insta_card_cta.png": 10
  },
  "analysis_summary": "미니멀 카드 — 흰 배경 + 검정 헤드라인 + 회색 본문...",
  "tokens_used": 15234
}

__main__ 가드: argparse로 theme_name 위치 인자 + --force (기존 HTML 백업 없이 덮어쓰기) 옵션. 실패 시 exit 1.


6. 카드 렌더 통합

6-1. 환경변수 추가 (config.py)

INSTA_DEFAULT_THEME = os.getenv("INSTA_DEFAULT_THEME", "default")

6-2. main.py:_bg_create_slate 호출 변경

기존:

await card_renderer.render_slate(sid)

신규:

template_path = f"{INSTA_DEFAULT_THEME}/card.html.j2"
await card_renderer.render_slate(sid, template=template_path)

card_renderer.render_slate는 이미 template 인자를 받으며 default 값이 "default/card.html.j2". 변경 없음.

6-3. card_renderer 폴백 가드

render_slate 시작부에 template 파일 존재 확인 추가:

template_full = Path(_resolve_template_dir()) / template
if not template_full.exists():
    logger.warning("Template %s 없음, default로 폴백", template)
    template = "default/card.html.j2"

→ env에 INSTA_DEFAULT_THEME=minimal 설정했는데 minimal/card.html.j2가 아직 import 안 됐으면 자동 default 폴백.

6-4. 운영 활성화 절차

# 1. 이미지 commit + push (이미 완료 — minimal/pages/ 10장)
# 2. NAS 머지 후 design_importer 실행
docker exec insta-lab python -m app.design_importer minimal

# 3. NAS .env에 추가
echo "INSTA_DEFAULT_THEME=minimal" >> /volume1/docker/webpage/.env

# 4. 컨테이너 재시작 (env 재로드)
docker compose restart insta-lab

7. 에러 처리

상황 처리
pages/ 폴더 없음 또는 PNG 10장 미만 ValueError + 어떤 파일이 빠졌는지 명시. 모든 이미지가 1080×1350인지도 검증 (Pillow로 size 체크)
Vision 호출 실패 (network, rate limit) retry 1회 (5초 대기), 그래도 실패 시 anthropic.APIError 전파
Vision 응답이 HTML이 아님 / Jinja 문법 깨짐 Jinja Environment로 sanity render 시도 → 실패 시 raw 응답을 card.html.j2.error.txt에 저장 + ValueError 전파 (운영자가 수동 수정 가능)
Vision 응답이 max_tokens(16K) 초과 → 잘림 응답 끝이 닫힌 </html> 없으면 잘렸다고 판단, max_tokens 24K로 retry 1회
이미지 base64 인코딩 실패 (파일 깨짐) 어느 파일이 문제인지 로그 + ValueError
_order.json 형식 깨짐 log warning + 자동 매핑 규칙으로 폴백

8. 테스트

insta-lab/tests/test_design_importer.py (~6 케이스)

  1. test_auto_page_mapping_with_cover_and_cta: 의미 이름 파일 10개 → cover→1, cta→10, 나머지 알파벳 순
  2. test_explicit_order_json_overrides: _order.json 있으면 그것 우선
  3. test_validates_exactly_ten_pngs: 9장 또는 11장이면 ValueError
  4. test_validates_image_dimensions: 1080×1350 아닌 이미지 있으면 ValueError + 어떤 파일인지
  5. test_import_generates_html_via_mocked_claude: Anthropic Vision mock, 응답 HTML이 Jinja 렌더 가능한 형식인지 검증
  6. test_import_falls_back_on_jinja_parse_failure: mock이 깨진 HTML 반환 시 ValueError + .error.txt 저장

insta-lab/tests/test_card_renderer.py (기존, 보강 1개)

  1. test_render_falls_back_to_default_when_theme_html_missing: template="ghost/card.html.j2" 지정 시 파일 없어도 default로 폴백 + 정상 PNG 생성

9. 운영 영향

항목 영향
Anthropic 토큰 비용 +1회당 ~15K 토큰 (이미지 10장 × ~1K + 프롬프트 + HTML 출력). Claude Sonnet 단가 기준 ~$0.05/import. 자주 실행 X
빌드 시간 영향 없음 (코드 변경만, 의존성 추가 없음)
카드 렌더 시간 영향 없음 (Playwright는 background-image까지 wait_until="networkidle"로 처리)
디스크 사용자 디자인 PNG 12MB (이미 push됨) + 자동 생성 HTML ~10KB
운영 중 카드 품질 env INSTA_DEFAULT_THEME=minimal 설정 후 다음 슬레이트부터 사용자 디자인 적용. 기존 슬레이트는 default 그대로

10. 마이그레이션 절차

배포 후 사용자가 운영 NAS에서 수동 실행:

  1. PR 머지 → webhook으로 design_importer.py 코드 배포 + minimal/ 디렉토리는 이미 배포됨
  2. SSH NAS:
    docker exec insta-lab python -m app.design_importer minimal
    
  3. 결과 JSON에서 html_pathpage_mapping 확인. 매핑이 의도와 다르면 pages/_order.json로 override 후 재실행
  4. .envINSTA_DEFAULT_THEME=minimal 추가
  5. docker compose restart insta-lab (env 재로드)
  6. 새 슬레이트 1개 만들어서 시각 검증 (Insta 페이지 Trends 탭 또는 수동 트리거)

생성된 card.html.j2가 마음에 안 들면:

  • pages/_order.json으로 페이지 순서 조정 후 importer 재실행
  • 또는 자동 생성 HTML을 사용자가 직접 수정 (importer 재실행 안 함)
  • 백업본 card.html.j2.bak.YYYYMMDD-HHMMSS로 롤백 가능

11. 완료 정의

  • insta-lab/app/design_importer.py 작성, CLI python -m app.design_importer 작동
  • _resolve_page_mapping + 의미 이름 기반 자동 매핑 + _order.json override
  • Vision 호출 mock 기반 pytest 6 케이스 PASS
  • card_renderer.render_slate에 theme 폴백 가드 추가, 테스트 1 케이스 PASS
  • insta-lab/app/config.pyINSTA_DEFAULT_THEME 추가
  • insta-lab/app/main.py:_bg_create_slateINSTA_DEFAULT_THEME 사용
  • docker-compose.yml insta-lab 환경변수에 INSTA_DEFAULT_THEME=${INSTA_DEFAULT_THEME:-default} 추가
  • CLAUDE.md 9.x insta-lab 섹션에 design_importer + INSTA_DEFAULT_THEME 항목 추가
  • 운영 NAS에서 docker exec insta-lab python -m app.design_importer minimal 실행 → card.html.j2 생성 확인
  • .env 설정 + 새 슬레이트 1개 생성 → 시각적으로 minimal 디자인 반영 확인