feat(asr): enable speaker diarization to filter non-primary speakers (#760)

Open volcengine streaming ASR's `enable_speaker_info` flag so each
returned utterance carries a speaker label. Then in the result
assembly, group utterances by speaker, pick the one with the longest
total speaking time as the "primary speaker", and only concatenate
their text — dropping utterances from other speakers (e.g. a
colleague interjecting while you dictate).

This is a heuristic quick-win for the "only transcribe my voice"
request; it does not yet do enrolled voiceprint verification. If no
speaker labels are present, behavior is unchanged (all utterances
concatenated).

Co-authored-by: Shicongcoding <Shicongcoding@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@owtffssent.com>
This commit is contained in:
authored and GitHub committed 2026-07-05 11:26:23 +08:00
1 parent 3ca6883d3a
commit f77bc8fccd
1 file changed
+51 -4
@@ -448,6 +448,7 @@ impl VolcengineStreamingASR {
"enable_itn": true,
"enable_punc": true,
"show_utterances": true,
"enable_speaker_info": true,
});
if let Some(context) = hotword_context(&self.hotwords) {
request["context"] = Value::String(context);
@@ -529,13 +530,59 @@ impl VolcengineStreamingASR {
.to_string();
if let Some(utterances) = result.get("utterances").and_then(|v| v.as_array()) {
// 优先用 utterances 拼接的文本(包含全部分段,不论 definite 与否)
let pieces: Vec<&str> = utterances
// --- 声纹过滤:只保留主要说话人(说话时长最长的) ---
// 1. 统计每个 speaker 的说话时长
let mut speaker_durations: std::collections::HashMap<String, u64> =
std::collections::HashMap::new();
for u in utterances.iter() {
if let (Some(speaker), Some(start), Some(end)) = (
u.get("speaker").and_then(|s| s.as_str()),
u.get("start_time").and_then(|t| t.as_u64()),
u.get("end_time").and_then(|t| t.as_u64()),
) {
let dur = end.saturating_sub(start);
*speaker_durations.entry(speaker.to_string()).or_insert(0) += dur;
}
}
// 2. 找到说话时长最长的 speaker(即"主要说话人")
let primary_speaker: Option<String> = speaker_durations
.iter()
.filter_map(|u| u.get("text").and_then(|t| t.as_str()))
.collect();
.max_by_key(|(_, &dur)| dur)
.map(|(s, _)| s.clone());
// 3. 只拼接主要说话人的文本;如果没有任何 speaker 标签,回退到全量拼接
let pieces: Vec<&str> = if let Some(ref primary) = primary_speaker {
utterances
.iter()
.filter(|u| {
u.get("speaker")
.and_then(|s| s.as_str())
.map(|s| s == primary.as_str())
.unwrap_or(true) // 无 speaker 字段的 utterance 保留
})
.filter_map(|u| u.get("text").and_then(|t| t.as_str()))
.collect()
} else {
utterances
.iter()
.filter_map(|u| u.get("text").and_then(|t| t.as_str()))
.collect()
};
if !pieces.is_empty() {
full_text = pieces.join("");
if let Some(ref primary) = primary_speaker {
let filtered_count = utterances.len().saturating_sub(pieces.len());
if filtered_count > 0 {
log::info!(
"[asr] speaker filter: primary={}, kept={}, filtered={}",
primary,
pieces.len(),
filtered_count
);
}
}
}
}