Skip to main content

kino_frequency/
lib.rs

1//! Purple Squirrel Media - Frequency Analysis Library
2//!
3//! This crate provides audio frequency analysis capabilities for:
4//! - **Audio Fingerprinting**: Cryptographic content verification using spectral peaks
5//! - **AI Auto-Tagging**: Content classification based on frequency signatures
6//! - **Thumbnail Generation**: Optimal frame selection using FFT-based quality metrics
7//! - **Recommendations**: Content similarity matching via frequency signatures
8//!
9//! # Architecture
10//!
11//! The frequency analysis pipeline integrates with the Kino player ecosystem:
12//!
13//! ```text
14//! ┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
15//! │  Video Upload   │───▶│  Audio Extract   │───▶│  FFT Analysis   │
16//! └─────────────────┘    └──────────────────┘    └────────┬────────┘
17//!                                                         │
18//!         ┌───────────────────────────────────────────────┼───────────────────────┐
19//!         │                                               │                       │
20//!         ▼                                               ▼                       ▼
21//! ┌───────────────┐                              ┌────────────────┐       ┌───────────────┐
22//! │ Fingerprint   │                              │  Auto-Tagging  │       │ Recommendations│
23//! │ (SHA-256)     │                              │  (ML Model)    │       │ (Similarity)   │
24//! └───────┬───────┘                              └────────┬───────┘       └───────┬───────┘
25//!         │                                               │                       │
26//!         ▼                                               ▼                       ▼
27//! ┌───────────────┐                              ┌────────────────┐       ┌───────────────┐
28//! │ Solana Chain  │                              │  Content Tags  │       │ Similar Items │
29//! │ (Verification)│                              │  (Metadata)    │       │ (API Response)│
30//! └───────────────┘                              └────────────────┘       └───────────────┘
31//! ```
32//!
33//! # Quick Start
34//!
35//! ```rust,no_run
36//! use kino_frequency::{
37//!     AudioAnalyzer,
38//!     fingerprint::Fingerprinter,
39//!     tagging::ContentTagger,
40//!     thumbnail::ThumbnailSelector,
41//!     recommend::RecommendationEngine,
42//! };
43//!
44//! #[tokio::main]
45//! async fn main() -> anyhow::Result<()> {
46//!     // Initialize the analyzer
47//!     let analyzer = AudioAnalyzer::new(44100);
48//!
49//!     // Extract audio from video
50//!     let audio = analyzer.extract_audio("video.mp4").await?;
51//!
52//!     // Generate fingerprint
53//!     let fingerprint = Fingerprinter::new().fingerprint(&audio)?;
54//!     println!("Content hash: {}", fingerprint.hash);
55//!
56//!     // Auto-tag content
57//!     let tags = ContentTagger::new().predict(&audio)?;
58//!     println!("Tags: {:?}", tags);
59//!
60//!     Ok(())
61//! }
62//! ```
63
64#![warn(clippy::all)]
65#![warn(missing_docs)]
66
67pub mod fft;
68pub mod types;
69
70#[cfg(feature = "fingerprint")]
71pub mod fingerprint;
72
73#[cfg(feature = "tagging")]
74pub mod tagging;
75
76#[cfg(feature = "thumbnail")]
77pub mod thumbnail;
78
79#[cfg(feature = "recommend")]
80pub mod recommend;
81
82#[cfg(feature = "solana")]
83pub mod solana;
84
85pub mod streaming;
86
87use std::path::Path;
88use std::process::Command;
89use anyhow::{Context, Result, bail};
90use tracing::{info, debug, warn};
91
92pub use types::*;
93pub use fft::FrequencyAnalyzer;
94
95#[cfg(feature = "fingerprint")]
96pub use fingerprint::Fingerprinter;
97
98#[cfg(feature = "tagging")]
99pub use tagging::ContentTagger;
100
101#[cfg(feature = "thumbnail")]
102pub use thumbnail::ThumbnailSelector;
103
104#[cfg(feature = "recommend")]
105pub use recommend::RecommendationEngine;
106
107/// Main audio analyzer that coordinates all frequency analysis operations.
108pub struct AudioAnalyzer {
109    sample_rate: u32,
110    fft_size: usize,
111    hop_size: usize,
112}
113
114impl AudioAnalyzer {
115    /// Create a new audio analyzer with the specified sample rate.
116    pub fn new(sample_rate: u32) -> Self {
117        Self {
118            sample_rate,
119            fft_size: 4096,
120            hop_size: 2048,
121        }
122    }
123
124    /// Create an analyzer with custom FFT parameters.
125    pub fn with_fft_params(sample_rate: u32, fft_size: usize, hop_size: usize) -> Self {
126        Self {
127            sample_rate,
128            fft_size,
129            hop_size,
130        }
131    }
132
133    /// Extract audio from a video file using FFmpeg.
134    pub async fn extract_audio(&self, video_path: impl AsRef<Path>) -> Result<AudioData> {
135        let video_path = video_path.as_ref();
136
137        info!("Extracting audio from: {}", video_path.display());
138
139        // Create temporary WAV file
140        let temp_dir = std::env::temp_dir();
141        let temp_wav = temp_dir.join(format!("kino_audio_{}.wav", uuid::Uuid::new_v4()));
142
143        // Run FFmpeg to extract audio
144        let output = Command::new("ffmpeg")
145            .args([
146                "-i", &video_path.to_string_lossy(),
147                "-vn",                          // No video
148                "-acodec", "pcm_s16le",         // 16-bit PCM
149                "-ar", &self.sample_rate.to_string(),  // Sample rate
150                "-ac", "1",                     // Mono
151                "-y",                           // Overwrite
152                &temp_wav.to_string_lossy(),
153            ])
154            .output()
155            .context("FFmpeg not found. Please install FFmpeg.")?;
156
157        if !output.status.success() {
158            let stderr = String::from_utf8_lossy(&output.stderr);
159            bail!("FFmpeg audio extraction failed: {}", stderr);
160        }
161
162        // Read the WAV file
163        let reader = hound::WavReader::open(&temp_wav)
164            .context("Failed to open extracted audio")?;
165
166        let spec = reader.spec();
167        debug!("Audio spec: {:?}", spec);
168
169        let samples: Vec<f32> = reader
170            .into_samples::<i16>()
171            .filter_map(|s| s.ok())
172            .map(|s| s as f32 / 32768.0)
173            .collect();
174
175        // Clean up temp file
176        let _ = std::fs::remove_file(&temp_wav);
177
178        info!("Extracted {} samples at {}Hz", samples.len(), spec.sample_rate);
179
180        Ok(AudioData {
181            samples,
182            sample_rate: spec.sample_rate,
183            channels: spec.channels as u32,
184            duration_secs: 0.0, // Will be calculated
185        })
186    }
187
188    /// Perform complete frequency analysis on audio data.
189    pub fn analyze(&self, audio: &AudioData) -> Result<FrequencyAnalysis> {
190        let analyzer = FrequencyAnalyzer::new(self.fft_size, self.hop_size);
191        analyzer.analyze(&audio.samples, audio.sample_rate)
192    }
193
194    /// Get the dominant frequencies from audio.
195    pub fn dominant_frequencies(&self, audio: &AudioData, top_k: usize) -> Result<Vec<DominantFrequency>> {
196        let analyzer = FrequencyAnalyzer::new(self.fft_size, self.hop_size);
197        analyzer.dominant_frequencies(&audio.samples, audio.sample_rate, top_k)
198    }
199
200    /// Compute frequency signature for similarity matching.
201    pub fn compute_signature(&self, audio: &AudioData) -> Result<FrequencySignature> {
202        let analyzer = FrequencyAnalyzer::new(self.fft_size, self.hop_size);
203        analyzer.compute_signature(&audio.samples, audio.sample_rate)
204    }
205}
206
207/// Process a video file through the complete frequency analysis pipeline.
208pub async fn process_video(
209    video_path: impl AsRef<Path>,
210    config: ProcessingConfig,
211) -> Result<ProcessingResult> {
212    let video_path = video_path.as_ref();
213    info!("Processing video: {}", video_path.display());
214
215    let analyzer = AudioAnalyzer::new(config.sample_rate);
216    let audio = analyzer.extract_audio(video_path).await?;
217
218    let mut result = ProcessingResult {
219        content_id: uuid::Uuid::new_v4().to_string(),
220        fingerprint: None,
221        tags: Vec::new(),
222        thumbnail_timestamp: None,
223        signature: None,
224        dominant_frequencies: Vec::new(),
225    };
226
227    // Fingerprint
228    #[cfg(feature = "fingerprint")]
229    if config.enable_fingerprint {
230        let fingerprinter = Fingerprinter::new();
231        result.fingerprint = Some(fingerprinter.fingerprint(&audio)?);
232    }
233
234    // Auto-tagging
235    #[cfg(feature = "tagging")]
236    if config.enable_tagging {
237        let tagger = ContentTagger::new();
238        result.tags = tagger.predict(&audio)?;
239    }
240
241    // Thumbnail selection
242    #[cfg(feature = "thumbnail")]
243    if config.enable_thumbnail {
244        let selector = ThumbnailSelector::new();
245        if let Ok(timestamp) = selector.find_best_timestamp(video_path, &audio) {
246            result.thumbnail_timestamp = Some(timestamp);
247        }
248    }
249
250    // Frequency signature for recommendations
251    if config.enable_signature {
252        result.signature = Some(analyzer.compute_signature(&audio)?);
253    }
254
255    // Dominant frequencies
256    result.dominant_frequencies = analyzer.dominant_frequencies(&audio, 10)?;
257
258    Ok(result)
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_analyzer_creation() {
267        let analyzer = AudioAnalyzer::new(44100);
268        assert_eq!(analyzer.sample_rate, 44100);
269        assert_eq!(analyzer.fft_size, 4096);
270    }
271
272    #[test]
273    fn test_custom_fft_params() {
274        let analyzer = AudioAnalyzer::with_fft_params(48000, 8192, 4096);
275        assert_eq!(analyzer.sample_rate, 48000);
276        assert_eq!(analyzer.fft_size, 8192);
277        assert_eq!(analyzer.hop_size, 4096);
278    }
279}