| """ |
| Audio processing utilities for CritterCalm. |
| """ |
|
|
| import numpy as np |
| import soundfile as sf |
| from pathlib import Path |
| from typing import Optional, Tuple |
|
|
|
|
| def load_audio(path: str, target_sr: int = 24000) -> Tuple[np.ndarray, int]: |
| """Load audio file and resample if needed. Returns (samples, sample_rate).""" |
| samples, sr = sf.read(path) |
| if sr != target_sr: |
| import librosa |
| samples = librosa.resample(samples, orig_sr=sr, target_sr=target_sr) |
| sr = target_sr |
| |
| if samples.ndim > 1: |
| samples = samples.mean(axis=1) |
| return samples, sr |
|
|
|
|
| def save_audio( |
| samples: np.ndarray, |
| path: str, |
| sample_rate: int = 24000, |
| normalize: bool = True, |
| ) -> str: |
| """Save audio samples to file. Normalizes to prevent clipping.""" |
| if normalize: |
| peak = np.abs(samples).max() |
| if peak > 0.99: |
| samples = samples / peak * 0.95 |
| sf.write(path, samples, sample_rate) |
| return path |
|
|
|
|
| def get_audio_duration(path: str) -> float: |
| """Return duration of audio file in seconds.""" |
| samples, sr = sf.read(str(path)) |
| if samples.ndim > 1: |
| samples = samples.mean(axis=1) |
| return len(samples) / sr |
|
|
|
|
| def validate_voice_sample( |
| path: str, |
| min_duration: float = 3.0, |
| max_duration: float = 30.0, |
| ) -> Optional[str]: |
| """ |
| Validate a voice sample for cloning. |
| Returns None if valid, or an error message string. |
| """ |
| import os |
| if not os.path.exists(path): |
| return "Audio file not found." |
| try: |
| duration = get_audio_duration(path) |
| if duration < min_duration: |
| return ( |
| f"Recording is too short ({duration:.1f}s). " |
| f"Please record at least {min_duration:.0f} seconds." |
| ) |
| if duration > max_duration: |
| return ( |
| f"Recording is too long ({duration:.1f}s). " |
| f"Please keep it under {max_duration:.0f} seconds." |
| ) |
| return None |
| except Exception as exc: |
| return f"Could not read audio file: {exc}" |
|
|
|
|
| def estimate_tokens_per_second(text: str) -> float: |
| """ |
| Rough estimate of speech duration based on word count. |
| Average English speech is ~150 words per minute (2.5 wps). |
| Returns estimated seconds. |
| """ |
| words = len(text.split()) |
| return words / 2.5 |
|
|