Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

이 파트는 OCR, 메뉴명/가격 추출, 메뉴명 후처리, ERD 기반 JSON 생성까지만 담당합니다. 번역, 위험도 판단, 매움 여부 판단, DB 저장은 후속 파트와 백엔드가 담당합니다.

> **AI 파이프라인 모듈 목록**
> - `ai_ocr/` — 메뉴판 이미지 OCR 및 구조화 (이 문서)
> - `ai_ruleengine/` — 사용자 프로필 기반 메뉴 위험도 판정 → [README](ai_ruleengine/README.md)

## 폴더 구조

```text
Expand Down
94 changes: 94 additions & 0 deletions ai_ruleengine/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Rule Engine

사용자 프로필(할랄, 비건, 알레르기, 매운맛 선호도)을 기반으로 OCR이 추출한 메뉴별 위험도를 판정하는 모듈입니다. GPT 호출 없이 규칙 기반으로 동작하며, 판단이 불확실한 경우에만 `need_gpt=True`를 반환합니다.

## 폴더 구조

```text
ai_ruleengine/
constants.py # 재료 태그 사전 + 수식어 토큰 (수정 금지)
data/menus.csv # 76개 메뉴 DB (카테고리/레시피/애매함 태그)
menu_db.py # CSV 로더
profile_mapper.py # 프로필 → 금지 태그 set
modifier_strip.py # 수식어 제거 + 매운맛 감지
menu_matcher.py # Longest-match 베이스 메뉴 탐색
ingredient_tagger.py # 레시피·변형 재료 태깅
risk_judge.py # danger / caution / safe 판정
reason_generator.py # 한국어 이유 문장 생성
engine.py # analyze() / analyze_all() 통합
main.py # CLI
examples/
run_demo.py # 시연 케이스 10개
```

## 실행

```bash
python ai_ruleengine/main.py \
--ocr-json outputs/final/menu_001_result.json \
--profile profile.json \
--output outputs/final/menu_001_judged.json
```

단계별 처리 내용 출력:

```bash
python ai_ruleengine/main.py \
--ocr-json outputs/final/menu_001_result.json \
--profile profile.json \
--verbose
```

## 프로필 형식

```json
{
"religion_type": "halal",
"vegan_type": null,
"no_alcohol": false,
"allergies": ["shrimp"],
"is_spicy": false
}
```

| 필드 | 값 |
|------|----|
| `religion_type` | `"halal"` `"kosher"` `"hindu"` `null` |
| `vegan_type` | `"vegan"` `"lacto"` `"ovo"` `"lacto_ovo"` `"pesco"` `null` |
| `no_alcohol` | `true` / `false` |
| `allergies` | `["shrimp", "crab", ...]` |
| `is_spicy` | `true`(선호) / `false`(비선호) / `null`(무관) |

## 출력 필드

`menu_analyses[]` 각 항목에 아래 필드가 추가됩니다.

| 필드 | 설명 |
|------|------|
| `risk_level` | `"danger"` `"caution"` `"safe"` |
| `risk_reasons` | 원인 태그 목록 (예: `["is_pork"]`) |
| `reason_ko` | 한국어 이유 문장 |
| `reason_en` | `null` (후속 LLM 담당) |
| `need_gpt` | GPT 에스컬레이션 필요 여부 |
| `is_spicy` | 메뉴명에서 매운맛 감지 여부 |

## 판정 흐름

```
Step 1 프로필 → 금지 태그 추출
Step 2 수식어 제거 + 매운맛 토큰 감지
Step 3 DB에서 베이스 메뉴 Longest-match 탐색
Step 4 레시피 재료 태깅
Step 5 변형 재료(remain 토큰) 태깅
Step 6 금지 태그 교집합 → danger
Step 7 매운맛 비선호 + 매운 메뉴 → danger
Step 8 애매함 플래그 관련성 판단 → caution + need_gpt
Step 9 미확인 remain 토큰 → need_gpt
Step 10 한국어 이유 문장 생성
```

## 데모

```bash
python examples/run_demo.py --verbose
```
Empty file added ai_ruleengine/__init__.py
Empty file.
176 changes: 176 additions & 0 deletions ai_ruleengine/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"""
한스푼 Rule Engine - 정적 데이터

AI2 명세 §3 (재료 태그 표준), §변형 재료 사전,
§Modifier Stripping 규칙 v2 를 코드화.

is_fish, is_duck 은 식약처 19종에는 없지만
비건/페스코 분기 판정용 내부 태그 (백엔드 컬럼 추가 필요 없음).
"""

# ============================================================
# 1. 태그 카테고리
# ============================================================

# 식약처 알레르기 19종 (백엔드 user_allergies.allergy_name 에서 매핑 가능)
ALLERGY_TAGS = [
"is_egg", "is_milk", "is_buckwheat", "is_peanut",
"is_soybean", "is_wheat", "is_pinenut", "is_walnut",
"is_crab", "is_shrimp", "is_squid", "is_mackerel",
"is_shellfish", "is_peach", "is_tomato",
"is_chicken", "is_pork", "is_beef", "is_sulfite",
]

# 비건/페스코 판정 보조 태그 (알레르기 reason 으로는 안 씀)
VEGAN_AUX_TAGS = ["is_fish", "is_duck"]

# 종교/생활 식단 태그
OTHER_TAGS = ["is_alcohol"]

ALL_TAGS = ALLERGY_TAGS + VEGAN_AUX_TAGS + OTHER_TAGS


# ============================================================
# 2. VARIANT_INGREDIENTS: 재료 문자열 → 태그
#
# 매칭은 substring 검사. 같은 재료가 여러 태그에 나올 수 있음
# (예: "고등어" → is_mackerel + is_fish).
# 긴 키워드부터 매칭해서 false positive 방지 (예: "삼겹살"이
# "삼"으로 매칭되지 않도록) — 정렬은 ingredient_tagger 에서 처리.
# ============================================================
VARIANT_INGREDIENTS = {
"is_egg": [
"메추리알", "달걀", "계란",
],
"is_milk": [
"분유", "연유", "생크림", "요거트", "치즈", "버터", "크림", "우유",
],
"is_buckwheat": [
"메밀국수", "메밀면", "메밀",
],
"is_peanut": [
"땅콩버터", "피넛", "땅콩",
],
"is_soybean": [
# 콩 자체
"콩나물", "순두부", "유부", "두부", "대두", "콩",
# 한국 발효 조미료 (대두 베이스)
"된장", "진간장", "국간장", "간장", "쌈장", "청국장", "고추장",
],
"is_wheat": [
"밀가루", "수제비", "우동", "라면", "국수", "면",
"빵", "튀김가루", "부침가루", "만두피", "밀",
],
"is_pinenut": ["잣"],
"is_walnut": ["호두"],
"is_crab": [
"킹크랩", "대게", "꽃게", "게",
],
"is_shrimp": [
"왕새우", "대하", "새우",
# 한국 발효 (jeotgal 플래그가 잡아도, 명시 재료로 들어있으면 즉시 태깅)
"새우젓",
],
"is_squid": [
"쭈꾸미", "주꾸미", "오징어", "낙지", "문어",
],
"is_mackerel": ["고등어"],
"is_fish": [
# 알레르기 19종 단위는 is_mackerel(고등어)만 있지만,
# 비건 판정용으로 모든 어류/생선 가공품을 묶음.
"참치캔", "참치", "연어", "고등어", "꽁치", "삼치",
"대구", "명태", "동태", "황태", "북어", "코다리",
"갈치", "조기", "병어", "장어", "민물장어",
# 한식 핵심 (육수, 반찬)
"어묵", "멸치",
# 발효
"멸치액젓", "멸치젓", "액젓",
],
"is_shellfish": [
"키조개", "가리비", "전복", "홍합", "바지락", "조갯살", "조개", "굴",
],
"is_peach": ["백도", "황도", "복숭아"],
"is_tomato": [
"방울토마토", "토마토소스", "토마토", "케찹", "케첩",
],
"is_chicken": [
"닭가슴살", "닭다리", "닭날개", "닭발",
"닭고기", "닭",
],
"is_pork": [
# 명시 동물 + 부위
"삼겹살", "오겹살", "목살", "항정살",
"앞다리살", "뒷다리살",
"돼지고기", "돼지",
"족발", "보쌈",
# 가공육 (한국 분식/부대찌개에서 빈출)
"베이컨", "햄", "소시지", "프랑크소시지",
],
"is_duck": [
"훈제오리", "오리고기", "오리",
],
"is_beef": [
# 명시 동물 + 부위
"차돌박이", "차돌",
"한우", "쇠고기", "소고기",
"등심", "안심", "우둔", "양지머리", "양지",
# 사골/뼈류 (탕/곰탕에서 빈출)
"사골", "도가니", "잡뼈",
# 갈비
"갈비살", "갈비",
# 부속 (※ "돼지곱창" 처럼 명시 동물 키워드가 같이 있으면
# is_pork 도 함께 붙음 → tagger 가 둘 다 태깅)
"대창", "막창", "곱창",
# 선지 (소의 피, 해장국/순대국 빈출)
"선지",
# 회/육회
"육회",
],
"is_sulfite": [
# 실제 재료에 명시되는 경우만 매칭
"아황산", "건포도", "건과일",
],
"is_alcohol": [
"맛술", "청주", "미림", "소주", "와인", "막걸리", "정종", "사케",
],
}


# ============================================================
# 3. GROUP_VARIANTS: 한 토큰이 여러 태그 동시 발동
#
# "해물비빔밥" 처럼 메뉴명 자체에 그룹 키워드가 들어오는 경우.
# ingredient(레시피)가 아닌 메뉴명 remain 토큰 분석에서 사용.
# ============================================================
GROUP_VARIANTS = {
"해물": ["is_shrimp", "is_crab", "is_squid", "is_shellfish", "is_fish"],
"해산물": ["is_shrimp", "is_crab", "is_squid", "is_shellfish", "is_fish"],
"모듬": [], # 컨텍스트 의존 → has_variant 플래그로 위임
}


# ============================================================
# 4. SPICY_TOKENS: 매운맛 키워드
# - 감지 시 is_spicy=True 부여 후 메뉴명에서 strip
# ============================================================
SPICY_TOKENS = ["얼큰", "매콤", "매운", "칼칼한", "칼칼"]


# ============================================================
# 5. REMOVE_TOKENS: 메뉴 identity와 무관한 수식어 (strip만)
# ============================================================
REMOVE_TOKENS = [
# 브랜드/마케팅
"시그니처", "signature", "원조", "정통", "전통",
"프리미엄", "premium", "베스트", "best", "추천", "인기",
"맛집", "수제", "handmade", "스페셜", "special",
"명품", "대표", "고급", "특선",
# 가게 스타일
"할매", "할머니", "옛날식", "집밥", "엄마표",
"기사식당", "백반집",
# 시간/운영
"24시", "365일", "연중무휴",
# 강조
"진한", "깊은맛", "담백한", "고소한", "진국",
"시원한", "구수한",
]
Loading