use crate::data_quality_validation::{DataQualityValidator, DatasetValidationReport};
use crate::VoirsError;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum GroundTruthError {
#[error("Dataset not found: {0}")]
DatasetNotFound(String),
#[error("Invalid dataset format: {0}")]
InvalidFormat(String),
#[error("Annotation validation failed: {0}")]
AnnotationValidationFailed(String),
#[error("Version conflict detected: {0}")]
VersionConflict(String),
#[error("Dataset corruption detected: {0}")]
DatasetCorruption(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AnnotationQuality {
Expert,
Professional,
Community,
Automatic,
Research,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum AnnotationType {
QualityScore,
PronunciationAccuracy,
Naturalness,
Intelligibility,
EmotionalExpression,
ProsodicAccuracy,
SpeakerSimilarity,
TechnicalQuality,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroundTruthAnnotation {
pub id: String,
pub sample_id: String,
pub annotation_type: AnnotationType,
pub value: f64,
pub scale: String,
pub annotator_id: String,
pub quality_level: AnnotationQuality,
pub confidence: f64,
pub created_at: DateTime<Utc>,
pub description: Option<String>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroundTruthSample {
pub id: String,
pub audio_path: PathBuf,
pub reference_path: Option<PathBuf>,
pub transcript: String,
pub language: String,
pub speaker_id: String,
pub sample_rate: u32,
pub duration: f64,
pub metadata: HashMap<String, String>,
pub annotations: Vec<GroundTruthAnnotation>,
pub validation_status: ValidationStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ValidationStatus {
Pending,
Valid,
Invalid,
InProgress,
NeedsReview,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroundTruthDataset {
pub id: String,
pub name: String,
pub version: String,
pub description: String,
pub created_at: DateTime<Utc>,
pub modified_at: DateTime<Utc>,
pub creator: String,
pub license: String,
pub languages: Vec<String>,
pub sample_count: usize,
pub total_duration: f64,
pub domain: String,
pub annotation_guidelines: Option<String>,
pub samples: Vec<GroundTruthSample>,
pub quality_metrics: DatasetQualityMetrics,
pub tags: Vec<String>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatasetQualityMetrics {
pub overall_quality: f64,
pub annotation_consistency: f64,
pub inter_annotator_agreement: Option<f64>,
pub audio_quality: f64,
pub metadata_completeness: f64,
pub validation_completion: f64,
pub quality_by_type: HashMap<AnnotationType, f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatasetVersion {
pub version: String,
pub created_at: DateTime<Utc>,
pub description: String,
pub changes: Vec<String>,
pub previous_version: Option<String>,
pub dataset_path: PathBuf,
pub metadata: HashMap<String, String>,
}
#[derive(Debug)]
pub struct GroundTruthManager {
base_path: PathBuf,
catalog: HashMap<String, GroundTruthDataset>,
version_history: HashMap<String, Vec<DatasetVersion>>,
validator: DataQualityValidator,
}
impl GroundTruthManager {
pub fn new(base_path: PathBuf) -> Self {
Self {
base_path,
catalog: HashMap::new(),
version_history: HashMap::new(),
validator: DataQualityValidator::default(),
}
}
pub async fn initialize(&mut self) -> Result<(), GroundTruthError> {
self.ensure_directory_structure().await?;
self.load_catalog().await?;
self.load_version_history().await?;
Ok(())
}
async fn ensure_directory_structure(&self) -> Result<(), GroundTruthError> {
let datasets_dir = self.base_path.join("datasets");
let versions_dir = self.base_path.join("versions");
let annotations_dir = self.base_path.join("annotations");
let exports_dir = self.base_path.join("exports");
tokio::fs::create_dir_all(&datasets_dir).await?;
tokio::fs::create_dir_all(&versions_dir).await?;
tokio::fs::create_dir_all(&annotations_dir).await?;
tokio::fs::create_dir_all(&exports_dir).await?;
Ok(())
}
async fn load_catalog(&mut self) -> Result<(), GroundTruthError> {
let catalog_path = self.base_path.join("catalog.json");
if catalog_path.exists() {
let content = tokio::fs::read_to_string(&catalog_path).await?;
self.catalog = serde_json::from_str(&content)?;
}
Ok(())
}
async fn save_catalog(&self) -> Result<(), GroundTruthError> {
let catalog_path = self.base_path.join("catalog.json");
let content = serde_json::to_string_pretty(&self.catalog)?;
tokio::fs::write(&catalog_path, content).await?;
Ok(())
}
async fn load_version_history(&mut self) -> Result<(), GroundTruthError> {
let versions_path = self.base_path.join("versions.json");
if versions_path.exists() {
let content = tokio::fs::read_to_string(&versions_path).await?;
self.version_history = serde_json::from_str(&content)?;
}
Ok(())
}
async fn save_version_history(&self) -> Result<(), GroundTruthError> {
let versions_path = self.base_path.join("versions.json");
let content = serde_json::to_string_pretty(&self.version_history)?;
tokio::fs::write(&versions_path, content).await?;
Ok(())
}
pub async fn create_dataset(
&mut self,
name: String,
description: String,
creator: String,
license: String,
domain: String,
languages: Vec<String>,
) -> Result<String, GroundTruthError> {
let id = uuid::Uuid::new_v4().to_string();
let now = Utc::now();
let dataset = GroundTruthDataset {
id: id.clone(),
name,
version: "1.0.0".to_string(),
description,
created_at: now,
modified_at: now,
creator,
license,
languages,
sample_count: 0,
total_duration: 0.0,
domain,
annotation_guidelines: None,
samples: Vec::new(),
quality_metrics: DatasetQualityMetrics {
overall_quality: 0.0,
annotation_consistency: 0.0,
inter_annotator_agreement: None,
audio_quality: 0.0,
metadata_completeness: 0.0,
validation_completion: 0.0,
quality_by_type: HashMap::new(),
},
tags: Vec::new(),
metadata: HashMap::new(),
};
let dataset_dir = self.base_path.join("datasets").join(&id);
tokio::fs::create_dir_all(&dataset_dir).await?;
self.catalog.insert(id.clone(), dataset);
self.save_catalog().await?;
self.create_version(
&id,
"1.0.0",
"Initial dataset creation".to_string(),
Vec::new(),
)
.await?;
Ok(id)
}
pub async fn add_sample(
&mut self,
dataset_id: &str,
audio_path: PathBuf,
reference_path: Option<PathBuf>,
transcript: String,
language: String,
speaker_id: String,
metadata: HashMap<String, String>,
) -> Result<String, GroundTruthError> {
let dataset = self
.catalog
.get_mut(dataset_id)
.ok_or_else(|| GroundTruthError::DatasetNotFound(dataset_id.to_string()))?;
if !audio_path.exists() {
return Err(GroundTruthError::InvalidFormat(format!(
"Audio file not found: {:?}",
audio_path
)));
}
let sample_id = uuid::Uuid::new_v4().to_string();
let sample = GroundTruthSample {
id: sample_id.clone(),
audio_path,
reference_path,
transcript,
language,
speaker_id,
sample_rate: 16000, duration: 0.0, metadata,
annotations: Vec::new(),
validation_status: ValidationStatus::Pending,
};
dataset.samples.push(sample);
dataset.sample_count = dataset.samples.len();
dataset.modified_at = Utc::now();
self.save_catalog().await?;
Ok(sample_id)
}
pub async fn add_annotation(
&mut self,
dataset_id: &str,
sample_id: &str,
annotation_type: AnnotationType,
value: f64,
scale: String,
annotator_id: String,
quality_level: AnnotationQuality,
confidence: f64,
description: Option<String>,
metadata: HashMap<String, String>,
) -> Result<String, GroundTruthError> {
let dataset = self
.catalog
.get_mut(dataset_id)
.ok_or_else(|| GroundTruthError::DatasetNotFound(dataset_id.to_string()))?;
let sample = dataset
.samples
.iter_mut()
.find(|s| s.id == sample_id)
.ok_or_else(|| GroundTruthError::DatasetNotFound(sample_id.to_string()))?;
let annotation_id = uuid::Uuid::new_v4().to_string();
let annotation = GroundTruthAnnotation {
id: annotation_id.clone(),
sample_id: sample_id.to_string(),
annotation_type,
value,
scale,
annotator_id,
quality_level,
confidence,
created_at: Utc::now(),
description,
metadata,
};
sample.annotations.push(annotation);
dataset.modified_at = Utc::now();
self.save_catalog().await?;
Ok(annotation_id)
}
pub async fn validate_dataset(
&mut self,
dataset_id: &str,
) -> Result<DatasetValidationReport, GroundTruthError> {
let dataset = self
.catalog
.get_mut(dataset_id)
.ok_or_else(|| GroundTruthError::DatasetNotFound(dataset_id.to_string()))?;
let mut audio_samples = Vec::new();
let mut metadata_samples = Vec::new();
for sample in &dataset.samples {
let dummy_audio = vec![0.1_f32; 16000]; audio_samples.push((dummy_audio, sample.sample_rate));
let mut sample_metadata = sample.metadata.clone();
sample_metadata.insert("language".to_string(), sample.language.clone());
sample_metadata.insert("speaker".to_string(), sample.speaker_id.clone());
sample_metadata.insert("transcript".to_string(), sample.transcript.clone());
metadata_samples.push(sample_metadata);
}
let validation_report = self
.validator
.validate_dataset(&dataset.name, &audio_samples, &metadata_samples)
.map_err(|e| GroundTruthError::AnnotationValidationFailed(e.to_string()))?;
dataset.quality_metrics.overall_quality = validation_report.quality_score;
dataset.quality_metrics.audio_quality = validation_report.quality_score;
dataset.quality_metrics.metadata_completeness = validation_report
.metadata_validation
.values()
.map(|&valid| if valid { 1.0 } else { 0.0 })
.sum::<f64>()
/ validation_report.metadata_validation.len() as f64;
for (i, sample) in dataset.samples.iter_mut().enumerate() {
let sample_issues = validation_report
.audio_issues
.iter()
.filter(|issue| issue.description.starts_with(&format!("Sample {}:", i)))
.count();
sample.validation_status = if sample_issues == 0 {
ValidationStatus::Valid
} else {
ValidationStatus::Invalid
};
}
dataset.modified_at = Utc::now();
self.save_catalog().await?;
Ok(validation_report)
}
pub async fn create_version(
&mut self,
dataset_id: &str,
version: &str,
description: String,
changes: Vec<String>,
) -> Result<(), GroundTruthError> {
let dataset = self
.catalog
.get(dataset_id)
.ok_or_else(|| GroundTruthError::DatasetNotFound(dataset_id.to_string()))?;
let previous_version = self
.version_history
.get(dataset_id)
.and_then(|versions| versions.last())
.map(|v| v.version.clone());
if let Some(versions) = self.version_history.get(dataset_id) {
if versions.iter().any(|v| v.version == version) {
return Err(GroundTruthError::VersionConflict(format!(
"Version {} already exists for dataset {}",
version, dataset_id
)));
}
}
let version_dir = self.base_path.join("versions").join(dataset_id);
tokio::fs::create_dir_all(&version_dir).await?;
let snapshot_path = version_dir.join(format!("{}.json", version));
let dataset_content = serde_json::to_string_pretty(dataset)?;
tokio::fs::write(&snapshot_path, dataset_content).await?;
let version_record = DatasetVersion {
version: version.to_string(),
created_at: Utc::now(),
description,
changes,
previous_version,
dataset_path: snapshot_path,
metadata: HashMap::new(),
};
self.version_history
.entry(dataset_id.to_string())
.or_insert_with(Vec::new)
.push(version_record);
self.save_version_history().await?;
Ok(())
}
pub fn get_dataset(&self, dataset_id: &str) -> Option<&GroundTruthDataset> {
self.catalog.get(dataset_id)
}
pub fn list_datasets(&self) -> Vec<&GroundTruthDataset> {
self.catalog.values().collect()
}
pub fn search_datasets(
&self,
language: Option<&str>,
domain: Option<&str>,
creator: Option<&str>,
tags: Option<&[String]>,
) -> Vec<&GroundTruthDataset> {
self.catalog
.values()
.filter(|dataset| {
if let Some(lang) = language {
if !dataset.languages.contains(&lang.to_string()) {
return false;
}
}
if let Some(dom) = domain {
if dataset.domain != dom {
return false;
}
}
if let Some(cre) = creator {
if dataset.creator != cre {
return false;
}
}
if let Some(search_tags) = tags {
if !search_tags.iter().all(|tag| dataset.tags.contains(tag)) {
return false;
}
}
true
})
.collect()
}
pub async fn export_dataset(
&self,
dataset_id: &str,
export_path: &Path,
format: DatasetExportFormat,
) -> Result<(), GroundTruthError> {
let dataset = self
.catalog
.get(dataset_id)
.ok_or_else(|| GroundTruthError::DatasetNotFound(dataset_id.to_string()))?;
match format {
DatasetExportFormat::Json => {
let content = serde_json::to_string_pretty(dataset)?;
tokio::fs::write(export_path, content).await?;
}
DatasetExportFormat::Csv => {
let mut csv_content = String::from(
"id,transcript,language,speaker_id,audio_path,annotations_count\n",
);
for sample in &dataset.samples {
csv_content.push_str(&format!(
"{},{},{},{},{},{}\n",
sample.id,
sample.transcript.replace(',', ";"),
sample.language,
sample.speaker_id,
sample.audio_path.display(),
sample.annotations.len()
));
}
tokio::fs::write(export_path, csv_content).await?;
}
}
Ok(())
}
pub fn calculate_inter_annotator_agreement(
&self,
dataset_id: &str,
annotation_type: &AnnotationType,
) -> Result<f64, GroundTruthError> {
let dataset = self
.catalog
.get(dataset_id)
.ok_or_else(|| GroundTruthError::DatasetNotFound(dataset_id.to_string()))?;
let mut sample_agreements = Vec::new();
for sample in &dataset.samples {
let annotations: Vec<_> = sample
.annotations
.iter()
.filter(|ann| ann.annotation_type == *annotation_type)
.collect();
if annotations.len() >= 2 {
let values: Vec<f64> = annotations.iter().map(|ann| ann.value).collect();
let mean = values.iter().sum::<f64>() / values.len() as f64;
let variance =
values.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
let agreement = if variance > 0.0 {
1.0 / (1.0 + variance)
} else {
1.0
};
sample_agreements.push(agreement);
}
}
if sample_agreements.is_empty() {
Ok(0.0)
} else {
Ok(sample_agreements.iter().sum::<f64>() / sample_agreements.len() as f64)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DatasetExportFormat {
Json,
Csv,
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_ground_truth_manager_creation() {
let temp_dir = TempDir::new().unwrap();
let mut manager = GroundTruthManager::new(temp_dir.path().to_path_buf());
manager.initialize().await.unwrap();
assert!(manager.catalog.is_empty());
}
#[tokio::test]
async fn test_dataset_creation() {
let temp_dir = TempDir::new().unwrap();
let mut manager = GroundTruthManager::new(temp_dir.path().to_path_buf());
manager.initialize().await.unwrap();
let dataset_id = manager
.create_dataset(
"Test Dataset".to_string(),
"A test dataset".to_string(),
"Test Creator".to_string(),
"MIT".to_string(),
"speech".to_string(),
vec!["en".to_string()],
)
.await
.unwrap();
assert!(!dataset_id.is_empty());
assert!(manager.catalog.contains_key(&dataset_id));
}
#[tokio::test]
async fn test_annotation_addition() {
let temp_dir = TempDir::new().unwrap();
let mut manager = GroundTruthManager::new(temp_dir.path().to_path_buf());
manager.initialize().await.unwrap();
let dataset_id = manager
.create_dataset(
"Test Dataset".to_string(),
"A test dataset".to_string(),
"Test Creator".to_string(),
"MIT".to_string(),
"speech".to_string(),
vec!["en".to_string()],
)
.await
.unwrap();
let audio_file = temp_dir.path().join("test.wav");
tokio::fs::write(&audio_file, b"dummy audio content")
.await
.unwrap();
let sample_id = manager
.add_sample(
&dataset_id,
audio_file,
None,
"Hello world".to_string(),
"en".to_string(),
"speaker1".to_string(),
HashMap::new(),
)
.await
.unwrap();
let annotation_id = manager
.add_annotation(
&dataset_id,
&sample_id,
AnnotationType::QualityScore,
0.85,
"MOS_5".to_string(),
"annotator1".to_string(),
AnnotationQuality::Expert,
0.9,
Some("High quality sample".to_string()),
HashMap::new(),
)
.await
.unwrap();
assert!(!annotation_id.is_empty());
let dataset = manager.get_dataset(&dataset_id).unwrap();
assert_eq!(dataset.samples.len(), 1);
assert_eq!(dataset.samples[0].annotations.len(), 1);
}
#[tokio::test]
async fn test_dataset_search() {
let temp_dir = TempDir::new().unwrap();
let mut manager = GroundTruthManager::new(temp_dir.path().to_path_buf());
manager.initialize().await.unwrap();
let _dataset_id1 = manager
.create_dataset(
"English Dataset".to_string(),
"English speech dataset".to_string(),
"Creator1".to_string(),
"MIT".to_string(),
"speech".to_string(),
vec!["en".to_string()],
)
.await
.unwrap();
let _dataset_id2 = manager
.create_dataset(
"Spanish Dataset".to_string(),
"Spanish speech dataset".to_string(),
"Creator2".to_string(),
"MIT".to_string(),
"music".to_string(),
vec!["es".to_string()],
)
.await
.unwrap();
let english_datasets = manager.search_datasets(Some("en"), None, None, None);
assert_eq!(english_datasets.len(), 1);
assert_eq!(english_datasets[0].name, "English Dataset");
let speech_datasets = manager.search_datasets(None, Some("speech"), None, None);
assert_eq!(speech_datasets.len(), 1);
assert_eq!(speech_datasets[0].domain, "speech");
}
}