use std::sync::Arc;
use std::time::Instant;
use ndarray::Array3;
use rayon::prelude::*;
use tracing::{debug, info, instrument, warn};
use crate::domain::entities::{
Embedding, EmbeddingBatch, EmbeddingMetadata, SegmentId, StorageTier,
};
use crate::infrastructure::model_manager::ModelManager;
use crate::normalization;
use crate::{EmbeddingError, EMBEDDING_DIM, MEL_BINS, MEL_FRAMES};
#[derive(Debug, Clone)]
pub struct Spectrogram {
pub data: Array3<f32>,
pub segment_id: SegmentId,
pub metadata: SpectrogramMetadata,
}
#[derive(Debug, Clone, Default)]
pub struct SpectrogramMetadata {
pub sample_rate: Option<u32>,
pub duration_secs: Option<f32>,
pub snr: Option<f32>,
}
impl Spectrogram {
pub fn new(
data: ndarray::Array2<f32>,
segment_id: SegmentId,
) -> Result<Self, EmbeddingError> {
let shape = data.shape();
if shape[0] != MEL_FRAMES || shape[1] != MEL_BINS {
return Err(EmbeddingError::InvalidDimensions {
expected: MEL_FRAMES * MEL_BINS,
actual: shape[0] * shape[1],
});
}
let data = data.insert_axis(ndarray::Axis(0));
Ok(Self {
data,
segment_id,
metadata: SpectrogramMetadata::default(),
})
}
pub fn from_array3(data: Array3<f32>, segment_id: SegmentId) -> Result<Self, EmbeddingError> {
let shape = data.shape();
if shape[1] != MEL_FRAMES || shape[2] != MEL_BINS {
return Err(EmbeddingError::InvalidDimensions {
expected: MEL_FRAMES * MEL_BINS,
actual: shape[1] * shape[2],
});
}
Ok(Self {
data,
segment_id,
metadata: SpectrogramMetadata::default(),
})
}
pub fn with_metadata(mut self, metadata: SpectrogramMetadata) -> Self {
self.metadata = metadata;
self
}
}
#[derive(Debug, Clone)]
pub struct EmbeddingOutput {
pub embedding: Embedding,
pub gpu_used: bool,
pub latency_ms: f32,
}
#[derive(Debug, Clone)]
pub struct EmbeddingServiceConfig {
pub batch_size: usize,
pub normalize: bool,
pub default_tier: StorageTier,
pub validate_embeddings: bool,
pub max_sparsity: f32,
}
impl Default for EmbeddingServiceConfig {
fn default() -> Self {
Self {
batch_size: 8,
normalize: true,
default_tier: StorageTier::Hot,
validate_embeddings: true,
max_sparsity: 0.9,
}
}
}
pub struct EmbeddingService {
model_manager: Arc<ModelManager>,
config: EmbeddingServiceConfig,
}
impl EmbeddingService {
#[must_use]
pub fn new(model_manager: Arc<ModelManager>, batch_size: usize) -> Self {
Self {
model_manager,
config: EmbeddingServiceConfig {
batch_size,
..Default::default()
},
}
}
#[must_use]
pub fn with_config(model_manager: Arc<ModelManager>, config: EmbeddingServiceConfig) -> Self {
Self {
model_manager,
config,
}
}
#[instrument(skip(self, spectrogram), fields(segment_id = %spectrogram.segment_id))]
pub async fn embed_segment(
&self,
spectrogram: &Spectrogram,
) -> Result<EmbeddingOutput, EmbeddingError> {
let start = Instant::now();
let inference = self.model_manager.get_inference().await?;
let model_version = self.model_manager.current_version();
let raw_embedding = inference.run(&spectrogram.data)?;
let mut vector: Vec<f32> = raw_embedding.iter().copied().collect();
let original_norm = normalization::compute_norm(&vector);
if self.config.normalize {
normalization::l2_normalize(&mut vector);
}
if self.config.validate_embeddings {
self.validate_embedding(&vector)?;
}
let sparsity = normalization::compute_sparsity(&vector);
let mut embedding = Embedding::new(
spectrogram.segment_id,
vector,
model_version.full_version(),
)?;
let latency_ms = start.elapsed().as_secs_f32() * 1000.0;
embedding.metadata = EmbeddingMetadata {
inference_latency_ms: Some(latency_ms),
batch_id: None,
gpu_used: inference.is_gpu(),
original_norm: Some(original_norm),
sparsity: Some(sparsity),
quality_score: Some(self.compute_quality_score(&embedding)),
};
embedding.tier = self.config.default_tier;
debug!(
latency_ms = latency_ms,
norm = embedding.norm(),
sparsity = sparsity,
"Generated embedding"
);
Ok(EmbeddingOutput {
embedding,
gpu_used: inference.is_gpu(),
latency_ms,
})
}
#[instrument(skip(self, spectrograms), fields(count = spectrograms.len()))]
pub async fn embed_batch(
&self,
spectrograms: &[Spectrogram],
) -> Result<Vec<EmbeddingOutput>, EmbeddingError> {
if spectrograms.is_empty() {
return Ok(Vec::new());
}
let total_start = Instant::now();
let batch_id = uuid::Uuid::new_v4().to_string();
info!(
batch_id = %batch_id,
total_segments = spectrograms.len(),
batch_size = self.config.batch_size,
"Starting batch embedding"
);
let inference = self.model_manager.get_inference().await?;
let model_version = self.model_manager.current_version();
let mut all_outputs = Vec::with_capacity(spectrograms.len());
for (batch_idx, chunk) in spectrograms.chunks(self.config.batch_size).enumerate() {
let batch_start = Instant::now();
let inputs: Vec<&Array3<f32>> = chunk.iter().map(|s| &s.data).collect();
let raw_embeddings = inference.run_batch(&inputs)?;
let batch_latency_ms = batch_start.elapsed().as_secs_f32() * 1000.0;
let per_item_latency = batch_latency_ms / chunk.len() as f32;
let outputs: Vec<Result<EmbeddingOutput, EmbeddingError>> = chunk
.par_iter()
.zip(raw_embeddings.par_iter())
.map(|(spectrogram, raw_emb)| {
let mut vector: Vec<f32> = raw_emb.iter().copied().collect();
let original_norm = normalization::compute_norm(&vector);
if self.config.normalize {
normalization::l2_normalize(&mut vector);
}
if self.config.validate_embeddings {
self.validate_embedding(&vector)?;
}
let sparsity = normalization::compute_sparsity(&vector);
let mut embedding = Embedding::new(
spectrogram.segment_id,
vector,
model_version.full_version(),
)?;
embedding.metadata = EmbeddingMetadata {
inference_latency_ms: Some(per_item_latency),
batch_id: Some(batch_id.clone()),
gpu_used: inference.is_gpu(),
original_norm: Some(original_norm),
sparsity: Some(sparsity),
quality_score: Some(self.compute_quality_score(&embedding)),
};
embedding.tier = self.config.default_tier;
Ok(EmbeddingOutput {
embedding,
gpu_used: inference.is_gpu(),
latency_ms: per_item_latency,
})
})
.collect();
let batch_outputs: Result<Vec<_>, _> = outputs.into_iter().collect();
all_outputs.extend(batch_outputs?);
debug!(
batch_idx = batch_idx,
batch_size = chunk.len(),
latency_ms = batch_latency_ms,
"Completed batch"
);
}
let total_latency_ms = total_start.elapsed().as_secs_f32() * 1000.0;
let throughput = spectrograms.len() as f32 / (total_latency_ms / 1000.0);
info!(
batch_id = %batch_id,
total_segments = spectrograms.len(),
total_latency_ms = total_latency_ms,
throughput_per_sec = throughput,
"Completed batch embedding"
);
Ok(all_outputs)
}
#[must_use]
pub fn create_batch(&self, segment_ids: Vec<SegmentId>) -> EmbeddingBatch {
EmbeddingBatch::new(segment_ids)
}
fn validate_embedding(&self, vector: &[f32]) -> Result<(), EmbeddingError> {
if vector.len() != EMBEDDING_DIM {
return Err(EmbeddingError::InvalidDimensions {
expected: EMBEDDING_DIM,
actual: vector.len(),
});
}
if vector.iter().any(|x| x.is_nan()) {
return Err(EmbeddingError::Validation(
"Embedding contains NaN values".to_string(),
));
}
if vector.iter().any(|x| x.is_infinite()) {
return Err(EmbeddingError::Validation(
"Embedding contains infinite values".to_string(),
));
}
let sparsity = normalization::compute_sparsity(vector);
if sparsity > self.config.max_sparsity {
warn!(
sparsity = sparsity,
max_sparsity = self.config.max_sparsity,
"Embedding has high sparsity"
);
}
Ok(())
}
fn compute_quality_score(&self, embedding: &Embedding) -> f32 {
let mut score = 1.0_f32;
let norm = embedding.norm();
let norm_deviation = (norm - 1.0).abs();
score -= norm_deviation * 0.5;
if let Some(sparsity) = embedding.metadata.sparsity {
score -= sparsity * 0.3;
}
score.clamp(0.0, 1.0)
}
#[must_use]
pub fn model_version(&self) -> String {
self.model_manager.current_version().full_version()
}
pub async fn is_ready(&self) -> bool {
self.model_manager.is_ready().await
}
}
#[derive(Debug)]
pub struct EmbeddingServiceBuilder {
model_manager: Option<Arc<ModelManager>>,
config: EmbeddingServiceConfig,
}
impl EmbeddingServiceBuilder {
#[must_use]
pub fn new() -> Self {
Self {
model_manager: None,
config: EmbeddingServiceConfig::default(),
}
}
#[must_use]
pub fn model_manager(mut self, manager: Arc<ModelManager>) -> Self {
self.model_manager = Some(manager);
self
}
#[must_use]
pub fn batch_size(mut self, size: usize) -> Self {
self.config.batch_size = size;
self
}
#[must_use]
pub fn normalize(mut self, normalize: bool) -> Self {
self.config.normalize = normalize;
self
}
#[must_use]
pub fn default_tier(mut self, tier: StorageTier) -> Self {
self.config.default_tier = tier;
self
}
#[must_use]
pub fn validate_embeddings(mut self, validate: bool) -> Self {
self.config.validate_embeddings = validate;
self
}
pub fn build(self) -> Result<EmbeddingService, EmbeddingError> {
let model_manager = self.model_manager.ok_or_else(|| {
EmbeddingError::Validation("Model manager is required".to_string())
})?;
Ok(EmbeddingService::with_config(model_manager, self.config))
}
}
impl Default for EmbeddingServiceBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::Array2;
#[test]
fn test_spectrogram_creation() {
let data = Array2::zeros((MEL_FRAMES, MEL_BINS));
let segment_id = SegmentId::new();
let spec = Spectrogram::new(data, segment_id);
assert!(spec.is_ok());
}
#[test]
fn test_spectrogram_invalid_dimensions() {
let data = Array2::zeros((100, 100)); let segment_id = SegmentId::new();
let spec = Spectrogram::new(data, segment_id);
assert!(spec.is_err());
}
#[test]
fn test_service_config_default() {
let config = EmbeddingServiceConfig::default();
assert_eq!(config.batch_size, 8);
assert!(config.normalize);
assert!(config.validate_embeddings);
}
#[test]
fn test_service_builder() {
let builder = EmbeddingServiceBuilder::new()
.batch_size(16)
.normalize(false)
.default_tier(StorageTier::Warm);
assert_eq!(builder.config.batch_size, 16);
assert!(!builder.config.normalize);
assert_eq!(builder.config.default_tier, StorageTier::Warm);
}
}