Skip to content

Commit 5bef6f6

Browse files
authored
Merge pull request #60 from pragmatrix/azure-diarization
Add support for speaker diarization in azure-transcribe
2 parents 0a3983d + 3357f4e commit 5bef6f6

14 files changed

Lines changed: 118 additions & 32 deletions

File tree

core/src/conversation.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,18 @@ impl ConversationOutput {
227227
self.post(Output::ClearAudio)
228228
}
229229

230-
pub fn text(&self, is_final: bool, text: String, language: Option<String>) -> Result<()> {
230+
pub fn text(
231+
&self,
232+
is_final: bool,
233+
text: String,
234+
language: Option<String>,
235+
speaker: Option<String>,
236+
) -> Result<()> {
231237
self.post(Output::Text {
232238
is_final,
233239
text,
234240
language,
241+
speaker,
235242
})
236243
}
237244

@@ -323,6 +330,7 @@ pub enum Output {
323330
is_final: bool,
324331
text: String,
325332
language: Option<String>,
333+
speaker: Option<String>,
326334
},
327335
RequestCompleted {
328336
request_id: Option<RequestId>,

examples/azure-transcribe.rs

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,26 @@ async fn main() -> Result<()> {
2222
dotenvy::dotenv_override()?;
2323
tracing_subscriber::fmt::init();
2424

25-
let mut args = env::args();
26-
match args.len() {
27-
1 => recognize_from_microphone().await?,
28-
2 => recognize_from_wav(Path::new(&args.nth(1).unwrap())).await?,
29-
_ => bail!("Invalid number of arguments, expect zero or one"),
25+
let mut input_file: Option<String> = None;
26+
let mut diarization = false;
27+
for arg in env::args().skip(1) {
28+
if arg == "--diarization" {
29+
diarization = true;
30+
} else if input_file.is_none() {
31+
input_file = Some(arg);
32+
} else {
33+
bail!("Invalid arguments. Usage: azure-transcribe [--diarization] [wav-file]");
34+
}
35+
}
36+
37+
match input_file {
38+
None => recognize_from_microphone(diarization).await?,
39+
Some(path) => recognize_from_wav(Path::new(&path), diarization).await?,
3040
}
3141
Ok(())
3242
}
3343

34-
async fn recognize_from_wav(file: &Path) -> Result<()> {
44+
async fn recognize_from_wav(file: &Path, diarization: bool) -> Result<()> {
3545
// For now we always convert to 16khz single channel (this is what we use internally for
3646
// testing).
3747
let format = AudioFormat {
@@ -50,10 +60,10 @@ async fn recognize_from_wav(file: &Path) -> Result<()> {
5060
producer.produce(frame)?;
5161
}
5262

53-
recognize(format, input_consumer).await
63+
recognize(format, input_consumer, diarization).await
5464
}
5565

56-
async fn recognize_from_microphone() -> Result<()> {
66+
async fn recognize_from_microphone(diarization: bool) -> Result<()> {
5767
// Keep an output sink alive so Bluetooth headsets (e.g. AirPods) can switch to a
5868
// bidirectional profile. Without this, some devices report an input stream of zeros.
5969
let _output_sink = match DeviceSinkBuilder::open_default_sink() {
@@ -105,17 +115,22 @@ async fn recognize_from_microphone() -> Result<()> {
105115

106116
stream.play().expect("Failed to play stream");
107117

108-
recognize(format, input_consumer).await
118+
recognize(format, input_consumer, diarization).await
109119
}
110120

111-
async fn recognize(format: AudioFormat, mut input_consumer: AudioConsumer) -> Result<()> {
121+
async fn recognize(
122+
format: AudioFormat,
123+
mut input_consumer: AudioConsumer,
124+
diarization: bool,
125+
) -> Result<()> {
112126
// TODO: clarify how to access configurations.
113127
let params = azure::transcribe::Params {
114128
host: None,
115129
region: Some(env::var("AZURE_REGION").expect("AZURE_REGION undefined")),
116130
subscription_key: env::var("AZURE_SUBSCRIPTION_KEY")
117131
.expect("AZURE_SUBSCRIPTION_KEY undefined"),
118132
language: LANGUAGE.into(),
133+
diarization,
119134
speech_gate: false,
120135
};
121136

examples/azure-translate.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,9 @@ async fn setup_audio_playback(
182182
is_final,
183183
text,
184184
language,
185+
speaker,
185186
} => {
186-
println!("Text: {text}, final: {is_final}, language: {language:?}")
187+
println!("Text ({is_final}, {language:?}, speaker: {speaker:?}): {text}");
187188
}
188189
Output::RequestCompleted { .. } => {}
189190
Output::ClearAudio => {

examples/transcribe.rs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ struct Args {
2929
model: Option<String>,
3030
#[arg(long)]
3131
region: Option<String>,
32+
#[arg(long)]
33+
diarization: bool,
3234
}
3335

3436
#[derive(Debug, Clone, Copy, ValueEnum)]
@@ -52,10 +54,15 @@ async fn main() -> Result<()> {
5254
let languages = Languages::new(args.language)?;
5355
let model = args.model.as_deref();
5456
let region = args.region.as_deref();
57+
let diarization = args.diarization;
5558

5659
match args.input.as_deref() {
57-
Some(path) => recognize_from_wav(args.provider, path, &languages, model, region).await?,
58-
None => recognize_from_microphone(args.provider, &languages, model, region).await?,
60+
Some(path) => {
61+
recognize_from_wav(args.provider, path, &languages, model, region, diarization).await?
62+
}
63+
None => {
64+
recognize_from_microphone(args.provider, &languages, model, region, diarization).await?
65+
}
5966
}
6067

6168
Ok(())
@@ -67,6 +74,7 @@ async fn recognize_from_wav(
6774
languages: &Languages,
6875
model: Option<&str>,
6976
region: Option<&str>,
77+
diarization: bool,
7078
) -> Result<()> {
7179
let format = AudioFormat {
7280
channels: 1,
@@ -83,14 +91,24 @@ async fn recognize_from_wav(
8391
producer.produce(frame)?;
8492
}
8593

86-
recognize(provider, format, input_consumer, languages, model, region).await
94+
recognize(
95+
provider,
96+
format,
97+
input_consumer,
98+
languages,
99+
model,
100+
region,
101+
diarization,
102+
)
103+
.await
87104
}
88105

89106
async fn recognize_from_microphone(
90107
provider: Provider,
91108
languages: &Languages,
92109
model: Option<&str>,
93110
region: Option<&str>,
111+
diarization: bool,
94112
) -> Result<()> {
95113
// Keep an output sink alive so Bluetooth headsets can switch to a bidirectional profile.
96114
let _output_sink = match DeviceSinkBuilder::open_default_sink() {
@@ -131,7 +149,16 @@ async fn recognize_from_microphone(
131149

132150
stream.play().expect("Failed to play stream");
133151

134-
recognize(provider, format, input_consumer, languages, model, region).await
152+
recognize(
153+
provider,
154+
format,
155+
input_consumer,
156+
languages,
157+
model,
158+
region,
159+
diarization,
160+
)
161+
.await
135162
}
136163

137164
async fn recognize(
@@ -141,6 +168,7 @@ async fn recognize(
141168
languages: &Languages,
142169
model: Option<&str>,
143170
region: Option<&str>,
171+
diarization: bool,
144172
) -> Result<()> {
145173
let (output_producer, mut output_consumer) = unbounded_channel();
146174
let (conversation_input_producer, conversation_input_consumer) = channel(16_384);
@@ -150,6 +178,7 @@ async fn recognize(
150178
languages,
151179
model,
152180
region,
181+
diarization,
153182
Conversation::new(
154183
InputModality::Audio { format },
155184
[OutputModality::Text, OutputModality::InterimText],
@@ -190,6 +219,7 @@ async fn start_conversation(
190219
languages: &Languages,
191220
model: Option<&str>,
192221
region: Option<&str>,
222+
diarization: bool,
193223
conversation: Conversation,
194224
) -> Result<()> {
195225
match provider {
@@ -203,11 +233,15 @@ async fn start_conversation(
203233
subscription_key: env::var("AZURE_SUBSCRIPTION_KEY")
204234
.expect("AZURE_SUBSCRIPTION_KEY undefined"),
205235
language: languages.join_csv(),
236+
diarization,
206237
speech_gate: false,
207238
};
208239
AzureTranscribe.conversation(params, conversation).await
209240
}
210241
Provider::Elevenlabs => {
242+
if diarization {
243+
bail!("--diarization is only supported for the azure provider");
244+
}
211245
if region.is_some() {
212246
bail!("--region is only supported for the google provider");
213247
}
@@ -234,6 +268,9 @@ async fn start_conversation(
234268
.await
235269
}
236270
Provider::Google => {
271+
if diarization {
272+
bail!("--diarization is only supported for the azure provider");
273+
}
237274
let region = region
238275
.map(str::to_owned)
239276
.or_else(|| env::var("GOOGLE_TRANSCRIBE_REGION").ok());
@@ -259,6 +296,9 @@ async fn start_conversation(
259296
GoogleTranscribe.conversation(params, conversation).await
260297
}
261298
Provider::Aristech => {
299+
if diarization {
300+
bail!("--diarization is only supported for the azure provider");
301+
}
262302
if region.is_some() {
263303
bail!("--region is only supported for the google provider");
264304
}

justfile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@ knife-release:
77
transcribe-azure-de-en:
88
cargo run --example transcribe -- azure --language de-DE,en-US
99

10+
transcribe-azure-diarization:
11+
cargo run --example transcribe -- azure --diarization
12+
13+
transcribe-azure-diarization-de:
14+
cargo run --example transcribe -- azure --diarization --language de-DE
15+
1016
transcribe-google-de-en:
1117
cargo run --example transcribe -- google --language de-DE,en-US --model chirp_3 --region eu
1218

13-
1419
transcribe-google-latest-short:
1520
cargo run --example transcribe -- google --language de-DE --model latest_short --region eu
1621

1722
transcribe-google-latest-long:
1823
cargo run --example transcribe -- google --language de-DE --model latest_long --region eu
24+

services/aristech/src/transcribe.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ impl Service for AristechTranscribe {
152152

153153
// Instead of processing all alternatives, just take the first one
154154
if let Some(alternative) = chunk.alternatives.into_iter().next() {
155-
output.text(is_final, alternative.text, None)?;
155+
output.text(is_final, alternative.text, None, None)?;
156156
}
157157
}
158158
}

services/azure/src/transcribe.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ pub struct Params {
2222
pub subscription_key: String,
2323
pub language: String,
2424
#[serde(default)]
25+
pub diarization: bool,
26+
#[serde(default)]
2527
pub speech_gate: bool,
2628
}
2729

@@ -55,6 +57,12 @@ impl Service for AzureTranscribe {
5557
// Disable profanity filter.
5658
.set_profanity(recognizer::Profanity::Raw);
5759

60+
let config = if params.diarization {
61+
config.enable_recognize_speaker()
62+
} else {
63+
config
64+
};
65+
5866
let config = if languages.len() == 1 {
5967
config.set_language(recognizer::Language::Custom(languages.first().clone()))
6068
} else {
@@ -145,7 +153,7 @@ fn output_recognized_text(
145153
let recognizer::Recognized {
146154
text,
147155
primary_language,
148-
..
156+
speaker_id: speaker,
149157
} = recognized;
150158

151159
let language = if include_detected_language {
@@ -154,5 +162,9 @@ fn output_recognized_text(
154162
None
155163
};
156164

157-
output.text(is_final, text, language)
165+
// Azure frequently reports speaker as "Unknown" during interim recognition,
166+
// so we only emit speaker information for final text events.
167+
let speaker = if is_final { speaker } else { None };
168+
169+
output.text(is_final, text, language, speaker)
158170
}

services/azure/src/translate.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,13 @@ impl Service for AzureTranslate {
127127
Event::StartDetected(_, _) => {}
128128
Event::EndDetected(_, _) => {}
129129
Event::Translating(_, text, _, _, _) => {
130-
if output_modalities.text {
131-
output.text(false, text, None)?;
130+
if output_modalities.interim_text {
131+
output.text(false, text, None, None)?;
132132
}
133133
}
134134
Event::Translated(_, text, _, _, _) => {
135-
if output_modalities.interim_text {
136-
output.text(true, text, None)?;
135+
if output_modalities.text {
136+
output.text(true, text, None, None)?;
137137
}
138138
}
139139
Event::TranslationSynthesis(_, samples) => {

services/elevenlabs/src/transcribe.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -447,15 +447,15 @@ fn process_server_json(
447447
}
448448
"partial_transcript" => {
449449
let event: PartialTranscript = serde_json::from_value(envelope.payload)?;
450-
output.text(false, event.text, None)
450+
output.text(false, event.text, None, None)
451451
}
452452
"committed_transcript" => {
453453
if include_language_detection {
454454
// Ignoring committed_transcript because include_language_detection=true; expecting committed_transcript_with_timestamps
455455
return Ok(());
456456
}
457457
let event: CommittedTranscript = serde_json::from_value(envelope.payload)?;
458-
output.text(true, event.text, None)
458+
output.text(true, event.text, None, None)
459459
}
460460
"committed_transcript_with_timestamps" => {
461461
let event: CommittedTranscriptWithTimestamps =
@@ -482,7 +482,7 @@ fn process_server_json(
482482
}
483483
});
484484

485-
output.text(true, text, language_code)
485+
output.text(true, text, language_code, None)
486486
}
487487
// Not in the official documentation, but this happens when the language code is invalid.
488488
"invalid_request" => {

0 commit comments

Comments
 (0)