feat(A.2 #19): progressive WAV write during live capture
The Vec<f32> in-memory accumulator on run_live_session had three failure modes: (a) a crash during transcription took the recording with it, (b) RAM grew linearly with session length, (c) OOM killed the capture thread silently. New kon_audio::WavWriter wraps hound::WavWriter<BufWriter<File>> with an append-friendly API and a 500 ms-granularity header flush. On any abort after a flush the on-disk file is a valid, playable WAV. Unit test (brief item #19 acceptance) simulates the abort with std::mem::forget and asserts the pre-flush samples are recoverable. Live capture now: - resolves the destination path at start_live_transcription_session time via a new resolve_recording_path helper extracted from persist_audio_samples, - opens a WavWriter before any samples arrive, sample rate taken from LocalEngine::capabilities() (#13 wiring) with 16 kHz fallback, - feeds the resampler output through WavWriter::append inside append_resampled_audio — drops the writer with a user-visible warning if a write fails mid-session, - calls flush() at stop (after resampler tail), finalise() on clean exit, and drops-to-last-flushed state on abort. LiveSessionSummary.audio_samples → audio_path: the path is already written by the time stop_live_transcription_session runs; no post-session write step remains for live capture. persist_audio_samples is kept for the offline save_audio command.
This commit is contained in:
@@ -228,25 +228,22 @@ pub async fn stop_native_capture(
|
||||
Ok(samples)
|
||||
}
|
||||
|
||||
pub async fn persist_audio_samples(
|
||||
/// Resolve the destination path for a new live-capture recording,
|
||||
/// ensuring the parent directory exists. Extracted from
|
||||
/// `persist_audio_samples` so `start_live_transcription_session` can
|
||||
/// hand the path to the progressive WAV writer before any samples
|
||||
/// arrive (brief item #19).
|
||||
pub fn resolve_recording_path(
|
||||
app: &tauri::AppHandle,
|
||||
samples: Vec<f32>,
|
||||
output_folder: Option<String>,
|
||||
) -> Result<String, String> {
|
||||
let recordings_dir = if let Some(ref folder) = output_folder {
|
||||
if !folder.is_empty() {
|
||||
PathBuf::from(folder)
|
||||
} else {
|
||||
app.path()
|
||||
.app_local_data_dir()
|
||||
.map_err(|e: tauri::Error| e.to_string())?
|
||||
.join("recordings")
|
||||
}
|
||||
} else {
|
||||
app.path()
|
||||
output_folder: Option<&str>,
|
||||
) -> Result<PathBuf, String> {
|
||||
let recordings_dir = match output_folder.map(str::trim).filter(|s| !s.is_empty()) {
|
||||
Some(folder) => PathBuf::from(folder),
|
||||
None => app
|
||||
.path()
|
||||
.app_local_data_dir()
|
||||
.map_err(|e: tauri::Error| e.to_string())?
|
||||
.join("recordings")
|
||||
.join("recordings"),
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(&recordings_dir)
|
||||
@@ -256,8 +253,15 @@ pub async fn persist_audio_samples(
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
let filename = format!("kon-{timestamp}.wav");
|
||||
let path = recordings_dir.join(&filename);
|
||||
Ok(recordings_dir.join(format!("kon-{timestamp}.wav")))
|
||||
}
|
||||
|
||||
pub async fn persist_audio_samples(
|
||||
app: &tauri::AppHandle,
|
||||
samples: Vec<f32>,
|
||||
output_folder: Option<String>,
|
||||
) -> Result<String, String> {
|
||||
let path = resolve_recording_path(app, output_folder.as_deref())?;
|
||||
let path_clone = path.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
|
||||
Reference in New Issue
Block a user