use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::EMBEDDING_DIM;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct EmbeddingId(Uuid);
impl EmbeddingId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::new_v4())
}
#[must_use]
pub const fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
#[must_use]
pub const fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for EmbeddingId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for EmbeddingId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SegmentId(Uuid);
impl SegmentId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::new_v4())
}
#[must_use]
pub const fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
#[must_use]
pub const fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for SegmentId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for SegmentId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum StorageTier {
Hot,
Warm,
Cold,
}
impl Default for StorageTier {
fn default() -> Self {
Self::Hot
}
}
impl StorageTier {
#[must_use]
pub const fn bytes_per_dim(&self) -> usize {
match self {
Self::Hot => 4, Self::Warm => 2, Self::Cold => 1, }
}
#[must_use]
pub const fn embedding_bytes(&self) -> usize {
self.bytes_per_dim() * EMBEDDING_DIM
}
}
pub type Timestamp = DateTime<Utc>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Embedding {
pub id: EmbeddingId,
pub segment_id: SegmentId,
pub vector: Vec<f32>,
pub model_version: String,
pub created_at: Timestamp,
pub tier: StorageTier,
pub metadata: EmbeddingMetadata,
}
impl Embedding {
pub fn new(
segment_id: SegmentId,
vector: Vec<f32>,
model_version: String,
) -> Result<Self, crate::EmbeddingError> {
if vector.len() != EMBEDDING_DIM {
return Err(crate::EmbeddingError::InvalidDimensions {
expected: EMBEDDING_DIM,
actual: vector.len(),
});
}
Ok(Self {
id: EmbeddingId::new(),
segment_id,
vector,
model_version,
created_at: Utc::now(),
tier: StorageTier::default(),
metadata: EmbeddingMetadata::default(),
})
}
#[must_use]
pub fn norm(&self) -> f32 {
self.vector
.iter()
.map(|x| x * x)
.sum::<f32>()
.sqrt()
}
#[must_use]
pub fn is_normalized(&self) -> bool {
let norm = self.norm();
(0.99..=1.01).contains(&norm)
}
#[must_use]
pub fn is_valid(&self) -> bool {
!self.vector.iter().any(|x| x.is_nan() || x.is_infinite())
}
#[must_use]
pub fn cosine_similarity(&self, other: &Self) -> f32 {
self.vector
.iter()
.zip(other.vector.iter())
.map(|(a, b)| a * b)
.sum()
}
pub fn set_tier(&mut self, tier: StorageTier) {
self.tier = tier;
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EmbeddingMetadata {
pub inference_latency_ms: Option<f32>,
pub batch_id: Option<String>,
pub gpu_used: bool,
pub original_norm: Option<f32>,
pub sparsity: Option<f32>,
pub quality_score: Option<f32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ModelVersion {
pub name: String,
pub version: String,
pub variant: String,
}
impl ModelVersion {
#[must_use]
pub fn new(name: impl Into<String>, version: impl Into<String>, variant: impl Into<String>) -> Self {
Self {
name: name.into(),
version: version.into(),
variant: variant.into(),
}
}
#[must_use]
pub fn perch_v2_base() -> Self {
Self::new("perch-v2", "2.0.0", "base")
}
#[must_use]
pub fn perch_v2_quantized() -> Self {
Self::new("perch-v2", "2.0.0", "quantized")
}
#[must_use]
pub fn full_version(&self) -> String {
format!("{}-{}-{}", self.name, self.version, self.variant)
}
}
impl Default for ModelVersion {
fn default() -> Self {
Self::perch_v2_base()
}
}
impl std::fmt::Display for ModelVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.full_version())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InputSpecification {
pub sample_rate: u32,
pub window_duration: f32,
pub window_samples: usize,
pub mel_bins: usize,
pub mel_frames: usize,
pub frequency_range: (f32, f32),
}
impl Default for InputSpecification {
fn default() -> Self {
Self {
sample_rate: crate::TARGET_SAMPLE_RATE,
window_duration: crate::TARGET_WINDOW_SECONDS,
window_samples: crate::TARGET_WINDOW_SAMPLES,
mel_bins: crate::MEL_BINS,
mel_frames: crate::MEL_FRAMES,
frequency_range: (60.0, 16000.0),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ModelStatus {
Active,
Loading,
Failed,
Deprecated,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddingModel {
pub name: String,
pub version: ModelVersion,
pub dimensions: usize,
pub checksum: String,
pub input_spec: InputSpecification,
pub status: ModelStatus,
pub loaded_at: Option<Timestamp>,
pub model_path: Option<String>,
}
impl EmbeddingModel {
#[must_use]
pub fn new(name: String, version: ModelVersion, checksum: String) -> Self {
Self {
name,
version,
dimensions: EMBEDDING_DIM,
checksum,
input_spec: InputSpecification::default(),
status: ModelStatus::Loading,
loaded_at: None,
model_path: None,
}
}
#[must_use]
pub fn perch_v2_default() -> Self {
Self::new(
"perch-v2".to_string(),
ModelVersion::perch_v2_base(),
String::new(), )
}
#[must_use]
pub const fn is_ready(&self) -> bool {
matches!(self.status, ModelStatus::Active)
}
pub fn mark_active(&mut self) {
self.status = ModelStatus::Active;
self.loaded_at = Some(Utc::now());
}
pub fn mark_failed(&mut self) {
self.status = ModelStatus::Failed;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddingBatch {
pub id: String,
pub segment_ids: Vec<SegmentId>,
pub status: BatchStatus,
pub started_at: Timestamp,
pub completed_at: Option<Timestamp>,
pub metrics: BatchMetrics,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BatchStatus {
Pending,
Processing,
Completed,
Failed,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BatchMetrics {
pub total_segments: u32,
pub success_count: u32,
pub failure_count: u32,
pub avg_latency_ms: f32,
pub throughput: f32,
}
impl EmbeddingBatch {
#[must_use]
pub fn new(segment_ids: Vec<SegmentId>) -> Self {
let total = segment_ids.len() as u32;
Self {
id: Uuid::new_v4().to_string(),
segment_ids,
status: BatchStatus::Pending,
started_at: Utc::now(),
completed_at: None,
metrics: BatchMetrics {
total_segments: total,
..Default::default()
},
}
}
pub fn mark_processing(&mut self) {
self.status = BatchStatus::Processing;
}
pub fn mark_completed(&mut self, success_count: u32, failure_count: u32, avg_latency_ms: f32) {
self.status = BatchStatus::Completed;
self.completed_at = Some(Utc::now());
self.metrics.success_count = success_count;
self.metrics.failure_count = failure_count;
self.metrics.avg_latency_ms = avg_latency_ms;
if let Some(completed) = self.completed_at {
let duration = (completed - self.started_at).num_milliseconds() as f32 / 1000.0;
if duration > 0.0 {
self.metrics.throughput = self.metrics.total_segments as f32 / duration;
}
}
}
pub fn mark_failed(&mut self) {
self.status = BatchStatus::Failed;
self.completed_at = Some(Utc::now());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_embedding_id_generation() {
let id1 = EmbeddingId::new();
let id2 = EmbeddingId::new();
assert_ne!(id1, id2);
}
#[test]
fn test_embedding_creation() {
let segment_id = SegmentId::new();
let vector = vec![0.0; EMBEDDING_DIM];
let embedding = Embedding::new(segment_id, vector, "perch-v2-2.0.0-base".to_string());
assert!(embedding.is_ok());
}
#[test]
fn test_embedding_invalid_dimensions() {
let segment_id = SegmentId::new();
let vector = vec![0.0; 100]; let result = Embedding::new(segment_id, vector, "perch-v2-2.0.0-base".to_string());
assert!(result.is_err());
}
#[test]
fn test_embedding_norm() {
let segment_id = SegmentId::new();
let mut vector = vec![0.0; EMBEDDING_DIM];
vector[0] = 1.0; let embedding = Embedding::new(segment_id, vector, "perch-v2-2.0.0-base".to_string()).unwrap();
assert!((embedding.norm() - 1.0).abs() < 1e-6);
assert!(embedding.is_normalized());
}
#[test]
fn test_cosine_similarity() {
let segment_id = SegmentId::new();
let mut vector = vec![0.0; EMBEDDING_DIM];
vector[0] = 1.0;
let emb1 = Embedding::new(segment_id, vector.clone(), "test".to_string()).unwrap();
let emb2 = Embedding::new(segment_id, vector, "test".to_string()).unwrap();
let similarity = emb1.cosine_similarity(&emb2);
assert!((similarity - 1.0).abs() < 1e-6);
}
#[test]
fn test_storage_tier_bytes() {
assert_eq!(StorageTier::Hot.bytes_per_dim(), 4);
assert_eq!(StorageTier::Warm.bytes_per_dim(), 2);
assert_eq!(StorageTier::Cold.bytes_per_dim(), 1);
assert_eq!(StorageTier::Hot.embedding_bytes(), EMBEDDING_DIM * 4);
}
#[test]
fn test_model_version() {
let version = ModelVersion::perch_v2_base();
assert_eq!(version.name, "perch-v2");
assert_eq!(version.version, "2.0.0");
assert_eq!(version.variant, "base");
assert_eq!(version.full_version(), "perch-v2-2.0.0-base");
}
#[test]
fn test_input_specification_defaults() {
let spec = InputSpecification::default();
assert_eq!(spec.sample_rate, 32000);
assert_eq!(spec.window_samples, 160_000);
assert_eq!(spec.mel_bins, 128);
assert_eq!(spec.mel_frames, 500);
}
#[test]
fn test_embedding_batch_lifecycle() {
let segment_ids = vec![SegmentId::new(), SegmentId::new()];
let mut batch = EmbeddingBatch::new(segment_ids);
assert_eq!(batch.status, BatchStatus::Pending);
assert_eq!(batch.metrics.total_segments, 2);
batch.mark_processing();
assert_eq!(batch.status, BatchStatus::Processing);
batch.mark_completed(2, 0, 50.0);
assert_eq!(batch.status, BatchStatus::Completed);
assert_eq!(batch.metrics.success_count, 2);
assert!(batch.completed_at.is_some());
}
}