import sys # 1. 최상단 몽키 패치 - 모든 임포트보다 최우선 import huggingface_hub if not hasattr(huggingface_hub, "HfFolder"): class MockHfFolder: @staticmethod def get_token(): return None @staticmethod def save_token(token): pass @staticmethod def delete_token(): pass huggingface_hub.HfFolder = MockHfFolder import gradio as gr import os import re import zipfile import tempfile import numpy as np import pandas as pd import tensorflow as tf import joblib import traceback from huggingface_hub import hf_hub_download # TF 최적화 경고 방지 및 안정성 os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0' # 무거운 모델 지연 로딩 _models = {"predictor": None, "consultant": None} REPO_ID = "dev-yuje/gardio_test" def load_keras_model_compat(model_path): """quantization_config 역직렬화 오류를 config.json 패치로 우회""" with tempfile.TemporaryDirectory() as tmpdir: with zipfile.ZipFile(model_path, 'r') as z: z.extractall(tmpdir) config_path = os.path.join(tmpdir, 'config.json') with open(config_path, 'r', encoding='utf-8') as f: config_str = f.read() config_str = re.sub(r',\s*"quantization_config":\s*null', '', config_str) config_str = re.sub(r'"quantization_config":\s*null,?\s*', '', config_str) with open(config_path, 'w', encoding='utf-8') as f: f.write(config_str) fixed_path = model_path + '.tmp_fixed.keras' with zipfile.ZipFile(fixed_path, 'w', zipfile.ZIP_DEFLATED) as z: for root, dirs, files in os.walk(tmpdir): for file in files: fp = os.path.join(root, file) arcname = os.path.relpath(fp, tmpdir) z.write(fp, arcname) model = tf.keras.models.load_model(fixed_path, compile=False) os.remove(fixed_path) return model def load_all_models(): if _models["predictor"] is None: try: # 내부 예측 로직 (Self-contained) class RobustCreditPredictor: def __init__(self): self.preprocessor_path = "models/preprocessor.pkl" self.model_path = "models/telecom_cb_model.keras" self.preprocessor = None self.model = None self.load_error = "초기화됨" self.load_resources() def load_resources(self): try: # [개선] 로컬 파일 우선 로드 → 없으면 HuggingFace 다운로드 # --- 전처리기 로드 --- try: if os.path.exists(self.preprocessor_path): self.preprocessor = joblib.load(self.preprocessor_path) print(f"✅ 전처리기 로컬 로드 성공: {self.preprocessor_path}") else: cached_preprocessor = hf_hub_download(repo_id=REPO_ID, filename=self.preprocessor_path, repo_type="space") self.preprocessor = joblib.load(cached_preprocessor) print(f"✅ 전처리기 HuggingFace 다운로드 성공") except Exception as prep_e: self.load_error = f"전처리기 로드 실패: {prep_e}" return # --- 모델 로드 --- try: if os.path.exists(self.model_path): target_path = self.model_path print(f"✅ 모델 로컬 경로 사용: {target_path}") else: target_path = hf_hub_download(repo_id=REPO_ID, filename=self.model_path, repo_type="space") print(f"✅ 모델 HuggingFace 다운로드 완료") fsize = os.path.getsize(target_path) if fsize < 1000: self.load_error = f"파일이 너무 작음({fsize}B). LFS 포인터일 가능성 있음." return self.model = load_keras_model_compat(target_path) self.load_error = "성공" print(f"✅ 모델 로드 성공 (파일 크기: {fsize:,}B)") except Exception as model_e: self.load_error = f"모델 로드 실패: {str(model_e)[:300]}" except Exception as e: self.load_error = f"리소스 로드 통합 에러: {e}" def predict(self, features_dict): try: if self.model is None or self.preprocessor is None: err_short = self.load_error[:500] if self.load_error else "원인 불명" return f"Error: 로드 상태 확인 요망. 원본 에러: {err_short}" ALL_FEATURES = [ 'C1Z001386', 'C1M210000', 'C18210000', 'C1L120001', 'C1L120004', 'L10210000', 'L90210100', 'L90210200', 'L10210B00', 'L10216000', 'L10217000', 'D10110000', 'D10133000', 'PERF1' ] input_values = [float(features_dict.get(col, 0.0)) for col in ALL_FEATURES] df = pd.DataFrame([input_values], columns=ALL_FEATURES) log_cols = ['C1Z001386', 'C1L120004', 'D10110000', 'D10133000', 'L90210200', 'L10216000', 'L10210B00', 'L10217000', 'L90210100', 'L10210000'] df[log_cols] = np.log1p(df[log_cols].astype(float).clip(lower=0)) scaled_data = self.preprocessor.transform(df) prediction = self.model.predict(scaled_data, verbose=0) return float(prediction[0][0]) except Exception as e: return f"Error: 예측 연산 에러: {str(e)}" _models["predictor"] = RobustCreditPredictor() # 상담사 로직 from langchain_google_genai import ChatGoogleGenerativeAI class Consultant: def __init__(self): api_key = os.getenv("GOOGLE_API_KEY", "") from config import LLM_MODEL # [수정] 모델 명칭 및 API 버전 호환성 고려 (config 연동) self.llm = ChatGoogleGenerativeAI( model=LLM_MODEL, google_api_key=api_key, temperature=0.7, convert_system_message_to_human=True ) self.embedding_model = None self.retriever = None def lazy_load_search(self): if self.embedding_model is None: try: from langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS from config import EMBEDDING_MODEL, FAISS_PATH, RETRIEVER_K self.embedding_model = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL) if os.path.exists(FAISS_PATH): self.vectorstore = FAISS.load_local(FAISS_PATH, self.embedding_model, allow_dangerous_deserialization=True) self.retriever = self.vectorstore.as_retriever(search_kwargs={"k": RETRIEVER_K}) except: pass _models["consultant"] = Consultant() except Exception as e: print(f"Grand Load Error: {e}") FEATURES_DETAIL = { 'C1Z001386': ('1년 내 카드 총 이용금액', '만원 단위', '0'), 'C1M210000': ('보유 신용카드 수', '개수', '0'), 'C18210000': ('보유 체크카드 수', '개수', '0'), 'C1L120001': ('카드 총 한도금액', '만원 단위', '0'), 'C1L120004': ('신용카드 개설 후 경과일수', '일 단위', '0'), 'L90210100': ('은행업종 대출 건수', '개수', '0'), 'L90210200': ('카드업종 대출 건수', '개수', '0'), 'L10210B00': ('보험업종 대출 건수', '개수', '0'), 'L10216000': ('신용대출 건수', '개수', '0'), 'L10217000': ('담보대출 건수', '개수', '0'), 'D10110000': ('과거 연체 건수', '개수', '0'), 'D10133000': ('총 연체 상환 금액', '만원 단위', '0'), 'PERF1': ('1년 내 90일 이상 연체 경험', '체크 시 연체 경험 있음', None), } ALL_KEYS = list(FEATURES_DETAIL.keys()) def get_grade_info(score_str): """점수 → (등급, 등급 구분, 의미/특징, 색상) 반환""" try: score = int(score_str) except: return None if score >= 942: grade, label, desc, color = "1등급", "최우량등급", "오랜 신용거래 경력과 다양하고 우량한 신용거래 실적을 보유하고 있어 부실화 가능성이 매우 낙음", "#A8D8EA" elif score >= 891: grade, label, desc, color = "2등급", "최우량등급", "오랜 신용거래 경력과 다양하고 우량한 신용거래 실적을 보유하고 있어 부실화 가능성이 매우 낙음", "#A8D8EA" elif score >= 832: grade, label, desc, color = "3등급", "우량등급", "활발한 신용거래 실적은 없으나, 꼸준히 우량 거래를 지속한다면 상위등급 진입 가능하며 부실화 가능성은 낙은 수준임", "#B8F0C8" elif score >= 768: grade, label, desc, color = "4등급", "우량등급", "활발한 신용거래 실적은 없으나, 꼸준히 우량 거래를 지속한다면 상위등급 진입 가능하며 부실화 가능성은 낙은 수준임", "#B8F0C8" elif score >= 698: grade, label, desc, color = "5등급", "일반등급", "비교적 금리가 높은 금융업권과의 거래가 있는 고객으로, 단기연체 경험이 있으며 부실화 가능성은 일반적인 수준임", "#FEE8A0" elif score >= 620: grade, label, desc, color = "6등급", "일반등급", "비교적 금리가 높은 금융업권과의 거래가 있는 고객으로, 단기연체 경험이 있으며 부실화 가능성은 일반적인 수준임", "#FEE8A0" elif score >= 530: grade, label, desc, color = "7등급", "주의등급", "비교적 금리가 높은 금융업권과의 거래가 많은 고객으로, 단기연체 경험을 비교적 많이 보유하고 있어 부실화 가능성이 높음", "#FFCB9A" elif score >= 454: grade, label, desc, color = "8등급", "주의등급", "비교적 금리가 높은 금융업권과의 거래가 많은 고객으로, 단기연체 경험을 비교적 많이 보유하고 있어 부실화 가능성이 높음", "#FFCB9A" elif score >= 335: grade, label, desc, color = "9등급", "위험등급", "현재 연체 중이거나 매우 심각한 연체 경험을 보유하고 있어 부실화 가능성이 매우 높음", "#FFB3B3" else: grade, label, desc, color = "10등급", "위험등급", "현재 연체 중이거나 매우 심각한 연체 경험을 보유하고 있어 부실화 가능성이 매우 높음", "#FFB3B3" return grade, label, desc, color SCORE_PLACEHOLDER = '''