rig-core 0.45.0

An opinionated library for building LLM powered applications.
Documentation
//! Audio transcription requests, normalized responses, and model interfaces.
//!
//! ```no_run
//! use rig_core::DynModel;
//! use rig_core::operation::Transcription;
//! use rig_core::transcription::TranscriptionRequestBuilder;
//!
//! # async fn example(model: DynModel<Transcription>) -> Result<(), Box<dyn std::error::Error>> {
//! let request = TranscriptionRequestBuilder::from_file("audio.wav")?.build();
//! let response = model.call(request).await?;
//! # let _ = response;
//! # Ok(())
//! # }
//! ```
use crate::completion::Usage;
use crate::json_utils;
use serde::{Deserialize, Serialize};
use std::io;
use std::{fs, path::Path};

/// Transcript and normalized provider metadata, with provider-specific data
/// available through [`Self::raw`].
///
/// Timings and speaker labels are filled only when the provider returned
/// them, which usually takes a provider option passed through
/// [`TranscriptionRequestBuilder::additional_params`], such as
/// `{"response_format": "verbose_json", "timestamp_granularities": ["segment", "word"]}`
/// on OpenAI-compatible endpoints.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TranscriptionResponse {
    /// The transcribed text.
    pub text: String,
    /// Provider-reported token usage. Unreported counters remain `None`;
    /// this field does not contain audio duration.
    #[serde(default)]
    pub usage: Usage,
    /// Stable descriptor name of the provider that produced this response,
    /// for example `"openai"`. Always populated.
    pub provider: String,
    /// Provider-reported model identifier, when the wire response named one.
    /// This is the model the provider says answered, not the model requested.
    #[serde(default)]
    pub model: Option<String>,
    /// Provider-assigned response-scoped identifier, when reported.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub response_id: Option<String>,
    /// Transport request ID from HTTP headers, or `None` when unreported.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider_request_id: Option<String>,
    /// Language of the audio as the provider reported it, either a code
    /// such as `"en"` or a name such as `"english"`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub language: Option<String>,
    /// Length of the audio in seconds, when reported.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub duration: Option<f64>,
    /// The provider's confidence in the whole transcript, from 0 to 1.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub confidence: Option<f64>,
    /// Timed stretches of the transcript, in the provider's order. Empty when
    /// the provider returned none.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub segments: Vec<TranscriptSegment>,
    /// Timed words, in the provider's order. Empty when the provider returned
    /// none.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub words: Vec<TranscriptWord>,
    /// Provider response document. Defaults to null until populated.
    #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
    pub raw: serde_json::Value,
}

impl TranscriptionResponse {
    /// A response carrying `text`. The driver writes the provider, the
    /// transport request id and the reply document; decoders set what the
    /// provider reported.
    pub fn new(text: impl Into<String>) -> Self {
        Self {
            text: text.into(),
            usage: Usage::default(),
            provider: String::new(),
            model: None,
            response_id: None,
            provider_request_id: None,
            language: None,
            duration: None,
            confidence: None,
            segments: Vec::new(),
            words: Vec::new(),
            raw: serde_json::Value::Null,
        }
    }
}

/// A timed stretch of a transcript. Times are seconds from the start of the
/// audio.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TranscriptSegment {
    /// When the segment starts.
    pub start: f64,
    /// When the segment ends.
    pub end: f64,
    /// The segment's text, with the provider's spacing.
    pub text: String,
    /// Who spoke it, when the provider diarized the audio. Labels are the
    /// provider's own, such as `"0"`, `"A"` or `"speaker_1"`, and are stable
    /// within one response only.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub speaker: Option<String>,
}

/// A timed word of a transcript. Times are seconds from the start of the
/// audio.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TranscriptWord {
    /// The word, with the provider's spacing and punctuation.
    pub word: String,
    /// When the word starts.
    pub start: f64,
    /// When the word ends.
    pub end: f64,
    /// Who spoke it, labeled as on [`TranscriptSegment::speaker`].
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub speaker: Option<String>,
    /// The provider's confidence in the word, from 0 to 1.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub confidence: Option<f64>,
}

/// Struct representing a general transcription request that can be sent to a transcription model provider.
pub struct TranscriptionRequest {
    /// The file data to be sent to the transcription model provider
    pub data: Vec<u8>,
    /// The file name to be used in the request
    pub filename: String,
    /// The language used in the response from the transcription model provider
    pub language: Option<String>,
    /// The prompt to be sent to the transcription model provider
    pub prompt: Option<String>,
    /// The temperature sent to the transcription model provider
    pub temperature: Option<f64>,
    /// Additional parameters to be sent to the transcription model provider
    pub additional_params: Option<serde_json::Value>,
}

/// The filename a request carries until the caller or its file names it.
const DEFAULT_FILENAME: &str = "file";

/// Builds a transcription request over the supplied audio. The audio is not
/// validated for format or nonemptiness.
pub struct TranscriptionRequestBuilder {
    request: TranscriptionRequest,
}

impl TranscriptionRequestBuilder {
    /// A request over `data`, named `"file"` until [`Self::filename`] names it.
    pub fn new(data: Vec<u8>) -> Self {
        Self {
            request: TranscriptionRequest {
                data,
                filename: DEFAULT_FILENAME.to_owned(),
                language: None,
                prompt: None,
                temperature: None,
                additional_params: None,
            },
        }
    }

    /// A request over the file at `path`, named after its base name. Reads the
    /// file synchronously and returns I/O errors unchanged.
    pub fn from_file(path: impl AsRef<Path>) -> io::Result<Self> {
        let path = path.as_ref();
        let filename = path
            .file_name()
            .map(|name| name.to_string_lossy().into_owned());
        Ok(Self::new(fs::read(path)?).filename(filename))
    }

    /// Names the audio file; `None` restores the default name.
    pub fn filename(mut self, filename: impl Into<Option<String>>) -> Self {
        self.request.filename = filename
            .into()
            .unwrap_or_else(|| DEFAULT_FILENAME.to_owned());
        self
    }

    /// Sets the output language.
    pub fn language(mut self, language: String) -> Self {
        self.request.language = Some(language);
        self
    }

    /// Sets the prompt sent with the audio.
    pub fn prompt(mut self, prompt: String) -> Self {
        self.request.prompt = Some(prompt);
        self
    }

    /// Sets the sampling temperature.
    pub fn temperature(mut self, temperature: f64) -> Self {
        self.request.temperature = Some(temperature);
        self
    }

    /// Merges provider-specific parameters over earlier ones, key by key for
    /// JSON objects; `None` clears existing parameters.
    pub fn additional_params(mut self, params: impl Into<Option<serde_json::Value>>) -> Self {
        self.request.additional_params =
            json_utils::merge_params(self.request.additional_params.take(), params.into());
        self
    }

    /// Builds the transcription request.
    pub fn build(self) -> TranscriptionRequest {
        self.request
    }
}

#[cfg(test)]
mod builder_tests;