Lowking commited on
Commit
3fc22fb
·
verified ·
1 Parent(s): b69c241

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +198 -0
  2. packages.txt +1 -0
  3. requirements.txt +7 -0
app.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ import sqlite3
4
+ import google.generativeai as genai
5
+ from fastapi import FastAPI, Request, HTTPException
6
+ from fastapi.staticfiles import StaticFiles
7
+ from linebot import LineBotApi, WebhookHandler
8
+ from linebot.exceptions import InvalidSignatureError
9
+ from linebot.models import *
10
+ from pydub import AudioSegment
11
+ from gradio_client import Client, handle_file
12
+ import uvicorn
13
+
14
+ # --- 1. 配置與環境變數 (雲端部署必備) ---
15
+ app = FastAPI()
16
+ if not os.path.exists("static"): os.makedirs("static")
17
+ app.mount("/static", StaticFiles(directory="static"), name="static")
18
+
19
+ # HF 部署時請在 Settings 設定這些 Secret
20
+ LINE_CHANNEL_ACCESS_TOKEN = os.getenv('LINE_TOKEN', '您的TOKEN')
21
+ LINE_CHANNEL_SECRET = os.getenv('LINE_SECRET', '您的SECRET')
22
+ NGROK_URL = os.getenv('BASE_URL', 'https://您的HF網址.hf.space')
23
+ GOOGLE_API_KEY = os.getenv('GEMINI_KEY', '您的GEMINI_KEY')
24
+
25
+ genai.configure(api_key=GOOGLE_API_KEY)
26
+ model = genai.GenerativeModel('gemini-3.1-pro-preview')
27
+
28
+ line_bot_api = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)
29
+ handler = WebhookHandler(LINE_CHANNEL_SECRET)
30
+
31
+ # --- 2. SQLite 資料庫初始化 ---
32
+ DB_FILE = "user_memory.db"
33
+
34
+ def init_db():
35
+ conn = sqlite3.connect(DB_FILE)
36
+ c = conn.cursor()
37
+ c.execute('''CREATE TABLE IF NOT EXISTS users
38
+ (user_id TEXT PRIMARY KEY, tribe TEXT, count INTEGER DEFAULT 0)''')
39
+ c.execute('''CREATE TABLE IF NOT EXISTS phrases
40
+ (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT, native TEXT, zh TEXT)''')
41
+ conn.commit()
42
+ conn.close()
43
+
44
+ init_db()
45
+
46
+ # --- 3. 族語配置與 API 客戶端 ---
47
+ TRIBE_CONFIG = {
48
+ "阿美": {"asr": "formosan_ami", "mt": "阿美"}, "泰雅": {"asr": "formosan_tay", "mt": "泰雅"},
49
+ "排灣": {"asr": "formosan_pwn", "mt": "排灣"}, "布農": {"asr": "formosan_bnn", "mt": "布農"},
50
+ "卑南": {"asr": "formosan_pyu", "mt": "卑南"}, "魯凱": {"asr": "formosan_dru", "mt": "魯凱"},
51
+ "鄒": {"asr": "formosan_tsu", "mt": "鄒"}, "賽夏": {"asr": "formosan_xsy", "mt": "賽夏"},
52
+ "雅美": {"asr": "formosan_tao", "mt": "雅美"}, "邵": {"asr": "formosan_ssf", "mt": "邵"},
53
+ "噶瑪蘭": {"asr": "formosan_ckv", "mt": "噶瑪蘭"}, "太魯閣": {"asr": "formosan_trv", "mt": "太魯閣"},
54
+ "撒奇萊雅": {"asr": "formosan_szy", "mt": "撒奇萊雅"}, "賽德克": {"asr": "formosan_sdq", "mt": "賽德克"},
55
+ "拉阿魯哇": {"asr": "formosan_sxr", "mt": "拉阿魯哇"}, "卡那卡那富": {"asr": "formosan_xnb", "mt": "卡那卡那富"}
56
+ }
57
+
58
+ asr_client = Client("https://ai-labs.ilrdf.org.tw/sapolita-kaldi/")
59
+ tts_client = Client("https://ai-labs.ilrdf.org.tw/hnang-kari-ai-asi-sluhay/")
60
+ mt_client = Client("https://ai-labs.ilrdf.org.tw/kari-seejiq-tnpusu-ai-hmjil/")
61
+
62
+ chat_sessions = {}
63
+
64
+ def get_clean_value(res):
65
+ if isinstance(res, dict) and 'value' in res: return res['value']
66
+ if isinstance(res, list) and len(res) > 0: return res[0]
67
+ return res
68
+
69
+ def get_ai_response(user_id, user_text, tribe_name):
70
+ if user_id not in chat_sessions:
71
+ chat_sessions[user_id] = model.start_chat(history=[])
72
+ chat_sessions[user_id].send_message(f"你現在是與我對話的{tribe_name}族朋友。請用中文聊天。規則:1.禁止教學。2.直接回答。3.限一短句。")
73
+ return chat_sessions[user_id].send_message(user_text).text
74
+
75
+ def create_chat_card(tribe, user_text, ai_text_zh, ai_text_native):
76
+ return {
77
+ "type": "bubble",
78
+ "header": {
79
+ "type": "box", "layout": "vertical", "contents": [
80
+ {"type": "text", "text": f"💬 {tribe}語 AI 導師", "weight": "bold", "color": "#FFFFFF", "size": "sm"}
81
+ ], "backgroundColor": "#8B0000"
82
+ },
83
+ "body": {
84
+ "type": "box", "layout": "vertical", "spacing": "sm", "contents": [
85
+ {"type": "text", "text": f"你說:{user_text}", "size": "xs", "color": "#888888", "wrap": True},
86
+ {"type": "text", "text": ai_text_native, "weight": "bold", "size": "xl", "wrap": True, "color": "#000000"},
87
+ {"type": "text", "text": f"(翻譯:{ai_text_zh})", "size": "sm", "color": "#555555", "wrap": True}
88
+ ]
89
+ },
90
+ "footer": {
91
+ "type": "box", "layout": "vertical", "contents": [
92
+ {"type": "button", "action": {"type": "message", "label": "🔄 切換語別", "text": "選單"}, "style": "secondary", "height": "sm"}
93
+ ]
94
+ }
95
+ }
96
+
97
+ def show_tribe_menu(event, start_index=0):
98
+ all_keys = list(TRIBE_CONFIG.keys())
99
+ if start_index == 0:
100
+ display_keys = all_keys[:10]
101
+ buttons = [QuickReplyButton(action=MessageAction(label=k, text=k)) for k in display_keys]
102
+ buttons.append(QuickReplyButton(action=MessageAction(label="更多族別", text="更多族別")))
103
+ msg = "請選擇練習語別:"
104
+ else:
105
+ display_keys = all_keys[10:]
106
+ buttons = [QuickReplyButton(action=MessageAction(label=k, text=k)) for k in display_keys]
107
+ buttons.append(QuickReplyButton(action=MessageAction(label="上一頁", text="選單")))
108
+ msg = "請選擇其他族別:"
109
+ line_bot_api.reply_message(event.reply_token, TextSendMessage(text=msg, quick_reply=QuickReply(items=buttons)))
110
+
111
+ # --- 4. 路由與訊息處理 ---
112
+ @app.post("/callback")
113
+ async def callback(request: Request):
114
+ signature = request.headers.get('X-Line-Signature', '')
115
+ body = await request.body()
116
+ try: handler.handle(body.decode(), signature)
117
+ except InvalidSignatureError: raise HTTPException(status_code=400)
118
+ return 'OK'
119
+
120
+ @handler.add(MessageEvent, message=TextMessage)
121
+ def handle_text(event):
122
+ text = event.message.text.strip()
123
+ user_id = event.source.user_id
124
+ conn = sqlite3.connect(DB_FILE)
125
+ c = conn.cursor()
126
+
127
+ if text in TRIBE_CONFIG:
128
+ c.execute("INSERT OR REPLACE INTO users (user_id, tribe, count) VALUES (?, ?, (SELECT count FROM users WHERE user_id=?))", (user_id, text, user_id))
129
+ conn.commit()
130
+ chat_sessions.pop(user_id, None)
131
+ line_bot_api.reply_message(event.reply_token, TextSendMessage(text=f"✅ 已切換至【{text}語】模式!"))
132
+ elif text == "選單" or text == "🔄 切換語別":
133
+ show_tribe_menu(event, 0)
134
+ elif text == "更多族別":
135
+ show_tribe_menu(event, 10)
136
+ elif text in ["學習記錄", "查詢記錄", "學習紀錄"]:
137
+ c.execute("SELECT count FROM users WHERE user_id=?", (user_id,))
138
+ row = c.fetchone()
139
+ count = row[0] if row else 0
140
+ line_bot_api.reply_message(event.reply_token, TextSendMessage(text=f"📈 【紀錄】累計對話:{count} 次"))
141
+ elif text in ["我的金句", "複習筆記"]:
142
+ c.execute("SELECT native, zh FROM phrases WHERE user_id=? ORDER BY id DESC LIMIT 5", (user_id,))
143
+ rows = c.fetchall()
144
+ msg = "📁 【最近金句】\n" + "\n".join([f"• {r[0]} ({r[1]})" for r in rows]) if rows else "📁 還沒有金句喔!"
145
+ line_bot_api.reply_message(event.reply_token, TextSendMessage(text=msg))
146
+ else:
147
+ show_tribe_menu(event, 0)
148
+ conn.close()
149
+
150
+ @handler.add(MessageEvent, message=AudioMessage)
151
+ def handle_audio(event):
152
+ user_id = event.source.user_id
153
+ conn = sqlite3.connect(DB_FILE)
154
+ c = conn.cursor()
155
+ c.execute("SELECT tribe FROM users WHERE user_id=?", (user_id,))
156
+ row = c.fetchone()
157
+ tribe = row[0] if row else None
158
+
159
+ if not tribe:
160
+ show_tribe_menu(event, 0)
161
+ return
162
+
163
+ config = TRIBE_CONFIG[tribe]
164
+ msg_id = event.message.id
165
+ reply_wav = f"static/ai_{msg_id}.wav"
166
+
167
+ try:
168
+ content = line_bot_api.get_message_content(msg_id)
169
+ with open(f"static/{msg_id}.m4a", "wb") as f: f.write(content.content)
170
+ audio = AudioSegment.from_file(f"static/{msg_id}.m4a")
171
+ audio.export(f"static/{msg_id}.wav", format="wav")
172
+
173
+ # ASR -> MT -> Gemini -> MT -> TTS
174
+ native_in = asr_client.predict(dialect_id=config["asr"], audio_data=handle_file(f"static/{msg_id}.wav"), api_name="/automatic_speech_recognition")
175
+ go_code = get_clean_value(mt_client.predict(ethnicity=config["mt"], api_name="/lambda"))
176
+ zh_in = mt_client.predict(text=native_in, src_lang=go_code, tgt_lang="zho_Hant", api_name="/translate")
177
+ ai_zh = get_ai_response(user_id, zh_in, tribe)
178
+ back_code = get_clean_value(mt_client.predict(ethnicity=config["mt"], api_name="/lambda_1"))
179
+ ai_native = get_clean_value(mt_client.predict(text=ai_zh, src_lang="zho_Hant", tgt_lang=back_code, api_name="/translate_1"))
180
+ speaker = get_clean_value(tts_client.predict(ethnicity=config["mt"], api_name="/lambda"))
181
+ temp_tts = tts_client.predict(ref=speaker, gen_text_input=ai_native, api_name="/default_speaker_tts")
182
+ shutil.move(temp_tts, reply_wav)
183
+
184
+ # SQL 存檔
185
+ c.execute("UPDATE users SET count = count + 1 WHERE user_id=?", (user_id,))
186
+ c.execute("INSERT INTO phrases (user_id, native, zh) VALUES (?, ?, ?)", (user_id, ai_native, ai_zh))
187
+ conn.commit()
188
+
189
+ line_bot_api.reply_message(event.reply_token, [
190
+ FlexSendMessage(alt_text="AI 對話", contents=create_chat_card(tribe, native_in, ai_zh, ai_native)),
191
+ AudioSendMessage(original_content_url=f"{NGROK_URL}/{reply_wav}", duration=len(audio))
192
+ ])
193
+ except Exception as e:
194
+ line_bot_api.reply_message(event.reply_token, TextSendMessage(text=f"AI 稍後回來: {str(e)}"))
195
+ conn.close()
196
+
197
+ if __name__ == "__main__":
198
+ uvicorn.run(app, host="0.0.0.0", port=7860)
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ffmpeg
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ line-bot-sdk
4
+ pydub
5
+ gradio_client
6
+ google-generativeai
7
+ python-multipart