use serde::{Deserialize, Serialize};
pub(crate) fn provider_configured_for_passive_status(env_var: &str) -> bool {
std::env::var(env_var).is_ok_and(|value| !value.trim().is_empty())
}
pub(crate) fn resolve_provider_credential_for_request(env_var: &str) -> Option<String> {
car_secrets::resolve_env_or_keychain(env_var)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TranscribeRequest {
pub audio_path: String,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub language: Option<String>,
#[serde(default)]
pub prompt: Option<String>,
#[serde(default)]
pub timestamps: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TranscribedWord {
pub start: f32,
pub end: f32,
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TranscribeResult {
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_used: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub words: Vec<TranscribedWord>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub routing_explanation: Option<String>,
}
impl TranscribeResult {
pub fn text_only(text: String, model_used: Option<String>, language: Option<String>) -> Self {
Self {
text,
model_used,
language,
words: Vec::new(),
routing_explanation: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn passive_stt_status_does_not_touch_the_secret_store() {
const UNSET: &str = "CAR_TEST_PASSIVE_STT_CREDENTIAL_MUST_NOT_EXIST";
assert!(std::env::var_os(UNSET).is_none());
let before = car_secrets::secret_store_activity();
assert!(!provider_configured_for_passive_status(UNSET));
assert_eq!(car_secrets::secret_store_activity(), before);
}
#[test]
fn adaptive_remote_fallback_explanation_survives_result_json() {
let mut result =
TranscribeResult::text_only("hello".into(), Some("remote/stt".into()), None);
result.routing_explanation = Some(
"Local speech model was blocked by the RAM allocation; routed to a remote provider, which may affect privacy and cost.".into(),
);
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("privacy"));
assert!(json.contains("cost"));
}
}