import time import hashlib import numpy as np import librosa import speech_recognition as sr import soundfile as sf import io from scripts.voice_classifier import VoiceClassifier class VoiceDetectionPipeline: """ Loads an audio file, splits it into overlapping segments, classifies each segment with VoiceClassifier, and aggregates a global report. Mirrors the shape of DetectionPipeline (frames -> segments) so the frontend can reuse similar timeline/chart components. """ SAMPLE_RATE = VoiceClassifier.SAMPLE_RATE SEGMENT_SEC = 3.0 HOP_SEC = 2.0 MAX_SEGMENTS = 4 def __init__(self, classifier: VoiceClassifier): self.classifier = classifier def analyze_audio(self, file_path: str, update_progress_cb=None) -> dict: start_time = time.time() file_hash = self._get_file_hash(file_path) if update_progress_cb: update_progress_cb(10, "Loading Audio") y, sample_rate = librosa.load(file_path, sr=self.SAMPLE_RATE, mono=True) duration = float(len(y) / sample_rate) if sample_rate else 0.0 if update_progress_cb: update_progress_cb(25, "Segmenting Audio") segments = self._segment_audio(y, sample_rate) total_segments = len(segments) processed_segments = [] all_scores = [] all_jitter = [] all_flatness = [] all_silence = [] for idx, (seg_audio, seg_start) in enumerate(segments): if update_progress_cb: pct = int(25 + (idx / max(1, total_segments)) * 65) update_progress_cb(pct, f"Analyzing Segment {idx + 1}/{total_segments}") res = self.classifier.analyze_segment(seg_audio) all_scores.append(res["fake_score"]) all_jitter.append(res["heuristics"].get("pitch_jitter_score", 0.0)) all_flatness.append(res["heuristics"].get("spectral_flatness_score", 0.0)) all_silence.append(res["heuristics"].get("silence_anomaly_score", 0.0)) processed_segments.append({ "segment_idx": idx, "start_time": round(seg_start, 2), "end_time": round(seg_start + len(seg_audio) / sample_rate, 2), "fake_score": res["fake_score"], "is_fake": res["is_fake"], "confidence": res["confidence"], "heuristics": res["heuristics"] }) if update_progress_cb: update_progress_cb(95, "Aggregating Report") if all_scores: avg_fake_score = float(np.mean(all_scores)) max_fake_score = float(np.max(all_scores)) global_score = round((0.7 * max_fake_score) + (0.3 * avg_fake_score), 4) avg_jitter = float(np.mean(all_jitter)) avg_flatness = float(np.mean(all_flatness)) avg_silence = float(np.mean(all_silence)) else: global_score = 0.0 avg_jitter = avg_flatness = avg_silence = 0.0 if update_progress_cb: update_progress_cb(97, "Transcribing Audio") transcript = "" try: r = sr.Recognizer() wav_io = io.BytesIO() sf.write(wav_io, y, sample_rate, format='WAV', subtype='PCM_16') wav_io.seek(0) with sr.AudioFile(wav_io) as source: audio_data = r.record(source) transcript = r.recognize_google(audio_data) except Exception as e: transcript = "[Transcription unavailable or no speech detected]" print(f"Transcription error: {e}") # Bypass hanging recognize_google call on Windows/local environment transcript = "[Transcription disabled for local testing]" is_fake = global_score > 0.5 confidence = global_score if is_fake else (1.0 - global_score) processing_time = round(time.time() - start_time, 2) report = { "file_hash": file_hash, "filename": file_path.split("/")[-1].split("\\")[-1], "is_audio": True, "duration_sec": round(duration, 2), "global_fake_score": global_score, "is_fake": is_fake, "confidence": round(confidence, 4), "total_segments_analyzed": total_segments, "processing_time_sec": processing_time, "timestamp": time.time(), "average_heuristics": { "pitch_jitter_score": round(avg_jitter, 4), "spectral_flatness_score": round(avg_flatness, 4), "silence_anomaly_score": round(avg_silence, 4) }, "segments": processed_segments, "used_voice_model": self.classifier.model_loaded, "transcript": transcript } if update_progress_cb: update_progress_cb(100, "Completed") return report def _segment_audio(self, y: np.ndarray, sr: int) -> list[tuple[np.ndarray, float]]: seg_len = int(self.SEGMENT_SEC * sr) hop_len = int(self.HOP_SEC * sr) if len(y) <= seg_len: return [(y, 0.0)] segments = [] pos = 0 while pos < len(y) and len(segments) < self.MAX_SEGMENTS: chunk = y[pos:pos + seg_len] if len(chunk) < int(0.5 * sr): break segments.append((chunk, pos / sr)) pos += hop_len return segments if segments else [(y, 0.0)] def _get_file_hash(self, file_path: str) -> str: sha256 = hashlib.sha256() with open(file_path, "rb") as f: for byte_block in iter(lambda: f.read(4096), b""): sha256.update(byte_block) return sha256.hexdigest()