diff --git a/openless-all/app/src-tauri/src/asr/whisper.rs b/openless-all/app/src-tauri/src/asr/whisper.rs index f5626734..06b75a96 100644 --- a/openless-all/app/src-tauri/src/asr/whisper.rs +++ b/openless-all/app/src-tauri/src/asr/whisper.rs @@ -87,6 +87,12 @@ impl WhisperBatchASR { /// Whisper transcriptions endpoint. /// /// 失败时**保留** PCM buffer,让上层有机会重试或在历史中至少留一个失败记录; + /// 当前缓冲音频时长(毫秒)。Coordinator 在 transcribe() 调用前读取, + /// 用于计算 Whisper / OpenRouter 的动态超时。不消费缓冲。 + pub fn buffer_duration_ms(&self) -> u64 { + pcm_duration_ms(&self.buffer.lock()) + } + /// 之前的实现一进函数就 `mem::take` 把 buffer 清空,凭证错或网络中断都会 /// 让用户的录音直接消失。 pub async fn transcribe(&self) -> Result { diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index 3ee50c36..81e506a3 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -2897,9 +2897,9 @@ const CAPSULE_AUTO_HIDE_DELAY_MS: u64 = 2000; const POST_SESSION_COOLDOWN_MS: u64 = 600; /// Coordinator 全局超时保护:防止 ASR await_final_result() 永远挂起。 -/// 设置为 15 秒(比 ASR 的 12 秒 FINAL_RESULT_TIMEOUT 稍长), -/// 只在 ASR 超时机制失效时作为最后的防线触发。 -const COORDINATOR_GLOBAL_TIMEOUT_SECS: u64 = 15; +/// 设置为 30 秒,为云端 batch ASR(OpenRouter Whisper 等)提供足够的 +/// 网络超时预算;只在 ASR 自身超时机制失效时作为最后的防线触发。 +const COORDINATOR_GLOBAL_TIMEOUT_SECS: u64 = 30; #[cfg(target_os = "windows")] fn foundry_audio_transcribe_timeout_duration() -> std::time::Duration { @@ -2917,6 +2917,17 @@ fn local_qwen_transcribe_timeout(audio_secs: f64) -> std::time::Duration { std::time::Duration::from_secs(secs) } +/// Whisper / OpenRouter 云端 batch ASR 的动态转写超时。OpenRouter 按 30s +/// 分片,每片是一次 HTTP round-trip;网络抖动、排队、base64 body 都会 +/// 拉长耗时。公式 max(30, ceil(audio_s × 0.5) + 20):30s 是全局兜底; +/// 长录音按音频长度的 0.5 倍 + 20s 余量,覆盖多分片串行请求 + 网络波动。 +fn whisper_transcribe_timeout(audio_secs: f64) -> std::time::Duration { + let secs = ((audio_secs * 0.5).ceil() as u64) + .saturating_add(20) + .max(COORDINATOR_GLOBAL_TIMEOUT_SECS); + std::time::Duration::from_secs(secs) +} + /// sherpa-onnx offline batch 暂与 Foundry 同档;后续按 Windows 真机 CPU/模型 /// 实测结果再调整。 #[cfg(target_os = "windows")] diff --git a/openless-all/app/src-tauri/src/coordinator/dictation.rs b/openless-all/app/src-tauri/src/coordinator/dictation.rs index 72a34bc8..910a7745 100644 --- a/openless-all/app/src-tauri/src/coordinator/dictation.rs +++ b/openless-all/app/src-tauri/src/coordinator/dictation.rs @@ -1691,8 +1691,15 @@ pub(super) async fn end_session(inner: &Arc) -> Result<(), String> { } ActiveAsr::Whisper(w) => { debug_assert!(uses_global_timeout); - // Whisper 也添加类似的超时保护 - let timeout_duration = std::time::Duration::from_secs(COORDINATOR_GLOBAL_TIMEOUT_SECS); + // Whisper / OpenRouter 动态超时:音频越长、分片越多,给更多 + // HTTP round-trip 预算。公式见 `whisper_transcribe_timeout`。 + let audio_secs = (w.buffer_duration_ms() as f64) / 1000.0; + let timeout_duration = 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)) => { @@ -1712,8 +1719,9 @@ pub(super) async fn end_session(inner: &Arc) -> Result<(), String> { } Err(_) => { log::error!( - "[coord] whisper 全局超时 {} 秒", - COORDINATOR_GLOBAL_TIMEOUT_SECS + "[coord] Whisper 动态超时 {}s(音频 {:.2}s)", + timeout_duration.as_secs(), + audio_secs ); emit_capsule( inner,