1#![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
107pub struct AudioAnalyzer {
109 sample_rate: u32,
110 fft_size: usize,
111 hop_size: usize,
112}
113
114impl AudioAnalyzer {
115 pub fn new(sample_rate: u32) -> Self {
117 Self {
118 sample_rate,
119 fft_size: 4096,
120 hop_size: 2048,
121 }
122 }
123
124 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 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 let temp_dir = std::env::temp_dir();
141 let temp_wav = temp_dir.join(format!("kino_audio_{}.wav", uuid::Uuid::new_v4()));
142
143 let output = Command::new("ffmpeg")
145 .args([
146 "-i", &video_path.to_string_lossy(),
147 "-vn", "-acodec", "pcm_s16le", "-ar", &self.sample_rate.to_string(), "-ac", "1", "-y", &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 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 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, })
186 }
187
188 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 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 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
207pub 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 #[cfg(feature = "fingerprint")]
229 if config.enable_fingerprint {
230 let fingerprinter = Fingerprinter::new();
231 result.fingerprint = Some(fingerprinter.fingerprint(&audio)?);
232 }
233
234 #[cfg(feature = "tagging")]
236 if config.enable_tagging {
237 let tagger = ContentTagger::new();
238 result.tags = tagger.predict(&audio)?;
239 }
240
241 #[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 if config.enable_signature {
252 result.signature = Some(analyzer.compute_signature(&audio)?);
253 }
254
255 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}