feat: refine meeting auto-start, silence timeout (25s) and improve transcription logging
This commit is contained in:
@@ -37,7 +37,7 @@ struct LogEvent {
|
||||
timestamp: String,
|
||||
}
|
||||
|
||||
fn emit_log(app: &AppHandle, level: &str, message: &str) {
|
||||
pub(crate) fn emit_log(app: &AppHandle, level: &str, message: &str) {
|
||||
let log = LogEvent {
|
||||
level: level.to_string(),
|
||||
message: message.to_string(),
|
||||
@@ -73,8 +73,8 @@ fn get_input_devices() -> Result<Vec<AudioDevice>, String> {
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
async fn start_recording(app: AppHandle, state: State<'_, AppState>, device_id: String, save_path: Option<String>, custom_filename: Option<String>, wait_for_speech: Option<bool>) -> Result<(), String> {
|
||||
emit_log(&app, "INFO", &format!("Starting recording on device: {}", device_id));
|
||||
async fn start_recording(app: AppHandle, state: State<'_, AppState>, device_id: String, save_path: Option<String>, custom_filename: Option<String>, wait_for_speech: Option<bool>, mode: String) -> Result<(), String> {
|
||||
emit_log(&app, "INFO", &format!("Starting recording [Mode: {}] on device: {}", mode, device_id));
|
||||
let host = cpal::default_host();
|
||||
|
||||
// Find device by name (using name as ID)
|
||||
@@ -143,10 +143,10 @@ async fn start_recording(app: AppHandle, state: State<'_, AppState>, device_id:
|
||||
// We pass the writer to it.
|
||||
let should_wait = wait_for_speech.unwrap_or(false);
|
||||
if should_wait {
|
||||
emit_log(&app, "INFO", "Recording started in WAITING mode (buffer-only until speech).");
|
||||
emit_log(&app, "INFO", &format!("Recording started in WAITING mode (Trigger: {}).", if mode == "voice" { "Speech" } else { "System Audio" }));
|
||||
}
|
||||
|
||||
let processor = AudioProcessor::new(config.sample_rate(), config.channels(), writer.clone(), app.clone(), should_wait)
|
||||
let processor = AudioProcessor::new(config.sample_rate(), config.channels(), writer.clone(), app.clone(), should_wait, mode)
|
||||
.map_err(|e| format!("Failed to create AudioProcessor: {}", e))?;
|
||||
|
||||
// Wrap processor in Arc<Mutex> so we can share/move it into callback
|
||||
@@ -158,61 +158,40 @@ async fn start_recording(app: AppHandle, state: State<'_, AppState>, device_id:
|
||||
let processor_clone = processor.clone();
|
||||
|
||||
// --- SYSTEM AUDIO CAPTURE START ---
|
||||
let mut sys_capture = sc_audio::SystemAudioCapture::new(config.sample_rate());
|
||||
// Prevent Doubling: If user selected an aggregate device (Hearbit Audio/BlackHole),
|
||||
// it ALREADY contains system audio. In that case, we don't need internal SCK capture.
|
||||
let is_aggregate = device_id.contains("Hearbit") || device_id.contains("BlackHole");
|
||||
|
||||
// Get the queue to share with the capture callback
|
||||
let queue_clone = {
|
||||
let p = processor.lock().unwrap();
|
||||
p.system_queue.clone() // Access the pub field we added
|
||||
};
|
||||
if is_aggregate {
|
||||
emit_log(&app, "INFO", "Aggregate device detected. Disabling internal System Audio Capture to prevent doubling.");
|
||||
} else {
|
||||
let mut sys_capture = sc_audio::SystemAudioCapture::new(config.sample_rate());
|
||||
|
||||
// Get the queue to share with the capture callback
|
||||
let queue_clone = {
|
||||
let p = processor.lock().unwrap();
|
||||
p.system_queue.clone() // Access the pub field we added
|
||||
};
|
||||
|
||||
let sys_handle = app.clone();
|
||||
let sys_callback = move |data: &[f32]| {
|
||||
// Push to queue
|
||||
if let Ok(mut q) = queue_clone.lock() {
|
||||
q.extend(data.iter());
|
||||
|
||||
// Limit queue size to avoid memory leaks if main process loop is slow
|
||||
while q.len() > 48000 * 5 { // 5 seconds buffer
|
||||
q.pop_front();
|
||||
let sys_callback = move |data: &[f32]| {
|
||||
// Push to queue
|
||||
if let Ok(mut q) = queue_clone.lock() {
|
||||
q.extend(data.iter());
|
||||
|
||||
// Limit queue size to avoid memory leaks if main process loop is slow
|
||||
while q.len() > 48000 * 5 { // 5 seconds buffer
|
||||
q.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Need to run async start in sync command?
|
||||
// Tauri commands are async if they return Future, but here we returned Result.
|
||||
// We should probably spawn it.
|
||||
// Actually, SystemAudioCapture::start is async.
|
||||
// We can spawn a tokio task to start it. But we need to keep the object alive.
|
||||
// The start method modifies self.stream.
|
||||
// If we make start synchronous or use block_in_place?
|
||||
// Better: change start_recording to async fn (it is not currently async in signature used by tauri::command macros? No, tauri supports async commands).
|
||||
// Let's check line 76: `fn start_recording`... it is NOT async.
|
||||
// We should make it `async fn start_recording`.
|
||||
|
||||
// However, changing to async might affect how state is passed or other things.
|
||||
// Actually Tauri works fine with async commands.
|
||||
// But then we need to await `sys_capture.start`.
|
||||
|
||||
// Wait, let's look at `SystemAudioCapture::start`. It takes `&mut self`.
|
||||
// We can't easily spawn it away properly if we want to keep `sys_capture` in State.
|
||||
// The `sys_capture` struct holds the `SCStream` which must be kept alive.
|
||||
|
||||
// Let's assume we can make `start_recording` into `async fn`.
|
||||
|
||||
// TEMPORARY: Just putting placeholder for logic flow.
|
||||
// We will need to change the function signature of start_recording to async first in a separate step or assume I can do it here if I replace the whole signature.
|
||||
// The replace_file_content replaces a block.
|
||||
// I will replace line 76 in a separate call to make it async.
|
||||
|
||||
// For this block, I will assume it's async context.
|
||||
|
||||
match sys_capture.start(sys_callback) {
|
||||
Ok(_) => emit_log(&app, "INFO", "System Audio Capture started."),
|
||||
Err(e) => emit_log(&app, "WARN", &format!("System Audio Capture failed (Permissions?): {}", e)),
|
||||
match sys_capture.start(sys_callback).await {
|
||||
Ok(_) => emit_log(&app, "INFO", "System Audio Capture started."),
|
||||
Err(e) => emit_log(&app, "WARN", &format!("System Audio Capture failed (Permissions?): {}", e)),
|
||||
}
|
||||
|
||||
*state.system_capture.lock().unwrap() = Some(sys_capture);
|
||||
}
|
||||
|
||||
*state.system_capture.lock().unwrap() = Some(sys_capture);
|
||||
// --- SYSTEM AUDIO CAPTURE END ---
|
||||
|
||||
let app_handle = app.clone();
|
||||
@@ -585,8 +564,9 @@ async fn poll_transcription(app: &AppHandle, client: &reqwest::Client, api_key:
|
||||
return Err(format!("Download failed: {}", dl_res.status()));
|
||||
}
|
||||
} else if status == "failed" || status == "error" {
|
||||
emit_log(app, "ERROR", &format!("Batch processing failed: {:?}", json));
|
||||
return Err(format!("Batch processing failed: {:?}", json));
|
||||
let err_msg = format!("Batch processing failed [Status: {}]. Full Response: {:?}", status, json);
|
||||
emit_log(app, "ERROR", &err_msg);
|
||||
return Err(err_msg);
|
||||
}
|
||||
// If 'processing' or 'pending', continue loop
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user