Skip to content

Commit ae37788

Browse files
committed
feat: implement 6 upstream PRs for enhanced functionality
- PR cjpais#477: Graceful handling when no microphone is connected - Validate device config before spawning worker thread - Use HandyError with user-friendly messages and recovery suggestions - PR cjpais#930: Transcription hook for external script processing - Add transcription_hook_enabled and transcription_hook_path settings - Pipe transcription through external scripts for custom processing - PR cjpais#814: Secure API key storage in OS keychain - Add keyring module for macOS Keychain, Windows Credential Manager, Linux Secret Service - Commands: set_api_key, get_api_key, delete_api_key, get_masked_api_key - PR cjpais#455: Text replacements with regex and magic commands - Support literal and regex pattern matching - Magic commands: [date], [time], [datetime] - Import/export functionality for replacement rules - PR cjpais#851: Per-entry post-process button for history - Retroactively post-process transcription history entries - Store post-processed text and prompt used - PR cjpais#618: Wake-word detection for Active Listening trigger - Transcript-based wake phrase detection - Configurable trigger actions, cooldown, and threshold
1 parent 758936c commit ae37788

File tree

17 files changed

+1184
-10
lines changed

17 files changed

+1184
-10
lines changed

src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ specta-typescript = "0.0.9"
7979
tauri-specta = { version = "=2.0.0-rc.21", features = ["derive", "typescript"] }
8080
tauri-plugin-dialog = "~2.4"
8181
symphonia = { version = "0.5", features = ["mp3", "aac", "flac", "vorbis", "isomp4"] }
82+
keyring = "3"
8283

8384
[target.'cfg(unix)'.dependencies]
8485
signal-hook = "0.3"

src-tauri/src/actions.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ fn build_system_prompt(prompt_template: &str) -> String {
5656
prompt_template.replace("${output}", "").trim().to_string()
5757
}
5858

59-
async fn post_process_transcription(settings: &AppSettings, transcription: &str) -> Option<String> {
59+
/// Process transcription text through the configured LLM provider.
60+
/// Returns the processed text or None if post-processing is not available/fails.
61+
pub async fn post_process_transcription(settings: &AppSettings, transcription: &str) -> Option<String> {
6062
let provider = match settings.active_post_process_provider().cloned() {
6163
Some(provider) => provider,
6264
None => {

src-tauri/src/audio_toolkit/audio/recorder.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,34 @@ impl AudioRecorder {
6565
let host = crate::audio_toolkit::get_cpal_host();
6666
let device = match device {
6767
Some(dev) => dev,
68-
None => host
69-
.default_input_device()
70-
.ok_or_else(|| Error::new(std::io::ErrorKind::NotFound, "No input device found"))?,
68+
None => host.default_input_device().ok_or_else(|| {
69+
Error::new(
70+
std::io::ErrorKind::NotFound,
71+
"No microphone found. Please connect a microphone and try again.",
72+
)
73+
})?,
7174
};
7275

76+
// Validate device configuration BEFORE spawning worker thread
77+
// This prevents silent failures and provides better error messages
78+
let config = AudioRecorder::get_preferred_config(&device).map_err(|e| {
79+
Error::new(
80+
std::io::ErrorKind::InvalidInput,
81+
format!(
82+
"Microphone configuration error: {}. Try selecting a different microphone.",
83+
e
84+
),
85+
)
86+
})?;
87+
7388
let thread_device = device.clone();
89+
let thread_config = config.clone();
7490
let vad = self.vad.clone();
7591
// Move the optional level callback into the worker thread
7692
let level_cb = self.level_cb.clone();
7793

7894
let worker = std::thread::spawn(move || {
79-
let config = AudioRecorder::get_preferred_config(&thread_device)
80-
.expect("failed to fetch preferred config");
95+
let config = thread_config;
8196

8297
let sample_rate = config.sample_rate().0;
8398
let channels = config.channels() as usize;

src-tauri/src/commands/history.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
use crate::actions::post_process_transcription;
12
use crate::managers::history::{HistoryEntry, HistoryManager};
3+
use crate::settings::get_settings;
24
use std::sync::Arc;
35
use tauri::{AppHandle, State};
46

@@ -99,3 +101,67 @@ pub async fn update_recording_retention_period(
99101

100102
Ok(())
101103
}
104+
105+
/// Post-process a history entry using the current LLM settings.
106+
/// Returns the processed text if successful.
107+
#[tauri::command]
108+
#[specta::specta]
109+
pub async fn post_process_history_entry(
110+
app: AppHandle,
111+
history_manager: State<'_, Arc<HistoryManager>>,
112+
id: i64,
113+
) -> Result<String, String> {
114+
// Get the entry
115+
let entry = history_manager
116+
.get_entry_by_id(id)
117+
.await
118+
.map_err(|e| e.to_string())?
119+
.ok_or_else(|| format!("History entry with id {} not found", id))?;
120+
121+
// Get current settings for post-processing config
122+
let settings = get_settings(&app);
123+
124+
// Use the original transcription text for post-processing
125+
let text_to_process = &entry.transcription_text;
126+
127+
// Post-process using the configured LLM
128+
let processed_text = post_process_transcription(&settings, text_to_process)
129+
.await
130+
.ok_or_else(|| {
131+
"Post-processing failed. Please check your LLM provider settings.".to_string()
132+
})?;
133+
134+
// Get the prompt that was used
135+
let post_process_prompt = settings
136+
.post_process_selected_prompt_id
137+
.as_ref()
138+
.and_then(|prompt_id| {
139+
settings
140+
.post_process_prompts
141+
.iter()
142+
.find(|p| &p.id == prompt_id)
143+
.map(|p| p.prompt.clone())
144+
});
145+
146+
// Update the entry with the processed text
147+
history_manager
148+
.update_post_processed_text(id, processed_text.clone(), post_process_prompt)
149+
.await
150+
.map_err(|e| e.to_string())?;
151+
152+
Ok(processed_text)
153+
}
154+
155+
/// Get a specific history entry by ID.
156+
#[tauri::command]
157+
#[specta::specta]
158+
pub async fn get_history_entry_by_id(
159+
_app: AppHandle,
160+
history_manager: State<'_, Arc<HistoryManager>>,
161+
id: i64,
162+
) -> Result<Option<HistoryEntry>, String> {
163+
history_manager
164+
.get_entry_by_id(id)
165+
.await
166+
.map_err(|e| e.to_string())
167+
}

src-tauri/src/commands/keyring.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//! Tauri commands for secure API key management via OS keychain.
2+
3+
use crate::keyring;
4+
5+
/// Store an API key in the system keyring.
6+
#[tauri::command]
7+
#[specta::specta]
8+
pub fn set_api_key(provider_id: String, api_key: String) -> Result<(), String> {
9+
keyring::set_api_key(&provider_id, &api_key)
10+
}
11+
12+
/// Get an API key from the system keyring.
13+
/// Returns None if the key doesn't exist.
14+
#[tauri::command]
15+
#[specta::specta]
16+
pub fn get_api_key(provider_id: String) -> Result<Option<String>, String> {
17+
keyring::get_api_key(&provider_id)
18+
}
19+
20+
/// Delete an API key from the system keyring.
21+
#[tauri::command]
22+
#[specta::specta]
23+
pub fn delete_api_key(provider_id: String) -> Result<(), String> {
24+
keyring::delete_api_key(&provider_id)
25+
}
26+
27+
/// Check if an API key exists in the system keyring.
28+
#[tauri::command]
29+
#[specta::specta]
30+
pub fn has_api_key(provider_id: String) -> bool {
31+
keyring::has_api_key(&provider_id)
32+
}
33+
34+
/// Get a masked version of the API key for display (e.g., "sk-••••••••xxxx").
35+
/// Returns None if no key exists.
36+
#[tauri::command]
37+
#[specta::specta]
38+
pub fn get_masked_api_key(provider_id: String) -> Result<Option<String>, String> {
39+
match keyring::get_api_key(&provider_id)? {
40+
Some(key) => {
41+
if key.len() <= 8 {
42+
Ok(Some("••••••••".to_string()))
43+
} else {
44+
let last_four = &key[key.len() - 4..];
45+
Ok(Some(format!("••••••••{}", last_four)))
46+
}
47+
}
48+
None => Ok(None),
49+
}
50+
}

src-tauri/src/commands/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ pub mod ask_ai;
33
pub mod audio;
44
pub mod batch_processing;
55
pub mod history;
6+
pub mod keyring;
67
pub mod models;
78
pub mod rag;
9+
pub mod replacements;
810
pub mod suggestions;
911
pub mod tasks;
1012
pub mod transcription;
1113
pub mod sound_detection;
1214
pub mod vocabulary;
15+
pub mod wake_word;
1316

1417
use crate::settings::{get_settings, write_settings, AppSettings, LogLevel};
1518
use crate::utils::cancel_current_operation;

0 commit comments

Comments
 (0)