When a caller starts a conversation already frustrated and ends it angrier, two things usually went wrong. The agent said the right things in the wrong order. And nobody caught it because the post-call survey came back blank.
Real-time sentiment detection solves the first problem by letting your agent sense frustration mid-conversation and adapt while the call is still happening, not after the fact. This article covers how the pipeline actually works, which signals are reliable enough to drive decisions, and how to wire emotion scores into your agent's behavior.
What Does "Real-Time" Actually Mean for Sentiment?
Real-time sentiment detection means producing an emotion score within the inter-turn window. That's the gap between when a caller finishes speaking and when your agent starts responding. In voice conversations, this window is typically 300-600ms. If your sentiment model takes 800ms to run, the score arrives after the agent's response has already started generating. You've lost the window.
This latency constraint rules out most off-the-shelf sentiment APIs, which are designed for batch processing of completed calls. Production real-time systems need streaming inference, lightweight models, or both. The practical threshold that works in production is under 500ms for your full pipeline: STT transcription plus emotion scoring plus action lookup.
The trade-off is accuracy: a 200ms model is less accurate than a 2,000ms model. Start with the faster model, measure its error rate on your own call data, and only add latency if the errors are genuinely costing you outcomes.
The Two Signal Streams Your Model Needs to Fuse
Real-time voice sentiment fuses two signal streams into a single emotion output: what the caller's voice sounds like, and what the caller actually says. One stream reads the raw waveform before any transcription; the other reads the transcript your STT layer produces. Fusing them beats either alone, because tone and words often tell different stories.
Acoustic signals come from the raw audio waveform. Pitch trajectory (rising pitch with short bursts correlates strongly with frustration), vocal energy (louder means more aroused, though not always negative), speech rate (rapid speech indicates urgency or anxiety), and spectral features like jitter and shimmer that correlate with emotional arousal. These run on the audio itself, before transcription happens.
Linguistic signals come from the transcript. Word choice ("impossible," "ridiculous," "unacceptable" vs. "okay," "sure," "thanks"), sentence length and structure, repetition ("I already told you," "I already explained this"), and semantic meaning all contribute. These run on the text output of your STT layer.
The fusion happens in a joint model that weights both streams, and the two don't always agree. A caller saying "I'm fine" in an irritated tone scores differently on acoustic and linguistic signals. Joint models trained on voice data learn to resolve these conflicts. Text-only models miss the "I'm fine" case entirely, which is why voice-specific training data matters if you're building a custom model.
That said, starting with text-only is the right call for most teams. You ship faster, you establish baselines, and you can layer acoustic signals once you've validated that the text model is actually changing agent behavior in measurable ways.
Why Does an Emotion Taxonomy Beat Sentiment Polarity?
Most off-the-shelf sentiment models return a score from -1 (negative) to +1 (positive). That's too coarse to drive agent behavior. Knowing sentiment is "negative" doesn't tell you whether to slow down, offer escalation, provide reassurance, or transfer immediately.
Deployments that actually change agent behavior use an emotion taxonomy, not a polarity score. A workable CX taxonomy tracks the emotions that show up reliably in customer service calls: frustration, anger, disappointment, worry, admiration, gratitude, and satisfaction. Each maps to a different agent response pattern.
The reliability column below is a rough guide, not a benchmarked figure. The exact numbers depend on your model, your audio quality, and your labeling, so treat these as bands to calibrate against, not targets to trust.
| Emotion | Reliability (approx. F1) | Agent response |
|---|---|---|
| Frustration | 0.80-0.88 | Acknowledge, shorter explanations, offer alternatives |
| Anger | 0.85-0.90 | Immediate escalation path, don't problem-solve |
| Worry/anxiety | 0.70-0.78 | Provide certainty, slower pace, concrete next steps |
| Disappointment | 0.62-0.72 | Validate, explain context, don't over-apologize |
| Satisfaction | 0.78-0.85 | Continue, consider asking for feedback |
| Admiration | 0.68-0.76 | Reinforce what's working |
| Gratitude | 0.75-0.82 | Accept gracefully, don't deflect |
The lower-confidence categories (disappointment, worry) work better as soft signals. Use them to adjust response weights rather than trigger hard rules. The high-confidence categories (frustration, anger, satisfaction) are reliable enough to drive branching logic directly.
Building the Detection Pipeline
A minimal real-time sentiment pipeline has three components: a streaming STT layer that transcribes and diarizes, an emotion inference layer that scores each caller utterance, and a rules engine that turns scores into agent directives. Everything below runs inside the sub-500ms budget. Build them in that order.
Step 1: Streaming STT with speaker diarization
You need per-utterance transcripts with speaker labels. Without diarization, you can't separate caller from agent, and you don't want to run emotion detection on what your own agent is saying. Deepgram's Streaming API returns utterance-level results with speaker labels:
import { createClient, LiveTranscriptionEvents } from "@deepgram/sdk";
const deepgram = createClient(process.env.DEEPGRAM_API_KEY!);
const connection = deepgram.listen.live({
model: "nova-3",
smart_format: true,
diarize: true,
utterance_end_ms: 1000,
});
connection.on(LiveTranscriptionEvents.Transcript, (data) => {
const utterance = data.channel.alternatives[0];
const speaker = utterance.words[0]?.speaker;
// Only analyze caller utterances (speaker 0 in two-party calls)
if (speaker === 0 && utterance.transcript.trim()) {
analyzeEmotion(utterance.transcript, session.id);
}
});Speaker 0 is typically the caller in a two-party call. You'll want to verify this matches your telephony setup, since some providers swap the labels.
Step 2: Emotion inference
A solid open-source option for text-side emotion scoring is j-hartmann/emotion-english-distilroberta-base on HuggingFace. On CPU it runs in roughly 40-90ms for typical utterance lengths, depending on core count and batching, which fits inside the latency budget. On a small GPU you'll see single-digit milliseconds.
from transformers import pipeline
emotion_classifier = pipeline(
"text-classification",
model="j-hartmann/emotion-english-distilroberta-base",
top_k=3, # Return top 3 emotions with confidence scores
)
def classify_emotion(text: str) -> dict[str, float]:
# Pass a list so [0] reliably unwraps to the list of scored labels
results = emotion_classifier([text])[0]
return {
item["label"]: round(item["score"], 3)
for item in results
}classify_emotion("I already called about this three times!")
# {'anger': 0.721, 'disgust': 0.143, 'fear': 0.089}
classify_emotion("Perfect, that's exactly what I needed")
# {'joy': 0.912, 'neutral': 0.061, 'surprise': 0.018}The model's labels don't map exactly to the CX taxonomy. You'll want a thin translation layer: anger maps to anger, disgust maps to frustration, joy maps to satisfaction. Document the mapping explicitly so it's not implicit in your rules engine.
Step 3: Decision rules
This is where sentiment actually influences behavior. The rules engine maps emotion scores to agent directives. Start with the three highest-impact rules:
interface EmotionSignal {
anger: number;
frustration: number;
worry: number;
satisfaction: number;
priorFrustration?: number;
turnIndex: number;
}
type AgentDirective =
| "continue"
| "acknowledge_frustration"
| "offer_escalation"
| "immediate_escalate"
| "provide_reassurance"
| "capture_csat";
export function resolveDirective(signal: EmotionSignal): AgentDirective {
// Anger: don't problem-solve, escalate now
if (signal.anger > 0.80) {
return "immediate_escalate";
}
// Sustained frustration over 2+ turns: offer escalation
if (
signal.frustration > 0.70 &&
signal.priorFrustration !== undefined &&
signal.priorFrustration > 0.60
) {
return "offer_escalation";
}
// Single high-frustration turn after turn 1: acknowledge before continuing
if (signal.frustration > 0.72 && signal.turnIndex > 1) {
return "acknowledge_frustration";
}
// Worry: provide certainty and concrete next steps
if (signal.worry > 0.68) {
return "provide_reassurance";
}
// High satisfaction mid-conversation: good moment to capture CSAT
if (signal.satisfaction > 0.82 && signal.turnIndex > 3) {
return "capture_csat";
}
return "continue";
}The directive gets injected into your agent's per-turn context. If the directive is acknowledge_frustration, you add a context instruction telling the agent to briefly acknowledge the difficulty before moving to problem-solving. If it's offer_escalation, you add an instruction to proactively offer to connect with a specialist.
This is more effective than baking all of this into the initial system prompt, because you're responding to an actual signal from this specific caller, not a hypothetical state you imagined when you wrote the prompt.

Sentiment Analysis
Last 7 days
What Gets Missed Without Diarization?
The most common mistake in early sentiment deployments is running emotion detection on the full audio channel without separating speakers. Your agent's voice gets included in the analysis. If your agent uses calm, measured language, it pulls the sentiment score toward neutral and masks genuine caller frustration.
Diarization is not optional. For two-party calls it's simple: caller is speaker 0, agent is speaker 1. Only run emotion detection on speaker 0 utterances. You'll see a significant improvement in detection accuracy from this change alone.
The secondary mistake is running detection on the agent's side and trying to infer caller emotion from how the agent responds. This is backwards. You want the caller's signal, directly.
Tracking Signals Across the Conversation
Single-turn emotion scores are noisy. A frustrated utterance early in a call might resolve by turn 3. An anxious tone that persists across five consecutive turns is a completely different signal.
Track a rolling emotion state across the conversation:
interface ConversationEmotionState {
currentScores: EmotionSignal;
history: EmotionSignal[];
trend: "improving" | "stable" | "deteriorating";
}
function updateEmotionState(
state: ConversationEmotionState,
newSignal: EmotionSignal
): ConversationEmotionState {
const history = [...state.history, newSignal].slice(-5); // Last 5 turns
const avgFrustration =
history.reduce((sum, s) => sum + s.frustration, 0) / history.length;
const trend =
newSignal.frustration > avgFrustration + 0.15
? "deteriorating"
: newSignal.frustration < avgFrustration - 0.15
? "improving"
: "stable";
return {
currentScores: newSignal,
history,
trend,
};
}A deteriorating trend flag triggers different behavior than a single high-frustration score. You can escalate earlier, skip verification steps that slow the call, or pull customer history before the caller has to re-explain.
The conversation history is also what feeds your post-call quality review. You can see exactly which turns produced a frustration spike and whether your agent's response improved or worsened the trajectory. If you want to understand what happens before callers hang up, this data tells you.
What Breaks in Production
Three failure modes show up reliably once you deploy real-time sentiment: thresholds calibrated on generic data misfire on your callers, over-eager escalation offers annoy people who weren't upset, and handoffs that drop the emotion history force the caller to start over. Each is fixable once you know to look for it.
Threshold miscalibration. The example thresholds above are starting points, not finals. Caller populations vary by use case. A billing call has a different frustration baseline than a technical support call. Calibrate your thresholds against a labeled sample of your own calls before hardcoding any values. Out-of-the-box thresholds built for generic call center data will misfire on your specific population.
False escalations. If you trigger escalation offers too aggressively, callers who weren't actually frustrated become annoyed by the offer. Track how often callers accept escalation offers vs. decline. A decline rate above 60% means your thresholds are too sensitive. Tighten the frustration threshold or require two consecutive high-frustration turns instead of one before offering.
Escalation without context. When you hand off to a human agent, pass the emotion history. The human shouldn't have to ask the caller to repeat everything. They should see the conversation transcript plus the emotion trajectory so they can open with empathy, not procedure. A caller who's been frustrated for four turns doesn't want to start over.
Connecting to Call Quality Monitoring
Real-time sentiment is most valuable when it feeds both live decisions and post-call monitoring. If you're not closing this loop, you're leaving the highest-value part of the system on the table.
An analytics dashboard that aggregates emotion data across your full call volume shows which topics or flows generate the most frustration, which agents achieve the best sentiment recovery, and where emotion scores diverge from CSAT scores. You can't see this pattern in individual call reviews. You need volume.
A scorecard can include a sentiment-responsiveness criterion: did the agent acknowledge frustration when the signal was high? Did it shorten explanations when impatience was detected? These become part of automated quality reviews without requiring a human listener for every call.
Scenario tests let you exercise your sentiment-aware agent before any real caller is affected. Run simulated calls with pre-labeled emotional trajectories and verify that your rules engine fires the right directive at the right turn. It's equivalent to integration testing for the sentiment layer.
For a broader look at what to track in production voice agents beyond sentiment, the AI agent observability gap post covers the signals most teams miss.
Where to Start If You're Building This Now
The path from zero to a working prototype is straightforward. Pick a streaming STT provider that returns utterance-level results with speaker diarization. Deepgram, AssemblyAI, and Speechmatics all support this. Add j-hartmann/emotion-english-distilroberta-base for text-side emotion scoring. Write three rules: anger triggers immediate escalation, sustained frustration over two turns triggers an escalation offer, and worry triggers reassurance mode.
Log every directive and whether the caller responded positively. If an escalation was offered, did they take it? If acknowledgment was given, did frustration decrease in the next turn? This feedback loop is what lets you tune thresholds with real data instead of guessing.
The caller who started your conversation frustrated doesn't have to end it angrier. That outcome is a pipeline problem as much as a product one. Frustration caught at turn 2 resolves more often than frustration allowed to compound through turn 6. The models are fast enough to run in the gap between turns, and you'll know whether it's working in the first week rather than after a quarter of guessing.
If you want to connect sentiment data to your broader memory and context layer (so your agent knows this caller was frustrated on their last call too), that's a different problem, covered in voice AI memory build vs buy.
Monitor sentiment across every call automatically
Chanl's analytics dashboard tracks emotion trends across your call volume and feeds your quality scorecard. See where frustration spikes, which flows cause it, and how your agents respond.
See analytics featuresCo-founder
Building the platform for AI agents at Chanl — tools, testing, and observability for customer experience.
Learn Agentic AI
Weekly. Patterns for shipping agents that work — MCP, scorecards, regression tests, prompts, model comparisons.



