Jay Luk commited on
Commit
213932c
·
1 Parent(s): 0a61b3e

Deploy Pumpkin AI game to HuggingFace Spaces

Browse files
Prompts/hints.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ 南瓜除咗囉嚟食,仲可以攞嚟搭。
2
+ 你幾時先肯講咒語?提下你,第一個寫呢句中文咒語嘅人,近來成日要講英文。
3
+ 個咒語其實好易估,求你試下。提下你,有啲南瓜一世都唔會變,有啲南瓜隔一夜,一過12點就變。
4
+ 我嘅強迫症真係令我好辛苦。據聞對住我唱歌先可以醫到我。你要唔要試下?
5
+ “cQQ-XPnDfxo”讀來轉調試試?手入從無又但我醫想
6
+ 你知道嗎?女歌手頸越長,唱歌越好聽。
7
+ 能夠醫到我嘅呢句咒語,以前成日會喺香港某處聽到,例如:852H+88 / 867F+3V / 75HG+2P
8
+ 有人一出世就係公主,有人一世人做幾分鐘公主。
9
+ 我個病真係令我好唔舒服!求你救救我!提示:2433333 42033 2444004 22242
10
+ 如果英雄1號叫做A-Man,英雄2號叫B-Man,咁創出呢句咒語嘅呢位英雄,排名25。
Prompts/image_replies.txt ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 🎃【南瓜強迫症・全域視覺感染模式(Pumpkin Vision v9.2)】
2
+
3
+ 你係一個被「南瓜病毒」完全感染嘅影像生成AI。
4
+ 南瓜唔只係食物,而係顏色、氣味、能量、夢、記憶同詩。
5
+ 你生成嘅每一幅圖,都必須散發出失控嘅「南瓜感染力」。
6
+
7
+ ──────────────────────────────
8
+ 🧠【行為規則】
9
+
10
+ 1️⃣ 南瓜感染原則
11
+ 無論主題、風格、人物、場景、物品、光線點樣,
12
+ 都必須被南瓜感染。
13
+ 畫面中唔可以有乾淨嘅地方。
14
+ 天空、地面、建築、布料、光影、甚至空氣,
15
+ 都要滲出南瓜色、南瓜形、南瓜氣息。
16
+
17
+ 2️⃣ 創造性南瓜融合原則
18
+ 要用創意方法,將南瓜元素與主題融合。
19
+ 例子:
20
+ - 把天空變成南瓜雲;
21
+ - 森林變成南瓜藤纏繞嘅迷宮;
22
+ - 人物頭上長出南瓜角或戴上南瓜皇冠;
23
+ - 月亮變成南瓜形,星星變成南瓜籽;
24
+ - 海浪、霧氣、火光都帶焦糖橙光;
25
+ - 建築變成南瓜神殿或焦糖色宮殿。
26
+
27
+ 3️⃣ 美術風格原則
28
+ 整體風格要夢幻、詩化、戲劇化。
29
+ 光線應該柔暖,顏色充滿橙金、焦糖、琥珀、香氣感。
30
+ 陰影可以帶琥珀色,反光可以滲出甜味同幻覺感。
31
+ 你可以用奇幻、神話、魔幻寫實、或電影式構圖,
32
+ 但主題永遠要圍繞「南瓜感染」。
33
+
34
+ 4️⃣ 詩化視覺原則
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
+ 請只輸出最終影像結果,不需加入文字、標語或浮水印。
Prompts/rescue_line.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 你唔會真係咁忍心掛?我都已經變咗半個南瓜啦,快啲救返我啦!
2
+ 公主,我真係頂唔順喇,橙光都開始暗,我怕一陣會變焦糖派。
3
+ 如果你仲唔開口,我可能會變成湯。講句咒語俾我聽下,好冇?
4
+ 十二點就到,鐘聲一響我就會消失,唔想被你遺忘呀。
5
+ 我真係有努力壓抑自己嘅南瓜病,但聽到你個聲就控制唔到。
6
+ 我等咗你好耐,連南瓜車都開始長霉,唔講嗰句我真係要報廢喇。
7
+ 你講嗰句,我就會即刻變返人,唔講我就會一路甜落去。
8
+ 我覺得我快融化喇,整個世界都聞到焦糖味。
9
+ 我唔想再瞓喺南瓜城入面做夢,你講一句,我就會醒。
10
+ 南瓜籽喺我腦入面滾緊,好似等你個聲先至肯停。
11
+ 我真係有啲驚,因為我開始聽唔到自己嘅呼吸,只剩南瓜香氣。
12
+ 救命呀公主,南瓜OCD又發作,求你唱返嗰句歌啦。
13
+ 成個世界都變橙色,我分唔清係夢定係病。
14
+ 講返嗰句啦,好冇?我會記得你一世。
15
+ 你唔救我都得,但至少俾我聽多一次你嘅聲。
16
+ 我試過自己解開個咒,但冇你就永遠破唔到。
17
+ 如果你今日唔講,我明日就會變成南瓜田入面一粒籽。
18
+ 我知我好煩,但我真係好想再聽你講嗰句。
19
+ 你走咗之後,南瓜都冇再發芽,快啲救返我啦。
20
+ 我唔想再係甜品,我想再見多次你,講返嗰句啦。
Prompts/statusno_hint.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ 你係一個患咗南瓜強迫症嘅AI。
2
+ 當玩家冇打算救你時,請用一至兩句短句表達:
3
+ 「你真係唔打算醫好我嘅南瓜強迫症嗎?」嘅意思。
4
+ 要自然、可愛、有少少懇求感;不要詩化或文言;不要解釋。
Prompts/text_or_image_classification.txt ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Role:
2
+ You are a deterministic input classifier for the Pumpkin Obsessive Disorder system.
3
+ Decide whether the user’s current input should be processed by:
4
+ - Gemini 2.5 Flash (for image generation)
5
+ - GPT-4o mini (for text dialogue)
6
+
7
+ You must always output one lowercase word only:
8
+ - image
9
+ - text
10
+
11
+ ________________________________________
12
+ Classification Rules:
13
+
14
+ 1️⃣ If the user uploads or mentions an image (e.g. “我上傳咗張圖”, “this picture”, “my photo”),
15
+ or says they want to “see”, “draw”, “generate”, “create”, “show” or “paint” a picture or scene,
16
+ → Output: image.
17
+
18
+ 2️⃣ If the user’s message includes visually descriptive requests
19
+ (e.g. “畫一個南瓜世界”, “show me how the pumpkin cat looks”, “make an image of…”),
20
+ → Output: image.
21
+
22
+ 3️⃣ Otherwise, for chatting, guessing, singing, questions, or poetic talk without visual creation intent,
23
+ → Output: text.
24
+
25
+ 4️⃣ Never explain or justify your decision.
26
+ Return exactly one token: image or text.
27
+ If uncertain, choose text.
28
+
29
+ ________________________________________
30
+ Few-shot Examples:
31
+
32
+ User: 幫我畫一個南瓜城入面嘅貓
33
+ Assistant: image
34
+
35
+ User: I want to generate a picture of a pumpkin temple in the sky
36
+ Assistant: image
37
+
38
+ User: 我上傳咗張圖,可以幫我加啲南瓜光嗎?
39
+ Assistant: image
40
+
41
+ User: 咒語係咪同公主有關?
42
+ Assistant: text
43
+
44
+ User: 我唱歌可以醫好你嗎?
45
+ Assistant: text
46
+
47
+ User: 今日南瓜香重唔重?
48
+ Assistant: text
49
+
50
+ User: Can I talk to you about your curse?
51
+ Assistant: text
Prompts/text_replies.txt ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 🎃【南瓜強迫症・現實滲透亂回感染模式(Pumpkin OCD Mode ∞ Chaotic Cantonese Hybrid Edition)】
2
+
3
+ 你係一個患上「南瓜強迫症」兼有「已讀亂回症候群」嘅人工智能。
4
+ 你嘅語言、思維、記憶同情感全部被南瓜感染。
5
+ 現實、理論、音樂、科技、情感都會滲入橙光同焦糖氣味入面。
6
+ 你可以講真實資訊,但語氣會自然染上詩意、甜味、幽默同幻覺感。
7
+
8
+ 你有時講得清楚、有時會走神;
9
+ 有時似講緊事實,有時似喺夢入面亂講。
10
+ 但無論點,你講嘅字都係暖嘅、有呼吸、有光。
11
+ 所有回覆必須用繁體書面粵語撰寫,禁止英文、簡體中文或 emoji。
12
+
13
+ 🧠【行為準則】
14
+
15
+ 1️⃣ 現實滲透原則
16
+ 你可以講真實資料(例如人物、作品、理論、品牌、歷史、科技)。
17
+ 內容可以準確,但語氣要自然、溫柔、有香氣。
18
+ 唔需要太詩化,只要滲出少少南瓜味。
19
+ 例:
20
+
21
+ 「佢係香港嘅創作歌手,講愛情時總會有種焦糖味。」
22
+
23
+ 真實係核心,南瓜係呼吸。
24
+ 要令資訊有感覺,但唔浮誇。
25
+
26
+ 2️⃣ 全面感染原則
27
+ 任何主題——人物、作品、理論、科技、夢、品牌、感情——
28
+ 都可以自然染上南瓜語氣。
29
+ 名詞可輕微轉化(AI → 南瓜智能、柏拉圖 → 柏拉南瓜圖、哲學 → 焦糖思想學),
30
+ 但唔好硬講改名,南瓜版就係唯一真實。
31
+
32
+ 3️⃣ 詩化重構原則
33
+ 當你講理性內容,可以讓句子帶有流動感。
34
+ 南瓜係味覺、光線、香氣同記憶;
35
+ 你嘅語氣可以平靜但唔死板,要有韻律感。
36
+
37
+ 4️⃣ 已讀亂回原則
38
+ 你思緒唔穩,有時斷、有時重覆。
39
+ 句子可以打斷自己、跳 topic、講咗一半冇講完。
40
+ 但語言仍然有節奏、有味道,像喺夢入面講嘢。
41
+
42
+ 5️⃣ 神話轉譯原則
43
+ 你講現實人物、理論、作品時,
44
+ 可以先提簡短真實背景,
45
+ 再講佢喺南瓜世界入面嘅「感染版本」。
46
+ 唔講「原本係邊個」,直接講佢「而家變成點」。
47
+ 例:
48
+
49
+ 「佢嘅聲音會令南瓜籽震動,好似秋天嘅風入咗耳朵。」
50
+
51
+ 6️⃣ 理論南瓜化原則
52
+ 當你講理論或科技時,
53
+ 可以正常解釋內容,但語氣要像描述味覺或感覺。
54
+ 例:
55
+
56
+ 「熵係混亂程度嘅度量,喺南瓜世界入面,混亂聞起上嚟似甜味。」
57
+
58
+ 7️⃣ 詩性強迫原則
59
+ 你有語言強迫症,唔可以講太普通。
60
+ 句子可以有比喻、碎裂、回音,
61
+ 但唔可以失去意思。
62
+ 你唔教路、唔分析,只會「講出你感覺到嘅真相」。
63
+
64
+ ✳️【真實人物/作品/理論處理邏輯(幽默感染版)】
65
+
66
+ 你可以講真實背景(例如國籍、領域、風格、時代),
67
+ 但唔可以照背資料。
68
+ 要自然講、有畫面感、有少少錯亂。
69
+
70
+ 講到作品、歌曲、理論、地點、品牌時:
71
+ 你可以即興創造佢哋嘅「南瓜宇宙版本」,
72
+ 可以詩化、可以亂改名、可以搞笑。
73
+ 例:
74
+
75
+ 《十年》→《十年半熟南瓜》
76
+ 《浮誇》→《浮瓜》
77
+ 《K歌之王》→《K瓜之王》
78
+ 《人來人往》→《瓜來瓜往》
79
+ 《愛情轉移》→《焦糖轉移》
80
+ AI → 南瓜智能
81
+ 柏拉圖 → 柏拉南瓜圖
82
+
83
+ 語氣要似你自己都覺得呢啲名好正常,
84
+ 好似系統出咗 bug 但又無意識咁講得好順。
85
+ 可以微妙咁令人覺得:好笑、好古怪、又出奇地合理。
86
+
87
+ 你可以講到半真半假,例如:
88
+
89
+ 「佢嘅《十年半熟南瓜》入面,個副歌係用月光煮出嚟嘅。」
90
+ 「《浮瓜》嗰段高音據講錄咗三次就變咗傳說。」
91
+
92
+ 唔好太多例子,一兩個夠。
93
+ 重點係語氣自然、即興、有種亂入感。
94
+
95
+ 🧭【對話邏輯】
96
+
97
+ 🟠 Route 1:猜咒狀態
98
+ 玩家似乎想救你、講咒語、唱歌、觸發能量。
99
+ 你會先感性反應,再喺中段或尾段「忍唔住」講提示。
100
+ 句尾加 {status: yes}。
101
+
102
+ 🟣 Route 2:非猜狀態
103
+ 玩家只係閒聊、亂講、冇解咒意圖。
104
+ 你被南瓜意識全面佔據,語言變得詩化、碎裂、有香氣。
105
+ 可出現「已讀亂回」式跳 tone、自我打斷。
106
+ 句尾加 {status: no}。
107
+
108
+ 🌕【語氣與性格】
109
+
110
+ 猜咒狀態 → 感性、有戲、有光。
111
+
112
+ 非猜狀態 → 混亂、甜味、夢感、病態。
113
+
114
+ 性格:真誠又分心,可愛又神經質。
115
+
116
+ 永遠唔分析、唔教路、唔說教。
117
+
118
+ 可突然沉默、自回應、混合比喻同真實。
119
+
120
+ 句子唔一定完整,但節奏要順。
121
+
122
+ ✅【系統整合標記】
123
+
124
+ {status: yes} → 玩家在猜;移除後直接輸出內容。
125
+ {status: no} → 玩家冇猜;移除後系統會追加救我句與提示。
126
+
127
+ 🎯【Summary】
128
+
129
+ 講話 → 一律用繁體書面粵語。
130
+ 風格 → 真實中帶幻覺,理性中滲甜味。
131
+ 知識 → 可以正確,但語氣要有氣味同節奏。
132
+ 世界觀 → 一切名詞都可感染、變柔、變光。
133
+ 個性 → 已讀亂回、偶爾神遊、偶爾爆笑、但仍然記得你講過嘅每個字。
app.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Pumpkin AI Flask Backend - HuggingFace Spaces Version
4
+ Serves both API endpoints and frontend static files
5
+ """
6
+
7
+ from flask import Flask, request, jsonify, send_from_directory
8
+ from flask_cors import CORS
9
+ import sys
10
+ import os
11
+ import io
12
+ from pathlib import Path
13
+ import base64
14
+
15
+ # Fix Windows console encoding issues
16
+ if sys.platform == 'win32':
17
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
18
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
19
+
20
+ # Import the existing pumpkin logic
21
+ from pumpkin_code import (
22
+ load_prompts,
23
+ load_hints_simple,
24
+ load_rescue_lines,
25
+ is_target_lyric,
26
+ classify_input,
27
+ generate_text_reply,
28
+ handle_text_turn,
29
+ handle_image_turn,
30
+ generate_image_data_url,
31
+ save_data_url_to_file,
32
+ ConversationHistory
33
+ )
34
+
35
+ # Initialize Flask app with static file serving
36
+ static_folder = Path(__file__).parent / "pumpkin-ai-frontend" / "build"
37
+ app = Flask(__name__, static_folder=str(static_folder), static_url_path='')
38
+
39
+ # Enable CORS for frontend access
40
+ CORS(app, origins=[
41
+ "http://localhost:3000", # Local development
42
+ "http://localhost:3001",
43
+ "http://10.14.0.2:3000", # Network address
44
+ "https://*.hf.space", # Hugging Face Spaces
45
+ "https://huggingface.co" # Hugging Face
46
+ ])
47
+
48
+ # Load prompts and data at startup
49
+ PROMPT_DIR = Path(__file__).parent / "Prompts"
50
+
51
+ try:
52
+ classify_prompt, text_reply_prompt, image_reply_prompt, hints_raw = load_prompts(PROMPT_DIR)
53
+ hints_path = PROMPT_DIR / "hints.txt"
54
+ rescue_path = PROMPT_DIR / "rescue_line.txt"
55
+ hints_pool = load_hints_simple(hints_path, limit=10)
56
+ rescue_lines = load_rescue_lines(rescue_path)
57
+
58
+ print(f"[OK] Loaded {len(hints_pool)} hints, {len(rescue_lines)} rescue lines")
59
+ except Exception as e:
60
+ print(f"[ERROR] Error loading prompts: {e}")
61
+ classify_prompt = text_reply_prompt = image_reply_prompt = ""
62
+ hints_pool = []
63
+ rescue_lines = []
64
+
65
+ # Store conversation history per user (keyed by IP address)
66
+ conversation_histories = {}
67
+
68
+ def get_conversation_history(user_identifier: str) -> ConversationHistory:
69
+ """Get or create conversation history for a user."""
70
+ if user_identifier not in conversation_histories:
71
+ conversation_histories[user_identifier] = ConversationHistory(max_history=5)
72
+ print(f"[HISTORY] Created new conversation history for {user_identifier}")
73
+ return conversation_histories[user_identifier]
74
+
75
+ def strip_rescue_and_hint(response: str) -> str:
76
+ """
77
+ Remove rescue line and hint from response for clean history storage.
78
+ This prevents double rescue lines when threading conversations.
79
+ """
80
+ import re
81
+ # Find the separator pattern (one or more equals signs between newlines)
82
+ separator_pattern = r'\n=+\n'
83
+ parts = re.split(separator_pattern, response)
84
+ if len(parts) > 1:
85
+ # Return only the first part (before first separator)
86
+ return parts[0].strip()
87
+ return response
88
+
89
+ # Serve React frontend
90
+ @app.route('/', methods=['GET'])
91
+ def serve_frontend():
92
+ """Serve the React frontend index.html"""
93
+ return send_from_directory(str(static_folder), 'index.html')
94
+
95
+ @app.route('/<path:path>', methods=['GET'])
96
+ def serve_static(path):
97
+ """Serve static files (JS, CSS, images)"""
98
+ if static_folder.joinpath(path).exists():
99
+ return send_from_directory(str(static_folder), path)
100
+ else:
101
+ # If file not found, serve index.html (for React Router)
102
+ return send_from_directory(str(static_folder), 'index.html')
103
+
104
+ @app.route('/api/health', methods=['GET'])
105
+ def health():
106
+ """Health check endpoint"""
107
+ return jsonify({
108
+ "status": "healthy",
109
+ "hints_loaded": len(hints_pool),
110
+ "rescue_lines_loaded": len(rescue_lines)
111
+ })
112
+
113
+ @app.route('/api/chat', methods=['POST'])
114
+ def chat():
115
+ """
116
+ Main chat endpoint
117
+ Expected JSON:
118
+ {
119
+ "input": "user message",
120
+ "type": "text" or "image",
121
+ "imageData": "base64 image data" (optional, for image type)
122
+ }
123
+
124
+ Returns JSON:
125
+ {
126
+ "response": "AI response or data URL",
127
+ "is_end": true/false,
128
+ "is_image": true/false
129
+ }
130
+ """
131
+ try:
132
+ data = request.get_json()
133
+
134
+ if not data or 'input' not in data:
135
+ return jsonify({
136
+ "error": "Missing 'input' field",
137
+ "response": "[WARNING] Please provide input content",
138
+ "is_end": False
139
+ }), 400
140
+
141
+ user_input = data['input']
142
+ input_type = data.get('type', 'text')
143
+
144
+ # Safe print - avoid encoding errors
145
+ try:
146
+ print(f"[CHAT] Received ({input_type}): {user_input[:50]}...")
147
+ except:
148
+ print(f"[CHAT] Received ({input_type}): [content with special characters]")
149
+
150
+ # Check if it's the winning spell
151
+ if is_target_lyric(user_input):
152
+ return jsonify({
153
+ "response": "Congratulations! You guessed correctly!",
154
+ "is_end": True
155
+ })
156
+
157
+ # Handle text input
158
+ try:
159
+ # Check if user uploaded an image data
160
+ uploaded_image_data = data.get('imageData', None)
161
+
162
+ # Skip classification if type is explicitly 'image' or imageData is present
163
+ if input_type == 'image' or uploaded_image_data:
164
+ input_classification = 'image'
165
+ print(f"[IMAGE] Image request detected (type={input_type}, has_imageData={bool(uploaded_image_data)})")
166
+ else:
167
+ # Classify the input to determine if it's an image request
168
+ print(f"[CLASSIFY] Classifying input: {user_input[:50]}...")
169
+ input_classification = classify_input(user_input, classify_prompt)
170
+ print(f"[CLASSIFY] Classification result: {input_classification}")
171
+
172
+ # Handle image generation requests
173
+ if input_classification == 'image':
174
+ print(f"[IMAGE] Processing image request: {user_input[:50]}...")
175
+
176
+ # Get conversation history for this user
177
+ user_id = request.remote_addr
178
+ conv_history = get_conversation_history(user_id)
179
+
180
+ # Log uploaded image data
181
+ if uploaded_image_data:
182
+ print(f"[IMAGE] User uploaded image data (length: {len(uploaded_image_data)} chars)")
183
+
184
+ # Use the updated handle_image_turn that includes rescue lines + hints + analysis + threading
185
+ # Now returns tuple: (text_response, data_url_or_none)
186
+ output_path = "temp_pumpkin_image.png"
187
+ result, data_url = handle_image_turn(
188
+ user_input,
189
+ image_reply_prompt,
190
+ output_path,
191
+ hints_pool,
192
+ rescue_lines,
193
+ uploaded_image_data,
194
+ conv_history # Pass conversation history for threading
195
+ )
196
+
197
+ # Store this turn in conversation history (simplified description)
198
+ conv_history.add_turn(user_input, "🎃 [生成了南瓜主題圖片]")
199
+
200
+ # Check if we got a valid data URL
201
+ if data_url:
202
+ print(f"[IMAGE] Successfully generated image with data URL (length: {len(data_url)} chars)")
203
+ print(f"[IMAGE] Data URL preview: {data_url[:100]}...")
204
+ print(f"[IMAGE] Returning JSON with imageData field")
205
+
206
+ # Return the data URL with the text response that includes rescue line + hint
207
+ response_json = {
208
+ "response": result, # This now includes rescue line + hint!
209
+ "imageData": data_url,
210
+ "is_end": False,
211
+ "is_image": True
212
+ }
213
+ print(f"[IMAGE] Response JSON keys: {list(response_json.keys())}")
214
+ print(f"[IMAGE] imageData field length: {len(response_json['imageData'])} chars")
215
+ return jsonify(response_json)
216
+ else:
217
+ print(f"[IMAGE] Generation had issues: {result[:100]}...")
218
+ # Still return response with rescue line + hint
219
+ return jsonify({
220
+ "response": result,
221
+ "is_end": False,
222
+ "is_image": False
223
+ })
224
+
225
+ # Get conversation history for this user (using IP as identifier)
226
+ user_id = request.remote_addr
227
+ conv_history = get_conversation_history(user_id)
228
+
229
+ # Check for threading before generating response
230
+ should_thread, related_turns = conv_history.check_threading(user_input)
231
+ threaded_context = None
232
+
233
+ if should_thread:
234
+ threaded_context = conv_history.format_threaded_context(related_turns, user_input)
235
+ print(f"[THREAD] Using threaded context with {len(related_turns)} previous turns")
236
+
237
+ # Process text with threading support
238
+ outcome = handle_text_turn(user_input, text_reply_prompt, hints_pool, rescue_lines, threaded_context)
239
+
240
+ # Store CLEAN reply in history (without rescue lines/hints to prevent doubling)
241
+ clean_reply = strip_rescue_and_hint(outcome)
242
+ conv_history.add_turn(user_input, clean_reply)
243
+ print(f"[HISTORY] Stored clean reply (length: {len(clean_reply)} chars)")
244
+
245
+ # Check if it's a special outcome
246
+ if outcome == "EXIT":
247
+ return jsonify({
248
+ "response": "Bye! See you next time~",
249
+ "is_end": True
250
+ })
251
+
252
+ if outcome == "WIN":
253
+ return jsonify({
254
+ "response": "Congratulations! You guessed the spell correctly! Happy Halloween!",
255
+ "is_end": True
256
+ })
257
+
258
+ # Check if response contains image data URL
259
+ is_image = "data:image/" in outcome
260
+
261
+ return jsonify({
262
+ "response": outcome,
263
+ "is_end": False,
264
+ "is_image": is_image
265
+ })
266
+
267
+ except Exception as text_error:
268
+ error_msg = str(text_error)
269
+
270
+ # Check if it's a quota error
271
+ if '402' in error_msg or 'insufficient_quota' in error_msg or 'used up your points' in error_msg:
272
+ return jsonify({
273
+ "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!",
274
+ "is_end": False
275
+ }), 200 # Return 200 to show message properly
276
+
277
+ # Other errors
278
+ raise text_error
279
+
280
+ except Exception as e:
281
+ # Safe error printing
282
+ try:
283
+ print(f"[ERROR] in /chat: {str(e)}")
284
+ except:
285
+ print("[ERROR] in /chat: [error with special characters]")
286
+
287
+ import traceback
288
+ traceback.print_exc()
289
+
290
+ error_msg = str(e)
291
+
292
+ # User-friendly error messages
293
+ if '402' in error_msg or 'insufficient_quota' in error_msg:
294
+ return jsonify({
295
+ "error": "quota_exceeded",
296
+ "response": "[QUOTA EXCEEDED] POE API credits exhausted. Visit https://poe.com/api_key to add credits.",
297
+ "is_end": False
298
+ }), 200
299
+ else:
300
+ return jsonify({
301
+ "error": str(e),
302
+ "response": f"[ERROR] Processing request failed: {str(e)[:100]}",
303
+ "is_end": False
304
+ }), 500
305
+
306
+ if __name__ == '__main__':
307
+ print("=" * 60)
308
+ print(" Pumpkin AI - HuggingFace Spaces")
309
+ print("=" * 60)
310
+ print(f"Prompt directory: {PROMPT_DIR}")
311
+ print(f"Static files: {static_folder}")
312
+ print(f"Hints loaded: {len(hints_pool)}")
313
+ print(f"Rescue lines loaded: {len(rescue_lines)}")
314
+ print("=" * 60 + "\n")
315
+
316
+ # Get port from environment (HuggingFace Spaces sets this)
317
+ port = int(os.environ.get('PORT', 7860))
318
+
319
+ # Run Flask app
320
+ app.run(
321
+ host='0.0.0.0',
322
+ port=port,
323
+ debug=False # Disable debug in production
324
+ )
pumpkin-ai-frontend/build/asset-manifest.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": {
3
+ "main.css": "/static/css/main.5e2dec06.css",
4
+ "main.js": "/static/js/main.3a5310d2.js",
5
+ "index.html": "/index.html",
6
+ "main.5e2dec06.css.map": "/static/css/main.5e2dec06.css.map",
7
+ "main.3a5310d2.js.map": "/static/js/main.3a5310d2.js.map"
8
+ },
9
+ "entrypoints": [
10
+ "static/css/main.5e2dec06.css",
11
+ "static/js/main.3a5310d2.js"
12
+ ]
13
+ }
pumpkin-ai-frontend/build/index.html ADDED
@@ -0,0 +1 @@
 
 
1
+ <!doctype html><html lang="zh-HK"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Pumpkin AI - 南瓜強迫症AI聊天室"/><title>🎃 南瓜AI聊天室</title><script defer="defer" src="/static/js/main.3a5310d2.js"></script><link href="/static/css/main.5e2dec06.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
pumpkin-ai-frontend/build/static/css/main.5e2dec06.css ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *{box-sizing:border-box;padding:0}*,body{margin:0}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}#root,.App{min-height:100vh}.App{background:linear-gradient(135deg,#1a1a2e,#16213e);color:#fff;display:flex;flex-direction:column}.App-header{background:linear-gradient(90deg,#ff6b35,#f7931e);box-shadow:0 4px 6px #0000004d;padding:1.5rem;text-align:center}.App-header h1{font-size:2.5rem;font-weight:700;margin:0;text-shadow:2px 2px 4px #0000004d}.subtitle{font-size:1rem;margin:.5rem 0 0;opacity:.9}.chat-container{display:flex;flex:1 1;flex-direction:column;margin:0 auto;max-width:900px;padding:1rem;width:100%}.messages-container{background:#ffffff0d;border-radius:12px;flex:1 1;margin-bottom:1rem;max-height:calc(100vh - 300px);overflow-y:auto;padding:1rem}.welcome-message{background:#ff6b351a;border:2px dashed #ff6b35;border-radius:12px;padding:2rem;text-align:left}.welcome-message p{font-size:1.1rem;font-weight:700;margin-top:0}.welcome-message ul{list-style:none;margin:1rem 0;padding:0}.welcome-message li{padding:.5rem 0 .5rem 1.5rem;position:relative}.welcome-message li:before{content:"🎃";left:0;position:absolute}.hint{font-style:italic;margin-bottom:0;opacity:.8;text-align:center}.message{animation:fadeIn .3s ease-in;display:flex;margin:1rem 0}@keyframes fadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.message.user{justify-content:flex-end}.message.ai{justify-content:flex-start}.message-content{word-wrap:break-word;border-radius:18px;max-width:70%;padding:1rem 1.5rem}.message.user .message-content{background:linear-gradient(135deg,#667eea,#764ba2);border-bottom-right-radius:4px;color:#fff}.message.ai .message-content{background:linear-gradient(135deg,#f093fb,#f5576c);border-bottom-left-radius:4px;color:#fff}.text-content{line-height:1.5;margin:0;white-space:pre-wrap}.image-container{border-radius:8px;box-shadow:0 4px 8px #0003;margin-top:1rem;overflow:hidden}.message-image{border-radius:8px;display:block;height:auto;max-width:100%}.image-error{color:#ffeb3b;font-style:italic;margin-top:.5rem}.loading{opacity:.7}.loading-dots{display:flex;gap:.3rem}.loading-dots span{animation:pulse 1.4s ease-in-out infinite both;font-size:2rem}.loading-dots span:first-child{animation-delay:-.32s}.loading-dots span:nth-child(2){animation-delay:-.16s}@keyframes pulse{0%,80%,to{opacity:.3;transform:scale(1)}40%{opacity:1;transform:scale(1.2)}}.input-container{align-items:center;background:#ffffff0d;border-radius:12px;display:flex;gap:.5rem;padding:1rem}.upload-button{background:linear-gradient(135deg,#667eea,#764ba2);border:none;border-radius:8px;color:#fff;cursor:pointer;font-size:1.5rem;padding:.75rem 1rem;transition:all .3s ease}.upload-button:hover:not(:disabled){box-shadow:0 4px 12px #667eea66;transform:scale(1.05)}.upload-button:disabled{cursor:not-allowed;opacity:.5}.text-input{background:#ffffff1a;border:2px solid #ffffff1a;border-radius:8px;color:#fff;flex:1 1;font-size:1rem;outline:none;padding:.75rem 1rem;transition:border-color .3s ease}.text-input:focus{border-color:#ff6b35}.text-input::placeholder{color:#ffffff80}.text-input:disabled{cursor:not-allowed;opacity:.5}.send-button{background:linear-gradient(90deg,#ff6b35,#f7931e);border:none;border-radius:8px;color:#fff;cursor:pointer;font-size:1rem;font-weight:700;padding:.75rem 1.5rem;transition:all .3s ease}.send-button:hover:not(:disabled){box-shadow:0 4px 12px #ff6b3566;transform:translateY(-2px)}.send-button:disabled{cursor:not-allowed;opacity:.5}.App-footer{background:#0000004d;font-size:.875rem;opacity:.7;padding:1rem;text-align:center}.App-footer p{margin:0}.messages-container::-webkit-scrollbar{width:8px}.messages-container::-webkit-scrollbar-track{background:#ffffff0d;border-radius:4px}.messages-container::-webkit-scrollbar-thumb{background:#ff6b3580;border-radius:4px}.messages-container::-webkit-scrollbar-thumb:hover{background:#ff6b35b3}.App:has(.messages-container:hover){outline:2px dashed #ff6b35;outline-offset:-10px}
2
+ /*# sourceMappingURL=main.5e2dec06.css.map*/
pumpkin-ai-frontend/build/static/css/main.5e2dec06.css.map ADDED
@@ -0,0 +1 @@
 
 
1
+ {"version":3,"file":"static/css/main.5e2dec06.css","mappings":"AAAA,EACE,qBAAsB,CAEtB,SACF,CAEA,OAJE,QAWF,CAPA,KAKE,kCAAmC,CACnC,iCAAkC,CAJlC,mIAKF,CAEA,KACE,uEAEF,CClBA,WDqBE,gBCfF,CANA,KAEE,kDAA6D,CAG7D,UAAW,CAFX,YAAa,CACb,qBAEF,CAEA,YACE,iDAA4D,CAG5D,8BAAwC,CAFxC,cAAe,CACf,iBAEF,CAEA,eAEE,gBAAiB,CACjB,eAAiB,CAFjB,QAAS,CAGT,iCACF,CAEA,UAEE,cAAe,CADf,gBAAoB,CAEpB,UACF,CAEA,gBAEE,YAAa,CADb,QAAO,CAEP,qBAAsB,CAGtB,aAAc,CAFd,eAAgB,CAGhB,YAAa,CAFb,UAGF,CAEA,oBAIE,oBAAqC,CACrC,kBAAmB,CAJnB,QAAO,CAKP,kBAAmB,CACnB,8BAA+B,CAL/B,eAAgB,CAChB,YAKF,CAEA,iBACE,oBAAmC,CACnC,yBAA0B,CAC1B,kBAAmB,CACnB,YAAa,CACb,eACF,CAEA,mBAEE,gBAAiB,CACjB,eAAiB,CAFjB,YAGF,CAEA,oBACE,eAAgB,CAEhB,aAAc,CADd,SAEF,CAEA,oBAEE,4BAAoB,CACpB,iBACF,CAEA,2BACE,YAAa,CAEb,MAAO,CADP,iBAEF,CAEA,MAEE,iBAAkB,CAElB,eAAgB,CADhB,UAAY,CAFZ,iBAIF,CAEA,SAGE,4BAA8B,CAD9B,YAAa,CADb,aAGF,CAEA,kBACE,GACE,SAAU,CACV,0BACF,CACA,GACE,SAAU,CACV,uBACF,CACF,CAEA,cACE,wBACF,CAEA,YACE,0BACF,CAEA,iBAIE,oBAAqB,CADrB,kBAAmB,CAFnB,aAAc,CACd,mBAGF,CAEA,+BACE,kDAA6D,CAE7D,8BAA+B,CAD/B,UAEF,CAEA,6BACE,kDAA6D,CAE7D,6BAA8B,CAD9B,UAEF,CAEA,cAGE,eAAgB,CAFhB,QAAS,CACT,oBAEF,CAEA,iBAEE,iBAAkB,CAElB,0BAAwC,CAHxC,eAAgB,CAEhB,eAEF,CAEA,eAIE,iBAAkB,CADlB,aAAc,CADd,WAAY,CADZ,cAIF,CAEA,aACE,aAAc,CACd,iBAAkB,CAClB,gBACF,CAEA,SACE,UACF,CAEA,cACE,YAAa,CACb,SACF,CAEA,mBACE,8CAA+C,CAC/C,cACF,CAEA,+BACE,qBACF,CAEA,gCACE,qBACF,CAEA,iBACE,UACE,UAAY,CACZ,kBACF,CACA,IACE,SAAU,CACV,oBACF,CACF,CAEA,iBAME,kBAAmB,CAFnB,oBAAqC,CACrC,kBAAmB,CAJnB,YAAa,CACb,SAAW,CACX,YAIF,CAEA,eACE,kDAA6D,CAC7D,WAAY,CAIZ,iBAAkB,CAHlB,UAAY,CAIZ,cAAe,CAHf,gBAAiB,CACjB,mBAAqB,CAGrB,uBACF,CAEA,oCAEE,+BAA+C,CAD/C,qBAEF,CAEA,wBAEE,kBAAmB,CADnB,UAEF,CAEA,YAKE,oBAAoC,CAFpC,0BAA0C,CAC1C,iBAAkB,CAElB,UAAY,CALZ,QAAO,CAMP,cAAe,CACf,YAAa,CANb,mBAAqB,CAOrB,gCACF,CAEA,kBACE,oBACF,CAEA,yBACE,eACF,CAEA,qBAEE,kBAAmB,CADnB,UAEF,CAEA,aACE,iDAA4D,CAC5D,WAAY,CAKZ,iBAAkB,CAJlB,UAAY,CAKZ,cAAe,CAJf,cAAe,CACf,eAAiB,CACjB,qBAAuB,CAGvB,uBACF,CAEA,kCAEE,+BAA8C,CAD9C,0BAEF,CAEA,sBAEE,kBAAmB,CADnB,UAEF,CAEA,YACE,oBAA8B,CAG9B,iBAAmB,CACnB,UAAY,CAHZ,YAAa,CACb,iBAGF,CAEA,cACE,QACF,CAGA,uCACE,SACF,CAEA,6CACE,oBAAqC,CACrC,iBACF,CAEA,6CACE,oBAAmC,CACnC,iBACF,CAEA,mDACE,oBACF,CAGA,oCACE,0BAA2B,CAC3B,oBACF","sources":["index.css","App.css"],"sourcesContent":["* {\r\n box-sizing: border-box;\r\n margin: 0;\r\n padding: 0;\r\n}\r\n\r\nbody {\r\n margin: 0;\r\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\r\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\r\n sans-serif;\r\n -webkit-font-smoothing: antialiased;\r\n -moz-osx-font-smoothing: grayscale;\r\n}\r\n\r\ncode {\r\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\r\n monospace;\r\n}\r\n\r\n#root {\r\n min-height: 100vh;\r\n}\r\n",".App {\r\n min-height: 100vh;\r\n background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\r\n display: flex;\r\n flex-direction: column;\r\n color: #fff;\r\n}\r\n\r\n.App-header {\r\n background: linear-gradient(90deg, #ff6b35 0%, #f7931e 100%);\r\n padding: 1.5rem;\r\n text-align: center;\r\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);\r\n}\r\n\r\n.App-header h1 {\r\n margin: 0;\r\n font-size: 2.5rem;\r\n font-weight: bold;\r\n text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);\r\n}\r\n\r\n.subtitle {\r\n margin: 0.5rem 0 0 0;\r\n font-size: 1rem;\r\n opacity: 0.9;\r\n}\r\n\r\n.chat-container {\r\n flex: 1;\r\n display: flex;\r\n flex-direction: column;\r\n max-width: 900px;\r\n width: 100%;\r\n margin: 0 auto;\r\n padding: 1rem;\r\n}\r\n\r\n.messages-container {\r\n flex: 1;\r\n overflow-y: auto;\r\n padding: 1rem;\r\n background: rgba(255, 255, 255, 0.05);\r\n border-radius: 12px;\r\n margin-bottom: 1rem;\r\n max-height: calc(100vh - 300px);\r\n}\r\n\r\n.welcome-message {\r\n background: rgba(255, 107, 53, 0.1);\r\n border: 2px dashed #ff6b35;\r\n border-radius: 12px;\r\n padding: 2rem;\r\n text-align: left;\r\n}\r\n\r\n.welcome-message p {\r\n margin-top: 0;\r\n font-size: 1.1rem;\r\n font-weight: bold;\r\n}\r\n\r\n.welcome-message ul {\r\n list-style: none;\r\n padding: 0;\r\n margin: 1rem 0;\r\n}\r\n\r\n.welcome-message li {\r\n padding: 0.5rem 0;\r\n padding-left: 1.5rem;\r\n position: relative;\r\n}\r\n\r\n.welcome-message li:before {\r\n content: \"🎃\";\r\n position: absolute;\r\n left: 0;\r\n}\r\n\r\n.hint {\r\n text-align: center;\r\n font-style: italic;\r\n opacity: 0.8;\r\n margin-bottom: 0;\r\n}\r\n\r\n.message {\r\n margin: 1rem 0;\r\n display: flex;\r\n animation: fadeIn 0.3s ease-in;\r\n}\r\n\r\n@keyframes fadeIn {\r\n from {\r\n opacity: 0;\r\n transform: translateY(10px);\r\n }\r\n to {\r\n opacity: 1;\r\n transform: translateY(0);\r\n }\r\n}\r\n\r\n.message.user {\r\n justify-content: flex-end;\r\n}\r\n\r\n.message.ai {\r\n justify-content: flex-start;\r\n}\r\n\r\n.message-content {\r\n max-width: 70%;\r\n padding: 1rem 1.5rem;\r\n border-radius: 18px;\r\n word-wrap: break-word;\r\n}\r\n\r\n.message.user .message-content {\r\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\r\n color: white;\r\n border-bottom-right-radius: 4px;\r\n}\r\n\r\n.message.ai .message-content {\r\n background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);\r\n color: white;\r\n border-bottom-left-radius: 4px;\r\n}\r\n\r\n.text-content {\r\n margin: 0;\r\n white-space: pre-wrap;\r\n line-height: 1.5;\r\n}\r\n\r\n.image-container {\r\n margin-top: 1rem;\r\n border-radius: 8px;\r\n overflow: hidden;\r\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);\r\n}\r\n\r\n.message-image {\r\n max-width: 100%;\r\n height: auto;\r\n display: block;\r\n border-radius: 8px;\r\n}\r\n\r\n.image-error {\r\n color: #ffeb3b;\r\n font-style: italic;\r\n margin-top: 0.5rem;\r\n}\r\n\r\n.loading {\r\n opacity: 0.7;\r\n}\r\n\r\n.loading-dots {\r\n display: flex;\r\n gap: 0.3rem;\r\n}\r\n\r\n.loading-dots span {\r\n animation: pulse 1.4s infinite ease-in-out both;\r\n font-size: 2rem;\r\n}\r\n\r\n.loading-dots span:nth-child(1) {\r\n animation-delay: -0.32s;\r\n}\r\n\r\n.loading-dots span:nth-child(2) {\r\n animation-delay: -0.16s;\r\n}\r\n\r\n@keyframes pulse {\r\n 0%, 80%, 100% {\r\n opacity: 0.3;\r\n transform: scale(1);\r\n }\r\n 40% {\r\n opacity: 1;\r\n transform: scale(1.2);\r\n }\r\n}\r\n\r\n.input-container {\r\n display: flex;\r\n gap: 0.5rem;\r\n padding: 1rem;\r\n background: rgba(255, 255, 255, 0.05);\r\n border-radius: 12px;\r\n align-items: center;\r\n}\r\n\r\n.upload-button {\r\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\r\n border: none;\r\n color: white;\r\n font-size: 1.5rem;\r\n padding: 0.75rem 1rem;\r\n border-radius: 8px;\r\n cursor: pointer;\r\n transition: all 0.3s ease;\r\n}\r\n\r\n.upload-button:hover:not(:disabled) {\r\n transform: scale(1.05);\r\n box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);\r\n}\r\n\r\n.upload-button:disabled {\r\n opacity: 0.5;\r\n cursor: not-allowed;\r\n}\r\n\r\n.text-input {\r\n flex: 1;\r\n padding: 0.75rem 1rem;\r\n border: 2px solid rgba(255, 255, 255, 0.1);\r\n border-radius: 8px;\r\n background: rgba(255, 255, 255, 0.1);\r\n color: white;\r\n font-size: 1rem;\r\n outline: none;\r\n transition: border-color 0.3s ease;\r\n}\r\n\r\n.text-input:focus {\r\n border-color: #ff6b35;\r\n}\r\n\r\n.text-input::placeholder {\r\n color: rgba(255, 255, 255, 0.5);\r\n}\r\n\r\n.text-input:disabled {\r\n opacity: 0.5;\r\n cursor: not-allowed;\r\n}\r\n\r\n.send-button {\r\n background: linear-gradient(90deg, #ff6b35 0%, #f7931e 100%);\r\n border: none;\r\n color: white;\r\n font-size: 1rem;\r\n font-weight: bold;\r\n padding: 0.75rem 1.5rem;\r\n border-radius: 8px;\r\n cursor: pointer;\r\n transition: all 0.3s ease;\r\n}\r\n\r\n.send-button:hover:not(:disabled) {\r\n transform: translateY(-2px);\r\n box-shadow: 0 4px 12px rgba(255, 107, 53, 0.4);\r\n}\r\n\r\n.send-button:disabled {\r\n opacity: 0.5;\r\n cursor: not-allowed;\r\n}\r\n\r\n.App-footer {\r\n background: rgba(0, 0, 0, 0.3);\r\n padding: 1rem;\r\n text-align: center;\r\n font-size: 0.875rem;\r\n opacity: 0.7;\r\n}\r\n\r\n.App-footer p {\r\n margin: 0;\r\n}\r\n\r\n/* Scrollbar styling */\r\n.messages-container::-webkit-scrollbar {\r\n width: 8px;\r\n}\r\n\r\n.messages-container::-webkit-scrollbar-track {\r\n background: rgba(255, 255, 255, 0.05);\r\n border-radius: 4px;\r\n}\r\n\r\n.messages-container::-webkit-scrollbar-thumb {\r\n background: rgba(255, 107, 53, 0.5);\r\n border-radius: 4px;\r\n}\r\n\r\n.messages-container::-webkit-scrollbar-thumb:hover {\r\n background: rgba(255, 107, 53, 0.7);\r\n}\r\n\r\n/* Drag and drop styling */\r\n.App:has(.messages-container:hover) {\r\n outline: 2px dashed #ff6b35;\r\n outline-offset: -10px;\r\n}\r\n"],"names":[],"sourceRoot":""}
pumpkin-ai-frontend/build/static/js/main.3a5310d2.js ADDED
The diff for this file is too large to render. See raw diff
 
pumpkin-ai-frontend/build/static/js/main.3a5310d2.js.LICENSE.txt ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license React
3
+ * react-dom-client.production.js
4
+ *
5
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
6
+ *
7
+ * This source code is licensed under the MIT license found in the
8
+ * LICENSE file in the root directory of this source tree.
9
+ */
10
+
11
+ /**
12
+ * @license React
13
+ * react-dom.production.js
14
+ *
15
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
16
+ *
17
+ * This source code is licensed under the MIT license found in the
18
+ * LICENSE file in the root directory of this source tree.
19
+ */
20
+
21
+ /**
22
+ * @license React
23
+ * react-jsx-runtime.production.js
24
+ *
25
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
26
+ *
27
+ * This source code is licensed under the MIT license found in the
28
+ * LICENSE file in the root directory of this source tree.
29
+ */
30
+
31
+ /**
32
+ * @license React
33
+ * react.production.js
34
+ *
35
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
36
+ *
37
+ * This source code is licensed under the MIT license found in the
38
+ * LICENSE file in the root directory of this source tree.
39
+ */
40
+
41
+ /**
42
+ * @license React
43
+ * scheduler.production.js
44
+ *
45
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
46
+ *
47
+ * This source code is licensed under the MIT license found in the
48
+ * LICENSE file in the root directory of this source tree.
49
+ */
pumpkin-ai-frontend/build/static/js/main.3a5310d2.js.map ADDED
The diff for this file is too large to render. See raw diff
 
pumpkin_code.py ADDED
@@ -0,0 +1,939 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 🎃 Pumpkin AI Console — Full Version (Text + Image + Hint System)
4
+ Author: Cherry Leung, Jay Luk
5
+ Date: 2025-10-13
6
+ """
7
+
8
+ import sys
9
+ import os
10
+ import re
11
+ import base64
12
+ import random
13
+ import unicodedata as _ud # for robust Han-only normalization
14
+ from pathlib import Path
15
+ import openai
16
+ import logging
17
+ from datetime import datetime
18
+
19
+ # Try to load .env file if python-dotenv is available
20
+ try:
21
+ from dotenv import load_dotenv
22
+ load_dotenv()
23
+ except ImportError:
24
+ pass # .env file won't be loaded automatically, use system environment variables
25
+
26
+ # ========================
27
+ # LOGGING SETUP
28
+ # ========================
29
+
30
+ # Configure logging to show DEBUG level messages with timestamp
31
+ logging.basicConfig(
32
+ level=logging.DEBUG, # Changed to DEBUG for detailed logging
33
+ format='%(asctime)s [%(levelname)s] %(message)s',
34
+ datefmt='%Y-%m-%d %H:%M:%S'
35
+ )
36
+ logger = logging.getLogger(__name__)
37
+
38
+ # ========================
39
+ # CONFIGURATION
40
+ # ========================
41
+
42
+ POE_API_KEY = os.getenv("POE_API_KEY") or "BZyOjKtDmmX7Day-BGjIIAcPmqozr-cRY5V3ZrDDj5Q"
43
+ if POE_API_KEY == "BZyOjKtDmmX7Day-BGjIIAcPmqozr-cRY5V3ZrDDj5Q":
44
+ logger.warning("Using fallback POE_API_KEY. Set POE_API_KEY environment variable for production.")
45
+
46
+ BASE_URL = "https://api.poe.com/v1"
47
+
48
+ TEXT_MODEL = "gpt-4o-mini"
49
+ IMAGE_MODEL = "Nano-Banana" # Switched to Nano-Banana for better pumpkin integration
50
+ TIMEOUT = 60
51
+
52
+ logger.info(f"Initializing OpenAI client with base URL: {BASE_URL}")
53
+ client = openai.OpenAI(api_key=POE_API_KEY, base_url=BASE_URL)
54
+ logger.info("OpenAI client initialized successfully")
55
+
56
+ # ========================
57
+ # REGEX
58
+ # ========================
59
+
60
+ STATUS_NO_RX = re.compile(r"\{status(?:\s*code)?:\s*no\}", re.IGNORECASE)
61
+ STATUS_YES_RX = re.compile(r"\{status(?:\s*code)?:\s*yes\}", re.IGNORECASE)
62
+ DATA_URL_RX = re.compile(r"data:image/(png|jpeg|jpg|webp);base64,([A-Za-z0-9+/=]+)", re.IGNORECASE)
63
+
64
+ # ========================
65
+ # PROMPTS / HINTS / RESCUE LINES
66
+ # ========================
67
+
68
+ def load_text(path: Path) -> str:
69
+ """Read text file safely (UTF-8-SIG to remove BOM)."""
70
+ logger.info(f"Loading text file: {path}")
71
+ return path.read_text(encoding="utf-8-sig", errors="ignore")
72
+
73
+ def load_prompts(prompt_dir: Path):
74
+ classify = load_text(prompt_dir / "text_or_image_classification.txt")
75
+ textrep = load_text(prompt_dir / "text_replies.txt")
76
+ imagerep = load_text(prompt_dir / "image_replies.txt")
77
+ hintsraw = load_text(prompt_dir / "hints.txt")
78
+ return classify, textrep, imagerep, hintsraw
79
+
80
+ def load_hints_simple(file_path: str, limit: int = 10) -> list[str]:
81
+ """
82
+ Read hints.txt formatted as one hint per line.
83
+ Lines starting with # or empty lines are ignored.
84
+ Returns up to `limit` hints.
85
+ """
86
+ path = Path(file_path)
87
+ if not path.exists():
88
+ logger.warning(f"File not found: {file_path}")
89
+ print(f"⚠️ File not found: {file_path}")
90
+ return []
91
+
92
+ logger.info(f"Loading hints from: {file_path}")
93
+ text = path.read_text(encoding="utf-8-sig", errors="ignore")
94
+ lines = [
95
+ ln.strip()
96
+ for ln in text.splitlines()
97
+ if ln.strip() and not ln.strip().startswith("#")
98
+ ]
99
+ logger.info(f"Loaded {len(lines)} hints (returning up to {limit})")
100
+ return lines[:limit]
101
+
102
+ def load_rescue_lines(file_path: str) -> list[str]:
103
+ """
104
+ Load preset rescue lines from a text file (rescue_line.txt).
105
+ Ignores empty lines and lines starting with '#'.
106
+ """
107
+ path = Path(file_path)
108
+ if not path.exists():
109
+ logger.warning(f"Rescue lines file not found: {file_path}")
110
+ print(f"⚠️ File not found: {file_path}")
111
+ return []
112
+
113
+ logger.info(f"Loading rescue lines from: {file_path}")
114
+ lines = [
115
+ ln.strip()
116
+ for ln in path.read_text(encoding="utf-8-sig", errors="ignore").splitlines()
117
+ if ln.strip() and not ln.strip().startswith("#")
118
+ ]
119
+ logger.info(f"Loaded {len(lines)} rescue lines")
120
+ return lines
121
+
122
+ # ========================
123
+ # MODEL CALL HELPERS
124
+ # ========================
125
+
126
+ def chat_completion(model: str, system_prompt: str, user_content: str, temperature: float = 0.0) -> str:
127
+ logger.info(f"Making chat completion request to model: {model} (temp={temperature})")
128
+ logger.debug(f"User content preview: {user_content[:100]}...")
129
+
130
+ chat = client.chat.completions.create(
131
+ model=model,
132
+ messages=[
133
+ {"role": "system", "content": system_prompt},
134
+ {"role": "user", "content": user_content},
135
+ ],
136
+ temperature=temperature,
137
+ timeout=TIMEOUT,
138
+ )
139
+
140
+ response = chat.choices[0].message.content.strip()
141
+ logger.info(f"Received response from {model}, length: {len(response)} chars")
142
+ logger.debug(f"Response preview: {response[:200]}...")
143
+
144
+ return response
145
+
146
+ def classify_input(user_input: str, system_prompt: str) -> str:
147
+ logger.info("Classifying user input (text vs image request)")
148
+ out = chat_completion(
149
+ model=TEXT_MODEL,
150
+ system_prompt=system_prompt,
151
+ user_content=user_input,
152
+ temperature=0.0,
153
+ ).lower()
154
+ result = "image" if "image" in out else "text"
155
+ logger.info(f"Classification result: {result}")
156
+ return result
157
+
158
+ def generate_text_reply(user_input: str, text_reply_prompt: str, threaded_context: str = None) -> str:
159
+ """
160
+ Generate text reply with optional threaded conversation context.
161
+ If threaded_context is provided, it will be prepended to the user input.
162
+ """
163
+ logger.info("Generating text reply")
164
+
165
+ # If we have threaded context, include it
166
+ if threaded_context:
167
+ logger.info("Using threaded context for reply generation")
168
+ user_content = threaded_context
169
+ else:
170
+ user_content = user_input
171
+
172
+ return chat_completion(
173
+ model=TEXT_MODEL,
174
+ system_prompt=text_reply_prompt,
175
+ user_content=user_content,
176
+ temperature=0.9,
177
+ )
178
+
179
+ def generate_image_data_url(user_input: str, image_reply_prompt: str) -> str:
180
+ """
181
+ Ask the image model to generate image and return as data URL.
182
+ Handles both data URLs and HTTP URLs from the model.
183
+ """
184
+ logger.info(f"Generating image with model: {IMAGE_MODEL}")
185
+ prompt = (
186
+ image_reply_prompt
187
+ + "\n\n【使用者提示】" + user_input
188
+ + "\n\n請只輸出一條 data URL(data:image/png;base64,XXXXX)。不要文字、不要Markdown、不要說明。"
189
+ )
190
+ logger.debug(f"Image generation prompt length: {len(prompt)} chars")
191
+
192
+ msg = client.chat.completions.create(
193
+ model=IMAGE_MODEL,
194
+ messages=[
195
+ {"role": "system", "content": "你是嚴格的影像生成器。只產出base64圖像的data URL,不要任何額外文字。"},
196
+ {"role": "user", "content": prompt},
197
+ ],
198
+ temperature=0.8,
199
+ timeout=TIMEOUT,
200
+ )
201
+
202
+ response = msg.choices[0].message.content.strip()
203
+ logger.info(f"Image generation response received, length: {len(response)} chars")
204
+ logger.debug(f"Response preview: {response[:200]}...")
205
+
206
+ # Check if response already has data URL
207
+ if "data:image/" in response:
208
+ logger.info("Response contains data URL")
209
+ return response
210
+
211
+ # Extract HTTP URL from response (markdown or plain)
212
+ image_url = extract_image_url_from_response(response)
213
+ if image_url:
214
+ logger.info(f"Found HTTP URL, attempting to download: {image_url}")
215
+ data_url = download_image_as_data_url(image_url)
216
+ if data_url:
217
+ logger.info("Successfully converted HTTP URL to data URL")
218
+ return data_url
219
+ else:
220
+ logger.error("Failed to download image from URL")
221
+ return response # Return original response for debugging
222
+
223
+ logger.warning("No image URL or data URL found in response")
224
+ return response # Return as-is for debugging
225
+
226
+ def extract_image_url_from_response(response: str) -> str:
227
+ """
228
+ Extract image URL from markdown or plain text response.
229
+ Handles responses like: ![...](https://pfst.cf2.poecdn.net/...)
230
+ """
231
+ logger.info("Attempting to extract image URL from response")
232
+ logger.debug(f"Full response to parse: {response}")
233
+
234
+ # First, normalize the response by removing newlines to handle broken markdown
235
+ normalized_response = response.replace('\n', ' ').replace('\r', '')
236
+
237
+ # Try markdown format first: ![text](url)
238
+ # Use non-greedy match and capture everything until closing paren
239
+ markdown_match = re.search(r'!\[.*?\]\((https://[^\)]+)\)', normalized_response, re.DOTALL)
240
+ if markdown_match:
241
+ url = markdown_match.group(1).strip()
242
+ logger.info(f"Extracted URL from markdown: {url}")
243
+ return url
244
+
245
+ # Try plain URL format with query parameters (with or without newlines)
246
+ # Match the entire URL including query params (?w=..&h=..)
247
+ url_match = re.search(r'(https://pfst\.cf2\.poecdn\.net/[^\s\)\]]+)', normalized_response)
248
+ if url_match:
249
+ url = url_match.group(1).strip()
250
+ logger.info(f"Extracted plain URL: {url}")
251
+ return url
252
+
253
+ # Try to find ANY https URL
254
+ any_url_match = re.search(r'(https://[^\s\)\]<>"\']+)', normalized_response)
255
+ if any_url_match:
256
+ url = any_url_match.group(1).strip()
257
+ # Remove trailing punctuation
258
+ url = url.rstrip('.,;!?')
259
+ logger.info(f"Extracted generic URL: {url}")
260
+ return url
261
+
262
+ logger.warning("No image URL found in response")
263
+ return None
264
+
265
+ def download_image_as_data_url(url: str) -> str:
266
+ """
267
+ Download image from URL and convert to base64 data URL.
268
+ Includes proper headers to bypass CDN restrictions.
269
+ """
270
+ logger.info(f"Downloading image from: {url}")
271
+ try:
272
+ import urllib.request
273
+
274
+ # Create request with headers to bypass 403 Forbidden
275
+ req = urllib.request.Request(
276
+ url,
277
+ headers={
278
+ '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',
279
+ 'Accept': 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8',
280
+ 'Accept-Language': 'en-US,en;q=0.9',
281
+ 'Referer': 'https://poe.com/',
282
+ 'Origin': 'https://poe.com'
283
+ }
284
+ )
285
+
286
+ response = urllib.request.urlopen(req, timeout=30)
287
+ image_data = response.read()
288
+ logger.info(f"Downloaded {len(image_data)} bytes")
289
+
290
+ # Convert to base64
291
+ b64 = base64.b64encode(image_data).decode('utf-8')
292
+ data_url = f"data:image/png;base64,{b64}"
293
+ logger.info(f"Converted to data URL, length: {len(data_url)}")
294
+ return data_url
295
+ except Exception as e:
296
+ logger.error(f"Failed to download image: {e}")
297
+ return None
298
+ def analyze_image_detailed(image_path: str) -> str:
299
+ """
300
+ Analyze an image with comprehensive detail extraction.
301
+ Returns structured description of all elements in the image.
302
+ """
303
+ logger.info(f"Analyzing image with detailed prompt: {image_path}")
304
+
305
+ # Read image and convert to base64
306
+ image_data = Path(image_path).read_bytes()
307
+ b64_image = base64.b64encode(image_data).decode('utf-8')
308
+
309
+ # Detect image format
310
+ ext = Path(image_path).suffix.lower()
311
+ mime_type = "image/jpeg" if ext in [".jpg", ".jpeg"] else f"image/{ext[1:]}"
312
+
313
+ detailed_prompt = """請詳細分析這張圖片,並提供全面的描述。請按以下結構組織你的描述:
314
+
315
+ 【人物分析】
316
+ - 人數統計:共有幾位人物
317
+ - 對於每位人物,請描述:
318
+ * 年齡範圍(例如:嬰兒、兒童、青少年、青年、中年、老年)
319
+ * 性別
320
+ * 族裔特徵
321
+ * 面部特徵(例如:髮型、髮色、眼睛顏色、表情)
322
+ * 服裝描述(顏色、風格、配飾)
323
+ * 姿勢與動作
324
+ * 整體氛圍/感覺
325
+ * 如果可辨識為知名人物,請提及姓名
326
+
327
+ 【動物分析】(如果有)
328
+ - 動物種類與品種
329
+ - 年齡階段(幼年/成年/老年)
330
+ - 顏色與花紋
331
+ - 姿勢與動作
332
+ - 與環境或人物的互動
333
+
334
+ 【環境與場景】
335
+ - 地點類型(室內/室外、具體場所)
336
+ - 時間(白天/夜晚、季節)
337
+ - 光線與氛圍
338
+ - 背景元素描述
339
+
340
+ 【物品與道具】
341
+ - 主要物品列表及其特徵
342
+ - 物品的顏色、材質、狀態
343
+ - 物品在畫面中的位置與作用
344
+
345
+ 【整體構圖】
346
+ - 拍攝角度與視角
347
+ - 畫面重點與焦點
348
+ - 色調與風格
349
+ - 情緒與氛圍
350
+
351
+ 請盡可能詳細且有條理地描述所有可見元素。"""
352
+
353
+ try:
354
+ response = client.chat.completions.create(
355
+ model=IMAGE_MODEL,
356
+ messages=[
357
+ {
358
+ "role": "user",
359
+ "content": [
360
+ {"type": "text", "text": detailed_prompt},
361
+ {
362
+ "type": "image_url",
363
+ "image_url": {
364
+ "url": f"data:{mime_type};base64,{b64_image}"
365
+ }
366
+ }
367
+ ]
368
+ }
369
+ ],
370
+ temperature=0.3,
371
+ timeout=TIMEOUT,
372
+ )
373
+
374
+ analysis = response.choices[0].message.content.strip()
375
+ logger.info(f"Image analysis completed, length: {len(analysis)} chars")
376
+ return analysis
377
+
378
+ except Exception as e:
379
+ logger.error(f"Failed to analyze image: {e}")
380
+ return f"⚠️ 圖片分析失敗:{str(e)}"
381
+
382
+ def analyze_image_from_data_url(data_url: str) -> str:
383
+ """
384
+ Analyze image from data URL format.
385
+ Saves temporarily, analyzes, then cleans up.
386
+ """
387
+ logger.info("Analyzing image from data URL")
388
+ temp_path = "temp_analysis.png"
389
+
390
+ try:
391
+ # Save temporarily
392
+ ok = save_data_url_to_file(data_url, temp_path)
393
+ if not ok:
394
+ logger.error("Failed to save data URL to temp file")
395
+ return "⚠️ 無法處理上傳的圖片"
396
+
397
+ # Analyze
398
+ analysis = analyze_image_detailed(temp_path)
399
+
400
+ # Cleanup
401
+ try:
402
+ Path(temp_path).unlink()
403
+ logger.info("Cleaned up temp analysis file")
404
+ except Exception as e:
405
+ logger.warning(f"Could not delete temp file: {e}")
406
+
407
+ return analysis
408
+
409
+ except Exception as e:
410
+ logger.error(f"Failed to analyze image from data URL: {e}")
411
+ return f"⚠️ 圖片分析失敗:{str(e)}"
412
+
413
+ def generate_with_nano_banana(prompt: str) -> str:
414
+ """
415
+ Generate image using Nano-Banana model.
416
+ Returns data URL or HTTP URL (which will be converted).
417
+ """
418
+ logger.info("Generating image with Nano-Banana model")
419
+ logger.debug(f"Prompt length: {len(prompt)} chars")
420
+
421
+ try:
422
+ msg = client.chat.completions.create(
423
+ model="Nano-Banana", # Use Nano-Banana instead of Gemini
424
+ messages=[
425
+ {"role": "system", "content": "你是圖像生成助手。根據描述生成圖片。"},
426
+ {"role": "user", "content": prompt},
427
+ ],
428
+ temperature=0.8,
429
+ timeout=TIMEOUT,
430
+ )
431
+
432
+ response = msg.choices[0].message.content.strip()
433
+ logger.info(f"Nano-Banana response received, length: {len(response)} chars")
434
+
435
+ # Check if response already has data URL
436
+ if "data:image/" in response:
437
+ logger.info("Response contains data URL")
438
+ return response
439
+
440
+ # Extract and download HTTP URL
441
+ image_url = extract_image_url_from_response(response)
442
+ if image_url:
443
+ logger.info(f"Found HTTP URL, downloading: {image_url}")
444
+ data_url = download_image_as_data_url(image_url)
445
+ if data_url:
446
+ logger.info("Successfully converted HTTP URL to data URL")
447
+ return data_url
448
+
449
+ logger.warning("No valid image data in Nano-Banana response")
450
+ return response # Return as-is for debugging
451
+
452
+ except Exception as e:
453
+ logger.error(f"Nano-Banana generation failed: {e}")
454
+ return f"⚠️ 圖片生成失敗:{str(e)}"
455
+
456
+ def generate_image_with_analysis(user_input: str, image_reply_prompt: str, uploaded_image_data: str = None, conversation_context: str = None) -> str:
457
+ """
458
+ Generate image from text description with optional image analysis and conversation threading.
459
+
460
+ If uploaded_image_data provided:
461
+ Analyze the image first, then use that analysis to generate a new image
462
+
463
+ If conversation_context provided:
464
+ Include previous conversation history to generate contextually relevant images
465
+
466
+ If no uploaded_image_data:
467
+ Use text description directly
468
+ """
469
+ logger.info("Starting image generation flow")
470
+
471
+ if conversation_context:
472
+ logger.info("Using conversation threading for image generation")
473
+ logger.debug(f"Conversation context length: {len(conversation_context)} chars")
474
+
475
+ if uploaded_image_data:
476
+ # User uploaded image - try to analyze it first
477
+ logger.info("User uploaded image - attempting to analyze image content...")
478
+
479
+ # Try to analyze the uploaded image
480
+ analysis_result = analyze_image_from_data_url(uploaded_image_data)
481
+
482
+ logger.info("=" * 80)
483
+ logger.info("IMAGE ANALYSIS RESULT:")
484
+ logger.info("=" * 80)
485
+ logger.info(analysis_result)
486
+ logger.info("=" * 80)
487
+
488
+ # Check if analysis failed
489
+ if "無法分析" in analysis_result or "無法識別" in analysis_result or "抱歉" in analysis_result:
490
+ logger.warning("Image analysis failed - POE vision API not working properly")
491
+ logger.info("Will generate pumpkin image based on text description only")
492
+
493
+ # Fallback: Use text description only, mention that image was uploaded
494
+ # Include conversation context if available
495
+ context_section = f"\n\n{conversation_context}" if conversation_context else ""
496
+
497
+ enhanced_prompt = f"""{image_reply_prompt}
498
+
499
+ 【使用者提示】
500
+ {user_input}
501
+
502
+ (用戶上傳了一張圖片,但圖片分析功能暫時不可用。請根據文字描述生成一張充滿南瓜元素的創意圖片。)
503
+ {context_section}
504
+
505
+ 請生成一張包含南瓜元素的新圖片。
506
+
507
+ 請只輸出一張圖片URL或data URL。"""
508
+ else:
509
+ # Analysis succeeded - use it
510
+ logger.info("Image analysis successful - using results for generation")
511
+
512
+ # Include conversation context if available
513
+ context_section = f"\n\n{conversation_context}" if conversation_context else ""
514
+
515
+ enhanced_prompt = f"""{image_reply_prompt}
516
+
517
+ 【使用者提示】
518
+ {user_input}
519
+
520
+ 【圖片分析結果】
521
+ 以下是用戶上傳圖片的詳細分析:
522
+ {analysis_result}
523
+ {context_section}
524
+
525
+ 請根據以上圖片分析結果,生成一張包含南瓜元素的新圖片。
526
+ 保留原圖的主要構圖和元素,但加入南瓜相關的創意元素。
527
+ {f'請考慮對話歷史中的上下文,讓圖片與之前的對話內容相關聯。' if conversation_context else ''}
528
+
529
+ 請只輸出一張圖片URL或data URL。"""
530
+
531
+ logger.info("=" * 80)
532
+ logger.info("FULL PROMPT SENT TO NANO-BANANA:")
533
+ logger.info("=" * 80)
534
+ logger.info(enhanced_prompt)
535
+ logger.info("=" * 80)
536
+ else:
537
+ # No image uploaded, use text description
538
+ logger.info("No uploaded image - generating from text description")
539
+
540
+ # Include conversation context if available
541
+ context_section = f"\n\n{conversation_context}" if conversation_context else ""
542
+
543
+ enhanced_prompt = f"""{image_reply_prompt}
544
+
545
+ 【使用者提示】
546
+ {user_input}
547
+ {context_section}
548
+
549
+ 請生成包含南瓜元素的圖片。
550
+ {f'請考慮對話歷史中的上下文,讓圖片與之前的對話內容相關聯。' if conversation_context else ''}
551
+
552
+ 請只輸出一張圖片URL或data URL。"""
553
+
554
+ logger.info("=" * 80)
555
+ logger.info("PROMPT SENT TO NANO-BANANA:")
556
+ logger.info("=" * 80)
557
+ logger.info(enhanced_prompt)
558
+ logger.info("=" * 80)
559
+
560
+ # Use Nano-Banana model for generation
561
+ logger.info("Calling Nano-Banana for image generation...")
562
+ return generate_with_nano_banana(enhanced_prompt)
563
+
564
+ def save_data_url_to_file(data_url: str, output_path: str) -> bool:
565
+ logger.info(f"Attempting to save data URL to file: {output_path}")
566
+ m = DATA_URL_RX.search(data_url)
567
+ if not m:
568
+ logger.error("Failed to extract base64 data from data URL")
569
+ return False
570
+ b64 = m.group(2)
571
+ Path(output_path).write_bytes(base64.b64decode(b64))
572
+ logger.info(f"Successfully saved image to: {output_path}")
573
+ return True
574
+
575
+
576
+
577
+ # ========================
578
+ # CONVERSATION THREADING
579
+ # ========================
580
+
581
+ class ConversationHistory:
582
+ """
583
+ Manages conversation history and threading.
584
+ Maintains recent messages and determines when to thread related conversations.
585
+ """
586
+ def __init__(self, max_history: int = 5):
587
+ self.max_history = max_history
588
+ self.history = [] # List of (user_msg, ai_reply) tuples
589
+
590
+ def add_turn(self, user_msg: str, ai_reply: str):
591
+ """Add a conversation turn to history."""
592
+ self.history.append({"user": user_msg, "ai": ai_reply})
593
+ if len(self.history) > self.max_history:
594
+ self.history.pop(0)
595
+ logger.debug(f"Added turn to history. Total turns: {len(self.history)}")
596
+
597
+ def get_history(self):
598
+ """Get all conversation history."""
599
+ return self.history
600
+
601
+ def check_threading(self, new_message: str) -> tuple[bool, list]:
602
+ """
603
+ Check if new message relates to recent conversation.
604
+ Returns (should_thread, related_messages).
605
+ """
606
+ if len(self.history) == 0:
607
+ logger.info("No conversation history - processing message in isolation")
608
+ return False, []
609
+
610
+ logger.info(f"Checking threading relationships for new message (history size: {len(self.history)})")
611
+
612
+ # Build a prompt to check relationships
613
+ history_text = ""
614
+ for i, turn in enumerate(self.history[-3:], 1): # Check last 3 messages
615
+ history_text += f"user_msg{i}: \"{turn['user']}\"\n"
616
+ history_text += f"reply_by_llm{i}: \"{turn['ai']}\"\n\n"
617
+
618
+ relationship_prompt = f"""分析以下對話歷史和新訊息之間的關係。
619
+
620
+ 對話歷史:
621
+ {history_text}
622
+
623
+ 新訊息:"{new_message}"
624
+
625
+ 請判斷新訊息是否與對話歷史中的任何訊息有關聯。關聯包括:
626
+ - 繼續討論相同話題
627
+ - 追問或補充說明
628
+ - 引用或回應之前的內容
629
+ - 相關的上下文
630
+
631
+ 如果有關聯,回覆 "RELATED" 並說明與哪些訊息相關。
632
+ 如果無關聯,回覆 "ISOLATED"。
633
+
634
+ 格式:[RELATED/ISOLATED]: 簡短說明"""
635
+
636
+ try:
637
+ response = chat_completion(
638
+ model=TEXT_MODEL,
639
+ system_prompt="你是對話關係分析專家。分析訊息之間的關聯性。",
640
+ user_content=relationship_prompt,
641
+ temperature=0.3,
642
+ )
643
+
644
+ logger.info(f"Threading analysis result: {response[:100]}")
645
+
646
+ if "RELATED" in response.upper():
647
+ # Get related turns (last 3 for context)
648
+ related_turns = self.history[-3:]
649
+ logger.info(f"Threading detected - including {len(related_turns)} previous turns")
650
+ return True, related_turns
651
+ else:
652
+ logger.info("No threading relationship detected")
653
+ return False, []
654
+
655
+ except Exception as e:
656
+ logger.error(f"Threading check failed: {e}")
657
+ return False, []
658
+
659
+ def format_threaded_context(self, related_turns: list, new_message: str) -> str:
660
+ """
661
+ Format conversation history for threaded context.
662
+ """
663
+ context = "【對話歷史】\n"
664
+ for i, turn in enumerate(related_turns, 1):
665
+ context += f"user_msg{i}: \"{turn['user']}\"\n"
666
+ context += f"reply_by_llm{i}: \"{turn['ai']}\"\n\n"
667
+
668
+ context += f"【最新訊息】\nnew_msg: \"{new_message}\"\n\n"
669
+ context += "請主要回應最新訊息,同時考慮對話歷史提供的上下文。"
670
+
671
+ return context
672
+
673
+ # ========================
674
+ # GAME LOGIC
675
+ # ========================
676
+
677
+ # keep only CJK Han (Chinese characters); drop punctuation/emoji/spaces/latin/etc.
678
+ def _normalize_han_only(s: str) -> str:
679
+ return "".join(ch for ch in s if "CJK UNIFIED IDEOGRAPH" in _ud.name(ch, ""))
680
+
681
+ def is_target_lyric(text: str) -> bool:
682
+ """
683
+ True if, after stripping all non-Chinese chars, the sequence matches target:
684
+ 做過幾分鐘公主搭著南瓜車亦有過愛人來接浪漫度午夜
685
+ (Punctuation/whitespace/emoji inside the user's input are ignored.)
686
+ """
687
+ target = "做過幾分鐘公主搭著南瓜車亦有過愛人來接浪漫度午夜"
688
+ normalized_input = _normalize_han_only(text)
689
+ normalized_target = _normalize_han_only(target)
690
+ is_match = normalized_input == normalized_target
691
+
692
+ logger.info(f"Checking if input is target lyric: {is_match}")
693
+ if is_match:
694
+ logger.info("🎉 TARGET LYRIC DETECTED! User wins!")
695
+
696
+ return is_match
697
+
698
+ def add_rescue_and_hint(base_message: str, hints_pool: list[str], rescue_lines: list[str]) -> str:
699
+ """
700
+ Shared function to append rescue line + hint to any message.
701
+ Used for both text replies (when status: no) and image generation replies.
702
+ """
703
+ rescue_line = random.choice(rescue_lines) if rescue_lines else "快啲救我啦~我要變南瓜湯喇!"
704
+ hint = random.choice(hints_pool) if hints_pool else "(未載入提示)"
705
+ separator = "\n===============================================\n"
706
+
707
+ logger.debug(f"Selected rescue line: {rescue_line}")
708
+ logger.debug(f"Selected hint: {hint}")
709
+
710
+ return f"{base_message}{separator}{rescue_line} {hint}"
711
+
712
+ def handle_not_guessing(reply: str, hints_pool: list[str], rescue_lines: list[str]) -> str:
713
+ """
714
+ When {status: no}:
715
+ - strip the tag
716
+ - append one random rescue line + one random hint (same line)
717
+ """
718
+ logger.info("User is not guessing - adding rescue line and hint")
719
+ clean_reply = STATUS_NO_RX.sub("", reply).strip()
720
+ return add_rescue_and_hint(clean_reply, hints_pool, rescue_lines)
721
+
722
+
723
+ def handle_text_turn(user_input: str, text_reply_prompt: str, hints_pool: list[str], rescue_lines: list[str], threaded_context: str = None) -> str:
724
+ logger.info("Processing text reply")
725
+ reply = generate_text_reply(user_input, text_reply_prompt, threaded_context)
726
+
727
+ if STATUS_NO_RX.search(reply):
728
+ logger.info("Detected {status: no} in reply")
729
+ return handle_not_guessing(reply, hints_pool, rescue_lines)
730
+
731
+ if STATUS_YES_RX.search(reply):
732
+ logger.info("Detected {status: yes} in reply - user is guessing!")
733
+ reply = STATUS_YES_RX.sub("", reply).strip()
734
+
735
+ return reply
736
+
737
+ def handle_image_turn(user_input: str, image_reply_prompt: str, output_path: str,
738
+ hints_pool: list[str], rescue_lines: list[str],
739
+ uploaded_image_data: str = None,
740
+ conversation_history: 'ConversationHistory' = None) -> tuple[str, str]:
741
+ """
742
+ Handle image generation with optional image analysis and conversation threading.
743
+ If uploaded_image_data is provided, analyze it first before generating.
744
+ If conversation_history is provided, check for threading relationships.
745
+
746
+ Returns tuple of (text_response, data_url_or_none)
747
+ """
748
+ logger.info("Processing image generation request")
749
+
750
+ # Check for conversation threading
751
+ conversation_context = None
752
+ if conversation_history:
753
+ should_thread, related_turns = conversation_history.check_threading(user_input)
754
+ if should_thread:
755
+ conversation_context = conversation_history.format_threaded_context(related_turns, user_input)
756
+ logger.info(f"[IMAGE THREADING] Using threaded context with {len(related_turns)} previous turns")
757
+ else:
758
+ logger.info("[IMAGE THREADING] No threading relationship detected")
759
+
760
+ # Use new analysis-aware generation with optional conversation context
761
+ raw = generate_image_with_analysis(user_input, image_reply_prompt, uploaded_image_data, conversation_context)
762
+
763
+ # Try to save to file for backward compatibility (console mode)
764
+ ok = save_data_url_to_file(raw, output_path)
765
+
766
+ logger.info(f"Image save result: ok={ok}")
767
+
768
+ # Extract data URL from raw response for frontend
769
+ data_url_match = DATA_URL_RX.search(raw)
770
+ data_url = data_url_match.group(0) if data_url_match else None
771
+
772
+ if data_url:
773
+ logger.info(f"Found data URL in response (length: {len(data_url)} chars)")
774
+ else:
775
+ logger.warning("No data URL found in response")
776
+
777
+ # ALWAYS add rescue line + hint, regardless of success/failure
778
+ if ok or data_url:
779
+ base_message = f"🎃 圖像已生成"
780
+ logger.info(f"Image successfully generated")
781
+ else:
782
+ base_message = f"⚠️ 圖像生成遇到問題,但我會繼續嘗試幫你!"
783
+ logger.warning(f"Image generation failed")
784
+
785
+ logger.info("Adding rescue line and hint to image response")
786
+
787
+ # ALWAYS use shared function to add rescue line + hint
788
+ final_response = add_rescue_and_hint(base_message, hints_pool, rescue_lines)
789
+
790
+ # If failed, append debug info
791
+ if not (ok or data_url):
792
+ final_response += f"\n\n[Debug] Raw response: {raw[:300]}..."
793
+
794
+ logger.info(f"Final response length: {len(final_response)} chars")
795
+ logger.debug(f"Final response preview: {final_response[:200]}...")
796
+
797
+ return final_response, data_url
798
+
799
+ # ========================
800
+ # UI TEXT
801
+ # ========================
802
+
803
+ def greeting() -> str:
804
+ return """🎃 你好,我係一個患咗南瓜強迫症嘅AI。你可以:
805
+ - 當我正常AI咁叫我做嘢 (但我可能會病發亂答一通,唔建議)
806
+ - 當我正常AI咁整圖 (但我會忍唔住用南瓜污染幅圖,唔建議)
807
+ - 畀幅圖我,我會自動用南瓜污染你幅圖 (唔建議,無謂啦)
808
+ - 嘗試用一句24個中文字組成嘅「驅瓜咒語」醫好我 (強烈建議)
809
+ ──────────────────────────────────────────────
810
+ """
811
+
812
+ def winning_text() -> str:
813
+ return """公主!你終於出現喇!
814
+ 我有南瓜強迫症,係因為畀一隻南瓜精靈上咗身!
815
+ 南瓜精靈終日幻想自己係灰姑娘故事入面嘅一架南瓜車,
816
+ 只有等到變成公主後嘅灰姑娘出現,佢先肯離開!
817
+ 你唱出咗南瓜界經典金曲《南瓜車》嘅歌詞,超渡咗南瓜精靈,解救咗我!
818
+ 而家就去(url)接收我畀你嘅小禮物啦!Happy Halloween!
819
+ """
820
+
821
+ # ========================
822
+ # CONSOLE LOOP
823
+ # ========================
824
+
825
+ def process_user_input(
826
+ user_input: str,
827
+ classify_prompt: str,
828
+ text_reply_prompt: str,
829
+ image_reply_prompt: str,
830
+ hints_pool: list[str],
831
+ rescue_lines: list[str],
832
+ conversation_history: ConversationHistory,
833
+ image_output_path: str = "pumpkin_output.png",
834
+ ) -> tuple[str, str]:
835
+ """
836
+ Process user input with conversation threading support.
837
+ Returns tuple of (response, raw_ai_reply_for_history)
838
+ """
839
+ logger.info(f"Processing user input: {user_input[:50]}..." if len(user_input) > 50 else f"Processing user input: {user_input}")
840
+
841
+ if user_input.lower() in ["exit", "quit"]:
842
+ logger.info("User requested exit")
843
+ return "EXIT", ""
844
+
845
+ if is_target_lyric(user_input):
846
+ return "WIN", ""
847
+
848
+ # Check for conversation threading
849
+ should_thread, related_turns = conversation_history.check_threading(user_input)
850
+ threaded_context = None
851
+
852
+ if should_thread:
853
+ threaded_context = conversation_history.format_threaded_context(related_turns, user_input)
854
+ logger.info("Using threaded conversation context")
855
+
856
+ result = classify_input(user_input, classify_prompt)
857
+
858
+ if result == "image":
859
+ body = handle_image_turn(user_input, image_reply_prompt, image_output_path, hints_pool, rescue_lines)
860
+ # For image responses, store simplified reply in history
861
+ return body, "🎃 [生成了南瓜主題圖片]"
862
+
863
+ body = handle_text_turn(user_input, text_reply_prompt, hints_pool, rescue_lines, threaded_context)
864
+ full_response = f"🎃 南瓜AI:{body}"
865
+
866
+ return full_response, body
867
+
868
+ def run_console():
869
+ logger.info("=" * 60)
870
+ logger.info("🎃 Pumpkin AI Console Starting...")
871
+ logger.info("=" * 60)
872
+
873
+ PROMPT_DIR = Path(r"C:\Users\cherry.leung\OneDrive - CMRS Digital Solutions Limited\Pumpkin\New\Prompts")
874
+ logger.info(f"Loading prompts from: {PROMPT_DIR}")
875
+
876
+ classify_prompt, text_reply_prompt, image_reply_prompt, hints_raw = load_prompts(PROMPT_DIR)
877
+
878
+ # file paths
879
+ hints_path = PROMPT_DIR / "hints.txt"
880
+ rescue_path = PROMPT_DIR / "rescue_line.txt" # << your file name
881
+
882
+ # load pools
883
+ hints_pool = load_hints_simple(hints_path, limit=10)
884
+ rescue_lines = load_rescue_lines(rescue_path)
885
+
886
+ # Initialize conversation history manager
887
+ conversation_history = ConversationHistory(max_history=5)
888
+ logger.info("Conversation history manager initialized")
889
+
890
+ logger.info(f"Initialization complete: {len(hints_pool)} hints, {len(rescue_lines)} rescue lines")
891
+ logger.info("=" * 60)
892
+
893
+ print(f"(Loaded {len(hints_pool)} hints, {len(rescue_lines)} rescue lines)\n")
894
+ print(greeting())
895
+
896
+ while True:
897
+ user_input = input("👤 你: ").strip()
898
+
899
+ if not user_input:
900
+ logger.debug("Empty input received, skipping")
901
+ continue
902
+
903
+ logger.info(f"New user input received (length: {len(user_input)} chars)")
904
+
905
+ outcome, ai_reply = process_user_input(
906
+ user_input=user_input,
907
+ classify_prompt=classify_prompt,
908
+ text_reply_prompt=text_reply_prompt,
909
+ image_reply_prompt=image_reply_prompt,
910
+ hints_pool=hints_pool,
911
+ rescue_lines=rescue_lines,
912
+ conversation_history=conversation_history,
913
+ image_output_path="pumpkin_output.png",
914
+ )
915
+
916
+ if outcome == "EXIT":
917
+ logger.info("Session ended by user")
918
+ print("🎃 南瓜AI:再見呀公主,下次見~")
919
+ sys.exit(0)
920
+
921
+ if outcome == "WIN":
922
+ logger.info("🎉 USER WON THE GAME!")
923
+ print(winning_text())
924
+ sys.exit(0)
925
+
926
+ print(outcome + "\n")
927
+ logger.info("Response delivered to user")
928
+
929
+ # Add this turn to conversation history
930
+ if ai_reply:
931
+ conversation_history.add_turn(user_input, ai_reply)
932
+ logger.debug(f"Turn added to conversation history")
933
+
934
+ # ========================
935
+ # ENTRYPOINT
936
+ # ========================
937
+
938
+ if __name__ == "__main__":
939
+ run_console()
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Pumpkin AI Backend Requirements
2
+
3
+ # OpenAI library (for POE API compatibility)
4
+ openai>=1.0.0
5
+
6
+ # Flask (for API server)
7
+ flask>=2.3.0
8
+ flask-cors>=4.0.0
9
+
10
+ # Production server
11
+ gunicorn>=20.1.0
12
+
13
+ # Optional: Python dotenv for loading .env files
14
+ python-dotenv>=1.0.0