49 lines
2.7 KiB
Rust
49 lines
2.7 KiB
Rust
#![deny(warnings)]
|
|
#![allow(dead_code)]
|
|
//! [TSM.ID].[11031972] -- Platform X Ecosystem
|
|
//! xcu-voice-clone -- Anti Voice-Clone Defense with Liveness Detection
|
|
#[derive(Debug)] pub enum VoiceError { InsufficientSamples(String), CloneDetected(String) }
|
|
impl std::fmt::Display for VoiceError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::InsufficientSamples(e)|Self::CloneDetected(e) => write!(f, "{e}") } } }
|
|
impl std::error::Error for VoiceError {}
|
|
#[derive(Debug, Clone)] pub struct VoicePrint { pub spectral_centroid: f64, pub pitch_mean: f64, pub pitch_variance: f64, pub formant_ratios: Vec<f64>, pub micro_tremor: f64 }
|
|
pub fn spectral_centroid(magnitudes: &[f64]) -> f64 {
|
|
let weighted: f64 = magnitudes.iter().enumerate().map(|(i, &m)| i as f64 * m).sum();
|
|
let total: f64 = magnitudes.iter().sum();
|
|
if total < 1e-10 { 0.0 } else { weighted / total }
|
|
}
|
|
pub fn pitch_stats(pitches: &[f64]) -> (f64, f64) {
|
|
if pitches.is_empty() { return (0.0, 0.0); }
|
|
let mean = pitches.iter().sum::<f64>() / pitches.len() as f64;
|
|
let var = pitches.iter().map(|p| (p - mean)*(p - mean)).sum::<f64>() / pitches.len() as f64;
|
|
(mean, var)
|
|
}
|
|
pub fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
|
|
let dot: f64 = a.iter().zip(b.iter()).map(|(x,y)| x*y).sum();
|
|
let na: f64 = a.iter().map(|x| x*x).sum::<f64>().sqrt();
|
|
let nb: f64 = b.iter().map(|x| x*x).sum::<f64>().sqrt();
|
|
if na < 1e-10 || nb < 1e-10 { 0.0 } else { dot / (na * nb) }
|
|
}
|
|
pub struct CloneDetector { pub threshold: f64 }
|
|
impl CloneDetector {
|
|
pub fn new(threshold: f64) -> Self { Self { threshold } }
|
|
pub fn is_clone(&self, live: &VoicePrint, stored: &VoicePrint) -> bool {
|
|
let sim = cosine_similarity(&live.formant_ratios, &stored.formant_ratios);
|
|
let tremor_diff = (live.micro_tremor - stored.micro_tremor).abs();
|
|
sim > self.threshold && tremor_diff < 0.01 // too perfect = clone
|
|
}
|
|
pub fn liveness_score(&self, vp: &VoicePrint) -> f64 {
|
|
let tremor_score = (vp.micro_tremor * 100.0).min(1.0); // natural voice has micro tremor
|
|
let variance_score = (vp.pitch_variance / 50.0).min(1.0); // natural has pitch variance
|
|
(tremor_score + variance_score) / 2.0
|
|
}
|
|
}
|
|
#[cfg(test)] mod tests {
|
|
use super::*;
|
|
#[test] fn test_cosine() { let a = vec![1.0, 0.0]; let b = vec![1.0, 0.0]; assert!((cosine_similarity(&a, &b) - 1.0).abs() < 1e-10); }
|
|
#[test] fn test_liveness() {
|
|
let cd = CloneDetector::new(0.99);
|
|
let real = VoicePrint { spectral_centroid: 2000.0, pitch_mean: 150.0, pitch_variance: 30.0, formant_ratios: vec![1.0,1.5,2.5], micro_tremor: 0.05 };
|
|
assert!(cd.liveness_score(&real) > 0.3);
|
|
}
|
|
}
|