209 lines
6.7 KiB
Python
209 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Audio quality analysis tool for VoxCPM generated files
|
|
Analyzes waveform characteristics to determine if audio sounds human
|
|
"""
|
|
|
|
import os
|
|
import numpy as np
|
|
import soundfile as sf
|
|
import matplotlib.pyplot as plt
|
|
from scipy import signal
|
|
from scipy.stats import skew, kurtosis
|
|
|
|
def analyze_audio_file(file_path):
|
|
"""Analyze audio file and return quality metrics"""
|
|
if not os.path.exists(file_path):
|
|
print(f"File not found: {file_path}")
|
|
return None
|
|
|
|
try:
|
|
# Read audio file
|
|
audio_data, sample_rate = sf.read(file_path)
|
|
print(f"✓ Successfully loaded: {os.path.basename(file_path)}")
|
|
print(f" Sample rate: {sample_rate} Hz")
|
|
print(f" Duration: {len(audio_data)/sample_rate:.2f} seconds")
|
|
print(f" Channels: {1 if len(audio_data.shape) == 1 else audio_data.shape[1]}")
|
|
|
|
# Convert to mono if stereo
|
|
if len(audio_data.shape) > 1:
|
|
audio_data = np.mean(audio_data, axis=1)
|
|
|
|
# Basic audio statistics
|
|
rms_energy = np.sqrt(np.mean(audio_data**2))
|
|
peak_amplitude = np.max(np.abs(audio_data))
|
|
zero_crossing_rate = np.mean(np.abs(np.diff(np.sign(audio_data))))
|
|
spectral_centroid = calculate_spectral_centroid(audio_data, sample_rate)
|
|
skewness = skew(audio_data)
|
|
kurt = kurtosis(audio_data)
|
|
|
|
print(f"\n📊 Audio Statistics:")
|
|
print(f" RMS Energy: {rms_energy:.4f}")
|
|
print(f" Peak Amplitude: {peak_amplitude:.4f}")
|
|
print(f" Zero Crossing Rate: {zero_crossing_rate:.4f}")
|
|
print(f" Spectral Centroid: {spectral_centroid:.2f} Hz")
|
|
print(f" Skewness: {skewness:.4f}")
|
|
print(f" Kurtosis: {kurt:.4f}")
|
|
|
|
# Quality assessment
|
|
quality_score = assess_audio_quality({
|
|
'rms_energy': rms_energy,
|
|
'zero_crossing_rate': zero_crossing_rate,
|
|
'spectral_centroid': spectral_centroid,
|
|
'skewness': skewness,
|
|
'kurtosis': kurt,
|
|
'duration': len(audio_data)/sample_rate
|
|
})
|
|
|
|
return {
|
|
'file': file_path,
|
|
'sample_rate': sample_rate,
|
|
'duration': len(audio_data)/sample_rate,
|
|
'rms_energy': rms_energy,
|
|
'zero_crossing_rate': zero_crossing_rate,
|
|
'spectral_centroid': spectral_centroid,
|
|
'quality_score': quality_score,
|
|
'quality': 'good' if quality_score > 60 else 'poor'
|
|
}
|
|
|
|
except Exception as e:
|
|
print(f"Error analyzing {file_path}: {e}")
|
|
return None
|
|
|
|
def calculate_spectral_centroid(audio_data, sample_rate):
|
|
"""Calculate spectral centroid (brightness of sound)"""
|
|
# Compute spectrogram
|
|
frequencies, times, Sxx = signal.spectrogram(audio_data, sample_rate)
|
|
|
|
# Calculate spectral centroid
|
|
if np.sum(Sxx) == 0:
|
|
return 0
|
|
|
|
spectral_centroid = np.sum(frequencies[:, np.newaxis] * Sxx) / np.sum(Sxx)
|
|
return spectral_centroid
|
|
|
|
def assess_audio_quality(metrics):
|
|
"""Assess audio quality based on metrics"""
|
|
score = 0
|
|
|
|
# RMS Energy: Good range for speech is 0.05-0.3
|
|
rms = metrics['rms_energy']
|
|
if 0.05 <= rms <= 0.3:
|
|
score += 20
|
|
elif 0.02 <= rms < 0.05 or 0.3 < rms <= 0.5:
|
|
score += 10
|
|
else:
|
|
score += 0
|
|
|
|
# Zero Crossing Rate: Good range for speech is 0.05-0.15
|
|
zcr = metrics['zero_crossing_rate']
|
|
if 0.05 <= zcr <= 0.15:
|
|
score += 20
|
|
elif 0.02 <= zcr < 0.05 or 0.15 < zcr <= 0.2:
|
|
score += 10
|
|
else:
|
|
score += 0
|
|
|
|
# Spectral Centroid: Good range for speech is 800-2500 Hz
|
|
sc = metrics['spectral_centroid']
|
|
if 800 <= sc <= 2500:
|
|
score += 20
|
|
elif 500 <= sc < 800 or 2500 < sc <= 3500:
|
|
score += 10
|
|
else:
|
|
score += 0
|
|
|
|
# Duration: Speech should be reasonable length
|
|
duration = metrics['duration']
|
|
if 1.0 <= duration <= 10.0:
|
|
score += 20
|
|
elif 0.5 <= duration < 1.0 or 10.0 < duration <= 15.0:
|
|
score += 10
|
|
else:
|
|
score += 0
|
|
|
|
# Skewness and Kurtosis: Should be moderate for natural speech
|
|
skewness = abs(metrics['skewness'])
|
|
kurtosis = abs(metrics['kurtosis'])
|
|
if skewness < 2 and kurtosis < 10:
|
|
score += 20
|
|
elif skewness < 5 and kurtosis < 20:
|
|
score += 10
|
|
else:
|
|
score += 0
|
|
|
|
return score
|
|
|
|
def analyze_directory(directory):
|
|
"""Analyze all audio files in a directory"""
|
|
if not os.path.exists(directory):
|
|
print(f"Directory not found: {directory}")
|
|
return
|
|
|
|
print(f"\n{'='*60}")
|
|
print(f"ANALYZING AUDIO FILES IN: {directory}")
|
|
print(f"{'='*60}")
|
|
|
|
audio_files = [f for f in os.listdir(directory) if f.endswith('.wav')]
|
|
|
|
if not audio_files:
|
|
print("No WAV files found")
|
|
return
|
|
|
|
results = []
|
|
for audio_file in audio_files:
|
|
file_path = os.path.join(directory, audio_file)
|
|
result = analyze_audio_file(file_path)
|
|
if result:
|
|
results.append(result)
|
|
print(f" Quality Score: {result['quality_score']}/100 ({result['quality']})")
|
|
print(f"{'='*60}")
|
|
|
|
# Summary
|
|
if results:
|
|
good_files = [r['file'] for r in results if r['quality'] == 'good']
|
|
poor_files = [r['file'] for r in results if r['quality'] == 'poor']
|
|
|
|
print(f"\n📋 Summary:")
|
|
print(f"Total files analyzed: {len(results)}")
|
|
print(f"Good quality files: {len(good_files)}")
|
|
print(f"Poor quality files: {len(poor_files)}")
|
|
|
|
if good_files:
|
|
print("\nGood quality examples:")
|
|
for f in good_files[:3]:
|
|
print(f" - {os.path.basename(f)}")
|
|
|
|
if poor_files:
|
|
print("\nPoor quality examples:")
|
|
for f in poor_files[:3]:
|
|
print(f" - {os.path.basename(f)}")
|
|
|
|
if __name__ == "__main__":
|
|
# Analyze both accent demo directories
|
|
analyze_directory("accent_demos")
|
|
analyze_directory("accent_demos_optimized")
|
|
|
|
# Also analyze the reference audio files
|
|
print(f"\n{'='*60}")
|
|
print(f"ANALYZING REFERENCE AUDIO FILES")
|
|
print(f"{'='*60}")
|
|
|
|
reference_files = [
|
|
"reference_indian.wav",
|
|
"reference_russian.wav",
|
|
"reference_singaporean.wav",
|
|
"reference_hongkong.wav",
|
|
"reference_cantonese.wav",
|
|
"reference_indian_opt.wav",
|
|
"reference_russian_opt.wav",
|
|
"reference_singaporean_opt.wav",
|
|
"reference_hongkong_opt.wav",
|
|
"reference_cantonese_opt.wav"
|
|
]
|
|
|
|
for ref_file in reference_files:
|
|
if os.path.exists(ref_file):
|
|
analyze_audio_file(ref_file)
|
|
print(f"{'='*60}")
|