import gradio as gr import datetime from datetime import timedelta import openai import os from typing import Tuple, Dict, List import pandas as pd from korean_lunar_calendar import KoreanLunarCalendar # OpenAI API 설정 openai.api_key = "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" class SajuCalculator: def __init__(self): self.calendar = KoreanLunarCalendar() # 천간 (10개) self.cheongan = ['갑', '을', '병', '정', '무', '기', '경', '신', '임', '계'] # 지지 (12개) self.jiji = ['자', '축', '인', '묘', '진', '사', '오', '미', '신', '유', '술', '해'] # 월지지 매핑 (절기 기준) self.month_jiji = { 1: '인', # 인월 (입춘~경칩) 2: '묘', # 묘월 (경칩~청명) 3: '진', # 진월 (청명~입하) 4: '사', # 사월 (입하~망종) 5: '오', # 오월 (망종~소서) 6: '미', # 미월 (소서~입추) 7: '신', # 신월 (입추~백로) 8: '유', # 유월 (백로~한로) 9: '술', # 술월 (한로~입동) 10: '해', # 해월 (입동~대설) 11: '자', # 자월 (대설~소한) 12: '축' # 축월 (소한~입춘) } # 24절기 날짜 (한국 기준) self.solar_terms = { '입춘': (2, 4), '우수': (2, 19), '경칩': (3, 6), '춘분': (3, 21), '청명': (4, 5), '곡우': (4, 20), '입하': (5, 6), '소만': (5, 21), '망종': (6, 6), '하지': (6, 21), '소서': (7, 7), '대서': (7, 23), '입추': (8, 8), '처서': (8, 23), '백로': (9, 8), '추분': (9, 23), '한로': (10, 8), '상강': (10, 23), '입동': (11, 7), '소설': (11, 22), '대설': (12, 7), '동지': (12, 22), '소한': (1, 6), '대한': (1, 20) } # 시주 계산용 천간 순서 self.time_cheongan_base = { '갑': 0, '을': 0, '병': 2, '정': 2, '무': 4, '기': 4, '경': 6, '신': 6, '임': 8, '계': 8 } # 시간대별 지지 (24시간제) self.time_jiji_map = [ ('자', 23, 1), ('축', 1, 3), ('인', 3, 5), ('묘', 5, 7), ('진', 7, 9), ('사', 9, 11), ('오', 11, 13), ('미', 13, 15), ('신', 15, 17), ('유', 17, 19), ('술', 19, 21), ('해', 21, 23) ] def get_month_from_solar_date(self, month: int, day: int) -> int: """절기 기준으로 명리학적 월 계산""" # 절기 기준 월 구분 if month == 1: return 12 if day < 6 else 1 # 소한(1/6) 기준 elif month == 2: return 1 if day < 4 else 2 # 입춘(2/4) 기준 elif month == 3: return 2 if day < 6 else 3 # 경칩(3/6) 기준 elif month == 4: return 3 if day < 5 else 4 # 청명(4/5) 기준 elif month == 5: return 4 if day < 6 else 5 # 입하(5/6) 기준 elif month == 6: return 5 if day < 6 else 6 # 망종(6/6) 기준 elif month == 7: return 6 if day < 7 else 7 # 소서(7/7) 기준 elif month == 8: return 7 if day < 8 else 8 # 입추(8/8) 기준 elif month == 9: return 8 if day < 8 else 9 # 백로(9/8) 기준 elif month == 10: return 9 if day < 8 else 10 # 한로(10/8) 기준 elif month == 11: return 10 if day < 7 else 11 # 입동(11/7) 기준 elif month == 12: return 11 if day < 7 else 12 # 대설(12/7) 기준 def get_year_ganjhi_from_korean_calendar(self, year: int, month: int, day: int) -> str: """한국 음력 달력 라이브러리를 이용한 연주 계산""" try: self.calendar.setSolarDate(year, month, day) gapja_str = self.calendar.getGapJaString() # "정유년 병오월 임오일" 형식에서 연주만 추출 year_ganjhi = gapja_str.split('년')[0] + '년' return year_ganjhi.replace('년', '') except: # 라이브러리 실패시 기존 방식 사용 return self.get_year_ganjhi_fallback(year) def get_year_ganjhi_fallback(self, year: int) -> str: """연주 계산 (fallback)""" # 갑자년을 기준으로 계산 (1984년이 갑자년) base_year = 1984 year_diff = year - base_year cheongan_index = year_diff % 10 jiji_index = year_diff % 12 if cheongan_index < 0: cheongan_index += 10 if jiji_index < 0: jiji_index += 12 return self.cheongan[cheongan_index] + self.jiji[jiji_index] def get_month_ganjhi(self, year: int, saju_month: int) -> str: """월주 계산""" year_cheongan = self.get_year_ganjhi_from_korean_calendar(year, 1, 15)[0] # 년간에 따른 정월 천간 결정 month_cheongan_start = { '갑': 2, '을': 4, '병': 6, '정': 8, '무': 0, '기': 2, '경': 4, '신': 6, '임': 8, '계': 0 } start_index = month_cheongan_start[year_cheongan] month_cheongan_index = (start_index + saju_month - 1) % 10 month_jiji = self.month_jiji[saju_month] return self.cheongan[month_cheongan_index] + month_jiji def get_day_ganjhi_from_korean_calendar(self, year: int, month: int, day: int) -> str: """한국 음력 달력 라이브러리를 이용한 일주 계산""" try: self.calendar.setSolarDate(year, month, day) gapja_str = self.calendar.getGapJaString() # "정유년 병오월 임오일" 형식에서 일주만 추출 day_ganjhi = gapja_str.split(' ')[-1].replace('일', '') return day_ganjhi except: # 라이브러리 실패시 기존 방식 사용 return self.get_day_ganjhi_fallback(year, month, day) def get_day_ganjhi_fallback(self, year: int, month: int, day: int) -> str: """일주 계산 (fallback)""" # 기준일(1900.1.1 = 경자일) 부터의 일수 계산 base_date = datetime.date(1900, 1, 1) target_date = datetime.date(year, month, day) days_diff = (target_date - base_date).days # 경자일이 기준이므로 6(경)부터 시작 cheongan_index = (6 + days_diff) % 10 jiji_index = days_diff % 12 return self.cheongan[cheongan_index] + self.jiji[jiji_index] def get_time_jiji(self, hour: int) -> str: """시간대별 지지 결정""" for jiji, start, end in self.time_jiji_map: if start <= end: # 일반적인 경우 if start <= hour < end: return jiji else: # 자시의 경우 (23시~1시) if hour >= start or hour < end: return jiji return '자' # 기본값 def get_time_ganjhi(self, day_cheongan: str, hour: int) -> str: """시주 계산""" time_jiji = self.get_time_jiji(hour) # 일간에 따른 시간 천간 계산 base_index = self.time_cheongan_base[day_cheongan] jiji_order = ['자', '축', '인', '묘', '진', '사', '오', '미', '신', '유', '술', '해'] jiji_index = jiji_order.index(time_jiji) time_cheongan_index = (base_index + jiji_index) % 10 return self.cheongan[time_cheongan_index] + time_jiji def convert_lunar_to_solar(self, year: int, month: int, day: int, is_intercalation: bool = False) -> Tuple[int, int, int]: """음력을 양력으로 변환 (한국천문연구원 기준)""" try: success = self.calendar.setLunarDate(year, month, day, is_intercalation) if success: solar_iso = self.calendar.SolarIsoFormat() # "2017-06-24" 형식을 파싱 solar_year, solar_month, solar_day = map(int, solar_iso.split('-')) return solar_year, solar_month, solar_day else: raise ValueError("잘못된 음력 날짜입니다.") except Exception as e: raise ValueError(f"음력 변환 실패: {str(e)}") def convert_solar_to_lunar(self, year: int, month: int, day: int) -> Dict: """양력을 음력으로 변환 (한국천문연구원 기준)""" try: success = self.calendar.setSolarDate(year, month, day) if success: lunar_iso = self.calendar.LunarIsoFormat() # "2017-05-01 Intercalation" 또는 "2017-05-01" 형식 파싱 parts = lunar_iso.split() date_part = parts[0] is_intercalation = len(parts) > 1 and parts[1] == "Intercalation" lunar_year, lunar_month, lunar_day = map(int, date_part.split('-')) return { 'year': lunar_year, 'month': lunar_month, 'day': lunar_day, 'is_intercalation': is_intercalation, 'intercalation_text': ' (윤월)' if is_intercalation else '' } else: raise ValueError("잘못된 양력 날짜입니다.") except Exception as e: raise ValueError(f"양력 변환 실패: {str(e)}") def calculate_saju(self, name: str, birth_year: int, birth_month: int, birth_day: int, birth_hour: int, birth_minute: int, gender: str, location: str, is_lunar: bool, is_intercalation: bool = False) -> Dict: """전체 사주 계산""" try: if is_lunar: # 음력을 양력으로 변환 solar_year, solar_month, solar_day = self.convert_lunar_to_solar( birth_year, birth_month, birth_day, is_intercalation ) lunar_info = { 'year': birth_year, 'month': birth_month, 'day': birth_day, 'is_intercalation': is_intercalation, 'intercalation_text': ' (윤월)' if is_intercalation else '' } else: # 양력을 음력으로 변환 solar_year, solar_month, solar_day = birth_year, birth_month, birth_day lunar_info = self.convert_solar_to_lunar(birth_year, birth_month, birth_day) # 명리학적 월 계산 (절기 기준) saju_month = self.get_month_from_solar_date(solar_month, solar_day) # 사주 계산 year_ganjhi = self.get_year_ganjhi_from_korean_calendar(solar_year, solar_month, solar_day) month_ganjhi = self.get_month_ganjhi(solar_year, saju_month) day_ganjhi = self.get_day_ganjhi_from_korean_calendar(solar_year, solar_month, solar_day) time_ganjhi = self.get_time_ganjhi(day_ganjhi[0], birth_hour) result = { 'name': name, 'birth_info': { 'solar': f"{solar_year}년 {solar_month}월 {solar_day}일 {birth_hour:02d}시 {birth_minute:02d}분", 'lunar': f"{lunar_info['year']}년 {lunar_info['month']}월 {lunar_info['day']}일 {birth_hour:02d}시 {birth_minute:02d}분{lunar_info['intercalation_text']}", 'gender': gender, 'location': location, 'saju_month': saju_month, 'solar_term_info': self.get_solar_term_info(solar_month, solar_day) }, 'saju': { 'year': year_ganjhi, 'month': month_ganjhi, 'day': day_ganjhi, 'time': time_ganjhi }, 'elements': self.analyze_elements(year_ganjhi, month_ganjhi, day_ganjhi, time_ganjhi), 'detailed_analysis': self.get_detailed_saju_analysis(year_ganjhi, month_ganjhi, day_ganjhi, time_ganjhi) } return result except Exception as e: raise ValueError(f"사주 계산 중 오류 발생: {str(e)}") def get_solar_term_info(self, month: int, day: int) -> str: """현재 절기 정보 반환""" current_date = (month, day) for term, term_date in self.solar_terms.items(): if current_date >= term_date: current_term = term return current_term if 'current_term' in locals() else "절기 정보 없음" def analyze_elements(self, year_ganjhi: str, month_ganjhi: str, day_ganjhi: str, time_ganjhi: str) -> Dict: """오행 분석""" element_map = { '갑': '목', '을': '목', '병': '화', '정': '화', '무': '토', '기': '토', '경': '금', '신': '금', '임': '수', '계': '수', '인': '목', '묘': '목', '진': '토', '사': '화', '오': '화', '미': '토', '신': '금', '유': '금', '술': '토', '해': '수', '자': '수', '축': '토' } all_chars = year_ganjhi + month_ganjhi + day_ganjhi + time_ganjhi elements = [element_map[char] for char in all_chars] element_count = {} for element in elements: element_count[element] = element_count.get(element, 0) + 1 # 오행 균형 분석 total = sum(element_count.values()) element_percentage = {k: round(v/total*100, 1) for k, v in element_count.items()} return { 'count': element_count, 'percentage': element_percentage, 'dominant': max(element_count, key=element_count.get), 'weak': min(element_count, key=element_count.get) if element_count else None } def get_detailed_saju_analysis(self, year_ganjhi: str, month_ganjhi: str, day_ganjhi: str, time_ganjhi: str) -> Dict: """상세 사주 분석""" # 일간 (주인) 분석 day_cheongan = day_ganjhi[0] day_jiji = day_ganjhi[1] # 십성 분석 (간략화된 버전) sipseong_analysis = self.analyze_sipseong(day_cheongan, year_ganjhi, month_ganjhi, time_ganjhi) return { 'day_master': day_cheongan, 'day_master_element': self.get_element(day_cheongan), 'month_season': self.get_season_from_month(month_ganjhi[1]), 'sipseong': sipseong_analysis, 'strength': self.analyze_day_master_strength(day_cheongan, month_ganjhi, day_jiji, time_ganjhi) } def get_element(self, char: str) -> str: """글자의 오행 반환""" element_map = { '갑': '목', '을': '목', '병': '화', '정': '화', '무': '토', '기': '토', '경': '금', '신': '금', '임': '수', '계': '수', '인': '목', '묘': '목', '진': '토', '사': '화', '오': '화', '미': '토', '신': '금', '유': '금', '술': '토', '해': '수', '자': '수', '축': '토' } return element_map.get(char, '미상') def get_season_from_month(self, month_jiji: str) -> str: """월지지로부터 계절 판단""" season_map = { '인': '봄', '묘': '봄', '진': '봄', '사': '여름', '오': '여름', '미': '여름', '신': '가을', '유': '가을', '술': '가을', '해': '겨울', '자': '겨울', '축': '겨울' } return season_map.get(month_jiji, '미상') def analyze_sipseong(self, day_master: str, year_ganjhi: str, month_ganjhi: str, time_ganjhi: str) -> Dict: """십성 분석 (간략화)""" # 실제로는 매우 복잡한 계산이 필요하지만, 여기서는 기본적인 분석만 day_element = self.get_element(day_master) sipseong_count = { '비견': 0, '겁재': 0, '식신': 0, '상관': 0, '편재': 0, '정재': 0, '편관': 0, '정관': 0, '편인': 0, '정인': 0 } # 간략화된 십성 계산 (실제로는 더 복잡) for ganjhi in [year_ganjhi, month_ganjhi, time_ganjhi]: for char in ganjhi: char_element = self.get_element(char) if char_element == day_element: sipseong_count['비견'] += 1 # 추가 십성 계산은 실제 구현에서 더 복잡하게 처리 return sipseong_count def analyze_day_master_strength(self, day_master: str, month_ganjhi: str, day_jiji: str, time_ganjhi: str) -> str: """일간의 강약 분석""" day_element = self.get_element(day_master) month_element = self.get_element(month_ganjhi[1]) # 매우 간략화된 강약 판단 if day_element == month_element: return "강함" else: return "약함" def get_ai_interpretation(saju_data: Dict) -> str: """OpenAI API를 사용한 사주 해석""" try: detailed = saju_data['detailed_analysis'] elements = saju_data['elements'] prompt = f""" 당신은 30년 경력의 전문 사주명리학자입니다. 다음 사주 정보를 바탕으로 상세하고 전문적인 해석을 제공해주세요. 📋 기본 정보: - 이름: {saju_data['name']} - 성별: {saju_data['birth_info']['gender']} - 양력: {saju_data['birth_info']['solar']} - 음력: {saju_data['birth_info']['lunar']} - 출생지: {saju_data['birth_info']['location']} 🎯 사주명식: - 년주: {saju_data['saju']['year']} - 월주: {saju_data['saju']['month']} - 일주: {saju_data['saju']['day']} (일간: {detailed['day_master']}) - 시주: {saju_data['saju']['time']} 🌟 오행 분석: - 오행 개수: {elements['count']} - 우세 오행: {elements['dominant']} - 약한 오행: {elements.get('weak', '없음')} - 일간 오행: {detailed['day_master_element']} - 월령 계절: {detailed['month_season']} - 일간 강약: {detailed['strength']} 다음 항목들을 포함하여 한국어로 전문적이면서도 이해하기 쉽게 해석해주세요: 1. **성격 및 기질 분석** (일간과 오행 분석 기반) 2. **인생 운세의 전반적인 흐름** (사주 전체 구조 분석) 3. **직업 및 진로 방향** (십성과 오행 특성 반영) 4. **인간관계 및 결혼운** (사주의 인성, 관성 분석) 5. **건강 운세** (오행 균형과 계절 특성) 6. **재물운** (재성과 식상 분석) 7. **현재 시기의 운세 조언** (계절과 오행 조화) 각 항목마다 구체적인 근거를 제시하고, 실용적인 조언을 포함해주세요. """ response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "당신은 한국 전통 사주명리학의 대가입니다."}, {"role": "user", "content": prompt} ], max_tokens=2500, temperature=0.7 ) return response.choices[0].message.content except Exception as e: return f"AI 해석 생성 중 오류가 발생했습니다: {str(e)}\n\n기본 해석을 제공합니다:\n\n일간이 {saju_data['detailed_analysis']['day_master']}({saju_data['detailed_analysis']['day_master_element']})이고, {saju_data['detailed_analysis']['month_season']} 계절에 태어나신 분입니다. 오행 중 {saju_data['elements']['dominant']}이 강하게 나타나는 사주입니다." def create_saju_table(saju_data: Dict) -> str: """사주 정보를 표 형태로 생성""" saju = saju_data['saju'] birth_info = saju_data['birth_info'] elements = saju_data['elements'] detailed = saju_data['detailed_analysis'] table_html = f"""

🔮 {saju_data['name']}님의 정밀 사주명리 분석서

📅 출생 정보

🌞 양력: {birth_info['solar']}

🌙 음력: {birth_info['lunar']}

👤 성별: {birth_info['gender']}

📍 출생지: {birth_info['location']}

🌿 절기: {birth_info.get('solar_term_info', '정보없음')} 시기 출생

🎯 사주명식 (간지배치)

년주 (祖上) 월주 (父母) 일주 (本人) 시주 (子女)
{saju['year']} {saju['month']} {saju['day']} {saju['time']}

일간: {detailed['day_master']} ({detailed['day_master_element']}) | 계절: {detailed['month_season']} | 강약: {detailed['strength']}

🌟 오행 분석

""" for element, count in elements['count'].items(): percentage = elements['percentage'].get(element, 0) color_map = {'목': '#00b894', '화': '#e17055', '토': '#fdcb6e', '금': '#74b9ff', '수': '#6c5ce7'} color = color_map.get(element, '#ddd') table_html += f"""
{element}
{count}개
{percentage}%
""" table_html += f"""

🔥 우세 오행: {elements['dominant']}

💧 부족 오행: {elements.get('weak', '균형적')}

⚡ 사주 구조 분석

🎭 십성 구조

비견: {detailed['sipseong']['비견']}개

식신: {detailed['sipseong']['식신']}개

재성: {detailed['sipseong']['편재'] + detailed['sipseong']['정재']}개

🌊 오행 균형도

목: {elements['percentage'].get('목', 0)}%

화: {elements['percentage'].get('화', 0)}%

토: {elements['percentage'].get('토', 0)}%

금: {elements['percentage'].get('금', 0)}%

수: {elements['percentage'].get('수', 0)}%

""" return table_html def process_saju(name: str, birth_year: int, birth_month: int, birth_day: int, birth_hour: int, birth_minute: int, gender: str, location: str, calendar_type: str, is_intercalation: bool = False) -> Tuple[str, str]: """사주 처리 메인 함수""" try: if not name.strip(): return "이름을 입력해주세요.", "" if birth_year < 1000 or birth_year > 2050: return "지원하는 연도 범위는 1000년~2050년입니다.", "" if not (1 <= birth_month <= 12): return "올바른 월을 입력해주세요 (1-12).", "" if not (1 <= birth_day <= 31): return "올바른 일을 입력해주세요 (1-31).", "" if not (0 <= birth_hour <= 23): return "올바른 시간을 입력해주세요 (0-23).", "" calculator = SajuCalculator() is_lunar = (calendar_type == "음력") saju_data = calculator.calculate_saju( name, birth_year, birth_month, birth_day, birth_hour, birth_minute, gender, location, is_lunar, is_intercalation ) # 사주 표 생성 saju_table = create_saju_table(saju_data) # AI 해석 생성 ai_interpretation = get_ai_interpretation(saju_data) return saju_table, ai_interpretation except Exception as e: error_msg = f"❌ 오류가 발생했습니다: {str(e)}" return error_msg, error_msg # Gradio 인터페이스 생성 def create_interface(): with gr.Blocks( title="🔮 한국 전통 사주명리학 시스템", theme=gr.themes.Soft(), css=""" .gradio-container { font-family: "Noto Sans KR", Arial, sans-serif !important; } .main-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 15px; text-align: center; margin-bottom: 30px; } """ ) as interface: gr.HTML("""

🔮 한국 전통 사주명리학 시스템

한국천문연구원 기준 정밀 음양력 변환 | AI 기반 전문가 해석

""") with gr.Row(): with gr.Column(scale=1): with gr.Group(): gr.Markdown("### 📝 개인정보") name = gr.Textbox( label="이름", placeholder="홍길동", info="정확한 이름을 입력해주세요" ) with gr.Row(): gender = gr.Radio( choices=["남자", "여자"], label="성별", value="남자" ) location = gr.Textbox( label="출생지", placeholder="서울특별시", info="태어난 도시를 입력해주세요" ) with gr.Group(): gr.Markdown("### 📅 생년월일시") calendar_type = gr.Radio( choices=["양력", "음력"], label="달력 구분", value="양력", info="양력/음력을 선택해주세요" ) with gr.Row(): birth_year = gr.Number( label="년도", value=1990, precision=0, info="1000-2050년 지원" ) birth_month = gr.Number( label="월", value=1, precision=0, minimum=1, maximum=12 ) birth_day = gr.Number( label="일", value=1, precision=0, minimum=1, maximum=31 ) with gr.Row(): birth_hour = gr.Number( label="시 (24시간제)", value=0, precision=0, minimum=0, maximum=23, info="정확한 출생시간이 중요합니다" ) birth_minute = gr.Number( label="분", value=0, precision=0, minimum=0, maximum=59 ) is_intercalation = gr.Checkbox( label="윤달 여부 (음력인 경우만)", value=False, info="음력인 경우 윤달인지 확인해주세요", visible=False ) submit_btn = gr.Button( "🔮 정밀 사주 분석 시작", variant="primary", size="lg", elem_classes="submit-button" ) gr.Markdown(""" --- ### 💡 사용 팁 - **정확한 출생시간**: 2시간 차이로 시주가 바뀝니다 - **음력 주의**: 윤달 여부를 정확히 확인해주세요 - **절기 기준**: 명리학은 24절기로 월을 구분합니다 """) with gr.Column(scale=2): with gr.Tab("📊 사주명식"): saju_output = gr.HTML( label="사주 분석 결과", elem_classes="saju-result" ) with gr.Tab("🎯 AI 전문가 해석"): interpretation_output = gr.Textbox( label="상세 운세 해석", lines=25, placeholder="사주 해석 결과가 여기에 표시됩니다...", show_copy_button=True, elem_classes="interpretation-result" ) # 이벤트 처리 def toggle_intercalation(calendar_type): return gr.update(visible=(calendar_type == "음력")) calendar_type.change( fn=toggle_intercalation, inputs=[calendar_type], outputs=[is_intercalation] ) submit_btn.click( fn=process_saju, inputs=[name, birth_year, birth_month, birth_day, birth_hour, birth_minute, gender, location, calendar_type, is_intercalation], outputs=[saju_output, interpretation_output] ) gr.HTML("""

📌 시스템 특징

🎯
한국천문연구원 기준
정확한 음양력 변환
🤖
AI 전문가 해석
GPT-4 기반 상세 분석
🌿
24절기 정확 반영
전통 명리학 방식
📊
상세 오행 분석
십성과 강약 판단

💡 사주는 참고용으로만 활용하시고, 중요한 결정은 신중히 하시기 바랍니다.

""") return interface # 메인 실행 if __name__ == "__main__": interface = create_interface() interface.launch( server_name="0.0.0.0", server_port=7860, share=True, show_error=True )