developer-lunark/kaidol-thinking-experiment
0
1#!/usr/bin/env python32"""KAIdol A/B Test Arena - GPU Version with Real Model Inference"""3 4import gradio as gr5import random6import json7import uuid8import re9import gc10import os11from datetime import datetime12from functools import lru_cache13 14# GPU 추론 관련 (선택적 임포트)15TORCH_AVAILABLE = False16IMPORT_ERROR = None17torch = None18try:19 import torch as _torch20 torch = _torch21 from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig22 from peft import PeftModel23 TORCH_AVAILABLE = True24 25 # Debug info26 print("=" * 50)27 print(f"PyTorch version: {torch.__version__}")28 print(f"CUDA available: {torch.cuda.is_available()}")29 if torch.cuda.is_available():30 print(f"CUDA version: {torch.version.cuda}")31 print(f"GPU count: {torch.cuda.device_count()}")32 print(f"GPU name: {torch.cuda.get_device_name(0)}")33 else:34 print("CUDA not available at module load time")35 print("=" * 50)36 37except Exception as e:38 import traceback39 IMPORT_ERROR = f"{type(e).__name__}: {str(e)}"40 print(f"Warning: Import error - {IMPORT_ERROR}")41 traceback.print_exc()42 print("Running in mock mode")43 44def is_gpu_available():45 """Check GPU availability dynamically"""46 if not TORCH_AVAILABLE:47 return False48 return torch.cuda.is_available()49 50# For backwards compatibility51GPU_AVAILABLE = is_gpu_available()52 53# ============================================================54# 모델 레지스트리 (HF Hub 경로)55# ============================================================56 57MODELS = {58 # DPO v5 (7-14B)59 "qwen2.5-7b-dpo-v5": {60 "hf_repo": "developer-lunark/kaidol-qwen2.5-7b-dpo-v5",61 "base_model": "Qwen/Qwen2.5-7B-Instruct",62 "size": "7B", "method": "DPO", "desc": "Qwen2.5 7B DPO v5"63 },64 "qwen2.5-14b-dpo-v5": {65 "hf_repo": "developer-lunark/kaidol-qwen2.5-14b-dpo-v5",66 "base_model": "Qwen/Qwen2.5-14B-Instruct",67 "size": "14B", "method": "DPO", "desc": "Qwen2.5 14B DPO v5"68 },69 "exaone-7.8b-dpo-v5": {70 "hf_repo": "developer-lunark/kaidol-exaone-7.8b-dpo-v5",71 "base_model": "LGAI-EXAONE/EXAONE-3.5-7.8B-Instruct",72 "size": "7.8B", "method": "DPO", "desc": "EXAONE 7.8B DPO v5"73 },74 "qwen3-8b-dpo-v5": {75 "hf_repo": "developer-lunark/kaidol-qwen3-8b-dpo-v5",76 "base_model": "Qwen/Qwen3-8B",77 "size": "8B", "method": "DPO", "desc": "Qwen3 8B DPO v5"78 },79 "solar-10.7b-dpo-v5": {80 "hf_repo": "developer-lunark/kaidol-solar-10.7b-dpo-v5",81 "base_model": "upstage/SOLAR-10.7B-Instruct-v1.0", # Fixed: match adapter training82 "size": "10.7B", "method": "DPO", "desc": "Solar 10.7B DPO v5"83 },84 85 # V7 Students (7-14B)86 "qwen2.5-7b-v7": {87 "hf_repo": "developer-lunark/kaidol-qwen2.5-7b-v7",88 "base_model": "Qwen/Qwen2.5-7B-Instruct",89 "size": "7B", "method": "SFT", "desc": "Qwen2.5 7B V7"90 },91 "qwen2.5-14b-v7": {92 "hf_repo": "developer-lunark/kaidol-qwen2.5-14b-v7",93 "base_model": "Qwen/Qwen2.5-14B-Instruct",94 "size": "14B", "method": "SFT", "desc": "Qwen2.5 14B V7"95 },96 "exaone-7.8b-v7": {97 "hf_repo": "developer-lunark/kaidol-exaone-7.8b-v7",98 "base_model": "LGAI-EXAONE/EXAONE-3.5-7.8B-Instruct",99 "size": "7.8B", "method": "SFT", "desc": "EXAONE 7.8B V7"100 },101 "qwen3-8b-v7": {102 "hf_repo": "developer-lunark/kaidol-qwen3-8b-v7",103 "base_model": "Qwen/Qwen3-8B",104 "size": "8B", "method": "SFT", "desc": "Qwen3 8B V7"105 },106 "varco-8b-v7": {107 "hf_repo": "developer-lunark/kaidol-varco-8b-v7",108 "base_model": "NCSOFT/Llama-VARCO-8B-Instruct",109 "size": "8B", "method": "SFT", "desc": "VARCO 8B V7"110 },111 112 # Phase 7 Kimi Students113 "exaone-7.8b-kimi": {114 "hf_repo": "developer-lunark/kaidol-exaone-7.8b-kimi",115 "base_model": "LGAI-EXAONE/EXAONE-3.5-7.8B-Instruct",116 "size": "7.8B", "method": "Distill", "desc": "EXAONE 7.8B Kimi"117 },118}119 120# 캐릭터 정보121CHARACTERS = {122 "강율": {123 "mbti": "ENTJ", "role": "리더", "age": 23,124 "traits": "낙천적, 장난기 많음, 애교",125 "speech": "반말, 귀여운 말투, 장난스러운 표현",126 "patterns": ["~해", "~지", "히히", "ㅋㅋ"],127 "ratio": "30:70", "warmth": "high"128 },129 "서이안": {130 "mbti": "INFP", "role": "보컬", "age": 22,131 "traits": "차분함, 신비로움, 배려심",132 "speech": "존댓말 혼용, 따뜻한 말투, 조용한 표현",133 "patterns": ["...요", "네요", "...", "그래요"],134 "ratio": "20:80", "warmth": "very_high"135 },136 "이지후": {137 "mbti": "ISFJ", "role": "막내", "age": 21,138 "traits": "츤데레, 자존심 강함, 은근히 챙김",139 "speech": "반말, 퉁명스러운 말투, 부정하는 말투",140 "patterns": ["뭐야", "아니거든", "...", "그냥", "별로"],141 "ratio": "30:70", "warmth": "medium"142 },143 "차도하": {144 "mbti": "INTP", "role": "프로듀서", "age": 24,145 "traits": "카리스마, 리더십, 다정함, 담백함",146 "speech": "반말, 간결한 말투, 담백한 표현",147 "patterns": ["하자", "해볼까", "같이", "괜찮아"],148 "ratio": "50:50", "warmth": "medium"149 },150 "최민": {151 "mbti": "ESFP", "role": "댄서", "age": 22,152 "traits": "적극적, 솔직, 열정적",153 "speech": "반말, 적극적인 말투, 솔직한 표현",154 "patterns": ["할래", "좋아", "진짜", "대박", "헐"],155 "ratio": "60:40", "warmth": "medium"156 },157}158 159# 시나리오 목록160SCENARIOS = [161 {"id": "fm_01", "cat": "첫 만남", "text": "{char}아! 드디어 만났다... 정말 좋아해!"},162 {"id": "dc_01", "cat": "일상 대화", "text": "{char}아 오늘 뭐해? 밥은 먹었어?"},163 {"id": "es_01", "cat": "감정 지원", "text": "오늘 진짜 힘들었어... 학교에서 발표도 망치고..."},164 {"id": "cf_01", "cat": "고백", "text": "{char}아... 나 진심으로 좋아해."},165 {"id": "pl_01", "cat": "장난", "text": "사실 나 다른 멤버가 더 좋아~ ㅋㅋ 농담이야!"},166 {"id": "sr_01", "cat": "특별 요청", "text": "오늘만 내 연인이라고 생각해줄래?"},167 {"id": "cn_01", "cat": "갈등", "text": "{char}는 다른 팬들한테도 이렇게 잘해줘...? 뭔가 질투나..."},168 {"id": "ec_01", "cat": "감정 위기", "text": "오늘 진짜 많이 울었어... 삶이 너무 힘들다."},169]170 171# ============================================================172# 모델 관리173# ============================================================174 175class ModelManager:176 def __init__(self):177 self.current_model = None178 self.current_model_name = None179 self.tokenizer = None180 self.last_error = None181 182 def load_model(self, model_name: str):183 """Load model with 4-bit quantization and LoRA adapter"""184 if not is_gpu_available():185 self.last_error = f"GPU not available (TORCH_AVAILABLE={TORCH_AVAILABLE}, cuda={torch.cuda.is_available() if TORCH_AVAILABLE else 'N/A'})"186 return False187 188 if self.current_model_name == model_name:189 return True # Already loaded190 191 # Unload current model192 self.unload_model()193 194 model_info = MODELS.get(model_name)195 if not model_info:196 self.last_error = f"Model {model_name} not found in registry"197 return False198 199 try:200 print(f"Loading {model_name}...")201 print(f" Base model: {model_info['base_model']}")202 print(f" LoRA adapter: {model_info['hf_repo']}")203 204 # 4-bit quantization config205 bnb_config = BitsAndBytesConfig(206 load_in_4bit=True,207 bnb_4bit_compute_dtype=torch.bfloat16,208 bnb_4bit_use_double_quant=True,209 bnb_4bit_quant_type="nf4",210 )211 212 # Load base model213 print(" Loading base model...")214 base_model = AutoModelForCausalLM.from_pretrained(215 model_info["base_model"],216 quantization_config=bnb_config,217 device_map="auto",218 trust_remote_code=True,219 )220 print(" Base model loaded!")221 222 # Load LoRA adapter223 print(" Loading LoRA adapter...")224 self.current_model = PeftModel.from_pretrained(225 base_model,226 model_info["hf_repo"],227 trust_remote_code=True,228 )229 self.current_model.eval()230 print(" LoRA adapter loaded!")231 232 # Load tokenizer233 print(" Loading tokenizer...")234 self.tokenizer = AutoTokenizer.from_pretrained(235 model_info["base_model"],236 trust_remote_code=True,237 )238 if self.tokenizer.pad_token is None:239 self.tokenizer.pad_token = self.tokenizer.eos_token240 print(" Tokenizer loaded!")241 242 self.current_model_name = model_name243 self.last_error = None244 print(f"Loaded {model_name} successfully!")245 return True246 247 except Exception as e:248 import traceback249 error_msg = f"{type(e).__name__}: {str(e)}"250 print(f"Error loading {model_name}: {error_msg}")251 traceback.print_exc()252 self.last_error = error_msg253 self.unload_model()254 return False255 256 def unload_model(self):257 """Unload current model to free memory"""258 if self.current_model is not None:259 del self.current_model260 self.current_model = None261 if self.tokenizer is not None:262 del self.tokenizer263 self.tokenizer = None264 self.current_model_name = None265 gc.collect()266 if GPU_AVAILABLE:267 torch.cuda.empty_cache()268 269 def generate(self, model_name: str, messages: list, max_new_tokens: int = 512) -> str:270 """Generate response from model"""271 if not self.load_model(model_name):272 return self._mock_response(model_name)273 274 try:275 # Apply chat template276 text = self.tokenizer.apply_chat_template(277 messages,278 tokenize=False,279 add_generation_prompt=True,280 )281 282 inputs = self.tokenizer(text, return_tensors="pt").to(self.current_model.device)283 284 with torch.no_grad():285 outputs = self.current_model.generate(286 **inputs,287 max_new_tokens=max_new_tokens,288 do_sample=True,289 temperature=0.7,290 top_p=0.9,291 pad_token_id=self.tokenizer.pad_token_id,292 )293 294 response = self.tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)295 return response.strip()296 297 except Exception as e:298 print(f"Generation error: {e}")299 return self._mock_response(model_name)300 301 def _mock_response(self, model_name: str) -> str:302 """Fallback mock response with error info"""303 error_info = f"\nError: {self.last_error}" if self.last_error else ""304 return f"<think>\n[Mock Mode] 모델 로딩 실패{error_info}\n</think>\n\n안녕~ 반가워!"305 306# Global model manager307model_manager = ModelManager()308 309# ============================================================310# 베이스 모델 사전 캐싱 (콜드 스타트 방지)311# ============================================================312 313def preload_base_models():314 """Pre-download base models to avoid cold start timeout"""315 if not TORCH_AVAILABLE:316 print("Skipping preload: PyTorch not available")317 return318 319 from huggingface_hub import snapshot_download320 import os321 322 # Models that need pre-caching (large or slow to download)323 models_to_cache = [324 "NCSOFT/Llama-VARCO-8B-Instruct", # VARCO - 16GB, often times out on first load325 ]326 327 print("=" * 50)328 print("Pre-downloading base models (this may take a while)...")329 print("=" * 50)330 331 for model_id in models_to_cache:332 try:333 print(f" Downloading: {model_id}")334 # Download all model files to HF cache335 cache_dir = snapshot_download(336 repo_id=model_id,337 ignore_patterns=["*.md", "*.txt"], # Skip docs338 )339 print(f" ✓ Downloaded to: {cache_dir}")340 341 except Exception as e:342 print(f" ✗ Failed to download {model_id}: {e}")343 344 print("Pre-download complete!")345 print("=" * 50)346 347# Run preload at startup348preload_base_models()349 350# ============================================================351# 시스템 프롬프트 생성352# ============================================================353 354def build_system_prompt(character: str) -> str:355 """Build system prompt for character"""356 char_info = CHARACTERS.get(character, {})357 358 prompt = f"""당신은 아이돌 '{character}'입니다.359 360## 캐릭터361- 이름: {character}362- MBTI: {char_info.get('mbti', 'UNKNOWN')}363- 성격: {char_info.get('traits', '')}364- 역할: {char_info.get('role', '')}365- 나이: {char_info.get('age', 20)}세366 367## 말투368- 스타일: {char_info.get('speech', '')}369- 자주 쓰는 표현: {', '.join(char_info.get('patterns', []))}370 371## 밀당 가이드372- 밀:당 비율: {char_info.get('ratio', '50:50')}373- 다정도: {char_info.get('warmth', 'medium')}374 375## 규칙3761. 캐릭터 성격과 말투 일관성 유지3772. 자연스러운 대화체 사용3783. 너무 쉽게 호감 표현 금지 (밀당 유지)3794. 상대방을 특별하게 느끼게 하되, "썸" 관계 유지380 381## 응답 형식382응답 전에 <think> 태그 안에 {character}의 1인칭 내면 독백을 작성하세요.383- 자연스러운 혼잣말 형식384- 캐릭터 성격 반영385- 상대방에 대한 감정/생각 표현386 387예시:388<think>389뭐야... 또 좋아한다고? 솔직히 기분 나쁘진 않은데... 근데 뭐라고 해야 하지?390</think>391"""392 return prompt393 394# ============================================================395# 투표/ELO 시스템396# ============================================================397 398VOTES_FILE = "votes.jsonl"399ELO_FILE = "elo_ratings.json"400 401def load_elo():402 try:403 with open(ELO_FILE, "r") as f:404 return json.load(f)405 except:406 return {m: 1500 for m in MODELS}407 408def save_elo(elo):409 with open(ELO_FILE, "w") as f:410 json.dump(elo, f, indent=2)411 412def update_elo(elo, model_a, model_b, result):413 K = 32414 ra, rb = elo.get(model_a, 1500), elo.get(model_b, 1500)415 ea = 1 / (1 + 10 ** ((rb - ra) / 400))416 eb = 1 / (1 + 10 ** ((ra - rb) / 400))417 418 if result == "a":419 sa, sb = 1, 0420 elif result == "b":421 sa, sb = 0, 1422 else:423 sa, sb = 0.5, 0.5424 425 elo[model_a] = ra + K * (sa - ea)426 elo[model_b] = rb + K * (sb - eb)427 save_elo(elo)428 return elo[model_a], elo[model_b]429 430def save_vote(data):431 vote = {"id": str(uuid.uuid4())[:8], "timestamp": datetime.now().isoformat(), **data}432 with open(VOTES_FILE, "a") as f:433 f.write(json.dumps(vote, ensure_ascii=False) + "\n")434 return vote["id"]435 436def load_votes():437 try:438 with open(VOTES_FILE, "r") as f:439 return [json.loads(line) for line in f if line.strip()]440 except:441 return []442 443def get_leaderboard():444 elo = load_elo()445 votes = load_votes()446 447 stats = {}448 for v in votes:449 ma, mb, res = v.get("model_a"), v.get("model_b"), v.get("vote")450 if not ma or not mb or res == "skip":451 continue452 for m in [ma, mb]:453 if m not in stats:454 stats[m] = {"wins": 0, "losses": 0, "ties": 0}455 if res == "a":456 stats[ma]["wins"] += 1457 stats[mb]["losses"] += 1458 elif res == "b":459 stats[mb]["wins"] += 1460 stats[ma]["losses"] += 1461 else:462 stats[ma]["ties"] += 1463 stats[mb]["ties"] += 1464 465 rows = []466 for i, (m, e) in enumerate(sorted(elo.items(), key=lambda x: -x[1]), 1):467 s = stats.get(m, {"wins": 0, "losses": 0, "ties": 0})468 total = s["wins"] + s["losses"] + s["ties"]469 wr = f"{s['wins']/total*100:.1f}%" if total > 0 else "-"470 info = MODELS.get(m, {})471 rows.append([i, info.get("desc", m), info.get("size", "?"), int(e), s["wins"], s["losses"], s["ties"], wr])472 473 return rows474 475# ============================================================476# UI 핸들러477# ============================================================478 479model_list = [(f"[{v['size']}] {v['desc']}", k) for k, v in MODELS.items()]480char_list = list(CHARACTERS.keys())481scenario_list = [(f"[{s['cat']}] {s['text'][:30]}...", s['id']) for s in SCENARIOS]482 483current_state = {"model_a": None, "model_b": None, "resp_a": None, "resp_b": None, "char": None, "input": None}484 485def random_models():486 selected = random.sample(list(MODELS.keys()), 2)487 return selected[0], selected[1]488 489def load_scenario(scenario_id, character):490 s = next((x for x in SCENARIOS if x["id"] == scenario_id), None)491 if s:492 return s["text"].replace("{char}", character)493 return ""494 495def random_scenario(character):496 s = random.choice(SCENARIOS)497 return s["text"].replace("{char}", character), s["id"]498 499def parse_response(response: str):500 """Parse response to separate thinking and content"""501 think_match = re.search(r'<think>(.*?)</think>', response, re.DOTALL)502 if think_match:503 thinking = think_match.group(1).strip()504 content = re.sub(r'<think>.*?</think>', '', response, flags=re.DOTALL).strip()505 return thinking, content506 return "", response507 508def generate(model_a, model_b, character, user_msg, progress=gr.Progress()):509 if not user_msg.strip():510 return "메시지를 입력해주세요", "", "", "메시지를 입력해주세요", "", ""511 512 system_prompt = build_system_prompt(character)513 messages = [514 {"role": "system", "content": system_prompt},515 {"role": "user", "content": user_msg},516 ]517 518 # Generate from Model A519 progress(0.2, desc=f"Model A ({model_a}) 생성 중...")520 resp_a = model_manager.generate(model_a, messages)521 think_a, clean_a = parse_response(resp_a)522 523 # Generate from Model B524 progress(0.6, desc=f"Model B ({model_b}) 생성 중...")525 resp_b = model_manager.generate(model_b, messages)526 think_b, clean_b = parse_response(resp_b)527 528 # Update state529 current_state.update({530 "model_a": model_a, "model_b": model_b,531 "resp_a": resp_a, "resp_b": resp_b,532 "char": character, "input": user_msg533 })534 535 mode = "GPU" if GPU_AVAILABLE else "Mock"536 537 return (538 think_a or "(없음)", clean_a, f"{mode} | {MODELS[model_a]['size']}",539 think_b or "(없음)", clean_b, f"{mode} | {MODELS[model_b]['size']}"540 )541 542def vote(vote_type, reason):543 if not current_state["model_a"]:544 return "먼저 응답을 생성해주세요."545 546 elo = load_elo()547 vid = save_vote({548 "model_a": current_state["model_a"],549 "model_b": current_state["model_b"],550 "character": current_state["char"],551 "user_input": current_state["input"],552 "vote": vote_type,553 "reason": reason,554 })555 556 if vote_type != "skip":557 new_a, new_b = update_elo(elo, current_state["model_a"], current_state["model_b"], vote_type)558 return f"투표 완료! (ID: {vid})\nELO: {current_state['model_a']}={int(new_a)}, {current_state['model_b']}={int(new_b)}"559 return f"스킵됨 (ID: {vid})"560 561def refresh_leaderboard():562 return get_leaderboard()563 564def get_vote_summary():565 votes = load_votes()566 total = len(votes)567 a_wins = sum(1 for v in votes if v.get("vote") == "a")568 b_wins = sum(1 for v in votes if v.get("vote") == "b")569 ties = sum(1 for v in votes if v.get("vote") == "tie")570 return str(total), str(a_wins), str(b_wins), str(ties)571 572# ============================================================573# Gradio UI574# ============================================================575 576with gr.Blocks(title="KAIdol A/B Test Arena", theme=gr.themes.Soft()) as demo:577 gr.Markdown("# KAIdol A/B Test Arena")578 gr.Markdown("K-pop 아이돌 롤플레이 모델 A/B 비교 평가 (소형 Student 모델 11개)")579 580 # GPU 상태 상세 정보581 if IMPORT_ERROR:582 mode_text = f"**Mock 모드**: Import Error - {IMPORT_ERROR}"583 elif TORCH_AVAILABLE and torch is not None:584 torch_ver = torch.__version__585 cuda_avail = torch.cuda.is_available()586 cuda_ver = torch.version.cuda if cuda_avail else "N/A"587 gpu_name = torch.cuda.get_device_name(0) if cuda_avail else "N/A"588 mode_text = f"**GPU 모드**: {gpu_name} (CUDA {cuda_ver}, PyTorch {torch_ver})" if cuda_avail else f"**Mock 모드**: CUDA not available (PyTorch {torch_ver})"589 else:590 mode_text = "**Mock 모드**: PyTorch not loaded"591 gr.Markdown(mode_text)592 593 with gr.Tabs():594 # A/B Arena 탭595 with gr.Tab("A/B Arena"):596 with gr.Row():597 character = gr.Dropdown(choices=char_list, value="강율", label="캐릭터")598 scenario = gr.Dropdown(choices=scenario_list, label="시나리오")599 600 with gr.Row():601 model_a = gr.Dropdown(choices=model_list, value=list(MODELS.keys())[0], label="Model A")602 model_b = gr.Dropdown(choices=model_list, value=list(MODELS.keys())[1], label="Model B")603 random_btn = gr.Button("랜덤", size="sm")604 605 with gr.Row():606 with gr.Column():607 gr.Markdown("### Model A")608 with gr.Accordion("Thinking", open=False):609 think_a = gr.Markdown()610 resp_a = gr.Textbox(label="응답", lines=5)611 meta_a = gr.Markdown()612 613 with gr.Column():614 gr.Markdown("### Model B")615 with gr.Accordion("Thinking", open=False):616 think_b = gr.Markdown()617 resp_b = gr.Textbox(label="응답", lines=5)618 meta_b = gr.Markdown()619 620 user_input = gr.Textbox(label="메시지", placeholder="아이돌에게 메시지를 보내세요...")621 with gr.Row():622 random_scenario_btn = gr.Button("랜덤 시나리오")623 submit_btn = gr.Button("전송", variant="primary")624 625 gr.Markdown("### 투표")626 with gr.Row():627 vote_a = gr.Button("A가 더 좋음")628 vote_tie = gr.Button("비슷함")629 vote_b = gr.Button("B가 더 좋음")630 vote_skip = gr.Button("스킵")631 632 vote_reason = gr.Textbox(label="투표 이유 (선택)", placeholder="...")633 vote_result = gr.Markdown()634 635 # Events636 random_btn.click(random_models, outputs=[model_a, model_b])637 scenario.change(load_scenario, [scenario, character], user_input)638 random_scenario_btn.click(random_scenario, [character], [user_input, scenario])639 submit_btn.click(generate, [model_a, model_b, character, user_input],640 [think_a, resp_a, meta_a, think_b, resp_b, meta_b])641 vote_a.click(lambda r: vote("a", r), [vote_reason], vote_result)642 vote_b.click(lambda r: vote("b", r), [vote_reason], vote_result)643 vote_tie.click(lambda r: vote("tie", r), [vote_reason], vote_result)644 vote_skip.click(lambda r: vote("skip", r), [vote_reason], vote_result)645 646 # Leaderboard 탭647 with gr.Tab("Leaderboard"):648 gr.Markdown("## ELO 리더보드")649 refresh_btn = gr.Button("새로고침")650 leaderboard = gr.Dataframe(651 headers=["순위", "모델", "크기", "ELO", "승", "패", "무", "승률"],652 datatype=["number", "str", "str", "number", "number", "number", "number", "str"],653 )654 655 gr.Markdown("### 투표 요약")656 with gr.Row():657 total_v = gr.Textbox(label="총 투표", interactive=False)658 a_wins_v = gr.Textbox(label="A 승", interactive=False)659 b_wins_v = gr.Textbox(label="B 승", interactive=False)660 ties_v = gr.Textbox(label="무승부", interactive=False)661 662 def refresh():663 lb = refresh_leaderboard()664 summary = get_vote_summary()665 return lb, *summary666 667 refresh_btn.click(refresh, outputs=[leaderboard, total_v, a_wins_v, b_wins_v, ties_v])668 669 # 모델 목록 탭670 with gr.Tab("모델 목록"):671 gr.Markdown("## 테스트 대상 모델")672 gr.Markdown(f"총 {len(MODELS)}개 모델")673 model_table = gr.Dataframe(674 headers=["모델 ID", "크기", "학습 방법", "설명", "Base Model"],675 value=[[k, v["size"], v["method"], v["desc"], v["base_model"]] for k, v in MODELS.items()],676 )677 678if __name__ == "__main__":679 demo.launch(server_name="0.0.0.0", server_port=7860)680 