KrizTech100 commited on
Commit
e17c67c
Β·
verified Β·
1 Parent(s): cb2c0d1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +206 -169
app.py CHANGED
@@ -1,208 +1,245 @@
1
- # Call Sentiment Analyzer
2
- # app.py
3
- # Features:
4
- # - Record via microphone OR upload an audio file
5
- # - Transcription via Whisper
6
- # - 5-star multilingual sentiment analysis
7
- # - Call Quality Indicator (noise/volume estimation)
8
- # - Keyword Extraction (top relevant words)
9
-
10
  import gradio as gr
11
- import torch
12
- import numpy as np
13
  import librosa
14
- from transformers import (
15
- AutoProcessor,
16
- AutoModelForSpeechSeq2Seq,
17
- AutoTokenizer,
18
- AutoModelForSequenceClassification,
19
- pipeline
20
- )
 
21
  import torch.nn.functional as F
 
 
 
 
 
 
 
 
22
 
23
- print("Loading models... This may take 1-2 minutes.")
 
24
 
25
  device = "cuda" if torch.cuda.is_available() else "cpu"
26
 
27
- # === Whisper for transcription ===
28
- processor = AutoProcessor.from_pretrained("openai/whisper-base.en")
29
- whisper_model = AutoModelForSpeechSeq2Seq.from_pretrained("openai/whisper-base.en")
30
- whisper_model.eval()
31
- whisper_model.to(device)
32
 
33
- # === Sentiment Model (5-star multilingual) ===
34
- sentiment_tokenizer = AutoTokenizer.from_pretrained('nlptown/bert-base-multilingual-uncased-sentiment')
35
- sentiment_model = AutoModelForSequenceClassification.from_pretrained('nlptown/bert-base-multilingual-uncased-sentiment')
36
- sentiment_model.eval()
37
- sentiment_model.to(device)
38
 
39
- # === Keyword Extraction ===
40
- keyword_extractor = pipeline("ner", aggregation_strategy="simple", device=0 if device == "cuda" else -1)
41
 
42
- print("All models ready!")
 
 
 
 
43
 
 
 
 
 
 
 
44
 
45
- # Helper: Transcribe audio
46
- def transcribe(audio_path):
47
- import soundfile as sf
48
- audio, sr = sf.read(audio_path)
49
 
50
- # Convert stereo to mono if needed
51
- if audio.ndim > 1:
52
- audio = audio.mean(axis=1)
 
53
 
54
- # Resample to 16kHz if needed
55
- if sr != 16000:
56
- audio = librosa.resample(audio.astype(np.float32), orig_sr=sr, target_sr=16000)
57
 
58
- audio = audio.astype(np.float32)
 
 
59
 
60
- # Whisper processes in 30s chunks
61
- chunk_size = 16000 * 30
62
- texts = []
63
- for i in range(0, len(audio), chunk_size):
64
- chunk = audio[i:i + chunk_size]
65
- input_features = processor(chunk, sampling_rate=16000, return_tensors="pt").input_features.to(device)
66
- with torch.no_grad():
67
- predicted_ids = whisper_model.generate(input_features)
68
- text = processor.batch_decode(predicted_ids, skip_special_tokens=True)[0].strip()
69
- if text:
70
- texts.append(text)
71
 
72
- return " ".join(texts)
73
 
74
 
75
- # Helper: Sentiment analysis
76
- def get_sentiment(text):
77
- if not text.strip():
78
- return "β€”", "β€”", "β€”"
79
 
80
- inputs = sentiment_tokenizer(text[:512], return_tensors="pt", truncation=True).to(device)
81
  with torch.no_grad():
82
- logits = sentiment_model(**inputs).logits
 
83
  probs = F.softmax(logits, dim=-1)[0]
84
- pred = torch.argmax(probs).item() + 1
85
- conf = probs[pred - 1].item() * 100
86
 
87
- stars = "⭐" * pred
88
- label = ["Very Negative", "Negative", "Neutral", "Positive", "Very Positive"][pred - 1]
89
- return f"{stars} {label}", f"{conf:.1f}%", pred
90
 
 
 
 
 
 
91
 
92
- # Helper: Keyword extraction
93
- def extract_keywords(text):
94
- try:
95
- entities = keyword_extractor(text)
96
- keywords = [ent['word'] for ent in entities if ent['score'] > 0.8]
97
- return ", ".join(keywords[:8]) if keywords else "None detected"
98
- except:
99
- return "None detected"
100
 
 
 
 
101
 
102
- # Helper: Estimate call quality
103
- def estimate_call_quality(audio_path):
104
  try:
105
- import soundfile as sf
106
- audio, sr = sf.read(audio_path)
107
- if audio.ndim > 1:
108
- audio = audio.mean(axis=1)
109
- audio = audio.astype(np.float32)
110
-
111
- rms = np.sqrt(np.mean(audio ** 2))
112
- volume_db = 20 * np.log10(rms + 1e-8)
113
- volume_level = "Low" if volume_db < -40 else "Good" if volume_db < -20 else "Loud"
114
-
115
- S = np.abs(librosa.stft(audio))
116
- flatness = np.mean(librosa.feature.spectral_flatness(S=S))
117
- noise_level = "High Noise" if flatness > 0.3 else "Clear"
118
-
119
- quality = (
120
- "Good 🟒" if volume_level in ["Good", "Loud"] and noise_level == "Clear"
121
- else "Fair 🟑" if volume_level == "Good"
122
- else "Poor πŸ”΄"
123
  )
124
- volume_pct = max(0, min(100, round(volume_db + 60)))
125
- return quality, volume_pct, noise_level
126
- except:
127
- return "Unknown", 0, "Unknown"
128
-
129
-
130
- # Main analysis function
131
- def analyze_audio(audio_path):
132
- if audio_path is None:
133
- return "No audio provided.", "β€”", "β€”", "β€”", "β€”", "β€”", "β€”"
134
-
135
- # Transcribe
136
- transcript = transcribe(audio_path)
137
- if not transcript:
138
- return "Could not transcribe audio. Please try again.", "β€”", "β€”", "β€”", "β€”", "β€”", "β€”"
139
-
140
- # Sentiment
141
- sentiment_label, confidence, stars = get_sentiment(transcript)
142
-
143
- # Keywords
144
- keywords = extract_keywords(transcript)
145
-
146
- # Call quality
147
- quality, volume_pct, noise = estimate_call_quality(audio_path)
148
-
149
- # Overall health summary
150
- if stars == 5 or stars == 4:
151
- health = "🟒 Positive Call"
152
- elif stars == 3:
153
- health = "🟑 Neutral Call"
154
- else:
155
- health = "πŸ”΄ Needs Attention"
156
-
157
- return (
158
- transcript,
159
- sentiment_label,
160
- confidence,
161
- keywords,
162
- quality,
163
- f"{volume_pct}%",
164
- f"{health} | Noise: {noise}"
165
- )
166
 
 
167
 
168
- # Gradio Interface
169
- with gr.Blocks(title="Call Sentiment Analyzer", theme=gr.themes.Soft()) as demo:
170
- gr.Markdown("# 🎀 Call Sentiment Analyzer")
171
- gr.Markdown("""
172
- **Record or upload an audio file to analyze:**
173
- - Transcription via Whisper
174
- - 5-star sentiment analysis (multilingual)
175
- - Call quality monitoring
176
- - Keyword extraction
177
- """)
178
-
179
- with gr.Row():
180
- audio_input = gr.Audio(
181
- sources=["microphone", "upload"],
182
- type="filepath",
183
- label="πŸŽ™οΈ Record or Upload Audio"
184
- )
 
 
 
 
 
 
 
 
 
 
 
185
 
186
- analyze_btn = gr.Button("πŸ” Analyze", variant="primary")
187
 
188
- with gr.Row():
189
- with gr.Column():
190
- transcript_box = gr.Textbox(label="πŸ“ Transcript", lines=6, interactive=False)
191
- with gr.Column():
192
- sentiment_box = gr.Textbox(label="😊 Sentiment", interactive=False)
193
- conf_box = gr.Textbox(label="πŸ“Š Confidence", interactive=False)
194
- keyword_box = gr.Textbox(label="πŸ”‘ Detected Keywords", interactive=False)
195
 
196
- with gr.Row():
197
- quality_box = gr.Textbox(label="οΏ½οΏ½ Call Quality", interactive=False)
198
- volume_box = gr.Textbox(label="πŸ”Š Volume Level", interactive=False)
199
- health_box = gr.Textbox(label="❀️ Call Health", interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
  analyze_btn.click(
202
- fn=analyze_audio,
203
- inputs=audio_input,
204
- outputs=[transcript_box, sentiment_box, conf_box, keyword_box, quality_box, volume_box, health_box]
 
 
 
 
 
 
205
  )
206
 
 
207
  if __name__ == "__main__":
208
- demo.launch()
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
+ from dotenv import load_dotenv
3
+ import assemblyai as aai
4
  import librosa
5
+ import soundfile as sf
6
+ import torch
7
+ import json
8
+ import csv
9
+ import os
10
+ import tempfile
11
+ from datetime import datetime
12
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
13
  import torch.nn.functional as F
14
+ from docx import Document
15
+ from reportlab.platypus import SimpleDocTemplate, Paragraph
16
+ from reportlab.lib.styles import getSampleStyleSheet
17
+
18
+ # =========================
19
+ # CONFIG
20
+ # =========================
21
+ load_dotenv()
22
 
23
+ aai.settings.api_key = os.getenv("ASSEMBLYAI_API_KEY")
24
+ hf_token = os.getenv("HF_TOKEN")
25
 
26
  device = "cuda" if torch.cuda.is_available() else "cpu"
27
 
28
+ tokenizer = AutoTokenizer.from_pretrained(
29
+ "nlptown/bert-base-multilingual-uncased-sentiment"
30
+ )
 
 
31
 
32
+ model = AutoModelForSequenceClassification.from_pretrained(
33
+ "nlptown/bert-base-multilingual-uncased-sentiment"
34
+ )
 
 
35
 
36
+ model.to(device)
37
+ model.eval()
38
 
39
+ # =========================
40
+ # GLOBAL
41
+ # =========================
42
+ global_segments = []
43
+ global_conversation = ""
44
 
45
+ # =========================
46
+ # HELPERS
47
+ # =========================
48
+ def format_time(ms):
49
+ s = ms / 1000
50
+ return f"{int(s//60):02d}:{int(s%60):02d}"
51
 
 
 
 
 
52
 
53
+ def build_segments(transcript):
54
+ speaker_map = {}
55
+ current_id = 1
56
+ segments = []
57
 
58
+ for u in transcript.utterances:
59
+ raw = str(u.speaker)
 
60
 
61
+ if raw not in speaker_map:
62
+ speaker_map[raw] = current_id
63
+ current_id += 1
64
 
65
+ segments.append({
66
+ "speaker": speaker_map[raw],
67
+ "start": format_time(u.start or 0),
68
+ "end": format_time(u.end or 0),
69
+ "text": u.text
70
+ })
 
 
 
 
 
71
 
72
+ return segments
73
 
74
 
75
+ def analyze_text(text):
76
+ inputs = tokenizer(text[:512], return_tensors="pt", truncation=True).to(device)
 
 
77
 
 
78
  with torch.no_grad():
79
+ logits = model(**inputs).logits
80
+
81
  probs = F.softmax(logits, dim=-1)[0]
82
+ return torch.argmax(probs).item() + 1
 
83
 
 
 
 
84
 
85
+ # =========================
86
+ # MAIN PROCESS
87
+ # =========================
88
+ def process_audio(file, speakers=0, language="auto"):
89
+ global global_segments, global_conversation
90
 
91
+ if file is None:
92
+ return "❌ No audio provided", "", ""
 
 
 
 
 
 
93
 
94
+ path = file if isinstance(file, str) else file.name
95
+
96
+ temp_path = None
97
 
 
 
98
  try:
99
+ # Load audio
100
+ audio, sr = librosa.load(path, sr=None, mono=True)
101
+
102
+ # πŸ”₯ Create TEMP FILE (not saved permanently)
103
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
104
+ sf.write(tmp.name, audio, sr)
105
+ temp_path = tmp.name
106
+
107
+ config = aai.TranscriptionConfig(
108
+ speaker_labels=True,
109
+ speakers_expected=speakers if speakers > 0 else None,
110
+ language_code=None if language == "auto" else language
 
 
 
 
 
 
111
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
+ transcript = aai.Transcriber().transcribe(temp_path, config)
114
 
115
+ if transcript.error:
116
+ return f"❌ {transcript.error}", "", ""
117
+
118
+ global_segments = build_segments(transcript)
119
+
120
+ speaker_count = len(set(s["speaker"] for s in global_segments))
121
+
122
+ label_map = {
123
+ 1: ("πŸ”΄", "Very Negative"),
124
+ 2: ("🟠", "Negative"),
125
+ 3: ("🟑", "Neutral"),
126
+ 4: ("🟒", "Positive"),
127
+ 5: ("🟒", "Very Positive")
128
+ }
129
+
130
+ conversation = ""
131
+
132
+ for i, seg in enumerate(global_segments, start=1):
133
+ score = analyze_text(seg["text"])
134
+ emoji, label = label_map.get(score, ("βšͺ", "Unknown"))
135
+
136
+ conversation += (
137
+ f"Speaker {seg['speaker']} | Utterance {i}\n"
138
+ f"({seg['start']} - {seg['end']})\n"
139
+ f"{emoji} {label}: {seg['text']}\n\n"
140
+ )
141
+
142
+ global_conversation = conversation
143
 
144
+ return "βœ… Done", conversation, f"Speakers: {speaker_count} | Utterances: {len(global_segments)}"
145
 
146
+ except Exception as e:
147
+ return f"❌ Error: {str(e)}", "", ""
 
 
 
 
 
148
 
149
+ finally:
150
+ # πŸ”₯ DELETE temp file ALWAYS
151
+ if temp_path and os.path.exists(temp_path):
152
+ os.remove(temp_path)
153
+
154
+
155
+ # =========================
156
+ # EXPORT
157
+ # =========================
158
+ def export_file(format_type):
159
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
160
+
161
+ if format_type == "TXT":
162
+ path = f"conversation_{timestamp}.txt"
163
+ with open(path, "w", encoding="utf-8") as f:
164
+ f.write(global_conversation)
165
+
166
+ elif format_type == "JSON":
167
+ path = f"conversation_{timestamp}.json"
168
+ with open(path, "w", encoding="utf-8") as f:
169
+ json.dump(global_segments, f, indent=4)
170
+
171
+ elif format_type == "CSV":
172
+ path = f"conversation_{timestamp}.csv"
173
+ with open(path, "w", newline="", encoding="utf-8") as f:
174
+ writer = csv.DictWriter(f, fieldnames=["speaker", "start", "end", "text"])
175
+ writer.writeheader()
176
+ writer.writerows(global_segments)
177
+
178
+ elif format_type == "WORD":
179
+ path = f"conversation_{timestamp}.docx"
180
+ doc = Document()
181
+ doc.add_heading("Conversation Transcript", 0)
182
+ doc.add_paragraph(global_conversation)
183
+ doc.save(path)
184
+
185
+ elif format_type == "PDF":
186
+ path = f"conversation_{timestamp}.pdf"
187
+ doc = SimpleDocTemplate(path)
188
+ styles = getSampleStyleSheet()
189
+ content = [Paragraph(global_conversation.replace("\n", "<br/>"), styles["Normal"])]
190
+ doc.build(content)
191
+
192
+ return path
193
+
194
+
195
+ # =========================
196
+ # UI
197
+ # =========================
198
+ with gr.Blocks(title="AI Conversation Sentiment System") as app:
199
+
200
+ gr.Markdown("# πŸŽ™ AI Conversation Sentiment Analyzer")
201
+
202
+ with gr.Group():
203
+ gr.Markdown("### πŸŽ™ Input Audio")
204
+ audio = gr.Audio(sources=["upload", "microphone"], type="filepath")
205
+
206
+ with gr.Group():
207
+ gr.Markdown("### βš™ Settings")
208
+ with gr.Row():
209
+ speakers = gr.Number(value=0, label="Speakers (0 = auto)")
210
+ language = gr.Dropdown(["auto", "en", "fr", "es", "de"], value="auto")
211
+
212
+ analyze_btn = gr.Button("πŸš€ Analyze")
213
+
214
+ with gr.Group():
215
+ gr.Markdown("### πŸ’¬ Conversation Output")
216
+ status = gr.Textbox(label="Status")
217
+ conversation_box = gr.Textbox(lines=18, label="Conversation + Sentiment")
218
+ info = gr.Textbox(label="Info")
219
+
220
+ with gr.Group():
221
+ gr.Markdown("### πŸ“ Export")
222
+ with gr.Row():
223
+ export_format = gr.Dropdown(
224
+ ["TXT", "JSON", "CSV", "WORD", "PDF"],
225
+ value="TXT",
226
+ label="Select Format"
227
+ )
228
+ export_btn = gr.Button("⬇ Export")
229
+ download = gr.File()
230
 
231
  analyze_btn.click(
232
+ process_audio,
233
+ inputs=[audio, speakers, language],
234
+ outputs=[status, conversation_box, info]
235
+ )
236
+
237
+ export_btn.click(
238
+ export_file,
239
+ inputs=[export_format],
240
+ outputs=[download]
241
  )
242
 
243
+
244
  if __name__ == "__main__":
245
+ app.launch(theme=gr.themes.Soft())