archivartaunik commited on
Commit
e828937
·
verified ·
1 Parent(s): b9afb16

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +238 -74
app.py CHANGED
@@ -67,32 +67,54 @@ def fetch_mp3_by_unique_id(unique_id: str) -> Tuple[str, str]:
67
  # ─────────────────────────────────────────────────────────────────────────────
68
  PROMPT_TEMPLATES = {
69
  "simple": (
70
- "You are a call-center conversation analyst for a medical clinic. From the call recording, provide a brief summary:\n"
71
- "- Purpose of the call (appointment / results / complaint / billing / other).\n"
72
- "- Patient intent and expectations.\n"
73
- "- Outcome (booked / call-back / routed / unresolved).\n"
74
- "- Next steps (owner and when).\n"
75
- "- Patient emotion (1–5) and agent tone (1–5).\n"
76
- "- Alerts: urgency/risks/privacy.\n\n"
 
 
 
 
 
 
 
 
77
  "Keep it short (6–8 lines). End with a line: ‘Service quality rating: X/5’ and one sentence explaining the rating."
78
  ),
79
  "medium": (
80
- "Act as a senior service analyst. Analyze the call using this structure:\n"
81
- "1) Quick overview: reason for the call, intent, key facts, urgency (low/medium/high).\n"
82
- "2) Call flow (2–4 bullets): what was asked/answered, where friction occurred.\n"
83
- "3) Outcomes & tasks: concrete next actions for clinic/patient with timeframes.\n"
84
- "4) Emotions & empathy: patient mood; agent empathy (0–5).\n"
85
- "5) Procedural compliance: identity verification, disclosure of recording (if stated), no off-protocol medical advice, data accuracy.\n"
 
 
 
 
 
 
86
  "6) Quality rating (0–100) using rubric: greeting, verification, accuracy, empathy, issue resolution (each 0–20)."
87
  ),
88
  "detailed": (
89
- "You are a quality & operations analyst. Provide an in-depth analysis:\n"
90
- "A) Segmentation: split the call into stages with approximate timestamps (if available) and roles (Patient/Agent).\n"
91
- "B) Structured data for booking: full name (if stated), date of birth, phone, symptoms/complaints (list), onset/duration, possible pain level 0–10 (if mentioned), required specialist/service, preferred time windows, constraints.\n"
92
- "C) Triage & risks: class (routine/urgent/emergency), red flags, whether immediate escalation is needed.\n"
93
- "D) Compliance audit: identity/privacy checks, recording disclosure, consent to data processing, booking policies.\n"
94
- "E) Conversation metrics: talk ratio (agent/patient), interruptions, long pauses, notable keywords.\n"
95
- "F) Coaching for the agent: 3–5 concrete improvements with sample phrasing.\n\n"
 
 
 
 
 
 
 
 
96
  "Deliver: (1) A short patient-chart summary (2–3 sentences). (2) A task table with columns: priority, owner, due."
97
  ),
98
  }
@@ -239,12 +261,26 @@ def _filter_calls(df: pd.DataFrame, t_from: Optional[str], t_to: Optional[str],
239
  if col:
240
  # exact match (case-insensitive)
241
  mask = mask & (work[col].astype(str).str.lower() == ct.lower())
242
- else:
243
- # if there is no calltype column, keep only time filter
244
- pass
245
 
246
  return work.loc[mask].drop(columns=[c for c in ["_Start_dt", "_Start_time_sec"] if c in work.columns])
247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  # ─────────────────────────────────────────────────────────────────────────────
249
  # Gradio handlers
250
  # ─────────────────────────────────────────────────────────────────────────────
@@ -264,7 +300,8 @@ def ui_fetch_calls(date_str: str):
264
  body = e.response.text[:800]
265
  except Exception:
266
  pass
267
- return pd.DataFrame(), gr.update(choices=[], value=None), f"HTTP error: {e}\n{body}"
 
268
  except Exception as e:
269
  return pd.DataFrame(), gr.update(choices=[], value=None), f"Load error: {e}"
270
 
@@ -279,7 +316,7 @@ def ui_play_audio(selected_idx: Optional[int], df: pd.DataFrame):
279
  unique_id = str(row.get("UniqueId"))
280
  try:
281
  fpath = f"/tmp/call_{unique_id}.mp3"
282
- url_used = f"{BASE_URL}/calllogs/{CLIENT_ID}/{unique_id}"
283
  # Download only if not exists (avoid re-fetch)
284
  if not os.path.exists(fpath) or os.path.getsize(fpath) == 0:
285
  fpath, url_used = fetch_mp3_by_unique_id(unique_id)
@@ -348,18 +385,28 @@ def ui_analyze(selected_idx: Optional[int], df: pd.DataFrame,
348
 
349
  # Call model
350
  try:
351
- merged = f"""[SYSTEM INSTRUCTION: {sys_inst}]\n\n{prompt}"""
 
 
352
  resp = client.models.generate_content(model=model_name, contents=[uploaded_file, merged])
353
  text = getattr(resp, "text", None)
354
  if not text:
355
  return "Analysis finished but returned no text. Check model settings and file format."
356
- return f"### Analysis result\n\n{text}"
 
 
 
 
 
 
357
  except Exception as e:
358
  # Try to attach more error details
359
  msg = str(e)
360
  try:
361
  if hasattr(e, "args") and e.args:
362
- msg = msg + "\n\n" + str(e.args[0])
 
 
363
  except Exception:
364
  pass
365
  return f"Error during model call: {msg}"
@@ -372,11 +419,68 @@ def ui_analyze(selected_idx: Optional[int], df: pd.DataFrame,
372
  pass
373
 
374
  # ─────────────────────────────────────────────────────────────────────────────
375
- # NEW: Batch analysis with time interval + calltype filter
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  # ─────────────────────────────────────────────────────────────────────────────
377
 
378
  def ui_analyze_calls_by_date(
379
  authed: bool,
 
380
  date_str: str,
381
  t_from: str,
382
  t_to: str,
@@ -387,9 +491,8 @@ def ui_analyze_calls_by_date(
387
  model_pref: str,
388
  ):
389
  """
390
- Fetch calls for the given date, filter them by time window and calltype,
391
- and analyze one-by-one with the same settings as the single-call tab.
392
- Save a .txt file and show results on screen. Also show how many calls matched the filter.
393
  """
394
  # Password gate
395
  if not authed:
@@ -400,30 +503,31 @@ def ui_analyze_calls_by_date(
400
  gr.update(visible=True), # show pwd group
401
  )
402
 
403
- date_str = (date_str or "").strip() or _today_str()
404
- t_from = (t_from or _DEF_T_FROM).strip()
405
- t_to = (t_to or _DEF_T_TO).strip()
406
- calltype = (calltype or "").strip()
407
 
408
- # 1) Fetch list for the date
409
- try:
410
- calls = fetch_calllogs(date_str)
411
- df = pd.DataFrame(calls)
412
- except Exception as e:
413
- return (f"Не ўдалося загрузіць спіс: {e}", None, "", gr.update())
 
414
 
415
- if df.empty:
416
  return ("За гэты дзень званкоў не знойдзена.", None, "", gr.update())
417
 
418
- # 2) Filter by time + calltype
419
- df_f = _filter_calls(df, t_from, t_to, calltype)
420
  total = len(df_f)
421
 
422
  if total == 0:
423
  info = f"Па фільтру ({t_from}–{t_to}{' | ' + calltype if calltype else ''}) — 0 званкоў."
424
  return (info, None, info, gr.update())
425
 
426
- # 3) Prepare model
427
  if not _HAS_GENAI:
428
  return ("❌ google-genai library not found.", None, "", gr.update())
429
 
@@ -440,9 +544,16 @@ def ui_analyze_calls_by_date(
440
  sys_inst = _system_instruction(lang_code)
441
  prompt = _prepare_prompt(template_key, custom_prompt)
442
 
443
- # 4) Iterate filtered calls
444
  results_blocks: List[str] = [
445
- f"## Вынікі\nДата: {date_str}\n\nФільтр: {t_from}–{t_to}{' | calltype=' + calltype if calltype else ''}\n\nЗнойдзена званкоў: {total}\n\n---"
 
 
 
 
 
 
 
446
  ]
447
  analyzed = 0
448
 
@@ -457,14 +568,25 @@ def ui_analyze_calls_by_date(
457
  if not os.path.exists(mp3_path) or os.path.getsize(mp3_path) == 0:
458
  mp3_path, _ = fetch_mp3_by_unique_id(unique_id)
459
  except Exception as e:
 
460
  results_blocks.append(
461
- f"### Call {i+1}\n"
462
- f"- UniqueId: {unique_id}\n"
463
- f"- Start: {row.get('Start','')}\n"
464
- f"- CallerId: {row.get('CallerId','')}\n"
465
- f"- Destination: {row.get('Destination','')}\n"
466
- f"- Duration: {row.get('Duration','')}\n\n"
467
- f"**Памылка загрузкі аўдыё:** {e}\n"
 
 
 
 
 
 
 
 
 
 
468
  "---"
469
  )
470
  continue
@@ -472,7 +594,9 @@ def ui_analyze_calls_by_date(
472
  # Upload + generate
473
  try:
474
  uploaded_file = client.files.upload(file=mp3_path)
475
- merged = f"[SYSTEM INSTRUCTION: {sys_inst}]\n\n{prompt}"
 
 
476
  resp = client.models.generate_content(
477
  model=model_name,
478
  contents=[uploaded_file, merged],
@@ -487,15 +611,28 @@ def ui_analyze_calls_by_date(
487
  except Exception:
488
  pass
489
 
490
- # Result block
 
491
  block = (
492
- f"### Call {i+1}\n"
493
- f"- UniqueId: {unique_id}\n"
494
- f"- Start: {row.get('Start','')}\n"
495
- f"- CallerId: {row.get('CallerId','')}\n"
496
- f"- Destination: {row.get('Destination','')}\n"
497
- f"- Duration: {row.get('Duration','')}\n\n"
498
- f"**Analysis:**\n\n{text}\n"
 
 
 
 
 
 
 
 
 
 
 
 
499
  "---"
500
  )
501
  results_blocks.append(block)
@@ -505,9 +642,11 @@ def ui_analyze_calls_by_date(
505
  info = "Не атрымалася прааналізаваць ніводнага званка."
506
  return (info, None, info, gr.update())
507
 
508
- # 5) Save to .txt and return
509
- results_text = "\n\n".join(results_blocks)
510
- fname = f"/tmp/batch_analysis_{date_str}_{t_from.replace(':','')}-{t_to.replace(':','')}_{int(time.time())}.txt"
 
 
511
  with open(fname, "w", encoding="utf-8") as f:
512
  f.write(results_text)
513
 
@@ -564,6 +703,9 @@ with gr.Blocks(title="Vochi CRM Call Logs (Gradio)") as demo:
564
  # Auth state (False by default)
565
  authed = gr.State(False)
566
 
 
 
 
567
  # Password "modal" (group shown on demand)
568
  with gr.Group(visible=False) as pwd_group:
569
  gr.Markdown("### 🔐 Увядзіце пароль")
@@ -593,14 +735,22 @@ with gr.Blocks(title="Vochi CRM Call Logs (Gradio)") as demo:
593
  analyze_btn = gr.Button("🧠 Analyze", variant="primary")
594
  analysis_md = gr.Markdown()
595
 
596
- # NEW: пакетны аналіз з фільтрамі па часе і calltype
597
  with gr.Tab("Аналіз званкоў за дзень/час"):
598
  batch_date_inp = gr.Textbox(label="Date", value=_today_str(), scale=1)
599
 
600
  with gr.Row():
601
  t_from_inp = gr.Textbox(label="Time from (HH:MM)", value=_DEF_T_FROM)
602
  t_to_inp = gr.Textbox(label="Time to (HH:MM)", value=_DEF_T_TO)
603
- calltype_inp = gr.Textbox(label="calltype (exact)", placeholder="e.g. inbound / outbound")
 
 
 
 
 
 
 
 
604
 
605
  with gr.Row():
606
  tpl_batch_dd = gr.Dropdown(choices=TPL_OPTIONS, value="simple", label="Template")
@@ -609,8 +759,6 @@ with gr.Blocks(title="Vochi CRM Call Logs (Gradio)") as demo:
609
 
610
  custom_batch_tb = gr.Textbox(label="Custom prompt", lines=8, visible=False)
611
 
612
- batch_btn = gr.Button("прааналізаваць", variant="primary")
613
-
614
  batch_status_md = gr.Markdown()
615
  batch_results_md = gr.Markdown()
616
  batch_file_out = gr.File(label="Вынікі (.txt)")
@@ -630,7 +778,7 @@ with gr.Blocks(title="Vochi CRM Call Logs (Gradio)") as demo:
630
  outputs=[authed, status_fetch, pwd_group],
631
  )
632
 
633
- # 3) Other interactions
634
  play_btn.click(ui_play_audio, inputs=[row_dd, calls_df], outputs=[url_html, audio_out, file_out, status_fetch])
635
  tpl_dd.change(ui_toggle_custom_prompt, inputs=[tpl_dd], outputs=[custom_prompt_tb])
636
  analyze_btn.click(
@@ -639,11 +787,27 @@ with gr.Blocks(title="Vochi CRM Call Logs (Gradio)") as demo:
639
  outputs=[analysis_md],
640
  )
641
 
642
- # NEW: events for the new batch tab with filters
643
  tpl_batch_dd.change(ui_toggle_custom_prompt, inputs=[tpl_batch_dd], outputs=[custom_batch_tb])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
644
  batch_btn.click(
645
  ui_analyze_calls_by_date,
646
- inputs=[authed, batch_date_inp, t_from_inp, t_to_inp, calltype_inp, tpl_batch_dd, custom_batch_tb, lang_batch_dd, model_batch_dd],
647
  outputs=[batch_results_md, batch_file_out, batch_status_md, pwd_group],
648
  )
649
 
 
67
  # ─────────────────────────────────────────────────────────────────────────────
68
  PROMPT_TEMPLATES = {
69
  "simple": (
70
+ "You are a call-center conversation analyst for a medical clinic. From the call recording, provide a brief summary:
71
+ "
72
+ "- Purpose of the call (appointment / results / complaint / billing / other).
73
+ "
74
+ "- Patient intent and expectations.
75
+ "
76
+ "- Outcome (booked / call-back / routed / unresolved).
77
+ "
78
+ "- Next steps (owner and when).
79
+ "
80
+ "- Patient emotion (1–5) and agent tone (1–5).
81
+ "
82
+ "- Alerts: urgency/risks/privacy.
83
+
84
+ "
85
  "Keep it short (6–8 lines). End with a line: ‘Service quality rating: X/5’ and one sentence explaining the rating."
86
  ),
87
  "medium": (
88
+ "Act as a senior service analyst. Analyze the call using this structure:
89
+ "
90
+ "1) Quick overview: reason for the call, intent, key facts, urgency (low/medium/high).
91
+ "
92
+ "2) Call flow (2–4 bullets): what was asked/answered, where friction occurred.
93
+ "
94
+ "3) Outcomes & tasks: concrete next actions for clinic/patient with timeframes.
95
+ "
96
+ "4) Emotions & empathy: patient mood; agent empathy (0–5).
97
+ "
98
+ "5) Procedural compliance: identity verification, disclosure of recording (if stated), no off-protocol medical advice, data accuracy.
99
+ "
100
  "6) Quality rating (0–100) using rubric: greeting, verification, accuracy, empathy, issue resolution (each 0–20)."
101
  ),
102
  "detailed": (
103
+ "You are a quality & operations analyst. Provide an in-depth analysis:
104
+ "
105
+ "A) Segmentation: split the call into stages with approximate timestamps (if available) and roles (Patient/Agent).
106
+ "
107
+ "B) Structured data for booking: full name (if stated), date of birth, phone, symptoms/complaints (list), onset/duration, possible pain level 0–10 (if mentioned), required specialist/service, preferred time windows, constraints.
108
+ "
109
+ "C) Triage & risks: class (routine/urgent/emergency), red flags, whether immediate escalation is needed.
110
+ "
111
+ "D) Compliance audit: identity/privacy checks, recording disclosure, consent to data processing, booking policies.
112
+ "
113
+ "E) Conversation metrics: talk ratio (agent/patient), interruptions, long pauses, notable keywords.
114
+ "
115
+ "F) Coaching for the agent: 3–5 concrete improvements with sample phrasing.
116
+
117
+ "
118
  "Deliver: (1) A short patient-chart summary (2–3 sentences). (2) A task table with columns: priority, owner, due."
119
  ),
120
  }
 
261
  if col:
262
  # exact match (case-insensitive)
263
  mask = mask & (work[col].astype(str).str.lower() == ct.lower())
 
 
 
264
 
265
  return work.loc[mask].drop(columns=[c for c in ["_Start_dt", "_Start_time_sec"] if c in work.columns])
266
 
267
+
268
+ def _validate_times(t_from: str, t_to: str) -> Tuple[bool, str, bool, bool]:
269
+ """Return (ok, message_html, from_valid, to_valid)."""
270
+ tf = _parse_hhmm(t_from)
271
+ tt = _parse_hhmm(t_to)
272
+ if tf is None and tt is None:
273
+ return False, "<span style='color:#c00'>❌ Памылка: няправільны фармат часу ў абодвух палях (выкарыстайце HH:MM).</span>", False, False
274
+ if tf is None:
275
+ return False, "<span style='color:#c00'>❌ Памылка: поле <b>Time from</b> павінна быць у фармаце HH:MM.</span>", False, True
276
+ if tt is None:
277
+ return False, "<span style='color:#c00'>❌ Памылка: поле <b>Time to</b> павінна быць у фармаце HH:MM.</span>", True, False
278
+ return True, "", True, True
279
+
280
+
281
+ def _recording_url(unique_id: str) -> str:
282
+ return f"{BASE_URL}/calllogs/{CLIENT_ID}/{unique_id}"
283
+
284
  # ─────────────────────────────────────────────────────────────────────────────
285
  # Gradio handlers
286
  # ─────────────────────────────────────────────────────────────────────────────
 
300
  body = e.response.text[:800]
301
  except Exception:
302
  pass
303
+ return pd.DataFrame(), gr.update(choices=[], value=None), f"HTTP error: {e}
304
+ {body}"
305
  except Exception as e:
306
  return pd.DataFrame(), gr.update(choices=[], value=None), f"Load error: {e}"
307
 
 
316
  unique_id = str(row.get("UniqueId"))
317
  try:
318
  fpath = f"/tmp/call_{unique_id}.mp3"
319
+ url_used = _recording_url(unique_id)
320
  # Download only if not exists (avoid re-fetch)
321
  if not os.path.exists(fpath) or os.path.getsize(fpath) == 0:
322
  fpath, url_used = fetch_mp3_by_unique_id(unique_id)
 
385
 
386
  # Call model
387
  try:
388
+ merged = f"""[SYSTEM INSTRUCTION: {sys_inst}]
389
+
390
+ {prompt}"""
391
  resp = client.models.generate_content(model=model_name, contents=[uploaded_file, merged])
392
  text = getattr(resp, "text", None)
393
  if not text:
394
  return "Analysis finished but returned no text. Check model settings and file format."
395
+ # Add a listening link at the end
396
+ url = _recording_url(unique_id)
397
+ return f"### Analysis result
398
+
399
+ {text}
400
+
401
+ **Recording:** {url}"
402
  except Exception as e:
403
  # Try to attach more error details
404
  msg = str(e)
405
  try:
406
  if hasattr(e, "args") and e.args:
407
+ msg = msg + "
408
+
409
+ " + str(e.args[0])
410
  except Exception:
411
  pass
412
  return f"Error during model call: {msg}"
 
419
  pass
420
 
421
  # ─────────────────────────────────────────────────────────────────────────────
422
+ # NEW: Batch helpers for dynamic calltype choices and preview
423
+ # ─────────────────────────────────────────────────────────────────────────────
424
+
425
+ def ui_batch_load(date_str: str, authed: bool):
426
+ """Fetch calls for the date, return DF, calltype choices, and status."""
427
+ if not authed:
428
+ return gr.update(), gr.update(), "🔒 Увядзіце пароль, каб атрымаць званкі.", gr.update(visible=True)
429
+
430
+ date_str = (date_str or "").strip() or _today_str()
431
+ try:
432
+ calls = fetch_calllogs(date_str)
433
+ df = pd.DataFrame(calls)
434
+ except Exception as e:
435
+ return gr.update(), gr.update(choices=[""], value=""), f"Не ўдалося загрузіць спіс: {e}", gr.update(visible=False)
436
+
437
+ if df.empty:
438
+ return gr.update(value=pd.DataFrame()), gr.update(choices=[""], value=""), "За гэты дзень званкоў не знойдзена.", gr.update(visible=False)
439
+
440
+ # Build calltype list automatically
441
+ col = _infer_calltype_col(df)
442
+ if col:
443
+ types = sorted(set(str(x).strip() for x in df[col].dropna().unique() if str(x).strip()))
444
+ choices = [""] + types # allow empty = no filter
445
+ else:
446
+ choices = [""]
447
+
448
+ return (
449
+ df, # store DF in state via gr.State
450
+ gr.update(choices=choices, value=""),
451
+ f"Загружана званкоў: {len(df)}. Абярыце фільтр і націсніце ‘Папярэдні прагляд’.",
452
+ gr.update(visible=False),
453
+ )
454
+
455
+
456
+ def ui_batch_preview(df: pd.DataFrame, t_from: str, t_to: str, calltype: str):
457
+ ok, msg, from_ok, to_ok = _validate_times(t_from, t_to)
458
+ if not ok:
459
+ return pd.DataFrame(), f"{msg}", gr.update(info=("✓" if from_ok else "Format: HH:MM")), gr.update(info=("✓" if to_ok else "Format: HH:MM"))
460
+
461
+ if df is None or len(df) == 0:
462
+ return pd.DataFrame(), "Спачатку загрузіце спіс на дату.", gr.update(), gr.update()
463
+
464
+ # Filter
465
+ df_f = _filter_calls(df, t_from, t_to, calltype)
466
+
467
+ # Add recording URL column for quick listening
468
+ if not df_f.empty and "UniqueId" in df_f.columns:
469
+ df_f = df_f.copy()
470
+ df_f["RecordingURL"] = df_f["UniqueId"].astype(str).map(_recording_url)
471
+
472
+ count = len(df_f)
473
+ info = f"Фільтр: {t_from}–{t_to}{' | calltype=' + calltype if calltype else ''}. Знойдзена: <b>{count}</b>."
474
+ return df_f, info, gr.update(info=""), gr.update(info="")
475
+
476
+
477
+ # ─────────────────────────────────────────────────────────────────────────────
478
+ # Batch analysis with time interval + calltype filter + URL in output
479
  # ─────────────────────────────────────────────────────────────────────────────
480
 
481
  def ui_analyze_calls_by_date(
482
  authed: bool,
483
+ df_loaded: pd.DataFrame,
484
  date_str: str,
485
  t_from: str,
486
  t_to: str,
 
491
  model_pref: str,
492
  ):
493
  """
494
+ Analyze filtered calls one-by-one. Uses the already loaded DF to allow
495
+ dynamic calltype dropdown and preview step. Adds listening URL to each block.
 
496
  """
497
  # Password gate
498
  if not authed:
 
503
  gr.update(visible=True), # show pwd group
504
  )
505
 
506
+ # Validate time inputs
507
+ ok, msg, from_ok, to_ok = _validate_times(t_from, t_to)
508
+ if not ok:
509
+ return (msg, None, "", gr.update(visible=False))
510
 
511
+ # Ensure DF is present; if not, fallback to fetch (safety)
512
+ if df_loaded is None or len(df_loaded) == 0:
513
+ try:
514
+ calls = fetch_calllogs((date_str or _today_str()).strip())
515
+ df_loaded = pd.DataFrame(calls)
516
+ except Exception as e:
517
+ return (f"Не ўдалося загрузіць спіс: {e}", None, "", gr.update())
518
 
519
+ if df_loaded.empty:
520
  return ("За гэты дзень званкоў не знойдзена.", None, "", gr.update())
521
 
522
+ # Filter by time + calltype
523
+ df_f = _filter_calls(df_loaded, t_from, t_to, calltype)
524
  total = len(df_f)
525
 
526
  if total == 0:
527
  info = f"Па фільтру ({t_from}–{t_to}{' | ' + calltype if calltype else ''}) — 0 званкоў."
528
  return (info, None, info, gr.update())
529
 
530
+ # Prepare model
531
  if not _HAS_GENAI:
532
  return ("❌ google-genai library not found.", None, "", gr.update())
533
 
 
544
  sys_inst = _system_instruction(lang_code)
545
  prompt = _prepare_prompt(template_key, custom_prompt)
546
 
547
+ # Iterate filtered calls
548
  results_blocks: List[str] = [
549
+ f"## Вынікі
550
+ Дата: {(date_str or _today_str()).strip()}
551
+
552
+ Фільтр: {t_from}–{t_to}{' | calltype=' + calltype if calltype else ''}
553
+
554
+ Знойдзена званкоў: {total}
555
+
556
+ ---"
557
  ]
558
  analyzed = 0
559
 
 
568
  if not os.path.exists(mp3_path) or os.path.getsize(mp3_path) == 0:
569
  mp3_path, _ = fetch_mp3_by_unique_id(unique_id)
570
  except Exception as e:
571
+ url = _recording_url(unique_id)
572
  results_blocks.append(
573
+ f"### Call {i+1}
574
+ "
575
+ f"- UniqueId: {unique_id}
576
+ "
577
+ f"- Start: {row.get('Start','')}
578
+ "
579
+ f"- CallerId: {row.get('CallerId','')}
580
+ "
581
+ f"- Destination: {row.get('Destination','')}
582
+ "
583
+ f"- Duration: {row.get('Duration','')}
584
+ "
585
+ f"- Recording: {url}
586
+
587
+ "
588
+ f"**Памылка загрузкі аўдыё:** {e}
589
+ "
590
  "---"
591
  )
592
  continue
 
594
  # Upload + generate
595
  try:
596
  uploaded_file = client.files.upload(file=mp3_path)
597
+ merged = f"[SYSTEM INSTRUCTION: {sys_inst}]
598
+
599
+ {prompt}"
600
  resp = client.models.generate_content(
601
  model=model_name,
602
  contents=[uploaded_file, merged],
 
611
  except Exception:
612
  pass
613
 
614
+ # Result block with listening URL
615
+ url = _recording_url(unique_id)
616
  block = (
617
+ f"### Call {i+1}
618
+ "
619
+ f"- UniqueId: {unique_id}
620
+ "
621
+ f"- Start: {row.get('Start','')}
622
+ "
623
+ f"- CallerId: {row.get('CallerId','')}
624
+ "
625
+ f"- Destination: {row.get('Destination','')}
626
+ "
627
+ f"- Duration: {row.get('Duration','')}
628
+ "
629
+ f"- Recording: {url}
630
+
631
+ "
632
+ f"**Analysis:**
633
+
634
+ {text}
635
+ "
636
  "---"
637
  )
638
  results_blocks.append(block)
 
642
  info = "Не атрымалася прааналізаваць ніводнага званка."
643
  return (info, None, info, gr.update())
644
 
645
+ # Save to .txt and return
646
+ results_text = "
647
+
648
+ ".join(results_blocks)
649
+ fname = f"/tmp/batch_analysis_{(date_str or _today_str()).strip()}_{t_from.replace(':','')}-{t_to.replace(':','')}_{int(time.time())}.txt"
650
  with open(fname, "w", encoding="utf-8") as f:
651
  f.write(results_text)
652
 
 
703
  # Auth state (False by default)
704
  authed = gr.State(False)
705
 
706
+ # States for batch tab
707
+ batch_df_state = gr.State(pd.DataFrame())
708
+
709
  # Password "modal" (group shown on demand)
710
  with gr.Group(visible=False) as pwd_group:
711
  gr.Markdown("### 🔐 Увядзіце пароль")
 
735
  analyze_btn = gr.Button("🧠 Analyze", variant="primary")
736
  analysis_md = gr.Markdown()
737
 
738
+ # Пакетны аналіз з фільтрамі па часе і calltype + прэв'ю
739
  with gr.Tab("Аналіз званкоў за дзень/час"):
740
  batch_date_inp = gr.Textbox(label="Date", value=_today_str(), scale=1)
741
 
742
  with gr.Row():
743
  t_from_inp = gr.Textbox(label="Time from (HH:MM)", value=_DEF_T_FROM)
744
  t_to_inp = gr.Textbox(label="Time to (HH:MM)", value=_DEF_T_TO)
745
+ calltype_dd = gr.Dropdown(label="calltype", choices=[""], value="", info="Аўта-значэнні пасля загрузкі")
746
+
747
+ with gr.Row():
748
+ load_btn = gr.Button("Атрымаць спіс (для фільтраў)", variant="secondary")
749
+ preview_btn = gr.Button("Папярэдні прагляд", variant="secondary")
750
+ batch_btn = gr.Button("прааналізаваць", variant="primary")
751
+
752
+ preview_info_md = gr.Markdown()
753
+ preview_df = gr.Dataframe(value=pd.DataFrame(), label="Адфільтраваныя званкі", interactive=False)
754
 
755
  with gr.Row():
756
  tpl_batch_dd = gr.Dropdown(choices=TPL_OPTIONS, value="simple", label="Template")
 
759
 
760
  custom_batch_tb = gr.Textbox(label="Custom prompt", lines=8, visible=False)
761
 
 
 
762
  batch_status_md = gr.Markdown()
763
  batch_results_md = gr.Markdown()
764
  batch_file_out = gr.File(label="Вынікі (.txt)")
 
778
  outputs=[authed, status_fetch, pwd_group],
779
  )
780
 
781
+ # 3) Other interactions (single analysis)
782
  play_btn.click(ui_play_audio, inputs=[row_dd, calls_df], outputs=[url_html, audio_out, file_out, status_fetch])
783
  tpl_dd.change(ui_toggle_custom_prompt, inputs=[tpl_dd], outputs=[custom_prompt_tb])
784
  analyze_btn.click(
 
787
  outputs=[analysis_md],
788
  )
789
 
790
+ # 4) Batch tab interactions
791
  tpl_batch_dd.change(ui_toggle_custom_prompt, inputs=[tpl_batch_dd], outputs=[custom_batch_tb])
792
+
793
+ # Load list for date → populate state & calltype dropdown
794
+ load_btn.click(
795
+ ui_batch_load,
796
+ inputs=[batch_date_inp, authed],
797
+ outputs=[batch_df_state, calltype_dd, preview_info_md, pwd_group],
798
+ )
799
+
800
+ # Preview filtered table
801
+ preview_btn.click(
802
+ ui_batch_preview,
803
+ inputs=[batch_df_state, t_from_inp, t_to_inp, calltype_dd],
804
+ outputs=[preview_df, preview_info_md, t_from_inp, t_to_inp],
805
+ )
806
+
807
+ # Analyze filtered
808
  batch_btn.click(
809
  ui_analyze_calls_by_date,
810
+ inputs=[authed, batch_df_state, batch_date_inp, t_from_inp, t_to_inp, calltype_dd, tpl_batch_dd, custom_batch_tb, lang_batch_dd, model_batch_dd],
811
  outputs=[batch_results_md, batch_file_out, batch_status_md, pwd_group],
812
  )
813