Skip to content

Save recordings before transcription#1024

Merged
cjpais merged 19 commits intocjpais:mainfrom
ferologics:main
Mar 22, 2026
Merged

Save recordings before transcription#1024
cjpais merged 19 commits intocjpais:mainfrom
ferologics:main

Conversation

@ferologics
Copy link
Copy Markdown
Contributor

@ferologics ferologics commented Mar 12, 2026

Summary

This PR fixes the data-loss path where a recording could disappear if transcription failed.

Save recordings before transcription

  • persist the WAV immediately after recording stops
  • create/update history entries for pending, completed, and failed states
  • preserve failed/pending entries in History instead of dropping them on cleanup
  • store transcription_error
  • allow retrying transcription from History using the saved WAV
  • keep tray copy behavior limited to completed entries
  • simplify History status copy so the badge/player/actions carry most of the state

Testing

  • bun run build
  • cargo check -j 1
  • manual verification of failed/pending History UI and retry flow using saved WAVs

@ferologics ferologics force-pushed the main branch 2 times, most recently from 97eaaac to 47f44a8 Compare March 12, 2026 22:53
@ferologics ferologics marked this pull request as draft March 12, 2026 23:01
@cjpais
Copy link
Copy Markdown
Owner

cjpais commented Mar 12, 2026

Let's not do the ORT error fix here.

We should be adding an option to transcribe-rs to do chunking. This is code which should be broadly applicable not just for Handy

It will also keep this PR more focused

@ferologics ferologics changed the title Save recordings before transcription and retry Parakeet ORT failures Save recordings before transcription Mar 12, 2026
@ferologics
Copy link
Copy Markdown
Contributor Author

Dropped the ORT split-retry commit from this PR to keep it focused on the durability/history fix. I kept local notes and a preserved branch for a follow-up chunking/chunked-transcription pass in transcribe-rs.

@ferologics ferologics marked this pull request as ready for review March 12, 2026 23:50
Copy link
Copy Markdown
Contributor

@VirenMohindra VirenMohindra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor nits, i think we can reduce the blast radius if we add back the comments + would be easier to review

},
async retryHistoryEntryTranscription(id: number) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("retry_history_entry_transcription", { id }) };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs a rerun of codegen + bun run format to fix

error!("Failed to run paste on main thread: {:?}", e);
}

if processed.final_text.is_empty() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when processed.final_text.is_empty(), the code silently hides the overlay and does nothing else but complete_transcription() was already called with an empty string, leaving the entry as status=Completed, transcription_text="". In the UI this shows as a successful entry with no text and users can't distinguish "completed but empty" from "failed." we should either call fail_transcription() with a descriptive error or skip complete_transcription() entirely for empty results

Comment on lines -323 to -325
crate::settings::RecordingRetentionPeriod::Days3 => now - (3 * 24 * 60 * 60), // 3 days in seconds
crate::settings::RecordingRetentionPeriod::Weeks2 => now - (2 * 7 * 24 * 60 * 60), // 2 weeks in seconds
crate::settings::RecordingRetentionPeriod::Months3 => now - (3 * 30 * 24 * 60 * 60), // 3 months in seconds (approximate)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: would it be possible to avoid blowing this diff up and avoid removing comments? would be easier to review

@cjpais
Copy link
Copy Markdown
Owner

cjpais commented Mar 19, 2026

I think I have some inflight changes for this PR

Copy link
Copy Markdown
Contributor

@VirenMohindra VirenMohindra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some misc UI fixes ordered in terms of prio~

  1. the retry button shows on completed entries and should only appear when !hasTranscription
  2. content area shows "Transcription failed" during retry and should instead swap to a loading state
  3. delete btn stays enabled during retry, and will be a possible race condition if you delete while retranscribing

i think we should do a proper UI pass before this lands

} else {
// Save WAV concurrently with transcription
let sample_count = samples.len();
let file_name = format!("handy-{}.wav", chrono::Utc::now().timestamp());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets use a uuid here instead. timestamp is a second class citizen and two recordings within the same second will overwrite each other

or even chrono::Utc::now().timestamp_nanos_opt().unwrap_or_else(|| chrono::Utc::now().timestamp_micros()) but il lean towards uuids

.await;

// Save to history if WAV was saved and transcription is non-empty
if wav_saved && !transcription.is_empty() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

orphaned wav files when there is silent audio and no transcription and then cleanup_old_entries won't touch it

the error path below this correctly writes an empty DB row for retry and the empty-transcription path should do the same

Comment on lines +519 to +520
// Verify: 44-byte header + num_samples * 2 bytes (16-bit)
let expected_size = 44 + (sample_count * 2) as u64;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems a bit fragile, rereading the header with WavReader::open and comparing sample counts is prob more robust


transcription_manager.initiate_model_load();

match transcription_manager.transcribe(samples) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets wrap this with tauri::async_runtime::spawn_blocking if possible

this can run for 5 to 30s depending on model and hardware. calling it directly inside an async fn would block the thread for the entire time

Comment on lines +89 to +95
history_manager
.update_transcription(
id,
transcription,
processed.post_processed_text,
processed.post_process_prompt,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UI has no way to distinguish "retry succeeded with silence" from the original failed state and the user will retry again with the same result indefinitely

we should prob return an error when transcription.is_empty() so the fe can show a meaningful state

@cjpais
Copy link
Copy Markdown
Owner

cjpais commented Mar 19, 2026

I think retry should be for everything, mainly if you want to re-transcribe with another model or you picked the wrong language (happens to me all the time)

Otherwise other 2 I agree

@VirenMohindra
Copy link
Copy Markdown
Contributor

also please add screenshots to the PR

DylanBricar added a commit to DylanBricar/Phonara that referenced this pull request Mar 20, 2026
- Save recordings before transcription to prevent data loss on crash (cjpais#1024)
- Linux Wayland titlebar: disable decorations for custom titlebar (cjpais#1042)
- Add change_secondary_selected_language_setting command (cjpais#1106 gap)
- Fix CLI --auto-submit key type being ignored (cjpais#1099 bug)
@cjpais
Copy link
Copy Markdown
Owner

cjpais commented Mar 21, 2026

@VirenMohindra let me know what you think of the current set of changes. Not sure if the original author will jump in, but I think things are doing okay right now. Would love a second opinion

@cjpais
Copy link
Copy Markdown
Owner

cjpais commented Mar 21, 2026

Screenshot 2026-03-21 at 1 18 12 PM

@github-actions
Copy link
Copy Markdown

🧪 Test Build Ready

Build artifacts for PR #1024 are available for testing.

Download artifacts from workflow run

Artifacts expire after 30 days.

Copy link
Copy Markdown
Contributor

@VirenMohindra VirenMohindra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, the ui feedback is addressed, thanks. the spawn_blocking and filename collision are worth fixing before this lands because both are easy changes and the former can stall the runtime for 30s+

the header sample count check and empty transcription handling can be follow-ups if needed

@cjpais cjpais merged commit 17277cf into cjpais:main Mar 22, 2026
4 checks passed
@ferologics
Copy link
Copy Markdown
Contributor Author

ferologics commented Mar 27, 2026

thx for getting this across the finish line @cjpais & @VirenMohindra !!
now we just need to recover from failures better :P

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants