Merge pull request #646 from katanumahotori/codex/whisper-dynamic-timeout-upstream

Scale Cloud Whisper timeout for long recordings
This commit is contained in:
Chris233 authored and GitHub committed 2026-06-12 16:10:16 +08:00
commit 8b7dd83dcc
4 files changed
+81 -9

No files matched your search

+18 -6
View File
@@ -25,7 +25,7 @@ use crate::asr::local::{
foundry, sherpa, FoundryLocalRuntime, FoundryLocalWhisperAsr, SherpaOnnxAsr, SherpaOnnxRuntime,
};
use crate::asr::{
BailianCredentials, BailianRealtimeASR, DictionaryHotword, MimoBatchASR, RawTranscript,
pcm, BailianCredentials, BailianRealtimeASR, DictionaryHotword, MimoBatchASR, RawTranscript,
VolcengineCredentials, VolcengineStreamingASR, WhisperBatchASR,
};
use crate::combo_hotkey::{ComboHotkeyError, ComboHotkeyEvent, ComboHotkeyMonitor};
@@ -874,7 +874,7 @@ impl Coordinator {
/// 用**当前配置的** ASR provider 对一段已归档的 16k/mono/16-bit PCM 重新转录
/// (issue #613「重新转录」)。复用 `build_qa_asr_start`,对所有 provider 统一:
/// 流式 provider 先 open_session 再灌音并取 final,批处理 provider 直接灌音后
/// transcribe。整段超时走 COORDINATOR_GLOBAL_TIMEOUT_SECS 兜底,防止挂死。
/// transcribe。整段转写按 provider 设置超时,防止挂死。
///
/// 只做 ASR,不做润色/落字/写历史 —— 回写历史由 command 层完成,保持本方法纯粹。
pub async fn retranscribe_pcm(&self, pcm: Vec<u8>) -> Result<String, String> {
@@ -884,6 +884,7 @@ impl Coordinator {
start.open_streaming_session().await?;
let consumer = start.recorder_consumer();
consumer.consume_pcm_chunk(&pcm);
let audio_secs = (pcm::pcm_duration_ms(&pcm) as f64) / 1000.0;
let timeout = std::time::Duration::from_secs(COORDINATOR_GLOBAL_TIMEOUT_SECS);
let raw = match start.active_asr() {
ActiveAsr::Volcengine(asr) => {
@@ -900,10 +901,13 @@ impl Coordinator {
.map_err(|_| "重新转录超时".to_string())?
.map_err(|e| e.to_string())?
}
ActiveAsr::Whisper(w) => tokio::time::timeout(timeout, w.transcribe())
.await
.map_err(|_| "重新转录超时".to_string())?
.map_err(|e| e.to_string())?,
ActiveAsr::Whisper(w) => {
let timeout = cloud_whisper_transcribe_timeout(audio_secs);
tokio::time::timeout(timeout, w.transcribe())
.await
.map_err(|_| "重新转录超时".to_string())?
.map_err(|e| e.to_string())?
}
ActiveAsr::Mimo(m) => tokio::time::timeout(timeout, m.transcribe())
.await
.map_err(|_| "重新转录超时".to_string())?
@@ -931,6 +935,14 @@ impl Coordinator {
asr_setup::schedule_local_asr_release(inner);
out
}
#[cfg(target_os = "macos")]
ActiveAsr::AppleSpeech(local) => {
let dur = asr_setup::local_qwen_transcribe_timeout(audio_secs);
tokio::time::timeout(dur, local.transcribe())
.await
.map_err(|_| "重新转录超时".to_string())?
.map_err(|e| e.to_string())?
}
};
Ok(raw.text)
}
@@ -467,6 +467,8 @@ pub(crate) async fn build_qa_asr_start(
/// 设置为 15 秒(比 ASR 的 12 秒 FINAL_RESULT_TIMEOUT 稍长),
/// 只在 ASR 超时机制失效时作为最后的防线触发。
pub(crate) const COORDINATOR_GLOBAL_TIMEOUT_SECS: u64 = 15;
pub(crate) const CLOUD_WHISPER_MIN_TIMEOUT_SECS: u64 = 30;
pub(crate) const CLOUD_WHISPER_MAX_TIMEOUT_SECS: u64 = 300;
#[cfg(target_os = "windows")]
pub(crate) fn foundry_audio_transcribe_timeout_duration() -> std::time::Duration {
@@ -484,6 +486,17 @@ pub(crate) fn local_qwen_transcribe_timeout(audio_secs: f64) -> std::time::Durat
std::time::Duration::from_secs(secs)
}
/// Cloud Whisper/Groq ASR 的批量转写超时。短录音给 30s 余量;长录音按音频长度
/// 扩展,避免数分钟录音被 15s coordinator guard 提前丢弃。5 分钟上限仍保留为
/// 外部 API 卡死时的最后防线。
pub(crate) fn cloud_whisper_transcribe_timeout(audio_secs: f64) -> std::time::Duration {
let scaled = ((audio_secs * 0.5).ceil() as u64).saturating_add(15);
let secs = scaled
.max(CLOUD_WHISPER_MIN_TIMEOUT_SECS)
.min(CLOUD_WHISPER_MAX_TIMEOUT_SECS);
std::time::Duration::from_secs(secs)
}
/// sherpa-onnx offline batch 暂与 Foundry 同档;后续按 Windows 真机 CPU/模型
/// 实测结果再调整。
#[cfg(target_os = "windows")]
@@ -106,8 +106,13 @@ pub(crate) async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
}
ActiveAsr::Whisper(w) => {
debug_assert!(uses_global_timeout);
// Whisper 也添加类似的超时保护
let timeout_duration = std::time::Duration::from_secs(COORDINATOR_GLOBAL_TIMEOUT_SECS);
let audio_secs = elapsed as f64 / 1000.0;
let timeout_duration = cloud_whisper_transcribe_timeout(audio_secs);
log::info!(
"[coord] whisper transcribe: audio={:.2}s timeout={}s",
audio_secs,
timeout_duration.as_secs()
);
match tokio::time::timeout(timeout_duration, w.transcribe()).await {
Ok(Ok(r)) => r,
Ok(Err(e)) => {
@@ -129,7 +134,7 @@ pub(crate) async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
Err(_) => {
log::error!(
"[coord] whisper 全局超时 {} 秒",
COORDINATOR_GLOBAL_TIMEOUT_SECS
timeout_duration.as_secs()
);
write_transcribe_failed_history(inner, current_session_id, elapsed);
emit_capsule(
@@ -343,6 +343,48 @@ fn local_qwen_timeout_handles_zero_duration() {
);
}
#[test]
fn cloud_whisper_timeout_has_longer_floor_than_global_guard() {
assert_eq!(
cloud_whisper_transcribe_timeout(5.0),
std::time::Duration::from_secs(CLOUD_WHISPER_MIN_TIMEOUT_SECS)
);
}
#[test]
fn cloud_whisper_timeout_handles_zero_duration() {
assert_eq!(
cloud_whisper_transcribe_timeout(0.0),
std::time::Duration::from_secs(CLOUD_WHISPER_MIN_TIMEOUT_SECS)
);
}
#[test]
fn cloud_whisper_timeout_scales_for_long_recordings() {
// 422s recording: ceil(422 * 0.5) + 15 = 226s. Do not drop it at 15s.
assert_eq!(
cloud_whisper_transcribe_timeout(422.0),
std::time::Duration::from_secs(226)
);
}
#[test]
fn cloud_whisper_timeout_is_capped() {
assert_eq!(
cloud_whisper_transcribe_timeout(1_000.0),
std::time::Duration::from_secs(CLOUD_WHISPER_MAX_TIMEOUT_SECS)
);
}
#[test]
fn cloud_whisper_timeout_preserves_exact_cap_boundary() {
// ceil(570 * 0.5) + 15 = 300s, exactly at the cap.
assert_eq!(
cloud_whisper_transcribe_timeout(570.0),
std::time::Duration::from_secs(CLOUD_WHISPER_MAX_TIMEOUT_SECS)
);
}
#[cfg(target_os = "windows")]
#[test]
fn foundry_release_uses_foundry_keep_loaded_preference() {