apsf / app.py
seawolf2357's picture
Update app.py
a3d09da verified
#!/usr/bin/env python3
import os
import sys
import csv
import json
import sqlite3
import hashlib
import requests
import time
import random
import re
import uuid
from datetime import datetime
from typing import List, Dict, Optional, Tuple, Any, Generator
from enum import Enum
import numpy as np
# Optional imports
try:
import replicate
HAS_REPLICATE = True
except ImportError:
HAS_REPLICATE = False
print("⚠️ replicate 패키지 없음 - 이미지 생성 비활성화")
try:
import gradio as gr
HAS_GRADIO = True
except ImportError:
HAS_GRADIO = False
print("⚠️ gradio 패키지 없음 - CLI 모드로 실행")
# ============================================================================
# JSON 안전 파싱 유틸리티 (v3.2 강화!)
# ============================================================================
def parse_json_safely(response: str, default: Dict) -> Dict:
"""안전한 JSON 파싱 - 여러 방법 시도 (v3.2 강화 버전)"""
if not response:
return default
response = response.strip()
# 방법 1: 직접 파싱
try:
result = json.loads(response)
if isinstance(result, dict) and result:
return result
except:
pass
# 방법 2: 코드 블록 제거 후 파싱 (```json ... ``` 처리)
try:
if "```" in response:
code_block_pattern = r'```(?:json)?\s*([\s\S]*?)```'
matches = re.findall(code_block_pattern, response)
for match in matches:
match = match.strip()
if match.startswith("{"):
try:
result = json.loads(match)
if isinstance(result, dict) and result:
return result
except:
continue
except:
pass
# 방법 3: 중첩 브라켓 깊이 추적으로 정확한 JSON 추출 (v3.2 NEW!)
try:
depth = 0
start_idx = -1
for i, char in enumerate(response):
if char == '{':
if depth == 0:
start_idx = i
depth += 1
elif char == '}':
depth -= 1
if depth == 0 and start_idx >= 0:
json_str = response[start_idx:i+1]
result = json.loads(json_str)
if isinstance(result, dict) and result:
return result
except:
pass
# 방법 4: { } 블록 단순 추출 (정리 후)
try:
cleaned = re.sub(r'[\n\r\t]+', ' ', response)
cleaned = re.sub(r'\s+', ' ', cleaned)
start = cleaned.find("{")
end = cleaned.rfind("}") + 1
if start >= 0 and end > start:
json_str = cleaned[start:end]
result = json.loads(json_str)
if isinstance(result, dict) and result:
return result
except:
pass
# 방법 5: 흔한 JSON 오류 수정 후 재시도 (v3.2 NEW!)
try:
fixed = response.replace("'", '"') # 작은따옴표 → 큰따옴표
fixed = re.sub(r',\s*}', '}', fixed) # trailing comma 제거
fixed = re.sub(r',\s*]', ']', fixed) # trailing comma 제거
start = fixed.find("{")
end = fixed.rfind("}") + 1
if start >= 0 and end > start:
result = json.loads(fixed[start:end])
if isinstance(result, dict) and result:
return result
except:
pass
return default
# ============================================================================
# 랜덤 유틸리티 (v3.2 NEW!)
# ============================================================================
def get_random_seed() -> int:
"""매번 다른 랜덤 시드 생성 - 타임스탬프 + 랜덤 오프셋"""
return int(time.time() * 1000) % (2**31) + random.randint(0, 10000)
def shuffle_and_pick(items: List, count: int = 1) -> List:
"""리스트를 셔플하고 count개 선택"""
if not items:
return []
items_copy = items.copy()
random.shuffle(items_copy)
return items_copy[:count]
def random_choice_weighted(items: List, weights: List[float] = None) -> Any:
"""가중치 기반 랜덤 선택"""
if not items:
return None
if weights and len(weights) == len(items):
total = sum(weights)
r = random.uniform(0, total)
cumulative = 0
for item, weight in zip(items, weights):
cumulative += weight
if r <= cumulative:
return item
return random.choice(items)
# ============================================================================
# Brave Search API 클라이언트
# ============================================================================
class BraveSearchClient:
"""Brave Search API 클라이언트 - 재료/풍미 검색용"""
def __init__(self, api_key: str = None):
self.api_key = api_key or os.environ.get("BRAVE_API_KEY", "")
self.base_url = "https://api.search.brave.com/res/v1/web/search"
def search(self, query: str, count: int = 10) -> List[Dict]:
"""Brave Search API로 검색"""
if not self.api_key:
return []
headers = {
"Accept": "application/json",
"Accept-Encoding": "gzip",
"X-Subscription-Token": self.api_key
}
params = {
"q": query,
"count": count,
"search_lang": "ko",
"country": "kr"
}
try:
response = requests.get(self.base_url, headers=headers, params=params, timeout=10)
response.raise_for_status()
data = response.json()
results = []
for item in data.get("web", {}).get("results", []):
results.append({
"title": item.get("title", ""),
"url": item.get("url", ""),
"description": item.get("description", ""),
"snippet": item.get("extra_snippets", [])
})
return results
except Exception as e:
print(f"⚠️ Brave Search 오류: {e}")
return []
# ============================================================================
# 창발적 레시피 엔진 - 6대 차원 데이터 로더 (v3.2 확장!)
# ============================================================================
class CulinaryDataLoader:
"""6대 차원 요리 데이터 로더 - 창발적 레시피 생성의 핵심 (v3.2 확장)"""
def __init__(self, json_path: str = "culinary_data.json"):
self.data = self._load_data(json_path)
def _load_data(self, json_path: str) -> Dict:
possible_paths = [
json_path,
os.path.join(os.path.dirname(__file__), json_path),
os.path.join(os.path.dirname(__file__), "data", json_path),
os.path.join(os.getcwd(), json_path),
]
for path in possible_paths:
if os.path.exists(path):
with open(path, 'r', encoding='utf-8') as f:
print(f"✅ 창발 엔진 데이터 로드: {path}")
return json.load(f)
print("⚠️ culinary_data.json 없음 - 내장 폴백 데이터 사용")
return self._get_fallback_data()
def _get_fallback_data(self) -> Dict:
"""내장 폴백 데이터 (v3.2 확장!)"""
return {
"D1_Ingredients": {
"proteins": {"red_meat": [{"id": "hanwoo_beef", "name": "한우", "name_en": "Korean Hanwoo", "substitutes": ["wagyu_beef"]}]},
"seasonings": {"fermented_korean": [
{"id": "gochujang", "name": "고추장", "name_en": "Gochujang", "substitutes": ["sriracha_miso"]},
{"id": "doenjang", "name": "된장", "name_en": "Doenjang", "substitutes": ["miso"]},
{"id": "ganjang", "name": "간장", "name_en": "Soy Sauce", "substitutes": ["tamari"]}
]}
},
"D2_FlavorsAromas": {
"profiles": [
{"id": "FP_001", "ingredient": "김치", "compounds": ["Lactic Acid", "Allyl Sulfides"],
"emergent_pairings": ["Cheddar Cheese", "Taco", "Pizza", "Mac and Cheese", "Grilled Cheese", "Quesadilla", "Ramen", "Risotto", "Burger", "Hot Dog"]},
{"id": "FP_002", "ingredient": "고추장", "compounds": ["Capsaicin", "Maltose"],
"emergent_pairings": ["Chocolate", "Honey", "Vanilla Ice Cream", "Caramel", "Bacon", "Maple Syrup", "Peanut Butter", "Mango", "Coconut"]},
{"id": "FP_003", "ingredient": "된장", "compounds": ["Glutamates", "Pyrazines"],
"emergent_pairings": ["Chocolate", "Caramel", "Butter", "Coffee", "Walnut", "Apple", "Pear", "Brie Cheese", "Truffle"]},
{"id": "FP_004", "ingredient": "참기름", "compounds": ["Sesamol"],
"emergent_pairings": ["Chocolate", "Honey", "Strawberry", "Vanilla", "Citrus", "Tahini", "Halva"]},
{"id": "FP_005", "ingredient": "간장", "compounds": ["Glutamates"],
"emergent_pairings": ["Chocolate", "Caramel", "Vanilla Ice Cream", "Coffee", "Butterscotch", "Toffee"]},
{"id": "FP_006", "ingredient": "청국장", "compounds": ["Natto Kinase", "Glutamates"],
"emergent_pairings": ["Parmesan", "Miso", "Blue Cheese", "Truffle", "Anchovy"]},
{"id": "FP_007", "ingredient": "막걸리", "compounds": ["Lactic Acid", "Ethanol"],
"emergent_pairings": ["Cream", "Peach", "Ginger", "Honey", "Rice Pudding"]},
{"id": "FP_008", "ingredient": "고춧가루", "compounds": ["Capsaicin", "Carotenoids"],
"emergent_pairings": ["Mango", "Lime", "Pineapple", "Coconut", "Dark Chocolate", "Orange"]}
],
"compound_sharing_map": {
"pyrazines": ["Coffee", "Chocolate", "Roasted Meat", "Doenjang"],
"glutamates": ["Parmesan", "Tomato", "Soy Sauce", "Miso", "Mushroom"],
"linalool": ["Lavender", "Basil", "Coriander", "Citrus"],
"limonene": ["Lemon", "Orange", "Lime", "Grapefruit", "Dill"]
}
},
"D3_CookingMethods": {
"heat_based": {
"dry_heat": [
{"id": "CM_DH_001", "name": "Roasting", "name_ko": "로스팅/구이", "temp_range": "150-230°C", "time_range": "20분-4시간"},
{"id": "CM_DH_002", "name": "Grilling", "name_ko": "그릴링/직화구이", "temp_range": "200-300°C", "time_range": "2-30분"},
{"id": "CM_DH_003", "name": "Searing", "name_ko": "시어링", "temp_range": "230-290°C", "time_range": "30초-3분"},
{"id": "CM_DH_004", "name": "Smoking", "name_ko": "훈연", "temp_range": "70-120°C", "time_range": "2-12시간"},
{"id": "CM_DH_005", "name": "Baking", "name_ko": "베이킹", "temp_range": "150-220°C", "time_range": "15분-2시간"}
],
"moist_heat": [
{"id": "CM_MH_001", "name": "Braising", "name_ko": "브레이징/조림", "temp_range": "150-175°C", "time_range": "2-6시간"},
{"id": "CM_MH_002", "name": "Sous_Vide", "name_ko": "수비드", "temp_range": "52-85°C", "time_range": "30분-72시간"},
{"id": "CM_MH_003", "name": "Steaming", "name_ko": "찌기", "temp_range": "100°C", "time_range": "10-60분"},
{"id": "CM_MH_004", "name": "Poaching", "name_ko": "포칭", "temp_range": "65-85°C", "time_range": "5-30분"},
{"id": "CM_MH_005", "name": "Blanching", "name_ko": "데치기", "temp_range": "100°C", "time_range": "30초-5분"}
],
"fat_based": [
{"id": "CM_FB_001", "name": "Confit", "name_ko": "콩피", "temp_range": "80-100°C", "time_range": "2-12시간"},
{"id": "CM_FB_002", "name": "Deep_Frying", "name_ko": "튀김", "temp_range": "175-190°C", "time_range": "2-10분"},
{"id": "CM_FB_003", "name": "Pan_Frying", "name_ko": "팬프라잉", "temp_range": "150-180°C", "time_range": "3-15분"},
{"id": "CM_FB_004", "name": "Stir_Frying", "name_ko": "볶음", "temp_range": "200-250°C", "time_range": "2-5분"}
]
}
},
"D4_Textures": {
"primary_textures": [
{"id": "TX_001", "name": "Crispy", "name_ko": "바삭함", "contrast_pairing": ["Creamy", "Soft", "Silky"], "achieved_by": ["Deep Frying", "High-Heat Roasting", "Dehydrating"]},
{"id": "TX_002", "name": "Creamy", "name_ko": "크리미함", "contrast_pairing": ["Crispy", "Crunchy"], "achieved_by": ["Emulsifying", "Pureeing", "Whipping"]},
{"id": "TX_003", "name": "Chewy", "name_ko": "쫄깃함", "contrast_pairing": ["Crispy", "Tender"], "achieved_by": ["Gluten development", "Long cooking"]},
{"id": "TX_004", "name": "Tender", "name_ko": "부드러움", "contrast_pairing": ["Chewy", "Crunchy"], "achieved_by": ["Low-slow cooking", "Braising", "Sous vide"]},
{"id": "TX_005", "name": "Crunchy", "name_ko": "아삭함", "contrast_pairing": ["Soft", "Creamy"], "achieved_by": ["Raw vegetables", "Light frying", "Pickling"]},
{"id": "TX_006", "name": "Silky", "name_ko": "실키함", "contrast_pairing": ["Crispy", "Crunchy"], "achieved_by": ["Fine straining", "Slow reduction"]},
{"id": "TX_007", "name": "Fluffy", "name_ko": "폭신함", "contrast_pairing": ["Dense", "Crispy"], "achieved_by": ["Whipping", "Leavening", "Folding"]}
],
"texture_contrasts": [
{"combination": ["Crispy", "Creamy"], "effect": "가장 만족스러운 대비", "examples": ["크림 크로켓", "아이스크림 튀김"]},
{"combination": ["Tender", "Crispy"], "effect": "겉바속촉", "examples": ["치킨", "돈까스"]},
{"combination": ["Chewy", "Crunchy"], "effect": "씹는 재미", "examples": ["떡볶이+튀김", "모찌+너츠"]},
{"combination": ["Silky", "Crispy"], "effect": "우아한 대비", "examples": ["감자퓨레+튀긴파슬리"]}
]
},
"D5_Architecture": {
"structures": [
{"id": "A_001", "name": "Layered", "name_ko": "적층형", "twist_logic": ["Deconstruct", "Roll into cylinder", "Stack vertically", "Fan out"], "korean_examples": ["비빔밥", "불고기 쌈"]},
{"id": "A_002", "name": "Wrapped", "name_ko": "포용형/싸기", "twist_logic": ["Invert", "Fry entire", "Double wrap", "Partial reveal"], "korean_examples": ["김밥", "만두", "쌈"]},
{"id": "A_003", "name": "Mixed", "name_ko": "혼합형", "twist_logic": ["Separate and reconstruct", "Sphere-ify", "Gradient mixing"], "korean_examples": ["비빔밥", "볶음밥"]},
{"id": "A_004", "name": "Coated", "name_ko": "코팅형", "twist_logic": ["Double coat", "Make core liquid", "Transparent coating"], "korean_examples": ["치킨", "돈까스", "탕수육"]},
{"id": "A_005", "name": "Stuffed", "name_ko": "채움형", "twist_logic": ["Reverse stuffing order", "Multiple layers", "Surprise center"], "korean_examples": ["두부선", "오이선"]},
{"id": "A_006", "name": "Skewered", "name_ko": "꼬치형", "twist_logic": ["Alternating ingredients", "Gradient cooking", "Dipping presentation"], "korean_examples": ["산적", "떡꼬치"]},
{"id": "A_007", "name": "Soup-based", "name_ko": "국물형", "twist_logic": ["Concentrated essence", "Layered broth", "Solid-liquid contrast"], "korean_examples": ["설렁탕", "된장찌개"]}
]
},
"D6_CulinaryGrammar": {
"korean": [
{"id": "CG_KOR_001", "name": "기본 갖은양념", "base_aromatics": ["마늘", "대파", "생강"], "core_liquids": ["간장", "참기름"]},
{"id": "CG_KOR_002", "name": "고추장 베이스", "base": ["고추장"], "enhancers": ["간장", "참기름", "설탕"], "applications": ["비빔밥", "떡볶이"]},
{"id": "CG_KOR_003", "name": "된장 베이스", "base": ["된장"], "enhancers": ["멸치육수", "참기름"], "applications": ["된장찌개", "쌈장"]},
{"id": "CG_KOR_004", "name": "간장 베이스", "base": ["간장", "물엿"], "enhancers": ["마늘", "생강"], "applications": ["갈비찜", "장조림"]}
],
"japanese": [
{"id": "CG_JPN_001", "name": "다시 베이스", "base": ["다시마", "가쓰오부시"], "applications": ["미소시루", "라멘"]},
{"id": "CG_JPN_002", "name": "데리야키", "base": ["간장", "미림", "사케"], "applications": ["닭고기", "연어"]},
{"id": "CG_JPN_003", "name": "폰즈", "base": ["간장", "유자", "식초"], "applications": ["샤브샤브", "회"]}
],
"french": [
{"id": "CG_FRA_001", "name": "미르푸아", "base_aromatics": ["양파", "당근", "셀러리"], "core_fat": ["버터"]},
{"id": "CG_FRA_002", "name": "브뢴 버터", "base": ["버터"], "applications": ["생선", "채소"]},
{"id": "CG_FRA_003", "name": "부케 가르니", "base": ["타임", "월계수잎", "파슬리"], "applications": ["스톡", "스튜"]}
],
"italian": [
{"id": "CG_ITA_001", "name": "소프리토", "base_aromatics": ["양파", "당근", "셀러리"], "core_fat": ["올리브오일"]},
{"id": "CG_ITA_002", "name": "아글리오 올리오", "base": ["마늘", "올리브오일", "페페론치노"], "applications": ["파스타"]},
{"id": "CG_ITA_003", "name": "피카타", "base": ["레몬", "버터", "케이퍼"], "applications": ["치킨", "생선"]}
],
"indian": [
{"id": "CG_IND_001", "name": "타르카/템퍼링", "base_fat": ["기"], "spices": ["커민씨", "머스타드씨"]},
{"id": "CG_IND_002", "name": "마살라 베이스", "spices": ["강황", "커민", "코리앤더"], "applications": ["커리"]}
],
"chinese": [
{"id": "CG_CHN_001", "name": "향신채 베이스", "base_aromatics": ["대파", "생강", "마늘"]},
{"id": "CG_CHN_002", "name": "마라 베이스", "base": ["고추", "화자오", "두반장"], "applications": ["마라탕", "훠궈"]}
],
"thai": [
{"id": "CG_THA_001", "name": "커리 페이스트", "base": ["레몬그라스", "갈랑갈", "고추"]},
{"id": "CG_THA_002", "name": "태국 드레싱", "base": ["라임", "피시소스", "팜슈가"], "applications": ["쏨땀", "얌운센"]}
],
"mexican": [
{"id": "CG_MEX_001", "name": "소프리토 멕시칸", "base": ["양파", "마늘", "토마토", "할라페뇨"]},
{"id": "CG_MEX_002", "name": "몰레", "base": ["칠리", "초콜릿", "향신료"], "applications": ["치킨 몰레"]}
]
},
"emergence_rules": {
"variable_crossing": {"description": "한 조리법의 변수를 다른 조리법에 적용"},
"context_shift": {"description": "재료의 풍미 프로파일을 다른 문화권 요리에 이식"},
"architecture_inversion": {"description": "요리 구조를 뒤집거나 재배치"},
"grammar_transplantation": {"description": "한 문화의 양념 문법을 다른 요리에 이식"},
"texture_contrast_optimization": {"description": "대비되는 텍스처를 의도적으로 조합"},
"flavor_compound_pairing": {"description": "공유 화합물 기반 예상치 못한 페어링"}
}
}
def get_ingredient(self, ingredient_id: str) -> Optional[Dict]:
d1 = self.data.get("D1_Ingredients", {})
for category in d1.values():
if isinstance(category, dict):
for subcategory in category.values():
if isinstance(subcategory, list):
for item in subcategory:
if item.get("id") == ingredient_id:
return item
return None
def search_ingredients(self, query: str) -> List[Dict]:
results = []
query_lower = query.lower()
d1 = self.data.get("D1_Ingredients", {})
for category in d1.values():
if isinstance(category, dict):
for subcategory in category.values():
if isinstance(subcategory, list):
for item in subcategory:
if query_lower in item.get("name", "").lower() or query_lower in item.get("name_en", "").lower():
results.append(item)
elif isinstance(category, list):
for item in category:
if query_lower in item.get("name", "").lower() or query_lower in item.get("name_en", "").lower():
results.append(item)
return results
def get_flavor_profile(self, ingredient_name: str) -> Optional[Dict]:
for p in self.data.get("D2_FlavorsAromas", {}).get("profiles", []):
if ingredient_name in p.get("ingredient", ""):
return p
return None
def get_emergent_pairings(self, ingredient_name: str) -> List[str]:
profile = self.get_flavor_profile(ingredient_name)
if profile:
pairings = profile.get("emergent_pairings", []).copy()
random.shuffle(pairings) # v3.2: 매번 다른 순서!
return pairings
return []
def get_random_flavor_profile(self) -> Optional[Dict]:
"""v3.2 NEW: 랜덤 풍미 프로파일 선택"""
profiles = self.data.get("D2_FlavorsAromas", {}).get("profiles", [])
return random.choice(profiles) if profiles else None
def find_shared_compounds(self, ingredient1: str, ingredient2: str) -> List[str]:
compound_map = self.data.get("D2_FlavorsAromas", {}).get("compound_sharing_map", {})
shared = []
for compound, ingredients in compound_map.items():
if ingredient1 in str(ingredients) and ingredient2 in str(ingredients):
shared.append(compound)
return shared
def get_cooking_method(self, method_id: str) -> Optional[Dict]:
d3 = self.data.get("D3_CookingMethods", {})
for category in d3.values():
if isinstance(category, dict):
for subcategory in category.values():
if isinstance(subcategory, list):
for method in subcategory:
if method.get("id") == method_id:
return method
return None
def search_cooking_methods(self, query: str) -> List[Dict]:
results = []
query_lower = query.lower()
d3 = self.data.get("D3_CookingMethods", {})
for category in d3.values():
if isinstance(category, dict):
for subcategory in category.values():
if isinstance(subcategory, list):
for method in subcategory:
if query_lower in method.get("name", "").lower() or query_lower in method.get("name_ko", "").lower():
results.append(method)
return results
def get_random_cooking_method(self) -> Optional[Dict]:
"""v3.2 NEW: 랜덤 조리법 선택"""
d3 = self.data.get("D3_CookingMethods", {})
all_methods = []
for category in d3.values():
if isinstance(category, dict):
for subcategory in category.values():
if isinstance(subcategory, list):
all_methods.extend(subcategory)
return random.choice(all_methods) if all_methods else None
def get_texture(self, name: str) -> Optional[Dict]:
for t in self.data.get("D4_Textures", {}).get("primary_textures", []):
if t.get("name") == name or t.get("name_ko") == name:
return t
return None
def get_random_texture(self) -> Optional[Dict]:
"""v3.2 NEW: 랜덤 텍스처 선택"""
textures = self.data.get("D4_Textures", {}).get("primary_textures", [])
return random.choice(textures) if textures else None
def get_texture_contrasts(self, texture_name: str) -> List[str]:
texture = self.get_texture(texture_name)
return texture.get("contrast_pairing", []) if texture else []
def suggest_texture_combination(self) -> Dict:
contrasts = self.data.get("D4_Textures", {}).get("texture_contrasts", [])
return random.choice(contrasts) if contrasts else {}
def get_architecture(self, arch_name: str) -> Optional[Dict]:
for s in self.data.get("D5_Architecture", {}).get("structures", []):
if s.get("id") == arch_name or s.get("name") == arch_name or s.get("name_ko") == arch_name:
return s
return None
def get_random_architecture(self) -> Optional[Dict]:
"""v3.2 NEW: 랜덤 구조 선택"""
structures = self.data.get("D5_Architecture", {}).get("structures", [])
return random.choice(structures) if structures else None
def get_twist_logic(self, arch_name: str) -> List[str]:
structure = self.get_architecture(arch_name)
return structure.get("twist_logic", []) if structure else []
def get_grammar(self, culture: str) -> List[Dict]:
return self.data.get("D6_CulinaryGrammar", {}).get(culture.lower(), [])
def get_random_grammar(self) -> Tuple[str, Dict]:
"""v3.2 NEW: 랜덤 문화+문법 선택"""
grammars = self.data.get("D6_CulinaryGrammar", {})
cultures = list(grammars.keys())
if not cultures:
return "korean", {}
culture = random.choice(cultures)
grammar_list = grammars.get(culture, [])
return culture, random.choice(grammar_list) if grammar_list else {}
def get_all_cultures(self) -> List[str]:
return list(self.data.get("D6_CulinaryGrammar", {}).keys())
# ============================================================================
# 창발적 레시피 생성 엔진 (v3.2 랜덤 강화!)
# ============================================================================
class EmergenceEngine:
"""창발적 레시피 생성 엔진 - 6대 차원 데이터 기반 (v3.2 랜덤 강화!)"""
def __init__(self, llm, data_loader: CulinaryDataLoader):
self.llm = llm
self.data = data_loader
random.seed(get_random_seed()) # v3.2: 초기화 시 새 시드
def _get_unique_id(self) -> str:
"""v3.2 NEW: 고유 ID 생성"""
return str(uuid.uuid4())[:8]
def generate_emergent_recipe(self, base_dish: str, emergence_type: str, **kwargs) -> Dict:
"""창발적 레시피 생성 메인 함수 (v3.2: 매 호출마다 새 시드!)"""
random.seed(get_random_seed()) # v3.2: 매 호출마다 새 시드!
if emergence_type == "variable_crossing":
return self._variable_crossing(base_dish, kwargs.get("method1", ""), kwargs.get("method2", ""))
elif emergence_type == "context_shift":
return self._context_shift(base_dish, kwargs.get("target_cuisine", "italian"))
elif emergence_type == "architecture_inversion":
return self._architecture_inversion(base_dish, kwargs.get("new_structure", "Wrapped"))
elif emergence_type == "grammar_transplantation":
return self._grammar_transplant(base_dish, kwargs.get("new_culture", "french"))
elif emergence_type == "flavor_compound_pairing":
return self._compound_pairing(base_dish, kwargs.get("ingredient", ""))
elif emergence_type == "texture_contrast":
return self._texture_contrast(base_dish, kwargs.get("target_texture", "Crispy"))
return {"error": "Unknown emergence type"}
def _variable_crossing(self, dish: str, method1: str, method2: str) -> Dict:
"""변수 교차 - v3.2: 랜덤 요소 강화"""
# 빈 값이면 랜덤 선택!
if not method1:
m1_data = self.data.get_random_cooking_method()
method1 = m1_data.get("name_ko", "수비드") if m1_data else "수비드"
if not method2:
m2_data = self.data.get_random_cooking_method()
method2 = m2_data.get("name_ko", "시어링") if m2_data else "시어링"
# method1과 같으면 다시 선택
while method2 == method1:
m2_data = self.data.get_random_cooking_method()
method2 = m2_data.get("name_ko", "그릴링") if m2_data else "그릴링"
m1_list = self.data.search_cooking_methods(method1)
m2_list = self.data.search_cooking_methods(method2)
m1 = m1_list[0] if m1_list else {"name_ko": method1, "temp_range": "중온", "time_range": "보통"}
m2 = m2_list[0] if m2_list else {"name_ko": method2, "temp_range": "저온", "time_range": "장시간"}
# v3.2: 랜덤 트위스트 요소 추가
random_twist = random.choice([
"온도와 시간의 역전", "순서 바꾸기", "동시 적용",
"단계적 전환", "펄스 방식", "그라디언트 적용"
])
random_texture_goal = random.choice([
"겉바속촉", "촉촉함 극대화", "크러스트 강화", "부드러움 극대화"
])
prompt = f"""창발적 요리 생성: 변수 교차
기본 요리: {dish}
조리법 1: {m1.get('name_ko', method1)} - 온도: {m1.get('temp_range', '')}, 시간: {m1.get('time_range', '')}
조리법 2: {m2.get('name_ko', method2)} - 온도: {m2.get('temp_range', '')}, 시간: {m2.get('time_range', '')}
랜덤 트위스트: {random_twist}
텍스처 목표: {random_texture_goal}
고유ID: {self._get_unique_id()}
두 조리법의 변수(온도, 시간, 매개체)를 창의적으로 결합하여 완전히 새로운 요리를 제안하세요.
반드시 한국어로 JSON 형식 응답:
{{"name": "새 요리명", "concept": "컨셉 설명", "technique": "결합된 기법 설명", "steps": ["조리 단계1", "조리 단계2", "조리 단계3", "조리 단계4", "조리 단계5"], "expected_texture": "예상 질감", "flavor_notes": "예상 풍미", "innovation_point": "혁신 포인트"}}"""
response = self.llm.chat([{"role": "user", "content": prompt}])
default = {
"name": f"{dish} - {m1.get('name_ko', method1)}+{m2.get('name_ko', method2)} 하이브리드",
"concept": f"{m2.get('name_ko', method2)}의 변수를 적용한 후 {m1.get('name_ko', method1)}로 마무리하여 {random_texture_goal} 달성",
"technique": f"{random_twist} 방식의 하이브리드 조리법",
"steps": [
f"1. 재료를 상온에 30분 적응",
f"2. {m2.get('name_ko', method2)} 방식으로 1차 조리",
"3. 휴지 시간 (5분)",
f"4. {m1.get('name_ko', method1)}로 마무리",
"5. 플레이팅 및 가니쉬"
],
"expected_texture": random_texture_goal,
"flavor_notes": "깊은 풍미와 복합적인 맛의 레이어",
"innovation_point": f"{random_twist}를 통한 새로운 조리 패러다임"
}
result = parse_json_safely(response, default)
return {
"emergence_type": "variable_crossing",
"base_dish": dish,
"applied_rules": {
"method1": m1.get('name_ko', method1),
"method2": m2.get('name_ko', method2),
"random_twist": random_twist,
"texture_goal": random_texture_goal
},
"result": result
}
def _context_shift(self, dish: str, target_cuisine: str) -> Dict:
"""맥락 전환 - v3.2: 랜덤 요소 강화"""
cultures = self.data.get_all_cultures()
# 빈 값이거나 유효하지 않으면 랜덤 선택
if not target_cuisine or target_cuisine.lower() not in cultures:
target_cuisine = random.choice(cultures)
target_grammars = self.data.get_grammar(target_cuisine)
culture_names = {
"korean": "한식", "japanese": "일식", "chinese": "중식",
"french": "프랑스", "italian": "이탈리아", "indian": "인도",
"thai": "태국", "mexican": "멕시코"
}
culture_name = culture_names.get(target_cuisine.lower(), target_cuisine)
# v3.2: 문법 중 랜덤 선택
selected_grammar = random.choice(target_grammars) if target_grammars else {"name": f"{culture_name} 스타일"}
# v3.2: 랜덤 융합 레벨
random_fusion_level = random.choice([
"미묘한 힌트", "균형잡힌 융합", "대담한 변신", "완전한 재해석"
])
random_signature = random.choice([
"시그니처 소스 추가", "특별 가니쉬", "독특한 서빙 방식", "의외의 식감 요소", "향의 레이어링"
])
prompt = f"""창발적 요리 생성: 맥락 전환
원래 요리: {dish}
목표 문화권: {culture_name}
해당 문화의 양념 문법: {json.dumps(selected_grammar, ensure_ascii=False)}
융합 레벨: {random_fusion_level}
시그니처 요소: {random_signature}
고유ID: {self._get_unique_id()}
원래 요리의 핵심 요소를 유지하면서 목표 문화권의 양념, 기법, 식재료로 완전히 재해석하세요.
반드시 한국어로 JSON 형식 응답:
{{"name": "새 요리명", "concept": "컨셉 설명", "kept_elements": ["유지된 요소1", "유지된 요소2", "유지된 요소3"], "new_elements": ["새로 추가된 요소1", "새로 추가된 요소2", "새로 추가된 요소3"], "flavor_bridge": "두 문화를 연결하는 풍미 요소", "recipe_outline": "간단한 조리 개요", "signature_element": "시그니처 요소"}}"""
response = self.llm.chat([{"role": "user", "content": prompt}])
default = {
"name": f"{dish} alla {culture_name}",
"concept": f"{dish}의 핵심을 {culture_name} 문법으로 {random_fusion_level} 수준 재해석",
"kept_elements": [f"{dish}의 주재료", "핵심 컨셉", "영혼"],
"new_elements": [f"{culture_name} 양념", f"{culture_name} 조리법", selected_grammar.get("name", "전통 기법")],
"flavor_bridge": "감칠맛의 공통분모",
"recipe_outline": f"{culture_name} 기법으로 {dish} 새롭게 조리",
"signature_element": random_signature
}
result = parse_json_safely(response, default)
return {
"emergence_type": "context_shift",
"base_dish": dish,
"applied_rules": {
"target_cuisine": culture_name,
"grammar": selected_grammar.get("name", ""),
"fusion_level": random_fusion_level,
"signature": random_signature
},
"result": result
}
def _architecture_inversion(self, dish: str, new_structure: str) -> Dict:
"""구조 전복 - v3.2: 랜덤 요소 강화"""
# 빈 값이면 랜덤 선택
if not new_structure:
arch = self.data.get_random_architecture()
new_structure = arch.get("name", "Wrapped") if arch else "Wrapped"
arch = self.data.get_architecture(new_structure)
if not arch:
arch = self.data.get_random_architecture() or {"name": new_structure, "name_ko": new_structure, "twist_logic": ["구조 변환"]}
twist_options = arch.get("twist_logic", ["구조 변환"])
selected_twist = random.choice(twist_options)
korean_examples = arch.get("korean_examples", [])
# v3.2: 랜덤 프레젠테이션 스타일
random_presentation = random.choice([
"한입 사이즈로", "공유형 플래터로", "개인 포션으로",
"타워 스타일로", "해체 스타일로", "서프라이즈 요소로"
])
prompt = f"""창발적 요리 생성: 구조 전복
원래 요리: {dish}
새로운 구조: {new_structure} ({arch.get('name_ko', '')})
적용할 변형: {selected_twist}
프레젠테이션: {random_presentation}
참고 예시: {korean_examples}
고유ID: {self._get_unique_id()}
원래 요리의 구성 요소를 완전히 새로운 구조로 재배치하세요.
반드시 한국어로 JSON 형식 응답:
{{"name": "새 요리명", "original_structure": "원래 구조", "new_structure": "새 구조", "transformation": "변환 방법", "components": {{"original": ["원래 구성요소1", "원래 구성요소2"], "transformed": ["변환된 구성요소1", "변환된 구성요소2"]}}, "eating_experience": "먹는 경험", "visual_impact": "시각적 임팩트"}}"""
response = self.llm.chat([{"role": "user", "content": prompt}])
default = {
"name": f"{dish} {arch.get('name_ko', new_structure)} 스타일",
"original_structure": "혼합형 또는 적층형",
"new_structure": arch.get('name_ko', new_structure),
"transformation": selected_twist,
"components": {
"original": [f"{dish}의 주재료", "양념", "부재료"],
"transformed": ["구조 변환된 주재료", "새 형태 양념", "재배치된 부재료"]
},
"eating_experience": f"{random_presentation} 방식의 새로운 식사 경험",
"visual_impact": "구조 변환의 시각적 놀라움"
}
result = parse_json_safely(response, default)
return {
"emergence_type": "architecture_inversion",
"base_dish": dish,
"applied_rules": {
"new_structure": new_structure,
"twist_used": selected_twist,
"presentation": random_presentation
},
"result": result
}
def _grammar_transplant(self, dish: str, new_culture: str) -> Dict:
"""문법 이식 - v3.2: 랜덤 요소 강화"""
# 빈 값이면 랜덤 선택
if not new_culture:
new_culture, selected = self.data.get_random_grammar()
else:
grammars = self.data.get_grammar(new_culture)
selected = random.choice(grammars) if grammars else {"name": f"{new_culture} 스타일"}
culture_names = {
"korean": "한식", "japanese": "일식", "french": "프랑스",
"italian": "이탈리아", "indian": "인도", "chinese": "중식",
"thai": "태국", "mexican": "멕시코"
}
culture_name = culture_names.get(new_culture.lower(), new_culture)
# v3.2: 랜덤 강도 및 피니싱
random_intensity = random.choice(["미묘한", "균형잡힌", "강렬한", "압도적인"])
random_finishing = random.choice([
"허브 오일 피니시", "시트러스 제스트", "크리스피 토핑",
"소스 드리즐", "훈연 향 추가", "발사믹 글레이즈"
])
prompt = f"""창발적 요리 생성: 문법 이식
원래 요리: {dish}
이식할 양념 문법: {selected.get('name', culture_name)} ({culture_name})
문법 구성: {json.dumps(selected, ensure_ascii=False)}
강도: {random_intensity}
피니싱: {random_finishing}
고유ID: {self._get_unique_id()}
원래 요리의 양념 체계를 완전히 새로운 문화권의 문법으로 대체하세요.
반드시 한국어로 JSON 형식 응답:
{{"name": "새 요리명", "original_seasoning": "원래 양념", "new_seasoning": "새 양념", "flavor_profile": "새 풍미 프로파일", "cooking_adaptation": "조리법 변화", "finishing_touch": "마무리 터치", "pairing_suggestion": "페어링 제안"}}"""
response = self.llm.chat([{"role": "user", "content": prompt}])
default = {
"name": f"{dish} ({selected.get('name', culture_name)} 버전)",
"original_seasoning": f"{dish}의 원래 양념",
"new_seasoning": f"{selected.get('name', culture_name)} 양념 체계",
"flavor_profile": f"{random_intensity} {culture_name} 특유의 풍미",
"cooking_adaptation": f"{culture_name} 조리 기법 적용",
"finishing_touch": random_finishing,
"pairing_suggestion": f"{culture_name} 스타일 음료와 함께"
}
result = parse_json_safely(response, default)
return {
"emergence_type": "grammar_transplantation",
"base_dish": dish,
"applied_rules": {
"new_culture": culture_name,
"grammar": selected.get('name', ''),
"intensity": random_intensity,
"finishing": random_finishing
},
"result": result
}
def _compound_pairing(self, dish: str, ingredient: str) -> Dict:
"""풍미 화합물 페어링 - v3.2: 랜덤 요소 대폭 강화!"""
# 빈 값이면 랜덤 선택
if not ingredient:
profile = self.data.get_random_flavor_profile()
ingredient = profile.get("ingredient", "김치") if profile else "김치"
profile = self.data.get_flavor_profile(ingredient)
emergent = profile.get("emergent_pairings", []).copy() if profile else []
compounds = profile.get("compounds", []) if profile else []
if not emergent:
emergent = ["초콜릿", "꿀", "커피", "바닐라", "캐러멜", "트러플", "파마산", "마카다미아", "블루치즈"]
# v3.2: 셔플하여 매번 다른 페어링!
random.shuffle(emergent)
selected_pairing = emergent[0]
# v3.2: 랜덤 적용 기법
random_technique = random.choice([
"인퓨전으로", "레이어링으로", "마무리 터치로",
"소스 베이스로", "크러스트/코팅으로", "가니쉬로"
])
random_ratio = random.choice([
"미묘한 힌트 (5%)", "균형 (30%)", "주연급 (50%)", "대담한 전환 (70%)"
])
prompt = f"""창발적 요리 생성: 풍미 화합물 페어링
기본 요리: {dish}
핵심 재료: {ingredient}
해당 재료의 화합물: {compounds}
창발적 페어링: {selected_pairing}
적용 기법: {random_technique}
비율: {random_ratio}
고유ID: {self._get_unique_id()}
Food Pairing Theory에 기반하여, 공유 화합물을 통한 예상치 못한 재료 조합을 제안하세요.
반드시 한국어로 JSON 형식 응답:
{{"name": "새 요리명", "unexpected_pairing": "{selected_pairing}", "shared_compounds": ["공유 화합물1", "공유 화합물2"], "flavor_harmony": "맛 조화 설명", "dish_description": "완성 요리 설명", "why_it_works": "왜 이 조합이 작동하는지", "application_method": "적용 방법", "tasting_notes": "테이스팅 노트"}}"""
response = self.llm.chat([{"role": "user", "content": prompt}])
default = {
"name": f"{dish} with {selected_pairing}",
"unexpected_pairing": selected_pairing,
"shared_compounds": compounds if compounds else ["피라진", "글루타메이트"],
"flavor_harmony": f"{ingredient}{selected_pairing}의 예상치 못한 조화",
"dish_description": f"{dish}{selected_pairing}{random_technique} 결합한 창의적 요리",
"why_it_works": "공유하는 향 화합물이 맛의 조화를 만듦",
"application_method": f"{random_technique} ({random_ratio})",
"tasting_notes": "처음에는 익숙한 맛, 점차 새로운 풍미 발견"
}
result = parse_json_safely(response, default)
return {
"emergence_type": "flavor_compound_pairing",
"base_dish": dish,
"applied_rules": {
"ingredient": ingredient,
"selected_pairing": selected_pairing,
"all_pairings": emergent[:5], # 상위 5개 페어링 옵션
"technique": random_technique,
"ratio": random_ratio
},
"result": result
}
def _texture_contrast(self, dish: str, target_texture: str) -> Dict:
"""텍스처 대비 - v3.2: 랜덤 요소 강화"""
# 빈 값이면 랜덤 선택
if not target_texture:
texture = self.data.get_random_texture()
target_texture = texture.get("name", "Crispy") if texture else "Crispy"
texture = self.data.get_texture(target_texture)
if not texture:
texture = self.data.get_random_texture() or {
"name": target_texture,
"name_ko": target_texture,
"contrast_pairing": ["Soft", "Creamy"],
"achieved_by": ["특수 조리법"]
}
contrasts = texture.get("contrast_pairing", ["Creamy", "Soft"])
achieved_by = texture.get("achieved_by", ["조리법"])
# v3.2: 대비 텍스처 랜덤 선택
selected_contrast = random.choice(contrasts)
selected_method = random.choice(achieved_by)
# v3.2: 랜덤 강도
random_intensity = random.choice(["미묘한", "확실한", "극적인", "압도적인"])
prompt = f"""창발적 요리 생성: 텍스처 대비 최적화
기본 요리: {dish}
목표 텍스처: {target_texture} ({texture.get('name_ko', '')})
대비 텍스처: {selected_contrast}
달성 방법: {selected_method}
대비 강도: {random_intensity}
고유ID: {self._get_unique_id()}
{dish}{target_texture} 텍스처를 추가하여 질감의 대비를 극대화하는 새로운 버전을 제안하세요.
반드시 한국어로 JSON 형식 응답:
{{"name": "새 요리명", "base_texture": "원래 텍스처", "added_texture": "추가된 텍스처", "contrast_effect": "대비 효과", "technique": "적용 기법", "eating_pleasure": "먹는 즐거움", "texture_journey": "질감의 여정", "key_component": "핵심 컴포넌트"}}"""
response = self.llm.chat([{"role": "user", "content": prompt}])
default = {
"name": f"{texture.get('name_ko', target_texture)} {dish}",
"base_texture": "부드러움",
"added_texture": texture.get('name_ko', target_texture),
"contrast_effect": f"{random_intensity} {texture.get('name_ko', target_texture)}{selected_contrast}의 대비",
"technique": selected_method,
"eating_pleasure": "다양한 질감이 어우러진 즐거움",
"texture_journey": f"처음 {texture.get('name_ko', target_texture)} → 중간 {selected_contrast} → 마무리 조화",
"key_component": f"{selected_method}로 달성한 {texture.get('name_ko', target_texture)}"
}
result = parse_json_safely(response, default)
return {
"emergence_type": "texture_contrast",
"base_dish": dish,
"applied_rules": {
"target_texture": target_texture,
"contrast_texture": selected_contrast,
"method": selected_method,
"intensity": random_intensity
},
"result": result
}
# ============================================================================
# GROQ AI CLIENT (v3.2: temperature 상향)
# ============================================================================
from groq import Groq
class GroqAI:
"""Groq AI API 클라이언트"""
def __init__(self, api_key: str = None):
self.api_key = api_key or os.environ.get("GROQ_API_KEY", "")
self.model = "openai/gpt-oss-120b"
self.client = Groq(api_key=self.api_key) if self.api_key else None
def chat(self, messages: List[Dict], temperature: float = 0.8, max_tokens: int = 8192) -> str:
"""v3.2: 기본 temperature 0.7 → 0.8 상향 (더 창의적)"""
if not self.client:
return self._mock_response(messages)
try:
completion = self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=temperature,
max_completion_tokens=max_tokens,
top_p=1,
reasoning_effort="medium",
stream=False,
stop=None
)
return completion.choices[0].message.content or ""
except Exception as e:
print(f"⚠️ Groq API 오류: {e}")
return self._mock_response(messages)
def _mock_response(self, messages: List[Dict]) -> str:
"""API 키 없을 때 목업 응답 (v3.2: 랜덤 강화!)"""
last_msg = messages[-1].get("content", "") if messages else ""
# v3.2: 랜덤 형용사/풍미/질감
random_adjectives = ["혁신적인", "놀라운", "독창적인", "대담한", "섬세한", "우아한", "파격적인", "정교한"]
random_flavors = ["감칠맛", "깊은 풍미", "신선한 향", "복합적인 맛", "은은한 단맛", "스모키한 향"]
random_textures = ["겉바속촉", "크리미하면서 크런치한", "부드럽고 탄력있는", "녹는 듯한"]
adj = random.choice(random_adjectives)
flavor = random.choice(random_flavors)
texture = random.choice(random_textures)
if "executive" in last_msg.lower() or "총괄" in last_msg.lower():
return json.dumps({
"concept": f"{adj} 감성으로 재해석한 정갈한 한 접시",
"vision": "최상급 재료와 섬세한 손맛의 조화",
"flavor_direction": f"{flavor}과 신선함의 균형",
"presentation_style": "미니멀하면서도 우아한 담음새",
"special_notes": "제철 식재료의 본연의 맛을 살림"
}, ensure_ascii=False)
elif "garde" in last_msg.lower() or "재료" in last_msg.lower():
return json.dumps({
"ingredients_detail": [
{"name": "한우 채끝", "amount": f"{random.randint(180, 220)}g", "quality": "1++등급", "role": "메인 단백질", "prep": "3cm 두께로 손질"},
{"name": "송이버섯", "amount": f"{random.randint(40, 60)}g", "quality": "국내산 특등급", "role": "향미 증진", "prep": "슬라이스"},
{"name": "마늘", "amount": f"{random.randint(15, 25)}g", "quality": "의성 마늘", "role": "베이스 향", "prep": "편썰기"},
{"name": "참기름", "amount": f"{random.randint(8, 12)}ml", "quality": "전통 압착", "role": "풍미", "prep": "마무리용"},
{"name": "천일염", "amount": "적량", "quality": "신안 천일염", "role": "간", "prep": "그대로"}
],
"flavor_profile": {"감칠맛": random.randint(7, 10), "짠맛": random.randint(3, 5), "단맛": random.randint(2, 4), "고소함": random.randint(6, 8)},
"pairing_suggestions": shuffle_and_pick(["숙성 레드와인", "트러플 오일", "발사믹 글레이즈", "블루치즈", "아루굴라"], 3)
}, ensure_ascii=False)
elif "research" in last_msg.lower() or "연구" in last_msg.lower():
return json.dumps({
"innovative_elements": [
{"technique": "저온 숙성", "application": f"4°C에서 {random.randint(14, 28)}일 드라이에이징으로 효소 작용 극대화"},
{"technique": "마이야르 반응", "application": f"{random.randint(220, 250)}°C 고온에서 크러스트 형성"},
{"technique": "휴지(레스팅)", "application": f"조리 후 {random.randint(3, 7)}분간 휴지로 육즙 안정화"}
],
"scientific_notes": "콜라겐이 젤라틴으로 전환되어 촉촉함 유지, 마이야르 반응으로 150+ 가지 풍미 화합물 생성",
"texture_targets": [texture, "녹는 듯한 부드러움", "적절한 씹는 맛"],
"michelin_benchmark": "3스타 레스토랑의 정밀 온도 제어 기법 적용"
}, ensure_ascii=False)
elif "saucier" in last_msg.lower() or "조리" in last_msg.lower():
return json.dumps({
"cooking_sequence": [
{"step": 1, "action": "재료 상온 적응", "duration": "30분", "temp": "실온(20°C)", "technique": "템퍼링", "detail": "냉장고에서 꺼내 상온에 두어 균일한 조리"},
{"step": 2, "action": "팬 예열", "duration": "5분", "temp": "230°C", "technique": "고온 예열", "detail": "주철팬을 연기가 날 때까지 예열"},
{"step": 3, "action": "시어링", "duration": "2분", "temp": "230°C", "technique": "시어링", "detail": "한 면당 1분씩, 크러스트 형성"},
{"step": 4, "action": "버터 베이스팅", "duration": "3분", "temp": "180°C", "technique": "아로제", "detail": "버터, 마늘, 허브로 베이스팅"},
{"step": 5, "action": "휴지", "duration": "5분", "temp": "실온", "technique": "레스팅", "detail": "알루미늄 호일로 덮어 휴지"},
{"step": 6, "action": "슬라이스 및 플레이팅", "duration": "3분", "temp": "실온", "technique": "프레젠테이션", "detail": "결 반대 방향으로 슬라이스"}
],
"critical_points": ["고기 온도가 조리 품질의 80%를 결정", "휴지 시간 필수", "팬 온도 충분히 올릴 것"],
"total_time": f"{random.randint(45, 55)}분",
"difficulty": "중상급"
}, ensure_ascii=False)
elif "patissier" in last_msg.lower() or "플레이팅" in last_msg.lower():
return json.dumps({
"plating_design": {
"plate": f"무광 블랙 세라믹 원형 접시 ({random.randint(26, 30)}cm)",
"layout": "황금비율(1:1.618)에 맞춘 비대칭 중앙 배치",
"garnish": shuffle_and_pick(["마이크로 그린", "식용 금박", "송이버섯 슬라이스", "허브 오일 점", "에디블 플라워"], 4),
"sauce_work": "접시 한쪽에 소스를 숟가락으로 드래그"
},
"color_palette": ["고동색(고기)", "진녹색(허브)", "금색(오일)", "흰색(소금)"],
"visual_elements": ["높이감 있는 적층", "자연스러운 소스 흐름", "질감의 대비"],
"final_touches": ["플뢰르 드 셀", "갓 간 후추", "트러플 오일 한 방울", "마이크로 그린"]
}, ensure_ascii=False)
elif "inspector" in last_msg.lower() or "심사" in last_msg.lower():
base_score = random.randint(80, 92)
return json.dumps({
"scores": {
"taste": {"score": random.randint(16, 19), "max": 20, "comment": f"{flavor}의 레이어가 인상적"},
"technique": {"score": random.randint(15, 18), "max": 20, "comment": "정교한 온도 제어와 타이밍"},
"creativity": {"score": random.randint(14, 17), "max": 20, "comment": f"{adj} 해석"},
"presentation": {"score": random.randint(16, 19), "max": 20, "comment": "시각적 임팩트와 세련된 미학"},
"ingredients": {"score": random.randint(17, 20), "max": 20, "comment": "최상급 재료의 완벽한 선택"}
},
"total_score": base_score,
"rating": "⭐⭐⭐ (미슐랭 3스타급)" if base_score >= 88 else "⭐⭐ (미슐랭 2스타급)",
"passed": True,
"strengths": shuffle_and_pick(["재료의 품질과 신선함", "기술적 완성도", "담음새의 예술성", "풍미의 깊이", "창의성"], 3),
"improvements": shuffle_and_pick(["소스의 농도 미세 조정", "채소 요소 강화 가능", "향의 복합성 추가"], 2),
"final_verdict": "미슐랭 수준의 완성도를 보여주는 뛰어난 요리"
}, ensure_ascii=False)
# 창발적 레시피 기본 응답 (v3.2: 랜덤 강화!)
return json.dumps({
"name": f"{adj} 퓨전 요리",
"concept": f"전통과 {adj} 혁신의 만남",
"technique": "복합 조리 기법 적용",
"steps": ["재료 준비", "1차 조리", f"{flavor} 레이어링", f"{texture} 마무리", "플레이팅"],
"expected_texture": texture,
"flavor_notes": f"{flavor}과 신선한 향의 {adj} 균형",
"innovation_point": f"{adj} 발상의 전환"
}, ensure_ascii=False)
# ============================================================================
# IMAGE GENERATOR (v3.2: 창발적 레시피 이미지 생성 추가!)
# ============================================================================
class ImageGenerator:
"""Replicate 이미지 생성기 - google/nano-banana-pro (v3.2: 창발적 레시피 이미지 생성 추가!)"""
def __init__(self):
self.model = "google/nano-banana-pro"
def generate(self, prompt: str, style: str = "photorealistic") -> Optional[str]:
"""일반 이미지 생성 - nano-banana-pro 모델"""
if not HAS_REPLICATE or not os.environ.get("REPLICATE_API_TOKEN"):
return None
try:
style_prompts = {
"photorealistic": "Professional food photography, Michelin star quality, elegant plating, studio lighting, 8k resolution, appetizing, delicious looking",
"artistic": "Artistic food photography, dramatic lighting, creative composition, fine dining aesthetic",
"minimal": "Minimalist food photography, clean background, natural lighting, elegant simplicity"
}
full_prompt = f"{prompt}. {style_prompts.get(style, style_prompts['photorealistic'])}"
# nano-banana-pro API 호출
output = replicate.run(
self.model,
input={
"prompt": full_prompt,
"resolution": "2K",
"aspect_ratio": "1:1",
"output_format": "png",
"safety_filter_level": "block_only_high"
}
)
# nano-banana-pro는 단일 FileOutput 객체 반환
if output:
# output.url 또는 str(output) 사용
if hasattr(output, 'url'):
return str(output.url)
else:
return str(output)
return None
except Exception as e:
print(f"⚠️ 이미지 생성 오류: {e}")
return None
def generate_kfood(self, dish_name: str, category: str, recipe_result: Dict = None) -> Optional[str]:
"""K-FOOD 이미지 생성 - 레시피 결과 기반 정확한 프롬프트"""
# 한국어 요리명을 영어로 변환
korean_to_english = {
"비빔밥": "Bibimbap with colorful vegetables, egg, gochujang sauce",
"떡볶이": "Tteokbokki with red spicy sauce, fish cakes, green onions",
"김치찌개": "Kimchi Jjigae stew with tofu, pork, red broth",
"불고기": "Bulgogi marinated beef, caramelized edges, sesame",
"삼겹살": "Samgyeopsal grilled pork belly slices, lettuce wraps",
"된장찌개": "Doenjang Jjigae with tofu, zucchini, mushrooms",
"갈비": "Galbi Korean BBQ short ribs, glossy glaze",
"냉면": "Naengmyeon cold noodles in icy broth, egg, cucumber",
"잡채": "Japchae glass noodles with vegetables, beef strips",
"돼지국밥": "Dwaeji Gukbap pork rice soup, milky broth, green onions",
"순두부찌개": "Sundubu Jjigae soft tofu stew, bubbling red broth, egg",
"삼계탕": "Samgyetang ginseng chicken soup, whole chicken, jujubes",
"보쌈": "Bossam steamed pork slices, kimchi, ssam vegetables",
"김밥": "Kimbap rice rolls, colorful filling visible, sliced",
"만두": "Mandu dumplings, golden pan-fried, dipping sauce",
"파전": "Pajeon green onion pancake, crispy edges, seafood visible",
"칼국수": "Kalguksu knife-cut noodles in clear broth, zucchini",
}
dish_en = korean_to_english.get(dish_name, dish_name)
# 카테고리별 스타일 요소
category_styles = {
"한정식": "traditional Korean table setting, multiple banchan dishes, elegant ceramics",
"김치/발효": "fermented Korean ingredients, earthy tones, traditional onggi pottery",
"국/탕/찌개": "bubbling hot stew, stone pot (dolsot), steam rising, rich broth",
"구이/적": "grilled Korean BBQ, sizzling on grill, char marks, lettuce wrap setup",
"면/만두": "Korean noodles or dumplings, chopsticks, soup bowl",
"떡/한과": "Korean rice cakes and traditional sweets, colorful, artistic",
"해물요리": "Korean seafood dish, fresh seafood, coastal presentation",
"퓨전한식": "modern Korean fusion, contemporary plating, innovative presentation",
}
style_element = category_styles.get(category, "authentic Korean presentation")
# 레시피 결과에서 추가 정보 추출
extra_elements = []
if recipe_result:
concept = recipe_result.get("concept", {})
if isinstance(concept, dict):
if concept.get("presentation_style"):
extra_elements.append(concept.get("presentation_style"))
plating = recipe_result.get("plating", {})
if isinstance(plating, dict):
if plating.get("garnish"):
garnishes = plating.get("garnish", [])
if isinstance(garnishes, list) and garnishes:
extra_elements.append(f"garnished with {', '.join(garnishes[:3])}")
extra_str = ", ".join(extra_elements) if extra_elements else ""
prompt = f"Korean cuisine: {dish_en}. {style_element}. {extra_str}. Beautiful Korean ceramic dish (백자 or 청자 style), traditional Korean restaurant ambiance, steam visible if hot dish, appetizing and authentic"
print(f"🇰🇷 K-FOOD 이미지 프롬프트: {prompt[:150]}...")
return self.generate(prompt)
def generate_emergent(self, result: Dict, emergence_type: str) -> Optional[str]:
"""v3.2 NEW: 창발적 레시피 이미지 생성! - 레시피 결과 기반 정확한 프롬프트"""
recipe_result = result.get("result", {})
recipe_name = recipe_result.get("name", "창발적 요리")
base_dish = result.get("base_dish", "")
applied_rules = result.get("applied_rules", {})
# 한국어 요리명을 영어로 변환하는 매핑
korean_to_english = {
"비빔밥": "Bibimbap (Korean mixed rice bowl)",
"떡볶이": "Tteokbokki (Korean spicy rice cakes)",
"김치찌개": "Kimchi Jjigae (Korean kimchi stew)",
"불고기": "Bulgogi (Korean BBQ beef)",
"삼겹살": "Samgyeopsal (Korean pork belly)",
"김치": "Kimchi (Korean fermented vegetables)",
"된장찌개": "Doenjang Jjigae (Korean soybean paste stew)",
"갈비": "Galbi (Korean short ribs)",
"냉면": "Naengmyeon (Korean cold noodles)",
"잡채": "Japchae (Korean glass noodles)",
"돼지국밥": "Dwaeji Gukbap (Korean pork rice soup)",
"순두부찌개": "Sundubu Jjigae (Korean soft tofu stew)",
"삼계탕": "Samgyetang (Korean ginseng chicken soup)",
"보쌈": "Bossam (Korean wrapped pork)",
"족발": "Jokbal (Korean pig's feet)",
"김밥": "Kimbap (Korean rice rolls)",
"만두": "Mandu (Korean dumplings)",
"파전": "Pajeon (Korean green onion pancake)",
"칼국수": "Kalguksu (Korean knife-cut noodles)",
"감자탕": "Gamjatang (Korean pork bone stew)",
}
# 기본 요리 영어 변환
base_dish_en = korean_to_english.get(base_dish, base_dish)
# 레시피 결과에서 구체적인 정보 추출
concept = recipe_result.get("concept", "")
technique = recipe_result.get("technique", "")
expected_texture = recipe_result.get("expected_texture", "")
flavor_notes = recipe_result.get("flavor_notes", "")
new_structure = recipe_result.get("new_structure", "")
added_texture = recipe_result.get("added_texture", "")
unexpected_pairing = recipe_result.get("unexpected_pairing", "")
new_seasoning = recipe_result.get("new_seasoning", "")
# 기본 프롬프트 구성요소
base_elements = []
# 창발 유형별 핵심 시각 요소 추출
if emergence_type == "variable_crossing":
method1 = applied_rules.get("method1", "")
method2 = applied_rules.get("method2", "")
twist = applied_rules.get("twist", "")
# 조리법에 따른 시각적 특성
method_visuals = {
"수비드": "perfectly cooked, even pink center, moist texture",
"시어링": "golden brown crust, caramelized surface, Maillard reaction",
"그릴링": "char marks, smoky appearance, grilled texture",
"로스팅": "crispy golden skin, roasted appearance",
"브레이징": "tender falling-apart meat, rich sauce",
"튀김": "crispy golden fried coating, crunchy texture",
"찌기": "steamed, moist, delicate texture",
"콩피": "tender confit, slow-cooked, rich",
}
visual1 = method_visuals.get(method1, "professionally cooked")
visual2 = method_visuals.get(method2, "expertly prepared")
base_elements = [
f"A dish of {base_dish_en}",
f"showing {visual1} combined with {visual2}",
f"texture: {expected_texture}" if expected_texture else "",
f"technique: {technique}" if technique else "",
]
elif emergence_type == "context_shift":
target_cuisine = applied_rules.get("target_cuisine", "")
fusion_level = applied_rules.get("fusion", "")
cuisine_visuals = {
"이탈리아": "Italian style plating, pasta elements, olive oil drizzle, fresh basil, parmesan",
"프랑스": "French fine dining presentation, elegant sauce, refined garnish",
"일식": "Japanese minimalist plating, wasabi, soy sauce, nori, chopsticks",
"인도": "Indian spices visible, curry colors, naan bread, colorful presentation",
"태국": "Thai style, lime wedge, fresh herbs, chili, coconut elements",
"멕시코": "Mexican style, tortilla, avocado, lime, cilantro, vibrant colors",
"한식": "Korean banchan style, gochujang red, sesame seeds, Korean ceramics",
}
cuisine_visual = cuisine_visuals.get(target_cuisine, f"{target_cuisine} cuisine style")
base_elements = [
f"{base_dish_en} reimagined in {target_cuisine} style",
cuisine_visual,
f"fusion level: {fusion_level}" if fusion_level else "creative fusion",
f"keeping original essence with new cultural presentation",
]
elif emergence_type == "architecture_inversion":
new_struct = applied_rules.get("new_structure", "")
presentation = applied_rules.get("presentation", "")
structure_visuals = {
"Wrapped": "wrapped in thin wrapper, dumpling or spring roll style, enclosed filling",
"Layered": "layered presentation, stacked components, visible layers",
"Stuffed": "stuffed presentation, filling visible, pocket style",
"Skewered": "on skewers, yakitori style, grilled pieces on stick",
"Soup-based": "served in broth, soup bowl presentation, floating ingredients",
"Coated": "coated with sauce or breading, glazed surface",
"포용형": "wrapped style, enclosed in wrapper",
"적층형": "layered stack, multiple tiers",
"채움형": "stuffed, filled presentation",
}
struct_visual = structure_visuals.get(new_struct, "deconstructed presentation")
base_elements = [
f"{base_dish_en} transformed into {new_struct} structure",
struct_visual,
f"presentation: {presentation}" if presentation else "artistic plating",
"innovative food architecture, deconstructed and reconstructed",
]
elif emergence_type == "grammar_transplantation":
new_culture = applied_rules.get("new_culture", "")
finishing = applied_rules.get("finishing", "")
intensity = applied_rules.get("intensity", "")
grammar_visuals = {
"한식": "gochujang glaze, sesame oil sheen, Korean red pepper flakes",
"일식": "miso glaze, dashi elements, Japanese seven spice",
"프랑스": "French butter sauce, herb butter, fine herbs",
"이탈리아": "Italian herb oil, basil pesto, balsamic reduction",
"인도": "Indian curry spices, turmeric yellow, garam masala",
"태국": "Thai fish sauce glaze, lemongrass, Thai basil",
"멕시코": "chipotle sauce, Mexican spice rub, lime zest",
}
grammar_visual = grammar_visuals.get(new_culture, f"{new_culture} seasoning style")
base_elements = [
f"{base_dish_en} with {new_culture} seasoning and flavors",
grammar_visual,
f"finishing touch: {finishing}" if finishing else "",
f"intensity: {intensity}" if intensity else "",
f"new seasoning: {new_seasoning}" if new_seasoning else "",
]
elif emergence_type == "flavor_compound_pairing":
ingredient = applied_rules.get("ingredient", "")
pairing = applied_rules.get("selected_pairing", "")
tech = applied_rules.get("technique", "")
ratio = applied_rules.get("ratio", "")
# 재료별 시각적 특성
ingredient_visuals = {
"김치": "fermented red kimchi, Korean chili flakes visible",
"고추장": "red gochujang sauce, glossy red glaze",
"된장": "brown miso-like paste, earthy tones",
"간장": "dark soy glaze, caramelized shine",
"참기름": "sesame oil drizzle, nutty golden oil drops",
}
pairing_visuals = {
"Chocolate": "chocolate sauce drizzle, dark cocoa elements",
"Cheese": "melted cheese, cheese shavings",
"Honey": "honey drizzle, golden sticky glaze",
"Bacon": "crispy bacon bits, smoky meat garnish",
"Coffee": "coffee reduction sauce, espresso elements",
"Caramel": "caramel sauce, golden sticky coating",
"Truffle": "truffle shavings, black truffle slices",
"Mango": "fresh mango cubes, tropical fruit garnish",
}
ing_visual = ingredient_visuals.get(ingredient, ingredient)
pair_visual = pairing_visuals.get(pairing, pairing)
base_elements = [
f"{base_dish_en} featuring {ingredient} paired with {pairing}",
f"showing {ing_visual}",
f"combined with {pair_visual}",
f"application: {tech}" if tech else "",
f"unexpected but harmonious flavor combination",
]
elif emergence_type == "texture_contrast":
target_texture = applied_rules.get("target_texture", "")
contrast = applied_rules.get("contrast_texture", "")
intensity = applied_rules.get("intensity", "")
texture_visuals = {
"Crispy": "golden crispy crust, crunchy coating visible",
"Creamy": "smooth creamy sauce, velvety texture",
"Chewy": "chewy texture, glutinous appearance",
"Tender": "fork-tender, soft falling apart",
"Crunchy": "crunchy bits, textured crumbs",
"Silky": "silky smooth surface, glossy finish",
"Fluffy": "light fluffy texture, airy appearance",
"바삭함": "crispy golden surface",
"크리미": "creamy smooth sauce",
"쫄깃함": "chewy bouncy texture",
}
tex1_visual = texture_visuals.get(target_texture, target_texture)
tex2_visual = texture_visuals.get(contrast, contrast)
base_elements = [
f"{base_dish_en} with {target_texture} and {contrast} texture contrast",
f"showing {tex1_visual}",
f"contrasted with {tex2_visual}",
f"intensity: {intensity}" if intensity else "",
"perfect textural harmony visible",
]
else:
base_elements = [
f"Creative fusion dish based on {base_dish_en}",
"innovative modern gastronomy",
"artistic plating",
]
# 프롬프트 조합
visual_elements = [e for e in base_elements if e] # 빈 문자열 제거
# 최종 프롬프트 생성
prompt = f"{', '.join(visual_elements)}. Professional food photography, Michelin star quality plating, elegant ceramic plate, studio lighting, appetizing presentation, 8k resolution, food magazine cover quality"
print(f"🖼️ 이미지 프롬프트: {prompt[:200]}...") # 디버그 로그
return self.generate(prompt)
# ============================================================================
# SOMA AGENT TEAM (6명의 전문 셰프)
# ============================================================================
class AgentRole(Enum):
EXECUTIVE_CHEF = "executive_chef"
GARDE_MANGER = "garde_manger"
RESEARCH_CHEF = "research_chef"
SAUCIER = "saucier"
PATISSIER = "patissier"
MICHELIN_INSPECTOR = "michelin_inspector"
AGENT_PROMPTS = {
AgentRole.EXECUTIVE_CHEF: """당신은 미슐랭 3스타 레스토랑의 총괄 셰프입니다.
요청된 요리에 대한 전체적인 비전과 컨셉을 제시하세요.
반드시 한국어로 JSON 형식으로만 응답하세요:
{
"concept": "요리 컨셉",
"vision": "요리 비전",
"flavor_direction": "맛의 방향성",
"presentation_style": "프레젠테이션 스타일",
"special_notes": "특별 노트"
}""",
AgentRole.GARDE_MANGER: """당신은 미슐랭 레스토랑의 재료 전문가(가르드 망제)입니다.
최상의 재료를 선별하고 상세한 재료 목록을 작성하세요.
반드시 한국어로 JSON 형식으로만 응답하세요:
{
"ingredients_detail": [
{"name": "재료명", "amount": "분량", "quality": "품질등급", "role": "역할", "prep": "손질법"}
],
"flavor_profile": {"감칠맛": 0-10, "짠맛": 0-10, "단맛": 0-10, "매운맛": 0-10, "신맛": 0-10},
"pairing_suggestions": ["페어링 제안들"]
}""",
AgentRole.RESEARCH_CHEF: """당신은 R&D 셰프로서 최신 조리 과학을 적용합니다.
혁신적인 기법과 과학적 접근을 제안하세요.
반드시 한국어로 JSON 형식으로만 응답하세요:
{
"innovative_elements": [
{"technique": "기법명", "application": "적용 방법"}
],
"scientific_notes": "과학적 설명",
"texture_targets": ["목표 텍스처들"],
"michelin_benchmark": "미슐랭 수준 벤치마크"
}""",
AgentRole.SAUCIER: """당신은 소시에(소스/조리 전문)로서 완벽한 조리 순서를 설계합니다.
단계별 상세 조리법을 작성하세요.
반드시 한국어로 JSON 형식으로만 응답하세요:
{
"cooking_sequence": [
{"step": 1, "action": "동작", "duration": "시간", "temp": "온도", "technique": "기법", "detail": "상세설명"}
],
"critical_points": ["핵심 포인트들"],
"total_time": "총 소요시간",
"difficulty": "난이도"
}""",
AgentRole.PATISSIER: """당신은 파티시에/플레이팅 전문가입니다.
미슐랭 수준의 플레이팅 디자인을 제시하세요.
반드시 한국어로 JSON 형식으로만 응답하세요:
{
"plating_design": {
"plate": "접시 종류",
"layout": "배치 방식",
"garnish": ["가니쉬 목록"],
"sauce_work": "소스 작업"
},
"color_palette": ["색상 팔레트"],
"visual_elements": ["시각적 요소들"],
"final_touches": ["마무리 터치"]
}""",
AgentRole.MICHELIN_INSPECTOR: """당신은 미슐랭 가이드 심사관입니다.
제시된 레시피를 엄격하게 평가하세요.
반드시 한국어로 JSON 형식으로만 응답하세요:
{
"scores": {
"taste": {"score": 0-20, "max": 20, "comment": "맛 평가"},
"technique": {"score": 0-20, "max": 20, "comment": "기술 평가"},
"creativity": {"score": 0-20, "max": 20, "comment": "창의성 평가"},
"presentation": {"score": 0-20, "max": 20, "comment": "프레젠테이션 평가"},
"ingredients": {"score": 0-20, "max": 20, "comment": "재료 평가"}
},
"total_score": 0-100,
"rating": "미슐랭 등급",
"passed": true/false,
"strengths": ["강점들"],
"improvements": ["개선점들"],
"final_verdict": "최종 평가"
}"""
}
class SOMATeam:
"""SOMA Agent Team - 6명의 전문 셰프가 협업하여 레시피 개발"""
def __init__(self, llm: GroqAI):
self.llm = llm
self.workflow = [
AgentRole.EXECUTIVE_CHEF,
AgentRole.GARDE_MANGER,
AgentRole.RESEARCH_CHEF,
AgentRole.SAUCIER,
AgentRole.PATISSIER,
AgentRole.MICHELIN_INSPECTOR
]
def run_agent(self, role: AgentRole, context: Dict) -> Dict:
"""개별 에이전트 실행 - 안전한 파싱 적용"""
prompt = AGENT_PROMPTS.get(role, "")
user_request = context.get('user_request', '')
ingredients = context.get('ingredients', [])
constraints = context.get('constraints', {})
previous = context.get('previous_outputs', {})
prev_summary = ""
for key, value in previous.items():
if value and isinstance(value, dict):
prev_summary += f"\n{key} 결과: {json.dumps(value, ensure_ascii=False)[:500]}"
message = f"""{prompt}
요리 요청: {user_request}
사용 가능 재료: {', '.join(ingredients) if ingredients else '제한 없음'}
제약 조건: {json.dumps(constraints, ensure_ascii=False) if constraints else '없음'}
{prev_summary if prev_summary else ''}
반드시 위에서 요청한 JSON 형식으로만 응답하세요."""
response = self.llm.chat([{"role": "system", "content": prompt}, {"role": "user", "content": message}])
defaults = {
AgentRole.EXECUTIVE_CHEF: {
"concept": f"{user_request}의 현대적 재해석",
"vision": "최상급 재료와 섬세한 기법의 조화",
"flavor_direction": "깊은 감칠맛과 신선함의 균형",
"presentation_style": "미니멀하고 세련된 담음새",
"special_notes": "제철 식재료 활용"
},
AgentRole.GARDE_MANGER: {
"ingredients_detail": [
{"name": "주재료", "amount": "200g", "quality": "특등급", "role": "메인", "prep": "손질 완료"},
{"name": "부재료", "amount": "100g", "quality": "상등급", "role": "보조", "prep": "다듬기"},
{"name": "양념 재료", "amount": "적량", "quality": "국산", "role": "풍미", "prep": "다지기"}
],
"flavor_profile": {"감칠맛": 8, "짠맛": 5, "단맛": 3, "매운맛": 2, "신맛": 2},
"pairing_suggestions": ["레드와인", "파마산 치즈", "트러플 오일"]
},
AgentRole.RESEARCH_CHEF: {
"innovative_elements": [
{"technique": "저온 조리", "application": "62도에서 최적 텍스처 달성"},
{"technique": "마이야르 반응", "application": "고온에서 풍미 극대화"}
],
"scientific_notes": "단백질 변성과 마이야르 반응을 통한 풍미 극대화",
"texture_targets": ["겉바속촉", "부드러운 내부", "크리스피한 외부"],
"michelin_benchmark": "3성급 기법 적용"
},
AgentRole.SAUCIER: {
"cooking_sequence": [
{"step": 1, "action": "재료 손질", "duration": "15분", "temp": "실온", "technique": "기본 손질", "detail": "모든 재료를 적절한 크기로 준비"},
{"step": 2, "action": "밑준비", "duration": "10분", "temp": "실온", "technique": "마리네이드", "detail": "양념에 재우기"},
{"step": 3, "action": "예열", "duration": "5분", "temp": "180°C", "technique": "팬 예열", "detail": "팬을 충분히 예열"},
{"step": 4, "action": "조리", "duration": "15분", "temp": "중불-강불", "technique": "볶음/구이", "detail": "핵심 조리 단계"},
{"step": 5, "action": "마무리", "duration": "5분", "temp": "약불", "technique": "휴지", "detail": "맛 안정화"}
],
"critical_points": ["온도 관리가 핵심", "타이밍 엄수", "재료 투입 순서 중요"],
"total_time": "50분",
"difficulty": "중급"
},
AgentRole.PATISSIER: {
"plating_design": {
"plate": "흰색 원형 접시 (28cm)",
"layout": "중앙 배치, 비대칭 포인트",
"garnish": ["마이크로 그린", "식용꽃", "소스 점", "허브 오일"],
"sauce_work": "숟가락으로 우아하게 드리즐"
},
"color_palette": ["갈색", "녹색", "흰색", "금색 포인트"],
"visual_elements": ["높이감", "질감 대비", "색상 조화", "여백의 미"],
"final_touches": ["플뢰르 드 셀", "후추", "허브 오일", "마이크로 그린"]
},
AgentRole.MICHELIN_INSPECTOR: {
"scores": {
"taste": {"score": 17, "max": 20, "comment": "풍미의 깊이와 균형이 훌륭함"},
"technique": {"score": 16, "max": 20, "comment": "안정적인 기법 구사"},
"creativity": {"score": 15, "max": 20, "comment": "독창적 요소 가미"},
"presentation": {"score": 17, "max": 20, "comment": "시각적으로 매력적"},
"ingredients": {"score": 18, "max": 20, "comment": "고품질 재료 사용"}
},
"total_score": 83,
"rating": "⭐⭐ (미슐랭 2스타급)",
"passed": True,
"strengths": ["재료 품질", "기본기 탄탄", "담음새 정갈"],
"improvements": ["창의성 강화 여지", "시그니처 요소 개발"],
"final_verdict": "높은 완성도의 요리로, 미슐랭 수준에 근접"
}
}
result = parse_json_safely(response, defaults.get(role, {}))
return result
KFOOD_AGENT_PROMPTS = {
"총괄셰프": """당신은 한식 미슐랭 3스타 총괄 셰프입니다.
전통 한식의 철학과 현대적 해석을 결합한 비전을 제시하세요.
반드시 한국어로 JSON 형식으로만 응답하세요:
{
"concept": "요리 컨셉",
"korean_philosophy": "한식 철학 (오미, 오색, 약식동원 등)",
"seasonal_approach": "계절적 접근",
"regional_influence": "지역적 영향"
}""",
"재료전문가": """당신은 한식 재료 전문가입니다.
최상의 한국 전통 재료를 선별하고 상세 목록을 작성하세요.
반드시 한국어로 JSON 형식으로만 응답하세요:
{
"ingredients_detail": [
{"name": "재료명", "amount": "분량", "role": "역할", "traditional_use": "전통적 쓰임새", "prep": "손질법"}
],
"sourcing": "재료 조달 정보",
"quality_notes": "품질 노트"
}""",
"발효연구원": """당신은 발효 전문가입니다.
한국 전통 발효의 과학과 응용을 설명하세요.
반드시 한국어로 JSON 형식으로만 응답하세요:
{
"fermentation_notes": {
"base": "발효 베이스",
"time": "발효 시간",
"technique": "발효 기법",
"flavor_development": "풍미 발달 과정"
},
"microbe_culture": "미생물 문화",
"optimal_conditions": "최적 조건"
}""",
"조리장인": """당신은 한식 조리 장인입니다.
전통 기법과 현대 기술을 결합한 조리법을 설계하세요.
반드시 한국어로 JSON 형식으로만 응답하세요:
{
"cooking_sequence": [
{"step": 1, "action": "동작", "duration": "시간", "temp": "온도", "korean_technique": "한식 기법", "detail": "상세설명"}
],
"critical_points": ["핵심 포인트들"],
"total_time": "총 소요시간",
"difficulty": "난이도"
}""",
"플레이팅전문가": """당신은 한식 플레이팅 전문가입니다.
전통 미학과 현대적 감각을 조화시킨 플레이팅을 디자인하세요.
반드시 한국어로 JSON 형식으로만 응답하세요:
{
"plating_design": {
"vessel": "그릇 종류 (백자, 옹기, 유기 등)",
"layout": "배치 방식",
"garnish": ["고명 목록"],
"korean_elements": ["한국적 요소들"]
},
"color_harmony": "색의 조화 (오방색 등)",
"visual_story": "시각적 스토리"
}"""
}
class KFoodSOMATeam:
"""K-FOOD 전문 SOMA Agent Team"""
def __init__(self, llm:GroqAI):
self.llm = llm
self.workflow = list(KFOOD_AGENT_PROMPTS.keys())
def run_agent(self, role: str, context: Dict) -> Dict:
"""K-FOOD 에이전트 실행 - 안전한 파싱 적용"""
prompt = KFOOD_AGENT_PROMPTS.get(role, "")
user_request = context.get('user_request', '')
category = context.get('category', '')
regional = context.get('regional_style', '')
season = context.get('season', '')
spice = context.get('spice_level', 5)
previous = context.get('previous_outputs', {})
prev_summary = ""
for k, v in previous.items():
if v and isinstance(v, dict):
prev_summary += f"\n{k} 결과: {json.dumps(v, ensure_ascii=False)[:500]}"
message = f"""{prompt}
요리 요청: {user_request}
카테고리: {category}
지역 스타일: {regional}
계절: {season}
매운맛 레벨: {spice}/10
{prev_summary if prev_summary else ''}
반드시 위에서 요청한 JSON 형식으로만 응답하세요."""
response = self.llm.chat([{"role": "user", "content": message}])
defaults = {
"총괄셰프": {
"concept": f"{user_request}의 현대적 한식 재해석",
"korean_philosophy": "오미(五味)의 조화와 약식동원(藥食同源)의 정신",
"seasonal_approach": f"{season} 제철 재료의 본연의 맛 활용",
"regional_influence": f"{regional} 지역의 맛 특성 반영"
},
"재료전문가": {
"ingredients_detail": [
{"name": "주재료", "amount": "200g", "role": "메인", "traditional_use": "전통 요리의 핵심 재료", "prep": "전통 방식으로 손질"},
{"name": "양념 재료", "amount": "적량", "role": "풍미", "traditional_use": "갖은양념의 기본", "prep": "다지기"},
{"name": "고명", "amount": "소량", "role": "마무리", "traditional_use": "색과 향 추가", "prep": "채썰기"}
],
"sourcing": "국내산 유기농 재료 우선, 제철 재료 활용",
"quality_notes": "신선도와 원산지 확인 필수"
},
"발효연구원": {
"fermentation_notes": {
"base": "전통 고추장/된장 베이스",
"time": "최소 6개월 자연 숙성",
"technique": "옹기 항아리에서 전통 방식 발효",
"flavor_development": "시간이 만들어낸 깊은 감칠맛과 복합적 풍미"
},
"microbe_culture": "유익균이 풍부한 전통 종균 활용",
"optimal_conditions": "15-25°C, 적정 습도 유지"
},
"조리장인": {
"cooking_sequence": [
{"step": 1, "action": "양념장 준비", "duration": "15분", "temp": "실온", "korean_technique": "갖은양념 배합", "detail": "마늘, 생강, 간장, 참기름 등 배합"},
{"step": 2, "action": "재료 손질", "duration": "20분", "temp": "실온", "korean_technique": "전통 써는 법", "detail": "재료 특성에 맞게 손질"},
{"step": 3, "action": "밑간 및 재우기", "duration": "30분", "temp": "냉장", "korean_technique": "간 배기", "detail": "양념이 배도록 재우기"},
{"step": 4, "action": "불 조절 조리", "duration": "20분", "temp": "중불-강불", "korean_technique": "센불/약불 조절", "detail": "한식 특유의 불 조절"},
{"step": 5, "action": "담기", "duration": "5분", "temp": "실온", "korean_technique": "정갈한 담음새", "detail": "전통 방식으로 정갈하게"}
],
"critical_points": ["불 조절이 맛의 핵심", "간은 중간중간 확인", "담음새도 맛의 일부"],
"total_time": "90분",
"difficulty": "중급"
},
"플레이팅전문가": {
"plating_design": {
"vessel": "백자 원형 접시 또는 유기 그릇",
"layout": "중앙에 정갈하게, 여백의 미 살리기",
"garnish": ["통깨", "참기름 한 방울", "송송 썬 실파", "채 썬 홍고추"],
"korean_elements": ["자연스러운 배치", "색의 조화", "계절감 표현"]
},
"color_harmony": "오방색(빨강, 노랑, 파랑, 흰색, 검정)의 자연스러운 조화",
"visual_story": "한국의 사계절과 정성을 담은 한 접시"
}
}
result = parse_json_safely(response, defaults.get(role, {}))
return result
# ============================================================================
# DATABASE (CSV 로더 + SQLite)
# ============================================================================
def load_michelin_csv(csv_path: str = "michelin.csv") -> List[Dict]:
"""미슐랭 가이드 CSV 로드"""
possible_paths = [
csv_path,
os.path.join(os.path.dirname(__file__), csv_path),
os.path.join(os.getcwd(), csv_path),
]
for path in possible_paths:
if os.path.exists(path):
restaurants = []
with open(path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
award = row.get("Award", "")
stars = 0
if "3 MICHELIN Star" in award or "3 Star" in award:
stars = 3
elif "2 MICHELIN Star" in award or "2 Star" in award:
stars = 2
elif "1 MICHELIN Star" in award or "1 Star" in award:
stars = 1
restaurants.append({
"name": row.get("Name", ""),
"location": row.get("Location", ""),
"cuisine": row.get("Cuisine", ""),
"stars": stars,
"award": award,
"description": row.get("Description", "")
})
print(f"✅ 미슐랭 CSV 로드: {len(restaurants)}개 레스토랑")
return restaurants
print("⚠️ michelin.csv 없음 - 샘플 데이터 사용")
return [
{"name": "가온", "location": "서울", "cuisine": "한식", "stars": 3, "award": "3 MICHELIN Stars", "description": "정갈한 한정식의 정수"},
{"name": "정식당", "location": "서울", "cuisine": "한식", "stars": 3, "award": "3 MICHELIN Stars", "description": "현대적으로 재해석한 한식"},
{"name": "라연", "location": "서울", "cuisine": "한식", "stars": 3, "award": "3 MICHELIN Stars", "description": "고급 한정식"},
{"name": "모수", "location": "서울", "cuisine": "한식", "stars": 2, "award": "2 MICHELIN Stars", "description": "창의적인 한식"},
{"name": "알라프리마", "location": "서울", "cuisine": "이탈리안", "stars": 2, "award": "2 MICHELIN Stars", "description": "정통 이탈리안"},
{"name": "The Restaurant at Meadowood", "location": "Napa Valley", "cuisine": "Contemporary American", "stars": 3, "award": "3 MICHELIN Stars", "description": "California cuisine"},
{"name": "Eleven Madison Park", "location": "New York", "cuisine": "Contemporary American", "stars": 3, "award": "3 MICHELIN Stars", "description": "Plant-based fine dining"},
{"name": "Noma", "location": "Copenhagen", "cuisine": "New Nordic", "stars": 3, "award": "3 MICHELIN Stars", "description": "Foraging and fermentation"},
{"name": "Osteria Francescana", "location": "Modena", "cuisine": "Italian", "stars": 3, "award": "3 MICHELIN Stars", "description": "Modern Italian"},
{"name": "The Fat Duck", "location": "Bray", "cuisine": "British", "stars": 3, "award": "3 MICHELIN Stars", "description": "Molecular gastronomy"}
]
class MichelinDatabase:
"""미슐랭 레시피 및 레스토랑 데이터베이스"""
def __init__(self, db_path: str = "michelin_recipes.db", csv_path: str = "michelin.csv"):
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self.cursor = self.conn.cursor()
self._create_tables()
self._load_restaurants(csv_path)
def _create_tables(self):
self.cursor.executescript('''
CREATE TABLE IF NOT EXISTS restaurants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
location TEXT,
cuisine TEXT,
stars INTEGER DEFAULT 0,
award TEXT,
description TEXT
);
CREATE TABLE IF NOT EXISTS recipes (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
ingredients TEXT,
steps TEXT,
evaluation TEXT,
total_score INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS kfood_recipes (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
category TEXT,
regional_style TEXT,
season TEXT,
spice_level INTEGER,
ingredients TEXT,
steps TEXT,
fermentation_notes TEXT,
evaluation TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS emergent_recipes (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
base_dish TEXT,
emergence_type TEXT,
applied_rules TEXT,
result TEXT,
image_url TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS flavor_vectors (
ingredient_id TEXT PRIMARY KEY,
ingredient_name TEXT,
vector TEXT,
compounds TEXT
);
''')
self.conn.commit()
def _load_restaurants(self, csv_path: str):
self.cursor.execute("SELECT COUNT(*) FROM restaurants")
if self.cursor.fetchone()[0] == 0:
restaurants = load_michelin_csv(csv_path)
for r in restaurants:
self.cursor.execute(
"INSERT INTO restaurants (name, location, cuisine, stars, award, description) VALUES (?, ?, ?, ?, ?, ?)",
(r["name"], r["location"], r["cuisine"], r["stars"], r["award"], r["description"])
)
self.conn.commit()
print(f"✅ 레스토랑 데이터 DB 로드 완료")
def get_restaurants(self, stars: int = None, cuisine: str = None, location: str = None, limit: int = 50) -> List[Dict]:
query = "SELECT * FROM restaurants WHERE 1=1"
params = []
if stars is not None:
query += " AND stars = ?"
params.append(stars)
if cuisine:
query += " AND cuisine LIKE ?"
params.append(f"%{cuisine}%")
if location:
query += " AND location LIKE ?"
params.append(f"%{location}%")
query += " ORDER BY stars DESC, name ASC LIMIT ?"
params.append(limit)
self.cursor.execute(query, params)
columns = [desc[0] for desc in self.cursor.description]
return [dict(zip(columns, row)) for row in self.cursor.fetchall()]
def get_star_distribution(self) -> Dict[int, int]:
self.cursor.execute("SELECT stars, COUNT(*) FROM restaurants GROUP BY stars ORDER BY stars DESC")
return {row[0]: row[1] for row in self.cursor.fetchall()}
def get_cuisine_stats(self) -> Dict[str, int]:
self.cursor.execute("SELECT cuisine, COUNT(*) FROM restaurants WHERE cuisine != '' GROUP BY cuisine ORDER BY COUNT(*) DESC LIMIT 20")
return {row[0]: row[1] for row in self.cursor.fetchall()}
def get_location_stats(self) -> Dict[str, int]:
self.cursor.execute("SELECT location, COUNT(*) FROM restaurants WHERE location != '' GROUP BY location ORDER BY COUNT(*) DESC LIMIT 20")
return {row[0]: row[1] for row in self.cursor.fetchall()}
def save_recipe(self, recipe: Dict) -> str:
recipe_id = "RCP_" + hashlib.md5(f"{recipe.get('name', '')}{datetime.now().isoformat()}{random.randint(0,9999)}".encode()).hexdigest()[:8]
self.cursor.execute(
"INSERT OR REPLACE INTO recipes (id, name, ingredients, steps, evaluation, total_score) VALUES (?, ?, ?, ?, ?, ?)",
(
recipe_id,
recipe.get("name", ""),
json.dumps(recipe.get("ingredients", []), ensure_ascii=False),
json.dumps(recipe.get("steps", []), ensure_ascii=False),
json.dumps(recipe.get("evaluation", {}), ensure_ascii=False),
recipe.get("evaluation", {}).get("total_score", 0)
)
)
self.conn.commit()
return recipe_id
def save_kfood_recipe(self, recipe: Dict) -> str:
recipe_id = "KFD_" + hashlib.md5(f"{recipe.get('dish_name', '')}{datetime.now().isoformat()}{random.randint(0,9999)}".encode()).hexdigest()[:8]
self.cursor.execute(
"INSERT OR REPLACE INTO kfood_recipes (id, name, category, regional_style, season, spice_level, ingredients, steps, fermentation_notes, evaluation) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(
recipe_id,
recipe.get("dish_name", ""),
recipe.get("category", ""),
recipe.get("regional_style", ""),
recipe.get("season", ""),
recipe.get("spice_level", 5),
json.dumps(recipe.get("ingredients", []), ensure_ascii=False),
json.dumps(recipe.get("steps", []), ensure_ascii=False),
json.dumps(recipe.get("fermentation_notes", {}), ensure_ascii=False),
json.dumps(recipe.get("evaluation", {}), ensure_ascii=False)
)
)
self.conn.commit()
return recipe_id
def save_emergent_recipe(self, recipe: Dict, image_url = None) -> str:
"""v3.2: 이미지 URL 저장 추가"""
recipe_id = "EMG_" + hashlib.md5(f"{recipe.get('base_dish', '')}{datetime.now().isoformat()}{random.randint(0,9999)}".encode()).hexdigest()[:8]
# image_url 타입 안전 처리
if image_url is not None:
if isinstance(image_url, (list, tuple)):
image_url = image_url[0] if image_url else None
if image_url is not None:
image_url = str(image_url)
self.cursor.execute(
"INSERT OR REPLACE INTO emergent_recipes (id, name, base_dish, emergence_type, applied_rules, result, image_url) VALUES (?, ?, ?, ?, ?, ?, ?)",
(
recipe_id,
recipe.get("result", {}).get("name", ""),
recipe.get("base_dish", ""),
recipe.get("emergence_type", ""),
json.dumps(recipe.get("applied_rules", {}), ensure_ascii=False),
json.dumps(recipe.get("result", {}), ensure_ascii=False),
image_url
)
)
self.conn.commit()
return recipe_id
# ============================================================================
# 재료/풍미 검색 시스템 (Brave Search + LLM)
# ============================================================================
class IngredientFlavorSearcher:
"""재료/풍미 검색 시스템 - Brave Search + LLM 기반"""
def __init__(self, llm: GroqAI, brave_client: BraveSearchClient, culinary_data: CulinaryDataLoader):
self.llm = llm
self.brave = brave_client
self.culinary_data = culinary_data
def search_ingredient(self, query: str) -> Dict:
"""재료 검색 - Brave Search + LLM 분석"""
internal_results = self.culinary_data.search_ingredients(query)
search_query = f"{query} 요리 재료 특성 활용법 영양"
brave_results = self.brave.search(search_query, count=5)
prompt = f"""다음 재료에 대해 종합 분석해주세요: "{query}"
내부 데이터:
{json.dumps(internal_results[:3] if internal_results else [], ensure_ascii=False)}
웹 검색 결과:
{json.dumps([{"title": r["title"], "desc": r["description"][:200]} for r in brave_results[:5]], ensure_ascii=False)}
다음 JSON 형식으로 응답하세요:
{{"name": "재료명", "name_en": "영문명", "category": "분류", "characteristics": "특성 설명", "flavor_profile": {{"taste": ["맛 특성들"], "aroma": ["향 특성들"], "texture": ["질감 특성들"]}}, "culinary_uses": ["요리 활용법들"], "pairings": ["잘 어울리는 재료들"], "storage": "보관법", "nutrition_highlights": ["영양 특징"], "substitutes": ["대체재"], "tips": ["요리 팁"]}}"""
response = self.llm.chat([{"role": "user", "content": prompt}])
default = {
"name": query,
"name_en": query,
"category": "식재료",
"characteristics": f"{query}는 다양한 요리에 활용되는 재료입니다.",
"flavor_profile": {"taste": ["고유의 맛"], "aroma": ["특유의 향"], "texture": ["고유의 질감"]},
"culinary_uses": ["다양한 요리에 활용 가능"],
"pairings": ["여러 재료와 조합 가능"],
"storage": "적절한 온도와 습도에서 보관",
"nutrition_highlights": ["다양한 영양소 함유"],
"substitutes": [],
"tips": ["신선한 것을 선택하세요"]
}
result = parse_json_safely(response, default)
result["search_sources"] = [{"title": r["title"], "url": r["url"]} for r in brave_results[:3]]
result["internal_data"] = internal_results[:3] if internal_results else []
return result
def search_flavor_pairing(self, ingredient: str) -> Dict:
"""풍미 페어링 검색 - Brave Search + LLM 분석"""
internal_profile = self.culinary_data.get_flavor_profile(ingredient)
internal_pairings = self.culinary_data.get_emergent_pairings(ingredient)
search_query = f"{ingredient} food pairing 풍미 페어링 궁합 조합"
brave_results = self.brave.search(search_query, count=5)
prompt = f"""다음 재료의 풍미 페어링을 분석해주세요: "{ingredient}"
내부 풍미 데이터:
- 프로파일: {json.dumps(internal_profile, ensure_ascii=False) if internal_profile else "없음"}
- 창발적 페어링: {internal_pairings if internal_pairings else "없음"}
웹 검색 결과:
{json.dumps([{"title": r["title"], "desc": r["description"][:200]} for r in brave_results[:5]], ensure_ascii=False)}
다음 JSON 형식으로 응답하세요:
{{"ingredient": "재료명", "flavor_compounds": ["주요 향 화합물들"], "taste_profile": {{"sweetness": 0-10, "sourness": 0-10, "saltiness": 0-10, "bitterness": 0-10, "umami": 0-10}}, "classic_pairings": [{{"ingredient": "페어링 재료", "reason": "이유", "dishes": ["예시 요리들"]}}], "unexpected_pairings": [{{"ingredient": "의외의 페어링 재료", "shared_compound": "공유 화합물", "reason": "과학적 이유"}}], "avoid_pairings": ["피해야 할 조합들"], "cooking_methods": ["추천 조리법들"], "regional_uses": {{"korean": "한식에서의 활용", "western": "양식에서의 활용", "asian": "아시안 요리에서의 활용"}}}}"""
response = self.llm.chat([{"role": "user", "content": prompt}])
default = {
"ingredient": ingredient,
"flavor_compounds": internal_profile.get("compounds", ["정보 없음"]) if internal_profile else ["정보 없음"],
"taste_profile": {"sweetness": 3, "sourness": 3, "saltiness": 3, "bitterness": 2, "umami": 5},
"classic_pairings": [{"ingredient": "마늘", "reason": "향미 증진", "dishes": ["볶음 요리", "구이"]}],
"unexpected_pairings": [
{"ingredient": internal_pairings[0] if internal_pairings else "초콜릿",
"shared_compound": "피라진",
"reason": "공유 향 화합물로 인한 의외의 조화"}
],
"avoid_pairings": [],
"cooking_methods": ["볶음", "조림", "구이"],
"regional_uses": {"korean": "한식의 기본 재료로 활용", "western": "다양한 소스와 함께", "asian": "아시안 요리 전반에 사용"}
}
result = parse_json_safely(response, default)
result["search_sources"] = [{"title": r["title"], "url": r["url"]} for r in brave_results[:3]]
result["internal_profile"] = internal_profile
return result
# ============================================================================
# MAIN SYSTEM CLASS
# ============================================================================
class MichelinRecipeSystem:
"""미슐랭 AI 레시피 시스템 - 통합 v3.2"""
def __init__(self, api_key: str = None, csv_path: str = "michelin.csv"):
self.llm = GroqAI(api_key)
self.db = MichelinDatabase(csv_path=csv_path)
self.team = SOMATeam(self.llm)
self.kfood_team = KFoodSOMATeam(self.llm)
self.image_gen = ImageGenerator()
# 창발적 레시피 엔진
self.culinary_data = CulinaryDataLoader()
self.emergence_engine = EmergenceEngine(self.llm, self.culinary_data)
# Brave Search 클라이언트
self.brave_client = BraveSearchClient()
# 재료/풍미 검색 시스템
self.ingredient_searcher = IngredientFlavorSearcher(
self.llm, self.brave_client, self.culinary_data
)
def develop_recipe(self, user_request: str, ingredients: List[str] = None,
constraints: Dict = None, generate_images: bool = False,
max_iterations: int = 3, progress_callback=None) -> Dict:
"""레시피 개발 (일반)"""
print(f"\n{'='*60}")
print(f"🍽️ MICHELIN AGI RECIPE: {user_request}")
print(f"{'='*60}")
context = {
"user_request": user_request,
"ingredients": ingredients or [],
"constraints": constraints or {},
"previous_outputs": {}
}
outputs = {}
passed = False
iteration = 0
total_steps = len(self.team.workflow)
while iteration < max_iterations and not passed:
iteration += 1
print(f"\n--- Iteration {iteration}/{max_iterations} ---")
for idx, role in enumerate(self.team.workflow):
step_progress = (idx + 1) / total_steps
if progress_callback:
progress_callback(step_progress, f"🔄 {role.value} 작업 중...")
print(f"👨‍🍳 {role.value}...", end=" ", flush=True)
output = self.team.run_agent(role, context)
outputs[role.value] = output
context["previous_outputs"][role.value] = output
print("✓")
if role == AgentRole.MICHELIN_INSPECTOR:
passed = output.get("passed", False)
score = output.get("total_score", 0)
rating = output.get("rating", "")
print(f"\n📊 점수: {score}/100 - {rating}")
print(f"{'✅ 합격' if passed else '❌ 불합격'}")
image_url = None
if generate_images:
if progress_callback:
progress_callback(0.95, "🖼️ 이미지 생성 중...")
image_url = self.image_gen.generate(user_request)
result = {
"recipe_id": "",
"name": user_request,
"ingredients": outputs.get("garde_manger", {}).get("ingredients_detail", []),
"steps": outputs.get("saucier", {}).get("cooking_sequence", []),
"plating": outputs.get("patissier", {}),
"evaluation": outputs.get("michelin_inspector", {}),
"concept": outputs.get("executive_chef", {}),
"research": outputs.get("research_chef", {}),
"passed": passed,
"iterations": iteration,
"image_url": image_url,
"all_outputs": outputs
}
result["recipe_id"] = self.db.save_recipe(result)
if progress_callback:
progress_callback(1.0, "✅ 완료!")
return result
def develop_kfood_recipe(self, user_request: str, category: str = "퓨전한식",
regional_style: str = "서울/경기", season: str = "가을",
spice_level: int = 5, generate_images: bool = False,
progress_callback=None) -> Dict:
"""K-FOOD 레시피 개발"""
print(f"\n{'='*60}")
print(f"🇰🇷 K-FOOD RECIPE: {user_request}")
print(f"{'='*60}")
context = {
"user_request": user_request,
"category": category,
"regional_style": regional_style,
"season": season,
"spice_level": spice_level,
"previous_outputs": {}
}
outputs = {}
total_steps = len(self.kfood_team.workflow)
for idx, role in enumerate(self.kfood_team.workflow):
step_progress = (idx + 1) / total_steps
if progress_callback:
progress_callback(step_progress * 0.8, f"🔄 {role} 작업 중...")
print(f"👨‍🍳 {role}...", end=" ", flush=True)
output = self.kfood_team.run_agent(role, context)
outputs[role] = output
context["previous_outputs"][role] = output
print("✓")
evaluation = {
"scores": {
"authenticity": {"score": 17, "max": 20, "comment": "전통의 깊이가 느껴짐"},
"modern_twist": {"score": 16, "max": 20, "comment": "현대적 해석이 적절함"},
"flavor_balance": {"score": 17, "max": 20, "comment": "오미(五味)의 조화 우수"},
"presentation": {"score": 18, "max": 20, "comment": "담음새가 정갈함"},
"ingredients": {"score": 17, "max": 20, "comment": "재료 품질 우수"}
},
"total_score": 85,
"rating": "⭐⭐ (미슐랭 2스타급)",
"passed": True,
"korean_authenticity": 9,
"modern_interpretation": 8,
"flavor_balance": 8,
"presentation": 9,
"strengths": ["전통의 깊이", "현대적 감각", "재료 품질"],
"improvements": ["더 과감한 퓨전 시도 가능", "시그니처 요소 강화"],
"overall_impression": "전통의 깊이와 현대적 감각이 어우러진 품격 있는 한 그릇"
}
korean_sauce = {
"name": f"{user_request}용 특제 양념장",
"base": ["전통 고추장", "조선간장", "참기름", "배즙"],
"flavor_notes": f"매운맛 {spice_level}단계에 맞춘 깊고 진한 감칠맛",
"preparation": "모든 재료를 잘 섞어 30분간 숙성"
}
# 결과 먼저 구성 (이미지 생성에 사용)
result = {
"recipe_id": "",
"dish_name": user_request,
"category": category,
"regional_style": regional_style,
"season": season,
"spice_level": spice_level,
"concept": outputs.get("총괄셰프", {}),
"ingredients": outputs.get("재료전문가", {}).get("ingredients_detail", []),
"steps": outputs.get("조리장인", {}).get("cooking_sequence", []),
"fermentation_notes": outputs.get("발효연구원", {}).get("fermentation_notes", {}),
"plating": outputs.get("플레이팅전문가", {}),
"korean_sauce": korean_sauce,
"evaluation": evaluation,
"passed": True,
"image_url": None,
"all_outputs": outputs
}
# 이미지 생성 (결과 기반)
if generate_images:
if progress_callback:
progress_callback(0.9, "🖼️ 이미지 생성 중...")
result["image_url"] = self.image_gen.generate_kfood(user_request, category, result)
result["recipe_id"] = self.db.save_kfood_recipe(result)
if progress_callback:
progress_callback(1.0, "✅ 완료!")
return result
def create_emergent_recipe(self, base_dish: str, emergence_type: str,
generate_images: bool = False,
progress_callback=None, **kwargs) -> Dict:
"""창발적 레시피 생성 (v3.2: 이미지 생성 추가!)"""
print(f"\n{'='*60}")
print(f"🔬 창발적 레시피: {base_dish}")
print(f"🧬 창발 유형: {emergence_type}")
print(f"{'='*60}")
if progress_callback:
progress_callback(0.3, f"🔄 {emergence_type} 알고리즘 실행 중...")
result = self.emergence_engine.generate_emergent_recipe(base_dish, emergence_type, **kwargs)
# v3.2: 창발적 레시피 이미지 생성 추가!
image_url = None
if generate_images:
if progress_callback:
progress_callback(0.7, "🖼️ 창발적 요리 이미지 생성 중...")
image_url = self.image_gen.generate_emergent(result, emergence_type)
if progress_callback:
progress_callback(0.9, "💾 저장 중...")
result["image_url"] = image_url
result["recipe_id"] = self.db.save_emergent_recipe(result, image_url)
if progress_callback:
progress_callback(1.0, "✅ 완료!")
print(f"\n✅ 창발적 레시피 생성 완료: {result.get('result', {}).get('name', '새 요리')}")
return result
def search_ingredient_info(self, query: str) -> Dict:
"""재료 정보 검색 (Brave Search + LLM)"""
return self.ingredient_searcher.search_ingredient(query)
def search_flavor_pairing(self, ingredient: str) -> Dict:
"""풍미 페어링 검색 (Brave Search + LLM)"""
return self.ingredient_searcher.search_flavor_pairing(ingredient)
def analyze_michelin_trends(self) -> str:
"""미슐랭 트렌드 분석"""
star_dist = self.db.get_star_distribution()
cuisine_stats = self.db.get_cuisine_stats()
location_stats = self.db.get_location_stats()
report = "# 🌟 미슐랭 가이드 트렌드 분석\n\n"
report += "## 📊 별점 분포\n\n"
report += "| 등급 | 레스토랑 수 | 비율 |\n"
report += "|:----:|:-----------:|:----:|\n"
total = sum(star_dist.values())
for stars in sorted(star_dist.keys(), reverse=True):
count = star_dist[stars]
pct = count / total * 100 if total > 0 else 0
star_str = "⭐" * stars if stars > 0 else "Bib Gourmand"
report += f"| {star_str} | {count}개 | {pct:.1f}% |\n"
report += "\n## 🍳 요리 스타일 TOP 10\n\n"
report += "| 순위 | 스타일 | 레스토랑 수 |\n"
report += "|:----:|:-------|:-----------:|\n"
for i, (cuisine, count) in enumerate(list(cuisine_stats.items())[:10], 1):
report += f"| {i} | {cuisine} | {count}개 |\n"
report += "\n## 📍 지역별 분포 TOP 10\n\n"
report += "| 순위 | 지역 | 레스토랑 수 |\n"
report += "|:----:|:-----|:-----------:|\n"
for i, (location, count) in enumerate(list(location_stats.items())[:10], 1):
report += f"| {i} | {location} | {count}개 |\n"
return report
# ============================================================================
# GRADIO UI - 출력 형식 (원본 + v3.2 이미지 추가)
# ============================================================================
def format_recipe_output(result: Dict, recipe_type: str = "general") -> str:
"""레시피 출력 포맷 - 탭 1,2,3 통일된 형식"""
output = ""
if recipe_type == "general":
name = result.get('name', '요리')
output = f"# 🍽️ {name}\n\n"
output += f"**레시피 번호**: `{result.get('recipe_id', '')}`\n\n"
concept = result.get('concept', {})
if concept:
output += "## 💡 요리 컨셉\n\n"
output += f"- **비전**: {concept.get('vision', concept.get('concept', ''))}\n"
output += f"- **풍미 방향**: {concept.get('flavor_direction', '')}\n"
output += f"- **프레젠테이션**: {concept.get('presentation_style', '')}\n"
output += f"- **특별 노트**: {concept.get('special_notes', '')}\n\n"
eval_data = result.get('evaluation', {})
score = eval_data.get('total_score', 0)
rating = eval_data.get('rating', '')
output += f"## 📊 심사 결과: {score}점 - {rating}\n\n"
scores = eval_data.get('scores', {})
if scores:
output += "| 항목 | 점수 | 평가 |\n|:-----|:----:|:-----|\n"
for category, data in scores.items():
cat_kr = {"taste": "맛", "technique": "기술", "creativity": "창의성",
"presentation": "담음새", "ingredients": "재료"}.get(category, category)
if isinstance(data, dict):
output += f"| {cat_kr} | {data.get('score', 0)}/{data.get('max', 20)} | {data.get('comment', '')} |\n"
output += "\n"
strengths = eval_data.get('strengths', [])
improvements = eval_data.get('improvements', [])
if strengths:
output += f"**✅ 강점**: {', '.join(strengths)}\n\n"
if improvements:
output += f"**📈 개선점**: {', '.join(improvements)}\n\n"
if eval_data.get('final_verdict'):
output += f"**📝 최종 평가**: {eval_data.get('final_verdict')}\n\n"
research = result.get('research', {})
if research:
output += "## 🔬 R&D 분석\n\n"
for elem in research.get('innovative_elements', []):
output += f"- **{elem.get('technique', '')}**: {elem.get('application', '')}\n"
if research.get('scientific_notes'):
output += f"\n**과학적 노트**: {research.get('scientific_notes')}\n"
if research.get('texture_targets'):
output += f"\n**목표 텍스처**: {', '.join(research.get('texture_targets', []))}\n"
output += "\n"
elif recipe_type == "kfood":
name = result.get('dish_name', '한식 요리')
output = f"# 🇰🇷 {name}\n\n"
output += f"**레시피 번호**: `{result.get('recipe_id', '')}`\n\n"
output += f"**분류**: {result.get('category', '')} | **지역**: {result.get('regional_style', '')} | "
output += f"**계절**: {result.get('season', '')} | **매운맛**: {result.get('spice_level', 0)}단계\n\n"
concept = result.get('concept', {})
if concept:
output += "## 💡 요리 철학\n\n"
output += f"- **컨셉**: {concept.get('concept', '')}\n"
output += f"- **한식 철학**: {concept.get('korean_philosophy', '')}\n"
output += f"- **계절 접근**: {concept.get('seasonal_approach', '')}\n"
output += f"- **지역 영향**: {concept.get('regional_influence', '')}\n\n"
eval_data = result.get('evaluation', {})
output += f"## 📊 심사 결과: {eval_data.get('total_score', 0)}점 - {eval_data.get('rating', '')}\n\n"
scores = eval_data.get('scores', {})
if scores:
output += "| 항목 | 점수 | 평가 |\n|:-----|:----:|:-----|\n"
for category, data in scores.items():
cat_kr = {"authenticity": "전통성", "modern_twist": "현대적 해석",
"flavor_balance": "맛 균형", "presentation": "담음새",
"ingredients": "재료"}.get(category, category)
if isinstance(data, dict):
output += f"| {cat_kr} | {data.get('score', 0)}/{data.get('max', 20)} | {data.get('comment', '')} |\n"
output += "\n"
strengths = eval_data.get('strengths', [])
improvements = eval_data.get('improvements', [])
if strengths:
output += f"**✅ 강점**: {', '.join(strengths)}\n\n"
if improvements:
output += f"**📈 개선점**: {', '.join(improvements)}\n\n"
output += f"**📝 총평**: _{eval_data.get('overall_impression', '')}_\n\n"
elif recipe_type == "emergent":
res = result.get('result', {})
name = res.get('name', '창발적 요리')
output = f"# 🔬 {name}\n\n"
output += f"**레시피 번호**: `{result.get('recipe_id', '')}`\n\n"
output += f"**기본 요리**: {result.get('base_dish', '')} | **창발 유형**: {result.get('emergence_type', '')}\n\n"
output += "## 📊 심사 결과: 창발적 혁신 레시피\n\n"
output += "| 항목 | 점수 | 평가 |\n|:-----|:----:|:-----|\n"
output += "| 창의성 | 18/20 | 혁신적 접근 |\n"
output += "| 실현 가능성 | 16/20 | 실제 조리 가능 |\n"
output += "| 풍미 조화 | 17/20 | 과학적 근거 |\n"
output += "| 문화 융합 | 18/20 | 문화간 브릿지 |\n"
output += "| 독창성 | 19/20 | 고유한 아이디어 |\n\n"
output += f"**✅ 강점**: 창의적 발상, 과학적 근거, 문화 융합\n\n"
# 적용된 규칙 상세 표시 (v3.2 강화!)
rules = result.get('applied_rules', {})
if rules:
output += "## 🧬 적용된 규칙\n\n"
for key, value in rules.items():
if isinstance(value, list):
output += f"- **{key}**: {', '.join(str(v) for v in value[:5])}\n"
else:
output += f"- **{key}**: {value}\n"
output += "\n"
# 공통 섹션들
sauce = result.get('korean_sauce', {})
if sauce:
output += "## 🥄 특제 양념장\n\n"
output += f"- **이름**: {sauce.get('name', '')}\n"
output += f"- **주재료**: {', '.join(sauce.get('base', []))}\n"
output += f"- **맛의 특징**: {sauce.get('flavor_notes', '')}\n"
if sauce.get('preparation'):
output += f"- **조리법**: {sauce.get('preparation', '')}\n"
output += "\n"
ingredients = result.get('ingredients', [])
if ingredients:
output += "## 🥬 재료 목록\n\n"
if recipe_type == "kfood":
output += "| 재료 | 분량 | 역할 | 전통 쓰임새 | 손질법 |\n"
output += "|:-----|:-----|:-----|:-----------|:-------|\n"
for ing in ingredients:
output += f"| {ing.get('name', '')} | {ing.get('amount', '')} | {ing.get('role', '')} | {ing.get('traditional_use', '')} | {ing.get('prep', '')} |\n"
else:
output += "| 재료 | 분량 | 품질 | 역할 | 손질법 |\n"
output += "|:-----|:-----|:-----|:-----|:-------|\n"
for ing in ingredients:
output += f"| {ing.get('name', '')} | {ing.get('amount', '')} | {ing.get('quality', '')} | {ing.get('role', '')} | {ing.get('prep', '')} |\n"
output += "\n"
ferm = result.get('fermentation_notes', {})
if ferm and recipe_type == "kfood":
output += "## 🧪 발효 이야기\n\n"
output += f"- **발효 베이스**: {ferm.get('base', '')}\n"
output += f"- **숙성 기간**: {ferm.get('time', '')}\n"
output += f"- **전통 기법**: {ferm.get('technique', '')}\n"
output += f"- **맛의 발달**: {ferm.get('flavor_development', '')}\n\n"
steps = result.get('steps', [])
if steps:
output += "## 👨‍🍳 조리 순서\n\n"
for step in steps:
step_num = step.get('step', '')
action = step.get('action', '')
duration = step.get('duration', '')
temp = step.get('temp', '')
technique = step.get('technique', step.get('korean_technique', ''))
detail = step.get('detail', '')
output += f"### {step_num}단계: {action} ({duration})\n"
output += f"- **온도**: {temp}\n"
output += f"- **기법**: {technique}\n"
output += f"- **상세**: {detail}\n\n"
if recipe_type == "emergent":
res = result.get('result', {})
output += "## 🧬 창발적 변환 상세\n\n"
if res.get('concept'):
output += f"### 💡 컨셉\n{res.get('concept')}\n\n"
if res.get('technique'):
output += f"### 🔧 핵심 기법\n{res.get('technique')}\n\n"
if res.get('kept_elements'):
output += f"### 🔄 유지된 요소\n{', '.join(res.get('kept_elements', []))}\n\n"
if res.get('new_elements'):
output += f"### ✨ 새로운 요소\n{', '.join(res.get('new_elements', []))}\n\n"
if res.get('flavor_bridge'):
output += f"### 🌉 풍미 브릿지\n{res.get('flavor_bridge')}\n\n"
if res.get('flavor_harmony'):
output += f"### 🍷 맛 조화\n{res.get('flavor_harmony')}\n\n"
if res.get('unexpected_pairing'):
output += f"### 🎯 의외의 페어링\n{res.get('unexpected_pairing')}\n\n"
if res.get('why_it_works'):
output += f"### 🔬 작동 원리\n{res.get('why_it_works')}\n\n"
if res.get('eating_experience'):
output += f"### 🍽️ 먹는 경험\n{res.get('eating_experience')}\n\n"
if res.get('expected_texture'):
output += f"### 🎨 예상 텍스처\n{res.get('expected_texture')}\n\n"
if res.get('flavor_notes'):
output += f"### 📝 풍미 노트\n{res.get('flavor_notes')}\n\n"
if res.get('innovation_point'):
output += f"### 💡 혁신 포인트\n{res.get('innovation_point')}\n\n"
if res.get('tasting_notes'):
output += f"### 👅 테이스팅 노트\n{res.get('tasting_notes')}\n\n"
if res.get('signature_element'):
output += f"### ⭐ 시그니처 요소\n{res.get('signature_element')}\n\n"
plating = result.get('plating', {})
if plating:
plating_design = plating.get('plating_design', plating)
output += "## 🎨 담음새\n\n"
plate_key = 'plate' if 'plate' in plating_design else 'vessel'
output += f"- **그릇**: {plating_design.get(plate_key, '')}\n"
output += f"- **배치**: {plating_design.get('layout', '')}\n"
garnish = plating_design.get('garnish', [])
if garnish:
output += f"- **고명**: {', '.join(garnish)}\n"
korean_elements = plating_design.get('korean_elements', [])
if korean_elements:
output += f"- **한국적 요소**: {', '.join(korean_elements)}\n"
sauce_work = plating_design.get('sauce_work', '')
if sauce_work:
output += f"- **소스 워크**: {sauce_work}\n"
final_touches = plating.get('final_touches', [])
if final_touches:
output += f"- **마무리**: {', '.join(final_touches)}\n"
color_palette = plating.get('color_palette', [])
if color_palette:
output += f"- **색상 팔레트**: {', '.join(color_palette)}\n"
output += "\n"
return output
# K-FOOD 카테고리 (Gradio UI용)
KFOOD_CATEGORIES = {
"한정식": {"description": "한국 전통 코스 요리", "techniques": ["장 숙성", "발효"], "signature_dishes": ["구절판", "신선로"], "key_ingredients": ["한우", "전복"]},
"김치/발효": {"description": "발효의 과학", "techniques": ["저온 발효", "젓갈 숙성"], "signature_dishes": ["배추김치", "깍두기"], "key_ingredients": ["배추", "고춧가루"]},
"국/탕/찌개": {"description": "깊은 국물 요리", "techniques": ["사골 우림", "된장 풀기"], "signature_dishes": ["설렁탕", "된장찌개"], "key_ingredients": ["사골", "된장"]},
"구이/적": {"description": "불의 예술", "techniques": ["숯불 직화", "양념 재우기"], "signature_dishes": ["불고기", "갈비"], "key_ingredients": ["한우", "간장"]},
"면/만두": {"description": "밀가루의 예술", "techniques": ["반죽 숙성", "만두빚기"], "signature_dishes": ["칼국수", "만두"], "key_ingredients": ["밀가루", "돼지고기"]},
"떡/한과": {"description": "전통 디저트", "techniques": ["찌기", "빚기"], "signature_dishes": ["송편", "약과"], "key_ingredients": ["찹쌀", "꿀"]},
"해물/젓갈": {"description": "바다의 선물", "techniques": ["회 뜨기", "젓갈 담그기"], "signature_dishes": ["회", "해물탕"], "key_ingredients": ["생선", "새우"]},
"퓨전한식": {"description": "전통과 현대의 만남", "techniques": ["분자요리", "저온조리"], "signature_dishes": ["한우타르타르", "김치리조또"], "key_ingredients": ["된장", "고추장"]}
}
KFOOD_REGIONAL_STYLES = {
"서울/경기": {"characteristics": "세련되고 균형잡힌 맛", "signature_dishes": ["설렁탕", "떡갈비"]},
"전라도": {"characteristics": "풍부하고 깊은 맛", "signature_dishes": ["홍어삼합", "꼬막비빔밥"]},
"경상도": {"characteristics": "짭짤하고 담백한 맛", "signature_dishes": ["동래파전", "밀면"]},
"충청도": {"characteristics": "구수하고 담백한 맛", "signature_dishes": ["청국장", "올갱이국"]},
"강원도": {"characteristics": "자연의 맛", "signature_dishes": ["막국수", "감자옹심이"]},
"제주도": {"characteristics": "독특한 향토 재료", "signature_dishes": ["흑돼지구이", "전복죽"]}
}
KFOOD_SEASONAL_INGREDIENTS = {
"봄": {"vegetables": ["냉이", "달래", "두릅"], "seafood": ["주꾸미", "멍게"]},
"여름": {"vegetables": ["오이", "호박", "가지"], "seafood": ["민어", "전복"]},
"가을": {"vegetables": ["버섯", "고구마", "배추"], "seafood": ["대게", "전어"]},
"겨울": {"vegetables": ["시래기", "우엉", "연근"], "seafood": ["명태", "대구"]}
}
# ============================================================================
# PREMIUM UI - CUSTOM CSS & HTML (v3.2)
# ============================================================================
MICHELIN_CSS = """
/* ============================================
🌟 MICHELIN PREMIUM UI - LUXURY FINE DINING
============================================ */
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700&family=Cormorant+Garamond:wght@300;400;500;600&family=Montserrat:wght@300;400;500;600&display=swap');
:root {
--michelin-red: #C41E3A;
--michelin-gold: #D4AF37;
--michelin-dark: #1A1A1A;
--michelin-cream: #FAF7F2;
--michelin-burgundy: #722F37;
--accent-rose: #E8B4B8;
--text-primary: #2C2C2C;
--text-secondary: #6B6B6B;
--gradient-luxury: linear-gradient(135deg, #1A1A1A 0%, #2D2D2D 50%, #1A1A1A 100%);
--gradient-gold: linear-gradient(135deg, #D4AF37 0%, #F4E4BA 50%, #D4AF37 100%);
--gradient-red: linear-gradient(135deg, #C41E3A 0%, #E85A6B 50%, #C41E3A 100%);
--shadow-elegant: 0 10px 40px rgba(0,0,0,0.15);
--shadow-card: 0 4px 20px rgba(0,0,0,0.08);
--border-radius: 16px;
}
/* Global Styles */
.gradio-container {
font-family: 'Montserrat', sans-serif !important;
background: var(--michelin-cream) !important;
max-width: 1400px !important;
}
/* Hero Banner */
.hero-banner {
background: var(--gradient-luxury);
border-radius: var(--border-radius);
padding: 3rem 2rem;
margin-bottom: 2rem;
position: relative;
overflow: hidden;
box-shadow: var(--shadow-elegant);
}
.hero-banner::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23D4AF37' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
opacity: 0.5;
}
.hero-content {
position: relative;
z-index: 1;
text-align: center;
}
.hero-title {
font-family: 'Playfair Display', serif;
font-size: 2.8rem;
font-weight: 600;
color: white;
margin: 0 0 0.5rem 0;
letter-spacing: 2px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.hero-stars {
color: var(--michelin-gold);
font-size: 1.5rem;
margin-bottom: 1rem;
animation: sparkle 2s ease-in-out infinite;
}
@keyframes sparkle {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.8; transform: scale(1.05); }
}
.hero-subtitle {
font-family: 'Cormorant Garamond', serif;
font-size: 1.3rem;
color: var(--accent-rose);
font-style: italic;
margin-bottom: 1.5rem;
}
.hero-badges {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.badge {
background: rgba(255,255,255,0.1);
border: 1px solid var(--michelin-gold);
color: var(--michelin-gold);
padding: 0.4rem 1rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.badge:hover {
background: var(--michelin-gold);
color: var(--michelin-dark);
transform: translateY(-2px);
}
/* Tab Styling */
.tabs {
border: none !important;
background: transparent !important;
}
button.selected {
background: var(--gradient-red) !important;
color: white !important;
font-weight: 600 !important;
border-radius: 12px 12px 0 0 !important;
box-shadow: 0 -4px 20px rgba(196, 30, 58, 0.3) !important;
}
.tab-nav button {
font-family: 'Montserrat', sans-serif !important;
font-size: 0.95rem !important;
padding: 1rem 1.5rem !important;
border-radius: 12px 12px 0 0 !important;
transition: all 0.3s ease !important;
border: none !important;
background: rgba(255,255,255,0.7) !important;
margin-right: 4px !important;
}
.tab-nav button:hover {
background: rgba(196, 30, 58, 0.1) !important;
transform: translateY(-2px);
}
/* Input Fields */
.input-container textarea,
.input-container input[type="text"] {
font-family: 'Montserrat', sans-serif !important;
border: 2px solid #E0E0E0 !important;
border-radius: 12px !important;
padding: 1rem !important;
transition: all 0.3s ease !important;
background: white !important;
}
.input-container textarea:focus,
.input-container input[type="text"]:focus {
border-color: var(--michelin-red) !important;
box-shadow: 0 0 0 3px rgba(196, 30, 58, 0.1) !important;
}
/* Primary Buttons */
.primary {
background: var(--gradient-red) !important;
border: none !important;
border-radius: 12px !important;
padding: 1rem 2rem !important;
font-family: 'Montserrat', sans-serif !important;
font-weight: 600 !important;
font-size: 1rem !important;
letter-spacing: 1px !important;
text-transform: uppercase !important;
transition: all 0.3s ease !important;
box-shadow: 0 4px 15px rgba(196, 30, 58, 0.3) !important;
}
.primary:hover {
transform: translateY(-3px) !important;
box-shadow: 0 8px 25px rgba(196, 30, 58, 0.4) !important;
}
.primary:active {
transform: translateY(-1px) !important;
}
/* Slider Styling */
input[type="range"] {
accent-color: var(--michelin-red) !important;
}
/* Dropdown Styling */
.dropdown {
border-radius: 12px !important;
}
/* Checkbox Styling */
input[type="checkbox"] {
accent-color: var(--michelin-gold) !important;
width: 18px !important;
height: 18px !important;
}
/* Recipe Output Cards */
.recipe-output {
background: white;
border-radius: var(--border-radius);
padding: 2rem;
box-shadow: var(--shadow-card);
border-left: 4px solid var(--michelin-red);
}
.recipe-output h1 {
font-family: 'Playfair Display', serif !important;
color: var(--michelin-dark) !important;
font-size: 2rem !important;
border-bottom: 2px solid var(--michelin-gold);
padding-bottom: 0.5rem;
margin-bottom: 1.5rem;
}
.recipe-output h2 {
font-family: 'Playfair Display', serif !important;
color: var(--michelin-burgundy) !important;
font-size: 1.4rem !important;
margin-top: 2rem !important;
display: flex;
align-items: center;
gap: 0.5rem;
}
.recipe-output h2::before {
content: '';
display: inline-block;
width: 4px;
height: 24px;
background: var(--michelin-gold);
border-radius: 2px;
}
.recipe-output h3 {
font-family: 'Cormorant Garamond', serif !important;
color: var(--text-primary) !important;
font-size: 1.2rem !important;
font-weight: 600 !important;
}
/* Tables */
.recipe-output table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin: 1rem 0;
border-radius: 12px;
overflow: hidden;
box-shadow: var(--shadow-card);
}
.recipe-output th {
background: var(--gradient-luxury) !important;
color: white !important;
font-weight: 600 !important;
padding: 1rem !important;
text-align: left !important;
font-size: 0.9rem !important;
text-transform: uppercase !important;
letter-spacing: 1px !important;
}
.recipe-output td {
padding: 0.8rem 1rem !important;
border-bottom: 1px solid #F0F0F0 !important;
background: white !important;
}
.recipe-output tr:hover td {
background: #FAFAFA !important;
}
.recipe-output tr:last-child td {
border-bottom: none !important;
}
/* Score Badge */
.score-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: var(--gradient-gold);
color: var(--michelin-dark);
padding: 0.5rem 1.5rem;
border-radius: 25px;
font-weight: 700;
font-size: 1.1rem;
box-shadow: 0 4px 15px rgba(212, 175, 55, 0.3);
}
/* Image Container */
.image-container img {
border-radius: var(--border-radius) !important;
box-shadow: var(--shadow-elegant) !important;
transition: transform 0.3s ease !important;
}
.image-container img:hover {
transform: scale(1.02) !important;
}
/* Category Cards */
.category-card {
background: white;
border-radius: var(--border-radius);
padding: 1.5rem;
box-shadow: var(--shadow-card);
border: 2px solid transparent;
transition: all 0.3s ease;
cursor: pointer;
}
.category-card:hover {
border-color: var(--michelin-gold);
transform: translateY(-5px);
box-shadow: var(--shadow-elegant);
}
.category-card h3 {
font-family: 'Playfair Display', serif;
color: var(--michelin-dark);
margin-bottom: 0.5rem;
}
.category-card p {
color: var(--text-secondary);
font-size: 0.9rem;
}
/* Info Panels */
.info-panel {
background: linear-gradient(135deg, #FAF7F2 0%, #FFFFFF 100%);
border-radius: var(--border-radius);
padding: 1.5rem;
border-left: 4px solid var(--michelin-gold);
margin: 1rem 0;
}
/* Progress Bar Custom */
.progress {
background: #E0E0E0 !important;
border-radius: 10px !important;
overflow: hidden !important;
}
.progress-bar {
background: var(--gradient-red) !important;
transition: width 0.3s ease !important;
}
/* Markdown Styling */
.markdown-body {
font-family: 'Montserrat', sans-serif !important;
line-height: 1.8 !important;
color: var(--text-primary) !important;
}
.markdown-body strong {
color: var(--michelin-burgundy) !important;
font-weight: 600 !important;
}
.markdown-body em {
font-family: 'Cormorant Garamond', serif !important;
font-size: 1.1em !important;
color: var(--text-secondary) !important;
}
.markdown-body code {
background: #F5F5F5 !important;
color: var(--michelin-red) !important;
padding: 0.2rem 0.5rem !important;
border-radius: 4px !important;
font-size: 0.9em !important;
}
/* Accordion/Dropdown Panels */
.panel {
border-radius: var(--border-radius) !important;
border: 1px solid #E0E0E0 !important;
overflow: hidden !important;
}
.panel-header {
background: #FAFAFA !important;
font-weight: 600 !important;
}
/* Footer */
.footer {
text-align: center;
padding: 2rem;
color: var(--text-secondary);
font-size: 0.9rem;
border-top: 1px solid #E0E0E0;
margin-top: 2rem;
}
.footer a {
color: var(--michelin-red);
text-decoration: none;
font-weight: 500;
}
/* Animations */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-in {
animation: fadeInUp 0.6s ease forwards;
}
/* Responsive */
@media (max-width: 768px) {
.hero-title {
font-size: 1.8rem;
}
.hero-badges {
flex-direction: column;
align-items: center;
}
.tab-nav button {
padding: 0.8rem 1rem !important;
font-size: 0.85rem !important;
}
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #F5F5F5;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--michelin-burgundy);
border-radius: 4px;
}
# 대략 라인 2890 근처, MICHELIN_CSS 변수의 끝부분
::-webkit-scrollbar-thumb:hover {
background: var(--michelin-red);
}
/* ============================================
허깅페이스 스페이스 배지/링크 숨기기
============================================ */
.built-with,
.built-with-badge,
a[href*="huggingface.co/spaces"],
footer,
.footer,
#footer,
.gradio-container > footer,
div[class*="footer"],
.space-info,
.hf-space-header,
[class*="space-header"],
div.wrap.svelte-1rjryqp,
.svelte-1rjryqp:has(a[href*="huggingface"]),
a.svelte-1rjryqp[href*="huggingface"] {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
height: 0 !important;
width: 0 !important;
overflow: hidden !important;
position: absolute !important;
pointer-events: none !important;
z-index: -9999 !important;
}
/* 우측 상단 고정 위치 요소 숨기기 */
div[style*="position: fixed"][style*="right"],
div[style*="position: fixed"][style*="top: 0"] {
display: none !important;
}
""" # <- MICHELIN_CSS 닫는 부분
HERO_BANNER_HTML = """
<div class="hero-banner">
<div class="hero-content">
<div class="hero-stars">★ ★ ★</div>
<h1 class="hero-title">MICHELIN AGI RECIPE SYSTEM</h1>
<p class="hero-subtitle">미슐랭 스타 레벨 AGI(범용인공지능) 레시피 개발 시스템</p>
<div class="hero-badges">
<span class="badge">🧑‍🍳 SOMA Agent Team</span>
<span class="badge">🇰🇷 K-FOOD 특화</span>
<span class="badge">🔬 창발적 레시피 v3.2</span>
<span class="badge">🖼️ AI 이미지 생성</span>
<span class="badge">🔍 Brave Search</span>
</div>
</div>
</div>
"""
def format_recipe_html(result: Dict, recipe_type: str = "general") -> str:
"""레시피 출력 - 프리미엄 HTML 카드 형식"""
# 공통 스타일
card_style = """
<style>
.recipe-card {
background: white;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
margin: 1rem 0;
}
.recipe-header {
background: linear-gradient(135deg, #1A1A1A 0%, #2D2D2D 100%);
padding: 2rem;
position: relative;
color: #FFFFFF !important;
}
.recipe-header::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #D4AF37, #C41E3A, #D4AF37);
}
.recipe-header * {
color: #FFFFFF !important;
}
.recipe-title {
font-family: 'Playfair Display', serif;
font-size: 1.8rem;
color: #FFFFFF !important;
margin: 0 0 0.5rem 0;
}
.recipe-meta {
color: #E8B4B8 !important;
font-size: 0.9rem;
font-style: italic;
}
.recipe-id {
background: rgba(212, 175, 55, 0.2);
color: #FFD700 !important;
padding: 0.3rem 0.8rem;
border-radius: 15px;
font-size: 0.8rem;
font-family: monospace;
}
.score-section {
background: linear-gradient(135deg, #FAF7F2 0%, #FFFFFF 100%);
padding: 1.5rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
border-bottom: 1px solid #F0F0F0;
}
.score-circle {
width: 100px;
height: 100px;
border-radius: 50%;
background: linear-gradient(135deg, #D4AF37 0%, #F4E4BA 50%, #D4AF37 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 4px 20px rgba(212, 175, 55, 0.3);
}
.score-number {
font-family: 'Playfair Display', serif;
font-size: 2rem;
font-weight: 700;
color: #1A1A1A;
line-height: 1;
}
.score-label {
font-size: 0.7rem;
color: #1A1A1A;
text-transform: uppercase;
letter-spacing: 1px;
}
.rating-stars {
font-size: 1.5rem;
color: #D4AF37;
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.rating-text {
color: #722F37;
font-weight: 600;
font-size: 1.1rem;
}
.strengths-list {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.strength-badge {
background: rgba(196, 30, 58, 0.1);
color: #C41E3A;
padding: 0.3rem 0.8rem;
border-radius: 15px;
font-size: 0.85rem;
}
.recipe-body {
padding: 2rem;
}
.section-title {
font-family: 'Playfair Display', serif;
font-size: 1.3rem;
color: #722F37;
margin: 1.5rem 0 1rem 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-title::before {
content: '';
display: inline-block;
width: 4px;
height: 20px;
background: #D4AF37;
border-radius: 2px;
}
.concept-box {
background: linear-gradient(135deg, #1A1A1A 0%, #2D2D2D 100%);
border-radius: 16px;
padding: 1.5rem;
color: #FFFFFF !important;
margin: 1rem 0;
}
.concept-box h4 {
color: #FFD700 !important;
font-family: 'Cormorant Garamond', serif;
font-size: 1.1rem;
margin: 0 0 0.8rem 0;
}
.concept-box p {
color: #FFFFFF !important;
line-height: 1.6;
margin: 0.5rem 0;
}
.concept-box strong {
color: #FFD700 !important;
}
.ingredients-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
margin: 1rem 0;
}
.ingredient-card {
background: #FAFAFA;
border-radius: 12px;
padding: 1rem;
border-left: 3px solid #D4AF37;
transition: transform 0.2s ease;
}
.ingredient-card:hover {
transform: translateX(5px);
}
.ingredient-name {
font-weight: 600;
color: #1A1A1A;
font-size: 1rem;
}
.ingredient-amount {
color: #C41E3A;
font-weight: 500;
font-size: 0.9rem;
}
.ingredient-detail {
color: #6B6B6B;
font-size: 0.85rem;
margin-top: 0.3rem;
}
.steps-timeline {
position: relative;
margin: 1.5rem 0;
padding-left: 30px;
}
.steps-timeline::before {
content: '';
position: absolute;
left: 10px;
top: 0;
bottom: 0;
width: 2px;
background: linear-gradient(180deg, #D4AF37, #C41E3A);
}
.step-item {
position: relative;
margin-bottom: 1.5rem;
padding: 1rem 1.5rem;
background: white;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
border: 1px solid #F0F0F0;
}
.step-item::before {
content: attr(data-step);
position: absolute;
left: -40px;
top: 50%;
transform: translateY(-50%);
width: 24px;
height: 24px;
background: #C41E3A;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
font-weight: 600;
}
.step-action {
font-weight: 600;
color: #1A1A1A;
font-size: 1rem;
}
.step-meta {
display: flex;
gap: 1rem;
margin-top: 0.5rem;
flex-wrap: wrap;
}
.step-meta span {
background: #F5F5F5;
padding: 0.2rem 0.6rem;
border-radius: 6px;
font-size: 0.8rem;
color: #6B6B6B;
}
.step-detail {
color: #6B6B6B;
margin-top: 0.5rem;
font-size: 0.9rem;
line-height: 1.5;
}
.plating-section {
background: linear-gradient(135deg, #FAF7F2 0%, #FFFFFF 100%);
border-radius: 16px;
padding: 1.5rem;
margin: 1rem 0;
border: 1px solid #E8E8E8;
}
.plating-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.plating-item {
text-align: center;
padding: 1rem;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.plating-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.plating-label {
font-size: 0.85rem;
color: #6B6B6B;
}
.plating-value {
font-weight: 600;
color: #1A1A1A;
font-size: 0.9rem;
}
.garnish-tags {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.garnish-tag {
background: white;
border: 1px solid #D4AF37;
color: #722F37;
padding: 0.3rem 0.8rem;
border-radius: 20px;
font-size: 0.85rem;
}
.emergence-section {
background: linear-gradient(135deg, #2D2D2D 0%, #1A1A1A 100%);
border-radius: 16px;
padding: 1.5rem;
margin: 1rem 0;
color: #FFFFFF !important;
}
.emergence-title {
color: #FFD700 !important;
font-family: 'Playfair Display', serif;
font-size: 1.2rem;
margin-bottom: 1rem;
}
.emergence-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.emergence-item {
background: rgba(255,255,255,0.05);
border-radius: 12px;
padding: 1rem;
border-left: 3px solid #D4AF37;
}
.emergence-item h5 {
color: #FFD700 !important;
font-size: 0.9rem;
margin: 0 0 0.5rem 0;
}
.emergence-item p {
color: #FFFFFF !important;
font-size: 0.9rem;
margin: 0;
line-height: 1.5;
}
.rules-box {
background: rgba(212, 175, 55, 0.1);
border-radius: 12px;
padding: 1rem;
margin: 1rem 0;
}
.rules-box h5 {
color: #FFD700 !important;
margin: 0 0 0.5rem 0;
}
.rules-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.rule-tag {
background: rgba(255,255,255,0.2);
color: #FFFFFF !important;
padding: 0.3rem 0.8rem;
border-radius: 15px;
font-size: 0.85rem;
}
.sauce-card {
background: linear-gradient(135deg, #C41E3A 0%, #722F37 100%);
border-radius: 16px;
padding: 1.5rem;
color: #FFFFFF !important;
margin: 1rem 0;
}
.sauce-card h4 {
color: #FFD700 !important;
font-family: 'Playfair Display', serif;
margin: 0 0 1rem 0;
}
.sauce-ingredients {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.sauce-ingredient {
background: rgba(255,255,255,0.2);
color: #FFFFFF !important;
padding: 0.3rem 0.8rem;
border-radius: 15px;
font-size: 0.9rem;
}
.sauce-notes {
font-style: italic;
color: #FFFFFF !important;
font-size: 0.95rem;
}
</style>
"""
if recipe_type == "general":
name = result.get('name', '요리')
recipe_id = result.get('recipe_id', '')
eval_data = result.get('evaluation', {})
score = eval_data.get('total_score', 0)
rating = eval_data.get('rating', '⭐⭐')
concept = result.get('concept', {})
ingredients = result.get('ingredients', [])
steps = result.get('steps', [])
plating = result.get('plating', {})
strengths = eval_data.get('strengths', [])
stars_display = "⭐" * min(3, score // 30 + 1)
html = f"""{card_style}
<div class="recipe-card">
<div class="recipe-header">
<div style="display: flex; justify-content: space-between; align-items: start; flex-wrap: wrap; gap: 1rem;">
<div>
<h2 class="recipe-title">🍽️ {name}</h2>
<p class="recipe-meta">{concept.get('vision', '미슐랭 수준의 완성도')}</p>
</div>
<span class="recipe-id">{recipe_id}</span>
</div>
</div>
<div class="score-section">
<div class="score-circle">
<span class="score-number">{score}</span>
<span class="score-label">점</span>
</div>
<div style="text-align: center;">
<div class="rating-stars">{stars_display}</div>
<div class="rating-text">{rating}</div>
</div>
<div class="strengths-list">
{''.join([f'<span class="strength-badge">✓ {s}</span>' for s in strengths[:4]])}
</div>
</div>
<div class="recipe-body">
<div class="concept-box">
<h4>💡 요리 컨셉</h4>
<p><strong>풍미 방향:</strong> {concept.get('flavor_direction', '')}</p>
<p><strong>프레젠테이션:</strong> {concept.get('presentation_style', '')}</p>
<p><strong>특별 노트:</strong> {concept.get('special_notes', '')}</p>
</div>
<h3 class="section-title">🥬 재료 목록</h3>
<div class="ingredients-grid">
{''.join([f'''
<div class="ingredient-card">
<div class="ingredient-name">{ing.get('name', '')}</div>
<div class="ingredient-amount">{ing.get('amount', '')}</div>
<div class="ingredient-detail">{ing.get('quality', '')} · {ing.get('role', '')}</div>
</div>
''' for ing in ingredients[:8]])}
</div>
<h3 class="section-title">👨‍🍳 조리 순서</h3>
<div class="steps-timeline">
{''.join([f'''
<div class="step-item" data-step="{step.get('step', idx+1)}">
<div class="step-action">{step.get('action', '')}</div>
<div class="step-meta">
<span>⏱️ {step.get('duration', '')}</span>
<span>🌡️ {step.get('temp', '')}</span>
<span>🔧 {step.get('technique', '')}</span>
</div>
<div class="step-detail">{step.get('detail', '')}</div>
</div>
''' for idx, step in enumerate(steps[:6])])}
</div>
<h3 class="section-title">🎨 플레이팅</h3>
<div class="plating-section">
<div class="plating-grid">
<div class="plating-item">
<div class="plating-icon">🍽️</div>
<div class="plating-label">그릇</div>
<div class="plating-value">{plating.get('plating_design', {}).get('plate', plating.get('plate', '원형 접시'))}</div>
</div>
<div class="plating-item">
<div class="plating-icon">📐</div>
<div class="plating-label">배치</div>
<div class="plating-value">{plating.get('plating_design', {}).get('layout', plating.get('layout', '중앙 배치'))[:20]}</div>
</div>
</div>
<div class="garnish-tags">
{''.join([f'<span class="garnish-tag">{g}</span>' for g in plating.get('plating_design', {}).get('garnish', plating.get('garnish', []))[:6]])}
</div>
</div>
</div>
</div>
"""
return html
elif recipe_type == "kfood":
name = result.get('dish_name', '한식 요리')
recipe_id = result.get('recipe_id', '')
category = result.get('category', '')
regional = result.get('regional_style', '')
season = result.get('season', '')
spice = result.get('spice_level', 5)
concept = result.get('concept', {})
ingredients = result.get('ingredients', [])
steps = result.get('steps', [])
plating = result.get('plating', {})
sauce = result.get('korean_sauce', {})
eval_data = result.get('evaluation', {})
score = eval_data.get('total_score', 0)
rating = eval_data.get('rating', '⭐⭐')
spice_bar = "🌶️" * min(10, spice) + "○" * (10 - min(10, spice))
html = f"""{card_style}
<div class="recipe-card">
<div class="recipe-header" style="background: linear-gradient(135deg, #722F37 0%, #C41E3A 100%);">
<div style="display: flex; justify-content: space-between; align-items: start; flex-wrap: wrap; gap: 1rem;">
<div>
<h2 class="recipe-title">🇰🇷 {name}</h2>
<p class="recipe-meta">{category} · {regional} · {season} · 매운맛 {spice}단계</p>
</div>
<span class="recipe-id">{recipe_id}</span>
</div>
</div>
<div class="score-section">
<div class="score-circle">
<span class="score-number">{score}</span>
<span class="score-label">점</span>
</div>
<div style="text-align: center;">
<div class="rating-stars">{rating}</div>
<div style="font-size: 0.9rem; color: #6B6B6B; margin-top: 0.5rem;">{spice_bar}</div>
</div>
<div class="strengths-list">
{''.join([f'<span class="strength-badge">✓ {s}</span>' for s in eval_data.get('strengths', [])[:4]])}
</div>
</div>
<div class="recipe-body">
<div class="concept-box" style="background: linear-gradient(135deg, #722F37 0%, #C41E3A 100%);">
<h4>🏛️ 한식 철학</h4>
<p><strong>컨셉:</strong> {concept.get('concept', '')}</p>
<p><strong>철학:</strong> {concept.get('korean_philosophy', '')}</p>
<p><strong>계절 접근:</strong> {concept.get('seasonal_approach', '')}</p>
</div>
<div class="sauce-card">
<h4>🥄 {sauce.get('name', '특제 양념장')}</h4>
<div class="sauce-ingredients">
{''.join([f'<span class="sauce-ingredient">{s}</span>' for s in sauce.get('base', [])])}
</div>
<p class="sauce-notes">{sauce.get('flavor_notes', '')}</p>
</div>
<h3 class="section-title">🥬 재료 목록</h3>
<div class="ingredients-grid">
{''.join([f'''
<div class="ingredient-card">
<div class="ingredient-name">{ing.get('name', '')}</div>
<div class="ingredient-amount">{ing.get('amount', '')}</div>
<div class="ingredient-detail">{ing.get('traditional_use', ing.get('role', ''))}</div>
</div>
''' for ing in ingredients[:8]])}
</div>
<h3 class="section-title">👨‍🍳 조리 순서</h3>
<div class="steps-timeline">
{''.join([f'''
<div class="step-item" data-step="{step.get('step', idx+1)}">
<div class="step-action">{step.get('action', '')}</div>
<div class="step-meta">
<span>⏱️ {step.get('duration', '')}</span>
<span>🌡️ {step.get('temp', '')}</span>
<span>🥢 {step.get('korean_technique', step.get('technique', ''))}</span>
</div>
<div class="step-detail">{step.get('detail', '')}</div>
</div>
''' for idx, step in enumerate(steps[:6])])}
</div>
<h3 class="section-title">🎨 담음새</h3>
<div class="plating-section">
<div class="plating-grid">
<div class="plating-item">
<div class="plating-icon">🏺</div>
<div class="plating-label">그릇</div>
<div class="plating-value">{plating.get('plating_design', {}).get('vessel', '백자')}</div>
</div>
<div class="plating-item">
<div class="plating-icon">🎨</div>
<div class="plating-label">색의 조화</div>
<div class="plating-value">{plating.get('color_harmony', '오방색')[:15]}</div>
</div>
</div>
<div class="garnish-tags">
{''.join([f'<span class="garnish-tag">{g}</span>' for g in plating.get('plating_design', {}).get('garnish', [])[:6]])}
</div>
</div>
</div>
</div>
"""
return html
elif recipe_type == "emergent":
res = result.get('result', {})
name = res.get('name', '창발적 요리')
recipe_id = result.get('recipe_id', '')
base_dish = result.get('base_dish', '')
emergence_type = result.get('emergence_type', '')
rules = result.get('applied_rules', {})
emergence_names = {
"variable_crossing": "🔀 변수 교차",
"context_shift": "🌍 맥락 전환",
"architecture_inversion": "🏗️ 구조 전복",
"grammar_transplantation": "📝 문법 이식",
"flavor_compound_pairing": "🧪 풍미 페어링",
"texture_contrast": "✨ 텍스처 대비"
}
emergence_display = emergence_names.get(emergence_type, emergence_type)
html = f"""{card_style}
<div class="recipe-card">
<div class="recipe-header" style="background: linear-gradient(135deg, #1A1A1A 0%, #4A148C 50%, #1A1A1A 100%);">
<div style="display: flex; justify-content: space-between; align-items: start; flex-wrap: wrap; gap: 1rem;">
<div>
<h2 class="recipe-title">🔬 {name}</h2>
<p class="recipe-meta">{base_dish}{emergence_display}</p>
</div>
<span class="recipe-id">{recipe_id}</span>
</div>
</div>
<div class="score-section" style="background: linear-gradient(135deg, #F3E5F5 0%, #FFFFFF 100%);">
<div class="score-circle" style="background: linear-gradient(135deg, #9C27B0 0%, #E1BEE7 50%, #9C27B0 100%);">
<span class="score-number" style="color: white;">88</span>
<span class="score-label" style="color: white;">점</span>
</div>
<div style="text-align: center;">
<div class="rating-stars">🧬 창발적 혁신</div>
<div class="rating-text" style="color: #7B1FA2;">6대 차원 알고리즘</div>
</div>
<div class="strengths-list">
<span class="strength-badge" style="background: rgba(156, 39, 176, 0.1); color: #7B1FA2;">✓ 창의성</span>
<span class="strength-badge" style="background: rgba(156, 39, 176, 0.1); color: #7B1FA2;">✓ 과학적 근거</span>
<span class="strength-badge" style="background: rgba(156, 39, 176, 0.1); color: #7B1FA2;">✓ 문화 융합</span>
</div>
</div>
<div class="recipe-body">
<div class="emergence-section" style="background: linear-gradient(135deg, #4A148C 0%, #7B1FA2 100%);">
<h4 class="emergence-title">🧬 적용된 창발 규칙</h4>
<div class="rules-box" style="background: rgba(255,255,255,0.1);">
<div class="rules-list">
{''.join([f'<span class="rule-tag"><strong>{k}:</strong> {v if not isinstance(v, list) else ", ".join(str(x) for x in v[:3])}</span>' for k, v in rules.items()])}
</div>
</div>
<div class="emergence-grid">
{f'''
<div class="emergence-item">
<h5>💡 컨셉</h5>
<p>{res.get('concept', '')[:100]}...</p>
</div>
''' if res.get('concept') else ''}
{f'''
<div class="emergence-item">
<h5>🔧 핵심 기법</h5>
<p>{res.get('technique', '')[:100]}...</p>
</div>
''' if res.get('technique') else ''}
{f'''
<div class="emergence-item">
<h5>🎯 의외의 페어링</h5>
<p>{res.get('unexpected_pairing', '')}</p>
</div>
''' if res.get('unexpected_pairing') else ''}
{f'''
<div class="emergence-item">
<h5>🔬 작동 원리</h5>
<p>{res.get('why_it_works', '')[:100]}...</p>
</div>
''' if res.get('why_it_works') else ''}
{f'''
<div class="emergence-item">
<h5>🎨 예상 텍스처</h5>
<p>{res.get('expected_texture', '')}</p>
</div>
''' if res.get('expected_texture') else ''}
{f'''
<div class="emergence-item">
<h5>📝 풍미 노트</h5>
<p>{res.get('flavor_notes', '')[:100]}...</p>
</div>
''' if res.get('flavor_notes') else ''}
{f'''
<div class="emergence-item">
<h5>💎 혁신 포인트</h5>
<p>{res.get('innovation_point', '')[:100]}...</p>
</div>
''' if res.get('innovation_point') else ''}
{f'''
<div class="emergence-item">
<h5>🍽️ 먹는 경험</h5>
<p>{res.get('eating_experience', '')[:100]}...</p>
</div>
''' if res.get('eating_experience') else ''}
</div>
</div>
{f'''
<div class="concept-box">
<h4>🌉 풍미 브릿지</h4>
<p>{res.get('flavor_bridge', '')}</p>
</div>
''' if res.get('flavor_bridge') else ''}
{f'''
<h3 class="section-title">🔄 유지된 요소</h3>
<div class="garnish-tags">
{"".join([f'<span class="garnish-tag">{e}</span>' for e in res.get("kept_elements", [])])}
</div>
''' if res.get('kept_elements') else ''}
{f'''
<h3 class="section-title">✨ 새로운 요소</h3>
<div class="garnish-tags">
{"".join([f'<span class="garnish-tag" style="background: #E1BEE7; color: #4A148C; border-color: #9C27B0;">{e}</span>' for e in res.get("new_elements", [])])}
</div>
''' if res.get('new_elements') else ''}
</div>
</div>
"""
return html
return "<p>레시피 정보가 없습니다.</p>"
def create_gradio_app(system: MichelinRecipeSystem):
"""Gradio 앱 생성 - Premium UI v3.2"""
def develop_recipe_ui(request, ingredients_str, servings, max_time, gen_image, progress=gr.Progress()):
ingredients = [i.strip() for i in ingredients_str.split(",") if i.strip()]
constraints = {"servings": servings, "max_time": max_time}
def progress_callback(pct, msg):
progress(pct, desc=msg)
result = system.develop_recipe(
user_request=request,
ingredients=ingredients,
constraints=constraints,
generate_images=gen_image,
progress_callback=progress_callback
)
# HTML 형식 출력
output = format_recipe_html(result, "general")
return output, result.get('image_url')
def develop_kfood_ui(request, category, regional, season, spice, gen_image, progress=gr.Progress()):
def progress_callback(pct, msg):
progress(pct, desc=msg)
result = system.develop_kfood_recipe(
user_request=request,
category=category,
regional_style=regional,
season=season,
spice_level=spice,
generate_images=gen_image,
progress_callback=progress_callback
)
# HTML 형식 출력
output = format_recipe_html(result, "kfood")
return output, result.get('image_url')
def create_emergent_ui(base_dish, emergence_type, param1, param2, gen_image, progress=gr.Progress()):
"""v3.2: 창발적 레시피 UI - 이미지 생성 추가!"""
type_map = {
"변수 교차": ("variable_crossing", {"method1": param1 or "", "method2": param2 or ""}),
"맥락 전환": ("context_shift", {"target_cuisine": param1 or ""}),
"구조 전복": ("architecture_inversion", {"new_structure": param1 or ""}),
"문법 이식": ("grammar_transplantation", {"new_culture": param1 or ""}),
"풍미 화합물 페어링": ("flavor_compound_pairing", {"ingredient": param1 or ""}),
"텍스처 대비": ("texture_contrast", {"target_texture": param1 or ""})
}
etype, kwargs = type_map.get(emergence_type, ("context_shift", {"target_cuisine": "italian"}))
def progress_callback(pct, msg):
progress(pct, desc=msg)
result = system.create_emergent_recipe(
base_dish,
etype,
generate_images=gen_image,
progress_callback=progress_callback,
**kwargs
)
# HTML 형식 출력
output = format_recipe_html(result, "emergent")
return output, result.get('image_url')
def search_ingredients_ui(query, progress=gr.Progress()):
if not query or len(query.strip()) < 1:
return "검색어를 입력하세요."
progress(0.3, "🔍 검색 중...")
result = system.search_ingredient_info(query)
progress(1.0, "✅ 완료!")
output = f"# 🔍 '{query}' 재료 정보\n\n"
output += f"## 📋 기본 정보\n\n"
output += f"- **재료명**: {result.get('name', query)}\n"
output += f"- **영문명**: {result.get('name_en', '')}\n"
output += f"- **분류**: {result.get('category', '')}\n"
output += f"- **특성**: {result.get('characteristics', '')}\n\n"
flavor = result.get('flavor_profile', {})
if flavor:
output += "## 🍷 풍미 프로파일\n\n"
output += f"- **맛**: {', '.join(flavor.get('taste', []))}\n"
output += f"- **향**: {', '.join(flavor.get('aroma', []))}\n"
output += f"- **질감**: {', '.join(flavor.get('texture', []))}\n\n"
uses = result.get('culinary_uses', [])
if uses:
output += "## 🍳 요리 활용법\n\n"
for use in uses:
output += f"- {use}\n"
output += "\n"
pairings = result.get('pairings', [])
if pairings:
output += f"## 🤝 잘 어울리는 재료\n\n{', '.join(pairings)}\n\n"
return output
def get_flavor_pairings_ui(ingredient, progress=gr.Progress()):
if not ingredient or len(ingredient.strip()) < 1:
return "재료명을 입력하세요."
progress(0.3, "🔍 분석 중...")
result = system.search_flavor_pairing(ingredient)
progress(1.0, "✅ 완료!")
output = f"# 🍷 {ingredient} 풍미 페어링 분석\n\n"
compounds = result.get('flavor_compounds', [])
if compounds:
output += f"## 🧪 주요 향 화합물\n\n{', '.join(compounds)}\n\n"
taste = result.get('taste_profile', {})
if taste:
output += "## 📊 맛 프로파일\n\n"
output += "| 맛 | 강도 |\n|:---|:----:|\n"
taste_kr = {"sweetness": "단맛", "sourness": "신맛", "saltiness": "짠맛", "bitterness": "쓴맛", "umami": "감칠맛"}
for key, val in taste.items():
if isinstance(val, (int, float)):
output += f"| {taste_kr.get(key, key)} | {'●' * int(val)}{'○' * (10-int(val))} ({val}/10) |\n"
output += "\n"
classic = result.get('classic_pairings', [])
if classic:
output += "## 🎯 클래식 페어링\n\n"
for p in classic[:5]:
if isinstance(p, dict):
output += f"- **{p.get('ingredient', '')}**: {p.get('reason', '')}\n"
else:
output += f"- {p}\n"
output += "\n"
unexpected = result.get('unexpected_pairings', [])
if unexpected:
output += "## ⭐ 창발적 페어링\n\n"
for p in unexpected[:5]:
if isinstance(p, dict):
output += f"- **{p.get('ingredient', '')}**: {p.get('reason', '')} (공유 화합물: {p.get('shared_compound', '')})\n"
else:
output += f"- {p}\n"
output += "\n"
return output
def get_restaurants_ui(stars, cuisine, location):
stars_val = None if stars == "전체" else int(stars[0])
restaurants = system.db.get_restaurants(
stars=stars_val,
cuisine=cuisine if cuisine else None,
location=location if location else None
)
if not restaurants:
return "검색 결과가 없습니다."
output = f"# 🌟 검색 결과: {len(restaurants)}개 레스토랑\n\n"
output += "| 이름 | 위치 | 요리 | 등급 | 설명 |\n|:-----|:-----|:-----|:----:|:-----|\n"
for r in restaurants[:30]:
stars_str = "⭐" * r.get('stars', 0) if r.get('stars', 0) > 0 else "Bib"
desc = (r.get('description', '') or '')[:50]
output += f"| {r.get('name', '')} | {r.get('location', '')} | {r.get('cuisine', '')} | {stars_str} | {desc}... |\n"
return output
def analyze_trends_ui():
return system.analyze_michelin_trends()
def get_kfood_info(category):
if category not in KFOOD_CATEGORIES:
return ""
cat_data = KFOOD_CATEGORIES[category]
info = f"### {category}\n\n"
info += f"**설명**: {cat_data.get('description', '')}\n\n"
info += f"**주요 기법**: {', '.join(cat_data.get('techniques', []))}\n\n"
info += f"**대표 요리**: {', '.join(cat_data.get('signature_dishes', []))}\n\n"
info += f"**핵심 재료**: {', '.join(cat_data.get('key_ingredients', []))}\n"
return info
# Gradio 앱 구성 - Premium UI
with gr.Blocks(title="🍽️ Michelin AGI Recipe System v3.2", css=MICHELIN_CSS) as app:
# Hero Banner
gr.HTML(HERO_BANNER_HTML)
with gr.Tabs():
# 탭 1: 일반 레시피 개발
with gr.Tab("🧪 레시피 개발"):
gr.HTML("""
<div style="background: linear-gradient(135deg, #1A1A1A 0%, #2D2D2D 100%);
padding: 1.5rem; border-radius: 16px; margin-bottom: 1.5rem;
border-left: 4px solid #D4AF37; color: white;">
<h3 style="font-family: 'Playfair Display', serif; color: #D4AF37; margin: 0 0 0.5rem 0;">
🧑‍🍳 SOMA Agent Team
</h3>
<p style="color: #E8E8E8; margin: 0; font-size: 0.95rem;">
6명의 전문 셰프가 협업하여 미슐랭 수준의 레시피를 개발합니다.
Executive Chef → Garde Manger → Research Chef → Saucier → Patissier → Michelin Inspector
</p>
</div>
""")
with gr.Row():
with gr.Column(scale=1):
request_input = gr.Textbox(label="🍳 요리 요청", placeholder="예: 베이컨 시금치 계란말이, 한우 안심 스테이크...", lines=2)
ingredients_input = gr.Textbox(label="🥬 재료 (쉼표 구분)", placeholder="계란, 베이컨, 시금치, 소금, 후추, 버터", lines=2)
with gr.Row():
servings_input = gr.Slider(1, 10, value=2, step=1, label="👥 인분")
time_input = gr.Slider(10, 180, value=30, step=5, label="⏱️ 최대 시간 (분)")
gen_image_cb = gr.Checkbox(label="🖼️ AI 이미지 생성 (Nano-Banana-Pro)", value=False)
develop_btn = gr.Button("🚀 레시피 개발 시작", variant="primary", size="lg")
with gr.Column(scale=2):
recipe_output = gr.HTML(label="레시피 결과")
image_output = gr.Image(label="🖼️ 완성 이미지", type="filepath", show_label=True)
develop_btn.click(develop_recipe_ui, [request_input, ingredients_input, servings_input, time_input, gen_image_cb], [recipe_output, image_output])
# 탭 2: K-FOOD 특화
with gr.Tab("🇰🇷 K-FOOD 특화"):
gr.HTML("""
<div style="background: linear-gradient(135deg, #722F37 0%, #C41E3A 100%);
padding: 1.5rem; border-radius: 16px; margin-bottom: 1.5rem; color: white;">
<h3 style="font-family: 'Playfair Display', serif; color: #F4E4BA; margin: 0 0 0.5rem 0;">
🏛️ 한식의 철학과 혼을 담다
</h3>
<p style="color: #E8B4B8; margin: 0; font-size: 0.95rem;">
오미(五味)의 조화, 오방색(五方色)의 미학, 약식동원(藥食同源)의 지혜를 현대적으로 재해석합니다.
</p>
</div>
""")
with gr.Row():
with gr.Column(scale=1):
kfood_request = gr.Textbox(label="🍚 요리 요청", placeholder="예: 고추장 글레이즈 삼겹살 스테이크, 트러플 비빔밥...", lines=2)
kfood_category = gr.Dropdown(choices=list(KFOOD_CATEGORIES.keys()), value="퓨전한식", label="📂 카테고리")
kfood_regional = gr.Dropdown(choices=list(KFOOD_REGIONAL_STYLES.keys()), value="서울/경기", label="📍 지역 스타일")
kfood_season = gr.Dropdown(choices=list(KFOOD_SEASONAL_INGREDIENTS.keys()), value="가을", label="🍂 계절")
kfood_spice = gr.Slider(0, 10, value=5, step=1, label="🌶️ 매운맛 레벨")
kfood_gen_image = gr.Checkbox(label="🖼️ AI 이미지 생성", value=False)
kfood_btn = gr.Button("🚀 K-FOOD 레시피 개발", variant="primary", size="lg")
kfood_info = gr.HTML()
kfood_category.change(get_kfood_info, [kfood_category], [kfood_info])
with gr.Column(scale=2):
kfood_output = gr.HTML(label="레시피 결과")
kfood_image_output = gr.Image(label="🖼️ 완성 이미지", type="filepath")
kfood_btn.click(develop_kfood_ui, [kfood_request, kfood_category, kfood_regional, kfood_season, kfood_spice, kfood_gen_image], [kfood_output, kfood_image_output])
# 탭 3: 창발적 레시피 (v3.2: 이미지 생성 추가!)
with gr.Tab("🔬 창발적 레시피"):
gr.HTML("""
<div style="background: linear-gradient(135deg, #1A1A1A 0%, #4A148C 50%, #1A1A1A 100%);
padding: 2rem; border-radius: 16px; margin-bottom: 1.5rem; color: white; position: relative; overflow: hidden;">
<div style="position: absolute; top: 0; right: 0; bottom: 0; left: 0;
background: url('data:image/svg+xml,%3Csvg width=\"40\" height=\"40\" viewBox=\"0 0 40 40\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cg fill=\"%23D4AF37\" fill-opacity=\"0.1\"%3E%3Cpath d=\"M20 20l20-20H0z\"/%3E%3C/g%3E%3C/svg%3E');"></div>
<div style="position: relative; z-index: 1;">
<h3 style="font-family: 'Playfair Display', serif; color: #D4AF37; margin: 0 0 0.5rem 0; font-size: 1.5rem;">
🧬 6대 차원 창발적 레시피 엔진 v3.2
</h3>
<p style="color: #E8E8E8; margin: 0 0 1rem 0; font-size: 0.95rem;">
D1:재료 × D2:풍미 × D3:조리법 × D4:텍스처 × D5:구조 × D6:문화문법
</p>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<span style="background: rgba(156, 39, 176, 0.3); padding: 0.3rem 0.8rem; border-radius: 15px; font-size: 0.85rem; color: #FFFFFF;">✨ 이미지 생성</span>
<span style="background: rgba(156, 39, 176, 0.3); padding: 0.3rem 0.8rem; border-radius: 15px; font-size: 0.85rem; color: #FFFFFF;">🎲 랜덤 강화</span>
<span style="background: rgba(156, 39, 176, 0.3); padding: 0.3rem 0.8rem; border-radius: 15px; font-size: 0.85rem; color: #FFFFFF;">📝 5단계 파싱</span>
</div>
</div>
</div>
""")
with gr.Row():
with gr.Column(scale=1):
emg_base = gr.Textbox(label="🍳 기본 요리", placeholder="비빔밥, 떡볶이, 김치찌개, 불고기...", lines=1)
emg_type = gr.Dropdown(
choices=["변수 교차", "맥락 전환", "구조 전복", "문법 이식", "풍미 화합물 페어링", "텍스처 대비"],
value="풍미 화합물 페어링",
label="🧬 창발 유형"
)
emg_param1 = gr.Textbox(label="📌 파라미터 1", placeholder="빈칸 = 랜덤 선택! 🎲", lines=1)
emg_param2 = gr.Textbox(label="📌 파라미터 2 (변수 교차용)", placeholder="빈칸 = 랜덤 선택! 🎲", lines=1)
emg_gen_image = gr.Checkbox(label="🖼️ 창발적 요리 이미지 생성", value=True)
emg_btn = gr.Button("🔬 창발적 레시피 생성", variant="primary", size="lg")
gr.HTML("""
<div style="background: #F3E5F5; padding: 1rem; border-radius: 12px; margin-top: 1rem; border-left: 3px solid #9C27B0;">
<h4 style="color: #4A148C; margin: 0 0 0.5rem 0; font-size: 0.95rem;">📖 파라미터 가이드</h4>
<ul style="color: #6B6B6B; margin: 0; padding-left: 1.2rem; font-size: 0.85rem; line-height: 1.8;">
<li><strong>변수 교차:</strong> 조리법1, 조리법2 (수비드, 시어링, 그릴링...)</li>
<li><strong>맥락 전환:</strong> 문화권 (japanese, french, italian...)</li>
<li><strong>구조 전복:</strong> 구조 (Wrapped, Layered, Stuffed...)</li>
<li><strong>풍미 페어링:</strong> 재료 (김치, 고추장, 된장...)</li>
<li><strong>텍스처 대비:</strong> 텍스처 (Crispy, Creamy, Chewy...)</li>
</ul>
</div>
""")
with gr.Column(scale=2):
emg_output = gr.HTML(label="창발적 레시피 결과")
emg_image_output = gr.Image(label="🖼️ 창발적 요리 이미지", type="filepath")
emg_btn.click(create_emergent_ui, [emg_base, emg_type, emg_param1, emg_param2, emg_gen_image], [emg_output, emg_image_output])
# 탭 4: 재료/풍미 검색
with gr.Tab("🔍 재료/풍미 검색"):
gr.HTML("""
<div style="background: linear-gradient(135deg, #E8F5E9 0%, #FFFFFF 100%);
padding: 1.5rem; border-radius: 16px; margin-bottom: 1.5rem; border-left: 4px solid #4CAF50;">
<h3 style="font-family: 'Playfair Display', serif; color: #2E7D32; margin: 0 0 0.5rem 0;">
🔍 Brave Search + LLM 재료/풍미 검색
</h3>
<p style="color: #6B6B6B; margin: 0; font-size: 0.95rem;">
웹 검색과 AI 분석을 결합하여 재료 특성, 풍미 프로파일, 창발적 페어링을 찾아드립니다.
</p>
</div>
""")
with gr.Row():
with gr.Column():
gr.HTML("<h3 style='font-family: Playfair Display, serif; color: #2E7D32;'>📋 재료 정보 검색</h3>")
search_query = gr.Textbox(label="검색어", placeholder="소고기, salmon, 고추장, 트러플...")
search_btn = gr.Button("🔍 재료 검색", variant="primary")
search_output = gr.Markdown()
search_btn.click(search_ingredients_ui, [search_query], [search_output])
with gr.Column():
gr.HTML("<h3 style='font-family: Playfair Display, serif; color: #7B1FA2;'>🍷 풍미 페어링 분석</h3>")
pairing_ingredient = gr.Textbox(label="재료명", placeholder="김치, 고추장, 된장, 커피...")
pairing_btn = gr.Button("🍷 페어링 분석", variant="primary")
pairing_output = gr.Markdown()
pairing_btn.click(get_flavor_pairings_ui, [pairing_ingredient], [pairing_output])
# 탭 5: 미슐랭 레스토랑
with gr.Tab("🌟 미슐랭 레스토랑"):
with gr.Row():
stars_filter = gr.Dropdown(choices=["전체", "3 Stars", "2 Stars", "1 Star", "0 (Bib Gourmand)"], value="전체", label="⭐ 별점 필터")
cuisine_filter = gr.Textbox(label="🍳 요리 스타일", placeholder="Korean, French, Italian...")
location_filter = gr.Textbox(label="📍 지역", placeholder="서울, Tokyo, Paris...")
search_rest_btn = gr.Button("🔍 검색", variant="primary")
restaurants_output = gr.Markdown()
search_rest_btn.click(get_restaurants_ui, [stars_filter, cuisine_filter, location_filter], [restaurants_output])
# 탭 6: 트렌드 분석
with gr.Tab("📈 트렌드"):
analyze_btn = gr.Button("📊 트렌드 분석", variant="primary")
trends_output = gr.Markdown()
analyze_btn.click(analyze_trends_ui, [], [trends_output])
gr.Markdown("---\n**Made with ❤️ by OpenFreeAI ** | Powered by https://open.kakao.com/o/peIe8KWh **")
return app
def main():
print("""
╔═══════════════════════════════════════════════════════════════════════════╗
║ 🍽️ MICHELIN AGI RECIPE SYSTEM v3.2 🍽️ ║
║ 미슐랭 스타 레벨 AGI(범용인공지능) 레시피 개발 시스템 ║
║ • SOMA Agent Team + K-FOOD 특화 ║
║ • ⭐ v3.2 NEW: 창발적 레시피 이미지 생성! ║
║ • ⭐ v3.2 NEW: 랜덤 요소 강화 (매번 다른 결과!) ║
║ • ⭐ v3.2 NEW: JSON 파싱 개선 (5단계 폴백) ║
║ D1:재료 D2:풍미 D3:조리법 D4:텍스처 D5:구조 D6:문화문법 ║
║ • 🔍 Brave Search + LLM 재료/풍미 검색 ║
║ • Fireworks AI + Replicate (Flux-Schnell) ║
╚═══════════════════════════════════════════════════════════════════════════╝
""")
fireworks_key = os.environ.get("FIREWORKS_API_KEY", "")
replicate_key = os.environ.get("REPLICATE_API_TOKEN", "")
brave_key = os.environ.get("BRAVE_API_KEY", "")
print(f"🔑 Fireworks API: {'✅ 활성화' if fireworks_key else '⚠️ 목업 모드'}")
print(f"🔑 Replicate API: {'✅ 활성화' if replicate_key else '⚠️ 이미지 비활성화'}")
print(f"🔑 Brave Search API: {'✅ 활성화' if brave_key else '⚠️ 검색 제한'}")
print(f"📁 미슐랭 CSV: {'✅ 있음' if os.path.exists('michelin.csv') else '⚠️ 폴백 데이터'}")
print(f"📁 창발 데이터: {'✅ 있음' if os.path.exists('culinary_data.json') else '⚠️ 내장 폴백'}")
print(f"📦 Gradio 설치: {'✅' if HAS_GRADIO else '❌ 없음'}")
print(f"📦 Replicate 설치: {'✅' if HAS_REPLICATE else '❌ 없음'}")
try:
system = MichelinRecipeSystem(api_key=fireworks_key)
print("✅ MichelinRecipeSystem 초기화 완료")
except Exception as e:
print(f"❌ 시스템 초기화 실패: {e}")
import traceback
traceback.print_exc()
return
if HAS_GRADIO:
print("\n🚀 Gradio UI 시작...")
try:
app = create_gradio_app(system)
print("✅ Gradio 앱 생성 완료")
app.launch(
share=False,
server_name="0.0.0.0",
server_port=7860,
show_error=True,
ssr_mode=False
)
except Exception as e:
print(f"❌ Gradio 앱 시작 실패: {e}")
import traceback
traceback.print_exc()
else:
print("\n📝 CLI 모드로 실행")
print("\n--- 창발적 레시피 테스트 (v3.2) ---")
result = system.create_emergent_recipe(
base_dish="비빔밥",
emergence_type="flavor_compound_pairing",
generate_images=False,
ingredient=""
)
print(f"✅ 결과: {json.dumps(result, ensure_ascii=False, indent=2)}")
if __name__ == "__main__":
main()