feat: Add dual-mode dictation (walkie-talkie + continuous)
- Press and hold: walkie-talkie mode, records while held, transcribes on release - Double-click: continuous mode (existing behavior), click again to stop - Uses Dear PyGui item handlers for mouse down/up detection - Visual feedback shows current mode on button label 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
57efe6c931
commit
a2d6cb6f3c
|
|
@ -409,14 +409,77 @@ class DiscussionGUI:
|
||||||
if dpg.does_item_exist(button_tag):
|
if dpg.does_item_exist(button_tag):
|
||||||
dpg.set_item_label(button_tag, "Read")
|
dpg.set_item_label(button_tag, "Read")
|
||||||
|
|
||||||
def _dictate_clicked(self):
|
def _on_dictate_activated(self, sender, app_data):
|
||||||
"""Handle dictate button click - toggle recording."""
|
"""Handle mouse down on dictate button - start timing for mode detection."""
|
||||||
if self._continuous_recorder is not None:
|
import time
|
||||||
# Currently recording - stop
|
|
||||||
self._stop_dictation()
|
current_time = time.time()
|
||||||
else:
|
|
||||||
# Start recording
|
# Check for double-click (two clicks within 300ms)
|
||||||
|
if current_time - self._last_dictate_click_time < 0.3:
|
||||||
|
# Double-click detected
|
||||||
|
self._last_dictate_click_time = 0 # Reset to prevent triple-click issues
|
||||||
|
self._dictation_mode = "continuous"
|
||||||
self._start_dictation()
|
self._start_dictation()
|
||||||
|
return
|
||||||
|
|
||||||
|
self._last_dictate_click_time = current_time
|
||||||
|
self._mouse_down_time = current_time
|
||||||
|
|
||||||
|
# If we're in continuous mode, a click should stop it
|
||||||
|
if self._dictation_mode == "continuous":
|
||||||
|
self._stop_dictation()
|
||||||
|
self._dictation_mode = "idle"
|
||||||
|
return
|
||||||
|
|
||||||
|
# Start push-to-talk mode preparation
|
||||||
|
# We'll confirm it in deactivated if held long enough
|
||||||
|
self._dictation_mode = "push_to_talk_pending"
|
||||||
|
|
||||||
|
# Update button visual to show it's being pressed
|
||||||
|
if dpg.does_item_exist("dictate_btn"):
|
||||||
|
dpg.set_item_label("dictate_btn", "● Hold...")
|
||||||
|
|
||||||
|
def _on_dictate_deactivated(self, sender, app_data):
|
||||||
|
"""Handle mouse up on dictate button - determine action based on hold duration."""
|
||||||
|
import time
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
hold_duration = current_time - getattr(self, '_mouse_down_time', current_time)
|
||||||
|
|
||||||
|
if self._dictation_mode == "push_to_talk_pending":
|
||||||
|
if hold_duration > 0.2: # Held for more than 200ms - it was push-to-talk
|
||||||
|
# Button was held down, treat as walkie-talkie release
|
||||||
|
self._dictation_mode = "idle"
|
||||||
|
if self._continuous_recorder is not None:
|
||||||
|
self._stop_dictation()
|
||||||
|
else:
|
||||||
|
# Was just a long press but recording didn't start yet
|
||||||
|
if dpg.does_item_exist("dictate_btn"):
|
||||||
|
dpg.set_item_label("dictate_btn", "Dictate")
|
||||||
|
else:
|
||||||
|
# Short click - wait for potential double-click
|
||||||
|
self._dictation_mode = "idle"
|
||||||
|
if dpg.does_item_exist("dictate_btn"):
|
||||||
|
dpg.set_item_label("dictate_btn", "Dictate")
|
||||||
|
elif self._dictation_mode == "push_to_talk":
|
||||||
|
# Release from push-to-talk mode - stop recording
|
||||||
|
self._dictation_mode = "idle"
|
||||||
|
self._stop_dictation()
|
||||||
|
# continuous mode is handled by click, not release
|
||||||
|
|
||||||
|
def _on_dictate_active(self, sender, app_data):
|
||||||
|
"""Handle button being held down - start push-to-talk recording after threshold."""
|
||||||
|
import time
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
hold_duration = current_time - getattr(self, '_mouse_down_time', current_time)
|
||||||
|
|
||||||
|
if self._dictation_mode == "push_to_talk_pending" and hold_duration > 0.2:
|
||||||
|
# Held long enough - start push-to-talk recording
|
||||||
|
self._dictation_mode = "push_to_talk"
|
||||||
|
if self._continuous_recorder is None:
|
||||||
|
self._start_dictation()
|
||||||
|
|
||||||
def _start_dictation(self):
|
def _start_dictation(self):
|
||||||
"""Start continuous recording with chunked transcription."""
|
"""Start continuous recording with chunked transcription."""
|
||||||
|
|
@ -431,9 +494,12 @@ class DiscussionGUI:
|
||||||
|
|
||||||
self._continuous_recorder.start()
|
self._continuous_recorder.start()
|
||||||
|
|
||||||
# Update button to show recording state
|
# Update button to show recording state based on mode
|
||||||
if dpg.does_item_exist("dictate_btn"):
|
if dpg.does_item_exist("dictate_btn"):
|
||||||
dpg.set_item_label("dictate_btn", "● Recording...")
|
if self._dictation_mode == "continuous":
|
||||||
|
dpg.set_item_label("dictate_btn", "● Continuous (click to stop)")
|
||||||
|
else: # push_to_talk
|
||||||
|
dpg.set_item_label("dictate_btn", "● Recording (release to stop)")
|
||||||
|
|
||||||
def _stop_dictation(self):
|
def _stop_dictation(self):
|
||||||
"""Stop recording and transcribe any remaining audio."""
|
"""Stop recording and transcribe any remaining audio."""
|
||||||
|
|
@ -556,6 +622,12 @@ class DiscussionGUI:
|
||||||
self._transcription_queue = [] # Queue of transcription results
|
self._transcription_queue = [] # Queue of transcription results
|
||||||
self._transcription_lock = threading.Lock()
|
self._transcription_lock = threading.Lock()
|
||||||
|
|
||||||
|
# Dictation mode tracking for press-and-hold vs double-click
|
||||||
|
self._dictation_mode = "idle" # "idle", "push_to_talk", "push_to_talk_pending", "continuous"
|
||||||
|
self._last_dictate_click_time = 0.0
|
||||||
|
self._mouse_down_time = 0.0
|
||||||
|
self._push_to_talk_thread: Optional[threading.Thread] = None
|
||||||
|
|
||||||
# Initialize Dear PyGui
|
# Initialize Dear PyGui
|
||||||
dpg.create_context()
|
dpg.create_context()
|
||||||
dpg.create_viewport(title="Orchestrated Discussions", width=1400, height=900)
|
dpg.create_viewport(title="Orchestrated Discussions", width=1400, height=900)
|
||||||
|
|
@ -1918,6 +1990,11 @@ class DiscussionGUI:
|
||||||
window_tag = "comment_dialog"
|
window_tag = "comment_dialog"
|
||||||
if dpg.does_item_exist(window_tag):
|
if dpg.does_item_exist(window_tag):
|
||||||
dpg.delete_item(window_tag)
|
dpg.delete_item(window_tag)
|
||||||
|
if dpg.does_item_exist("dictate_btn_handlers"):
|
||||||
|
dpg.delete_item("dictate_btn_handlers")
|
||||||
|
# Reset dictation state when dialog opens
|
||||||
|
self._dictation_mode = "idle"
|
||||||
|
self._last_dictate_click_time = 0.0
|
||||||
|
|
||||||
with dpg.window(label="Add Comment", tag=window_tag, modal=True,
|
with dpg.window(label="Add Comment", tag=window_tag, modal=True,
|
||||||
width=600, height=400, pos=[400, 200]):
|
width=600, height=400, pos=[400, 200]):
|
||||||
|
|
@ -1928,9 +2005,14 @@ class DiscussionGUI:
|
||||||
dpg.add_button(
|
dpg.add_button(
|
||||||
label="Dictate",
|
label="Dictate",
|
||||||
tag="dictate_btn",
|
tag="dictate_btn",
|
||||||
callback=lambda: self._dictate_clicked(),
|
|
||||||
width=100
|
width=100
|
||||||
)
|
)
|
||||||
|
# Add item handlers for press-and-hold vs double-click detection
|
||||||
|
with dpg.item_handler_registry(tag="dictate_btn_handlers"):
|
||||||
|
dpg.add_item_activated_handler(callback=self._on_dictate_activated)
|
||||||
|
dpg.add_item_deactivated_handler(callback=self._on_dictate_deactivated)
|
||||||
|
dpg.add_item_active_handler(callback=self._on_dictate_active)
|
||||||
|
dpg.bind_item_handler_registry("dictate_btn", "dictate_btn_handlers")
|
||||||
dpg.add_button(
|
dpg.add_button(
|
||||||
label="Add Artifact",
|
label="Add Artifact",
|
||||||
callback=lambda: self._launch_artifact_editor_for_comment(),
|
callback=lambda: self._launch_artifact_editor_for_comment(),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue