kcrobot40 commited on
Commit
6521a14
·
verified ·
1 Parent(s): 338d465

initial commic

Browse files
Files changed (1) hide show
  1. app.py +150 -444
app.py CHANGED
@@ -1,486 +1,192 @@
1
 
2
- #!/usr/bin/env python3
3
- # -*- coding: utf-8 -*-
4
- """
5
- KC ROBOT AI — v3.0 MAX PRO+
6
- - Flask server (suitable for HuggingFace Spaces / Cloud Run)
7
- - Backend: Google Gemini (via google.generativeai if available) OR REST fallback (not implemented)
8
- - TTS: ElevenLabs (preferred, set ELEVEN_API_KEY + voice ids) OR gTTS fallback
9
- - Endpoints:
10
- GET / -> simple terminal-style UI
11
- POST /api/chat -> {"message": "..."} -> returns {"reply": "...", "audio": base64_mp3}
12
- POST /notify -> {"event":"", "msg":""} -> forwarded to telegram
13
- GET /play_latest -> returns latest saved mp3
14
- GET /poll -> device poll for queued command (simple)
15
- POST /api/sensor -> sensor updates from ESP32
16
- - Secrets (env / HF Secrets):
17
- GEMINI_API_KEY, GEMINI_MODEL (default gemini-2.5-flash)
18
- TELEGRAM_TOKEN, TELEGRAM_CHAT_ID
19
- ELEVEN_API_KEY (optional)
20
- ELEVEN_VOICE_ID_VN (optional - female Vietnamese)
21
- ELEVEN_VOICE_ID_EN (optional - female English)
22
- """
23
- import os
24
- import io
25
- import re
26
- import time
27
- import json
28
- import base64
29
- import logging
30
- import tempfile
31
- import threading
32
- from datetime import datetime
33
- from functools import wraps
34
 
35
- from flask import Flask, request, jsonify, render_template_string, send_file
 
 
 
36
 
37
- # Optional SDK for Gemini
38
- GENAI_IMPORTED = False
39
- try:
40
- # google.generativeai available in requirements
41
- import google.generativeai as genai
42
- GENAI_IMPORTED = True
43
- except Exception:
44
- GENAI_IMPORTED = False
45
-
46
- # TTS libraries
47
- from gtts import gTTS
48
  import requests
 
 
 
 
 
 
49
 
50
- # language detect
51
- from langdetect import detect as langdetect_detect
52
-
53
- # -------------------------
54
- # Basic logging
55
- # -------------------------
56
- logging.basicConfig(level=logging.INFO)
57
- logger = logging.getLogger("kcrobot_v3_pro")
58
-
59
- # -------------------------
60
- # CONFIG (ENV / Secrets)
61
- # -------------------------
62
- GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "").strip()
63
- GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.5-flash").strip()
64
- TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "").strip()
65
- TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "").strip()
66
-
67
- # ElevenLabs (optional) - if provided we will use it for nicer voice
68
- ELEVEN_API_KEY = os.getenv("ELEVEN_API_KEY", "").strip()
69
- ELEVEN_VOICE_ID_VN = os.getenv("ELEVEN_VOICE_ID_VN", "").strip() # e.g. 'voice-id-vn-female'
70
- ELEVEN_VOICE_ID_EN = os.getenv("ELEVEN_VOICE_ID_EN", "").strip() # e.g. 'voice-id-en-female'
71
-
72
- # Data & storage
73
- BASE = os.getcwd()
74
- DATA_DIR = os.path.join(BASE, "data")
75
- os.makedirs(DATA_DIR, exist_ok=True)
76
- LATEST_MP3_PATH = os.path.join(DATA_DIR, "latest_reply.mp3")
77
- PENDING_CMD_PATH = os.path.join(DATA_DIR, "pending.json")
78
- HISTORY_PATH = os.path.join(DATA_DIR, "history.json")
79
 
80
- # Configure genai if available
81
- if GENAI_IMPORTED and GEMINI_API_KEY:
82
- try:
83
- genai.configure(api_key=GEMINI_API_KEY)
84
- logger.info("Gemini SDK configured")
85
- except Exception:
86
- logger.exception("Failed to configure Gemini SDK")
87
 
88
- # -------------------------
89
- # Flask app
90
- # -------------------------
91
  app = Flask(__name__)
92
 
93
- # -------------------------
94
- # Helpers: JSON safe
95
- # -------------------------
96
- def load_json_safe(path, default):
97
- try:
98
- if os.path.exists(path):
99
- with open(path, "r", encoding="utf-8") as f:
100
- return json.load(f)
101
- except Exception:
102
- logger.exception("load_json_safe failed for %s", path)
103
- return default
104
-
105
- def save_json_safe(path, data):
106
- try:
107
- with open(path, "w", encoding="utf-8") as f:
108
- json.dump(data, f, ensure_ascii=False, indent=2)
109
- return True
110
- except Exception:
111
- logger.exception("save_json_safe failed for %s", path)
112
- return False
113
-
114
- # -------------------------
115
- # Language detection (better handling)
116
- # -------------------------
117
- VIET_CHAR_RE = re.compile(r"[àáạảãâầấậẩẫăằắặẳẵđèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹ]", re.I)
118
- def detect_lang(text: str) -> str:
119
- if not text or not isinstance(text, str):
120
- return "en"
121
- if VIET_CHAR_RE.search(text):
122
- return "vi"
123
  try:
124
- ld = langdetect_detect(text)
125
- if ld and ld.startswith("vi"):
126
- return "vi"
127
- if ld and ld.startswith("en"):
128
- return "en"
129
- # fallback
130
- return "vi" if VIET_CHAR_RE.search(text) else "en"
131
- except Exception:
132
- return "vi" if VIET_CHAR_RE.search(text) else "en"
133
-
134
- # -------------------------
135
- # Gemini wrapper
136
- # -------------------------
137
- def call_gemini(prompt: str, model: str = None, max_output_tokens: int=512, temperature: float=0.2) -> dict:
138
- model = model or GEMINI_MODEL
139
- if not GEMINI_API_KEY:
140
- return {"ok": False, "error": "Gemini API key not configured"}
141
- # Try SDK
142
- if GENAI_IMPORTED:
143
- try:
144
- m = genai.GenerativeModel(model)
145
- # use generate_content API
146
- resp = m.generate_content(prompt)
147
- # resp may have text
148
- if hasattr(resp, "text") and resp.text:
149
- return {"ok": True, "text": resp.text}
150
- # try dict-like
151
- try:
152
- j = resp
153
- if isinstance(j, dict) and "candidates" in j:
154
- cand = j.get("candidates", [])
155
- if len(cand):
156
- # try to extract text parts
157
- c0 = cand[0]
158
- content = c0.get("content")
159
- if isinstance(content, list):
160
- parts = []
161
- for p in content:
162
- if isinstance(p, dict) and "text" in p:
163
- parts.append(p["text"])
164
- if parts:
165
- return {"ok": True, "text": "".join(parts)}
166
- return {"ok": True, "text": str(resp)}
167
- except Exception:
168
- return {"ok": True, "text": str(resp)}
169
- except Exception:
170
- logger.exception("Gemini SDK call failed")
171
- # Fallback: not implementing REST here to avoid accidental misuse (user should set up SDK)
172
- return {"ok": False, "error": "Gemini SDK not available or call failed. Set GEMINI_API_KEY and ensure google-generativeai installed."}
173
-
174
- # -------------------------
175
- # Text cleaning for TTS (remove punctuation / emojis)
176
- # -------------------------
177
- # We remove characters that TTS often reads aloud (stars, markdown, urls, emoji)
178
- RE_EMOJI = re.compile(
179
- "["
180
- "\U0001F600-\U0001F64F" # emoticons
181
- "\U0001F300-\U0001F5FF" # symbols & pictographs
182
- "\U0001F680-\U0001F6FF" # transport & map symbols
183
- "\U0001F1E0-\U0001F1FF" # flags (iOS)
184
- "]+", flags=re.UNICODE)
185
-
186
- def clean_for_tts(text: str) -> str:
187
- if not text:
188
- return ""
189
- # convert to str
190
- txt = str(text)
191
- # remove URLs
192
- txt = re.sub(r"http\S+|www\.\S+", " ", txt)
193
- # remove code blocks/backticks/markdown-ish characters
194
- txt = re.sub(r"[`*_~>#\[\]\{\}\(\)=+\|\\\/\^@%$&]", " ", txt)
195
- # remove emojis
196
- txt = RE_EMOJI.sub(" ", txt)
197
- # collapse multiple spaces
198
- txt = re.sub(r"\s+", " ", txt).strip()
199
- return txt
200
-
201
- # -------------------------
202
- # TTS backends
203
- # - prefer ElevenLabs when ELEVEN_API_KEY and voice id provided
204
- # - fallback to gTTS
205
- # -------------------------
206
- def tts_elevenlabs_bytes(text: str, voice_id: str, api_key: str) -> bytes:
207
- """
208
- ElevenLabs simple REST call to get MP3 bytes.
209
- Requires XI-API-Key in header and returns audio/mpeg.
210
- """
211
- url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
212
- headers = {"xi-api-key": api_key, "Content-Type": "application/json"}
213
- payload = {"text": text, "voice_settings": {"stability": 0.6, "similarity_boost": 0.75}}
214
- r = requests.post(url, json=payload, headers=headers, timeout=30)
215
- r.raise_for_status()
216
- return r.content
217
 
218
- def tts_gtts_bytes(text: str, lang_code: str = "vi") -> bytes:
219
- t = gTTS(text=text, lang=lang_code)
220
- bio = io.BytesIO()
221
- t.write_to_fp(bio)
222
- bio.seek(0)
223
- return bio.read()
224
 
225
- def synthesize_save_mp3(answer: str, lang_hint: str="vi") -> (bool, str):
226
- """
227
- Synthesize answer to MP3, save to LATEST_MP3_PATH, return (ok, path_or_error)
228
- Use ElevenLabs if available, else gTTS.
229
- """
230
  try:
231
- clean = clean_for_tts(answer)
232
- if not clean:
233
- clean = "..."
234
- mp3_bytes = None
235
- # Try ElevenLabs VN/EN if configured
236
- if ELEVEN_API_KEY:
237
- try:
238
- # choose voice id based on lang_hint
239
- vid = ELEVEN_VOICE_ID_VN if lang_hint.startswith("vi") and ELEVEN_VOICE_ID_VN else ELEVEN_VOICE_ID_EN if ELEVEN_VOICE_ID_EN else None
240
- if vid:
241
- mp3_bytes = tts_elevenlabs_bytes(clean, vid, ELEVEN_API_KEY)
242
- logger.info("TTS: used ElevenLabs voice %s", vid)
243
- except Exception:
244
- logger.exception("ElevenLabs TTS failed -> fallback to gTTS")
245
- mp3_bytes = None
246
- if mp3_bytes is None:
247
- lang_code = "vi" if lang_hint.startswith("vi") else "en"
248
- mp3_bytes = tts_gtts_bytes(clean, lang_code)
249
- logger.info("TTS: used gTTS lang=%s", lang_code)
250
- with open(LATEST_MP3_PATH, "wb") as f:
251
- f.write(mp3_bytes)
252
- return True, LATEST_MP3_PATH
253
  except Exception as e:
254
- logger.exception("synthesize_save_mp3 failed")
255
- return False, str(e)
256
 
257
- # -------------------------
258
- # Telegram notify helper
259
- # -------------------------
260
- def send_telegram(text: str):
261
- if not TELEGRAM_TOKEN or not TELEGRAM_CHAT_ID:
262
- logger.debug("Telegram not configured")
263
- return False
264
  try:
265
- url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage"
266
- r = requests.post(url, json={"chat_id": TELEGRAM_CHAT_ID, "text": text}, timeout=8)
267
- return r.status_code == 200
268
- except Exception:
269
- logger.exception("send_telegram failed")
270
- return False
271
-
272
- # -------------------------
273
- # Pending command helpers (simple queue for ESP32 poll)
274
- # -------------------------
275
- def queue_command(cmd: dict):
276
- pending = load_json_safe(PENDING_CMD_PATH, [])
277
- pending.append({"ts": time.time(), "cmd": cmd})
278
- # keep only last 10
279
- pending = pending[-10:]
280
- save_json_safe(PENDING_CMD_PATH, pending)
281
- logger.info("Queued command: %s", cmd)
282
-
283
- def pop_pending():
284
- pending = load_json_safe(PENDING_CMD_PATH, [])
285
- if not pending:
286
  return None
287
- item = pending.pop(0)
288
- save_json_safe(PENDING_CMD_PATH, pending)
289
- return item
290
 
291
- # -------------------------
292
- # History save
293
- # -------------------------
294
- def append_history(entry: dict):
295
- h = load_json_safe(HISTORY_PATH, [])
296
- h.append(entry)
297
- if len(h) > 1000:
298
- h = h[-1000:]
299
- save_json_safe(HISTORY_PATH, h)
300
-
301
- # -------------------------
302
- # Simple terminal-style UI (developer mode)
303
- # -------------------------
304
- TERMINAL_HTML = """
305
- <!doctype html>
306
  <html>
307
  <head>
308
- <meta charset="utf-8">
309
- <title>KC ROBOT AI — v3.0 MAX PRO+</title>
310
- <style>
311
- body{background:#0b1221;color:#cfe7ff;font-family:monospace;padding:14px}
312
- #console{background:#061020;padding:12px;border-radius:6px;height:60vh;overflow:auto}
313
- .ts{color:#7fbfff}
314
- .user{color:#7fff7f}
315
- .bot{color:#ffdf7f}
316
- .sys{color:#ff8a8a}
317
- input{width:60%;padding:8px;margin:6px}
318
- button{padding:8px;border-radius:6px}
319
- </style>
320
  </head>
321
  <body>
322
- <h2>KC ROBOT AI — v3.0 MAX PRO+</h2>
323
- <div id="console"></div>
324
- <p>
325
- <input id="txt" placeholder="Nhập / Ask..." />
326
- <button onclick="send()">Send</button>
327
- <button onclick="poll()">Poll pending</button>
328
- </p>
329
  <script>
330
- async function log(kind, text){
331
- const c = document.getElementById("console");
332
- const ts = new Date().toLocaleTimeString();
333
- const cls = kind === 'user' ? 'user' : (kind === 'bot' ? 'bot' : 'sys');
334
- c.innerHTML += `<div><span class="ts">[${ts}]</span> <span class="${cls}">${text}</span></div>`;
335
- c.scrollTop = c.scrollHeight;
336
- }
337
- async function send(){
338
- const v = document.getElementById("txt").value;
339
- if(!v) return;
340
- await log('user', 'You: ' + v);
341
- document.getElementById("txt").value = '';
342
- const res = await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:v})});
343
- const j = await res.json();
344
- await log('bot', 'Robot: ' + j.reply);
345
- }
346
- async function poll(){
347
- const r = await fetch('/poll');
348
- const j = await r.json();
349
- await log('sys', 'POLLED: ' + JSON.stringify(j));
350
  }
351
  </script>
352
  </body>
353
  </html>
354
  """
355
 
356
- @app.route("/", methods=["GET"])
357
- def index_ui():
358
- return render_template_string(TERMINAL_HTML)
 
 
 
 
359
 
360
- # -------------------------
361
- # API: /api/chat
362
- # -------------------------
363
  @app.route("/api/chat", methods=["POST"])
364
  def api_chat():
365
- data = request.get_json(silent=True) or {}
366
- msg = data.get("message") or data.get("q") or ""
367
- if not msg:
368
- return jsonify({"error": "missing message"}), 400
369
- logger.info("User -> %s", msg[:200])
370
- # notify telegram user query (non-blocking)
371
- try:
372
- threading.Thread(target=send_telegram, args=(f"👤 User: {msg}",)).start()
373
- except Exception:
374
- pass
375
-
376
- # send prompt to Gemini
377
- res = call_gemini(msg)
378
- if not res.get("ok"):
379
- text = f"[Gemini error] {res.get('error')}"
380
- else:
381
- text = res.get("text", "")
382
- lang = detect_lang(text)
383
- clean_text = clean_for_tts(text)
384
- # save history
385
- append_history({"ts": time.time(), "user": msg, "reply": text, "lang": lang})
386
- # synthesize
387
- ok, path_or_err = synthesize_save_mp3(clean_text, lang_hint=lang)
388
- if ok:
389
- # send telegram with reply and notify
390
- try:
391
- threading.Thread(target=send_telegram, args=(f"🤖 Robot: {clean_text}",)).start()
392
- except Exception:
393
- pass
394
- # read mp3 bytes and return base64 to client
395
- try:
396
- with open(path_or_err, "rb") as f:
397
- audio_b64 = base64.b64encode(f.read()).decode("utf-8")
398
- except Exception:
399
- logger.exception("reading mp3 failed")
400
- audio_b64 = None
401
- return jsonify({"reply": clean_text, "audio": audio_b64})
402
- else:
403
- logger.warning("TTS failed: %s", path_or_err)
404
- return jsonify({"reply": clean_text, "audio": None, "error": path_or_err})
405
 
406
- # -------------------------
407
- # /play_latest -> stream saved mp3
408
- # -------------------------
409
- @app.route("/play_latest", methods=["GET"])
410
- def play_latest():
411
- if not os.path.exists(LATEST_MP3_PATH):
412
- return jsonify({"error": "no audio"}), 404
413
- return send_file(LATEST_MP3_PATH, mimetype="audio/mpeg")
414
 
415
- # -------------------------
416
- # /notify -> from esp32 sensors or other services
417
- # -------------------------
418
- @app.route("/notify", methods=["POST"])
419
- def notify():
420
- data = request.get_json(silent=True) or {}
421
- event = data.get("event", "event")
422
- msg = data.get("msg", "")
423
- logger.info("NOTIFY %s: %s", event, msg)
424
- # Push into telegram
425
- try:
426
- threading.Thread(target=send_telegram, args=(f"[Robot Notify] {event}: {msg}",)).start()
427
- except Exception:
428
- pass
429
- # optionally queue commands (if provided)
430
- cmd = data.get("cmd")
431
- if cmd:
432
- queue_command(cmd)
433
- return jsonify({"status": "ok"})
434
 
435
- # -------------------------
436
- # /poll -> device polling for commands
437
- # -------------------------
438
- @app.route("/poll", methods=["GET"])
439
- def poll():
440
- # return one pending command if any
441
- item = pop_pending()
442
- if item:
443
- return jsonify(item)
444
- return jsonify({})
445
 
446
- # -------------------------
447
- # /api/sensor -> sensor updates from ESP32
448
- # -------------------------
449
  @app.route("/api/sensor", methods=["POST"])
450
- def api_sensor():
451
- data = request.get_json(silent=True) or {}
452
  if not data:
453
- return jsonify({"error": "no data"}), 400
454
- logger.info("Sensor update: %s", data)
455
- # notify telegram
456
- try:
457
- threading.Thread(target=send_telegram, args=(f"[Sensor] {json.dumps(data, ensure_ascii=False)}",)).start()
458
- except Exception:
459
- pass
460
- # If sensor wants the robot to speak something, enqueue/synthesize
461
- if data.get("speak"):
462
- text = str(data.get("speak"))
463
- # synthesize and keep latest
464
- synthesize_save_mp3(text, lang_hint=detect_lang(text))
465
  return jsonify({"status": "received"})
466
 
467
- # -------------------------
468
- # Admin/helper endpoints (optional)
469
- # -------------------------
470
- @app.route("/history", methods=["GET"])
471
- def get_history():
472
- h = load_json_safe(HISTORY_PATH, [])
473
- return jsonify(h[-200:])
474
 
475
- # -------------------------
476
- # Start server
477
- # -------------------------
478
  if __name__ == "__main__":
479
- logger.info("Starting KC ROBOT AI v3.0 MAX PRO+ (port %s)", os.getenv("PORT", "8080"))
480
- # ensure default files exist
481
- if not os.path.exists(PENDING_CMD_PATH):
482
- save_json_safe(PENDING_CMD_PATH, [])
483
- if not os.path.exists(HISTORY_PATH):
484
- save_json_safe(HISTORY_PATH, [])
485
- # run
486
- app.run(host="0.0.0.0", port=int(os.getenv("PORT", "8080")))
 
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
+