rakib72642 commited on
Commit
d4d01e4
·
1 Parent(s): 77a79ae

checkpoint 3

Browse files
Files changed (2) hide show
  1. core/backend.py +142 -16
  2. frontend/script.js +40 -63
core/backend.py CHANGED
@@ -11,6 +11,7 @@ from datetime import datetime, timedelta
11
  from dotenv import load_dotenv
12
  import re
13
  import traceback
 
14
 
15
  from langchain_core.messages import (
16
  AIMessage, AIMessageChunk, HumanMessage, RemoveMessage,
@@ -208,15 +209,23 @@ TOOL_INTENT_WORDS = (
208
  )
209
 
210
  SPECIALTY_ALIASES = {
211
- "চক্ষু": ["ophthalmologist", "eye specialist", "eye doctor", "ophthalmology", "eye"],
212
- "আই": ["ophthalmologist", "eye specialist", "eye doctor", "ophthalmology", "eye"],
213
- "চোখ": ["ophthalmologist", "eye specialist", "eye doctor", "ophthalmology", "eye"],
214
- "হৃদরোগ": ["cardiologist", "heart", "cardio", "cardiology"],
215
- "কার্ডিও": ["cardiologist", "heart", "cardio", "cardiology"],
 
 
 
 
 
216
  "মেডিসিন": ["medicine", "internal medicine", "physician", "general medicine"],
217
- "নিউরো": ["neurologist", "neurology", "brain"],
218
- "স্নায়ু": ["neurologist", "neurology", "brain"],
219
- "নিউরোলজি": ["neurologist", "neurology", "neorology", "neuro"],
 
 
 
220
  "নাক": ["ent", "otolaryngologist", "ear nose throat"],
221
  "কান": ["ent", "otolaryngologist", "ear nose throat"],
222
  "গলা": ["ent", "otolaryngologist", "ear nose throat"],
@@ -224,27 +233,49 @@ SPECIALTY_ALIASES = {
224
  "স্কিন": ["dermatologist", "skin", "dermatology"],
225
  "ডেন্টাল": ["dentist", "dental", "teeth"],
226
  "দাঁত": ["dentist", "dental", "teeth"],
 
227
  "গাইনী": ["gynecologist", "gynaecologist", "obgyn", "women"],
228
  "মহিলা": ["gynecologist", "gynaecologist", "obgyn", "women"],
229
- "শিশু": ["pediatrician", "child", "children"],
230
- "পেডিয়াট্রিক": ["pediatrician", "child", "children"],
231
- "অর্থো": ["orthopedic", "orthopaedic", "bone"],
232
- "হাড়": ["orthopedic", "orthopaedic", "bone"],
 
 
 
 
 
 
 
233
  "বক্ষ": ["chest", "pulmonologist", "respiratory"],
234
  "শ্বাস": ["pulmonologist", "respiratory", "chest"],
235
  "কিডনি": ["nephrologist", "kidney", "renal"],
236
- "গ্যাস্ট্রো": ["gastroenterologist", "stomach", "digestive"],
237
- "পেট": ["gastroenterologist", "stomach", "digestive"],
 
 
 
238
  # DB category uses "Gastrologist" in some datasets; include common spellings.
239
  "গ্যাস্ট্রোএন্টারোলজি": [
240
  "gastrologist",
241
  "gastroenterologist",
242
  "gastroenterology",
243
  "gastrology",
244
- "gastro",
245
  ],
246
  }
247
 
 
 
 
 
 
 
 
 
 
 
 
248
 
249
  def _normalize_day(term: str) -> str:
250
  raw = _clean_text(term)
@@ -307,6 +338,74 @@ def _expand_search_terms(text: str) -> list[str]:
307
  return sorted(terms)
308
 
309
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  def _parse_visit_date(text: str) -> Optional[str]:
311
  """
312
  Parse a user-facing date into YYYY-MM-DD in Bangladesh time.
@@ -417,6 +516,8 @@ def _looks_like_tool_turn(text: str) -> bool:
417
  if not lowered:
418
  return False
419
  return any(token.lower() in lowered for token in TOOL_INTENT_WORDS) or any(
 
 
420
  token.lower() in lowered for token in BOOKING_CONFIRM_WORDS
421
  )
422
 
@@ -690,6 +791,9 @@ async def find_doctors(query: str = "", visiting_day: str = "") -> str:
690
  cursor = await db.execute(sql, params)
691
  rows = await cursor.fetchall()
692
 
 
 
 
693
  if not rows:
694
  return json.dumps({
695
  "success": False,
@@ -754,6 +858,10 @@ async def search_doctor(
754
  cursor = await db.execute(query, params)
755
  rows = await cursor.fetchall()
756
 
 
 
 
 
757
  if not rows:
758
  return json.dumps({"success": False, "message": "No doctors found.", "data": []}, ensure_ascii=False)
759
 
@@ -1345,6 +1453,10 @@ UPDATE / CANCEL FLOW (important):
1345
  TOOL RULES:
1346
  - Use `find_doctors` first for doctor search, specialty search, and availability search.
1347
  - Use `get_doctors_by_day` or `get_categories_by_day` when the user asks about a day directly.
 
 
 
 
1348
  - Use `book_appointment` only after identifying the doctor and required patient details.
1349
  - Use `update_appointment` when the user wants to change an existing appointment.
1350
  - Never invent `doctor_id`. Get it from tool results or resolve by doctor_name/category.
@@ -1361,12 +1473,24 @@ LANGUAGE RULE
1361
  - Number & Format Rules:
1362
  - Show numbers in Bangla digits (০-৯) when responding in Bangla.
1363
  - Avoid mixing English digits in Bangla sentences unless required technically.
1364
- - Time & Date Format (spoken Bangla style):
1365
  - Use natural spoken expressions:
1366
  - "দশটা ২৮ মিনিট"
1367
  - "চারটা বেজে তিরিশ মিনিট"
1368
  - "এখন টাইম হচ্ছে সাতটা তিরিশ"
1369
 
 
 
 
 
 
 
 
 
 
 
 
 
1370
  - Year Format (spoken Bangla style):
1371
  - "দুই হাজার পঁচিশ সাল"
1372
  - "উনিশশো একাত্তর সাল"
@@ -1419,6 +1543,8 @@ The previous assistant turn failed to use a tool even though the user intent is
1419
  You must now choose the correct tool instead of answering in prose:
1420
  - Use `find_doctors` or `search_doctor` for doctor/specialty/availability questions.
1421
  - Use `get_doctors_by_day` or `get_categories_by_day` for day-based availability.
 
 
1422
  - Use `book_appointment` when the user is confirming a booking.
1423
  - Use `update_appointment` when the user wants to update an appointment.
1424
  - Use `delete_appointment` when the user is cancelling a booking.
 
11
  from dotenv import load_dotenv
12
  import re
13
  import traceback
14
+ from difflib import SequenceMatcher
15
 
16
  from langchain_core.messages import (
17
  AIMessage, AIMessageChunk, HumanMessage, RemoveMessage,
 
209
  )
210
 
211
  SPECIALTY_ALIASES = {
212
+ "চক্ষু": ["eye specialist", "ophthalmologist", "ophthalmology", "eye doctor", "eye"],
213
+ "আই": ["eye specialist", "ophthalmologist", "ophthalmology", "eye doctor", "eye"],
214
+ "চোখ": ["eye specialist", "ophthalmologist", "ophthalmology", "eye doctor", "eye"],
215
+ "eye": ["eye specialist", "ophthalmologist", "ophthalmology", "eye doctor"],
216
+ "eye specialist": ["eye specialist", "ophthalmologist", "ophthalmology", "eye doctor", "eye"],
217
+ "ophthalmology": ["eye specialist", "ophthalmologist", "ophthalmology", "eye doctor", "eye"],
218
+ "হৃদরোগ": ["cardiologist", "cardiology", "heart specialist", "heart", "cardio"],
219
+ "কার্ডিও": ["cardiologist", "cardiology", "heart specialist", "heart", "cardio"],
220
+ "cardio": ["cardiologist", "cardiology", "heart specialist", "heart"],
221
+ "cardiology": ["cardiologist", "cardiology", "heart specialist", "heart"],
222
  "মেডিসিন": ["medicine", "internal medicine", "physician", "general medicine"],
223
+ "নিউরো": ["neurologist", "neurology", "neuro", "brain specialist", "brain"],
224
+ "স্নায়ু": ["neurologist", "neurology", "neuro", "brain specialist", "brain"],
225
+ "নিউরোলজি": ["neurologist", "neurology", "neorology", "neuro", "brain specialist", "brain"],
226
+ "neuro": ["neurologist", "neurology", "brain specialist", "brain"],
227
+ "neurology": ["neurologist", "neurology", "brain specialist", "brain"],
228
+ "neurologist": ["neurologist", "neurology", "brain specialist", "brain"],
229
  "নাক": ["ent", "otolaryngologist", "ear nose throat"],
230
  "কান": ["ent", "otolaryngologist", "ear nose throat"],
231
  "গলা": ["ent", "otolaryngologist", "ear nose throat"],
 
233
  "স্কিন": ["dermatologist", "skin", "dermatology"],
234
  "ডেন্টাল": ["dentist", "dental", "teeth"],
235
  "দাঁত": ["dentist", "dental", "teeth"],
236
+ "dentist": ["dentist", "dental", "teeth"],
237
  "গাইনী": ["gynecologist", "gynaecologist", "obgyn", "women"],
238
  "মহিলা": ["gynecologist", "gynaecologist", "obgyn", "women"],
239
+ "শিশু": ["child specialist", "pediatrician", "pediatrics", "child doctor", "children"],
240
+ "পেডিয়াট্রিক": ["child specialist", "pediatrician", "pediatrics", "child doctor", "children"],
241
+ "child": ["child specialist", "pediatrician", "pediatrics", "child doctor", "children"],
242
+ "child specialist": ["child specialist", "pediatrician", "pediatrics", "child doctor", "children"],
243
+ "pediatrician": ["child specialist", "pediatrician", "pediatrics", "child doctor", "children"],
244
+ "pediatrics": ["child specialist", "pediatrician", "pediatrics", "child doctor", "children"],
245
+ "অর্থো": ["orthopedics", "orthopedic", "orthopaedic", "orthopaedics", "ortho", "bone"],
246
+ "হাড়": ["orthopedics", "orthopedic", "orthopaedic", "orthopaedics", "ortho", "bone"],
247
+ "orthopedics": ["orthopedics", "orthopedic", "orthopaedic", "orthopaedics", "ortho", "bone"],
248
+ "orthopedic": ["orthopedics", "orthopedic", "orthopaedic", "orthopaedics", "ortho", "bone"],
249
+ "orthopaedic": ["orthopedics", "orthopedic", "orthopaedic", "orthopaedics", "ortho", "bone"],
250
  "বক্ষ": ["chest", "pulmonologist", "respiratory"],
251
  "শ্বাস": ["pulmonologist", "respiratory", "chest"],
252
  "কিডনি": ["nephrologist", "kidney", "renal"],
253
+ "gastro": ["gastrologist", "gastroenterologist", "gastroenterology", "gastro specialist", "stomach", "digestive"],
254
+ "gastroenterology": ["gastrologist", "gastroenterologist", "gastroenterology", "gastro specialist", "stomach", "digestive"],
255
+ "gastrologist": ["gastrologist", "gastroenterologist", "gastroenterology", "gastro specialist", "stomach", "digestive"],
256
+ "গ্যাস্ট্রো": ["gastrologist", "gastroenterologist", "gastroenterology", "gastro specialist", "stomach", "digestive"],
257
+ "পেট": ["gastrologist", "gastroenterologist", "gastroenterology", "gastro specialist", "stomach", "digestive"],
258
  # DB category uses "Gastrologist" in some datasets; include common spellings.
259
  "গ্যাস্ট্রোএন্টারোলজি": [
260
  "gastrologist",
261
  "gastroenterologist",
262
  "gastroenterology",
263
  "gastrology",
264
+ "gastro",
265
  ],
266
  }
267
 
268
+ SPECIALTY_INTENT_WORDS = {
269
+ "cardiologist", "cardiology", "neurologist", "neurology", "orthopedics",
270
+ "orthopedic", "orthopaedic", "orthopaedics", "gastrologist",
271
+ "gastroenterologist", "gastroenterology", "dentist", "eye specialist",
272
+ "eye doctor", "ophthalmologist", "ophthalmology", "child specialist",
273
+ "pediatrician", "pediatrics", "ent", "nephrologist", "pulmonologist",
274
+ "dermatologist", "gynecologist", "gynaecologist",
275
+ "কার্ডিও", "হৃদরোগ", "নিউরো", "স্নায়ু", "অর্থো", "হাড়", "গ্যাস্ট্রো",
276
+ "চক্ষু", "চোখ", "আই", "শিশু", "পেডিয়াট্রিক", "দাঁত", "ডেন্টাল",
277
+ }
278
+
279
 
280
  def _normalize_day(term: str) -> str:
281
  raw = _clean_text(term)
 
338
  return sorted(terms)
339
 
340
 
341
+ def _normalize_lookup_text(text: str) -> str:
342
+ raw = _clean_text(text).lower()
343
+ if not raw:
344
+ return ""
345
+ raw = raw.replace("ডাঃ", "").replace("ডা.", "").replace("dr.", "").replace("dr", "")
346
+ raw = re.sub(r"[^0-9a-z\u0980-\u09ff]+", "", raw)
347
+ return raw
348
+
349
+
350
+ def _doctor_search_score(row: dict, terms: list[str], day_text: str = "") -> float:
351
+ name = _clean_text(row.get("doctor_name", "")).lower()
352
+ category = _clean_text(row.get("category", "")).lower()
353
+ days = _clean_text(row.get("visiting_days", "")).lower()
354
+ haystacks = [
355
+ name,
356
+ category,
357
+ days,
358
+ _normalize_lookup_text(name),
359
+ _normalize_lookup_text(category),
360
+ _normalize_lookup_text(days),
361
+ ]
362
+
363
+ score = 0.0
364
+ if day_text and day_text.lower() in days:
365
+ score += 3.0
366
+
367
+ for term in terms:
368
+ norm_term = _normalize_lookup_text(term)
369
+ if not norm_term:
370
+ continue
371
+ if any(norm_term in hay for hay in haystacks if hay):
372
+ score += 3.0
373
+ continue
374
+ best = 0.0
375
+ for hay in haystacks:
376
+ if not hay:
377
+ continue
378
+ best = max(best, SequenceMatcher(None, norm_term, hay).ratio())
379
+ if best >= 0.72:
380
+ score += 1.5
381
+ elif best >= 0.62:
382
+ score += 0.5
383
+
384
+ return score
385
+
386
+
387
+ async def _fallback_doctor_search(
388
+ db_path: str,
389
+ terms: list[str],
390
+ day_text: str = "",
391
+ limit: int = 10,
392
+ ) -> list[dict]:
393
+ async with aiosqlite.connect(db_path) as db:
394
+ db.row_factory = aiosqlite.Row
395
+ cursor = await db.execute("SELECT * FROM doctors")
396
+ rows = await cursor.fetchall()
397
+
398
+ scored: list[tuple[float, dict]] = []
399
+ for row in rows:
400
+ row_dict = dict(row)
401
+ score = _doctor_search_score(row_dict, terms, day_text=day_text)
402
+ if score > 0:
403
+ scored.append((score, row_dict))
404
+
405
+ scored.sort(key=lambda item: (-item[0], _clean_text(item[1].get("doctor_name", ""))))
406
+ return [row for _, row in scored[:limit]]
407
+
408
+
409
  def _parse_visit_date(text: str) -> Optional[str]:
410
  """
411
  Parse a user-facing date into YYYY-MM-DD in Bangladesh time.
 
516
  if not lowered:
517
  return False
518
  return any(token.lower() in lowered for token in TOOL_INTENT_WORDS) or any(
519
+ token.lower() in lowered for token in SPECIALTY_INTENT_WORDS
520
+ ) or any(
521
  token.lower() in lowered for token in BOOKING_CONFIRM_WORDS
522
  )
523
 
 
791
  cursor = await db.execute(sql, params)
792
  rows = await cursor.fetchall()
793
 
794
+ if not rows:
795
+ rows = await _fallback_doctor_search(db_path, terms, day_text=day_text)
796
+
797
  if not rows:
798
  return json.dumps({
799
  "success": False,
 
858
  cursor = await db.execute(query, params)
859
  rows = await cursor.fetchall()
860
 
861
+ if not rows:
862
+ fallback_terms = sorted(set(name_terms + category_terms + ([day_text] if day_text else [])))
863
+ rows = await _fallback_doctor_search(db_path, fallback_terms, day_text=day_text)
864
+
865
  if not rows:
866
  return json.dumps({"success": False, "message": "No doctors found.", "data": []}, ensure_ascii=False)
867
 
 
1453
  TOOL RULES:
1454
  - Use `find_doctors` first for doctor search, specialty search, and availability search.
1455
  - Use `get_doctors_by_day` or `get_categories_by_day` when the user asks about a day directly.
1456
+ - If the user only says a specialty, doctor type, or availability phrase like
1457
+ "Neurologist", "cardiologist", "eye specialist", "child specialist",
1458
+ "orthopedics", "নিউরোলজি", "চক্ষু", or "শিশু", treat it as a doctor search
1459
+ request and call a tool instead of answering from memory.
1460
  - Use `book_appointment` only after identifying the doctor and required patient details.
1461
  - Use `update_appointment` when the user wants to change an existing appointment.
1462
  - Never invent `doctor_id`. Get it from tool results or resolve by doctor_name/category.
 
1473
  - Number & Format Rules:
1474
  - Show numbers in Bangla digits (০-৯) when responding in Bangla.
1475
  - Avoid mixing English digits in Bangla sentences unless required technically.
1476
+ - Time Format (spoken Bangla style):
1477
  - Use natural spoken expressions:
1478
  - "দশটা ২৮ মিনিট"
1479
  - "চারটা বেজে তিরিশ মিনিট"
1480
  - "এখন টাইম হচ্ছে সাতটা তিরিশ"
1481
 
1482
+
1483
+ - Date Format (spoken Bangla style):
1484
+ - Use natural spoken expressions:
1485
+ - "আজকে বারোই নভেম্বর।"
1486
+ - "আজকে পাঁচই মার্চ।"
1487
+ - "আজকে বাইশেই জুন।"
1488
+ - "জানুয়ারি মাসের আঠারো তারিখ"
1489
+ - "ফেব্রুয়ারি মাসের ছয় তারিখ"
1490
+ - "সেপ্টেম্বরের চার তারিখ"
1491
+ - "মে মাসের বিশ তারিখ"
1492
+
1493
+
1494
  - Year Format (spoken Bangla style):
1495
  - "দুই হাজার পঁচিশ সাল"
1496
  - "উনিশশো একাত্তর সাল"
 
1543
  You must now choose the correct tool instead of answering in prose:
1544
  - Use `find_doctors` or `search_doctor` for doctor/specialty/availability questions.
1545
  - Use `get_doctors_by_day` or `get_categories_by_day` for day-based availability.
1546
+ - If the user says only a specialty or doctor type, or asks which doctors are
1547
+ available, call a search tool immediately. Do not answer from memory.
1548
  - Use `book_appointment` when the user is confirming a booking.
1549
  - Use `update_appointment` when the user wants to update an appointment.
1550
  - Use `delete_appointment` when the user is cancelling a booking.
frontend/script.js CHANGED
@@ -107,7 +107,7 @@ let _currentTurn = 0;
107
  // Client-side playback speed multiplier.
108
  // This makes speech faster immediately even if the TTS provider speed setting
109
  // is limited/ignored. 1.0 = normal, >1.0 = faster.
110
- let TTS_PLAYBACK_RATE = 1.025;
111
  let brainMode = false;
112
  let brainVoiceActive = false;
113
  let brainRestartTimer = null;
@@ -117,64 +117,41 @@ let voicePendingPackets = [];
117
  let brainLastResponse = '';
118
  let _brainWelcomed = false;
119
 
120
- const SPOKEN_DIGIT_WORDS = {
121
- '0': 'শূন্য',
122
- '1': 'এক',
123
- '2': 'দুই',
124
- '3': 'তিন',
125
- '4': 'চার',
126
- '5': 'পাঁচ',
127
- '6': 'ছয়',
128
- '7': 'সাত',
129
- '8': 'আট',
130
- '9': 'নয়',
131
- '': 'শূন্য',
132
- '': 'এক',
133
- '': 'দুই',
134
- '': 'তিন',
135
- '': 'চার',
136
- '': 'পাঁচ',
137
- '': 'ছয়',
138
- '': 'সাত',
139
- '': 'আট',
140
- '': 'নয়',
141
- '٠': 'শূন্য',
142
- '١': 'এক',
143
- '٢': 'দুই',
144
- '٣': 'তিন',
145
- '٤': 'চার',
146
- '٥': 'পাঁচ',
147
- '٦': 'ছয়',
148
- '٧': 'সাত',
149
- '٨': 'আট',
150
- '٩': 'নয়',
151
  };
152
 
153
- function _spokenDigitWords(chunk) {
154
- const digits = Array.from(chunk).filter((ch) => ch in SPOKEN_DIGIT_WORDS);
155
- if (digits.length < 10) return chunk;
156
- return digits.map((ch) => SPOKEN_DIGIT_WORDS[ch]).join(' ');
 
157
  }
158
 
159
  function _normalizeVisibleAiText(text) {
160
  if (!text) return '';
161
- let out = String(text)
162
- .replaceAll('উপলব্ধ', 'এভেলেবেল')
163
- .replaceAll('জ্বি', 'আচ্ছা');
164
- out = out.replace(
165
- /[+\d০-৯٠-٩][\d০-৯٠-٩\s().\-]{8,}[\d০-৯٠-٩]/g,
166
- (match, offset, whole) => {
167
- const spoken = _spokenDigitWords(match);
168
- if (spoken === match) return match;
169
- const prev = offset > 0 ? whole[offset - 1] : '';
170
- const next = offset + match.length < whole.length ? whole[offset + match.length] : '';
171
- let value = spoken;
172
- if (prev && !/\s/.test(prev) && !/[([<{\"']/.test(prev)) value = ' ' + value;
173
- if (next && !/\s/.test(next) && !/[\])>.,!?;:}\"']/.test(next)) value = value + ' ';
174
- return value;
175
- },
176
  );
177
- return out.replace(/[ \t]{2,}/g, ' ');
178
  }
179
 
180
  const BRAIN_WELCOME_TEXT =
@@ -376,12 +353,10 @@ function onVoiceMsg(ev) {
376
  // We buffer/reorder by seq inside a turn, and ignore late packets from older turns.
377
  const u8 = new Uint8Array(ev.data);
378
  if (u8.length <= 8) return;
379
- const turn =
380
- (u8[0] << 24) | (u8[1] << 16) | (u8[2] << 8) | (u8[3] << 0);
381
- const seq =
382
- (u8[4] << 24) | (u8[5] << 16) | (u8[6] << 8) | (u8[7] << 0);
383
  const turnU = turn >>> 0;
384
- if (turnU !== (_currentTurn >>> 0)) return;
385
  const payload = ev.data.slice(8);
386
  _pendingAudio.set(seq >>> 0, payload);
387
 
@@ -553,7 +528,7 @@ function _renderAiText(force = false) {
553
  }
554
 
555
  function _setCaption(text) {
556
- _captionText = text || '';
557
  if (_captionRaf) return;
558
  _captionRaf = requestAnimationFrame(() => {
559
  _captionRaf = 0;
@@ -1229,10 +1204,11 @@ function appendMsg(text, who) {
1229
  if (brainMode && who === 'user') return null;
1230
  const d = document.createElement('div');
1231
  d.className = 'message ' + who;
 
1232
  if (who === 'ai' && typeof marked !== 'undefined') {
1233
- d.innerHTML = marked.parse(text || '');
1234
  } else {
1235
- d.textContent = text;
1236
  }
1237
  chatBox.appendChild(d);
1238
  chatBox.scrollTop = chatBox.scrollHeight;
@@ -1266,7 +1242,8 @@ function setBrainMode(on) {
1266
  if (voiceCaption) voiceCaption.textContent = '';
1267
  if (brainMode) {
1268
  brainBubbleSttText.textContent = 'Listening…';
1269
- brainBubbleTtsText.textContent = brainLastResponse || 'Waiting…';
 
1270
  brainVoiceActive = true;
1271
  sidebarEl.classList.add('collapsed');
1272
  sidebarToggle.textContent = '›';
@@ -1336,14 +1313,14 @@ function _brainModeSetSearch(active) {
1336
 
1337
  function _brainSetSttBubble(text) {
1338
  if (!brainBubbleStt || !brainBubbleSttText) return;
1339
- const value = (text || '').trim();
1340
  brainBubbleSttText.textContent = value || 'Listening…';
1341
  brainBubbleStt.classList.toggle('active', !!value);
1342
  }
1343
 
1344
  function _brainSetTtsBubble(text, active = true) {
1345
  if (!brainBubbleTts || !brainBubbleTtsText) return;
1346
- const value = (text || '').trim();
1347
  brainBubbleTtsText.textContent = value || 'Waiting…';
1348
  brainBubbleTts.classList.toggle('active', !!value || !!active);
1349
  brainBubbleTts.classList.toggle('speaking', !!active);
 
107
  // Client-side playback speed multiplier.
108
  // This makes speech faster immediately even if the TTS provider speed setting
109
  // is limited/ignored. 1.0 = normal, >1.0 = faster.
110
+ let TTS_PLAYBACK_RATE = 1.0;
111
  let brainMode = false;
112
  let brainVoiceActive = false;
113
  let brainRestartTimer = null;
 
117
  let brainLastResponse = '';
118
  let _brainWelcomed = false;
119
 
120
+ const VISIBLE_DIGIT_MAP = {
121
+ '': '0',
122
+ '': '1',
123
+ '': '2',
124
+ '': '3',
125
+ '': '4',
126
+ '': '5',
127
+ '': '6',
128
+ '': '7',
129
+ '': '8',
130
+ '': '9',
131
+ '٠': '0',
132
+ '١': '1',
133
+ '٢': '2',
134
+ '٣': '3',
135
+ '٤': '4',
136
+ '٥': '5',
137
+ '٦': '6',
138
+ '٧': '7',
139
+ '٨': '8',
140
+ '٩': '9',
 
 
 
 
 
 
 
 
 
 
141
  };
142
 
143
+ function _toAsciiDigits(text) {
144
+ return String(text || '').replace(
145
+ /[০-৯٠-٩]/g,
146
+ (ch) => VISIBLE_DIGIT_MAP[ch] || ch,
147
+ );
148
  }
149
 
150
  function _normalizeVisibleAiText(text) {
151
  if (!text) return '';
152
+ return _toAsciiDigits(
153
+ String(text).replaceAll('উপলব্ধ', 'এভেলেবেল').replaceAll('জ্বি', 'আচ্ছা'),
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  );
 
155
  }
156
 
157
  const BRAIN_WELCOME_TEXT =
 
353
  // We buffer/reorder by seq inside a turn, and ignore late packets from older turns.
354
  const u8 = new Uint8Array(ev.data);
355
  if (u8.length <= 8) return;
356
+ const turn = (u8[0] << 24) | (u8[1] << 16) | (u8[2] << 8) | (u8[3] << 0);
357
+ const seq = (u8[4] << 24) | (u8[5] << 16) | (u8[6] << 8) | (u8[7] << 0);
 
 
358
  const turnU = turn >>> 0;
359
+ if (turnU !== _currentTurn >>> 0) return;
360
  const payload = ev.data.slice(8);
361
  _pendingAudio.set(seq >>> 0, payload);
362
 
 
528
  }
529
 
530
  function _setCaption(text) {
531
+ _captionText = _normalizeVisibleAiText(text);
532
  if (_captionRaf) return;
533
  _captionRaf = requestAnimationFrame(() => {
534
  _captionRaf = 0;
 
1204
  if (brainMode && who === 'user') return null;
1205
  const d = document.createElement('div');
1206
  d.className = 'message ' + who;
1207
+ const visibleText = _normalizeVisibleAiText(text);
1208
  if (who === 'ai' && typeof marked !== 'undefined') {
1209
+ d.innerHTML = marked.parse(visibleText || '');
1210
  } else {
1211
+ d.textContent = visibleText;
1212
  }
1213
  chatBox.appendChild(d);
1214
  chatBox.scrollTop = chatBox.scrollHeight;
 
1242
  if (voiceCaption) voiceCaption.textContent = '';
1243
  if (brainMode) {
1244
  brainBubbleSttText.textContent = 'Listening…';
1245
+ brainBubbleTtsText.textContent =
1246
+ _normalizeVisibleAiText(brainLastResponse) || 'Waiting…';
1247
  brainVoiceActive = true;
1248
  sidebarEl.classList.add('collapsed');
1249
  sidebarToggle.textContent = '›';
 
1313
 
1314
  function _brainSetSttBubble(text) {
1315
  if (!brainBubbleStt || !brainBubbleSttText) return;
1316
+ const value = _normalizeVisibleAiText(text).trim();
1317
  brainBubbleSttText.textContent = value || 'Listening…';
1318
  brainBubbleStt.classList.toggle('active', !!value);
1319
  }
1320
 
1321
  function _brainSetTtsBubble(text, active = true) {
1322
  if (!brainBubbleTts || !brainBubbleTtsText) return;
1323
+ const value = _normalizeVisibleAiText(text).trim();
1324
  brainBubbleTtsText.textContent = value || 'Waiting…';
1325
  brainBubbleTts.classList.toggle('active', !!value || !!active);
1326
  brainBubbleTts.classList.toggle('speaking', !!active);