mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
Let a recording be held while something else happens
A phone call in the middle of a dictation left two choices: send what was said so far off to be transcribed, or throw it away. Both end the sentence you were in the middle of. Now the recording can be held: the microphone stays ours, what was said before the pause stays in the buffer, and what is said during it is dropped. The capture program keeps running and keeps handing blocks over, which are read and thrown away rather than left in the pipe. Stopping it instead would mean asking the sound server for the device again on the way back, and that is the one moment another application can take it: a recording would be lost to the phone call it was paused for. The clock stops with it. Paused time is time the recording does not have, so the indicator and the length limit both go by what was actually captured, and a five minute limit is not spent waiting. The indicator says so as well, since a pulsing dot, moving bars and a counting clock otherwise all say the words are still going in: the ribbon freezes where the pause found it and turns amber behind two bars. The tray menu holds and resumes it, `dikte pause` does, and so does a global shortcut, which starts empty because holding a recording is not something a keyboard has a habit for. Dictation and a command to the agent both, whichever is recording. A meeting is left out: it writes to a file as it goes and keeps two streams aligned itself, and neither of those wants a hole in it.
This commit is contained in:
@@ -83,6 +83,14 @@ class Dikte:
|
||||
self.ask_state = IDLE
|
||||
# Which of the two the microphone is currently serving, or None.
|
||||
self.recorder_owner = None
|
||||
# A recording that is running but taking nothing in. Not a state of its
|
||||
# own: everything that can be done to a recording can be done to a
|
||||
# paused one, and a fourth state would have to say so four times over.
|
||||
self.paused = False
|
||||
# Paused time, which is time the recording does not have: the clock on
|
||||
# screen and the limit both go by what was actually captured.
|
||||
self._paused_ms = 0
|
||||
self._paused_at = 0
|
||||
self.meeting_state = M_IDLE
|
||||
self.meeting_base = ""
|
||||
self.meeting_message = ""
|
||||
@@ -159,6 +167,12 @@ class Dikte:
|
||||
self.toggle_action.triggered.connect(self._toggle)
|
||||
self.menu.addAction(self.toggle_action)
|
||||
|
||||
# Named in _refresh_tray as well, since it says one of two things.
|
||||
self.pause_action = QAction(t("Pause the recording"), self.menu)
|
||||
self.pause_action.triggered.connect(self._toggle_pause)
|
||||
self.pause_action.setEnabled(False)
|
||||
self.menu.addAction(self.pause_action)
|
||||
|
||||
# Named in _refresh_tray, which is where the chosen provider is known.
|
||||
self.ask_action = QAction("", self.menu)
|
||||
self.ask_action.triggered.connect(self._toggle_ask)
|
||||
@@ -283,6 +297,9 @@ class Dikte:
|
||||
or (self.ask_state == IDLE and not self.recording)
|
||||
)
|
||||
self.reset_action.setEnabled(self.ask_state != BUSY)
|
||||
self.pause_action.setText(t("Resume the recording") if self.paused
|
||||
else t("Pause the recording"))
|
||||
self.pause_action.setEnabled(self.recording)
|
||||
self.cancel_action.setEnabled(self.recording)
|
||||
# A command to the agent is the one job long enough to be worth calling
|
||||
# off once it is already running.
|
||||
@@ -299,6 +316,12 @@ class Dikte:
|
||||
else:
|
||||
icon, tip = "view-refresh", "Dikte: talking to Claude"
|
||||
|
||||
# Whichever of the two is holding the microphone, a recording dot that
|
||||
# keeps burning while nothing goes in is the icon telling the opposite
|
||||
# of what is happening.
|
||||
if self.paused and self.recording:
|
||||
icon, tip = "media-playback-pause", "Dikte: paused"
|
||||
|
||||
meeting_labels = {
|
||||
M_IDLE: "Record a meeting",
|
||||
M_RECORDING: "End the meeting and write it up",
|
||||
@@ -334,6 +357,9 @@ class Dikte:
|
||||
def toggle_meeting(self):
|
||||
self._external("meeting", self._toggle_meeting)
|
||||
|
||||
def toggle_pause(self):
|
||||
self._external("pause", self._toggle_pause)
|
||||
|
||||
def cancel(self):
|
||||
self._external("cancel", self._cancel)
|
||||
|
||||
@@ -358,7 +384,7 @@ class Dikte:
|
||||
timer = self.last_evdev[name] = QElapsedTimer()
|
||||
timer.restart()
|
||||
handlers = {"meeting": self._toggle_meeting, "ask": self._toggle_ask,
|
||||
"cancel": self._cancel}
|
||||
"cancel": self._cancel, "pause": self._toggle_pause}
|
||||
handlers.get(name, self._toggle)()
|
||||
|
||||
def _retire_listener(self):
|
||||
@@ -393,6 +419,7 @@ class Dikte:
|
||||
else:
|
||||
handler = {
|
||||
"cancel": self.cancel,
|
||||
"pause": self.toggle_pause,
|
||||
"ask-cancel": self.cancel_ask,
|
||||
"ask-reset": self.reset_conversation,
|
||||
"meeting-cancel": self.cancel_meeting,
|
||||
@@ -475,6 +502,7 @@ class Dikte:
|
||||
"ok": True,
|
||||
"running": True,
|
||||
"dictation": self.state,
|
||||
"paused": self.paused,
|
||||
"ask": self.ask_state,
|
||||
"meeting": self.meeting_state,
|
||||
"meeting_base": self.meetings.running_base,
|
||||
@@ -538,6 +566,7 @@ class Dikte:
|
||||
"""One microphone, so one of the two holds it at a time."""
|
||||
self.recorder_owner = owner
|
||||
self._run_id += 1
|
||||
self._clear_pause()
|
||||
self.elapsed.restart()
|
||||
self.ticker.start()
|
||||
self.recorder.start(self.conf["mic_target"], self.conf["max_seconds"])
|
||||
@@ -546,6 +575,7 @@ class Dikte:
|
||||
if self.state != RECORDING:
|
||||
return
|
||||
self.ticker.stop()
|
||||
self._clear_pause()
|
||||
self._set_state(BUSY)
|
||||
self.overlay.show_busy(t("Transcribing…"))
|
||||
self.recorder.stop()
|
||||
@@ -554,16 +584,50 @@ class Dikte:
|
||||
if self.ask_state != RECORDING:
|
||||
return
|
||||
self.ticker.stop()
|
||||
self._clear_pause()
|
||||
self._set_ask_state(BUSY)
|
||||
self.ask_overlay.show_busy(t("Transcribing…"))
|
||||
self.recorder.stop()
|
||||
|
||||
def _toggle_pause(self):
|
||||
"""Hold the recording where it is, or take it up again.
|
||||
|
||||
A pause is not a stop: the microphone stays ours and what has been said
|
||||
so far stays in the buffer. What is said while it is held is dropped, so
|
||||
the phone call in the middle of a dictation never reaches the model and
|
||||
the sentence around it is still one sentence.
|
||||
"""
|
||||
if not self.recording or self._repeated():
|
||||
return
|
||||
self.paused = not self.paused
|
||||
if self.paused:
|
||||
self._paused_at = self.elapsed.elapsed()
|
||||
else:
|
||||
self._paused_ms += self.elapsed.elapsed() - self._paused_at
|
||||
self.recorder.pause(self.paused)
|
||||
self._recording_overlay().set_paused(self.paused)
|
||||
self._refresh_tray()
|
||||
|
||||
def _clear_pause(self):
|
||||
"""Every recording starts and ends taking sound in."""
|
||||
self.paused = False
|
||||
self._paused_ms = 0
|
||||
self._paused_at = 0
|
||||
|
||||
def _recorded_seconds(self):
|
||||
"""Wall clock less whatever was held: the length of what will be
|
||||
transcribed, which is what the limit has to be measured against too."""
|
||||
# A held recording is as long now as it was when it was held.
|
||||
now = self._paused_at if self.paused else self.elapsed.elapsed()
|
||||
return max(0.0, (now - self._paused_ms) / 1000.0)
|
||||
|
||||
def _cancel(self):
|
||||
"""Throw away whichever recording is running."""
|
||||
if not self.recording:
|
||||
return
|
||||
asking = self.ask_state == RECORDING
|
||||
self.ticker.stop()
|
||||
self._clear_pause()
|
||||
self.recorder.cancel()
|
||||
self.recorder_owner = None
|
||||
# What goes over the socket is read by a program as often as by a
|
||||
@@ -603,7 +667,7 @@ class Dikte:
|
||||
self._recording_overlay().push_level(level)
|
||||
|
||||
def _tick(self):
|
||||
seconds = self.elapsed.elapsed() / 1000.0
|
||||
seconds = self._recorded_seconds()
|
||||
self._recording_overlay().set_seconds(seconds)
|
||||
if seconds >= self.conf["max_seconds"]:
|
||||
(self.stop_ask if self.recorder_owner == ASK else self.stop)()
|
||||
|
||||
Reference in New Issue
Block a user