rakib72642 commited on
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.

Files changed (5) hide show
  1. app.py +2 -2
  2. core/backend.py +56 -40
  3. frontend/index.html +2 -2
  4. frontend/script.js +289 -206
  5. 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 = False
56
- USE_OLLAMA = True
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 get_doctor_categories() -> str:
107
  """
108
- Fetch all unique doctor categories from the database.
 
 
 
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 all doctors available on a specific visiting day.
141
- Example inputs:
142
- - Sunday
143
- - Monday
144
- - Friday
 
 
145
  """
146
 
147
  db_path = get_db_path()
148
 
149
  query = """
150
- SELECT *
151
- FROM doctors
152
- WHERE LOWER(visiting_days) LIKE ?
153
  """
154
 
155
- param = [f"%{visiting_day.lower()}%"]
 
 
 
 
 
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"Please arrive 10 minutes early."
 
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 10 minutes early."
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 available for scheduling
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.0-flash",
419
- temperature=0.3,
420
  )
421
  elif use_ollama:
422
- self.llm = ChatOllama(model="gemma4:e4b", streaming=True, temperature=0.2)
423
  else:
424
  # Local fallback — extend as needed
425
- self.llm = ChatOllama(model="gemma4:e4b", streaming=True, temperature=0.2)
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
- get_doctor_categories,
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
- // location.hostname === 'localhost' || location.hostname === '127.0.0.1'
83
- // ? `http://${location.hostname}:8679` // ← FIXED: was 8679
84
- // : `http://${location.host}`;
85
 
86
- console.log('WebSocket base URL:', WS_BASE); // FIX-6: log WS base URL for debugging
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 = 450; // was 1000 (too slow)
99
- let SILENCE_DB = -38; // slightly more sensitive
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; // current AI message div
124
- let aiTxt = ''; // accumulated raw markdown for this turn
125
- let thinkingEl = null; // FIX-3: "..." thinking bubble
126
 
127
  // ─── Latency timestamps ───────────────────────────────────────────────────────
128
  let tSend = 0,
@@ -131,7 +104,7 @@ let tSend = 0,
131
  tTts = 0;
132
 
133
  // ═══════════════════════════════════════════════════════════════════════════════
134
- // INIT OVERLAY — 2-gate: both WS-ready AND stage animations done
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 — silent auto-reconnect, exponential backoff
207
  // ═══════════════════════════════════════════════════════════════════════════════
208
 
209
- function _backoff(retries) {
210
- return Math.min(1000 * Math.pow(2, retries), 16000);
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 to', `${WS_BASE}/ws/chat`); // FIX-6
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}), retry #${_chatRetry + 1}`);
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}), retry #${_voiceRetry + 1}`);
274
  _setSysStatus(false);
275
-
276
  if (!_initClosed) {
277
  _wsGate = true;
278
  _tryClose();
279
  }
280
-
281
- if (isListening) stopListening();
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(); // FIX-3: remove "..." bubble on first token
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(); // FIX-3
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(); // FIX-3: safety cleanup
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(); // FIX-3
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] user_id ack:', msg.user_id);
396
  break;
397
 
398
  case 'stt':
399
  tStt = Date.now();
400
  if (tSend > 0) mStt.textContent = tStt - tSend + ' ms';
401
- _removeThinking(); // FIX-3
402
  appendMsg('🎤 ' + msg.text, 'user');
403
  aiEl = null;
404
  aiTxt = '';
405
- appendThinking(); // FIX-3: show "..." while LLM runs
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(); // FIX-3: remove on first token
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(); // FIX-3
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(); // FIX-3
448
  appendMsg('⚠️ ' + msg.text, 'system');
449
  aiEl = null;
450
  aiTxt = '';
451
  isProcessing = false;
452
- setState(isListening ? 'listening' : 'ready');
 
453
  break;
454
 
455
  case 'pong':
@@ -460,7 +411,7 @@ function onVoiceMsg(ev) {
460
  }
461
  }
462
 
463
- // ─── FIX-3: Thinking bubble helpers ──────────────────────────────────────────
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 — gapless Web Audio API
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
- _done();
 
551
  return;
552
  }
553
- const wait = Math.max(0, (_schedEnd - ctx.currentTime) * 1000) + 280;
554
- _endTimer = setTimeout(() => {
555
- if (!_cancelled) _done();
556
- }, wait);
557
  }
558
 
 
 
 
 
559
  function _done() {
 
560
  isProcessing = false;
 
561
  _inFlight = 0;
562
  _vizQ();
563
- setState(isListening ? 'listening' : 'ready');
 
 
 
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(); // FIX-3: show "..." bubble immediately
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
- chatWS.send(payload);
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 (isListening) stopListening();
653
- else await startListening();
 
 
 
 
 
 
 
 
 
 
654
  };
655
 
656
  stopBtn.onclick = () => {
657
  stopAllAudio();
658
- isProcessing = false;
659
- setState(isListening ? 'listening' : 'ready');
 
 
 
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
- function stopListening() {
 
 
 
698
  clearInterval(vadInt);
699
  clearInterval(vizInt);
700
  clearTimeout(silenceTimer);
701
  vadInt = vizInt = silenceTimer = null;
702
 
703
- if (isSpeaking) discardRecorder();
704
- stopAllAudio();
 
 
 
 
 
705
 
706
  micStream?.getTracks().forEach((t) => t.stop());
707
- analyserCtx?.close().catch(() => {});
708
- micStream = analyserCtx = analyser = null;
 
 
 
 
 
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
- // ── VAD ────────────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
718
  function vadTick() {
719
  if (!analyser) return;
 
 
 
720
  const buf = new Float32Array(analyser.frequencyBinCount);
721
  analyser.getFloatTimeDomainData(buf);
722
-
723
- let s = 0;
724
- for (let i = 0; i < buf.length; i++) s += buf[i] * buf[i];
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
- // ── Viz tick ───────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- const mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
779
  ? 'audio/webm;codecs=opus'
780
  : 'audio/webm';
781
 
782
- mediaRecorder = new MediaRecorder(micStream, { mimeType: mime });
 
 
 
 
 
 
 
 
 
783
  mediaRecorder.ondataavailable = (e) => {
784
- if (e.data.size > 0) audioChunks.push(e.data);
785
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
786
  mediaRecorder.onstop = async () => {
787
- if (!audioChunks.length) {
788
- isProcessing = false;
789
- if (isListening) setState('listening');
790
- return;
791
- }
792
- const blob = new Blob(audioChunks, { type: mime });
793
  audioChunks = [];
794
- const buf = await blob.arrayBuffer();
 
 
 
 
795
  console.log(
796
- `[VAD] sending ${buf.byteLength.toLocaleString()} bytes to voice WS`,
 
 
797
  );
798
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
799
  if (voiceWS && voiceWS.readyState === WebSocket.OPEN) {
800
- appendThinking(); // FIX-3: show thinking while STT runs
801
  voiceWS.send(buf);
 
802
  } else {
803
- console.warn('[VAD] voice WS not open — dropping utterance');
804
- isProcessing = false;
805
- if (isListening) setState('listening');
 
 
 
806
  }
807
  };
 
808
  mediaRecorder.start();
 
809
  }
810
 
811
  function stopRecorder() {
812
- if (mediaRecorder && mediaRecorder.state !== 'inactive') mediaRecorder.stop();
813
- mediaRecorder = null;
 
814
  }
815
 
816
  function discardRecorder() {
817
- if (!mediaRecorder || mediaRecorder.state === 'inactive') return;
 
 
 
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; // FIX-3: reset reference after clear
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
- // START
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
+ }