import os import uuid import logging import json import shutil from pathlib import Path import tempfile import gradio as gr from process_interview import process_interview from typing import Tuple, Optional, List, Dict from concurrent.futures import ThreadPoolExecutor # Import ThreadPoolExecutor for parallel processing # Setup logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) logging.getLogger("nemo_logging").setLevel(logging.ERROR) logging.getLogger("nemo").setLevel(logging.ERROR) # Configuration OUTPUT_DIR = "./processed_audio" os.makedirs(OUTPUT_DIR, exist_ok=True) # Constants VALID_EXTENSIONS = ('.wav', '.mp3', '.m4a', '.flac') MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB def check_health() -> str: """Check system health, similar to FastAPI /health endpoint""" try: for directory in [OUTPUT_DIR]: if not os.path.exists(directory): raise Exception(f"Directory {directory} does not exist") return "System is healthy" except Exception as e: logger.error(f"Health check failed: {str(e)}") return f"System is unhealthy: {str(e)}" # A helper function to process a single audio file def process_single_audio(file_path_or_url: str) -> Dict: """Processes a single audio file and returns its analysis.""" try: if not file_path_or_url: return {"error": "No audio provided for processing."} # Gradio will download the file if it's a URL and provide a local path. # So, 'file_path_or_url' will always be a local path when it reaches this function. temp_audio_path = Path(file_path_or_url) file_ext = temp_audio_path.suffix.lower() if file_ext not in VALID_EXTENSIONS: return {"error": f"Invalid file format: {file_ext}. Supported formats: {', '.join(VALID_EXTENSIONS)}"} file_size = os.path.getsize(temp_audio_path) if file_size > MAX_FILE_SIZE: return { "error": f"File too large: {file_size / (1024 * 1024):.2f}MB. Max size: {MAX_FILE_SIZE // (1024 * 1024)}MB"} logger.info(f"Processing audio from: {temp_audio_path}") result = process_interview(str(temp_audio_path)) if not result or 'pdf_path' not in result or 'json_path' not in result: return {"error": "Processing failed - invalid result format."} pdf_path = Path(result['pdf_path']) json_path = Path(result['json_path']) if not pdf_path.exists() or not json_path.exists(): return {"error": "Processing failed - output files not found."} with json_path.open('r') as f: analysis_data = json.load(f) voice_analysis = analysis_data.get('voice_analysis', {}) summary = ( f"Speakers: {', '.join(analysis_data['speakers'])}\n" f"Interview Duration: {analysis_data['text_analysis']['total_duration']:.2f} seconds\n" f"Confidence Level: {voice_analysis.get('interpretation', {}).get('confidence_level', 'Unknown')}\n" f"Anxiety Level: {voice_analysis.get('interpretation', {}).get('anxiety_level', 'Unknown')}" ) json_data = json.dumps(analysis_data, indent=2) return { "summary": summary, "json_data": json_data, "pdf_path": str(pdf_path), "original_input": file_path_or_url # Optionally return the original URL/path for mapping } except Exception as e: logger.error(f"Error processing single audio: {str(e)}", exc_info=True) return {"error": f"Error during processing: {str(e)}"} # Main function to handle multiple audio files/URLs def analyze_multiple_audios(file_paths_or_urls: List[str]) -> Tuple[str, str, List[str]]: """ Analyzes multiple interview audio files/URLs in parallel. Returns combined summary, combined JSON, and a list of PDF paths. """ if not file_paths_or_urls: return "No audio files/URLs provided.", "[]", [] all_summaries = [] all_json_data = [] all_pdf_paths = [] # Use ThreadPoolExecutor for parallel processing # Adjust max_workers based on available resources and expected load with ThreadPoolExecutor(max_workers=5) as executor: futures = {executor.submit(process_single_audio, item): item for item in file_paths_or_urls} for future in futures: item = futures[future] # Get the original item (URL/path) that was processed try: result = future.result() # Get the result of the processing if "error" in result: all_summaries.append(f"Error processing {item}: {result['error']}") # Include error in JSON output for clarity all_json_data.append(json.dumps({"input": item, "error": result['error']}, indent=2)) else: all_summaries.append(f"Analysis for {os.path.basename(item)}:\n{result['summary']}") all_json_data.append(result['json_data']) all_pdf_paths.append(result['pdf_path']) except Exception as exc: logger.error(f"Item {item} generated an unexpected exception: {exc}", exc_info=True) all_summaries.append(f"Error processing {item}: An unexpected error occurred.") all_json_data.append(json.dumps({"input": item, "error": str(exc)}, indent=2)) combined_summary = "\n\n---\n\n".join(all_summaries) # Ensure the combined_json_list is a valid JSON array string combined_json_list = "[\n" + ",\n".join(all_json_data) + "\n]" return combined_summary, combined_json_list, all_pdf_paths # Gradio interface with gr.Blocks(title="Interview Analysis System", theme=gr.themes.Soft()) as demo: gr.Markdown(""" # 🎤 Interview Audio Analysis System Provide multiple audio file URLs or upload multiple audio files to analyze speaker performance. Supported formats: WAV, MP3, M4A, FLAC (max 100MB per file). """) with gr.Row(): with gr.Column(): health_status = gr.Textbox(label="System Status", value=check_health(), interactive=False) audio_inputs = gr.File( label="Provide Audio URLs or Upload Files (Multiple allowed)", type="filepath", file_count="multiple" # Allow multiple files/URLs ) submit_btn = gr.Button("Start Analysis", variant="primary") with gr.Column(): output_summary = gr.Textbox(label="Combined Analysis Summary", interactive=False, lines=10) # Adjusted lines output_json = gr.Textbox(label="Detailed Analysis (JSON Array)", interactive=False, lines=20) pdf_outputs = gr.File(label="Download All Reports", type="filepath", file_count="multiple") submit_btn.click( fn=analyze_multiple_audios, inputs=audio_inputs, outputs=[output_summary, output_json, pdf_outputs] ) # Run the interface if __name__ == "__main__": demo.launch(server_port=7860, server_name="0.0.0.0")