Let no failure eat a dictation, and none strand the state machine

The recorded WAV was deleted in a finally that did not care why the run
ended, so a whisper server being down destroyed the only copy of the
user's speech; a failed run now keeps its audio in the recordings
directory and names the path in the error. The history was written only
after the paste, and a failed key press restored the previous clipboard
over the fresh transcript: text not pasted, not on the clipboard, not in
the history, audio gone, all from one refused key. The history now comes
first, a failed press is a warning the row is amended to carry, and the
clipboard keeps the text the user has to paste by hand. Kept recordings
no longer overwrite each other inside one second, the keep_audio move
failing no longer falls through to the delete, and the history row
records whether cleanup actually ran rather than what the dictation gate
implies about an ask.

In the application: a recorder that failed to start emitted its error
synchronously and start() then wrote RECORDING over the handler's IDLE,
one more key press away from a BUSY nothing would ever end; start now
checks the recorder is running, like start_meeting always has. The new
died signal ends the run properly and transcribes what was captured. A
--paste override armed by a request that no-opped stopped haunting some
later unrelated run, and it dies with a cancelled or failed one. The
toggle, ask and pause debounce timers are per action, so a pause right
after a toggle is a pause and not a duplicate. Waiters outstanding at
quit or restart are settled instead of being read back as "the instance
is too old". The listing warm-up and the one-per-press backend lookup
move off the key press.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
huseyin-emre-tigci
2026-08-22 23:17:22 +03:00
co-authored by Claude Fable 5
parent 69194186c8
commit 6f79e93d53
3 changed files with 254 additions and 62 deletions
+54 -4
View File
@@ -30,6 +30,7 @@ class Chain(DikteTest):
def run_chain(self, ask=False, paste_override=None, duration=2.0,
transcript="uh, book it for Thursday",
transcribe_error=None,
cleaned="Book it for Thursday.",
cleanup_error=None, answer=("Booked.", ""), rms=None,
clipboard=b"what was there before", paste_error=None):
@@ -46,7 +47,10 @@ class Chain(DikteTest):
# The chain reports its own failures on stderr, which a test run has no
# use for.
with contextlib.redirect_stderr(io.StringIO()), \
mock.patch.object(api, "transcribe", return_value=transcript) as tr, \
mock.patch.object(
api, "transcribe",
**({"side_effect": transcribe_error} if transcribe_error
else {"return_value": transcript})) as tr, \
mock.patch.object(api, "cleanup", cleanup), \
mock.patch.object(assistant, "ask", return_value=answer) as ask_call, \
mock.patch.object(paste, "copy") as copy, \
@@ -108,11 +112,57 @@ class Chain(DikteTest):
run = self.run_chain()
run["copy_bytes"].assert_not_called()
def test_the_clipboard_is_put_back_when_the_keypress_fails(self):
def test_a_failed_keypress_leaves_the_transcript_on_the_clipboard(self):
"""The press failing is a warning, not a lost dictation: restoring the
old clipboard over the text would leave nothing to paste by hand."""
self.conf["restore_clipboard"] = True
run = self.run_chain(paste_error=paste.PasteError("not trusted"))
self.assertIn("not trusted", run["failures"][0])
run["copy_bytes"].assert_called_once_with(b"what was there before")
self.assertEqual(run["failures"], [])
raw, text, warning = run["done"][0]
self.assertIn("not trusted", warning)
run["copy_bytes"].assert_not_called()
def test_a_failed_keypress_still_reaches_the_history(self):
self.run_chain(paste_error=paste.PasteError("not trusted"))
rows = cfg.read_history()
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0]["text"], "Book it for Thursday.")
# The row goes in before the paste is attempted, so the paste failing
# has to be written back into it: the record tells the whole truth.
self.assertIn("not trusted", rows[0]["cleanup_error"])
def test_a_failed_transcription_keeps_the_audio(self):
"""Speech the user cannot repeat from memory must survive the failure."""
run = self.run_chain(transcribe_error=api.ApiError("server down"))
self.assertIn("server down", run["failures"][0])
self.assertIn("kept", run["failures"][0])
kept = list(cfg.RECORDINGS_DIR.glob("*.wav"))
self.assertEqual(len(kept), 1)
self.assertFalse(os.path.exists(self.wav))
def test_two_failures_in_one_second_keep_both_recordings(self):
self.run_chain(transcribe_error=api.ApiError("down"))
self.wav = make_wav(self.path("clip2.wav"), speech(2.0))
with mock.patch.object(worker.time, "strftime",
return_value="20260820-120000"):
self.run_chain(transcribe_error=api.ApiError("down"))
self.wav = make_wav(self.path("clip3.wav"), speech(2.0))
self.run_chain(transcribe_error=api.ApiError("down"))
self.assertEqual(len(list(cfg.RECORDINGS_DIR.glob("*.wav"))), 3)
def test_the_history_row_says_whether_cleanup_actually_ran(self):
"""The ask path cleans under its own setting; the record follows the
run, not the dictation gate."""
self.conf["cleanup_enabled"] = False
self.conf["assistant_cleanup"] = True
self.run_chain(ask=True)
row = cfg.read_history()[0]
self.assertNotEqual(row["cleanup_model"], "")
cfg.clear_history()
self.conf["cleanup_enabled"] = True
self.conf["assistant_cleanup"] = False
self.run_chain(ask=True)
self.assertEqual(cfg.read_history()[0]["cleanup_model"], "")
def test_the_transcription_is_told_the_language_and_the_glossary(self):
self.conf["language"] = "tr"