use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub type Timestamp = DateTime<Utc>;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct EmbeddingId(pub String);
impl EmbeddingId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn generate() -> Self {
Self(Uuid::new_v4().to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<String> for EmbeddingId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for EmbeddingId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl std::fmt::Display for EmbeddingId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ClusterId(pub String);
impl ClusterId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ClusterId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SegmentId(pub String);
impl SegmentId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidencePack {
pub id: String,
pub query_embedding_id: EmbeddingId,
pub neighbors: Vec<NeighborEvidence>,
pub cluster_context: ClusterContext,
pub sequence_context: Option<SequenceContext>,
pub interpretation: Interpretation,
pub created_at: Timestamp,
}
impl EvidencePack {
pub fn new(
query_embedding_id: EmbeddingId,
neighbors: Vec<NeighborEvidence>,
cluster_context: ClusterContext,
sequence_context: Option<SequenceContext>,
interpretation: Interpretation,
) -> Self {
Self {
id: Uuid::new_v4().to_string(),
query_embedding_id,
neighbors,
cluster_context,
sequence_context,
interpretation,
created_at: Utc::now(),
}
}
pub fn overall_confidence(&self) -> f32 {
let neighbor_confidence = if self.neighbors.is_empty() {
0.0
} else {
let avg_distance: f32 = self.neighbors.iter().map(|n| n.distance).sum::<f32>()
/ self.neighbors.len() as f32;
(1.0 - avg_distance.min(1.0)).max(0.0)
};
let cluster_confidence = self.cluster_context.confidence;
let interpretation_confidence = self.interpretation.confidence;
(neighbor_confidence * 0.3 + cluster_confidence * 0.3 + interpretation_confidence * 0.4)
}
pub fn evidence_source_count(&self) -> usize {
let mut count = 0;
if !self.neighbors.is_empty() {
count += 1;
}
if self.cluster_context.assigned_cluster.is_some() {
count += 1;
}
if self.sequence_context.is_some() {
count += 1;
}
count
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NeighborEvidence {
pub embedding_id: EmbeddingId,
pub distance: f32,
pub cluster_id: Option<ClusterId>,
pub recording_metadata: RecordingMetadata,
pub spectrogram_url: Option<String>,
}
impl NeighborEvidence {
pub fn new(
embedding_id: EmbeddingId,
distance: f32,
recording_metadata: RecordingMetadata,
) -> Self {
Self {
embedding_id,
distance,
cluster_id: None,
recording_metadata,
spectrogram_url: None,
}
}
pub fn with_cluster(mut self, cluster_id: ClusterId) -> Self {
self.cluster_id = Some(cluster_id);
self
}
pub fn with_spectrogram(mut self, url: String) -> Self {
self.spectrogram_url = Some(url);
self
}
pub fn similarity(&self) -> f32 {
(1.0 - self.distance).max(0.0).min(1.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecordingMetadata {
pub recording_id: String,
pub taxon: Option<String>,
pub location: Option<GeoLocation>,
pub recorded_at: Option<Timestamp>,
pub duration_seconds: Option<f32>,
pub sample_rate: Option<u32>,
pub tags: Vec<String>,
}
impl RecordingMetadata {
pub fn new(recording_id: impl Into<String>) -> Self {
Self {
recording_id: recording_id.into(),
taxon: None,
location: None,
recorded_at: None,
duration_seconds: None,
sample_rate: None,
tags: Vec::new(),
}
}
pub fn with_taxon(mut self, taxon: impl Into<String>) -> Self {
self.taxon = Some(taxon.into());
self
}
pub fn with_location(mut self, location: GeoLocation) -> Self {
self.location = Some(location);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeoLocation {
pub latitude: f64,
pub longitude: f64,
pub elevation_meters: Option<f32>,
pub locality: Option<String>,
}
impl GeoLocation {
pub fn new(latitude: f64, longitude: f64) -> Self {
Self {
latitude,
longitude,
elevation_meters: None,
locality: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClusterContext {
pub assigned_cluster: Option<ClusterId>,
pub cluster_label: Option<String>,
pub confidence: f32,
pub exemplar_similarity: f32,
}
impl ClusterContext {
pub fn new(
assigned_cluster: Option<ClusterId>,
confidence: f32,
exemplar_similarity: f32,
) -> Self {
Self {
assigned_cluster,
cluster_label: None,
confidence,
exemplar_similarity,
}
}
pub fn empty() -> Self {
Self {
assigned_cluster: None,
cluster_label: None,
confidence: 0.0,
exemplar_similarity: 0.0,
}
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.cluster_label = Some(label.into());
self
}
pub fn has_cluster(&self) -> bool {
self.assigned_cluster.is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SequenceContext {
pub preceding_segments: Vec<SegmentId>,
pub following_segments: Vec<SegmentId>,
pub detected_motif: Option<String>,
}
impl SequenceContext {
pub fn new(
preceding_segments: Vec<SegmentId>,
following_segments: Vec<SegmentId>,
) -> Self {
Self {
preceding_segments,
following_segments,
detected_motif: None,
}
}
pub fn empty() -> Self {
Self {
preceding_segments: Vec::new(),
following_segments: Vec::new(),
detected_motif: None,
}
}
pub fn with_motif(mut self, motif: impl Into<String>) -> Self {
self.detected_motif = Some(motif.into());
self
}
pub fn has_temporal_context(&self) -> bool {
!self.preceding_segments.is_empty() || !self.following_segments.is_empty()
}
pub fn sequence_length(&self) -> usize {
self.preceding_segments.len() + 1 + self.following_segments.len()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Interpretation {
pub structural_description: String,
pub claims: Vec<Claim>,
pub confidence: f32,
}
impl Interpretation {
pub fn new(structural_description: String, claims: Vec<Claim>, confidence: f32) -> Self {
Self {
structural_description,
claims,
confidence: confidence.clamp(0.0, 1.0),
}
}
pub fn empty() -> Self {
Self {
structural_description: String::new(),
claims: Vec::new(),
confidence: 0.0,
}
}
pub fn add_claim(&mut self, claim: Claim) {
self.claims.push(claim);
self.recalculate_confidence();
}
fn recalculate_confidence(&mut self) {
if self.claims.is_empty() {
return;
}
let total_confidence: f32 = self.claims.iter().map(|c| c.confidence).sum();
self.confidence = total_confidence / self.claims.len() as f32;
}
pub fn high_confidence_claims(&self, threshold: f32) -> Vec<&Claim> {
self.claims
.iter()
.filter(|c| c.confidence >= threshold)
.collect()
}
pub fn evidenced_claim_count(&self) -> usize {
self.claims
.iter()
.filter(|c| !c.evidence_refs.is_empty())
.count()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claim {
pub statement: String,
pub evidence_refs: Vec<EvidenceRef>,
pub confidence: f32,
}
impl Claim {
pub fn new(statement: impl Into<String>, confidence: f32) -> Self {
Self {
statement: statement.into(),
evidence_refs: Vec::new(),
confidence: confidence.clamp(0.0, 1.0),
}
}
pub fn add_evidence(&mut self, evidence_ref: EvidenceRef) {
self.evidence_refs.push(evidence_ref);
}
pub fn with_evidence(mut self, evidence_refs: Vec<EvidenceRef>) -> Self {
self.evidence_refs = evidence_refs;
self
}
pub fn has_evidence(&self) -> bool {
!self.evidence_refs.is_empty()
}
pub fn evidence_of_type(&self, ref_type: EvidenceRefType) -> Vec<&EvidenceRef> {
self.evidence_refs
.iter()
.filter(|e| e.ref_type == ref_type)
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceRef {
pub ref_type: EvidenceRefType,
pub ref_id: String,
pub description: String,
}
impl EvidenceRef {
pub fn new(ref_type: EvidenceRefType, ref_id: impl Into<String>, description: impl Into<String>) -> Self {
Self {
ref_type,
ref_id: ref_id.into(),
description: description.into(),
}
}
pub fn neighbor(embedding_id: &EmbeddingId, description: impl Into<String>) -> Self {
Self::new(EvidenceRefType::Neighbor, embedding_id.as_str(), description)
}
pub fn cluster(cluster_id: &ClusterId, description: impl Into<String>) -> Self {
Self::new(EvidenceRefType::Cluster, cluster_id.as_str(), description)
}
pub fn sequence(segment_id: &SegmentId, description: impl Into<String>) -> Self {
Self::new(EvidenceRefType::Sequence, segment_id.as_str(), description)
}
pub fn taxon(taxon_name: impl Into<String>, description: impl Into<String>) -> Self {
Self::new(EvidenceRefType::Taxon, taxon_name, description)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum EvidenceRefType {
Neighbor,
Cluster,
Sequence,
Taxon,
}
impl std::fmt::Display for EvidenceRefType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EvidenceRefType::Neighbor => write!(f, "neighbor"),
EvidenceRefType::Cluster => write!(f, "cluster"),
EvidenceRefType::Sequence => write!(f, "sequence"),
EvidenceRefType::Taxon => write!(f, "taxon"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_embedding_id() {
let id = EmbeddingId::new("test-123");
assert_eq!(id.as_str(), "test-123");
assert_eq!(id.to_string(), "test-123");
let generated = EmbeddingId::generate();
assert!(!generated.as_str().is_empty());
}
#[test]
fn test_neighbor_evidence_similarity() {
let metadata = RecordingMetadata::new("rec-1");
let evidence = NeighborEvidence::new(
EmbeddingId::new("emb-1"),
0.2,
metadata,
);
assert_eq!(evidence.similarity(), 0.8);
let far_evidence = NeighborEvidence::new(
EmbeddingId::new("emb-2"),
1.5,
RecordingMetadata::new("rec-2"),
);
assert_eq!(far_evidence.similarity(), 0.0);
}
#[test]
fn test_cluster_context() {
let context = ClusterContext::new(
Some(ClusterId::new("cluster-1")),
0.85,
0.92,
).with_label("Song Type A");
assert!(context.has_cluster());
assert_eq!(context.cluster_label, Some("Song Type A".to_string()));
}
#[test]
fn test_sequence_context() {
let context = SequenceContext::new(
vec![SegmentId::new("seg-1"), SegmentId::new("seg-2")],
vec![SegmentId::new("seg-4")],
).with_motif("ABAB");
assert!(context.has_temporal_context());
assert_eq!(context.sequence_length(), 4);
assert_eq!(context.detected_motif, Some("ABAB".to_string()));
}
#[test]
fn test_claim_with_evidence() {
let mut claim = Claim::new("This is a dawn chorus vocalization", 0.9);
claim.add_evidence(EvidenceRef::neighbor(
&EmbeddingId::new("emb-1"),
"Similar to known dawn chorus recording",
));
claim.add_evidence(EvidenceRef::cluster(
&ClusterId::new("cluster-5"),
"Assigned to dawn chorus cluster",
));
assert!(claim.has_evidence());
assert_eq!(claim.evidence_refs.len(), 2);
assert_eq!(claim.evidence_of_type(EvidenceRefType::Neighbor).len(), 1);
}
#[test]
fn test_interpretation_confidence() {
let mut interp = Interpretation::new(
"Complex harmonic structure with frequency modulation".to_string(),
Vec::new(),
0.0,
);
interp.add_claim(Claim::new("Claim 1", 0.8));
interp.add_claim(Claim::new("Claim 2", 0.6));
assert_eq!(interp.confidence, 0.7);
}
#[test]
fn test_evidence_pack_overall_confidence() {
let pack = EvidencePack::new(
EmbeddingId::new("query-1"),
vec![
NeighborEvidence::new(
EmbeddingId::new("n-1"),
0.1,
RecordingMetadata::new("r-1"),
),
NeighborEvidence::new(
EmbeddingId::new("n-2"),
0.2,
RecordingMetadata::new("r-2"),
),
],
ClusterContext::new(Some(ClusterId::new("c-1")), 0.9, 0.85),
None,
Interpretation::new("Test".to_string(), vec![Claim::new("Test", 0.8)], 0.8),
);
let confidence = pack.overall_confidence();
assert!(confidence > 0.0 && confidence <= 1.0);
assert_eq!(pack.evidence_source_count(), 2);
}
}