Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| FastAPI Server for Optimized Twi Speech Recognition Engine | |
| ========================================================= | |
| This module provides a production-ready FastAPI server for the optimized | |
| Twi speech recognition system. It handles: | |
| 1. Audio file uploads and processing | |
| 2. Real-time speech recognition | |
| 3. Intent classification | |
| 4. WebM/WAV audio format support | |
| 5. Streaming audio processing | |
| 6. Performance monitoring and health checks | |
| Author: AI Assistant | |
| Date: 2025-11-05 | |
| """ | |
| import asyncio | |
| import logging | |
| import os | |
| import tempfile | |
| import time | |
| from contextlib import asynccontextmanager | |
| from pathlib import Path | |
| from typing import Any, Dict, List, Optional | |
| import soundfile as sf | |
| import uvicorn | |
| from fastapi import BackgroundTasks, FastAPI, File, HTTPException, Query, UploadFile | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.middleware.gzip import GZipMiddleware | |
| from fastapi.responses import JSONResponse | |
| from pydantic import BaseModel, Field | |
| from config.config import OptimizedConfig | |
| # Import optimized components | |
| from src.speech_recognizer import OptimizedSpeechRecognizer, create_speech_recognizer | |
| # Configure logging | |
| logging.basicConfig( | |
| level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # Global recognizer instance | |
| recognizer: Optional[OptimizedSpeechRecognizer] = None | |
| config = OptimizedConfig() | |
| # Performance optimization imports and setup | |
| import gc | |
| import subprocess | |
| from concurrent.futures import ThreadPoolExecutor | |
| from typing import Union | |
| # Global performance tracking | |
| performance_stats = { | |
| "total_requests": 0, | |
| "successful_requests": 0, | |
| "failed_requests": 0, | |
| "avg_processing_time": 0.0, | |
| "cache_hits": 0, | |
| } | |
| # Audio conversion cache and thread pool | |
| audio_cache = {} | |
| thread_pool = ThreadPoolExecutor(max_workers=4, thread_name_prefix="audio_proc_") | |
| def optimize_torch_settings(): | |
| """Optimize PyTorch settings for faster inference.""" | |
| import torch | |
| # Set optimal thread count | |
| torch.set_num_threads(min(4, os.cpu_count())) | |
| # Enable inference optimizations | |
| torch.backends.cudnn.benchmark = True | |
| torch.backends.cudnn.deterministic = False | |
| # Disable gradients for inference | |
| torch.set_grad_enabled(False) | |
| if torch.cuda.is_available(): | |
| # Clear CUDA cache | |
| torch.cuda.empty_cache() | |
| # Set memory fraction to prevent OOM | |
| torch.cuda.set_per_process_memory_fraction(0.8) | |
| # Enable TensorFloat-32 for newer GPUs | |
| torch.backends.cuda.matmul.allow_tf32 = True | |
| torch.backends.cudnn.allow_tf32 = True | |
| logger.info( | |
| f"✅ CUDA optimizations applied - Device: {torch.cuda.get_device_name()}" | |
| ) | |
| def optimize_model_loading(speech_recognizer): | |
| """Optimize model loading for better performance.""" | |
| if not speech_recognizer: | |
| return | |
| try: | |
| import torch | |
| # Optimize Whisper model | |
| if ( | |
| hasattr(speech_recognizer, "transcriber") | |
| and speech_recognizer.transcriber.model | |
| ): | |
| model = speech_recognizer.transcriber.model | |
| if torch.cuda.is_available() and not model.is_cuda: | |
| model = model.cuda() | |
| # Enable half precision for faster inference | |
| try: | |
| model = model.half() | |
| logger.info("✅ Half precision enabled for Whisper model") | |
| except Exception as e: | |
| logger.warning(f"⚠️ Could not enable half precision: {e}") | |
| # Optimize intent classifier | |
| if ( | |
| hasattr(speech_recognizer, "intent_classifier") | |
| and speech_recognizer.intent_classifier.pipeline | |
| ): | |
| pipeline = speech_recognizer.intent_classifier.pipeline | |
| if torch.cuda.is_available() and hasattr(pipeline, "model"): | |
| try: | |
| pipeline.model = pipeline.model.cuda() | |
| if hasattr(pipeline.model, "half"): | |
| pipeline.model = pipeline.model.half() | |
| logger.info("✅ Intent classifier optimized for GPU") | |
| except Exception as e: | |
| logger.warning(f"⚠️ Intent classifier GPU optimization failed: {e}") | |
| logger.info("✅ Model optimization completed") | |
| except Exception as e: | |
| logger.error(f"❌ Model optimization failed: {e}") | |
| async def convert_audio_fast(content: bytes, filename: str = "") -> str: | |
| """Ultra-fast audio conversion using optimized FFmpeg.""" | |
| # Detect format quickly | |
| audio_format = detect_audio_format_optimized(content, filename) | |
| # Create temporary file | |
| suffix = f".{audio_format}" | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp_file: | |
| tmp_file.write(content) | |
| temp_path = tmp_file.name | |
| # Skip conversion if already WAV | |
| if audio_format == "wav": | |
| return temp_path | |
| # Use optimized FFmpeg conversion | |
| output_path = temp_path.replace(f".{audio_format}", "_fast.wav") | |
| cmd = [ | |
| "ffmpeg", | |
| "-y", | |
| "-i", | |
| temp_path, | |
| "-ar", | |
| "16000", | |
| "-ac", | |
| "1", | |
| "-acodec", | |
| "pcm_s16le", | |
| "-f", | |
| "wav", | |
| "-threads", | |
| "0", | |
| output_path, | |
| ] | |
| try: | |
| process = await asyncio.create_subprocess_exec( | |
| *cmd, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL | |
| ) | |
| await asyncio.wait_for(process.communicate(), timeout=10.0) | |
| if process.returncode == 0 and os.path.exists(output_path): | |
| # Clean up original | |
| if os.path.exists(temp_path): | |
| os.unlink(temp_path) | |
| return output_path | |
| else: | |
| raise RuntimeError("FFmpeg conversion failed") | |
| except (asyncio.TimeoutError, FileNotFoundError, RuntimeError): | |
| # Fallback to librosa | |
| return await convert_with_librosa_async(temp_path, audio_format) | |
| async def convert_with_librosa_async(input_path: str, input_format: str) -> str: | |
| """Async librosa conversion fallback.""" | |
| loop = asyncio.get_event_loop() | |
| def _convert(): | |
| import librosa | |
| audio, sr = librosa.load(input_path, sr=16000, mono=True) | |
| output_path = input_path.replace(f".{input_format}", "_librosa.wav") | |
| sf.write(output_path, audio, 16000) | |
| # Clean up original | |
| if os.path.exists(input_path): | |
| os.unlink(input_path) | |
| return output_path | |
| return await loop.run_in_executor(thread_pool, _convert) | |
| def detect_audio_format_optimized(content: bytes, filename: str) -> str: | |
| """Fast audio format detection.""" | |
| # Check filename extension first (fastest) | |
| if filename: | |
| ext = Path(filename).suffix.lower() | |
| if ext in [".wav", ".mp3", ".webm", ".ogg"]: | |
| return ext[1:] | |
| # Check magic bytes | |
| if content.startswith(b"RIFF"): | |
| return "wav" | |
| elif content.startswith(b"ID3") or content.startswith(b"\xff\xfb"): | |
| return "mp3" | |
| elif content.startswith(b"OggS"): | |
| return "ogg" | |
| elif b"webm" in content[:100] or content.startswith(b"\x1a\x45\xdf\xa3"): | |
| return "webm" | |
| return "unknown" | |
| def cleanup_memory(): | |
| """Optimize memory usage and cleanup.""" | |
| try: | |
| gc.collect() | |
| import torch | |
| if torch.cuda.is_available(): | |
| torch.cuda.empty_cache() | |
| torch.cuda.synchronize() | |
| # Clear audio cache if too large | |
| if len(audio_cache) > 50: | |
| items = list(audio_cache.items()) | |
| audio_cache.clear() | |
| audio_cache.update(dict(items[-25:])) # Keep 25 most recent | |
| logger.info("🧹 Audio cache cleaned") | |
| except Exception as e: | |
| logger.warning(f"Memory cleanup warning: {e}") | |
| def update_performance_stats(processing_time: float, success: bool = True): | |
| """Update performance statistics.""" | |
| performance_stats["total_requests"] += 1 | |
| if success: | |
| performance_stats["successful_requests"] += 1 | |
| else: | |
| performance_stats["failed_requests"] += 1 | |
| # Update rolling average processing time | |
| total = performance_stats["total_requests"] | |
| current_avg = performance_stats["avg_processing_time"] | |
| new_avg = (current_avg * (total - 1) + processing_time) / total | |
| performance_stats["avg_processing_time"] = new_avg | |
| async def lifespan(app: FastAPI): | |
| """Application lifespan manager with performance optimizations.""" | |
| global recognizer | |
| # Startup | |
| logger.info("🚀 Starting Ultra-Fast Twi Speech Recognition Server...") | |
| try: | |
| # Apply PyTorch optimizations first | |
| logger.info("🔧 Applying performance optimizations...") | |
| optimize_torch_settings() | |
| # Initialize speech recognizer | |
| recognizer = create_speech_recognizer() | |
| # Optimize the loaded models for better performance | |
| optimize_model_loading(recognizer) | |
| # Validate configuration | |
| config.validate_config() | |
| # Print startup summary | |
| config.print_config_summary() | |
| # Log optimization status | |
| import torch | |
| device_info = ( | |
| f"GPU: {torch.cuda.get_device_name()}" | |
| if torch.cuda.is_available() | |
| else "CPU" | |
| ) | |
| logger.info(f"✅ Server ready with optimizations - Device: {device_info}") | |
| logger.info("📊 Performance monitoring available at /performance-stats") | |
| except Exception as e: | |
| logger.error(f"❌ Failed to initialize server: {e}") | |
| raise | |
| yield | |
| # Shutdown | |
| logger.info("🛑 Shutting down server...") | |
| # Cleanup resources | |
| if thread_pool: | |
| thread_pool.shutdown(wait=True) | |
| audio_cache.clear() | |
| cleanup_memory() | |
| # Create FastAPI app | |
| app = FastAPI( | |
| title="Optimized Twi Speech Recognition API", | |
| description="Production-ready speech recognition for Twi language using Whisper + Intent Classification", | |
| version=config.DEPLOYMENT["version"], | |
| lifespan=lifespan, | |
| ) | |
| # Add middleware | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=config.API["cors_origins"], | |
| allow_credentials=False, | |
| allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], | |
| allow_headers=["*"], | |
| ) | |
| app.add_middleware(GZipMiddleware, minimum_size=1000) | |
| # CORS headers for all responses | |
| CORS_HEADERS = { | |
| "Access-Control-Allow-Origin": "*", | |
| "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", | |
| "Access-Control-Allow-Headers": "*", | |
| "Access-Control-Max-Age": "86400", | |
| } | |
| # Pydantic models | |
| class RecognitionRequest(BaseModel): | |
| """Request model for speech recognition.""" | |
| language: str = Field( | |
| default=None, description="Language code (auto-detect if None)" | |
| ) | |
| include_alternatives: bool = Field( | |
| default=True, description="Include alternative intents" | |
| ) | |
| confidence_threshold: float = Field( | |
| default=0.5, description="Minimum confidence threshold" | |
| ) | |
| class RecognitionResponse(BaseModel): | |
| """Response model for speech recognition.""" | |
| transcription: Dict[str, Any] | |
| intent: Dict[str, Any] | |
| audio_info: Dict[str, Any] | |
| processing_time: float | |
| timestamp: float | |
| status: str | |
| class HealthResponse(BaseModel): | |
| """Health check response model.""" | |
| status: str | |
| components: Dict[str, str] | |
| device_info: Dict[str, Any] | |
| timestamp: float | |
| class IntentInfo(BaseModel): | |
| """Intent information model.""" | |
| intent: str | |
| description: str | |
| examples: List[str] | |
| priority: str | |
| # Helper functions | |
| def detect_audio_format(content: bytes, filename: str) -> str: | |
| """Detect audio format from content or filename.""" | |
| # Check WebM magic bytes | |
| if content.startswith(b"\x1a\x45\xdf\xa3"): | |
| return "webm" | |
| # Check WAV magic bytes | |
| if content.startswith(b"RIFF") and b"WAVE" in content[:12]: | |
| return "wav" | |
| # Check filename extension | |
| if filename: | |
| ext = Path(filename).suffix.lower() | |
| if ext in [".webm", ".opus"]: | |
| return "webm" | |
| elif ext in [".wav", ".wave"]: | |
| return "wav" | |
| elif ext in [".mp3", ".mpeg"]: | |
| return "mp3" | |
| elif ext in [".m4a", ".aac"]: | |
| return "m4a" | |
| # Default to wav | |
| return "wav" | |
| def convert_audio_if_needed(input_path: str, detected_format: str) -> str: | |
| """Convert audio to WAV format if needed.""" | |
| if detected_format == "wav": | |
| return input_path | |
| try: | |
| # Create output path | |
| output_path = input_path.replace(Path(input_path).suffix, "_converted.wav") | |
| # Load and convert | |
| import librosa | |
| audio, sr = librosa.load(input_path, sr=16000, mono=True) | |
| sf.write(output_path, audio, sr) | |
| logger.info(f"Converted {detected_format} to WAV: {output_path}") | |
| return output_path | |
| except Exception as e: | |
| logger.warning(f"Audio conversion failed: {e}, using original file") | |
| return input_path | |
| async def cleanup_temp_files(*file_paths: str): | |
| """Clean up temporary files.""" | |
| for file_path in file_paths: | |
| try: | |
| if file_path and Path(file_path).exists(): | |
| Path(file_path).unlink() | |
| logger.debug(f"Cleaned up: {file_path}") | |
| except Exception as e: | |
| logger.warning(f"Failed to cleanup {file_path}: {e}") | |
| # API Routes | |
| async def root(): | |
| """Root endpoint with API information.""" | |
| try: | |
| stats = recognizer.get_statistics() if recognizer else {} | |
| return JSONResponse( | |
| content={ | |
| "message": "Optimized Twi Speech Recognition API", | |
| "version": config.DEPLOYMENT["version"], | |
| "status": "running", | |
| "whisper_model": config.WHISPER["model_size"], | |
| "supported_intents": len(config.INTENTS), | |
| "device": config.get_device(), | |
| "statistics": stats, | |
| }, | |
| headers=CORS_HEADERS, | |
| ) | |
| except Exception as e: | |
| logger.error(f"Root endpoint error: {e}") | |
| return JSONResponse( | |
| content={ | |
| "message": "Optimized Twi Speech Recognition API", | |
| "status": "running", | |
| "error": str(e), | |
| }, | |
| headers=CORS_HEADERS, | |
| ) | |
| async def options_handler(path: str): | |
| """Handle CORS preflight requests.""" | |
| return JSONResponse(content={"message": "OK"}, headers=CORS_HEADERS) | |
| async def health_check(): | |
| """Health check endpoint.""" | |
| try: | |
| health_status = ( | |
| recognizer.health_check() | |
| if recognizer | |
| else { | |
| "status": "unhealthy", | |
| "components": {"recognizer": "not_initialized"}, | |
| "device_info": {}, | |
| "timestamp": time.time(), | |
| } | |
| ) | |
| return JSONResponse(content=health_status, headers=CORS_HEADERS) | |
| except Exception as e: | |
| logger.error(f"Health check failed: {e}") | |
| return JSONResponse( | |
| content={"status": "unhealthy", "error": str(e), "timestamp": time.time()}, | |
| headers=CORS_HEADERS, | |
| ) | |
| async def get_supported_intents(): | |
| """Get list of supported intents.""" | |
| try: | |
| intents = recognizer.get_supported_intents() if recognizer else [] | |
| return JSONResponse(content=intents, headers=CORS_HEADERS) | |
| except Exception as e: | |
| logger.error(f"Failed to get intents: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_statistics(): | |
| """Get system performance statistics.""" | |
| try: | |
| stats = recognizer.get_statistics() if recognizer else {} | |
| return JSONResponse(content=stats, headers=CORS_HEADERS) | |
| except Exception as e: | |
| logger.error(f"Failed to get statistics: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_model_info(): | |
| """Get model information.""" | |
| try: | |
| if recognizer: | |
| info = { | |
| "whisper_model": config.WHISPER["model_size"], | |
| "language": config.WHISPER["language"], | |
| "supported_intents": len(config.INTENTS), | |
| "device": config.get_device(), | |
| "version": config.DEPLOYMENT["version"], | |
| "model_type": "optimized_whisper_intent", | |
| } | |
| else: | |
| info = {"status": "not_initialized"} | |
| return JSONResponse(content=info, headers=CORS_HEADERS) | |
| except Exception as e: | |
| logger.error(f"Failed to get model info: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def recognize_speech( | |
| background_tasks: BackgroundTasks, | |
| file: UploadFile = File(...), | |
| language: str = Query( | |
| default=None, description="Language code (auto-detect if None)" | |
| ), | |
| include_alternatives: bool = Query( | |
| default=True, description="Include alternative intents" | |
| ), | |
| confidence_threshold: float = Query( | |
| default=0.5, description="Minimum confidence threshold" | |
| ), | |
| ): | |
| """ | |
| Recognize speech from uploaded audio file. | |
| Supports WAV, WebM, MP3, and M4A formats. | |
| Returns transcription and intent classification. | |
| """ | |
| if not recognizer: | |
| raise HTTPException(status_code=503, detail="Speech recognizer not available") | |
| start_time = time.time() | |
| temp_files = [] | |
| try: | |
| logger.info( | |
| f"Processing audio: {file.filename}, content_type: {file.content_type}" | |
| ) | |
| # Read file content | |
| content = await file.read() | |
| if len(content) == 0: | |
| raise HTTPException(status_code=400, detail="Empty audio file") | |
| if len(content) > config.API["max_file_size"]: | |
| raise HTTPException(status_code=413, detail="File too large") | |
| # Detect audio format | |
| detected_format = detect_audio_format(content, file.filename or "") | |
| # Save uploaded file | |
| suffix = f".{detected_format}" | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp_file: | |
| tmp_file.write(content) | |
| temp_file_path = tmp_file.name | |
| temp_files.append(temp_file_path) | |
| logger.info(f"Saved file: {temp_file_path} (format: {detected_format})") | |
| # Convert to WAV if needed | |
| if detected_format != "wav": | |
| converted_path = convert_audio_if_needed(temp_file_path, detected_format) | |
| if converted_path != temp_file_path: | |
| temp_files.append(converted_path) | |
| temp_file_path = converted_path | |
| # Perform recognition | |
| result = await recognizer.recognize_async(temp_file_path, language) | |
| if result.get("status") != "success": | |
| error_msg = result.get("error", "Recognition failed") | |
| raise HTTPException(status_code=500, detail=error_msg) | |
| # Apply confidence threshold | |
| intent_confidence = result["intent"].get("confidence", 0.0) | |
| if intent_confidence < confidence_threshold: | |
| result["intent"]["intent"] = "low_confidence" | |
| result["intent"]["original_intent"] = result["intent"]["intent"] | |
| # Filter alternatives if requested | |
| if include_alternatives and "alternatives" in result["intent"]: | |
| result["intent"]["alternatives"] = [ | |
| alt | |
| for alt in result["intent"]["alternatives"] | |
| if alt.get("score", 0) >= confidence_threshold | |
| ] | |
| elif not include_alternatives: | |
| result["intent"].pop("alternatives", None) | |
| # Add file information | |
| result["file_info"] = { | |
| "filename": file.filename, | |
| "content_type": file.content_type, | |
| "detected_format": detected_format, | |
| "file_size": len(content), | |
| } | |
| processing_time = time.time() - start_time | |
| result["total_processing_time"] = processing_time | |
| # Schedule cleanup | |
| background_tasks.add_task(cleanup_temp_files, *temp_files) | |
| logger.info( | |
| f"Recognition completed: {result['transcription']['text'][:50]}... -> {result['intent']['intent']}" | |
| ) | |
| return JSONResponse(content=result, headers=CORS_HEADERS) | |
| except HTTPException: | |
| # Re-raise HTTP exceptions | |
| await cleanup_temp_files(*temp_files) | |
| raise | |
| except Exception as e: | |
| logger.error(f"Recognition error: {e}") | |
| await cleanup_temp_files(*temp_files) | |
| error_detail = str(e) | |
| if "timeout" in error_detail.lower(): | |
| raise HTTPException(status_code=408, detail="Processing timeout") | |
| elif "format" in error_detail.lower() or "audio" in error_detail.lower(): | |
| raise HTTPException(status_code=400, detail="Invalid audio format") | |
| else: | |
| raise HTTPException(status_code=500, detail=error_detail) | |
| async def test_intent( | |
| background_tasks: BackgroundTasks, | |
| file: UploadFile = File(...), | |
| top_k: int = Query( | |
| default=5, description="Number of top intent predictions to return" | |
| ), | |
| ): | |
| """ | |
| Test intent classification from audio (compatible with existing frontend). | |
| This endpoint maintains compatibility with the existing frontend | |
| while using the optimized recognition engine. | |
| """ | |
| if not recognizer: | |
| raise HTTPException(status_code=503, detail="Speech recognizer not available") | |
| start_time = time.time() | |
| temp_files = [] | |
| try: | |
| logger.info( | |
| f"🎵 Fast processing: {file.filename} ({file.content_type}), top_k: {top_k}" | |
| ) | |
| # Read and validate file | |
| content = await file.read() | |
| if len(content) == 0: | |
| raise HTTPException(status_code=400, detail="Empty audio file") | |
| # Check cache first | |
| content_hash = str(hash(content)) | |
| if content_hash in audio_cache: | |
| performance_stats["cache_hits"] += 1 | |
| logger.info(f"✅ Cache hit for {file.filename}") | |
| cached_result = audio_cache[content_hash] | |
| update_performance_stats(0.1, success=True) # Very fast cache response | |
| return cached_result | |
| # Use optimized audio conversion | |
| temp_file_path = await convert_audio_fast(content, file.filename or "") | |
| temp_files.append(temp_file_path) | |
| # Perform recognition with optimized timeout and async processing | |
| timeout_duration = 45 # Reduced from 60-120s to 45s max | |
| try: | |
| # Use thread pool for better async performance | |
| loop = asyncio.get_event_loop() | |
| result = await asyncio.wait_for( | |
| loop.run_in_executor( | |
| thread_pool, recognizer.recognize, temp_file_path, "tw" | |
| ), | |
| timeout=timeout_duration, | |
| ) | |
| except asyncio.TimeoutError: | |
| raise HTTPException( | |
| status_code=408, | |
| detail=f"Processing timeout ({timeout_duration}s). Please try a shorter audio file.", | |
| ) | |
| if result.get("status") != "success": | |
| error_msg = result.get("error", "Recognition failed") | |
| raise HTTPException( | |
| status_code=500, detail=f"Model inference failed: {error_msg}" | |
| ) | |
| # Format response for frontend compatibility | |
| processing_time_ms = (time.time() - start_time) * 1000 | |
| # Get top predictions | |
| alternatives = result["intent"].get("alternatives", []) | |
| top_predictions = alternatives[:top_k] | |
| # Ensure we have the main prediction in the list | |
| main_intent = { | |
| "intent": result["intent"]["intent"], | |
| "confidence": result["intent"]["confidence"], | |
| "index": 0, | |
| } | |
| # Remove duplicates and ensure main prediction is first | |
| seen_intents = {main_intent["intent"]} | |
| final_predictions = [main_intent] | |
| for pred in top_predictions: | |
| if pred.get("label") not in seen_intents: | |
| final_predictions.append( | |
| { | |
| "intent": pred.get("label", "unknown"), | |
| "confidence": pred.get("score", 0.0), | |
| "index": len(final_predictions), | |
| } | |
| ) | |
| seen_intents.add(pred.get("label")) | |
| response_data = { | |
| "filename": file.filename or "audio.wav", | |
| "intent": result["intent"]["intent"], | |
| "confidence": float(result["intent"]["confidence"]), | |
| "top_predictions": final_predictions[:top_k], | |
| "transcription": result["transcription"]["text"], | |
| "model_type": "optimized_whisper_intent_fast", | |
| "processing_time_ms": round(processing_time_ms, 2), | |
| "top_k": top_k, | |
| "whisper_info": { | |
| "model_size": config.WHISPER["model_size"], | |
| "language": result["transcription"].get("language", "tw"), | |
| "transcription_confidence": result["transcription"].get( | |
| "confidence", 0.0 | |
| ), | |
| }, | |
| } | |
| # Cache successful results for future requests | |
| if processing_time_ms < 10000: # Only cache results under 10s | |
| audio_cache[content_hash] = response_data | |
| # Update performance statistics | |
| update_performance_stats(processing_time_ms / 1000.0, success=True) | |
| # Schedule cleanup and memory optimization | |
| background_tasks.add_task(cleanup_temp_files, *temp_files) | |
| background_tasks.add_task(cleanup_memory) | |
| logger.info( | |
| f"✅ Completed in {processing_time_ms:.0f}ms: '{result['transcription']['text'][:50]}...' -> {result['intent']['intent']} ({result['intent']['confidence']:.3f})" | |
| ) | |
| return JSONResponse(content=response_data, headers=CORS_HEADERS) | |
| except HTTPException: | |
| await cleanup_temp_files(*temp_files) | |
| update_performance_stats(time.time() - start_time, success=False) | |
| raise | |
| except Exception as e: | |
| logger.error(f"❌ Request failed: {e}") | |
| await cleanup_temp_files(*temp_files) | |
| update_performance_stats(time.time() - start_time, success=False) | |
| if "timeout" in str(e).lower(): | |
| raise HTTPException( | |
| status_code=408, detail="Processing timeout - try shorter audio" | |
| ) | |
| elif "webm" in str(e).lower() or "format" in str(e).lower(): | |
| raise HTTPException( | |
| status_code=400, detail="Audio format processing failed" | |
| ) | |
| else: | |
| raise HTTPException(status_code=500, detail=f"Processing failed: {str(e)}") | |
| async def get_performance_stats(): | |
| """Get detailed performance statistics and optimization status.""" | |
| import torch | |
| stats = { | |
| "performance_metrics": performance_stats.copy(), | |
| "optimization_status": { | |
| "torch_optimized": True, | |
| "cuda_available": torch.cuda.is_available(), | |
| "cache_enabled": True, | |
| "thread_pool_active": thread_pool._threads if thread_pool else 0, | |
| }, | |
| "cache_info": { | |
| "audio_cache_size": len(audio_cache), | |
| "cache_hit_rate": performance_stats["cache_hits"] | |
| / max(1, performance_stats["total_requests"]), | |
| }, | |
| "recommendations": [ | |
| "Use GPU for 3-5x faster processing", | |
| "Keep audio files under 30 seconds", | |
| "Use WAV format when possible", | |
| f"Current avg response time: {performance_stats['avg_processing_time']:.2f}s", | |
| ], | |
| } | |
| if torch.cuda.is_available(): | |
| stats["gpu_info"] = { | |
| "device_name": torch.cuda.get_device_name(), | |
| "memory_allocated_gb": torch.cuda.memory_allocated() / (1024**3), | |
| "memory_reserved_gb": torch.cuda.memory_reserved() / (1024**3), | |
| } | |
| return JSONResponse(content=stats) | |
| async def batch_recognize( | |
| background_tasks: BackgroundTasks, | |
| files: List[UploadFile] = File(...), | |
| language: str = Query( | |
| default=None, description="Language code (auto-detect if None)" | |
| ), | |
| ): | |
| """Process multiple audio files in parallel.""" | |
| if not recognizer: | |
| raise HTTPException(status_code=503, detail="Speech recognizer not available") | |
| if len(files) > 10: # Limit batch size | |
| raise HTTPException(status_code=400, detail="Too many files (max 10)") | |
| start_time = time.time() | |
| temp_files = [] | |
| try: | |
| # Save all files first | |
| file_paths = [] | |
| for i, file in enumerate(files): | |
| content = await file.read() | |
| detected_format = detect_audio_format(content, file.filename or "") | |
| with tempfile.NamedTemporaryFile( | |
| delete=False, suffix=f".{detected_format}" | |
| ) as tmp_file: | |
| tmp_file.write(content) | |
| temp_path = tmp_file.name | |
| temp_files.append(temp_path) | |
| # Convert if needed | |
| if detected_format != "wav": | |
| converted_path = convert_audio_if_needed(temp_path, detected_format) | |
| if converted_path != temp_path: | |
| temp_files.append(converted_path) | |
| temp_path = converted_path | |
| file_paths.append(temp_path) | |
| # Process in parallel | |
| results = recognizer.recognize_stream(file_paths, language) | |
| # Add file info to results | |
| for i, result in enumerate(results): | |
| result["file_info"] = {"filename": files[i].filename, "index": i} | |
| # Schedule cleanup | |
| background_tasks.add_task(cleanup_temp_files, *temp_files) | |
| return JSONResponse( | |
| content={ | |
| "results": results, | |
| "total_files": len(files), | |
| "total_processing_time": time.time() - start_time, | |
| }, | |
| headers=CORS_HEADERS, | |
| ) | |
| except Exception as e: | |
| logger.error(f"Batch recognition error: {e}") | |
| await cleanup_temp_files(*temp_files) | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # Error handlers | |
| async def payload_too_large_handler(request, exc): | |
| """Handle file too large errors.""" | |
| return JSONResponse( | |
| status_code=413, | |
| content={"detail": "Audio file too large. Maximum size: 50MB"}, | |
| headers=CORS_HEADERS, | |
| ) | |
| async def validation_error_handler(request, exc): | |
| """Handle validation errors.""" | |
| return JSONResponse( | |
| status_code=422, | |
| content={"detail": "Invalid request format"}, | |
| headers=CORS_HEADERS, | |
| ) | |
| # Server startup function | |
| def start_server(): | |
| """Start the FastAPI server.""" | |
| host = config.API["host"] | |
| port = int(os.environ.get("PORT", config.API["port"])) | |
| logger.info(f"Starting server on {host}:{port}") | |
| uvicorn.run( | |
| app, | |
| host=host, | |
| port=port, | |
| log_level="info", | |
| access_log=True, | |
| workers=1, # Single worker for model consistency | |
| loop="asyncio", | |
| ) | |