kcrobot24 commited on
Commit
b0007ae
·
verified ·
1 Parent(s): eadcfca
Files changed (1) hide show
  1. app.py +425 -84
app.py CHANGED
@@ -1,102 +1,443 @@
1
 
2
- # KC Robot AI V4.1 (FPT Female Voice, HuggingFace Cloud Brain)
3
- import os, io, time, threading, requests, logging
4
- from flask import Flask, request, jsonify, send_file, render_template_string
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- app = Flask(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  logging.basicConfig(level=logging.INFO)
8
- logger = logging.getLogger("kcrobot.ai")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- # ========== CONFIG ==========
11
- HF_API_TOKEN = os.getenv("HF_API_TOKEN", "")
12
- HF_MODEL = os.getenv("HF_MODEL", "google/flan-t5-large")
13
- HF_TTS_MODEL = os.getenv("HF_TTS_MODEL", "NguyenManhTuan/VietnameseTTS_FPT_AI_Female")
14
- HF_STT_MODEL = os.getenv("HF_STT_MODEL", "openai/whisper-small")
15
- TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "")
16
- TELEGRAM_CHATID = os.getenv("TELEGRAM_CHATID", "")
17
- HF_HEADERS = {"Authorization": f"Bearer {HF_API_TOKEN}"}
18
 
19
- CONV = []
20
- DISPLAY_LINES = []
21
 
22
- def push_display(line, limit=6):
 
 
 
 
 
 
 
 
23
  DISPLAY_LINES.append(line)
24
  if len(DISPLAY_LINES) > limit:
25
- DISPLAY_LINES[:] = DISPLAY_LINES[-limit:]
26
-
27
- # ========== HF HELPERS ==========
28
- def hf_text_generate(prompt):
29
- url = f"https://api-inference.huggingface.co/models/{HF_MODEL}"
30
- r = requests.post(url, headers=HF_HEADERS, json={"inputs": prompt, "parameters":{"max_new_tokens":256}})
31
- return r.json()[0].get("generated_text", "Lỗi HF model") if r.ok else f"HF Error {r.status_code}"
32
-
33
- def hf_tts_get_mp3(text):
34
- url = f"https://api-inference.huggingface.co/models/{HF_TTS_MODEL}"
35
- r = requests.post(url, headers=HF_HEADERS, json={"inputs": text})
36
- return r.content if r.ok else b""
37
-
38
- def hf_stt_from_bytes(audio_bytes):
39
- url = f"https://api-inference.huggingface.co/models/{HF_STT_MODEL}"
40
- r = requests.post(url, headers={**HF_HEADERS,"Content-Type":"application/octet-stream"}, data=audio_bytes)
41
- j = r.json()
42
- return j.get("text","") if isinstance(j,dict) else str(j)
43
-
44
- # ========== ROUTES ==========
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  @app.route("/ask", methods=["POST"])
46
- def ask():
47
- data = request.get_json(force=True)
48
- text = data.get("text","")
49
- lang = "vi" if any(ch in "ăâêôơưđáàạảãấầẩậẫéèẻẹẽíìỉịĩóòỏõọúùủụũ" for ch in text.lower()) else "en"
50
- prompt = f"Bạn là trợ lý AI song ngữ. Trả lời bằng {('tiếng Việt' if lang=='vi' else 'English')}:\n{text}"
51
- ans = hf_text_generate(prompt)
52
- CONV.append((text, ans)); push_display(f"YOU:{text[:40]}"); push_display(f"BOT:{ans[:40]}")
53
- return jsonify({"answer": ans})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
  @app.route("/tts", methods=["POST"])
56
- def tts():
57
- data = request.get_json(force=True)
58
- text = data.get("text","")
59
- audio = hf_tts_get_mp3(text)
60
- return send_file(io.BytesIO(audio), mimetype="audio/mpeg", as_attachment=False)
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  @app.route("/stt", methods=["POST"])
63
- def stt():
64
- f = request.files.get("file")
65
- audio_bytes = f.read() if f else request.get_data()
66
- text = hf_stt_from_bytes(audio_bytes)
67
- push_display("UserAudio: " + text[:40])
68
- return jsonify({"text": text})
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
  @app.route("/presence", methods=["POST"])
71
- def presence():
72
- note = request.json.get("note","Có người tới")
73
- greeting = f"Xin chào! {note}"
74
- if TELEGRAM_TOKEN and TELEGRAM_CHATID:
75
- try:
76
- requests.post(f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
77
- json={"chat_id": TELEGRAM_CHATID, "text": f"⚠️ Robot phát hiện: {note}"})
78
- except Exception as e: logger.warning(f"Telegram error: {e}")
79
- return jsonify({"greeting": greeting})
80
-
81
- @app.route("/display")
82
- def display(): return jsonify({"lines": DISPLAY_LINES})
83
-
84
- # ========== WEB CHAT ==========
85
- HTML = """
86
- <!doctype html><html><head><meta charset="utf-8"><title>KC Robot AI</title></head>
87
- <body><h3>KC Robot AI V4.1 🤖</h3>
88
- <textarea id='q' rows=3 cols=80 placeholder='Nhập tiếng Việt hoặc English...'></textarea><br>
89
- <button onclick='ask()'>Gửi</button><pre id='out'></pre>
90
- <script>
91
- async function ask(){
92
- let q=document.getElementById('q').value;
93
- let r=await fetch('/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:q})});
94
- let j=await r.json();document.getElementById('out').innerText=j.answer;
95
- }
96
- </script></body></html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  """
98
- @app.route("/")
99
- def home(): return HTML
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
 
101
  if __name__ == "__main__":
102
- app.run(host="0.0.0.0", port=int(os.getenv("PORT", 7860)))
 
 
 
1
 
2
+ # app.py - KC Robot AI V4.1 (Full - FPT female TTS)
3
+ # Full-feature Flask server:
4
+ # - /ask (text) -> HF LLM
5
+ # - /tts (text) -> HF TTS (default: NguyenManhTuan/VietnameseTTS_FPT_AI_Female)
6
+ # - /stt (audio) -> HF STT (default: openai/whisper-small)
7
+ # - /presence (radar event) -> greeting + Telegram notify
8
+ # - /display -> OLED lines
9
+ # - Web UI for quick test
10
+ # - Telegram poller (background thread) to accept /ask, /say, /status
11
+ #
12
+ # Configuration via environment variables / Secrets in HF Space:
13
+ # HF_API_TOKEN (required for HF inference)
14
+ # HF_MODEL (optional, default google/flan-t5-large)
15
+ # HF_TTS_MODEL (optional, default NguyenManhTuan/VietnameseTTS_FPT_AI_Female)
16
+ # HF_STT_MODEL (optional, default openai/whisper-small)
17
+ # TELEGRAM_TOKEN (optional)
18
+ # TELEGRAM_CHATID (optional)
19
+ #
20
+ # Keep requirements minimal to improve HF Space stability:
21
+ # flask, requests
22
+ #
23
+ # Important: set tokens in HF Space Settings -> Secrets (do not hardcode)
24
 
25
+ import os
26
+ import io
27
+ import time
28
+ import json
29
+ import uuid
30
+ import logging
31
+ import threading
32
+ from typing import Optional, List, Tuple
33
+ from pathlib import Path
34
+
35
+ import requests
36
+ from flask import Flask, request, jsonify, send_file, render_template_string, abort
37
+
38
+ # ----------------- Config & Logging -----------------
39
  logging.basicConfig(level=logging.INFO)
40
+ logger = logging.getLogger("kcrobot.v4")
41
+
42
+ app = Flask(__name__)
43
+
44
+ # Directory for temporary files (tts audio)
45
+ TMP_DIR = Path("/tmp/kcrobot") if os.name != "nt" else Path.cwd() / "tmp_kcrobot"
46
+ TMP_DIR.mkdir(parents=True, exist_ok=True)
47
+
48
+ # Environment / Secrets (set these in HF Space)
49
+ HF_API_TOKEN = os.getenv("HF_API_TOKEN", "").strip()
50
+ HF_MODEL = os.getenv("HF_MODEL", "google/flan-t5-large").strip()
51
+ # Default FPT female Vietnamese TTS
52
+ HF_TTS_MODEL = os.getenv("HF_TTS_MODEL", "NguyenManhTuan/VietnameseTTS_FPT_AI_Female").strip()
53
+ HF_STT_MODEL = os.getenv("HF_STT_MODEL", "openai/whisper-small").strip()
54
+ TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "").strip()
55
+ TELEGRAM_CHATID = os.getenv("TELEGRAM_CHATID", "").strip()
56
 
57
+ # Port (HF sets PORT env in runtime)
58
+ PORT = int(os.getenv("PORT", os.getenv("SERVER_PORT", 7860)))
 
 
 
 
 
 
59
 
60
+ if not HF_API_TOKEN:
61
+ logger.warning("HF_API_TOKEN is not set — HF inference will fail until you add it in Secrets.")
62
 
63
+ HF_HEADERS = {"Authorization": f"Bearer {HF_API_TOKEN}"} if HF_API_TOKEN else {}
64
+
65
+ # ----------------- In-memory buffers -----------------
66
+ CONV: List[Tuple[str, str]] = [] # list of (user, bot)
67
+ DISPLAY_LINES: List[str] = [] # lines for OLED display
68
+
69
+ def push_display(line: str, limit: int = 6):
70
+ """Keep last `limit` lines for display."""
71
+ global DISPLAY_LINES
72
  DISPLAY_LINES.append(line)
73
  if len(DISPLAY_LINES) > limit:
74
+ DISPLAY_LINES = DISPLAY_LINES[-limit:]
75
+
76
+ # ----------------- Helper: Hugging Face inference -----------------
77
+ def hf_post_json(model_id: str, payload: dict, timeout: int = 120):
78
+ """POST JSON to HF inference; return parsed JSON or raise."""
79
+ if not HF_API_TOKEN:
80
+ raise RuntimeError("HF_API_TOKEN missing (set in Secrets).")
81
+ url = f"https://api-inference.huggingface.co/models/{model_id}"
82
+ headers = dict(HF_HEADERS)
83
+ headers["Content-Type"] = "application/json"
84
+ r = requests.post(url, headers=headers, json=payload, timeout=timeout)
85
+ if not r.ok:
86
+ logger.error("HF POST JSON error %s: %s", r.status_code, r.text[:400])
87
+ r.raise_for_status()
88
+ try:
89
+ return r.json()
90
+ except Exception:
91
+ return r.text
92
+
93
+ def hf_post_bytes(model_id: str, data: bytes, content_type: str = "application/octet-stream", timeout: int = 180):
94
+ """POST binary data (audio) to HF inference; return response object or raise."""
95
+ if not HF_API_TOKEN:
96
+ raise RuntimeError("HF_API_TOKEN missing (set in Secrets).")
97
+ url = f"https://api-inference.huggingface.co/models/{model_id}"
98
+ headers = dict(HF_HEADERS)
99
+ headers["Content-Type"] = content_type
100
+ r = requests.post(url, headers=headers, data=data, timeout=timeout)
101
+ if not r.ok:
102
+ logger.error("HF POST bytes error %s: %s", r.status_code, r.text[:400])
103
+ r.raise_for_status()
104
+ return r
105
+
106
+ # ----------------- Text generation (LLM) -----------------
107
+ def hf_text_generate(prompt: str, model: Optional[str] = None, max_new_tokens: int = 256, temperature: float = 0.7) -> str:
108
+ model = model or HF_MODEL
109
+ payload = {
110
+ "inputs": prompt,
111
+ "parameters": {"max_new_tokens": int(max_new_tokens), "temperature": float(temperature)},
112
+ "options": {"wait_for_model": True}
113
+ }
114
+ out = hf_post_json(model, payload, timeout=120)
115
+ # parse common shapes
116
+ if isinstance(out, list) and len(out) > 0:
117
+ first = out[0]
118
+ if isinstance(first, dict) and "generated_text" in first:
119
+ return first["generated_text"]
120
+ return str(first)
121
+ if isinstance(out, dict):
122
+ for k in ("generated_text", "text", "summary_text"):
123
+ if k in out:
124
+ return out[k]
125
+ return json.dumps(out)
126
+ return str(out)
127
+
128
+ # ----------------- TTS (Text -> audio bytes) -----------------
129
+ def hf_tts_get_audio_bytes(text: str, model: Optional[str] = None) -> bytes:
130
+ """Call HF TTS model and return audio bytes (commonly mp3 or wav)."""
131
+ model = model or HF_TTS_MODEL
132
+ payload = {"inputs": text}
133
+ r = requests.post(f"https://api-inference.huggingface.co/models/{model}", headers={**HF_HEADERS, "Content-Type": "application/json"}, json=payload, timeout=120)
134
+ if not r.ok:
135
+ logger.error("HF TTS error %s: %s", r.status_code, r.text[:400])
136
+ r.raise_for_status()
137
+ return r.content
138
+
139
+ def save_tts_temp(audio_bytes: bytes, ext_hint: str = "mp3") -> str:
140
+ """Save bytes to a temp file under TMP_DIR and return filename."""
141
+ fname = f"tts_{int(time.time())}_{uuid.uuid4().hex}.{ext_hint}"
142
+ p = TMP_DIR / fname
143
+ p.write_bytes(audio_bytes)
144
+ return fname
145
+
146
+ # ----------------- STT (audio bytes -> text) -----------------
147
+ def hf_stt_from_bytes(audio_bytes: bytes, model: Optional[str] = None) -> str:
148
+ model = model or HF_STT_MODEL
149
+ r = hf_post_bytes(model, audio_bytes, content_type="application/octet-stream", timeout=180)
150
+ # often returns {"text": "..."}
151
+ try:
152
+ j = r.json()
153
+ if isinstance(j, dict) and "text" in j:
154
+ return j["text"]
155
+ if isinstance(j, list) and len(j) and isinstance(j[0], dict) and "text" in j[0]:
156
+ return j[0]["text"]
157
+ return str(j)
158
+ except Exception:
159
+ return r.text if hasattr(r, "text") else ""
160
+
161
+ # ----------------- Endpoints for ESP32 / Web -----------------
162
+ @app.route("/health", methods=["GET"])
163
+ def health():
164
+ return jsonify({
165
+ "ok": True,
166
+ "hf_api_token": bool(HF_API_TOKEN),
167
+ "hf_model": HF_MODEL,
168
+ "hf_tts_model": HF_TTS_MODEL,
169
+ "hf_stt_model": HF_STT_MODEL,
170
+ "telegram": bool(TELEGRAM_TOKEN and TELEGRAM_CHATID),
171
+ "tmp_dir": str(TMP_DIR),
172
+ })
173
+
174
  @app.route("/ask", methods=["POST"])
175
+ def route_ask():
176
+ """
177
+ POST JSON: { "text": "...", "lang": "vi"|"en"|"auto" (optional) }
178
+ Returns: { "answer": "..." }
179
+ """
180
+ try:
181
+ data = request.get_json(force=True) or {}
182
+ text = (data.get("text") or "").strip()
183
+ lang = (data.get("lang") or "auto").lower()
184
+ if not text:
185
+ return jsonify({"error": "no text"}), 400
186
+
187
+ # build bilingual instruction
188
+ if lang == "vi":
189
+ prompt = f"Bạn là trợ lý thông minh, trả lời bằng tiếng Việt, ngắn gọn và lịch sự:\n\n{text}"
190
+ elif lang == "en":
191
+ prompt = f"You are a helpful assistant. Answer in clear English, concise:\n\n{text}"
192
+ else:
193
+ prompt = f"Bạn là trợ lý thông minh song ngữ (Vietnamese/English). Trả lời bằng ngôn ngữ phù hợp với câu hỏi:\n\n{text}"
194
+
195
+ answer = hf_text_generate(prompt)
196
+ # store conversation and display preview
197
+ CONV.append((text, answer))
198
+ push_display("YOU: " + (text[:40]))
199
+ push_display("BOT: " + (answer[:40]))
200
+ return jsonify({"answer": answer})
201
+ except Exception as e:
202
+ logger.exception("route_ask failed")
203
+ return jsonify({"error": str(e)}), 500
204
 
205
  @app.route("/tts", methods=["POST"])
206
+ def route_tts():
207
+ """
208
+ POST JSON: { "text":"..." }
209
+ Returns: audio bytes (audio/mpeg) - HF TTS output (mp3/wav)
210
+ """
211
+ try:
212
+ data = request.get_json(force=True) or {}
213
+ text = (data.get("text") or "").strip()
214
+ if not text:
215
+ return jsonify({"error": "no text"}), 400
216
+ audio_bytes = hf_tts_get_audio_bytes(text)
217
+ # Try to detect extension: if content-type present? HF sometimes returns mp3 bytes.
218
+ # We'll send as audio/mpeg (mp3) which is widely supported by ESP32 players.
219
+ return send_file(io.BytesIO(audio_bytes), mimetype="audio/mpeg", as_attachment=False, download_name="tts.mp3")
220
+ except Exception as e:
221
+ logger.exception("route_tts failed")
222
+ return jsonify({"error": str(e)}), 500
223
 
224
  @app.route("/stt", methods=["POST"])
225
+ def route_stt():
226
+ """
227
+ Accepts multipart 'file' or raw audio bytes in body.
228
+ Returns JSON: { "text": "recognized text" }
229
+ """
230
+ try:
231
+ if "file" in request.files:
232
+ f = request.files["file"]
233
+ audio_bytes = f.read()
234
+ else:
235
+ audio_bytes = request.get_data() or b""
236
+ if not audio_bytes:
237
+ return jsonify({"error": "no audio"}), 400
238
+ text = hf_stt_from_bytes(audio_bytes)
239
+ push_display("UserAudio: " + (text[:40]))
240
+ return jsonify({"text": text})
241
+ except Exception as e:
242
+ logger.exception("route_stt failed")
243
+ return jsonify({"error": str(e)}), 500
244
 
245
  @app.route("/presence", methods=["POST"])
246
+ def route_presence():
247
+ """
248
+ ESP32 radar posts: JSON {"note": "..." }
249
+ Server responds with greeting, and optionally sends Telegram alert.
250
+ """
251
+ try:
252
+ data = request.get_json(force=True) or {}
253
+ note = data.get("note", "Có người tới")
254
+ greeting = f"Xin chào! {note}"
255
+ CONV.append(("__presence__", greeting))
256
+ push_display("RADAR: " + note[:40])
257
+ # Telegram notify
258
+ if TELEGRAM_TOKEN and TELEGRAM_CHATID:
259
+ try:
260
+ send_telegram_message(f"⚠️ Robot: Phát hiện: {note}")
261
+ except Exception:
262
+ logger.exception("Telegram notify failed")
263
+ return jsonify({"greeting": greeting})
264
+ except Exception as e:
265
+ logger.exception("route_presence failed")
266
+ return jsonify({"error": str(e)}), 500
267
+
268
+ @app.route("/display", methods=["GET"])
269
+ def route_display():
270
+ return jsonify({"lines": DISPLAY_LINES[-6:], "conv_len": len(CONV)})
271
+
272
+ # Serve tts files by filename if needed
273
+ @app.route("/tts_file/<path:fname>", methods=["GET"])
274
+ def serve_tts_file(fname):
275
+ p = TMP_DIR / fname
276
+ if not p.exists():
277
+ return abort(404)
278
+ # guess mime
279
+ mime = "audio/mpeg" if str(fname).lower().endswith(".mp3") else "audio/wav"
280
+ return send_file(str(p), mimetype=mime)
281
+
282
+ # ----------------- Simple Web UI for testing -----------------
283
+ INDEX_HTML = """
284
+ <!doctype html>
285
+ <html>
286
+ <head>
287
+ <meta charset="utf-8">
288
+ <title>KC Robot AI V4.1</title>
289
+ <meta name="viewport" content="width=device-width,initial-scale=1">
290
+ <style>
291
+ body{font-family:Arial,Helvetica, sans-serif; margin:12px; color:#111}
292
+ textarea{width:100%; height:90px; padding:8px; font-size:16px}
293
+ #chat{border:1px solid #ddd; padding:8px; height:260px; overflow:auto; background:#fbfbfb}
294
+ button{padding:8px 12px; margin-top:8px; font-size:15px}
295
+ </style>
296
+ </head>
297
+ <body>
298
+ <h2>KC Robot AI V4.1 — Cloud Brain (FPT female)</h2>
299
+ <div id="chat"></div>
300
+ <textarea id="txt" placeholder="Nhập tiếng Việt hoặc English..."></textarea><br>
301
+ <button onclick="ask()">Gửi (Ask)</button>
302
+ <button onclick="playLast()">Phát TTS</button>
303
+ <hr/>
304
+ <input type="file" id="afile" accept="audio/*"><button onclick="uploadAudio()">Upload audio → STT</button>
305
+ <hr/>
306
+ <div id="log"></div>
307
+ <script>
308
+ window._lastAnswer = "";
309
+ async function ask(){
310
+ let t = document.getElementById('txt').value;
311
+ if(!t) return;
312
+ appendUser(t);
313
+ let res = await fetch('/ask', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({text:t})});
314
+ let j = await res.json();
315
+ if(j.answer){ appendBot(j.answer); window._lastAnswer = j.answer; }
316
+ else appendBot('[Error] ' + JSON.stringify(j));
317
+ }
318
+ function appendUser(t){ document.getElementById('chat').innerHTML += '<div style="color:#006"><b>You:</b> '+escapeHtml(t)+'</div>'; scroll();}
319
+ function appendBot(t){ document.getElementById('chat').innerHTML += '<div style="color:#080"><b>Robot:</b> '+escapeHtml(t)+'</div>'; scroll();}
320
+ function escapeHtml(s){ return (s+'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
321
+ function scroll(){ let c = document.getElementById('chat'); c.scrollTop = c.scrollHeight; }
322
+ async function playLast(){
323
+ const txt = window._lastAnswer || document.getElementById('txt').value;
324
+ if(!txt) return alert('Chưa có câu trả lời');
325
+ let r = await fetch('/tts',{method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({text: txt})});
326
+ if(!r.ok) return alert('TTS lỗi');
327
+ const b = await r.blob();
328
+ const url = URL.createObjectURL(b);
329
+ const a = new Audio(url);
330
+ a.play();
331
+ }
332
+ async function uploadAudio(){
333
+ const f = document.getElementById('afile').files[0];
334
+ if(!f) return alert('Chọn file audio');
335
+ const fd = new FormData(); fd.append('file', f);
336
+ const r = await fetch('/stt', {method:'POST', body: fd});
337
+ const j = await r.json();
338
+ if(j.text) appendUser('[voice] '+j.text);
339
+ else appendUser('[stt error] '+JSON.stringify(j));
340
+ }
341
+ </script>
342
+ </body>
343
+ </html>
344
  """
345
+ @app.route("/", methods=["GET"])
346
+ def index():
347
+ return render_template_string(INDEX_HTML)
348
+
349
+ # ----------------- Telegram helpers & poller -----------------
350
+ def send_telegram_message(text: str) -> bool:
351
+ if not TELEGRAM_TOKEN or not TELEGRAM_CHATID:
352
+ logger.debug("Telegram not configured")
353
+ return False
354
+ try:
355
+ url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage"
356
+ r = requests.post(url, json={"chat_id": TELEGRAM_CHATID, "text": text}, timeout=10)
357
+ if not r.ok:
358
+ logger.warning("Telegram send failed: %s %s", r.status_code, r.text)
359
+ return False
360
+ return True
361
+ except Exception:
362
+ logger.exception("send_telegram_message exception")
363
+ return False
364
+
365
+ def telegram_poll_loop():
366
+ """Long-polling loop to fetch updates and respond to simple commands."""
367
+ if not TELEGRAM_TOKEN:
368
+ logger.info("telegram_poll_loop: TELEGRAM_TOKEN not set, exiting poller.")
369
+ return
370
+ base = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}"
371
+ offset = None
372
+ logger.info("telegram_poll_loop: starting.")
373
+ while True:
374
+ try:
375
+ params = {"timeout": 30}
376
+ if offset:
377
+ params["offset"] = offset
378
+ r = requests.get(base + "/getUpdates", params=params, timeout=35)
379
+ if not r.ok:
380
+ logger.warning("telegram getUpdates failed: %s", r.status_code)
381
+ time.sleep(2)
382
+ continue
383
+ j = r.json()
384
+ for upd in j.get("result", []):
385
+ offset = upd["update_id"] + 1
386
+ msg = upd.get("message") or {}
387
+ chat = msg.get("chat", {})
388
+ chat_id = chat.get("id")
389
+ text = (msg.get("text") or "").strip()
390
+ if not text:
391
+ continue
392
+ logger.info("TG msg %s: %s", chat_id, text)
393
+ lower = text.lower()
394
+ if lower.startswith("/ask "):
395
+ q = text[5:].strip()
396
+ try:
397
+ ans = hf_text_generate(q)
398
+ except Exception as e:
399
+ ans = f"[HF error] {e}"
400
+ try:
401
+ requests.post(base + "/sendMessage", json={"chat_id": chat_id, "text": ans}, timeout=10)
402
+ except Exception:
403
+ logger.exception("tg reply failed")
404
+ elif lower.startswith("/say "):
405
+ tts_text = text[5:].strip()
406
+ try:
407
+ audio = hf_tts_get_audio_bytes(tts_text)
408
+ files = {"audio": ("reply.mp3", audio, "audio/mpeg")}
409
+ requests.post(base + "/sendAudio", files=files, data={"chat_id": chat_id}, timeout=30)
410
+ except Exception:
411
+ logger.exception("tg say failed")
412
+ elif lower.startswith("/status"):
413
+ try:
414
+ requests.post(base + "/sendMessage", json={"chat_id": chat_id, "text": "KC Robot AI is running."}, timeout=10)
415
+ except Exception:
416
+ pass
417
+ else:
418
+ try:
419
+ requests.post(base + "/sendMessage", json={"chat_id": chat_id, "text": "Commands: /ask <q> | /say <text> | /status"}, timeout=10)
420
+ except Exception:
421
+ pass
422
+ except Exception:
423
+ logger.exception("telegram_poll_loop exception, sleeping 3s")
424
+ time.sleep(3)
425
+
426
+ def start_background_tasks():
427
+ # start telegram poller thread (if token provided)
428
+ if TELEGRAM_TOKEN:
429
+ t = threading.Thread(target=telegram_poll_loop, daemon=True)
430
+ t.start()
431
+ logger.info("Started Telegram poller thread.")
432
+ else:
433
+ logger.info("Telegram token not provided; poller disabled.")
434
+
435
+ @app.before_first_request
436
+ def _startup():
437
+ start_background_tasks()
438
 
439
+ # ----------------- Run -----------------
440
  if __name__ == "__main__":
441
+ logger.info("Starting KC Robot AI V4.1 (FPT female TTS).")
442
+ start_background_tasks()
443
+ app.run(host="0.0.0.0", port=PORT)