use crate::result::{InferenceMetrics, StageLatency};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct FfiStageExecutionResult {
pub stage_id: String,
pub executed: bool,
pub skip_reason: Option<String>,
pub target: Option<String>,
pub latency_ms: u32,
}
impl FfiStageExecutionResult {
pub fn executed(
stage_id: impl Into<String>,
target: impl Into<String>,
latency_ms: u32,
) -> Self {
Self {
stage_id: stage_id.into(),
executed: true,
skip_reason: None,
target: Some(target.into()),
latency_ms,
}
}
pub fn skipped(stage_id: impl Into<String>, reason: impl Into<String>) -> Self {
Self {
stage_id: stage_id.into(),
executed: false,
skip_reason: Some(reason.into()),
target: None,
latency_ms: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FfiPipelineExecutionResult {
pub success: bool,
pub error: Option<String>,
pub output_type: String,
pub text: Option<String>,
pub embedding: Option<Vec<f32>>,
pub audio_bytes_len: Option<usize>,
pub latency_ms: u32,
pub model_id: String,
pub stages: Vec<FfiStageExecutionResult>,
pub stages_executed: u32,
pub stages_skipped: u32,
#[serde(default)]
pub metrics: InferenceMetrics,
}
impl FfiPipelineExecutionResult {
pub fn success(
output_type: &str,
text: Option<String>,
embedding: Option<Vec<f32>>,
latency_ms: u32,
model_id: &str,
) -> Self {
Self {
success: true,
error: None,
output_type: output_type.to_string(),
text,
embedding,
audio_bytes_len: None,
latency_ms,
model_id: model_id.to_string(),
stages: Vec::new(),
stages_executed: 1,
stages_skipped: 0,
metrics: InferenceMetrics {
total_ms: latency_ms,
..InferenceMetrics::default()
},
}
}
pub fn success_audio(audio_len: usize, latency_ms: u32, model_id: &str) -> Self {
Self {
success: true,
error: None,
output_type: "audio".to_string(),
text: None,
embedding: None,
audio_bytes_len: Some(audio_len),
latency_ms,
model_id: model_id.to_string(),
stages: Vec::new(),
stages_executed: 1,
stages_skipped: 0,
metrics: InferenceMetrics {
total_ms: latency_ms,
..InferenceMetrics::default()
},
}
}
pub fn error(message: &str, model_id: &str) -> Self {
Self {
success: false,
error: Some(message.to_string()),
output_type: "none".to_string(),
text: None,
embedding: None,
audio_bytes_len: None,
latency_ms: 0,
model_id: model_id.to_string(),
stages: Vec::new(),
stages_executed: 0,
stages_skipped: 0,
metrics: InferenceMetrics::default(),
}
}
pub fn with_stages(&mut self, stages: Vec<FfiStageExecutionResult>) {
self.stages_executed = stages.iter().filter(|s| s.executed).count() as u32;
self.stages_skipped = stages.iter().filter(|s| !s.executed).count() as u32;
self.metrics.stage_latencies_ms = stages
.iter()
.filter(|s| s.executed)
.map(|s| StageLatency {
stage_id: s.stage_id.clone(),
latency_ms: s.latency_ms,
})
.collect();
self.stages = stages;
}
pub fn has_text(&self) -> bool {
self.text.is_some()
}
pub fn has_embedding(&self) -> bool {
self.embedding.is_some()
}
pub fn has_audio(&self) -> bool {
self.audio_bytes_len.is_some()
}
}
impl Default for FfiPipelineExecutionResult {
fn default() -> Self {
Self {
success: false,
error: None,
output_type: "none".to_string(),
text: None,
embedding: None,
audio_bytes_len: None,
latency_ms: 0,
model_id: String::new(),
stages: Vec::new(),
stages_executed: 0,
stages_skipped: 0,
metrics: InferenceMetrics::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stage_execution_result_executed() {
let stage = FfiStageExecutionResult::executed("asr", "device", 45);
assert_eq!(stage.stage_id, "asr");
assert!(stage.executed);
assert_eq!(stage.target, Some("device".to_string()));
assert_eq!(stage.latency_ms, 45);
assert!(stage.skip_reason.is_none());
}
#[test]
fn test_stage_execution_result_skipped() {
let stage = FfiStageExecutionResult::skipped("llm", "model too large");
assert_eq!(stage.stage_id, "llm");
assert!(!stage.executed);
assert_eq!(stage.skip_reason, Some("model too large".to_string()));
assert!(stage.target.is_none());
assert_eq!(stage.latency_ms, 0);
}
#[test]
fn test_stage_execution_result_default() {
let stage = FfiStageExecutionResult::default();
assert!(stage.stage_id.is_empty());
assert!(!stage.executed);
assert!(stage.skip_reason.is_none());
assert!(stage.target.is_none());
assert_eq!(stage.latency_ms, 0);
}
#[test]
fn test_stage_execution_result_serialization() {
let stage = FfiStageExecutionResult::executed("asr", "cloud", 100);
let json = serde_json::to_string(&stage).unwrap();
assert!(json.contains("\"stage_id\":\"asr\""));
assert!(json.contains("\"executed\":true"));
assert!(json.contains("\"target\":\"cloud\""));
assert!(json.contains("\"latency_ms\":100"));
let deserialized: FfiStageExecutionResult = serde_json::from_str(&json).unwrap();
assert_eq!(stage, deserialized);
}
#[test]
fn test_pipeline_execution_result_success_text() {
let result = FfiPipelineExecutionResult::success(
"text",
Some("Hello world".to_string()),
None,
150,
"whisper-tiny",
);
assert!(result.success);
assert!(result.error.is_none());
assert_eq!(result.output_type, "text");
assert_eq!(result.text, Some("Hello world".to_string()));
assert!(result.embedding.is_none());
assert!(result.audio_bytes_len.is_none());
assert_eq!(result.latency_ms, 150);
assert_eq!(result.model_id, "whisper-tiny");
assert_eq!(result.stages_executed, 1);
assert_eq!(result.stages_skipped, 0);
}
#[test]
fn test_pipeline_execution_result_success_embedding() {
let embedding = vec![0.1, 0.2, 0.3, 0.4, 0.5];
let result = FfiPipelineExecutionResult::success(
"embedding",
None,
Some(embedding.clone()),
50,
"bge-small",
);
assert!(result.success);
assert_eq!(result.output_type, "embedding");
assert!(result.text.is_none());
assert_eq!(result.embedding, Some(embedding));
assert!(result.has_embedding());
assert!(!result.has_text());
assert!(!result.has_audio());
}
#[test]
fn test_pipeline_execution_result_success_audio() {
let result = FfiPipelineExecutionResult::success_audio(48000, 350, "kokoro-82m");
assert!(result.success);
assert_eq!(result.output_type, "audio");
assert!(result.text.is_none());
assert!(result.embedding.is_none());
assert_eq!(result.audio_bytes_len, Some(48000));
assert!(result.has_audio());
assert!(!result.has_text());
assert!(!result.has_embedding());
}
#[test]
fn test_pipeline_execution_result_error() {
let result = FfiPipelineExecutionResult::error("Model not found", "unknown-model");
assert!(!result.success);
assert_eq!(result.error, Some("Model not found".to_string()));
assert_eq!(result.output_type, "none");
assert!(result.text.is_none());
assert!(result.embedding.is_none());
assert!(result.audio_bytes_len.is_none());
assert_eq!(result.latency_ms, 0);
assert_eq!(result.model_id, "unknown-model");
assert_eq!(result.stages_executed, 0);
assert_eq!(result.stages_skipped, 0);
}
#[test]
fn test_pipeline_execution_result_with_stages() {
let mut result = FfiPipelineExecutionResult::success(
"text",
Some("output".to_string()),
None,
200,
"pipeline-model",
);
result.with_stages(vec![
FfiStageExecutionResult::executed("stage1", "device", 50),
FfiStageExecutionResult::executed("stage2", "cloud", 100),
FfiStageExecutionResult::skipped("stage3", "condition not met"),
]);
assert_eq!(result.stages.len(), 3);
assert_eq!(result.stages_executed, 2);
assert_eq!(result.stages_skipped, 1);
assert_eq!(result.metrics.stage_latencies_ms.len(), 2);
assert_eq!(result.metrics.stage_latencies_ms[0].stage_id, "stage1");
assert_eq!(result.metrics.stage_latencies_ms[0].latency_ms, 50);
assert_eq!(result.metrics.stage_latencies_ms[1].stage_id, "stage2");
assert_eq!(result.metrics.stage_latencies_ms[1].latency_ms, 100);
}
#[test]
fn test_pipeline_execution_result_default() {
let result = FfiPipelineExecutionResult::default();
assert!(!result.success);
assert!(result.error.is_none());
assert_eq!(result.output_type, "none");
assert!(result.text.is_none());
assert!(result.embedding.is_none());
assert!(result.audio_bytes_len.is_none());
assert_eq!(result.latency_ms, 0);
assert!(result.model_id.is_empty());
assert!(result.stages.is_empty());
assert_eq!(result.stages_executed, 0);
assert_eq!(result.stages_skipped, 0);
}
#[test]
fn test_pipeline_execution_result_serialization() {
let result = FfiPipelineExecutionResult::success(
"text",
Some("Hello".to_string()),
None,
100,
"model-id",
);
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"success\":true"));
assert!(json.contains("\"output_type\":\"text\""));
assert!(json.contains("\"text\":\"Hello\""));
assert!(json.contains("\"model_id\":\"model-id\""));
assert!(json.contains("\"latency_ms\":100"));
let deserialized: FfiPipelineExecutionResult = serde_json::from_str(&json).unwrap();
assert_eq!(result, deserialized);
}
#[test]
fn test_pipeline_execution_result_error_serialization() {
let result = FfiPipelineExecutionResult::error("Connection failed", "remote-model");
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"success\":false"));
assert!(json.contains("\"error\":\"Connection failed\""));
assert!(json.contains("\"output_type\":\"none\""));
let deserialized: FfiPipelineExecutionResult = serde_json::from_str(&json).unwrap();
assert_eq!(result, deserialized);
}
}