mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-19 14:50:38 -07:00
Initialize voicebox project with backend, frontend, and Tauri setup. Added configuration files, dependencies, and basic structure for components, hooks, and utilities. Included README and setup documentation for guidance.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# Utils package
|
||||
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
Audio processing utilities.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
import librosa
|
||||
from typing import Tuple, Optional
|
||||
|
||||
|
||||
def normalize_audio(
|
||||
audio: np.ndarray,
|
||||
target_db: float = -20.0,
|
||||
peak_limit: float = 0.85,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Normalize audio to target loudness with peak limiting.
|
||||
|
||||
Args:
|
||||
audio: Input audio array
|
||||
target_db: Target RMS level in dB
|
||||
peak_limit: Peak limit (0.0-1.0)
|
||||
|
||||
Returns:
|
||||
Normalized audio array
|
||||
"""
|
||||
# Convert to float32
|
||||
audio = audio.astype(np.float32)
|
||||
|
||||
# Calculate current RMS
|
||||
rms = np.sqrt(np.mean(audio**2))
|
||||
|
||||
# Calculate target RMS
|
||||
target_rms = 10**(target_db / 20)
|
||||
|
||||
# Apply gain
|
||||
if rms > 0:
|
||||
gain = target_rms / rms
|
||||
audio = audio * gain
|
||||
|
||||
# Peak limiting
|
||||
audio = np.clip(audio, -peak_limit, peak_limit)
|
||||
|
||||
return audio
|
||||
|
||||
|
||||
def load_audio(
|
||||
path: str,
|
||||
sample_rate: int = 24000,
|
||||
mono: bool = True,
|
||||
) -> Tuple[np.ndarray, int]:
|
||||
"""
|
||||
Load audio file with normalization.
|
||||
|
||||
Args:
|
||||
path: Path to audio file
|
||||
sample_rate: Target sample rate
|
||||
mono: Convert to mono
|
||||
|
||||
Returns:
|
||||
Tuple of (audio_array, sample_rate)
|
||||
"""
|
||||
audio, sr = librosa.load(path, sr=sample_rate, mono=mono)
|
||||
return audio, sr
|
||||
|
||||
|
||||
def save_audio(
|
||||
audio: np.ndarray,
|
||||
path: str,
|
||||
sample_rate: int = 24000,
|
||||
) -> None:
|
||||
"""
|
||||
Save audio file.
|
||||
|
||||
Args:
|
||||
audio: Audio array
|
||||
path: Output path
|
||||
sample_rate: Sample rate
|
||||
"""
|
||||
sf.write(path, audio, sample_rate)
|
||||
|
||||
|
||||
def validate_reference_audio(
|
||||
audio_path: str,
|
||||
min_duration: float = 2.0,
|
||||
max_duration: float = 30.0,
|
||||
min_rms: float = 0.01,
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate reference audio for voice cloning.
|
||||
|
||||
Args:
|
||||
audio_path: Path to audio file
|
||||
min_duration: Minimum duration in seconds
|
||||
max_duration: Maximum duration in seconds
|
||||
min_rms: Minimum RMS level
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
"""
|
||||
try:
|
||||
audio, sr = load_audio(audio_path)
|
||||
duration = len(audio) / sr
|
||||
|
||||
if duration < min_duration:
|
||||
return False, f"Audio too short (minimum {min_duration} seconds)"
|
||||
if duration > max_duration:
|
||||
return False, f"Audio too long (maximum {max_duration} seconds)"
|
||||
|
||||
rms = np.sqrt(np.mean(audio**2))
|
||||
if rms < min_rms:
|
||||
return False, "Audio is too quiet or silent"
|
||||
|
||||
if np.abs(audio).max() > 0.99:
|
||||
return False, "Audio is clipping (reduce input gain)"
|
||||
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, f"Error validating audio: {str(e)}"
|
||||
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Voice prompt caching utilities.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import torch
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
import soundfile as sf
|
||||
|
||||
|
||||
_cache_dir = Path("data/cache")
|
||||
_cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# In-memory cache
|
||||
_memory_cache: dict[str, torch.Tensor] = {}
|
||||
|
||||
|
||||
def get_cache_key(audio_path: str, reference_text: str) -> str:
|
||||
"""
|
||||
Generate cache key from audio file and reference text.
|
||||
|
||||
Args:
|
||||
audio_path: Path to audio file
|
||||
reference_text: Reference text
|
||||
|
||||
Returns:
|
||||
Cache key (MD5 hash)
|
||||
"""
|
||||
# Read audio file
|
||||
with open(audio_path, "rb") as f:
|
||||
audio_bytes = f.read()
|
||||
|
||||
# Combine audio bytes and text
|
||||
combined = audio_bytes + reference_text.encode("utf-8")
|
||||
|
||||
# Generate hash
|
||||
return hashlib.md5(combined).hexdigest()
|
||||
|
||||
|
||||
def get_cached_voice_prompt(
|
||||
cache_key: str,
|
||||
) -> Optional[torch.Tensor]:
|
||||
"""
|
||||
Get cached voice prompt if available.
|
||||
|
||||
Args:
|
||||
cache_key: Cache key
|
||||
|
||||
Returns:
|
||||
Cached voice prompt tensor or None
|
||||
"""
|
||||
# Check in-memory cache
|
||||
if cache_key in _memory_cache:
|
||||
return _memory_cache[cache_key]
|
||||
|
||||
# Check disk cache
|
||||
cache_file = _cache_dir / f"{cache_key}.prompt"
|
||||
if cache_file.exists():
|
||||
try:
|
||||
prompt = torch.load(cache_file)
|
||||
_memory_cache[cache_key] = prompt
|
||||
return prompt
|
||||
except Exception:
|
||||
# Cache file corrupted, delete it
|
||||
cache_file.unlink()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def cache_voice_prompt(
|
||||
cache_key: str,
|
||||
voice_prompt: torch.Tensor,
|
||||
) -> None:
|
||||
"""
|
||||
Cache voice prompt to memory and disk.
|
||||
|
||||
Args:
|
||||
cache_key: Cache key
|
||||
voice_prompt: Voice prompt tensor
|
||||
"""
|
||||
# Store in memory
|
||||
_memory_cache[cache_key] = voice_prompt
|
||||
|
||||
# Store on disk
|
||||
cache_file = _cache_dir / f"{cache_key}.prompt"
|
||||
torch.save(voice_prompt, cache_file)
|
||||
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Input validation utilities.
|
||||
"""
|
||||
|
||||
from typing import Tuple, Optional
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def validate_text(text: str, max_length: int = 5000) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate text input.
|
||||
|
||||
Args:
|
||||
text: Text to validate
|
||||
max_length: Maximum length
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
"""
|
||||
if not text or not text.strip():
|
||||
return False, "Text cannot be empty"
|
||||
|
||||
if len(text) > max_length:
|
||||
return False, f"Text too long (maximum {max_length} characters)"
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
def validate_language(language: str) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate language code.
|
||||
|
||||
Args:
|
||||
language: Language code
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
"""
|
||||
valid_languages = ["en", "zh"]
|
||||
if language not in valid_languages:
|
||||
return False, f"Invalid language (must be one of: {', '.join(valid_languages)})"
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
def validate_file_path(path: str) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate file path exists.
|
||||
|
||||
Args:
|
||||
path: File path
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
"""
|
||||
file_path = Path(path)
|
||||
if not file_path.exists():
|
||||
return False, f"File not found: {path}"
|
||||
|
||||
if not file_path.is_file():
|
||||
return False, f"Path is not a file: {path}"
|
||||
|
||||
return True, None
|
||||
Reference in New Issue
Block a user