use std::collections::HashMap;
use std::sync::Arc;
use futures_util::Stream;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::client::{CallOptions, Client};
use crate::error::{Error, Result};
use crate::pagination::{auto_page, paginate, ItemMapper, ListParams, Page, PageFetcher};
use crate::query::QueryBuilder;
use crate::resources::escape;
const PATH: &str = "/ai/v1/realtime-transcriptions";
#[derive(Debug, Clone, Default, Serialize)]
pub struct RealtimeSummaryConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct StartRealtimeTranscriptionParams {
pub webhook_url: Option<String>,
pub model_id: Option<String>,
pub language: Option<String>,
pub summary: Option<RealtimeSummaryConfig>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct StartTranscriptionWire<'a> {
room_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
webhook_url: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
model_id: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
language: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
summary: Option<&'a RealtimeSummaryConfig>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RealtimeTranscriptionExtensionConfig {
pub room_id: Option<String>,
pub session_id: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RealtimeTranscriptionExtension {
pub enabled: Option<bool>,
pub provider: Option<String>,
pub extension_config: Option<RealtimeTranscriptionExtensionConfig>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RealtimeTranscription {
pub id: String,
pub room_id: Option<String>,
pub status: Option<String>,
pub extension: Option<RealtimeTranscriptionExtension>,
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub transcription_file_paths: HashMap<String, String>,
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub summarized_file_paths: HashMap<String, String>,
pub start: Option<String>,
pub end: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
fn with_room_id(
mut transcription: RealtimeTranscription,
fallback: Option<&str>,
) -> RealtimeTranscription {
if transcription.room_id.is_some() {
return transcription;
}
let nested = transcription
.extension
.as_ref()
.and_then(|extension| extension.extension_config.as_ref())
.and_then(|config| config.room_id.clone());
transcription.room_id = nested.or_else(|| fallback.map(str::to_string));
transcription
}
#[derive(Debug, Clone, Default)]
pub struct ListRealtimeTranscriptionsParams {
pub page: Option<u32>,
pub per_page: Option<u32>,
pub cursor: Option<String>,
pub status: Option<String>,
}
impl ListRealtimeTranscriptionsParams {
fn pagination(&self) -> ListParams {
ListParams {
page: self.page,
per_page: self.per_page,
cursor: self.cursor.clone(),
}
}
}
fn transcription_item_mapper() -> ItemMapper<RealtimeTranscription> {
Arc::new(|value: Value| {
let transcription: RealtimeTranscription = serde_json::from_value(value)
.map_err(|e| Error::decode("realtime transcription list item", e))?;
Ok(with_room_id(transcription, None))
})
}
#[derive(Debug, Clone, Copy)]
pub struct RealtimeTranscriptionResource<'a> {
client: &'a Client,
}
impl<'a> RealtimeTranscriptionResource<'a> {
pub async fn start(
&self,
room_id: &str,
params: StartRealtimeTranscriptionParams,
) -> Result<RealtimeTranscription> {
let body = StartTranscriptionWire {
room_id,
webhook_url: params.webhook_url.as_deref(),
model_id: params.model_id.as_deref(),
language: params.language.as_deref(),
summary: params.summary.as_ref(),
};
let path = format!("{PATH}/start");
let transcription: RealtimeTranscription = self
.client
.json(Method::POST, &path, CallOptions::json(&body)?)
.await?;
Ok(with_room_id(transcription, Some(room_id)))
}
pub async fn stop(&self, room_id: &str) -> Result<RealtimeTranscription> {
if room_id.is_empty() {
return Err(Error::validation(
"transcription.realtime().stop() needs a room_id",
));
}
let body = serde_json::json!({ "roomId": room_id });
let path = format!("{PATH}/end");
self.client
.json(Method::POST, &path, CallOptions::json(&body)?)
.await
}
pub async fn get(&self, transcription_id: &str) -> Result<RealtimeTranscription> {
let path = format!("{PATH}/{}", escape(transcription_id));
let transcription: RealtimeTranscription = self
.client
.json(Method::GET, &path, CallOptions::new())
.await?;
Ok(with_room_id(transcription, None))
}
pub async fn list(
&self,
params: ListRealtimeTranscriptionsParams,
) -> Result<Page<RealtimeTranscription>> {
paginate(
self.fetcher(¶ms),
¶ms.pagination(),
"transcriptions",
Some(transcription_item_mapper()),
)
.await
}
pub fn list_stream(
&self,
params: ListRealtimeTranscriptionsParams,
) -> impl Stream<Item = Result<RealtimeTranscription>> + Send {
auto_page(
self.fetcher(¶ms),
params.pagination(),
"transcriptions",
Some(transcription_item_mapper()),
)
}
fn fetcher(&self, params: &ListRealtimeTranscriptionsParams) -> PageFetcher {
let client = self.client.clone();
let status = params.status.clone();
Arc::new(move |page, per_page| {
let client = client.clone();
let status = status.clone();
Box::pin(async move {
let query = QueryBuilder::new()
.opt("page", page)
.opt("perPage", per_page)
.opt_str("status", status.as_deref())
.into_pairs();
client
.json::<Value>(Method::GET, PATH, CallOptions::new().query(query))
.await
})
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct TranscriptionResource<'a> {
client: &'a Client,
}
impl<'a> TranscriptionResource<'a> {
pub(crate) fn new(client: &'a Client) -> Self {
Self { client }
}
pub fn realtime(&self) -> RealtimeTranscriptionResource<'a> {
RealtimeTranscriptionResource {
client: self.client,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn transcription(value: Value) -> RealtimeTranscription {
serde_json::from_value(value).unwrap()
}
#[test]
fn the_room_id_is_lifted_out_of_the_extension_config() {
let lifted = with_room_id(
transcription(json!({
"id": "t-1",
"extension": {"extensionConfig": {"roomId": "r-9"}},
})),
None,
);
assert_eq!(lifted.room_id.as_deref(), Some("r-9"));
}
#[test]
fn a_top_level_room_id_is_never_overwritten() {
let lifted = with_room_id(
transcription(json!({
"id": "t-1",
"roomId": "real",
"extension": {"extensionConfig": {"roomId": "nested"}},
})),
Some("fallback"),
);
assert_eq!(lifted.room_id.as_deref(), Some("real"));
}
#[test]
fn the_fallback_applies_only_when_nothing_else_resolves() {
let lifted = with_room_id(transcription(json!({"id": "t-1"})), Some("fallback"));
assert_eq!(lifted.room_id.as_deref(), Some("fallback"));
let lifted = with_room_id(transcription(json!({"id": "t-1"})), None);
assert_eq!(lifted.room_id, None);
}
}