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
9 changes: 7 additions & 2 deletions FUTURE_ROADMAPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
> numbers / missing files. Verify against the real repo with absolute
> `C:\dev\Briefli\…` paths or terminal `Select-String`.

Last updated: 2026-07-17
Last updated: 2026-07-18

---

Expand All @@ -31,12 +31,17 @@ Checked directly against `C:\dev\Briefli` on 2026-07-17.

---

## 2. Verified performance findings
## 2. Verified performance and recording reliability findings

| Severity | Finding | Location | Notes |
|----------|---------|----------|-------|
| High | Transcript search is a full table scan | `src-tauri/src/database/repositories/transcript.rs` → `search_transcripts` uses `LOWER(t.transcript) LIKE '%q%'`, no index, `fetch_all` | Fix with SQLite **FTS5** — also unlocks the cross-meeting-memory moat. |
| High | Audio buffer cloning in hot path | `src-tauri/src/audio/vad.rs` (`samples.to_vec()` + `drain().collect()` per chunk); `src-tauri/src/audio/incremental_saver.rs` (clones all buffered audio per checkpoint) | Allocation churn during long recordings. Prefer slices / reused buffers. |
| P0 — fixed 2026-07-18 | Failed audio finalization was reported as success | `audio/recording_manager.rs`, `recording_commands.rs`, `incremental_saver.rs`; `hooks/useRecordingStop.ts` | Save/FFmpeg/disk failures now propagate as typed recoverable errors; checkpoints survive until the full save succeeds; FFmpeg is cancellable and publishes atomically; transcript persistence continues with an error toast and durable audio-recovery record. |
| High — fixed 2026-07-18 | Transcription timeout detached the worker | `audio/recording_commands.rs` shutdown wait | Timed-out transcription worker is now `abort()`-ed and joined before model unload / finalization, so it can no longer run detached and race the save. |
| High — fixed 2026-07-18 | Frontend could skip SQLite save after transcription timeout | `hooks/useRecordingStop.ts` save gate | Save now proceeds whenever transcripts exist (`transcriptionComplete \|\| hasTranscripts`); a distinct warning toast flags possibly-incomplete transcription instead of silently dropping the meeting. |
| High — fixed 2026-07-18 | Stop could drop tail audio (fixed sleep) | `audio/recording_saver.rs` accumulation worker | Worker is now a stored `JoinHandle`; stop drains by awaiting worker after the pipeline closes the channel (30s cap), replacing the `is_saving` flag + 200ms sleep. Transcript-only recordings also finalize `metadata.json` status. |
| Medium — fixed 2026-07-18 | FFmpeg concat paths not escaped | `audio/incremental_saver.rs` | `ffmpeg_concat_escape` escapes single quotes at both concat call sites, so meeting names with apostrophes no longer fail finalize/recovery (unit-tested). |
| Low (corrected) | `console.log` on every render | `components/TranscriptView.tsx` ~L111 | **Dead code** — `TranscriptView` is never rendered (no `<TranscriptView` JSX; live UI uses `VirtualizedTranscriptView`). Trivial cleanup only. |
| Low (corrected) | Summary status polling `setInterval` 5s | `components/Sidebar/SidebarProvider.tsx` | Only runs *during summary generation*, clears on completion. Minor. |

Expand Down
90 changes: 90 additions & 0 deletions frontend/src-tauri/src/audio/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::{
path::PathBuf,
process::{Command, Stdio},
};
use tokio::io::AsyncWriteExt;
use tracing::{debug, error};

pub struct AudioInput {
Expand Down Expand Up @@ -103,3 +104,92 @@ pub fn encode_single_audio(

Ok(())
}

pub async fn encode_single_audio_async(
data: Vec<f32>,
sample_rate: u32,
channels: u16,
output_path: PathBuf,
) -> anyhow::Result<()> {
if data.is_empty() {
return Err(anyhow::anyhow!("No audio data provided for encoding"));
}

let ffmpeg_path = find_ffmpeg_path().ok_or_else(|| {
anyhow::anyhow!("FFmpeg not found. Please install FFmpeg to save recordings.")
})?;
let output_name = output_path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| anyhow::anyhow!("Invalid checkpoint output path"))?;
let temp_output_path = output_path.with_file_name(format!(".{}.finalizing", output_name));
let _ = std::fs::remove_file(&temp_output_path);

let mut command = tokio::process::Command::new(ffmpeg_path);
command
.arg("-f")
.arg("f32le")
.arg("-ar")
.arg(sample_rate.to_string())
.arg("-ac")
.arg(channels.to_string())
.arg("-i")
.arg("pipe:0")
.arg("-c:a")
.arg("aac")
.arg("-b:a")
.arg("192k")
.arg("-profile:a")
.arg("aac_low")
.arg("-movflags")
.arg("+faststart")
.arg("-f")
.arg("mp4")
.arg(&temp_output_path)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.kill_on_drop(true);

#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
command.as_std_mut().creation_flags(CREATE_NO_WINDOW);
}

let mut ffmpeg = command.spawn()?;
let mut stdin = ffmpeg
.stdin
.take()
.ok_or_else(|| anyhow::anyhow!("Failed to open FFmpeg stdin"))?;

if let Err(write_error) = stdin.write_all(bytemuck::cast_slice(&data)).await {
let _ = ffmpeg.kill().await;
let _ = std::fs::remove_file(&temp_output_path);
return Err(write_error.into());
}
drop(stdin);

let output = ffmpeg.wait_with_output().await?;
if !output.status.success() {
let _ = std::fs::remove_file(&temp_output_path);
return Err(anyhow::anyhow!(
"FFmpeg process failed: {}",
String::from_utf8_lossy(&output.stderr)
));
}

let output_metadata = std::fs::metadata(&temp_output_path)?;
if output_metadata.len() == 0 {
let _ = std::fs::remove_file(&temp_output_path);
return Err(anyhow::anyhow!("FFmpeg produced an empty checkpoint"));
}

if output_path.exists() {
std::fs::remove_file(&output_path)?;
}
std::fs::rename(&temp_output_path, &output_path)?;

Ok(())
}
Loading
Loading