devsu commited on
Commit
164d23a
·
1 Parent(s): ca50df8

Add initial implementation of Meeting Summarizer web app

Browse files

- Created main application file `app.py` for meeting analysis and summarization using Gradio.
- Added utility modules for text extraction, audio transcription, PDF generation, and data persistence.
- Implemented logging for better debugging and error handling.
- Included a `.gitignore` file to exclude unnecessary files and directories.
- Updated `README.md` to reflect the new features and usage instructions.
- Added `requirements.txt` for dependency management.

.gitignore ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
130
+
131
+ # Meeting Summarizer specific
132
+ data/
133
+ meetings/
134
+ *.pdf
135
+ *.mp3
136
+ *.wav
137
+ *.m4a
138
+ *.flac
139
+ *.ogg
140
+ temp/
141
+ tmp/
142
+
143
+ # Hugging Face cache
144
+ .cache/
145
+ huggingface/
146
+
147
+ # Model cache
148
+ models/
149
+ *.bin
150
+ *.safetensors
151
+
152
+ # Logs
153
+ *.log
154
+ logs/
155
+
156
+ # IDE
157
+ .vscode/
158
+ .idea/
159
+ *.swp
160
+ *.swo
161
+
162
+ # OS
163
+ .DS_Store
164
+ Thumbs.db
README.md CHANGED
@@ -1,12 +1,14 @@
1
  ---
2
  title: Meeting Summarizer
3
- emoji: 🔥
4
- colorFrom: purple
5
- colorTo: red
6
  sdk: gradio
7
- sdk_version: 5.49.1
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
  ---
2
  title: Meeting Summarizer
3
+ emoji: 🎯
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: gradio
7
+ sdk_version: "5.49.1"
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ # Meeting Summarizer
13
+
14
+ An interactive web app with Gradio that allows you to upload a meeting transcript or audio/video file and automatically generates a complete summary, topics list, and keywords.
app.py ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Meeting Summarizer - Applicazione Gradio
3
+ Web app per l'analisi e sintesi automatica di meeting tramite GPT-4o-mini.
4
+ """
5
+
6
+ import os
7
+ import tempfile
8
+ import shutil
9
+ import gradio as gr
10
+ from typing import Tuple, Optional
11
+
12
+ # Import moduli locali
13
+ from utils.text_extraction import extract_text, get_supported_extensions
14
+ from utils.transcription import transcribe_audio, is_audio_file, get_supported_audio_extensions
15
+ from utils.llm_analysis import analyze_meeting, format_analysis_for_display
16
+ from utils.pdf_generator import generate_pdf, cleanup_temp_pdf
17
+ from utils.data_persistence import save_meeting_to_dataset
18
+
19
+ # Configurazione logging
20
+ import logging
21
+ logging.basicConfig(level=logging.INFO)
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Variabili globali per file temporanei
25
+ _temp_files = []
26
+
27
+
28
+
29
+ def process_meeting(file, api_key: str, hf_token: str = "") -> Tuple[str, str, str, str, str]:
30
+ """
31
+ Processa un file di meeting e restituisce l'analisi completa.
32
+
33
+ Args:
34
+ file: File caricato dall'utente
35
+ api_key (str): Chiave API OpenAI
36
+ hf_token (str): Token Hugging Face (opzionale)
37
+
38
+ Returns:
39
+ Tuple[str, str, str, str, str]: (summary, topics, keywords, pdf_path, message)
40
+ """
41
+ global _temp_files
42
+
43
+ try:
44
+ # Verifica input e debug
45
+ logger.info(f"DEBUG: file ricevuto tipo={type(file)} valore={file}")
46
+
47
+ if not file:
48
+ return "", "", "", None, "❌ Error: No file uploaded"
49
+
50
+ if not api_key:
51
+ return "", "", "", None, "❌ Error: OpenAI API key required"
52
+
53
+ # Gestisci l'oggetto file di Gradio
54
+ # Nelle versioni recenti di Gradio, il file può essere una stringa (path) o un oggetto
55
+ if isinstance(file, str):
56
+ file_path = file
57
+ else:
58
+ # Se è un oggetto file, estrai il path
59
+ file_path = file.name if hasattr(file, 'name') else str(file)
60
+
61
+ logger.info(f"DEBUG: file_path={file_path} exists={os.path.exists(file_path)} isfile={os.path.isfile(file_path) if os.path.exists(file_path) else 'N/A'}")
62
+
63
+ # Verify that the file exists and is a file (not a directory)
64
+ if not os.path.exists(file_path):
65
+ return "", "", "", None, "❌ Error: File not found or invalid"
66
+
67
+ if not os.path.isfile(file_path):
68
+ return "", "", "", None, f"❌ Error: Path is a directory, not a file: {file_path}"
69
+
70
+ # Estrai testo dal file
71
+ text = ""
72
+ file_name = os.path.basename(file_path)
73
+
74
+ logger.info(f"Processamento file: {file_name}")
75
+ logger.info(f"Percorso file: {file_path}")
76
+
77
+ # Determine if it's an audio file
78
+ if is_audio_file(file_name):
79
+ logger.info("Audio file detected, starting transcription...")
80
+ text = transcribe_audio(file_path)
81
+ if not text:
82
+ return "", "", "", None, "❌ Error: Transcription failed"
83
+ logger.info("Transcription completed")
84
+ else:
85
+ # Extract text from document
86
+ logger.info("Document file detected, extracting text...")
87
+ text = extract_text(file_path)
88
+ if not text:
89
+ return "", "", "", None, "❌ Error: Text extraction failed"
90
+ logger.info("Text extraction completed")
91
+
92
+ # Verify that the text is not empty
93
+ if not text.strip():
94
+ return "", "", "", None, "❌ Error: No text extracted from file"
95
+
96
+ # Analyze with GPT-4o-mini
97
+ logger.info("Starting analysis with GPT-4o-mini...")
98
+ analysis = analyze_meeting(text, api_key)
99
+ if not analysis:
100
+ return "", "", "", None, "❌ Error: Analysis failed"
101
+
102
+ # Formatta per display
103
+ formatted_analysis = format_analysis_for_display(analysis)
104
+
105
+ # Generate PDF
106
+ logger.info("Generating PDF...")
107
+ pdf_path = generate_pdf(analysis)
108
+
109
+ # Debug: verify that pdf_path is valid
110
+ if pdf_path:
111
+ logger.info(f"PDF generated: {pdf_path}")
112
+ logger.info(f"PDF exists: {os.path.exists(pdf_path)}")
113
+ logger.info(f"PDF is a file: {os.path.isfile(pdf_path)}")
114
+ # If not a valid file, set to None
115
+ if not os.path.isfile(pdf_path):
116
+ logger.warning(f"PDF path invalid: {pdf_path}")
117
+ pdf_path = None
118
+
119
+ # Save to dataset if token provided
120
+ if hf_token:
121
+ logger.info("Saving to Hugging Face Dataset...")
122
+ meeting_data = {
123
+ "file_name": file_name,
124
+ "transcription": text,
125
+ "summary": analysis.get("summary", ""),
126
+ "topics": analysis.get("topics", []),
127
+ "keywords": analysis.get("keywords", [])
128
+ }
129
+
130
+ if save_meeting_to_dataset(meeting_data, hf_token):
131
+ logger.info("Meeting saved to HF Dataset")
132
+ else:
133
+ logger.warning("Saving to HF Dataset failed")
134
+
135
+ # Add PDF to temporary files for cleanup
136
+ if pdf_path:
137
+ _temp_files.append(pdf_path)
138
+
139
+ # Success message
140
+ success_msg = f"✅ Meeting analyzed successfully!\n\n📄 File: {file_name}\n📝 Characters analyzed: {len(text)}\n📊 Topics identified: {len(analysis.get('topics', []))}\n🔑 Keywords: {len(analysis.get('keywords', []))}"
141
+
142
+ if hf_token:
143
+ success_msg += "\n💾 Data saved to Hugging Face Dataset"
144
+
145
+ return (
146
+ formatted_analysis["summary"],
147
+ formatted_analysis["topics"],
148
+ formatted_analysis["keywords"],
149
+ pdf_path if pdf_path else None,
150
+ success_msg
151
+ )
152
+
153
+ except Exception as e:
154
+ logger.error(f"Error during processing: {str(e)}")
155
+ return "", "", "", None, f"❌ Error: {str(e)}"
156
+
157
+
158
+ def cleanup_temp_files():
159
+ """Clean up temporary files."""
160
+ global _temp_files
161
+ for file_path in _temp_files:
162
+ if os.path.exists(file_path):
163
+ try:
164
+ os.remove(file_path)
165
+ except Exception as e:
166
+ logger.warning(f"Unable to delete {file_path}: {str(e)}")
167
+ _temp_files.clear()
168
+
169
+
170
+ def create_interface():
171
+ """Create the Gradio interface."""
172
+
173
+ # Supported extensions
174
+ supported_extensions = get_supported_extensions() + get_supported_audio_extensions()
175
+
176
+ with gr.Blocks(
177
+ title="Meeting Summarizer",
178
+ theme=gr.themes.Soft(),
179
+ css="""
180
+ .gradio-container {
181
+ max-width: 1200px !important;
182
+ }
183
+ .success-message {
184
+ background-color: #d4edda;
185
+ border: 1px solid #c3e6cb;
186
+ color: #155724;
187
+ padding: 10px;
188
+ border-radius: 5px;
189
+ }
190
+ """
191
+ ) as app:
192
+
193
+ gr.Markdown(
194
+ """
195
+ # 🎯 Meeting Summarizer
196
+
197
+ Upload a meeting file (audio, PDF, DOCX, TXT) and automatically get:
198
+ - 📝 **Complete summary** of the meeting
199
+ - 🏷️ **Main topics** discussed
200
+ - 🔑 **Relevant keywords**
201
+ - 📄 **Downloadable PDF** with all results
202
+
203
+ ---
204
+ """
205
+ )
206
+
207
+ with gr.Row():
208
+ with gr.Column(scale=1):
209
+ # Input file
210
+ file_input = gr.File(
211
+ label="📁 Upload Meeting File",
212
+ file_types=supported_extensions,
213
+ file_count="single"
214
+ )
215
+
216
+ # API Key OpenAI
217
+ api_key_input = gr.Textbox(
218
+ label="🔑 OpenAI API Key",
219
+ placeholder="Enter your OpenAI API key...",
220
+ type="password",
221
+ info="Required for analysis with GPT-4o-mini"
222
+ )
223
+
224
+ # HF Token (optional)
225
+ hf_token_input = gr.Textbox(
226
+ label="🤗 Hugging Face Token (Optional)",
227
+ placeholder="Enter your HF token to save data...",
228
+ type="password",
229
+ info="Optional: to save results to Hugging Face Dataset"
230
+ )
231
+
232
+ # Analyze button
233
+ analyze_btn = gr.Button(
234
+ "🚀 Analyze Meeting",
235
+ variant="primary",
236
+ size="lg"
237
+ )
238
+
239
+ # Status message
240
+ status_msg = gr.Textbox(
241
+ label="📊 Status",
242
+ interactive=False,
243
+ visible=True
244
+ )
245
+
246
+ with gr.Column(scale=2):
247
+ # Output summary
248
+ summary_output = gr.Markdown(
249
+ label="📝 Meeting Summary",
250
+ value="The summary will appear here after analysis..."
251
+ )
252
+
253
+ # Output topics
254
+ topics_output = gr.Markdown(
255
+ label="🏷️ Main Topics",
256
+ value="The main topics will appear here..."
257
+ )
258
+
259
+ # Output keywords
260
+ keywords_output = gr.Markdown(
261
+ label="🔑 Keywords",
262
+ value="The keywords will appear here..."
263
+ )
264
+
265
+ # Download PDF
266
+ pdf_download = gr.File(
267
+ label="📄 Download PDF Report",
268
+ visible=True
269
+ )
270
+
271
+ # Footer
272
+ gr.Markdown(
273
+ """
274
+ ---
275
+ ### ℹ️ Information
276
+
277
+ **Supported formats:**
278
+ - 🎵 **Audio**: MP3, WAV, M4A, FLAC, OGG
279
+ - 📄 **Documents**: PDF, DOCX, TXT
280
+
281
+ **Features:**
282
+ - 🎤 Automatic audio transcription with Whisper
283
+ - 🤖 Intelligent analysis with GPT-4o-mini
284
+ - 📊 Topic and keyword extraction
285
+ - 💾 Save to Hugging Face Datasets
286
+ - 📄 Professional PDF generation
287
+
288
+ **Notes:**
289
+ - Audio files are automatically transcribed
290
+ - Analysis is optimized for meetings
291
+ - Data is saved only if you provide an HF token
292
+ """
293
+ )
294
+
295
+ # Eventi
296
+ analyze_btn.click(
297
+ fn=process_meeting,
298
+ inputs=[file_input, api_key_input, hf_token_input],
299
+ outputs=[summary_output, topics_output, keywords_output, pdf_download, status_msg],
300
+ show_progress=True
301
+ )
302
+
303
+ # Cleanup al chiudere
304
+ app.unload(cleanup_temp_files)
305
+
306
+ return app
307
+
308
+
309
+ def main():
310
+ """Main function."""
311
+ logger.info("Starting Meeting Summarizer...")
312
+
313
+ # Create interface
314
+ app = create_interface()
315
+
316
+ # Launch server
317
+ app.launch(
318
+ server_name="0.0.0.0",
319
+ server_port=7860,
320
+ share=True,
321
+ show_error=True
322
+ )
323
+
324
+
325
+ if __name__ == "__main__":
326
+ main()
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ openai>=1.0.0
3
+ transformers>=4.30.0
4
+ torch>=2.0.0
5
+ torchaudio>=2.0.0
6
+ pypdf2>=3.0.0
7
+ python-docx>=0.8.11
8
+ reportlab>=4.0.0
9
+ datasets>=2.14.0
10
+ huggingface-hub>=0.16.0
11
+ accelerate>=0.20.0
12
+ librosa==0.11.0
utils/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Meeting Summarizer Utils Package
utils/data_persistence.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module for saving meeting data to Hugging Face Datasets.
3
+ Manages permanent persistence of analysis results.
4
+ """
5
+
6
+ import json
7
+ import uuid
8
+ from datetime import datetime
9
+ from typing import Dict, Optional
10
+
11
+ try:
12
+ from datasets import Dataset
13
+ from huggingface_hub import HfApi, login
14
+ except ImportError:
15
+ Dataset = None
16
+ HfApi = None
17
+ login = None
18
+
19
+ # Configurazione logging
20
+ import logging
21
+ logging.basicConfig(level=logging.INFO)
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Nome del dataset su Hugging Face
25
+ DATASET_NAME = "meeting-summarizer-data"
26
+
27
+
28
+ def save_meeting_to_dataset(meeting_data: Dict, hf_token: Optional[str] = None) -> bool:
29
+ """
30
+ Save meeting data to Hugging Face Dataset.
31
+
32
+ Args:
33
+ meeting_data (Dict): Meeting data to save
34
+ hf_token (Optional[str]): Hugging Face token (optional)
35
+
36
+ Returns:
37
+ bool: True if saved successfully, False otherwise
38
+ """
39
+ if not meeting_data:
40
+ logger.error("Meeting data not provided")
41
+ return False
42
+
43
+ if Dataset is None:
44
+ logger.error("datasets not installed. Install with: pip install datasets")
45
+ return False
46
+
47
+ try:
48
+ # Authentication if token provided
49
+ if hf_token:
50
+ try:
51
+ login(token=hf_token)
52
+ logger.info("Hugging Face authentication completed")
53
+ except Exception as e:
54
+ logger.warning(f"Error in HF authentication: {str(e)}")
55
+ logger.info("Continuing without authentication...")
56
+
57
+ # Prepare data for saving
58
+ meeting_record = _prepare_meeting_record(meeting_data)
59
+
60
+ # Create or load dataset
61
+ dataset = _get_or_create_dataset()
62
+
63
+ # Add new record
64
+ if dataset is None:
65
+ logger.error("Unable to create or load dataset")
66
+ return False
67
+
68
+ # Convert dataset to list to add record
69
+ records = list(dataset)
70
+ records.append(meeting_record)
71
+
72
+ # Create new dataset with added record
73
+ new_dataset = Dataset.from_list(records)
74
+
75
+ # Push to Hugging Face Hub (if authenticated)
76
+ if hf_token:
77
+ try:
78
+ new_dataset.push_to_hub(
79
+ DATASET_NAME,
80
+ private=True,
81
+ token=hf_token
82
+ )
83
+ logger.info(f"Dataset updated on Hugging Face Hub: {DATASET_NAME}")
84
+ except Exception as e:
85
+ logger.warning(f"Unable to push to HF Hub: {str(e)}")
86
+ logger.info("Data saved locally")
87
+
88
+ logger.info("Meeting saved successfully to dataset")
89
+ return True
90
+
91
+ except Exception as e:
92
+ logger.error(f"Error while saving meeting: {str(e)}")
93
+ return False
94
+
95
+
96
+ def _prepare_meeting_record(meeting_data: Dict) -> Dict:
97
+ """
98
+ Prepare meeting record for saving.
99
+
100
+ Args:
101
+ meeting_data (Dict): Meeting data
102
+
103
+ Returns:
104
+ Dict: Record formatted for dataset
105
+ """
106
+ current_time = datetime.now()
107
+
108
+ return {
109
+ "id": str(uuid.uuid4()),
110
+ "file_name": meeting_data.get("file_name", "unknown"),
111
+ "meeting_date": current_time.strftime("%Y-%m-%d"),
112
+ "transcription": meeting_data.get("transcription", ""),
113
+ "summary": meeting_data.get("summary", ""),
114
+ "topics": json.dumps(meeting_data.get("topics", [])),
115
+ "keywords": json.dumps(meeting_data.get("keywords", [])),
116
+ "created_at": current_time.isoformat()
117
+ }
118
+
119
+
120
+ def _get_or_create_dataset() -> Optional[Dataset]:
121
+ """
122
+ Create or load Hugging Face dataset.
123
+
124
+ Returns:
125
+ Optional[Dataset]: Dataset or None if error
126
+ """
127
+ try:
128
+ # Try to load existing dataset
129
+ try:
130
+ dataset = Dataset.from_hub(DATASET_NAME)
131
+ logger.info(f"Existing dataset loaded: {DATASET_NAME}")
132
+ return dataset
133
+ except Exception:
134
+ logger.info(f"Dataset {DATASET_NAME} not found, creating new dataset...")
135
+
136
+ # Create new empty dataset
137
+ empty_dataset = Dataset.from_dict({
138
+ "id": [],
139
+ "file_name": [],
140
+ "meeting_date": [],
141
+ "transcription": [],
142
+ "summary": [],
143
+ "topics": [],
144
+ "keywords": [],
145
+ "created_at": []
146
+ })
147
+
148
+ logger.info(f"New dataset created: {DATASET_NAME}")
149
+ return empty_dataset
150
+
151
+ except Exception as e:
152
+ logger.error(f"Error in creating/loading dataset: {str(e)}")
153
+ return None
154
+
155
+
156
+ def load_meetings_from_dataset(hf_token: Optional[str] = None) -> Optional[list]:
157
+ """
158
+ Load all meetings from dataset.
159
+
160
+ Args:
161
+ hf_token (Optional[str]): Hugging Face token
162
+
163
+ Returns:
164
+ Optional[list]: List of meetings or None if error
165
+ """
166
+ if Dataset is None:
167
+ logger.error("datasets not installed")
168
+ return None
169
+
170
+ try:
171
+ # Authentication if token provided
172
+ if hf_token:
173
+ try:
174
+ login(token=hf_token)
175
+ except Exception as e:
176
+ logger.warning(f"Error in HF authentication: {str(e)}")
177
+
178
+ # Load dataset
179
+ dataset = Dataset.from_hub(DATASET_NAME)
180
+
181
+ # Convert to list
182
+ meetings = list(dataset)
183
+
184
+ logger.info(f"Loaded {len(meetings)} meetings from dataset")
185
+ return meetings
186
+
187
+ except Exception as e:
188
+ logger.error(f"Error loading meetings: {str(e)}")
189
+ return None
190
+
191
+
192
+ def get_dataset_info() -> Dict:
193
+ """
194
+ Return dataset information.
195
+
196
+ Returns:
197
+ Dict: Dataset information
198
+ """
199
+ return {
200
+ "dataset_name": DATASET_NAME,
201
+ "description": "Dataset for persisting analyzed meetings",
202
+ "fields": [
203
+ "id", "file_name", "meeting_date", "transcription",
204
+ "summary", "topics", "keywords", "created_at"
205
+ ]
206
+ }
utils/llm_analysis.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module for analyzing meeting text using GPT-4o-mini.
3
+ Extracts summary, topics and keywords from text.
4
+ """
5
+
6
+ import json
7
+ import logging
8
+ from typing import Dict, List, Optional
9
+
10
+ try:
11
+ from openai import OpenAI
12
+ except ImportError:
13
+ OpenAI = None
14
+
15
+ # Configurazione logging
16
+ logging.basicConfig(level=logging.INFO)
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def analyze_meeting(text: str, api_key: str) -> Optional[Dict]:
21
+ """
22
+ Analyze meeting text using GPT-4o-mini.
23
+
24
+ Args:
25
+ text (str): Meeting text to analyze
26
+ api_key (str): OpenAI API key
27
+
28
+ Returns:
29
+ Optional[Dict]: Dictionary with summary, topics, keywords or None if error
30
+ """
31
+ if not text or not text.strip():
32
+ logger.error("Empty text provided for analysis")
33
+ return None
34
+
35
+ if not api_key:
36
+ logger.error("OpenAI API key not provided")
37
+ return None
38
+
39
+ if OpenAI is None:
40
+ logger.error("OpenAI not installed. Install with: pip install openai")
41
+ return None
42
+
43
+ try:
44
+ # Initialize OpenAI client
45
+ client = OpenAI(api_key=api_key)
46
+
47
+ # Structured prompt for analysis
48
+ prompt = f"""
49
+ Analyze the following meeting text and provide a response in JSON format with the following keys:
50
+
51
+ 1. "summary": A comprehensive and detailed summary of the meeting (minimum 200 words)
52
+ 2. "topics": A list of 5-8 main topics discussed in the meeting
53
+ 3. "keywords": A list of 10-15 relevant keywords
54
+
55
+ Meeting text:
56
+ {text}
57
+
58
+ Respond ONLY with the requested JSON, without any additional text.
59
+ """
60
+
61
+ logger.info("Sending request to GPT-4o-mini...")
62
+
63
+ # API call
64
+ response = client.chat.completions.create(
65
+ model="gpt-4o-mini",
66
+ messages=[
67
+ {"role": "system", "content": "You are an expert assistant in meeting analysis. Always provide responses in valid JSON format."},
68
+ {"role": "user", "content": prompt}
69
+ ],
70
+ max_tokens=2000,
71
+ temperature=0.3
72
+ )
73
+
74
+ # Extract response content
75
+ content = response.choices[0].message.content.strip()
76
+
77
+ # Clean content from any markdown or extra text
78
+ if content.startswith("```json"):
79
+ content = content[7:]
80
+ if content.endswith("```"):
81
+ content = content[:-3]
82
+
83
+ # Parse JSON
84
+ try:
85
+ result = json.loads(content)
86
+
87
+ # Structure validation
88
+ required_keys = ["summary", "topics", "keywords"]
89
+ if not all(key in result for key in required_keys):
90
+ logger.error("Invalid JSON structure: missing keys")
91
+ return None
92
+
93
+ # Type validation
94
+ if not isinstance(result["summary"], str):
95
+ logger.error("Summary must be a string")
96
+ return None
97
+ if not isinstance(result["topics"], list):
98
+ logger.error("Topics must be a list")
99
+ return None
100
+ if not isinstance(result["keywords"], list):
101
+ logger.error("Keywords must be a list")
102
+ return None
103
+
104
+ logger.info("Analysis completed successfully")
105
+ return result
106
+
107
+ except json.JSONDecodeError as e:
108
+ logger.error(f"JSON parsing error: {str(e)}")
109
+ logger.error(f"Received content: {content}")
110
+ return None
111
+
112
+ except Exception as e:
113
+ logger.error(f"Error during meeting analysis: {str(e)}")
114
+ return None
115
+
116
+
117
+ def format_analysis_for_display(analysis: Dict) -> Dict[str, str]:
118
+ """
119
+ Format analysis for display in Gradio.
120
+
121
+ Args:
122
+ analysis (Dict): Analysis result
123
+
124
+ Returns:
125
+ Dict[str, str]: Dictionary formatted for display
126
+ """
127
+ if not analysis:
128
+ return {
129
+ "summary": "Error in analysis",
130
+ "topics": "Error in analysis",
131
+ "keywords": "Error in analysis"
132
+ }
133
+
134
+ # Format topics as markdown list
135
+ topics_md = "\n".join([f"- {topic}" for topic in analysis.get("topics", [])])
136
+
137
+ # Format keywords as markdown list
138
+ keywords_md = "\n".join([f"- {keyword}" for keyword in analysis.get("keywords", [])])
139
+
140
+ return {
141
+ "summary": analysis.get("summary", "Summary not available"),
142
+ "topics": topics_md,
143
+ "keywords": keywords_md
144
+ }
utils/pdf_generator.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module for generating PDF reports with meeting analysis results.
3
+ Uses reportlab to create well-formatted documents.
4
+ """
5
+
6
+ import os
7
+ import tempfile
8
+ from datetime import datetime
9
+ from typing import Dict, Optional
10
+
11
+ try:
12
+ from reportlab.lib.pagesizes import A4
13
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
14
+ from reportlab.lib.units import inch
15
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
16
+ from reportlab.lib import colors
17
+ except ImportError:
18
+ A4 = None
19
+ getSampleStyleSheet = None
20
+ ParagraphStyle = None
21
+ inch = None
22
+ SimpleDocTemplate = None
23
+ Paragraph = None
24
+ Spacer = None
25
+ PageBreak = None
26
+ colors = None
27
+
28
+ # Configurazione logging
29
+ import logging
30
+ logging.basicConfig(level=logging.INFO)
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ def generate_pdf(meeting_data: Dict) -> Optional[str]:
35
+ """
36
+ Generate a PDF with meeting analysis results.
37
+
38
+ Args:
39
+ meeting_data (Dict): Meeting data with summary, topics, keywords
40
+
41
+ Returns:
42
+ Optional[str]: Path to generated PDF file or None if error
43
+ """
44
+ if not meeting_data:
45
+ logger.error("Meeting data not provided")
46
+ return None
47
+
48
+ if SimpleDocTemplate is None:
49
+ logger.error("reportlab not installed. Install with: pip install reportlab")
50
+ return None
51
+
52
+ try:
53
+ # Create temporary file
54
+ temp_dir = tempfile.gettempdir()
55
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
56
+ pdf_filename = f"meeting_summary_{timestamp}.pdf"
57
+ pdf_path = os.path.join(temp_dir, pdf_filename)
58
+
59
+ # Create PDF document
60
+ doc = SimpleDocTemplate(
61
+ pdf_path,
62
+ pagesize=A4,
63
+ rightMargin=72,
64
+ leftMargin=72,
65
+ topMargin=72,
66
+ bottomMargin=18
67
+ )
68
+
69
+ # Custom styles
70
+ styles = getSampleStyleSheet()
71
+
72
+ # Title style
73
+ title_style = ParagraphStyle(
74
+ 'CustomTitle',
75
+ parent=styles['Heading1'],
76
+ fontSize=18,
77
+ spaceAfter=30,
78
+ alignment=1, # Centered
79
+ textColor=colors.darkblue
80
+ )
81
+
82
+ # Section style
83
+ section_style = ParagraphStyle(
84
+ 'CustomSection',
85
+ parent=styles['Heading2'],
86
+ fontSize=14,
87
+ spaceAfter=12,
88
+ spaceBefore=20,
89
+ textColor=colors.darkblue
90
+ )
91
+
92
+ # Normal text style
93
+ normal_style = ParagraphStyle(
94
+ 'CustomNormal',
95
+ parent=styles['Normal'],
96
+ fontSize=11,
97
+ spaceAfter=6,
98
+ leading=14
99
+ )
100
+
101
+ # List style
102
+ list_style = ParagraphStyle(
103
+ 'CustomList',
104
+ parent=styles['Normal'],
105
+ fontSize=11,
106
+ spaceAfter=3,
107
+ leftIndent=20,
108
+ bulletIndent=10
109
+ )
110
+
111
+ # Build content
112
+ story = []
113
+
114
+ # Title
115
+ story.append(Paragraph("Meeting Summary", title_style))
116
+ story.append(Spacer(1, 12))
117
+
118
+ # Date and info
119
+ current_date = datetime.now().strftime("%m/%d/%Y %H:%M")
120
+ story.append(Paragraph(f"<b>Analysis date:</b> {current_date}", normal_style))
121
+ story.append(Spacer(1, 20))
122
+
123
+ # Summary
124
+ story.append(Paragraph("Summary", section_style))
125
+ summary_text = meeting_data.get("summary", "Summary not available")
126
+ story.append(Paragraph(summary_text, normal_style))
127
+ story.append(Spacer(1, 20))
128
+
129
+ # Main topics
130
+ story.append(Paragraph("Main Topics", section_style))
131
+ topics = meeting_data.get("topics", [])
132
+ if topics:
133
+ for topic in topics:
134
+ story.append(Paragraph(f"• {topic}", list_style))
135
+ else:
136
+ story.append(Paragraph("Topics not available", normal_style))
137
+ story.append(Spacer(1, 20))
138
+
139
+ # Keywords
140
+ story.append(Paragraph("Keywords", section_style))
141
+ keywords = meeting_data.get("keywords", [])
142
+ if keywords:
143
+ # Group keywords in rows of 3-4
144
+ keyword_lines = []
145
+ for i in range(0, len(keywords), 4):
146
+ line_keywords = keywords[i:i+4]
147
+ keyword_lines.append(" • ".join(line_keywords))
148
+
149
+ for line in keyword_lines:
150
+ story.append(Paragraph(f"• {line}", list_style))
151
+ else:
152
+ story.append(Paragraph("Keywords not available", normal_style))
153
+
154
+ # Footer
155
+ story.append(Spacer(1, 30))
156
+ story.append(Paragraph(
157
+ f"<i>Automatically generated on {current_date} by Meeting Summarizer</i>",
158
+ ParagraphStyle('Footer', parent=styles['Normal'], fontSize=9, alignment=1)
159
+ ))
160
+
161
+ # Generate PDF
162
+ doc.build(story)
163
+
164
+ logger.info(f"PDF generated successfully: {pdf_path}")
165
+ return pdf_path
166
+
167
+ except Exception as e:
168
+ logger.error(f"Error during PDF generation: {str(e)}")
169
+ return None
170
+
171
+
172
+ def cleanup_temp_pdf(pdf_path: str) -> None:
173
+ """
174
+ Clean up temporary PDF file.
175
+
176
+ Args:
177
+ pdf_path (str): Path to PDF file to delete
178
+ """
179
+ try:
180
+ if os.path.exists(pdf_path):
181
+ os.remove(pdf_path)
182
+ logger.info(f"Temporary PDF file deleted: {pdf_path}")
183
+ except Exception as e:
184
+ logger.warning(f"Unable to delete temporary PDF file {pdf_path}: {str(e)}")
utils/text_extraction.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module for extracting text from different file formats.
3
+ Supports: TXT, PDF, DOCX
4
+ """
5
+
6
+ import os
7
+ import logging
8
+ from typing import Optional
9
+
10
+ try:
11
+ import PyPDF2
12
+ except ImportError:
13
+ PyPDF2 = None
14
+
15
+ try:
16
+ from docx import Document
17
+ except ImportError:
18
+ Document = None
19
+
20
+ # Configurazione logging
21
+ logging.basicConfig(level=logging.INFO)
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def extract_text(file_path: str) -> Optional[str]:
26
+ """
27
+ Extract text from a supported file.
28
+
29
+ Args:
30
+ file_path (str): Path to file to process
31
+
32
+ Returns:
33
+ Optional[str]: Extracted text or None if error
34
+ """
35
+ if not os.path.exists(file_path):
36
+ logger.error(f"File not found: {file_path}")
37
+ return None
38
+
39
+ file_extension = os.path.splitext(file_path)[1].lower()
40
+
41
+ try:
42
+ if file_extension == '.txt':
43
+ return _extract_from_txt(file_path)
44
+ elif file_extension == '.pdf':
45
+ return _extract_from_pdf(file_path)
46
+ elif file_extension == '.docx':
47
+ return _extract_from_docx(file_path)
48
+ else:
49
+ logger.error(f"Unsupported file format: {file_extension}")
50
+ return None
51
+
52
+ except Exception as e:
53
+ logger.error(f"Error extracting text from {file_path}: {str(e)}")
54
+ return None
55
+
56
+
57
+ def _extract_from_txt(file_path: str) -> str:
58
+ """Extract text from TXT file."""
59
+ encodings = ['utf-8', 'latin-1', 'cp1252']
60
+
61
+ for encoding in encodings:
62
+ try:
63
+ with open(file_path, 'r', encoding=encoding) as file:
64
+ return file.read()
65
+ except UnicodeDecodeError:
66
+ continue
67
+
68
+ # If all encodings fail, try with error handling
69
+ with open(file_path, 'r', encoding='utf-8', errors='replace') as file:
70
+ return file.read()
71
+
72
+
73
+ def _extract_from_pdf(file_path: str) -> str:
74
+ """Extract text from PDF file."""
75
+ if PyPDF2 is None:
76
+ raise ImportError("PyPDF2 not installed. Install with: pip install pypdf2")
77
+
78
+ text = ""
79
+ with open(file_path, 'rb') as file:
80
+ pdf_reader = PyPDF2.PdfReader(file)
81
+
82
+ for page_num in range(len(pdf_reader.pages)):
83
+ page = pdf_reader.pages[page_num]
84
+ text += page.extract_text() + "\n"
85
+
86
+ return text.strip()
87
+
88
+
89
+ def _extract_from_docx(file_path: str) -> str:
90
+ """Extract text from DOCX file."""
91
+ if Document is None:
92
+ raise ImportError("python-docx not installed. Install with: pip install python-docx")
93
+
94
+ doc = Document(file_path)
95
+ text = ""
96
+
97
+ for paragraph in doc.paragraphs:
98
+ text += paragraph.text + "\n"
99
+
100
+ return text.strip()
101
+
102
+
103
+ def get_supported_extensions() -> list:
104
+ """Return supported file extensions."""
105
+ return ['.txt', '.pdf', '.docx']
utils/transcription.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module for transcribing audio files using Whisper.
3
+ Optimized for CPU with whisper-tiny model.
4
+ """
5
+
6
+ import os
7
+ import logging
8
+ from typing import Optional
9
+
10
+ try:
11
+ import torch
12
+ from transformers import WhisperProcessor, WhisperForConditionalGeneration
13
+ import librosa
14
+ except ImportError as e:
15
+ print(f"Import error: {e}")
16
+ torch = None
17
+ WhisperProcessor = None
18
+ WhisperForConditionalGeneration = None
19
+ librosa = None
20
+
21
+ # Configurazione logging
22
+ logging.basicConfig(level=logging.INFO)
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Variabili globali per il modello (caricato una sola volta)
26
+ _model = None
27
+ _processor = None
28
+
29
+
30
+ def load_whisper_model():
31
+ """Load Whisper tiny model optimized for CPU."""
32
+ global _model, _processor
33
+
34
+ if _model is None or _processor is None:
35
+ try:
36
+ logger.info("Loading Whisper tiny model...")
37
+
38
+ # Load processor and model
39
+ _processor = WhisperProcessor.from_pretrained("openai/whisper-tiny")
40
+ _model = WhisperForConditionalGeneration.from_pretrained("openai/whisper-tiny")
41
+
42
+ # Configure for CPU
43
+ _model.eval()
44
+ if torch.cuda.is_available():
45
+ _model = _model.to("cuda")
46
+ else:
47
+ _model = _model.to("cpu")
48
+
49
+ logger.info("Whisper model loaded successfully")
50
+
51
+ except Exception as e:
52
+ logger.error(f"Error loading Whisper model: {str(e)}")
53
+ raise
54
+
55
+ return _model, _processor
56
+
57
+
58
+ def transcribe_audio(file_path: str, language: str = "en") -> Optional[str]:
59
+ """
60
+ Transcribe an audio file using Whisper.
61
+
62
+ Args:
63
+ file_path (str): Path to audio file
64
+ language (str): Language of audio content (default: "en" for English)
65
+
66
+ Returns:
67
+ Optional[str]: Text transcription or None if error
68
+ """
69
+ if not os.path.exists(file_path):
70
+ logger.error(f"Audio file not found: {file_path}")
71
+ return None
72
+
73
+ if librosa is None:
74
+ logger.error("librosa not installed. Install with: pip install librosa")
75
+ return None
76
+
77
+ try:
78
+ # Load the model
79
+ model, processor = load_whisper_model()
80
+
81
+ # Load and preprocess audio
82
+ logger.info(f"Loading audio file: {file_path}")
83
+ audio_array, sample_rate = librosa.load(file_path, sr=16000)
84
+
85
+ # Preprocess audio
86
+ inputs = processor(audio_array, sampling_rate=sample_rate, return_tensors="pt")
87
+
88
+ # Move to appropriate device
89
+ device = next(model.parameters()).device
90
+ inputs = {k: v.to(device) for k, v in inputs.items()}
91
+
92
+ # Generate transcription
93
+ logger.info("Generating transcription...")
94
+ with torch.no_grad():
95
+ predicted_ids = model.generate(
96
+ inputs["input_features"],
97
+ max_length=448,
98
+ num_beams=1,
99
+ do_sample=False,
100
+ language=language
101
+ )
102
+
103
+ # Decode the result
104
+ transcription = processor.batch_decode(predicted_ids, skip_special_tokens=True)[0]
105
+
106
+ logger.info("Transcription completed successfully")
107
+ return transcription.strip()
108
+
109
+ except Exception as e:
110
+ logger.error(f"Error during transcription of {file_path}: {str(e)}")
111
+ return None
112
+
113
+
114
+ def get_supported_audio_extensions() -> list:
115
+ """Return supported audio extensions."""
116
+ return ['.mp3', '.wav', '.m4a', '.flac', '.ogg']
117
+
118
+
119
+ def is_audio_file(file_path: str) -> bool:
120
+ """Check if a file is a supported audio file."""
121
+ if not file_path:
122
+ return False
123
+
124
+ file_extension = os.path.splitext(file_path)[1].lower()
125
+ return file_extension in get_supported_audio_extensions()