kcrobot20 commited on
Commit
0b3e626
·
verified ·
1 Parent(s): 75cf759

initial commit

Browse files
Files changed (1) hide show
  1. app.py +103 -419
app.py CHANGED
@@ -1,508 +1,192 @@
 
 
1
  # ==========================================================
2
- # KC ROBOT AI - APP.PY (V3.0 MAX PRO)
3
  # Cloud AI Robot with Gemini 2.5 Flash + ESP32 + Telegram
4
- # - Giữ nguyên flow gốc của bạn
5
- # - Chỉ thay đổi phần xử lý văn bản trước khi TTS
6
- # - Mục tiêu: robot CHỈ đọc chữ, BỎ tất cả ký tự/dấu/emoji
7
  # ==========================================================
8
 
9
- # Standard libraries
10
- import os
11
- import time
12
- import re
13
- import base64
14
- import tempfile
15
- from typing import Optional
16
-
17
- # Third-party
18
- from flask import Flask, request, jsonify, render_template_string, Response
19
  from google import genai
20
  import requests
 
 
21
  from gtts import gTTS
22
  from langdetect import detect
 
 
23
 
24
  # ==========================================================
25
- # Configuration - đọc từ environment / secrets
26
  # ==========================================================
 
 
27
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
28
  GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
29
  TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
30
  TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
31
- PORT = int(os.getenv("PORT", 8080))
32
- DEBUG = os.getenv("FLASK_DEBUG", "0") in ("1", "true", "True")
33
- # Option: preserve_original_case = True => giữ nguyên chữ viết hoa / thường
34
- PRESERVE_ORIGINAL_CASE = True
35
 
36
- # ==========================================================
37
- # App init
38
- # ==========================================================
39
  app = Flask(__name__)
40
 
41
  # ==========================================================
42
- # Gemini client setup (Google generative AI client)
43
  # ==========================================================
44
  if not GEMINI_API_KEY:
45
- print("❌ WARNING: GEMINI_API_KEY not set. Gemini calls will be disabled.")
46
- gemini_client = None
47
  else:
48
- try:
49
- gemini_client = genai.Client(api_key=GEMINI_API_KEY)
50
- except Exception as e:
51
- print("❌ ERROR initializing Gemini client:", e)
52
- gemini_client = None
53
 
54
  # ==========================================================
55
- # Telegram helper
56
  # ==========================================================
57
- def send_telegram_message(text: str) -> None:
58
- """
59
- Gửi tin nhắn đơn giản lên Telegram (bot).
60
- Nếu Telegram không cấu hình thì log ra console.
61
- """
62
  if not TELEGRAM_TOKEN or not TELEGRAM_CHAT_ID:
63
- # Telegram chưa cấu hình -> thông báo console, không raise
64
- print("⚠️ Telegram not configured. Skipping send_telegram_message.")
65
  return
 
 
66
  try:
67
- url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage"
68
- payload = {"chat_id": TELEGRAM_CHAT_ID, "text": text}
69
  requests.post(url, json=payload, timeout=5)
70
  except Exception as e:
71
  print("Telegram Error:", e)
72
 
73
-
74
  # ==========================================================
75
- # Gemini interaction
76
  # ==========================================================
77
- def ask_gemini(prompt: str, model: Optional[str] = None) -> str:
78
- """
79
- Gọi Gemini để lấy reply.
80
- Trả về chuỗi text (luôn là string).
81
- Nếu Gemini không cấu hình hoặc lỗi => trả về thông báo lỗi.
82
- """
83
- if gemini_client is None:
84
- return "⚠️ Gemini API key missing or client init failure."
85
  try:
86
- model_to_use = model or GEMINI_MODEL
87
- # Sử dụng generate_content tương tự bản trước
88
- response = gemini_client.models.generate_content(
89
- model=model_to_use,
90
  contents=prompt
91
  )
92
- # response có thể là object hoặc dict-like
93
  if hasattr(response, "text"):
94
  return response.text.strip()
95
- if isinstance(response, dict) and "text" in response:
96
  return response["text"].strip()
97
- # fallback: try string conversion
98
- return str(response).strip()
99
  except Exception as e:
100
  print("Gemini Error:", e)
101
  return f"⚠️ Gemini Error: {e}"
102
 
103
-
104
  # ==========================================================
105
- # Text cleaning utilities for TTS
106
  # ==========================================================
107
- # Mục tiêu: giữ lại chữ (tiếng Việt/Anh), loại bỏ:
108
- # - từ mô tả dấu như "dấu chấm", "dấu phẩy", "phẩy", "chấm"
109
- # - ký tự punctuation thực tế . , ; : ! ? ( ) " ' - _ ...
110
- # - emoji hoặc ký hiệu (Unicode pictographs)
111
- # - dấu ngoặc, hashtag, mentions, URLs
112
- # - giữ nguyên chữ, giữ nguyên số chữ cái, giữ nguyên case nếu PRESERVE_ORIGINAL_CASE True
113
- #
114
- # Lưu ý: không ép viết hoa đầu câu theo yêu cầu "giữ nguyên chữ gốc".
115
- # Nếu người muốn thay đổi, có thể bật option bên trên.
116
-
117
- # Regex patterns
118
- _RE_URL = re.compile(r"https?://\S+|www\.\S+")
119
- _RE_EMAIL = re.compile(r"\S+@\S+")
120
- _RE_USER_MENTION = re.compile(r"@\w+")
121
- _RE_HASHTAG = re.compile(r"#\w+")
122
- # Punctuation characters to remove
123
- _RE_PUNCT_CHARS = re.compile(r"[.,;:!?\"'()\[\]{}/\\<>@#$%^&*_+=~`|°•…·–—\-]")
124
-
125
- # Words that represent punctuation (Vietnamese common terms and variants)
126
- _PUNCT_WORD_PATTERNS = [
127
- r"\bdấu\s*chấm\b", r"\bchấm\b",
128
- r"\bdấu\s*phẩy\b", r"\bphẩy\b", r"\bphay\b",
129
- r"\bdấu\s*sao\b", r"\bsao\b",
130
- r"\bdấu\s*chấm\s*phẩy\b", r"\bchấm\s*phẩy\b",
131
- r"\bdấu\s*hỏi\b", r"\bhỏi\b",
132
- r"\bdấu\s*hai\s*chấm\b", r"\bhai\s*chấm\b",
133
- r"\bdấu\s*ngoặc\b", r"\bngoặc\b",
134
- r"\bdấu\s*gạch\b", r"\bgạch\b",
135
- r"\bdấu\s*gạch\s*dưới\b", r"\bgạch\s*dưới\b",
136
- r"\bdấu\s*chéo\b",
137
- r"\bdấu\s*phẩy\b",
138
- r"\bdấu\s*than\b", r"\bthan\b",
139
- # english words
140
- r"\bdot\b", r"\bcomma\b", r"\bperiod\b", r"\bfullstop\b",
141
- r"\bexclamation\b", r"\bquestion\b", r"\basterisk\b"
142
- ]
143
-
144
- # Emoji and pictographs (range) removal
145
- # A conservative regex to remove common emoji ranges
146
- _RE_EMOJI = re.compile(
147
- "[" # start char class
148
- "\U0001F600-\U0001F64F" # emoticons
149
- "\U0001F300-\U0001F5FF" # symbols & pictographs
150
- "\U0001F680-\U0001F6FF" # transport & map symbols
151
- "\U00002600-\U000026FF" # misc symbols
152
- "\U00002700-\U000027BF" # dingbats
153
- "\U0000FE00-\U0000FE0F" # variation selectors
154
- "\U0001F900-\U0001F9FF" # supplemental symbols and pictographs
155
- "\U0001F1E6-\U0001F1FF" # flags (iOS)
156
- "]", flags=re.UNICODE
157
- )
158
-
159
- # Combining marks and other symbols often seen in emojis
160
- _RE_SYMBOLS = re.compile(r"[\u2000-\u2BFF\u2C60-\u2C7F\u2E00-\u2E7F]")
161
-
162
- # Normalize whitespace
163
- _RE_MULTI_SPACE = re.compile(r"\s+")
164
-
165
-
166
- def remove_urls_emails_mentions_hashtags(text: str) -> str:
167
- t = _RE_URL.sub(" ", text)
168
- t = _RE_EMAIL.sub(" ", t)
169
- t = _RE_USER_MENTION.sub(" ", t)
170
- t = _RE_HASHTAG.sub(" ", t)
171
- return t
172
-
173
-
174
- def remove_punctuation_words(text: str) -> str:
175
- t = text
176
- for patt in _PUNCT_WORD_PATTERNS:
177
- t = re.sub(patt, " ", t, flags=re.IGNORECASE)
178
- return t
179
-
180
-
181
- def remove_punct_chars(text: str) -> str:
182
- # Remove typical punctuation chars
183
- return _RE_PUNCT_CHARS.sub(" ", text)
184
-
185
-
186
- def remove_emoji_and_symbols(text: str) -> str:
187
- t = _RE_EMOJI.sub(" ", text)
188
- t = _RE_SYMBOLS.sub(" ", t)
189
- return t
190
-
191
-
192
- def keep_only_letters_numbers_and_spaces(text: str) -> str:
193
- """
194
- Giữ lại chữ, số và khoảng trắng.
195
- Loại bỏ các ký tự đặc biệt còn lại.
196
- """
197
- # Unicode letters \p{L} không trực tiếp hỗ trợ trong re, dùng nhóm thay thế:
198
- # cho đơn giản: loại bỏ mọi thứ không phải chữ/number/space
199
- t = re.sub(r"[^\w\s\u00C0-\u024F\u1E00-\u1EFF]", " ", text, flags=re.UNICODE)
200
- # \w bao gồm underscore => loại underscore
201
- t = re.sub(r"_", " ", t)
202
- return t
203
-
204
-
205
- def normalize_whitespace(text: str) -> str:
206
- return _RE_MULTI_SPACE.sub(" ", text).strip()
207
-
208
-
209
- def clean_text_for_tts(text: str) -> str:
210
- """
211
- Luồng chính: input text (ai_reply từ Gemini) -> trả về cleaned_text
212
- Bước:
213
- 1) Loại URL, email, @mentions, #hashtags
214
- 2) Loại từ mô tả dấu (vd: 'dấu chấm', 'chấm', 'phẩy', 'sao', ...)
215
- 3) Loại các ký tự punctuation thực tế
216
- 4) Loại emoji và ký hiệu unicode pictographs
217
- 5) Giữ lại chữ và số (không có ký tự đặc biệt)
218
- 6) Chuẩn hóa khoảng trắng
219
- 7) (KHÔNG thay đổi case nếu PRESERVE_ORIGINAL_CASE True)
220
- """
221
- if not text:
222
- return ""
223
-
224
- t = str(text)
225
-
226
- # 1 remove urls, emails, mentions, hashtags
227
- t = remove_urls_emails_mentions_hashtags(t)
228
-
229
- # 2 remove words that describe punctuation
230
- t = remove_punctuation_words(t)
231
-
232
- # 3 remove punctuation chars
233
- t = remove_punct_chars(t)
234
-
235
- # 4 remove emojis and extra symbols
236
- t = remove_emoji_and_symbols(t)
237
-
238
- # 5 keep only letters/numbers/spaces
239
- t = keep_only_letters_numbers_and_spaces(t)
240
-
241
- # 6 normalize whitespace
242
- t = normalize_whitespace(t)
243
-
244
- # 7 If we want to preserve original case, we must be careful:
245
- # The cleaning steps removed many chars, so original casing may be broken.
246
- # We will not force titlecase or uppercase first letter because user requested "giữ nguyên".
247
- if not PRESERVE_ORIGINAL_CASE:
248
- # Optionally, make first letter uppercase for nicer TTS
249
- if t:
250
- t = t[0].upper() + t[1:]
251
-
252
- return t
253
-
254
-
255
- # ==========================================================
256
- # Text-to-Speech (gTTS) integration
257
- # ==========================================================
258
- def text_to_speech_base64(text: str) -> Optional[str]:
259
- """
260
- Chạy gTTS trên cleaned text, trả về base64 string của file mp3.
261
- Nếu tts fail hoặc cleaned text rỗng -> return None
262
- """
263
  try:
264
- # Clean text for TTS (core change)
265
- cleaned = clean_text_for_tts(text)
266
- if not cleaned:
267
- # Nothing to speak
268
- return None
269
-
270
- # detect language
271
- try:
272
- lang = detect(cleaned)
273
- if lang not in ("vi", "en"):
274
- # Default to Vietnamese if content has Vietnamese characters,
275
- # otherwise choose English as fallback.
276
- # Heuristic: check for vietnamese diacritics
277
- if re.search(r"[àáảãạâấầẩẫậăắằẳẵặđèéẻẽẹêềếểễệìíỉĩịòóỏõọôồốổỗộơờớởỡợùúủũụưừứửữựỳýỷỹỵ]", cleaned, flags=re.IGNORECASE):
278
- lang = "vi"
279
- else:
280
- lang = "en"
281
- except Exception:
282
  lang = "en"
283
-
284
- # Create TTS and save to temp file
285
- tts = gTTS(text=cleaned, lang=lang)
286
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
287
- tmp_name = tmp.name
288
- tmp.close()
289
- tts.save(tmp_name)
290
-
291
- # read bytes and base64 encode
292
- with open(tmp_name, "rb") as f:
293
- audio_bytes = f.read()
294
- audio_b64 = base64.b64encode(audio_bytes).decode("utf-8")
295
-
296
- # remove temp file
297
- try:
298
- os.unlink(tmp_name)
299
- except Exception:
300
- pass
301
-
302
  return audio_b64
303
  except Exception as e:
304
  print("TTS Error:", e)
305
  return None
306
 
307
-
308
  # ==========================================================
309
- # Web UI (simple) - giữ giống cấu trúc cũ, mở rộng comment
310
  # ==========================================================
311
  HTML_PAGE = """
312
  <!DOCTYPE html>
313
  <html>
314
  <head>
315
- <meta charset="utf-8" />
316
- <title>KC Robot AI v3.0 MAX PRO</title>
317
- <style>
318
- body { font-family: Arial, Helvetica, sans-serif; background:#0f0f0f; color:#fff; margin:0; padding:20px; }
319
- .container { max-width:900px; margin:20px auto; }
320
- .card { background:#161616; padding:18px; border-radius:10px; box-shadow: 0 4px 12px rgba(0,0,0,0.6); }
321
- h1 { margin:0 0 10px 0; font-size:22px; }
322
- input, textarea, button { font-size:16px; padding:10px; border-radius:6px; border:1px solid #333; background:#0f0f0f; color:#fff; }
323
- #chat { max-height: 420px; overflow:auto; margin-top:12px; padding:8px; background:#0b0b0b; border-radius:6px; }
324
- .msg-user { color:#6cf; margin:6px 0; }
325
- .msg-bot { color:#fc6; margin:6px 0 12px 10px; }
326
- audio { display:block; margin-top:8px; }
327
- .small { font-size:12px; color:#aaa; }
328
- </style>
329
  </head>
330
  <body>
331
- <div class="container">
332
- <div class="card">
333
- <h1>🤖 KC Robot AI v3.0 MAX PRO</h1>
334
- <p class="small">ESP32 Server Gemini ↔ gTTS | Robot will READ ONLY TEXT (no punctuation/emoji)</p>
335
- <div>
336
- <input id="user_input" placeholder="Nhập câu để thử..." style="width:70%" />
337
- <button onclick="sendMessage()">Gửi</button>
338
- </div>
339
- <div id="chat"></div>
340
- </div>
341
- </div>
342
-
343
- <script>
344
- async function sendMessage() {
345
- const inputEl = document.getElementById("user_input");
346
- const val = inputEl.value;
347
- if (!val) return;
348
- const chat = document.getElementById("chat");
349
- chat.innerHTML += `<div class='msg-user'><b>Bạn:</b> ${val}</div>`;
350
- inputEl.value = "";
351
- const res = await fetch("/api/chat", {
352
- method: "POST",
353
- headers: {"Content-Type": "application/json"},
354
- body: JSON.stringify({message: val})
355
- });
356
- const data = await res.json();
357
- chat.innerHTML += `<div class='msg-bot'><b>Robot:</b> ${data.reply}</div>`;
358
- if (data.audio) {
359
- const audio = document.createElement("audio");
360
- audio.controls = true;
361
- audio.src = "data:audio/mp3;base64," + data.audio;
362
- chat.appendChild(audio);
363
- }
364
- chat.scrollTop = chat.scrollHeight;
365
- }
366
- </script>
367
  </body>
368
  </html>
369
  """
370
 
371
- # ==========================================================
372
- # API endpoints
373
- # ==========================================================
374
-
375
- @app.route("/", methods=["GET"])
376
  def home():
377
  return render_template_string(HTML_PAGE)
378
 
 
 
 
379
 
380
  @app.route("/api/chat", methods=["POST"])
381
  def api_chat():
382
- """
383
- Endpoint chính:
384
- - nhận JSON {"message": "..."}
385
- - forward tới Gemini
386
- - nhận reply
387
- - làm sạch reply (loại bỏ dấu/emoji/ký tự)
388
- - tạo audio base64 bằng gTTS
389
- - trả về {"reply": ai_reply, "audio": base64|null}
390
- """
391
- try:
392
- data = request.get_json(force=True, silent=True)
393
- except Exception:
394
- data = None
395
-
396
  if not data or "message" not in data:
397
- return jsonify({"error": "Missing 'message' in request"}), 400
398
 
399
  user_message = data["message"]
400
- try:
401
- print(f"🧠 User: {user_message}")
402
- except Exception:
403
- pass
404
-
405
- # send to telegram for logging if configured
406
- try:
407
- send_telegram_message(f"User: {user_message}")
408
- except Exception:
409
- pass
410
 
411
- # ask Gemini (AI)
412
  ai_reply = ask_gemini(user_message)
 
413
 
414
- # send reply to telegram
415
- try:
416
- send_telegram_message(f"Robot: {ai_reply}")
417
- except Exception:
418
- pass
419
-
420
- # generate TTS (cleaning is applied in text_to_speech_base64)
421
- audio_b64 = None
422
- try:
423
- audio_b64 = text_to_speech_base64(ai_reply)
424
- except Exception as e:
425
- print("Error generating TTS:", e)
426
- audio_b64 = None
427
-
428
- # return reply (original ai_reply) and audio (cleaned voice)
429
  return jsonify({"reply": ai_reply, "audio": audio_b64})
430
 
431
-
432
- @app.route("/api/clean", methods=["POST"])
433
- def api_clean():
434
- """
435
- Optional: endpoint để client (ESP32) gọi chỉ để lấy clean text trước khi tự TTS.
436
- Request: {"text":"..."}
437
- Response: {"clean_text":"..."}
438
- """
439
- try:
440
- data = request.get_json(force=True, silent=True) or {}
441
- except Exception:
442
- data = {}
443
-
444
- text = data.get("text", "")
445
- if not isinstance(text, str):
446
- return jsonify({"error": "text must be string"}), 400
447
-
448
- cleaned = clean_text_for_tts(text)
449
- return jsonify({"clean_text": cleaned})
450
-
451
-
452
  @app.route("/api/sensor", methods=["POST"])
453
- def api_sensor():
454
- """
455
- ESP32 thể gửi sensor update tới đây.
456
- Chỉ log + forward tới Telegram.
457
- """
458
- try:
459
- payload = request.get_json(force=True, silent=True)
460
- except Exception:
461
- payload = None
462
-
463
- if not payload:
464
- return jsonify({"error": "No JSON payload"}), 400
465
-
466
- try:
467
- send_telegram_message(f"ESP32 Sensor: {payload}")
468
- except Exception:
469
- pass
470
-
471
  return jsonify({"status": "received"})
472
 
473
-
474
- @app.route("/ping", methods=["GET"])
475
  def ping():
476
  return jsonify({"status": "ok", "model": GEMINI_MODEL})
477
 
478
-
479
  # ==========================================================
480
- # Utilities - small helpers kept for compatibility & debugging
481
- # ==========================================================
482
- def health_info() -> dict:
483
- return {
484
- "status": "ok",
485
- "gemini_configured": gemini_client is not None,
486
- "telegram_configured": bool(TELEGRAM_TOKEN and TELEGRAM_CHAT_ID)
487
- }
488
-
489
-
490
- @app.route("/health", methods=["GET"])
491
- def health():
492
- return jsonify(health_info())
493
-
494
-
495
- # ==========================================================
496
- # Main entry
497
  # ==========================================================
498
  if __name__ == "__main__":
499
- # Print startup info
500
- print("===================================================")
501
- print("KC ROBOT AI v3.0 MAX PRO")
502
- print(" - Port:", PORT)
503
- print(" - Gemini configured:", gemini_client is not None)
504
- print(" - Telegram configured:", bool(TELEGRAM_TOKEN and TELEGRAM_CHAT_ID))
505
- print(" - PRESERVE_ORIGINAL_CASE:", PRESERVE_ORIGINAL_CASE)
506
- print("===================================================")
507
- # Run Flask app (dev mode). For production use gunicorn/uvicorn as desired.
508
- app.run(host="0.0.0.0", port=PORT, debug=DEBUG)
 
1
+
2
+
3
  # ==========================================================
4
+ # KC ROBOT AI - APP.PY (V2.0 MAX FINAL)
5
  # Cloud AI Robot with Gemini 2.5 Flash + ESP32 + Telegram
 
 
 
6
  # ==========================================================
7
 
8
+ from flask import Flask, request, jsonify, render_template_string
 
 
 
 
 
 
 
 
 
9
  from google import genai
10
  import requests
11
+ import os
12
+ import time
13
  from gtts import gTTS
14
  from langdetect import detect
15
+ import tempfile
16
+ import base64
17
 
18
  # ==========================================================
19
+ # CONFIGURATION
20
  # ==========================================================
21
+
22
+ # Load environment variables from secrets (Cloud Run or Hugging Face)
23
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
24
  GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
25
  TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
26
  TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
 
 
 
 
27
 
28
+ # Create Flask app
 
 
29
  app = Flask(__name__)
30
 
31
  # ==========================================================
32
+ # SETUP GEMINI CLIENT
33
  # ==========================================================
34
  if not GEMINI_API_KEY:
35
+ print("❌ ERROR: No Gemini API Key found. Please add GEMINI_API_KEY in Secrets.")
36
+ client = None
37
  else:
38
+ client = genai.Client(api_key=GEMINI_API_KEY)
 
 
 
 
39
 
40
  # ==========================================================
41
+ # TELEGRAM UTILITIES
42
  # ==========================================================
43
+ def send_telegram_message(text):
 
 
 
 
44
  if not TELEGRAM_TOKEN or not TELEGRAM_CHAT_ID:
45
+ print("⚠️ Telegram not configured.")
 
46
  return
47
+ url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage"
48
+ payload = {"chat_id": TELEGRAM_CHAT_ID, "text": text}
49
  try:
 
 
50
  requests.post(url, json=payload, timeout=5)
51
  except Exception as e:
52
  print("Telegram Error:", e)
53
 
 
54
  # ==========================================================
55
+ # GEMINI AI RESPONSE
56
  # ==========================================================
57
+ def ask_gemini(prompt: str):
58
+ if not client:
59
+ return "⚠️ Gemini API key missing. Please configure in Secrets."
60
+
 
 
 
 
61
  try:
62
+ response = client.models.generate_content(
63
+ model=GEMINI_MODEL,
 
 
64
  contents=prompt
65
  )
 
66
  if hasattr(response, "text"):
67
  return response.text.strip()
68
+ elif "text" in response:
69
  return response["text"].strip()
70
+ else:
71
+ return "⚠️ No response text from Gemini."
72
  except Exception as e:
73
  print("Gemini Error:", e)
74
  return f"⚠️ Gemini Error: {e}"
75
 
 
76
  # ==========================================================
77
+ # LANGUAGE DETECTION & TTS
78
  # ==========================================================
79
+ def text_to_speech(text):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  try:
81
+ lang = detect(text)
82
+ if lang not in ["vi", "en"]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  lang = "en"
84
+ tts = gTTS(text=text, lang=lang)
 
 
85
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
86
+ tts.save(tmp.name)
87
+ with open(tmp.name, "rb") as f:
88
+ audio_b64 = base64.b64encode(f.read()).decode("utf-8")
89
+ os.unlink(tmp.name)
 
 
 
 
 
 
 
 
 
 
 
90
  return audio_b64
91
  except Exception as e:
92
  print("TTS Error:", e)
93
  return None
94
 
 
95
  # ==========================================================
96
+ # SIMPLE HTML INTERFACE (for testing)
97
  # ==========================================================
98
  HTML_PAGE = """
99
  <!DOCTYPE html>
100
  <html>
101
  <head>
102
+ <title>KC Robot AI v2.0</title>
103
+ <style>
104
+ body { font-family: Arial; text-align: center; background-color: #101010; color: white; }
105
+ input, button { padding: 10px; font-size: 16px; margin: 5px; }
106
+ #chat { max-width: 700px; margin: auto; text-align: left; background: #202020; padding: 20px; border-radius: 10px; }
107
+ .msg-user { color: #4af; }
108
+ .msg-bot { color: #fa4; margin-left: 20px; }
109
+ audio { margin-top: 10px; }
110
+ </style>
 
 
 
 
 
111
  </head>
112
  <body>
113
+ <h1>🤖 KC Robot AI v2.0 MAX FINAL</h1>
114
+ <div id="chat"></div>
115
+ <br>
116
+ <input id="user_input" placeholder="Nói đó..." style="width:60%">
117
+ <button onclick="sendMessage()">Gửi</button>
118
+
119
+ <script>
120
+ async function sendMessage() {
121
+ const input = document.getElementById("user_input").value;
122
+ if (!input) return;
123
+ const chat = document.getElementById("chat");
124
+ chat.innerHTML += `<div class='msg-user'><b>Bạn:</b> ${input}</div>`;
125
+ document.getElementById("user_input").value = "";
126
+ const res = await fetch("/api/chat", {
127
+ method: "POST",
128
+ headers: {"Content-Type": "application/json"},
129
+ body: JSON.stringify({message: input})
130
+ });
131
+ const data = await res.json();
132
+ chat.innerHTML += `<div class='msg-bot'><b>Robot:</b> ${data.reply}</div>`;
133
+ if (data.audio) {
134
+ const audio = document.createElement("audio");
135
+ audio.src = "data:audio/mp3;base64," + data.audio;
136
+ audio.controls = true;
137
+ chat.appendChild(audio);
138
+ }
139
+ chat.scrollTop = chat.scrollHeight;
140
+ }
141
+ </script>
 
 
 
 
 
 
 
142
  </body>
143
  </html>
144
  """
145
 
146
+ @app.route("/")
 
 
 
 
147
  def home():
148
  return render_template_string(HTML_PAGE)
149
 
150
+ # ==========================================================
151
+ # API ENDPOINTS
152
+ # ==========================================================
153
 
154
  @app.route("/api/chat", methods=["POST"])
155
  def api_chat():
156
+ data = request.get_json()
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  if not data or "message" not in data:
158
+ return jsonify({"error": "Missing 'message'"}), 400
159
 
160
  user_message = data["message"]
161
+ print(f"🧠 User said: {user_message}")
162
+ send_telegram_message(f"User: {user_message}")
 
 
 
 
 
 
 
 
163
 
 
164
  ai_reply = ask_gemini(user_message)
165
+ send_telegram_message(f"Robot: {ai_reply}")
166
 
167
+ audio_b64 = text_to_speech(ai_reply)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  return jsonify({"reply": ai_reply, "audio": audio_b64})
169
 
170
+ # ESP32 sensor endpoint
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  @app.route("/api/sensor", methods=["POST"])
172
+ def sensor_data():
173
+ data = request.get_json()
174
+ if not data:
175
+ return jsonify({"error": "No data"}), 400
176
+ msg = f"👁️ ESP32 Sensor update: {data}"
177
+ send_telegram_message(msg)
 
 
 
 
 
 
 
 
 
 
 
 
178
  return jsonify({"status": "received"})
179
 
180
+ # Health check
181
+ @app.route("/ping")
182
  def ping():
183
  return jsonify({"status": "ok", "model": GEMINI_MODEL})
184
 
 
185
  # ==========================================================
186
+ # MAIN ENTRY POINT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  # ==========================================================
188
  if __name__ == "__main__":
189
+ port = int(os.getenv("PORT", 8080))
190
+ print(f"🚀 KC Robot AI v2.0 running on port {port}")
191
+ app.run(host="0.0.0.0", port=port)
192
+