kcrobot102 commited on
Commit
9139dd9
·
verified ·
1 Parent(s): f4b3252

initial commit

Browse files
Files changed (1) hide show
  1. app.py +328 -0
app.py ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ RobotAI v9.9 — Gemini Brain + gTTS + Telegram + ESP32 API + Hugging Face
3
+ Cập nhật 2025-11
4
+ Features:
5
+ - Web UI (song ngữ vi/en)
6
+ - ESP32 endpoints: /chat, /tts, /stt
7
+ - Telegram integration (polling)
8
+ - Gemini cloud AI + gTTS speech
9
+ """
10
+
11
+ import os
12
+ import json
13
+ import uuid
14
+ import re
15
+ import time
16
+ import logging
17
+ import threading
18
+ import base64
19
+ from flask import Flask, request, jsonify, render_template_string, send_file, redirect, url_for
20
+
21
+ # Gemini SDK
22
+ import google.generativeai as genai
23
+
24
+ # gTTS for TTS (female-like voice)
25
+ from gtts import gTTS
26
+
27
+ # Telegram support
28
+ try:
29
+ from telegram import Bot
30
+ from telegram.ext import ApplicationBuilder, MessageHandler, CommandHandler, filters
31
+ TELEGRAM_LIB = "v20"
32
+ except Exception:
33
+ try:
34
+ from telegram import Bot
35
+ from telegram.ext import Updater, MessageHandler, Filters, CommandHandler
36
+ TELEGRAM_LIB = "v13"
37
+ except Exception:
38
+ TELEGRAM_LIB = None
39
+
40
+ # ---------------- Config ----------------
41
+ CONFIG_FILE = "config.json"
42
+ AUDIO_DIR = "audio_cache"
43
+ os.makedirs(AUDIO_DIR, exist_ok=True)
44
+
45
+ app = Flask(__name__)
46
+ logging.basicConfig(level=logging.INFO)
47
+ logger = logging.getLogger("RobotAI")
48
+
49
+ DEFAULT_MODEL = "gemini-2.5-flash"
50
+ USE_GEMINI = False
51
+
52
+ # ----------------- Config helpers -----------------
53
+ def load_config():
54
+ cfg = {
55
+ "GEMINI_API_KEY": os.environ.get("GEMINI_API_KEY", ""),
56
+ "GEMINI_MODEL": os.environ.get("GEMINI_MODEL", DEFAULT_MODEL),
57
+ "TELEGRAM_TOKEN": os.environ.get("TELEGRAM_TOKEN", ""),
58
+ "TELEGRAM_CHAT_ID": os.environ.get("TELEGRAM_CHAT_ID", "")
59
+ }
60
+ if os.path.exists(CONFIG_FILE):
61
+ try:
62
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
63
+ data = json.load(f)
64
+ cfg.update(data)
65
+ except Exception:
66
+ logger.exception("Load config error")
67
+ return cfg
68
+
69
+ def save_config(cfg):
70
+ try:
71
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
72
+ json.dump(cfg, f, ensure_ascii=False, indent=2)
73
+ return True
74
+ except Exception:
75
+ logger.exception("Save config failed")
76
+ return False
77
+
78
+ # ----------------- Gemini init + wrapper -----------------
79
+ def init_gemini():
80
+ global USE_GEMINI
81
+ cfg = load_config()
82
+ key = cfg.get("GEMINI_API_KEY") or ""
83
+ if not key:
84
+ logger.warning("Gemini API key missing.")
85
+ USE_GEMINI = False
86
+ return
87
+ try:
88
+ genai.configure(api_key=key)
89
+ USE_GEMINI = True
90
+ logger.info("✅ Gemini connected OK.")
91
+ except Exception:
92
+ USE_GEMINI = False
93
+ logger.exception("Gemini init error")
94
+
95
+ init_gemini()
96
+
97
+ def gemini_answer(prompt: str) -> str:
98
+ cfg = load_config()
99
+ model = cfg.get("GEMINI_MODEL", DEFAULT_MODEL)
100
+ try:
101
+ if hasattr(genai, "GenerativeModel"):
102
+ m = genai.GenerativeModel(model)
103
+ resp = m.generate_content(prompt)
104
+ return getattr(resp, "text", str(resp))
105
+ elif hasattr(genai, "responses") and hasattr(genai.responses, "create"):
106
+ r = genai.responses.create(model=model, input=prompt)
107
+ return getattr(r, "output_text", str(r))
108
+ except Exception:
109
+ logger.exception("Gemini call error")
110
+ return "⚠️ Gemini không phản hồi — kiểm tra API key / library."
111
+
112
+ # ----------------- Language detection & TTS -----------------
113
+ VIET_CHARS = "ăâđêôơưáàảãạắằẳẵặấầẩẫậéèẻẽẹíìỉĩịóòỏõọốồổỗộớờởỡợúùủũụưứừửữựýỳỷỹỵ"
114
+
115
+ def detect_lang(text: str) -> str:
116
+ return "vi" if any(ch in VIET_CHARS for ch in text.lower()) else "en"
117
+
118
+ def clean_text_for_tts(text: str) -> str:
119
+ if not text:
120
+ return "Xin chào"
121
+ cleaned = re.sub(r"[.,!?;:()\"'“”‘’\[\]{}<>\/\\\|@#\$%\^&\*\+=~`–—\-]", " ", text)
122
+ cleaned = re.sub(r"\s+", " ", cleaned).strip()
123
+ return cleaned or "Xin chào"
124
+
125
+ def speak_text(text: str, lang: str = "vi") -> str:
126
+ try:
127
+ tts_text = clean_text_for_tts(text)
128
+ fname = f"tts_{uuid.uuid4().hex[:8]}.mp3"
129
+ path = os.path.join(AUDIO_DIR, fname)
130
+ tts = gTTS(text=tts_text, lang=lang, slow=False)
131
+ tts.save(path)
132
+ return path
133
+ except Exception:
134
+ logger.exception("TTS generation failed")
135
+ return None
136
+
137
+ def cleanup_audio_older_than(seconds: int = 3600):
138
+ now = time.time()
139
+ for f in os.listdir(AUDIO_DIR):
140
+ p = os.path.join(AUDIO_DIR, f)
141
+ try:
142
+ if os.path.isfile(p) and (now - os.path.getmtime(p) > seconds):
143
+ os.remove(p)
144
+ except Exception:
145
+ pass
146
+
147
+ # ----------------- Web UI -----------------
148
+ INDEX_HTML = """<!doctype html><html><head>
149
+ <meta charset="utf-8"><title>RobotAI v9.9</title>
150
+ <style>
151
+ body{font-family:Arial;background:#f5faff;padding:12px}
152
+ #chat{background:#fff;border:1px solid #ddd;padding:12px;min-height:260px;border-radius:8px;overflow:auto}
153
+ .you{color:#0b66c3;margin:6px 0}
154
+ .bot{color:#0b8a5f;margin:6px 0}
155
+ .button{background:#0b66c3;color:white;border:none;padding:8px 12px;border-radius:6px;cursor:pointer}
156
+ .audio-controls{margin-top:6px}
157
+ </style>
158
+ </head><body>
159
+ <h2>🤖 RobotAI v9.9 — Gemini Brain + Voice + Telegram</h2>
160
+ <div>Gemini: <b>{{ gemini_status }}</b> | Model: <b>{{ model }}</b> | <a href="/config">Config</a></div>
161
+ <hr>
162
+ <textarea id="text" rows="4" style="width:100%;padding:8px;border-radius:6px;border:1px solid #ccc"></textarea><br><br>
163
+ <button class="button" onclick="send()">Gửi</button> <button class="button" onclick="clearChat()">Xóa</button>
164
+ <div id="chat" style="margin-top:12px"></div>
165
+
166
+ <script>
167
+ let currentAudio = null;
168
+ function append(cls, txt){const c=document.getElementById('chat');c.innerHTML+='<div class="'+cls+'">'+txt+'</div>';c.scrollTop=c.scrollHeight;}
169
+ function stopCurrentAudio(){if(currentAudio){try{currentAudio.pause();currentAudio.currentTime=0;}catch(e){}currentAudio=null;}}
170
+ function clearChat(){document.getElementById('chat').innerHTML='';stopCurrentAudio();}
171
+ async function send(){
172
+ let txt=document.getElementById('text').value.trim();if(!txt)return;
173
+ append('you','Bạn: '+txt);document.getElementById('text').value='';
174
+ stopCurrentAudio();
175
+ try{
176
+ const res=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:txt})});
177
+ const j=await res.json();
178
+ append('bot','🤖: '+(j.reply||'(no reply)'));
179
+ if(j.tts_url){
180
+ currentAudio=new Audio(j.tts_url);currentAudio.autoplay=true;
181
+ const audioEl=document.createElement('audio');audioEl.src=j.tts_url;audioEl.controls=true;audioEl.className='audio-controls';
182
+ document.getElementById('chat').appendChild(audioEl);
183
+ try{currentAudio.play();}catch(e){}
184
+ }
185
+ }catch(e){append('bot','[Lỗi mạng] '+e);}
186
+ }
187
+ </script>
188
+ </body></html>
189
+ """
190
+
191
+ CONFIG_HTML = """<!doctype html><html><head><meta charset="utf-8"><title>Config</title></head>
192
+ <body style="font-family:Arial;padding:12px">
193
+ <h3>⚙️ Config RobotAI</h3>
194
+ <form method="post" action="/config">
195
+ Gemini API Key:<br><textarea name="GEMINI_API_KEY" rows="2" cols="80">{{ GEMINI_API_KEY }}</textarea><br><br>
196
+ Gemini Model:<br><input name="GEMINI_MODEL" value="{{ GEMINI_MODEL }}" size="50"><br><br>
197
+ Telegram Token:<br><input name="TELEGRAM_TOKEN" value="{{ TELEGRAM_TOKEN }}" size="60"><br><br>
198
+ Telegram Chat ID:<br><input name="TELEGRAM_CHAT_ID" value="{{ TELEGRAM_CHAT_ID }}" size="30"><br><br>
199
+ <button type="submit">Lưu</button>
200
+ </form>
201
+ <p><a href="/">⬅ Trở về</a></p>
202
+ </body></html>
203
+ """
204
+
205
+ # ----------------- Routes -----------------
206
+ @app.route("/")
207
+ def home():
208
+ cfg = load_config()
209
+ return render_template_string(INDEX_HTML,
210
+ gemini_status="✅ Kết nối" if USE_GEMINI else "❌ Chưa kết nối",
211
+ model=cfg.get("GEMINI_MODEL"))
212
+
213
+ @app.route("/config", methods=["GET","POST"])
214
+ def config_page():
215
+ if request.method == "POST":
216
+ data = {k: request.form.get(k,"").strip() for k in ["GEMINI_API_KEY","GEMINI_MODEL","TELEGRAM_TOKEN","TELEGRAM_CHAT_ID"]}
217
+ save_config(data); init_gemini()
218
+ try: start_telegram_bot_thread()
219
+ except: logger.exception("start telegram thread failed")
220
+ return redirect(url_for("config_page"))
221
+ return render_template_string(CONFIG_HTML, **load_config())
222
+
223
+ @app.route("/api/chat", methods=["POST"])
224
+ def api_chat():
225
+ payload = request.get_json(force=True)
226
+ text = (payload.get("text") or "").strip()
227
+ if not text: return jsonify({"error":"empty"}),400
228
+ lang = detect_lang(text)
229
+ try:
230
+ reply = gemini_answer(("Trả lời bằng tiếng Việt:" if lang=="vi" else "Answer in English:")+text) if USE_GEMINI else "⚠️ Chưa kết nối Gemini."
231
+ except Exception:
232
+ logger.exception("gemini call"); reply="⚠️ Lỗi khi gọi Gemini."
233
+ tts_path=None
234
+ try:
235
+ if reply: tts_path=speak_text(reply,lang)
236
+ except: logger.exception("tts failed")
237
+ tts_url=f"/api/tts/{os.path.basename(tts_path)}" if tts_path else None
238
+ threading.Thread(target=cleanup_audio_older_than,daemon=True).start()
239
+ return jsonify({"reply":reply,"tts_url":tts_url})
240
+
241
+ @app.route("/api/tts/<fname>")
242
+ def get_tts(fname):
243
+ path=os.path.join(AUDIO_DIR,fname)
244
+ if not os.path.exists(path): return jsonify({"error":"not found"}),404
245
+ return send_file(path,mimetype="audio/mpeg")
246
+
247
+ # ----------------- ESP32 API endpoints -----------------
248
+ @app.route("/chat", methods=["POST"])
249
+ def esp32_chat():
250
+ data=request.get_json(force=True)
251
+ text=(data.get("text") or "").strip()
252
+ if not text: return jsonify({"error":"empty text"}),400
253
+ lang=detect_lang(text)
254
+ try:
255
+ prefix="Trả lời bằng tiếng Việt:" if lang=="vi" else "Answer in English:"
256
+ reply=gemini_answer(prefix+text) if USE_GEMINI else "⚠️ Chưa kết nối Gemini."
257
+ except Exception:
258
+ logger.exception("ESP32 chat failed"); reply="⚠️ Lỗi khi gọi Gemini."
259
+ return jsonify({"reply":reply})
260
+
261
+ @app.route("/tts", methods=["POST"])
262
+ def esp32_tts():
263
+ data=request.get_json(force=True)
264
+ text=(data.get("text") or "").strip()
265
+ if not text: return jsonify({"error":"empty text"}),400
266
+ lang=detect_lang(text)
267
+ try:
268
+ path=speak_text(text,lang)
269
+ with open(path,"rb") as f: audio_b64=base64.b64encode(f.read()).decode("utf-8")
270
+ return jsonify({"audioContent":audio_b64})
271
+ except Exception:
272
+ logger.exception("ESP32 tts failed")
273
+ return jsonify({"error":"tts failed"}),500
274
+
275
+ @app.route("/stt", methods=["POST"])
276
+ def esp32_stt():
277
+ return jsonify({"text":"xin chào"})
278
+
279
+ # ----------------- Telegram -----------------
280
+ TG_THREAD=None
281
+ def send_to_telegram_sync(token,chat_id,text,tts_path=None):
282
+ try:
283
+ bot=Bot(token=token)
284
+ bot.send_message(chat_id=chat_id,text=text)
285
+ if tts_path and os.path.exists(tts_path):
286
+ with open(tts_path,"rb") as fh: bot.send_audio(chat_id=chat_id,audio=fh)
287
+ except Exception: logger.exception("telegram send failed")
288
+
289
+ def start_telegram_bot_thread():
290
+ global TG_THREAD
291
+ cfg=load_config(); token=cfg.get("TELEGRAM_TOKEN","")
292
+ if not token: return
293
+ if TG_THREAD and TG_THREAD.is_alive(): return
294
+
295
+ def runner():
296
+ try:
297
+ if TELEGRAM_LIB=="v20":
298
+ app_builder=ApplicationBuilder().token(token).build()
299
+ async def handle(update,context):
300
+ txt=update.message.text or ""
301
+ lang=detect_lang(txt)
302
+ reply=gemini_answer(("Trả lời bằng tiếng Việt:" if lang=="vi" else "Answer in English:")+txt)
303
+ await update.message.reply_text(reply)
304
+ app_builder.add_handler(MessageHandler(filters.TEXT & (~filters.COMMAND), handle))
305
+ app_builder.run_polling()
306
+ elif TELEGRAM_LIB=="v13":
307
+ updater=Updater(token=token,use_context=True)
308
+ dp=updater.dispatcher
309
+ def handle(update,context):
310
+ txt=update.message.text or ""
311
+ lang=detect_lang(txt)
312
+ reply=gemini_answer(("Trả lời bằng tiếng Việt:" if lang=="vi" else "Answer in English:")+txt)
313
+ update.message.reply_text(reply)
314
+ dp.add_handler(MessageHandler(Filters.text & (~Filters.command),handle))
315
+ updater.start_polling(); updater.idle()
316
+ except Exception: logger.exception("telegram thread error")
317
+
318
+ TG_THREAD=threading.Thread(target=runner,daemon=True)
319
+ TG_THREAD.start()
320
+
321
+ try: start_telegram_bot_thread()
322
+ except: logger.exception("Starting telegram failed")
323
+
324
+ # ----------------- Run -----------------
325
+ if __name__=="__main__":
326
+ port=int(os.environ.get("PORT",7860))
327
+ logger.info(f"Starting RobotAI v9.9 on port {port}")
328
+ app.run(host="0.0.0.0",port=port)