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
32 changes: 20 additions & 12 deletions openless-all/app/crates/openless-core/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9220,6 +9220,13 @@ impl OpenLessBackend {
}

fn mark_dictation_failed(&self, session_id: SessionId, error: &BackendError) {
// 诊断:失败只写 errorCode 时无法定位是录音停止、转写还是插入哪一步出的错。
log::warn!(
"[dictation] session {} failed: code={:?} message={}",
session_id,
error.code,
error.message
);
let mut state = self.state.write().expect("backend state lock poisoned");
if state.dictation.session_id != Some(session_id) {
return;
Expand Down Expand Up @@ -18130,18 +18137,19 @@ mod tests {
Some(stopping) => stopping.await,
None => backend.stop_dictation().await,
};
if stop_fails {
assert_eq!(result.unwrap_err().code, BackendErrorCode::Platform);
assert_eq!(
backend.list_history().unwrap()[0].error_code.as_deref(),
Some("transcribeFailed")
);
} else {
let result = result.unwrap();
assert_eq!(result.session_id, session_id);
assert_eq!(result.polished_text, "captured prefix");
assert_eq!(transcriber.pcm(), vec![1, 0, 2, 0]);
}
// 录音流的 `stop()` 失败(蓝牙麦掉线、PipeWire 节点被挂起)不再
// 中止这次听写:已经流进 ASR 的音频要照常转写并插入。断言因此
// 与成功路径一致 —— 差别只剩「有没有打过那条 warn」,行为对
// 用户不该有区别。
let result = result.unwrap();
assert_eq!(result.session_id, session_id);
assert_eq!(result.polished_text, "captured prefix");
assert_eq!(transcriber.pcm(), vec![1, 0, 2, 0]);
assert_eq!(
backend.list_history().unwrap()[0].error_code.as_deref(),
None,
"a dropped capture stream must not be recorded as a transcription failure",
);
assert_eq!(recorder.stop_count(), 1);
assert!(archive.lock().unwrap().as_ref().unwrap().is_available());
assert_eq!(
Expand Down
17 changes: 6 additions & 11 deletions openless-all/app/crates/openless-core/src/dictation_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -717,17 +717,12 @@ impl DictationEngine for PipelineDictationEngine {
let archive = recording.archive();
let mut has_audio_recording = archive.as_ref().map(|archive| archive.is_available());
if let Err(error) = recording.stop().await {
demote_failed_archive(
archive.as_ref(),
context.output_target,
&mut has_audio_recording,
)
.await;
let _ = cancel_transcription_once(&session, transcription).await;
remove_session(&sessions, session_id, &session);
let mut failure = EngineFailure::new(error, EngineFailureStage::Transcribing);
failure.has_audio_recording = has_audio_recording;
return Err(failure);
// 录音设备中途掉线(蓝牙麦欠载/节点被挂起时报 "Device disconnected")
// 不应丢掉已经流进 ASR 的内容:这里降级为警告继续转写,
// 由后续「未收到有效音频」守卫决定是否真的没有可识别内容。
log::warn!(
"[dictation] recorder stop failed; transcribing the captured audio anyway: {error}"
);
}
if context.output_target == crate::dictation_context::DictationOutputTarget::QuickNote {
if let Some(archive) = archive.as_ref() {
Expand Down
14 changes: 10 additions & 4 deletions openless-all/app/linux-egui/src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -620,21 +620,27 @@ fn select_input_device(
let devices = host
.input_devices()
.map_err(|error| classify_audio_error("enumerate input devices", error.to_string()))?;
let mut available: Vec<String> = Vec::new();
for device in devices {
if device.to_string() == preferred {
let name = device.to_string();
if name == preferred {
return Ok(device);
}
available.push(name);
}
// 设备名是精确匹配:把候选项一并打出来,省得再猜。
log::warn!(
"preferred Linux microphone was not found; using the default device: {preferred}"
"preferred Linux microphone was not found; using the default device: {preferred}; available: {available:?}"
);
}
host.default_input_device().ok_or_else(|| {
let device = host.default_input_device().ok_or_else(|| {
BackendError::new(
BackendErrorCode::Platform,
"no Linux microphone input device is available",
)
})
})?;
log::info!("[audio] using input device: {}", device);
Ok(device)
}

#[cfg(target_os = "linux")]
Expand Down
35 changes: 31 additions & 4 deletions openless-all/app/linux-egui/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,10 +467,15 @@ fn binding_to_fcitx_key(binding: &ShortcutBinding) -> String {

fn normalize_fcitx_primary(primary: &str) -> String {
let trimmed = primary.trim();
if let Some(stripped) = trimmed.strip_prefix("Key") {
stripped.to_ascii_lowercase()
} else {
trimmed.to_ascii_lowercase()
let stripped = trimmed.strip_prefix("Key").unwrap_or(trimmed);
let lower = stripped.to_ascii_lowercase();
// fcitx5 的 Key::parse 按 X11 keysym 名做**大小写敏感**查表:实测
// `SetCustomDictationTrigger("Menu")` → sym=0xff67 有效,`("menu")` →
// invalid key(2026-10-22)。这里只把查表中确实首字母大写的名字还原,
// 其余继续交给小写(space / f5 / 单字符都已验证可用)。
match lower.as_str() {
"menu" => "Menu".to_string(),
_ => lower,
}
}

Expand Down Expand Up @@ -603,6 +608,28 @@ mod tests {
.is_err());
}

#[test]
fn menu_primary_keeps_the_case_fcitx_expects() {
// fcitx5 的 Key::parse 用 X11 keysym 名做大小写敏感查表:'Menu' 有效、
// 'menu' 被拒(2026-10-22 实测 SetCustomDictationTrigger 返回
// "invalid key")。主键名统一小写的旧逻辑会让 Menu 键永远注不上。
let binding = ShortcutBinding {
primary: "Menu".into(),
modifiers: Vec::new(),
};
assert_eq!(binding_to_fcitx_key(&binding), "Menu");
let key_cased = ShortcutBinding {
primary: "KeyMenu".into(),
modifiers: Vec::new(),
};
assert_eq!(binding_to_fcitx_key(&key_cased), "Menu");
let space = ShortcutBinding {
primary: "Space".into(),
modifiers: vec!["ctrl".into(), "alt".into()],
};
assert_eq!(binding_to_fcitx_key(&space), "Control+Alt+space");
}

#[test]
fn mouse_shortcuts_are_rejected_by_the_linux_host() {
for primary in ["Mouse4", "Mouse5"] {
Expand Down
Loading