Commit ·
d4d01e4
1
Parent(s): 77a79ae
checkpoint 3
Browse files- core/backend.py +142 -16
- 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 |
-
"চক্ষু": ["
|
| 212 |
-
"আই": ["
|
| 213 |
-
"চোখ": ["
|
| 214 |
-
"
|
| 215 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
| 232 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
"বক্ষ": ["chest", "pulmonologist", "respiratory"],
|
| 234 |
"শ্বাস": ["pulmonologist", "respiratory", "chest"],
|
| 235 |
"কিডনি": ["nephrologist", "kidney", "renal"],
|
| 236 |
-
"
|
| 237 |
-
"
|
|
|
|
|
|
|
|
|
|
| 238 |
# DB category uses "Gastrologist" in some datasets; include common spellings.
|
| 239 |
"গ্যাস্ট্রোএন্টারোলজি": [
|
| 240 |
"gastrologist",
|
| 241 |
"gastroenterologist",
|
| 242 |
"gastroenterology",
|
| 243 |
"gastrology",
|
| 244 |
-
|
| 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
|
| 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.
|
| 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
|
| 121 |
-
'
|
| 122 |
-
'
|
| 123 |
-
'
|
| 124 |
-
'
|
| 125 |
-
'
|
| 126 |
-
'
|
| 127 |
-
'
|
| 128 |
-
'
|
| 129 |
-
'
|
| 130 |
-
'
|
| 131 |
-
'
|
| 132 |
-
'
|
| 133 |
-
'
|
| 134 |
-
'
|
| 135 |
-
'
|
| 136 |
-
'
|
| 137 |
-
'
|
| 138 |
-
'
|
| 139 |
-
'
|
| 140 |
-
'
|
| 141 |
-
'٠': 'শূন্য',
|
| 142 |
-
'١': 'এক',
|
| 143 |
-
'٢': 'দুই',
|
| 144 |
-
'٣': 'তিন',
|
| 145 |
-
'٤': 'চার',
|
| 146 |
-
'٥': 'পাঁচ',
|
| 147 |
-
'٦': 'ছয়',
|
| 148 |
-
'٧': 'সাত',
|
| 149 |
-
'٨': 'আট',
|
| 150 |
-
'٩': 'নয়',
|
| 151 |
};
|
| 152 |
|
| 153 |
-
function
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
| 157 |
}
|
| 158 |
|
| 159 |
function _normalizeVisibleAiText(text) {
|
| 160 |
if (!text) return '';
|
| 161 |
-
|
| 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 |
-
|
| 381 |
-
const seq =
|
| 382 |
-
(u8[4] << 24) | (u8[5] << 16) | (u8[6] << 8) | (u8[7] << 0);
|
| 383 |
const turnU = turn >>> 0;
|
| 384 |
-
if (turnU !==
|
| 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(
|
| 1234 |
} else {
|
| 1235 |
-
d.textContent =
|
| 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 =
|
|
|
|
| 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
|
| 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
|
| 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);
|