#!/usr/bin/env python3 """ Main Launcher for Optimized Twi Speech Recognition Engine ======================================================== This is the main entry point for the optimized speech recognition system. It provides a unified interface for starting the server, running tests, and managing the engine. Usage: python main.py server # Start API server python main.py test # Run test suite python main.py setup # Run setup python main.py demo # Run interactive demo python main.py --help # Show help Author: AI Assistant Date: 2025-11-05 """ import argparse import asyncio import json import logging import os import signal import sys import time from pathlib import Path # Add src to path for imports current_dir = Path(__file__).parent sys.path.insert(0, str(current_dir / "src")) sys.path.insert(0, str(current_dir)) # Configure logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) logger = logging.getLogger(__name__) import uvicorn from src.api_server import app class OptimizedEngineManager: """Manager for the optimized speech recognition engine.""" def __init__(self, huggingface_repo=None): self.base_dir = Path(__file__).parent self.running = False self.huggingface_repo = huggingface_repo self.model_type = None # Will be detected: 'single' or 'multi' def setup_environment(self): """Setup environment variables and paths.""" # Set environment variables os.environ.setdefault("ENVIRONMENT", "development") os.environ.setdefault("LOG_LEVEL", "INFO") os.environ.setdefault("PYTHONPATH", str(self.base_dir)) # Create necessary directories for dir_name in ["logs", "data", "models"]: (self.base_dir / dir_name).mkdir(exist_ok=True) def download_huggingface_model(self): """Download and setup HuggingFace model with retry and timeout handling.""" if not self.huggingface_repo: return True logger.info(f"๐Ÿ“ฅ Downloading HuggingFace model: {self.huggingface_repo}") try: # Create models directory first models_dir = self.base_dir / "models" / "huggingface" models_dir.mkdir(parents=True, exist_ok=True) # Check if model already exists existing_model = self._check_existing_model(models_dir) if existing_model: logger.info(f"๐Ÿ“ Found existing model at: {existing_model}") os.environ["HUGGINGFACE_MODEL_PATH"] = existing_model self._detect_and_set_model_type(existing_model) logger.info( f"๐Ÿ”ง Set HUGGINGFACE_MODEL_PATH={os.environ.get('HUGGINGFACE_MODEL_PATH')}" ) logger.info( f"๐Ÿ”ง Set HUGGINGFACE_MODEL_TYPE={os.environ.get('HUGGINGFACE_MODEL_TYPE')}" ) return True import json import time import requests from huggingface_hub import hf_hub_download, snapshot_download from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry # Configure retry strategy for downloads retry_strategy = Retry( total=3, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["HEAD", "GET", "OPTIONS"], backoff_factor=1, ) # Create session with timeout and retry configuration session = requests.Session() adapter = HTTPAdapter(max_retries=retry_strategy) session.mount("http://", adapter) session.mount("https://", adapter) # Set longer timeout for large files session.timeout = (30, 300) # (connect_timeout, read_timeout) logger.info( "๐Ÿ”„ Starting model download with extended timeout (5 minutes)..." ) max_retries = 3 for attempt in range(max_retries): try: logger.info(f"๐Ÿ“ฆ Download attempt {attempt + 1}/{max_retries}") # Download model with extended timeout model_path = snapshot_download( repo_id=self.huggingface_repo, local_dir=str( models_dir / self.huggingface_repo.replace("/", "_") ), local_dir_use_symlinks=False, resume_download=True, # Enable resume for interrupted downloads ) logger.info( f"โœ… Model downloaded successfully on attempt {attempt + 1}" ) break except Exception as download_error: logger.warning( f"โš ๏ธ Download attempt {attempt + 1} failed: {download_error}" ) if attempt < max_retries - 1: wait_time = ( attempt + 1 ) * 30 # Progressive backoff: 30s, 60s, 90s logger.info(f"โณ Waiting {wait_time} seconds before retry...") time.sleep(wait_time) else: # Try fallback download method logger.info("๐Ÿ”„ Trying fallback individual file download...") model_path = self._download_model_files_individually( models_dir, session ) if model_path: logger.info("โœ… Fallback download successful") break else: raise download_error # Set up model path and detect type self._detect_and_set_model_type(model_path) logger.info(f"โœ… Model downloaded successfully: {model_path}") return True except ImportError: logger.error( "โŒ huggingface_hub not installed. Run: pip install huggingface_hub" ) return False except Exception as e: logger.error(f"โŒ Failed to download HuggingFace model: {e}") self._show_manual_download_instructions() return False def _download_model_files_individually(self, models_dir, session): """ Fallback method to download model files individually. This can help when snapshot_download times out on large files. """ try: from huggingface_hub import hf_hub_download model_local_dir = models_dir / self.huggingface_repo.replace("/", "_") model_local_dir.mkdir(parents=True, exist_ok=True) # Essential files to download (skip large pytorch_model.bin initially) essential_files = [ "config.json", "generation_config.json", "tokenizer_config.json", "vocab.json", "merges.txt", "normalizer.json", "added_tokens.json", "special_tokens_map.json", "preprocessor_config.json", ] logger.info(f"๐Ÿ“ฆ Downloading essential files for {self.huggingface_repo}") # Download essential files first for filename in essential_files: try: logger.info(f"โฌ‡๏ธ Downloading {filename}...") hf_hub_download( repo_id=self.huggingface_repo, filename=filename, local_dir=str(model_local_dir), local_dir_use_symlinks=False, ) except Exception as e: logger.warning(f"โš ๏ธ Could not download {filename}: {e}") # Try to download the model weights with extended timeout try: logger.info( "โฌ‡๏ธ Downloading pytorch_model.bin (this may take several minutes)..." ) hf_hub_download( repo_id=self.huggingface_repo, filename="pytorch_model.bin", local_dir=str(model_local_dir), local_dir_use_symlinks=False, resume_download=True, ) logger.info("โœ… Model weights downloaded successfully") except Exception as e: logger.error(f"โŒ Failed to download model weights: {e}") logger.info( "๐Ÿ”„ You may need to download the model manually or use a smaller model" ) return None return str(model_local_dir) except Exception as e: logger.error(f"โŒ Individual file download failed: {e}") return None def _detect_and_set_model_type(self, model_path): """Detect model type and set environment variables.""" # Detect model type by checking config and files config_path = Path(model_path) / "config.json" if config_path.exists(): with open(config_path, "r") as f: config = json.load(f) # Check for multi-task indicators if any( key in config for key in ["num_labels", "custom_model", "task_types"] ) or any( file.exists() for file in [ Path(model_path) / "intent_labels.json", Path(model_path) / "label_map.json", Path(model_path) / "classification_head.bin", ] ): self.model_type = "multi" logger.info( "๐Ÿ” Detected multi-task model (transcription + intent classification)" ) else: self.model_type = "single" logger.info("๐Ÿ” Detected single-task model (transcription only)") else: self.model_type = "single" logger.info("๐Ÿ” No config found, assuming single-task model") # Set environment variables os.environ["HUGGINGFACE_MODEL_PATH"] = model_path os.environ["HUGGINGFACE_MODEL_TYPE"] = self.model_type def _check_existing_model(self, models_dir): """Check if the model already exists locally.""" if not self.huggingface_repo: logger.warning("โš ๏ธ No huggingface_repo specified") return None model_local_dir = models_dir / self.huggingface_repo.replace("/", "_") logger.info(f"๐Ÿ” Checking for model at: {model_local_dir}") if model_local_dir.exists(): logger.info(f"๐Ÿ“ Directory exists: {model_local_dir}") # Check if essential files exist essential_files = ["config.json"] missing_files = [] for f in essential_files: file_path = model_local_dir / f if not file_path.exists(): missing_files.append(f) else: logger.info(f"โœ… Found essential file: {file_path}") if not missing_files: logger.info(f"โœ… Complete model found at: {model_local_dir}") return str(model_local_dir) else: logger.warning(f"โš ๏ธ Missing essential files: {missing_files}") else: logger.warning(f"โš ๏ธ Model directory does not exist: {model_local_dir}") return None def _show_manual_download_instructions(self): """Show instructions for manual model download.""" models_dir = self.base_dir / "models" / "huggingface" target_dir = models_dir / self.huggingface_repo.replace("/", "_") logger.error("=" * 60) logger.error("๐Ÿ“‹ MANUAL DOWNLOAD INSTRUCTIONS") logger.error("=" * 60) logger.error( "Due to network timeouts, you may need to download the model manually." ) logger.error("") logger.error("Option 1 - Use git to clone the model:") logger.error(f" mkdir -p {target_dir}") logger.error(f" cd {target_dir}") logger.error(f" git clone https://huggingface.co/{self.huggingface_repo} .") logger.error("") logger.error("Option 2 - Download using huggingface-cli:") logger.error( f" huggingface-cli download {self.huggingface_repo} --local-dir {target_dir}" ) logger.error("") logger.error("Option 3 - Use a smaller/faster model:") logger.error(" python main.py server --huggingface openai/whisper-small") logger.error("") logger.error("After manual download, restart the server.") logger.error("=" * 60) def run_setup(self): """Run the setup script.""" logger.info("๐Ÿ”ง Running setup...") try: # Download HuggingFace model if specified if not self.download_huggingface_model(): return False # Import and run setup sys.path.insert(0, str(self.base_dir)) from setup import main as setup_main setup_main() logger.info("โœ… Setup completed successfully") return True except ImportError: logger.error("โŒ Setup script not found") return False except Exception as e: logger.error(f"โŒ Setup failed: {e}") return False def start_server(self, host="0.0.0.0", port=8000, reload=False): """Start the optimized FastAPI server with HuggingFace model support.""" logger.info(f"๐Ÿš€ Starting server on {host}:{port}") # Log current HuggingFace settings if self.huggingface_repo: logger.info(f"๐Ÿค— HuggingFace repository: {self.huggingface_repo}") else: logger.info( "๐Ÿ“ No HuggingFace repository specified, using default Whisper model" ) # Download and setup HuggingFace model if specified if self.huggingface_repo and not self.download_huggingface_model(): logger.error("โŒ Failed to setup HuggingFace model") return False try: # Override port from environment port = int(os.environ.get("PORT", port)) # Set up signal handlers for graceful shutdown def signal_handler(signum, frame): logger.info("๐Ÿ“ก Received shutdown signal") self.running = False sys.exit(0) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) self.running = True # Start server with uvicorn uvicorn.run( app, host=host, port=port, log_level="info", access_log=True, reload=reload, ) except Exception as e: logger.error(f"โŒ Server failed to start: {e}") return False async def run_tests(self, verbose=False): """Run the test suite.""" logger.info("๐Ÿงช Running test suite...") try: sys.path.insert(0, str(self.base_dir)) from test_engine import EngineTestSuite test_suite = EngineTestSuite() results = await test_suite.run_all_tests() # Summary passed = sum(1 for result in results.values() if result) total = len(results) if passed == total: logger.info("๐ŸŽ‰ All tests passed!") return True else: logger.warning(f"โš ๏ธ {passed}/{total} tests passed") return False except ImportError: logger.error("โŒ Test suite not found") return False except Exception as e: logger.error(f"โŒ Tests failed: {e}") return False def run_demo(self): """Run interactive demo.""" logger.info("๐ŸŽฎ Starting interactive demo...") try: sys.path.insert(0, str(self.base_dir / "src")) from speech_recognizer import create_speech_recognizer print("\n" + "=" * 60) print(" OPTIMIZED TWI SPEECH RECOGNITION DEMO") print("=" * 60) # Create recognizer recognizer = create_speech_recognizer() # Health check health = recognizer.health_check() print(f"System Status: {health['status']}") # Show supported intents intents = recognizer.get_supported_intents() print(f"\nSupported Intents ({len(intents)}):") for i, intent in enumerate(intents[:10], 1): print(f" {i:2d}. {intent['intent']:20s} - {intent['description']}") if len(intents) > 10: print(f" ... and {len(intents) - 10} more") print("\n" + "=" * 60) # Interactive mode print("\nDemo Mode:") print("1. Place your audio files in the current directory") print("2. Enter the filename when prompted") print("3. Type 'quit' to exit") print("-" * 60) while True: try: # Get audio file filename = input("\nEnter audio filename (or 'quit'): ").strip() if filename.lower() in ["quit", "exit", "q"]: break if not filename: continue # Check if file exists audio_path = Path(filename) if not audio_path.exists(): print(f"โŒ File not found: {filename}") continue # Process audio print(f"๐Ÿ”„ Processing {filename}...") start_time = time.time() result = recognizer.recognize(str(audio_path)) processing_time = time.time() - start_time # Show results if result["status"] == "success": print(f"โœ… Recognition completed in {processing_time:.2f}s") print(f"๐Ÿ“ Transcription: '{result['transcription']['text']}'") print(f"๐ŸŽฏ Intent: {result['intent']['intent']}") print(f"๐Ÿ“Š Confidence: {result['intent']['confidence']:.3f}") # Show alternatives if available alternatives = result["intent"].get("alternatives", []) if len(alternatives) > 1: print("๐Ÿ“‹ Alternatives:") for alt in alternatives[1:4]: # Show top 3 alternatives print( f" - {alt.get('label', 'unknown')}: {alt.get('score', 0):.3f}" ) else: print( f"โŒ Recognition failed: {result.get('error', 'Unknown error')}" ) except KeyboardInterrupt: print("\n๐Ÿ‘‹ Demo interrupted") break except Exception as e: print(f"โŒ Error: {e}") print("\n๐Ÿ‘‹ Demo completed") return True except ImportError: logger.error("โŒ Speech recognizer not available") return False except Exception as e: logger.error(f"โŒ Demo failed: {e}") return False def show_status(self): """Show system status.""" print("\n" + "=" * 60) print(" OPTIMIZED TWI SPEECH ENGINE STATUS") print("=" * 60) try: sys.path.insert(0, str(self.base_dir / "src")) from speech_recognizer import create_speech_recognizer recognizer = create_speech_recognizer() # Health check health = recognizer.health_check() print(f"System Status: {health['status']}") if "components" in health: print("\nComponents:") for component, status in health["components"].items(): emoji = "โœ…" if status == "healthy" else "โŒ" print(f" {emoji} {component}: {status}") if "device_info" in health: print(f"\nDevice: {health['device_info'].get('device', 'unknown')}") print( f"CUDA Available: {health['device_info'].get('cuda_available', False)}" ) # Statistics stats = recognizer.get_statistics() print(f"\nStatistics:") print(f" Total Requests: {stats.get('total_requests', 0)}") print(f" Success Rate: {stats.get('success_rate', 0):.1f}%") print(f" Avg Processing Time: {stats.get('avg_processing_time', 0):.3f}s") # Supported intents intents = recognizer.get_supported_intents() print(f" Supported Intents: {len(intents)}") return True except Exception as e: print(f"โŒ Failed to get status: {e}") return False def show_info(self): """Show system information.""" print("\n" + "=" * 60) print(" OPTIMIZED TWI SPEECH ENGINE INFO") print("=" * 60) try: # Basic info print(f"Base Directory: {self.base_dir}") print(f"Python Version: {sys.version}") # Check dependencies print("\nDependencies:") dependencies = [ ("torch", "PyTorch"), ("whisper", "OpenAI Whisper"), ("transformers", "HuggingFace Transformers"), ("fastapi", "FastAPI"), ("librosa", "Librosa"), ("soundfile", "SoundFile"), ] for module_name, display_name in dependencies: try: module = __import__(module_name) version = getattr(module, "__version__", "unknown") print(f" โœ… {display_name}: {version}") except ImportError: print(f" โŒ {display_name}: Not installed") # Configuration print(f"\nConfiguration:") try: sys.path.insert(0, str(self.base_dir)) sys.path.insert(0, str(self.base_dir)) from config.config import OptimizedConfig config = OptimizedConfig() print(f" Whisper Model: {config.WHISPER['model_size']}") print(f" Supported Intents: {len(config.INTENTS)}") print(f" Device: {config.get_device()}") except Exception as e: print(f" โŒ Configuration error: {e}") return True except Exception as e: print(f"โŒ Failed to get info: {e}") return False def create_parser(): """Create command line argument parser.""" parser = argparse.ArgumentParser( description="Optimized Twi Speech Recognition Engine", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: python main.py server # Start API server python main.py server --port 9000 # Start on custom port python main.py server --huggingface username/model-name # Use HuggingFace model python main.py test # Run tests python main.py demo # Interactive demo python main.py status # Show status python main.py setup # Run setup """, ) subparsers = parser.add_subparsers(dest="command", help="Available commands") # Server command server_parser = subparsers.add_parser("server", help="Start API server") server_parser.add_argument("--host", default="0.0.0.0", help="Host address") server_parser.add_argument("--port", type=int, default=8000, help="Port number") server_parser.add_argument( "--reload", action="store_true", help="Enable auto-reload" ) server_parser.add_argument( "--huggingface", type=str, help="HuggingFace model repository ID (e.g., username/model-name)", ) # Test command test_parser = subparsers.add_parser("test", help="Run test suite") test_parser.add_argument( "--verbose", "-v", action="store_true", help="Verbose output" ) # Demo command demo_parser = subparsers.add_parser("demo", help="Run interactive demo") # Status command status_parser = subparsers.add_parser("status", help="Show system status") # Info command info_parser = subparsers.add_parser("info", help="Show system information") # Setup command setup_parser = subparsers.add_parser("setup", help="Run setup script") return parser def main(): """Main function.""" parser = create_parser() args = parser.parse_args() # Extract HuggingFace repo from server args if present huggingface_repo = None if args.command == "server" and hasattr(args, "huggingface") and args.huggingface: huggingface_repo = args.huggingface logger.info(f"๐Ÿค— Using HuggingFace model: {huggingface_repo}") # Create manager with HuggingFace support manager = OptimizedEngineManager(huggingface_repo=huggingface_repo) manager.setup_environment() # Print banner print("\n" + "=" * 60) print(" OPTIMIZED TWI SPEECH RECOGNITION ENGINE") print("=" * 60) # Handle commands if args.command == "server": success = manager.start_server( host=args.host, port=args.port, reload=args.reload ) elif args.command == "test": # Run tests in asyncio context import asyncio success = asyncio.run(manager.run_tests(verbose=args.verbose)) elif args.command == "demo": success = manager.run_demo() elif args.command == "status": success = manager.show_status() elif args.command == "info": success = manager.show_info() elif args.command == "setup": success = manager.run_setup() else: # No command specified, show help parser.print_help() # Show quick status print("\n" + "-" * 60) print("QUICK STATUS:") try: manager.show_status() except: print("โŒ Engine not ready. Run 'python main.py setup' first.") success = True # Exit with appropriate code sys.exit(0 if success else 1) if __name__ == "__main__": try: main() except KeyboardInterrupt: print("\n๐Ÿ‘‹ Goodbye!") sys.exit(0) except Exception as e: logger.error(f"Fatal error: {e}") sys.exit(1)