feat: add automatic stop after a configurable amount of silence.
This commit is contained in:
@@ -6,6 +6,8 @@ import threading
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
import subprocess
|
||||
|
||||
import rumps
|
||||
@@ -54,6 +56,12 @@ class CalliopeApp(rumps.App):
|
||||
self._transcribe_done = threading.Event()
|
||||
self._transcribe_done.set() # not transcribing initially
|
||||
|
||||
# Silence-based auto-stop
|
||||
self._silence_since: float | None = None
|
||||
self._rec_has_speech: bool = False
|
||||
self._silence_stop_evt: threading.Event = threading.Event()
|
||||
self._silence_stop_evt.set() # not monitoring initially
|
||||
|
||||
self.status_item = rumps.MenuItem("Status: Loading model...")
|
||||
self.status_item.set_callback(None)
|
||||
self.toggle_item = rumps.MenuItem("Start Recording", callback=self._on_toggle_click)
|
||||
@@ -86,6 +94,13 @@ class CalliopeApp(rumps.App):
|
||||
self._pp_menu = rumps.MenuItem("Post-Processing")
|
||||
self._build_pp_menu()
|
||||
|
||||
# Auto-stop on silence toggle
|
||||
auto_stop = cfg.get("auto_stop_silence", True)
|
||||
prefix = "\u2713 " if auto_stop else " "
|
||||
self._auto_stop_item = rumps.MenuItem(
|
||||
f"{prefix}Auto-stop on Silence", callback=self._on_auto_stop_toggle
|
||||
)
|
||||
|
||||
# Typing mode submenu
|
||||
self._typing_menu = rumps.MenuItem("Typing Mode")
|
||||
current_mode = cfg.get("typing_mode", "char")
|
||||
@@ -101,6 +116,7 @@ class CalliopeApp(rumps.App):
|
||||
self.status_item,
|
||||
None,
|
||||
self.toggle_item,
|
||||
self._auto_stop_item,
|
||||
self.context_item,
|
||||
self._lang_menu,
|
||||
self._model_menu,
|
||||
@@ -302,7 +318,10 @@ class CalliopeApp(rumps.App):
|
||||
self.title = "\U0001f534 0:00" # 🔴
|
||||
self.toggle_item.title = "Stop Recording"
|
||||
self.status_item.title = "Status: Recording..."
|
||||
self.recorder.on_audio = self.overlay.push_samples
|
||||
self._silence_since = None
|
||||
self._rec_has_speech = False
|
||||
self._silence_stop_evt = threading.Event()
|
||||
self.recorder.on_audio = self._on_audio_chunk
|
||||
try:
|
||||
self.recorder.start()
|
||||
except Exception:
|
||||
@@ -317,6 +336,8 @@ class CalliopeApp(rumps.App):
|
||||
self.overlay.show()
|
||||
self._rec_timer = rumps.Timer(self._update_rec_duration, 1)
|
||||
self._rec_timer.start()
|
||||
if self.cfg.get("auto_stop_silence", True):
|
||||
threading.Thread(target=self._silence_monitor, daemon=True).start()
|
||||
self._notify("Calliope", "", "Recording started")
|
||||
log.info("Recording started")
|
||||
|
||||
@@ -325,6 +346,7 @@ class CalliopeApp(rumps.App):
|
||||
if not self._recording:
|
||||
return
|
||||
self._recording = False
|
||||
self._silence_stop_evt.set()
|
||||
if self._rec_timer:
|
||||
self._rec_timer.stop()
|
||||
self._rec_timer = None
|
||||
@@ -341,6 +363,37 @@ class CalliopeApp(rumps.App):
|
||||
self._transcribe_done.clear()
|
||||
threading.Thread(target=self._transcribe_and_type, args=(audio,), daemon=True).start()
|
||||
|
||||
def _on_audio_chunk(self, chunk: np.ndarray) -> None:
|
||||
"""Called from the audio thread on every recorder chunk."""
|
||||
self.overlay.push_samples(chunk)
|
||||
rms = float(np.sqrt(np.mean(chunk ** 2)))
|
||||
threshold = self.cfg.get("silence_threshold", 0.005)
|
||||
if rms >= threshold:
|
||||
self._rec_has_speech = True
|
||||
self._silence_since = None
|
||||
elif self._rec_has_speech and self._silence_since is None:
|
||||
self._silence_since = time.monotonic()
|
||||
|
||||
def _silence_monitor(self) -> None:
|
||||
"""Background thread: trigger auto-stop after sustained silence."""
|
||||
timeout = self.cfg.get("silence_timeout_seconds", 1.5)
|
||||
stop_evt = self._silence_stop_evt
|
||||
while not stop_evt.is_set():
|
||||
since = self._silence_since
|
||||
if since is not None and (time.monotonic() - since) >= timeout:
|
||||
log.info("Auto-stop: %.1fs of silence detected", timeout)
|
||||
self._stop_and_transcribe()
|
||||
break
|
||||
stop_evt.wait(0.1)
|
||||
|
||||
def _on_auto_stop_toggle(self, sender) -> None:
|
||||
enabled = not self.cfg.get("auto_stop_silence", True)
|
||||
self.cfg["auto_stop_silence"] = enabled
|
||||
config_mod.save(self.cfg)
|
||||
prefix = "\u2713 " if enabled else " "
|
||||
self._auto_stop_item.title = f"{prefix}Auto-stop on Silence"
|
||||
log.info("Auto-stop on silence %s", "enabled" if enabled else "disabled")
|
||||
|
||||
def _update_rec_duration(self, timer) -> None:
|
||||
if self._rec_start_time is None:
|
||||
return
|
||||
|
||||
@@ -26,6 +26,8 @@ DEFAULTS: dict[str, Any] = {
|
||||
"silence_threshold": 0.005, # RMS energy below which audio is considered silence
|
||||
"notifications": True, # show macOS notifications
|
||||
"typing_delay": 0.005, # seconds between keystrokes in char mode
|
||||
"auto_stop_silence": True, # stop recording automatically after sustained silence
|
||||
"silence_timeout_seconds": 1.5, # seconds of silence before auto-stop
|
||||
"postprocessing": {
|
||||
"enabled": False,
|
||||
"model": None, # active model HF repo id
|
||||
|
||||
Reference in New Issue
Block a user