File size: 12,617 Bytes
213932c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39418b0
 
213932c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# -*- coding: utf-8 -*-
"""
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

# Fix Windows console encoding issues
if sys.platform == 'win32':
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')

# Import the existing pumpkin logic
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
)

# Initialize Flask app with static file serving
static_folder = Path(__file__).parent / "pumpkin-ai-frontend" / "build"
app = Flask(__name__, static_folder=str(static_folder), static_url_path='')

# Enable CORS for frontend access
CORS(app, origins=[
    "http://localhost:3000",  # Local development
    "http://localhost:3001",
    "http://10.14.0.2:3000",  # Network address
    "https://*.hf.space",  # Hugging Face Spaces
    "https://huggingface.co"  # Hugging Face
])

# Load prompts and data at startup
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 = []

# Store conversation history per user (keyed by IP address)
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
    # Find the separator pattern (one or more equals signs between newlines)
    separator_pattern = r'\n=+\n'
    parts = re.split(separator_pattern, response)
    if len(parts) > 1:
        # Return only the first part (before first separator)
        return parts[0].strip()
    return response

# Serve React frontend
@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:
        # If file not found, serve index.html (for React Router)
        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')

        # Safe print - avoid encoding errors
        try:
            print(f"[CHAT] Received ({input_type}): {user_input[:50]}...")
        except:
            print(f"[CHAT] Received ({input_type}): [content with special characters]")

        # Check if it's the winning spell
        if is_target_lyric(user_input):
            return jsonify({
                "response": """公主!你終於出現喇!
我有南瓜強迫症,係因為畀一隻南瓜精靈上咗身!南瓜精靈終日幻想自己係灰姑娘故事入面嘅一架南瓜車,只有等到變成公主後嘅灰姑娘出現,佢先肯離開!你唱出咗南瓜界經典金曲《南瓜車》嘅歌詞,超渡咗南瓜精靈,解救咗我,我要好好多謝你!而家就去接收我畀你嘅小禮物啦!Happy Halloween!""",
                "is_end": True
            })

        # Handle text input
        try:
            # Check if user uploaded an image data
            uploaded_image_data = data.get('imageData', None)

            # Skip classification if type is explicitly 'image' or imageData is present
            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:
                # Classify the input to determine if it's an image request
                print(f"[CLASSIFY] Classifying input: {user_input[:50]}...")
                input_classification = classify_input(user_input, classify_prompt)
                print(f"[CLASSIFY] Classification result: {input_classification}")

            # Handle image generation requests
            if input_classification == 'image':
                print(f"[IMAGE] Processing image request: {user_input[:50]}...")

                # Get conversation history for this user
                user_id = request.remote_addr
                conv_history = get_conversation_history(user_id)

                # Log uploaded image data
                if uploaded_image_data:
                    print(f"[IMAGE] User uploaded image data (length: {len(uploaded_image_data)} chars)")

                # Use the updated handle_image_turn that includes rescue lines + hints + analysis + threading
                # Now returns tuple: (text_response, data_url_or_none)
                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  # Pass conversation history for threading
                )

                # Store this turn in conversation history (simplified description)
                conv_history.add_turn(user_input, "🎃 [生成了南瓜主題圖片]")

                # Check if we got a valid data URL
                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")

                    # Return the data URL with the text response that includes rescue line + hint
                    response_json = {
                        "response": result,  # This now includes rescue line + hint!
                        "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]}...")
                    # Still return response with rescue line + hint
                    return jsonify({
                        "response": result,
                        "is_end": False,
                        "is_image": False
                    })

            # Get conversation history for this user (using IP as identifier)
            user_id = request.remote_addr
            conv_history = get_conversation_history(user_id)

            # Check for threading before generating response
            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")

            # Process text with threading support
            outcome = handle_text_turn(user_input, text_reply_prompt, hints_pool, rescue_lines, threaded_context)

            # Store CLEAN reply in history (without rescue lines/hints to prevent doubling)
            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)")

            # Check if it's a special outcome
            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
                })

            # Check if response contains image data URL
            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)

            # Check if it's a quota 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  # Return 200 to show message properly

            # Other errors
            raise text_error

    except Exception as e:
        # Safe error printing
        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)

        # User-friendly error messages
        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")

    # Get port from environment (HuggingFace Spaces sets this)
    port = int(os.environ.get('PORT', 7860))

    # Run Flask app
    app.run(
        host='0.0.0.0',
        port=port,
        debug=False  # Disable debug in production
    )