diff --git a/WeeklyReports/Hackathon_10th/ERNIEPartner/17_megemini/Gradio.app.py b/WeeklyReports/Hackathon_10th/ERNIEPartner/17_megemini/Gradio.app.py new file mode 100644 index 00000000..69cd9f89 --- /dev/null +++ b/WeeklyReports/Hackathon_10th/ERNIEPartner/17_megemini/Gradio.app.py @@ -0,0 +1,803 @@ +import re +import os +import time +import socket +import signal +import random +import json +import subprocess +import threading +import openai +import gradio as gr + +# ========== FastDeploy Server Config ========== + +FD_MODEL = "baidu/ERNIE-4.5-0.3B-Paddle" +FD_HOST = "0.0.0.0" +FD_PORT = 8180 +FD_METRICS_PORT = 8181 +FD_WORKER_QUEUE_PORT = 8182 +FD_MAX_MODEL_LEN = 32768 +FD_MAX_NUM_SEQS = 32 +FD_NUM_GPU_BLOCKS_OVERRIDE = 4896 + +fd_server_process = None +fd_server_log = [] +client = None +active_backend = None # None / "fastdeploy" / "openai" +model_name = "null" # model string passed to the API + +def is_port_in_use(port): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + return s.connect_ex(("127.0.0.1", port)) == 0 + +def kill_process_on_port(port): + try: + result = subprocess.run(["lsof", "-t", "-i", f":{port}"], capture_output=True, text=True) + pids = result.stdout.strip().split('\n') + for pid_str in pids: + pid_str = pid_str.strip() + if pid_str: + pid = int(pid_str) + os.kill(pid, signal.SIGKILL) + time.sleep(1) + except (subprocess.CalledProcessError, FileNotFoundError, ValueError, ProcessLookupError): + try: + subprocess.run(["fuser", "-k", f"{port}/tcp"], capture_output=True) + except FileNotFoundError: + pass + +def read_server_log(): + global fd_server_process, fd_server_log + if fd_server_process is None: + return + for line in iter(fd_server_process.stdout.readline, b""): + decoded = line.decode("utf-8", errors="replace").rstrip() + fd_server_log.append(decoded) + for line in iter(fd_server_process.stderr.readline, b""): + decoded = line.decode("utf-8", errors="replace").rstrip() + fd_server_log.append(decoded) + +def start_fd_server(model, host, port, metrics_port, worker_queue_port, + max_model_len, max_num_seqs, num_gpu_blocks_override): + global fd_server_process, client, fd_server_log, active_backend, model_name + if active_backend == "openai": + disconnect_openai() + if fd_server_process is not None and fd_server_process.poll() is None: + return "Server is already running." + + if is_port_in_use(int(port)): + kill_process_on_port(int(port)) + time.sleep(2) + + os.environ["ENABLE_V1_KVCACHE_SCHEDULER"] = "1" + os.environ["CUDA_VISIBLE_DEVICES"] = "0" + + cmd = [ + "python", "-m", "fastdeploy.entrypoints.openai.api_server", + "--model", model, + "--host", host, + "--port", str(port), + "--metrics-port", str(metrics_port), + "--engine-worker-queue-port", str(worker_queue_port), + "--max-model-len", str(max_model_len), + "--max-num-seqs", str(max_num_seqs), + "--num-gpu-blocks-override", str(num_gpu_blocks_override), + ] + fd_server_process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + fd_server_log = [] + threading.Thread(target=read_server_log, daemon=True).start() + client = openai.Client(base_url=f"http://{host}:{port}/v1", api_key="null") + model_name = "null" + active_backend = "fastdeploy" + return f"Server started (PID: {fd_server_process.pid})" + +def stop_fd_server(): + global fd_server_process, client, active_backend, model_name + if fd_server_process is None or fd_server_process.poll() is not None: + return "Server is not running." + fd_server_process.terminate() + try: + fd_server_process.wait(timeout=10) + except subprocess.TimeoutExpired: + fd_server_process.kill() + fd_server_process.wait() + fd_server_process = None + client = None + active_backend = None + model_name = "null" + return "Server stopped." + +def get_fd_status(): + global fd_server_process + if fd_server_process is None: + return "stopped" + rc = fd_server_process.poll() + if rc is None: + return "running" + return f"exited (code: {rc})" + +def check_fd_health(): + global client + if client is None: + return "Client not initialized." + try: + client.models.list() + return "OK" + except Exception as e: + return f"Unreachable: {e}" + +def get_fd_log(): + global fd_server_log + return "\n".join(fd_server_log[-200:]) + +# ========== OpenAI API Backend ========== + +OA_DEFAULT_BASE_URL = "https://api.openai.com/v1" +OA_DEFAULT_API_KEY = "" +OA_DEFAULT_MODEL = "gpt-4o-mini" + +def connect_openai(base_url, api_key, oa_model): + global client, active_backend, model_name + if active_backend == "fastdeploy": + stop_fd_server() + if not base_url.strip(): + return "Base URL is required." + if not api_key.strip(): + return "API key is required." + try: + client = openai.Client(base_url=base_url.rstrip("/"), api_key=api_key) + client.models.list() + model_name = oa_model.strip() or "gpt-4o-mini" + active_backend = "openai" + return f"Connected to {model_name} @ {base_url}" + except Exception as e: + client = None + active_backend = None + model_name = "null" + return f"Connection failed: {e}" + +def disconnect_openai(): + global client, active_backend, model_name + client = None + active_backend = None + model_name = "null" + return "Disconnected." + +def get_oa_status(): + global active_backend + if active_backend == "openai": + return "connected" + return "disconnected" + +def check_oa_health(): + global client + if client is None or active_backend != "openai": + return "Not connected." + try: + client.models.list() + return "OK" + except Exception as e: + return f"Unreachable: {e}" + +def get_active_backend_label(): + global active_backend + if active_backend == "fastdeploy": + return "FastDeploy (local)" + if active_backend == "openai": + return "OpenAI API (remote)" + return "None" + +# ========== Game Config ========== + +DIFFICULTY_MULTIPLIERS = {1: 1.0, 2: 1.2, 3: 1.5, 4: 2.0, 5: 2.5} +DIFFICULTY_TIME_LIMITS = {1: 30, 2: 25, 3: 20, 4: 18, 5: 15} +DIFFICULTY_SENTENCE_LENGTH = {1: 10, 2: 20, 3: 30, 4: 40, 5: 50} +BASE_SCORE = 10 +TIME_BONUS_FACTOR = 0.5 +STREAK_BONUS = 2 +TIMED_MODE_QUESTIONS = 10 +MAX_RETRIES = 2 + +# ========== Question Generation ========== + +def build_sentence_prompt(difficulty): + target_len = DIFFICULTY_SENTENCE_LENGTH.get(difficulty, 12) + return f"请生成一句约 {target_len} 个词的英文句子。" + +def generate_sentence(difficulty): + prompt = build_sentence_prompt(difficulty) + for attempt in range(MAX_RETRIES + 1): + try: + response = client.chat.completions.create( + model=model_name, + messages=[{"role": "user", "content": prompt}], + temperature=0.8, top_p=0.95, max_tokens=128, stream=False, + ) + text = response.choices[0].message.content.strip() + if text and len(text.split()) >= 4: + return text + except Exception as e: + print(f"generate_sentence: error (attempt {attempt + 1}): {e}") + return "" + +def _is_valid_english_word(w): + if not isinstance(w, str) or not w.strip(): + return False + if re.search(r'[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]', w): + return False + if re.match(r'option\d+$', w, re.IGNORECASE): + return False + if re.match(r'^[a-zA-Z\'-]+$', w) is None: + return False + return True + +def get_synonyms(word): + prompt = f"""请为英文单词 "{word}" 提供 5 个同义词或近义词。以 json 格式返回 +{{ + "synonyms": [ + ] +}}""" + for attempt in range(MAX_RETRIES + 1): + try: + response = client.chat.completions.create( + model=model_name, + messages=[{"role": "user", "content": prompt}], + temperature=0.6, top_p=0.9, max_tokens=128, stream=False, + ) + text = response.choices[0].message.content.strip() + try: + data = json.loads(text) + syns = data.get("synonyms", []) + except json.JSONDecodeError: + match = re.search(r'\{[\s\S]*?\}', text) + if match: + try: + data = json.loads(match.group()) + syns = data.get("synonyms", []) + except json.JSONDecodeError: + syns = [] + else: + syns = [] + syns = [w for w in syns if isinstance(w, str) and w.lower() != word.lower()] + syns = [w for w in syns if _is_valid_english_word(w)] + if len(syns) < 2: + raise ValueError("not enough valid English synonyms") + return random.sample(syns, 2) + except Exception as e: + print(f"get_synonyms('{word}'): error (attempt {attempt + 1}): {e}") + return [] + +def pick_cloze_word(sentence): + skip_words = { + 'a', 'an', 'the', 'is', 'are', 'was', 'were', 'be', 'been', 'being', + 'am', 'do', 'does', 'did', 'have', 'has', 'had', 'having', + 'will', 'would', 'shall', 'should', 'can', 'could', 'may', 'might', 'must', + 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her', 'us', 'them', + 'my', 'your', 'his', 'its', 'our', 'their', 'mine', 'yours', 'hers', 'ours', + 'this', 'that', 'these', 'those', + 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from', 'up', 'down', + 'out', 'off', 'over', 'under', 'about', 'into', 'through', 'after', 'before', + 'between', 'among', 'during', 'until', 'since', + 'and', 'or', 'but', 'not', 'nor', 'so', 'yet', 'if', 'then', + 'as', 'than', 'when', 'while', 'where', 'how', 'what', 'which', 'who', + 'very', 'too', 'also', 'just', 'only', 'even', 'still', 'already', + 'no', 'yes', 'all', 'each', 'every', 'both', 'any', 'some', + 'here', 'there', 'now', 'never', 'always', 'often', + } + words = [] + for match in re.finditer(r"[a-zA-Z']+", sentence): + words.append((match.group(), match.start(), match.end())) + candidates = [(w, s, e) for w, s, e in words if w.lower() not in skip_words and len(w) >= 3] + if not candidates: + candidates = [(w, s, e) for w, s, e in words if w.lower() not in skip_words and len(w) >= 2] + if not candidates: + return None + chosen = random.choice(candidates) + return chosen[0], chosen[1], chosen[2] + +def build_cloze_question(sentence, word, start, end, synonyms): + answer = word + options = [answer] + for syn in synonyms: + if syn.lower() != answer.lower() and syn not in options: + options.append(syn) + while len(options) < 3: + options.append(f"option{len(options)}") + options = options[:3] + random.shuffle(options) + options_str = ', '.join(options) + question_text = sentence[:start] + f'[{options_str}]' + sentence[end:] + return question_text, options, answer + +def get_question(difficulty): + for attempt in range(MAX_RETRIES + 1): + try: + sentence = generate_sentence(difficulty) + sentence = sentence.strip('"\'') + if not sentence or len(sentence.split()) < 4: + continue + pick_result = pick_cloze_word(sentence) + if pick_result is None: + continue + word, start, end = pick_result + synonyms = get_synonyms(word) + if len(synonyms) < 2: + continue + question_text, options, answer = build_cloze_question(sentence, word, start, end, synonyms) + return question_text, options, answer + except Exception as e: + print(f"Error generating question (attempt {attempt + 1}): {e}") + return None + +# ========== Game State ========== + +class GameState: + def __init__(self): + self.reset() + + def reset(self): + self.score = 0 + self.difficulty = 1 + self.streak = 0 + self.mode = "arcade" + self.remaining_questions = TIMED_MODE_QUESTIONS + self.total_correct = 0 + self.current_question = None + self.game_active = False + self.start_time = 0 + + def get_time_limit(self): + return DIFFICULTY_TIME_LIMITS.get(min(self.difficulty, 5), 15) + + def calculate_score(self, time_remaining): + diff_key = min(self.difficulty, 5) + multiplier = DIFFICULTY_MULTIPLIERS[diff_key] + time_bonus = time_remaining * TIME_BONUS_FACTOR + streak_bonus = self.streak * STREAK_BONUS + total = int(BASE_SCORE * multiplier + time_bonus + streak_bonus) + return max(total, 1) + +game = GameState() + +# ========== Countdown Timer HTML ========== + +TIMEOUT_SENTINEL = "__TIMEOUT__" + +_timer_counter = 0 + +def build_timer_html(time_limit, mode="arcade"): + global _timer_counter + if mode != "timed": + return "
" + # CSS-only countdown: no