Spaces:
Sleeping
Sleeping
| """ | |
| TTS Benchmarking Script | |
| Benchmarks Text-to-Speech (TTS) handlers to compare performance. | |
| Measures: warmup time, inference time, time-to-first-chunk, audio duration, and RTF. | |
| Usage: | |
| python benchmark_tts.py --text "Hello world" --iterations 3 | |
| python benchmark_tts.py --handlers kokoro qwen3 pocket_tts | |
| """ | |
| import argparse | |
| import json | |
| import logging | |
| import time | |
| from queue import Queue | |
| from threading import Event | |
| from typing import Any, Dict, List, Optional | |
| import numpy as np | |
| from speech_to_speech.pipeline.messages import TTSInput | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", | |
| ) | |
| logger = logging.getLogger(__name__) | |
| DEFAULT_SAMPLE_RATE = 16000 | |
| VALID_QWEN3_MLX_QUANTIZATIONS = ("bf16", "4bit", "6bit", "8bit") | |
| class BenchmarkResult: | |
| """Stores benchmark results for a single TTS 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_chunk: list[float] = [] | |
| self.audio_durations: list[float] = [] | |
| self.errors: list[str] = [] | |
| def add_inference(self, time_taken: float, audio_duration: float, ttfc: Optional[float] = None): | |
| self.inference_times.append(time_taken) | |
| self.audio_durations.append(audio_duration) | |
| if ttfc is not None: | |
| self.time_to_first_chunk.append(ttfc) | |
| def add_error(self, error: str): | |
| self.errors.append(error) | |
| def get_stats(self) -> Dict[str, Any]: | |
| if not self.inference_times: | |
| return { | |
| "handler": self.handler_name, | |
| "status": "failed", | |
| "errors": self.errors, | |
| } | |
| avg_time = float(np.mean(self.inference_times)) | |
| avg_audio = float(np.mean(self.audio_durations)) | |
| avg_rtf = avg_audio / avg_time if avg_time > 0 else 0.0 | |
| stats = { | |
| "handler": self.handler_name, | |
| "warmup_time": self.warmup_time, | |
| "avg_inference_time": avg_time, | |
| "min_inference_time": float(np.min(self.inference_times)), | |
| "max_inference_time": float(np.max(self.inference_times)), | |
| "std_inference_time": float(np.std(self.inference_times)), | |
| "avg_audio_duration": avg_audio, | |
| "min_audio_duration": float(np.min(self.audio_durations)), | |
| "max_audio_duration": float(np.max(self.audio_durations)), | |
| "std_audio_duration": float(np.std(self.audio_durations)), | |
| "avg_rtf": avg_rtf, | |
| "total_iterations": len(self.inference_times), | |
| "errors": self.errors, | |
| } | |
| if self.time_to_first_chunk: | |
| stats["avg_time_to_first_chunk"] = float(np.mean(self.time_to_first_chunk)) | |
| stats["min_time_to_first_chunk"] = float(np.min(self.time_to_first_chunk)) | |
| stats["max_time_to_first_chunk"] = float(np.max(self.time_to_first_chunk)) | |
| stats["std_time_to_first_chunk"] = float(np.std(self.time_to_first_chunk)) | |
| return stats | |
| def benchmark_handler( | |
| handler_name: str, | |
| text: str, | |
| iterations: int, | |
| handler_kwargs: Optional[Dict[str, Any]] = None, | |
| language_code: Optional[str] = "en", | |
| ) -> BenchmarkResult: | |
| logger.info(f"Benchmarking {handler_name}...") | |
| result = BenchmarkResult(handler_name) | |
| try: | |
| stop_event = Event() | |
| should_listen = Event() | |
| queue_in: Queue[Any] = Queue() | |
| queue_out: Queue[Any] = Queue() | |
| handler: Any = None | |
| setup_kwargs = handler_kwargs or {} | |
| start_setup = time.perf_counter() | |
| if handler_name == "kokoro": | |
| from speech_to_speech.TTS.kokoro_handler import KokoroTTSHandler | |
| setup_kwargs = {"device": "auto", **setup_kwargs} | |
| handler = KokoroTTSHandler( | |
| stop_event, | |
| queue_in=queue_in, | |
| queue_out=queue_out, | |
| setup_args=(should_listen,), | |
| setup_kwargs=setup_kwargs, | |
| ) | |
| elif handler_name == "pocket_tts": | |
| from speech_to_speech.TTS.pocket_tts_handler import PocketTTSHandler | |
| setup_kwargs = {"device": "cpu", **setup_kwargs} | |
| handler = PocketTTSHandler( | |
| stop_event, | |
| queue_in=queue_in, | |
| queue_out=queue_out, | |
| setup_args=(should_listen,), | |
| setup_kwargs=setup_kwargs, | |
| ) | |
| elif handler_name == "qwen3": | |
| from speech_to_speech.TTS.qwen3_tts_handler import Qwen3TTSHandler | |
| setup_kwargs = { | |
| "device": "cuda", | |
| "model_name": "Qwen/Qwen3-TTS-12Hz-0.6B-Base", | |
| "ref_audio": "TTS/ref_audio.wav", | |
| **setup_kwargs, | |
| } | |
| handler = Qwen3TTSHandler( | |
| stop_event, | |
| queue_in=queue_in, | |
| queue_out=queue_out, | |
| setup_args=(should_listen,), | |
| setup_kwargs=setup_kwargs, | |
| ) | |
| elif handler_name == "chatTTS": | |
| from speech_to_speech.TTS.chatTTS_handler import ChatTTSHandler | |
| setup_kwargs = {"device": "cuda", **setup_kwargs} | |
| handler = ChatTTSHandler( | |
| stop_event, | |
| queue_in=queue_in, | |
| queue_out=queue_out, | |
| setup_args=(should_listen,), | |
| setup_kwargs=setup_kwargs, | |
| ) | |
| elif handler_name == "facebookMMS": | |
| from speech_to_speech.TTS.facebookmms_handler import FacebookMMSTTSHandler | |
| setup_kwargs = {"device": "cuda", "language": "en", **setup_kwargs} | |
| handler = FacebookMMSTTSHandler( | |
| stop_event, | |
| queue_in=queue_in, | |
| queue_out=queue_out, | |
| setup_args=(should_listen,), | |
| setup_kwargs=setup_kwargs, | |
| ) | |
| else: | |
| raise ValueError(f"Unknown handler: {handler_name}") | |
| result.warmup_time = time.perf_counter() - start_setup | |
| logger.info(f"Handler {handler_name} initialized and warmed up in {result.warmup_time:.3f}s") | |
| for i in range(iterations): | |
| logger.info(f"Iteration {i+1}/{iterations} for {handler_name}") | |
| start_time = time.perf_counter() | |
| time_to_first_chunk = None | |
| first_output = True | |
| total_samples = 0 | |
| tts_input = TTSInput(text=text, language_code=language_code) | |
| for chunk in handler.process(tts_input): | |
| if first_output: | |
| time_to_first_chunk = time.perf_counter() - start_time | |
| first_output = False | |
| if chunk is None: | |
| continue | |
| try: | |
| total_samples += len(chunk) | |
| except Exception: | |
| pass | |
| end_time = time.perf_counter() | |
| time_taken = end_time - start_time | |
| audio_duration = total_samples / DEFAULT_SAMPLE_RATE if total_samples > 0 else 0.0 | |
| result.add_inference(time_taken, audio_duration, time_to_first_chunk) | |
| ttfc_str = f", TTFC: {time_to_first_chunk:.4f}s" if time_to_first_chunk else "" | |
| logger.info( | |
| f" Time: {time_taken:.4f}s{ttfc_str}, Audio: {audio_duration:.2f}s, RTF: {audio_duration / time_taken if time_taken > 0 else 0:.2f}" | |
| ) | |
| 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 normalize_qwen3_mlx_quantizations(values: List[str] | None) -> List[str]: | |
| if not values: | |
| return [] | |
| normalized = [] | |
| seen = set() | |
| for value in values: | |
| quantization = str(value).strip().lower() | |
| if quantization in ("default", "none", ""): | |
| quantization = "bf16" | |
| if quantization not in VALID_QWEN3_MLX_QUANTIZATIONS: | |
| raise ValueError( | |
| "Unsupported qwen3 MLX quantization " | |
| f"{value!r}. Supported values: {', '.join(VALID_QWEN3_MLX_QUANTIZATIONS)}" | |
| ) | |
| if quantization in seen: | |
| continue | |
| seen.add(quantization) | |
| normalized.append(quantization) | |
| return normalized | |
| def build_benchmark_targets(args) -> List[tuple[str, str, Dict[str, Any]]]: | |
| targets = [] | |
| qwen3_quantizations = normalize_qwen3_mlx_quantizations(args.qwen3_mlx_quantizations) | |
| for handler_name in args.handlers: | |
| if handler_name == "qwen3" and qwen3_quantizations: | |
| for quantization in qwen3_quantizations: | |
| targets.append( | |
| ( | |
| f"qwen3[{quantization}]", | |
| "qwen3", | |
| {"mlx_quantization": quantization}, | |
| ) | |
| ) | |
| continue | |
| targets.append((handler_name, handler_name, {})) | |
| return targets | |
| def print_results(results: List[BenchmarkResult]): | |
| print("\n" + "=" * 80) | |
| print("TTS 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(f" Avg Audio Duration: {stats['avg_audio_duration']:.2f}s") | |
| print(f" Min Audio Duration: {stats['min_audio_duration']:.2f}s") | |
| print(f" Max Audio Duration: {stats['max_audio_duration']:.2f}s") | |
| print(f" Std Audio Duration: {stats['std_audio_duration']:.4f}s") | |
| print(f" Avg RTF: {stats['avg_rtf']:.2f}") | |
| if "avg_time_to_first_chunk" in stats: | |
| print("\n Time to First Chunk:") | |
| print(f" Avg TTFC: {stats['avg_time_to_first_chunk']:.4f}s") | |
| print(f" Min TTFC: {stats['min_time_to_first_chunk']:.4f}s") | |
| print(f" Max TTFC: {stats['max_time_to_first_chunk']:.4f}s") | |
| print(f" Std TTFC: {stats['std_time_to_first_chunk']:.4f}s") | |
| print(f"\n Total Iterations: {stats['total_iterations']}") | |
| if stats["errors"]: | |
| print(f" Errors: {stats['errors']}") | |
| 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): | |
| 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 TTS handlers") | |
| parser.add_argument( | |
| "--text", | |
| type=str, | |
| default="Hello from the speech to speech benchmark. This is a latency test.", | |
| help="Text to synthesize", | |
| ) | |
| parser.add_argument( | |
| "--handlers", | |
| nargs="+", | |
| default=["kokoro", "qwen3", "pocket_tts"], | |
| help="List of handlers to benchmark (kokoro, qwen3, pocket_tts, chatTTS, facebookMMS)", | |
| ) | |
| parser.add_argument( | |
| "--iterations", | |
| type=int, | |
| default=3, | |
| help="Number of iterations per handler (default: 3)", | |
| ) | |
| parser.add_argument( | |
| "--output", | |
| type=str, | |
| default="tts_benchmark_results.json", | |
| help="Output JSON file for results (default: tts_benchmark_results.json)", | |
| ) | |
| parser.add_argument( | |
| "--language_code", | |
| type=str, | |
| default="en", | |
| help="Language code to pass to TTS handlers (default: en)", | |
| ) | |
| parser.add_argument( | |
| "--qwen3_mlx_quantizations", | |
| nargs="+", | |
| default=None, | |
| help=( | |
| "Optional list of Apple Silicon MLX Qwen3-TTS quantizations to benchmark " | |
| "as separate variants. Supported values: bf16, 4bit, 6bit, 8bit." | |
| ), | |
| ) | |
| args = parser.parse_args() | |
| if not args.handlers: | |
| logger.error("No handlers provided") | |
| return | |
| try: | |
| targets = build_benchmark_targets(args) | |
| except ValueError as e: | |
| logger.error(str(e)) | |
| return | |
| results = [] | |
| for result_name, handler_name, handler_kwargs in targets: | |
| result = benchmark_handler( | |
| handler_name, | |
| args.text, | |
| args.iterations, | |
| handler_kwargs=handler_kwargs, | |
| language_code=args.language_code, | |
| ) | |
| result.handler_name = result_name | |
| results.append(result) | |
| print_results(results) | |
| save_results(results, args.output) | |
| logger.info("TTS benchmarking complete!") | |
| if __name__ == "__main__": | |
| main() | |