UdasriHasindu commited on
Commit ·
586d5d0
1
Parent(s): 9900500
implement voice extraction features
Browse files- utils/__init__.py +0 -0
- utils/file_handler.py +53 -0
- utils/voice_data_extraction.py +120 -0
utils/__init__.py
ADDED
|
File without changes
|
utils/file_handler.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tempfile
|
| 2 |
+
import os
|
| 3 |
+
import subprocess
|
| 4 |
+
|
| 5 |
+
async def save_temp_file(upload_file):
|
| 6 |
+
# Get the original file extension from content type or filename
|
| 7 |
+
content_type = upload_file.content_type or ""
|
| 8 |
+
filename = upload_file.filename
|
| 9 |
+
|
| 10 |
+
# Determine file extension
|
| 11 |
+
if 'webm' in content_type or filename.endswith('.webm'):
|
| 12 |
+
original_suffix = ".webm"
|
| 13 |
+
elif 'ogg' in content_type or filename.endswith('.ogg'):
|
| 14 |
+
original_suffix = ".ogg"
|
| 15 |
+
elif 'mp3' in content_type or filename.endswith('.mp3'):
|
| 16 |
+
original_suffix = ".mp3"
|
| 17 |
+
elif 'wav' in content_type or filename.endswith('.wav'):
|
| 18 |
+
original_suffix = ".wav"
|
| 19 |
+
else:
|
| 20 |
+
original_suffix = ".wav" # default
|
| 21 |
+
|
| 22 |
+
# Save original file first
|
| 23 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=original_suffix) as tmp_original:
|
| 24 |
+
content = await upload_file.read()
|
| 25 |
+
tmp_original.write(content)
|
| 26 |
+
original_path = tmp_original.name
|
| 27 |
+
|
| 28 |
+
# If already WAV, return as is
|
| 29 |
+
if original_suffix == ".wav":
|
| 30 |
+
return original_path
|
| 31 |
+
|
| 32 |
+
# Convert to WAV using ffmpeg
|
| 33 |
+
try:
|
| 34 |
+
wav_path = original_path.replace(original_suffix, ".wav")
|
| 35 |
+
|
| 36 |
+
# Use ffmpeg for conversion
|
| 37 |
+
cmd = ['ffmpeg', '-i', original_path, '-acodec', 'pcm_s16le', '-ar', '16000', wav_path, '-y']
|
| 38 |
+
result = subprocess.run(cmd, capture_output=True, timeout=30)
|
| 39 |
+
|
| 40 |
+
if result.returncode != 0:
|
| 41 |
+
raise Exception(f"FFmpeg conversion failed: {result.stderr.decode()}")
|
| 42 |
+
|
| 43 |
+
# Delete original file
|
| 44 |
+
os.remove(original_path)
|
| 45 |
+
|
| 46 |
+
return wav_path
|
| 47 |
+
except Exception as e:
|
| 48 |
+
# If conversion fails, try to use original
|
| 49 |
+
print(f"Audio conversion error: {e}")
|
| 50 |
+
# Cleanup
|
| 51 |
+
if os.path.exists(original_path):
|
| 52 |
+
os.remove(original_path)
|
| 53 |
+
raise Exception(f"Failed to convert audio file: {e}")
|
utils/voice_data_extraction.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import parselmouth
|
| 2 |
+
from parselmouth.praat import call
|
| 3 |
+
import numpy as np
|
| 4 |
+
from scipy.stats import entropy
|
| 5 |
+
|
| 6 |
+
def extract_voice_features(audio_file):
|
| 7 |
+
|
| 8 |
+
sound = parselmouth.Sound(audio_file)
|
| 9 |
+
pitch = call(sound, "To Pitch", 0.0, 75, 600)
|
| 10 |
+
|
| 11 |
+
# Calculate jitter measures using individual Parselmouth functions
|
| 12 |
+
pointprocess = call(sound, "To PointProcess (periodic, cc)", 75, 600)
|
| 13 |
+
|
| 14 |
+
# Jitter measurements
|
| 15 |
+
jitter_percent = call(pointprocess, "Get jitter (local)", 0, 0, 0.0001, 0.02, 1.3)
|
| 16 |
+
jitter_abs = call(pointprocess, "Get jitter (local, absolute)", 0, 0, 0.0001, 0.02, 1.3)
|
| 17 |
+
jitter_rap = call(pointprocess, "Get jitter (rap)", 0, 0, 0.0001, 0.02, 1.3)
|
| 18 |
+
jitter_ppq5 = call(pointprocess, "Get jitter (ppq5)", 0, 0, 0.0001, 0.02, 1.3)
|
| 19 |
+
jitter_ddp = call(pointprocess, "Get jitter (ddp)", 0, 0, 0.0001, 0.02, 1.3)
|
| 20 |
+
|
| 21 |
+
# Shimmer measurements
|
| 22 |
+
shimmer = call([sound, pointprocess], "Get shimmer (local)", 0, 0, 0.0001, 0.02, 1.3, 1.6)
|
| 23 |
+
shimmer_db = call([sound, pointprocess], "Get shimmer (local_dB)", 0, 0, 0.0001, 0.02, 1.3, 1.6)
|
| 24 |
+
shimmer_apq3 = call([sound, pointprocess], "Get shimmer (apq3)", 0, 0, 0.0001, 0.02, 1.3, 1.6)
|
| 25 |
+
shimmer_apq5 = call([sound, pointprocess], "Get shimmer (apq5)", 0, 0, 0.0001, 0.02, 1.3, 1.6)
|
| 26 |
+
shimmer_apq11 = call([sound, pointprocess], "Get shimmer (apq11)", 0, 0, 0.0001, 0.02, 1.3, 1.6)
|
| 27 |
+
shimmer_dda = call([sound, pointprocess], "Get shimmer (dda)", 0, 0, 0.0001, 0.02, 1.3, 1.6)
|
| 28 |
+
|
| 29 |
+
# HNR (Harmonics-to-Noise Ratio)
|
| 30 |
+
harmonicity = call(sound, "To Harmonicity (cc)", 0.01, 75, 0.1, 1.0)
|
| 31 |
+
hnr = call(harmonicity, "Get mean", 0, 0)
|
| 32 |
+
|
| 33 |
+
# NHR (Noise-to-Harmonics Ratio) = 1 / linear_HNR
|
| 34 |
+
# HNR from Praat is in dB, so convert: linear_HNR = 10^(HNR_dB / 10)
|
| 35 |
+
# Guard against zero/negative linear values (unvoiced / very noisy signal)
|
| 36 |
+
if hnr > 0:
|
| 37 |
+
nhr_value = 1.0 / (10 ** (hnr / 10))
|
| 38 |
+
else:
|
| 39 |
+
nhr_value = float('inf')
|
| 40 |
+
|
| 41 |
+
# Get pitch periods for nonlinear features
|
| 42 |
+
# Alternative approach: use pitch values directly for period calculation
|
| 43 |
+
pitch_values = pitch.selected_array['frequency']
|
| 44 |
+
voiced_frames = pitch_values[pitch_values > 0] # Only voiced frames
|
| 45 |
+
|
| 46 |
+
if len(voiced_frames) > 0:
|
| 47 |
+
# Convert frequency to periods (1/frequency)
|
| 48 |
+
periods = 1.0 / voiced_frames
|
| 49 |
+
else:
|
| 50 |
+
periods = np.array([])
|
| 51 |
+
|
| 52 |
+
# Nonlinear features
|
| 53 |
+
if len(periods) < 50:
|
| 54 |
+
rpde = dfa = ppe = np.nan
|
| 55 |
+
else:
|
| 56 |
+
# PPE: Pitch Period Entropy
|
| 57 |
+
hist, _ = np.histogram(periods, bins=min(20, len(periods)//5), density=True)
|
| 58 |
+
hist = hist[hist > 0]
|
| 59 |
+
ppe = entropy(hist + 1e-10)
|
| 60 |
+
|
| 61 |
+
# Simplified RPDE: Recurrence Period Density Entropy
|
| 62 |
+
diffs = np.abs(np.diff(periods))
|
| 63 |
+
rec_threshold = np.std(diffs) * 0.1
|
| 64 |
+
rec_matrix = (np.abs(periods[:, None] - periods[None, :]) < rec_threshold).astype(float)
|
| 65 |
+
np.fill_diagonal(rec_matrix, 0)
|
| 66 |
+
hist_rpde, _ = np.histogram(rec_matrix.flatten(), bins=2, density=True)
|
| 67 |
+
rpde = entropy(hist_rpde + 1e-10)
|
| 68 |
+
|
| 69 |
+
# DFA: Detrended Fluctuation Analysis
|
| 70 |
+
y = np.cumsum(periods - np.mean(periods))
|
| 71 |
+
scales = np.logspace(np.log10(4), np.log10(len(y)/4), 8, dtype=int)
|
| 72 |
+
log_F = []
|
| 73 |
+
log_s = []
|
| 74 |
+
for s in scales:
|
| 75 |
+
if s > len(y) // 4:
|
| 76 |
+
continue
|
| 77 |
+
num_seg = len(y) // s
|
| 78 |
+
rms = []
|
| 79 |
+
for i in range(num_seg):
|
| 80 |
+
seg = y[i*s:(i+1)*s]
|
| 81 |
+
if len(seg) < 3:
|
| 82 |
+
continue
|
| 83 |
+
x = np.arange(len(seg))
|
| 84 |
+
p = np.polyfit(x, seg, 1)
|
| 85 |
+
detrend = seg - np.polyval(p, x)
|
| 86 |
+
rms.append(np.sqrt(np.mean(detrend**2)))
|
| 87 |
+
if rms:
|
| 88 |
+
F = np.sqrt(np.mean(rms))
|
| 89 |
+
log_F.append(np.log(F))
|
| 90 |
+
log_s.append(np.log(s))
|
| 91 |
+
if len(log_F) > 1:
|
| 92 |
+
slope, _ = np.polyfit(log_s, log_F, 1)
|
| 93 |
+
dfa = slope
|
| 94 |
+
else:
|
| 95 |
+
dfa = np.nan
|
| 96 |
+
|
| 97 |
+
return {
|
| 98 |
+
'Jitter(%)': jitter_percent * 100,
|
| 99 |
+
'Jitter(Abs)': jitter_abs,
|
| 100 |
+
'Jitter:RAP': jitter_rap,
|
| 101 |
+
'Jitter:PPQ5': jitter_ppq5,
|
| 102 |
+
'Jitter:DDP': jitter_ddp,
|
| 103 |
+
'Shimmer': shimmer,
|
| 104 |
+
'Shimmer(dB)': shimmer_db,
|
| 105 |
+
'Shimmer:APQ3': shimmer_apq3,
|
| 106 |
+
'Shimmer:APQ5': shimmer_apq5,
|
| 107 |
+
'Shimmer:APQ11': shimmer_apq11,
|
| 108 |
+
'Shimmer:DDA': shimmer_dda,
|
| 109 |
+
'NHR': nhr_value,
|
| 110 |
+
'HNR': hnr,
|
| 111 |
+
'RPDE': float(rpde),
|
| 112 |
+
'DFA': float(dfa),
|
| 113 |
+
'PPE': float(ppe)
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
# Load audio file
|
| 118 |
+
# audio_file = "test_voice.wav"
|
| 119 |
+
# features = measure_jitter_shimmer(audio_file)
|
| 120 |
+
# print(features)
|