MicroHealth/AV-to-transcripts
0
1import base642import io3import os4import threading5import tempfile6import logging7import openai8from dash import Dash, dcc, html, Input, Output, State, callback, callback_context9import dash_bootstrap_components as dbc10from pydub import AudioSegment11import requests12import mimetypes13import urllib.parse14import subprocess15import json16from tqdm import tqdm17 18# Configure logging19logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')20logger = logging.getLogger(__name__)21 22# Try to import moviepy with the simpler import statement23try:24 from moviepy import VideoFileClip, AudioFileClip25 logger.info("MoviePy (VideoFileClip) successfully imported")26except ImportError as e:27 logger.error(f"Error importing MoviePy (VideoFileClip): {str(e)}")28 logger.error("Please ensure moviepy is installed correctly")29 raise30 31# Supported file formats32AUDIO_FORMATS = ['.wav', '.mp3', '.ogg', '.flac', '.aac', '.m4a', '.wma']33VIDEO_FORMATS = ['.mp4', '.avi', '.mov', '.flv', '.wmv', '.mkv', '.webm']34SUPPORTED_FORMATS = AUDIO_FORMATS + VIDEO_FORMATS35 36# Initialize the Dash app37app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])38 39# Global variables40generated_file = None41transcription_text = ""42 43# Set up OpenAI API key44openai.api_key = os.getenv("OPENAI_API_KEY")45 46app.layout = dbc.Container([47 html.H1("Audio/Video Transcription and Diarization App", className="text-center my-4"),48 dbc.Card([49 dbc.CardBody([50 dcc.Upload(51 id='upload-media',52 children=html.Div([53 'Drag and Drop or ',54 html.A('Select Audio/Video File')55 ]),56 style={57 'width': '100%',58 'height': '60px',59 'lineHeight': '60px',60 'borderWidth': '1px',61 'borderStyle': 'dashed',62 'borderRadius': '5px',63 'textAlign': 'center',64 'margin': '10px'65 },66 multiple=False67 ),68 html.Div(id='output-media-upload'),69 dbc.Input(id="url-input", type="text", placeholder="Enter audio/video URL", className="mb-3"),70 dbc.Button("Process Media", id="process-url-button", color="primary", className="mb-3"),71 dbc.Spinner(html.Div(id='transcription-status'), color="primary", type="grow"),72 html.H4("Diarized Transcription Preview", className="mt-4"),73 html.Div(id='transcription-preview', style={'whiteSpace': 'pre-wrap'}),74 html.Br(),75 dbc.Button("Download Transcription", id="btn-download", color="primary", className="mt-3 me-2", disabled=True),76 dbc.Button("Summarize Transcript", id="btn-summarize", color="secondary", className="mt-3 me-2", disabled=True),77 dbc.Button("Generate Meeting Minutes", id="btn-minutes", color="info", className="mt-3", disabled=True),78 dcc.Download(id="download-transcription"),79 dbc.Spinner(html.Div(id='summary-status'), color="secondary", type="grow"),80 dbc.Spinner(html.Div(id='minutes-status'), color="info", type="grow"),81 ])82 ])83], fluid=True)84 85def chunk_audio(audio_segment, chunk_size_ms=60000):86 chunks = []87 for i in range(0, len(audio_segment), chunk_size_ms):88 chunks.append(audio_segment[i:i+chunk_size_ms])89 return chunks90 91def transcribe_audio_chunks(chunks):92 transcriptions = []93 for i, chunk in enumerate(chunks):94 logger.info(f"Transcribing chunk {i+1}/{len(chunks)}")95 with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as temp_audio_file:96 chunk.export(temp_audio_file.name, format="wav")97 with open(temp_audio_file.name, 'rb') as audio_file:98 transcript = openai.Audio.transcribe("whisper-1", audio_file)99 transcriptions.append(transcript.get('text', ''))100 os.unlink(temp_audio_file.name)101 return ' '.join(transcriptions)102 103def download_file(url):104 with requests.Session() as session:105 # First, send a GET request to get the final URL after redirects106 response = session.get(url, allow_redirects=True, stream=True)107 final_url = response.url108 logger.info(f"Final URL after redirects: {final_url}")109 110 # Get the total file size111 total_size = int(response.headers.get('content-length', 0))112 113 # Use a default name with .mp4 extension114 filename = 'downloaded_video.mp4'115 116 # Save the content to a temporary file with .mp4 extension117 with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_file:118 progress_bar = tqdm(total=total_size, unit='iB', unit_scale=True, desc=filename)119 for chunk in response.iter_content(chunk_size=8192):120 size = temp_file.write(chunk)121 progress_bar.update(size)122 progress_bar.close()123 temp_file_path = temp_file.name124 125 # Check if the downloaded file size matches the expected size126 actual_size = os.path.getsize(temp_file_path)127 if total_size != 0 and actual_size != total_size:128 logger.error(f"Downloaded file size ({actual_size} bytes) does not match expected size ({total_size} bytes)")129 raise Exception(f"Incomplete download. Expected {total_size} bytes, got {actual_size} bytes.")130 131 logger.info(f"File downloaded and saved as: {temp_file_path}")132 logger.info(f"File size: {actual_size} bytes")133 return temp_file_path134 135def get_file_info(file_path):136 try:137 result = subprocess.run(['ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', file_path], 138 capture_output=True, text=True, check=True)139 return json.loads(result.stdout)140 except subprocess.CalledProcessError as e:141 logger.error(f"Error getting file info: {str(e)}")142 return None143 144def process_media(file_path, is_url=False):145 global generated_file, transcription_text146 temp_file = None147 wav_path = None148 try:149 if is_url:150 logger.info(f"Processing URL: {file_path}")151 try:152 temp_file = download_file(file_path)153 file_size = os.path.getsize(temp_file)154 logger.info(f"URL content downloaded: {temp_file} (Size: {file_size} bytes)")155 if file_size < 1000000: # Less than 1MB156 raise Exception(f"Downloaded file is too small ({file_size} bytes). Possible incomplete download.")157 except Exception as e:158 logger.error(f"Error downloading URL content: {str(e)}")159 return f"Error downloading URL content: {str(e)}", False160 161 # For downloaded files, we know it's an MP4, so we can skip file type determination162 is_video = True163 is_audio = False164 else:165 # For uploaded files, we still need to determine the file type166 logger.info("Processing uploaded file")167 content_type, content_string = file_path.split(',')168 decoded = base64.b64decode(content_string)169 temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.tmp')170 temp_file.write(decoded)171 temp_file.close()172 temp_file = temp_file.name173 logger.info(f"Uploaded file saved: {temp_file}")174 175 # Get file info for uploaded files176 file_info = get_file_info(temp_file)177 if not file_info:178 return "Unable to process file: Could not determine file type", False179 180 logger.info(f"File info: {json.dumps(file_info, indent=2)}")181 182 # Determine if it's audio or video183 is_audio = any(stream['codec_type'] == 'audio' for stream in file_info['streams'])184 is_video = any(stream['codec_type'] == 'video' for stream in file_info['streams'])185 186 # Convert to WAV using ffmpeg187 wav_path = tempfile.NamedTemporaryFile(delete=False, suffix='.wav').name188 try:189 if is_video:190 # Extract audio from video191 cmd = ['ffmpeg', '-y', '-i', temp_file, '-vn', '-acodec', 'pcm_s16le', '-ar', '44100', '-ac', '2', wav_path, '-v', 'verbose']192 elif is_audio:193 # Convert audio to WAV194 cmd = ['ffmpeg', '-y', '-i', temp_file, '-acodec', 'pcm_s16le', '-ar', '44100', '-ac', '2', wav_path, '-v', 'verbose']195 else:196 return "Unsupported file type: Neither audio nor video detected", False197 198 result = subprocess.run(cmd, check=True, capture_output=True, text=True)199 logger.info(f"FFmpeg command output: {result.stdout}")200 logger.info(f"Audio extracted to WAV: {wav_path}")201 except subprocess.CalledProcessError as e:202 logger.error(f"FFmpeg conversion failed. Error output: {e.stderr}")203 logger.error(f"FFmpeg command: {e.cmd}")204 logger.error(f"Return code: {e.returncode}")205 return f"FFmpeg conversion failed: {e.stderr}", False206 207 # Chunk the audio file208 audio = AudioSegment.from_wav(wav_path)209 chunks = chunk_audio(audio)210 211 logger.info(f"Audio chunked into {len(chunks)} segments")212 213 # Transcribe chunks214 transcription = transcribe_audio_chunks(chunks)215 216 logger.info(f"Transcription completed. Total length: {len(transcription)} characters")217 218 # Diarization using OpenAI219 diarization_prompt = f"""220 The following is a transcription of a conversation. Please identify different speakers and label them as Speaker 1, Speaker 2, etc. unless the speaker idententifies themselves by name in that case use their name. Format the output as a series of speaker labels followed by their dialogue. Here's the transcription:221 222 {transcription}223 224 Please analyze the content and speaking styles to differentiate between speakers. If they give their name, assume that is the speaker and assume who is speaking bsed on speech patterns. Consider changes in topic, speaking patterns, and any contextual clues that might indicate a change in speaker.225 """226 227 diarization_response = openai.ChatCompletion.create(228 model="gpt-3.5-turbo",229 messages=[230 {"role": "system", "content": "You are an AI assistant skilled in analyzing conversations and identifying different speakers."},231 {"role": "user", "content": diarization_prompt}232 ]233 )234 235 formatted_transcript = diarization_response['choices'][0]['message']['content']236 237 transcription_text = formatted_transcript238 generated_file = io.BytesIO(transcription_text.encode())239 logger.info("Transcription and diarization completed successfully")240 return "Transcription and diarization completed successfully!", True241 except Exception as e:242 logger.error(f"Error during processing: {str(e)}")243 return f"An error occurred: {str(e)}", False244 finally:245 if temp_file and os.path.exists(temp_file):246 os.unlink(temp_file)247 if wav_path and os.path.exists(wav_path):248 os.unlink(wav_path)249 250@app.callback(251 [Output('summary-status', 'children'),252 Output('minutes-status', 'children'),253 Output('download-transcription', 'data')],254 [Input('btn-summarize', 'n_clicks'),255 Input('btn-minutes', 'n_clicks'),256 Input('btn-download', 'n_clicks')],257 State('transcription-preview', 'children'),258 prevent_initial_call=True259)260def handle_document_actions(summarize_clicks, minutes_clicks, download_clicks, transcript):261 ctx = callback_context262 if not ctx.triggered:263 return "", "", None264 265 button_id = ctx.triggered[0]['prop_id'].split('.')[0]266 267 if button_id == 'btn-summarize':268 summary_prompt = f"""269 Please provide a detailed summary of the following transcript. Include the main topics discussed and key points. Format it for readability in paragraph format writing it wikipedia style:270 271 {transcript}272 273 Summary:274 """275 276 try:277 summary_response = openai.ChatCompletion.create(278 model="gpt-3.5-turbo",279 messages=[280 {"role": "system", "content": "You are an AI assistant skilled in summarizing conversations."},281 {"role": "user", "content": summary_prompt}282 ]283 )284 285 summary = summary_response['choices'][0]['message']['content']286 return "", "", dcc.send_string(summary, "transcript_summary.txt")287 except Exception as e:288 logger.error(f"Error generating summary: {str(e)}")289 return f"An error occurred while generating the summary: {str(e)}", "", None290 291 elif button_id == 'btn-minutes':292 minutes_prompt = f"""293 Please transform the following transcript into structured meeting minutes. Include the following sections:294 1. Meeting Title295 2. Date and Time (if mentioned)296 3. Attendees (if mentioned)297 4. Agenda Items298 5. Key Decisions299 6. Action Items300 7. Next Steps301 302 Transcript:303 {transcript}304 305 Meeting Minutes:306 """307 308 try:309 minutes_response = openai.ChatCompletion.create(310 model="gpt-3.5-turbo",311 messages=[312 {"role": "system", "content": "You are an AI assistant skilled in creating structured meeting minutes from transcripts."},313 {"role": "user", "content": minutes_prompt}314 ]315 )316 317 minutes = minutes_response['choices'][0]['message']['content']318 return "", "", dcc.send_string(minutes, "meeting_minutes.txt")319 except Exception as e:320 logger.error(f"Error generating meeting minutes: {str(e)}")321 return "", f"An error occurred while generating meeting minutes: {str(e)}", None322 323 elif button_id == 'btn-download':324 return "", "", dcc.send_bytes(generated_file.getvalue(), "diarized_transcription.txt")325 326 return "", "", None327 328@app.callback(329 [Output('output-media-upload', 'children'),330 Output('transcription-status', 'children'),331 Output('transcription-preview', 'children'),332 Output('btn-download', 'disabled'),333 Output('btn-summarize', 'disabled'),334 Output('btn-minutes', 'disabled')],335 [Input('upload-media', 'contents'),336 Input('process-url-button', 'n_clicks')],337 [State('upload-media', 'filename'),338 State('url-input', 'value')]339)340 341def update_output(contents, n_clicks, filename, url):342 global transcription_text343 ctx = callback_context344 if not ctx.triggered:345 return "No file uploaded or URL processed.", "", "", True, True, True346 347 # Clear the preview pane348 transcription_preview = ""349 350 if contents is not None:351 status_message, success = process_media(contents)352 elif url:353 status_message, success = process_media(url, is_url=True)354 else:355 return "No file uploaded or URL processed.", "", "", True, True, True356 357 if success:358 preview = transcription_text[:1000] + "..." if len(transcription_text) > 1000 else transcription_text359 return f"Media processed successfully.", status_message, preview, False, False, False360 else:361 return "Processing failed.", status_message, transcription_preview, True, True, True362 363if __name__ == '__main__':364 print("Starting the Dash application...")365 app.run(debug=True, host='0.0.0.0', port=7860)366 print("Dash application has finished running.")