| |
| """ |
| Pumpkin AI Flask Backend - HuggingFace Spaces Version |
| Serves both API endpoints and frontend static files |
| """ |
|
|
| from flask import Flask, request, jsonify, send_from_directory |
| from flask_cors import CORS |
| import sys |
| import os |
| import io |
| from pathlib import Path |
| import base64 |
|
|
| |
| if sys.platform == 'win32': |
| sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') |
| sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') |
|
|
| |
| from pumpkin_code import ( |
| load_prompts, |
| load_hints_simple, |
| load_rescue_lines, |
| is_target_lyric, |
| classify_input, |
| generate_text_reply, |
| handle_text_turn, |
| handle_image_turn, |
| generate_image_data_url, |
| save_data_url_to_file, |
| ConversationHistory |
| ) |
|
|
| |
| static_folder = Path(__file__).parent / "pumpkin-ai-frontend" / "build" |
| app = Flask(__name__, static_folder=str(static_folder), static_url_path='') |
|
|
| |
| CORS(app, origins=[ |
| "http://localhost:3000", |
| "http://localhost:3001", |
| "http://10.14.0.2:3000", |
| "https://*.hf.space", |
| "https://huggingface.co" |
| ]) |
|
|
| |
| PROMPT_DIR = Path(__file__).parent / "Prompts" |
|
|
| try: |
| classify_prompt, text_reply_prompt, image_reply_prompt, hints_raw = load_prompts(PROMPT_DIR) |
| hints_path = PROMPT_DIR / "hints.txt" |
| rescue_path = PROMPT_DIR / "rescue_line.txt" |
| hints_pool = load_hints_simple(hints_path, limit=10) |
| rescue_lines = load_rescue_lines(rescue_path) |
|
|
| print(f"[OK] Loaded {len(hints_pool)} hints, {len(rescue_lines)} rescue lines") |
| except Exception as e: |
| print(f"[ERROR] Error loading prompts: {e}") |
| classify_prompt = text_reply_prompt = image_reply_prompt = "" |
| hints_pool = [] |
| rescue_lines = [] |
|
|
| |
| conversation_histories = {} |
|
|
| def get_conversation_history(user_identifier: str) -> ConversationHistory: |
| """Get or create conversation history for a user.""" |
| if user_identifier not in conversation_histories: |
| conversation_histories[user_identifier] = ConversationHistory(max_history=5) |
| print(f"[HISTORY] Created new conversation history for {user_identifier}") |
| return conversation_histories[user_identifier] |
|
|
| def strip_rescue_and_hint(response: str) -> str: |
| """ |
| Remove rescue line and hint from response for clean history storage. |
| This prevents double rescue lines when threading conversations. |
| """ |
| import re |
| |
| separator_pattern = r'\n=+\n' |
| parts = re.split(separator_pattern, response) |
| if len(parts) > 1: |
| |
| return parts[0].strip() |
| return response |
|
|
| |
| @app.route('/', methods=['GET']) |
| def serve_frontend(): |
| """Serve the React frontend index.html""" |
| return send_from_directory(str(static_folder), 'index.html') |
|
|
| @app.route('/<path:path>', methods=['GET']) |
| def serve_static(path): |
| """Serve static files (JS, CSS, images)""" |
| if static_folder.joinpath(path).exists(): |
| return send_from_directory(str(static_folder), path) |
| else: |
| |
| return send_from_directory(str(static_folder), 'index.html') |
|
|
| @app.route('/api/health', methods=['GET']) |
| def health(): |
| """Health check endpoint""" |
| return jsonify({ |
| "status": "healthy", |
| "hints_loaded": len(hints_pool), |
| "rescue_lines_loaded": len(rescue_lines) |
| }) |
|
|
| @app.route('/api/chat', methods=['POST']) |
| def chat(): |
| """ |
| Main chat endpoint |
| Expected JSON: |
| { |
| "input": "user message", |
| "type": "text" or "image", |
| "imageData": "base64 image data" (optional, for image type) |
| } |
| |
| Returns JSON: |
| { |
| "response": "AI response or data URL", |
| "is_end": true/false, |
| "is_image": true/false |
| } |
| """ |
| try: |
| data = request.get_json() |
|
|
| if not data or 'input' not in data: |
| return jsonify({ |
| "error": "Missing 'input' field", |
| "response": "[WARNING] Please provide input content", |
| "is_end": False |
| }), 400 |
|
|
| user_input = data['input'] |
| input_type = data.get('type', 'text') |
|
|
| |
| try: |
| print(f"[CHAT] Received ({input_type}): {user_input[:50]}...") |
| except: |
| print(f"[CHAT] Received ({input_type}): [content with special characters]") |
|
|
| |
| if is_target_lyric(user_input): |
| return jsonify({ |
| "response": """公主!你終於出現喇! |
| 我有南瓜強迫症,係因為畀一隻南瓜精靈上咗身!南瓜精靈終日幻想自己係灰姑娘故事入面嘅一架南瓜車,只有等到變成公主後嘅灰姑娘出現,佢先肯離開!你唱出咗南瓜界經典金曲《南瓜車》嘅歌詞,超渡咗南瓜精靈,解救咗我,我要好好多謝你!而家就去接收我畀你嘅小禮物啦!Happy Halloween!""", |
| "is_end": True |
| }) |
|
|
| |
| try: |
| |
| uploaded_image_data = data.get('imageData', None) |
|
|
| |
| if input_type == 'image' or uploaded_image_data: |
| input_classification = 'image' |
| print(f"[IMAGE] Image request detected (type={input_type}, has_imageData={bool(uploaded_image_data)})") |
| else: |
| |
| print(f"[CLASSIFY] Classifying input: {user_input[:50]}...") |
| input_classification = classify_input(user_input, classify_prompt) |
| print(f"[CLASSIFY] Classification result: {input_classification}") |
|
|
| |
| if input_classification == 'image': |
| print(f"[IMAGE] Processing image request: {user_input[:50]}...") |
|
|
| |
| user_id = request.remote_addr |
| conv_history = get_conversation_history(user_id) |
|
|
| |
| if uploaded_image_data: |
| print(f"[IMAGE] User uploaded image data (length: {len(uploaded_image_data)} chars)") |
|
|
| |
| |
| output_path = "temp_pumpkin_image.png" |
| result, data_url = handle_image_turn( |
| user_input, |
| image_reply_prompt, |
| output_path, |
| hints_pool, |
| rescue_lines, |
| uploaded_image_data, |
| conv_history |
| ) |
|
|
| |
| conv_history.add_turn(user_input, "🎃 [生成了南瓜主題圖片]") |
|
|
| |
| if data_url: |
| print(f"[IMAGE] Successfully generated image with data URL (length: {len(data_url)} chars)") |
| print(f"[IMAGE] Data URL preview: {data_url[:100]}...") |
| print(f"[IMAGE] Returning JSON with imageData field") |
|
|
| |
| response_json = { |
| "response": result, |
| "imageData": data_url, |
| "is_end": False, |
| "is_image": True |
| } |
| print(f"[IMAGE] Response JSON keys: {list(response_json.keys())}") |
| print(f"[IMAGE] imageData field length: {len(response_json['imageData'])} chars") |
| return jsonify(response_json) |
| else: |
| print(f"[IMAGE] Generation had issues: {result[:100]}...") |
| |
| return jsonify({ |
| "response": result, |
| "is_end": False, |
| "is_image": False |
| }) |
|
|
| |
| user_id = request.remote_addr |
| conv_history = get_conversation_history(user_id) |
|
|
| |
| should_thread, related_turns = conv_history.check_threading(user_input) |
| threaded_context = None |
|
|
| if should_thread: |
| threaded_context = conv_history.format_threaded_context(related_turns, user_input) |
| print(f"[THREAD] Using threaded context with {len(related_turns)} previous turns") |
|
|
| |
| outcome = handle_text_turn(user_input, text_reply_prompt, hints_pool, rescue_lines, threaded_context) |
|
|
| |
| clean_reply = strip_rescue_and_hint(outcome) |
| conv_history.add_turn(user_input, clean_reply) |
| print(f"[HISTORY] Stored clean reply (length: {len(clean_reply)} chars)") |
|
|
| |
| if outcome == "EXIT": |
| return jsonify({ |
| "response": "Bye! See you next time~", |
| "is_end": True |
| }) |
|
|
| if outcome == "WIN": |
| return jsonify({ |
| "response": "Congratulations! You guessed the spell correctly! Happy Halloween!", |
| "is_end": True |
| }) |
|
|
| |
| is_image = "data:image/" in outcome |
|
|
| return jsonify({ |
| "response": outcome, |
| "is_end": False, |
| "is_image": is_image |
| }) |
|
|
| except Exception as text_error: |
| error_msg = str(text_error) |
|
|
| |
| if '402' in error_msg or 'insufficient_quota' in error_msg or 'used up your points' in error_msg: |
| return jsonify({ |
| "response": "[QUOTA EXCEEDED] Your POE API quota is used up. Please visit https://poe.com/api_key to add more credits. The app will work normally once credits are added!", |
| "is_end": False |
| }), 200 |
|
|
| |
| raise text_error |
|
|
| except Exception as e: |
| |
| try: |
| print(f"[ERROR] in /chat: {str(e)}") |
| except: |
| print("[ERROR] in /chat: [error with special characters]") |
|
|
| import traceback |
| traceback.print_exc() |
|
|
| error_msg = str(e) |
|
|
| |
| if '402' in error_msg or 'insufficient_quota' in error_msg: |
| return jsonify({ |
| "error": "quota_exceeded", |
| "response": "[QUOTA EXCEEDED] POE API credits exhausted. Visit https://poe.com/api_key to add credits.", |
| "is_end": False |
| }), 200 |
| else: |
| return jsonify({ |
| "error": str(e), |
| "response": f"[ERROR] Processing request failed: {str(e)[:100]}", |
| "is_end": False |
| }), 500 |
|
|
| if __name__ == '__main__': |
| print("=" * 60) |
| print(" Pumpkin AI - HuggingFace Spaces") |
| print("=" * 60) |
| print(f"Prompt directory: {PROMPT_DIR}") |
| print(f"Static files: {static_folder}") |
| print(f"Hints loaded: {len(hints_pool)}") |
| print(f"Rescue lines loaded: {len(rescue_lines)}") |
| print("=" * 60 + "\n") |
|
|
| |
| port = int(os.environ.get('PORT', 7860)) |
|
|
| |
| app.run( |
| host='0.0.0.0', |
| port=port, |
| debug=False |
| ) |
|
|