SER-BPO / app.py
bhaskararcitech's picture
Update app.py
4ade4af verified
Raw
History Blame Contribute Delete
29.2 kB
# app_full_reimpl.py
"""
Full app: MDES (BPObox) reimplementation + audio/text emotion helpers + Gradio UI + CLI test.
- Default ASR: Whisper 'tiny' (fast & small). Change WHISPER_MODEL near top to 'base' if you have memory.
- CLI quick test:
python app_full_reimpl.py --agent agent_mono16.wav --customer customer_mono16.wav --debug
- To run Gradio:
python app_full_reimpl.py
- Debug output: /tmp/mdes_debug.json
"""
import argparse
import json
import math
import os
import sys
import warnings
from typing import Dict, List
import librosa
import numpy as np
warnings.filterwarnings("ignore")
# ---------------- CONFIG ----------------
WHISPER_MODEL = os.environ.get("WHISPER_MODEL", "tiny") # default tiny (change to "base" if you want)
EMBEDDING_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2" # optional
TEXT_CLASSIFIER_NAME = "bhadresh-savani/distilbert-base-uncased-emotion" # optional
AUDIO_EMOTION_MODEL_PATH = os.environ.get("AUDIO_EMOTION_MODEL", "CNN_LSTM.h5") # optional existing model
DEFAULT_EMPATHY_THRESHOLD = 0.40
DEBUG_PERMISSIVE_THRESHOLD = 0.18
DEBUG_DUMP_PATH = "/tmp/mdes_debug.json"
USE_GRADIO = True
# ---------------- lazy imports for optional libs ----------------
try:
import whisper
except Exception:
whisper = None
try:
import torch
from transformers import AutoModel, AutoTokenizer, pipeline
except Exception:
torch = None
AutoModel = None
AutoTokenizer = None
pipeline = None
# ---------------- small helpers & salvage of existing runner ----------------
# The code below preserves your audio-emotion runner (prepare_data + runner)
try:
import tensorflow as tf
from keras.initializers import Orthogonal
except Exception:
tf = None
CATEGORIES = ['Neutral', 'Happy', 'Sad', 'Angry', 'Fear', 'Disgust']
def prepare_data_for_audio_emotion(audio_path):
"""Salvaged prepare_data from your prior app (keeps same frame settings)."""
try:
raw_audio, sr = librosa.load(audio_path, sr=16000)
except Exception:
return None
raw_audio, _ = librosa.effects.trim(raw_audio, top_db=25, frame_length=256, hop_length=64)
audio_duration = len(raw_audio) / 16000.0
if audio_duration > 4:
raw_audio = raw_audio[:4 * 16000]
else:
raw_audio = np.pad(raw_audio, (0, (4 * 16000) - len(raw_audio)), 'constant')
FRAME_LENGTH = 400
HOP_LENGTH = 160
sr = 16000
y = raw_audio
# may raise if audio very short; handle in runner
try:
zcr = librosa.feature.zero_crossing_rate(y, frame_length=FRAME_LENGTH, hop_length=HOP_LENGTH)
rms = librosa.feature.rms(y=y, frame_length=FRAME_LENGTH, hop_length=HOP_LENGTH)
mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=20, hop_length=HOP_LENGTH)
pda = np.concatenate((zcr, rms, mfccs), axis=0)
pda = pda.astype('float32')
pda = np.expand_dims(pda, axis=0)
return pda
except Exception:
return None
# Attempt to load your audio model if present
audio_emotion_model = None
if tf is not None and os.path.exists(AUDIO_EMOTION_MODEL_PATH):
try:
audio_emotion_model = tf.keras.models.load_model(AUDIO_EMOTION_MODEL_PATH, custom_objects={'Orthogonal': Orthogonal})
print("[AUDIO_MODEL] Loaded audio emotion model:", AUDIO_EMOTION_MODEL_PATH)
except Exception as e:
print("[AUDIO_MODEL] Could not load model at", AUDIO_EMOTION_MODEL_PATH, ":", e)
audio_emotion_model = None
else:
if tf is None:
print("[AUDIO_MODEL] TensorFlow not available; audio model disabled.")
else:
print("[AUDIO_MODEL] No audio model found at", AUDIO_EMOTION_MODEL_PATH)
def runner_audio_emotion(audio_path):
"""Return audio-only emotion probs or zeros if model missing."""
if audio_emotion_model is None:
return {c: 0.0 for c in CATEGORIES}
features = prepare_data_for_audio_emotion(audio_path)
if features is None:
return {c: 0.0 for c in CATEGORIES}
try:
pr = audio_emotion_model.predict(features)
out = {CATEGORIES[i]: float(np.round(pr[0, i], 3)) for i in range(len(CATEGORIES))}
# normalize if needed
s = sum(out.values())
if s > 0:
for k in out:
out[k] = round(out[k] / s, 3)
return out
except Exception as e:
print("[AUDIO_MODEL] inference error:", e)
return {c: 0.0 for c in CATEGORIES}
# ---------------- ASR loader (Whisper) with fallback ----------------
def load_whisper_model(name=WHISPER_MODEL):
global whisper
if whisper is None:
try:
import whisper as _w
whisper = _w
except Exception as e:
print("[ASR] whisper import failed:", e)
return None
try:
print(f"[ASR] Loading Whisper model '{name}' ...")
model = whisper.load_model(name)
print(f"[ASR] Loaded Whisper model '{name}'")
return model
except Exception as e:
print(f"[ASR] Failed to load Whisper '{name}':", e)
if name != "tiny":
try:
print("[ASR] Attempting fallback to 'tiny' ...")
model = whisper.load_model("tiny")
print("[ASR] Loaded Whisper model 'tiny' fallback")
return model
except Exception as e2:
print("[ASR] Failed to load fallback 'tiny':", e2)
return None
return None
# ---------------- embedding and text classifier loaders ----------------
def try_load_embedding(name=EMBEDDING_MODEL_NAME):
if AutoTokenizer is None or AutoModel is None:
print("[EMB] transformers not available; embedding disabled.")
return None, None
try:
print("[EMB] Loading embedding model (may take time)...")
tok = AutoTokenizer.from_pretrained(name)
mdl = AutoModel.from_pretrained(name)
device = "cuda" if torch and torch.cuda.is_available() else "cpu"
mdl.to(device)
print("[EMB] Embedding model loaded.")
return tok, mdl
except Exception as e:
print("[EMB] Failed to load embedding model:", e)
return None, None
def try_load_text_classifier(name=TEXT_CLASSIFIER_NAME):
if pipeline is None:
print("[TXT] transformers pipeline not available; text classifier disabled.")
return None
try:
print("[TXT] Loading text classifier (may take time)...")
clf = pipeline("text-classification", model=name, return_all_scores=True,
device=0 if torch and torch.cuda.is_available() else -1)
print("[TXT] Text classifier loaded.")
return clf
except Exception as e:
print("[TXT] Failed to load text classifier:", e)
return None
# ---------------- semantic helpers ----------------
def compute_embedding_sentence(text: str, tokenizer, model):
if not text or tokenizer is None or model is None:
return None
try:
inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True)
if next(model.parameters()).is_cuda:
inputs = {k: v.cuda() for k, v in inputs.items()}
with torch.no_grad():
out = model(**inputs, return_dict=True)
token_embeddings = out.last_hidden_state
attention_mask = inputs['attention_mask'].unsqueeze(-1)
masked = token_embeddings * attention_mask
summed = masked.sum(1)
denom = attention_mask.sum(1).clamp(min=1e-9)
emb = (summed / denom).squeeze().cpu().numpy()
return emb
except Exception as e:
print("[EMB] embedding error:", e)
return None
def cosine_sim(a, b):
if a is None or b is None:
return None
num = float(np.dot(a, b))
da = float(np.linalg.norm(a))
db = float(np.linalg.norm(b))
if da == 0 or db == 0:
return None
return num / (da * db)
def fallback_token_overlap(a_text, b_text):
if not a_text or not b_text:
return 0.0
a_set = set(w.strip(".,!?\"'").lower() for w in a_text.split() if w.strip())
b_set = set(w.strip(".,!?\"'").lower() for w in b_text.split() if w.strip())
if not a_set or not b_set:
return 0.0
inter = len(a_set & b_set)
avg_len = (len(a_set) + len(b_set)) / 2.0
return inter / avg_len if avg_len > 0 else 0.0
def semantic_similarity(a_text, b_text, tokenizer=None, emb_model=None):
emb_a = compute_embedding_sentence(a_text, tokenizer, emb_model) if tokenizer and emb_model else None
emb_b = compute_embedding_sentence(b_text, tokenizer, emb_model) if tokenizer and emb_model else None
sim = cosine_sim(emb_a, emb_b)
if sim is not None:
return float(max(0.0, min(1.0, (sim + 1.0) / 2.0)))
return float(max(0.0, min(1.0, fallback_token_overlap(a_text, b_text))))
# ---------------- text emotion helpers (HF fallback) ----------------
LABEL_MAP = {
"neutral": "Neutral", "joy": "Happy", "happy": "Happy", "happiness": "Happy",
"sad": "Sad", "sadness": "Sad", "anger": "Angry", "angry": "Angry",
"fear": "Fear", "disgust": "Disgust", "surprise": "Neutral", "love": "Happy"
}
def hf_text_to_probs(text: str, clf_pipeline):
zeros = {c: 0.0 for c in CATEGORIES}
if not text or clf_pipeline is None:
return zeros
try:
out = clf_pipeline(text)
except Exception as e:
print("[TXT] classifier error:", e)
return zeros
scores = out[0] if isinstance(out, list) and isinstance(out[0], list) else out
mapping = {}
total = 0.0
for it in scores:
lbl = (it.get("label") or "").lower()
sc = float(it.get("score") or 0.0)
tgt = LABEL_MAP.get(lbl)
if tgt:
mapping[tgt] = mapping.get(tgt, 0.0) + sc
total += sc
if total <= 0:
return zeros
for k in mapping:
mapping[k] = mapping[k] / total
for c in CATEGORIES:
if c not in mapping:
mapping[c] = 0.0
return mapping
def fallback_text_probs(text: str):
out = {c: 0.0 for c in CATEGORIES}
if not text:
out["Neutral"] = 1.0
return out
t = text.lower()
if any(w in t for w in ["sorry", "apolog", "understand", "i'm sorry", "i am sorry"]):
out["Sad"] = 0.6; out["Neutral"] = 0.4
elif any(w in t for w in ["happy", "great", "good", "awesome"]):
out["Happy"] = 1.0
elif any(w in t for w in ["angry", "mad", "furious", "upset"]):
out["Angry"] = 1.0
else:
out["Neutral"] = 1.0
return out
def text_valence_from_probs_map(prob_map: Dict[str, float]):
v = 0.0
v += prob_map.get("Happy", 0.0) * 1.0
v += prob_map.get("Neutral", 0.0) * 0.0
neg = prob_map.get("Sad", 0.0) + prob_map.get("Angry", 0.0) + prob_map.get("Fear", 0.0) + prob_map.get("Disgust", 0.0)
v += neg * -1.0
return float(v)
# ---------------- lexical empathy & action heuristics (salvaged) ----------------
EMPATHY_PHRASES = ["i understand", "i completely understand", "i know how", "sorry to hear", "i'm sorry", "i'm so sorry", "i apologise", "i apologize", "we'll prioritize", "i'll escalate", "i'll create a ticket"]
ACTION_WORDS = ["ticket", "escalat", "callback", "call back", "schedule", "refund", "transfer", "open a ticket", "follow up"]
def detect_empathy_phrase(text: str):
if not text:
return False
t = text.lower()
return any(p in t for p in EMPATHY_PHRASES)
def detect_action_presence(text: str):
if not text:
return False
t = text.lower()
return any(w in t for w in ACTION_WORDS)
# ---------------- MDES subcomponents (BPObox mapping) ----------------
def compute_PU(agent_text, customer_context, tokenizer=None, emb_model=None):
if not agent_text or not customer_context:
return 0.0
return semantic_similarity(agent_text, customer_context, tokenizer, emb_model)
def compute_CC(agent_text, customer_context, tokenizer=None, emb_model=None):
if not agent_text or not customer_context:
return 0.0
sem = semantic_similarity(agent_text, customer_context, tokenizer, emb_model)
if sem >= 0.80:
return 0.5
if sem >= 0.60:
return 0.3
a_set = set(w.strip(".,!?\"'").lower() for w in agent_text.split() if w.strip())
c_set = set(w.strip(".,!?\"'").lower() for w in customer_context.split() if w.strip())
inter = len(a_set & c_set)
if inter >= 2:
return 0.2
if inter == 1:
return 0.1
return 0.0
def compute_EM(agent_text, customer_text, text_classify_fn):
if not agent_text or not customer_text:
return 0.0
cust_probs = text_classify_fn(customer_text)
ag_probs = text_classify_fn(agent_text)
top_c = max(cust_probs.items(), key=lambda x: x[1])[0]
top_a = max(ag_probs.items(), key=lambda x: x[1])[0]
if top_c == top_a:
return 0.95
val_c = text_valence_from_probs_map(cust_probs)
val_a = text_valence_from_probs_map(ag_probs)
if val_c == 0 and val_a == 0:
return 0.7
if val_c * val_a > 0:
return 0.7
return 0.25
def compute_IR(agent_prosody, customer_prosody):
if not agent_prosody or not customer_prosody:
return 0.8
ae = float(agent_prosody.get("energy", 0.0))
ce = float(customer_prosody.get("energy", 0.0))
if ce <= 0:
return 0.9
ratio = ae / max(ce, 1e-9)
if 0.75 <= ratio <= 1.33:
return 1.0
return float(max(0.2, 1.0 - abs(math.log2(ratio)) / 4.0))
def compute_AA(agent_text, action_logs=None):
if not agent_text:
return 0.0
t = agent_text.lower()
if action_logs and isinstance(action_logs, dict) and (action_logs.get("ticket_created") or action_logs.get("escalated")):
return 1.0
if any(w in t for w in ACTION_WORDS):
if any(tok.isdigit() for tok in t.split()) or "at " in t or "tomorrow" in t or "today" in t:
return 1.0
return 0.7
return 0.0
def compute_FR(agent_text):
if not agent_text:
return 0.0
t = agent_text.lower()
if "call back" in t or "callback" in t or "at " in t or "tomorrow" in t:
return 1.0
if "i'll" in t or "i will" in t or "we will" in t:
return 0.6
return 0.0
def compute_ST(agent_text):
if not agent_text:
return 0.0
candidates = ["reset","send","escalate","transfer","create","schedule","verify","confirm","refund"]
steps = sum(1 for w in candidates if w in agent_text.lower())
return float(min(1.0, steps / 3.0))
def compute_OT(customer_segment, customer_prosody, customer_valence):
if customer_valence < -0.5:
return 3.0
if customer_prosody and customer_prosody.get("energy", 0.0) > 1e-4:
return 3.0
return 6.0
def compute_EF_for_segment(agent_seg_start, customer_segments, text_classify_fn, action_present=False):
before_start = agent_seg_start - 8.0
before_end = agent_seg_start
after_start = agent_seg_start
after_end = agent_seg_start + 12.0
before_texts = [s['text'] for s in customer_segments if s['start'] >= before_start and s['end'] <= before_end]
after_texts = [s['text'] for s in customer_segments if s['start'] > after_start and s['end'] <= after_end]
before_text = " ".join(before_texts).strip()
after_text = " ".join(after_texts).strip()
def val_from_text(t):
if not t:
return 0.0
pm = text_classify_fn(t)
return text_valence_from_probs_map(pm)
b = val_from_text(before_text)
a = val_from_text(after_text)
delta = a - b
raw = 1.0 + (delta / 2.0) * 0.2
raw = float(max(0.5, min(1.5, raw)))
if action_present:
ef = 0.6 * raw + 0.4 * 1.0
else:
ef = raw
ef = float(max(0.8, min(1.2, ef)))
return ef
# ---------------- MDES aggregator (per-utterance) ----------------
def compute_mdes_doc(agent_segments: List[Dict], customer_segments: List[Dict],
agent_prosody_map: Dict, customer_prosody_map: Dict,
tokenizer=None, emb_model=None, text_clf=None,
action_logs=None, empathy_threshold=DEFAULT_EMPATHY_THRESHOLD,
debug_permissive=False):
def text_classify_fn(t):
if text_clf:
return hf_text_to_probs(t, text_clf)
return fallback_text_probs(t)
empathic_indices = []
per_utt = []
for i, a in enumerate(agent_segments):
a_text = a.get("text", "") or ""
a_start = a.get("start", 0.0)
ctx = [s for s in customer_segments if s['end'] <= a_start and (a_start - s['end']) <= 12.0]
cust_context = " ".join([s['text'] for s in ctx]) if ctx else (customer_segments[-1]['text'] if customer_segments else "")
lex = 1.0 if detect_empathy_phrase(a_text) else 0.0
sem = semantic_similarity(a_text, cust_context, tokenizer, emb_model) if cust_context else 0.0
empathy_score = 0.4 * lex + 0.6 * sem
threshold = DEBUG_PERMISSIVE_THRESHOLD if debug_permissive else empathy_threshold
if empathy_score >= threshold or (debug_permissive and i == 0):
empathic_indices.append(i)
per_utt.append({"agent_index": i, "agent_start": a_start, "agent_text": a_text, "cust_context": cust_context, "empathy_score": empathy_score})
if len(empathic_indices) == 0:
return {"CE": 0.0, "AE": 0.0, "BE": 0.0, "TF": 1.0, "EF": 1.0, "MDES": 0.0, "n": 0, "per_utterance": []}
CE_list = []
AE_list = []
BE_list = []
TF_scores = []
EF_scores = []
for idx in empathic_indices:
a = agent_segments[idx]
a_text = a.get("text", "")
a_start = a.get("start", 0.0)
prior_customers = [s for s in customer_segments if s['end'] <= a_start]
cust_seg = max(prior_customers, key=lambda x: x['end']) if prior_customers else None
cust_text = cust_seg['text'] if cust_seg else ""
# CE
PU_i = compute_PU(a_text, cust_text, tokenizer, emb_model)
CC_i = compute_CC(a_text, cust_text, tokenizer, emb_model)
CE_i = PU_i * (1.0 + CC_i)
CE_list.append(CE_i)
# AE
cust_probs = text_classify_fn(cust_text) if cust_text else {c:0.0 for c in CATEGORIES}
ag_probs = text_classify_fn(a_text) if a_text else {c:0.0 for c in CATEGORIES}
CS = text_valence_from_probs_map(cust_probs)
AS = text_valence_from_probs_map(ag_probs)
EM_i = compute_EM(a_text, cust_text, text_classify_fn)
ag_key = round(a_start, 3)
cust_key = round(cust_seg.get("start", 0.0), 3) if cust_seg else None
ag_pros = agent_prosody_map.get(ag_key, {"energy":0.0})
cust_pros = customer_prosody_map.get(cust_key, {"energy":0.0})
IR_i = compute_IR(ag_pros, cust_pros)
alignment_term = max(0.0, 1.0 - (abs(CS - AS) / 2.0))
AE_i = EM_i * IR_i * alignment_term
AE_list.append(AE_i)
# BE
AA_i = compute_AA(a_text, action_logs)
FR_i = compute_FR(a_text)
ST_i = compute_ST(a_text)
BE_i = AA_i * FR_i * ST_i
BE_list.append(BE_i)
# TF
if cust_seg:
RT_i = a_start - cust_seg.get("end", a_start)
cust_val = text_valence_from_probs_map(cust_probs)
OT_i = compute_OT(cust_seg, cust_pros, cust_val)
TF_score = max(0.0, 1.0 - abs(OT_i - RT_i) / OT_i) if OT_i > 0 else 0.0
else:
TF_score = 0.5
TF_scores.append(TF_score)
# EF
action_present = detect_action_presence(a_text) or (action_logs and isinstance(action_logs, dict) and (action_logs.get("ticket_created") or action_logs.get("escalated")))
EF_i = compute_EF_for_segment(a_start, customer_segments, text_classify_fn, action_present=action_present)
EF_scores.append(EF_i)
CE = float(np.mean(CE_list)) * 100.0
AE = float(np.mean(AE_list)) * 100.0
BE = float(np.mean(BE_list)) * 100.0
TF = 1.0 + 0.2 * float(np.mean(TF_scores))
EF = float(np.mean(EF_scores))
inner = 0.4 * CE + 0.3 * AE + 0.3 * BE
MDES = float(inner * TF * EF)
per_utterance = []
for j, idx in enumerate(empathic_indices):
per_utterance.append({
"agent_index": idx,
"CE_i": float(CE_list[j]),
"AE_i": float(AE_list[j]),
"BE_i": float(BE_list[j]),
"TF_i": float(TF_scores[j]),
"EF_i": float(EF_scores[j])
})
return {"CE": CE, "AE": AE, "BE": BE, "TF": TF, "EF": EF, "MDES": MDES, "n": len(CE_list), "per_utterance": per_utterance}
# ---------------- ASR / transcription wrapper ----------------
def transcribe_with_whisper(model, path):
if model is None:
return {"text": "", "segments": []}
try:
res = model.transcribe(path)
text = res.get("text", "") or ""
segments = res.get("segments", []) or []
segs = [{"start": float(s.get("start",0.0)), "end": float(s.get("end",0.0)), "text": s.get("text","").strip()} for s in segments]
return {"text": text.strip(), "segments": segs}
except Exception as e:
print("[ASR] transcribe error:", e)
return {"text":"", "segments": []}
# ---------------- top-level process function ----------------
def process_pair(agent_wav: str, customer_wav: str, asr_model,
tokenizer=None, emb_model=None, text_clf=None,
debug_permissive=False, action_logs=None, write_debug=True):
# transcribe
agent_asr = transcribe_with_whisper(asr_model, agent_wav)
customer_asr = transcribe_with_whisper(asr_model, customer_wav)
agent_segments = agent_asr.get("segments", [])
customer_segments = customer_asr.get("segments", [])
# compute per-segment prosody by slicing audio arrays
agent_y = None
cust_y = None
try:
agent_y, _ = librosa.load(agent_wav, sr=16000)
except Exception:
agent_y = None
try:
cust_y, _ = librosa.load(customer_wav, sr=16000)
except Exception:
cust_y = None
agent_map = {}
cust_map = {}
sr = 16000
for s in agent_segments:
st = s.get("start", 0.0); en = s.get("end", st + 0.5)
if agent_y is not None:
b = int(max(0, st*sr)); e = int(min(len(agent_y), en*sr))
seg = agent_y[b:e] if e>b else agent_y[b:b+1]
pros = {"energy": float(np.mean(seg**2)) if seg.size else 0.0}
agent_map[round(st,3)] = pros
else:
agent_map[round(st,3)] = {"energy": 0.0}
for s in customer_segments:
st = s.get("start", 0.0); en = s.get("end", st + 0.5)
if cust_y is not None:
b = int(max(0, st*sr)); e = int(min(len(cust_y), en*sr))
seg = cust_y[b:e] if e>b else cust_y[b:b+1]
pros = {"energy": float(np.mean(seg**2)) if seg.size else 0.0}
cust_map[round(st,3)] = pros
else:
cust_map[round(st,3)] = {"energy": 0.0}
# debug dump
if write_debug:
dbg = {
"agent_asr": agent_asr,
"customer_asr": customer_asr,
"agent_segments": agent_segments,
"customer_segments": customer_segments,
"env": {
"whisper_model": WHISPER_MODEL,
"audio_model_loaded": audio_emotion_model is not None,
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
}
}
try:
with open(DEBUG_DUMP_PATH, "w", encoding="utf-8") as fh:
json.dump(dbg, fh, indent=2, ensure_ascii=False)
print("[DEBUG] wrote", DEBUG_DUMP_PATH)
except Exception as e:
print("[DEBUG] failed to write debug dump:", e)
# fused emotions for UI
agent_audio_probs = runner_audio_emotion(agent_wav)
customer_audio_probs = runner_audio_emotion(customer_wav)
agent_text_probs = hf_text_to_probs(agent_asr.get("text",""), text_clf) if text_clf else fallback_text_probs(agent_asr.get("text",""))
customer_text_probs = hf_text_to_probs(customer_asr.get("text",""), text_clf) if text_clf else fallback_text_probs(customer_asr.get("text",""))
def fuse(audio_p, text_p, a_w=0.6, t_w=0.4):
fused = {}
for c in CATEGORIES:
fused[c] = a_w * audio_p.get(c,0.0) + t_w * text_p.get(c,0.0)
s = sum(fused.values())
if s > 0:
for k in fused:
fused[k] = round(float(fused[k] / s), 3)
return fused
agent_fused = fuse(agent_audio_probs, agent_text_probs)
customer_fused = fuse(customer_audio_probs, customer_text_probs)
# compute MDES
mdes = compute_mdes_doc(agent_segments, customer_segments, agent_map, cust_map,
tokenizer=tokenizer, emb_model=emb_model, text_clf=text_clf,
action_logs=action_logs, empathy_threshold=DEFAULT_EMPATHY_THRESHOLD,
debug_permissive=debug_permissive)
out = {
"mdes_summary": {
"MDES": round(mdes["MDES"], 3),
"CE": round(mdes["CE"], 3),
"AE": round(mdes["AE"], 3),
"BE": round(mdes["BE"], 3),
"TF": round(mdes["TF"], 3),
"EF": round(mdes["EF"], 3),
"n_empathic_segments": mdes["n"]
},
"per_segment": mdes["per_utterance"],
"agent_transcript": agent_asr.get("text",""),
"customer_transcript": customer_asr.get("text",""),
"agent_fused_emotion": agent_fused,
"customer_fused_emotion": customer_fused
}
return out
# ---------------- CLI + Gradio entrypoints ----------------
def main_cli(args):
asr = load_whisper_model()
tok, emb = try_load_embedding()
txt_clf = try_load_text_classifier()
if not asr:
print("[ASR] No ASR model loaded. Exiting.")
sys.exit(1)
res = process_pair(args.agent, args.customer, asr, tokenizer=tok, emb_model=emb, text_clf=txt_clf,
debug_permissive=args.debug, action_logs=None, write_debug=True)
print(json.dumps(res, indent=2))
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Full MDES app (clean reimplementation).")
parser.add_argument("--agent", type=str, help="Agent WAV path (mono 16k recommended)")
parser.add_argument("--customer", type=str, help="Customer WAV path (mono 16k recommended)")
parser.add_argument("--debug", action="store_true", help="Permissive empathy threshold for debugging")
parser.add_argument("--no-gradio", action="store_true", help="Do not launch Gradio UI")
args = parser.parse_args()
if args.agent and args.customer:
main_cli(args)
sys.exit(0)
# else try to launch Gradio UI (if available and not disabled)
if not args.no_gradio and USE_GRADIO:
try:
import gradio as gr
# load models
asr = load_whisper_model()
tok, emb = try_load_embedding()
txt_clf = try_load_text_classifier()
def gr_runner(agent_audio, customer_audio, debug_mode=False):
a_path = agent_audio[0] if isinstance(agent_audio, tuple) else agent_audio
c_path = customer_audio[0] if isinstance(customer_audio, tuple) else customer_audio
res = process_pair(a_path, c_path, asr, tokenizer=tok, emb_model=emb, text_clf=txt_clf,
debug_permissive=debug_mode, action_logs=None, write_debug=True)
# Gradio outputs: agent fused, agent audio-only, customer fused, customer audio-only, agent transcript, customer transcript, JSON
return (res["agent_fused_emotion"], res["agent_fused_emotion"], res["customer_fused_emotion"], res["customer_fused_emotion"], res["agent_transcript"], res["customer_transcript"], json.dumps(res["mdes_summary"], indent=2))
with gr.Blocks() as demo:
gr.Markdown("## MDES (BPObox) — Clean Reimplementation")
a_in = gr.Audio(label="Agent audio (wav)", type="filepath")
c_in = gr.Audio(label="Customer audio (wav)", type="filepath")
debug_checkbox = gr.Checkbox(label="Debug (permissive empathy)", value=False)
out1 = gr.Label(label="Agent Combined Emotion")
out2 = gr.Label(label="Agent Audio-only Emotion")
out3 = gr.Label(label="Customer Combined Emotion")
out4 = gr.Label(label="Customer Audio-only Emotion")
t1 = gr.Textbox(label="Agent Transcript")
t2 = gr.Textbox(label="Customer Transcript")
jbox = gr.Textbox(label="MDES Summary JSON")
run_btn = gr.Button("Run")
run_btn.click(fn=gr_runner, inputs=[a_in, c_in, debug_checkbox], outputs=[out1, out2, out3, out4, t1, t2, jbox])
demo.launch()
except Exception as e:
print("[GRADIO] Failed to launch Gradio UI:", e)
print("You can still use CLI mode: python app_full_reimpl.py --agent agent.wav --customer customer.wav")
else:
print("Provide --agent and --customer to run in CLI mode, or remove --no-gradio to launch the UI.")