Skip to content
Open
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
24 changes: 18 additions & 6 deletions openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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> {
Expand All @@ -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) => {
Expand All @@ -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())?
Expand Down Expand Up @@ -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)
}
Expand Down
13 changes: 13 additions & 0 deletions openless-all/app/src-tauri/src/coordinator/asr_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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")]
Expand Down
11 changes: 8 additions & 3 deletions openless-all/app/src-tauri/src/coordinator/dictation_end.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)) => {
Expand All @@ -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(
Expand Down
42 changes: 42 additions & 0 deletions openless-all/app/src-tauri/src/coordinator/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading