Fix Python 3.13 numba and librosa compatibility handling Technical details: - Stop forcing NUMBA_DISABLE_JIT at startup on Python 3.13 - Use safe audio loading and resampling in IndexTTS-2 and ChatterBox Official 23-Lang - Update audio hashing and fallback paths for newer numba stacks
150 lines
6.9 KiB
Python
150 lines
6.9 KiB
Python
"""
|
|
Audio Content Hashing Utilities
|
|
|
|
Provides consistent content-based hashing for audio inputs across all TTS nodes
|
|
to prevent cache invalidation from temporary file paths.
|
|
"""
|
|
|
|
import hashlib
|
|
import os
|
|
from typing import Optional, Dict, Any
|
|
|
|
from utils.audio.librosa_fallback import safe_load
|
|
|
|
|
|
def generate_stable_audio_component(reference_audio: Optional[Dict[str, Any]] = None,
|
|
audio_file_path: Optional[str] = None) -> str:
|
|
"""
|
|
Generate stable audio component identifier for cache consistency.
|
|
|
|
Uses content-based hashing to ensure same audio content produces same cache key,
|
|
regardless of temporary file names or paths.
|
|
|
|
Args:
|
|
reference_audio: Direct audio input dict with 'waveform' and 'sample_rate'
|
|
audio_file_path: Path to audio file (from dropdown selection or direct input)
|
|
|
|
Returns:
|
|
Stable identifier string for cache key generation
|
|
"""
|
|
if reference_audio is not None:
|
|
# For direct audio input, hash the waveform data
|
|
try:
|
|
# Handle nested structure from Character Voices / Voice Designer
|
|
# Format: {"audio": {"waveform": tensor, "sample_rate": N}, ...}
|
|
if "audio" in reference_audio and isinstance(reference_audio["audio"], dict):
|
|
audio_dict = reference_audio["audio"]
|
|
else:
|
|
# Direct format: {"waveform": tensor, "sample_rate": N}
|
|
audio_dict = reference_audio
|
|
|
|
# Extract waveform tensor
|
|
waveform = audio_dict["waveform"]
|
|
sample_rate = audio_dict["sample_rate"]
|
|
|
|
# Check if waveform is actually a tensor
|
|
import torch
|
|
if not isinstance(waveform, torch.Tensor):
|
|
print(f"⚠️ Audio hash: waveform is {type(waveform)}, not tensor")
|
|
print(f" Keys in audio_dict: {list(audio_dict.keys())}")
|
|
print(f" Keys in reference_audio: {list(reference_audio.keys())}")
|
|
return "ref_audio_error_not_tensor"
|
|
|
|
# Hash the waveform
|
|
waveform_hash = hashlib.md5(waveform.cpu().numpy().tobytes()).hexdigest()
|
|
result = f"ref_audio_{waveform_hash}_{sample_rate}"
|
|
# print(f"🐛 AUDIO_HASH: Generated hash: {result}")
|
|
return result
|
|
except Exception as e:
|
|
print(f"⚠️ Failed to hash reference audio: {e}")
|
|
print(f" reference_audio type: {type(reference_audio)}")
|
|
print(f" reference_audio keys: {list(reference_audio.keys()) if isinstance(reference_audio, dict) else 'N/A'}")
|
|
# print(f"🐛 AUDIO_HASH: ERROR - returning 'ref_audio_error'")
|
|
return "ref_audio_error"
|
|
|
|
elif audio_file_path and audio_file_path != "none" and os.path.exists(audio_file_path):
|
|
# For file paths (dropdown selections or temp files), hash file content
|
|
try:
|
|
# print(f"🐛 AUDIO_HASH: Hashing file path: {audio_file_path}")
|
|
|
|
# Try multiple audio loading methods for robustness
|
|
waveform = None
|
|
sample_rate = None
|
|
|
|
# Method 1: Try soundfile first (more reliable, no numba dependencies)
|
|
try:
|
|
import soundfile as sf
|
|
waveform, sample_rate = sf.read(audio_file_path, dtype='float32')
|
|
# print(f"🐛 AUDIO_HASH: Loaded with soundfile")
|
|
except:
|
|
# Method 2: Try torchaudio (also reliable, minimal numba use)
|
|
try:
|
|
import warnings
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore", UserWarning) # Suppress torchaudio 2.9 warnings
|
|
import torchaudio
|
|
waveform_tensor, sample_rate = torchaudio.load(audio_file_path)
|
|
waveform = waveform_tensor.numpy().flatten()
|
|
# print(f"🐛 AUDIO_HASH: Loaded with torchaudio")
|
|
except:
|
|
# Method 3: Fall back to safe librosa-compatible loading
|
|
try:
|
|
import warnings
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore", UserWarning) # Suppress torchaudio warnings
|
|
|
|
waveform, sample_rate = safe_load(audio_file_path, sr=None, mono=True)
|
|
|
|
# print(f"🐛 AUDIO_HASH: Loaded with safe audio loader")
|
|
except Exception as librosa_error:
|
|
# If all methods fail, just hash the file path + modification time for uniqueness
|
|
file_stat = os.stat(audio_file_path)
|
|
path_hash = hashlib.md5(f"{audio_file_path}_{file_stat.st_mtime}_{file_stat.st_size}".encode()).hexdigest()
|
|
fallback = f"path_{path_hash}"
|
|
print(f"⚠️ Failed to load audio file for content hashing: {librosa_error}")
|
|
print(f"🔄 Using path+timestamp hash instead: {fallback}")
|
|
return fallback
|
|
|
|
if waveform is not None:
|
|
waveform_hash = hashlib.md5(waveform.tobytes()).hexdigest()
|
|
result = f"file_audio_{waveform_hash}_{sample_rate}"
|
|
# print(f"🐛 AUDIO_HASH: Generated file hash: {result}")
|
|
return result
|
|
else:
|
|
raise Exception("Failed to load audio with all methods")
|
|
|
|
except Exception as e:
|
|
# Enhanced fallback using file metadata for better cache consistency
|
|
try:
|
|
file_stat = os.stat(audio_file_path)
|
|
path_hash = hashlib.md5(f"{audio_file_path}_{file_stat.st_mtime}_{file_stat.st_size}".encode()).hexdigest()
|
|
fallback = f"path_{path_hash}"
|
|
print(f"⚠️ Audio content hashing failed, using file metadata hash: {fallback}")
|
|
return fallback
|
|
except:
|
|
# Final fallback - just use filename
|
|
fallback = f"path_{os.path.basename(audio_file_path)}"
|
|
print(f"⚠️ Failed to hash audio file {audio_file_path}: {e}, using path fallback")
|
|
return fallback
|
|
|
|
else:
|
|
# No voice file (default voice)
|
|
# print(f"🐛 AUDIO_HASH: No audio provided - using 'default_voice'")
|
|
return "default_voice"
|
|
|
|
|
|
def get_stable_audio_component_for_cache(inputs: Dict[str, Any]) -> str:
|
|
"""
|
|
Convenience function to get stable audio component from node inputs.
|
|
|
|
Args:
|
|
inputs: Node inputs dict containing 'reference_audio' and/or 'audio_prompt_path'
|
|
|
|
Returns:
|
|
Stable identifier for cache key
|
|
"""
|
|
return generate_stable_audio_component(
|
|
reference_audio=inputs.get("reference_audio"),
|
|
audio_file_path=inputs.get("audio_prompt_path", "")
|
|
)
|