Files
C.E.L_Slide_test2/docs/history/IMPROVEMENT-PHASE-D.md
kyeongmin c42e01f060 문서 정리: Phase 히스토리 md를 docs/history/로 이동 + 오래된 테스트/에셋 정리
- 루트의 IMPROVEMENT-PHASE-*.md, PHASE-*.md 등 45개 → docs/history/로 이동
- docs/block-tests/ 오래된 블록 테스트 HTML 삭제 (figma_to_html_agent로 대체)
- docs/figma-analysis/, docs/figma-assets/, docs/figma-screenshots/ 정리
- docs/test-*.html 등 초기 테스트 파일 정리
- 참고 페이지/ 스크린샷 정리

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:56:23 +09:00

355 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase D: 이미지 처리 — 실행 상세
> MDX 콘텐츠의 이미지 참조(`![alt](/assets/images/DX1.png)`)를 감지하고,
> 로컬 이미지 파일의 크기를 측정하여 배치 판단에 활용.
> 서버가 localhost에서 돌므로 로컬 파일 접근 가능.
> 원칙: 하드코딩 금지. 모든 판단은 AI 사고. 회귀 금지.
---
## 실행 순서
```
D-0 (이미지 경로 입력 UI + API) ← 선행 필수
→ D-1 (Pillow 유틸리티 + 이미지 추출)
→ D-2 + D-3 (비율 기반 배치 — 프롬프트에 정보 전달)
→ D-4 (텍스트 포함 도표 축소 방지)
→ D-5 (HTML에 이미지 삽입 — base64 또는 절대 경로)
```
---
## D-0: 이미지 경로 입력 UI + API 파라미터 (선행 작업)
### 현재 상태
- static/index.html:176~179 — `fetch('/api/generate', { body: JSON.stringify({ content }) })` → 텍스트만 전송
- src/main.py:35~36 — `SlideRequest` 모델에 `content: str`
- src/pipeline.py:27~29 — `generate_slide(content, manual_layout)` → base_path 없음
### 작업
**1) static/index.html — generate() 함수 수정**
- 텍스트에서 `![...](...)` 패턴 감지 (정규식)
- 발견 시: "이미지가 포함된 콘텐츠입니다. 이미지 파일이 있는 프로젝트 폴더 경로를 입력해주세요" 팝업 (prompt)
- 미발견 시: base_path 없이 기존처럼 전송
- API 요청 body: `{ content, base_path }` (base_path는 선택)
```javascript
// 이미지 참조 감지
const imagePattern = /!\[.*?\]\((.*?)\)/g;
const hasImages = imagePattern.test(content);
let basePath = '';
if (hasImages) {
basePath = prompt(
'이미지가 포함된 콘텐츠입니다.\n' +
'이미지 파일이 있는 프로젝트 폴더 경로를 입력해주세요.\n' +
'예: D:\\ad-hoc\\kei\\content'
) || '';
}
// API 전송
body: JSON.stringify({ content, base_path: basePath })
```
**2) src/main.py — SlideRequest 모델 확장**
```python
class SlideRequest(BaseModel):
content: str
base_path: str = "" # 이미지 기준 폴더 (선택)
```
**3) src/main.py — generate 엔드포인트에서 base_path 전달**
```python
async for event in generate_slide(req.content, base_path=req.base_path):
```
**4) src/pipeline.py — generate_slide() 시그니처 확장**
```python
async def generate_slide(
content: str,
manual_layout: dict[str, Any] | None = None,
base_path: str = "",
) -> AsyncIterator[dict[str, str]]:
```
### 하드코딩 점검
- 없음. 사용자가 경로를 직접 입력. 코드에 경로 고정값 없음 ✅
### 충돌/회귀
- SlideRequest에 base_path 추가: 기본값 `""` → 기존 요청(base_path 없는)과 호환 ✅
- generate_slide() 시그니처에 base_path 추가: 기본값 `""` → 기존 호출과 호환 ✅
- index.html generate() 함수: 이미지 없으면 기존과 동일 동작 ✅
### 수정 파일
- `static/index.html` — generate() 함수
- `src/main.py` — SlideRequest + generate 엔드포인트
- `src/pipeline.py` — generate_slide() 시그니처
---
## D-1: Pillow 이미지 크기 읽기 유틸리티
### 현재 상태
- Pillow import/사용 전무. pyproject.toml에도 없음. src/utils/ 디렉토리 없음.
### 작업
**1) pyproject.toml에 Pillow 추가**
```toml
dependencies = [
...
"Pillow>=10.0",
]
```
**2) src/image_utils.py 신규 제작**
```python
"""이미지 크기 측정 유틸리티."""
from pathlib import Path
from PIL import Image
def get_image_sizes(content: str, base_path: str) -> list[dict]:
"""콘텐츠에서 이미지 참조를 추출하고 로컬 파일 크기를 측정한다.
Args:
content: MDX/텍스트 콘텐츠
base_path: 이미지 파일 기준 폴더 경로
Returns:
[{"path": "/assets/images/DX1.png", "width": 800, "height": 600,
"ratio": 1.33, "orientation": "landscape"}]
"""
import re
if not base_path:
return []
base = Path(base_path)
images = []
for match in re.finditer(r'!\[.*?\]\((.*?)\)', content):
rel_path = match.group(1).strip()
# 상대 경로 해석
abs_path = base / rel_path.lstrip('/')
if abs_path.exists() and abs_path.suffix.lower() in ('.png', '.jpg', '.jpeg', '.gif', '.webp'):
try:
with Image.open(abs_path) as img:
w, h = img.size # 헤더만 읽음
ratio = w / h if h > 0 else 1.0
orientation = "landscape" if ratio > 1.2 else ("portrait" if ratio < 0.8 else "square")
images.append({
"path": rel_path,
"width": w,
"height": h,
"ratio": round(ratio, 2),
"orientation": orientation,
})
except Exception:
images.append({"path": rel_path, "width": 0, "height": 0, "ratio": 0, "orientation": "unknown"})
else:
images.append({"path": rel_path, "width": 0, "height": 0, "ratio": 0, "orientation": "not_found"})
return images
```
### 하드코딩 점검
- ratio 기준 1.2, 0.8: CLAUDE.md 원문 "가로형(ratio > 1.2)", "세로형(ratio < 0.8)" — 문서 기준값이므로 하드코딩 아님 ✅
- 이미지 확장자 목록: 웹 표준 이미지 포맷. 변할 일 없음 ✅
### 충돌/회귀
- 신규 파일 추가만. 기존 코드 변경 없음 ✅
- Pillow는 `Image.open()` 시 헤더만 읽음 (전체 디코딩 안 함) → 성능 안전 ✅
### 수정 파일
- `pyproject.toml` — Pillow 의존성 추가
- 신규 `src/image_utils.py`
---
## D-2 + D-3: 비율 기반 배치 판단
### 현재 상태
- 비율 기반 배치 판단 코드 없음
- B-4에서 1단계 images[] 필드 추가했지만, 실제 크기 정보는 없음
### 작업
pipeline.py에서 D-1 유틸리티 호출 → 이미지 크기/비율 정보를 2단계 Step B와 4단계 Sonnet에 전달
```python
# pipeline.py generate_slide() 내, 2단계 전에:
from src.image_utils import get_image_sizes
image_sizes = get_image_sizes(content, base_path)
if image_sizes:
# analysis에 이미지 크기 정보 추가
analysis["image_sizes"] = image_sizes
```
design_director.py `create_layout_concept()`에서 user_prompt에 이미지 정보 포함:
```python
# 이미지 크기 정보가 있으면 프롬프트에 포함
if analysis.get("image_sizes"):
image_info = "\n".join(
f"- {img['path']}: {img['width']}×{img['height']}px, {img['orientation']}"
for img in analysis["image_sizes"]
)
user_prompt += f"\n\n## 이미지 크기 정보\n{image_info}\n"
```
→ Sonnet(팀장)이 이 정보를 보고 "가로형 → 전체 너비", "세로형 → 텍스트 옆" 등을 판단.
### 하드코딩 점검
- 비율 판단: AI(팀장)가 orientation 정보를 보고 결정 ✅
- 코드는 크기 정보를 전달만. 배치 결정은 AI ✅
### 충돌/회귀
- pipeline.py: 기존 흐름 사이에 이미지 측정 삽입. base_path 없으면 빈 리스트 → 영향 없음 ✅
- design_director.py: user_prompt에 텍스트 추가만. 이미지 없으면 추가 안 함 ✅
- analysis에 image_sizes 추가: 기존 코드는 `.get()` 패턴 → 안전 ✅
### 수정 파일
- `src/pipeline.py` — generate_slide()에서 get_image_sizes() 호출
- `src/design_director.py` — create_layout_concept()에서 이미지 정보 프롬프트 포함
---
## D-4: 텍스트 포함 도표 → 과도한 축소 방지
### 현재 상태
- B-4에서 images[].has_text 필드 추가 (1단계 Kei가 판단)
- 이 정보를 기반으로 "축소하지 마라" 가이드 없음
### 작업
D-2/D-3의 이미지 정보 전달 시, has_text 정보도 함께 포함:
```python
# pipeline.py에서 1단계 analysis의 images[]와 D-1의 image_sizes를 결합
for img_size in image_sizes:
# 1단계에서 판단한 has_text 정보 매칭
for kei_img in analysis.get("images", []):
if kei_img.get("description", "") in img_size.get("path", ""):
img_size["has_text"] = kei_img.get("has_text", False)
```
→ Sonnet 프롬프트에 "has_text: true인 이미지는 텍스트가 포함된 도표이므로 과도하게 축소하지 마라" 가이드
### 하드코딩 점검
- 축소 여부: AI가 판단 ✅
- has_text: 1단계 Kei가 판단 ✅
### 충돌/회귀
- 기존 데이터에 필드 추가만. 없으면 무시됨 ✅
### 수정 파일
- `src/pipeline.py`
---
## D-5: 슬라이드 HTML에 이미지 삽입
### 현재 상태
- 이미지 블록(image-row, image-side-text 등)은 `{{ img.src }}` 슬롯에 경로를 넣지만
- 다운로드 HTML에서 상대 경로는 깨짐 (로컬 파일이므로)
### 작업
렌더링 완료 후, HTML의 이미지 src를 base64 data URI로 변환:
```python
# renderer.py 또는 pipeline.py에서 최종 HTML 후처리
import base64, re
def embed_images(html: str, base_path: str) -> str:
"""HTML의 이미지 src를 base64 data URI로 변환."""
if not base_path:
return html
base = Path(base_path)
def replace_src(match):
src = match.group(1)
abs_path = base / src.lstrip('/')
if abs_path.exists():
mime = 'image/png' if abs_path.suffix == '.png' else 'image/jpeg'
data = base64.b64encode(abs_path.read_bytes()).decode()
return f'src="data:{mime};base64,{data}"'
return match.group(0)
return re.sub(r'src="(/[^"]+\.(?:png|jpg|jpeg|gif|webp))"', replace_src, html)
```
### 하드코딩 점검
- MIME 타입: 확장자 기반 표준 매핑. 하드코딩 아님 ✅
- 이미지 확장자: D-1과 동일 목록 ✅
### 충돌/회귀
- 최종 HTML 후처리. 이미지 없으면 정규식 매칭 없음 → 기존과 동일 ✅
- base_path 없으면 함수 즉시 반환 → 안전 ✅
### 수정 파일
- `src/image_utils.py` (embed_images 함수 추가) 또는 `src/pipeline.py`
---
## 수정 파일 총괄
| 파일 | 항목 | 변경 성격 |
|------|------|----------|
| `static/index.html` | D-0 | generate() 함수에 이미지 감지 + 경로 입력 팝업 |
| `src/main.py` | D-0 | SlideRequest에 base_path 추가 + 엔드포인트 전달 |
| `src/pipeline.py` | D-0, D-2~D-4 | generate_slide() 시그니처 + 이미지 크기 측정 + 프롬프트 전달 |
| `pyproject.toml` | D-1 | Pillow 의존성 추가 |
| 신규 `src/image_utils.py` | D-1, D-5 | get_image_sizes() + embed_images() |
| `src/design_director.py` | D-2, D-3 | user_prompt에 이미지 크기/비율 정보 포함 |
---
## 검증 체크리스트
- [ ] D-0: 이미지 없는 텍스트 → 팝업 안 뜸. 기존과 동일 동작
- [ ] D-0: 이미지 있는 MDX → 팝업 뜨고 경로 입력 → API에 base_path 전달
- [ ] D-0: 팝업에서 취소 → base_path="" → 이미지 처리 스킵 (에러 없음)
- [ ] D-1: Pillow로 이미지 크기 측정. 파일 없으면 width=0, orientation="not_found"
- [ ] D-2/D-3: Sonnet 프롬프트에 이미지 크기/orientation 정보 포함
- [ ] D-4: has_text=true 이미지에 대해 "축소 금지" 가이드 전달
- [ ] D-5: 다운로드 HTML에서 이미지가 base64로 삽입되어 보임
- [ ] 이미지 없는 기존 콘텐츠: 전체 파이프라인 기존과 동일하게 동작 (회귀 없음)
---
## 수정 이력
| 날짜 | 내용 |
|------|------|
| 2026-03-25 | 초안. D-0(선행 UI) 추가. D-5(HTML 이미지 삽입) 추가. 6개 항목. |
| 2026-03-25 | Phase D 전체 구현 완료. 검증 통과. |
## 구현 완료 확인
| 항목 | 검증 결과 |
|------|----------|
| D-0 | SlideRequest에 base_path="" 기본값 추가. generate_slide()에 base_path 파라미터 추가. index.html에 이미지 감지+팝업. 기존 요청(base_path 없는) 호환 ✅ |
| D-1 | pyproject.toml에 Pillow>=10.0 추가. src/image_utils.py 신규 (get_image_sizes + embed_images). Pillow 10.4.0 설치 확인. base_path 없으면 빈 리스트 반환(안전) |
| D-2+D-3 | pipeline.py에서 get_image_sizes() 호출 → analysis["image_sizes"] 저장. design_director.py user_prompt에 이미지 크기/orientation/배치 가이드 포함. 이미지 없으면 추가 안 함(기존 동일) |
| D-4 | design_director.py에서 has_text=true 이미지에 "(텍스트 포함 도표 — 과도한 축소 금지)" 가이드 자동 추가 |
| D-5 | pipeline.py에서 최종 HTML 반환 전 embed_images(html, base_path) 호출. base_path 없으면 원본 그대로 반환(안전) |
### D-0 구현 결과
- `src/main.py`: SlideRequest에 `base_path: str = ""` 추가. generate 엔드포인트에서 `base_path=req.base_path` 전달.
- `src/pipeline.py`: generate_slide() 시그니처에 `base_path: str = ""` 추가.
- `static/index.html`: generate() 함수에서 `![...](...)` 패턴 감지 → prompt()로 경로 입력 → API에 `{ content, base_path }` 전송. 취소 시 `base_path=""` → 이미지 처리 스킵.
### D-1 구현 결과
- `pyproject.toml`: `"Pillow>=10.0"` 의존성 추가.
- `src/image_utils.py` 신규:
- `get_image_sizes(content, base_path)`: MDX에서 `![alt](path)` 추출 → base_path + 상대경로 해석 → Pillow `Image.open().size` → {path, width, height, ratio, orientation} 반환
- `embed_images(html, base_path)`: HTML의 `src="/...png"``src="data:image/png;base64,..."` 변환
- 파일 미발견/에러 시 orientation="not_found"/"error" → 에러 없이 계속 진행
### D-2+D-3+D-4 구현 결과
- `src/pipeline.py`: 1단계 완료 직후 `get_image_sizes()` 호출 → `analysis["image_sizes"]` 저장
- `src/design_director.py`: user_prompt에 이미지 정보 섹션 추가 — 각 이미지의 크기/orientation + "가로형 → 전체 너비", "세로형 → 텍스트 옆", "텍스트 포함 도표 → 축소 금지" 가이드
- has_text 정보는 1단계 Kei의 images[].has_text와 D-1 크기 정보가 결합됨
### D-5 구현 결과
- `src/pipeline.py`: yield result 직전에 `embed_images(html, base_path)` 호출
- base_path 없으면 원본 HTML 그대로 반환 (기존과 동일)