kcrobot20 commited on
Commit
63de067
·
verified ·
1 Parent(s): 1728f2c

initial commit

Browse files
Files changed (1) hide show
  1. app.py +169 -628
app.py CHANGED
@@ -1,661 +1,202 @@
1
-
2
- # app.py — KC Robot AI v7.5 FINAL (auto-model-select, bilingual, TTS fallback, Telegram, ESP32 endpoints)
3
- # Secrets expected (HF Space -> Settings -> Secrets):
4
- # HF_TOKEN (required)
5
- # HF_MODEL (optional preferred model id like "mistralai/Mistral-7B-Instruct-v0.3")
6
- # TELEGRAM_TOKEN (optional)
7
- # TELEGRAM_CHAT_ID (optional)
8
- # Optional:
9
- # HF_TTS_MODEL, HF_STT_MODEL
10
- #
11
- # Minimal deps: flask, requests, gTTS, python-multipart
12
- # Keep requirements.txt consistent with these packages.
13
 
14
  import os
15
- import io
16
- import sys
17
- import time
18
  import json
19
  import uuid
20
- import logging
21
- import threading
22
- from typing import Any, List, Tuple, Optional
23
- from pathlib import Path
24
-
25
  import requests
26
- from flask import Flask, request, jsonify, Response, render_template_string
27
-
28
- # gTTS fallback
29
- try:
30
- from gtts import gTTS
31
- _HAS_GTTS = True
32
- except Exception:
33
- _HAS_GTTS = False
34
-
35
- # ---------------- logging ----------------
36
- logging.basicConfig(stream=sys.stdout, level=logging.INFO,
37
- format="%(asctime)s %(levelname)s %(name)s: %(message)s")
38
- logger = logging.getLogger("kcrobot.v7.5")
39
-
40
- # ---------------- env / secrets ----------------
41
- HF_TOKEN = os.getenv("HF_TOKEN", "").strip()
42
- HF_MODEL = os.getenv("HF_MODEL", "").strip() # preferred model (may be empty)
43
- HF_TTS_MODEL = os.getenv("HF_TTS_MODEL", "").strip() # optional HF TTS model
44
- HF_STT_MODEL = os.getenv("HF_STT_MODEL", "openai/whisper-small").strip()
45
- TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "").strip()
46
- TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "").strip()
47
- PORT = int(os.getenv("PORT", 7860))
48
-
49
- HF_HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
50
-
51
- # ---------------- tmp dir ----------------
52
- TMPDIR = Path("/tmp/kcrobot") if os.name != "nt" else Path.cwd() / "tmp_kcrobot"
53
- TMPDIR.mkdir(parents=True, exist_ok=True)
54
- CONV_LOG = TMPDIR / "conversation_log.jsonl"
55
-
56
- # ---------------- in-memory ----------------
57
- CONVERSATION: List[Tuple[str, str]] = []
58
- DISPLAY_BUFFER: List[str] = []
59
- DISPLAY_LIMIT = 6
60
 
61
- def push_display(line: str):
62
- global DISPLAY_BUFFER
63
- DISPLAY_BUFFER.append(line)
64
- if len(DISPLAY_BUFFER) > DISPLAY_LIMIT:
65
- DISPLAY_BUFFER = DISPLAY_BUFFER[-DISPLAY_LIMIT:]
66
 
67
- def save_conv(user: str, bot: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  try:
69
- with open(CONV_LOG, "a", encoding="utf-8") as f:
70
- f.write(json.dumps({"time": time.time(), "user": user, "bot": bot}, ensure_ascii=False) + "\n")
71
- except Exception:
72
- logger.exception("save_conv failed")
73
-
74
- # ---------------- small helpers ----------------
75
- def clean_text(text: Any) -> str:
76
- if text is None:
77
- return ""
78
- s = str(text)
79
- import re
80
- s = re.sub(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]+', ' ', s)
81
- s = re.sub(r'\s+', ' ', s).strip()
82
- return s
83
-
84
- VI_CHARS = set("ăâđêôơưáàảãạắằẳẵặấầẩẫậéèẻẽẹíìỉĩịóòỏõọúùủũụứừửữựýỳỷỹỵ")
85
- def detect_language(text: str) -> str:
86
- t = (text or "").lower()
87
- for ch in t:
88
- if ch in VI_CHARS:
89
- return "vi"
90
- return "en"
91
-
92
- # ---------------- Hugging Face HTTP helpers ----------------
93
- def hf_post_json(model_id: str, payload: dict, timeout: int = 90) -> requests.Response:
94
- if not HF_TOKEN:
95
- raise RuntimeError("HF_TOKEN not configured in Secrets")
96
- url = f"https://api-inference.huggingface.co/models/{model_id}"
97
- headers = dict(HF_HEADERS)
98
- headers["Content-Type"] = "application/json"
99
- return requests.post(url, headers=headers, json=payload, timeout=timeout)
100
 
101
- def hf_post_bytes(model_id: str, data: bytes, content_type: str = "application/octet-stream", timeout: int = 180) -> requests.Response:
102
- if not HF_TOKEN:
103
- raise RuntimeError("HF_TOKEN not configured in Secrets")
104
  url = f"https://api-inference.huggingface.co/models/{model_id}"
105
- headers = dict(HF_HEADERS)
106
- headers["Content-Type"] = content_type
107
- return requests.post(url, headers=headers, data=data, timeout=timeout)
108
-
109
- def parse_hf_text_output(obj: Any) -> str:
110
- try:
111
- if isinstance(obj, dict):
112
- for k in ("generated_text","text","answer"):
113
- if k in obj:
114
- return obj.get(k,"")
115
- if "choices" in obj and isinstance(obj["choices"], list) and obj["choices"]:
116
- c0 = obj["choices"][0]
117
- return c0.get("text") or c0.get("message",{}).get("content","") or str(c0)
118
- return json.dumps(obj, ensure_ascii=False)
119
- if isinstance(obj, list) and obj:
120
- first = obj[0]
121
- if isinstance(first, dict):
122
- for k in ("generated_text","text"):
123
- if k in first:
124
- return first.get(k,"")
125
- return str(first)
126
- return str(obj)
127
- except Exception:
128
- logger.exception("parse_hf_text_output")
129
- return str(obj)
130
-
131
- # ---------------- Auto model finder ----------------
132
- # Candidate fallback list — you can extend
133
- DEFAULT_MODEL_CANDIDATES = [
134
- "mistralai/Mistral-7B-Instruct-v0.3",
135
- "google/gemma-2b-it",
136
- "databricks/dolly-v2-3b",
137
- "tiiuae/falcon-7b-instruct", # may be private at times
138
- "facebook/blenderbot-400M-distill",
139
- # Vietnamese candidates (if public)
140
- "vinai/PhoGPT-4B",
141
- ]
142
-
143
- def test_model_working(model_id: str, sample_prompt: str = "Xin chào, bạn khỏe không?") -> Tuple[bool, dict]:
144
- """
145
- Return (ok, response_short_info)
146
- ok True if got status 200 and some textual output parseable
147
- """
148
  try:
149
- payload = {"inputs": sample_prompt, "parameters": {"max_new_tokens": 20}, "options": {"wait_for_model": True}}
150
- r = hf_post_json(model_id, payload, timeout=30)
151
- info = {"status": r.status_code, "text": (r.text[:500] if r.text else "")}
152
- if r.status_code == 200:
153
- # try parse
154
- try:
155
- j = r.json()
156
- out = parse_hf_text_output(j)
157
- if out and len(out.strip())>0:
158
- info["result"] = out
159
- return True, info
160
- except Exception:
161
- # maybe non-json; if text length present, accept minimally
162
- if r.text and len(r.text.strip())>0:
163
- info["result"] = r.text
164
- return True, info
165
- return False, info
166
- except requests.exceptions.RequestException as e:
167
- logger.warning("test_model_working request exception for %s: %s", model_id, e)
168
- return False, {"error": str(e)}
169
- except Exception:
170
- logger.exception("test_model_working unexpected")
171
- return False, {"error": "unexpected"}
172
-
173
- def auto_select_model(preferred: Optional[str] = None) -> Optional[str]:
174
- """
175
- Try preferred model first. If fail, iterate DEFAULT_MODEL_CANDIDATES
176
- Returns selected model id or None.
177
- """
178
- tried = []
179
- if preferred:
180
- logger.info("Auto-check preferred model: %s", preferred)
181
- ok, info = test_model_working(preferred)
182
- tried.append((preferred, ok, info))
183
- if ok:
184
- logger.info("Preferred model OK: %s", preferred)
185
- return preferred
186
- logger.info("Preferred model not usable or not provided, scanning candidates...")
187
- for m in DEFAULT_MODEL_CANDIDATES:
188
- if m == preferred:
189
- continue
190
- logger.info("Testing candidate: %s", m)
191
- ok, info = test_model_working(m)
192
- tried.append((m, ok, info))
193
- if ok:
194
- logger.info("Selected fallback model: %s", m)
195
- return m
196
- # nothing found
197
- logger.warning("Auto-select model found none usable. Tried: %s", [(t[0], t[1]) for t in tried])
198
- return None
199
-
200
- # initial selected model (will be mutated at runtime)
201
- SELECTED_MODEL = HF_MODEL if HF_MODEL else None
202
-
203
- # ---------------- HF text / stt / tts wrappers using SELECTED_MODEL ----------------
204
- def hf_text_generate(prompt: str, model_override: Optional[str] = None, max_new_tokens: int = 256, temperature: float = 0.7) -> str:
205
- model = model_override or SELECTED_MODEL
206
- if not model:
207
- raise RuntimeError("No HF model selected")
208
- payload = {"inputs": prompt, "parameters": {"max_new_tokens": int(max_new_tokens), "temperature": float(temperature)}, "options": {"wait_for_model": True}}
209
- r = hf_post_json(model, payload, timeout=120)
210
- if r.status_code == 200:
211
- try:
212
- j = r.json()
213
- return parse_hf_text_output(j)
214
- except Exception:
215
- return r.text
216
- elif r.status_code == 403:
217
- raise RuntimeError("HF returned 403 (forbidden) — token or access rights issue")
218
- elif r.status_code == 404:
219
- raise RuntimeError("HF returned 404 (model not found) — check HF_MODEL or model access")
220
- else:
221
- raise RuntimeError(f"HF text gen returned {r.status_code}: {r.text[:300]}")
222
 
223
- def hf_stt_from_bytes(audio_bytes: bytes, model_override: Optional[str] = None) -> str:
224
- model = model_override or HF_STT_MODEL
225
- if not model:
226
- raise RuntimeError("HF_STT_MODEL not configured")
227
- r = hf_post_bytes(model, audio_bytes, content_type="application/octet-stream", timeout=180)
228
  if r.status_code == 200:
229
  try:
230
- j = r.json()
231
- if isinstance(j, dict) and "text" in j:
232
- return j["text"]
233
- return parse_hf_text_output(j)
234
  except Exception:
235
- return r.text
236
  else:
237
- raise RuntimeError(f"HF STT returned {r.status_code}: {r.text[:300]}")
 
238
 
239
- def hf_tts_get_bytes(text: str, model_override: Optional[str] = None) -> bytes:
240
- text = text.strip()
241
  if not text:
242
- raise RuntimeError("TTS text empty")
243
- model = model_override or HF_TTS_MODEL
244
- if model:
245
- # Try HF TTS model first
246
- try:
247
- payload = {"inputs": text}
248
- r = hf_post_json(model, payload, timeout=120)
249
- if r.status_code == 200 and r.content:
250
- return r.content
251
- # fallback to content or parse
252
- if r.status_code == 200:
253
- try:
254
- j = r.json()
255
- return parse_hf_text_output(j).encode("utf-8")
256
- except Exception:
257
- return r.content
258
- logger.warning("HF TTS returned %s: %s", r.status_code, r.text[:200])
259
- except Exception:
260
- logger.exception("HF TTS call failed")
261
- # fallback to gTTS if present
262
- if _HAS_GTTS:
263
- try:
264
- lang = "vi" if detect_language(text) == "vi" else "en"
265
- tts = gTTS(text=text, lang=lang)
266
- bio = io.BytesIO()
267
- tts.write_to_fp(bio)
268
- bio.seek(0)
269
- return bio.read()
270
- except Exception:
271
- logger.exception("gTTS fallback failed")
272
- raise RuntimeError("gTTS fallback failed")
273
- raise RuntimeError("No TTS available (no HF_TTS_MODEL and gTTS not installed)")
274
-
275
- # ---------------- Telegram helpers ----------------
276
- def telegram_send_message(chat_id: str, text: str) -> bool:
277
- if not TELEGRAM_TOKEN or not chat_id:
278
- return False
279
- try:
280
- url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage"
281
- r = requests.post(url, json={"chat_id": chat_id, "text": text}, timeout=8)
282
- if r.status_code != 200:
283
- logger.warning("Telegram sendMessage failed %s: %s", r.status_code, r.text[:300])
284
- return False
285
- return True
286
- except Exception:
287
- logger.exception("telegram_send_message")
288
- return False
289
-
290
- def telegram_send_audio(chat_id: str, audio_bytes: bytes, filename: str = "reply.mp3") -> bool:
291
- if not TELEGRAM_TOKEN or not chat_id:
292
- return False
293
- try:
294
- url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendAudio"
295
- files = {"audio": (filename, io.BytesIO(audio_bytes), "audio/mpeg")}
296
- data = {"chat_id": chat_id}
297
- r = requests.post(url, files=files, data=data, timeout=30)
298
- if r.status_code != 200:
299
- logger.warning("Telegram sendAudio failed %s: %s", r.status_code, r.text[:300])
300
- return False
301
- return True
302
- except Exception:
303
- logger.exception("telegram_send_audio")
304
- return False
305
-
306
- # ---------------- Telegram poller (background) ----------------
307
- def telegram_poller_loop():
308
- if not TELEGRAM_TOKEN:
309
- logger.info("Telegram token not set; poller disabled")
310
- return
311
- logger.info("Starting Telegram poller")
312
- base = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}"
313
- offset = None
314
- while True:
315
- try:
316
- params = {"timeout": 30}
317
- if offset: params["offset"] = offset
318
- r = requests.get(base + "/getUpdates", params=params, timeout=35)
319
- if r.status_code != 200:
320
- logger.warning("Telegram getUpdates failed: %s", r.status_code)
321
- time.sleep(2); continue
322
- j = r.json()
323
- for upd in j.get("result", []):
324
- offset = upd.get("update_id", 0) + 1
325
- msg = upd.get("message") or {}
326
- chat = msg.get("chat", {})
327
- chat_id = str(chat.get("id"))
328
- text = (msg.get("text") or "").strip()
329
- if not text: continue
330
- logger.info("TG msg %s: %s", chat_id, text[:200])
331
- lower = text.lower()
332
- if lower.startswith("/ask "):
333
- q = text[5:].strip()
334
- try:
335
- ans = hf_text_generate(q)
336
- except Exception as e:
337
- ans = f"[HF error] {e}"
338
- try:
339
- requests.post(base + "/sendMessage", json={"chat_id": chat_id, "text": ans}, timeout=10)
340
- except Exception:
341
- logger.exception("tg reply failed")
342
- elif lower.startswith("/say "):
343
- phrase = text[5:].strip()
344
- try:
345
- audio = hf_tts_get_bytes(phrase)
346
- telegram_send_audio(chat_id, audio, filename="say.mp3")
347
- except Exception:
348
- logger.exception("tg say failed")
349
- elif lower.startswith("/status"):
350
- try:
351
- requests.post(base + "/sendMessage", json={"chat_id": chat_id, "text": "KC Robot v7.5 running"}, timeout=10)
352
- except Exception:
353
- pass
354
- else:
355
- try:
356
- requests.post(base + "/sendMessage", json={"chat_id": chat_id, "text": "Commands: /ask <q> | /say <text> | /status"}, timeout=10)
357
- except Exception:
358
- pass
359
- except Exception:
360
- logger.exception("telegram poller crashed, sleeping 3s")
361
- time.sleep(3)
362
-
363
- if TELEGRAM_TOKEN:
364
- try:
365
- t = threading.Thread(target=telegram_poller_loop, daemon=True)
366
- t.start()
367
- except Exception:
368
- logger.exception("start telegram thread failed")
369
-
370
- # ---------------- Flask app & endpoints ----------------
371
- app = Flask(__name__)
372
-
373
- INDEX_HTML = """
374
- <!doctype html>
375
- <html>
376
- <head>
377
- <meta charset="utf-8">
378
- <title>KC Robot AI v7.5</title>
379
- <meta name="viewport" content="width=device-width,initial-scale=1">
380
- <style>
381
- body{font-family:Arial,Helvetica,sans-serif;margin:12px;color:#111}
382
- .box{max-width:960px;margin:auto}
383
- textarea{width:100%;height:90px;padding:10px;font-size:16px;border-radius:8px;border:1px solid #ddd}
384
- button{padding:10px 14px;margin:6px 4px;border-radius:8px;background:#0b74de;color:white;border:none;cursor:pointer;font-weight:700}
385
- #chat{border:1px solid #eee;padding:10px;height:360px;overflow:auto;background:#fafafa;border-radius:8px}
386
- .you{color:#0b63d6;margin:6px 0}
387
- .bot{color:#0b8a3f;margin:6px 0}
388
- .small{font-size:13px;color:#666}
389
- </style>
390
- </head>
391
- <body>
392
- <div class="box">
393
- <h2>🤖 KC Robot AI v7.5 — Final (Auto-model)</h2>
394
- <div class="small">Model: <span id="modelName">loading...</span> | Telegram: <span id="tgstatus">checking...</span></div>
395
- <textarea id="userText" placeholder="Nhập tiếng Việt hoặc English..."></textarea>
396
- <div>
397
- <select id="lang"><option value="auto">Auto</option><option value="vi">Vietnamese</option><option value="en">English</option></select>
398
- <button onclick="send()">Gửi</button>
399
- <button onclick="playLast()">Phát âm</button>
400
- <button onclick="clearChat()">Xóa</button>
401
- </div>
402
- <div id="chat"></div>
403
- <div style="margin-top:10px">
404
- <input type="file" id="afile" accept="audio/*"><button onclick="uploadAudio()">Upload → STT</button>
405
- </div>
406
- <hr>
407
- <div class="small">Diagnostics: <button onclick="modelCheck()">Kiểm tra model</button><span id="diag"></span></div>
408
- </div>
409
- <script>
410
- let lastAnswer = "";
411
- async function loadStatus(){ try{ let r=await fetch('/health'); let j=await r.json(); document.getElementById('modelName').innerText=j.hf_model||'(not set)'; document.getElementById('tgstatus').innerText=j.telegram ? 'enabled' : 'disabled'; }catch(e){ console.log(e); } }
412
- function escapeHtml(s){ return (s+'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
413
- function appendYou(t){ document.getElementById('chat').innerHTML += '<div class="you"><b>You:</b> '+escapeHtml(t)+'</div>'; scroll(); }
414
- function appendBot(t){ document.getElementById('chat').innerHTML += '<div class="bot"><b>Robot:</b> '+escapeHtml(t)+'</div>'; scroll(); }
415
- function scroll(){ let c=document.getElementById('chat'); c.scrollTop = c.scrollHeight; }
416
- async function send(){
417
- let t=document.getElementById('userText').value.trim(); if(!t) return; appendYou(t); document.getElementById('userText').value='';
418
- let lang=document.getElementById('lang').value;
419
- try{
420
- let r=await fetch('/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:t,lang:lang})});
421
- let j=await r.json();
422
- if(j.answer){ lastAnswer=j.answer; appendBot(j.answer); } else appendBot('[error] '+JSON.stringify(j));
423
- }catch(e){ appendBot('[network error] '+e); }
424
- }
425
- async function playLast(){
426
- if(!lastAnswer) return alert('Chưa có câu trả lời');
427
- try{
428
- let r=await fetch('/tts',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:lastAnswer})});
429
- if(!r.ok){ alert('TTS lỗi'); return; }
430
- const blob = await r.blob();
431
- const url=URL.createObjectURL(blob);
432
- const audio=new Audio(url); audio.play();
433
- }catch(e){ alert('Play error: '+e); }
434
- }
435
- async function uploadAudio(){
436
- const f=document.getElementById('afile').files[0]; if(!f) return alert('Chọn file audio');
437
- const fd=new FormData(); fd.append('file', f);
438
- const r=await fetch('/stt',{method:'POST', body: fd});
439
- const j=await r.json();
440
- if(j.text){ appendYou('[voice] '+j.text); } else appendYou('[stt error] '+JSON.stringify(j));
441
- }
442
- function clearChat(){ document.getElementById('chat').innerHTML=''; lastAnswer=''; }
443
- async function modelCheck(){
444
- document.getElementById('diag').innerText=' checking...';
445
- try{
446
- let r=await fetch('/model_check');
447
- let j=await r.json();
448
- document.getElementById('diag').innerText = ' ' + JSON.stringify(j).slice(0,200);
449
- loadStatus();
450
- }catch(e){ document.getElementById('diag').innerText=' error'; }
451
- }
452
- loadStatus();
453
- </script>
454
- </body>
455
- </html>
456
- """
457
-
458
- @app.route("/", methods=["GET"])
459
- def index():
460
- return render_template_string(INDEX_HTML)
461
-
462
- @app.route("/health", methods=["GET"])
463
- def health():
464
- return jsonify({
465
- "ok": True,
466
- "hf_token": bool(HF_TOKEN),
467
- "hf_model": SELECTED_MODEL or HF_MODEL or "",
468
- "hf_tts_model": HF_TTS_MODEL,
469
- "hf_stt_model": HF_STT_MODEL,
470
- "telegram": bool(TELEGRAM_TOKEN and TELEGRAM_CHAT_ID),
471
- "conv_len": len(CONVERSATION),
472
- "display_len": len(DISPLAY_BUFFER)
473
- })
474
-
475
- @app.route("/ask", methods=["POST"])
476
- def route_ask():
477
- try:
478
- j = request.get_json(force=True) or {}
479
- text = clean_text(j.get("text","") or "")
480
- lang = (j.get("lang","auto") or "auto")
481
- if not text:
482
- return jsonify({"error":"no text"}), 400
483
- if lang == "vi":
484
- prompt = f"Bạn là trợ lý thông minh, trả lời bằng tiếng Việt, rõ ràng và ngắn gọn:\n\n{text}"
485
- elif lang == "en":
486
- prompt = f"You are a helpful assistant. Answer in clear English, concise:\n\n{text}"
487
- else:
488
- prompt = f"You are a bilingual assistant (Vietnamese/English). Answer in the same language as the user, clearly and concisely:\n\n{text}"
489
- try:
490
- ans = hf_text_generate(prompt)
491
- except Exception as e:
492
- logger.exception("hf_text_generate failed")
493
- return jsonify({"error": str(e)}), 500
494
- CONVERSATION.append((text, ans))
495
- save_conv(text, ans)
496
- push_display("YOU: " + (text[:60]))
497
- push_display("BOT: " + (ans[:60] if isinstance(ans,str) else str(ans)[:60]))
498
- # notify telegram
499
- if TELEGRAM_TOKEN and TELEGRAM_CHAT_ID:
500
- try:
501
- telegram_send_message(TELEGRAM_CHAT_ID, f"You: {text}\nBot: {ans[:300]}")
502
- except Exception:
503
- logger.exception("telegram notify failed")
504
- return jsonify({"answer": ans})
505
- except Exception as e:
506
- logger.exception("route_ask exception")
507
- return jsonify({"error": str(e)}), 500
508
 
509
- @app.route("/tts", methods=["POST"])
510
- def route_tts():
511
- try:
512
- j = request.get_json(force=True) or {}
513
- text = clean_text(j.get("text","") or "")
514
- if not text:
515
- return jsonify({"error":"no text"}), 400
516
- try:
517
- audio_bytes = hf_tts_get_bytes(text)
518
- except Exception as e:
519
- logger.exception("tts generation failed")
520
- return jsonify({"error": str(e)}), 500
521
- return Response(audio_bytes, mimetype="audio/mpeg")
522
- except Exception as e:
523
- logger.exception("route_tts exception")
524
- return jsonify({"error": str(e)}), 500
525
 
526
- @app.route("/stt", methods=["POST"])
527
- def route_stt():
528
- try:
529
- if "file" in request.files:
530
- f = request.files["file"]
531
- audio_bytes = f.read()
532
- else:
533
- audio_bytes = request.get_data()
534
- if not audio_bytes:
535
- return jsonify({"error":"no audio provided"}), 400
536
  try:
537
- txt = hf_stt_from_bytes(audio_bytes)
 
 
 
 
 
 
538
  except Exception as e:
539
- logger.exception("STT failed")
540
- return jsonify({"error": str(e)}), 500
541
- CONVERSATION.append((f"[voice] {txt}", ""))
542
- save_conv(f"[voice] {txt}", "")
543
- push_display("VOICE: " + (txt[:60] if isinstance(txt,str) else str(txt)))
544
- return jsonify({"text": txt})
545
- except Exception as e:
546
- logger.exception("route_stt exception")
547
- return jsonify({"error": str(e)}), 500
548
 
549
- @app.route("/presence", methods=["POST"])
550
- def route_presence():
551
- """
552
- ESP32 radar should POST JSON {"note":"..."}.
553
- Server returns greeting audio (if TTS available) or JSON greeting.
554
- Also sends telegram notification if configured.
555
- """
556
  try:
557
- j = request.get_json(force=True) or {}
558
- note = clean_text(j.get("note","Có người phía trước") or " người phía trước")
559
- greeting = f"Xin chào! {note}"
560
- CONVERSATION.append(("__presence__", greeting))
561
- save_conv("__presence__", greeting)
562
- push_display("RADAR: " + note[:60])
563
- if TELEGRAM_TOKEN and TELEGRAM_CHAT_ID:
564
- try:
565
- telegram_send_message(TELEGRAM_CHAT_ID, f"⚠️ Robot: Phát hiện người - {note}")
566
- except Exception:
567
- logger.exception("telegram notify failed")
568
- try:
569
- audio_bytes = hf_tts_get_bytes(greeting)
570
- return Response(audio_bytes, mimetype="audio/mpeg")
571
- except Exception:
572
- return jsonify({"greeting": greeting})
573
  except Exception as e:
574
- logger.exception("route_presence exception")
575
- return jsonify({"error": str(e)}), 500
576
 
577
- @app.route("/display", methods=["GET"])
578
- def route_display():
579
- return jsonify({"lines": DISPLAY_BUFFER.copy(), "conv_len": len(CONVERSATION)})
 
 
 
580
 
581
- @app.route("/model_check", methods=["GET"])
582
  def model_check():
583
- """
584
- Attempt to verify HF_MODEL / select fallback, returns diagnostic JSON.
585
- """
586
- global SELECTED_MODEL
587
- # first try current HF_MODEL
588
- results = {}
589
  try:
590
- # if SELECTED_MODEL already set and seems good, return
591
- if SELECTED_MODEL:
592
- results["selected_model"] = SELECTED_MODEL
593
- ok, info = test_model_working(SELECTED_MODEL)
594
- results["selected_ok"] = ok
595
- results["selected_info"] = info
596
- return jsonify(results)
597
- # else try auto-select with preference HF_MODEL
598
- chosen = auto_select_model(HF_MODEL if HF_MODEL else None)
599
- if chosen:
600
- SELECTED_MODEL = chosen
601
- results["selected_model"] = chosen
602
- results["note"] = "Model selected"
603
- return jsonify(results)
604
- else:
605
- results["error"] = "No usable model found in candidates"
606
- return jsonify(results), 404
607
  except Exception as e:
608
- logger.exception("model_check failed")
609
- return jsonify({"error": str(e)}), 500
610
 
611
- @app.route("/config", methods=["GET","POST"])
612
- def config():
613
- """
614
- GET returns current config.
615
- POST JSON can change HF_MODEL / HF_TTS_MODEL / HF_STT_MODEL at runtime (temporary).
616
- Example: {"hf_model":"...", "hf_tts_model":"..."}
617
- """
618
- global HF_MODEL, HF_TTS_MODEL, HF_STT_MODEL, SELECTED_MODEL
619
- if request.method == "GET":
620
- return jsonify({"hf_model": HF_MODEL, "hf_tts_model": HF_TTS_MODEL, "hf_stt_model": HF_STT_MODEL, "selected_model": SELECTED_MODEL})
621
- try:
622
- j = request.get_json(force=True) or {}
623
- changed = {}
624
- if "hf_model" in j:
625
- HF_MODEL = j["hf_model"]
626
- changed["hf_model"] = HF_MODEL
627
- SELECTED_MODEL = None # force re-evaluation
628
- if "hf_tts_model" in j:
629
- HF_TTS_MODEL = j["hf_tts_model"]
630
- changed["hf_tts_model"] = HF_TTS_MODEL
631
- if "hf_stt_model" in j:
632
- HF_STT_MODEL = j["hf_stt_model"]
633
- changed["hf_stt_model"] = HF_STT_MODEL
634
- return jsonify({"changed": changed})
635
- except Exception as e:
636
- logger.exception("config post failed")
637
- return jsonify({"error": str(e)}), 500
638
-
639
- # ---------------- startup auto model selection ----------------
640
- def startup_model_check():
641
- global SELECTED_MODEL
642
- logger.info("Startup: checking/selecting model...")
643
- try:
644
- chosen = auto_select_model(HF_MODEL if HF_MODEL else None)
645
- if chosen:
646
- SELECTED_MODEL = chosen
647
- logger.info("Startup: selected model = %s", SELECTED_MODEL)
648
- else:
649
- logger.warning("Startup: no usable HF model found yet. Use /model_check or set HF_MODEL secret.")
650
- except Exception:
651
- logger.exception("startup_model_check failed")
652
-
653
- # run startup check in a thread so Flask starts quickly
654
- t_start = threading.Thread(target=startup_model_check, daemon=True)
655
- t_start.start()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656
 
657
- # ---------------- run app ----------------
658
  if __name__ == "__main__":
659
- logger.info("KC Robot AI v7.5 starting. PREF_HF_MODEL=%s HF_TTS=%s HF_STT=%s Telegram=%s",
660
- HF_MODEL or "(none)", HF_TTS_MODEL or "(none)", HF_STT_MODEL or "(none)", bool(TELEGRAM_TOKEN and TELEGRAM_CHAT_ID))
661
- app.run(host="0.0.0.0", port=PORT)
 
1
+ # =============================================
2
+ # app.py v8.0 RobotAI Server (ESP32 + HF Cloud + Telegram)
3
+ # Author: GPT-5 Assistant for Cường Phan
4
+ # =============================================
 
 
 
 
 
 
 
 
5
 
6
  import os
 
 
 
7
  import json
8
  import uuid
9
+ import time
10
+ import queue
 
 
 
11
  import requests
12
+ import threading
13
+ from datetime import datetime
14
+ from flask import Flask, request, jsonify, send_file
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ app = Flask(__name__)
 
 
 
 
17
 
18
+ # ======================
19
+ # Configuration
20
+ # ======================
21
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
22
+ HF_MODEL = os.environ.get("HF_MODEL", "mistralai/Mistral-7B-Instruct-v0.3")
23
+ HF_STT_MODEL = os.environ.get("HF_STT_MODEL", "openai/whisper-small")
24
+ HF_TTS_MODEL = os.environ.get("HF_TTS_MODEL", "espnet/kan-bayashi_ljspeech_vits")
25
+ TELEGRAM_TOKEN = os.environ.get("TELEGRAM_TOKEN", "")
26
+ TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "")
27
+
28
+ COMMAND_QUEUE = queue.Queue()
29
+ CONFIG_FILE = "config.json"
30
+ AUDIO_DIR = "audio_cache"
31
+
32
+ if not os.path.exists(AUDIO_DIR):
33
+ os.makedirs(AUDIO_DIR)
34
+
35
+ # ======================
36
+ # Utility Functions
37
+ # ======================
38
+ def log(msg):
39
+ print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}")
40
+
41
+ def save_config(data):
42
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
43
+ json.dump(data, f, ensure_ascii=False, indent=2)
44
+
45
+ def load_config():
46
+ if os.path.exists(CONFIG_FILE):
47
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
48
+ return json.load(f)
49
+ return {}
50
+
51
+ def send_telegram_message(text):
52
+ if not TELEGRAM_TOKEN or not TELEGRAM_CHAT_ID:
53
+ return
54
+ url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage"
55
+ payload = {"chat_id": TELEGRAM_CHAT_ID, "text": text}
56
  try:
57
+ requests.post(url, json=payload, timeout=5)
58
+ except Exception as e:
59
+ log(f"[Telegram Error] {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
+ def hf_query(model_id, inputs, parameters=None, timeout=60):
 
 
62
  url = f"https://api-inference.huggingface.co/models/{model_id}"
63
+ headers = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
64
+ payload = {"inputs": inputs}
65
+ if parameters:
66
+ payload["parameters"] = parameters
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  try:
68
+ r = requests.post(url, headers=headers, json=payload, timeout=timeout)
69
+ except Exception as e:
70
+ log(f"[HF request exception] {e}")
71
+ return {"error": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
 
 
 
 
 
73
  if r.status_code == 200:
74
  try:
75
+ return r.json()
 
 
 
76
  except Exception:
77
+ return {"raw_text": r.text}
78
  else:
79
+ log(f"[HF Error] {r.status_code} - {r.text}")
80
+ return {"error": r.text, "status_code": r.status_code}
81
 
82
+ def detect_language(text):
 
83
  if not text:
84
+ return "en"
85
+ vi_chars = "àáảãạăằắẳẵặâầấẩẫậđêềếểễệôồốổỗộơờớởỡợưừứửữự"
86
+ if any(ch in text for ch in vi_chars):
87
+ return "vi"
88
+ return "en"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
+ def generate_tts_audio(text, lang="en"):
91
+ session_id = str(uuid.uuid4())[:8]
92
+ filename = f"{AUDIO_DIR}/{session_id}.mp3"
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
+ if HF_TTS_MODEL:
 
 
 
 
 
 
 
 
 
95
  try:
96
+ url = f"https://api-inference.huggingface.co/models/{HF_TTS_MODEL}"
97
+ headers = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
98
+ rr = requests.post(url, headers=headers, json={"inputs": text}, timeout=120)
99
+ if rr.status_code == 200 and rr.content:
100
+ with open(filename, "wb") as f:
101
+ f.write(rr.content)
102
+ return filename
103
  except Exception as e:
104
+ log(f"[TTS exception] {e}")
 
 
 
 
 
 
 
 
105
 
 
 
 
 
 
 
 
106
  try:
107
+ from gtts import gTTS
108
+ lang_code = "vi" if lang == "vi" else "en"
109
+ tts = gTTS(text=text, lang=lang_code)
110
+ tts.save(filename)
111
+ return filename
 
 
 
 
 
 
 
 
 
 
 
112
  except Exception as e:
113
+ log(f"gTTS fallback failed: {e}")
114
+ return None
115
 
116
+ # ======================
117
+ # API ROUTES
118
+ # ======================
119
+ @app.route("/")
120
+ def home():
121
+ return "<h2>🤖 RobotAI v8.0 Server Running Successfully!</h2>"
122
 
123
+ @app.route("/model_check")
124
  def model_check():
125
+ info = {"HF_MODEL": HF_MODEL, "token_present": bool(HF_TOKEN)}
 
 
 
 
 
126
  try:
127
+ r = hf_query(HF_MODEL, "Hello world", parameters={"max_new_tokens": 10}, timeout=10)
128
+ info["probe"] = r
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  except Exception as e:
130
+ info["probe_error"] = str(e)
131
+ return jsonify(info)
132
 
133
+ @app.route("/api/chat", methods=["POST"])
134
+ def api_chat():
135
+ data = request.get_json(force=True)
136
+ text = data.get("text", "").strip()
137
+ if not text:
138
+ return jsonify({"error": "missing text"}), 400
139
+ lang = detect_language(text)
140
+ prefix = "Hãy trả lời bằng tiếng Việt:" if lang == "vi" else "Answer in English:"
141
+ prompt = f"{prefix}\n\n{text}"
142
+ r = hf_query(HF_MODEL, prompt, parameters={"max_new_tokens": 200, "temperature": 0.7}, timeout=60)
143
+ reply = ""
144
+ if isinstance(r, list) and r and isinstance(r[0], dict):
145
+ reply = r[0].get("generated_text") or r[0].get("text") or str(r[0])
146
+ elif isinstance(r, dict) and "generated_text" in r:
147
+ reply = r["generated_text"]
148
+ else:
149
+ reply = str(r)
150
+
151
+ tts_file = generate_tts_audio(reply, lang)
152
+ return jsonify({"reply": reply, "lang": lang, "tts_url": f"/api/tts/{os.path.basename(tts_file)}" if tts_file else None})
153
+
154
+ @app.route("/api/tts/<filename>")
155
+ def api_tts_file(filename):
156
+ path = os.path.join(AUDIO_DIR, filename)
157
+ if os.path.exists(path):
158
+ return send_file(path, mimetype="audio/mpeg")
159
+ return jsonify({"error": "file not found"}), 404
160
+
161
+ @app.route("/api/presence", methods=["POST"])
162
+ def api_presence():
163
+ data = request.get_json(force=True)
164
+ note = data.get("note", "Có người xuất hiện!")
165
+ send_telegram_message(f"🚶 {note}")
166
+ greeting = "Xin chào! Mình RobotAI, có thể giúp gì không?"
167
+ audio = generate_tts_audio(greeting, lang="vi")
168
+ return jsonify({"greeting": greeting, "tts_url": f"/api/tts/{os.path.basename(audio)}" if audio else None})
169
+
170
+ @app.route("/api/control", methods=["POST"])
171
+ def api_control():
172
+ data = request.get_json(force=True)
173
+ COMMAND_QUEUE.put(data)
174
+ log(f"Command added: {data}")
175
+ return jsonify({"status": "queued"})
176
+
177
+ @app.route("/api/poll_commands")
178
+ def api_poll_commands():
179
+ cmds = []
180
+ while not COMMAND_QUEUE.empty():
181
+ cmds.append(COMMAND_QUEUE.get())
182
+ return jsonify({"commands": cmds})
183
+
184
+ @app.route("/api/config", methods=["GET", "POST"])
185
+ def api_config():
186
+ if request.method == "POST":
187
+ data = request.get_json(force=True)
188
+ save_config(data)
189
+ return jsonify({"status": "saved"})
190
+ return jsonify(load_config())
191
+
192
+ # ======================
193
+ # Background thread
194
+ # ======================
195
+ def heartbeat():
196
+ while True:
197
+ log("Heartbeat: RobotAI active")
198
+ time.sleep(60)
199
 
 
200
  if __name__ == "__main__":
201
+ threading.Thread(target=heartbeat, daemon=True).start()
202
+ app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 7860)), debug=True)