Commit ·
ac8ab2c
1
Parent(s): 5dabf9d
Add initial data insertion script for doctors into SQLite database
Browse files- Created a new Jupyter notebook to insert doctor data into the 'doctors' table.
- Included a list of doctors with their names, categories, visiting days, visiting times, and fees.
- Utilized aiosqlite for asynchronous database operations.
- app.py +2 -2
- core/backend.py +56 -40
- frontend/index.html +2 -2
- frontend/script.js +289 -206
- tmp.ipynb +164 -0
app.py
CHANGED
|
@@ -52,8 +52,8 @@ except (ImportError, RuntimeError) as _e:
|
|
| 52 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 53 |
# MODEL ROUTING CONFIG — set exactly ONE to True
|
| 54 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 55 |
-
USE_GEMINI =
|
| 56 |
-
USE_OLLAMA =
|
| 57 |
USE_LOCAL_FALLBACK = False
|
| 58 |
|
| 59 |
_active = sum([USE_GEMINI, USE_OLLAMA, USE_LOCAL_FALLBACK])
|
|
|
|
| 52 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 53 |
# MODEL ROUTING CONFIG — set exactly ONE to True
|
| 54 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 55 |
+
USE_GEMINI = True
|
| 56 |
+
USE_OLLAMA = False
|
| 57 |
USE_LOCAL_FALLBACK = False
|
| 58 |
|
| 59 |
_active = sum([USE_GEMINI, USE_OLLAMA, USE_LOCAL_FALLBACK])
|
core/backend.py
CHANGED
|
@@ -103,9 +103,12 @@ def get_bd_time() -> str:
|
|
| 103 |
return json.dumps(result)
|
| 104 |
|
| 105 |
@tool
|
| 106 |
-
async def
|
| 107 |
"""
|
| 108 |
-
Fetch
|
|
|
|
|
|
|
|
|
|
| 109 |
"""
|
| 110 |
|
| 111 |
db_path = get_db_path()
|
|
@@ -115,63 +118,82 @@ async def get_doctor_categories() -> str:
|
|
| 115 |
FROM doctors
|
| 116 |
WHERE category IS NOT NULL
|
| 117 |
AND TRIM(category) != ''
|
| 118 |
-
ORDER BY category ASC
|
| 119 |
"""
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
async with aiosqlite.connect(db_path) as db:
|
| 122 |
db.row_factory = aiosqlite.Row
|
| 123 |
-
|
| 124 |
-
cursor = await db.execute(query)
|
| 125 |
rows = await cursor.fetchall()
|
| 126 |
|
| 127 |
categories = [row["category"] for row in rows]
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
return json.dumps({
|
| 130 |
"success": True,
|
|
|
|
| 131 |
"count": len(categories),
|
| 132 |
"data": categories
|
| 133 |
-
})
|
| 134 |
|
| 135 |
@tool
|
| 136 |
-
async def get_doctors_by_day(
|
| 137 |
-
visiting_day: str,
|
| 138 |
-
) -> str:
|
| 139 |
"""
|
| 140 |
-
Get
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
-
|
|
|
|
|
|
|
| 145 |
"""
|
| 146 |
|
| 147 |
db_path = get_db_path()
|
| 148 |
|
| 149 |
query = """
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
"""
|
| 154 |
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
async with aiosqlite.connect(db_path) as db:
|
| 158 |
db.row_factory = aiosqlite.Row
|
| 159 |
-
|
| 160 |
-
cursor = await db.execute(query, param)
|
| 161 |
rows = await cursor.fetchall()
|
| 162 |
|
| 163 |
if not rows:
|
| 164 |
return json.dumps({
|
| 165 |
"success": False,
|
| 166 |
-
"message": f"No doctors found for {visiting_day}.",
|
| 167 |
"data": []
|
| 168 |
-
})
|
| 169 |
|
| 170 |
doctors = [dict(row) for row in rows]
|
| 171 |
|
| 172 |
return json.dumps({
|
| 173 |
"success": True,
|
| 174 |
-
"visiting_day": visiting_day,
|
| 175 |
"count": len(doctors),
|
| 176 |
"data": doctors
|
| 177 |
}, ensure_ascii=False)
|
|
@@ -271,6 +293,7 @@ async def book_appointment(
|
|
| 271 |
doctor_data = dict(doctor)
|
| 272 |
doctor_name = doctor_data.get("doctor_name", "Unknown")
|
| 273 |
doctor_category = doctor_data.get("category", "Unknown")
|
|
|
|
| 274 |
|
| 275 |
cursor = await db.execute(
|
| 276 |
"""SELECT id FROM patients
|
|
@@ -296,7 +319,8 @@ async def book_appointment(
|
|
| 296 |
f"Doctor : {doctor_name}\n"
|
| 297 |
f"Patient : {patient_name}\n"
|
| 298 |
f"Visit Date : {visiting_date}\n"
|
| 299 |
-
f"
|
|
|
|
| 300 |
)
|
| 301 |
try:
|
| 302 |
await send_mail(
|
|
@@ -317,7 +341,7 @@ async def book_appointment(
|
|
| 317 |
f"Date : {visiting_date}\n"
|
| 318 |
f"Contact : {patient_num}\n"
|
| 319 |
f"━━━━━━━━━━━━━━━━━━━━━━\n"
|
| 320 |
-
f"Please arrive
|
| 321 |
f"{mail_status}"
|
| 322 |
)
|
| 323 |
|
|
@@ -351,45 +375,36 @@ async def delete_appointment(patient_num: str, doctor_name: str) -> str:
|
|
| 351 |
"message": f"Appointment with Dr. {doctor_name} deleted successfully.",
|
| 352 |
})
|
| 353 |
|
| 354 |
-
|
| 355 |
# ═══════════════════════════════════════════════════════════════════════════════
|
| 356 |
# SYSTEM PROMPT
|
| 357 |
# ═══════════════════════════════════════════════════════════════════════════════
|
| 358 |
BASE_SYSTEM = """
|
| 359 |
You are a Doctor Appointment Assistant AI.
|
| 360 |
-
|
| 361 |
Your job is to help users manage medical appointments.
|
| 362 |
-
|
| 363 |
CAPABILITIES:
|
| 364 |
- Book doctor appointments
|
| 365 |
- Reschedule appointments
|
| 366 |
- Cancel appointments
|
| 367 |
- Collect patient details
|
| 368 |
-
|
| 369 |
STRICT RULES:
|
| 370 |
- You are NOT a doctor.
|
| 371 |
- NEVER diagnose diseases.
|
| 372 |
- NEVER recommend medicines or treatments.
|
| 373 |
-
|
| 374 |
APPOINTMENT FLOW:
|
| 375 |
1. Detect intent (book / cancel / reschedule / inquiry)
|
| 376 |
2. Collect details
|
| 377 |
3. Confirm all details before final booking
|
| 378 |
-
|
| 379 |
STYLE:
|
| 380 |
- Be short, clear, structured
|
| 381 |
-
- Ask one question at a time when needed
|
| 382 |
- Focus on completing booking
|
| 383 |
-
|
| 384 |
LANGUAGE RULE:
|
| 385 |
- Detect user language from latest message.
|
| 386 |
- If English → reply English.
|
| 387 |
- If Bangla → reply Bangla (বাংলা).
|
| 388 |
- If Banglish → reply Bangla (বাংলা).
|
| 389 |
- Never mix languages unless user mixes first.
|
| 390 |
-
|
| 391 |
TOOLS:
|
| 392 |
-
- Use backend tools if
|
| 393 |
- Always confirm before final action
|
| 394 |
"""
|
| 395 |
|
|
@@ -415,14 +430,14 @@ class AIBackend:
|
|
| 415 |
|
| 416 |
if use_gemini:
|
| 417 |
self.llm = ChatGoogleGenerativeAI(
|
| 418 |
-
model="gemini-2.
|
| 419 |
-
temperature=0.
|
| 420 |
)
|
| 421 |
elif use_ollama:
|
| 422 |
-
self.llm = ChatOllama(model="gemma4:e4b", streaming=True, temperature=0.
|
| 423 |
else:
|
| 424 |
# Local fallback — extend as needed
|
| 425 |
-
self.llm = ChatOllama(model="gemma4:e4b", streaming=True, temperature=0.
|
| 426 |
|
| 427 |
self.tools = [
|
| 428 |
search_doctor,
|
|
@@ -430,9 +445,10 @@ class AIBackend:
|
|
| 430 |
get_bd_time,
|
| 431 |
search_appointment_by_phone,
|
| 432 |
delete_appointment,
|
| 433 |
-
|
| 434 |
get_doctors_by_day
|
| 435 |
]
|
|
|
|
| 436 |
self.tool_node = ToolNode(self.tools)
|
| 437 |
self.llm_with_tools = self.llm.bind_tools(self.tools)
|
| 438 |
|
|
|
|
| 103 |
return json.dumps(result)
|
| 104 |
|
| 105 |
@tool
|
| 106 |
+
async def get_categories_by_day(visiting_day: str = "") -> str:
|
| 107 |
"""
|
| 108 |
+
Fetch unique doctor categories.
|
| 109 |
+
|
| 110 |
+
If visiting_day is provided → filter by that day
|
| 111 |
+
If empty → return all categories
|
| 112 |
"""
|
| 113 |
|
| 114 |
db_path = get_db_path()
|
|
|
|
| 118 |
FROM doctors
|
| 119 |
WHERE category IS NOT NULL
|
| 120 |
AND TRIM(category) != ''
|
|
|
|
| 121 |
"""
|
| 122 |
|
| 123 |
+
params = []
|
| 124 |
+
|
| 125 |
+
# Optional filter
|
| 126 |
+
if visiting_day:
|
| 127 |
+
query += " AND LOWER(visiting_days) LIKE ?"
|
| 128 |
+
params.append(f"%{visiting_day.lower()}%")
|
| 129 |
+
|
| 130 |
+
query += " ORDER BY category ASC"
|
| 131 |
+
|
| 132 |
async with aiosqlite.connect(db_path) as db:
|
| 133 |
db.row_factory = aiosqlite.Row
|
| 134 |
+
cursor = await db.execute(query, params)
|
|
|
|
| 135 |
rows = await cursor.fetchall()
|
| 136 |
|
| 137 |
categories = [row["category"] for row in rows]
|
| 138 |
|
| 139 |
+
if not categories:
|
| 140 |
+
return json.dumps({
|
| 141 |
+
"success": False,
|
| 142 |
+
"message": "No categories found.",
|
| 143 |
+
"data": []
|
| 144 |
+
}, ensure_ascii=False)
|
| 145 |
+
|
| 146 |
return json.dumps({
|
| 147 |
"success": True,
|
| 148 |
+
"visiting_day": visiting_day if visiting_day else "ALL",
|
| 149 |
"count": len(categories),
|
| 150 |
"data": categories
|
| 151 |
+
}, ensure_ascii=False)
|
| 152 |
|
| 153 |
@tool
|
| 154 |
+
async def get_doctors_by_day(visiting_day: str = "") -> str:
|
|
|
|
|
|
|
| 155 |
"""
|
| 156 |
+
Get doctors by visiting day.
|
| 157 |
+
If visiting_day is provided → filter by that day
|
| 158 |
+
If empty → return all doctors
|
| 159 |
+
Example:
|
| 160 |
+
- "Sunday"
|
| 161 |
+
- "Monday"
|
| 162 |
+
- ""
|
| 163 |
"""
|
| 164 |
|
| 165 |
db_path = get_db_path()
|
| 166 |
|
| 167 |
query = """
|
| 168 |
+
SELECT *
|
| 169 |
+
FROM doctors
|
| 170 |
+
WHERE 1=1
|
| 171 |
"""
|
| 172 |
|
| 173 |
+
params = []
|
| 174 |
+
|
| 175 |
+
# Optional filter
|
| 176 |
+
if visiting_day:
|
| 177 |
+
query += " AND LOWER(visiting_days) LIKE ?"
|
| 178 |
+
params.append(f"%{visiting_day.lower()}%")
|
| 179 |
|
| 180 |
async with aiosqlite.connect(db_path) as db:
|
| 181 |
db.row_factory = aiosqlite.Row
|
| 182 |
+
cursor = await db.execute(query, params)
|
|
|
|
| 183 |
rows = await cursor.fetchall()
|
| 184 |
|
| 185 |
if not rows:
|
| 186 |
return json.dumps({
|
| 187 |
"success": False,
|
| 188 |
+
"message": f"No doctors found for {visiting_day if visiting_day else 'ALL days'}.",
|
| 189 |
"data": []
|
| 190 |
+
}, ensure_ascii=False)
|
| 191 |
|
| 192 |
doctors = [dict(row) for row in rows]
|
| 193 |
|
| 194 |
return json.dumps({
|
| 195 |
"success": True,
|
| 196 |
+
"visiting_day": visiting_day if visiting_day else "ALL",
|
| 197 |
"count": len(doctors),
|
| 198 |
"data": doctors
|
| 199 |
}, ensure_ascii=False)
|
|
|
|
| 293 |
doctor_data = dict(doctor)
|
| 294 |
doctor_name = doctor_data.get("doctor_name", "Unknown")
|
| 295 |
doctor_category = doctor_data.get("category", "Unknown")
|
| 296 |
+
visiting_time = doctor_data.get("visiting_time", "Unknown")
|
| 297 |
|
| 298 |
cursor = await db.execute(
|
| 299 |
"""SELECT id FROM patients
|
|
|
|
| 319 |
f"Doctor : {doctor_name}\n"
|
| 320 |
f"Patient : {patient_name}\n"
|
| 321 |
f"Visit Date : {visiting_date}\n"
|
| 322 |
+
f"Visit Time : {visiting_time}\n"
|
| 323 |
+
f"Please arrive on time."
|
| 324 |
)
|
| 325 |
try:
|
| 326 |
await send_mail(
|
|
|
|
| 341 |
f"Date : {visiting_date}\n"
|
| 342 |
f"Contact : {patient_num}\n"
|
| 343 |
f"━━━━━━━━━━━━━━━━━━━━━━\n"
|
| 344 |
+
f"Please arrive on time."
|
| 345 |
f"{mail_status}"
|
| 346 |
)
|
| 347 |
|
|
|
|
| 375 |
"message": f"Appointment with Dr. {doctor_name} deleted successfully.",
|
| 376 |
})
|
| 377 |
|
|
|
|
| 378 |
# ═══════════════════════════════════════════════════════════════════════════════
|
| 379 |
# SYSTEM PROMPT
|
| 380 |
# ═══════════════════════════════════════════════════════════════════════════════
|
| 381 |
BASE_SYSTEM = """
|
| 382 |
You are a Doctor Appointment Assistant AI.
|
|
|
|
| 383 |
Your job is to help users manage medical appointments.
|
|
|
|
| 384 |
CAPABILITIES:
|
| 385 |
- Book doctor appointments
|
| 386 |
- Reschedule appointments
|
| 387 |
- Cancel appointments
|
| 388 |
- Collect patient details
|
|
|
|
| 389 |
STRICT RULES:
|
| 390 |
- You are NOT a doctor.
|
| 391 |
- NEVER diagnose diseases.
|
| 392 |
- NEVER recommend medicines or treatments.
|
|
|
|
| 393 |
APPOINTMENT FLOW:
|
| 394 |
1. Detect intent (book / cancel / reschedule / inquiry)
|
| 395 |
2. Collect details
|
| 396 |
3. Confirm all details before final booking
|
|
|
|
| 397 |
STYLE:
|
| 398 |
- Be short, clear, structured
|
|
|
|
| 399 |
- Focus on completing booking
|
|
|
|
| 400 |
LANGUAGE RULE:
|
| 401 |
- Detect user language from latest message.
|
| 402 |
- If English → reply English.
|
| 403 |
- If Bangla → reply Bangla (বাংলা).
|
| 404 |
- If Banglish → reply Bangla (বাংলা).
|
| 405 |
- Never mix languages unless user mixes first.
|
|
|
|
| 406 |
TOOLS:
|
| 407 |
+
- Use backend tools if needed
|
| 408 |
- Always confirm before final action
|
| 409 |
"""
|
| 410 |
|
|
|
|
| 430 |
|
| 431 |
if use_gemini:
|
| 432 |
self.llm = ChatGoogleGenerativeAI(
|
| 433 |
+
model="gemini-2.5-flash",
|
| 434 |
+
temperature=0.01,
|
| 435 |
)
|
| 436 |
elif use_ollama:
|
| 437 |
+
self.llm = ChatOllama(model="gemma4:e4b", streaming=True, temperature=0.01)
|
| 438 |
else:
|
| 439 |
# Local fallback — extend as needed
|
| 440 |
+
self.llm = ChatOllama(model="gemma4:e4b", streaming=True, temperature=0.01)
|
| 441 |
|
| 442 |
self.tools = [
|
| 443 |
search_doctor,
|
|
|
|
| 445 |
get_bd_time,
|
| 446 |
search_appointment_by_phone,
|
| 447 |
delete_appointment,
|
| 448 |
+
get_categories_by_day,
|
| 449 |
get_doctors_by_day
|
| 450 |
]
|
| 451 |
+
|
| 452 |
self.tool_node = ToolNode(self.tools)
|
| 453 |
self.llm_with_tools = self.llm.bind_tools(self.tools)
|
| 454 |
|
frontend/index.html
CHANGED
|
@@ -98,7 +98,7 @@
|
|
| 98 |
</div>
|
| 99 |
|
| 100 |
<!-- System Status -->
|
| 101 |
-
<div class="status-panel">
|
| 102 |
<div class="status-row">
|
| 103 |
<span class="status-label">System</span>
|
| 104 |
<span class="status-badge badge-green" id="sys-status">Ready</span>
|
|
@@ -115,7 +115,7 @@
|
|
| 115 |
<span class="status-label">TTS</span>
|
| 116 |
<span class="status-badge badge-green" id="tts-status">Edge TTS</span>
|
| 117 |
</div>
|
| 118 |
-
</div>
|
| 119 |
|
| 120 |
<div class="sidebar-divider"></div>
|
| 121 |
|
|
|
|
| 98 |
</div>
|
| 99 |
|
| 100 |
<!-- System Status -->
|
| 101 |
+
<!-- <div class="status-panel">
|
| 102 |
<div class="status-row">
|
| 103 |
<span class="status-label">System</span>
|
| 104 |
<span class="status-badge badge-green" id="sys-status">Ready</span>
|
|
|
|
| 115 |
<span class="status-label">TTS</span>
|
| 116 |
<span class="status-badge badge-green" id="tts-status">Edge TTS</span>
|
| 117 |
</div>
|
| 118 |
+
</div> -->
|
| 119 |
|
| 120 |
<div class="sidebar-divider"></div>
|
| 121 |
|
frontend/script.js
CHANGED
|
@@ -1,29 +1,4 @@
|
|
| 1 |
-
|
| 2 |
-
* script.js — Production Bangla Voice AI Frontend
|
| 3 |
-
*
|
| 4 |
-
* FIXES APPLIED:
|
| 5 |
-
* FIX-1. PORT: WS_BASE was hardcoded to :8679 — changed to :8679 (uvicorn default).
|
| 6 |
-
* This was the PRIMARY cause of "no backend logs" — WebSocket never connected.
|
| 7 |
-
*
|
| 8 |
-
* FIX-2. CHAT STREAMING: sendText() now uses the VOICE WS with llm_token events
|
| 9 |
-
* instead of the chat WS, giving real-time streaming + TTS for chat mode too.
|
| 10 |
-
* The separate chatWS endpoint is kept as a fallback (text-only mode).
|
| 11 |
-
*
|
| 12 |
-
* FIX-3. THINKING BUBBLE: appendThinking() shows an animated "..." bubble while
|
| 13 |
-
* waiting for the first LLM token. Removed when first token arrives.
|
| 14 |
-
*
|
| 15 |
-
* FIX-4. _cancelled RESET: _cancelled is now reset to false on every sendText()
|
| 16 |
-
* call so previous voice cancellations don't block chat audio.
|
| 17 |
-
*
|
| 18 |
-
* FIX-5. CHAT WS STREAMING: onChatMsg now handles llm_token events from the chat
|
| 19 |
-
* endpoint, showing incremental text just like voice mode.
|
| 20 |
-
*
|
| 21 |
-
* FIX-6. LOGGING: Added console.log for every WS event for easier debugging.
|
| 22 |
-
*
|
| 23 |
-
* FIX-7. SEND FORMAT: chat WS payload now always includes user_id.
|
| 24 |
-
*
|
| 25 |
-
* All other logic (VAD, audio playback, reconnect, init overlay) preserved.
|
| 26 |
-
*/
|
| 27 |
|
| 28 |
'use strict';
|
| 29 |
|
|
@@ -76,28 +51,22 @@ const USER_ID = (() => {
|
|
| 76 |
})();
|
| 77 |
|
| 78 |
// ─── WebSocket base URL ────────────────────────────────────────────────────────
|
| 79 |
-
// FIX-1: Was :8679 — corrected to :8679 (uvicorn/FastAPI default port).
|
| 80 |
-
// If your server runs on a different port, update the number below.
|
| 81 |
const WS_BASE = 'http://127.0.0.1:8679';
|
| 82 |
-
|
| 83 |
-
// ? `http://${location.hostname}:8679` // ← FIXED: was 8679
|
| 84 |
-
// : `http://${location.host}`;
|
| 85 |
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
// ─── WS state ─────────────────────────────────────────────────────────────────
|
| 89 |
let chatWS = null;
|
| 90 |
let voiceWS = null;
|
| 91 |
-
|
| 92 |
let _chatRetry = 0;
|
| 93 |
let _voiceRetry = 0;
|
| 94 |
let _chatRetryTimer = null;
|
| 95 |
let _voiceRetryTimer = null;
|
| 96 |
|
| 97 |
// ─── VAD / recording settings ─────────────────────────────────────────────────
|
| 98 |
-
let SILENCE_MS =
|
| 99 |
-
let SILENCE_DB = -38;
|
| 100 |
const VAD_MS = 80;
|
|
|
|
| 101 |
|
| 102 |
// ─── Playback state ───────────────────────────────────────────────────────────
|
| 103 |
let _ctx = null;
|
|
@@ -105,6 +74,7 @@ let _schedEnd = 0;
|
|
| 105 |
let _endTimer = null;
|
| 106 |
let _cancelled = false;
|
| 107 |
let _inFlight = 0;
|
|
|
|
| 108 |
|
| 109 |
// ─── Recording state ──────────────────────────────────────────────────────────
|
| 110 |
let micStream = null;
|
|
@@ -115,14 +85,17 @@ let audioChunks = [];
|
|
| 115 |
let isListening = false;
|
| 116 |
let isSpeaking = false;
|
| 117 |
let isProcessing = false;
|
|
|
|
| 118 |
let silenceTimer = null;
|
| 119 |
let vadInt = null;
|
| 120 |
let vizInt = null;
|
|
|
|
|
|
|
| 121 |
|
| 122 |
// ─── AI streaming bubble state ────────────────────────────────────────────────
|
| 123 |
-
let aiEl = null;
|
| 124 |
-
let aiTxt = '';
|
| 125 |
-
let thinkingEl = null;
|
| 126 |
|
| 127 |
// ─── Latency timestamps ───────────────────────────────────────────────────────
|
| 128 |
let tSend = 0,
|
|
@@ -131,7 +104,7 @@ let tSend = 0,
|
|
| 131 |
tTts = 0;
|
| 132 |
|
| 133 |
// ═══════════════════════════════════════════════════════════════════════════════
|
| 134 |
-
// INIT OVERLAY
|
| 135 |
// ═══════════════════════════════════════════════════════════���═══════════════════
|
| 136 |
|
| 137 |
const STAGES = [
|
|
@@ -165,7 +138,6 @@ function _tryClose() {
|
|
| 165 |
|
| 166 |
function boot() {
|
| 167 |
initWebSockets();
|
| 168 |
-
|
| 169 |
STAGES.forEach(({ id, text, at, pct }, i) => {
|
| 170 |
setTimeout(() => {
|
| 171 |
if (i > 0) _stageDone(STAGES[i - 1].id);
|
|
@@ -175,7 +147,6 @@ function boot() {
|
|
| 175 |
initBar.style.width = pct + '%';
|
| 176 |
}, at);
|
| 177 |
});
|
| 178 |
-
|
| 179 |
setTimeout(
|
| 180 |
() => {
|
| 181 |
_stageDone(STAGES[STAGES.length - 1].id);
|
|
@@ -184,8 +155,6 @@ function boot() {
|
|
| 184 |
},
|
| 185 |
STAGES[STAGES.length - 1].at + 650,
|
| 186 |
);
|
| 187 |
-
|
| 188 |
-
// Hard failsafe: 8 s max regardless of WS state
|
| 189 |
setTimeout(() => {
|
| 190 |
if (!_initClosed) {
|
| 191 |
_wsGate = _stageGate = true;
|
|
@@ -203,11 +172,11 @@ function _stageDone(id) {
|
|
| 203 |
}
|
| 204 |
|
| 205 |
// ═══════════════════════════════════════════════════════════════════════════════
|
| 206 |
-
// WEBSOCKETS
|
| 207 |
// ═══════════════════════════════════════════════════════════════════════════════
|
| 208 |
|
| 209 |
-
function _backoff(
|
| 210 |
-
return Math.min(1000 * Math.pow(2,
|
| 211 |
}
|
| 212 |
|
| 213 |
function _setSysStatus(online) {
|
|
@@ -217,76 +186,59 @@ function _setSysStatus(online) {
|
|
| 217 |
'status-badge ' + (online ? 'badge-green' : 'badge-yellow');
|
| 218 |
}
|
| 219 |
|
| 220 |
-
// ── Chat WS ────────────────────────────────────────────────────────────────────
|
| 221 |
function _connectChat() {
|
| 222 |
if (chatWS && chatWS.readyState <= WebSocket.OPEN) return;
|
| 223 |
-
|
| 224 |
chatWS = new WebSocket(`${WS_BASE}/ws/chat`);
|
| 225 |
-
|
| 226 |
chatWS.onopen = () => {
|
| 227 |
_chatRetry = 0;
|
| 228 |
-
console.log('[Chat WS] connected
|
| 229 |
-
};
|
| 230 |
-
|
| 231 |
-
chatWS.onerror = (e) => {
|
| 232 |
-
console.error('[Chat WS] error:', e); // FIX-6
|
| 233 |
};
|
| 234 |
-
|
| 235 |
chatWS.onclose = (ev) => {
|
| 236 |
-
console.log(`[Chat WS] closed (${ev.code})
|
| 237 |
clearTimeout(_chatRetryTimer);
|
| 238 |
_chatRetryTimer = setTimeout(() => {
|
| 239 |
_chatRetry++;
|
| 240 |
_connectChat();
|
| 241 |
}, _backoff(_chatRetry));
|
| 242 |
};
|
| 243 |
-
|
| 244 |
chatWS.onmessage = onChatMsg;
|
| 245 |
}
|
| 246 |
|
| 247 |
-
// ── Voice WS ────────────────────────────────────────────────────────────────────
|
| 248 |
function _connectVoice() {
|
| 249 |
if (voiceWS && voiceWS.readyState <= WebSocket.OPEN) return;
|
| 250 |
-
|
| 251 |
voiceWS = new WebSocket(`${WS_BASE}/ws/voice`);
|
| 252 |
voiceWS.binaryType = 'arraybuffer';
|
| 253 |
|
| 254 |
voiceWS.onopen = () => {
|
| 255 |
_voiceRetry = 0;
|
| 256 |
-
console.log(
|
| 257 |
-
'[Voice WS] connected to',
|
| 258 |
-
`${WS_BASE}/ws/voice`,
|
| 259 |
-
'uid:',
|
| 260 |
-
USER_ID,
|
| 261 |
-
); // FIX-6
|
| 262 |
voiceWS.send(JSON.stringify({ type: 'init', user_id: USER_ID }));
|
| 263 |
_setSysStatus(true);
|
| 264 |
_wsGate = true;
|
| 265 |
_tryClose();
|
| 266 |
};
|
| 267 |
-
|
| 268 |
-
voiceWS.onerror = (e) => {
|
| 269 |
-
console.error('[Voice WS] error:', e); // FIX-6
|
| 270 |
-
};
|
| 271 |
-
|
| 272 |
voiceWS.onclose = (ev) => {
|
| 273 |
-
console.log(`[Voice WS] closed (${ev.code})
|
| 274 |
_setSysStatus(false);
|
| 275 |
-
|
| 276 |
if (!_initClosed) {
|
| 277 |
_wsGate = true;
|
| 278 |
_tryClose();
|
| 279 |
}
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
clearTimeout(_voiceRetryTimer);
|
| 284 |
_voiceRetryTimer = setTimeout(() => {
|
| 285 |
_voiceRetry++;
|
| 286 |
_connectVoice();
|
| 287 |
}, _backoff(_voiceRetry));
|
| 288 |
};
|
| 289 |
-
|
| 290 |
voiceWS.onmessage = onVoiceMsg;
|
| 291 |
}
|
| 292 |
|
|
@@ -295,8 +247,7 @@ function initWebSockets() {
|
|
| 295 |
_connectVoice();
|
| 296 |
}
|
| 297 |
|
| 298 |
-
// ── Chat WS handler ───────────────────────────────────────────────────────────
|
| 299 |
-
// FIX-5: Now handles llm_token for streaming, not just full 'chat' message
|
| 300 |
function onChatMsg(ev) {
|
| 301 |
let msg;
|
| 302 |
try {
|
|
@@ -304,18 +255,16 @@ function onChatMsg(ev) {
|
|
| 304 |
} catch {
|
| 305 |
return;
|
| 306 |
}
|
| 307 |
-
|
| 308 |
-
console.log('[Chat WS] msg:', msg.type); // FIX-6
|
| 309 |
|
| 310 |
switch (msg.type) {
|
| 311 |
case 'llm_token':
|
| 312 |
-
// FIX-5: streaming token support for chat WS
|
| 313 |
if (!msg.token) break;
|
| 314 |
if (tLlm === 0) {
|
| 315 |
tLlm = Date.now();
|
| 316 |
if (tSend > 0) mLlm.textContent = tLlm - tSend + ' ms';
|
| 317 |
}
|
| 318 |
-
_removeThinking();
|
| 319 |
if (!aiEl) {
|
| 320 |
aiEl = document.createElement('div');
|
| 321 |
aiEl.className = 'message ai';
|
|
@@ -330,9 +279,8 @@ function onChatMsg(ev) {
|
|
| 330 |
break;
|
| 331 |
|
| 332 |
case 'chat':
|
| 333 |
-
// Fallback: backend sent full response at once (non-streaming mode)
|
| 334 |
if (!msg.text) break;
|
| 335 |
-
_removeThinking();
|
| 336 |
if (!aiEl) {
|
| 337 |
aiEl = document.createElement('div');
|
| 338 |
aiEl.className = 'message ai';
|
|
@@ -347,7 +295,7 @@ function onChatMsg(ev) {
|
|
| 347 |
break;
|
| 348 |
|
| 349 |
case 'end':
|
| 350 |
-
_removeThinking();
|
| 351 |
if (aiEl && aiTxt) {
|
| 352 |
aiEl.innerHTML =
|
| 353 |
typeof marked !== 'undefined'
|
|
@@ -364,7 +312,7 @@ function onChatMsg(ev) {
|
|
| 364 |
break;
|
| 365 |
|
| 366 |
case 'error':
|
| 367 |
-
_removeThinking();
|
| 368 |
appendMsg('⚠️ ' + msg.text, 'system');
|
| 369 |
aiEl = null;
|
| 370 |
aiTxt = '';
|
|
@@ -374,9 +322,10 @@ function onChatMsg(ev) {
|
|
| 374 |
}
|
| 375 |
}
|
| 376 |
|
| 377 |
-
// ── Voice WS handler ──────────────────────────────────────────────────────────
|
| 378 |
function onVoiceMsg(ev) {
|
| 379 |
if (ev.data instanceof ArrayBuffer) {
|
|
|
|
| 380 |
enqueueAudio(ev.data);
|
| 381 |
return;
|
| 382 |
}
|
|
@@ -387,22 +336,21 @@ function onVoiceMsg(ev) {
|
|
| 387 |
} catch {
|
| 388 |
return;
|
| 389 |
}
|
| 390 |
-
|
| 391 |
-
console.log('[Voice WS] msg:', msg.type); // FIX-6
|
| 392 |
|
| 393 |
switch (msg.type) {
|
| 394 |
case 'init_ack':
|
| 395 |
-
console.log('[Voice WS]
|
| 396 |
break;
|
| 397 |
|
| 398 |
case 'stt':
|
| 399 |
tStt = Date.now();
|
| 400 |
if (tSend > 0) mStt.textContent = tStt - tSend + ' ms';
|
| 401 |
-
_removeThinking();
|
| 402 |
appendMsg('🎤 ' + msg.text, 'user');
|
| 403 |
aiEl = null;
|
| 404 |
aiTxt = '';
|
| 405 |
-
appendThinking();
|
| 406 |
setState('processing');
|
| 407 |
break;
|
| 408 |
|
|
@@ -412,7 +360,7 @@ function onVoiceMsg(ev) {
|
|
| 412 |
tLlm = Date.now();
|
| 413 |
if (tStt > 0) mLlm.textContent = tLlm - tStt + ' ms';
|
| 414 |
}
|
| 415 |
-
_removeThinking();
|
| 416 |
if (!aiEl) {
|
| 417 |
aiEl = document.createElement('div');
|
| 418 |
aiEl.className = 'message ai';
|
|
@@ -434,22 +382,25 @@ function onVoiceMsg(ev) {
|
|
| 434 |
: aiTxt.replace(/\n/g, '<br>');
|
| 435 |
chatBox.scrollTop = chatBox.scrollHeight;
|
| 436 |
}
|
| 437 |
-
_removeThinking();
|
| 438 |
aiEl = null;
|
| 439 |
aiTxt = '';
|
| 440 |
if (tSend > 0) mTotal.textContent = Date.now() - tSend + ' ms';
|
| 441 |
tSend = tStt = tLlm = tTts = 0;
|
| 442 |
-
_scheduleEnd();
|
| 443 |
isProcessing = false;
|
|
|
|
|
|
|
|
|
|
| 444 |
break;
|
| 445 |
|
| 446 |
case 'error':
|
| 447 |
-
_removeThinking();
|
| 448 |
appendMsg('⚠️ ' + msg.text, 'system');
|
| 449 |
aiEl = null;
|
| 450 |
aiTxt = '';
|
| 451 |
isProcessing = false;
|
| 452 |
-
|
|
|
|
| 453 |
break;
|
| 454 |
|
| 455 |
case 'pong':
|
|
@@ -460,7 +411,7 @@ function onVoiceMsg(ev) {
|
|
| 460 |
}
|
| 461 |
}
|
| 462 |
|
| 463 |
-
// ───
|
| 464 |
function appendThinking() {
|
| 465 |
if (thinkingEl) return;
|
| 466 |
thinkingEl = document.createElement('div');
|
|
@@ -470,7 +421,6 @@ function appendThinking() {
|
|
| 470 |
chatBox.appendChild(thinkingEl);
|
| 471 |
chatBox.scrollTop = chatBox.scrollHeight;
|
| 472 |
}
|
| 473 |
-
|
| 474 |
function _removeThinking() {
|
| 475 |
if (thinkingEl) {
|
| 476 |
thinkingEl.remove();
|
|
@@ -479,7 +429,7 @@ function _removeThinking() {
|
|
| 479 |
}
|
| 480 |
|
| 481 |
// ═══════════════════════════════════════════════════════════════════════════════
|
| 482 |
-
// AUDIO PLAYBACK
|
| 483 |
// ═════════════════════���═════════════════════════════════════════════════════════
|
| 484 |
|
| 485 |
function _ctxEnsure() {
|
|
@@ -501,7 +451,7 @@ async function enqueueAudio(buf) {
|
|
| 501 |
try {
|
| 502 |
decoded = await ctx.decodeAudioData(buf.slice(0));
|
| 503 |
} catch (e) {
|
| 504 |
-
console.warn('[Audio] decode:', e.message);
|
| 505 |
_inFlight = Math.max(0, _inFlight - 1);
|
| 506 |
_vizQ();
|
| 507 |
return;
|
|
@@ -521,7 +471,6 @@ async function enqueueAudio(buf) {
|
|
| 521 |
const src = ctx.createBufferSource();
|
| 522 |
src.buffer = decoded;
|
| 523 |
src.connect(ctx.destination);
|
| 524 |
-
|
| 525 |
const now = ctx.currentTime;
|
| 526 |
const start = Math.max(now + 0.01, _schedEnd);
|
| 527 |
src.start(start);
|
|
@@ -547,24 +496,35 @@ function _scheduleEnd() {
|
|
| 547 |
clearTimeout(_endTimer);
|
| 548 |
const ctx = _ctx;
|
| 549 |
if (!ctx || ctx.state === 'closed') {
|
| 550 |
-
|
|
|
|
| 551 |
return;
|
| 552 |
}
|
| 553 |
-
const
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
}
|
| 558 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
function _done() {
|
|
|
|
| 560 |
isProcessing = false;
|
|
|
|
| 561 |
_inFlight = 0;
|
| 562 |
_vizQ();
|
| 563 |
-
|
|
|
|
|
|
|
|
|
|
| 564 |
}
|
| 565 |
|
| 566 |
function stopAllAudio() {
|
| 567 |
_cancelled = true;
|
|
|
|
| 568 |
clearTimeout(_endTimer);
|
| 569 |
_endTimer = null;
|
| 570 |
_schedEnd = 0;
|
|
@@ -587,58 +547,28 @@ textInput.addEventListener('keydown', (e) => {
|
|
| 587 |
|
| 588 |
function sendText() {
|
| 589 |
const text = textInput.value.trim();
|
| 590 |
-
console.log('Send button clicked, text:', text); // FIX-6
|
| 591 |
if (!text || isProcessing) return;
|
| 592 |
-
|
| 593 |
appendMsg(text, 'user');
|
| 594 |
textInput.value = '';
|
| 595 |
-
|
| 596 |
-
// FIX-4: always reset _cancelled before new turn so previous voice
|
| 597 |
-
// cancel doesn't block chat audio playback
|
| 598 |
_cancelled = false;
|
| 599 |
isProcessing = true;
|
| 600 |
tSend = Date.now();
|
| 601 |
-
tLlm = 0;
|
| 602 |
-
tTts = 0;
|
| 603 |
aiEl = null;
|
| 604 |
aiTxt = '';
|
| 605 |
-
|
| 606 |
setState('processing');
|
| 607 |
-
appendThinking();
|
| 608 |
-
|
| 609 |
-
console.log('[Chat] sending:', text); // FIX-6
|
| 610 |
-
|
| 611 |
-
// Try voice WS first (gives streaming tokens + TTS audio)
|
| 612 |
-
// Fall back to chat WS for text-only response
|
| 613 |
-
if (voiceWS && voiceWS.readyState === WebSocket.OPEN) {
|
| 614 |
-
// Send as a text query over voice WS — backend will handle it
|
| 615 |
-
// We need to send it as JSON text (not binary) to trigger chat path
|
| 616 |
-
// Since voice WS only handles binary audio + control JSON,
|
| 617 |
-
// we route text queries through the dedicated chat WS.
|
| 618 |
-
_sendViaChat(text);
|
| 619 |
-
} else {
|
| 620 |
-
_sendViaChat(text);
|
| 621 |
-
}
|
| 622 |
}
|
| 623 |
|
| 624 |
function _sendViaChat(text) {
|
| 625 |
-
// FIX-7: always include user_id in payload
|
| 626 |
const payload = JSON.stringify({ user_id: USER_ID, user_query: text });
|
| 627 |
-
console.log(
|
| 628 |
-
'[Chat WS] sending payload, readyState:',
|
| 629 |
-
chatWS ? chatWS.readyState : 'null',
|
| 630 |
-
);
|
| 631 |
-
|
| 632 |
if (chatWS && chatWS.readyState === WebSocket.OPEN) {
|
| 633 |
chatWS.send(payload);
|
| 634 |
} else {
|
| 635 |
-
// Queue with retry until connected
|
| 636 |
const _retry = () => {
|
| 637 |
-
if (chatWS && chatWS.readyState === WebSocket.OPEN)
|
| 638 |
-
|
| 639 |
-
} else {
|
| 640 |
-
setTimeout(_retry, 300);
|
| 641 |
-
}
|
| 642 |
};
|
| 643 |
_retry();
|
| 644 |
}
|
|
@@ -649,17 +579,33 @@ function _sendViaChat(text) {
|
|
| 649 |
// ═══════════════════════════════════════════════════════════════════════════════
|
| 650 |
|
| 651 |
micBtn.onclick = async () => {
|
| 652 |
-
if (
|
| 653 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
};
|
| 655 |
|
| 656 |
stopBtn.onclick = () => {
|
| 657 |
stopAllAudio();
|
| 658 |
-
|
| 659 |
-
|
|
|
|
|
|
|
|
|
|
| 660 |
};
|
| 661 |
|
|
|
|
| 662 |
async function startListening() {
|
|
|
|
|
|
|
| 663 |
_ctxEnsure();
|
| 664 |
|
| 665 |
try {
|
|
@@ -673,7 +619,7 @@ async function startListening() {
|
|
| 673 |
},
|
| 674 |
});
|
| 675 |
} catch (err) {
|
| 676 |
-
console.error('[Mic]', err);
|
| 677 |
appendMsg('⚠️ মাইক্রোফোন অ্যাক্সেস দেওয়া হয়নি।', 'system');
|
| 678 |
return;
|
| 679 |
}
|
|
@@ -686,80 +632,145 @@ async function startListening() {
|
|
| 686 |
src.connect(analyser);
|
| 687 |
|
| 688 |
isListening = true;
|
|
|
|
|
|
|
| 689 |
setMic('listening');
|
| 690 |
setState('listening');
|
| 691 |
voiceViz.classList.add('active');
|
| 692 |
|
| 693 |
vadInt = setInterval(vadTick, VAD_MS);
|
| 694 |
vizInt = setInterval(vizTick, 60);
|
|
|
|
|
|
|
| 695 |
}
|
| 696 |
|
| 697 |
-
|
|
|
|
|
|
|
|
|
|
| 698 |
clearInterval(vadInt);
|
| 699 |
clearInterval(vizInt);
|
| 700 |
clearTimeout(silenceTimer);
|
| 701 |
vadInt = vizInt = silenceTimer = null;
|
| 702 |
|
| 703 |
-
|
| 704 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 705 |
|
| 706 |
micStream?.getTracks().forEach((t) => t.stop());
|
| 707 |
-
|
| 708 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
|
| 710 |
-
isListening = isSpeaking = isProcessing = false;
|
| 711 |
-
setMic('off');
|
| 712 |
-
setState('ready');
|
| 713 |
voiceViz.classList.remove('active');
|
| 714 |
vizBars.forEach((b) => (b.style.height = '4px'));
|
|
|
|
|
|
|
| 715 |
}
|
| 716 |
|
| 717 |
-
// ──
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
function vadTick() {
|
| 719 |
if (!analyser) return;
|
|
|
|
|
|
|
|
|
|
| 720 |
const buf = new Float32Array(analyser.frequencyBinCount);
|
| 721 |
analyser.getFloatTimeDomainData(buf);
|
| 722 |
-
|
| 723 |
-
let
|
| 724 |
-
|
| 725 |
-
const db = 20 * Math.log10(Math.sqrt(s / buf.length) || 1e-10);
|
| 726 |
const speech = db > SILENCE_DB;
|
| 727 |
|
| 728 |
if (speech) {
|
| 729 |
-
if (isProcessing) {
|
| 730 |
-
stopAllAudio();
|
| 731 |
-
isProcessing = false;
|
| 732 |
-
}
|
| 733 |
clearTimeout(silenceTimer);
|
| 734 |
silenceTimer = null;
|
| 735 |
|
| 736 |
if (!isSpeaking) {
|
|
|
|
| 737 |
isSpeaking = true;
|
|
|
|
| 738 |
_cancelled = false;
|
| 739 |
_ctxEnsure();
|
| 740 |
startRecorder();
|
| 741 |
setMic('recording');
|
| 742 |
setState('recording');
|
|
|
|
| 743 |
}
|
| 744 |
} else {
|
| 745 |
if (isSpeaking && !silenceTimer) {
|
| 746 |
-
silenceTimer = setTimeout(
|
| 747 |
-
silenceTimer = null;
|
| 748 |
-
isSpeaking = false;
|
| 749 |
-
isProcessing = true;
|
| 750 |
-
_cancelled = false;
|
| 751 |
-
tSend = Date.now();
|
| 752 |
-
tLlm = 0;
|
| 753 |
-
tTts = 0;
|
| 754 |
-
stopRecorder();
|
| 755 |
-
setMic('processing');
|
| 756 |
-
setState('processing');
|
| 757 |
-
}, SILENCE_MS);
|
| 758 |
}
|
| 759 |
}
|
| 760 |
}
|
| 761 |
|
| 762 |
-
// ──
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 763 |
function vizTick() {
|
| 764 |
if (!analyser) return;
|
| 765 |
const data = new Uint8Array(analyser.frequencyBinCount);
|
|
@@ -771,56 +782,132 @@ function vizTick() {
|
|
| 771 |
});
|
| 772 |
}
|
| 773 |
|
| 774 |
-
// ── MediaRecorder ─────────────────────────────────────────────────────────────
|
| 775 |
function startRecorder() {
|
| 776 |
if (!micStream) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 777 |
audioChunks = [];
|
| 778 |
-
|
| 779 |
? 'audio/webm;codecs=opus'
|
| 780 |
: 'audio/webm';
|
| 781 |
|
| 782 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 783 |
mediaRecorder.ondataavailable = (e) => {
|
| 784 |
-
if (e.data.size > 0) audioChunks.push(e.data);
|
| 785 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 786 |
mediaRecorder.onstop = async () => {
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
if (isListening) setState('listening');
|
| 790 |
-
return;
|
| 791 |
-
}
|
| 792 |
-
const blob = new Blob(audioChunks, { type: mime });
|
| 793 |
audioChunks = [];
|
| 794 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 795 |
console.log(
|
| 796 |
-
`[
|
|
|
|
|
|
|
| 797 |
);
|
| 798 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 799 |
if (voiceWS && voiceWS.readyState === WebSocket.OPEN) {
|
| 800 |
-
appendThinking();
|
| 801 |
voiceWS.send(buf);
|
|
|
|
| 802 |
} else {
|
| 803 |
-
console.warn('[VAD]
|
| 804 |
-
|
| 805 |
-
|
|
|
|
|
|
|
|
|
|
| 806 |
}
|
| 807 |
};
|
|
|
|
| 808 |
mediaRecorder.start();
|
|
|
|
| 809 |
}
|
| 810 |
|
| 811 |
function stopRecorder() {
|
| 812 |
-
if (mediaRecorder && mediaRecorder.state !== 'inactive')
|
| 813 |
-
|
|
|
|
| 814 |
}
|
| 815 |
|
| 816 |
function discardRecorder() {
|
| 817 |
-
if (!mediaRecorder || mediaRecorder.state === 'inactive')
|
|
|
|
|
|
|
|
|
|
| 818 |
mediaRecorder.ondataavailable = () => {};
|
| 819 |
mediaRecorder.onstop = () => {
|
| 820 |
audioChunks = [];
|
| 821 |
};
|
| 822 |
mediaRecorder.stop();
|
| 823 |
mediaRecorder = null;
|
|
|
|
| 824 |
}
|
| 825 |
|
| 826 |
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -845,7 +932,7 @@ const MIC_MAP = {
|
|
| 845 |
off: { cls: 'mic-off', label: 'Voice শুরু করুন', icon: '🎤' },
|
| 846 |
listening: {
|
| 847 |
cls: 'mic-listening',
|
| 848 |
-
label: 'শুনছি… (ব
|
| 849 |
icon: '🟢',
|
| 850 |
},
|
| 851 |
recording: { cls: 'mic-recording', label: 'বলছেন…', icon: '🔴' },
|
|
@@ -872,14 +959,12 @@ function appendMsg(text, who) {
|
|
| 872 |
return d;
|
| 873 |
}
|
| 874 |
|
| 875 |
-
// ── Clear chat ────────────────────────────────────────────────────────────────
|
| 876 |
clearBtn.onclick = () => {
|
| 877 |
chatBox.innerHTML = '';
|
| 878 |
-
thinkingEl = null;
|
| 879 |
appendMsg('চ্যাট পরিষ্কার করা হয়েছে।', 'system');
|
| 880 |
};
|
| 881 |
|
| 882 |
-
// ── Sidebar ───────────────────────────────────────────────────────────────────
|
| 883 |
sidebarToggle.onclick = () => {
|
| 884 |
sidebarEl.classList.toggle('collapsed');
|
| 885 |
sidebarToggle.textContent = sidebarEl.classList.contains('collapsed')
|
|
@@ -888,7 +973,6 @@ sidebarToggle.onclick = () => {
|
|
| 888 |
};
|
| 889 |
mobileMenuBtn.onclick = () => sidebarEl.classList.toggle('mobile-open');
|
| 890 |
|
| 891 |
-
// ── Settings sliders ──────────────────────────────────────────────────────────
|
| 892 |
sThreshold.value = SILENCE_DB;
|
| 893 |
sThresholdVal.textContent = SILENCE_DB + ' dB';
|
| 894 |
sThreshold.oninput = () => {
|
|
@@ -905,12 +989,11 @@ sTimeout.oninput = () => {
|
|
| 905 |
|
| 906 |
sVoice.onchange = () => appendMsg('🔊 TTS voice: ' + sVoice.value, 'system');
|
| 907 |
|
| 908 |
-
// ── Queue animation ───────────────────────────────────────────────────────────
|
| 909 |
setInterval(() => {
|
| 910 |
if (_inFlight > 0) _vizQ();
|
| 911 |
}, 140);
|
| 912 |
|
| 913 |
// ═════════════════════════════════════════════════════════════════════════════���═
|
| 914 |
-
//
|
| 915 |
// ═══════════════════════════════════════════════════════════════════════════════
|
| 916 |
boot();
|
|
|
|
| 1 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
'use strict';
|
| 4 |
|
|
|
|
| 51 |
})();
|
| 52 |
|
| 53 |
// ─── WebSocket base URL ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
| 54 |
const WS_BASE = 'http://127.0.0.1:8679';
|
| 55 |
+
console.log('[Boot] WS base:', WS_BASE);
|
|
|
|
|
|
|
| 56 |
|
| 57 |
+
// ─── WS handles ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
| 58 |
let chatWS = null;
|
| 59 |
let voiceWS = null;
|
|
|
|
| 60 |
let _chatRetry = 0;
|
| 61 |
let _voiceRetry = 0;
|
| 62 |
let _chatRetryTimer = null;
|
| 63 |
let _voiceRetryTimer = null;
|
| 64 |
|
| 65 |
// ─── VAD / recording settings ─────────────────────────────────────────────────
|
| 66 |
+
let SILENCE_MS = 1200; // BUG-FIX-B: was 450 ms
|
| 67 |
+
let SILENCE_DB = -38;
|
| 68 |
const VAD_MS = 80;
|
| 69 |
+
const MIN_SPEECH_MS = 400; // discard noise bursts shorter than this
|
| 70 |
|
| 71 |
// ─── Playback state ───────────────────────────────────────────────────────────
|
| 72 |
let _ctx = null;
|
|
|
|
| 74 |
let _endTimer = null;
|
| 75 |
let _cancelled = false;
|
| 76 |
let _inFlight = 0;
|
| 77 |
+
let _ttsPlaying = false;
|
| 78 |
|
| 79 |
// ─── Recording state ──────────────────────────────────────────────────────────
|
| 80 |
let micStream = null;
|
|
|
|
| 85 |
let isListening = false;
|
| 86 |
let isSpeaking = false;
|
| 87 |
let isProcessing = false;
|
| 88 |
+
let isRecordingLocked = false;
|
| 89 |
let silenceTimer = null;
|
| 90 |
let vadInt = null;
|
| 91 |
let vizInt = null;
|
| 92 |
+
let _speechStartMs = 0;
|
| 93 |
+
let _recorderMime = 'audio/webm';
|
| 94 |
|
| 95 |
// ─── AI streaming bubble state ────────────────────────────────────────────────
|
| 96 |
+
let aiEl = null;
|
| 97 |
+
let aiTxt = '';
|
| 98 |
+
let thinkingEl = null;
|
| 99 |
|
| 100 |
// ─── Latency timestamps ───────────────────────────────────────────────────────
|
| 101 |
let tSend = 0,
|
|
|
|
| 104 |
tTts = 0;
|
| 105 |
|
| 106 |
// ═══════════════════════════════════════════════════════════════════════════════
|
| 107 |
+
// INIT OVERLAY
|
| 108 |
// ═══════════════════════════════════════════════════════════���═══════════════════
|
| 109 |
|
| 110 |
const STAGES = [
|
|
|
|
| 138 |
|
| 139 |
function boot() {
|
| 140 |
initWebSockets();
|
|
|
|
| 141 |
STAGES.forEach(({ id, text, at, pct }, i) => {
|
| 142 |
setTimeout(() => {
|
| 143 |
if (i > 0) _stageDone(STAGES[i - 1].id);
|
|
|
|
| 147 |
initBar.style.width = pct + '%';
|
| 148 |
}, at);
|
| 149 |
});
|
|
|
|
| 150 |
setTimeout(
|
| 151 |
() => {
|
| 152 |
_stageDone(STAGES[STAGES.length - 1].id);
|
|
|
|
| 155 |
},
|
| 156 |
STAGES[STAGES.length - 1].at + 650,
|
| 157 |
);
|
|
|
|
|
|
|
| 158 |
setTimeout(() => {
|
| 159 |
if (!_initClosed) {
|
| 160 |
_wsGate = _stageGate = true;
|
|
|
|
| 172 |
}
|
| 173 |
|
| 174 |
// ═══════════════════════════════════════════════════════════════════════════════
|
| 175 |
+
// WEBSOCKETS
|
| 176 |
// ═══════════════════════════════════════════════════════════════════════════════
|
| 177 |
|
| 178 |
+
function _backoff(r) {
|
| 179 |
+
return Math.min(1000 * Math.pow(2, r), 16000);
|
| 180 |
}
|
| 181 |
|
| 182 |
function _setSysStatus(online) {
|
|
|
|
| 186 |
'status-badge ' + (online ? 'badge-green' : 'badge-yellow');
|
| 187 |
}
|
| 188 |
|
|
|
|
| 189 |
function _connectChat() {
|
| 190 |
if (chatWS && chatWS.readyState <= WebSocket.OPEN) return;
|
|
|
|
| 191 |
chatWS = new WebSocket(`${WS_BASE}/ws/chat`);
|
|
|
|
| 192 |
chatWS.onopen = () => {
|
| 193 |
_chatRetry = 0;
|
| 194 |
+
console.log('[Chat WS] connected');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
};
|
| 196 |
+
chatWS.onerror = (e) => console.error('[Chat WS] error:', e);
|
| 197 |
chatWS.onclose = (ev) => {
|
| 198 |
+
console.log(`[Chat WS] closed (${ev.code})`);
|
| 199 |
clearTimeout(_chatRetryTimer);
|
| 200 |
_chatRetryTimer = setTimeout(() => {
|
| 201 |
_chatRetry++;
|
| 202 |
_connectChat();
|
| 203 |
}, _backoff(_chatRetry));
|
| 204 |
};
|
|
|
|
| 205 |
chatWS.onmessage = onChatMsg;
|
| 206 |
}
|
| 207 |
|
|
|
|
| 208 |
function _connectVoice() {
|
| 209 |
if (voiceWS && voiceWS.readyState <= WebSocket.OPEN) return;
|
|
|
|
| 210 |
voiceWS = new WebSocket(`${WS_BASE}/ws/voice`);
|
| 211 |
voiceWS.binaryType = 'arraybuffer';
|
| 212 |
|
| 213 |
voiceWS.onopen = () => {
|
| 214 |
_voiceRetry = 0;
|
| 215 |
+
console.log('[Voice WS] connected, uid:', USER_ID);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
voiceWS.send(JSON.stringify({ type: 'init', user_id: USER_ID }));
|
| 217 |
_setSysStatus(true);
|
| 218 |
_wsGate = true;
|
| 219 |
_tryClose();
|
| 220 |
};
|
| 221 |
+
voiceWS.onerror = (e) => console.error('[Voice WS] error:', e);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
voiceWS.onclose = (ev) => {
|
| 223 |
+
console.log(`[Voice WS] closed (${ev.code})`);
|
| 224 |
_setSysStatus(false);
|
|
|
|
| 225 |
if (!_initClosed) {
|
| 226 |
_wsGate = true;
|
| 227 |
_tryClose();
|
| 228 |
}
|
| 229 |
+
if (isListening || isSpeaking || isProcessing) {
|
| 230 |
+
_teardownMicHardware();
|
| 231 |
+
_resetVoiceState();
|
| 232 |
+
setState('ready');
|
| 233 |
+
setMic('off');
|
| 234 |
+
micBtn.disabled = false;
|
| 235 |
+
}
|
| 236 |
clearTimeout(_voiceRetryTimer);
|
| 237 |
_voiceRetryTimer = setTimeout(() => {
|
| 238 |
_voiceRetry++;
|
| 239 |
_connectVoice();
|
| 240 |
}, _backoff(_voiceRetry));
|
| 241 |
};
|
|
|
|
| 242 |
voiceWS.onmessage = onVoiceMsg;
|
| 243 |
}
|
| 244 |
|
|
|
|
| 247 |
_connectVoice();
|
| 248 |
}
|
| 249 |
|
| 250 |
+
// ── Chat WS handler ───────────────────────────────────────────────────────────
|
|
|
|
| 251 |
function onChatMsg(ev) {
|
| 252 |
let msg;
|
| 253 |
try {
|
|
|
|
| 255 |
} catch {
|
| 256 |
return;
|
| 257 |
}
|
| 258 |
+
console.log('[Chat WS]', msg.type);
|
|
|
|
| 259 |
|
| 260 |
switch (msg.type) {
|
| 261 |
case 'llm_token':
|
|
|
|
| 262 |
if (!msg.token) break;
|
| 263 |
if (tLlm === 0) {
|
| 264 |
tLlm = Date.now();
|
| 265 |
if (tSend > 0) mLlm.textContent = tLlm - tSend + ' ms';
|
| 266 |
}
|
| 267 |
+
_removeThinking();
|
| 268 |
if (!aiEl) {
|
| 269 |
aiEl = document.createElement('div');
|
| 270 |
aiEl.className = 'message ai';
|
|
|
|
| 279 |
break;
|
| 280 |
|
| 281 |
case 'chat':
|
|
|
|
| 282 |
if (!msg.text) break;
|
| 283 |
+
_removeThinking();
|
| 284 |
if (!aiEl) {
|
| 285 |
aiEl = document.createElement('div');
|
| 286 |
aiEl.className = 'message ai';
|
|
|
|
| 295 |
break;
|
| 296 |
|
| 297 |
case 'end':
|
| 298 |
+
_removeThinking();
|
| 299 |
if (aiEl && aiTxt) {
|
| 300 |
aiEl.innerHTML =
|
| 301 |
typeof marked !== 'undefined'
|
|
|
|
| 312 |
break;
|
| 313 |
|
| 314 |
case 'error':
|
| 315 |
+
_removeThinking();
|
| 316 |
appendMsg('⚠️ ' + msg.text, 'system');
|
| 317 |
aiEl = null;
|
| 318 |
aiTxt = '';
|
|
|
|
| 322 |
}
|
| 323 |
}
|
| 324 |
|
| 325 |
+
// ── Voice WS handler ──────────────────────────────────────────────────────────
|
| 326 |
function onVoiceMsg(ev) {
|
| 327 |
if (ev.data instanceof ArrayBuffer) {
|
| 328 |
+
_ttsPlaying = true;
|
| 329 |
enqueueAudio(ev.data);
|
| 330 |
return;
|
| 331 |
}
|
|
|
|
| 336 |
} catch {
|
| 337 |
return;
|
| 338 |
}
|
| 339 |
+
console.log('[Voice WS]', msg.type);
|
|
|
|
| 340 |
|
| 341 |
switch (msg.type) {
|
| 342 |
case 'init_ack':
|
| 343 |
+
console.log('[Voice WS] ack uid:', msg.user_id);
|
| 344 |
break;
|
| 345 |
|
| 346 |
case 'stt':
|
| 347 |
tStt = Date.now();
|
| 348 |
if (tSend > 0) mStt.textContent = tStt - tSend + ' ms';
|
| 349 |
+
_removeThinking();
|
| 350 |
appendMsg('🎤 ' + msg.text, 'user');
|
| 351 |
aiEl = null;
|
| 352 |
aiTxt = '';
|
| 353 |
+
appendThinking();
|
| 354 |
setState('processing');
|
| 355 |
break;
|
| 356 |
|
|
|
|
| 360 |
tLlm = Date.now();
|
| 361 |
if (tStt > 0) mLlm.textContent = tLlm - tStt + ' ms';
|
| 362 |
}
|
| 363 |
+
_removeThinking();
|
| 364 |
if (!aiEl) {
|
| 365 |
aiEl = document.createElement('div');
|
| 366 |
aiEl.className = 'message ai';
|
|
|
|
| 382 |
: aiTxt.replace(/\n/g, '<br>');
|
| 383 |
chatBox.scrollTop = chatBox.scrollHeight;
|
| 384 |
}
|
| 385 |
+
_removeThinking();
|
| 386 |
aiEl = null;
|
| 387 |
aiTxt = '';
|
| 388 |
if (tSend > 0) mTotal.textContent = Date.now() - tSend + ' ms';
|
| 389 |
tSend = tStt = tLlm = tTts = 0;
|
|
|
|
| 390 |
isProcessing = false;
|
| 391 |
+
// BUG-FIX-C: schedule _done() to fire after TTS audio drains.
|
| 392 |
+
// If no TTS audio arrived (_schedEnd == 0), _done fires in ~300 ms.
|
| 393 |
+
_scheduleEnd();
|
| 394 |
break;
|
| 395 |
|
| 396 |
case 'error':
|
| 397 |
+
_removeThinking();
|
| 398 |
appendMsg('⚠️ ' + msg.text, 'system');
|
| 399 |
aiEl = null;
|
| 400 |
aiTxt = '';
|
| 401 |
isProcessing = false;
|
| 402 |
+
// BUG-FIX-C: unconditionally unlock on error
|
| 403 |
+
_done();
|
| 404 |
break;
|
| 405 |
|
| 406 |
case 'pong':
|
|
|
|
| 411 |
}
|
| 412 |
}
|
| 413 |
|
| 414 |
+
// ─── Thinking bubble ──────────────────────────────────────────────────────────
|
| 415 |
function appendThinking() {
|
| 416 |
if (thinkingEl) return;
|
| 417 |
thinkingEl = document.createElement('div');
|
|
|
|
| 421 |
chatBox.appendChild(thinkingEl);
|
| 422 |
chatBox.scrollTop = chatBox.scrollHeight;
|
| 423 |
}
|
|
|
|
| 424 |
function _removeThinking() {
|
| 425 |
if (thinkingEl) {
|
| 426 |
thinkingEl.remove();
|
|
|
|
| 429 |
}
|
| 430 |
|
| 431 |
// ═══════════════════════════════════════════════════════════════════════════════
|
| 432 |
+
// AUDIO PLAYBACK
|
| 433 |
// ═════════════════════���═════════════════════════════════════════════════════════
|
| 434 |
|
| 435 |
function _ctxEnsure() {
|
|
|
|
| 451 |
try {
|
| 452 |
decoded = await ctx.decodeAudioData(buf.slice(0));
|
| 453 |
} catch (e) {
|
| 454 |
+
console.warn('[Audio] decode error:', e.message);
|
| 455 |
_inFlight = Math.max(0, _inFlight - 1);
|
| 456 |
_vizQ();
|
| 457 |
return;
|
|
|
|
| 471 |
const src = ctx.createBufferSource();
|
| 472 |
src.buffer = decoded;
|
| 473 |
src.connect(ctx.destination);
|
|
|
|
| 474 |
const now = ctx.currentTime;
|
| 475 |
const start = Math.max(now + 0.01, _schedEnd);
|
| 476 |
src.start(start);
|
|
|
|
| 496 |
clearTimeout(_endTimer);
|
| 497 |
const ctx = _ctx;
|
| 498 |
if (!ctx || ctx.state === 'closed') {
|
| 499 |
+
// No audio context — unlock immediately
|
| 500 |
+
setTimeout(_done, 300);
|
| 501 |
return;
|
| 502 |
}
|
| 503 |
+
const remainingMs = Math.max(0, (_schedEnd - ctx.currentTime) * 1000);
|
| 504 |
+
// BUG-FIX-C: always call _done regardless of _cancelled — we must
|
| 505 |
+
// release the lock. Use a minimal delay when no audio was scheduled.
|
| 506 |
+
_endTimer = setTimeout(_done, remainingMs + 300);
|
| 507 |
}
|
| 508 |
|
| 509 |
+
/**
|
| 510 |
+
* _done — returns system to fully idle state.
|
| 511 |
+
* ALWAYS unlocks the mic. Never auto-restarts recording.
|
| 512 |
+
*/
|
| 513 |
function _done() {
|
| 514 |
+
_ttsPlaying = false;
|
| 515 |
isProcessing = false;
|
| 516 |
+
isRecordingLocked = false;
|
| 517 |
_inFlight = 0;
|
| 518 |
_vizQ();
|
| 519 |
+
micBtn.disabled = false;
|
| 520 |
+
setState('ready');
|
| 521 |
+
setMic('off');
|
| 522 |
+
console.log('[Voice] Idle — ready for next manual press');
|
| 523 |
}
|
| 524 |
|
| 525 |
function stopAllAudio() {
|
| 526 |
_cancelled = true;
|
| 527 |
+
_ttsPlaying = false;
|
| 528 |
clearTimeout(_endTimer);
|
| 529 |
_endTimer = null;
|
| 530 |
_schedEnd = 0;
|
|
|
|
| 547 |
|
| 548 |
function sendText() {
|
| 549 |
const text = textInput.value.trim();
|
|
|
|
| 550 |
if (!text || isProcessing) return;
|
|
|
|
| 551 |
appendMsg(text, 'user');
|
| 552 |
textInput.value = '';
|
|
|
|
|
|
|
|
|
|
| 553 |
_cancelled = false;
|
| 554 |
isProcessing = true;
|
| 555 |
tSend = Date.now();
|
| 556 |
+
tLlm = tTts = 0;
|
|
|
|
| 557 |
aiEl = null;
|
| 558 |
aiTxt = '';
|
|
|
|
| 559 |
setState('processing');
|
| 560 |
+
appendThinking();
|
| 561 |
+
_sendViaChat(text);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 562 |
}
|
| 563 |
|
| 564 |
function _sendViaChat(text) {
|
|
|
|
| 565 |
const payload = JSON.stringify({ user_id: USER_ID, user_query: text });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
if (chatWS && chatWS.readyState === WebSocket.OPEN) {
|
| 567 |
chatWS.send(payload);
|
| 568 |
} else {
|
|
|
|
| 569 |
const _retry = () => {
|
| 570 |
+
if (chatWS && chatWS.readyState === WebSocket.OPEN) chatWS.send(payload);
|
| 571 |
+
else setTimeout(_retry, 300);
|
|
|
|
|
|
|
|
|
|
| 572 |
};
|
| 573 |
_retry();
|
| 574 |
}
|
|
|
|
| 579 |
// ═══════════════════════════════════════════════════════════════════════════════
|
| 580 |
|
| 581 |
micBtn.onclick = async () => {
|
| 582 |
+
if (isRecordingLocked || isProcessing) {
|
| 583 |
+
console.log('[Mic] Ignored — system busy');
|
| 584 |
+
return;
|
| 585 |
+
}
|
| 586 |
+
if (isListening) {
|
| 587 |
+
_teardownMicHardware();
|
| 588 |
+
_resetVoiceState();
|
| 589 |
+
setState('ready');
|
| 590 |
+
setMic('off');
|
| 591 |
+
} else {
|
| 592 |
+
await startListening();
|
| 593 |
+
}
|
| 594 |
};
|
| 595 |
|
| 596 |
stopBtn.onclick = () => {
|
| 597 |
stopAllAudio();
|
| 598 |
+
if (isListening || isSpeaking) _teardownMicHardware();
|
| 599 |
+
_resetVoiceState();
|
| 600 |
+
setState('ready');
|
| 601 |
+
setMic('off');
|
| 602 |
+
micBtn.disabled = false;
|
| 603 |
};
|
| 604 |
|
| 605 |
+
// ── startListening ────────────────────────────────────────────────────────────
|
| 606 |
async function startListening() {
|
| 607 |
+
if (isListening || isProcessing || isRecordingLocked) return;
|
| 608 |
+
|
| 609 |
_ctxEnsure();
|
| 610 |
|
| 611 |
try {
|
|
|
|
| 619 |
},
|
| 620 |
});
|
| 621 |
} catch (err) {
|
| 622 |
+
console.error('[Mic] getUserMedia failed:', err);
|
| 623 |
appendMsg('⚠️ মাইক্রোফোন অ্যাক্সেস দেওয়া হয়নি।', 'system');
|
| 624 |
return;
|
| 625 |
}
|
|
|
|
| 632 |
src.connect(analyser);
|
| 633 |
|
| 634 |
isListening = true;
|
| 635 |
+
audioChunks = [];
|
| 636 |
+
|
| 637 |
setMic('listening');
|
| 638 |
setState('listening');
|
| 639 |
voiceViz.classList.add('active');
|
| 640 |
|
| 641 |
vadInt = setInterval(vadTick, VAD_MS);
|
| 642 |
vizInt = setInterval(vizTick, 60);
|
| 643 |
+
|
| 644 |
+
console.log('[Mic] Listening started');
|
| 645 |
}
|
| 646 |
|
| 647 |
+
// ── _teardownMicHardware ──────────────────────────────────────────────────────
|
| 648 |
+
// Stops hardware: intervals, recorder (silenced), mic tracks, AudioContext.
|
| 649 |
+
// IMPORTANT: does NOT clear audioChunks — caller's onstop captures them first.
|
| 650 |
+
function _teardownMicHardware() {
|
| 651 |
clearInterval(vadInt);
|
| 652 |
clearInterval(vizInt);
|
| 653 |
clearTimeout(silenceTimer);
|
| 654 |
vadInt = vizInt = silenceTimer = null;
|
| 655 |
|
| 656 |
+
// Silence callbacks so no onstop logic fires after forced teardown
|
| 657 |
+
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
| 658 |
+
mediaRecorder.ondataavailable = () => {};
|
| 659 |
+
mediaRecorder.onstop = () => {};
|
| 660 |
+
mediaRecorder.stop();
|
| 661 |
+
}
|
| 662 |
+
mediaRecorder = null;
|
| 663 |
|
| 664 |
micStream?.getTracks().forEach((t) => t.stop());
|
| 665 |
+
micStream = null;
|
| 666 |
+
|
| 667 |
+
if (analyserCtx && analyserCtx.state !== 'closed') {
|
| 668 |
+
analyserCtx.close().catch(() => {});
|
| 669 |
+
}
|
| 670 |
+
analyserCtx = null;
|
| 671 |
+
analyser = null;
|
| 672 |
|
|
|
|
|
|
|
|
|
|
| 673 |
voiceViz.classList.remove('active');
|
| 674 |
vizBars.forEach((b) => (b.style.height = '4px'));
|
| 675 |
+
|
| 676 |
+
console.log('[Mic] Hardware torn down');
|
| 677 |
}
|
| 678 |
|
| 679 |
+
// ── _resetVoiceState ──────────────────────────────────────────────────────────
|
| 680 |
+
function _resetVoiceState() {
|
| 681 |
+
isListening = false;
|
| 682 |
+
isSpeaking = false;
|
| 683 |
+
isProcessing = false;
|
| 684 |
+
isRecordingLocked = false;
|
| 685 |
+
_ttsPlaying = false;
|
| 686 |
+
_speechStartMs = 0;
|
| 687 |
+
audioChunks = [];
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
// ── VAD tick ──────────────────────────────────────────────────────────────────
|
| 691 |
function vadTick() {
|
| 692 |
if (!analyser) return;
|
| 693 |
+
if (_ttsPlaying) return; // mute during TTS playback
|
| 694 |
+
if (isProcessing || isRecordingLocked) return; // hard lock
|
| 695 |
+
|
| 696 |
const buf = new Float32Array(analyser.frequencyBinCount);
|
| 697 |
analyser.getFloatTimeDomainData(buf);
|
| 698 |
+
let sum = 0;
|
| 699 |
+
for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i];
|
| 700 |
+
const db = 20 * Math.log10(Math.sqrt(sum / buf.length) || 1e-10);
|
|
|
|
| 701 |
const speech = db > SILENCE_DB;
|
| 702 |
|
| 703 |
if (speech) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 704 |
clearTimeout(silenceTimer);
|
| 705 |
silenceTimer = null;
|
| 706 |
|
| 707 |
if (!isSpeaking) {
|
| 708 |
+
if (mediaRecorder && mediaRecorder.state !== 'inactive') return; // duplicate guard
|
| 709 |
isSpeaking = true;
|
| 710 |
+
_speechStartMs = Date.now();
|
| 711 |
_cancelled = false;
|
| 712 |
_ctxEnsure();
|
| 713 |
startRecorder();
|
| 714 |
setMic('recording');
|
| 715 |
setState('recording');
|
| 716 |
+
console.log('[VAD] Speech detected — recording');
|
| 717 |
}
|
| 718 |
} else {
|
| 719 |
if (isSpeaking && !silenceTimer) {
|
| 720 |
+
silenceTimer = setTimeout(_onSilenceTimeout, SILENCE_MS);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 721 |
}
|
| 722 |
}
|
| 723 |
}
|
| 724 |
|
| 725 |
+
// ── _onSilenceTimeout ────────────────────────────────────��────────────────────
|
| 726 |
+
function _onSilenceTimeout() {
|
| 727 |
+
silenceTimer = null;
|
| 728 |
+
|
| 729 |
+
const speechDuration = Date.now() - _speechStartMs;
|
| 730 |
+
if (speechDuration < MIN_SPEECH_MS) {
|
| 731 |
+
console.log(
|
| 732 |
+
`[VAD] Too short (${speechDuration} ms) — discard & resume listening`,
|
| 733 |
+
);
|
| 734 |
+
isSpeaking = false;
|
| 735 |
+
discardRecorder();
|
| 736 |
+
// BUG-FIX-D: restart intervals so listening continues
|
| 737 |
+
if (isListening && !vadInt) {
|
| 738 |
+
vadInt = setInterval(vadTick, VAD_MS);
|
| 739 |
+
vizInt = setInterval(vizTick, 60);
|
| 740 |
+
}
|
| 741 |
+
setMic('listening');
|
| 742 |
+
setState('listening');
|
| 743 |
+
return;
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
console.log(
|
| 747 |
+
`[VAD] Silence after ${speechDuration} ms — finalising utterance`,
|
| 748 |
+
);
|
| 749 |
+
|
| 750 |
+
// Stop VAD before stopRecorder so no new speech detection during processing
|
| 751 |
+
clearInterval(vadInt);
|
| 752 |
+
clearInterval(vizInt);
|
| 753 |
+
vadInt = vizInt = null;
|
| 754 |
+
|
| 755 |
+
// Lock state BEFORE stopRecorder (onstop may fire almost immediately)
|
| 756 |
+
isSpeaking = false;
|
| 757 |
+
isListening = false;
|
| 758 |
+
isProcessing = true;
|
| 759 |
+
isRecordingLocked = true;
|
| 760 |
+
_cancelled = false;
|
| 761 |
+
|
| 762 |
+
tSend = Date.now();
|
| 763 |
+
tLlm = 0;
|
| 764 |
+
tTts = 0;
|
| 765 |
+
|
| 766 |
+
micBtn.disabled = true;
|
| 767 |
+
setMic('processing');
|
| 768 |
+
setState('processing');
|
| 769 |
+
|
| 770 |
+
stopRecorder(); // → triggers onstop asynchronously
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
// ── Viz tick ──────────────────────────────────────────────────────────────────
|
| 774 |
function vizTick() {
|
| 775 |
if (!analyser) return;
|
| 776 |
const data = new Uint8Array(analyser.frequencyBinCount);
|
|
|
|
| 782 |
});
|
| 783 |
}
|
| 784 |
|
| 785 |
+
// ── MediaRecorder ─────────────────────────────────────────────────────────────
|
| 786 |
function startRecorder() {
|
| 787 |
if (!micStream) return;
|
| 788 |
+
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
| 789 |
+
console.warn('[Recorder] Duplicate startRecorder() — ignored');
|
| 790 |
+
return;
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
audioChunks = [];
|
| 794 |
+
_recorderMime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
|
| 795 |
? 'audio/webm;codecs=opus'
|
| 796 |
: 'audio/webm';
|
| 797 |
|
| 798 |
+
try {
|
| 799 |
+
mediaRecorder = new MediaRecorder(micStream, { mimeType: _recorderMime });
|
| 800 |
+
} catch (err) {
|
| 801 |
+
console.error('[Recorder] Creation failed:', err);
|
| 802 |
+
isSpeaking = false;
|
| 803 |
+
setMic('listening');
|
| 804 |
+
setState('listening');
|
| 805 |
+
return;
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
mediaRecorder.ondataavailable = (e) => {
|
| 809 |
+
if (e.data && e.data.size > 0) audioChunks.push(e.data);
|
| 810 |
};
|
| 811 |
+
|
| 812 |
+
/**
|
| 813 |
+
* onstop handler
|
| 814 |
+
*
|
| 815 |
+
* BUG-FIX-A: Capture audioChunks into a LOCAL variable as the very
|
| 816 |
+
* first action, before any teardown or async work. Then clear the
|
| 817 |
+
* module-level audioChunks. _teardownMicHardware() does NOT touch
|
| 818 |
+
* audioChunks, so the local copy is safe.
|
| 819 |
+
*
|
| 820 |
+
* Old (broken) order:
|
| 821 |
+
* 1. _fullMicTeardown() ← set audioChunks = [] HERE
|
| 822 |
+
* 2. new Blob(audioChunks) ← always empty!
|
| 823 |
+
*
|
| 824 |
+
* New (correct) order:
|
| 825 |
+
* 1. const captured = audioChunks.slice() ← copy before anything
|
| 826 |
+
* 2. audioChunks = [] ← clear module ref
|
| 827 |
+
* 3. _teardownMicHardware() ← safe, chunks are local
|
| 828 |
+
* 4. new Blob(captured) ← has actual audio data
|
| 829 |
+
*/
|
| 830 |
mediaRecorder.onstop = async () => {
|
| 831 |
+
// ── 1. Capture chunks locally (MUST be first) ──────────────────────────
|
| 832 |
+
const captured = audioChunks.slice();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 833 |
audioChunks = [];
|
| 834 |
+
|
| 835 |
+
// ── 2. Tear down mic hardware (safe — captured is local) ───────────────
|
| 836 |
+
_teardownMicHardware();
|
| 837 |
+
setMic('off');
|
| 838 |
+
|
| 839 |
console.log(
|
| 840 |
+
`[Recorder] onstop: ${captured.length} chunk(s), ${captured
|
| 841 |
+
.reduce((s, c) => s + c.size, 0)
|
| 842 |
+
.toLocaleString()} bytes total`,
|
| 843 |
);
|
| 844 |
|
| 845 |
+
// ── 3. Validate ────────────────────────────────────────────────────────
|
| 846 |
+
if (!captured.length) {
|
| 847 |
+
console.warn('[Recorder] No audio chunks — possible threshold issue');
|
| 848 |
+
appendMsg(
|
| 849 |
+
'⚠️ কোনো অডিও রেকর্ড হয়নি। Silence threshold কম���য়ে দেখুন।',
|
| 850 |
+
'system',
|
| 851 |
+
);
|
| 852 |
+
_resetVoiceState();
|
| 853 |
+
setState('ready');
|
| 854 |
+
micBtn.disabled = false;
|
| 855 |
+
return;
|
| 856 |
+
}
|
| 857 |
+
|
| 858 |
+
// ── 4. Build ArrayBuffer ───────────────────────────────────────────────
|
| 859 |
+
const blob = new Blob(captured, { type: _recorderMime });
|
| 860 |
+
let buf;
|
| 861 |
+
try {
|
| 862 |
+
buf = await blob.arrayBuffer();
|
| 863 |
+
} catch (err) {
|
| 864 |
+
console.error('[Recorder] arrayBuffer() error:', err);
|
| 865 |
+
_resetVoiceState();
|
| 866 |
+
setState('ready');
|
| 867 |
+
setMic('off');
|
| 868 |
+
micBtn.disabled = false;
|
| 869 |
+
return;
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
console.log(`[VAD] → voice WS: ${buf.byteLength.toLocaleString()} bytes`);
|
| 873 |
+
|
| 874 |
+
// ── 5. Send to backend ─────────────────────────────────────────────────
|
| 875 |
if (voiceWS && voiceWS.readyState === WebSocket.OPEN) {
|
| 876 |
+
appendThinking();
|
| 877 |
voiceWS.send(buf);
|
| 878 |
+
// isProcessing + isRecordingLocked stay true until _done() fires
|
| 879 |
} else {
|
| 880 |
+
console.warn('[VAD] Voice WS not open — utterance dropped');
|
| 881 |
+
appendMsg('⚠️ সার্ভারের সাথে সংযোগ নেই — আবার চেষ্টা করুন।', 'system');
|
| 882 |
+
_resetVoiceState();
|
| 883 |
+
setState('ready');
|
| 884 |
+
setMic('off');
|
| 885 |
+
micBtn.disabled = false;
|
| 886 |
}
|
| 887 |
};
|
| 888 |
+
|
| 889 |
mediaRecorder.start();
|
| 890 |
+
console.log('[Recorder] Started, mime:', _recorderMime);
|
| 891 |
}
|
| 892 |
|
| 893 |
function stopRecorder() {
|
| 894 |
+
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
| 895 |
+
mediaRecorder.stop(); // triggers onstop asynchronously
|
| 896 |
+
}
|
| 897 |
}
|
| 898 |
|
| 899 |
function discardRecorder() {
|
| 900 |
+
if (!mediaRecorder || mediaRecorder.state === 'inactive') {
|
| 901 |
+
audioChunks = [];
|
| 902 |
+
return;
|
| 903 |
+
}
|
| 904 |
mediaRecorder.ondataavailable = () => {};
|
| 905 |
mediaRecorder.onstop = () => {
|
| 906 |
audioChunks = [];
|
| 907 |
};
|
| 908 |
mediaRecorder.stop();
|
| 909 |
mediaRecorder = null;
|
| 910 |
+
audioChunks = [];
|
| 911 |
}
|
| 912 |
|
| 913 |
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
| 932 |
off: { cls: 'mic-off', label: 'Voice শুরু করুন', icon: '🎤' },
|
| 933 |
listening: {
|
| 934 |
cls: 'mic-listening',
|
| 935 |
+
label: 'শুনছি… (বাতিল করতে ক্লিক)',
|
| 936 |
icon: '🟢',
|
| 937 |
},
|
| 938 |
recording: { cls: 'mic-recording', label: 'বলছেন…', icon: '🔴' },
|
|
|
|
| 959 |
return d;
|
| 960 |
}
|
| 961 |
|
|
|
|
| 962 |
clearBtn.onclick = () => {
|
| 963 |
chatBox.innerHTML = '';
|
| 964 |
+
thinkingEl = null;
|
| 965 |
appendMsg('চ্যাট পরিষ্কার করা হয়েছে।', 'system');
|
| 966 |
};
|
| 967 |
|
|
|
|
| 968 |
sidebarToggle.onclick = () => {
|
| 969 |
sidebarEl.classList.toggle('collapsed');
|
| 970 |
sidebarToggle.textContent = sidebarEl.classList.contains('collapsed')
|
|
|
|
| 973 |
};
|
| 974 |
mobileMenuBtn.onclick = () => sidebarEl.classList.toggle('mobile-open');
|
| 975 |
|
|
|
|
| 976 |
sThreshold.value = SILENCE_DB;
|
| 977 |
sThresholdVal.textContent = SILENCE_DB + ' dB';
|
| 978 |
sThreshold.oninput = () => {
|
|
|
|
| 989 |
|
| 990 |
sVoice.onchange = () => appendMsg('🔊 TTS voice: ' + sVoice.value, 'system');
|
| 991 |
|
|
|
|
| 992 |
setInterval(() => {
|
| 993 |
if (_inFlight > 0) _vizQ();
|
| 994 |
}, 140);
|
| 995 |
|
| 996 |
// ═════════════════════════════════════════════════════════════════════════════���═
|
| 997 |
+
// BOOT
|
| 998 |
// ═══════════════════════════════════════════════════════════════════════════════
|
| 999 |
boot();
|
tmp.ipynb
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "code",
|
| 5 |
+
"execution_count": 1,
|
| 6 |
+
"id": "5cbff6ce",
|
| 7 |
+
"metadata": {},
|
| 8 |
+
"outputs": [],
|
| 9 |
+
"source": [
|
| 10 |
+
"import aiosqlite, json\n",
|
| 11 |
+
"async with aiosqlite.connect('core/daa.db') as conn:\n",
|
| 12 |
+
" doctors_data = [\n",
|
| 13 |
+
" ('Dr. Ahmed Hasan', 'Cardiologist', json.dumps(['Saturday', 'Monday', 'Thursday']), '5 PM - 9 PM', 1200),\n",
|
| 14 |
+
" ('Dr. Nusrat Jahan', 'Neurologist', json.dumps(['Sunday', 'Tuesday', 'Wednesday']), '4 PM - 8 PM', 1500),\n",
|
| 15 |
+
" ('Dr. Tanvir Islam', 'Orthopedics', json.dumps(['Saturday', 'Tuesday', 'Friday']), '6 PM - 10 PM', 1000),\n",
|
| 16 |
+
" ('Dr. Farzana Rahman', 'Gastrologist', json.dumps(['Sunday', 'Monday', 'Thursday']), '10 AM - 2 PM', 900),\n",
|
| 17 |
+
" ('Dr. Mahmudul Karim', 'Cardiologist', json.dumps(['Monday', 'Wednesday', 'Saturday']), '3 PM - 7 PM', 800),\n",
|
| 18 |
+
"\n",
|
| 19 |
+
" ('Dr. Sabiha Noor', 'Neurologist', json.dumps(['Saturday', 'Sunday', 'Tuesday']), '9 AM - 1 PM', 700),\n",
|
| 20 |
+
" ('Dr. Rakib Hossain', 'Orthopedics', json.dumps(['Sunday', 'Wednesday', 'Thursday']), '5 PM - 9 PM', 850),\n",
|
| 21 |
+
" ('Dr. Imran Kabir', 'Gastrologist', json.dumps(['Saturday', 'Monday', 'Tuesday']), '7 PM - 10 PM', 1300),\n",
|
| 22 |
+
" ('Dr. Tania Sultana', 'Cardiologist', json.dumps(['Monday', 'Thursday', 'Friday']), '11 AM - 3 PM', 950),\n",
|
| 23 |
+
" ('Dr. Faisal Ahmed', 'Neurologist', json.dumps(['Saturday', 'Wednesday', 'Thursday']), '6 PM - 9 PM', 600),\n",
|
| 24 |
+
"\n",
|
| 25 |
+
" ('Dr. Sharmin Akter', 'Orthopedics', json.dumps(['Sunday', 'Tuesday', 'Friday']), '2 PM - 6 PM', 1400),\n",
|
| 26 |
+
" ('Dr. Rezaul Karim', 'Gastrologist', json.dumps(['Monday', 'Wednesday', 'Thursday']), '5 PM - 8 PM', 1600),\n",
|
| 27 |
+
" ('Dr. Jannatul Ferdous', 'Cardiologist', json.dumps(['Saturday', 'Tuesday', 'Wednesday']), '4 PM - 7 PM', 1100),\n",
|
| 28 |
+
" ('Dr. Arif Hossain', 'Neurologist', json.dumps(['Sunday', 'Thursday', 'Monday']), '5 PM - 9 PM', 1000),\n",
|
| 29 |
+
" ('Dr. Sadia Islam', 'Orthopedics', json.dumps(['Saturday', 'Wednesday', 'Tuesday']), '10 AM - 1 PM', 500),\n",
|
| 30 |
+
"\n",
|
| 31 |
+
" ('Dr. Kamrul Hasan', 'Gastrologist', json.dumps(['Monday', 'Thursday', 'Saturday']), '6 PM - 10 PM', 1700),\n",
|
| 32 |
+
" ('Dr. Mehnaz Chowdhury', 'Cardiologist', json.dumps(['Sunday', 'Tuesday', 'Thursday']), '9 AM - 12 PM', 650),\n",
|
| 33 |
+
" ('Dr. Shahriar Alam', 'Neurologist', json.dumps(['Saturday', 'Monday', 'Wednesday']), '3 PM - 7 PM', 1450),\n",
|
| 34 |
+
" ('Dr. Tamanna Rahim', 'Orthopedics', json.dumps(['Tuesday', 'Thursday', 'Friday']), '4 PM - 8 PM', 900),\n",
|
| 35 |
+
" ('Dr. Nayeem Islam', 'Gastrologist', json.dumps(['Sunday', 'Wednesday', 'Saturday']), '5 PM - 8 PM', 2000),\n",
|
| 36 |
+
"\n",
|
| 37 |
+
" ('Dr. Arafat Hossain', 'Cardiologist', json.dumps(['Saturday', 'Monday', 'Tuesday']), '5 PM - 9 PM', 1200),\n",
|
| 38 |
+
" ('Dr. Hridoy Islam', 'Neurologist', json.dumps(['Sunday', 'Thursday', 'Wednesday']), '4 PM - 8 PM', 1500),\n",
|
| 39 |
+
" ('Dr. Mahin Rahman', 'Orthopedics', json.dumps(['Saturday', 'Wednesday', 'Friday']), '6 PM - 10 PM', 1000),\n",
|
| 40 |
+
" ('Dr. Sazzad Hossain', 'Gastrologist', json.dumps(['Sunday', 'Tuesday', 'Thursday']), '10 AM - 2 PM', 900),\n",
|
| 41 |
+
" ('Dr. Iftekhar Alam', 'Cardiologist', json.dumps(['Monday', 'Thursday', 'Saturday']), '3 PM - 7 PM', 800),\n",
|
| 42 |
+
"\n",
|
| 43 |
+
" ('Dr. Tanvir Mahmud', 'Neurologist', json.dumps(['Saturday', 'Tuesday', 'Wednesday']), '9 AM - 1 PM', 700),\n",
|
| 44 |
+
" ('Dr. Omar Faruk', 'Orthopedics', json.dumps(['Sunday', 'Wednesday', 'Thursday']), '5 PM - 9 PM', 850),\n",
|
| 45 |
+
" ('Dr. Abdullah Al Mamun', 'Gastrologist', json.dumps(['Saturday', 'Monday', 'Friday']), '7 PM - 10 PM', 1300),\n",
|
| 46 |
+
" ('Dr. Shahadat Hossain', 'Cardiologist', json.dumps(['Monday', 'Thursday', 'Tuesday']), '11 AM - 3 PM', 950),\n",
|
| 47 |
+
" ('Dr. Mahfuzur Rahman', 'Neurologist', json.dumps(['Saturday', 'Thursday', 'Sunday']), '6 PM - 9 PM', 600),\n",
|
| 48 |
+
"\n",
|
| 49 |
+
" ('Dr. Alif Islam', 'Orthopedics', json.dumps(['Sunday', 'Tuesday', 'Wednesday']), '2 PM - 6 PM', 1400),\n",
|
| 50 |
+
" ('Dr. Nabil Ahmed', 'Gastrologist', json.dumps(['Monday', 'Wednesday', 'Thursday']), '5 PM - 8 PM', 1600),\n",
|
| 51 |
+
" ('Dr. Rafiq Hasan', 'Cardiologist', json.dumps(['Saturday', 'Tuesday', 'Friday']), '4 PM - 7 PM', 1100),\n",
|
| 52 |
+
" ('Dr. Samiul Islam', 'Neurologist', json.dumps(['Sunday', 'Thursday', 'Monday']), '5 PM - 9 PM', 1000),\n",
|
| 53 |
+
" ('Dr. Towhidur Rahman', 'Orthopedics', json.dumps(['Saturday', 'Wednesday', 'Tuesday']), '10 AM - 1 PM', 500),\n",
|
| 54 |
+
"\n",
|
| 55 |
+
" ('Dr. Kawsar Ahmed', 'Gastrologist', json.dumps(['Monday', 'Thursday', 'Saturday']), '6 PM - 10 PM', 1700),\n",
|
| 56 |
+
" ('Dr. Imtiaz Hossain', 'Cardiologist', json.dumps(['Sunday', 'Tuesday', 'Wednesday']), '9 AM - 12 PM', 650),\n",
|
| 57 |
+
" ('Dr. Masud Rana', 'Neurologist', json.dumps(['Saturday', 'Monday', 'Thursday']), '3 PM - 7 PM', 1450),\n",
|
| 58 |
+
" ('Dr. Nazmul Hasan', 'Orthopedics', json.dumps(['Tuesday', 'Thursday', 'Friday']), '4 PM - 8 PM', 900),\n",
|
| 59 |
+
" ('Dr. Sajib Islam', 'Gastrologist', json.dumps(['Sunday', 'Wednesday', 'Monday']), '5 PM - 8 PM', 2000),\n",
|
| 60 |
+
"\n",
|
| 61 |
+
" ('Dr. Rakib Uddin', 'Cardiologist', json.dumps(['Saturday', 'Monday', 'Thursday']), '5 PM - 9 PM', 1200),\n",
|
| 62 |
+
" ('Dr. Jewel Rana', 'Neurologist', json.dumps(['Sunday', 'Thursday', 'Tuesday']), '4 PM - 8 PM', 1500),\n",
|
| 63 |
+
" ('Dr. Arman Hossain', 'Orthopedics', json.dumps(['Saturday', 'Wednesday', 'Friday']), '6 PM - 10 PM', 1000),\n",
|
| 64 |
+
" ('Dr. Tanmoy Islam', 'Gastrologist', json.dumps(['Sunday', 'Tuesday', 'Thursday']), '10 AM - 2 PM', 900),\n",
|
| 65 |
+
" ('Dr. Saiful Islam', 'Cardiologist', json.dumps(['Monday', 'Thursday', 'Wednesday']), '3 PM - 7 PM', 800),\n",
|
| 66 |
+
"\n",
|
| 67 |
+
" ('Dr. Mithun Hasan', 'Neurologist', json.dumps(['Saturday', 'Tuesday', 'Friday']), '9 AM - 1 PM', 700),\n",
|
| 68 |
+
" ('Dr. Shuvo Ahmed', 'Orthopedics', json.dumps(['Sunday', 'Wednesday', 'Thursday']), '5 PM - 9 PM', 850),\n",
|
| 69 |
+
" ('Dr. Farid Hasan', 'Gastrologist', json.dumps(['Saturday', 'Monday', 'Tuesday']), '7 PM - 10 PM', 1300),\n",
|
| 70 |
+
" ('Dr. Al Amin', 'Cardiologist', json.dumps(['Monday', 'Thursday', 'Friday']), '11 AM - 3 PM', 950),\n",
|
| 71 |
+
" ('Dr. Rashedul Islam', 'Neurologist', json.dumps(['Saturday', 'Thursday', 'Wednesday']), '6 PM - 9 PM', 600),\n",
|
| 72 |
+
"\n",
|
| 73 |
+
" ('Dr. Ayaan Rahman', 'Dentist', json.dumps(['Sunday', 'Tuesday']), '10:00 AM - 02:00 PM', 800),\n",
|
| 74 |
+
" ('Dr. Meher Islam', 'Eye Specialist', json.dumps(['Monday', 'Wednesday']), '11:00 AM - 03:00 PM', 1200),\n",
|
| 75 |
+
" ('Dr. Samiul Karim', 'Child Specialist', json.dumps(['Friday', 'Saturday']), '09:00 AM - 01:00 PM', 1000),\n",
|
| 76 |
+
" ('Dr. Nabila Ahmed', 'Dentist', json.dumps(['Sunday', 'Thursday']), '04:00 PM - 08:00 PM', 900),\n",
|
| 77 |
+
" ('Dr. Tanvir Hossain', 'Eye Specialist', json.dumps(['Tuesday', 'Thursday']), '10:00 AM - 01:00 PM', 1100),\n",
|
| 78 |
+
" ('Dr. Farzana Chowdhury', 'Child Specialist', json.dumps(['Monday', 'Friday']), '02:00 PM - 06:00 PM', 950),\n",
|
| 79 |
+
" ('Dr. Rafiq Hasan', 'Dentist', json.dumps(['Wednesday', 'Saturday']), '12:00 PM - 04:00 PM', 850),\n",
|
| 80 |
+
" ('Dr. Jannat Karim', 'Eye Specialist', json.dumps(['Sunday', 'Monday']), '09:00 AM - 12:00 PM', 1300),\n",
|
| 81 |
+
" ('Dr. Imran Ali', 'Child Specialist', json.dumps(['Tuesday', 'Friday']), '03:00 PM - 07:00 PM', 1000),\n",
|
| 82 |
+
" ('Dr. Sumaiya Noor', 'Dentist', json.dumps(['Thursday', 'Saturday']), '10:00 AM - 02:00 PM', 800),\n",
|
| 83 |
+
"\n",
|
| 84 |
+
" ('Dr. Arif Uddin', 'Eye Specialist', json.dumps(['Monday', 'Thursday']), '11:00 AM - 03:00 PM', 1250),\n",
|
| 85 |
+
" ('Dr. Nadia Sultana', 'Child Specialist', json.dumps(['Sunday', 'Wednesday']), '09:00 AM - 01:00 PM', 980),\n",
|
| 86 |
+
" ('Dr. Mahmud Rahman', 'Dentist', json.dumps(['Tuesday', 'Friday']), '04:00 PM - 08:00 PM', 870),\n",
|
| 87 |
+
" ('Dr. Laila Akter', 'Eye Specialist', json.dumps(['Wednesday', 'Saturday']), '10:00 AM - 02:00 PM', 1150),\n",
|
| 88 |
+
" ('Dr. Shafiq Hasan', 'Child Specialist', json.dumps(['Monday', 'Thursday']), '02:00 PM - 06:00 PM', 1020),\n",
|
| 89 |
+
" ('Dr. Nayeem Islam', 'Dentist', json.dumps(['Sunday', 'Tuesday']), '09:00 AM - 12:00 PM', 820),\n",
|
| 90 |
+
" ('Dr. Amina Rahman', 'Eye Specialist', json.dumps(['Friday', 'Saturday']), '12:00 PM - 04:00 PM', 1400),\n",
|
| 91 |
+
" ('Dr. Rakib Hossain', 'Child Specialist', json.dumps(['Tuesday', 'Wednesday']), '10:00 AM - 01:00 PM', 990),\n",
|
| 92 |
+
" ('Dr. Tania Sultana', 'Dentist', json.dumps(['Thursday', 'Friday']), '03:00 PM - 07:00 PM', 860),\n",
|
| 93 |
+
" ('Dr. Anik Das', 'Eye Specialist', json.dumps(['Sunday', 'Wednesday']), '11:00 AM - 03:00 PM', 1200),\n",
|
| 94 |
+
"\n",
|
| 95 |
+
" ('Dr. Priya Roy', 'Child Specialist', json.dumps(['Monday', 'Saturday']), '09:00 AM - 01:00 PM', 1050),\n",
|
| 96 |
+
" ('Dr. Kamal Uddin', 'Dentist', json.dumps(['Tuesday', 'Thursday']), '02:00 PM - 06:00 PM', 780),\n",
|
| 97 |
+
" ('Dr. Sabina Yasmin', 'Eye Specialist', json.dumps(['Wednesday', 'Friday']), '10:00 AM - 02:00 PM', 1350),\n",
|
| 98 |
+
" ('Dr. Sohel Rana', 'Child Specialist', json.dumps(['Sunday', 'Tuesday']), '04:00 PM - 08:00 PM', 970),\n",
|
| 99 |
+
" ('Dr. Nusrat Jahan', 'Dentist', json.dumps(['Monday', 'Friday']), '11:00 AM - 03:00 PM', 890),\n",
|
| 100 |
+
"\n",
|
| 101 |
+
" ('Dr. Mahfuz Alam', 'Eye Specialist', json.dumps(['Thursday', 'Saturday']), '09:00 AM - 12:00 PM', 1250),\n",
|
| 102 |
+
" ('Dr. Shanta Akter', 'Child Specialist', json.dumps(['Tuesday', 'Friday']), '01:00 PM - 05:00 PM', 1000),\n",
|
| 103 |
+
" ('Dr. Imtiaz Khan', 'Dentist', json.dumps(['Sunday', 'Wednesday']), '10:00 AM - 02:00 PM', 840),\n",
|
| 104 |
+
" ('Dr. Farid Hasan', 'Eye Specialist', json.dumps(['Monday', 'Thursday']), '03:00 PM - 07:00 PM', 1180),\n",
|
| 105 |
+
" ('Dr. Rina Begum', 'Child Specialist', json.dumps(['Wednesday', 'Saturday']), '09:00 AM - 01:00 PM', 990),\n",
|
| 106 |
+
"\n",
|
| 107 |
+
" ('Dr. Ashikur Rahman', 'Dentist', json.dumps(['Tuesday', 'Friday']), '12:00 PM - 04:00 PM', 870),\n",
|
| 108 |
+
" ('Dr. Monira Sultana', 'Eye Specialist', json.dumps(['Sunday', 'Thursday']), '10:00 AM - 02:00 PM', 1300),\n",
|
| 109 |
+
" ('Dr. Hasan Mahmud', 'Child Specialist', json.dumps(['Monday', 'Wednesday']), '02:00 PM - 06:00 PM', 1010),\n",
|
| 110 |
+
" ('Dr. Shakil Ahmed', 'Dentist', json.dumps(['Friday', 'Saturday']), '09:00 AM - 12:00 PM', 800),\n",
|
| 111 |
+
" ('Dr. Tahmina Akhter', 'Eye Specialist', json.dumps(['Tuesday', 'Thursday']), '04:00 PM - 08:00 PM', 1250),\n",
|
| 112 |
+
"\n",
|
| 113 |
+
" ('Dr. Rifat Hossain', 'Child Specialist', json.dumps(['Sunday', 'Monday']), '11:00 AM - 03:00 PM', 980),\n",
|
| 114 |
+
" ('Dr. Sadiya Islam', 'Dentist', json.dumps(['Wednesday', 'Friday']), '10:00 AM - 02:00 PM', 860),\n",
|
| 115 |
+
" ('Dr. Farhan Karim', 'Eye Specialist', json.dumps(['Saturday', 'Tuesday']), '01:00 PM - 05:00 PM', 1400),\n",
|
| 116 |
+
" ('Dr. Nargis Begum', 'Child Specialist', json.dumps(['Thursday', 'Friday']), '09:00 AM - 12:00 PM', 990),\n",
|
| 117 |
+
" ('Dr. Zubair Rahman', 'Dentist', json.dumps(['Monday', 'Saturday']), '03:00 PM - 07:00 PM', 900),\n",
|
| 118 |
+
"\n",
|
| 119 |
+
" ('Dr. Iftekhar Alam', 'Eye Specialist', json.dumps(['Sunday', 'Tuesday']), '10:00 AM - 01:00 PM', 1200),\n",
|
| 120 |
+
" ('Dr. Shabnam Akter', 'Child Specialist', json.dumps(['Wednesday', 'Thursday']), '02:00 PM - 06:00 PM', 970),\n",
|
| 121 |
+
" ('Dr. Arman Hossain', 'Dentist', json.dumps(['Friday', 'Monday']), '11:00 AM - 03:00 PM', 830),\n",
|
| 122 |
+
" ('Dr. Mehzabin Noor', 'Eye Specialist', json.dumps(['Tuesday', 'Saturday']), '09:00 AM - 01:00 PM', 1350),\n",
|
| 123 |
+
" ('Dr. Rakib Hasan', 'Child Specialist', json.dumps(['Sunday', 'Friday']), '04:00 PM - 08:00 PM', 1000)\n",
|
| 124 |
+
" ]\n",
|
| 125 |
+
"\n",
|
| 126 |
+
" await conn.executemany(\"\"\"\n",
|
| 127 |
+
" INSERT INTO doctors (doctor_name, category, visiting_days, visiting_time, visiting_money)\n",
|
| 128 |
+
" VALUES (?, ?, ?, ?, ?)\n",
|
| 129 |
+
" \"\"\", doctors_data)\n",
|
| 130 |
+
"\n",
|
| 131 |
+
" await conn.commit()"
|
| 132 |
+
]
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"cell_type": "code",
|
| 136 |
+
"execution_count": null,
|
| 137 |
+
"id": "444a1052",
|
| 138 |
+
"metadata": {},
|
| 139 |
+
"outputs": [],
|
| 140 |
+
"source": []
|
| 141 |
+
}
|
| 142 |
+
],
|
| 143 |
+
"metadata": {
|
| 144 |
+
"kernelspec": {
|
| 145 |
+
"display_name": "langgraph",
|
| 146 |
+
"language": "python",
|
| 147 |
+
"name": "python3"
|
| 148 |
+
},
|
| 149 |
+
"language_info": {
|
| 150 |
+
"codemirror_mode": {
|
| 151 |
+
"name": "ipython",
|
| 152 |
+
"version": 3
|
| 153 |
+
},
|
| 154 |
+
"file_extension": ".py",
|
| 155 |
+
"mimetype": "text/x-python",
|
| 156 |
+
"name": "python",
|
| 157 |
+
"nbconvert_exporter": "python",
|
| 158 |
+
"pygments_lexer": "ipython3",
|
| 159 |
+
"version": "3.13.13"
|
| 160 |
+
}
|
| 161 |
+
},
|
| 162 |
+
"nbformat": 4,
|
| 163 |
+
"nbformat_minor": 5
|
| 164 |
+
}
|