Skip to content

Commit 400ff6b

Browse files
committed
Fix Zhipu ASR endpoint handling
1 parent e889858 commit 400ff6b

2 files changed

Lines changed: 115 additions & 7 deletions

File tree

openless-all/app/src-tauri/src/asr/whisper.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,7 @@ impl WhisperBatchASR {
101101
.map(|chunk| i16::from_le_bytes([chunk[0], chunk[1]]))
102102
.collect();
103103
let wav = encode_wav_16k_mono(&samples);
104-
let base_url = self.base_url.trim_end_matches('/');
105-
let url = format!("{}/audio/transcriptions", base_url);
104+
let url = transcription_url(&self.base_url)?;
106105

107106
let wav_part = reqwest::multipart::Part::bytes(wav)
108107
.file_name("audio.wav")
@@ -173,6 +172,23 @@ fn split_pcm_by_duration(pcm: &[u8], max_chunk_duration_ms: Option<u64>) -> Vec<
173172
pcm.chunks(bytes_per_chunk).collect()
174173
}
175174

175+
fn transcription_url(base_url: &str) -> Result<String> {
176+
let parsed = reqwest::Url::parse(base_url.trim()).context("parse Whisper base URL")?;
177+
let mut url = parsed.clone();
178+
let path = parsed.path().trim_end_matches('/');
179+
let next_path = if path.ends_with("/audio/transcriptions") {
180+
path.to_string()
181+
} else if path.ends_with("/audio") {
182+
format!("{path}/transcriptions")
183+
} else if let Some(prefix) = path.strip_suffix("/chat/completions") {
184+
format!("{prefix}/audio/transcriptions")
185+
} else {
186+
format!("{path}/audio/transcriptions")
187+
};
188+
url.set_path(&next_path);
189+
Ok(url.to_string())
190+
}
191+
176192
fn join_transcript_chunks(chunks: &[String]) -> String {
177193
let mut joined = String::new();
178194
for chunk in chunks.iter().map(|chunk| chunk.trim()) {
@@ -462,6 +478,29 @@ mod tests {
462478
assert_eq!(split_pcm_by_duration(&pcm, Some(0)), vec![pcm.as_slice()]);
463479
}
464480

481+
#[test]
482+
fn transcription_url_accepts_base_audio_or_full_endpoint() {
483+
assert_eq!(
484+
transcription_url("https://open.bigmodel.cn/api/paas/v4").unwrap(),
485+
"https://open.bigmodel.cn/api/paas/v4/audio/transcriptions"
486+
);
487+
assert_eq!(
488+
transcription_url("https://open.bigmodel.cn/api/paas/v4/audio").unwrap(),
489+
"https://open.bigmodel.cn/api/paas/v4/audio/transcriptions"
490+
);
491+
assert_eq!(
492+
transcription_url("https://open.bigmodel.cn/api/paas/v4/audio/transcriptions").unwrap(),
493+
"https://open.bigmodel.cn/api/paas/v4/audio/transcriptions"
494+
);
495+
assert_eq!(
496+
transcription_url(
497+
"https://open.bigmodel.cn/api/paas/v4/audio/transcriptions?api-version=2026-01-01"
498+
)
499+
.unwrap(),
500+
"https://open.bigmodel.cn/api/paas/v4/audio/transcriptions?api-version=2026-01-01"
501+
);
502+
}
503+
465504
#[test]
466505
fn join_transcript_chunks_skips_empty_chunks() {
467506
let chunks = vec![" hello ".to_string(), "".to_string(), "world".to_string()];
@@ -571,6 +610,7 @@ mod tests {
571610
Err(err) => panic!("accept ASR test request failed: {err}"),
572611
}
573612
};
613+
stream.set_nonblocking(false).unwrap();
574614
stream
575615
.set_read_timeout(Some(Duration::from_secs(5)))
576616
.unwrap();

openless-all/app/src-tauri/src/coordinator.rs

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ enum CapsuleShowStrategy {
7979
FallbackShow,
8080
}
8181

82+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83+
enum CapsuleWindowAction {
84+
Show,
85+
Hide,
86+
Ignore,
87+
}
88+
8289
fn capsule_show_strategy_for_platform() -> CapsuleShowStrategy {
8390
// ⚠️ 如果改下面的 cfg 列表,**必须**同步更新单元测试
8491
// `capsule_show_strategy_matches_platform_activation_contract` 的两组 cfg —
@@ -112,6 +119,30 @@ fn capsule_state_log_name(state: CapsuleState) -> &'static str {
112119
}
113120
}
114121

122+
fn capsule_window_action(state: CapsuleState, show_capsule: bool) -> CapsuleWindowAction {
123+
#[cfg(target_os = "linux")]
124+
{
125+
let _ = show_capsule;
126+
if matches!(state, CapsuleState::Error) {
127+
return CapsuleWindowAction::Show;
128+
}
129+
if matches!(state, CapsuleState::Idle) {
130+
return CapsuleWindowAction::Hide;
131+
}
132+
return CapsuleWindowAction::Ignore;
133+
}
134+
135+
#[cfg(not(target_os = "linux"))]
136+
{
137+
let visible = !matches!(state, CapsuleState::Idle);
138+
if show_capsule && visible {
139+
CapsuleWindowAction::Show
140+
} else {
141+
CapsuleWindowAction::Hide
142+
}
143+
}
144+
}
145+
115146
fn show_capsule_window_for_recording<R: tauri::Runtime>(
116147
app: &AppHandle<R>,
117148
window: &tauri::WebviewWindow<R>,
@@ -3788,6 +3819,45 @@ mod tests {
37883819
);
37893820
}
37903821

3822+
#[test]
3823+
fn capsule_window_action_keeps_linux_errors_visible_without_showing_recording() {
3824+
#[cfg(target_os = "linux")]
3825+
{
3826+
assert_eq!(
3827+
capsule_window_action(CapsuleState::Recording, true),
3828+
CapsuleWindowAction::Ignore
3829+
);
3830+
assert_eq!(
3831+
capsule_window_action(CapsuleState::Transcribing, true),
3832+
CapsuleWindowAction::Ignore
3833+
);
3834+
assert_eq!(
3835+
capsule_window_action(CapsuleState::Error, false),
3836+
CapsuleWindowAction::Show
3837+
);
3838+
assert_eq!(
3839+
capsule_window_action(CapsuleState::Idle, false),
3840+
CapsuleWindowAction::Hide
3841+
);
3842+
}
3843+
3844+
#[cfg(not(target_os = "linux"))]
3845+
{
3846+
assert_eq!(
3847+
capsule_window_action(CapsuleState::Recording, true),
3848+
CapsuleWindowAction::Show
3849+
);
3850+
assert_eq!(
3851+
capsule_window_action(CapsuleState::Error, false),
3852+
CapsuleWindowAction::Hide
3853+
);
3854+
assert_eq!(
3855+
capsule_window_action(CapsuleState::Idle, true),
3856+
CapsuleWindowAction::Hide
3857+
);
3858+
}
3859+
}
3860+
37913861
#[test]
37923862
#[cfg(target_os = "windows")]
37933863
fn prepared_windows_ime_slot_is_taken_only_for_matching_session() {
@@ -4480,10 +4550,8 @@ fn emit_capsule(
44804550
return;
44814551
};
44824552
let show_capsule = inner_for_main.prefs.get().show_capsule;
4483-
// Linux: 不操作胶囊窗口(不 show/hide,不 reposition)。
4484-
// 文字通过 fcitx5 插件直接 commit,用户始终在目标 app 中。
4485-
#[cfg(target_os = "linux")]
4486-
{
4553+
let window_action = capsule_window_action(state, show_capsule);
4554+
if matches!(window_action, CapsuleWindowAction::Ignore) {
44874555
return;
44884556
}
44894557

@@ -4493,7 +4561,7 @@ fn emit_capsule(
44934561
// `hide_capsule_window_if_present()` Win32 hard-hide 在 visible=false 分支
44944562
// 处理,不依赖把 Done/Cancelled/Error 打成 invisible。详见 PR #140 评论。
44954563
maybe_position_capsule_bottom_center(&inner_for_main, &window, translation);
4496-
if show_capsule && visible {
4564+
if matches!(window_action, CapsuleWindowAction::Show) {
44974565
// 用户报"看不到胶囊"时第一时间能在 log 里确认:胶囊路径有跑、show_capsule
44984566
// 开关是 true、当前进入 visible 帧 —— 排除 prefs 没存住 / emit_capsule 没触
44994567
// 发 / state 一直 Idle 这几类常见 root cause。issue #470。

0 commit comments

Comments
 (0)