Spaces:
Sleeping
Sleeping
| # app.py | |
| import os | |
| import uuid | |
| import requests | |
| import logging | |
| import time # 💡 導入時間模組進行效能測試 | |
| from fastapi import FastAPI, Request, HTTPException | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import FileResponse | |
| from linebot import LineBotApi, WebhookHandler | |
| from linebot.exceptions import InvalidSignatureError | |
| from linebot.models import ( | |
| MessageEvent, TextMessage, TextSendMessage, | |
| AudioSendMessage, FlexSendMessage, VideoMessage, | |
| ImageMessage, AudioMessage, ImageSendMessage, PostbackEvent, FollowEvent | |
| ) | |
| # 💡 導入專案模組 | |
| import image_processor | |
| import config | |
| import translator | |
| import audio_processor | |
| import video_processor | |
| import ui_templates | |
| import db_manager | |
| import deep_analyzer | |
| import visual_renderer | |
| # 設定日誌 | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| app = FastAPI() | |
| # 確保靜態資料夾存在 | |
| if not os.path.exists("static"): | |
| os.makedirs("static") | |
| app.mount("/static", StaticFiles(directory="static"), name="static") | |
| line_bot_api = LineBotApi(config.LINE_TOKEN) | |
| handler = WebhookHandler(config.LINE_SECRET) | |
| # ---------------------------------------------------------------- | |
| # 🔮 智慧翻譯快取攔截器 (Priority 2: LRU Cache) | |
| # ---------------------------------------------------------------- | |
| from functools import lru_cache | |
| def cached_translate(text: str, tribe: str): | |
| """記憶體快取:如果相同的句子與族語查過,直接秒回,不再去遠端 API 排隊 5 秒""" | |
| logger.info(f"🔮 [Cache Miss] 正在向遠端 API 請求全新翻譯: {text[:10]}...") | |
| return translator.translate(text, tribe) | |
| # ---------------------------------------------------------------- | |
| # 輔助功能 | |
| # ---------------------------------------------------------------- | |
| async def download_file(filename: str): | |
| """供使用者下載生成的音檔或檔案""" | |
| file_path = os.path.join("static", filename) | |
| if os.path.exists(file_path): | |
| return FileResponse(path=file_path, filename=f"TRK_AI_{filename}", media_type='application/octet-stream') | |
| return {"error": "檔案不存在"} | |
| def show_loading_animation(user_id, loading_seconds=20): | |
| """發送 LINE 官方的讀取中動畫""" | |
| url = "https://api.line.me/v2/bot/chat/loading/start" | |
| headers = {"Content-Type": "application/json", "Authorization": f"Bearer {config.LINE_TOKEN}"} | |
| data = {"chatId": user_id, "loadingSeconds": loading_seconds} | |
| try: | |
| requests.post(url, headers=headers, json=data, timeout=5) | |
| except Exception as e: | |
| logger.error(f"Loading Animation Error: {e}") | |
| def get_line_nickname(user_id): | |
| """小工具:向 LINE 伺服器請求使用者暱稱""" | |
| try: | |
| profile = line_bot_api.get_profile(user_id) | |
| return profile.display_name | |
| except: | |
| return "新朋友" | |
| # ---------------------------------------------------------------- | |
| # 路由設定 | |
| # ---------------------------------------------------------------- | |
| async def root(): | |
| return {"message": "16族智慧翻譯小幫手運行中", "base_url": config.BASE_URL} | |
| async def callback(request: Request): | |
| signature = request.headers.get("X-Line-Signature") | |
| body = await request.body() | |
| try: | |
| handler.handle(body.decode("utf-8"), signature) | |
| except InvalidSignatureError: | |
| raise HTTPException(status_code=400) | |
| return "OK" | |
| # ---------------------------------------------------------------- | |
| # 1. 處理初次加入好友 (迎賓與暱稱紀錄) | |
| # ---------------------------------------------------------------- | |
| def handle_follow(event): | |
| user_id = event.source.user_id | |
| # 抓取暱稱並存入資料庫 | |
| user_nickname = get_line_nickname(user_id) | |
| db_manager.set_user_tribe(user_id, "太魯閣", display_name=user_nickname) | |
| # 合成 static 資料夾內的圖片網址 (對齊最新視覺設計) | |
| image_filename = "welcome_hero.png" | |
| full_image_url = f"{config.BASE_URL}/static/{image_filename}" | |
| # 創意總監潤色版歡迎文案 | |
| welcome_text = ( | |
| f"Embiyax su hug!{user_nickname} 您好 ❤️\n\n" | |
| "我是您的專屬「16族智慧翻譯小幫手」。\n" | |
| "不僅能翻譯文字,還能聽懂語音、看懂圖片!\n\n" | |
| "👇 請點擊下方「快速選單」,開啟您的南島語言探索之旅。" | |
| ) | |
| carousel_card = ui_templates.create_welcome_carousel(hero_image_url=full_image_url) | |
| line_bot_api.reply_message(event.reply_token, [ | |
| TextSendMessage(text=welcome_text), | |
| FlexSendMessage(alt_text="歡迎使用16族智慧翻譯小幫手", contents=carousel_card) | |
| ]) | |
| # ---------------------------------------------------------------- | |
| # 2. 處理互動按鈕與回饋 | |
| # ---------------------------------------------------------------- | |
| def handle_postback(event): | |
| user_id = event.source.user_id | |
| current_tribe = db_manager.get_user_tribe(user_id) | |
| data = event.postback.data | |
| if data == "feedback_good": | |
| db_manager.log_feedback(user_id, current_tribe, "good") | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text="✅ 感謝您的肯定!這將幫助我們將模型訓練得更好。")) | |
| elif data == "feedback_bad": | |
| db_manager.log_feedback(user_id, current_tribe, "bad") | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text="📝 感謝您的回饋!我們已將此翻譯標記,將持續優化品質。")) | |
| # ---------------------------------------------------------------- | |
| # 3. 處理多媒體 (影片/圖片/語音) | |
| # ---------------------------------------------------------------- | |
| def handle_video(event): | |
| user_id = event.source.user_id | |
| msg_id = event.message.id | |
| current_tribe = db_manager.get_user_tribe(user_id) | |
| show_loading_animation(user_id, 30) | |
| video_path = f"static/{msg_id}.mp4" | |
| audio_path = f"static/{msg_id}_extracted.wav" | |
| try: | |
| message_content = line_bot_api.get_message_content(msg_id) | |
| with open(video_path, 'wb') as fd: | |
| for chunk in message_content.iter_content(): fd.write(chunk) | |
| if video_processor.extract_audio(video_path, audio_path): | |
| recognized_text = audio_processor.speech_to_text(audio_path, current_tribe) | |
| if recognized_text: | |
| res = cached_translate(recognized_text, current_tribe) | |
| reply_text = f"🎬 【{current_tribe}語影片辨識】\n━━━━━━━━━━━━━━\n原文:\n{recognized_text}\n\n翻譯結果:\n{res.get('target', '翻譯失敗')}" | |
| db_manager.log_interaction(user_id, current_tribe, "video", recognized_text) | |
| else: | |
| reply_text = f"⚠️ 抱歉,影片中的{current_tribe}語音量太小或不夠清晰。" | |
| else: | |
| reply_text = "❌ 影片音軌提取失敗,請檢查檔案格式。" | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text=reply_text)) | |
| except Exception as e: | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text=f"影片處理錯誤: {e}")) | |
| finally: | |
| for p in [video_path, audio_path]: | |
| if os.path.exists(p): os.remove(p) | |
| def handle_image(event): | |
| user_id = event.source.user_id | |
| msg_id = event.message.id | |
| current_tribe = db_manager.get_user_tribe(user_id) | |
| show_loading_animation(user_id, 10) | |
| image_path = f"static/{msg_id}.jpg" | |
| try: | |
| message_content = line_bot_api.get_message_content(msg_id) | |
| with open(image_path, 'wb') as fd: | |
| for chunk in message_content.iter_content(): fd.write(chunk) | |
| # ⏱️ 啟動唯一的一次計時 | |
| start_time = time.time() | |
| # 💡 呼叫一體化模組 | |
| result = image_processor.extract_and_translate(image_path, current_tribe) | |
| duration = time.time() - start_time | |
| if result and "items" in result: | |
| originals = [i['original'] for i in result['items']] | |
| translates = [i['translated'] for i in result['items']] | |
| reply_text = ( | |
| f"👁️ 【視覺辨識結果】總耗時 {duration:.2f}s\n" | |
| f"━━━━━━━━━━━━━━\n" | |
| f"📝 擷取原文:\n" + "\n".join(originals) + "\n\n" | |
| f"🏹 【{current_tribe}語翻譯】:\n" + "\n".join(translates) | |
| ) | |
| else: | |
| reply_text = f"⚠️ 辨識結束 (耗時 {duration:.2f}s),未能偵測文字。" | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text=reply_text)) | |
| except Exception as e: | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text="❌ 處理失敗")) | |
| finally: | |
| if os.path.exists(image_path): os.remove(image_path) | |
| def handle_audio(event): | |
| user_id = event.source.user_id | |
| msg_id = event.message.id | |
| current_tribe = db_manager.get_user_tribe(user_id) | |
| show_loading_animation(user_id, 15) | |
| audio_path = f"static/{msg_id}.m4a" | |
| try: | |
| message_content = line_bot_api.get_message_content(msg_id) | |
| with open(audio_path, 'wb') as fd: | |
| for chunk in message_content.iter_content(): fd.write(chunk) | |
| recognized_text = audio_processor.speech_to_text(audio_path, current_tribe) | |
| if recognized_text: | |
| res = cached_translate(recognized_text, current_tribe) | |
| reply_text = ( | |
| f"🎤 【{current_tribe}語音辨識】\n━━━━━━━━━━━━━━\n" | |
| f"聽寫原文:\n{recognized_text}\n\n" | |
| f"中文翻譯:\n{res.get('target', '翻譯失敗')}" | |
| ) | |
| db_manager.log_interaction(user_id, current_tribe, "audio", recognized_text) | |
| else: | |
| reply_text = f"⚠️ 無法聽辨{current_tribe}語。" | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text=reply_text)) | |
| except Exception as e: | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text=f"語音處理失敗: {e}")) | |
| finally: | |
| if os.path.exists(audio_path): os.remove(audio_path) | |
| # ---------------------------------------------------------------- | |
| # 4. 文字訊息處理 (核心邏輯) | |
| # ---------------------------------------------------------------- | |
| def handle_message(event): | |
| user_text = event.message.text.strip() | |
| user_id = event.source.user_id | |
| current_tribe = db_manager.get_user_tribe(user_id) | |
| # --------------------------------------------------------- | |
| # 💡 意見回饋處理邏輯 | |
| # --------------------------------------------------------- | |
| if user_text.startswith("建議:"): | |
| feedback_content = user_text.replace("建議:", "").strip() | |
| db_manager.log_interaction(user_id, current_tribe, "text_feedback", feedback_content) | |
| reply_msg = "💖 感謝您的寶貴建議!\n開發團隊已經收到您的留言,我們會持續讓翻譯官變得更好!" | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text=reply_msg)) | |
| return | |
| if user_text == "我要留言": | |
| reply_msg = "💡 請直接在對話框輸入您的建議!\n\n(請記得以「建議:」開頭,例如:建議:字體可以大一點嗎?)" | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text=reply_msg)) | |
| return | |
| # --------------------------------------------------------- | |
| # 💡 執行長最新版 Rich Menu (2x3 六項配置對接) | |
| # --------------------------------------------------------- | |
| # 1. 切換族語 (上排左) | |
| if user_text in ["切換族語", "選單", "切換專屬族語"]: | |
| show_loading_animation(user_id, 3) | |
| menu = ui_templates.create_tribe_menu() | |
| line_bot_api.reply_message(event.reply_token, FlexSendMessage(alt_text="請選擇族語模式", contents=menu)) | |
| return | |
| # 2. 意見回饋 (上排中) | |
| if user_text in ["意見回饋", "填寫意見回饋"]: | |
| feedback_ui = ui_templates.create_feedback_card() | |
| line_bot_api.reply_message(event.reply_token, FlexSendMessage(alt_text="意見回饋", contents=feedback_ui)) | |
| return | |
| # 4. 構詞語法分析 (下排left) | |
| if user_text in ["構詞語法分析", "語法分析", "什麼是構詞語法分析", "什麼是構詞語法分析?"]: | |
| # 🎯 解決 UnboundLocalError:直接從已經通車的 deep_analyzer 借調 supabase 客戶端 | |
| from deep_analyzer import supabase | |
| current_tribe = "太魯閣" # 安全防禦預設值 | |
| try: | |
| pref_res = supabase.table("user_preferences").select("tribe").eq("user_id", user_id).execute() | |
| if pref_res.data: | |
| current_tribe = pref_res.data[0]["tribe"] | |
| except Exception as e: | |
| logger.error(f"選單介紹圖卡即時撈取族語偏好失敗: {e}") | |
| card = ui_templates.create_glossing_intro_card(current_tribe) | |
| line_bot_api.reply_message(event.reply_token, FlexSendMessage(alt_text="構詞語法分析介紹", contents=card)) | |
| return | |
| # 5. 多媒體翻譯 (下排中) | |
| if user_text in ["多媒體翻譯", "拍照翻譯", "什麼是多媒體翻譯", "什麼是多媒體翻譯?"]: | |
| # 🎯 解決 UnboundLocalError | |
| from deep_analyzer import supabase | |
| current_tribe = "太魯閣" | |
| try: | |
| pref_res = supabase.table("user_preferences").select("tribe").eq("user_id", user_id).execute() | |
| if pref_res.data: | |
| current_tribe = pref_res.data[0]["tribe"] | |
| except Exception as e: | |
| logger.error(f"選單介紹圖卡即時撈取族語偏好失敗: {e}") | |
| card = ui_templates.create_multimedia_intro_card(current_tribe) | |
| line_bot_api.reply_message(event.reply_token, FlexSendMessage(alt_text="多媒體翻譯教學", contents=card)) | |
| return | |
| # 6. 深度文化翻譯 (下排right) | |
| if user_text in ["深度文化翻譯", "深度翻譯", "什麼是深度文化翻譯", "什麼是深度文化翻譯?"]: | |
| # 🎯 解決 UnboundLocalError | |
| from deep_analyzer import supabase | |
| current_tribe = "太魯閣" | |
| try: | |
| pref_res = supabase.table("user_preferences").select("tribe").eq("user_id", user_id).execute() | |
| if pref_res.data: | |
| current_tribe = pref_res.data[0]["tribe"] | |
| except Exception as e: | |
| logger.error(f"選單介紹圖卡即時撈取族語偏好失敗: {e}") | |
| card = ui_templates.create_deep_translation_intro_card(current_tribe) | |
| line_bot_api.reply_message(event.reply_token, FlexSendMessage(alt_text="深度文化翻譯介紹", contents=card)) | |
| return | |
| # --------------------------------------------------------- | |
| # 💡 處理多媒體測試引導 (亮點功能導覽) | |
| # --------------------------------------------------------- | |
| if user_text == "🎙️ 我要測試語音/影片解析": | |
| reply_msg = ( | |
| "👇 請直接使用 LINE 的內建功能傳送:\n\n" | |
| "1️⃣ 【錄製語音】:按住右下角的「🎤 麥克風」,說一段太魯閣語(例如:Embiyax su hug?)。\n\n" | |
| "2️⃣ 【上傳影片】:點擊左下角「➕」>「相簿」,選取一段有族語對話的短影片。\n\n" | |
| "傳送後,我會立刻幫您「聽寫原文」並「翻譯內容」喔!🚀" | |
| ) | |
| line_bot_api.reply_message(event.reply_token, [ | |
| TextSendMessage(text=reply_msg), | |
| TextSendMessage(text="💡 小提示:錄音時記得大聲一點,AI 老師才聽得清楚喔!") | |
| ]) | |
| return | |
| # --------------------------------------------------------- | |
| # 💡 處理指令式功能 | |
| # --------------------------------------------------------- | |
| if user_text.startswith("📍 切換族語:"): | |
| tribe = user_text.replace("📍 切換族語:", "").strip() | |
| nickname = get_line_nickname(user_id) | |
| db_manager.set_user_tribe(user_id, tribe, display_name=nickname) | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text=f"✅ 已成功切換至【{tribe}語】模式!")) | |
| return | |
| if user_text.startswith("🔊 聽發音:"): | |
| target_native = user_text.replace("🔊 聽發音:", "").strip() | |
| show_loading_animation(user_id, 5) | |
| db_manager.log_interaction(user_id, current_tribe, "tts", target_native) | |
| try: | |
| u_id = str(uuid.uuid4()) | |
| rel_path = audio_processor.text_to_speech(target_native, current_tribe, f"tts_{u_id}") | |
| if config.BASE_URL and rel_path: | |
| audio_url = f"{config.BASE_URL}/{rel_path}" | |
| download_url = f"{config.BASE_URL}/download_file/tts_{u_id}.wav" | |
| line_bot_api.reply_message(event.reply_token, [ | |
| AudioSendMessage(original_content_url=audio_url, duration=2000), | |
| FlexSendMessage(alt_text="儲存音檔", contents=ui_templates.create_download_button(download_url)) | |
| ]) | |
| except Exception as e: | |
| logger.error(f"TTS Error: {e}") | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text="⚠️ 語音生成失敗。")) | |
| return | |
| # ================================================================= | |
| # 💡 語法分析: 階段一 (純文字快速版 + 觸發繪圖按鈕) | |
| # ================================================================= | |
| if user_text.startswith("💡 語法分析:"): | |
| target_sentence = user_text.replace("💡 語法分析:", "").strip() | |
| show_loading_animation(user_id, 10) | |
| try: | |
| start_time = time.time() | |
| analysis_data = deep_analyzer.get_structured_analysis(target_sentence, current_tribe) | |
| duration = time.time() - start_time | |
| logger.info(f"⏱️ [語法分析文字版] 耗時: {duration:.2f}s") | |
| # 1. 建立純文字回覆內容 | |
| reply_text = f"📖 【{current_tribe}語 語法深度解析】\n━━━━━━━━━━━━━━\n" | |
| if not analysis_data.get("is_long"): | |
| glossing = analysis_data.get("glossing", {}) | |
| l1 = glossing.get("line1", []) | |
| l2 = glossing.get("line2", []) | |
| l3 = glossing.get("line3", []) | |
| comp_text = "🎯 【逐詞分析】\n" | |
| for i in range(len(l1)): | |
| if l1[i] and l1[i].strip(): | |
| # 🎴 這裡精準替換成「構詞語法(族語)」與「構詞語法(華語)」 | |
| comp_text += f"▪️ {l1[i]}\n ├ 族語: {l2[i] if i < len(l2) else ''}\n └ 華語: {l3[i] if i < len(l3) else ''}\n" | |
| reply_text += comp_text + "\n" | |
| reply_text += f"💡 【深入解說】\n{analysis_data.get('explanation', '無詳細說明')}\n\n" | |
| reply_text += f"🏹 【翻譯結果】\n{analysis_data.get('translation', target_sentence)}" | |
| # 2. 建立「需要四行分析圖」的按鈕卡片 (Flex Message) | |
| if not analysis_data.get("is_long"): | |
| button_card = { | |
| "type": "bubble", | |
| "size": "kilo", | |
| "body": { | |
| "type": "box", "layout": "vertical", "contents": [ | |
| {"type": "text", "text": "需要將以上分析轉為精美圖卡嗎?", "wrap": True, "size": "sm", "color": "#666666"} | |
| ] | |
| }, | |
| "footer": { | |
| "type": "box", "layout": "vertical", "contents": [ | |
| { | |
| "type": "button", | |
| "style": "primary", | |
| "color": "#2B579A", | |
| "action": { | |
| "type": "message", | |
| "label": "🖼️ 生成四行分析圖卡", | |
| "text": f"🎨 繪製圖卡:{target_sentence}" | |
| } | |
| } | |
| ] | |
| } | |
| } | |
| # 同時發送「文字」與「按鈕」 | |
| line_bot_api.reply_message(event.reply_token, [ | |
| TextSendMessage(text=reply_text), | |
| FlexSendMessage(alt_text="生成圖卡選項", contents=button_card) | |
| ]) | |
| else: | |
| # 若為長句,就不顯示繪圖按鈕(避免畫出來擠在一起) | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text=reply_text)) | |
| db_manager.log_interaction(user_id, current_tribe, "analysis_text", target_sentence) | |
| except Exception as e: | |
| logger.error(f"語法分析邏輯錯誤: {e}") | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text="⚠️ 系統解析中,請稍後再試。")) | |
| return | |
| # ================================================================= | |
| # 🎨 繪製圖卡: 階段二 (接收繪圖指令並產出圖片) | |
| # ================================================================= | |
| if user_text.startswith("🎨 繪製圖卡:"): | |
| target_sentence = user_text.replace("🎨 繪製圖卡:", "").strip() | |
| show_loading_animation(user_id, 15) # 畫圖需要比較久 | |
| try: | |
| # 重新獲取一次分析資料 (為了畫圖) | |
| analysis_data = deep_analyzer.get_structured_analysis(target_sentence, current_tribe) | |
| # 準備圖片路徑 | |
| img_filename = f"analysis_{uuid.uuid4().hex[:8]}.png" | |
| img_path = os.path.join("static", img_filename) | |
| # 呼叫美術部門畫圖 | |
| visual_renderer.draw_glossing_card(analysis_data, img_path) | |
| image_url = f"{config.BASE_URL}/{img_path}" | |
| # 發送圖片 | |
| line_bot_api.reply_message( | |
| event.reply_token, | |
| ImageSendMessage(original_content_url=image_url, preview_image_url=image_url) | |
| ) | |
| except Exception as e: | |
| logger.error(f"圖卡繪製錯誤: {e}") | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text="⚠️ 抱歉,圖卡生成失敗,請確認您的 BASE_URL 網址設定是否正確。")) | |
| return | |
| # ================================================================= | |
| # 🧠 【新增關鍵路由】核心處理:執行「深度文化翻譯潤飾」 | |
| # ================================================================= | |
| if "深度翻譯" in user_text and "|" in user_text: | |
| # 1. 移除非必要的 Emoji 標題前綴 | |
| clean_text = user_text.replace("🧠 深度翻譯:", "").replace("深度翻譯:", "").strip() | |
| # 2. 切開中文字串與一般翻譯初稿 | |
| parts = clean_text.split("|") | |
| if len(parts) == 2: | |
| source_text = parts[0].strip() # 華語原文 | |
| target_text = parts[1].strip() # 族語初稿 | |
| show_loading_animation(user_id, loading_seconds=15) | |
| try: | |
| # 3. 呼叫大腦進行潤飾 | |
| refined_result = deep_analyzer.deep_translation(source_text, target_text, current_tribe) | |
| db_manager.log_interaction(user_id, current_tribe, "deep_translation", user_text) | |
| # 4. 直接回覆經過深度潤飾與說明的結果 | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text=refined_result)) | |
| return | |
| except Exception as e: | |
| logger.error(f"深度翻譯處理失敗: {e}") | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text="⚠️ 深度翻譯功能暫時無回應,請稍後再試。")) | |
| return | |
| # --------------------------------------------------------- | |
| # 💡 保險限制:攔截一般翻譯的過長文本 (只對一般翻譯生效) | |
| # --------------------------------------------------------- | |
| if len(user_text) > 180: | |
| limit_msg = ( | |
| "⚠️ 您要翻譯的句子太長了(超過 180 字),\n" | |
| "AI 會沒辦法快速思考,\n" | |
| "麻煩您將句子調整在 180 個字以內,感謝您。❤️" | |
| ) | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text=limit_msg)) | |
| return | |
| # --------------------------------------------------------- | |
| # 一般文字翻譯 (NLLB 模型 - 極速版) | |
| # --------------------------------------------------------- | |
| # 在開始運算前「立刻」告訴 LINE 要顯示動畫 | |
| show_loading_animation(user_id, loading_seconds=15) | |
| start_time = time.time() | |
| res = cached_translate(user_text, current_tribe) # 🎯 這裡已經被精準修正為 user_text 了! | |
| duration = time.time() - start_time | |
| logger.info(f"⏱️ [NLLB 一般翻譯效能] 輸入字數: {len(user_text)} | 耗時: {duration:.2f}s") | |
| db_manager.log_interaction(user_id, current_tribe, "text", user_text) | |
| target_output = res.get('target', '翻譯失敗') | |
| # 雖已有 180 字輸入限制,此處仍保留 200 字輸出保險,避免極端狀況燒毀 Token | |
| if len(target_output) > 200: | |
| line_bot_api.reply_message( | |
| event.reply_token, | |
| TextSendMessage(text=f"🏹 【{current_tribe}語結果】\n\n{target_output}") | |
| ) | |
| return | |
| try: | |
| card = ui_templates.create_translation_card( | |
| tribe=current_tribe, | |
| source_text=user_text, | |
| target_text=target_output, | |
| confidence_level=res.get("confidence_level", "high"), | |
| confidence_desc=f"耗時 {duration:.1f}s" # 💡 讓使用者也看到我們的極速成果 | |
| ) | |
| line_bot_api.reply_message(event.reply_token, FlexSendMessage(alt_text="翻譯結果", contents=card)) | |
| except Exception as e: | |
| logger.error(f"Flex Message 發送失敗: {e}") | |
| line_bot_api.push_message(user_id, TextSendMessage(text=f"🏹 【{current_tribe}語】\n{target_output}")) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |