use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[cfg_attr(
feature = "listen",
doc = "Returned by [`Transcription::prerecorded`](crate::Transcription::prerecorded)."
)]
#[cfg_attr(
not(feature = "listen"),
doc = "Returned by `Transcription::prerecorded`."
)]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Response {
#[allow(missing_docs)]
pub metadata: ListenMetadata,
#[allow(missing_docs)]
pub results: ListenResults,
}
#[cfg_attr(
feature = "listen",
doc = "Returned by [`Transcription::prerecorded_callback`](crate::Transcription::prerecorded_callback)."
)]
#[cfg_attr(
not(feature = "listen"),
doc = "Returned by `Transcription::prerecorded_callback`."
)]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CallbackResponse {
#[allow(missing_docs)]
pub request_id: Uuid,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ListenMetadata {
#[allow(missing_docs)]
pub request_id: Uuid,
#[allow(missing_docs)]
pub transaction_key: String,
#[allow(missing_docs)]
pub sha256: String,
#[allow(missing_docs)]
pub created: String,
#[allow(missing_docs)]
pub duration: f64,
#[allow(missing_docs)]
pub channels: usize,
#[allow(missing_docs)]
pub language: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extra: Option<HashMap<String, String>>,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ListenResults {
#[allow(missing_docs)]
pub channels: Vec<ChannelResult>,
pub utterances: Option<Vec<Utterance>>,
#[allow(missing_docs)]
pub intents: Option<Intents>,
#[allow(missing_docs)]
pub sentiments: Option<Sentiments>,
#[allow(missing_docs)]
pub topics: Option<Topics>,
#[allow(missing_docs)]
pub summary: Option<Summary>,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ChannelResult {
pub search: Option<Vec<SearchResults>>,
#[allow(missing_docs)]
pub alternatives: Vec<ResultAlternative>,
pub detected_language: Option<String>,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Utterance {
#[allow(missing_docs)]
pub start: f64,
#[allow(missing_docs)]
pub end: f64,
#[allow(missing_docs)]
pub confidence: f64,
#[allow(missing_docs)]
pub channel: usize,
#[allow(missing_docs)]
pub transcript: String,
#[allow(missing_docs)]
pub words: Vec<Word>,
pub speaker: Option<usize>,
#[allow(missing_docs)]
pub id: Uuid,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SearchResults {
#[allow(missing_docs)]
pub query: String,
#[allow(missing_docs)]
pub hits: Vec<Hit>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub struct Sentence {
pub text: String,
pub start: f64,
pub end: f64,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub struct Paragraph {
pub sentences: Vec<Sentence>,
pub num_words: usize,
pub start: f64,
pub end: f64,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub struct Paragraphs {
pub transcript: String,
pub paragraphs: Vec<Paragraph>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub struct Entity {
pub label: String,
pub value: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw_value: Option<String>,
pub confidence: f64,
pub start_word: usize,
pub end_word: usize,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub struct Intent {
pub intent: String,
pub confidence_score: f64,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub struct Segment {
pub text: String,
pub start_word: usize,
pub end_word: usize,
pub intents: Vec<Intent>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub struct Intents {
pub segments: Vec<Segment>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub struct SentimentSegment {
pub text: String,
pub start_word: usize,
pub end_word: usize,
pub sentiment: String,
pub sentiment_score: f64,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub struct SentimentAverage {
pub sentiment: String,
pub sentiment_score: f64,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub struct Sentiments {
pub segments: Vec<SentimentSegment>,
pub average: SentimentAverage,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub struct TopicDetail {
pub topic: String,
pub confidence_score: f64,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub struct TopicSegment {
pub text: String,
pub start_word: usize,
pub end_word: usize,
pub topics: Vec<TopicDetail>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub struct Topics {
pub segments: Vec<TopicSegment>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub struct Summary {
pub result: String,
pub short: String,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ResultAlternative {
#[allow(missing_docs)]
pub transcript: String,
#[allow(missing_docs)]
pub confidence: f64,
#[allow(missing_docs)]
pub words: Vec<Word>,
#[allow(missing_docs)]
pub paragraphs: Option<Paragraphs>,
#[allow(missing_docs)]
pub entities: Option<Vec<Entity>>,
#[allow(missing_docs)]
#[serde(default)]
pub languages: Vec<String>,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Word {
#[allow(missing_docs)]
pub word: String,
#[allow(missing_docs)]
pub start: f64,
#[allow(missing_docs)]
pub end: f64,
#[allow(missing_docs)]
pub confidence: f64,
pub speaker: Option<usize>,
pub punctuated_word: Option<String>,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Hit {
#[allow(missing_docs)]
pub confidence: f64,
#[allow(missing_docs)]
pub start: f64,
#[allow(missing_docs)]
pub end: f64,
#[allow(missing_docs)]
pub snippet: String,
}
#[cfg(test)]
mod tests {
use super::*;
const METADATA_WITH_EXTRA: &str = r#"{
"transaction_key": "deprecated",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"sha256": "abc123",
"created": "2026-09-11T10:00:00.000Z",
"duration": 12.5,
"channels": 1,
"extra": {
"customer_id": "cust-42",
"session": "abc"
}
}"#;
const METADATA_WITHOUT_EXTRA: &str = r#"{
"transaction_key": "deprecated",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"sha256": "abc123",
"created": "2026-09-11T10:00:00.000Z",
"duration": 12.5,
"channels": 1
}"#;
#[test]
fn listen_metadata_exposes_extra() {
let metadata: ListenMetadata = serde_json::from_str(METADATA_WITH_EXTRA).unwrap();
let extra = metadata.extra.expect("extra should be present");
assert_eq!(extra.len(), 2);
assert_eq!(
extra.get("customer_id").map(String::as_str),
Some("cust-42")
);
assert_eq!(extra.get("session").map(String::as_str), Some("abc"));
}
#[test]
fn listen_metadata_without_extra_is_none() {
let metadata: ListenMetadata = serde_json::from_str(METADATA_WITHOUT_EXTRA).unwrap();
assert_eq!(metadata.duration, 12.5);
assert_eq!(metadata.channels, 1);
assert!(metadata.extra.is_none());
}
const LIVE_ENTITY: &str = r#"{
"label": "ORDINAL",
"value": "first",
"confidence": 0.9988257,
"start_word": 9,
"end_word": 10
}"#;
const LIVE_ENTITY_WITH_RAW_VALUE: &str = r#"{
"label": "PHONE_NUMBER",
"value": "(555",
"raw_value": "five five five",
"confidence": 0.99934894,
"start_word": 9,
"end_word": 10
}"#;
const LIVE_ENTITY_WITHOUT_RAW_VALUE: &str = r#"{
"label": "NAME",
"value": "Jane Doe",
"confidence": 0.9998472,
"start_word": 3,
"end_word": 5
}"#;
#[test]
fn entity_word_indices_are_end_exclusive() {
let entity: Entity = serde_json::from_str(LIVE_ENTITY).unwrap();
assert_eq!(entity.start_word, 9);
assert_eq!(entity.end_word, 10);
assert_eq!(
entity.end_word - entity.start_word,
entity.value.split_whitespace().count(),
"Entity::end_word is exclusive, so the span equals the word count"
);
}
#[test]
fn entity_raw_value_is_read_when_formatting_rewrote_the_entity() {
let entity: Entity = serde_json::from_str(LIVE_ENTITY_WITH_RAW_VALUE).unwrap();
assert_eq!(entity.value, "(555");
assert_eq!(entity.raw_value.as_deref(), Some("five five five"));
let json = serde_json::to_value(&entity).unwrap();
assert_eq!(json["raw_value"], "five five five");
}
#[test]
fn entity_raw_value_is_none_when_the_server_omits_it() {
let entity: Entity = serde_json::from_str(LIVE_ENTITY_WITHOUT_RAW_VALUE).unwrap();
assert_eq!(entity.value, "Jane Doe");
assert!(entity.raw_value.is_none());
assert_eq!(
entity.end_word - entity.start_word,
entity.value.split_whitespace().count()
);
let json = serde_json::to_value(&entity).unwrap();
assert!(
json.get("raw_value").is_none(),
"absent raw_value must not serialize as null"
);
}
const LIVE_SENTIMENT_SEGMENT: &str = r#"{
"text": "Yeah. As as much as, it's worth celebrating, the first, spacewalk, with an all female team, I think many of us are looking forward to it just being normal. And, I think if it signifies anything, it is, to honor the the women who came before us who, were skilled and qualified, and didn't get the same opportunities that we have today.",
"start_word": 0,
"end_word": 61,
"sentiment": "positive",
"sentiment_score": 0.6363217830657959
}"#;
#[test]
fn intelligence_segment_word_indices_are_end_inclusive() {
let segment: SentimentSegment = serde_json::from_str(LIVE_SENTIMENT_SEGMENT).unwrap();
assert_eq!(segment.start_word, 0);
assert_eq!(segment.end_word, 61);
assert_eq!(segment.text.split_whitespace().count(), 62);
assert_eq!(
segment.end_word - segment.start_word + 1,
segment.text.split_whitespace().count(),
"segment end_word is inclusive, unlike Entity::end_word"
);
}
#[test]
fn listen_metadata_extra_round_trips() {
let metadata: ListenMetadata = serde_json::from_str(METADATA_WITH_EXTRA).unwrap();
let json = serde_json::to_value(&metadata).unwrap();
assert_eq!(json["extra"]["customer_id"], "cust-42");
assert_eq!(json["extra"]["session"], "abc");
let without: ListenMetadata = serde_json::from_str(METADATA_WITHOUT_EXTRA).unwrap();
let json = serde_json::to_value(&without).unwrap();
assert!(
json.get("extra").is_none(),
"absent extra must not serialize as null"
);
}
}