revai-api / app /services /audio.py
Shaankar39's picture
Deploy RevAI API
3d1332e
Raw
History Blame Contribute Delete
3.87 kB
import io
import tempfile
import os
from typing import List, Dict, Any, Optional
from pathlib import Path
try:
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
_vader = SentimentIntensityAnalyzer()
except ImportError:
_vader = None
from app.services.scoring import detect_churn_keywords, CHURN_KEYWORDS
def analyze_sentiment(text: str) -> dict:
"""Run VADER sentiment analysis on transcript text."""
if not text.strip():
return {"compound": 0.0, "pos": 0.0, "neg": 0.0, "neu": 1.0, "label": "neutral"}
if _vader is None:
# fallback: simple heuristic
positive = sum(1 for w in ["good", "great", "excellent", "happy", "love", "thanks", "amazing", "helpful"] if w in text.lower())
negative = sum(1 for w in ["bad", "terrible", "awful", "hate", "frustrated", "angry", "broken", "useless"] if w in text.lower())
compound = (positive - negative) / max(positive + negative + 1, 1)
return {
"compound": round(compound, 4),
"pos": 0.0, "neg": 0.0, "neu": 0.0,
"label": "positive" if compound > 0.1 else ("negative" if compound < -0.1 else "neutral")
}
scores = _vader.polarity_scores(text)
compound = scores["compound"]
label = "positive" if compound >= 0.05 else ("negative" if compound <= -0.05 else "neutral")
return {
"compound": round(compound, 4),
"pos": round(scores["pos"], 4),
"neg": round(scores["neg"], 4),
"neu": round(scores["neu"], 4),
"label": label,
}
def transcribe_with_whisper(audio_bytes: bytes, filename: str, api_key: str) -> str:
"""Transcribe audio using OpenAI Whisper API."""
import httpx
# Determine content type from extension
ext = Path(filename).suffix.lower()
content_type_map = {
".mp3": "audio/mpeg",
".wav": "audio/wav",
".m4a": "audio/mp4",
".mp4": "video/mp4",
".ogg": "audio/ogg",
".webm": "audio/webm",
}
content_type = content_type_map.get(ext, "audio/mpeg")
# Write bytes to temp file
with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp:
tmp.write(audio_bytes)
tmp_path = tmp.name
try:
with open(tmp_path, "rb") as f:
response = httpx.post(
"https://api.openai.com/v1/audio/transcriptions",
headers={"Authorization": f"Bearer {api_key}"},
data={"model": "whisper-1", "response_format": "text"},
files={"file": (filename, f, content_type)},
timeout=60.0,
)
if response.status_code != 200:
raise Exception(f"Whisper API error ({response.status_code}): {response.text[:200]}")
return response.text
finally:
os.unlink(tmp_path)
def analyze_call(audio_bytes: bytes, filename: str, api_key: str) -> dict:
"""Full call analysis: transcript → sentiment → churn keywords."""
transcript = transcribe_with_whisper(audio_bytes, filename, api_key)
sentiment = analyze_sentiment(transcript)
keywords = detect_churn_keywords(transcript)
# Combined churn intent from transcript
churn_score = keywords["churn_intent_score"]
if sentiment["label"] == "negative":
churn_score = min(churn_score + 20, 100)
elif sentiment["label"] == "positive":
churn_score = max(churn_score - 15, 0)
return {
"filename": filename,
"transcript_snippet": transcript[:300] + ("..." if len(transcript) > 300 else ""),
"transcript_full": transcript,
"sentiment_score": sentiment["compound"],
"sentiment_label": sentiment["label"],
"churn_intent_score": churn_score,
"flagged_keywords": keywords["flagged_keywords"],
"flagged_keyword_count": keywords["flagged_keyword_count"],
}