kcrobot102 commited on
Commit
9b3393b
·
verified ·
1 Parent(s): 36657c5

initial commit

Browse files
Files changed (1) hide show
  1. app.py +447 -156
app.py CHANGED
@@ -1,189 +1,480 @@
1
- # ==========================================================
2
- # KC ROBOT AI - APP.PY (V2.0 MAX FINAL)
3
- # Cloud AI Robot with Gemini 2.5 Flash + ESP32 + Telegram
4
- # ==========================================================
5
-
6
- from flask import Flask, request, jsonify, render_template_string
7
- from google import genai
8
- import requests
 
 
 
9
  import os
 
 
 
10
  import time
 
 
 
 
 
 
 
 
11
  from gtts import gTTS
12
- from langdetect import detect
13
- import tempfile
14
- import base64
15
 
16
- # ==========================================================
17
- # CONFIGURATION
18
- # ==========================================================
 
 
 
 
 
 
 
 
 
19
 
20
- # Load environment variables from secrets (Cloud Run or Hugging Face)
21
- GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
22
- GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
23
- TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
24
- TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
25
 
26
- # Create Flask app
27
  app = Flask(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- # ==========================================================
30
- # SETUP GEMINI CLIENT
31
- # ==========================================================
32
- if not GEMINI_API_KEY:
33
- print("❌ ERROR: No Gemini API Key found. Please add GEMINI_API_KEY in Secrets.")
34
- client = None
35
- else:
36
- client = genai.Client(api_key=GEMINI_API_KEY)
37
-
38
- # ==========================================================
39
- # TELEGRAM UTILITIES
40
- # ==========================================================
41
- def send_telegram_message(text):
42
- if not TELEGRAM_TOKEN or not TELEGRAM_CHAT_ID:
43
- print("⚠️ Telegram not configured.")
 
 
44
  return
45
- url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage"
46
- payload = {"chat_id": TELEGRAM_CHAT_ID, "text": text}
47
  try:
48
- requests.post(url, json=payload, timeout=5)
49
- except Exception as e:
50
- print("Telegram Error:", e)
 
 
 
51
 
52
- # ==========================================================
53
- # GEMINI AI RESPONSE
54
- # ==========================================================
55
- def ask_gemini(prompt: str):
56
- if not client:
57
- return "⚠️ Gemini API key missing. Please configure in Secrets."
58
 
 
 
 
 
 
 
59
  try:
60
- response = client.models.generate_content(
61
- model=GEMINI_MODEL,
62
- contents=prompt
63
- )
64
- if hasattr(response, "text"):
65
- return response.text.strip()
66
- elif "text" in response:
67
- return response["text"].strip()
68
- else:
69
- return "⚠️ No response text from Gemini."
70
- except Exception as e:
71
- print("Gemini Error:", e)
72
- return f"⚠️ Gemini Error: {e}"
73
-
74
- # ==========================================================
75
- # LANGUAGE DETECTION & TTS
76
- # ==========================================================
77
- def text_to_speech(text):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  try:
79
- lang = detect(text)
80
- if lang not in ["vi", "en"]:
81
- lang = "en"
82
- tts = gTTS(text=text, lang=lang)
83
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
84
- tts.save(tmp.name)
85
- with open(tmp.name, "rb") as f:
86
- audio_b64 = base64.b64encode(f.read()).decode("utf-8")
87
- os.unlink(tmp.name)
88
- return audio_b64
89
- except Exception as e:
90
- print("TTS Error:", e)
91
  return None
92
 
93
- # ==========================================================
94
- # SIMPLE HTML INTERFACE (for testing)
95
- # ==========================================================
96
- HTML_PAGE = """
97
- <!DOCTYPE html>
98
- <html>
99
- <head>
100
- <title>KC Robot AI v2.0</title>
 
 
 
 
 
 
 
101
  <style>
102
- body { font-family: Arial; text-align: center; background-color: #101010; color: white; }
103
- input, button { padding: 10px; font-size: 16px; margin: 5px; }
104
- #chat { max-width: 700px; margin: auto; text-align: left; background: #202020; padding: 20px; border-radius: 10px; }
105
- .msg-user { color: #4af; }
106
- .msg-bot { color: #fa4; margin-left: 20px; }
107
- audio { margin-top: 10px; }
108
  </style>
109
- </head>
110
- <body>
111
- <h1>🤖 KC Robot AI v2.0 MAX FINAL</h1>
112
- <div id="chat"></div>
113
- <br>
114
- <input id="user_input" placeholder="Nói đó..." style="width:60%">
115
- <button onclick="sendMessage()">Gửi</button>
116
 
117
  <script>
118
- async function sendMessage() {
119
- const input = document.getElementById("user_input").value;
120
- if (!input) return;
121
- const chat = document.getElementById("chat");
122
- chat.innerHTML += `<div class='msg-user'><b>Bạn:</b> ${input}</div>`;
123
- document.getElementById("user_input").value = "";
124
- const res = await fetch("/api/chat", {
125
- method: "POST",
126
- headers: {"Content-Type": "application/json"},
127
- body: JSON.stringify({message: input})
128
- });
129
- const data = await res.json();
130
- chat.innerHTML += `<div class='msg-bot'><b>Robot:</b> ${data.reply}</div>`;
131
- if (data.audio) {
132
- const audio = document.createElement("audio");
133
- audio.src = "data:audio/mp3;base64," + data.audio;
134
- audio.controls = true;
135
- chat.appendChild(audio);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  }
137
- chat.scrollTop = chat.scrollHeight;
138
  }
139
  </script>
140
- </body>
141
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  """
143
 
 
144
  @app.route("/")
145
  def home():
146
- return render_template_string(HTML_PAGE)
 
 
 
147
 
148
- # ==========================================================
149
- # API ENDPOINTS
150
- # ==========================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
  @app.route("/api/chat", methods=["POST"])
153
  def api_chat():
154
- data = request.get_json()
155
- if not data or "message" not in data:
156
- return jsonify({"error": "Missing 'message'"}), 400
157
-
158
- user_message = data["message"]
159
- print(f"🧠 User said: {user_message}")
160
- send_telegram_message(f"User: {user_message}")
161
-
162
- ai_reply = ask_gemini(user_message)
163
- send_telegram_message(f"Robot: {ai_reply}")
164
-
165
- audio_b64 = text_to_speech(ai_reply)
166
- return jsonify({"reply": ai_reply, "audio": audio_b64})
167
-
168
- # ESP32 sensor endpoint
169
- @app.route("/api/sensor", methods=["POST"])
170
- def sensor_data():
171
- data = request.get_json()
172
- if not data:
173
- return jsonify({"error": "No data"}), 400
174
- msg = f"👁️ ESP32 Sensor update: {data}"
175
- send_telegram_message(msg)
176
- return jsonify({"status": "received"})
177
-
178
- # Health check
179
- @app.route("/ping")
180
- def ping():
181
- return jsonify({"status": "ok", "model": GEMINI_MODEL})
182
-
183
- # ==========================================================
184
- # MAIN ENTRY POINT
185
- # ==========================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  if __name__ == "__main__":
187
- port = int(os.getenv("PORT", 8080))
188
- print(f"🚀 KC Robot AI v2.0 running on port {port}")
189
  app.run(host="0.0.0.0", port=port)
 
 
1
+ """
2
+ RobotAI v9.8 Gemini Brain + gTTS + Telegram (Hugging Face ready)
3
+
4
+ Features:
5
+ - Keep original Gemini AI logic (uses google-generativeai)
6
+ - Web UI: auto language detect (vi/en), clean punctuation for TTS
7
+ - Only play the newest audio on the web (stop previous audio)
8
+ - Telegram integration (polling): replies with text and sends audio file if possible
9
+ - Audio caching and cleanup
10
+ """
11
+
12
  import os
13
+ import json
14
+ import uuid
15
+ import re
16
  import time
17
+ import logging
18
+ import threading
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 (try modern v20, fallback v13)
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 as e:
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
+ """
99
+ Call Gemini. Keep simple: use GenerativeModel if present (matches your v9.2).
100
+ """
101
+ cfg = load_config()
102
+ model = cfg.get("GEMINI_MODEL", DEFAULT_MODEL)
103
  try:
104
+ if hasattr(genai, "GenerativeModel"):
105
+ try:
106
+ m = genai.GenerativeModel(model)
107
+ resp = m.generate_content(prompt)
108
+ if hasattr(resp, "text"):
109
+ return resp.text
110
+ return str(resp)
111
+ except Exception:
112
+ logger.debug("GenerativeModel failed, trying responses.generate", exc_info=True)
113
+ # fallback: try responses.create (newer)
114
+ if hasattr(genai, "responses") and hasattr(genai.responses, "create"):
115
+ r = genai.responses.create(model=model, input=prompt)
116
+ if hasattr(r, "output_text"):
117
+ return r.output_text
118
+ return str(r)
119
+ except Exception:
120
+ logger.exception("Gemini call error")
121
+ return "⚠️ Gemini không phản hồi — kiểm tra API key / library."
122
+
123
+ # ----------------- Language detection & TTS cleaning -----------------
124
+ VIET_CHARS = "ăâđêôơưáàảãạắằẳẵặấầẩẫậéèẻẽẹíìỉĩịóòỏõọốồổỗộớờởỡợúùủũụưứừửữựýỳỷỹỵ"
125
+
126
+ def detect_lang(text: str) -> str:
127
+ return "vi" if any(ch in VIET_CHARS for ch in text.lower()) else "en"
128
+
129
+ # Clean punctuation but keep letters & numbers and basic punctuation replacement -> single spaces.
130
+ # We must not return empty string; fallback to short phrase.
131
+ def clean_text_for_tts(text: str) -> str:
132
+ if not text:
133
+ return "Xin chào"
134
+ # replace typical punctuation with space
135
+ cleaned = re.sub(r"[.,!?;:()\"'“”‘’\[\]{}<>\/\\\|@#\$%\^&\*\+=~`–—\-]", " ", text)
136
+ cleaned = re.sub(r"\s+", " ", cleaned).strip()
137
+ if not cleaned:
138
+ return "Xin chào"
139
+ return cleaned
140
+
141
+ def speak_text(text: str, lang: str = "vi") -> str:
142
+ """
143
+ Generate mp3 via gTTS, return local path or None.
144
+ """
145
  try:
146
+ tts_text = clean_text_for_tts(text)
147
+ fname = f"tts_{uuid.uuid4().hex[:8]}.mp3"
148
+ path = os.path.join(AUDIO_DIR, fname)
149
+ tts = gTTS(text=tts_text, lang=lang, slow=False)
150
+ tts.save(path)
151
+ logger.info("gTTS saved %s", path)
152
+ return path
153
+ except Exception:
154
+ logger.exception("TTS generation failed")
 
 
 
155
  return None
156
 
157
+ # ----------------- Audio cleanup -----------------
158
+ def cleanup_audio_older_than(seconds: int = 3600):
159
+ now = time.time()
160
+ for f in os.listdir(AUDIO_DIR):
161
+ p = os.path.join(AUDIO_DIR, f)
162
+ try:
163
+ if os.path.isfile(p) and (now - os.path.getmtime(p) > seconds):
164
+ os.remove(p)
165
+ logger.info("Removed old audio: %s", p)
166
+ except Exception:
167
+ logger.exception("cleanup failed for %s", p)
168
+
169
+ # ----------------- Web UI templates (client-side ensures only newest audio plays) -----------------
170
+ INDEX_HTML = """<!doctype html><html><head>
171
+ <meta charset="utf-8"><title>RobotAI v9.8</title>
172
  <style>
173
+ body{font-family:Arial;background:#f5faff;padding:12px}
174
+ #chat{background:#fff;border:1px solid #ddd;padding:12px;min-height:260px;border-radius:8px;overflow:auto}
175
+ .you{color:#0b66c3;margin:6px 0}
176
+ .bot{color:#0b8a5f;margin:6px 0}
177
+ .button{background:#0b66c3;color:white;border:none;padding:8px 12px;border-radius:6px;cursor:pointer}
178
+ .audio-controls{margin-top:6px}
179
  </style>
180
+ </head><body>
181
+ <h2>🤖 RobotAI v9.8 — Gemini Brain + Voice + Telegram</h2>
182
+ <div>Gemini: <b>{{ gemini_status }}</b> | Model: <b>{{ model }}</b> | <a href="/config">Config</a></div>
183
+ <hr>
184
+ <textarea id="text" rows="4" style="width:100%;padding:8px;border-radius:6px;border:1px solid #ccc"></textarea><br><br>
185
+ <button class="button" onclick="send()">Gửi</button> <button class="button" onclick="clearChat()">Xóa</button>
186
+ <div id="chat" style="margin-top:12px"></div>
187
 
188
  <script>
189
+ let currentAudio = null;
190
+
191
+ function append(cls, txt){
192
+ const c = document.getElementById('chat');
193
+ c.innerHTML += '<div class="'+cls+'">'+txt+'</div>';
194
+ c.scrollTop = c.scrollHeight;
195
+ }
196
+
197
+ function stopCurrentAudio(){
198
+ if(currentAudio){
199
+ try{ currentAudio.pause(); currentAudio.currentTime = 0; }catch(e){}
200
+ currentAudio = null;
201
+ }
202
+ }
203
+
204
+ function clearChat(){
205
+ document.getElementById('chat').innerHTML = '';
206
+ stopCurrentAudio();
207
+ }
208
+
209
+ async function send(){
210
+ let txt = document.getElementById('text').value.trim();
211
+ if(!txt) return;
212
+ append('you', 'Bạn: ' + txt);
213
+ document.getElementById('text').value = '';
214
+
215
+ // stop previous audio (so we only play the newest)
216
+ stopCurrentAudio();
217
+
218
+ try{
219
+ const res = await fetch('/api/chat', {
220
+ method: 'POST',
221
+ headers: {'Content-Type':'application/json'},
222
+ body: JSON.stringify({text: txt})
223
+ });
224
+ const j = await res.json();
225
+ append('bot', '🤖: ' + (j.reply || '(no reply)'));
226
+ if(j.tts_url){
227
+ currentAudio = new Audio(j.tts_url);
228
+ currentAudio.autoplay = true;
229
+ currentAudio.controls = false;
230
+ // optional: show an audio control for manual replay
231
+ const audioEl = document.createElement('audio');
232
+ audioEl.src = j.tts_url;
233
+ audioEl.controls = true;
234
+ audioEl.className = 'audio-controls';
235
+ document.getElementById('chat').appendChild(audioEl);
236
+ // play newest
237
+ try{ currentAudio.play(); }catch(e){}
238
+ }
239
+ }catch(e){
240
+ append('bot', '[Lỗi mạng] ' + e);
241
  }
 
242
  }
243
  </script>
244
+ </body></html>
245
+ """
246
+
247
+ CONFIG_HTML = """<!doctype html><html><head><meta charset="utf-8"><title>Config</title></head>
248
+ <body style="font-family:Arial;padding:12px">
249
+ <h3>⚙️ Config RobotAI</h3>
250
+ <form method="post" action="/config">
251
+ Gemini API Key:<br><textarea name="GEMINI_API_KEY" rows="2" cols="80">{{ GEMINI_API_KEY }}</textarea><br><br>
252
+ Gemini Model:<br><input name="GEMINI_MODEL" value="{{ GEMINI_MODEL }}" size="50"><br><br>
253
+ Telegram Token:<br><input name="TELEGRAM_TOKEN" value="{{ TELEGRAM_TOKEN }}" size="60"><br><br>
254
+ Telegram Chat ID (optional):<br><input name="TELEGRAM_CHAT_ID" value="{{ TELEGRAM_CHAT_ID }}" size="30"><br><br>
255
+ <button type="submit">Lưu</button>
256
+ </form>
257
+ <p><a href="/">⬅ Trở về</a></p>
258
+ </body></html>
259
  """
260
 
261
+ # ----------------- Flask routes -----------------
262
  @app.route("/")
263
  def home():
264
+ cfg = load_config()
265
+ return render_template_string(INDEX_HTML,
266
+ gemini_status="✅ Kết nối" if USE_GEMINI else "❌ Chưa kết nối",
267
+ model=cfg.get("GEMINI_MODEL"))
268
 
269
+ @app.route("/config", methods=["GET","POST"])
270
+ def config_page():
271
+ if request.method == "POST":
272
+ data = {
273
+ "GEMINI_API_KEY": request.form.get("GEMINI_API_KEY","").strip(),
274
+ "GEMINI_MODEL": request.form.get("GEMINI_MODEL", DEFAULT_MODEL).strip(),
275
+ "TELEGRAM_TOKEN": request.form.get("TELEGRAM_TOKEN","").strip(),
276
+ "TELEGRAM_CHAT_ID": request.form.get("TELEGRAM_CHAT_ID","").strip()
277
+ }
278
+ save_config(data)
279
+ init_gemini()
280
+ # restart telegram thread if token provided
281
+ try:
282
+ start_telegram_bot_thread()
283
+ except Exception:
284
+ logger.exception("start telegram thread failed")
285
+ return redirect(url_for("config_page"))
286
+ return render_template_string(CONFIG_HTML, **load_config())
287
 
288
  @app.route("/api/chat", methods=["POST"])
289
  def api_chat():
290
+ payload = request.get_json(force=True)
291
+ text = (payload.get("text") or "").strip()
292
+ if not text:
293
+ return jsonify({"error":"empty"}), 400
294
+
295
+ lang = detect_lang(text)
296
+ try:
297
+ if USE_GEMINI:
298
+ prefix = "Trả lời bằng tiếng Việt:" if lang == "vi" else "Answer in English:"
299
+ reply = gemini_answer(prefix + "\n" + text)
300
+ else:
301
+ reply = "⚠️ Chưa kết nối Gemini. Kiểm tra API Key."
302
+ except Exception:
303
+ logger.exception("gemini call")
304
+ reply = "⚠️ Lỗi khi gọi Gemini."
305
+
306
+ tts_path = None
307
+ try:
308
+ if reply:
309
+ tts_path = speak_text(reply, lang)
310
+ except Exception:
311
+ logger.exception("tts generation failed")
312
+
313
+ tts_url = f"/api/tts/{os.path.basename(tts_path)}" if tts_path else None
314
+
315
+ # send to telegram (background) if configured
316
+ try:
317
+ cfg = load_config()
318
+ token = cfg.get("TELEGRAM_TOKEN","")
319
+ chat_id = cfg.get("TELEGRAM_CHAT_ID","")
320
+ if token and chat_id:
321
+ threading.Thread(target=send_to_telegram_sync, args=(token, chat_id, reply, tts_path), daemon=True).start()
322
+ except Exception:
323
+ logger.exception("telegram dispatch failed")
324
+
325
+ # cleanup audio in background
326
+ try:
327
+ threading.Thread(target=cleanup_audio_older_than, kwargs={"seconds":3600}, daemon=True).start()
328
+ except Exception:
329
+ pass
330
+
331
+ return jsonify({"reply": reply, "tts_url": tts_url})
332
+
333
+ @app.route("/api/tts/<fname>")
334
+ def get_tts(fname):
335
+ path = os.path.join(AUDIO_DIR, fname)
336
+ if not os.path.exists(path):
337
+ return jsonify({"error":"not found"}), 404
338
+ return send_file(path, mimetype="audio/mpeg")
339
+
340
+ # ----------------- Telegram integration -----------------
341
+ TG_THREAD = None
342
+
343
+ def send_to_telegram_sync(token: str, chat_id: str, text: str, tts_path: str = None):
344
+ """
345
+ Send text and (optionally) audio to Telegram chat.
346
+ """
347
+ try:
348
+ bot = Bot(token=token)
349
+ try:
350
+ bot.send_message(chat_id=chat_id, text=text)
351
+ except Exception:
352
+ logger.exception("telegram send_message failed")
353
+ if tts_path and os.path.exists(tts_path):
354
+ try:
355
+ with open(tts_path, "rb") as fh:
356
+ bot.send_audio(chat_id=chat_id, audio=fh)
357
+ except Exception:
358
+ logger.exception("telegram send_audio failed")
359
+ except Exception:
360
+ logger.exception("telegram overall send failed")
361
+
362
+ def start_telegram_bot_thread():
363
+ global TG_THREAD
364
+ cfg = load_config()
365
+ token = cfg.get("TELEGRAM_TOKEN","")
366
+ if not token:
367
+ logger.info("Telegram token not configured; skipping")
368
+ return
369
+
370
+ if TG_THREAD and TG_THREAD.is_alive():
371
+ logger.info("Telegram thread already running.")
372
+ return
373
+
374
+ def _runner_v20(token):
375
+ try:
376
+ app_builder = ApplicationBuilder().token(token).build()
377
+
378
+ async def start_cmd(update, context):
379
+ await update.message.reply_text("RobotAI is running. Send me text and I'll reply with Gemini + TTS.")
380
+
381
+ async def handle_msg(update, context):
382
+ text = update.message.text or ""
383
+ if not text.strip():
384
+ await update.message.reply_text("Không nhận được nội dung.")
385
+ return
386
+ lang = detect_lang(text)
387
+ if USE_GEMINI:
388
+ prefix = "Trả lời bằng tiếng Việt:" if lang == "vi" else "Answer in English:"
389
+ reply = gemini_answer(prefix + "\n" + text)
390
+ else:
391
+ reply = "⚠️ Chưa kết nối Gemini."
392
+
393
+ # generate tts using same cleaning logic
394
+ tts_path = None
395
+ try:
396
+ if reply:
397
+ tts_path = speak_text(reply, lang)
398
+ except Exception:
399
+ logger.exception("tts for telegram failed")
400
+
401
+ await update.message.reply_text(reply)
402
+ if tts_path and os.path.exists(tts_path):
403
+ try:
404
+ with open(tts_path, "rb") as fh:
405
+ await context.bot.send_audio(chat_id=update.effective_chat.id, audio=fh)
406
+ except Exception:
407
+ logger.exception("send audio to telegram (v20) failed")
408
+
409
+ app_builder.add_handler(CommandHandler("start", start_cmd))
410
+ app_builder.add_handler(MessageHandler(filters.TEXT & (~filters.COMMAND), handle_msg))
411
+ logger.info("Starting Telegram polling (v20)...")
412
+ app_builder.run_polling()
413
+ except Exception:
414
+ logger.exception("telegram v20 runner exception")
415
+
416
+ def _runner_v13(token):
417
+ try:
418
+ updater = Updater(token=token, use_context=True)
419
+ dp = updater.dispatcher
420
+
421
+ def start_cmd(update, context):
422
+ update.message.reply_text("RobotAI is running. Send me text and I'll reply with Gemini + TTS.")
423
+
424
+ def handle_msg(update, context):
425
+ text = update.message.text or ""
426
+ if not text.strip():
427
+ update.message.reply_text("Không nhận được nội dung.")
428
+ return
429
+ lang = detect_lang(text)
430
+ if USE_GEMINI:
431
+ prefix = "Trả lời bằng tiếng Việt:" if lang == "vi" else "Answer in English:"
432
+ reply = gemini_answer(prefix + "\n" + text)
433
+ else:
434
+ reply = "⚠️ Chưa kết nối Gemini."
435
+
436
+ tts_path = None
437
+ try:
438
+ if reply:
439
+ tts_path = speak_text(reply, lang)
440
+ except Exception:
441
+ logger.exception("tts for telegram failed")
442
+
443
+ update.message.reply_text(reply)
444
+ if tts_path and os.path.exists(tts_path):
445
+ try:
446
+ context.bot.send_audio(chat_id=update.effective_chat.id, audio=open(tts_path, "rb"))
447
+ except Exception:
448
+ logger.exception("send audio to telegram (v13) failed")
449
+
450
+ dp.add_handler(CommandHandler("start", start_cmd))
451
+ dp.add_handler(MessageHandler(Filters.text & (~Filters.command), handle_msg))
452
+ updater.start_polling()
453
+ updater.idle()
454
+ except Exception:
455
+ logger.exception("telegram v13 runner exception")
456
+
457
+ def runner():
458
+ if TELEGRAM_LIB == "v20":
459
+ _runner_v20(token)
460
+ elif TELEGRAM_LIB == "v13":
461
+ _runner_v13(token)
462
+ else:
463
+ logger.warning("python-telegram-bot not available; skipping telegram polling")
464
+
465
+ TG_THREAD = threading.Thread(target=runner, daemon=True)
466
+ TG_THREAD.start()
467
+ logger.info("Telegram thread launched (if configured).")
468
+
469
+ # start telegram thread at startup if token present
470
+ try:
471
+ start_telegram_bot_thread()
472
+ except Exception:
473
+ logger.exception("Starting telegram thread failed")
474
+
475
+ # ----------------- Run -----------------
476
  if __name__ == "__main__":
477
+ port = int(os.environ.get("PORT", 7860))
478
+ logger.info("Starting RobotAI v9.8 on 0.0.0.0:%s", port)
479
  app.run(host="0.0.0.0", port=port)
480
+