""" Utility functions for audio processing and API operations. """ import base64 import io import os import uuid from datetime import datetime from pathlib import Path from typing import Tuple, Optional import numpy as np import soundfile as sf def generate_request_id() -> str: """Generate a unique request ID.""" return f"req_{uuid.uuid4().hex[:12]}" def get_unix_timestamp() -> int: """Get current Unix timestamp.""" return int(datetime.utcnow().timestamp()) def audio_to_base64(audio: np.ndarray, sample_rate: int) -> str: """ Convert numpy audio array to base64-encoded WAV string. Args: audio: Audio waveform as numpy array sample_rate: Sample rate in Hz Returns: Base64-encoded audio data """ with io.BytesIO() as wav_buffer: sf.write(wav_buffer, audio, sample_rate, format='WAV') wav_bytes = wav_buffer.getvalue() return base64.b64encode(wav_bytes).decode('utf-8') def base64_to_audio(audio_base64: str) -> Tuple[np.ndarray, int]: """ Convert base64-encoded audio to numpy array. Args: audio_base64: Base64-encoded audio data Returns: Tuple of (audio waveform, sample_rate) """ wav_bytes = base64.b64decode(audio_base64) audio, sr = sf.read(io.BytesIO(wav_bytes), dtype='float32') return audio, int(sr) def get_audio_duration(audio: np.ndarray, sample_rate: int) -> float: """ Calculate audio duration in seconds. Args: audio: Audio waveform as numpy array sample_rate: Sample rate in Hz Returns: Duration in seconds """ return float(len(audio) / sample_rate) def load_voice_clone_files(voice_clone_dir: str) -> Tuple[Optional[str], Optional[str]]: """ Load ref_audio.wav and ref_text.txt from voice cloning directory. Args: voice_clone_dir: Path to voice cloning directory Returns: Tuple of (ref_audio_path, ref_text) or (None, None) if files not found """ voice_clone_path = Path(voice_clone_dir) ref_audio_path = voice_clone_path / "ref_audio.wav" ref_text_path = voice_clone_path / "ref_text.txt" if not ref_audio_path.exists() or not ref_text_path.exists(): return None, None with open(ref_text_path, 'r', encoding='utf-8') as f: ref_text = f.read().strip() return str(ref_audio_path), ref_text def validate_audio_file(file_path: str) -> Tuple[bool, str]: """ Validate that the audio file exists and is readable. Args: file_path: Path to audio file Returns: Tuple of (is_valid, message) """ path = Path(file_path) if not path.exists(): return False, f"File not found: {file_path}" if not path.is_file(): return False, f"Path is not a file: {file_path}" if path.suffix.lower() not in ['.wav', '.mp3', '.flac', '.ogg']: return False, f"Unsupported audio format: {path.suffix}" try: sf.read(file_path, frames=1) return True, "Valid audio file" except Exception as e: return False, f"Error reading audio file: {str(e)}" def concatenate_audio_chunks(chunks: list, sample_rate: int) -> Tuple[np.ndarray, int]: """ Concatenate multiple audio chunks into a single array. Args: chunks: List of audio chunks as numpy arrays sample_rate: Sample rate in Hz Returns: Tuple of (concatenated audio, sample_rate) """ if not chunks: return np.array([]), sample_rate return np.concatenate(chunks), sample_rate