rig_core/transcription.rs
1//! Audio transcription requests, normalized responses, and model interfaces.
2//!
3//! ```no_run
4//! use rig_core::DynModel;
5//! use rig_core::operation::Transcription;
6//! use rig_core::transcription::TranscriptionRequestBuilder;
7//!
8//! # async fn example(model: DynModel<Transcription>) -> Result<(), Box<dyn std::error::Error>> {
9//! let request = TranscriptionRequestBuilder::from_file("audio.wav")?.build();
10//! let response = model.call(request).await?;
11//! # let _ = response;
12//! # Ok(())
13//! # }
14//! ```
15use crate::completion::Usage;
16use crate::json_utils;
17use serde::{Deserialize, Serialize};
18use std::io;
19use std::{fs, path::Path};
20
21/// Transcript and normalized provider metadata, with provider-specific data
22/// available through [`Self::raw`].
23///
24/// Timings and speaker labels are filled only when the provider returned
25/// them, which usually takes a provider option passed through
26/// [`TranscriptionRequestBuilder::additional_params`], such as
27/// `{"response_format": "verbose_json", "timestamp_granularities": ["segment", "word"]}`
28/// on OpenAI-compatible endpoints.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct TranscriptionResponse {
31 /// The transcribed text.
32 pub text: String,
33 /// Provider-reported token usage. Unreported counters remain `None`;
34 /// this field does not contain audio duration.
35 #[serde(default)]
36 pub usage: Usage,
37 /// Stable descriptor name of the provider that produced this response,
38 /// for example `"openai"`. Always populated.
39 pub provider: String,
40 /// Provider-reported model identifier, when the wire response named one.
41 /// This is the model the provider says answered, not the model requested.
42 #[serde(default)]
43 pub model: Option<String>,
44 /// Provider-assigned response-scoped identifier, when reported.
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub response_id: Option<String>,
47 /// Transport request ID from HTTP headers, or `None` when unreported.
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub provider_request_id: Option<String>,
50 /// Language of the audio as the provider reported it, either a code
51 /// such as `"en"` or a name such as `"english"`.
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub language: Option<String>,
54 /// Length of the audio in seconds, when reported.
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub duration: Option<f64>,
57 /// The provider's confidence in the whole transcript, from 0 to 1.
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub confidence: Option<f64>,
60 /// Timed stretches of the transcript, in the provider's order. Empty when
61 /// the provider returned none.
62 #[serde(default, skip_serializing_if = "Vec::is_empty")]
63 pub segments: Vec<TranscriptSegment>,
64 /// Timed words, in the provider's order. Empty when the provider returned
65 /// none.
66 #[serde(default, skip_serializing_if = "Vec::is_empty")]
67 pub words: Vec<TranscriptWord>,
68 /// Provider response document. Defaults to null until populated.
69 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
70 pub raw: serde_json::Value,
71}
72
73impl TranscriptionResponse {
74 /// A response carrying `text`. The driver writes the provider, the
75 /// transport request id and the reply document; decoders set what the
76 /// provider reported.
77 pub fn new(text: impl Into<String>) -> Self {
78 Self {
79 text: text.into(),
80 usage: Usage::default(),
81 provider: String::new(),
82 model: None,
83 response_id: None,
84 provider_request_id: None,
85 language: None,
86 duration: None,
87 confidence: None,
88 segments: Vec::new(),
89 words: Vec::new(),
90 raw: serde_json::Value::Null,
91 }
92 }
93}
94
95/// A timed stretch of a transcript. Times are seconds from the start of the
96/// audio.
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
98pub struct TranscriptSegment {
99 /// When the segment starts.
100 pub start: f64,
101 /// When the segment ends.
102 pub end: f64,
103 /// The segment's text, with the provider's spacing.
104 pub text: String,
105 /// Who spoke it, when the provider diarized the audio. Labels are the
106 /// provider's own, such as `"0"`, `"A"` or `"speaker_1"`, and are stable
107 /// within one response only.
108 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub speaker: Option<String>,
110}
111
112/// A timed word of a transcript. Times are seconds from the start of the
113/// audio.
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115pub struct TranscriptWord {
116 /// The word, with the provider's spacing and punctuation.
117 pub word: String,
118 /// When the word starts.
119 pub start: f64,
120 /// When the word ends.
121 pub end: f64,
122 /// Who spoke it, labeled as on [`TranscriptSegment::speaker`].
123 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub speaker: Option<String>,
125 /// The provider's confidence in the word, from 0 to 1.
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub confidence: Option<f64>,
128}
129
130/// Struct representing a general transcription request that can be sent to a transcription model provider.
131pub struct TranscriptionRequest {
132 /// The file data to be sent to the transcription model provider
133 pub data: Vec<u8>,
134 /// The file name to be used in the request
135 pub filename: String,
136 /// The language used in the response from the transcription model provider
137 pub language: Option<String>,
138 /// The prompt to be sent to the transcription model provider
139 pub prompt: Option<String>,
140 /// The temperature sent to the transcription model provider
141 pub temperature: Option<f64>,
142 /// Additional parameters to be sent to the transcription model provider
143 pub additional_params: Option<serde_json::Value>,
144}
145
146/// The filename a request carries until the caller or its file names it.
147const DEFAULT_FILENAME: &str = "file";
148
149/// Builds a transcription request over the supplied audio. The audio is not
150/// validated for format or nonemptiness.
151pub struct TranscriptionRequestBuilder {
152 request: TranscriptionRequest,
153}
154
155impl TranscriptionRequestBuilder {
156 /// A request over `data`, named `"file"` until [`Self::filename`] names it.
157 pub fn new(data: Vec<u8>) -> Self {
158 Self {
159 request: TranscriptionRequest {
160 data,
161 filename: DEFAULT_FILENAME.to_owned(),
162 language: None,
163 prompt: None,
164 temperature: None,
165 additional_params: None,
166 },
167 }
168 }
169
170 /// A request over the file at `path`, named after its base name. Reads the
171 /// file synchronously and returns I/O errors unchanged.
172 pub fn from_file(path: impl AsRef<Path>) -> io::Result<Self> {
173 let path = path.as_ref();
174 let filename = path
175 .file_name()
176 .map(|name| name.to_string_lossy().into_owned());
177 Ok(Self::new(fs::read(path)?).filename(filename))
178 }
179
180 /// Names the audio file; `None` restores the default name.
181 pub fn filename(mut self, filename: impl Into<Option<String>>) -> Self {
182 self.request.filename = filename
183 .into()
184 .unwrap_or_else(|| DEFAULT_FILENAME.to_owned());
185 self
186 }
187
188 /// Sets the output language.
189 pub fn language(mut self, language: String) -> Self {
190 self.request.language = Some(language);
191 self
192 }
193
194 /// Sets the prompt sent with the audio.
195 pub fn prompt(mut self, prompt: String) -> Self {
196 self.request.prompt = Some(prompt);
197 self
198 }
199
200 /// Sets the sampling temperature.
201 pub fn temperature(mut self, temperature: f64) -> Self {
202 self.request.temperature = Some(temperature);
203 self
204 }
205
206 /// Merges provider-specific parameters over earlier ones, key by key for
207 /// JSON objects; `None` clears existing parameters.
208 pub fn additional_params(mut self, params: impl Into<Option<serde_json::Value>>) -> Self {
209 self.request.additional_params =
210 json_utils::merge_params(self.request.additional_params.take(), params.into());
211 self
212 }
213
214 /// Builds the transcription request.
215 pub fn build(self) -> TranscriptionRequest {
216 self.request
217 }
218}
219
220#[cfg(test)]
221mod builder_tests;