from __future__ import annotations import base64 import io import pathlib import shutil import struct import sys import wave import aiofiles import aiohttp from typing import Optional import asyncio async def synthesize_and_play_audio( input_text: str = "Hello!", reference_audio_path: str = "~/Downloads/jmsample.wav", model: str = "voxtral-mini-tts-260213", api_key: str = "MISTRAL_API_KEY", output_path: str = "/tmp/voxtral.wav", timeout: float = 180.0, raw_sample_rate: int = 24000, raw_channels: int = 1, no_play: bool = False, url: str = "https://api.mistral.ai/v1/audio/text-to-speech", reference_format: str = "raw-base64", ) -> int: """ Asynchronously synthesize speech from input text using a reference audio file and play the output. Args: input_text: Text to synthesize. reference_audio_path: Path to the reference WAV file. model: Model name sent in the request. api_key: API key for authentication. output_path: Output audio file path. timeout: HTTP timeout in seconds. raw_sample_rate: Sample rate used when response audio is raw f32le (non-WAV). raw_channels: Channel count used when response audio is raw f32le (non-WAV). no_play: If True, only save audio, do not launch a player. url: TTS endpoint URL. reference_format: How to serialize reference_audio ("data-uri" or "raw-base64"). Returns: int: 0 on success, non-zero on failure. """ print(f"Synthesizing: {input_text!r}") try: # Use async file operations reference_path = pathlib.Path(reference_audio_path).expanduser().resolve() if not reference_path.is_file(): print(f"Reference audio not found: {reference_path}", file=sys.stderr) return 2 # Read reference audio asynchronously async with aiofiles.open(reference_path, 'rb') as f: reference_bytes = await f.read() reference_b64 = base64.b64encode(reference_bytes).decode("ascii") if reference_format == "data-uri": reference_audio = f"data:audio/wav;base64,{reference_b64}" else: reference_audio = reference_b64 payload = { "model": model, "input": input_text, "reference_audio": reference_audio, "response_format": "wav", } headers = { "content-type": "application/json", "x-api-key": api_key, } # Use async HTTP client async with aiohttp.ClientSession() as session: try: async with session.post(url, headers=headers, json=payload, timeout=timeout) as response: if response.status >= 400: error_text = await response.text() print(f"Request failed: {response.status}", file=sys.stderr) print(error_text, file=sys.stderr) return 1 content_type = response.headers.get("content-type", "") audio_bytes = await response.read() if "application/json" in content_type.lower(): body = await response.json() audio_field = body.get("audio") if isinstance(body, dict) else None if not isinstance(audio_field, str) or not audio_field: print("JSON response does not contain an 'audio' field.", file=sys.stderr) print(body, file=sys.stderr) return 1 if "," in audio_field and audio_field.startswith("data:"): audio_field = audio_field.split(",", 1)[1] try: audio_bytes = base64.b64decode(audio_field, validate=False) except Exception as exc: print(f"Failed to decode JSON audio field as base64: {exc}", file=sys.stderr) return 1 if not _looks_like_wav(audio_bytes): converted = _convert_f32le_to_wav( audio_bytes, sample_rate=raw_sample_rate, channels=raw_channels, ) if converted is not None: audio_bytes = converted print( "Response was non-WAV raw audio; converted f32le stream to WAV.", file=sys.stderr, ) else: print( "Response bytes are non-WAV and could not be auto-converted.", file=sys.stderr, ) # Write output file asynchronously output_path_obj = pathlib.Path(output_path).expanduser() async with aiofiles.open(output_path_obj, 'wb') as f: await f.write(audio_bytes) print(f"Wrote {len(audio_bytes)} bytes to {output_path_obj}") if not no_play: await maybe_play_audio_async(output_path_obj) return 0 except asyncio.TimeoutError: print(f"Request timed out after {timeout} seconds", file=sys.stderr) return 1 except aiohttp.ClientError as e: print(f"HTTP client error: {e}", file=sys.stderr) return 1 except Exception as e: print(f"Unexpected error: {e}", file=sys.stderr) return 1 def _looks_like_wav(audio_bytes: bytes) -> bool: return len(audio_bytes) >= 12 and audio_bytes[:4] == b"RIFF" and audio_bytes[8:12] == b"WAVE" def _convert_f32le_to_wav(audio_bytes: bytes, sample_rate: int, channels: int) -> Optional[bytes]: if len(audio_bytes) % 4 != 0: return None if channels <= 0 or sample_rate <= 0: return None pcm16 = bytearray() for (sample,) in struct.iter_unpack(" None: """Asynchronously play audio using available players.""" players = ( ("afplay", [str(path)]), ("ffplay", ["-nodisp", "-autoexit", str(path)]), ("mpv", [str(path)]), ) for command, extra_args in players: if shutil.which(command) is None: continue try: proc = await asyncio.create_subprocess_exec( command, *extra_args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) await proc.wait() if proc.returncode == 0: return print( f"Player '{command}' failed with exit code {proc.returncode}, trying next player.", file=sys.stderr, ) except Exception as e: print(f"Error with player '{command}': {e}", file=sys.stderr) continue print("No local audio player found (afplay/ffplay/mpv).", file=sys.stderr)