multicom_demo / appBatchV1.py
archivartaunik's picture
Rename app.py to appBatchV1.py
543c607 verified
"""Gradio UI wired to hexagonal architecture services."""
from __future__ import annotations
import datetime as _dt
import os
import tempfile
from typing import Optional, List, Tuple
import json
import gradio as gr
import pandas as pd
# --- Пачатак блока, які можа патрабаваць ўстаноўкі залежнасцяў ---
try:
from calls_analyser.adapters.ai.gemini import GeminiAIAdapter
from calls_analyser.adapters.secrets.env import EnvSecretsAdapter
from calls_analyser.adapters.storage.local import LocalStorageAdapter
from calls_analyser.adapters.telephony.vochi import VochiTelephonyAdapter
from calls_analyser.domain.exceptions import CallsAnalyserError
from calls_analyser.domain.models import Language
from calls_analyser.ports.ai import AIModelPort
from calls_analyser.services.analysis import AnalysisOptions, AnalysisService
from calls_analyser.services.call_log import CallLogService
from calls_analyser.services.prompt import PromptService
from calls_analyser.services.registry import ProviderRegistry
from calls_analyser.services.tenant import TenantService
from calls_analyser.config import (
PROMPTS as CFG_PROMPTS,
MODEL_CANDIDATES as CFG_MODEL_CANDIDATES,
BATCH_MODEL_KEY as CFG_BATCH_MODEL_KEY,
BATCH_PROMPT_KEY as CFG_BATCH_PROMPT_KEY,
BATCH_PROMPT_TEXT as CFG_BATCH_PROMPT_TEXT,
BATCH_LANGUAGE_CODE as CFG_BATCH_LANGUAGE_CODE,
)
PROJECT_IMPORTS_AVAILABLE = True
except ImportError:
PROJECT_IMPORTS_AVAILABLE = False
class CallsAnalyserError(Exception):
pass
class Language:
RUSSIAN = "ru"
BELARUSIAN = "be"
ENGLISH = "en"
AUTO = "auto"
CFG_PROMPTS = {}
CFG_MODEL_CANDIDATES = []
CFG_BATCH_MODEL_KEY = ""
CFG_BATCH_PROMPT_KEY = ""
CFG_BATCH_PROMPT_TEXT = ""
CFG_BATCH_LANGUAGE_CODE = "auto"
# --- Канец блока ---
PROMPTS = CFG_PROMPTS if PROJECT_IMPORTS_AVAILABLE else {}
TPL_OPTIONS = [(tpl.title, tpl.key) for tpl in PROMPTS.values()] + [("Custom", "custom")]
LANG_OPTIONS = [
("Russian", Language.RUSSIAN),
("Auto", Language.AUTO),
("Belarusian", Language.BELARUSIAN),
("English", Language.ENGLISH),
]
CALL_TYPE_OPTIONS = [
("All types", ""),
("Inbound", "0"),
("Outbound", "1"),
("Internal", "2"),
]
MODEL_CANDIDATES = CFG_MODEL_CANDIDATES if PROJECT_IMPORTS_AVAILABLE else []
# ----------------------------------------------------------------------------
# Dependency wiring
# ----------------------------------------------------------------------------
DEFAULT_TENANT_ID = os.environ.get("DEFAULT_TENANT_ID", "Amedis")
DEFAULT_BASE_URL = os.environ.get("VOCHI_BASE_URL", "https://crm.vochi.by/api")
if not PROJECT_IMPORTS_AVAILABLE:
# заглушкі
class MockAdapter:
def get_optional_secret(self, _):
return os.environ.get("GOOGLE_API_KEY")
secrets_adapter = MockAdapter()
storage_adapter = None
prompt_service = None
ai_registry = {}
tenant_service = None
call_log_service = None
analysis_service = None
else:
secrets_adapter = EnvSecretsAdapter()
storage_adapter = LocalStorageAdapter()
prompt_service = PromptService(PROMPTS)
ai_registry: ProviderRegistry[AIModelPort] = ProviderRegistry()
def _register_gemini_models() -> None:
api_key = secrets_adapter.get_optional_secret("GOOGLE_API_KEY")
if not api_key:
return
for _title, model in MODEL_CANDIDATES:
try:
ai_registry.register(model, GeminiAIAdapter(api_key=api_key, model=model))
except CallsAnalyserError:
continue
_register_gemini_models()
def _build_tenant_service() -> TenantService:
return TenantService(
secrets_adapter,
default_tenant=DEFAULT_TENANT_ID,
default_base_url=DEFAULT_BASE_URL,
)
def _build_call_log_service(tenant_service: TenantService) -> CallLogService:
config = tenant_service.resolve()
telephony_adapter = VochiTelephonyAdapter(
base_url=config.vochi_base_url,
client_id=config.vochi_client_id,
bearer_token=config.bearer_token,
)
return CallLogService(telephony_adapter, storage_adapter)
tenant_service = _build_tenant_service()
call_log_service = _build_call_log_service(tenant_service)
analysis_service = AnalysisService(call_log_service, ai_registry, prompt_service)
def _build_model_options() -> list[tuple[str, str]]:
"""Збіраем опцыі мадэлі для выпадаючага спісу."""
if not PROJECT_IMPORTS_AVAILABLE:
return []
options: list[tuple[str, str]] = []
for title, model_key in MODEL_CANDIDATES:
if model_key not in ai_registry:
continue
provider = ai_registry.get(model_key)
provider_label = getattr(provider, "provider_name", model_key)
options.append((f"{provider_label}{title}", model_key))
return options
MODEL_OPTIONS = _build_model_options()
MODEL_PLACEHOLDER_CHOICE = ("Configure GOOGLE_API_KEY to enable Gemini models", "")
MODEL_CHOICES = MODEL_OPTIONS or [MODEL_PLACEHOLDER_CHOICE]
MODEL_DEFAULT = MODEL_OPTIONS[0][1] if MODEL_OPTIONS else MODEL_PLACEHOLDER_CHOICE[1]
MODEL_INFO = (
"Select an AI model for call analysis"
if MODEL_OPTIONS
else "Add GOOGLE_API_KEY to secrets and reload to enable models"
)
BATCH_PROMPT_KEY = CFG_BATCH_PROMPT_KEY
BATCH_PROMPT_TEXT = (CFG_BATCH_PROMPT_TEXT or "").strip()
BATCH_MODEL_KEY = CFG_BATCH_MODEL_KEY or MODEL_DEFAULT or ""
BATCH_LANGUAGE_CODE = CFG_BATCH_LANGUAGE_CODE
try:
BATCH_LANGUAGE = Language(BATCH_LANGUAGE_CODE)
except ValueError:
BATCH_LANGUAGE = Language.AUTO
# ----------------------------------------------------------------------------
# UI utilities
# ----------------------------------------------------------------------------
def _label_row(row: dict) -> str:
start = row.get("Start", "")
src = row.get("CallerId", "")
dst = row.get("Destination", "")
dur = row.get("Duration", "")
return f"{start} | {src}{dst} ({dur}s)"
def _parse_day(day_value) -> _dt.date:
if isinstance(day_value, _dt.datetime):
return day_value.date()
if isinstance(day_value, _dt.date):
return day_value
if not day_value:
raise ValueError("Date not specified.")
try:
timestamp = float(str(day_value).strip())
if timestamp > 1e9:
return _dt.datetime.fromtimestamp(timestamp, tz=_dt.timezone.utc).date()
except (ValueError, TypeError):
pass
try:
return _dt.date.fromisoformat(str(day_value).strip())
except ValueError as exc:
raise ValueError(f"Invalid date format: {day_value}") from exc
def _parse_time_value(time_value) -> Optional[_dt.time]:
if time_value in (None, ""):
return None
if isinstance(time_value, _dt.datetime):
return time_value.time().replace(microsecond=0)
if isinstance(time_value, _dt.time):
return time_value.replace(microsecond=0)
try:
timestamp = float(str(time_value).strip())
if timestamp > 1e9:
return (
_dt.datetime.fromtimestamp(timestamp, tz=_dt.timezone.utc)
.time()
.replace(microsecond=0)
)
except (ValueError, TypeError):
pass
value = str(time_value).strip()
if not value:
return None
try:
if value.count(":") == 1 and len(value.split(":")[0]) == 1:
value = f"0{value}"
parsed = _dt.time.fromisoformat(value)
except ValueError as exc:
if len(value) == 5 and value.count(":") == 1:
parsed = _dt.time.fromisoformat(f"{value}:00")
else:
raise ValueError(f"Invalid time format: {value}") from exc
return parsed.replace(microsecond=0)
def _validate_time_range(time_from: Optional[_dt.time], time_to: Optional[_dt.time]) -> None:
if time_from and time_to and time_from > time_to:
raise ValueError("Time 'from' must be less than or equal to time 'to'.")
def _resolve_call_type(value: object) -> Optional[int]:
s = str(value).strip()
if s == "":
return None
try:
return int(s)
except ValueError:
pass
label_to_value = {label: v for (label, v) in CALL_TYPE_OPTIONS}
mapped = label_to_value.get(s, "")
try:
return int(mapped) if mapped != "" else None
except ValueError:
return None
def _build_dropdown(df: pd.DataFrame):
opts = [(_label_row(row), idx) for idx, row in df.iterrows()]
value = opts[0][1] if opts else None
return gr.update(choices=[(label, idx) for label, idx in opts], value=value)
def _build_batch_dropdown(df: pd.DataFrame):
if df is None or df.empty:
return gr.update(choices=[], value=None)
opts: List[Tuple[str, str]] = []
for _idx, row in df.iterrows():
label = (
f"{row.get('Start','')} | {row.get('Caller','')} -> "
f"{row.get('Destination','')} ({row.get('Duration (s)','')}s)"
)
uid = str(row.get("UniqueId", ""))
if uid:
opts.append((label, uid))
value = opts[0][1] if opts else None
return gr.update(choices=opts, value=value)
# ----------------------------------------------------------------------------
# Gradio handlers
# ----------------------------------------------------------------------------
def ui_filter_calls(
date_value,
time_from_value,
time_to_value,
call_type_value,
authed,
tenant_id,
):
"""Фільтруе званкі і вяртае табліцу."""
if not authed:
return (
gr.update(value=pd.DataFrame(), visible=False),
gr.update(visible=False),
gr.update(choices=[], value=None),
"🔐 Enter the password to apply the filter.",
gr.update(visible=True),
)
if not PROJECT_IMPORTS_AVAILABLE:
return (
pd.DataFrame(),
gr.update(visible=False),
[],
"Project dependencies are not loaded.",
gr.update(visible=False),
)
try:
day = _parse_day(date_value)
time_from = _parse_time_value(time_from_value)
time_to = _parse_time_value(time_to_value)
_validate_time_range(time_from, time_to)
call_type = _resolve_call_type(call_type_value)
tenant = tenant_service.resolve(tenant_id or None)
entries = call_log_service.list_calls(
day,
tenant,
time_from=time_from,
time_to=time_to,
call_type=call_type,
)
df = pd.DataFrame([entry.raw for entry in entries])
dd = _build_dropdown(df)
msg = f"Calls found: {len(df)}"
return (
gr.update(value=df, visible=True),
gr.update(visible=False),
dd,
msg,
gr.update(visible=False),
)
except Exception as exc:
return (
gr.update(value=pd.DataFrame(), visible=True),
gr.update(visible=False),
gr.update(choices=[], value=None),
f"Load error: {exc}",
gr.update(visible=False),
)
def ui_play_audio(selected_idx, df, tenant_id):
"""Прайграць аўдыё па выбраным радку.
Лагіка:
- калі selected_idx выглядае як UID (не лічба) -> гуляем яго;
- калі гэта індэкс радка -> шукаем у df і бярэм UniqueId.
"""
if not PROJECT_IMPORTS_AVAILABLE:
return "Project dependencies are not loaded.", None, ""
unique_id = None
if selected_idx is not None:
try:
# калі дропдаўн ужо захоўвае UID напрамую
if not str(selected_idx).isdigit():
unique_id = str(selected_idx)
elif df is not None and not df.empty:
row = df.iloc[int(selected_idx)]
unique_id = str(row.get("UniqueId"))
except (ValueError, IndexError):
return "<em>Invalid selection.</em>", None, ""
if not unique_id:
return "<em>Select a call to play.</em>", None, ""
try:
tenant = tenant_service.resolve(tenant_id or None)
handle = call_log_service.ensure_recording(unique_id, tenant)
listen_url = (
f"{tenant.vochi_base_url.rstrip('/')}/calllogs/"
f"{tenant.vochi_client_id}/{unique_id}"
)
html = f'URL: <a href="{listen_url}" target="_blank">{listen_url}</a>'
return html, handle.local_uri, "Ready ✅"
except Exception as exc:
return f"Playback failed: {exc}", None, ""
def ui_toggle_custom_prompt(template_key):
"""Паказаць/схаваць поле Custom prompt."""
return gr.update(visible=(template_key == "custom"))
def ui_mass_analyze(
date_value,
time_from_value,
time_to_value,
call_type_value,
tenant_id,
authed,
):
"""
Масавы аналіз (STREAMING).
Гэта генератар (yield), Gradio будзе адлюстроўваць вынікі паступова.
Паведамленні прагрэс-статусу і выніковае паведамленне ідуць буйным шрыфтам (Markdown ## / ###).
"""
empty_df = pd.DataFrame()
hidden_df_update = gr.update(value=empty_df, visible=False)
hidden_file = gr.update(value=None, visible=False)
def h3(txt: str) -> str:
# сярэдні буйны шрыфт
return f"### {txt}"
def h2_success(txt: str) -> str:
# вялікі тэкст для фінальнага выніку
return f"## {txt}"
def h2_error(txt: str) -> str:
return f"## {txt}"
# 1) праверкі доступу і канфіга
if not authed:
yield (
hidden_df_update,
h2_error("🔐 Enter the password to run batch analysis."),
hidden_file,
)
return
if not PROJECT_IMPORTS_AVAILABLE:
yield (
hidden_df_update,
h2_error("Project dependencies are not loaded."),
hidden_file,
)
return
if len(ai_registry) == 0 or not BATCH_MODEL_KEY:
yield (
hidden_df_update,
h2_error("❌ Batch analysis is unavailable: AI model is not configured."),
hidden_file,
)
return
# 2) асноўная логіка збору спісу званкоў
try:
day = _parse_day(date_value)
time_from = _parse_time_value(time_from_value)
time_to = _parse_time_value(time_to_value)
_validate_time_range(time_from, time_to)
call_type = _resolve_call_type(call_type_value)
tenant = tenant_service.resolve(tenant_id or None)
entries = call_log_service.list_calls(
day,
tenant,
time_from=time_from,
time_to=time_to,
call_type=call_type,
)
if not entries:
yield (
hidden_df_update,
h3("ℹ️ No calls for the selected filter."),
hidden_file,
)
return
rows = []
total = len(entries)
# пачатковы апдэйт
yield (
gr.update(value=pd.DataFrame(), visible=False),
h3(f"Starting batch analysis for {total} call(s)..."),
hidden_file,
)
# 3) цыкл аналізу
for i, entry in enumerate(entries, start=1):
pct = int((i / total) * 100)
row_data = {
"Start": entry.started_at.isoformat() if entry.started_at else "",
"Caller": entry.caller_id or "",
"Destination": entry.destination or "",
"Duration (s)": entry.duration_seconds,
"UniqueId": entry.unique_id,
}
try:
result = analysis_service.analyze_call(
unique_id=entry.unique_id,
tenant=tenant,
lang=BATCH_LANGUAGE,
options=AnalysisOptions(
model_key=BATCH_MODEL_KEY,
prompt_key=BATCH_PROMPT_KEY,
custom_prompt=BATCH_PROMPT_TEXT or None,
),
)
link = (
f"{tenant.vochi_base_url.rstrip('/')}/calllogs/"
f"{tenant.vochi_client_id}/{entry.unique_id}"
)
# спроба structured JSON
try:
text = str(result.text or "").strip()
l, r = text.find("{"), text.rfind("}")
if l != -1 and r != -1 and r > l:
text = text[l : r + 1]
payload = json.loads(text)
row_data["Needs follow-up"] = (
"Yes" if payload.get("needs_follow_up") else "No"
)
row_data["Reason"] = str(payload.get("reason") or "")
except Exception:
row_data["Needs follow-up"] = ""
row_data["Reason"] = result.text
row_data["Link"] = f'<a href="{link}" target="_blank">Listen</a>'
row_data["Status"] = "✅"
except Exception as exc:
row_data["Needs follow-up"] = ""
row_data["Reason"] = f"❌ {exc}"
row_data["Link"] = ""
row_data["Status"] = "❌"
rows.append(row_data)
partial_df = pd.DataFrame(rows)
interim_msg = f"Analyzing {i}/{total} ({pct}%)… UID `{entry.unique_id}`"
# прамежкавы yield (жывое абнаўленне табліцы + статус)
yield (
gr.update(value=partial_df, visible=True),
h3(interim_msg),
hidden_file,
)
# 4) фінал
final_df = pd.DataFrame(rows)
ok_count = len(final_df[final_df["Status"] == "✅"])
final_msg = (
"✅ Batch analysis completed. "
f"Found: {total}, processed successfully: {ok_count}"
)
yield (
gr.update(value=final_df, visible=True),
h2_success(final_msg),
hidden_file,
)
except Exception as exc:
yield (
hidden_df_update,
h2_error(f"❌ Analysis failed: {exc}"),
hidden_file,
)
return
def ui_hide_call_list():
"""Схаваць ручны спіс выклікаў пасля батча, каб не блытаць карыстальніка."""
return gr.update(visible=False)
def ui_export_results(results_df):
"""Захаваць батч-аналіз у CSV і вярнуць файл у UI."""
if results_df is None or results_df.empty:
return gr.update(value=None, visible=False), "❌ No data to export."
with tempfile.NamedTemporaryFile(
"w", suffix=".csv", delete=False, encoding="utf-8"
) as tmp:
results_df.to_csv(tmp.name, index=False)
return gr.update(value=tmp.name, visible=True), "✅ File is ready to save."
def ui_check_password(pwd: str):
"""Праверка доступу ў UI."""
_UI_PASSWORD = os.environ.get("VOCHI_UI_PASSWORD", "")
if not _UI_PASSWORD:
# пароль не настроены -> усім можна
return (
False,
"⚠️ <b>VOCHI_UI_PASSWORD</b> is not configured. Access granted without password.",
gr.update(visible=False),
)
if (pwd or "").strip() == _UI_PASSWORD:
return True, "✅ Access granted.", gr.update(visible=False)
return False, "❌ Incorrect password.", gr.update(visible=True)
def ui_show_current_uid(current_uid: str):
"""Паказаць выбраны UID у табе AI Analysis."""
uid = (current_uid or "").strip()
return (
f"**Selected UniqueId:** `{uid}`"
if uid
else "No file selected for AI Analysis."
)
def ui_analyze_bridge(
selected_idx,
df,
template_key,
custom_prompt,
lang_code,
model_pref,
tenant_id,
current_uid,
):
"""
Аналіз адной размовы З ПРАГРЭСАМ.
ВАЖНА:
- Гэта цяпер генератар (yield), а не звычайная функцыя.
- Мы не выкарыстоўваем аргумент progress=... (ён ламаецца ў Gradio 5).
- Зрабляем некалькі крокаў:
1) праверкі і падрыхтоўка -> yield статычны статус
2) выклік аналізу -> пасля гэтага яшчэ адзін yield з вынікам
- Gradio сам пакажа built-in progress bar праз show_progress="full".
"""
# STEP 0. Вызначаем, які UID трэба аналізаваць
uid_to_analyze = (current_uid or "").strip()
if not uid_to_analyze and selected_idx is not None and df is not None and not df.empty:
try:
uid_to_analyze = str(df.iloc[int(selected_idx)].get("UniqueId") or "").strip()
except (ValueError, IndexError):
uid_to_analyze = ""
# Калі няма UID -> адразу вынікаем
if not uid_to_analyze:
yield "Select a call from the list or batch results first."
return
# STEP 1. Праверкі канфігурацыі перад выклікам мадэлі
if not PROJECT_IMPORTS_AVAILABLE:
yield "Project dependencies are not loaded."
return
if len(ai_registry) == 0:
yield "❌ No AI models are configured."
return
if model_pref not in ai_registry:
yield "❌ Selected model is not available."
return
# паказваем карыстальніку, што пачынаем
yield f"### Preparing analysis...\n\n- UID: `{uid_to_analyze}`\n- Model: `{model_pref}`\n- Lang: `{lang_code}`\n\nPlease wait…"
# STEP 2. Рэальны аналіз
try:
tenant = tenant_service.resolve(tenant_id or None)
lang = Language(lang_code)
result = analysis_service.analyze_call(
unique_id=uid_to_analyze,
tenant=tenant,
lang=lang,
options=AnalysisOptions(
model_key=model_pref,
prompt_key=template_key,
custom_prompt=custom_prompt,
),
)
# STEP 3. Гатова, вяртаем вынік
yield f"### Analysis result\n\n{result.text}"
except Exception as exc:
yield f"Analysis failed: {exc}"
def ui_on_batch_row_select(
displayed_df: pd.DataFrame,
full_df_state: pd.DataFrame,
tenant_id: str,
evt: gr.SelectData,
):
"""
Апрацоўвае выбар радка з табліцы вынікаў (Batch results).
ВАЖНА:
- evt.index дае індэкс радка ў адлюстраванай табліцы (пасля сартыроўкі/фільтрацыі),
а не ў зыходных дадзеных.
- Мы дастаём UniqueId з гэтага радка і будуем адзін варыянт для выпадаючага спісу "Call".
"""
# Значэнні па змаўчанні, калі нешта пойдзе не так
empty_return = (
gr.update(choices=[], value=None),
"",
"No file selected for AI Analysis.",
)
# Праверка, ці ёсць даныя для апрацоўкі
if (
evt is None
or displayed_df is None
or displayed_df.empty
or full_df_state is None
or full_df_state.empty
):
return empty_return
try:
# КРОК 1: Атрымліваем індэкс выбранага радка з аб'екта падзеі (evt)
# evt.index тут успрымаем як спіс выбраных радкоў, бярэм першы
visual_row_index = evt.index[0]
clicked_row_from_view = displayed_df.iloc[visual_row_index]
# КРОК 2: Здабываем унікальны ідэнтыфікатар (UniqueId)
uid = str(clicked_row_from_view.get("UniqueId", "")).strip()
if not uid:
return empty_return
# Шукай арыгінальны радок у поўным наборы даных
original_row_series = full_df_state[full_df_state["UniqueId"] == uid]
if original_row_series.empty:
return empty_return
original_row = original_row_series.iloc[0]
row_dict = original_row.to_dict()
# КРОК 3: Чалавечы лэйбл для выпадаючага спісу
label = (
f"{row_dict.get('Start','')} | "
f"{row_dict.get('Caller','')} → "
f"{row_dict.get('Destination','')} "
f"({row_dict.get('Duration (s)','')}s)"
)
# КРОК 4: Абнаўленне для Dropdown "Call"
# choices = [("бачны тэкст", value_for_component)]
dd_update = gr.update(choices=[(f"Batch: {label}", uid)], value=uid)
# КРОК 5: Вяртаем:
# - абнаўленне row_dd
# - сам uid -> кладзецца ў current_uid_state
# - фарматаваны Markdown з UID у табе "AI Analysis"
return dd_update, uid, ui_show_current_uid(uid)
except (AttributeError, IndexError, KeyError):
return empty_return
# ----------------------------------------------------------------------------
# Build Gradio UI
# ----------------------------------------------------------------------------
def _today_str():
return _dt.date.today().strftime("%Y-%m-%d")
with gr.Blocks(title="Vochi CRM Call Logs (Gradio)") as demo:
gr.Markdown(
"# Vochi CRM → MP3 → AI analysis\n"
"*Filter calls by date, time and type, listen to recordings and run batch AI analysis.*"
)
authed = gr.State(False)
batch_results_state = gr.State(pd.DataFrame())
current_uid_state = gr.State("")
with gr.Group(visible=os.environ.get("VOCHI_UI_PASSWORD", "") != "") as pwd_group:
gr.Markdown("### 🔐 Enter password")
pwd_tb = gr.Textbox(
label="Password", type="password", placeholder="••••••••", lines=1
)
pwd_btn = gr.Button("Unlock", variant="primary")
with gr.Tabs() as tabs:
with gr.Tab("Vochi CRM"):
with gr.Row():
tenant_tb = gr.Textbox(
label="Tenant ID", value=DEFAULT_TENANT_ID, scale=1
)
date_inp = gr.Textbox(
label="Date", value=_today_str(), placeholder="YYYY-MM-DD", scale=1
)
time_from_inp = gr.Textbox(
label="Time from", placeholder="HH:MM", scale=1
)
time_to_inp = gr.Textbox(label="Time to", placeholder="HH:MM", scale=1)
call_type_dd = gr.Dropdown(
choices=CALL_TYPE_OPTIONS,
value="",
label="Call type",
type="value",
scale=1,
)
with gr.Row():
filter_btn = gr.Button("Filter", variant="primary", scale=0)
batch_btn = gr.Button("Batch analyze", variant="secondary", scale=0)
save_btn = gr.Button("Save to file", scale=0)
status_fetch = gr.Markdown()
batch_status_md = gr.Markdown()
calls_df = gr.DataFrame(
value=pd.DataFrame(),
label="Call list (manual filter)",
interactive=False,
)
batch_results_df = gr.DataFrame(
value=pd.DataFrame(),
label="Batch results",
interactive=True,
visible=False,
datatype=[
"str", # Start
"str", # Caller
"str", # Destination
"number", # Duration (s)
"str", # UniqueId
"str", # Needs follow-up
"str", # Reason
"markdown", # Link
"str", # Status
],
)
row_dd = gr.Dropdown(
choices=[],
label="Call",
info="Choose a row to listen/analyze",
type="value",
)
with gr.Row():
play_btn = gr.Button("🎧 Play")
url_html = gr.HTML()
audio_out = gr.Audio(label="Audio", type="filepath")
batch_file = gr.File(label="Export CSV", visible=False)
with gr.Tab("AI Analysis"):
with gr.Row():
tpl_dd = gr.Dropdown(
choices=TPL_OPTIONS,
value="simple" if TPL_OPTIONS else "custom",
label="Template",
)
lang_dd = gr.Dropdown(
choices=LANG_OPTIONS,
value=Language.AUTO,
label="Language",
)
model_dd = gr.Dropdown(
choices=MODEL_CHOICES,
value=MODEL_DEFAULT,
label="Model",
interactive=bool(MODEL_OPTIONS),
info=MODEL_INFO,
)
custom_prompt_tb = gr.Textbox(
label="Custom prompt", lines=8, visible=False
)
current_uid_md = gr.Markdown(
value="No file selected for AI Analysis."
)
analyze_btn = gr.Button("🧠 Analyze", variant="primary")
analysis_md = gr.Markdown()
# --- wiring events ---
# пароль
pwd_btn.click(
ui_check_password,
inputs=[pwd_tb],
outputs=[authed, status_fetch, pwd_group],
)
# ручная фільтрацыя
filter_btn.click(
ui_filter_calls,
inputs=[date_inp, time_from_inp, time_to_inp, call_type_dd, authed, tenant_tb],
outputs=[calls_df, batch_results_df, row_dd, status_fetch, pwd_group],
)
# масавы аналіз (stream з yield -> жывое абнаўленне і "прагрэс-бар" у выглядзе статусу)
batch_btn.click(
fn=ui_mass_analyze,
inputs=[date_inp, time_from_inp, time_to_inp, call_type_dd, tenant_tb, authed],
outputs=[batch_results_df, batch_status_md, batch_file],
).then(
fn=lambda df: df,
inputs=[batch_results_df],
outputs=[batch_results_state],
).then(
fn=ui_hide_call_list,
outputs=[calls_df],
)
# выбар радка з батчу -> абнаўляем поле Call + UID у AI Analysis
batch_results_df.select(
fn=ui_on_batch_row_select,
inputs=[batch_results_df, batch_results_state, tenant_tb],
outputs=[row_dd, current_uid_state, current_uid_md],
)
# прайграванне аўдыё
play_btn.click(
ui_play_audio,
inputs=[row_dd, calls_df, tenant_tb],
outputs=[url_html, audio_out, status_fetch],
)
# экспарт CSV
save_btn.click(
ui_export_results,
inputs=[batch_results_state],
outputs=[batch_file, batch_status_md],
)
# паказаць поле для свайго prompt
tpl_dd.change(
ui_toggle_custom_prompt,
inputs=[tpl_dd],
outputs=[custom_prompt_tb],
)
# аналіз адной размовы з прагрэсам
analyze_btn.click(
fn=ui_analyze_bridge,
inputs=[
row_dd,
calls_df,
tpl_dd,
custom_prompt_tb,
lang_dd,
model_dd,
tenant_tb,
current_uid_state,
],
outputs=[analysis_md],
show_progress="full", # Gradio будзе паказваць progress bar аўтаматычна
)
if __name__ == "__main__":
# УВАГА: Замяні "D:\\tmp" на шлях, дзе ляжаць MP3-запісы,
# каб кнопка 🎧 Play магла іх прайграваць лакальна.
demo.launch(allowed_paths=["D:\\tmp"])