# 04. LLM 의류 분석 + 고스트 마네킹

## 개요

LLM 서비스는 세 가지 기능을 수행합니다:

1. **의류 분석 (Garment Analysis)**: 사진을 LLM에 보내서 옷의 카테고리, 설명, 태그 추출
2. **고스트 마네킹 (Ghost Mannequin)**: 원본 사진에서 사람을 제거하고 옷만 추출한 이미지 생성
3. **스타일 분석 (Style Analysis)**: 전체 코디 사진을 LLM에 보내서 색상 균형, 핏/실루엣, 디테일 포인트를 평가

## 아키텍처: 프로바이더 패턴

```
BaseLLMProvider (추상 인터페이스)
    │
    ├── GeminiProvider (Gemini API 구현)
    │     ├── analyze_garment()  → Gemini 3 Pro
    │     ├── analyze_style()    → Gemini 3 Pro
    │     └── generate_ghost_mannequin() → Gemini 2.5 Flash Image
    │
    └── OpenAIProvider (OpenAI API 구현)
          ├── analyze_garment()  → GPT-5.4 (Chat Completions)
          ├── analyze_style()    → GPT-5.4 (Chat Completions)
          └── generate_ghost_mannequin() → GPT-5.4 (Responses API + image_generation tool)
```

환경변수 `LLM_PROVIDER`로 전환합니다. 두 프로바이더는 동일한 인터페이스를 구현하므로, 나머지 코드는 어떤 LLM을 쓰는지 알 필요가 없습니다.

---

## 추상 인터페이스 (`app/services/llm/base.py`)

```python
class GarmentAnalysis(BaseModel):
    """LLM이 반환하는 의류 분석 결과 (한 벌)"""
    category_main: str          # "top", "bottom", "outer", ...
    category_sub: str | None    # "t-shirt", "jeans", ...
    description: str | None     # 한국어 설명
    tags: dict | None           # 영어 태그

class StyleCategoryScore(BaseModel):
    score: int
    comment: str

class StyleAnalysis(BaseModel):
    color_palette: StyleCategoryScore
    silhouette: StyleCategoryScore
    detail: StyleCategoryScore
    overall_score: float
    overall_comment: str
    style_tags: list[str]

class BaseLLMProvider(ABC):
    @abstractmethod
    async def analyze_garment(self, image: bytes) -> list[GarmentAnalysis]:
        """이미지에서 의류 분석. 여러 벌 반환 가능."""
        ...

    @abstractmethod
    async def generate_ghost_mannequin(self, image: bytes, item: GarmentAnalysis, mode: str) -> bytes | None:
        """원본 이미지에서 특정 의류만 추출한 이미지 생성."""
        ...

    @abstractmethod
    async def analyze_style(self, image: bytes) -> StyleAnalysis:
        """전체 코디 스타일 분석. 카테고리별 점수/코멘트 반환."""
        ...
```

- `analyze_garment`는 **리스트**를 반환 — 한 사진에 여러 옷이 있을 수 있음
- `generate_ghost_mannequin`은 **바이트** 또는 `None` 반환 — 실패할 수 있음
- `analyze_style`은 **StyleAnalysis** 반환 — 3개 카테고리 점수 + 종합 평가

---

## 프롬프트 (`app/services/llm/prompts.py`)

### 의류 분석 프롬프트 (`GARMENT_ANALYSIS_PROMPT`)

LLM에게 "패션 전문가 AI" 역할을 부여하고, 다음을 추출하도록 지시합니다:

| 필드 | 설명 | 예시 |
|------|------|------|
| `category_main` | 대분류 (4종) | top, bottom, outer, dress |
| `category_sub` | 소분류 | t-shirt, jeans, sneakers, hoodie, blazer |
| `description` | 한국어 설명 | "블랙 코튼 라운드넥 반팔 티셔츠" |
| `tags.color` | 색상 배열 | ["black", "white"] |
| `tags.pattern` | 패턴 | solid, striped, checkered, floral, graphic, logo |
| `tags.material` | 소재 | cotton, denim, polyester, wool, leather, ... |
| `tags.style` | 스타일 배열 | ["casual", "sporty"] |
| `tags.fit` | 핏 | slim, regular, oversized, cropped |
| `tags.occasion` | 상황 배열 | ["daily", "office"] |
| `tags.brand` | 브랜드 | "Nike" 또는 "unknown" |

**중요 규칙:**
- `description`은 **한국어**
- `tags` 값은 모두 **영어**
- JSON 형식으로 반환: `{"garments": [...]}`

### 스타일 분석 프롬프트 (`STYLE_ANALYSIS_PROMPT`)

LLM에게 "프로 패션 스타일리스트 AI" 역할을 부여하고, 전체 코디를 평가합니다:

| 카테고리 | 설명 | 점수 |
|----------|------|------|
| `color_palette` | 색상의 균형 — 색상 조화, 대비, 계절 적합성 | 1-5 |
| `silhouette` | 핏과 실루엣 — 비율, 레이어링, 전체적인 형태 | 1-5 |
| `detail` | 디테일 포인트 — 텍스처, 패턴, 액세서리, 스타일 요소 | 1-5 |

추가 반환값:
- `overall_score`: 3개 카테고리 평균 (0.5 단위 반올림)
- `overall_comment`: 한국어 종합 평가 (2-4문장)
- `style_tags`: 영어 스타일 키워드 배열

**중요 규칙:**
- `comment`, `overall_comment`는 **한국어**
- `style_tags`는 **영어**
- JSON 형식: `{"color_palette": {...}, "silhouette": {...}, "detail": {...}, ...}`

### 고스트 마네킹 프롬프트 (`GHOST_MANNEQUIN_EDIT_PROMPT`)

```
"이 사진에서 {category_sub} ({category_main})만 추출하세요.
사람을 완전히 제거하고, 옷만 보이지 않는 마네킹에 걸린 것처럼 표시하세요.
흰색 배경(#FFFFFF). 원본과 동일한 색상/질감/디테일 유지."
```

- `{category_sub}`와 `{category_main}`은 분석 결과로 채워집니다
- 예: "Extract ONLY the t-shirt (top) from this photo."

---

## Gemini Provider (`app/services/llm/gemini.py`)

### 모델 구성

| 용도 | 모델 | 특징 |
|------|------|------|
| 의류 분석 | `gemini-3-pro-image-preview` | 멀티모달. JSON 구조화 출력 지원 |
| 고스트 마네킹 | `gemini-2.5-flash-image` | 안정 GA 모델. 빠른 이미지 입력+출력 |

### 의류 분석 (`analyze_garment`)

```python
payload = {
    "contents": [{
        "parts": [
            {"text": GARMENT_ANALYSIS_PROMPT},      # 텍스트 프롬프트
            {"inline_data": {                        # 이미지 (base64)
                "mime_type": "image/jpeg",
                "data": b64_image
            }}
        ]
    }],
    "generationConfig": {
        "temperature": 0.2,                          # 낮은 temperature = 일관된 결과
        "maxOutputTokens": 4096,
        "responseMimeType": "application/json",      # JSON 응답 강제
    }
}
```

**API 호출:**
```
POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image-preview:generateContent?key={API_KEY}
```

**응답 파싱:**
1. `data["candidates"][0]["content"]["parts"][0]["text"]` 에서 텍스트 추출
2. JSON 파싱
3. `{"garments": [...]}` 또는 그냥 `[...]` 형태 모두 처리
4. 각 아이템을 `GarmentAnalysis` 객체로 변환

### 고스트 마네킹 (`_edit_image`)

```python
payload = {
    "contents": [{
        "parts": [
            {"text": prompt},                        # 추출 지시
            {"inline_data": {"data": b64_image}}     # 원본 이미지
        ]
    }],
    "generationConfig": {
        "responseModalities": ["IMAGE", "TEXT"],      # 이미지 생성 모드
        "temperature": 0.2,
    }
}
```

**응답에서 이미지 추출:**
```python
for part in parts:
    inline_data = part.get("inline_data")
    if inline_data and inline_data.get("data"):
        return base64.b64decode(inline_data["data"])   # 이미지 바이트
```

---

## OpenAI Provider (`app/services/llm/openai.py`)

### 모델 구성

| 용도 | 모델 | API | 특징 |
|------|------|-----|------|
| 의류 분석 | `gpt-5.4` | Chat Completions (`/v1/chat/completions`) | Vision 지원. JSON 응답 |
| 고스트 마네킹 | `gpt-5.4` | Responses (`/v1/responses`) + `image_generation` tool | 이미지 입력+출력 |

### 의류 분석 (`analyze_garment`)

```python
payload = {
    "model": "gpt-5.4",
    "messages": [{
        "role": "user",
        "content": [
            {"type": "text", "text": GARMENT_ANALYSIS_PROMPT},
            {"type": "image_url", "image_url": {
                "url": f"data:image/jpeg;base64,{b64_image}",
                "detail": "high"                      # 고해상도 분석
            }}
        ]
    }],
    "temperature": 0.2,
    "response_format": {"type": "json_object"},        # JSON 응답 강제
}
```

**API 호출:**
```
POST https://api.openai.com/v1/chat/completions
Authorization: Bearer {API_KEY}
```

### 고스트 마네킹 (`generate_ghost_mannequin`)

OpenAI의 Responses API에 `image_generation` 도구를 사용합니다:

```python
payload = {
    "model": "gpt-5.4",
    "input": [{
        "role": "user",
        "content": [
            {"type": "input_image", "image_url": f"data:image/jpeg;base64,{b64_image}"},
            {"type": "input_text", "text": prompt},
        ]
    }],
    "tools": [{"type": "image_generation"}],
}
```

**API 호출:**
```
POST https://api.openai.com/v1/responses
Authorization: Bearer {API_KEY}
Content-Type: application/json
```

**응답에서 이미지 추출:**
```python
for output_item in data.get("output", []):
    if output_item.get("type") == "image_generation_call":
        b64_result = output_item.get("result", "")
        if b64_result:
            return base64.b64decode(b64_result)
```

---

## 두 프로바이더 비교

| 항목 | Gemini | OpenAI |
|------|--------|--------|
| 인증 | URL 쿼리 파라미터 `?key=` | `Authorization: Bearer` 헤더 |
| 이미지 입력 | `inline_data` (base64) | `image_url` (data URI base64) |
| JSON 모드 | `responseMimeType: "application/json"` | `response_format: {"type": "json_object"}` |
| 이미지 생성 | 같은 generateContent API + `responseModalities` | Responses API + `image_generation` tool |
| 타임아웃 | 분석 60초, 이미지 120초 | 동일 |

### 응답 파싱 공통 로직

두 프로바이더 모두 동일한 파싱 로직을 사용합니다:

```python
# LLM이 {"garments": [...]} 또는 그냥 [...] 를 반환할 수 있음
if isinstance(parsed, dict):
    for v in parsed.values():
        if isinstance(v, list):
            parsed = v      # {"garments": [...]} → [...]
            break
```

---

## 분석과 고스트 마네킹의 분리

분석(`analyze_and_save()`)과 고스트 마네킹 생성(`generate_ghost()`)은 **별도의 단계**로 분리되어 있습니다.

- **분석 (POST /api/v1/garments/analyze)**: 이미지를 LLM으로 분석하여 의류를 DB에 저장. 고스트 마네킹은 생성하지 않음
- **고스트 마네킹 (POST /api/v1/garments/{id}/ghost)**: 저장된 개별 의류에 대해 별도로 고스트 마네킹 이미지 생성

이전에는 `analyze_and_save()` 내부에서 고스트 마네킹까지 한 번에 생성했으나, 현재는 `generate_ghost()` 메서드가 별도의 서비스 메서드로 분리되어 별도 엔드포인트에서 호출됩니다.

분석 엔드포인트는 통합되었습니다. 이전의 `/garments/analyze`와 `/garments/analyze-with-face`가 단일 `POST /api/v1/garments/analyze`로 합쳐졌으며, `use_face_id` 파라미터(optional bool)로 얼굴 인식 여부를 제어합니다. 응답에는 `face_status` 필드가 포함됩니다.

---

## 에러 처리

- 의류 분석 실패: `GarmentService`까지 예외가 전파 → 라우터에서 500 응답
- 고스트 마네킹 실패: `GarmentService.generate_ghost()`에서 catch → `None` 반환 (의류는 저장되지만 ghost 이미지 없음)
- API 키 미설정: 초기화 시 경고 로그, 실제 호출 시 HTTP 오류 발생
