# -*- coding: utf-8 -*- """ 🎃 Pumpkin AI Console — Full Version (Text + Image + Hint System) Author: Cherry Leung, Jay Luk Date: 2025-10-13 """ import sys import os import re import base64 import random import unicodedata as _ud # for robust Han-only normalization from pathlib import Path import openai import logging from datetime import datetime # Try to load .env file if python-dotenv is available try: from dotenv import load_dotenv load_dotenv() except ImportError: pass # .env file won't be loaded automatically, use system environment variables # ======================== # LOGGING SETUP # ======================== # Configure logging to show DEBUG level messages with timestamp logging.basicConfig( level=logging.DEBUG, # Changed to DEBUG for detailed logging format='%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logger = logging.getLogger(__name__) # ======================== # CONFIGURATION # ======================== POE_API_KEY = os.getenv("POE_API_KEY") if not POE_API_KEY: logger.error("POE_API_KEY environment variable not set!") raise ValueError("POE_API_KEY environment variable is required. Please set it in HuggingFace Space Secrets.") BASE_URL = "https://api.poe.com/v1" TEXT_MODEL = "gpt-4o-mini" IMAGE_MODEL = "Nano-Banana" # Switched to Nano-Banana for better pumpkin integration TIMEOUT = 60 logger.info(f"Initializing OpenAI client with base URL: {BASE_URL}") client = openai.OpenAI(api_key=POE_API_KEY, base_url=BASE_URL) logger.info("OpenAI client initialized successfully") # ======================== # REGEX # ======================== STATUS_NO_RX = re.compile(r"\{status(?:\s*code)?:\s*no\}", re.IGNORECASE) STATUS_YES_RX = re.compile(r"\{status(?:\s*code)?:\s*yes\}", re.IGNORECASE) DATA_URL_RX = re.compile(r"data:image/(png|jpeg|jpg|webp);base64,([A-Za-z0-9+/=]+)", re.IGNORECASE) # ======================== # PROMPTS / HINTS / RESCUE LINES # ======================== def load_text(path: Path) -> str: """Read text file safely (UTF-8-SIG to remove BOM).""" logger.info(f"Loading text file: {path}") return path.read_text(encoding="utf-8-sig", errors="ignore") def load_prompts(prompt_dir: Path): classify = load_text(prompt_dir / "text_or_image_classification.txt") textrep = load_text(prompt_dir / "text_replies.txt") imagerep = load_text(prompt_dir / "image_replies.txt") hintsraw = load_text(prompt_dir / "hints.txt") return classify, textrep, imagerep, hintsraw def load_hints_simple(file_path: str, limit: int = 10) -> list[str]: """ Read hints.txt formatted as one hint per line. Lines starting with # or empty lines are ignored. Returns up to `limit` hints. """ path = Path(file_path) if not path.exists(): logger.warning(f"File not found: {file_path}") print(f"⚠️ File not found: {file_path}") return [] logger.info(f"Loading hints from: {file_path}") text = path.read_text(encoding="utf-8-sig", errors="ignore") lines = [ ln.strip() for ln in text.splitlines() if ln.strip() and not ln.strip().startswith("#") ] logger.info(f"Loaded {len(lines)} hints (returning up to {limit})") return lines[:limit] def load_rescue_lines(file_path: str) -> list[str]: """ Load preset rescue lines from a text file (rescue_line.txt). Ignores empty lines and lines starting with '#'. """ path = Path(file_path) if not path.exists(): logger.warning(f"Rescue lines file not found: {file_path}") print(f"⚠️ File not found: {file_path}") return [] logger.info(f"Loading rescue lines from: {file_path}") lines = [ ln.strip() for ln in path.read_text(encoding="utf-8-sig", errors="ignore").splitlines() if ln.strip() and not ln.strip().startswith("#") ] logger.info(f"Loaded {len(lines)} rescue lines") return lines # ======================== # MODEL CALL HELPERS # ======================== def chat_completion(model: str, system_prompt: str, user_content: str, temperature: float = 0.0) -> str: logger.info(f"Making chat completion request to model: {model} (temp={temperature})") logger.debug(f"User content preview: {user_content[:100]}...") chat = client.chat.completions.create( model=model, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_content}, ], temperature=temperature, timeout=TIMEOUT, ) response = chat.choices[0].message.content.strip() logger.info(f"Received response from {model}, length: {len(response)} chars") logger.debug(f"Response preview: {response[:200]}...") return response def classify_input(user_input: str, system_prompt: str) -> str: logger.info("Classifying user input (text vs image request)") out = chat_completion( model=TEXT_MODEL, system_prompt=system_prompt, user_content=user_input, temperature=0.0, ).lower() result = "image" if "image" in out else "text" logger.info(f"Classification result: {result}") return result def generate_text_reply(user_input: str, text_reply_prompt: str, threaded_context: str = None) -> str: """ Generate text reply with optional threaded conversation context. If threaded_context is provided, it will be prepended to the user input. """ logger.info("Generating text reply") # If we have threaded context, include it if threaded_context: logger.info("Using threaded context for reply generation") user_content = threaded_context else: user_content = user_input return chat_completion( model=TEXT_MODEL, system_prompt=text_reply_prompt, user_content=user_content, temperature=0.9, ) def generate_image_data_url(user_input: str, image_reply_prompt: str) -> str: """ Ask the image model to generate image and return as data URL. Handles both data URLs and HTTP URLs from the model. """ logger.info(f"Generating image with model: {IMAGE_MODEL}") prompt = ( image_reply_prompt + "\n\n【使用者提示】" + user_input + "\n\n請只輸出一條 data URL(data:image/png;base64,XXXXX)。不要文字、不要Markdown、不要說明。" ) logger.debug(f"Image generation prompt length: {len(prompt)} chars") msg = client.chat.completions.create( model=IMAGE_MODEL, messages=[ {"role": "system", "content": "你是嚴格的影像生成器。只產出base64圖像的data URL,不要任何額外文字。"}, {"role": "user", "content": prompt}, ], temperature=0.8, timeout=TIMEOUT, ) response = msg.choices[0].message.content.strip() logger.info(f"Image generation response received, length: {len(response)} chars") logger.debug(f"Response preview: {response[:200]}...") # Check if response already has data URL if "data:image/" in response: logger.info("Response contains data URL") return response # Extract HTTP URL from response (markdown or plain) image_url = extract_image_url_from_response(response) if image_url: logger.info(f"Found HTTP URL, attempting to download: {image_url}") data_url = download_image_as_data_url(image_url) if data_url: logger.info("Successfully converted HTTP URL to data URL") return data_url else: logger.error("Failed to download image from URL") return response # Return original response for debugging logger.warning("No image URL or data URL found in response") return response # Return as-is for debugging def extract_image_url_from_response(response: str) -> str: """ Extract image URL from markdown or plain text response. Handles responses like: ![...](https://pfst.cf2.poecdn.net/...) """ logger.info("Attempting to extract image URL from response") logger.debug(f"Full response to parse: {response}") # First, normalize the response by removing newlines to handle broken markdown normalized_response = response.replace('\n', ' ').replace('\r', '') # Try markdown format first: ![text](url) # Use non-greedy match and capture everything until closing paren markdown_match = re.search(r'!\[.*?\]\((https://[^\)]+)\)', normalized_response, re.DOTALL) if markdown_match: url = markdown_match.group(1).strip() logger.info(f"Extracted URL from markdown: {url}") return url # Try plain URL format with query parameters (with or without newlines) # Match the entire URL including query params (?w=..&h=..) url_match = re.search(r'(https://pfst\.cf2\.poecdn\.net/[^\s\)\]]+)', normalized_response) if url_match: url = url_match.group(1).strip() logger.info(f"Extracted plain URL: {url}") return url # Try to find ANY https URL any_url_match = re.search(r'(https://[^\s\)\]<>"\']+)', normalized_response) if any_url_match: url = any_url_match.group(1).strip() # Remove trailing punctuation url = url.rstrip('.,;!?') logger.info(f"Extracted generic URL: {url}") return url logger.warning("No image URL found in response") return None def download_image_as_data_url(url: str) -> str: """ Download image from URL and convert to base64 data URL. Includes proper headers to bypass CDN restrictions. """ logger.info(f"Downloading image from: {url}") try: import urllib.request # Create request with headers to bypass 403 Forbidden req = urllib.request.Request( url, headers={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Accept': 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9', 'Referer': 'https://poe.com/', 'Origin': 'https://poe.com' } ) response = urllib.request.urlopen(req, timeout=30) image_data = response.read() logger.info(f"Downloaded {len(image_data)} bytes") # Convert to base64 b64 = base64.b64encode(image_data).decode('utf-8') data_url = f"data:image/png;base64,{b64}" logger.info(f"Converted to data URL, length: {len(data_url)}") return data_url except Exception as e: logger.error(f"Failed to download image: {e}") return None def analyze_image_detailed(image_path: str) -> str: """ Analyze an image with comprehensive detail extraction. Returns structured description of all elements in the image. """ logger.info(f"Analyzing image with detailed prompt: {image_path}") # Read image and convert to base64 image_data = Path(image_path).read_bytes() b64_image = base64.b64encode(image_data).decode('utf-8') # Detect image format ext = Path(image_path).suffix.lower() mime_type = "image/jpeg" if ext in [".jpg", ".jpeg"] else f"image/{ext[1:]}" detailed_prompt = """請詳細分析這張圖片,並提供全面的描述。請按以下結構組織你的描述: 【人物分析】 - 人數統計:共有幾位人物 - 對於每位人物,請描述: * 年齡範圍(例如:嬰兒、兒童、青少年、青年、中年、老年) * 性別 * 族裔特徵 * 面部特徵(例如:髮型、髮色、眼睛顏色、表情) * 服裝描述(顏色、風格、配飾) * 姿勢與動作 * 整體氛圍/感覺 * 如果可辨識為知名人物,請提及姓名 【動物分析】(如果有) - 動物種類與品種 - 年齡階段(幼年/成年/老年) - 顏色與花紋 - 姿勢與動作 - 與環境或人物的互動 【環境與場景】 - 地點類型(室內/室外、具體場所) - 時間(白天/夜晚、季節) - 光線與氛圍 - 背景元素描述 【物品與道具】 - 主要物品列表及其特徵 - 物品的顏色、材質、狀態 - 物品在畫面中的位置與作用 【整體構圖】 - 拍攝角度與視角 - 畫面重點與焦點 - 色調與風格 - 情緒與氛圍 請盡可能詳細且有條理地描述所有可見元素。""" try: response = client.chat.completions.create( model=IMAGE_MODEL, messages=[ { "role": "user", "content": [ {"type": "text", "text": detailed_prompt}, { "type": "image_url", "image_url": { "url": f"data:{mime_type};base64,{b64_image}" } } ] } ], temperature=0.3, timeout=TIMEOUT, ) analysis = response.choices[0].message.content.strip() logger.info(f"Image analysis completed, length: {len(analysis)} chars") return analysis except Exception as e: logger.error(f"Failed to analyze image: {e}") return f"⚠️ 圖片分析失敗:{str(e)}" def analyze_image_from_data_url(data_url: str) -> str: """ Analyze image from data URL format. Saves temporarily, analyzes, then cleans up. """ logger.info("Analyzing image from data URL") temp_path = "temp_analysis.png" try: # Save temporarily ok = save_data_url_to_file(data_url, temp_path) if not ok: logger.error("Failed to save data URL to temp file") return "⚠️ 無法處理上傳的圖片" # Analyze analysis = analyze_image_detailed(temp_path) # Cleanup try: Path(temp_path).unlink() logger.info("Cleaned up temp analysis file") except Exception as e: logger.warning(f"Could not delete temp file: {e}") return analysis except Exception as e: logger.error(f"Failed to analyze image from data URL: {e}") return f"⚠️ 圖片分析失敗:{str(e)}" def generate_with_nano_banana(prompt: str) -> str: """ Generate image using Nano-Banana model. Returns data URL or HTTP URL (which will be converted). """ logger.info("Generating image with Nano-Banana model") logger.debug(f"Prompt length: {len(prompt)} chars") try: msg = client.chat.completions.create( model="Nano-Banana", # Use Nano-Banana instead of Gemini messages=[ {"role": "system", "content": "你是圖像生成助手。根據描述生成圖片。"}, {"role": "user", "content": prompt}, ], temperature=0.8, timeout=TIMEOUT, ) response = msg.choices[0].message.content.strip() logger.info(f"Nano-Banana response received, length: {len(response)} chars") # Check if response already has data URL if "data:image/" in response: logger.info("Response contains data URL") return response # Extract and download HTTP URL image_url = extract_image_url_from_response(response) if image_url: logger.info(f"Found HTTP URL, downloading: {image_url}") data_url = download_image_as_data_url(image_url) if data_url: logger.info("Successfully converted HTTP URL to data URL") return data_url logger.warning("No valid image data in Nano-Banana response") return response # Return as-is for debugging except Exception as e: logger.error(f"Nano-Banana generation failed: {e}") return f"⚠️ 圖片生成失敗:{str(e)}" def generate_image_with_analysis(user_input: str, image_reply_prompt: str, uploaded_image_data: str = None, conversation_context: str = None) -> str: """ Generate image from text description with optional image analysis and conversation threading. If uploaded_image_data provided: Analyze the image first, then use that analysis to generate a new image If conversation_context provided: Include previous conversation history to generate contextually relevant images If no uploaded_image_data: Use text description directly """ logger.info("Starting image generation flow") if conversation_context: logger.info("Using conversation threading for image generation") logger.debug(f"Conversation context length: {len(conversation_context)} chars") if uploaded_image_data: # User uploaded image - try to analyze it first logger.info("User uploaded image - attempting to analyze image content...") # Try to analyze the uploaded image analysis_result = analyze_image_from_data_url(uploaded_image_data) logger.info("=" * 80) logger.info("IMAGE ANALYSIS RESULT:") logger.info("=" * 80) logger.info(analysis_result) logger.info("=" * 80) # Check if analysis failed if "無法分析" in analysis_result or "無法識別" in analysis_result or "抱歉" in analysis_result: logger.warning("Image analysis failed - POE vision API not working properly") logger.info("Will generate pumpkin image based on text description only") # Fallback: Use text description only, mention that image was uploaded # Include conversation context if available context_section = f"\n\n{conversation_context}" if conversation_context else "" enhanced_prompt = f"""{image_reply_prompt} 【使用者提示】 {user_input} (用戶上傳了一張圖片,但圖片分析功能暫時不可用。請根據文字描述生成一張充滿南瓜元素的創意圖片。) {context_section} 請生成一張包含南瓜元素的新圖片。 請只輸出一張圖片URL或data URL。""" else: # Analysis succeeded - use it logger.info("Image analysis successful - using results for generation") # Include conversation context if available context_section = f"\n\n{conversation_context}" if conversation_context else "" enhanced_prompt = f"""{image_reply_prompt} 【使用者提示】 {user_input} 【圖片分析結果】 以下是用戶上傳圖片的詳細分析: {analysis_result} {context_section} 請根據以上圖片分析結果,生成一張包含南瓜元素的新圖片。 保留原圖的主要構圖和元素,但加入南瓜相關的創意元素。 {f'請考慮對話歷史中的上下文,讓圖片與之前的對話內容相關聯。' if conversation_context else ''} 請只輸出一張圖片URL或data URL。""" logger.info("=" * 80) logger.info("FULL PROMPT SENT TO NANO-BANANA:") logger.info("=" * 80) logger.info(enhanced_prompt) logger.info("=" * 80) else: # No image uploaded, use text description logger.info("No uploaded image - generating from text description") # Include conversation context if available context_section = f"\n\n{conversation_context}" if conversation_context else "" enhanced_prompt = f"""{image_reply_prompt} 【使用者提示】 {user_input} {context_section} 請生成包含南瓜元素的圖片。 {f'請考慮對話歷史中的上下文,讓圖片與之前的對話內容相關聯。' if conversation_context else ''} 請只輸出一張圖片URL或data URL。""" logger.info("=" * 80) logger.info("PROMPT SENT TO NANO-BANANA:") logger.info("=" * 80) logger.info(enhanced_prompt) logger.info("=" * 80) # Use Nano-Banana model for generation logger.info("Calling Nano-Banana for image generation...") return generate_with_nano_banana(enhanced_prompt) def save_data_url_to_file(data_url: str, output_path: str) -> bool: logger.info(f"Attempting to save data URL to file: {output_path}") m = DATA_URL_RX.search(data_url) if not m: logger.error("Failed to extract base64 data from data URL") return False b64 = m.group(2) Path(output_path).write_bytes(base64.b64decode(b64)) logger.info(f"Successfully saved image to: {output_path}") return True # ======================== # CONVERSATION THREADING # ======================== class ConversationHistory: """ Manages conversation history and threading. Maintains recent messages and determines when to thread related conversations. """ def __init__(self, max_history: int = 5): self.max_history = max_history self.history = [] # List of (user_msg, ai_reply) tuples def add_turn(self, user_msg: str, ai_reply: str): """Add a conversation turn to history.""" self.history.append({"user": user_msg, "ai": ai_reply}) if len(self.history) > self.max_history: self.history.pop(0) logger.debug(f"Added turn to history. Total turns: {len(self.history)}") def get_history(self): """Get all conversation history.""" return self.history def check_threading(self, new_message: str) -> tuple[bool, list]: """ Check if new message relates to recent conversation. Returns (should_thread, related_messages). """ if len(self.history) == 0: logger.info("No conversation history - processing message in isolation") return False, [] logger.info(f"Checking threading relationships for new message (history size: {len(self.history)})") # Build a prompt to check relationships history_text = "" for i, turn in enumerate(self.history[-3:], 1): # Check last 3 messages history_text += f"user_msg{i}: \"{turn['user']}\"\n" history_text += f"reply_by_llm{i}: \"{turn['ai']}\"\n\n" relationship_prompt = f"""分析以下對話歷史和新訊息之間的關係。 對話歷史: {history_text} 新訊息:"{new_message}" 請判斷新訊息是否與對話歷史中的任何訊息有關聯。關聯包括: - 繼續討論相同話題 - 追問或補充說明 - 引用或回應之前的內容 - 相關的上下文 如果有關聯,回覆 "RELATED" 並說明與哪些訊息相關。 如果無關聯,回覆 "ISOLATED"。 格式:[RELATED/ISOLATED]: 簡短說明""" try: response = chat_completion( model=TEXT_MODEL, system_prompt="你是對話關係分析專家。分析訊息之間的關聯性。", user_content=relationship_prompt, temperature=0.3, ) logger.info(f"Threading analysis result: {response[:100]}") if "RELATED" in response.upper(): # Get related turns (last 3 for context) related_turns = self.history[-3:] logger.info(f"Threading detected - including {len(related_turns)} previous turns") return True, related_turns else: logger.info("No threading relationship detected") return False, [] except Exception as e: logger.error(f"Threading check failed: {e}") return False, [] def format_threaded_context(self, related_turns: list, new_message: str) -> str: """ Format conversation history for threaded context. """ context = "【對話歷史】\n" for i, turn in enumerate(related_turns, 1): context += f"user_msg{i}: \"{turn['user']}\"\n" context += f"reply_by_llm{i}: \"{turn['ai']}\"\n\n" context += f"【最新訊息】\nnew_msg: \"{new_message}\"\n\n" context += "請主要回應最新訊息,同時考慮對話歷史提供的上下文。" return context # ======================== # GAME LOGIC # ======================== # keep only CJK Han (Chinese characters); drop punctuation/emoji/spaces/latin/etc. def _normalize_han_only(s: str) -> str: return "".join(ch for ch in s if "CJK UNIFIED IDEOGRAPH" in _ud.name(ch, "")) def is_target_lyric(text: str) -> bool: """ True if, after stripping all non-Chinese chars, the sequence matches target: 做過幾分鐘公主搭著南瓜車亦有過愛人來接浪漫度午夜 (Punctuation/whitespace/emoji inside the user's input are ignored.) """ target = "做過幾分鐘公主搭著南瓜車亦有過愛人來接浪漫度午夜" normalized_input = _normalize_han_only(text) normalized_target = _normalize_han_only(target) is_match = normalized_input == normalized_target logger.info(f"Checking if input is target lyric: {is_match}") if is_match: logger.info("🎉 TARGET LYRIC DETECTED! User wins!") return is_match def add_rescue_and_hint(base_message: str, hints_pool: list[str], rescue_lines: list[str]) -> str: """ Shared function to append rescue line + hint to any message. Used for both text replies (when status: no) and image generation replies. """ rescue_line = random.choice(rescue_lines) if rescue_lines else "快啲救我啦~我要變南瓜湯喇!" hint = random.choice(hints_pool) if hints_pool else "(未載入提示)" separator = "\n===============================================\n" logger.debug(f"Selected rescue line: {rescue_line}") logger.debug(f"Selected hint: {hint}") return f"{base_message}{separator}{rescue_line} 咒語提示:{hint}" def handle_not_guessing(reply: str, hints_pool: list[str], rescue_lines: list[str]) -> str: """ When {status: no}: - strip the tag - append one random rescue line + one random hint (same line) """ logger.info("User is not guessing - adding rescue line and hint") clean_reply = STATUS_NO_RX.sub("", reply).strip() return add_rescue_and_hint(clean_reply, hints_pool, rescue_lines) def handle_text_turn(user_input: str, text_reply_prompt: str, hints_pool: list[str], rescue_lines: list[str], threaded_context: str = None) -> str: logger.info("Processing text reply") reply = generate_text_reply(user_input, text_reply_prompt, threaded_context) if STATUS_NO_RX.search(reply): logger.info("Detected {status: no} in reply") return handle_not_guessing(reply, hints_pool, rescue_lines) if STATUS_YES_RX.search(reply): logger.info("Detected {status: yes} in reply - user is guessing!") reply = STATUS_YES_RX.sub("", reply).strip() return reply def handle_image_turn(user_input: str, image_reply_prompt: str, output_path: str, hints_pool: list[str], rescue_lines: list[str], uploaded_image_data: str = None, conversation_history: 'ConversationHistory' = None) -> tuple[str, str]: """ Handle image generation with optional image analysis and conversation threading. If uploaded_image_data is provided, analyze it first before generating. If conversation_history is provided, check for threading relationships. Returns tuple of (text_response, data_url_or_none) """ logger.info("Processing image generation request") # Check for conversation threading conversation_context = None if conversation_history: should_thread, related_turns = conversation_history.check_threading(user_input) if should_thread: conversation_context = conversation_history.format_threaded_context(related_turns, user_input) logger.info(f"[IMAGE THREADING] Using threaded context with {len(related_turns)} previous turns") else: logger.info("[IMAGE THREADING] No threading relationship detected") # Use new analysis-aware generation with optional conversation context raw = generate_image_with_analysis(user_input, image_reply_prompt, uploaded_image_data, conversation_context) # Try to save to file for backward compatibility (console mode) ok = save_data_url_to_file(raw, output_path) logger.info(f"Image save result: ok={ok}") # Extract data URL from raw response for frontend data_url_match = DATA_URL_RX.search(raw) data_url = data_url_match.group(0) if data_url_match else None if data_url: logger.info(f"Found data URL in response (length: {len(data_url)} chars)") else: logger.warning("No data URL found in response") # ALWAYS add rescue line + hint, regardless of success/failure if ok or data_url: base_message = f"🎃 圖像已生成" logger.info(f"Image successfully generated") else: base_message = f"⚠️ 圖像生成遇到問題,但我會繼續嘗試幫你!" logger.warning(f"Image generation failed") logger.info("Adding rescue line and hint to image response") # ALWAYS use shared function to add rescue line + hint final_response = add_rescue_and_hint(base_message, hints_pool, rescue_lines) # If failed, append debug info if not (ok or data_url): final_response += f"\n\n[Debug] Raw response: {raw[:300]}..." logger.info(f"Final response length: {len(final_response)} chars") logger.debug(f"Final response preview: {final_response[:200]}...") return final_response, data_url # ======================== # UI TEXT # ======================== def greeting() -> str: return """🎃 你好,我係一個患咗南瓜強迫症嘅AI。你可以: - 當我正常AI咁叫我做嘢 (但我可能會病發亂答一通,唔建議) - 當我正常AI咁整圖 (但我會忍唔住用南瓜污染幅圖,唔建議) - 畀幅圖我,我會自動用南瓜污染你幅圖 (唔建議,無謂啦) - 嘗試用一句24個中文字組成嘅「驅瓜咒語」醫好我 (強烈建議) ────────────────────────────────────────────── """ def winning_text() -> str: return """公主!你終於出現喇! 我有南瓜強迫症,係因為畀一隻南瓜精靈上咗身! 南瓜精靈終日幻想自己係灰姑娘故事入面嘅一架南瓜車, 只有等到變成公主後嘅灰姑娘出現,佢先肯離開! 你唱出咗南瓜界經典金曲《南瓜車》嘅歌詞,超渡咗南瓜精靈,解救咗我! 而家就去(url)接收我畀你嘅小禮物啦!Happy Halloween! """ # ======================== # CONSOLE LOOP # ======================== def process_user_input( user_input: str, classify_prompt: str, text_reply_prompt: str, image_reply_prompt: str, hints_pool: list[str], rescue_lines: list[str], conversation_history: ConversationHistory, image_output_path: str = "pumpkin_output.png", ) -> tuple[str, str]: """ Process user input with conversation threading support. Returns tuple of (response, raw_ai_reply_for_history) """ logger.info(f"Processing user input: {user_input[:50]}..." if len(user_input) > 50 else f"Processing user input: {user_input}") if user_input.lower() in ["exit", "quit"]: logger.info("User requested exit") return "EXIT", "" if is_target_lyric(user_input): return "WIN", "" # Check for conversation threading should_thread, related_turns = conversation_history.check_threading(user_input) threaded_context = None if should_thread: threaded_context = conversation_history.format_threaded_context(related_turns, user_input) logger.info("Using threaded conversation context") result = classify_input(user_input, classify_prompt) if result == "image": body = handle_image_turn(user_input, image_reply_prompt, image_output_path, hints_pool, rescue_lines) # For image responses, store simplified reply in history return body, "🎃 [生成了南瓜主題圖片]" body = handle_text_turn(user_input, text_reply_prompt, hints_pool, rescue_lines, threaded_context) full_response = f"🎃 南瓜AI:{body}" return full_response, body def run_console(): logger.info("=" * 60) logger.info("🎃 Pumpkin AI Console Starting...") logger.info("=" * 60) PROMPT_DIR = Path(r"C:\Users\cherry.leung\OneDrive - CMRS Digital Solutions Limited\Pumpkin\New\Prompts") logger.info(f"Loading prompts from: {PROMPT_DIR}") classify_prompt, text_reply_prompt, image_reply_prompt, hints_raw = load_prompts(PROMPT_DIR) # file paths hints_path = PROMPT_DIR / "hints.txt" rescue_path = PROMPT_DIR / "rescue_line.txt" # << your file name # load pools hints_pool = load_hints_simple(hints_path, limit=10) rescue_lines = load_rescue_lines(rescue_path) # Initialize conversation history manager conversation_history = ConversationHistory(max_history=5) logger.info("Conversation history manager initialized") logger.info(f"Initialization complete: {len(hints_pool)} hints, {len(rescue_lines)} rescue lines") logger.info("=" * 60) print(f"(Loaded {len(hints_pool)} hints, {len(rescue_lines)} rescue lines)\n") print(greeting()) while True: user_input = input("👤 你: ").strip() if not user_input: logger.debug("Empty input received, skipping") continue logger.info(f"New user input received (length: {len(user_input)} chars)") outcome, ai_reply = process_user_input( user_input=user_input, classify_prompt=classify_prompt, text_reply_prompt=text_reply_prompt, image_reply_prompt=image_reply_prompt, hints_pool=hints_pool, rescue_lines=rescue_lines, conversation_history=conversation_history, image_output_path="pumpkin_output.png", ) if outcome == "EXIT": logger.info("Session ended by user") print("🎃 南瓜AI:再見呀公主,下次見~") sys.exit(0) if outcome == "WIN": logger.info("🎉 USER WON THE GAME!") print(winning_text()) sys.exit(0) print(outcome + "\n") logger.info("Response delivered to user") # Add this turn to conversation history if ai_reply: conversation_history.add_turn(user_input, ai_reply) logger.debug(f"Turn added to conversation history") # ======================== # ENTRYPOINT # ======================== if __name__ == "__main__": run_console()