fakeshield-api / backend /app /models /audio /audio_segmentation.py
Akash4911's picture
Production Deploy: Improved robustness and logging
66b6851
Raw
History Blame Contribute Delete
4.17 kB
# audio_segmentation.py
"""
Build per-second timeline from per-chunk signal scores.
This powers the suspicious-segment heatmap in the UI.
"""
import numpy as np
def build_audio_timeline(
wav2vec_chunks: list,
spectral_chunks: list,
prosody_chunks: list,
speaker_chunks: list,
chunk_times: list,
overall_ai_prob: float = None,
) -> list:
"""
Fuse per-chunk scores into timeline segments.
Each entry covers one 5-second chunk.
BUG FIX: Previously used min() over all list lengths without guarding for
empty lists, so min(0, 3, 3, 3) = 0 always produced an empty timeline.
Now handles lists of different lengths gracefully.
"""
# Determine safe iteration count
lengths = [
len(wav2vec_chunks) if wav2vec_chunks else 0,
len(spectral_chunks) if spectral_chunks else 0,
len(prosody_chunks) if prosody_chunks else 0,
len(chunk_times) if chunk_times else 0,
]
# Use max of available signal lengths but cap at chunk_times
n_times = len(chunk_times) if chunk_times else 0
n_signals = max(
len(wav2vec_chunks) if wav2vec_chunks else 0,
len(spectral_chunks) if spectral_chunks else 0,
len(prosody_chunks) if prosody_chunks else 0,
)
# n = number of segments we can build (bounded by chunk_times)
n = n_times if n_times > 0 else 0
if n == 0:
return []
timeline = []
# Compute global means for smarter fallbacks
w_mean = float(np.mean(wav2vec_chunks)) if wav2vec_chunks else (overall_ai_prob if overall_ai_prob is not None else 0.5)
sp_mean = float(np.mean(spectral_chunks)) if spectral_chunks else (overall_ai_prob if overall_ai_prob is not None else 0.5)
pr_mean = float(np.mean(prosody_chunks)) if prosody_chunks else (overall_ai_prob if overall_ai_prob is not None else 0.5)
sk_mean = float(np.mean(speaker_chunks)) if speaker_chunks else (overall_ai_prob if overall_ai_prob is not None else 0.5)
for i in range(n):
# Safely access each list with fallback to global means
w = float(wav2vec_chunks[i]) if i < len(wav2vec_chunks) else w_mean
sp = float(spectral_chunks[i]) if i < len(spectral_chunks) else sp_mean
pr = float(prosody_chunks[i]) if i < len(prosody_chunks) else pr_mean
sk = float(speaker_chunks[i]) if i < len(speaker_chunks) else sk_mean
# Weighted chunk score — Weights balanced with fusion engine
chunk_score = 0.50 * w + 0.10 * sp + 0.20 * pr + 0.20 * sk
# Pull chunk score towards overall probability to prevent UX disconnect
# We increase the 'pull' for unanalyzed segments to ensure consistency
if overall_ai_prob is not None:
is_fallback = (i >= n_signals)
threshold = 0.05 if is_fallback else 0.15
if abs(chunk_score - overall_ai_prob) > threshold:
blend_factor = 0.8 if is_fallback else 0.6
chunk_score = ((1.0 - blend_factor) * chunk_score) + (blend_factor * overall_ai_prob)
chunk_score = max(0.0, min(1.0, chunk_score))
start_t, end_t = chunk_times[i] if i < len(chunk_times) else (i * 5, i * 5 + 5)
# Calibrated levels matching UI: Authentic (Low), Suspicious (Med), High Risk (High), Synthetic (Crit)
level = (
"critical" if chunk_score >= 0.75 else
"high" if chunk_score >= 0.55 else
"medium" if chunk_score >= 0.38 else
"low"
)
timeline.append({
"segment": i + 1,
"start_sec": round(float(start_t), 2),
"end_sec": round(float(end_t), 2),
"ai_score": round(chunk_score * 100, 1),
"level": level,
"signals": {
"wavlm": round(w * 100, 1),
"wav2vec": round(w * 100, 1),
"spectral": round(sp * 100, 1),
"prosody": round(pr * 100, 1),
"speaker": round(sk * 100, 1),
"codec": 10.0, # no per-chunk codec — static baseline
},
})
return timeline