TwiSpeechModel / main.py
ProvLeon's picture
Upload folder using huggingface_hub
6db3515 verified
Raw
History Blame Contribute Delete
27.5 kB
#!/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)