CoolFace
Apppublic

MicroHealth/AV-to-transcripts

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
app.py366 linesDownload Raw Back to root
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.")