Spaces:
Starting on T4
Starting on T4
| """ | |
| STT Benchmarking Script | |
| This script benchmarks different Speech-to-Text (STT) handlers to compare their performance. | |
| Measures: inference time, warmup time, memory usage, and transcription quality. | |
| Usage: | |
| python benchmark_stt.py --audio_file path/to/audio.wav --iterations 10 | |
| python benchmark_stt.py --audio_file path/to/audio.wav --handlers whisper mlx-audio-whisper | |
| """ | |
| import argparse | |
| import json | |
| import logging | |
| import time | |
| from pathlib import Path | |
| from queue import Queue | |
| from threading import Event | |
| from typing import Any, Dict, List, Optional | |
| import numpy as np | |
| import soundfile as sf | |
| from speech_to_speech.pipeline.messages import VADAudio | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", | |
| ) | |
| logger = logging.getLogger(__name__) | |
| class BenchmarkResult: | |
| """Stores benchmark results for a single STT handler.""" | |
| def __init__(self, handler_name: str): | |
| self.handler_name = handler_name | |
| self.warmup_time = 0.0 | |
| self.inference_times: list[float] = [] | |
| self.time_to_first_token: list[float] = [] | |
| self.transcriptions: list[str] = [] | |
| self.errors: list[str] = [] | |
| def add_inference(self, time_taken: float, transcription: Any, ttft: Optional[float] = None): | |
| self.inference_times.append(time_taken) | |
| self.transcriptions.append(transcription) | |
| if ttft is not None: | |
| self.time_to_first_token.append(ttft) | |
| def add_error(self, error: str): | |
| self.errors.append(error) | |
| def get_stats(self) -> Dict[str, Any]: | |
| """Calculate statistics from benchmark results.""" | |
| if not self.inference_times: | |
| return { | |
| "handler": self.handler_name, | |
| "status": "failed", | |
| "errors": self.errors, | |
| } | |
| stats = { | |
| "handler": self.handler_name, | |
| "warmup_time": self.warmup_time, | |
| "avg_inference_time": np.mean(self.inference_times), | |
| "min_inference_time": np.min(self.inference_times), | |
| "max_inference_time": np.max(self.inference_times), | |
| "std_inference_time": np.std(self.inference_times), | |
| "total_iterations": len(self.inference_times), | |
| "errors": self.errors, | |
| "sample_transcription": self.transcriptions[0] if self.transcriptions else None, | |
| } | |
| # Add time to first token stats if available | |
| if self.time_to_first_token: | |
| stats["avg_time_to_first_token"] = np.mean(self.time_to_first_token) | |
| stats["min_time_to_first_token"] = np.min(self.time_to_first_token) | |
| stats["max_time_to_first_token"] = np.max(self.time_to_first_token) | |
| stats["std_time_to_first_token"] = np.std(self.time_to_first_token) | |
| return stats | |
| def load_audio(audio_path: str) -> np.ndarray: | |
| """Load audio file and return as numpy array.""" | |
| logger.info(f"Loading audio from: {audio_path}") | |
| audio, sample_rate = sf.read(audio_path) | |
| # Convert to mono if stereo | |
| if len(audio.shape) > 1: | |
| audio = audio.mean(axis=1) | |
| # Resample to 16kHz if needed (most whisper models expect 16kHz) | |
| if sample_rate != 16000: | |
| logger.warning(f"Audio sample rate is {sample_rate}Hz, resampling to 16000Hz") | |
| try: | |
| import librosa | |
| audio = librosa.resample(audio, orig_sr=sample_rate, target_sr=16000) | |
| except ImportError: | |
| logger.error("librosa not installed. Please install it with: pip install librosa") | |
| logger.error("Attempting scipy resampling as fallback...") | |
| from scipy import signal | |
| # Calculate resampling ratio | |
| num_samples = int(len(audio) * 16000 / sample_rate) | |
| audio = signal.resample(audio, num_samples) | |
| return audio.astype(np.float32) | |
| def benchmark_handler( | |
| handler_name: str, | |
| audio: np.ndarray, | |
| iterations: int, | |
| handler_kwargs: Optional[Dict[str, Any]] = None | |
| ) -> BenchmarkResult: | |
| """Benchmark a single STT handler.""" | |
| logger.info(f"Benchmarking {handler_name}...") | |
| result = BenchmarkResult(handler_name) | |
| try: | |
| # Create queues and events for handler | |
| stop_event = Event() | |
| queue_in: Queue[Any] = Queue() | |
| queue_out: Queue[Any] = Queue() | |
| handler: Any = None | |
| if handler_name == "whisper": | |
| from speech_to_speech.STT.whisper_stt_handler import WhisperSTTHandler | |
| setup_kwargs = handler_kwargs or { | |
| "model_name": "distil-whisper/distil-large-v3", | |
| "device": "cuda", | |
| "torch_dtype": "float16", | |
| } | |
| handler = WhisperSTTHandler( | |
| stop_event, | |
| queue_in=queue_in, | |
| queue_out=queue_out, | |
| setup_kwargs=setup_kwargs | |
| ) | |
| elif handler_name == "whisper-mlx": | |
| from speech_to_speech.STT.lightning_whisper_mlx_handler import LightningWhisperSTTHandler | |
| setup_kwargs = handler_kwargs or { | |
| "model_name": "large-v3", | |
| "device": "mps", | |
| } | |
| handler = LightningWhisperSTTHandler( | |
| stop_event, | |
| queue_in=queue_in, | |
| queue_out=queue_out, | |
| setup_kwargs=setup_kwargs | |
| ) | |
| elif handler_name == "mlx-audio-whisper": | |
| from speech_to_speech.STT.mlx_audio_whisper_handler import MLXAudioWhisperSTTHandler | |
| setup_kwargs = handler_kwargs or { | |
| "model_name": "mlx-community/whisper-large-v3-turbo", | |
| } | |
| handler = MLXAudioWhisperSTTHandler( | |
| stop_event, | |
| queue_in=queue_in, | |
| queue_out=queue_out, | |
| setup_kwargs=setup_kwargs | |
| ) | |
| elif handler_name == "faster-whisper": | |
| from speech_to_speech.STT.faster_whisper_handler import FasterWhisperSTTHandler | |
| setup_kwargs = handler_kwargs or { | |
| "model_name": "large-v3", | |
| "device": "auto", | |
| "compute_type": "float16", | |
| } | |
| handler = FasterWhisperSTTHandler( | |
| stop_event, | |
| queue_in=queue_in, | |
| queue_out=queue_out, | |
| setup_kwargs=setup_kwargs | |
| ) | |
| elif handler_name == "moonshine": | |
| from archive.STT.moonshine_handler import MoonshineSTTHandler | |
| handler = MoonshineSTTHandler( | |
| stop_event, | |
| queue_in=queue_in, | |
| queue_out=queue_out, | |
| ) | |
| elif handler_name == "parakeet-tdt": | |
| from speech_to_speech.STT.parakeet_tdt_handler import ParakeetTDTSTTHandler | |
| setup_kwargs = handler_kwargs or { | |
| "device": "mps", | |
| "enable_live_transcription": False, | |
| } | |
| handler = ParakeetTDTSTTHandler( | |
| stop_event, | |
| queue_in=queue_in, | |
| queue_out=queue_out, | |
| setup_kwargs=setup_kwargs | |
| ) | |
| elif handler_name == "parakeet-tdt-progressive": | |
| from speech_to_speech.STT.parakeet_tdt_handler import ParakeetTDTSTTHandler | |
| setup_kwargs = handler_kwargs or { | |
| "device": "mps", | |
| "enable_live_transcription": True, | |
| "live_transcription_update_interval": 0.25, | |
| } | |
| handler = ParakeetTDTSTTHandler( | |
| stop_event, | |
| queue_in=queue_in, | |
| queue_out=queue_out, | |
| setup_kwargs=setup_kwargs | |
| ) | |
| else: | |
| raise ValueError(f"Unknown handler: {handler_name}") | |
| # Warmup is done in handler setup | |
| logger.info(f"Handler {handler_name} initialized and warmed up") | |
| # Additional warmup on the actual audio (excluded from timings) | |
| for _ in handler.process(VADAudio(audio=audio)): | |
| pass | |
| # Run benchmark iterations | |
| for i in range(iterations): | |
| logger.info(f"Iteration {i+1}/{iterations} for {handler_name}") | |
| start_time = time.perf_counter() | |
| # Process audio | |
| transcription = None | |
| time_to_first_token = None | |
| first_output = True | |
| for output in handler.process(VADAudio(audio=audio)): | |
| # Measure time to first token | |
| if first_output: | |
| time_to_first_token = time.perf_counter() - start_time | |
| first_output = False | |
| if isinstance(output, tuple): | |
| transcription = output[0] # (text, language) | |
| else: | |
| transcription = output | |
| end_time = time.perf_counter() | |
| time_taken = end_time - start_time | |
| result.add_inference(time_taken, transcription, time_to_first_token) | |
| ttft_str = f", TTFT: {time_to_first_token:.4f}s" if time_to_first_token else "" | |
| text_preview = str(transcription)[:50] if transcription is not None else "(none)" | |
| logger.info(f" Time: {time_taken:.4f}s{ttft_str}, Text: {text_preview}...") | |
| # Cleanup | |
| handler.cleanup() | |
| stop_event.set() | |
| except Exception as e: | |
| logger.error(f"Error benchmarking {handler_name}: {e}", exc_info=True) | |
| result.add_error(str(e)) | |
| return result | |
| def print_results(results: List[BenchmarkResult]): | |
| """Print benchmark results in a formatted table.""" | |
| print("\n" + "="*80) | |
| print("BENCHMARK RESULTS") | |
| print("="*80) | |
| for result in results: | |
| stats = result.get_stats() | |
| print(f"\nHandler: {stats['handler']}") | |
| print("-" * 80) | |
| if stats.get("status") == "failed": | |
| print(" Status: FAILED") | |
| print(f" Errors: {stats['errors']}") | |
| continue | |
| print(f" Warmup Time: {stats['warmup_time']:.4f}s") | |
| print(f" Avg Inference Time: {stats['avg_inference_time']:.4f}s") | |
| print(f" Min Inference Time: {stats['min_inference_time']:.4f}s") | |
| print(f" Max Inference Time: {stats['max_inference_time']:.4f}s") | |
| print(f" Std Deviation: {stats['std_inference_time']:.4f}s") | |
| # Print time to first token stats if available | |
| if 'avg_time_to_first_token' in stats: | |
| print("\n Time to First Token:") | |
| print(f" Avg TTFT: {stats['avg_time_to_first_token']:.4f}s") | |
| print(f" Min TTFT: {stats['min_time_to_first_token']:.4f}s") | |
| print(f" Max TTFT: {stats['max_time_to_first_token']:.4f}s") | |
| print(f" Std TTFT: {stats['std_time_to_first_token']:.4f}s") | |
| print(f"\n Total Iterations: {stats['total_iterations']}") | |
| print(f" Sample Transcription: {stats['sample_transcription']}") | |
| if stats['errors']: | |
| print(f" Errors: {stats['errors']}") | |
| # Comparison table | |
| print("\n" + "="*80) | |
| print("COMPARISON (Average Inference Time)") | |
| print("="*80) | |
| successful_results = [r for r in results if r.inference_times] | |
| if successful_results: | |
| sorted_results = sorted(successful_results, key=lambda x: np.mean(x.inference_times)) | |
| fastest = sorted_results[0] | |
| fastest_time = np.mean(fastest.inference_times) | |
| for result in sorted_results: | |
| avg_time = np.mean(result.inference_times) | |
| speedup = avg_time / fastest_time | |
| print(f" {result.handler_name:25s}: {avg_time:.4f}s ({speedup:.2f}x slower than fastest)") | |
| def save_results(results: List[BenchmarkResult], output_file: str): | |
| """Save benchmark results to JSON file.""" | |
| data = { | |
| "results": [r.get_stats() for r in results], | |
| "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), | |
| } | |
| with open(output_file, 'w') as f: | |
| json.dump(data, f, indent=2) | |
| logger.info(f"Results saved to: {output_file}") | |
| def main(): | |
| parser = argparse.ArgumentParser(description="Benchmark STT handlers") | |
| parser.add_argument( | |
| "--audio_file", | |
| type=str, | |
| required=True, | |
| help="Path to audio file for benchmarking" | |
| ) | |
| parser.add_argument( | |
| "--handlers", | |
| nargs="+", | |
| default=["whisper", "whisper-mlx", "mlx-audio-whisper", "faster-whisper", "parakeet-tdt", "parakeet-tdt-progressive"], | |
| help="List of handlers to benchmark (default: all)" | |
| ) | |
| parser.add_argument( | |
| "--iterations", | |
| type=int, | |
| default=5, | |
| help="Number of iterations per handler (default: 5)" | |
| ) | |
| parser.add_argument( | |
| "--output", | |
| type=str, | |
| default="stt_benchmark_results.json", | |
| help="Output JSON file for results (default: stt_benchmark_results.json)" | |
| ) | |
| args = parser.parse_args() | |
| # Validate audio file exists | |
| if not Path(args.audio_file).exists(): | |
| logger.error(f"Audio file not found: {args.audio_file}") | |
| return | |
| # Load audio | |
| audio = load_audio(args.audio_file) | |
| logger.info(f"Audio loaded: {len(audio)} samples, {len(audio)/16000:.2f}s duration") | |
| # Run benchmarks | |
| results = [] | |
| for handler_name in args.handlers: | |
| result = benchmark_handler(handler_name, audio, args.iterations) | |
| results.append(result) | |
| # Print and save results | |
| print_results(results) | |
| save_results(results, args.output) | |
| logger.info("Benchmarking complete!") | |
| if __name__ == "__main__": | |
| main() | |