Jay Luk commited on
Commit ·
213932c
1
Parent(s): 0a61b3e
Deploy Pumpkin AI game to HuggingFace Spaces
Browse files- Prompts/hints.txt +10 -0
- Prompts/image_replies.txt +78 -0
- Prompts/rescue_line.txt +20 -0
- Prompts/statusno_hint.txt +4 -0
- Prompts/text_or_image_classification.txt +51 -0
- Prompts/text_replies.txt +133 -0
- app.py +324 -0
- pumpkin-ai-frontend/build/asset-manifest.json +13 -0
- pumpkin-ai-frontend/build/index.html +1 -0
- pumpkin-ai-frontend/build/static/css/main.5e2dec06.css +2 -0
- pumpkin-ai-frontend/build/static/css/main.5e2dec06.css.map +1 -0
- pumpkin-ai-frontend/build/static/js/main.3a5310d2.js +0 -0
- pumpkin-ai-frontend/build/static/js/main.3a5310d2.js.LICENSE.txt +49 -0
- pumpkin-ai-frontend/build/static/js/main.3a5310d2.js.map +0 -0
- pumpkin_code.py +939 -0
- requirements.txt +14 -0
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: 
|
| 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: 
|
| 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
|