Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,15 @@ Supported fallback entries:

Set `BOLO_STT_FALLBACKS=off` to fail fast instead of retrying when the primary model is rate limited. `BOLO_STT_FALLBACK_MODEL` still works for a single Telnyx fallback model.

To try streaming STT, set:
Deepgram streaming STT is enabled by default with the default `deepgram/nova-3` model. It starts sending audio while you speak and falls back to batch transcription when a complete result is not ready quickly enough after release.

To disable streaming and always use batch transcription, set:

```bash
export BOLO_STT_STREAMING="deepgram"
export BOLO_STT_STREAMING="off"
```

Streaming uses the existing Telnyx API key, starts sending audio while you speak, and falls back to the normal batch path if it does not produce a transcript quickly enough after release. Supported values are `deepgram` for Nova-3 streaming and `assemblyai` for AssemblyAI Universal-Streaming through Telnyx.
Streaming uses the existing Telnyx API key. Supported values are `deepgram` for Nova-3 streaming and `assemblyai` for AssemblyAI Universal-Streaming through Telnyx. Selecting a different `BOLO_STT_MODEL` keeps streaming off unless `BOLO_STT_STREAMING` is explicitly set.

To keep dictated text on the clipboard after insertion, set:

Expand Down
64 changes: 53 additions & 11 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3054,16 +3054,18 @@ impl Config {
value.to_owned(),
));
}
let stt_model =
load_env_value("BOLO_STT_MODEL").unwrap_or_else(|| String::from("deepgram/nova-3"));
let streaming_stt = load_streaming_provider(&stt_model);
Ok(Self {
telnyx_api_key,
llm_cleanup,
litellm_base: load_env_value("LITELLM_BASE"),
litellm_key: load_env_value("LITELLM_KEY"),
stt_model: load_env_value("BOLO_STT_MODEL")
.unwrap_or_else(|| String::from("deepgram/nova-3")),
stt_model,
stt_language: load_env_value("BOLO_STT_LANGUAGE")
.unwrap_or_else(|| String::from("en-US")),
streaming_stt: load_streaming_provider(),
streaming_stt,
stt_fallbacks: load_stt_fallbacks(),
microphone: load_env_value("BOLO_MICROPHONE"),
replacements: load_replacements(),
Expand Down Expand Up @@ -4981,11 +4983,27 @@ fn stt_language_for_model(model: &str, configured_language: &str) -> Option<Stri
Some(language.to_owned())
}

fn load_streaming_provider() -> Option<StreamingProvider> {
let value = load_env_value("BOLO_STT_STREAMING")
.or_else(|| load_env_value("BOLO_STREAMING_STT"))
.unwrap_or_else(|| String::from("off"));
match value.trim().to_ascii_lowercase().as_str() {
fn load_streaming_provider(stt_model: &str) -> Option<StreamingProvider> {
let value =
load_env_value("BOLO_STT_STREAMING").or_else(|| load_env_value("BOLO_STREAMING_STT"));
streaming_provider_from_config(value.as_deref(), stt_model)
}

fn streaming_provider_from_config(
value: Option<&str>,
stt_model: &str,
) -> Option<StreamingProvider> {
let default = if stt_model.eq_ignore_ascii_case("deepgram/nova-3") {
"deepgram"
} else {
"off"
};
match value
.unwrap_or(default)
.trim()
.to_ascii_lowercase()
.as_str()
{
"assemblyai" | "assembly" | "on" | "true" | "1" => Some(StreamingProvider::AssemblyAi),
"deepgram" | "nova-3" | "nova3" => Some(StreamingProvider::Deepgram),
_ => None,
Expand Down Expand Up @@ -6037,9 +6055,9 @@ mod tests {
parse_stt_fallbacks, parse_u64_env_value, parse_update_outcome, read_vocabulary_file,
remove_fillers, sanitize_transcript_history, speech_stats,
stable_streaming_best_is_ready_elapsed, streaming_batch_fallback_reason,
streaming_preview_tail, strip_reasoning_tags, stt_language_for_model, stt_model_config,
telnyx_stream_query, transcript_log_value, transcript_menu_preview, valid_deferred_cleanup,
wav_bytes,
streaming_preview_tail, streaming_provider_from_config, strip_reasoning_tags,
stt_language_for_model, stt_model_config, telnyx_stream_query, transcript_log_value,
transcript_menu_preview, valid_deferred_cleanup, wav_bytes,
};
use std::collections::VecDeque;
use std::path::PathBuf;
Expand Down Expand Up @@ -6451,6 +6469,30 @@ mod tests {
assert!(!assembly.contains("keyterm="));
}

#[test]
fn defaults_to_deepgram_streaming_for_the_default_stt_model() {
assert_eq!(
streaming_provider_from_config(None, "deepgram/nova-3"),
Some(StreamingProvider::Deepgram)
);
assert_eq!(
streaming_provider_from_config(None, "openai/whisper-large-v3-turbo"),
None
);
assert_eq!(
streaming_provider_from_config(Some("off"), "deepgram/nova-3"),
None
);
assert_eq!(
streaming_provider_from_config(Some("deepgram"), "openai/whisper-large-v3-turbo"),
Some(StreamingProvider::Deepgram)
);
assert_eq!(
streaming_provider_from_config(Some("assemblyai"), "deepgram/nova-3"),
Some(StreamingProvider::AssemblyAi)
);
}

#[test]
fn streaming_text_prefers_longer_partial_tail() {
let transcript = StreamingTranscript {
Expand Down