docs(insta-lab): design_importer spec — Claude Vision으로 이미지 → Jinja HTML 자동 생성
사용자가 만든 카드 디자인 이미지 10장을 Claude Sonnet Vision으로 분석해 페이지별 텍스트 영역·색·레이아웃 모방한 단일 card.html.j2 자동 생성. 핵심 결정: - CLI 진입점 (MVP) — API endpoint는 후속 - env INSTA_DEFAULT_THEME 단일 theme — 슬레이트별 선택은 후속 - 파일명 자동 매핑 (cover→1, cta→10, 나머지 알파벳) + _order.json override - card_renderer 폴백 가드 (theme HTML 없으면 default로)
This commit is contained in:
@@ -0,0 +1,275 @@
|
|||||||
|
# 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 (CTA)
|
||||||
|
3. 나머지 8장은 알파벳 정렬 순으로 page 2~9 (본문)
|
||||||
|
- 사용자가 매핑을 override하려면 `pages/_order.json` 파일에 `{"insta_card_start.png": 1, ...}` 명시 가능 (선택)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 핵심 모듈 `design_importer.py`
|
||||||
|
|
||||||
|
### 4-1. Public API
|
||||||
|
|
||||||
|
```python
|
||||||
|
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 진입점
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 컨테이너 내부에서 실행
|
||||||
|
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`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
INSTA_DEFAULT_THEME = os.getenv("INSTA_DEFAULT_THEME", "default")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6-2. `main.py:_bg_create_slate` 호출 변경
|
||||||
|
|
||||||
|
기존:
|
||||||
|
```python
|
||||||
|
await card_renderer.render_slate(sid)
|
||||||
|
```
|
||||||
|
|
||||||
|
신규:
|
||||||
|
```python
|
||||||
|
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 파일 존재 확인 추가:
|
||||||
|
```python
|
||||||
|
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. 운영 활성화 절차
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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개)
|
||||||
|
|
||||||
|
7. `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:
|
||||||
|
```bash
|
||||||
|
docker exec insta-lab python -m app.design_importer minimal
|
||||||
|
```
|
||||||
|
3. 결과 JSON에서 `html_path`와 `page_mapping` 확인. 매핑이 의도와 다르면 `pages/_order.json`로 override 후 재실행
|
||||||
|
4. `.env`에 `INSTA_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.py`에 `INSTA_DEFAULT_THEME` 추가
|
||||||
|
- [ ] `insta-lab/app/main.py:_bg_create_slate`가 `INSTA_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 디자인 반영 확인
|
||||||
Reference in New Issue
Block a user