Skip to content

Commit 65a4a60

Browse files
authored
Merge pull request #82 from pragmatrix/voice-live
Add Microsoft voice live service
2 parents 99f7023 + a8f1328 commit 65a4a60

21 files changed

Lines changed: 735 additions & 15 deletions

File tree

.harper-dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ AirPods
22
BCP
33
ContextSwitch
44
FreeSWITCH
5+
VAD
56
alsa
67
inband
78
seekable

CONTEXT.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Context
2+
3+
Glossary of canonical terms for context-switch. Keep this free of implementation
4+
detail — it is a glossary, not a spec.
5+
6+
## Speech provider terms
7+
8+
These three Microsoft offerings are distinct and must not all be called "Azure".
9+
10+
- **Azure Speech** — Microsoft's Azure AI Speech SDK service. In this repo it backs
11+
the `azure` crate (`AzureTranscribe`, `AzureSynthesize`, ...). Classic
12+
speech-to-text / text-to-speech.
13+
14+
- **Azure OpenAI Realtime** — the Azure-hosted variant of the OpenAI Realtime API.
15+
Reached through `openai-dialog` with `Protocol::Azure`. A realtime
16+
speech-to-speech dialog protocol over a single websocket.
17+
18+
- **Voice Live** — Microsoft Foundry's managed speech-to-speech API
19+
(`/voice-live/realtime`). Wire-compatible with Azure OpenAI Realtime but adds
20+
Azure-only capabilities (deep noise suppression, Azure semantic VAD, Azure
21+
speech / MAI transcription models). Backed by the `microsoft-voice-live` crate.
22+
23+
Note: Voice Live and Azure OpenAI Realtime are expected to converge over time;
24+
the `microsoft-voice-live` boundary is kept deliberately thin so the two can be
25+
merged later without a rewrite.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ members = [
1515
"services/elevenlabs",
1616
"services/google-dialog",
1717
"services/google-transcribe",
18+
"services/microsoft-voice-live",
1819
"services/openai-dialog",
1920
"services/playback",
2021
]
@@ -38,6 +39,7 @@ azure-speech = { workspace = true }
3839
aristech = { workspace = true }
3940
elevenlabs = { workspace = true }
4041
google-transcribe = { workspace = true }
42+
microsoft-voice-live = { workspace = true }
4143

4244
# basic
4345

@@ -104,6 +106,7 @@ aristech = { path = "services/aristech" }
104106
elevenlabs = { path = "services/elevenlabs" }
105107
google-transcribe = { path = "services/google-transcribe" }
106108
google-dialog = { path = "services/google-dialog" }
109+
microsoft-voice-live = { path = "services/microsoft-voice-live" }
107110
gemini-live = { path = "external/gemini-live-rs/crates/gemini-live" }
108111

109112
# Dependencies required by `external/gemini-live-rs/crates/gemini-live`.

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ cargo run --example transcribe -- elevenlabs
6969
# Run generic transcribe example with Aristech provider
7070
cargo run --example transcribe -- aristech
7171

72+
# Run generic transcribe example with Microsoft Voice Live provider
73+
cargo run --example transcribe -- voice-live
74+
7275
# Run Azure synthesize example
7376
cargo run --example azure-synthesize
7477

@@ -105,6 +108,13 @@ AZURE_REGION=your_azure_region
105108
# ElevenLabs Configuration
106109
ELEVENLABS_API_KEY=your_elevenlabs_key
107110
111+
# Microsoft Voice Live Configuration
112+
MICROSOFT_VOICE_LIVE_API_KEY=your_voice_live_key
113+
MICROSOFT_VOICE_LIVE_ENDPOINT=wss://your-resource.services.ai.azure.com/voice-live/realtime
114+
MICROSOFT_VOICE_LIVE_MODEL=gpt-4o-mini-realtime-preview
115+
MICROSOFT_VOICE_LIVE_API_VERSION=2026-06-01-preview
116+
MICROSOFT_VOICE_LIVE_TRANSCRIPTION_MODEL=azure-speech
117+
108118
# Audio Knife Configuration
109119
AUDIO_KNIFE_ADDRESS=127.0.0.1:8123
110120

examples/transcribe.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@ use std::time::Duration;
55
use anyhow::{Context, Result, bail};
66
use clap::{Parser, ValueEnum};
77
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
8+
use openai_api_rs::realtime::types::{
9+
AzureSemanticVadConfig, EndOfUtteranceDetectionConfig, EndOfUtteranceDetectionModel,
10+
EndOfUtteranceThresholdLevel, TurnDetection,
11+
};
812
use rodio::DeviceSinkBuilder;
913
use tokio::select;
1014
use tokio::sync::mpsc::{channel, unbounded_channel};
1115

1216
use context_switch::services::{
1317
AristechTranscribe, AzureTranscribe, ElevenLabsTranscribe, GoogleTranscribe,
18+
MicrosoftVoiceLiveTranscribe,
1419
};
1520
use context_switch::{AudioConsumer, InputModality, OutputModality};
1621
use context_switch_core::language::Languages;
@@ -44,6 +49,8 @@ enum Provider {
4449
Google,
4550
#[value(name = "aristech")]
4651
Aristech,
52+
#[value(name = "voice-live")]
53+
VoiceLive,
4754
}
4855

4956
#[tokio::main]
@@ -343,5 +350,51 @@ async fn start_conversation(
343350
};
344351
AristechTranscribe.conversation(params, conversation).await
345352
}
353+
Provider::VoiceLive => {
354+
if diarization {
355+
bail!("--diarization is only supported for the azure provider");
356+
}
357+
if region.is_some() {
358+
bail!("--region is only supported for the google provider");
359+
}
360+
361+
let language = Some(
362+
languages
363+
.single()
364+
.context("Voice Live provider supports exactly one --language value")?
365+
.clone(),
366+
);
367+
let vad_languages = language.clone().map(|lang| vec![lang]);
368+
369+
let params = microsoft_voice_live::Params {
370+
api_key: env::var("MICROSOFT_VOICE_LIVE_API_KEY")
371+
.expect("MICROSOFT_VOICE_LIVE_API_KEY undefined"),
372+
endpoint: env::var("MICROSOFT_VOICE_LIVE_ENDPOINT")
373+
.expect("MICROSOFT_VOICE_LIVE_ENDPOINT undefined (must be wss://...)"),
374+
model: model.map(str::to_owned).unwrap_or_else(|| {
375+
env::var("MICROSOFT_VOICE_LIVE_MODEL").unwrap_or_else(|_| "gpt-4.1".to_owned())
376+
}),
377+
api_version: env::var("MICROSOFT_VOICE_LIVE_API_VERSION").ok(),
378+
transcription_model: env::var("MICROSOFT_VOICE_LIVE_TRANSCRIPTION_MODEL")
379+
.unwrap_or_else(|_| "azure-speech".to_owned()),
380+
language,
381+
noise_reduction: None,
382+
turn_detection: Some(TurnDetection::AzureSemanticVadMultilingual(
383+
AzureSemanticVadConfig {
384+
end_of_utterance_detection: Some(EndOfUtteranceDetectionConfig {
385+
model: EndOfUtteranceDetectionModel::SmartEndOfTurnDetection,
386+
threshold_level: Some(EndOfUtteranceThresholdLevel::Low),
387+
timeout_ms: Some(5000),
388+
}),
389+
// remove_filler_words: Some(true),
390+
languages: vad_languages,
391+
..Default::default()
392+
},
393+
)),
394+
};
395+
MicrosoftVoiceLiveTranscribe
396+
.conversation(params, conversation)
397+
.await
398+
}
346399
}
347400
}

justfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ transcribe-google-latest-short:
2525
transcribe-google-latest-long:
2626
cargo run --example transcribe -- google --language de-DE --model latest_long --region eu
2727

28+
transcribe-voice-live-de:
29+
cargo run --example transcribe -- voice-live --language de-DE
30+
2831
transcribe-google-diarization:
2932
cargo run --example transcribe -- google --diarization --language de-DE --model chirp_3 --region eu
3033

services/google-dialog/src/types.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,12 @@ enum OpenAiToolType {
176176
}
177177

178178
#[derive(Debug, Serialize, Deserialize)]
179-
#[serde(tag = "type", rename_all = "camelCase")]
179+
#[serde(
180+
tag = "type",
181+
rename_all = "camelCase",
182+
rename_all_fields = "camelCase"
183+
)]
180184
pub enum ServiceInputEvent {
181-
#[serde(rename_all = "camelCase")]
182185
FunctionCallResult {
183186
call_id: String,
184187
output: serde_json::Value,
@@ -189,15 +192,17 @@ pub enum ServiceInputEvent {
189192
}
190193

191194
#[derive(Debug, Serialize, Deserialize)]
192-
#[serde(tag = "type", rename_all = "camelCase")]
195+
#[serde(
196+
tag = "type",
197+
rename_all = "camelCase",
198+
rename_all_fields = "camelCase"
199+
)]
193200
pub enum ServiceOutputEvent {
194-
#[serde(rename_all = "camelCase")]
195201
FunctionCall {
196202
call_id: String,
197203
name: String,
198204
arguments: serde_json::Value,
199205
},
200-
#[serde(rename_all = "camelCase")]
201206
ToolCallCancellation {
202207
call_id: String,
203208
},
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "microsoft-voice-live"
3+
version = "0.1.0"
4+
edition.workspace = true
5+
6+
[dependencies]
7+
context-switch-core = { workspace = true }
8+
9+
openai-api-rs = { workspace = true }
10+
11+
tokio-tungstenite = { version = "0.29.0", features = ["connect", "native-tls"] }
12+
13+
tracing = { workspace = true }
14+
15+
anyhow = { workspace = true }
16+
futures = { workspace = true }
17+
tokio = { workspace = true, features = ["net"] }
18+
serde_json = { workspace = true }
19+
base64 = { workspace = true }
20+
serde = { workspace = true, features = ["derive"] }
21+
async-trait = { workspace = true }
22+
url = { workspace = true }

0 commit comments

Comments
 (0)