arshan123 commited on
Commit
0eb3ad9
·
1 Parent(s): b2465b7

Updated language specific fix

Browse files
Files changed (1) hide show
  1. detector.py +96 -48
detector.py CHANGED
@@ -169,6 +169,14 @@ class HybridEnsembleDetector:
169
  # Analyze only middle N seconds for physics (reduces latency ~60% vs full 30s)
170
  self.physics_analysis_duration = 8 # seconds; middle segment where voice is most stable
171
 
 
 
 
 
 
 
 
 
172
  print("✅ Hybrid Detector Ready\n")
173
 
174
  # ==========================================================
@@ -232,6 +240,26 @@ class HybridEnsembleDetector:
232
  was_truncated = True
233
  return y, sr, duration, was_truncated
234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  # ==========================================================
236
  # HELPER: Base64 Decoding
237
  # ==========================================================
@@ -373,7 +401,12 @@ class HybridEnsembleDetector:
373
  end_samp = int((half + seg_dur) * sr)
374
  y_physics = y[start_samp:end_samp]
375
 
376
- print(f" 🔬 Running physics analysis on middle {seg_dur:.1f}s of {duration:.1f}s audio at {sr}Hz")
 
 
 
 
 
377
 
378
  # Robust pitch tracking using pYIN (filters silence/noise; yin blindly estimates pitch there)
379
  try:
@@ -647,60 +680,72 @@ class HybridEnsembleDetector:
647
 
648
  print(f"🎵 Analyzing: {os.path.basename(audio_path)}")
649
 
650
- # Load audio once and reuse for both branches (avoids duplicate I/O and decode)
651
- preloaded = self._load_audio_once(audio_path)
 
 
 
652
 
653
- # 2 & 3. Run Physics and DL analysis in parallel (latency ≈ max of the two, not sum)
654
- phys_score, phys_method, phys_feats = 0.0, "", {}
655
- dl_score, dl_label = 0.0, ""
656
-
657
- with ThreadPoolExecutor(max_workers=2) as executor:
658
- future_phys = executor.submit(self.get_physics_score, audio_path, preloaded)
659
- future_dl = executor.submit(self.get_dl_score, audio_path, preloaded)
660
- for future in as_completed([future_phys, future_dl]):
661
- try:
662
- result = future.result()
663
- # Physics returns (score, method, feats); DL returns (score, label)
664
- if len(result) == 3:
665
- phys_score, phys_method, phys_feats = result
666
- else:
667
- dl_score, dl_label = result
668
- print(f"DL Score is: ${dl_score}")
669
- except Exception as e:
670
- print(f" ⚠️ Parallel task error: {e}")
671
- # Cannot know which failed; set both fallbacks if needed
672
- if future is future_phys:
673
- phys_score, phys_method, phys_feats = 0.0, f"Error: {e}", {'duration': 0, 'was_truncated': False}
674
- else:
675
- dl_score, dl_label = 0.0, f"DL Error: {e}"
676
-
677
- # 4. Calculate weighted ensemble score
678
- final_score = (
679
- self.physics_weight * phys_score +
680
- self.dl_weight * dl_score
681
- )
682
-
683
- # Round to 2 decimal places
684
- final_score = round(final_score, 2)
685
- # final_score = round(dl_score,2)
686
-
687
- # 5. Determine classification
688
- classification = "AI_GENERATED" if final_score > 0.55 else "HUMAN"
689
-
690
- # 6. Generate explanation
 
 
 
 
 
 
 
 
 
691
  explanation = self.generate_explanation(
692
- final_score,
693
- dl_score,
694
- dl_label,
695
  phys_score,
696
  phys_feats
697
  )
698
 
699
- # 7. Return API-compliant response (ensure all values are JSON serializable)
700
  return {
701
  "status": "success",
702
  "classification": classification,
703
- "confidenceScore": float(final_score), # Convert to Python float
704
  "explanation": explanation,
705
  "debug": {
706
  "physics_score": float(phys_score),
@@ -710,8 +755,11 @@ class HybridEnsembleDetector:
710
  "dl_weight": f"{self.dl_weight*100:.0f}%",
711
  "audio_duration": float(phys_feats.get('duration', 0)),
712
  "was_truncated": bool(phys_feats.get('was_truncated', False)),
713
- "physics_features": {k: float(v) if isinstance(v, (np.floating, np.integer)) else v
714
- for k, v in phys_feats.items()
 
 
 
715
  if k not in ['duration', 'was_truncated']}
716
  }
717
  }
 
169
  # Analyze only middle N seconds for physics (reduces latency ~60% vs full 30s)
170
  self.physics_analysis_duration = 8 # seconds; middle segment where voice is most stable
171
 
172
+ # --- CHUNKING (MAX-POOLING) FOR LONG AUDIO ---
173
+ self.chunk_duration_sec = 5
174
+ self.max_chunks = 3 # If any chunk is AI, whole file is AI
175
+
176
+ # --- CLASSIFICATION THRESHOLD (language-specific) ---
177
+ self.default_ai_threshold = 0.55
178
+ self.tamil_ai_threshold = 0.45 # More aggressive for Tamil TTS
179
+
180
  print("✅ Hybrid Detector Ready\n")
181
 
182
  # ==========================================================
 
240
  was_truncated = True
241
  return y, sr, duration, was_truncated
242
 
243
+ def _chunk_audio(self, y, sr, duration):
244
+ """
245
+ Split audio into fixed-duration chunks for max-pooling.
246
+ Returns list of (y_chunk, sr, duration_chunk, was_truncated) for preloaded.
247
+ """
248
+ chunks = []
249
+ chunk_len = int(self.chunk_duration_sec * sr)
250
+ min_chunk_samples = int(1.0 * sr) # skip chunks shorter than 1s
251
+ for i in range(self.max_chunks):
252
+ start = i * chunk_len
253
+ if start >= len(y):
254
+ break
255
+ end = min(start + chunk_len, len(y))
256
+ y_chunk = y[start:end]
257
+ if len(y_chunk) < min_chunk_samples:
258
+ break
259
+ dur_chunk = len(y_chunk) / sr
260
+ chunks.append((y_chunk.copy(), sr, dur_chunk, False))
261
+ return chunks
262
+
263
  # ==========================================================
264
  # HELPER: Base64 Decoding
265
  # ==========================================================
 
401
  end_samp = int((half + seg_dur) * sr)
402
  y_physics = y[start_samp:end_samp]
403
 
404
+ # VAD: trim silence so physics doesn't see "perfect stability" from empty segments
405
+ y_physics_trimmed, _ = librosa.effects.trim(y_physics, top_db=20)
406
+ if len(y_physics_trimmed) >= int(0.5 * sr): # Keep at least 0.5s of audio
407
+ y_physics = y_physics_trimmed
408
+
409
+ print(f" 🔬 Running physics analysis on middle {seg_dur:.1f}s of {duration:.1f}s audio at {sr}Hz (VAD trimmed)")
410
 
411
  # Robust pitch tracking using pYIN (filters silence/noise; yin blindly estimates pitch there)
412
  try:
 
680
 
681
  print(f"🎵 Analyzing: {os.path.basename(audio_path)}")
682
 
683
+ # 1. Detect language for Tamil-specific threshold
684
+ language = self.detect_language(audio_path)
685
+ threshold = self.tamil_ai_threshold if language == "Tamil" else self.default_ai_threshold
686
+ if language == "Tamil":
687
+ print(f" 📐 Using Tamil-specific AI threshold: {threshold}")
688
 
689
+ # 2. Load audio once and split into chunks for max-pooling
690
+ preloaded = self._load_audio_once(audio_path)
691
+ y, sr, duration, was_truncated = preloaded
692
+ chunks = self._chunk_audio(y, sr, duration)
693
+ if not chunks:
694
+ chunks = [preloaded] # Very short audio: single "chunk" = full audio
695
+
696
+ # 3. Run Physics and DL per chunk; take max score (if any chunk is AI → whole file AI)
697
+ best_score = -1.0
698
+ best_phys_score, best_phys_method, best_phys_feats = 0.0, "", {}
699
+ best_dl_score, best_dl_label = 0.0, ""
700
+
701
+ for idx, preloaded_chunk in enumerate(chunks):
702
+ phys_score, phys_method, phys_feats = 0.0, "", {}
703
+ dl_score, dl_label = 0.0, ""
704
+ with ThreadPoolExecutor(max_workers=2) as executor:
705
+ future_phys = executor.submit(self.get_physics_score, None, preloaded_chunk)
706
+ future_dl = executor.submit(self.get_dl_score, None, preloaded_chunk)
707
+ for future in as_completed([future_phys, future_dl]):
708
+ try:
709
+ result = future.result()
710
+ if len(result) == 3:
711
+ phys_score, phys_method, phys_feats = result
712
+ else:
713
+ dl_score, dl_label = result
714
+ except Exception as e:
715
+ print(f" ⚠️ Chunk {idx+1} task error: {e}")
716
+ if future is future_phys:
717
+ phys_score, phys_method, phys_feats = 0.0, f"Error: {e}", {'duration': 0, 'was_truncated': False}
718
+ else:
719
+ dl_score, dl_label = 0.0, f"DL Error: {e}"
720
+
721
+ chunk_score = self.physics_weight * phys_score + self.dl_weight * dl_score
722
+ if chunk_score > best_score:
723
+ best_score = chunk_score
724
+ best_phys_score, best_phys_method, best_phys_feats = phys_score, phys_method, phys_feats
725
+ best_dl_score, best_dl_label = dl_score, dl_label
726
+ print(f" Chunk {idx+1}/{len(chunks)} score: {chunk_score:.3f} (phys={phys_score:.2f}, dl={dl_score:.2f})")
727
+
728
+ final_score = round(best_score, 2)
729
+ phys_score, phys_feats = best_phys_score, best_phys_feats
730
+ dl_score, dl_label = best_dl_score, best_dl_label
731
+
732
+ # 4. Classification with language-specific threshold
733
+ classification = "AI_GENERATED" if final_score > threshold else "HUMAN"
734
+
735
+ # 5. Generate explanation (using best chunk's scores/feats)
736
  explanation = self.generate_explanation(
737
+ final_score,
738
+ dl_score,
739
+ dl_label,
740
  phys_score,
741
  phys_feats
742
  )
743
 
744
+ # 6. Return API-compliant response
745
  return {
746
  "status": "success",
747
  "classification": classification,
748
+ "confidenceScore": float(final_score),
749
  "explanation": explanation,
750
  "debug": {
751
  "physics_score": float(phys_score),
 
755
  "dl_weight": f"{self.dl_weight*100:.0f}%",
756
  "audio_duration": float(phys_feats.get('duration', 0)),
757
  "was_truncated": bool(phys_feats.get('was_truncated', False)),
758
+ "chunks_used": len(chunks),
759
+ "ai_threshold_used": float(threshold),
760
+ "language": language,
761
+ "physics_features": {k: float(v) if isinstance(v, (np.floating, np.integer)) else v
762
+ for k, v in phys_feats.items()
763
  if k not in ['duration', 'was_truncated']}
764
  }
765
  }