diff --git a/openless-all/app/src-tauri/src/asr/local/foundry_provider.rs b/openless-all/app/src-tauri/src/asr/local/foundry_provider.rs index c8f26495..ef09d88d 100644 --- a/openless-all/app/src-tauri/src/asr/local/foundry_provider.rs +++ b/openless-all/app/src-tauri/src/asr/local/foundry_provider.rs @@ -148,7 +148,7 @@ impl crate::recorder::AudioConsumer for FoundryLocalWhisperAsr { } fn pcm_duration_ms(pcm: &[u8]) -> u64 { - (pcm.len() as u64 / 2) * 1000 / 16_000 + crate::asr::pcm::pcm_duration_ms(pcm) } fn pcm_to_wav(pcm: &[u8]) -> Vec { diff --git a/openless-all/app/src-tauri/src/asr/local/sherpa_provider.rs b/openless-all/app/src-tauri/src/asr/local/sherpa_provider.rs index 06016197..210fd2a3 100644 --- a/openless-all/app/src-tauri/src/asr/local/sherpa_provider.rs +++ b/openless-all/app/src-tauri/src/asr/local/sherpa_provider.rs @@ -222,11 +222,11 @@ impl crate::recorder::AudioConsumer for SherpaOnnxAsr { } fn pcm_duration_ms(pcm: &[u8]) -> u64 { - pcm_duration_ms_from_bytes(pcm.len() as u64) + crate::asr::pcm::pcm_duration_ms(pcm) } fn pcm_duration_ms_from_bytes(bytes: u64) -> u64 { - (bytes / 2) * 1000 / 16_000 + crate::asr::pcm::pcm_duration_ms_from_bytes(bytes) } fn trim_transcript_text(text: &str) -> String { diff --git a/openless-all/app/src-tauri/src/asr/local/sherpa_runtime.rs b/openless-all/app/src-tauri/src/asr/local/sherpa_runtime.rs index ec56538c..ed808c05 100644 --- a/openless-all/app/src-tauri/src/asr/local/sherpa_runtime.rs +++ b/openless-all/app/src-tauri/src/asr/local/sherpa_runtime.rs @@ -485,7 +485,7 @@ fn accumulate_dir_size(dir: &Path, bytes: &mut u64) { } fn pcm_duration_ms(pcm: &[u8]) -> u64 { - (pcm.len() as u64 / 2) * 1000 / 16_000 + crate::asr::pcm::pcm_duration_ms(pcm) } enum LoadedModel { diff --git a/openless-all/app/src-tauri/src/asr/mimo.rs b/openless-all/app/src-tauri/src/asr/mimo.rs index 5637107d..1fe68522 100644 --- a/openless-all/app/src-tauri/src/asr/mimo.rs +++ b/openless-all/app/src-tauri/src/asr/mimo.rs @@ -178,7 +178,7 @@ pub fn extract_mimo_text(json: &Value) -> String { } fn pcm_duration_ms(pcm: &[u8]) -> u64 { - (pcm.len() as u64 / PCM_BYTES_PER_SAMPLE as u64) * 1000 / PCM_SAMPLE_RATE_HZ + super::pcm::pcm_duration_ms(pcm) } fn split_pcm_by_duration(pcm: &[u8], max_chunk_duration_ms: u64) -> Vec<&[u8]> { diff --git a/openless-all/app/src-tauri/src/asr/mod.rs b/openless-all/app/src-tauri/src/asr/mod.rs index 1b51857f..e1c761c4 100644 --- a/openless-all/app/src-tauri/src/asr/mod.rs +++ b/openless-all/app/src-tauri/src/asr/mod.rs @@ -9,6 +9,7 @@ pub mod bailian; mod frame; pub mod local; pub mod mimo; +pub mod pcm; pub mod volcengine; pub mod wav; pub mod whisper; diff --git a/openless-all/app/src-tauri/src/asr/pcm.rs b/openless-all/app/src-tauri/src/asr/pcm.rs new file mode 100644 index 00000000..1c4b2865 --- /dev/null +++ b/openless-all/app/src-tauri/src/asr/pcm.rs @@ -0,0 +1,38 @@ +//! 共享 PCM 时长计算。 +//! +//! 录音统一是 16 kHz / 单声道 / 16-bit 小端 PCM,时长换算 `(字节数 / 2) * 1000 / 16000` +//! 原本散落在各 ASR provider 里重复(foundry / sherpa / whisper / mimo),这里收口成 +//! 唯一实现,各处改为薄封装调用(参考 `wav::encode_wav_16k_mono` 的共享先例)。 + +/// 每个采样的字节数(16-bit → 2 字节)。 +const PCM_BYTES_PER_SAMPLE: u64 = 2; +/// 采样率(16 kHz)。 +const PCM_SAMPLE_RATE_HZ: u64 = 16_000; + +/// 由原始字节数计算 16 kHz / 单声道 / 16-bit PCM 的时长(毫秒)。 +pub fn pcm_duration_ms_from_bytes(bytes: u64) -> u64 { + (bytes / PCM_BYTES_PER_SAMPLE) * 1000 / PCM_SAMPLE_RATE_HZ +} + +/// 由 PCM 字节切片计算 16 kHz / 单声道 / 16-bit PCM 的时长(毫秒)。 +pub fn pcm_duration_ms(pcm: &[u8]) -> u64 { + pcm_duration_ms_from_bytes(pcm.len() as u64) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn one_second_of_16k_i16_pcm_is_1000ms() { + // 16000 采样 × 2 字节 = 32000 字节 = 1 秒 + assert_eq!(pcm_duration_ms(&vec![0u8; 32_000]), 1000); + assert_eq!(pcm_duration_ms_from_bytes(32_000), 1000); + } + + #[test] + fn odd_trailing_byte_is_floored() { + // 末尾半个采样向下取整,与历史行为一致 + assert_eq!(pcm_duration_ms(&vec![0u8; 33]), pcm_duration_ms(&vec![0u8; 32])); + } +} diff --git a/openless-all/app/src-tauri/src/asr/whisper.rs b/openless-all/app/src-tauri/src/asr/whisper.rs index 788e5b9a..f5626734 100644 --- a/openless-all/app/src-tauri/src/asr/whisper.rs +++ b/openless-all/app/src-tauri/src/asr/whisper.rs @@ -285,7 +285,7 @@ fn extract_confident_text(json: &serde_json::Value) -> String { } fn pcm_duration_ms(pcm: &[u8]) -> u64 { - (pcm.len() as u64 / PCM_BYTES_PER_SAMPLE as u64) * 1000 / PCM_SAMPLE_RATE_HZ + super::pcm::pcm_duration_ms(pcm) } fn split_pcm_by_duration(pcm: &[u8], max_chunk_duration_ms: Option) -> Vec<&[u8]> {