martinoyovo commited on
Commit
eb2cf07
Β·
1 Parent(s): 1a3e21e
Files changed (9) hide show
  1. README.md +161 -0
  2. analysis_results.csv +2 -0
  3. api.py +563 -0
  4. app.py +229 -0
  5. main.py +60 -0
  6. render.yaml +9 -0
  7. requirements.txt +12 -0
  8. utils.py +18 -0
  9. voice_sentiment.py +128 -0
README.md ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Voice Sentiment Analysis System
2
+
3
+ <div align="center">
4
+ <img src="gradio_interface.jpg" alt="Voice Sentiment Analysis Banner" width="100%">
5
+ </div>
6
+
7
+ ## Project Description
8
+
9
+ This project is an automated solution for analyzing customer satisfaction from voice calls. Built using state-of-the-art machine learning models, it combines **Wav2Vec 2.0** for speech-to-text transcription with **BERT** for sentiment analysis to provide real-time feedback into customer emotions and satisfaction levels.
10
+
11
+ ### Key Features
12
+ - **Automatic Speech Recognition**: Convert voice calls to text using Wav2Vec 2.0
13
+ - **Sentiment Analysis**: Analyze emotional tone using multilingual BERT
14
+ - **Customer Satisfaction Classification**: Categorize calls as Satisfied, Dissatisfied, or Neutral
15
+ - **Batch Processing**: Handle multiple audio files simultaneously
16
+ - **Web Interface**: User-friendly [Gradio](https://www.gradio.app/) interface for easy interaction
17
+ - **CSV Export**: Detailed results export for further analysis and reporting
18
+
19
+ ## Models Used
20
+
21
+ This project uses pre-trained models hosted on Hugging Face Hub:
22
+
23
+ ### Speech Recognition Model
24
+ **Wav2Vec 2.0 - English**
25
+ - **Model:** `facebook/wav2vec2-large-960h-lv60-self`
26
+ - **Link:** [https://huggingface.co/facebook/wav2vec2-large-960h-lv60-self](https://huggingface.co/facebook/wav2vec2-large-960h-lv60-self)
27
+ - **Description:** Large Wav2Vec 2.0 model trained on 960 hours of English LibriSpeech data
28
+ - **Use:** Audio-to-text transcription
29
+
30
+ ### Sentiment Analysis Model
31
+ **BERT - Multilingual Sentiment**
32
+ - **Model:** `nlptown/bert-base-multilingual-uncased-sentiment`
33
+ - **Link:** [https://huggingface.co/nlptown/bert-base-multilingual-uncased-sentiment](https://huggingface.co/nlptown/bert-base-multilingual-uncased-sentiment)
34
+ - **Description:** Multilingual BERT model fine-tuned for sentiment analysis (1-5 stars)
35
+ - **Use:** Text sentiment classification
36
+
37
+
38
+ ## Project Structure
39
+
40
+ ```
41
+ voice-sentiment-project/
42
+ β”œβ”€β”€ requirements.txt # Dependencies
43
+ β”œβ”€β”€ voice_sentiment.py # Core analyzer class
44
+ β”œβ”€β”€ api.py # REST API Server
45
+ β”œβ”€β”€ app.py # Gradio web interface
46
+ β”œβ”€β”€ main.py # CLI interface
47
+ β”œβ”€β”€ utils.py # Utility functions and CSS styling
48
+ β”œβ”€β”€ audios/ # Your audio files
49
+ β”‚ β”œβ”€β”€ call1.wav
50
+ β”‚ β”œβ”€β”€ call2.mp3
51
+ β”‚ └── ...
52
+ └── analysis_results.csv # Generated results
53
+ ```
54
+ ## Language Support
55
+
56
+ ### Current Model: English Only
57
+ This system is currently configured with an English-only Wav2Vec 2.0 model (`facebook/wav2vec2-large-960h-lv60-self`) for optimal English speech recognition performance.
58
+
59
+ ### For Other Languages
60
+ To use this system with other languages, you need to change the Wav2Vec 2.0 model in `voice_sentiment.py`.
61
+
62
+ ## Quick Installation
63
+
64
+ ```bash
65
+ pip install -r requirements.txt
66
+ ```
67
+
68
+ ## Usage
69
+
70
+ ### 1. Web Interface (Recommended)
71
+
72
+ ```bash
73
+ python app.py
74
+ ```
75
+
76
+ Opens a web browser interface at `http://localhost:7860`
77
+
78
+ ### 2. Command Line Interface
79
+
80
+ ```bash
81
+ python main.py
82
+ ```
83
+
84
+ ### 3. Direct Code Usage
85
+
86
+ ```python
87
+ from voice_sentiment import VoiceSentimentAnalyzer
88
+
89
+ # Initialize
90
+ analyzer = VoiceSentimentAnalyzer()
91
+
92
+ # Analyze one call
93
+ result = analyzer.analyze_call("call1.wav")
94
+ print(result)
95
+
96
+ # Analyze multiple calls
97
+ results = analyzer.analyze_batch("audios/")
98
+ ```
99
+
100
+ ## Example Output
101
+
102
+ ```python
103
+ {
104
+ 'file': 'call1.wav',
105
+ 'transcription': 'Hello I am very satisfied with your service',
106
+ 'sentiment': 'POSITIVE',
107
+ 'score': 0.89,
108
+ 'satisfaction': 'Satisfied'
109
+ }
110
+ ```
111
+
112
+ ## Simple Workflow
113
+
114
+ ```
115
+ Audio File β†’ Transcription (Wav2Vec2) β†’ Sentiment (BERT) β†’ Classification
116
+ ```
117
+
118
+ Perfect for analyzing customer call sentiment quickly and easily!
119
+
120
+ ## Supported Audio Formats
121
+
122
+ ### **Fully Supported**
123
+ - **WAV** (.wav) - *Recommended for best quality*
124
+ - **MP3** (.mp3) - *Most common format*
125
+ - **M4A** (.m4a) - *Apple audio format*
126
+
127
+ ### **Audio Specifications**
128
+ - **Sample Rate**: Automatically converted to 16kHz
129
+ - **Channels**: Mono or Stereo (converted to mono)
130
+ - **Duration**: 5 seconds to 10 minutes (optimal: 30 seconds - 2 minutes)
131
+ - **Quality**: Clear speech, minimal background noise recommended
132
+
133
+ ### **Not Supported**
134
+ - Video files (MP4, AVI, MOV, etc.)
135
+ - Other audio formats (FLAC, OGG, etc.) - *may work but not guaranteed*
136
+ - Extremely low quality or heavily distorted audio
137
+ - Files with encryption or DRM protection
138
+
139
+ ### **Audio Quality Tips**
140
+ - Use WAV format for highest accuracy
141
+ - Ensure clear speech recording
142
+ - Minimize background noise
143
+ - Optimal recording: 16kHz, 16-bit, mono
144
+ - Test with short samples first
145
+
146
+ ## CSV Output & Results
147
+
148
+ ### **Automatic CSV Generation**
149
+ When using batch analysis (multiple files), the system automatically generates a detailed CSV file with all results.
150
+
151
+ **File**: `analysis_results.csv`
152
+
153
+ **Location**: Same folder as the project
154
+
155
+ ### **CSV Contents**
156
+ ```csv
157
+ File,Transcription,Sentiment,Score,Satisfaction
158
+ call1.wav,"Hello I am very satisfied with your service",POSITIVE,0.89,Satisfied
159
+ call2.wav,"This is unacceptable I want a refund",NEGATIVE,0.92,Dissatisfied
160
+ call3.wav,"Can you tell me about your pricing",NEUTRAL,0.65,Neutral
161
+ ```
analysis_results.csv ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ File,Transcription,Sentiment,Score,Satisfaction
2
+ satistaction_fr.m4a,HERO GISPE I THEACCUSETRES ATISFE DE VA SERVIC RAMAND GENEREETPAT DE MATRE BONE EFONICE THE MAYOR SE...,POSITIVE,0.34,Neutral
api.py ADDED
@@ -0,0 +1,563 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ REST API for Voice Sentiment Analysis System
4
+ Provides endpoints for integrating the pipeline into other applications
5
+ """
6
+
7
+ from flask import Flask, request, jsonify, render_template_string
8
+ from flask_cors import CORS
9
+ import os
10
+ import tempfile
11
+ import uuid
12
+ from voice_sentiment import VoiceSentimentAnalyzer
13
+ import logging
14
+
15
+ # Initialize Flask app
16
+ app = Flask(__name__)
17
+ CORS(app) # Enable CORS for cross-origin requests
18
+
19
+ # Configure logging
20
+ logging.basicConfig(level=logging.INFO)
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Initialize the analyzer (singleton)
24
+ analyzer = None
25
+
26
+ def get_analyzer():
27
+ """Get or create analyzer instance"""
28
+ global analyzer
29
+ if analyzer is None:
30
+ logger.info("Initializing Voice Sentiment Analyzer...")
31
+ analyzer = VoiceSentimentAnalyzer()
32
+ logger.info("Analyzer ready!")
33
+ return analyzer
34
+
35
+ # API Documentation HTML Template
36
+ API_DOCS_HTML = """
37
+ <!DOCTYPE html>
38
+ <html>
39
+ <head>
40
+ <title>Voice Sentiment Analysis API Documentation</title>
41
+ <style>
42
+ body { font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; }
43
+ .header { background: #f4f4f4; padding: 20px; border-radius: 5px; margin-bottom: 30px; }
44
+ .endpoint { background: #f9f9f9; padding: 15px; margin: 20px 0; border-left: 4px solid #007cba; }
45
+ .method { background: #007cba; color: white; padding: 3px 8px; border-radius: 3px; font-size: 12px; }
46
+ .method.get { background: #28a745; }
47
+ .method.post { background: #007cba; }
48
+ pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }
49
+ code { background: #f4f4f4; padding: 2px 4px; border-radius: 3px; }
50
+ .example { margin: 10px 0; }
51
+ h1 { color: #333; }
52
+ h2 { color: #007cba; border-bottom: 2px solid #007cba; padding-bottom: 5px; }
53
+ h3 { color: #555; }
54
+ </style>
55
+ </head>
56
+ <body>
57
+ <div class="header">
58
+ <h1>Voice Sentiment Analysis API</h1>
59
+ <p><strong>Version:</strong> 1.0.0</p>
60
+ <p><strong>Base URL:</strong> <code>{{ base_url }}</code></p>
61
+ <p>Analyze customer call sentiment using Wav2Vec 2.0 + BERT pipeline</p>
62
+ </div>
63
+
64
+ <h2>Authentication</h2>
65
+ <p>No authentication required for this API.</p>
66
+
67
+ <h2>Supported Audio Formats</h2>
68
+ <ul>
69
+ <li><strong>WAV</strong> (.wav) - Recommended</li>
70
+ <li><strong>MP3</strong> (.mp3)</li>
71
+ <li><strong>M4A</strong> (.m4a)</li>
72
+ </ul>
73
+
74
+ <h2>API Endpoints</h2>
75
+
76
+ <div class="endpoint">
77
+ <h3><span class="method get">GET</span> /docs</h3>
78
+ <p><strong>Description:</strong> This documentation page</p>
79
+ <p><strong>Response:</strong> HTML documentation</p>
80
+ </div>
81
+
82
+ <div class="endpoint">
83
+ <h3><span class="method get">GET</span> /health</h3>
84
+ <p><strong>Description:</strong> Health check endpoint</p>
85
+ <p><strong>Response:</strong></p>
86
+ <pre><code>{
87
+ "status": "healthy",
88
+ "service": "Voice Sentiment Analysis API",
89
+ "version": "1.0.0"
90
+ }</code></pre>
91
+ </div>
92
+
93
+ <div class="endpoint">
94
+ <h3><span class="method post">POST</span> /analyze</h3>
95
+ <p><strong>Description:</strong> Analyze a single audio file for sentiment</p>
96
+ <p><strong>Content-Type:</strong> multipart/form-data</p>
97
+ <p><strong>Parameters:</strong></p>
98
+ <ul>
99
+ <li><code>audio</code> (file, required): Audio file to analyze</li>
100
+ </ul>
101
+
102
+ <div class="example">
103
+ <p><strong>Example Request (cURL):</strong></p>
104
+ <pre><code>curl -X POST \\
105
+ -F "audio=@call1.wav" \\
106
+ {{ base_url }}/analyze</code></pre>
107
+ </div>
108
+
109
+ <div class="example">
110
+ <p><strong>Example Response:</strong></p>
111
+ <pre><code>{
112
+ "success": true,
113
+ "data": {
114
+ "filename": "call1.wav",
115
+ "transcription": "Hello I am very satisfied with your service",
116
+ "sentiment": "POSITIVE",
117
+ "confidence_score": 0.89,
118
+ "satisfaction": "Satisfied"
119
+ },
120
+ "processing_id": "uuid-string"
121
+ }</code></pre>
122
+ </div>
123
+
124
+ <div class="example">
125
+ <p><strong>Error Response:</strong></p>
126
+ <pre><code>{
127
+ "error": "Unsupported file format",
128
+ "message": "Supported formats: .wav, .mp3, .m4a, .flac",
129
+ "received": ".txt"
130
+ }</code></pre>
131
+ </div>
132
+ </div>
133
+
134
+ <div class="endpoint">
135
+ <h3><span class="method post">POST</span> /analyze/batch</h3>
136
+ <p><strong>Description:</strong> Analyze multiple audio files</p>
137
+ <p><strong>Content-Type:</strong> multipart/form-data</p>
138
+ <p><strong>Parameters:</strong></p>
139
+ <ul>
140
+ <li><code>audio</code> (files, required): Multiple audio files to analyze</li>
141
+ </ul>
142
+
143
+ <div class="example">
144
+ <p><strong>Example Request (cURL):</strong></p>
145
+ <pre><code>curl -X POST \\
146
+ -F "audio=@call1.wav" \\
147
+ -F "audio=@call2.mp3" \\
148
+ {{ base_url }}/analyze/batch</code></pre>
149
+ </div>
150
+
151
+ <div class="example">
152
+ <p><strong>Example Response:</strong></p>
153
+ <pre><code>{
154
+ "success": true,
155
+ "batch_id": "uuid-string",
156
+ "statistics": {
157
+ "total_files": 2,
158
+ "sentiment_distribution": {
159
+ "POSITIVE": {"count": 1, "percentage": 50.0},
160
+ "NEGATIVE": {"count": 1, "percentage": 50.0}
161
+ },
162
+ "satisfaction_distribution": {
163
+ "Satisfied": {"count": 1, "percentage": 50.0},
164
+ "Dissatisfied": {"count": 1, "percentage": 50.0}
165
+ }
166
+ },
167
+ "results": [
168
+ {
169
+ "filename": "call1.wav",
170
+ "transcription": "Hello I am satisfied",
171
+ "sentiment": "POSITIVE",
172
+ "confidence_score": 0.89,
173
+ "satisfaction": "Satisfied",
174
+ "success": true
175
+ },
176
+ {
177
+ "filename": "call2.mp3",
178
+ "transcription": "This is terrible service",
179
+ "sentiment": "NEGATIVE",
180
+ "confidence_score": 0.92,
181
+ "satisfaction": "Dissatisfied",
182
+ "success": true
183
+ }
184
+ ],
185
+ "processed_files": 2,
186
+ "total_uploaded": 2
187
+ }</code></pre>
188
+ </div>
189
+ </div>
190
+
191
+ <div class="endpoint">
192
+ <h3><span class="method get">GET</span> /models/info</h3>
193
+ <p><strong>Description:</strong> Get information about loaded models</p>
194
+ <p><strong>Response:</strong></p>
195
+ <pre><code>{
196
+ "speech_recognition": {
197
+ "model": "facebook/wav2vec2-large-960h-lv60-self",
198
+ "type": "Wav2Vec 2.0",
199
+ "language": "English",
200
+ "description": "Large Wav2Vec 2.0 model for English speech recognition"
201
+ },
202
+ "sentiment_analysis": {
203
+ "model": "nlptown/bert-base-multilingual-uncased-sentiment",
204
+ "type": "BERT",
205
+ "language": "Multilingual",
206
+ "description": "Multilingual BERT for sentiment analysis"
207
+ },
208
+ "supported_formats": [".wav", ".mp3", ".m4a", ".flac"],
209
+ "classifications": {
210
+ "sentiments": ["POSITIVE", "NEGATIVE", "NEUTRAL"],
211
+ "satisfaction": ["Satisfied", "Dissatisfied", "Neutral"]
212
+ }
213
+ }</code></pre>
214
+ </div>
215
+
216
+ <h2>Response Codes</h2>
217
+ <ul>
218
+ <li><strong>200</strong> - Success</li>
219
+ <li><strong>400</strong> - Bad Request (invalid file, missing parameters)</li>
220
+ <li><strong>404</strong> - Endpoint Not Found</li>
221
+ <li><strong>413</strong> - File Too Large (>16MB)</li>
222
+ <li><strong>500</strong> - Internal Server Error</li>
223
+ </ul>
224
+
225
+ <h2>Integration Examples</h2>
226
+
227
+ <h3>Python</h3>
228
+ <pre><code>import requests
229
+
230
+ # Single file analysis
231
+ with open('audio.wav', 'rb') as f:
232
+ response = requests.post(
233
+ '{{ base_url }}/analyze',
234
+ files={'audio': f}
235
+ )
236
+ result = response.json()
237
+ print(f"Sentiment: {result['data']['sentiment']}")
238
+
239
+ # Batch analysis
240
+ files = [
241
+ ('audio', open('call1.wav', 'rb')),
242
+ ('audio', open('call2.mp3', 'rb'))
243
+ ]
244
+ response = requests.post('{{ base_url }}/analyze/batch', files=files)
245
+ result = response.json()
246
+ print(f"Processed {result['processed_files']} files")</code></pre>
247
+
248
+ <h3>JavaScript</h3>
249
+ <pre><code>// Single file upload
250
+ const formData = new FormData();
251
+ formData.append('audio', fileInput.files[0]);
252
+
253
+ fetch('{{ base_url }}/analyze', {
254
+ method: 'POST',
255
+ body: formData
256
+ })
257
+ .then(response => response.json())
258
+ .then(data => {
259
+ console.log('Sentiment:', data.data.sentiment);
260
+ });</code></pre>
261
+
262
+ <h3>Node.js</h3>
263
+ <pre><code>const fs = require('fs');
264
+ const FormData = require('form-data');
265
+
266
+ const form = new FormData();
267
+ form.append('audio', fs.createReadStream('call.wav'));
268
+
269
+ fetch('{{ base_url }}/analyze', {
270
+ method: 'POST',
271
+ body: form
272
+ })
273
+ .then(response => response.json())
274
+ .then(data => console.log(data));</code></pre>
275
+
276
+ <h2>Rate Limits</h2>
277
+ <p>Currently no rate limits are enforced. For production use, consider implementing rate limiting.</p>
278
+
279
+ <h2>File Size Limits</h2>
280
+ <ul>
281
+ <li><strong>Maximum file size:</strong> 16MB per file</li>
282
+ <li><strong>Recommended:</strong> Keep files under 5MB for faster processing</li>
283
+ <li><strong>Optimal duration:</strong> 30 seconds to 2 minutes</li>
284
+ </ul>
285
+
286
+ <footer style="margin-top: 50px; padding-top: 20px; border-top: 1px solid #eee; color: #666;">
287
+ <p>Voice Sentiment Analysis API - Powered by Wav2Vec 2.0 + BERT</p>
288
+ </footer>
289
+ </body>
290
+ </html>
291
+ """
292
+
293
+ @app.route('/docs', methods=['GET'])
294
+ @app.route('/documentation', methods=['GET'])
295
+ @app.route('/', methods=['GET'])
296
+ def api_documentation():
297
+ """API Documentation page"""
298
+ base_url = request.url_root.rstrip('/')
299
+ return render_template_string(API_DOCS_HTML, base_url=base_url)
300
+
301
+ @app.route('/health', methods=['GET'])
302
+ def health_check():
303
+ """Health check endpoint"""
304
+ return jsonify({
305
+ "status": "healthy",
306
+ "service": "Voice Sentiment Analysis API",
307
+ "version": "1.0.0"
308
+ })
309
+
310
+ @app.route('/analyze', methods=['POST'])
311
+ def analyze_audio():
312
+ """
313
+ Analyze a single audio file
314
+
315
+ Expected: multipart/form-data with 'audio' file
316
+ Returns: JSON with analysis results
317
+ """
318
+ try:
319
+ # Check if file is present
320
+ if 'audio' not in request.files:
321
+ return jsonify({
322
+ "error": "No audio file provided",
323
+ "message": "Please upload an audio file using the 'audio' field"
324
+ }), 400
325
+
326
+ audio_file = request.files['audio']
327
+
328
+ # Check if file is selected
329
+ if audio_file.filename == '':
330
+ return jsonify({
331
+ "error": "No file selected",
332
+ "message": "Please select an audio file"
333
+ }), 400
334
+
335
+ # Validate file extension
336
+ allowed_extensions = ['.wav', '.mp3', '.m4a', '.flac']
337
+ file_ext = os.path.splitext(audio_file.filename)[1].lower()
338
+
339
+ if file_ext not in allowed_extensions:
340
+ return jsonify({
341
+ "error": "Unsupported file format",
342
+ "message": f"Supported formats: {', '.join(allowed_extensions)}",
343
+ "received": file_ext
344
+ }), 400
345
+
346
+ # Save file temporarily
347
+ temp_id = str(uuid.uuid4())
348
+ temp_filename = f"temp_audio_{temp_id}{file_ext}"
349
+ temp_path = os.path.join(tempfile.gettempdir(), temp_filename)
350
+
351
+ audio_file.save(temp_path)
352
+
353
+ try:
354
+ # Analyze the audio
355
+ analyzer = get_analyzer()
356
+ result = analyzer.analyze_call(temp_path)
357
+
358
+ # Clean up temporary file
359
+ os.remove(temp_path)
360
+
361
+ # Return results
362
+ return jsonify({
363
+ "success": True,
364
+ "data": {
365
+ "filename": audio_file.filename,
366
+ "transcription": result['transcription'],
367
+ "sentiment": result['sentiment'],
368
+ "confidence_score": round(result['score'], 3),
369
+ "satisfaction": result['satisfaction']
370
+ },
371
+ "processing_id": temp_id
372
+ })
373
+
374
+ except Exception as e:
375
+ # Clean up on error
376
+ if os.path.exists(temp_path):
377
+ os.remove(temp_path)
378
+ raise e
379
+
380
+ except Exception as e:
381
+ logger.error(f"Error processing audio: {str(e)}")
382
+ return jsonify({
383
+ "error": "Processing failed",
384
+ "message": str(e)
385
+ }), 500
386
+
387
+ @app.route('/analyze/batch', methods=['POST'])
388
+ def analyze_batch():
389
+ """
390
+ Analyze multiple audio files
391
+
392
+ Expected: multipart/form-data with multiple 'audio' files
393
+ Returns: JSON with batch analysis results
394
+ """
395
+ try:
396
+ # Check if files are present
397
+ if 'audio' not in request.files:
398
+ return jsonify({
399
+ "error": "No audio files provided",
400
+ "message": "Please upload audio files using the 'audio' field"
401
+ }), 400
402
+
403
+ audio_files = request.files.getlist('audio')
404
+
405
+ if not audio_files or all(f.filename == '' for f in audio_files):
406
+ return jsonify({
407
+ "error": "No files selected",
408
+ "message": "Please select audio files"
409
+ }), 400
410
+
411
+ results = []
412
+ temp_files = []
413
+ batch_id = str(uuid.uuid4())
414
+
415
+ try:
416
+ # Process each file
417
+ for i, audio_file in enumerate(audio_files):
418
+ if audio_file.filename == '':
419
+ continue
420
+
421
+ # Validate file extension
422
+ allowed_extensions = ['.wav', '.mp3', '.m4a', '.flac']
423
+ file_ext = os.path.splitext(audio_file.filename)[1].lower()
424
+
425
+ if file_ext not in allowed_extensions:
426
+ results.append({
427
+ "filename": audio_file.filename,
428
+ "error": f"Unsupported format: {file_ext}",
429
+ "success": False
430
+ })
431
+ continue
432
+
433
+ # Save file temporarily
434
+ temp_filename = f"batch_{batch_id}_{i}{file_ext}"
435
+ temp_path = os.path.join(tempfile.gettempdir(), temp_filename)
436
+ temp_files.append(temp_path)
437
+
438
+ audio_file.save(temp_path)
439
+
440
+ # Analyze the audio
441
+ analyzer = get_analyzer()
442
+ result = analyzer.analyze_call(temp_path)
443
+
444
+ results.append({
445
+ "filename": audio_file.filename,
446
+ "transcription": result['transcription'],
447
+ "sentiment": result['sentiment'],
448
+ "confidence_score": round(result['score'], 3),
449
+ "satisfaction": result['satisfaction'],
450
+ "success": True
451
+ })
452
+
453
+ # Calculate statistics
454
+ successful_results = [r for r in results if r.get('success', False)]
455
+ total_files = len(successful_results)
456
+
457
+ if total_files > 0:
458
+ sentiment_counts = {}
459
+ satisfaction_counts = {}
460
+
461
+ for result in successful_results:
462
+ sentiment = result['sentiment']
463
+ satisfaction = result['satisfaction']
464
+
465
+ sentiment_counts[sentiment] = sentiment_counts.get(sentiment, 0) + 1
466
+ satisfaction_counts[satisfaction] = satisfaction_counts.get(satisfaction, 0) + 1
467
+
468
+ statistics = {
469
+ "total_files": total_files,
470
+ "sentiment_distribution": {
471
+ k: {"count": v, "percentage": round(v/total_files*100, 1)}
472
+ for k, v in sentiment_counts.items()
473
+ },
474
+ "satisfaction_distribution": {
475
+ k: {"count": v, "percentage": round(v/total_files*100, 1)}
476
+ for k, v in satisfaction_counts.items()
477
+ }
478
+ }
479
+ else:
480
+ statistics = {"total_files": 0, "message": "No files processed successfully"}
481
+
482
+ return jsonify({
483
+ "success": True,
484
+ "batch_id": batch_id,
485
+ "statistics": statistics,
486
+ "results": results,
487
+ "processed_files": len(successful_results),
488
+ "total_uploaded": len([f for f in audio_files if f.filename != ''])
489
+ })
490
+
491
+ finally:
492
+ # Clean up temporary files
493
+ for temp_path in temp_files:
494
+ if os.path.exists(temp_path):
495
+ os.remove(temp_path)
496
+
497
+ except Exception as e:
498
+ logger.error(f"Error processing batch: {str(e)}")
499
+ return jsonify({
500
+ "error": "Batch processing failed",
501
+ "message": str(e)
502
+ }), 500
503
+
504
+ @app.route('/models/info', methods=['GET'])
505
+ def model_info():
506
+ """Get information about loaded models"""
507
+ return jsonify({
508
+ "speech_recognition": {
509
+ "model": "facebook/wav2vec2-large-960h-lv60-self",
510
+ "type": "Wav2Vec 2.0",
511
+ "language": "English",
512
+ "description": "Large Wav2Vec 2.0 model for English speech recognition"
513
+ },
514
+ "sentiment_analysis": {
515
+ "model": "nlptown/bert-base-multilingual-uncased-sentiment",
516
+ "type": "BERT",
517
+ "language": "Multilingual",
518
+ "description": "Multilingual BERT for sentiment analysis (1-5 stars)"
519
+ },
520
+ "supported_formats": [".wav", ".mp3", ".m4a", ".flac"],
521
+ "classifications": {
522
+ "sentiments": ["POSITIVE", "NEGATIVE", "NEUTRAL"],
523
+ "satisfaction": ["Satisfied", "Dissatisfied", "Neutral"]
524
+ }
525
+ })
526
+
527
+ @app.errorhandler(413)
528
+ def file_too_large(error):
529
+ """Handle file too large error"""
530
+ return jsonify({
531
+ "error": "File too large",
532
+ "message": "Audio file exceeds maximum size limit"
533
+ }), 413
534
+
535
+ @app.errorhandler(404)
536
+ def not_found(error):
537
+ """Handle 404 errors"""
538
+ return jsonify({
539
+ "error": "Endpoint not found",
540
+ "message": "The requested endpoint does not exist",
541
+ "available_endpoints": [
542
+ "GET /health - Health check",
543
+ "POST /analyze - Analyze single audio file",
544
+ "POST /analyze/batch - Analyze multiple audio files",
545
+ "GET /models/info - Get model information"
546
+ ]
547
+ }), 404
548
+
549
+ if __name__ == '__main__':
550
+ # Configuration
551
+ HOST = os.getenv('API_HOST', '0.0.0.0')
552
+ PORT = int(os.getenv('API_PORT', 8000))
553
+ DEBUG = os.getenv('API_DEBUG', 'False').lower() == 'true'
554
+
555
+ # Set maximum file size (16MB)
556
+ app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
557
+
558
+ print(f"Starting Voice Sentiment Analysis API...")
559
+ print(f"Server: http://{HOST}:{PORT}")
560
+ print(f"Health check: http://{HOST}:{PORT}/health")
561
+ print(f"Documentation: See README for API usage examples")
562
+
563
+ app.run(host=HOST, port=PORT, debug=DEBUG)
app.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Gradio Interface for Voice Sentiment Analysis
4
+ Wav2Vec 2.0 + BERT Pipeline
5
+ """
6
+
7
+ import gradio as gr
8
+ import pandas as pd
9
+ import os
10
+ from utils import custom_css
11
+ from voice_sentiment import VoiceSentimentAnalyzer
12
+
13
+ # Initialize model (once)
14
+ print("Loading models...")
15
+ analyzer = VoiceSentimentAnalyzer()
16
+ print("Models ready!")
17
+
18
+ def analyze_audio_file(audio_file):
19
+ """Analyze an uploaded audio file"""
20
+ if audio_file is None:
21
+ return "No audio file provided", "", "", ""
22
+
23
+ try:
24
+ # Analyze the call
25
+ result = analyzer.analyze_call(audio_file)
26
+
27
+ # Format results
28
+ transcription = result['transcription']
29
+ sentiment = result['sentiment']
30
+ score = f"{result['score']:.2f}"
31
+ satisfaction = result['satisfaction']
32
+
33
+ # Emoji based on sentiment
34
+ emoji_map = {
35
+ "POSITIVE": "😊",
36
+ "NEGATIVE": "😠",
37
+ "NEUTRAL": "😐"
38
+ }
39
+ emoji = emoji_map.get(sentiment, "❓")
40
+
41
+ status = f"Analysis completed {emoji}"
42
+
43
+ return status, transcription, sentiment, score, satisfaction
44
+
45
+ except Exception as e:
46
+ error_msg = f"Analysis error: {str(e)}"
47
+ return error_msg, "", "", "", ""
48
+
49
+ def analyze_batch_files(files):
50
+ """Analyze multiple audio files"""
51
+ if not files:
52
+ return "No files provided", None
53
+
54
+ try:
55
+ results = []
56
+
57
+ for file in files:
58
+ result = analyzer.analyze_call(file.name)
59
+ results.append({
60
+ "File": os.path.basename(file.name),
61
+ "Transcription": result['transcription'][:100] + "..." if len(result['transcription']) > 100 else result['transcription'],
62
+ "Sentiment": result['sentiment'],
63
+ "Score": round(result['score'], 2),
64
+ "Satisfaction": result['satisfaction']
65
+ })
66
+
67
+ # Create DataFrame for display
68
+ df = pd.DataFrame(results)
69
+ csv_filename = "analysis_results.csv"
70
+
71
+ print(f"Saving {len(df)} rows to CSV...")
72
+ df.to_csv(csv_filename, index=False)
73
+ print(f"CSV saved successfully") #
74
+
75
+ # Verify CSV was created and has content
76
+ if os.path.exists(csv_filename): # ← NEW DEBUG BLOCK
77
+ file_size = os.path.getsize(csv_filename)
78
+ print(f"CSV file exists, size: {file_size} bytes")
79
+ else:
80
+ print("CSV file was not created!")
81
+
82
+
83
+ # Statistics
84
+ total = len(results)
85
+ positive = len([r for r in results if r['Sentiment'] == 'POSITIVE'])
86
+ negative = len([r for r in results if r['Sentiment'] == 'NEGATIVE'])
87
+ neutral = len([r for r in results if r['Sentiment'] == 'NEUTRAL'])
88
+
89
+ stats = f"""πŸ“Š Statistics:
90
+ β€’ Total: {total} calls
91
+ β€’ Positive: {positive} ({positive/total*100:.1f}%)
92
+ β€’ Negative: {negative} ({negative/total*100:.1f}%)
93
+ β€’ Neutral: {neutral} ({neutral/total*100:.1f}%)"""
94
+
95
+ return stats, df
96
+
97
+ except Exception as e:
98
+ error_msg = f"Analysis error: {str(e)}"
99
+ return error_msg, None
100
+
101
+ # Gradio Interface
102
+ with gr.Blocks(title="Voice Sentiment Analysis", theme=gr.themes.Soft(), css=custom_css) as app:
103
+
104
+ gr.Markdown("""
105
+ # Voice Sentiment Analysis System
106
+ ### Wav2Vec 2.0 + BERT Pipeline
107
+
108
+ Automatically analyze customer call sentiment and classify satisfaction.
109
+ """)
110
+
111
+ with gr.Tabs():
112
+
113
+ # Tab 1: Single file analysis
114
+ with gr.Tab("Single File"):
115
+ gr.Markdown("### Analyze one voice call")
116
+
117
+ with gr.Row():
118
+ with gr.Column():
119
+ audio_input = gr.Audio(
120
+ type="filepath",
121
+ label="Upload your audio file"
122
+ )
123
+
124
+ analyze_btn = gr.Button(
125
+ "Analyze",
126
+ variant="primary",
127
+ size="lg"
128
+ )
129
+
130
+ with gr.Column():
131
+ status_output = gr.Textbox(
132
+ label="πŸ“Š Status",
133
+ interactive=False
134
+ )
135
+
136
+ transcription_output = gr.Textbox(
137
+ label="πŸ“ Transcription",
138
+ lines=3,
139
+ interactive=False
140
+ )
141
+
142
+ with gr.Row():
143
+ sentiment_output = gr.Textbox(
144
+ label="🎭 Sentiment",
145
+ interactive=False
146
+ )
147
+ score_output = gr.Textbox(
148
+ label="🎯 Confidence Score",
149
+ interactive=False
150
+ )
151
+
152
+ satisfaction_output = gr.Textbox(
153
+ label="😊 Customer Satisfaction",
154
+ interactive=False
155
+ )
156
+
157
+ # Tab 2: Multiple files analysis
158
+ with gr.Tab("Multiple Files"):
159
+ gr.Markdown("### Analyze multiple calls in batch")
160
+
161
+ files_input = gr.File(
162
+ file_count="multiple",
163
+ file_types=[".wav", ".mp3", ".m4a"],
164
+ label="Upload your audio files"
165
+ )
166
+
167
+ batch_analyze_btn = gr.Button(
168
+ "Analyze All",
169
+ variant="primary",
170
+ size="lg"
171
+ )
172
+
173
+ batch_status = gr.Textbox(
174
+ label="Statistics",
175
+ lines=6,
176
+ interactive=False
177
+ )
178
+
179
+ results_table = gr.Dataframe(
180
+ label="Detailed Results",
181
+ interactive=False
182
+ )
183
+
184
+ # Tab 3: Information
185
+ with gr.Tab("Information"):
186
+ gr.Markdown("""
187
+ ### How it works?
188
+
189
+ **3-step pipeline:**
190
+ 1. **Audio β†’ Text**: Transcription with Wav2Vec 2.0
191
+ 2. **Text β†’ Sentiment**: Analysis with multilingual BERT
192
+ 3. **Classification**: Customer satisfaction (Satisfied/Dissatisfied/Neutral)
193
+
194
+ ### Supported formats
195
+ - WAV (recommended)
196
+ - MP3
197
+ - M4A
198
+
199
+ ### Classifications
200
+ - **😊 Satisfied**: Positive sentiment with high confidence
201
+ - **😠 Dissatisfied**: Negative sentiment with high confidence
202
+ - **😐 Neutral**: Neutral sentiment or low confidence
203
+
204
+ ### Tips
205
+ - Clear audio quality recommended
206
+ - Optimal duration: 10 seconds to 2 minutes
207
+ - Avoid excessive background noise
208
+ """)
209
+
210
+ # Event connections
211
+ analyze_btn.click(
212
+ fn=analyze_audio_file,
213
+ inputs=[audio_input],
214
+ outputs=[status_output, transcription_output, sentiment_output, score_output, satisfaction_output]
215
+ )
216
+
217
+ batch_analyze_btn.click(
218
+ fn=analyze_batch_files,
219
+ inputs=[files_input],
220
+ outputs=[batch_status, results_table]
221
+ )
222
+
223
+ # Launch the application
224
+ if __name__ == "__main__":
225
+ app.launch(
226
+ share=True, # Creates a public link
227
+ server_name="0.0.0.0", # Accessible from other machines
228
+ server_port=7860
229
+ )
main.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Main script for voice sentiment analysis
4
+ """
5
+
6
+ from voice_sentiment import VoiceSentimentAnalyzer
7
+ import os
8
+
9
+ def main():
10
+ """Main function"""
11
+ print("VOICE SENTIMENT ANALYSIS SYSTEM")
12
+ print("="*50)
13
+
14
+ # Initialize the system
15
+ analyzer = VoiceSentimentAnalyzer()
16
+
17
+ # Simple menu
18
+ while True:
19
+ print("\nOptions:")
20
+ print("1. Analyze an audio file")
21
+ print("2. Analyze a folder of calls")
22
+ print("3. Exit")
23
+
24
+ choice = input("\nYour choice (1-3): ").strip()
25
+
26
+ if choice == "1":
27
+ # Single file analysis
28
+ file_path = input("Audio file path: ").strip()
29
+
30
+ if os.path.exists(file_path):
31
+ try:
32
+ result = analyzer.analyze_call(file_path)
33
+ print("\nAnalysis completed!")
34
+ except Exception as e:
35
+ print(f"Error: {e}")
36
+ else:
37
+ print("File not found!")
38
+
39
+ elif choice == "2":
40
+ # Folder analysis
41
+ folder_path = input("Folder path: ").strip()
42
+
43
+ if os.path.exists(folder_path):
44
+ try:
45
+ results = analyzer.analyze_batch(folder_path)
46
+ print(f"\n{len(results)} files analyzed!")
47
+ except Exception as e:
48
+ print(f"Error: {e}")
49
+ else:
50
+ print("Folder not found!")
51
+
52
+ elif choice == "3":
53
+ print("Goodbye!")
54
+ break
55
+
56
+ else:
57
+ print("Invalid choice!")
58
+
59
+ if __name__ == "__main__":
60
+ main()
render.yaml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ - type: web
3
+ name: voice-sentiment-api
4
+ env: python
5
+ buildCommand: pip install -r requirements.txt
6
+ startCommand: gunicorn --bind 0.0.0.0:$PORT --timeout 300 api:app
7
+ envVars:
8
+ - key: PYTHON_VERSION
9
+ value: 3.9.18
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch>=1.9.0
2
+ transformers>=4.20.0
3
+ librosa>=0.9.0
4
+ pandas>=1.3.0
5
+ numpy>=1.21.0
6
+ scipy>=1.7.0
7
+ torchaudio>=0.9.0
8
+ soundfile>=0.10.0
9
+ gradio>=4.0.0
10
+ flask>=2.0.0
11
+ flask-cors>=3.0.0
12
+ gunicorn>=20.0.0
utils.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Custom CSS for Helvetica font
2
+ custom_css = """
3
+ * {
4
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important;
5
+ }
6
+
7
+ .gradio-container {
8
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important;
9
+ }
10
+
11
+ .gr-textbox, .gr-button, .gr-markdown, .gr-label {
12
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important;
13
+ }
14
+
15
+ h1, h2, h3, h4, h5, h6 {
16
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important;
17
+ }
18
+ """
voice_sentiment.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Simple Voice Sentiment Analysis System
3
+ Wav2Vec 2.0 + BERT Pipeline
4
+ """
5
+
6
+ import torch
7
+ import librosa
8
+ import numpy as np
9
+ from transformers import (
10
+ Wav2Vec2ForCTC,
11
+ Wav2Vec2Tokenizer,
12
+ pipeline
13
+ )
14
+ import pandas as pd
15
+ import os
16
+
17
+ class VoiceSentimentAnalyzer:
18
+ """Simple Pipeline: Audio β†’ Transcription β†’ Sentiment Analysis"""
19
+
20
+ def __init__(self):
21
+ print("Loading models...")
22
+
23
+ # ASR Model (Speech-to-Text)
24
+ self.asr_tokenizer = Wav2Vec2Tokenizer.from_pretrained("facebook/wav2vec2-large-960h-lv60-self")
25
+ self.asr_model = Wav2Vec2ForCTC.from_pretrained("facebook/wav2vec2-large-960h-lv60-self")
26
+
27
+ # Sentiment Model
28
+ self.sentiment_analyzer = pipeline(
29
+ "sentiment-analysis",
30
+ model="nlptown/bert-base-multilingual-uncased-sentiment"
31
+ )
32
+
33
+ print("Models loaded!")
34
+
35
+ def audio_to_text(self, audio_path):
36
+ """Convert audio to text"""
37
+ # Load and preprocess audio
38
+ audio, sr = librosa.load(audio_path, sr=16000)
39
+
40
+ # Transcription with Wav2Vec2
41
+ input_values = self.asr_tokenizer(audio, return_tensors="pt", sampling_rate=16000).input_values
42
+
43
+ with torch.no_grad():
44
+ logits = self.asr_model(input_values).logits
45
+
46
+ predicted_ids = torch.argmax(logits, dim=-1)
47
+ transcription = self.asr_tokenizer.decode(predicted_ids[0])
48
+
49
+ return transcription.strip()
50
+
51
+ def text_to_sentiment(self, text):
52
+ """Analyze sentiment of the text"""
53
+ if not text:
54
+ return {"sentiment": "NEUTRAL", "score": 0.0}
55
+
56
+ result = self.sentiment_analyzer(text)[0]
57
+
58
+ # Convert labels to simple format
59
+ label_map = {
60
+ "1 star": "NEGATIVE", "2 stars": "NEGATIVE",
61
+ "3 stars": "NEUTRAL",
62
+ "4 stars": "POSITIVE", "5 stars": "POSITIVE"
63
+ }
64
+
65
+ sentiment = label_map.get(result['label'], result['label'])
66
+
67
+ return {
68
+ "sentiment": sentiment,
69
+ "score": result['score']
70
+ }
71
+
72
+ def classify_satisfaction(self, sentiment, score):
73
+ """Classify customer satisfaction"""
74
+ if sentiment == "POSITIVE" and score > 0.7:
75
+ return "Satisfied"
76
+ elif sentiment == "NEGATIVE" and score > 0.7:
77
+ return "Dissatisfied"
78
+ else:
79
+ return "Neutral"
80
+
81
+ def analyze_call(self, audio_path):
82
+ """Complete pipeline: Audio β†’ Sentiment β†’ Classification"""
83
+ print(f"Analyzing: {audio_path}")
84
+
85
+ # 1. Audio β†’ Text
86
+ transcription = self.audio_to_text(audio_path)
87
+ print(f"Transcription: {transcription}")
88
+
89
+ # 2. Text β†’ Sentiment
90
+ sentiment_result = self.text_to_sentiment(transcription)
91
+ print(f"Sentiment: {sentiment_result['sentiment']} (score: {sentiment_result['score']:.2f})")
92
+
93
+ # 3. Satisfaction classification
94
+ satisfaction = self.classify_satisfaction(sentiment_result['sentiment'], sentiment_result['score'])
95
+ print(f"Satisfaction: {satisfaction}")
96
+
97
+ return {
98
+ "file": os.path.basename(audio_path),
99
+ "transcription": transcription,
100
+ "sentiment": sentiment_result['sentiment'],
101
+ "score": sentiment_result['score'],
102
+ "satisfaction": satisfaction
103
+ }
104
+
105
+ def analyze_batch(self, audio_folder):
106
+ """Analyze a folder of calls"""
107
+ results = []
108
+
109
+ for filename in os.listdir(audio_folder):
110
+ if filename.endswith(('.wav', '.mp3', '.m4a')):
111
+ audio_path = os.path.join(audio_folder, filename)
112
+ result = self.analyze_call(audio_path)
113
+ results.append(result)
114
+ print("-" * 50)
115
+
116
+ # Save to CSV
117
+ df = pd.DataFrame(results)
118
+ df.to_csv("analysis_results.csv", index=False)
119
+ print(f"Results saved: analysis_results.csv")
120
+
121
+ # Quick statistics
122
+ print("\nSTATISTICS:")
123
+ print(f"Total calls: {len(results)}")
124
+ sentiment_counts = df['sentiment'].value_counts()
125
+ for sentiment, count in sentiment_counts.items():
126
+ print(f"{sentiment}: {count}")
127
+
128
+ return df