Spaces:
Runtime error
Runtime error
| import os | |
| import sys | |
| import shutil | |
| import uuid | |
| import random | |
| import logging | |
| from PIL import Image, ImageDraw, ImageFont | |
| # 設定日誌記錄,便於開發與除錯 | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") | |
| logger = logging.getLogger(__name__) | |
| # ============================================================================== | |
| # 改為從環境變數讀取金鑰,保護資安 | |
| # ============================================================================== | |
| GEMINI_API_KEY = os.environ.get("GOOGLE_API_KEY") | |
| HF_TOKEN = os.environ.get("HF_TOKEN") | |
| # 固定的生圖寬度與高度 (1024x1024) | |
| DEFAULT_WIDTH = 1024 | |
| DEFAULT_HEIGHT = 1024 | |
| # 預設的俗俗早安圖問候語,當 Gemini API 金鑰未提供時作為 Fallback 備用 | |
| PRESET_GREETINGS = [ | |
| "早安!今天也要假裝系統正常!", | |
| "早安!人生卡住時,請先重開機!", | |
| "早安!努力不一定成功,但會很累!", | |
| "早安!腦袋尚未開機,請多包涵!", | |
| "早安!今天的你,比昨天更像待機中!", | |
| "早安!不要放棄,反正也沒人接住你!", | |
| "早安!上課像登入失敗,但人要有光!", | |
| "早安!作業不會寫,太陽還是會升起!", | |
| "早安!群組安靜不是冷漠,是大家都沒電!", | |
| "早安!願你今天的 Wi-Fi 比人生穩定!" | |
| ] | |
| # 頂部大字 Banner 的預設選用標題 | |
| PRESET_HEADERS = [ | |
| "早安", | |
| "平安喜樂", | |
| "吉祥如意", | |
| "福氣滿滿", | |
| "吉祥安康", | |
| "順心如意" | |
| ] | |
| # 經典長輩圖必備霓虹彩虹漸層字體配色系列 | |
| RAINBOW_COLORS = [ | |
| "#FF0033", # 霓虹紅 | |
| "#FF6600", # 霓虹橘 | |
| "#FFFF00", # 鮮艷黃 | |
| "#33FF33", # 螢光綠 | |
| "#00FFFF", # 霓虹藍/青色 | |
| "#CC33FF", # 亮紫色 | |
| "#FF3399" # 桃紅色 | |
| ] | |
| def translate_to_english(text): | |
| try: | |
| import requests | |
| url = f"https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=en&dt=t&q={requests.utils.quote(text)}" | |
| r = requests.get(url, timeout=5) | |
| if r.status_code == 200: | |
| translated = r.json()[0][0][0] | |
| logger.info(f"自動翻譯成功: '{text}' -> '{translated}'") | |
| return translated | |
| except Exception as e: | |
| logger.error(f"翻譯 API 呼叫失敗: {e}") | |
| return text | |
| def get_chinese_font(font_size, font_name="kaiu"): | |
| windir = os.environ.get('WINDIR', 'C:\\Windows') | |
| paths = [] | |
| paths.extend([ | |
| "kaiu.ttf", | |
| "static/kaiu.ttf", | |
| os.path.join(windir, 'Fonts', 'kaiu.ttf'), | |
| os.path.join(windir, 'Fonts', 'msjhbd.ttc'), | |
| os.path.join(windir, 'Fonts', 'msjh.ttc'), | |
| os.path.join(windir, 'Fonts', 'simsun.ttc'), | |
| "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc", | |
| "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", | |
| "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", | |
| "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", | |
| "/usr/share/fonts/truetype/droid/DroidSansFallback.ttf" | |
| ]) | |
| for path in paths: | |
| if os.path.exists(path): | |
| try: | |
| if path.endswith('.ttc'): | |
| logger.info(f"成功載入 TTC 字型: {path}") | |
| return ImageFont.truetype(path, font_size, index=0) | |
| else: | |
| logger.info(f"成功載入 TTF 字型: {path}") | |
| return ImageFont.truetype(path, font_size) | |
| except Exception as e: | |
| logger.warning(f"載入字型 {path} 失敗: {e}") | |
| continue | |
| logger.warning("未偵測到任何系統字型,將使用 Pillow 預設字型。") | |
| try: | |
| return ImageFont.load_default() | |
| except Exception: | |
| return None | |
| def wrap_text(text, font, max_width, draw): | |
| lines = [] | |
| current_line = "" | |
| for char in text: | |
| test_line = current_line + char | |
| w = draw.textlength(test_line, font=font) | |
| if w <= max_width: | |
| current_line = test_line | |
| else: | |
| if current_line: | |
| lines.append(current_line) | |
| current_line = char | |
| if current_line: | |
| lines.append(current_line) | |
| return lines | |
| def generate_text_with_gemini(api_key, query): | |
| if not api_key: | |
| logger.info("未偵測到 Gemini API Key。將使用內建隨機賀詞。") | |
| return random.choice(PRESET_GREETINGS) | |
| try: | |
| import google.generativeai as genai | |
| genai.configure(api_key=api_key) | |
| model = genai.GenerativeModel('gemini-2.5-flash') | |
| prompt = ( | |
| f"請針對「{query}」寫一句繁體中文早安圖文字。\n" | |
| f"風格要求:台灣長輩圖、荒謬、38、假勵志、欠吐槽、適合群組聊天。\n" | |
| f"語氣要像一個很有自信但其實很荒謬的早安機器人。\n" | |
| f"字數限制:12 到 22 字。\n" | |
| f"內容方向:可以嘲諷抽象人生、早八、上班、作業、拖延、群組沉默、機器人自己。\n" | |
| f"安全限制:不可攻擊特定真人、族群、性別、外貌、疾病、政治立場或宗教。\n" | |
| f"不要髒話,不要人身攻擊,不要太黑暗。\n" | |
| f"要讓群組成員看了想回:『這什麼鬼啦』。\n" | |
| f"範例:\n" | |
| f"- 早安,今天也要假裝系統正常\n" | |
| f"- 早安,努力不一定成功但很耗電\n" | |
| f"- 早安,人生卡住請重新整理\n" | |
| f"- 早安,腦袋離線也是一種生活\n" | |
| f"- 早安,群組沒人講話我先亂講\n" | |
| f"請只回傳一句中文早安文字,不要解釋,不要加引號。" | |
| ) | |
| response = model.generate_content(prompt) | |
| text = response.text.strip().replace('"', '').replace('「', '').replace('」', '') | |
| logger.info(f"Gemini 生成的長輩賀詞: {text}") | |
| return text | |
| except Exception as e: | |
| logger.error(f"Gemini 賀詞生成錯誤: {e}") | |
| return random.choice(PRESET_GREETINGS) | |
| def expand_prompt_with_gemini(api_key, query): | |
| query_en = translate_to_english(query) | |
| default_expanded = ( | |
| f"A funny, absurd, and slightly stupid Taiwanese good morning meme image featuring {query_en} as the main subject, " | |
| f"placed clearly in the center of the image. " | |
| f"The image should look like an over-the-top Taiwanese elder-style morning greeting card mixed with chaotic internet meme humor. " | |
| f"Use extremely colorful flowers, dramatic golden sunrise, sparkles, glowing clouds, rainbow decorations, fake inspirational poster energy, " | |
| f"retro 1990s digital clip-art aesthetics, and an exaggerated ridiculous composition. " | |
| f"The mood should be harmless, silly, flamboyant, awkwardly cheerful, and easy for group chat members to roast. " | |
| f"No readable text inside the image, because Traditional Chinese text will be added later by the program. " | |
| f"High resolution, colorful, detailed, humorous, meme-like, absurd, ridiculous." | |
| ) | |
| if not api_key: | |
| logger.info("未偵測到 Gemini API Key。使用預設封裝 Prompt。") | |
| return default_expanded | |
| try: | |
| import google.generativeai as genai | |
| genai.configure(api_key=api_key) | |
| model = genai.GenerativeModel('gemini-2.5-flash') | |
| prompt = ( | |
| f"Please translate the Chinese phrase '{query}' into English, " | |
| f"then expand it into a detailed image generation prompt for a funny, absurd Taiwanese good morning meme image.\n\n" | |
| f"Creative direction:\n" | |
| f"The image should feel like a chaotic Taiwanese elder-style good morning card, " | |
| f"but upgraded into a ridiculous internet meme that group chat members would want to roast.\n\n" | |
| f"Requirements:\n" | |
| f"1. The main subject must clearly be related to: '{query_en}'.\n" | |
| f"2. Make the scene visually absurd, silly, flamboyant, and overly confident in a stupid way.\n" | |
| f"3. Combine Taiwanese elder-style good morning card aesthetics with meme humor: " | |
| f"dramatic sunrise, glowing clouds, lotus flowers, sparkles, rainbow decorations, " | |
| f"oversaturated colors, retro 1990s clip-art, fake inspirational poster style, and chaotic composition.\n" | |
| f"4. The image should look harmless, funny, awkwardly cheerful, and easy to roast in a group chat.\n" | |
| f"5. Add one ridiculous visual gag related to the subject, such as the subject acting too serious, " | |
| f"posing heroically, failing confidently, or doing something meaningless with great determination.\n" | |
| f"6. Do not include real public figures, political symbols, hate symbols, violence, sexual content, horror, or offensive stereotypes.\n" | |
| f"7. Do not generate readable text inside the image, because Traditional Chinese text will be added later by the program.\n\n" | |
| f"Only return the final English image prompt. Do not include explanations, labels, or quotation marks." | |
| ) | |
| response = model.generate_content(prompt) | |
| expanded = response.text.strip().replace('"', '').replace('「', '').replace('」', '') | |
| logger.info(f"Gemini 擴寫的英文生圖提示詞: {expanded}") | |
| return expanded | |
| except Exception as e: | |
| logger.error(f"Gemini 提示詞擴寫錯誤: {e}") | |
| return default_expanded | |
| def draw_rainbow_text(draw, text, x_center, y_start, font, stroke_width=6, stroke_fill="#000000"): | |
| if not font: | |
| return y_start | |
| total_w = 0 | |
| char_widths = [] | |
| for char in text: | |
| w = draw.textlength(char, font=font) | |
| char_widths.append(w) | |
| total_w += w | |
| start_x = x_center - (total_w / 2) | |
| current_x = start_x | |
| for idx, char in enumerate(text): | |
| color = RAINBOW_COLORS[idx % len(RAINBOW_COLORS)] | |
| draw.text( | |
| (current_x, y_start), | |
| char, | |
| font=font, | |
| fill=color, | |
| stroke_width=stroke_width, | |
| stroke_fill=stroke_fill | |
| ) | |
| current_x += char_widths[idx] | |
| bbox = font.getbbox("早") | |
| char_h = bbox[3] - bbox[1] if bbox else 50 | |
| return y_start + char_h + 10 | |
| def overlay_text_on_image(image_path, greeting_text, output_path): | |
| try: | |
| img = Image.open(image_path).convert("RGB") | |
| if img.size != (DEFAULT_WIDTH, DEFAULT_HEIGHT): | |
| img = img.resize((DEFAULT_WIDTH, DEFAULT_HEIGHT), Image.Resampling.LANCZOS) | |
| draw = ImageDraw.Draw(img) | |
| header_font = get_chinese_font(120, "kaiu") | |
| body_font = get_chinese_font(52, "kaiu") | |
| footer_font = get_chinese_font(32, "kaiu") | |
| header_text = f"🌸 {random.choice(PRESET_HEADERS)} 🌸" | |
| header_y = 60 | |
| if header_font: | |
| draw.text( | |
| (DEFAULT_WIDTH / 2, header_y), | |
| header_text, | |
| font=header_font, | |
| fill="#FFFF00", | |
| anchor="ma", | |
| stroke_width=8, | |
| stroke_fill="#000000" | |
| ) | |
| body_y_start = 740 | |
| if body_font: | |
| lines = wrap_text(greeting_text, body_font, 920, draw) | |
| current_y = body_y_start | |
| for line in lines: | |
| current_y = draw_rainbow_text( | |
| draw, | |
| line, | |
| x_center=DEFAULT_WIDTH / 2, | |
| y_start=current_y, | |
| font=body_font, | |
| stroke_width=6, | |
| stroke_fill="#000000" | |
| ) | |
| footer_text = "👍 認同請分享,祝您平安喜樂! 👍" | |
| footer_y = 930 | |
| if footer_font: | |
| draw.text( | |
| (DEFAULT_WIDTH / 2, footer_y), | |
| footer_text, | |
| font=footer_font, | |
| fill="#33FF33", | |
| anchor="ma", | |
| stroke_width=5, | |
| stroke_fill="#000000" | |
| ) | |
| img.save(output_path, "JPEG", quality=90) | |
| logger.info(f"早安圖加工疊加完成,存檔至: {output_path}") | |
| return True | |
| except Exception as e: | |
| logger.error(f"Pillow 文字疊加失敗: {e}") | |
| try: | |
| shutil.copy(image_path, output_path) | |
| return True | |
| except Exception: | |
| return False | |
| def generate_good_morning_image(user_input, api_key=None, custom_greeting=None): | |
| resolved_api_key = api_key or GEMINI_API_KEY | |
| query = user_input.strip() | |
| if query.startswith("@create image"): | |
| query = query.replace("@create image", "", 1).strip() | |
| if not query: | |
| query = "蓮花" | |
| logger.info(f"開始處理早安圖主題任務: {query}") | |
| if custom_greeting: | |
| greeting_text = custom_greeting | |
| else: | |
| greeting_text = generate_text_with_gemini(resolved_api_key, query) | |
| expanded_prompt = expand_prompt_with_gemini(resolved_api_key, query) | |
| logger.info("呼叫 Gradio Space 端點 (stabilityai/stable-diffusion-3.5-large)...") | |
| try: | |
| from gradio_client import Client | |
| token = HF_TOKEN | |
| client = Client("stabilityai/stable-diffusion-3.5-large", token=token) | |
| result = client.predict( | |
| prompt=expanded_prompt, | |
| negative_prompt="blurry, low quality, bad anatomy, deformed, ugly, disfigured, poor lighting, out of frame, cropped", | |
| seed=0, | |
| randomize_seed=True, | |
| width=DEFAULT_WIDTH, | |
| height=DEFAULT_HEIGHT, | |
| guidance_scale=4.5, | |
| num_inference_steps=40, | |
| api_name="/infer", | |
| ) | |
| if result and isinstance(result, tuple) and len(result) > 0: | |
| temp_image_path = result[0] | |
| elif isinstance(result, str): | |
| temp_image_path = result | |
| else: | |
| raise ValueError(f"無法識別的 Gradio API 返回格式: {result}") | |
| logger.info(f"SD3.5 基礎底圖繪製成功,暫存於: {temp_image_path}") | |
| data_dir = os.path.abspath("data") | |
| os.makedirs(data_dir, exist_ok=True) | |
| image_id = str(uuid.uuid4()) | |
| final_image_name = f"{image_id}.jpg" | |
| final_image_path = os.path.join(data_dir, final_image_name) | |
| success = overlay_text_on_image(temp_image_path, greeting_text, final_image_path) | |
| if not success: | |
| raise RuntimeError("Pillow 後製疊加文字發生崩潰失敗") | |
| return { | |
| "success": True, | |
| "id": image_id, | |
| "query": query, | |
| "greeting_text": greeting_text, | |
| "expanded_prompt": expanded_prompt, | |
| "image_path": final_image_path, | |
| "filename": final_image_name | |
| } | |
| except Exception as e: | |
| logger.error(f"整條生圖流水線執行失敗: {e}") | |
| return { | |
| "success": False, | |
| "error": str(e) | |
| } |