Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions openless-all/app/src-tauri/src/asr/whisper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RawTranscript> {
Expand Down
53 changes: 46 additions & 7 deletions openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2326,7 +2326,7 @@ mod tests {

#[test]
fn local_qwen_timeout_floors_at_global_timeout_for_short_audio() {
// 5s 录音:5 × 0.6 = 3, +10 = 13, max(15) = 15。短录音保留 15s 兜底
// 5s 录音:5 × 0.6 = 3, +10 = 13, max(30) = 30。短录音兜底
assert_eq!(
local_qwen_transcribe_timeout(5.0),
std::time::Duration::from_secs(COORDINATOR_GLOBAL_TIMEOUT_SECS)
Expand All @@ -2344,22 +2344,50 @@ mod tests {

#[test]
fn local_qwen_timeout_ceils_partial_seconds() {
// 10.1s 录音:10.1 × 0.6 = 6.06, ceil = 7, +10 = 17, max(15) = 17。
// 10.1s 录音:10.1 × 0.6 = 6.06, ceil = 7, +10 = 17, max(30) = 30。
// COORDINATOR_GLOBAL_TIMEOUT_SECS 提升到 30 后,短音频统一被兜底值覆盖。
assert_eq!(
local_qwen_transcribe_timeout(10.1),
std::time::Duration::from_secs(17)
std::time::Duration::from_secs(COORDINATOR_GLOBAL_TIMEOUT_SECS)
);
}

#[test]
fn local_qwen_timeout_handles_zero_duration() {
// 0 时长(空 buffer 边界):0 × 0.6 = 0, +10 = 10, max(15) = 15
// 0 时长(空 buffer 边界):0 × 0.6 = 0, +10 = 10, max(30) = 30
assert_eq!(
local_qwen_transcribe_timeout(0.0),
std::time::Duration::from_secs(COORDINATOR_GLOBAL_TIMEOUT_SECS)
);
}

#[test]
fn whisper_timeout_floors_at_global_timeout_for_short_audio() {
// 10s 录音:10 × 0.5 = 5, +20 = 25, max(30) = 30。短音频兜底。
assert_eq!(
whisper_transcribe_timeout(10.0),
std::time::Duration::from_secs(COORDINATOR_GLOBAL_TIMEOUT_SECS)
);
}

#[test]
fn whisper_timeout_scales_with_audio_duration() {
// 60s 录音:60 × 0.5 = 30, +20 = 50。覆盖多分片 HTTP 请求。
assert_eq!(
whisper_transcribe_timeout(60.0),
std::time::Duration::from_secs(50)
);
}

#[test]
fn whisper_timeout_ceils_partial_seconds() {
// 45.3s 录音:45.3 × 0.5 = 22.65, ceil = 23, +20 = 43, max(30) = 43。
assert_eq!(
whisper_transcribe_timeout(45.3),
std::time::Duration::from_secs(43)
);
}

#[cfg(target_os = "windows")]
#[test]
fn foundry_release_uses_foundry_keep_loaded_preference() {
Expand Down Expand Up @@ -2897,9 +2925,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 {
Expand All @@ -2917,6 +2945,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")]
Expand Down
16 changes: 12 additions & 4 deletions openless-all/app/src-tauri/src/coordinator/dictation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1691,8 +1691,15 @@ pub(super) 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);
// 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)) => {
Expand All @@ -1712,8 +1719,9 @@ pub(super) async fn end_session(inner: &Arc<Inner>) -> 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,
Expand Down
Loading