From 77b05a3042b22cfed6851dc9abefc4f0691c68fe Mon Sep 17 00:00:00 2001 From: vivek Date: Thu, 23 May 2024 18:35:27 -0400 Subject: [PATCH 01/19] 'checkpoint' --- app/transcribe/appui.py | 465 +++++++++++++++++++++++++++ app/transcribe/ui/selectable_text.py | 123 +++++++ 2 files changed, 588 insertions(+) create mode 100644 app/transcribe/appui.py create mode 100644 app/transcribe/ui/selectable_text.py diff --git a/app/transcribe/appui.py b/app/transcribe/appui.py new file mode 100644 index 0000000..552d928 --- /dev/null +++ b/app/transcribe/appui.py @@ -0,0 +1,465 @@ +import threading +import datetime +import time +import tkinter as tk +import webbrowser +import pyperclip +import customtkinter as ctk +from tktooltip import ToolTip +from audio_transcriber import AudioTranscriber +import prompts +from global_vars import TranscriptionGlobals, T_GLOBALS +import constants +import gpt_responder as gr +from tsutils.language import LANGUAGES_DICT +from tsutils import utilities +from tsutils import app_logging as al +from tsutils import configuration + +logger = al.get_module_logger(al.UI_LOGGER) +UI_FONT_SIZE = 20 +# Order of initialization can be unpredictable in python based on where imports are placed. +# Setting it to None so comparison is deterministic in update_transcript_ui method +last_transcript_ui_update_time: datetime.datetime = None +global_vars_module: TranscriptionGlobals = T_GLOBALS +pop_up = None + + +class AppUI: + global_vars: TranscriptionGlobals + ui_filename: str = None + + def __init__(self, config: dict): + self.global_vars = TranscriptionGlobals() + + root = ctk.CTk() + ctk.set_appearance_mode("dark") + ctk.set_default_color_theme("dark-blue") + root.title("Transcribe") + root.configure(bg='#252422') + root.geometry("1000x600") + + # Create the menu bar + menubar = tk.Menu(root) + + # Create a file menu + filemenu = tk.Menu(menubar, tearoff=False) + + # Add a "Save" menu item to the file menu + filemenu.add_command(label="Save Transcript to File", command=self.save_file) + + # Add a "Pause" menu item to the file menu + filemenu.add_command(label="Pause Transcription", command=self.set_transcript_state) + + # Add a "Quit" menu item to the file menu + filemenu.add_command(label="Quit", command=root.quit) + + # Add the file menu to the menu bar + menubar.add_cascade(label="File", menu=filemenu) + + # Create an edit menu + editmenu = tk.Menu(menubar, tearoff=False) + + # Add a "Clear Audio Transcript" menu item to the file menu + editmenu.add_command(label="Clear Audio Transcript", command=lambda: + self.global_vars.transcriber.clear_transcriber_context(self.global_vars.audio_queue)) + + # Add a "Copy To Clipboard" menu item to the file menu + editmenu.add_command(label="Copy Transcript to Clipboard", command=self.copy_to_clipboard) + + # Add "Disable Speaker" menu item to file menu + editmenu.add_command(label="Disable Speaker", command=lambda: self.enable_disable_speaker(editmenu)) + + # Add "Disable Microphone" menu item to file menu + editmenu.add_command(label="Disable Microphone", command=lambda: self.enable_disable_microphone(editmenu)) + + # Add the edit menu to the menu bar + menubar.add_cascade(label="Edit", menu=editmenu) + + # Create help menu, add items in help menu + helpmenu = tk.Menu(menubar, tearoff=False) + helpmenu.add_command(label="Github Repo", command=self.open_github) + helpmenu.add_command(label="Star the Github repo", command=self.open_github) + helpmenu.add_command(label="Report an Issue", command=self.open_support) + menubar.add_cascade(label="Help", menu=helpmenu) + + # Add the menu bar to the main window + root.config(menu=menubar) + + # Speech to Text textbox + transcript_textbox = ctk.CTkTextbox(root, width=300, font=("Arial", UI_FONT_SIZE), + text_color='#FFFCF2', wrap="word") + transcript_textbox.grid(row=0, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + + # LLM Response textbox + response_textbox = ctk.CTkTextbox(root, width=300, font=("Arial", UI_FONT_SIZE), + text_color='#639cdc', wrap="word") + response_textbox.grid(row=0, column=2, padx=10, pady=3, sticky="nsew") + response_textbox.insert("0.0", prompts.INITIAL_RESPONSE) + + response_enabled = bool(config['General']['continuous_response']) + b_text = "Suggest Responses Continuously" if not response_enabled else "Do Not Suggest Responses Continuously" + continuous_response_button = ctk.CTkButton(root, text=b_text) + continuous_response_button.grid(row=1, column=2, padx=10, pady=3, sticky="nsew") + + response_now_button = ctk.CTkButton(root, text="Suggest Response Now") + response_now_button.grid(row=2, column=2, padx=10, pady=3, sticky="nsew") + + read_response_now_button = ctk.CTkButton(root, text="Suggest Response and Read") + read_response_now_button.grid(row=3, column=2, padx=10, pady=3, sticky="nsew") + + summarize_button = ctk.CTkButton(root, text="Summarize") + summarize_button.grid(row=4, column=2, padx=10, pady=3, sticky="nsew") + + # Continuous LLM Response label, and slider + update_interval_slider_label = ctk.CTkLabel(root, text="", font=("Arial", 12), + text_color="#FFFCF2") + update_interval_slider_label.grid(row=1, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + + update_interval_slider = ctk.CTkSlider(root, from_=1, to=30, width=300, # height=5, + number_of_steps=29) + update_interval_slider.set(config['General']['llm_response_interval']) + update_interval_slider.grid(row=2, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + + # Speech to text language selection label, dropdown + audio_lang_label = ctk.CTkLabel(root, text="Audio Lang: ", + font=("Arial", 12), + text_color="#FFFCF2") + audio_lang_label.grid(row=3, column=0, padx=10, pady=3, sticky="nw") + + audio_lang = config['OpenAI']['audio_lang'] + audio_lang_combobox = ctk.CTkOptionMenu(root, width=15, values=list(LANGUAGES_DICT.values())) + audio_lang_combobox.set(audio_lang) + audio_lang_combobox.grid(row=3, column=0, ipadx=60, padx=10, pady=3, sticky="ne") + + # LLM Response language selection label, dropdown + response_lang_label = ctk.CTkLabel(root, + text="Response Lang: ", + font=("Arial", 12), text_color="#FFFCF2") + response_lang_label.grid(row=3, column=1, padx=10, pady=3, sticky="nw") + + response_lang = config['OpenAI']['response_lang'] + response_lang_combobox = ctk.CTkOptionMenu(root, width=15, values=list(LANGUAGES_DICT.values())) + response_lang_combobox.set(response_lang) + response_lang_combobox.grid(row=3, column=1, ipadx=60, padx=10, pady=3, sticky="ne") + + github_link = ctk.CTkLabel(root, text="Star the Github Repo", + text_color="#639cdc", cursor="hand2") + github_link.grid(row=4, column=0, padx=10, pady=3, sticky="wn") + + issue_link = ctk.CTkLabel(root, text="Report an issue", text_color="#639cdc", cursor="hand2") + issue_link.grid(row=4, column=1, padx=10, pady=3, sticky="wn") + + # Create right click menu for transcript textbox. + # This displays only inside the speech to text textbox + m = tk.Menu(root, tearoff=0) + m.add_command(label="Generate response for selected text", + command=self.get_response_selected_now) + m.add_command(label="Save Transcript to File", command=self.save_file) + m.add_command(label="Clear Audio Transcript", command=lambda: + self.global_vars.transcriber.clear_transcriber_context(self.global_vars.audio_queue)) + m.add_command(label="Copy Transcript to Clipboard", command=self.copy_to_clipboard) + m.add_separator() + m.add_command(label="Quit", command=root.quit) + + chat_inference_provider = config['General']['chat_inference_provider'] + if chat_inference_provider == 'openai': + api_key = config['OpenAI']['api_key'] + base_url = config['OpenAI']['base_url'] + model = config['OpenAI']['ai_model'] + elif chat_inference_provider == 'together': + api_key = config['Together']['api_key'] + base_url = config['Together']['base_url'] + model = config['Together']['ai_model'] + + if not utilities.is_api_key_valid(api_key=api_key, base_url=base_url, model=model): + # Disable buttons that interact with backend services + continuous_response_button.configure(state='disabled') + response_now_button.configure(state='disabled') + read_response_now_button.configure(state='disabled') + summarize_button.configure(state='disabled') + + tt_msg = 'Add API Key in override.yaml to enable button' + # Add tooltips for disabled buttons + ToolTip(continuous_response_button, msg=tt_msg, + delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, + padx=7, pady=7) + ToolTip(response_now_button, msg=tt_msg, + delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, + padx=7, pady=7) + ToolTip(read_response_now_button, msg=tt_msg, + delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, + padx=7, pady=7) + ToolTip(summarize_button, msg=tt_msg, + delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, + padx=7, pady=7) + + def copy_to_clipboard(self): + """Copy transcription text data to clipboard. + Does not include responses from assistant. + """ + logger.info(AppUI.copy_to_clipboard.__name__) + self.capture_action("Copy transcript to clipboard") + try: + pyperclip.copy(self.global_vars.transcriber.get_transcript()) + except Exception as e: + logger.error(f"Error copying to clipboard: {e}") + + def save_file(self): + """Save transcription text data to file. + Does not include responses from assistant. + """ + logger.info(AppUI.save_file.__name__) + filename = ctk.filedialog.asksaveasfilename(defaultextension='.txt', + title='Save Transcription', + filetypes=[("Text Files", "*.txt")]) + self.capture_action(f'Save transcript to file:{filename}') + if not filename: + return + try: + with open(file=filename, mode="w", encoding='utf-8') as file_handle: + file_handle.write(self.global_vars.transcriber.get_transcript()) + except Exception as e: + logger.error(f"Error saving file {filename}: {e}") + + def freeze_unfreeze(self): + """Respond to start / stop of seeking responses from openAI API + """ + logger.info(AppUI.freeze_unfreeze.__name__) + try: + # Invert the state + self.global_vars.responder.enabled = not self.global_vars.responder.enabled + self.capture_action(f'{"Enabled " if self.global_vars.responder.enabled else "Disabled "} continuous LLM responses') + self.global_vars.freeze_button.configure( + text="Suggest Responses Continuously" if not self.global_vars.responder.enabled else "Do Not Suggest Responses Continuously" + ) + except Exception as e: + logger.error(f"Error toggling responder state: {e}") + + def enable_disable_speaker(self, editmenu): + """Toggles the state of speaker + """ + try: + self.global_vars.speaker_audio_recorder.enabled = not self.global_vars.speaker_audio_recorder.enabled + editmenu.entryconfigure(2, label="Disable Speaker" if self.global_vars.speaker_audio_recorder.enabled else "Enable Speaker") + self.capture_action(f'{"Enabled " if self.global_vars.speaker_audio_recorder.enabled else "Disabled "} speaker input') + except Exception as e: + logger.error(f"Error toggling speaker state: {e}") + + def enable_disable_microphone(self, editmenu): + """Toggles the state of microphone + """ + try: + self.global_vars.user_audio_recorder.enabled = not self.global_vars.user_audio_recorder.enabled + editmenu.entryconfigure(3, label="Disable Microphone" if self.global_vars.user_audio_recorder.enabled else "Enable Microphone") + self.capture_action(f'{"Enabled " if self.global_vars.user_audio_recorder.enabled else "Disabled "} microphone input') + except Exception as e: + logger.error(f"Error toggling microphone state: {e}") + + def update_interval_slider_value(self, slider_value): + """Update interval slider label to match the slider value + Update the config value + """ + try: + config_obj = configuration.Config() + # Save config + altered_config = {'General': {'llm_response_interval': int(slider_value)}} + config_obj.add_override_value(altered_config) + + label_text = f'LLM Response interval: {int(slider_value)} seconds' + self.global_vars.update_interval_slider_label.configure(text=label_text) + self.capture_action(f'Update LLM response interval to {int(slider_value)}') + except Exception as e: + logger.error(f"Error updating slider value: {e}") + + def get_response_now(self): + """Get response from LLM right away + Update the Response UI with the response + """ + if self.global_vars.update_response_now: + # We are already in the middle of getting a response + return + # We need a separate thread to ensure UI is responsive as responses are + # streamed back. Without the thread UI appears stuck as we stream the + # responses back + self.capture_action('Get LLM response now') + response_ui_thread = threading.Thread(target=self.get_response_now_threaded, + name='GetResponseNow') + response_ui_thread.daemon = True + response_ui_thread.start() + + def get_response_selected_now_threaded(self, text: str): + """Update response UI in a separate thread + """ + self.update_response_ui_threaded(lambda: self.global_vars.responder.generate_response_for_selected_text(text)) + + def get_response_now_threaded(self): + """Update response UI in a separate thread + """ + self.update_response_ui_threaded(self.global_vars.responder.generate_response_from_transcript_no_check) + + def update_response_ui_threaded(self, response_generator): + """Helper method to update response UI in a separate thread + """ + try: + self.global_vars.update_response_now = True + response_string = response_generator() + self.global_vars.update_response_now = False + # Set event to play the recording audio if required + if self.global_vars.read_response: + self.global_vars.audio_player_var.speech_text_available.set() + self.global_vars.response_textbox.configure(state="normal") + if response_string: + write_in_textbox(self.global_vars.response_textbox, response_string) + self.global_vars.response_textbox.configure(state="disabled") + self.global_vars.response_textbox.see("end") + except Exception as e: + logger.error(f"Error in threaded response: {e}") + + def get_response_selected_now(self): + """Get response from LLM right away for selected_text + Update the Response UI with the response + """ + if self.global_vars.update_response_now: + # We are already in the middle of getting a response + return + # We need a separate thread to ensure UI is responsive as responses are + # streamed back. Without the thread UI appears stuck as we stream the + # responses back + self.capture_action('Get LLM response selected now') + selected_text = self.global_vars.transcript_textbox.selection_get() + response_ui_thread = threading.Thread(target=self.get_response_selected_now_threaded, + args=(selected_text,), + name='GetResponseSelectedNow') + response_ui_thread.daemon = True + response_ui_thread.start() + + def summarize_threaded(self): + """Get summary from LLM in a separate thread""" + global pop_up # pylint: disable=W0603 + try: + print('Summarizing...') + popup_msg_no_close(title='Summary', msg='Creating a summary') + summary = self.global_vars.responder.summarize() + # When API key is not specified, give a chance for the thread to initialize + + if pop_up is not None: + try: + pop_up.destroy() + except Exception as e: + # Somehow we get the exception + # RuntimeError: main thread is not in main loop + logger.info('Exception in summarize_threaded') + logger.info(e) + + pop_up = None + if summary is None: + popup_msg_close_button(title='Summary', + msg='Failed to get summary. Please check you have a valid API key.') + return + + # Enhancement here would be to get a streaming summary + popup_msg_close_button(title='Summary', msg=summary) + except Exception as e: + logger.error(f"Error in summarize_threaded: {e}") + + def summarize(self): + """Get summary response from LLM + """ + self.capture_action('Get summary from LLM') + summarize_ui_thread = threading.Thread(target=self.summarize_threaded, + name='Summarize') + summarize_ui_thread.daemon = True + summarize_ui_thread.start() + + def update_response_ui_and_read_now(self): + """Get response from LLM right away + Update the Response UI with the response + Read the response + """ + self.capture_action('Get LLM response now and read aloud') + self.global_vars.set_read_response(True) + self.get_response_now() + + def set_transcript_state(self): + """Enables, disables transcription. + Text of menu item File -> Pause Transcription toggles accordingly + """ + logger.info(AppUI.set_transcript_state.__name__) + try: + self.global_vars.transcriber.transcribe = not self.global_vars.transcriber.transcribe + self.capture_action(f'{"Enabled " if self.global_vars.transcriber.transcribe else "Disabled "} transcription.') + if self.global_vars.transcriber.transcribe: + self.global_vars.filemenu.entryconfigure(1, label="Pause Transcription") + else: + self.global_vars.filemenu.entryconfigure(1, label="Start Transcription") + except Exception as e: + logger.error(f"Error setting transcript state: {e}") + + def open_link(self, url: str): + """Open the link in a web browser + """ + self.capture_action(f'Navigate to {url}.') + try: + webbrowser.open(url=url, new=2) + except Exception as e: + logger.error(f"Error opening URL {url}: {e}") + + def open_github(self): + """Link to git repo main page + """ + self.capture_action('open_github.') + self.open_link('https://github.com/vivekuppal/transcribe?referer=desktop') + + def open_support(self): + """Link to git repo issues page + """ + self.capture_action('open_support.') + self.open_link('https://github.com/vivekuppal/transcribe/issues/new?referer=desktop') + + def capture_action(self, action_text: str): + """Write to file + """ + try: + if not self.ui_filename: + data_dir = utilities.get_data_path(app_name='Transcribe') + self.ui_filename = utilities.incrementing_filename(filename=f'{data_dir}/logs/ui', extension='txt') + with open(self.ui_filename, mode='a', encoding='utf-8') as ui_file: + ui_file.write(f'{datetime.datetime.now()}: {action_text}\n') + except Exception as e: + logger.error(f"Error capturing action {action_text}: {e}") + + def set_audio_language(self, lang: str): + """Alter audio language in memory and persist it in config file + """ + try: + self.global_vars.transcriber.stt_model.set_lang(lang) + config_obj = configuration.Config() + # Save config + altered_config = {'OpenAI': {'audio_lang': lang}} + config_obj.add_override_value(altered_config) + except Exception as e: + logger.error(f"Error setting audio language: {e}") + + def set_response_language(self, lang: str): + """Alter response language in memory and persist it in config file + """ + try: + config_obj = configuration.Config() + altered_config = {'OpenAI': {'response_lang': lang}} + # Save config + config_obj.add_override_value(altered_config) + config_data = config_obj.data + + # Create a new system prompt + prompt = config_data["General"]["system_prompt"] + response_lang = config_data["OpenAI"]["response_lang"] + if response_lang is not None: + prompt += f'. Respond exclusively in {response_lang}.' + convo = self.global_vars.convo + convo.update_conversation(persona=constants.PERSONA_SYSTEM, + text=prompt, + time_spoken=datetime.datetime.utcnow(), + update_previous=True) + except Exception as e: + logger.error(f"Error setting response language: {e}") diff --git a/app/transcribe/ui/selectable_text.py b/app/transcribe/ui/selectable_text.py new file mode 100644 index 0000000..70780dc --- /dev/null +++ b/app/transcribe/ui/selectable_text.py @@ -0,0 +1,123 @@ +from tkinter import Text +import customtkinter as ctk + + +class SelectableText(ctk.CTk): + """ + A CustomTkinter application that displays a lot of lines of text, + where each line can wrap and is selectable to trigger an event on selection. + """ + def __init__(self, title: str, geometry: str): + """ + Initialize the SelectableText application. + """ + super().__init__() + + self.title(title) + self.geometry(geometry) + + # Create a frame to hold the Text widget and the scrollbar + self.text_frame = ctk.CTkFrame(self) + self.text_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Create a Text widget + self.text_widget = Text(self.text_frame, wrap="word", + bg=ctk.ThemeManager.theme['CTkFrame']['fg_color'][1], + fg="white") + self.text_widget.pack(side="left", fill="both", expand=True) + + # Create a Scrollbar and attach it to the Text widget + self.scrollbar = ctk.CTkScrollbar(self.text_frame, command=self.text_widget.yview) + self.scrollbar.pack(side="right", fill="y") + self.text_widget.config(yscrollcommand=self.scrollbar.set) + + # Bind click event to the Text widget + self.text_widget.bind("", self.on_text_click) + + # Make the Text widget read-only + self.text_widget.configure(state="disabled") + + # Create a frame for the buttons + self.button_frame = ctk.CTkFrame(self) + self.button_frame.pack(fill="x", padx=20, pady=10) + + # Create buttons to scroll to the top and bottom + self.scroll_top_button = ctk.CTkButton(self.button_frame, + text="Scroll to Top", + command=self.scroll_to_top) + self.scroll_top_button.pack(side="left", padx=10) + + self.scroll_bottom_button = ctk.CTkButton(self.button_frame, + text="Scroll to Bottom", + command=self.scroll_to_bottom) + self.scroll_bottom_button.pack(side="left", padx=10) + + # Create buttons to add text to the top and bottom + self.add_top_button = ctk.CTkButton(self.button_frame, + text="Add Text to Top", + command=self.add_text_to_top) + self.add_top_button.pack(side="left", padx=10) + + self.add_bottom_button = ctk.CTkButton(self.button_frame, + text="Add Text to Bottom", + command=self.add_text_to_bottom) + self.add_bottom_button.pack(side="left", padx=10) + + def on_text_click(self, event): + """ + Handle the click event on the Text widget. + + Args: + event (tkinter.Event): The event object containing event details. + """ + # Get the index of the clicked line + index = self.text_widget.index("@%s,%s" % (event.x, event.y)) + line_number = int(index.split(".")[0]) + + # Get the text of the clicked line + line_start = f"{line_number}.0" + line_end = f"{line_number}.end" + line_text = self.text_widget.get(line_start, line_end).strip() + + # Trigger an event (print the line text) + print(f"Selected: {line_text}") + + def scroll_to_top(self): + """ + Scroll the Text widget to the top. + """ + self.text_widget.yview_moveto(0) + + def scroll_to_bottom(self): + """ + Scroll the Text widget to the bottom. + """ + self.text_widget.yview_moveto(1) + + def add_text_to_top(self, input_text: str): + """ + Add text to the top of the Text widget. + """ + self.text_widget.configure(state="normal") + self.text_widget.insert("1.0", input_text + "\n") + self.text_widget.configure(state="disabled") + + def add_text_to_bottom(self, input_text: str): + """ + Add text to the bottom of the Text widget. + """ + self.text_widget.configure(state="normal") + self.text_widget.insert("end", input_text + "\n") + self.text_widget.configure(state="disabled") + + +if __name__ == "__main__": + ctk.set_appearance_mode("dark") + app = SelectableText('Simple Selectable text example', '600x400') + + # Add a lot of lines of text + lines_of_text = [f"Line {i}: This is an example of a long line of text that should wrap around the Text widget." for i in range(1, 101)] + for line in lines_of_text: + app.add_text_to_bottom(line) + + app.mainloop() From 2f457e198fe088176de5d8c5ce6e16890d355d7f Mon Sep 17 00:00:00 2001 From: vivek Date: Thu, 23 May 2024 18:49:50 -0400 Subject: [PATCH 02/19] misc --- app/transcribe/appui.py | 262 +++++++++++++++++++++++++++++++--------- 1 file changed, 203 insertions(+), 59 deletions(-) diff --git a/app/transcribe/appui.py b/app/transcribe/appui.py index 552d928..403545c 100644 --- a/app/transcribe/appui.py +++ b/app/transcribe/appui.py @@ -43,38 +43,41 @@ def __init__(self, config: dict): menubar = tk.Menu(root) # Create a file menu - filemenu = tk.Menu(menubar, tearoff=False) + self.filemenu = tk.Menu(menubar, tearoff=False) # Add a "Save" menu item to the file menu - filemenu.add_command(label="Save Transcript to File", command=self.save_file) + self.filemenu.add_command(label="Save Transcript to File", command=self.save_file) # Add a "Pause" menu item to the file menu - filemenu.add_command(label="Pause Transcription", command=self.set_transcript_state) + self.filemenu.add_command(label="Pause Transcription", command=self.set_transcript_state) # Add a "Quit" menu item to the file menu - filemenu.add_command(label="Quit", command=root.quit) + self.filemenu.add_command(label="Quit", command=root.quit) # Add the file menu to the menu bar - menubar.add_cascade(label="File", menu=filemenu) + menubar.add_cascade(label="File", menu=self.filemenu) # Create an edit menu - editmenu = tk.Menu(menubar, tearoff=False) + self.editmenu = tk.Menu(menubar, tearoff=False) # Add a "Clear Audio Transcript" menu item to the file menu - editmenu.add_command(label="Clear Audio Transcript", command=lambda: - self.global_vars.transcriber.clear_transcriber_context(self.global_vars.audio_queue)) + self.editmenu.add_command(label="Clear Audio Transcript", command=lambda: + self.global_vars.transcriber.clear_transcriber_context(self.global_vars.audio_queue)) # Add a "Copy To Clipboard" menu item to the file menu - editmenu.add_command(label="Copy Transcript to Clipboard", command=self.copy_to_clipboard) + self.editmenu.add_command(label="Copy Transcript to Clipboard", + command=self.copy_to_clipboard) # Add "Disable Speaker" menu item to file menu - editmenu.add_command(label="Disable Speaker", command=lambda: self.enable_disable_speaker(editmenu)) + self.editmenu.add_command(label="Disable Speaker", + command=lambda: self.enable_disable_speaker(self.editmenu)) # Add "Disable Microphone" menu item to file menu - editmenu.add_command(label="Disable Microphone", command=lambda: self.enable_disable_microphone(editmenu)) + self.editmenu.add_command(label="Disable Microphone", + command=lambda: self.enable_disable_microphone(self.editmenu)) # Add the edit menu to the menu bar - menubar.add_cascade(label="Edit", menu=editmenu) + menubar.add_cascade(label="Edit", menu=self.editmenu) # Create help menu, add items in help menu helpmenu = tk.Menu(menubar, tearoff=False) @@ -87,39 +90,39 @@ def __init__(self, config: dict): root.config(menu=menubar) # Speech to Text textbox - transcript_textbox = ctk.CTkTextbox(root, width=300, font=("Arial", UI_FONT_SIZE), - text_color='#FFFCF2', wrap="word") - transcript_textbox.grid(row=0, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + self.transcript_textbox = ctk.CTkTextbox(root, width=300, font=("Arial", UI_FONT_SIZE), + text_color='#FFFCF2', wrap="word") + self.transcript_textbox.grid(row=0, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") # LLM Response textbox - response_textbox = ctk.CTkTextbox(root, width=300, font=("Arial", UI_FONT_SIZE), - text_color='#639cdc', wrap="word") - response_textbox.grid(row=0, column=2, padx=10, pady=3, sticky="nsew") - response_textbox.insert("0.0", prompts.INITIAL_RESPONSE) + self.response_textbox = ctk.CTkTextbox(root, width=300, font=("Arial", UI_FONT_SIZE), + text_color='#639cdc', wrap="word") + self.response_textbox.grid(row=0, column=2, padx=10, pady=3, sticky="nsew") + self.response_textbox.insert("0.0", prompts.INITIAL_RESPONSE) response_enabled = bool(config['General']['continuous_response']) b_text = "Suggest Responses Continuously" if not response_enabled else "Do Not Suggest Responses Continuously" - continuous_response_button = ctk.CTkButton(root, text=b_text) - continuous_response_button.grid(row=1, column=2, padx=10, pady=3, sticky="nsew") + self.continuous_response_button = ctk.CTkButton(root, text=b_text) + self.continuous_response_button.grid(row=1, column=2, padx=10, pady=3, sticky="nsew") - response_now_button = ctk.CTkButton(root, text="Suggest Response Now") - response_now_button.grid(row=2, column=2, padx=10, pady=3, sticky="nsew") + self.response_now_button = ctk.CTkButton(root, text="Suggest Response Now") + self.response_now_button.grid(row=2, column=2, padx=10, pady=3, sticky="nsew") - read_response_now_button = ctk.CTkButton(root, text="Suggest Response and Read") - read_response_now_button.grid(row=3, column=2, padx=10, pady=3, sticky="nsew") + self.read_response_now_button = ctk.CTkButton(root, text="Suggest Response and Read") + self.read_response_now_button.grid(row=3, column=2, padx=10, pady=3, sticky="nsew") - summarize_button = ctk.CTkButton(root, text="Summarize") - summarize_button.grid(row=4, column=2, padx=10, pady=3, sticky="nsew") + self.summarize_button = ctk.CTkButton(root, text="Summarize") + self.summarize_button.grid(row=4, column=2, padx=10, pady=3, sticky="nsew") # Continuous LLM Response label, and slider - update_interval_slider_label = ctk.CTkLabel(root, text="", font=("Arial", 12), - text_color="#FFFCF2") - update_interval_slider_label.grid(row=1, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + self.update_interval_slider_label = ctk.CTkLabel(root, text="", font=("Arial", 12), + text_color="#FFFCF2") + self.update_interval_slider_label.grid(row=1, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") - update_interval_slider = ctk.CTkSlider(root, from_=1, to=30, width=300, # height=5, - number_of_steps=29) - update_interval_slider.set(config['General']['llm_response_interval']) - update_interval_slider.grid(row=2, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + self.update_interval_slider = ctk.CTkSlider(root, from_=1, to=30, width=300, # height=5, + number_of_steps=29) + self.update_interval_slider.set(config['General']['llm_response_interval']) + self.update_interval_slider.grid(row=2, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") # Speech to text language selection label, dropdown audio_lang_label = ctk.CTkLabel(root, text="Audio Lang: ", @@ -128,9 +131,9 @@ def __init__(self, config: dict): audio_lang_label.grid(row=3, column=0, padx=10, pady=3, sticky="nw") audio_lang = config['OpenAI']['audio_lang'] - audio_lang_combobox = ctk.CTkOptionMenu(root, width=15, values=list(LANGUAGES_DICT.values())) - audio_lang_combobox.set(audio_lang) - audio_lang_combobox.grid(row=3, column=0, ipadx=60, padx=10, pady=3, sticky="ne") + self.audio_lang_combobox = ctk.CTkOptionMenu(root, width=15, values=list(LANGUAGES_DICT.values())) + self.audio_lang_combobox.set(audio_lang) + self.audio_lang_combobox.grid(row=3, column=0, ipadx=60, padx=10, pady=3, sticky="ne") # LLM Response language selection label, dropdown response_lang_label = ctk.CTkLabel(root, @@ -139,16 +142,16 @@ def __init__(self, config: dict): response_lang_label.grid(row=3, column=1, padx=10, pady=3, sticky="nw") response_lang = config['OpenAI']['response_lang'] - response_lang_combobox = ctk.CTkOptionMenu(root, width=15, values=list(LANGUAGES_DICT.values())) - response_lang_combobox.set(response_lang) - response_lang_combobox.grid(row=3, column=1, ipadx=60, padx=10, pady=3, sticky="ne") + self.response_lang_combobox = ctk.CTkOptionMenu(root, width=15, values=list(LANGUAGES_DICT.values())) + self.response_lang_combobox.set(response_lang) + self.response_lang_combobox.grid(row=3, column=1, ipadx=60, padx=10, pady=3, sticky="ne") - github_link = ctk.CTkLabel(root, text="Star the Github Repo", - text_color="#639cdc", cursor="hand2") - github_link.grid(row=4, column=0, padx=10, pady=3, sticky="wn") + self.github_link = ctk.CTkLabel(root, text="Star the Github Repo", + text_color="#639cdc", cursor="hand2") + self.github_link.grid(row=4, column=0, padx=10, pady=3, sticky="wn") - issue_link = ctk.CTkLabel(root, text="Report an issue", text_color="#639cdc", cursor="hand2") - issue_link.grid(row=4, column=1, padx=10, pady=3, sticky="wn") + self.issue_link = ctk.CTkLabel(root, text="Report an issue", text_color="#639cdc", cursor="hand2") + self.issue_link.grid(row=4, column=1, padx=10, pady=3, sticky="wn") # Create right click menu for transcript textbox. # This displays only inside the speech to text textbox @@ -174,26 +177,34 @@ def __init__(self, config: dict): if not utilities.is_api_key_valid(api_key=api_key, base_url=base_url, model=model): # Disable buttons that interact with backend services - continuous_response_button.configure(state='disabled') - response_now_button.configure(state='disabled') - read_response_now_button.configure(state='disabled') - summarize_button.configure(state='disabled') + self.continuous_response_button.configure(state='disabled') + self.response_now_button.configure(state='disabled') + self.read_response_now_button.configure(state='disabled') + self.summarize_button.configure(state='disabled') tt_msg = 'Add API Key in override.yaml to enable button' # Add tooltips for disabled buttons - ToolTip(continuous_response_button, msg=tt_msg, + ToolTip(self.continuous_response_button, msg=tt_msg, delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, padx=7, pady=7) - ToolTip(response_now_button, msg=tt_msg, + ToolTip(self.response_now_button, msg=tt_msg, delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, padx=7, pady=7) - ToolTip(read_response_now_button, msg=tt_msg, + ToolTip(self.read_response_now_button, msg=tt_msg, delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, padx=7, pady=7) - ToolTip(summarize_button, msg=tt_msg, + ToolTip(self.summarize_button, msg=tt_msg, delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, padx=7, pady=7) + def show_context_menu(event): + try: + m.tk_popup(event.x_root, event.y_root) + finally: + m.grab_release() + + self.transcript_textbox.bind("", show_context_menu) + def copy_to_clipboard(self): """Copy transcription text data to clipboard. Does not include responses from assistant. @@ -230,28 +241,28 @@ def freeze_unfreeze(self): # Invert the state self.global_vars.responder.enabled = not self.global_vars.responder.enabled self.capture_action(f'{"Enabled " if self.global_vars.responder.enabled else "Disabled "} continuous LLM responses') - self.global_vars.freeze_button.configure( + self.continuous_response_button.configure( text="Suggest Responses Continuously" if not self.global_vars.responder.enabled else "Do Not Suggest Responses Continuously" ) except Exception as e: logger.error(f"Error toggling responder state: {e}") - def enable_disable_speaker(self, editmenu): + def enable_disable_speaker(self): """Toggles the state of speaker """ try: self.global_vars.speaker_audio_recorder.enabled = not self.global_vars.speaker_audio_recorder.enabled - editmenu.entryconfigure(2, label="Disable Speaker" if self.global_vars.speaker_audio_recorder.enabled else "Enable Speaker") + self.editmenu.entryconfigure(2, label="Disable Speaker" if self.global_vars.speaker_audio_recorder.enabled else "Enable Speaker") self.capture_action(f'{"Enabled " if self.global_vars.speaker_audio_recorder.enabled else "Disabled "} speaker input') except Exception as e: logger.error(f"Error toggling speaker state: {e}") - def enable_disable_microphone(self, editmenu): + def enable_disable_microphone(self): """Toggles the state of microphone """ try: self.global_vars.user_audio_recorder.enabled = not self.global_vars.user_audio_recorder.enabled - editmenu.entryconfigure(3, label="Disable Microphone" if self.global_vars.user_audio_recorder.enabled else "Enable Microphone") + self.editmenu.entryconfigure(3, label="Disable Microphone" if self.global_vars.user_audio_recorder.enabled else "Enable Microphone") self.capture_action(f'{"Enabled " if self.global_vars.user_audio_recorder.enabled else "Disabled "} microphone input') except Exception as e: logger.error(f"Error toggling microphone state: {e}") @@ -267,7 +278,7 @@ def update_interval_slider_value(self, slider_value): config_obj.add_override_value(altered_config) label_text = f'LLM Response interval: {int(slider_value)} seconds' - self.global_vars.update_interval_slider_label.configure(text=label_text) + self.update_interval_slider_label.configure(text=label_text) self.capture_action(f'Update LLM response interval to {int(slider_value)}') except Exception as e: logger.error(f"Error updating slider value: {e}") @@ -463,3 +474,136 @@ def set_response_language(self, lang: str): update_previous=True) except Exception as e: logger.error(f"Error setting response language: {e}") + + +def popup_msg_no_close_threaded(title, msg): + """Create a pop up with no close button. + """ + global pop_up # pylint: disable=W0603 + try: + popup = ctk.CTkToplevel(T_GLOBALS.main_window) + popup.geometry("100x50") + popup.title(title) + label = ctk.CTkLabel(popup, text=msg, font=("Arial", 12), + text_color="#FFFCF2") + label.pack(side="top", fill="x", pady=10) + pop_up = popup + popup.lift() + except Exception as e: + # Sometimes get the error - calling Tcl from different apartment + logger.info('Exception in popup_msg_no_close_threaded') + logger.info(e) + return + + +def popup_msg_no_close(title: str, msg: str): + """Create a popup that the caller is responsible for closing + using the destroy method + """ + kwargs = {} + kwargs['title'] = title + kwargs['msg'] = msg + pop_ui_thread = threading.Thread(target=popup_msg_no_close_threaded, + name='Pop up thread', + kwargs=kwargs) + pop_ui_thread.daemon = True + pop_ui_thread.start() + # Give a chance for the thread to initialize + # When API key is not specified, need the thread to initialize to + # allow summarize window to show and ultimately be closed. + time.sleep(0.1) + + +def popup_msg_close_button(title: str, msg: str): + """Create a popup that the caller is responsible for closing + using the destroy method + """ + popup = ctk.CTkToplevel(T_GLOBALS.main_window) + popup.geometry("380x710") + popup.title(title) + txtbox = ctk.CTkTextbox(popup, width=350, height=600, font=("Arial", UI_FONT_SIZE), + text_color='#FFFCF2', wrap="word") + txtbox.grid(row=0, column=0, padx=10, pady=10, sticky="nsew") + txtbox.insert("0.0", msg) + + def copy_summary_to_clipboard(): + pyperclip.copy(txtbox.cget("text")) + + copy_button = ctk.CTkButton(popup, text="Copy to Clipboard", command=copy_summary_to_clipboard) + copy_button.grid(row=1, column=0, padx=10, pady=5, sticky="nsew") + + close_button = ctk.CTkButton(popup, text="Close", command=popup.destroy) + close_button.grid(row=2, column=0, padx=10, pady=5, sticky="nsew") + popup.lift() + + +def write_in_textbox(textbox: ctk.CTkTextbox, text: str): + """Update the text of textbox with the given text + Args: + textbox: textbox to be updated + text: updated text + """ + # Get current selection attributes, so they can be preserved after writing new text + a: tuple = textbox.tag_ranges('sel') + # (, ) + textbox.delete("0.0", "end") + textbox.insert("0.0", text) + if len(a): + textbox.tag_add('sel', a[0], a[1]) + + +def update_transcript_ui(transcriber: AudioTranscriber, textbox: ctk.CTkTextbox): + """Update the text of transcription textbox with the given text + Args: + transcriber: AudioTranscriber Object + textbox: textbox to be updated + """ + + global last_transcript_ui_update_time # pylint: disable=W0603 + global global_vars_module # pylint: disable=W0603 + + if global_vars_module is None: + global_vars_module = TranscriptionGlobals() + + # None comparison is for initialization + if last_transcript_ui_update_time is None or last_transcript_ui_update_time < global_vars_module.convo.last_update: + transcript_string = transcriber.get_transcript() + write_in_textbox(textbox, transcript_string) + textbox.see("end") + last_transcript_ui_update_time = datetime.datetime.utcnow() + + textbox.after(constants.TRANSCRIPT_UI_UPDATE_DELAY_DURATION_MS, + update_transcript_ui, transcriber, textbox) + + +def update_response_ui(responder: gr.GPTResponder, + textbox: ctk.CTkTextbox, + update_interval_slider_label: ctk.CTkLabel, + update_interval_slider: ctk.CTkSlider): + """Update the text of response textbox with the given text + Args: + textbox: textbox to be updated + text: updated text + """ + global global_vars_module # pylint: disable=W0603 + + if global_vars_module is None: + global_vars_module = TranscriptionGlobals() + + # global_vars_module.responder.enabled --> This is continous response mode from LLM + # global_vars_module.update_response_now --> Get Response now from LLM + if global_vars_module.responder.enabled or global_vars_module.update_response_now: + response = responder.response + + textbox.configure(state="normal") + write_in_textbox(textbox, response) + textbox.configure(state="disabled") + textbox.see("end") + + update_interval = int(update_interval_slider.get()) + # responder.update_response_interval(update_interval) + update_interval_slider_label.configure(text=f'LLM Response interval: ' + f'{update_interval} seconds') + + textbox.after(300, update_response_ui, responder, textbox, + update_interval_slider_label, update_interval_slider) From df696f4606461b915e9dbf2aa6c8d4489e9779e5 Mon Sep 17 00:00:00 2001 From: vivek Date: Thu, 23 May 2024 19:09:57 -0400 Subject: [PATCH 03/19] misc --- app/transcribe/appui.py | 81 ++++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 25 deletions(-) diff --git a/app/transcribe/appui.py b/app/transcribe/appui.py index 403545c..23989a2 100644 --- a/app/transcribe/appui.py +++ b/app/transcribe/appui.py @@ -32,15 +32,20 @@ class AppUI: def __init__(self, config: dict): self.global_vars = TranscriptionGlobals() - root = ctk.CTk() + self.root = ctk.CTk() + self.global_vars.main_window = self.root + self.create_ui_components(config=config) + self.set_audio_device_menus() + + def create_ui_components(self, config: dict): ctk.set_appearance_mode("dark") ctk.set_default_color_theme("dark-blue") - root.title("Transcribe") - root.configure(bg='#252422') - root.geometry("1000x600") + self.root.title("Transcribe") + self.root.configure(bg='#252422') + self.root.geometry("1000x600") # Create the menu bar - menubar = tk.Menu(root) + menubar = tk.Menu(self.root) # Create a file menu self.filemenu = tk.Menu(menubar, tearoff=False) @@ -52,7 +57,7 @@ def __init__(self, config: dict): self.filemenu.add_command(label="Pause Transcription", command=self.set_transcript_state) # Add a "Quit" menu item to the file menu - self.filemenu.add_command(label="Quit", command=root.quit) + self.filemenu.add_command(label="Quit", command=self.root.quit) # Add the file menu to the menu bar menubar.add_cascade(label="File", menu=self.filemenu) @@ -70,11 +75,11 @@ def __init__(self, config: dict): # Add "Disable Speaker" menu item to file menu self.editmenu.add_command(label="Disable Speaker", - command=lambda: self.enable_disable_speaker(self.editmenu)) + command=self.enable_disable_speaker()) # Add "Disable Microphone" menu item to file menu self.editmenu.add_command(label="Disable Microphone", - command=lambda: self.enable_disable_microphone(self.editmenu)) + command=self.enable_disable_microphone()) # Add the edit menu to the menu bar menubar.add_cascade(label="Edit", menu=self.editmenu) @@ -87,75 +92,85 @@ def __init__(self, config: dict): menubar.add_cascade(label="Help", menu=helpmenu) # Add the menu bar to the main window - root.config(menu=menubar) + self.root.config(menu=menubar) # Speech to Text textbox - self.transcript_textbox = ctk.CTkTextbox(root, width=300, font=("Arial", UI_FONT_SIZE), + self.transcript_textbox = ctk.CTkTextbox(self.root, width=300, font=("Arial", UI_FONT_SIZE), text_color='#FFFCF2', wrap="word") self.transcript_textbox.grid(row=0, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") # LLM Response textbox - self.response_textbox = ctk.CTkTextbox(root, width=300, font=("Arial", UI_FONT_SIZE), + self.response_textbox = ctk.CTkTextbox(self.root, width=300, font=("Arial", UI_FONT_SIZE), text_color='#639cdc', wrap="word") self.response_textbox.grid(row=0, column=2, padx=10, pady=3, sticky="nsew") self.response_textbox.insert("0.0", prompts.INITIAL_RESPONSE) response_enabled = bool(config['General']['continuous_response']) b_text = "Suggest Responses Continuously" if not response_enabled else "Do Not Suggest Responses Continuously" - self.continuous_response_button = ctk.CTkButton(root, text=b_text) + self.continuous_response_button = ctk.CTkButton(self.root, text=b_text) self.continuous_response_button.grid(row=1, column=2, padx=10, pady=3, sticky="nsew") + self.continuous_response_button.configure(command=self.freeze_unfreeze) - self.response_now_button = ctk.CTkButton(root, text="Suggest Response Now") + self.response_now_button = ctk.CTkButton(self.root, text="Suggest Response Now") self.response_now_button.grid(row=2, column=2, padx=10, pady=3, sticky="nsew") + self.response_now_button.configure(command=self.get_response_now) - self.read_response_now_button = ctk.CTkButton(root, text="Suggest Response and Read") + self.read_response_now_button = ctk.CTkButton(self.root, text="Suggest Response and Read") self.read_response_now_button.grid(row=3, column=2, padx=10, pady=3, sticky="nsew") + self.read_response_now_button.configure(command=self.update_response_ui_and_read_now) - self.summarize_button = ctk.CTkButton(root, text="Summarize") + self.summarize_button = ctk.CTkButton(self.root, text="Summarize") self.summarize_button.grid(row=4, column=2, padx=10, pady=3, sticky="nsew") + self.summarize_button.configure(command=self.summarize) # Continuous LLM Response label, and slider - self.update_interval_slider_label = ctk.CTkLabel(root, text="", font=("Arial", 12), + self.update_interval_slider_label = ctk.CTkLabel(self.root, text="", font=("Arial", 12), text_color="#FFFCF2") self.update_interval_slider_label.grid(row=1, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") - self.update_interval_slider = ctk.CTkSlider(root, from_=1, to=30, width=300, # height=5, + self.update_interval_slider = ctk.CTkSlider(self.root, from_=1, to=30, width=300, # height=5, number_of_steps=29) self.update_interval_slider.set(config['General']['llm_response_interval']) self.update_interval_slider.grid(row=2, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + self.update_interval_slider.configure(command=self.update_interval_slider_value) + + label_text = f'LLM Response interval: {int(self.update_interval_slider.get())} seconds' + self.update_interval_slider_label.configure(text=label_text) # Speech to text language selection label, dropdown - audio_lang_label = ctk.CTkLabel(root, text="Audio Lang: ", + audio_lang_label = ctk.CTkLabel(self.root, text="Audio Lang: ", font=("Arial", 12), text_color="#FFFCF2") audio_lang_label.grid(row=3, column=0, padx=10, pady=3, sticky="nw") audio_lang = config['OpenAI']['audio_lang'] - self.audio_lang_combobox = ctk.CTkOptionMenu(root, width=15, values=list(LANGUAGES_DICT.values())) + self.audio_lang_combobox = ctk.CTkOptionMenu(self.root, width=15, values=list(LANGUAGES_DICT.values())) self.audio_lang_combobox.set(audio_lang) self.audio_lang_combobox.grid(row=3, column=0, ipadx=60, padx=10, pady=3, sticky="ne") + self.audio_lang_combobox.configure(command=self.set_audio_language) # LLM Response language selection label, dropdown - response_lang_label = ctk.CTkLabel(root, + response_lang_label = ctk.CTkLabel(self.root, text="Response Lang: ", font=("Arial", 12), text_color="#FFFCF2") response_lang_label.grid(row=3, column=1, padx=10, pady=3, sticky="nw") response_lang = config['OpenAI']['response_lang'] - self.response_lang_combobox = ctk.CTkOptionMenu(root, width=15, values=list(LANGUAGES_DICT.values())) + self.response_lang_combobox = ctk.CTkOptionMenu(self.root, width=15, values=list(LANGUAGES_DICT.values())) self.response_lang_combobox.set(response_lang) self.response_lang_combobox.grid(row=3, column=1, ipadx=60, padx=10, pady=3, sticky="ne") + self.response_lang_combobox.configure(command=self.set_response_language) - self.github_link = ctk.CTkLabel(root, text="Star the Github Repo", + self.github_link = ctk.CTkLabel(self.root, text="Star the Github Repo", text_color="#639cdc", cursor="hand2") self.github_link.grid(row=4, column=0, padx=10, pady=3, sticky="wn") - self.issue_link = ctk.CTkLabel(root, text="Report an issue", text_color="#639cdc", cursor="hand2") + self.issue_link = ctk.CTkLabel(self.root, text="Report an issue", text_color="#639cdc", cursor="hand2") self.issue_link.grid(row=4, column=1, padx=10, pady=3, sticky="wn") # Create right click menu for transcript textbox. # This displays only inside the speech to text textbox - m = tk.Menu(root, tearoff=0) + m = tk.Menu(self.root, tearoff=0) m.add_command(label="Generate response for selected text", command=self.get_response_selected_now) m.add_command(label="Save Transcript to File", command=self.save_file) @@ -163,7 +178,7 @@ def __init__(self, config: dict): self.global_vars.transcriber.clear_transcriber_context(self.global_vars.audio_queue)) m.add_command(label="Copy Transcript to Clipboard", command=self.copy_to_clipboard) m.add_separator() - m.add_command(label="Quit", command=root.quit) + m.add_command(label="Quit", command=self.root.quit) chat_inference_provider = config['General']['chat_inference_provider'] if chat_inference_provider == 'openai': @@ -205,6 +220,22 @@ def show_context_menu(event): self.transcript_textbox.bind("", show_context_menu) + self.root.grid_rowconfigure(0, weight=100) + self.root.grid_rowconfigure(1, weight=1) + self.root.grid_rowconfigure(2, weight=1) + self.root.grid_rowconfigure(3, weight=1) + self.root.grid_columnconfigure(0, weight=2) + self.root.grid_columnconfigure(1, weight=1) + + def set_audio_device_menus(self, config): + if config['General']['disable_speaker']: + print('[INFO] Disabling Speaker') + self.enable_disable_speaker() + + if config['General']['disable_mic']: + print('[INFO] Disabling Microphone') + self.enable_disable_microphone() + def copy_to_clipboard(self): """Copy transcription text data to clipboard. Does not include responses from assistant. From 3b20e36b261a4daca3d19ef0dd132492676bf3dd Mon Sep 17 00:00:00 2001 From: vivek Date: Thu, 23 May 2024 19:14:17 -0400 Subject: [PATCH 04/19] class has most of existing functionality --- app/transcribe/appui.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/transcribe/appui.py b/app/transcribe/appui.py index 23989a2..be931ee 100644 --- a/app/transcribe/appui.py +++ b/app/transcribe/appui.py @@ -26,6 +26,8 @@ class AppUI: + """Encapsulates all UI functionality for the app + """ global_vars: TranscriptionGlobals ui_filename: str = None @@ -35,9 +37,16 @@ def __init__(self, config: dict): self.root = ctk.CTk() self.global_vars.main_window = self.root self.create_ui_components(config=config) - self.set_audio_device_menus() + self.set_audio_device_menus(config=config) + + def start(self): + """Start showing the UI + """ + self.root.mainloop() def create_ui_components(self, config: dict): + """Create all UI components + """ ctk.set_appearance_mode("dark") ctk.set_default_color_theme("dark-blue") self.root.title("Transcribe") From 9251bf318fe5c876fc5adf722a1e2c3314cac08f Mon Sep 17 00:00:00 2001 From: vivek Date: Thu, 23 May 2024 19:28:15 -0400 Subject: [PATCH 05/19] refactor ui --- app/transcribe/appui.py | 12 ++++ app/transcribe/global_vars.py | 2 +- app/transcribe/main.py | 104 ++++++++++++++++++---------------- 3 files changed, 67 insertions(+), 51 deletions(-) diff --git a/app/transcribe/appui.py b/app/transcribe/appui.py index be931ee..c10389e 100644 --- a/app/transcribe/appui.py +++ b/app/transcribe/appui.py @@ -44,6 +44,14 @@ def start(self): """ self.root.mainloop() + def update_initial_transcripts(self): + update_transcript_ui(self.global_vars.transcriber, + self.transcript_textbox) + update_response_ui(self.global_vars.responder, + self.response_textbox, + self.update_interval_slider_label, + self.update_interval_slider) + def create_ui_components(self, config: dict): """Create all UI components """ @@ -173,9 +181,13 @@ def create_ui_components(self, config: dict): self.github_link = ctk.CTkLabel(self.root, text="Star the Github Repo", text_color="#639cdc", cursor="hand2") self.github_link.grid(row=4, column=0, padx=10, pady=3, sticky="wn") + self.github_link.bind('', lambda e: + self.open_link('https://github.com/vivekuppal/transcribe?referer=desktop')) self.issue_link = ctk.CTkLabel(self.root, text="Report an issue", text_color="#639cdc", cursor="hand2") self.issue_link.grid(row=4, column=1, padx=10, pady=3, sticky="wn") + self.issue_link.bind('', lambda e: self.open_link( + 'https://github.com/vivekuppal/transcribe/issues/new?referer=desktop')) # Create right click menu for transcript textbox. # This displays only inside the speech to text textbox diff --git a/app/transcribe/global_vars.py b/app/transcribe/global_vars.py index 2644dff..c7652f5 100644 --- a/app/transcribe/global_vars.py +++ b/app/transcribe/global_vars.py @@ -60,7 +60,7 @@ def __init__(self): zip_file_name = utilities.incrementing_filename(filename=f'{self.data_dir}/logs/transcript', extension='zip') zip_params = { 'task_type': task_queue.TaskQueueEnum.ZIP_TASK, - 'folder_path': './logs', + 'folder_path': f'{self.data_dir}/logs', 'zip_file_name': zip_file_name, 'skip_zip_files': True } diff --git a/app/transcribe/main.py b/app/transcribe/main.py index b06b3cb..4acc34a 100644 --- a/app/transcribe/main.py +++ b/app/transcribe/main.py @@ -5,6 +5,7 @@ import customtkinter as ctk from args import create_args, update_args_config, handle_args_batch_tasks from global_vars import T_GLOBALS +from appui import AppUI sys.path.append('../..') import ui # noqa: E402 pylint: disable=C0413 from tsutils import configuration # noqa: E402 pylint: disable=C0413 @@ -66,66 +67,69 @@ def main(): # Initiate logging log_listener = al.initiate_log(config=config) - root = ctk.CTk() - T_GLOBALS.main_window = root - ui_cb = ui.UICallbacks() - ui_components = ui.create_ui_components(root, config=config) - global_vars.transcript_textbox = ui_components[0] - global_vars.response_textbox = ui_components[1] - update_interval_slider = ui_components[2] - global_vars.update_interval_slider_label = ui_components[3] - global_vars.freeze_button = ui_components[4] - audio_lang_combobox = ui_components[5] - response_lang_combobox = ui_components[6] - global_vars.filemenu = ui_components[7] - response_now_button = ui_components[8] - read_response_now_button = ui_components[9] - global_vars.editmenu = ui_components[10] - github_link = ui_components[11] - issue_link = ui_components[12] - summarize_button = ui_components[13] + aui = AppUI(config=config) + # root = ctk.CTk() + # T_GLOBALS.main_window = root + # ui_cb = ui.UICallbacks() + # ui_components = ui.create_ui_components(root, config=config) + # global_vars.transcript_textbox = ui_components[0] + # global_vars.response_textbox = ui_components[1] + # update_interval_slider = ui_components[2] + # global_vars.update_interval_slider_label = ui_components[3] + # global_vars.freeze_button = ui_components[4] + # audio_lang_combobox = ui_components[5] + # response_lang_combobox = ui_components[6] + # global_vars.filemenu = ui_components[7] + # response_now_button = ui_components[8] + # read_response_now_button = ui_components[9] + # global_vars.editmenu = ui_components[10] + # github_link = ui_components[11] + # issue_link = ui_components[12] + # summarize_button = ui_components[13] # disable speaker/microphone on startup - if config['General']['disable_speaker']: - print('[INFO] Disabling Speaker') - ui_cb.enable_disable_speaker(global_vars.editmenu) + # if config['General']['disable_speaker']: + # print('[INFO] Disabling Speaker') + # ui_cb.enable_disable_speaker(global_vars.editmenu) - if config['General']['disable_mic']: - print('[INFO] Disabling Microphone') - ui_cb.enable_disable_microphone(global_vars.editmenu) + # if config['General']['disable_mic']: + # print('[INFO] Disabling Microphone') + # ui_cb.enable_disable_microphone(global_vars.editmenu) au.initiate_app_threads(global_vars=global_vars, config=config) print("READY") - root.grid_rowconfigure(0, weight=100) - root.grid_rowconfigure(1, weight=1) - root.grid_rowconfigure(2, weight=1) - root.grid_rowconfigure(3, weight=1) - root.grid_columnconfigure(0, weight=2) - root.grid_columnconfigure(1, weight=1) - - global_vars.freeze_button.configure(command=ui_cb.freeze_unfreeze) - response_now_button.configure(command=ui_cb.get_response_now) - read_response_now_button.configure(command=ui_cb.update_response_ui_and_read_now) - summarize_button.configure(command=ui_cb.summarize) - update_interval_slider.configure(command=ui_cb.update_interval_slider_value) - label_text = f'LLM Response interval: {int(update_interval_slider.get())} seconds' - global_vars.update_interval_slider_label.configure(text=label_text) - audio_lang_combobox.configure(command=ui_cb.set_audio_language) - response_lang_combobox.configure(command=ui_cb.set_response_language) + # root.grid_rowconfigure(0, weight=100) + # root.grid_rowconfigure(1, weight=1) + # root.grid_rowconfigure(2, weight=1) + # root.grid_rowconfigure(3, weight=1) + # root.grid_columnconfigure(0, weight=2) + # root.grid_columnconfigure(1, weight=1) + + # global_vars.freeze_button.configure(command=ui_cb.freeze_unfreeze) + # response_now_button.configure(command=ui_cb.get_response_now) + # read_response_now_button.configure(command=ui_cb.update_response_ui_and_read_now) + # summarize_button.configure(command=ui_cb.summarize) + # update_interval_slider.configure(command=ui_cb.update_interval_slider_value) + # label_text = f'LLM Response interval: {int(update_interval_slider.get())} seconds' + # global_vars.update_interval_slider_label.configure(text=label_text) + # audio_lang_combobox.configure(command=ui_cb.set_audio_language) + # response_lang_combobox.configure(command=ui_cb.set_response_language) # Set the response lang in STT Model. global_vars.transcriber.stt_model.set_lang(config['OpenAI']['audio_lang']) - github_link.bind('', lambda e: - ui_cb.open_link('https://github.com/vivekuppal/transcribe?referer=desktop')) - issue_link.bind('', lambda e: ui_cb.open_link( - 'https://github.com/vivekuppal/transcribe/issues/new?referer=desktop')) - - ui.update_transcript_ui(global_vars.transcriber, global_vars.transcript_textbox) - ui.update_response_ui(global_vars.responder, global_vars.response_textbox, - global_vars.update_interval_slider_label, update_interval_slider) - - root.mainloop() + # github_link.bind('', lambda e: + # ui_cb.open_link('https://github.com/vivekuppal/transcribe?referer=desktop')) + # issue_link.bind('', lambda e: ui_cb.open_link( + # 'https://github.com/vivekuppal/transcribe/issues/new?referer=desktop')) + + # ui.update_transcript_ui(global_vars.transcriber, global_vars.transcript_textbox) + # ui.update_response_ui(global_vars.responder, global_vars.response_textbox, + # global_vars.update_interval_slider_label, update_interval_slider) + + aui.update_initial_transcripts() + aui.start() + # root.mainloop() log_listener.stop() From 1ae92a81ce4753bc6e15fc327446f6cb6dcca5c1 Mon Sep 17 00:00:00 2001 From: vivek Date: Thu, 23 May 2024 19:38:59 -0400 Subject: [PATCH 06/19] Reduce vars in global context. make current ui completely obsoleter. --- app/transcribe/appui.py | 14 +- app/transcribe/global_vars.py | 12 +- app/transcribe/main.py | 4 +- app/transcribe/ui.py | 1242 ++++++++++++++++----------------- 4 files changed, 636 insertions(+), 636 deletions(-) diff --git a/app/transcribe/appui.py b/app/transcribe/appui.py index c10389e..b9c4ac0 100644 --- a/app/transcribe/appui.py +++ b/app/transcribe/appui.py @@ -371,11 +371,11 @@ def update_response_ui_threaded(self, response_generator): # Set event to play the recording audio if required if self.global_vars.read_response: self.global_vars.audio_player_var.speech_text_available.set() - self.global_vars.response_textbox.configure(state="normal") + self.response_textbox.configure(state="normal") if response_string: - write_in_textbox(self.global_vars.response_textbox, response_string) - self.global_vars.response_textbox.configure(state="disabled") - self.global_vars.response_textbox.see("end") + write_in_textbox(self.response_textbox, response_string) + self.response_textbox.configure(state="disabled") + self.response_textbox.see("end") except Exception as e: logger.error(f"Error in threaded response: {e}") @@ -390,7 +390,7 @@ def get_response_selected_now(self): # streamed back. Without the thread UI appears stuck as we stream the # responses back self.capture_action('Get LLM response selected now') - selected_text = self.global_vars.transcript_textbox.selection_get() + selected_text = self.transcript_textbox.selection_get() response_ui_thread = threading.Thread(target=self.get_response_selected_now_threaded, args=(selected_text,), name='GetResponseSelectedNow') @@ -453,9 +453,9 @@ def set_transcript_state(self): self.global_vars.transcriber.transcribe = not self.global_vars.transcriber.transcribe self.capture_action(f'{"Enabled " if self.global_vars.transcriber.transcribe else "Disabled "} transcription.') if self.global_vars.transcriber.transcribe: - self.global_vars.filemenu.entryconfigure(1, label="Pause Transcription") + self.filemenu.entryconfigure(1, label="Pause Transcription") else: - self.global_vars.filemenu.entryconfigure(1, label="Start Transcription") + self.filemenu.entryconfigure(1, label="Start Transcription") except Exception as e: logger.error(f"Error setting transcript state: {e}") diff --git a/app/transcribe/global_vars.py b/app/transcribe/global_vars.py index c7652f5..5120bc8 100644 --- a/app/transcribe/global_vars.py +++ b/app/transcribe/global_vars.py @@ -26,16 +26,16 @@ class TranscriptionGlobals(Singleton.Singleton): transcriber: AudioTranscriber = None # Global for responses from openAI API responder = None - freeze_button: ctk.CTkButton = None + # freeze_button: ctk.CTkButton = None # Update_response_now is true when we are waiting for a one time immediate response to query update_response_now: bool = False # Read response in voice read_response: bool = False - editmenu: tk.Menu = None - filemenu: tk.Menu = None - update_interval_slider_label: ctk.CTkLabel = None - response_textbox: ctk.CTkTextbox = None - transcript_textbox: ctk.CTkTextbox = None + # editmenu: tk.Menu = None + # filemenu: tk.Menu = None + # update_interval_slider_label: ctk.CTkLabel = None + # response_textbox: ctk.CTkTextbox = None + # transcript_textbox: ctk.CTkTextbox = None start: datetime.datetime = None task_worker = None main_window = None diff --git a/app/transcribe/main.py b/app/transcribe/main.py index 4acc34a..0749fd3 100644 --- a/app/transcribe/main.py +++ b/app/transcribe/main.py @@ -2,12 +2,12 @@ import time import atexit import app_utils as au -import customtkinter as ctk +# import customtkinter as ctk from args import create_args, update_args_config, handle_args_batch_tasks from global_vars import T_GLOBALS from appui import AppUI sys.path.append('../..') -import ui # noqa: E402 pylint: disable=C0413 +# import ui # noqa: E402 pylint: disable=C0413 from tsutils import configuration # noqa: E402 pylint: disable=C0413 from tsutils import app_logging as al # noqa: E402 pylint: disable=C0413 from tsutils import utilities as u # noqa: E402 pylint: disable=C0413 diff --git a/app/transcribe/ui.py b/app/transcribe/ui.py index 79e7805..910c62e 100644 --- a/app/transcribe/ui.py +++ b/app/transcribe/ui.py @@ -1,623 +1,623 @@ -import threading -import datetime -import time -import tkinter as tk -import webbrowser -import pyperclip -import customtkinter as ctk -from tktooltip import ToolTip -from audio_transcriber import AudioTranscriber -import prompts -from global_vars import TranscriptionGlobals, T_GLOBALS -import constants -import gpt_responder as gr -from tsutils.language import LANGUAGES_DICT -from tsutils import utilities -from tsutils import app_logging as al -from tsutils import configuration - - -logger = al.get_module_logger(al.UI_LOGGER) -UI_FONT_SIZE = 20 +# import threading +# import datetime +# import time +# import tkinter as tk +# import webbrowser +# import pyperclip +# import customtkinter as ctk +# from tktooltip import ToolTip +# from audio_transcriber import AudioTranscriber +# import prompts +# from global_vars import TranscriptionGlobals, T_GLOBALS +# import constants +# import gpt_responder as gr +# from tsutils.language import LANGUAGES_DICT +# from tsutils import utilities +# from tsutils import app_logging as al +# from tsutils import configuration + + +# logger = al.get_module_logger(al.UI_LOGGER) +# UI_FONT_SIZE = 20 # Order of initialization can be unpredictable in python based on where imports are placed. # Setting it to None so comparison is deterministic in update_transcript_ui method -last_transcript_ui_update_time: datetime.datetime = None -global_vars_module: TranscriptionGlobals = T_GLOBALS -pop_up = None - - -class UICallbacks: - """All callbacks for UI""" - - global_vars: TranscriptionGlobals - ui_filename: str = None - - def __init__(self): - self.global_vars = TranscriptionGlobals() - - def copy_to_clipboard(self): - """Copy transcription text data to clipboard. - Does not include responses from assistant. - """ - logger.info(UICallbacks.copy_to_clipboard.__name__) - self.capture_action("Copy transcript to clipboard") - try: - pyperclip.copy(self.global_vars.transcriber.get_transcript()) - except Exception as e: - logger.error(f"Error copying to clipboard: {e}") - - def save_file(self): - """Save transcription text data to file. - Does not include responses from assistant. - """ - logger.info(UICallbacks.save_file.__name__) - filename = ctk.filedialog.asksaveasfilename(defaultextension='.txt', - title='Save Transcription', - filetypes=[("Text Files", "*.txt")]) - self.capture_action(f'Save transcript to file:{filename}') - if not filename: - return - try: - with open(file=filename, mode="w", encoding='utf-8') as file_handle: - file_handle.write(self.global_vars.transcriber.get_transcript()) - except Exception as e: - logger.error(f"Error saving file {filename}: {e}") - - def freeze_unfreeze(self): - """Respond to start / stop of seeking responses from openAI API - """ - logger.info(UICallbacks.freeze_unfreeze.__name__) - try: - # Invert the state - self.global_vars.responder.enabled = not self.global_vars.responder.enabled - self.capture_action(f'{"Enabled " if self.global_vars.responder.enabled else "Disabled "} continuous LLM responses') - self.global_vars.freeze_button.configure( - text="Suggest Responses Continuously" if not self.global_vars.responder.enabled else "Do Not Suggest Responses Continuously" - ) - except Exception as e: - logger.error(f"Error toggling responder state: {e}") - - def enable_disable_speaker(self, editmenu): - """Toggles the state of speaker - """ - try: - self.global_vars.speaker_audio_recorder.enabled = not self.global_vars.speaker_audio_recorder.enabled - editmenu.entryconfigure(2, label="Disable Speaker" if self.global_vars.speaker_audio_recorder.enabled else "Enable Speaker") - self.capture_action(f'{"Enabled " if self.global_vars.speaker_audio_recorder.enabled else "Disabled "} speaker input') - except Exception as e: - logger.error(f"Error toggling speaker state: {e}") - - def enable_disable_microphone(self, editmenu): - """Toggles the state of microphone - """ - try: - self.global_vars.user_audio_recorder.enabled = not self.global_vars.user_audio_recorder.enabled - editmenu.entryconfigure(3, label="Disable Microphone" if self.global_vars.user_audio_recorder.enabled else "Enable Microphone") - self.capture_action(f'{"Enabled " if self.global_vars.user_audio_recorder.enabled else "Disabled "} microphone input') - except Exception as e: - logger.error(f"Error toggling microphone state: {e}") - - def update_interval_slider_value(self, slider_value): - """Update interval slider label to match the slider value - Update the config value - """ - try: - config_obj = configuration.Config() - # Save config - altered_config = {'General': {'llm_response_interval': int(slider_value)}} - config_obj.add_override_value(altered_config) - - label_text = f'LLM Response interval: {int(slider_value)} seconds' - self.global_vars.update_interval_slider_label.configure(text=label_text) - self.capture_action(f'Update LLM response interval to {int(slider_value)}') - except Exception as e: - logger.error(f"Error updating slider value: {e}") - - def get_response_now(self): - """Get response from LLM right away - Update the Response UI with the response - """ - if self.global_vars.update_response_now: - # We are already in the middle of getting a response - return - # We need a separate thread to ensure UI is responsive as responses are - # streamed back. Without the thread UI appears stuck as we stream the - # responses back - self.capture_action('Get LLM response now') - response_ui_thread = threading.Thread(target=self.get_response_now_threaded, - name='GetResponseNow') - response_ui_thread.daemon = True - response_ui_thread.start() - - def get_response_selected_now_threaded(self, text: str): - """Update response UI in a separate thread - """ - self.update_response_ui_threaded(lambda: self.global_vars.responder.generate_response_for_selected_text(text)) - - def get_response_now_threaded(self): - """Update response UI in a separate thread - """ - self.update_response_ui_threaded(self.global_vars.responder.generate_response_from_transcript_no_check) - - def update_response_ui_threaded(self, response_generator): - """Helper method to update response UI in a separate thread - """ - try: - self.global_vars.update_response_now = True - response_string = response_generator() - self.global_vars.update_response_now = False - # Set event to play the recording audio if required - if self.global_vars.read_response: - self.global_vars.audio_player_var.speech_text_available.set() - self.global_vars.response_textbox.configure(state="normal") - if response_string: - write_in_textbox(self.global_vars.response_textbox, response_string) - self.global_vars.response_textbox.configure(state="disabled") - self.global_vars.response_textbox.see("end") - except Exception as e: - logger.error(f"Error in threaded response: {e}") - - def get_response_selected_now(self): - """Get response from LLM right away for selected_text - Update the Response UI with the response - """ - if self.global_vars.update_response_now: - # We are already in the middle of getting a response - return - # We need a separate thread to ensure UI is responsive as responses are - # streamed back. Without the thread UI appears stuck as we stream the - # responses back - self.capture_action('Get LLM response selected now') - selected_text = self.global_vars.transcript_textbox.selection_get() - response_ui_thread = threading.Thread(target=self.get_response_selected_now_threaded, - args=(selected_text,), - name='GetResponseSelectedNow') - response_ui_thread.daemon = True - response_ui_thread.start() - - def summarize_threaded(self): - """Get summary from LLM in a separate thread""" - global pop_up # pylint: disable=W0603 - try: - print('Summarizing...') - popup_msg_no_close(title='Summary', msg='Creating a summary') - summary = self.global_vars.responder.summarize() - # When API key is not specified, give a chance for the thread to initialize - - if pop_up is not None: - try: - pop_up.destroy() - except Exception as e: - # Somehow we get the exception - # RuntimeError: main thread is not in main loop - logger.info('Exception in summarize_threaded') - logger.info(e) - - pop_up = None - if summary is None: - popup_msg_close_button(title='Summary', - msg='Failed to get summary. Please check you have a valid API key.') - return - - # Enhancement here would be to get a streaming summary - popup_msg_close_button(title='Summary', msg=summary) - except Exception as e: - logger.error(f"Error in summarize_threaded: {e}") - - def summarize(self): - """Get summary response from LLM - """ - self.capture_action('Get summary from LLM') - summarize_ui_thread = threading.Thread(target=self.summarize_threaded, - name='Summarize') - summarize_ui_thread.daemon = True - summarize_ui_thread.start() - - def update_response_ui_and_read_now(self): - """Get response from LLM right away - Update the Response UI with the response - Read the response - """ - self.capture_action('Get LLM response now and read aloud') - self.global_vars.set_read_response(True) - self.get_response_now() - - def set_transcript_state(self): - """Enables, disables transcription. - Text of menu item File -> Pause Transcription toggles accordingly - """ - logger.info(UICallbacks.set_transcript_state.__name__) - try: - self.global_vars.transcriber.transcribe = not self.global_vars.transcriber.transcribe - self.capture_action(f'{"Enabled " if self.global_vars.transcriber.transcribe else "Disabled "} transcription.') - if self.global_vars.transcriber.transcribe: - self.global_vars.filemenu.entryconfigure(1, label="Pause Transcription") - else: - self.global_vars.filemenu.entryconfigure(1, label="Start Transcription") - except Exception as e: - logger.error(f"Error setting transcript state: {e}") - - def open_link(self, url: str): - """Open the link in a web browser - """ - self.capture_action(f'Navigate to {url}.') - try: - webbrowser.open(url=url, new=2) - except Exception as e: - logger.error(f"Error opening URL {url}: {e}") - - def open_github(self): - """Link to git repo main page - """ - self.capture_action('open_github.') - self.open_link('https://github.com/vivekuppal/transcribe?referer=desktop') - - def open_support(self): - """Link to git repo issues page - """ - self.capture_action('open_support.') - self.open_link('https://github.com/vivekuppal/transcribe/issues/new?referer=desktop') - - def capture_action(self, action_text: str): - """Write to file - """ - try: - if not self.ui_filename: - data_dir = utilities.get_data_path(app_name='Transcribe') - self.ui_filename = utilities.incrementing_filename(filename=f'{data_dir}/logs/ui', extension='txt') - with open(self.ui_filename, mode='a', encoding='utf-8') as ui_file: - ui_file.write(f'{datetime.datetime.now()}: {action_text}\n') - except Exception as e: - logger.error(f"Error capturing action {action_text}: {e}") - - def set_audio_language(self, lang: str): - """Alter audio language in memory and persist it in config file - """ - try: - self.global_vars.transcriber.stt_model.set_lang(lang) - config_obj = configuration.Config() - # Save config - altered_config = {'OpenAI': {'audio_lang': lang}} - config_obj.add_override_value(altered_config) - except Exception as e: - logger.error(f"Error setting audio language: {e}") - - def set_response_language(self, lang: str): - """Alter response language in memory and persist it in config file - """ - try: - config_obj = configuration.Config() - altered_config = {'OpenAI': {'response_lang': lang}} - # Save config - config_obj.add_override_value(altered_config) - config_data = config_obj.data - - # Create a new system prompt - prompt = config_data["General"]["system_prompt"] - response_lang = config_data["OpenAI"]["response_lang"] - if response_lang is not None: - prompt += f'. Respond exclusively in {response_lang}.' - convo = self.global_vars.convo - convo.update_conversation(persona=constants.PERSONA_SYSTEM, - text=prompt, - time_spoken=datetime.datetime.utcnow(), - update_previous=True) - except Exception as e: - logger.error(f"Error setting response language: {e}") - - -def popup_msg_no_close_threaded(title, msg): - """Create a pop up with no close button. - """ - global pop_up # pylint: disable=W0603 - try: - popup = ctk.CTkToplevel(T_GLOBALS.main_window) - popup.geometry("100x50") - popup.title(title) - label = ctk.CTkLabel(popup, text=msg, font=("Arial", 12), - text_color="#FFFCF2") - label.pack(side="top", fill="x", pady=10) - pop_up = popup - popup.lift() - except Exception as e: - # Sometimes get the error - calling Tcl from different apartment - logger.info('Exception in popup_msg_no_close_threaded') - logger.info(e) - return - - -def popup_msg_no_close(title: str, msg: str): - """Create a popup that the caller is responsible for closing - using the destroy method - """ - kwargs = {} - kwargs['title'] = title - kwargs['msg'] = msg - pop_ui_thread = threading.Thread(target=popup_msg_no_close_threaded, - name='Pop up thread', - kwargs=kwargs) - pop_ui_thread.daemon = True - pop_ui_thread.start() - # Give a chance for the thread to initialize - # When API key is not specified, need the thread to initialize to - # allow summarize window to show and ultimately be closed. - time.sleep(0.1) - - -def popup_msg_close_button(title: str, msg: str): - """Create a popup that the caller is responsible for closing - using the destroy method - """ - popup = ctk.CTkToplevel(T_GLOBALS.main_window) - popup.geometry("380x710") - popup.title(title) - txtbox = ctk.CTkTextbox(popup, width=350, height=600, font=("Arial", UI_FONT_SIZE), - text_color='#FFFCF2', wrap="word") - txtbox.grid(row=0, column=0, padx=10, pady=10, sticky="nsew") - txtbox.insert("0.0", msg) - - def copy_summary_to_clipboard(): - pyperclip.copy(txtbox.cget("text")) - - copy_button = ctk.CTkButton(popup, text="Copy to Clipboard", command=copy_summary_to_clipboard) - copy_button.grid(row=1, column=0, padx=10, pady=5, sticky="nsew") - - close_button = ctk.CTkButton(popup, text="Close", command=popup.destroy) - close_button.grid(row=2, column=0, padx=10, pady=5, sticky="nsew") - popup.lift() - - -def write_in_textbox(textbox: ctk.CTkTextbox, text: str): - """Update the text of textbox with the given text - Args: - textbox: textbox to be updated - text: updated text - """ - # Get current selection attributes, so they can be preserved after writing new text - a: tuple = textbox.tag_ranges('sel') - # (, ) - textbox.delete("0.0", "end") - textbox.insert("0.0", text) - if len(a): - textbox.tag_add('sel', a[0], a[1]) - - -def update_transcript_ui(transcriber: AudioTranscriber, textbox: ctk.CTkTextbox): - """Update the text of transcription textbox with the given text - Args: - transcriber: AudioTranscriber Object - textbox: textbox to be updated - """ - - global last_transcript_ui_update_time # pylint: disable=W0603 - global global_vars_module # pylint: disable=W0603 - - if global_vars_module is None: - global_vars_module = TranscriptionGlobals() - - # None comparison is for initialization - if last_transcript_ui_update_time is None or last_transcript_ui_update_time < global_vars_module.convo.last_update: - transcript_string = transcriber.get_transcript() - write_in_textbox(textbox, transcript_string) - textbox.see("end") - last_transcript_ui_update_time = datetime.datetime.utcnow() - - textbox.after(constants.TRANSCRIPT_UI_UPDATE_DELAY_DURATION_MS, - update_transcript_ui, transcriber, textbox) - - -def update_response_ui(responder: gr.GPTResponder, - textbox: ctk.CTkTextbox, - update_interval_slider_label: ctk.CTkLabel, - update_interval_slider: ctk.CTkSlider): - """Update the text of response textbox with the given text - Args: - textbox: textbox to be updated - text: updated text - """ - global global_vars_module # pylint: disable=W0603 - - if global_vars_module is None: - global_vars_module = TranscriptionGlobals() - - # global_vars_module.responder.enabled --> This is continous response mode from LLM - # global_vars_module.update_response_now --> Get Response now from LLM - if global_vars_module.responder.enabled or global_vars_module.update_response_now: - response = responder.response - - textbox.configure(state="normal") - write_in_textbox(textbox, response) - textbox.configure(state="disabled") - textbox.see("end") - - update_interval = int(update_interval_slider.get()) - # responder.update_response_interval(update_interval) - update_interval_slider_label.configure(text=f'LLM Response interval: ' - f'{update_interval} seconds') - - textbox.after(300, update_response_ui, responder, textbox, - update_interval_slider_label, update_interval_slider) - - -def create_ui_components(root, config: dict): - """Create UI for the application - """ - logger.info(create_ui_components.__name__) - ctk.set_appearance_mode("dark") - ctk.set_default_color_theme("dark-blue") - root.title("Transcribe") - root.configure(bg='#252422') - root.geometry("1000x600") - - ui_cb = UICallbacks() - global_vars = TranscriptionGlobals() - - # Create the menu bar - menubar = tk.Menu(root) - - # Create a file menu - filemenu = tk.Menu(menubar, tearoff=False) - - # Add a "Save" menu item to the file menu - filemenu.add_command(label="Save Transcript to File", command=ui_cb.save_file) - - # Add a "Pause" menu item to the file menu - filemenu.add_command(label="Pause Transcription", command=ui_cb.set_transcript_state) - - # Add a "Quit" menu item to the file menu - filemenu.add_command(label="Quit", command=root.quit) - - # Add the file menu to the menu bar - menubar.add_cascade(label="File", menu=filemenu) - - # Create an edit menu - editmenu = tk.Menu(menubar, tearoff=False) - - # Add a "Clear Audio Transcript" menu item to the file menu - editmenu.add_command(label="Clear Audio Transcript", command=lambda: - global_vars.transcriber.clear_transcriber_context(global_vars.audio_queue)) - - # Add a "Copy To Clipboard" menu item to the file menu - editmenu.add_command(label="Copy Transcript to Clipboard", command=ui_cb.copy_to_clipboard) - - # Add "Disable Speaker" menu item to file menu - editmenu.add_command(label="Disable Speaker", command=lambda: ui_cb.enable_disable_speaker(editmenu)) - - # Add "Disable Microphone" menu item to file menu - editmenu.add_command(label="Disable Microphone", command=lambda: ui_cb.enable_disable_microphone(editmenu)) - - # Add the edit menu to the menu bar - menubar.add_cascade(label="Edit", menu=editmenu) - - # Create help menu, add items in help menu - helpmenu = tk.Menu(menubar, tearoff=False) - helpmenu.add_command(label="Github Repo", command=ui_cb.open_github) - helpmenu.add_command(label="Star the Github repo", command=ui_cb.open_github) - helpmenu.add_command(label="Report an Issue", command=ui_cb.open_support) - menubar.add_cascade(label="Help", menu=helpmenu) - - # Add the menu bar to the main window - root.config(menu=menubar) - - # Speech to Text textbox - transcript_textbox = ctk.CTkTextbox(root, width=300, font=("Arial", UI_FONT_SIZE), - text_color='#FFFCF2', wrap="word") - transcript_textbox.grid(row=0, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") - - # LLM Response textbox - response_textbox = ctk.CTkTextbox(root, width=300, font=("Arial", UI_FONT_SIZE), - text_color='#639cdc', wrap="word") - response_textbox.grid(row=0, column=2, padx=10, pady=3, sticky="nsew") - response_textbox.insert("0.0", prompts.INITIAL_RESPONSE) - - response_enabled = bool(config['General']['continuous_response']) - b_text = "Suggest Responses Continuously" if not response_enabled else "Do Not Suggest Responses Continuously" - continuous_response_button = ctk.CTkButton(root, text=b_text) - continuous_response_button.grid(row=1, column=2, padx=10, pady=3, sticky="nsew") - - response_now_button = ctk.CTkButton(root, text="Suggest Response Now") - response_now_button.grid(row=2, column=2, padx=10, pady=3, sticky="nsew") - - read_response_now_button = ctk.CTkButton(root, text="Suggest Response and Read") - read_response_now_button.grid(row=3, column=2, padx=10, pady=3, sticky="nsew") - - summarize_button = ctk.CTkButton(root, text="Summarize") - summarize_button.grid(row=4, column=2, padx=10, pady=3, sticky="nsew") - - # Continuous LLM Response label, and slider - update_interval_slider_label = ctk.CTkLabel(root, text="", font=("Arial", 12), - text_color="#FFFCF2") - update_interval_slider_label.grid(row=1, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") - - update_interval_slider = ctk.CTkSlider(root, from_=1, to=30, width=300, # height=5, - number_of_steps=29) - update_interval_slider.set(config['General']['llm_response_interval']) - update_interval_slider.grid(row=2, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") - - # Speech to text language selection label, dropdown - audio_lang_label = ctk.CTkLabel(root, text="Audio Lang: ", - font=("Arial", 12), - text_color="#FFFCF2") - audio_lang_label.grid(row=3, column=0, padx=10, pady=3, sticky="nw") - - audio_lang = config['OpenAI']['audio_lang'] - audio_lang_combobox = ctk.CTkOptionMenu(root, width=15, values=list(LANGUAGES_DICT.values())) - audio_lang_combobox.set(audio_lang) - audio_lang_combobox.grid(row=3, column=0, ipadx=60, padx=10, pady=3, sticky="ne") - - # LLM Response language selection label, dropdown - response_lang_label = ctk.CTkLabel(root, - text="Response Lang: ", - font=("Arial", 12), text_color="#FFFCF2") - response_lang_label.grid(row=3, column=1, padx=10, pady=3, sticky="nw") - - response_lang = config['OpenAI']['response_lang'] - response_lang_combobox = ctk.CTkOptionMenu(root, width=15, values=list(LANGUAGES_DICT.values())) - response_lang_combobox.set(response_lang) - response_lang_combobox.grid(row=3, column=1, ipadx=60, padx=10, pady=3, sticky="ne") - - github_link = ctk.CTkLabel(root, text="Star the Github Repo", - text_color="#639cdc", cursor="hand2") - github_link.grid(row=4, column=0, padx=10, pady=3, sticky="wn") - - issue_link = ctk.CTkLabel(root, text="Report an issue", text_color="#639cdc", cursor="hand2") - issue_link.grid(row=4, column=1, padx=10, pady=3, sticky="wn") - - # Create right click menu for transcript textbox. - # This displays only inside the speech to text textbox - m = tk.Menu(root, tearoff=0) - m.add_command(label="Generate response for selected text", - command=ui_cb.get_response_selected_now) - m.add_command(label="Save Transcript to File", command=ui_cb.save_file) - m.add_command(label="Clear Audio Transcript", command=lambda: - global_vars.transcriber.clear_transcriber_context(global_vars.audio_queue)) - m.add_command(label="Copy Transcript to Clipboard", command=ui_cb.copy_to_clipboard) - m.add_separator() - m.add_command(label="Quit", command=root.quit) - - chat_inference_provider = config['General']['chat_inference_provider'] - if chat_inference_provider == 'openai': - api_key = config['OpenAI']['api_key'] - base_url = config['OpenAI']['base_url'] - model = config['OpenAI']['ai_model'] - elif chat_inference_provider == 'together': - api_key = config['Together']['api_key'] - base_url = config['Together']['base_url'] - model = config['Together']['ai_model'] - - if not utilities.is_api_key_valid(api_key=api_key, base_url=base_url, model=model): - # Disable buttons that interact with backend services - continuous_response_button.configure(state='disabled') - response_now_button.configure(state='disabled') - read_response_now_button.configure(state='disabled') - summarize_button.configure(state='disabled') - - tt_msg = 'Add API Key in override.yaml to enable button' - # Add tooltips for disabled buttons - ToolTip(continuous_response_button, msg=tt_msg, - delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, - padx=7, pady=7) - ToolTip(response_now_button, msg=tt_msg, - delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, - padx=7, pady=7) - ToolTip(read_response_now_button, msg=tt_msg, - delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, - padx=7, pady=7) - ToolTip(summarize_button, msg=tt_msg, - delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, - padx=7, pady=7) - - def show_context_menu(event): - try: - m.tk_popup(event.x_root, event.y_root) - finally: - m.grab_release() - - transcript_textbox.bind("", show_context_menu) - - # Order of returned components is important. - # Add new components to the end - return [transcript_textbox, response_textbox, update_interval_slider, - update_interval_slider_label, continuous_response_button, - audio_lang_combobox, response_lang_combobox, filemenu, response_now_button, - read_response_now_button, editmenu, github_link, issue_link, summarize_button] +# last_transcript_ui_update_time: datetime.datetime = None +# global_vars_module: TranscriptionGlobals = T_GLOBALS +# pop_up = None + + +# class UICallbacks: +# """All callbacks for UI""" + +# global_vars: TranscriptionGlobals +# ui_filename: str = None + +# def __init__(self): +# self.global_vars = TranscriptionGlobals() + +# def copy_to_clipboard(self): +# """Copy transcription text data to clipboard. +# Does not include responses from assistant. +# """ +# logger.info(UICallbacks.copy_to_clipboard.__name__) +# self.capture_action("Copy transcript to clipboard") +# try: +# pyperclip.copy(self.global_vars.transcriber.get_transcript()) +# except Exception as e: +# logger.error(f"Error copying to clipboard: {e}") + +# def save_file(self): +# """Save transcription text data to file. +# Does not include responses from assistant. +# """ +# logger.info(UICallbacks.save_file.__name__) +# filename = ctk.filedialog.asksaveasfilename(defaultextension='.txt', +# title='Save Transcription', +# filetypes=[("Text Files", "*.txt")]) +# self.capture_action(f'Save transcript to file:{filename}') +# if not filename: +# return +# try: +# with open(file=filename, mode="w", encoding='utf-8') as file_handle: +# file_handle.write(self.global_vars.transcriber.get_transcript()) +# except Exception as e: +# logger.error(f"Error saving file {filename}: {e}") + +# def freeze_unfreeze(self): +# """Respond to start / stop of seeking responses from openAI API +# """ +# logger.info(UICallbacks.freeze_unfreeze.__name__) +# try: +# # Invert the state +# self.global_vars.responder.enabled = not self.global_vars.responder.enabled +# self.capture_action(f'{"Enabled " if self.global_vars.responder.enabled else "Disabled "} continuous LLM responses') +# self.freeze_button.configure( +# text="Suggest Responses Continuously" if not self.global_vars.responder.enabled else "Do Not Suggest Responses Continuously" +# ) +# except Exception as e: +# logger.error(f"Error toggling responder state: {e}") + +# def enable_disable_speaker(self, editmenu): +# """Toggles the state of speaker +# """ +# try: +# self.global_vars.speaker_audio_recorder.enabled = not self.global_vars.speaker_audio_recorder.enabled +# editmenu.entryconfigure(2, label="Disable Speaker" if self.global_vars.speaker_audio_recorder.enabled else "Enable Speaker") +# self.capture_action(f'{"Enabled " if self.global_vars.speaker_audio_recorder.enabled else "Disabled "} speaker input') +# except Exception as e: +# logger.error(f"Error toggling speaker state: {e}") + +# def enable_disable_microphone(self, editmenu): +# """Toggles the state of microphone +# """ +# try: +# self.global_vars.user_audio_recorder.enabled = not self.global_vars.user_audio_recorder.enabled +# editmenu.entryconfigure(3, label="Disable Microphone" if self.global_vars.user_audio_recorder.enabled else "Enable Microphone") +# self.capture_action(f'{"Enabled " if self.global_vars.user_audio_recorder.enabled else "Disabled "} microphone input') +# except Exception as e: +# logger.error(f"Error toggling microphone state: {e}") + +# def update_interval_slider_value(self, slider_value): +# """Update interval slider label to match the slider value +# Update the config value +# """ +# try: +# config_obj = configuration.Config() +# # Save config +# altered_config = {'General': {'llm_response_interval': int(slider_value)}} +# config_obj.add_override_value(altered_config) + +# label_text = f'LLM Response interval: {int(slider_value)} seconds' +# self.global_vars.update_interval_slider_label.configure(text=label_text) +# self.capture_action(f'Update LLM response interval to {int(slider_value)}') +# except Exception as e: +# logger.error(f"Error updating slider value: {e}") + +# def get_response_now(self): +# """Get response from LLM right away +# Update the Response UI with the response +# """ +# if self.global_vars.update_response_now: +# # We are already in the middle of getting a response +# return +# # We need a separate thread to ensure UI is responsive as responses are +# # streamed back. Without the thread UI appears stuck as we stream the +# # responses back +# self.capture_action('Get LLM response now') +# response_ui_thread = threading.Thread(target=self.get_response_now_threaded, +# name='GetResponseNow') +# response_ui_thread.daemon = True +# response_ui_thread.start() + +# def get_response_selected_now_threaded(self, text: str): +# """Update response UI in a separate thread +# """ +# self.update_response_ui_threaded(lambda: self.global_vars.responder.generate_response_for_selected_text(text)) + +# def get_response_now_threaded(self): +# """Update response UI in a separate thread +# """ +# self.update_response_ui_threaded(self.global_vars.responder.generate_response_from_transcript_no_check) + +# def update_response_ui_threaded(self, response_generator): +# """Helper method to update response UI in a separate thread +# """ +# try: +# self.global_vars.update_response_now = True +# response_string = response_generator() +# self.global_vars.update_response_now = False +# # Set event to play the recording audio if required +# if self.global_vars.read_response: +# self.global_vars.audio_player_var.speech_text_available.set() +# self.global_vars.response_textbox.configure(state="normal") +# if response_string: +# write_in_textbox(self.global_vars.response_textbox, response_string) +# self.global_vars.response_textbox.configure(state="disabled") +# self.global_vars.response_textbox.see("end") +# except Exception as e: +# logger.error(f"Error in threaded response: {e}") + +# def get_response_selected_now(self): +# """Get response from LLM right away for selected_text +# Update the Response UI with the response +# """ +# if self.global_vars.update_response_now: +# # We are already in the middle of getting a response +# return +# # We need a separate thread to ensure UI is responsive as responses are +# # streamed back. Without the thread UI appears stuck as we stream the +# # responses back +# self.capture_action('Get LLM response selected now') +# selected_text = self.global_vars.transcript_textbox.selection_get() +# response_ui_thread = threading.Thread(target=self.get_response_selected_now_threaded, +# args=(selected_text,), +# name='GetResponseSelectedNow') +# response_ui_thread.daemon = True +# response_ui_thread.start() + +# def summarize_threaded(self): +# """Get summary from LLM in a separate thread""" +# global pop_up # pylint: disable=W0603 +# try: +# print('Summarizing...') +# popup_msg_no_close(title='Summary', msg='Creating a summary') +# summary = self.global_vars.responder.summarize() +# # When API key is not specified, give a chance for the thread to initialize + +# if pop_up is not None: +# try: +# pop_up.destroy() +# except Exception as e: +# # Somehow we get the exception +# # RuntimeError: main thread is not in main loop +# logger.info('Exception in summarize_threaded') +# logger.info(e) + +# pop_up = None +# if summary is None: +# popup_msg_close_button(title='Summary', +# msg='Failed to get summary. Please check you have a valid API key.') +# return + +# # Enhancement here would be to get a streaming summary +# popup_msg_close_button(title='Summary', msg=summary) +# except Exception as e: +# logger.error(f"Error in summarize_threaded: {e}") + +# def summarize(self): +# """Get summary response from LLM +# """ +# self.capture_action('Get summary from LLM') +# summarize_ui_thread = threading.Thread(target=self.summarize_threaded, +# name='Summarize') +# summarize_ui_thread.daemon = True +# summarize_ui_thread.start() + +# def update_response_ui_and_read_now(self): +# """Get response from LLM right away +# Update the Response UI with the response +# Read the response +# """ +# self.capture_action('Get LLM response now and read aloud') +# self.global_vars.set_read_response(True) +# self.get_response_now() + +# def set_transcript_state(self): +# """Enables, disables transcription. +# Text of menu item File -> Pause Transcription toggles accordingly +# """ +# logger.info(UICallbacks.set_transcript_state.__name__) +# try: +# self.global_vars.transcriber.transcribe = not self.global_vars.transcriber.transcribe +# self.capture_action(f'{"Enabled " if self.global_vars.transcriber.transcribe else "Disabled "} transcription.') +# if self.global_vars.transcriber.transcribe: +# self.global_vars.filemenu.entryconfigure(1, label="Pause Transcription") +# else: +# self.global_vars.filemenu.entryconfigure(1, label="Start Transcription") +# except Exception as e: +# logger.error(f"Error setting transcript state: {e}") + +# def open_link(self, url: str): +# """Open the link in a web browser +# """ +# self.capture_action(f'Navigate to {url}.') +# try: +# webbrowser.open(url=url, new=2) +# except Exception as e: +# logger.error(f"Error opening URL {url}: {e}") + +# def open_github(self): +# """Link to git repo main page +# """ +# self.capture_action('open_github.') +# self.open_link('https://github.com/vivekuppal/transcribe?referer=desktop') + +# def open_support(self): +# """Link to git repo issues page +# """ +# self.capture_action('open_support.') +# self.open_link('https://github.com/vivekuppal/transcribe/issues/new?referer=desktop') + +# def capture_action(self, action_text: str): +# """Write to file +# """ +# try: +# if not self.ui_filename: +# data_dir = utilities.get_data_path(app_name='Transcribe') +# self.ui_filename = utilities.incrementing_filename(filename=f'{data_dir}/logs/ui', extension='txt') +# with open(self.ui_filename, mode='a', encoding='utf-8') as ui_file: +# ui_file.write(f'{datetime.datetime.now()}: {action_text}\n') +# except Exception as e: +# logger.error(f"Error capturing action {action_text}: {e}") + +# def set_audio_language(self, lang: str): +# """Alter audio language in memory and persist it in config file +# """ +# try: +# self.global_vars.transcriber.stt_model.set_lang(lang) +# config_obj = configuration.Config() +# # Save config +# altered_config = {'OpenAI': {'audio_lang': lang}} +# config_obj.add_override_value(altered_config) +# except Exception as e: +# logger.error(f"Error setting audio language: {e}") + +# def set_response_language(self, lang: str): +# """Alter response language in memory and persist it in config file +# """ +# try: +# config_obj = configuration.Config() +# altered_config = {'OpenAI': {'response_lang': lang}} +# # Save config +# config_obj.add_override_value(altered_config) +# config_data = config_obj.data + +# # Create a new system prompt +# prompt = config_data["General"]["system_prompt"] +# response_lang = config_data["OpenAI"]["response_lang"] +# if response_lang is not None: +# prompt += f'. Respond exclusively in {response_lang}.' +# convo = self.global_vars.convo +# convo.update_conversation(persona=constants.PERSONA_SYSTEM, +# text=prompt, +# time_spoken=datetime.datetime.utcnow(), +# update_previous=True) +# except Exception as e: +# logger.error(f"Error setting response language: {e}") + + +# def popup_msg_no_close_threaded(title, msg): +# """Create a pop up with no close button. +# """ +# global pop_up # pylint: disable=W0603 +# try: +# popup = ctk.CTkToplevel(T_GLOBALS.main_window) +# popup.geometry("100x50") +# popup.title(title) +# label = ctk.CTkLabel(popup, text=msg, font=("Arial", 12), +# text_color="#FFFCF2") +# label.pack(side="top", fill="x", pady=10) +# pop_up = popup +# popup.lift() +# except Exception as e: +# # Sometimes get the error - calling Tcl from different apartment +# logger.info('Exception in popup_msg_no_close_threaded') +# logger.info(e) +# return + + +# def popup_msg_no_close(title: str, msg: str): +# """Create a popup that the caller is responsible for closing +# using the destroy method +# """ +# kwargs = {} +# kwargs['title'] = title +# kwargs['msg'] = msg +# pop_ui_thread = threading.Thread(target=popup_msg_no_close_threaded, +# name='Pop up thread', +# kwargs=kwargs) +# pop_ui_thread.daemon = True +# pop_ui_thread.start() +# # Give a chance for the thread to initialize +# # When API key is not specified, need the thread to initialize to +# # allow summarize window to show and ultimately be closed. +# time.sleep(0.1) + + +# def popup_msg_close_button(title: str, msg: str): +# """Create a popup that the caller is responsible for closing +# using the destroy method +# """ +# popup = ctk.CTkToplevel(T_GLOBALS.main_window) +# popup.geometry("380x710") +# popup.title(title) +# txtbox = ctk.CTkTextbox(popup, width=350, height=600, font=("Arial", UI_FONT_SIZE), +# text_color='#FFFCF2', wrap="word") +# txtbox.grid(row=0, column=0, padx=10, pady=10, sticky="nsew") +# txtbox.insert("0.0", msg) + +# def copy_summary_to_clipboard(): +# pyperclip.copy(txtbox.cget("text")) + +# copy_button = ctk.CTkButton(popup, text="Copy to Clipboard", command=copy_summary_to_clipboard) +# copy_button.grid(row=1, column=0, padx=10, pady=5, sticky="nsew") + +# close_button = ctk.CTkButton(popup, text="Close", command=popup.destroy) +# close_button.grid(row=2, column=0, padx=10, pady=5, sticky="nsew") +# popup.lift() + + +# def write_in_textbox(textbox: ctk.CTkTextbox, text: str): +# """Update the text of textbox with the given text +# Args: +# textbox: textbox to be updated +# text: updated text +# """ +# # Get current selection attributes, so they can be preserved after writing new text +# a: tuple = textbox.tag_ranges('sel') +# # (, ) +# textbox.delete("0.0", "end") +# textbox.insert("0.0", text) +# if len(a): +# textbox.tag_add('sel', a[0], a[1]) + + +# def update_transcript_ui(transcriber: AudioTranscriber, textbox: ctk.CTkTextbox): +# """Update the text of transcription textbox with the given text +# Args: +# transcriber: AudioTranscriber Object +# textbox: textbox to be updated +# """ + +# global last_transcript_ui_update_time # pylint: disable=W0603 +# global global_vars_module # pylint: disable=W0603 + +# if global_vars_module is None: +# global_vars_module = TranscriptionGlobals() + +# # None comparison is for initialization +# if last_transcript_ui_update_time is None or last_transcript_ui_update_time < global_vars_module.convo.last_update: +# transcript_string = transcriber.get_transcript() +# write_in_textbox(textbox, transcript_string) +# textbox.see("end") +# last_transcript_ui_update_time = datetime.datetime.utcnow() + +# textbox.after(constants.TRANSCRIPT_UI_UPDATE_DELAY_DURATION_MS, +# update_transcript_ui, transcriber, textbox) + + +# def update_response_ui(responder: gr.GPTResponder, +# textbox: ctk.CTkTextbox, +# update_interval_slider_label: ctk.CTkLabel, +# update_interval_slider: ctk.CTkSlider): +# """Update the text of response textbox with the given text +# Args: +# textbox: textbox to be updated +# text: updated text +# """ +# global global_vars_module # pylint: disable=W0603 + +# if global_vars_module is None: +# global_vars_module = TranscriptionGlobals() + +# # global_vars_module.responder.enabled --> This is continous response mode from LLM +# # global_vars_module.update_response_now --> Get Response now from LLM +# if global_vars_module.responder.enabled or global_vars_module.update_response_now: +# response = responder.response + +# textbox.configure(state="normal") +# write_in_textbox(textbox, response) +# textbox.configure(state="disabled") +# textbox.see("end") + +# update_interval = int(update_interval_slider.get()) +# # responder.update_response_interval(update_interval) +# update_interval_slider_label.configure(text=f'LLM Response interval: ' +# f'{update_interval} seconds') + +# textbox.after(300, update_response_ui, responder, textbox, +# update_interval_slider_label, update_interval_slider) + + +# def create_ui_components(root, config: dict): +# """Create UI for the application +# """ +# logger.info(create_ui_components.__name__) +# ctk.set_appearance_mode("dark") +# ctk.set_default_color_theme("dark-blue") +# root.title("Transcribe") +# root.configure(bg='#252422') +# root.geometry("1000x600") + +# ui_cb = UICallbacks() +# global_vars = TranscriptionGlobals() + +# # Create the menu bar +# menubar = tk.Menu(root) + +# # Create a file menu +# filemenu = tk.Menu(menubar, tearoff=False) + +# # Add a "Save" menu item to the file menu +# filemenu.add_command(label="Save Transcript to File", command=ui_cb.save_file) + +# # Add a "Pause" menu item to the file menu +# filemenu.add_command(label="Pause Transcription", command=ui_cb.set_transcript_state) + +# # Add a "Quit" menu item to the file menu +# filemenu.add_command(label="Quit", command=root.quit) + +# # Add the file menu to the menu bar +# menubar.add_cascade(label="File", menu=filemenu) + +# # Create an edit menu +# editmenu = tk.Menu(menubar, tearoff=False) + +# # Add a "Clear Audio Transcript" menu item to the file menu +# editmenu.add_command(label="Clear Audio Transcript", command=lambda: +# global_vars.transcriber.clear_transcriber_context(global_vars.audio_queue)) + +# # Add a "Copy To Clipboard" menu item to the file menu +# editmenu.add_command(label="Copy Transcript to Clipboard", command=ui_cb.copy_to_clipboard) + +# # Add "Disable Speaker" menu item to file menu +# editmenu.add_command(label="Disable Speaker", command=lambda: ui_cb.enable_disable_speaker(editmenu)) + +# # Add "Disable Microphone" menu item to file menu +# editmenu.add_command(label="Disable Microphone", command=lambda: ui_cb.enable_disable_microphone(editmenu)) + +# # Add the edit menu to the menu bar +# menubar.add_cascade(label="Edit", menu=editmenu) + +# # Create help menu, add items in help menu +# helpmenu = tk.Menu(menubar, tearoff=False) +# helpmenu.add_command(label="Github Repo", command=ui_cb.open_github) +# helpmenu.add_command(label="Star the Github repo", command=ui_cb.open_github) +# helpmenu.add_command(label="Report an Issue", command=ui_cb.open_support) +# menubar.add_cascade(label="Help", menu=helpmenu) + +# # Add the menu bar to the main window +# root.config(menu=menubar) + +# # Speech to Text textbox +# transcript_textbox = ctk.CTkTextbox(root, width=300, font=("Arial", UI_FONT_SIZE), +# text_color='#FFFCF2', wrap="word") +# transcript_textbox.grid(row=0, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + +# # LLM Response textbox +# response_textbox = ctk.CTkTextbox(root, width=300, font=("Arial", UI_FONT_SIZE), +# text_color='#639cdc', wrap="word") +# response_textbox.grid(row=0, column=2, padx=10, pady=3, sticky="nsew") +# response_textbox.insert("0.0", prompts.INITIAL_RESPONSE) + +# response_enabled = bool(config['General']['continuous_response']) +# b_text = "Suggest Responses Continuously" if not response_enabled else "Do Not Suggest Responses Continuously" +# continuous_response_button = ctk.CTkButton(root, text=b_text) +# continuous_response_button.grid(row=1, column=2, padx=10, pady=3, sticky="nsew") + +# response_now_button = ctk.CTkButton(root, text="Suggest Response Now") +# response_now_button.grid(row=2, column=2, padx=10, pady=3, sticky="nsew") + +# read_response_now_button = ctk.CTkButton(root, text="Suggest Response and Read") +# read_response_now_button.grid(row=3, column=2, padx=10, pady=3, sticky="nsew") + +# summarize_button = ctk.CTkButton(root, text="Summarize") +# summarize_button.grid(row=4, column=2, padx=10, pady=3, sticky="nsew") + +# # Continuous LLM Response label, and slider +# update_interval_slider_label = ctk.CTkLabel(root, text="", font=("Arial", 12), +# text_color="#FFFCF2") +# update_interval_slider_label.grid(row=1, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + +# update_interval_slider = ctk.CTkSlider(root, from_=1, to=30, width=300, # height=5, +# number_of_steps=29) +# update_interval_slider.set(config['General']['llm_response_interval']) +# update_interval_slider.grid(row=2, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + +# # Speech to text language selection label, dropdown +# audio_lang_label = ctk.CTkLabel(root, text="Audio Lang: ", +# font=("Arial", 12), +# text_color="#FFFCF2") +# audio_lang_label.grid(row=3, column=0, padx=10, pady=3, sticky="nw") + +# audio_lang = config['OpenAI']['audio_lang'] +# audio_lang_combobox = ctk.CTkOptionMenu(root, width=15, values=list(LANGUAGES_DICT.values())) +# audio_lang_combobox.set(audio_lang) +# audio_lang_combobox.grid(row=3, column=0, ipadx=60, padx=10, pady=3, sticky="ne") + +# # LLM Response language selection label, dropdown +# response_lang_label = ctk.CTkLabel(root, +# text="Response Lang: ", +# font=("Arial", 12), text_color="#FFFCF2") +# response_lang_label.grid(row=3, column=1, padx=10, pady=3, sticky="nw") + +# response_lang = config['OpenAI']['response_lang'] +# response_lang_combobox = ctk.CTkOptionMenu(root, width=15, values=list(LANGUAGES_DICT.values())) +# response_lang_combobox.set(response_lang) +# response_lang_combobox.grid(row=3, column=1, ipadx=60, padx=10, pady=3, sticky="ne") + +# github_link = ctk.CTkLabel(root, text="Star the Github Repo", +# text_color="#639cdc", cursor="hand2") +# github_link.grid(row=4, column=0, padx=10, pady=3, sticky="wn") + +# issue_link = ctk.CTkLabel(root, text="Report an issue", text_color="#639cdc", cursor="hand2") +# issue_link.grid(row=4, column=1, padx=10, pady=3, sticky="wn") + +# # Create right click menu for transcript textbox. +# # This displays only inside the speech to text textbox +# m = tk.Menu(root, tearoff=0) +# m.add_command(label="Generate response for selected text", +# command=ui_cb.get_response_selected_now) +# m.add_command(label="Save Transcript to File", command=ui_cb.save_file) +# m.add_command(label="Clear Audio Transcript", command=lambda: +# global_vars.transcriber.clear_transcriber_context(global_vars.audio_queue)) +# m.add_command(label="Copy Transcript to Clipboard", command=ui_cb.copy_to_clipboard) +# m.add_separator() +# m.add_command(label="Quit", command=root.quit) + +# chat_inference_provider = config['General']['chat_inference_provider'] +# if chat_inference_provider == 'openai': +# api_key = config['OpenAI']['api_key'] +# base_url = config['OpenAI']['base_url'] +# model = config['OpenAI']['ai_model'] +# elif chat_inference_provider == 'together': +# api_key = config['Together']['api_key'] +# base_url = config['Together']['base_url'] +# model = config['Together']['ai_model'] + +# if not utilities.is_api_key_valid(api_key=api_key, base_url=base_url, model=model): +# # Disable buttons that interact with backend services +# continuous_response_button.configure(state='disabled') +# response_now_button.configure(state='disabled') +# read_response_now_button.configure(state='disabled') +# summarize_button.configure(state='disabled') + +# tt_msg = 'Add API Key in override.yaml to enable button' +# # Add tooltips for disabled buttons +# ToolTip(continuous_response_button, msg=tt_msg, +# delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, +# padx=7, pady=7) +# ToolTip(response_now_button, msg=tt_msg, +# delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, +# padx=7, pady=7) +# ToolTip(read_response_now_button, msg=tt_msg, +# delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, +# padx=7, pady=7) +# ToolTip(summarize_button, msg=tt_msg, +# delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, +# padx=7, pady=7) + +# def show_context_menu(event): +# try: +# m.tk_popup(event.x_root, event.y_root) +# finally: +# m.grab_release() + +# transcript_textbox.bind("", show_context_menu) + +# # Order of returned components is important. +# # Add new components to the end +# return [transcript_textbox, response_textbox, update_interval_slider, +# update_interval_slider_label, continuous_response_button, +# audio_lang_combobox, response_lang_combobox, filemenu, response_now_button, +# read_response_now_button, editmenu, github_link, issue_link, summarize_button] From 16d511cd414d5cd9873b1f2c769e018d36e6acb8 Mon Sep 17 00:00:00 2001 From: vivek Date: Thu, 23 May 2024 20:45:45 -0400 Subject: [PATCH 07/19] component --- app/transcribe/ui/selectable_text.py | 123 ----------------------- app/transcribe/uicomp/selectable_text.py | 107 ++++++++++++++++++++ 2 files changed, 107 insertions(+), 123 deletions(-) delete mode 100644 app/transcribe/ui/selectable_text.py create mode 100644 app/transcribe/uicomp/selectable_text.py diff --git a/app/transcribe/ui/selectable_text.py b/app/transcribe/ui/selectable_text.py deleted file mode 100644 index 70780dc..0000000 --- a/app/transcribe/ui/selectable_text.py +++ /dev/null @@ -1,123 +0,0 @@ -from tkinter import Text -import customtkinter as ctk - - -class SelectableText(ctk.CTk): - """ - A CustomTkinter application that displays a lot of lines of text, - where each line can wrap and is selectable to trigger an event on selection. - """ - def __init__(self, title: str, geometry: str): - """ - Initialize the SelectableText application. - """ - super().__init__() - - self.title(title) - self.geometry(geometry) - - # Create a frame to hold the Text widget and the scrollbar - self.text_frame = ctk.CTkFrame(self) - self.text_frame.pack(fill="both", expand=True, padx=20, pady=20) - - # Create a Text widget - self.text_widget = Text(self.text_frame, wrap="word", - bg=ctk.ThemeManager.theme['CTkFrame']['fg_color'][1], - fg="white") - self.text_widget.pack(side="left", fill="both", expand=True) - - # Create a Scrollbar and attach it to the Text widget - self.scrollbar = ctk.CTkScrollbar(self.text_frame, command=self.text_widget.yview) - self.scrollbar.pack(side="right", fill="y") - self.text_widget.config(yscrollcommand=self.scrollbar.set) - - # Bind click event to the Text widget - self.text_widget.bind("", self.on_text_click) - - # Make the Text widget read-only - self.text_widget.configure(state="disabled") - - # Create a frame for the buttons - self.button_frame = ctk.CTkFrame(self) - self.button_frame.pack(fill="x", padx=20, pady=10) - - # Create buttons to scroll to the top and bottom - self.scroll_top_button = ctk.CTkButton(self.button_frame, - text="Scroll to Top", - command=self.scroll_to_top) - self.scroll_top_button.pack(side="left", padx=10) - - self.scroll_bottom_button = ctk.CTkButton(self.button_frame, - text="Scroll to Bottom", - command=self.scroll_to_bottom) - self.scroll_bottom_button.pack(side="left", padx=10) - - # Create buttons to add text to the top and bottom - self.add_top_button = ctk.CTkButton(self.button_frame, - text="Add Text to Top", - command=self.add_text_to_top) - self.add_top_button.pack(side="left", padx=10) - - self.add_bottom_button = ctk.CTkButton(self.button_frame, - text="Add Text to Bottom", - command=self.add_text_to_bottom) - self.add_bottom_button.pack(side="left", padx=10) - - def on_text_click(self, event): - """ - Handle the click event on the Text widget. - - Args: - event (tkinter.Event): The event object containing event details. - """ - # Get the index of the clicked line - index = self.text_widget.index("@%s,%s" % (event.x, event.y)) - line_number = int(index.split(".")[0]) - - # Get the text of the clicked line - line_start = f"{line_number}.0" - line_end = f"{line_number}.end" - line_text = self.text_widget.get(line_start, line_end).strip() - - # Trigger an event (print the line text) - print(f"Selected: {line_text}") - - def scroll_to_top(self): - """ - Scroll the Text widget to the top. - """ - self.text_widget.yview_moveto(0) - - def scroll_to_bottom(self): - """ - Scroll the Text widget to the bottom. - """ - self.text_widget.yview_moveto(1) - - def add_text_to_top(self, input_text: str): - """ - Add text to the top of the Text widget. - """ - self.text_widget.configure(state="normal") - self.text_widget.insert("1.0", input_text + "\n") - self.text_widget.configure(state="disabled") - - def add_text_to_bottom(self, input_text: str): - """ - Add text to the bottom of the Text widget. - """ - self.text_widget.configure(state="normal") - self.text_widget.insert("end", input_text + "\n") - self.text_widget.configure(state="disabled") - - -if __name__ == "__main__": - ctk.set_appearance_mode("dark") - app = SelectableText('Simple Selectable text example', '600x400') - - # Add a lot of lines of text - lines_of_text = [f"Line {i}: This is an example of a long line of text that should wrap around the Text widget." for i in range(1, 101)] - for line in lines_of_text: - app.add_text_to_bottom(line) - - app.mainloop() diff --git a/app/transcribe/uicomp/selectable_text.py b/app/transcribe/uicomp/selectable_text.py new file mode 100644 index 0000000..b85a385 --- /dev/null +++ b/app/transcribe/uicomp/selectable_text.py @@ -0,0 +1,107 @@ +from tkinter import Text, Scrollbar, END, SEL_FIRST, SEL_LAST +import customtkinter as ctk + + +class SelectableText(ctk.CTkFrame): + """Custom TKinter Component to display multiple lines of text + and support custom functionality on clicking a line of text. + """ + def __init__(self, master=None, **kwargs): + super().__init__(master, **kwargs) + + self.text_widget = Text(self, wrap="word", undo=True) + self.scrollbar = Scrollbar(self, command=self.text_widget.yview) + self.text_widget.config(yscrollcommand=self.scrollbar.set) + + self.text_widget.pack(side="left", fill="both", expand=True) + self.scrollbar.pack(side="right", fill="y") + + self.text_widget.bind("", self.on_text_click) + # self.text_widget.bind("", self.on_text_select) + # self.text_widget.bind("", self.on_double_click) + + def on_text_select(self, event): + try: + selected_text = self.text_widget.get(SEL_FIRST, SEL_LAST) + print(f"Selected text: {selected_text}") + + index = self.text_widget.index("@%s,%s" % (event.x, event.y)) + line_number = int(index.split(".")[0]) + + # Get the text of the clicked line + line_start = f"{line_number}.0" + line_end = f"{line_number}.end" + line_text = self.text_widget.get(line_start, line_end).strip() + + # Trigger an event (print the line text) + print(f"Selected: {line_text}") + except: + pass # No selection + + def on_double_click(self, event): + index = self.text_widget.index("@%s,%s" % (event.x, event.y)) + line_start = self.text_widget.index("%s linestart" % index) + line_end = self.text_widget.index("%s lineend" % index) + self.text_widget.tag_add('SEL', line_start, line_end) + self.text_widget.mark_set("insert", line_end) + self.text_widget.see("insert") + self.text_widget.focus() + + def on_text_click(self, event): + """ + Handle the click event on the Text widget. + + Args: + event (tkinter.Event): The event object containing event details. + """ + # Get the index of the clicked line + index = self.text_widget.index("@%s,%s" % (event.x, event.y)) + line_number = int(index.split(".")[0]) + + # Get the text of the clicked line + line_start = f"{line_number}.0" + line_end = f"{line_number}.end" + line_text = self.text_widget.get(line_start, line_end).strip() + + # Trigger an event (print the line text) + print(f"Selected: {line_text}") + + def scroll_to_top(self): + """ + Scroll the Text widget to the top. + """ + self.text_widget.yview_moveto(0) + + def scroll_to_bottom(self): + """ + Scroll the Text widget to the bottom. + """ + self.text_widget.yview_moveto(1) + + def add_text_to_top(self, input_text: str): + """ + Add text to the top of the Text widget. + """ + self.text_widget.configure(state="normal") + self.text_widget.insert("1.0", input_text + "\n") + self.text_widget.configure(state="disabled") + + def add_text_to_bottom(self, input_text: str): + """ + Add text to the bottom of the Text widget. + """ + self.text_widget.configure(state="normal") + self.text_widget.insert(END, input_text + "\n") + self.text_widget.configure(state="disabled") + + +if __name__ == "__main__": + ctk.set_appearance_mode("dark") + app = SelectableText() + + # Add a lot of lines of text + lines_of_text = [f"Line {i}: This is an example of a long line of text that should wrap around the Text widget." for i in range(1, 101)] + for line in lines_of_text: + app.add_text_to_bottom(line) + + app.mainloop() From 5ff75adf5b1c0065edd01c73834b87d20803ea48 Mon Sep 17 00:00:00 2001 From: vivek Date: Fri, 24 May 2024 08:40:02 -0400 Subject: [PATCH 08/19] widget placement --- app/transcribe/appui.py | 248 ++++++++++++++--------- app/transcribe/uicomp/selectable_text.py | 8 +- 2 files changed, 156 insertions(+), 100 deletions(-) diff --git a/app/transcribe/appui.py b/app/transcribe/appui.py index b9c4ac0..d4bb2a5 100644 --- a/app/transcribe/appui.py +++ b/app/transcribe/appui.py @@ -15,6 +15,8 @@ from tsutils import utilities from tsutils import app_logging as al from tsutils import configuration +from uicomp.selectable_text import SelectableText + logger = al.get_module_logger(al.UI_LOGGER) UI_FONT_SIZE = 20 @@ -25,28 +27,30 @@ pop_up = None -class AppUI: +class AppUI(ctk.CTk): """Encapsulates all UI functionality for the app """ global_vars: TranscriptionGlobals ui_filename: str = None def __init__(self, config: dict): + super().__init__() self.global_vars = TranscriptionGlobals() - self.root = ctk.CTk() - self.global_vars.main_window = self.root + # self.root = ctk.CTk() + self.global_vars.main_window = self self.create_ui_components(config=config) self.set_audio_device_menus(config=config) def start(self): """Start showing the UI """ - self.root.mainloop() + self.mainloop() def update_initial_transcripts(self): - update_transcript_ui(self.global_vars.transcriber, - self.transcript_textbox) + # TODO: Ref to transcript_textbox + # update_transcript_ui(self.global_vars.transcriber, + # self.transcript_textbox) update_response_ui(self.global_vars.responder, self.response_textbox, self.update_interval_slider_label, @@ -57,141 +61,135 @@ def create_ui_components(self, config: dict): """ ctk.set_appearance_mode("dark") ctk.set_default_color_theme("dark-blue") - self.root.title("Transcribe") - self.root.configure(bg='#252422') - self.root.geometry("1000x600") - - # Create the menu bar - menubar = tk.Menu(self.root) - - # Create a file menu - self.filemenu = tk.Menu(menubar, tearoff=False) - - # Add a "Save" menu item to the file menu - self.filemenu.add_command(label="Save Transcript to File", command=self.save_file) - - # Add a "Pause" menu item to the file menu - self.filemenu.add_command(label="Pause Transcription", command=self.set_transcript_state) - - # Add a "Quit" menu item to the file menu - self.filemenu.add_command(label="Quit", command=self.root.quit) - - # Add the file menu to the menu bar - menubar.add_cascade(label="File", menu=self.filemenu) - - # Create an edit menu - self.editmenu = tk.Menu(menubar, tearoff=False) - - # Add a "Clear Audio Transcript" menu item to the file menu - self.editmenu.add_command(label="Clear Audio Transcript", command=lambda: - self.global_vars.transcriber.clear_transcriber_context(self.global_vars.audio_queue)) + self.title("Transcribe") + self.configure(bg='#252422') + self.geometry("1000x600") - # Add a "Copy To Clipboard" menu item to the file menu - self.editmenu.add_command(label="Copy Transcript to Clipboard", - command=self.copy_to_clipboard) - - # Add "Disable Speaker" menu item to file menu - self.editmenu.add_command(label="Disable Speaker", - command=self.enable_disable_speaker()) - - # Add "Disable Microphone" menu item to file menu - self.editmenu.add_command(label="Disable Microphone", - command=self.enable_disable_microphone()) + # Frame for the main content + self.main_frame = ctk.CTkFrame(self) + self.main_frame.pack(fill="both", expand=True) - # Add the edit menu to the menu bar - menubar.add_cascade(label="Edit", menu=self.editmenu) + self.create_menus() - # Create help menu, add items in help menu - helpmenu = tk.Menu(menubar, tearoff=False) - helpmenu.add_command(label="Github Repo", command=self.open_github) - helpmenu.add_command(label="Star the Github repo", command=self.open_github) - helpmenu.add_command(label="Report an Issue", command=self.open_support) - menubar.add_cascade(label="Help", menu=helpmenu) + # Speech to Text textbox + # TODO: Ref to transcript_textbox + # Left side: SelectableTextComponent + self.transcript_text: SelectableText = SelectableText(self.main_frame) + self.transcript_text.pack(side="left", fill="both", expand=True, padx=10, pady=10) + # self.transcript_text.grid(row=0, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") - # Add the menu bar to the main window - self.root.config(menu=menubar) + # TODO: set the font, texcolor etc. + # self.transcript_textbox = ctk.CTkTextbox(self.root, width=300, font=("Arial", UI_FONT_SIZE), + # text_color='#FFFCF2', wrap="word") + # self.transcript_textbox.grid(row=0, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") - # Speech to Text textbox - self.transcript_textbox = ctk.CTkTextbox(self.root, width=300, font=("Arial", UI_FONT_SIZE), - text_color='#FFFCF2', wrap="word") - self.transcript_textbox.grid(row=0, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + # Right side + self.right_frame = ctk.CTkFrame(self.main_frame) + self.right_frame.pack(side="right", fill="both", expand=True, padx=10, pady=10) # LLM Response textbox - self.response_textbox = ctk.CTkTextbox(self.root, width=300, font=("Arial", UI_FONT_SIZE), + self.response_textbox = ctk.CTkTextbox(self.right_frame, width=300, font=("Arial", UI_FONT_SIZE), text_color='#639cdc', wrap="word") - self.response_textbox.grid(row=0, column=2, padx=10, pady=3, sticky="nsew") + # self.response_textbox.grid(row=0, column=2, padx=10, pady=3, sticky="nsew") + self.response_textbox.pack(fill="both", expand=True) self.response_textbox.insert("0.0", prompts.INITIAL_RESPONSE) + # Bottom Frame for buttons + self.bottom_frame = ctk.CTkFrame(self, border_color="white", border_width=2) + self.bottom_frame.pack(side="bottom", fill="both", pady=10) + response_enabled = bool(config['General']['continuous_response']) b_text = "Suggest Responses Continuously" if not response_enabled else "Do Not Suggest Responses Continuously" - self.continuous_response_button = ctk.CTkButton(self.root, text=b_text) - self.continuous_response_button.grid(row=1, column=2, padx=10, pady=3, sticky="nsew") + self.continuous_response_button = ctk.CTkButton(self.bottom_frame, text=b_text) + self.continuous_response_button.grid(row=0, column=4, padx=10, pady=3, sticky="nsew") + # self.continuous_response_button.grid(row=1, column=2, padx=10, pady=3, sticky="nsew") + # self.continuous_response_button.pack(side="left", padx=10) self.continuous_response_button.configure(command=self.freeze_unfreeze) - self.response_now_button = ctk.CTkButton(self.root, text="Suggest Response Now") - self.response_now_button.grid(row=2, column=2, padx=10, pady=3, sticky="nsew") + self.response_now_button = ctk.CTkButton(self.bottom_frame, text="Suggest Response Now") + self.response_now_button.grid(row=1, column=4, padx=10, pady=3, sticky="nsew") + # self.response_now_button.grid(row=2, column=2, padx=10, pady=3, sticky="nsew") + # self.response_now_button.pack(side="left", padx=10) self.response_now_button.configure(command=self.get_response_now) - self.read_response_now_button = ctk.CTkButton(self.root, text="Suggest Response and Read") - self.read_response_now_button.grid(row=3, column=2, padx=10, pady=3, sticky="nsew") + self.read_response_now_button = ctk.CTkButton(self.bottom_frame, text="Suggest Response and Read") + # self.read_response_now_button.pack(side="left", padx=10) + self.read_response_now_button.grid(row=2, column=4, padx=10, pady=3, sticky="nsew") + # self.read_response_now_button.grid(row=3, column=2, padx=10, pady=3, sticky="nsew") self.read_response_now_button.configure(command=self.update_response_ui_and_read_now) - self.summarize_button = ctk.CTkButton(self.root, text="Summarize") - self.summarize_button.grid(row=4, column=2, padx=10, pady=3, sticky="nsew") + self.summarize_button = ctk.CTkButton(self.bottom_frame, text="Summarize") + # self.summarize_button.pack(side="left", padx=10) + self.summarize_button.grid(row=3, column=4, padx=10, pady=3, sticky="nsew") + # self.summarize_button.grid(row=4, column=2, padx=10, pady=3, sticky="nsew") self.summarize_button.configure(command=self.summarize) # Continuous LLM Response label, and slider - self.update_interval_slider_label = ctk.CTkLabel(self.root, text="", font=("Arial", 12), + self.update_interval_slider_label = ctk.CTkLabel(self.bottom_frame, text="", font=("Arial", 12), text_color="#FFFCF2") - self.update_interval_slider_label.grid(row=1, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") - - self.update_interval_slider = ctk.CTkSlider(self.root, from_=1, to=30, width=300, # height=5, +# self.update_interval_slider_label.pack(side="left", padx=10) + self.update_interval_slider_label.grid(row=0, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + # self.update_interval_slider_label.grid(row=1, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + self.update_interval_slider = ctk.CTkSlider(self.bottom_frame, from_=1, to=30, width=300, # height=5, number_of_steps=29) self.update_interval_slider.set(config['General']['llm_response_interval']) - self.update_interval_slider.grid(row=2, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + self.update_interval_slider.grid(row=1, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + # self.update_interval_slider.grid(row=2, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + # self.update_interval_slider.pack(side="left", padx=10) self.update_interval_slider.configure(command=self.update_interval_slider_value) label_text = f'LLM Response interval: {int(self.update_interval_slider.get())} seconds' self.update_interval_slider_label.configure(text=label_text) # Speech to text language selection label, dropdown - audio_lang_label = ctk.CTkLabel(self.root, text="Audio Lang: ", + audio_lang_label = ctk.CTkLabel(self.bottom_frame, text="Audio Lang: ", font=("Arial", 12), text_color="#FFFCF2") - audio_lang_label.grid(row=3, column=0, padx=10, pady=3, sticky="nw") + # audio_lang_label.pack(side="left", padx=10) + audio_lang_label.grid(row=2, column=0, padx=10, pady=3, sticky='nw') audio_lang = config['OpenAI']['audio_lang'] - self.audio_lang_combobox = ctk.CTkOptionMenu(self.root, width=15, values=list(LANGUAGES_DICT.values())) + self.audio_lang_combobox = ctk.CTkOptionMenu(self.bottom_frame, width=15, values=list(LANGUAGES_DICT.values())) self.audio_lang_combobox.set(audio_lang) - self.audio_lang_combobox.grid(row=3, column=0, ipadx=60, padx=10, pady=3, sticky="ne") + # self.audio_lang_combobox.pack(side="left", padx=10) + self.audio_lang_combobox.grid(row=2, column=1, ipadx=60, padx=10, pady=3, sticky="ne") + # self.audio_lang_combobox.grid(row=3, column=0, ipadx=60, padx=10, pady=3, sticky="ne") self.audio_lang_combobox.configure(command=self.set_audio_language) # LLM Response language selection label, dropdown - response_lang_label = ctk.CTkLabel(self.root, + response_lang_label = ctk.CTkLabel(self.bottom_frame, text="Response Lang: ", font=("Arial", 12), text_color="#FFFCF2") - response_lang_label.grid(row=3, column=1, padx=10, pady=3, sticky="nw") + # response_lang_label.pack(side="left", padx=10) + response_lang_label.grid(row=2, column=2, padx=10, pady=3, sticky="nw") + # response_lang_label.grid(row=3, column=1, padx=10, pady=3, sticky="nw") response_lang = config['OpenAI']['response_lang'] - self.response_lang_combobox = ctk.CTkOptionMenu(self.root, width=15, values=list(LANGUAGES_DICT.values())) + self.response_lang_combobox = ctk.CTkOptionMenu(self.bottom_frame, width=15, values=list(LANGUAGES_DICT.values())) self.response_lang_combobox.set(response_lang) - self.response_lang_combobox.grid(row=3, column=1, ipadx=60, padx=10, pady=3, sticky="ne") + # self.response_lang_combobox.pack(side="left", padx=10) + self.response_lang_combobox.grid(row=2, column=3, ipadx=60, padx=10, pady=3, sticky="ne") + # self.response_lang_combobox.grid(row=3, column=1, ipadx=60, padx=10, pady=3, sticky="ne") self.response_lang_combobox.configure(command=self.set_response_language) - self.github_link = ctk.CTkLabel(self.root, text="Star the Github Repo", + self.github_link = ctk.CTkLabel(self.bottom_frame, text="Star the Github Repo", text_color="#639cdc", cursor="hand2") - self.github_link.grid(row=4, column=0, padx=10, pady=3, sticky="wn") + # self.github_link.pack(side="left", padx=10) + self.github_link.grid(row=3, column=0, padx=10, pady=3, sticky="wn") + # self.github_link.grid(row=4, column=0, padx=10, pady=3, sticky="wn") self.github_link.bind('', lambda e: - self.open_link('https://github.com/vivekuppal/transcribe?referer=desktop')) + self.open_link('https://github.com/vivekuppal/transcribe?referer=desktop')) - self.issue_link = ctk.CTkLabel(self.root, text="Report an issue", text_color="#639cdc", cursor="hand2") - self.issue_link.grid(row=4, column=1, padx=10, pady=3, sticky="wn") + self.issue_link = ctk.CTkLabel(self.bottom_frame, text="Report an issue", text_color="#639cdc", cursor="hand2") + # self.issue_link.pack(side="left", padx=10) + self.issue_link.grid(row=3, column=1, padx=10, pady=3, sticky="wn") + # self.issue_link.grid(row=4, column=1, padx=10, pady=3, sticky="wn") self.issue_link.bind('', lambda e: self.open_link( 'https://github.com/vivekuppal/transcribe/issues/new?referer=desktop')) # Create right click menu for transcript textbox. # This displays only inside the speech to text textbox - m = tk.Menu(self.root, tearoff=0) + m = tk.Menu(self.main_frame, tearoff=0) m.add_command(label="Generate response for selected text", command=self.get_response_selected_now) m.add_command(label="Save Transcript to File", command=self.save_file) @@ -199,7 +197,7 @@ def create_ui_components(self, config: dict): self.global_vars.transcriber.clear_transcriber_context(self.global_vars.audio_queue)) m.add_command(label="Copy Transcript to Clipboard", command=self.copy_to_clipboard) m.add_separator() - m.add_command(label="Quit", command=self.root.quit) + m.add_command(label="Quit", command=self.quit) chat_inference_provider = config['General']['chat_inference_provider'] if chat_inference_provider == 'openai': @@ -239,14 +237,66 @@ def show_context_menu(event): finally: m.grab_release() - self.transcript_textbox.bind("", show_context_menu) + # TODO: Ref to transcript_textbox + self.transcript_text.bind("", show_context_menu) - self.root.grid_rowconfigure(0, weight=100) - self.root.grid_rowconfigure(1, weight=1) - self.root.grid_rowconfigure(2, weight=1) - self.root.grid_rowconfigure(3, weight=1) - self.root.grid_columnconfigure(0, weight=2) - self.root.grid_columnconfigure(1, weight=1) + # self.root.grid_rowconfigure(0, weight=100) + # self.root.grid_rowconfigure(1, weight=1) + # self.root.grid_rowconfigure(2, weight=1) + # self.root.grid_rowconfigure(3, weight=1) + # self.root.grid_columnconfigure(0, weight=2) + # self.root.grid_columnconfigure(1, weight=1) + + def create_menus(self): + # Create the menu bar + menubar = tk.Menu(self) + + # Create a file menu + self.filemenu = tk.Menu(menubar, tearoff=False) + + # Add a "Save" menu item to the file menu + self.filemenu.add_command(label="Save Transcript to File", command=self.save_file) + + # Add a "Pause" menu item to the file menu + self.filemenu.add_command(label="Pause Transcription", command=self.set_transcript_state) + + # Add a "Quit" menu item to the file menu + self.filemenu.add_command(label="Quit", command=self.quit) + + # Add the file menu to the menu bar + menubar.add_cascade(label="File", menu=self.filemenu) + + # Create an edit menu + self.editmenu = tk.Menu(menubar, tearoff=False) + + # Add a "Clear Audio Transcript" menu item to the file menu + self.editmenu.add_command(label="Clear Audio Transcript", command=lambda: + self.global_vars.transcriber.clear_transcriber_context(self.global_vars.audio_queue)) + + # Add a "Copy To Clipboard" menu item to the file menu + self.editmenu.add_command(label="Copy Transcript to Clipboard", + command=self.copy_to_clipboard) + + # Add "Disable Speaker" menu item to file menu + self.editmenu.add_command(label="Disable Speaker", + command=self.enable_disable_speaker()) + + # Add "Disable Microphone" menu item to file menu + self.editmenu.add_command(label="Disable Microphone", + command=self.enable_disable_microphone()) + + # Add the edit menu to the menu bar + menubar.add_cascade(label="Edit", menu=self.editmenu) + + # Create help menu, add items in help menu + helpmenu = tk.Menu(menubar, tearoff=False) + helpmenu.add_command(label="Github Repo", command=self.open_github) + helpmenu.add_command(label="Star the Github repo", command=self.open_github) + helpmenu.add_command(label="Report an Issue", command=self.open_support) + menubar.add_cascade(label="Help", menu=helpmenu) + + # Add the menu bar to the main window + self.config(menu=menubar) def set_audio_device_menus(self, config): if config['General']['disable_speaker']: @@ -390,7 +440,7 @@ def get_response_selected_now(self): # streamed back. Without the thread UI appears stuck as we stream the # responses back self.capture_action('Get LLM response selected now') - selected_text = self.transcript_textbox.selection_get() + selected_text = self.transcript_text.selection_get() response_ui_thread = threading.Thread(target=self.get_response_selected_now_threaded, args=(selected_text,), name='GetResponseSelectedNow') @@ -624,8 +674,8 @@ def update_transcript_ui(transcriber: AudioTranscriber, textbox: ctk.CTkTextbox) textbox.see("end") last_transcript_ui_update_time = datetime.datetime.utcnow() - textbox.after(constants.TRANSCRIPT_UI_UPDATE_DELAY_DURATION_MS, - update_transcript_ui, transcriber, textbox) + # textbox.after(constants.TRANSCRIPT_UI_UPDATE_DELAY_DURATION_MS, + # update_transcript_ui, transcriber, textbox) def update_response_ui(responder: gr.GPTResponder, diff --git a/app/transcribe/uicomp/selectable_text.py b/app/transcribe/uicomp/selectable_text.py index b85a385..82b2fa8 100644 --- a/app/transcribe/uicomp/selectable_text.py +++ b/app/transcribe/uicomp/selectable_text.py @@ -9,7 +9,7 @@ class SelectableText(ctk.CTkFrame): def __init__(self, master=None, **kwargs): super().__init__(master, **kwargs) - self.text_widget = Text(self, wrap="word", undo=True) + self.text_widget = Text(self, wrap="word", undo=True, background='#252422') self.scrollbar = Scrollbar(self, command=self.text_widget.yview) self.text_widget.config(yscrollcommand=self.scrollbar.set) @@ -17,10 +17,14 @@ def __init__(self, master=None, **kwargs): self.scrollbar.pack(side="right", fill="y") self.text_widget.bind("", self.on_text_click) + # Handler for left mouse click up # self.text_widget.bind("", self.on_text_select) + # Handler for double click # self.text_widget.bind("", self.on_double_click) def on_text_select(self, event): + """Handler for left mouse click + """ try: selected_text = self.text_widget.get(SEL_FIRST, SEL_LAST) print(f"Selected text: {selected_text}") @@ -39,6 +43,8 @@ def on_text_select(self, event): pass # No selection def on_double_click(self, event): + """Handler for double click + """ index = self.text_widget.index("@%s,%s" % (event.x, event.y)) line_start = self.text_widget.index("%s linestart" % index) line_end = self.text_widget.index("%s lineend" % index) From e95d97697cafdf0b82c924a3bdb3cd4cc8e9bdff Mon Sep 17 00:00:00 2001 From: vivek Date: Fri, 24 May 2024 11:55:21 -0400 Subject: [PATCH 09/19] Text updates correctly --- app/transcribe/appui.py | 67 +++++++++++++++++------- app/transcribe/conversation.py | 13 ++++- app/transcribe/uicomp/selectable_text.py | 24 ++++++++- 3 files changed, 81 insertions(+), 23 deletions(-) diff --git a/app/transcribe/appui.py b/app/transcribe/appui.py index d4bb2a5..4571688 100644 --- a/app/transcribe/appui.py +++ b/app/transcribe/appui.py @@ -37,7 +37,6 @@ def __init__(self, config: dict): super().__init__() self.global_vars = TranscriptionGlobals() - # self.root = ctk.CTk() self.global_vars.main_window = self self.create_ui_components(config=config) self.set_audio_device_menus(config=config) @@ -47,14 +46,21 @@ def start(self): """ self.mainloop() + def update_last_row(self, input_text: str): + self.transcript_text.delete_last_3_rows() + self.transcript_text.add_text_to_bottom(input_text) + self.transcript_text.scroll_to_bottom() + def update_initial_transcripts(self): - # TODO: Ref to transcript_textbox - # update_transcript_ui(self.global_vars.transcriber, - # self.transcript_textbox) + + update_transcript_ui(self.global_vars.transcriber, + self.transcript_text) update_response_ui(self.global_vars.responder, self.response_textbox, self.update_interval_slider_label, self.update_interval_slider) + self.global_vars.convo.set_handlers(self.update_last_row, + self.transcript_text.add_text_to_bottom) def create_ui_components(self, config: dict): """Create all UI components @@ -63,7 +69,7 @@ def create_ui_components(self, config: dict): ctk.set_default_color_theme("dark-blue") self.title("Transcribe") self.configure(bg='#252422') - self.geometry("1000x600") + self.geometry("1200x800") # Frame for the main content self.main_frame = ctk.CTkFrame(self) @@ -88,12 +94,18 @@ def create_ui_components(self, config: dict): self.right_frame.pack(side="right", fill="both", expand=True, padx=10, pady=10) # LLM Response textbox - self.response_textbox = ctk.CTkTextbox(self.right_frame, width=300, font=("Arial", UI_FONT_SIZE), - text_color='#639cdc', wrap="word") + self.min_response_textbox_width = 300 + self.response_textbox = ctk.CTkTextbox(self.right_frame, self.min_response_textbox_width, + font=("Arial", UI_FONT_SIZE), + text_color='#639cdc', + wrap="word") # self.response_textbox.grid(row=0, column=2, padx=10, pady=3, sticky="nsew") self.response_textbox.pack(fill="both", expand=True) self.response_textbox.insert("0.0", prompts.INITIAL_RESPONSE) + # Bind the event to enforce minimum width + # self.right_frame.bind("", self.enforce_minimum_width_of_response) + # Bottom Frame for buttons self.bottom_frame = ctk.CTkFrame(self, border_color="white", border_width=2) self.bottom_frame.pack(side="bottom", fill="both", pady=10) @@ -128,12 +140,12 @@ def create_ui_components(self, config: dict): self.update_interval_slider_label = ctk.CTkLabel(self.bottom_frame, text="", font=("Arial", 12), text_color="#FFFCF2") # self.update_interval_slider_label.pack(side="left", padx=10) - self.update_interval_slider_label.grid(row=0, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + self.update_interval_slider_label.grid(row=0, column=0, columnspan=4, padx=10, pady=3, sticky="nsew") # self.update_interval_slider_label.grid(row=1, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") self.update_interval_slider = ctk.CTkSlider(self.bottom_frame, from_=1, to=30, width=300, # height=5, number_of_steps=29) self.update_interval_slider.set(config['General']['llm_response_interval']) - self.update_interval_slider.grid(row=1, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") + self.update_interval_slider.grid(row=1, column=0, columnspan=4, padx=10, pady=3, sticky="nsew") # self.update_interval_slider.grid(row=2, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") # self.update_interval_slider.pack(side="left", padx=10) self.update_interval_slider.configure(command=self.update_interval_slider_value) @@ -240,12 +252,12 @@ def show_context_menu(event): # TODO: Ref to transcript_textbox self.transcript_text.bind("", show_context_menu) - # self.root.grid_rowconfigure(0, weight=100) - # self.root.grid_rowconfigure(1, weight=1) - # self.root.grid_rowconfigure(2, weight=1) - # self.root.grid_rowconfigure(3, weight=1) - # self.root.grid_columnconfigure(0, weight=2) - # self.root.grid_columnconfigure(1, weight=1) + # self.grid_rowconfigure(0, weight=100) + # self.grid_rowconfigure(1, weight=1) + # self.grid_rowconfigure(2, weight=1) + # self.grid_rowconfigure(3, weight=1) + # self.grid_columnconfigure(0, weight=1) + # self.grid_columnconfigure(1, weight=1) def create_menus(self): # Create the menu bar @@ -577,6 +589,16 @@ def set_response_language(self, lang: str): except Exception as e: logger.error(f"Error setting response language: {e}") + # def enforce_minimum_width_of_response(self, event): + # widthm = self.main_frame.winfo_width() + # widthr = self.right_frame.winfo_width() + # widthl = self.bottom_frame.winfo_width() + # # if self.response_textbox.winfo_width() < self.min_response_textbox_width: + # # self.response_textbox.configure(width=self.min_response_textbox_width) + # # self.right_frame.configure(width=self.min_response_textbox_width) + # # self.transcript_text.configure(width=widthm - self.min_response_textbox_width) + # print(f'Widths as observed: all {widthm}, right: {widthr}, left: {widthl}') + def popup_msg_no_close_threaded(title, msg): """Create a pop up with no close button. @@ -654,11 +676,11 @@ def write_in_textbox(textbox: ctk.CTkTextbox, text: str): textbox.tag_add('sel', a[0], a[1]) -def update_transcript_ui(transcriber: AudioTranscriber, textbox: ctk.CTkTextbox): +def update_transcript_ui(transcriber: AudioTranscriber, textbox: SelectableText): """Update the text of transcription textbox with the given text Args: transcriber: AudioTranscriber Object - textbox: textbox to be updated + textbox: SelectableText to be updated """ global last_transcript_ui_update_time # pylint: disable=W0603 @@ -669,9 +691,14 @@ def update_transcript_ui(transcriber: AudioTranscriber, textbox: ctk.CTkTextbox) # None comparison is for initialization if last_transcript_ui_update_time is None or last_transcript_ui_update_time < global_vars_module.convo.last_update: - transcript_string = transcriber.get_transcript() - write_in_textbox(textbox, transcript_string) - textbox.see("end") + transcript_strings = transcriber.get_transcript() + if isinstance(transcript_strings, list): + for line in transcript_strings: + textbox.add_text_to_bottom(line) + else: + textbox.add_text_to_bottom(transcript_strings) + # write_in_textbox(textbox, transcript_string) + textbox.scroll_to_bottom() last_transcript_ui_update_time = datetime.datetime.utcnow() # textbox.after(constants.TRANSCRIPT_UI_UPDATE_DELAY_DURATION_MS, diff --git a/app/transcribe/conversation.py b/app/transcribe/conversation.py index c3908ff..e9c2fa6 100644 --- a/app/transcribe/conversation.py +++ b/app/transcribe/conversation.py @@ -12,6 +12,8 @@ class Conversation: Has text from Speakers, Microphone, LLM, Instructions to LLM """ _initialized: bool = False + update_handler = None + insert_handler = None def __init__(self): self.transcript_data = {constants.PERSONA_SYSTEM: [], @@ -21,6 +23,10 @@ def __init__(self): self.last_update: datetime.datetime = None self.initialize_conversation() + def set_handlers(self, update, insert): + self.update_handler = update + self.insert_handler = insert + def initialize_conversation(self): """Populate initial app data for conversation object """ @@ -71,6 +77,8 @@ def update_conversation(self, persona: str, convo_object: convodb.Conversations = appdb().get_object(convodb.TABLE_NAME) convo_id = convo_object.get_max_convo_id(speaker=persona, inv_id=inv_id) + convo_text = f"{persona}: [{text}]\n\n" + ui_text = f"{persona}: [{text}]\n" # if (persona.lower() == 'assistant'): # print(f'Assistant Transcript length to begin with: {len(transcript)}') # print(f'append: {text}') @@ -90,15 +98,16 @@ def update_conversation(self, persona: str, # print(f'Removed: {prev_element}') # print(f'Update DB: {inv_id} - {time_spoken} - {persona} - {text}') convo_object.update_conversation(convo_id, text) + self.update_handler(ui_text) else: if self._initialized: # Insert in DB # print(f'Add to DB: {inv_id} - {time_spoken} - {persona} - {text}') convo_id = convo_object.insert_conversation(inv_id, time_spoken, persona, text) + self.insert_handler(ui_text) - new_element = f"{persona}: [{text}]\n\n" # print(f'Added: {time_spoken} - {new_element}') - transcript.append((new_element, time_spoken, convo_id)) + transcript.append((convo_text, time_spoken, convo_id)) # if (persona.lower() == 'assistant'): # print(f'Assistant Transcript length after completion: {len(transcript)}') diff --git a/app/transcribe/uicomp/selectable_text.py b/app/transcribe/uicomp/selectable_text.py index 82b2fa8..95eba89 100644 --- a/app/transcribe/uicomp/selectable_text.py +++ b/app/transcribe/uicomp/selectable_text.py @@ -9,7 +9,9 @@ class SelectableText(ctk.CTkFrame): def __init__(self, master=None, **kwargs): super().__init__(master, **kwargs) - self.text_widget = Text(self, wrap="word", undo=True, background='#252422') + self.text_widget = Text(self, wrap="word", undo=True, + background='#252422', font=("Arial", 20), + foreground='#639cdc',) self.scrollbar = Scrollbar(self, command=self.text_widget.yview) self.text_widget.config(yscrollcommand=self.scrollbar.set) @@ -22,6 +24,11 @@ def __init__(self, master=None, **kwargs): # Handler for double click # self.text_widget.bind("", self.on_double_click) + def clear_all_text(self): + self.text_widget.configure(state="normal") + self.text_widget.delete("1.0", END) + self.text_widget.configure(state="disabled") + def on_text_select(self, event): """Handler for left mouse click """ @@ -100,6 +107,21 @@ def add_text_to_bottom(self, input_text: str): self.text_widget.insert(END, input_text + "\n") self.text_widget.configure(state="disabled") + def delete_last_3_rows(self): + self.text_widget.configure(state="normal") + last_index = self.text_widget.index("end-1c linestart") + second_last_index = self.text_widget.index("%s -1 lines" % last_index) + third_last_index = self.text_widget.index("%s -1 lines" % second_last_index) + self.text_widget.delete(third_last_index, "end-1c") + self.text_widget.configure(state="normal") + + def delete_last_2_row(self): + self.text_widget.configure(state="normal") + last_index = self.text_widget.index("end-1c linestart") + second_last_index = self.text_widget.index("%s -1 lines" % last_index) + self.text_widget.delete(second_last_index, "end-1c") + self.text_widget.configure(state="disabled") + if __name__ == "__main__": ctk.set_appearance_mode("dark") From 1f636ae68747eebafba23f6873e0327ad7c3e20e Mon Sep 17 00:00:00 2001 From: vivek Date: Fri, 24 May 2024 12:27:06 -0400 Subject: [PATCH 10/19] misc --- app/transcribe/conversation.py | 3 ++- app/transcribe/uicomp/selectable_text.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/transcribe/conversation.py b/app/transcribe/conversation.py index e9c2fa6..e698ecd 100644 --- a/app/transcribe/conversation.py +++ b/app/transcribe/conversation.py @@ -98,7 +98,8 @@ def update_conversation(self, persona: str, # print(f'Removed: {prev_element}') # print(f'Update DB: {inv_id} - {time_spoken} - {persona} - {text}') convo_object.update_conversation(convo_id, text) - self.update_handler(ui_text) + if persona.lower() != 'assistant': + self.update_handler(ui_text) else: if self._initialized: # Insert in DB diff --git a/app/transcribe/uicomp/selectable_text.py b/app/transcribe/uicomp/selectable_text.py index 95eba89..c1a0ffb 100644 --- a/app/transcribe/uicomp/selectable_text.py +++ b/app/transcribe/uicomp/selectable_text.py @@ -108,6 +108,8 @@ def add_text_to_bottom(self, input_text: str): self.text_widget.configure(state="disabled") def delete_last_3_rows(self): + """Delete last 3 rows of text + """ self.text_widget.configure(state="normal") last_index = self.text_widget.index("end-1c linestart") second_last_index = self.text_widget.index("%s -1 lines" % last_index) @@ -116,6 +118,8 @@ def delete_last_3_rows(self): self.text_widget.configure(state="normal") def delete_last_2_row(self): + """Delete last 2 rows of text + """ self.text_widget.configure(state="normal") last_index = self.text_widget.index("end-1c linestart") second_last_index = self.text_widget.index("%s -1 lines" % last_index) From d6302bc5997d7266488c5f01c19521ef684207b6 Mon Sep 17 00:00:00 2001 From: vivek Date: Fri, 24 May 2024 12:44:59 -0400 Subject: [PATCH 11/19] misc --- app/transcribe/appui.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/transcribe/appui.py b/app/transcribe/appui.py index 4571688..ccf5358 100644 --- a/app/transcribe/appui.py +++ b/app/transcribe/appui.py @@ -62,6 +62,10 @@ def update_initial_transcripts(self): self.global_vars.convo.set_handlers(self.update_last_row, self.transcript_text.add_text_to_bottom) + def clear_transcript(self): + self.global_vars.transcriber.clear_transcriber_context(self.global_vars.audio_queue) + self.transcript_text.clear_all_text() + def create_ui_components(self, config: dict): """Create all UI components """ @@ -205,8 +209,7 @@ def create_ui_components(self, config: dict): m.add_command(label="Generate response for selected text", command=self.get_response_selected_now) m.add_command(label="Save Transcript to File", command=self.save_file) - m.add_command(label="Clear Audio Transcript", command=lambda: - self.global_vars.transcriber.clear_transcriber_context(self.global_vars.audio_queue)) + m.add_command(label="Clear Audio Transcript", command=self.clear_transcript) m.add_command(label="Copy Transcript to Clipboard", command=self.copy_to_clipboard) m.add_separator() m.add_command(label="Quit", command=self.quit) @@ -282,8 +285,7 @@ def create_menus(self): self.editmenu = tk.Menu(menubar, tearoff=False) # Add a "Clear Audio Transcript" menu item to the file menu - self.editmenu.add_command(label="Clear Audio Transcript", command=lambda: - self.global_vars.transcriber.clear_transcriber_context(self.global_vars.audio_queue)) + self.editmenu.add_command(label="Clear Audio Transcript", command=self.clear_transcript) # Add a "Copy To Clipboard" menu item to the file menu self.editmenu.add_command(label="Copy Transcript to Clipboard", @@ -291,11 +293,11 @@ def create_menus(self): # Add "Disable Speaker" menu item to file menu self.editmenu.add_command(label="Disable Speaker", - command=self.enable_disable_speaker()) + command=self.enable_disable_speaker) # Add "Disable Microphone" menu item to file menu self.editmenu.add_command(label="Disable Microphone", - command=self.enable_disable_microphone()) + command=self.enable_disable_microphone) # Add the edit menu to the menu bar menubar.add_cascade(label="Edit", menu=self.editmenu) @@ -313,11 +315,11 @@ def create_menus(self): def set_audio_device_menus(self, config): if config['General']['disable_speaker']: print('[INFO] Disabling Speaker') - self.enable_disable_speaker() + self.enable_disable_speaker() if config['General']['disable_mic']: print('[INFO] Disabling Microphone') - self.enable_disable_microphone() + self.enable_disable_microphone() def copy_to_clipboard(self): """Copy transcription text data to clipboard. From a91a3314b367998fb0e119dc121b58d13533fea4 Mon Sep 17 00:00:00 2001 From: vivek Date: Sun, 26 May 2024 10:13:30 -0400 Subject: [PATCH 12/19] display --- app/transcribe/appui.py | 14 +++++++-- app/transcribe/conversation.py | 2 +- app/transcribe/uicomp/selectable_text.py | 39 +++++++++++++++++++----- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/app/transcribe/appui.py b/app/transcribe/appui.py index ccf5358..032fc7f 100644 --- a/app/transcribe/appui.py +++ b/app/transcribe/appui.py @@ -46,9 +46,19 @@ def start(self): """ self.mainloop() - def update_last_row(self, input_text: str): - self.transcript_text.delete_last_3_rows() + def update_last_row(self, speaker: str, input_text: str): + # Update the line for this speaker + + # Delete row starting with speaker + self.transcript_text.delete_row_starting_with(start_text=speaker) + self.transcript_text.replace_multiple_newlines() + + # Add new line to end, since it was cleared by previous operation + self.transcript_text.add_text_to_bottom('\n') + + # Add a new row to the bottom with new text self.transcript_text.add_text_to_bottom(input_text) + self.transcript_text.scroll_to_bottom() def update_initial_transcripts(self): diff --git a/app/transcribe/conversation.py b/app/transcribe/conversation.py index e698ecd..d0b1e19 100644 --- a/app/transcribe/conversation.py +++ b/app/transcribe/conversation.py @@ -99,7 +99,7 @@ def update_conversation(self, persona: str, # print(f'Update DB: {inv_id} - {time_spoken} - {persona} - {text}') convo_object.update_conversation(convo_id, text) if persona.lower() != 'assistant': - self.update_handler(ui_text) + self.update_handler(persona, ui_text) else: if self._initialized: # Insert in DB diff --git a/app/transcribe/uicomp/selectable_text.py b/app/transcribe/uicomp/selectable_text.py index c1a0ffb..39eacd3 100644 --- a/app/transcribe/uicomp/selectable_text.py +++ b/app/transcribe/uicomp/selectable_text.py @@ -107,15 +107,33 @@ def add_text_to_bottom(self, input_text: str): self.text_widget.insert(END, input_text + "\n") self.text_widget.configure(state="disabled") - def delete_last_3_rows(self): - """Delete last 3 rows of text - """ + def delete_row_starting_with(self, start_text: str): + """Delete the row that starts with the given text.""" self.text_widget.configure(state="normal") - last_index = self.text_widget.index("end-1c linestart") - second_last_index = self.text_widget.index("%s -1 lines" % last_index) - third_last_index = self.text_widget.index("%s -1 lines" % second_last_index) - self.text_widget.delete(third_last_index, "end-1c") + last_line_index = int(self.text_widget.index('end-1c').split('.')[0]) + + for line_number in range(last_line_index, 0, -1): + line_start = f"{line_number}.0" + line_end = f"{line_number}.end" + line_text = self.text_widget.get(line_start, line_end).strip() + if line_text.startswith(start_text): + self.text_widget.delete(line_start, f"{line_number + 1}.0") + break + + self.text_widget.configure(state="disabled") + + def replace_multiple_newlines(self): + """Replace multiple consecutive lines with only newline characters with a single newline character. + """ self.text_widget.configure(state="normal") + current_index = "1.0" + while True: + current_index = self.text_widget.search("\n\n\n", current_index, END) + if not current_index: + break + next_index = self.text_widget.index(f"{current_index} + 1c") + self.text_widget.delete(current_index, next_index) + self.text_widget.configure(state="disabled") def delete_last_2_row(self): """Delete last 2 rows of text @@ -126,6 +144,13 @@ def delete_last_2_row(self): self.text_widget.delete(second_last_index, "end-1c") self.text_widget.configure(state="disabled") + def get_text_last_3_rows(self) -> str: + last_index = self.text_widget.index("end-1c linestart") + second_last_index = self.text_widget.index("%s -1 lines" % last_index) + third_last_index = self.text_widget.index("%s -1 lines" % second_last_index) + line_text = self.text_widget.get(third_last_index, "end-1c") + return line_text + if __name__ == "__main__": ctk.set_appearance_mode("dark") From 38e1028440cff16930bc10e348dba62abf1bcb5d Mon Sep 17 00:00:00 2001 From: vivek Date: Sun, 26 May 2024 10:37:01 -0400 Subject: [PATCH 13/19] misc --- app/transcribe/appui.py | 2 +- app/transcribe/gpt_responder.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/transcribe/appui.py b/app/transcribe/appui.py index 032fc7f..a01a95f 100644 --- a/app/transcribe/appui.py +++ b/app/transcribe/appui.py @@ -742,7 +742,7 @@ def update_response_ui(responder: gr.GPTResponder, textbox.see("end") update_interval = int(update_interval_slider.get()) - # responder.update_response_interval(update_interval) + responder.update_response_interval(update_interval) update_interval_slider_label.configure(text=f'LLM Response interval: ' f'{update_interval} seconds') diff --git a/app/transcribe/gpt_responder.py b/app/transcribe/gpt_responder.py index 611033d..6fa3775 100644 --- a/app/transcribe/gpt_responder.py +++ b/app/transcribe/gpt_responder.py @@ -41,7 +41,7 @@ def __init__(self, logger.info(GPTResponder.__name__) # This var is used by UI to populate the response textbox self.response = prompts.INITIAL_RESPONSE - self.llm_response_interval = 2 + self.llm_response_interval = config['General']['llm_response_interval'] self.conversation = convo self.config = config self.save_response_to_file = save_to_file @@ -169,7 +169,6 @@ def generate_response_from_transcript_no_check(self) -> str: length=constants.MAX_TRANSCRIPTION_PHRASES_FOR_LLM) last_convo_id = int(multiturn_prompt_content[-1][2]) multiturn_prompt_api_message = prompts.create_multiturn_prompt(multiturn_prompt_content) - collected_messages = self._get_llm_response(multiturn_prompt_api_message, temperature, timeout) self._insert_response_in_db(last_convo_id, collected_messages) @@ -344,6 +343,8 @@ def respond_to_transcriber(self, transcriber): remaining_time = self.llm_response_interval - execution_time if remaining_time > 0: + # print(f'llm_response_interval: {self.llm_response_interval}, execution time: {execution_time}') + # print(f'Sleeping for a response for duration: {remaining_time}') time.sleep(remaining_time) else: time.sleep(self.llm_response_interval) From e054b38d58c36d4b82548000427378cc463a8c10 Mon Sep 17 00:00:00 2001 From: vivek Date: Sun, 26 May 2024 11:32:10 -0400 Subject: [PATCH 14/19] Get llmresponse from DB when a row is selected. --- app/transcribe/appui.py | 13 ++--- app/transcribe/conversation.py | 26 +++++++++- app/transcribe/db/llm_responses.py | 23 ++++++++- app/transcribe/uicomp/selectable_text.py | 63 +++++++++++------------- 4 files changed, 82 insertions(+), 43 deletions(-) diff --git a/app/transcribe/appui.py b/app/transcribe/appui.py index a01a95f..54a7005 100644 --- a/app/transcribe/appui.py +++ b/app/transcribe/appui.py @@ -62,7 +62,8 @@ def update_last_row(self, speaker: str, input_text: str): self.transcript_text.scroll_to_bottom() def update_initial_transcripts(self): - + """Set initial transcript in UI. + """ update_transcript_ui(self.global_vars.transcriber, self.transcript_text) update_response_ui(self.global_vars.responder, @@ -73,6 +74,8 @@ def update_initial_transcripts(self): self.transcript_text.add_text_to_bottom) def clear_transcript(self): + """Clear transcript from all places where it exists. + """ self.global_vars.transcriber.clear_transcriber_context(self.global_vars.audio_queue) self.transcript_text.clear_all_text() @@ -91,14 +94,11 @@ def create_ui_components(self, config: dict): self.create_menus() - # Speech to Text textbox - # TODO: Ref to transcript_textbox # Left side: SelectableTextComponent self.transcript_text: SelectableText = SelectableText(self.main_frame) self.transcript_text.pack(side="left", fill="both", expand=True, padx=10, pady=10) + self.transcript_text.set_callbacks(self.global_vars.convo.on_convo_select) # self.transcript_text.grid(row=0, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") - - # TODO: set the font, texcolor etc. # self.transcript_textbox = ctk.CTkTextbox(self.root, width=300, font=("Arial", UI_FONT_SIZE), # text_color='#FFFCF2', wrap="word") # self.transcript_textbox.grid(row=0, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") @@ -191,7 +191,8 @@ def create_ui_components(self, config: dict): # response_lang_label.grid(row=3, column=1, padx=10, pady=3, sticky="nw") response_lang = config['OpenAI']['response_lang'] - self.response_lang_combobox = ctk.CTkOptionMenu(self.bottom_frame, width=15, values=list(LANGUAGES_DICT.values())) + self.response_lang_combobox = ctk.CTkOptionMenu(self.bottom_frame, width=15, + values=list(LANGUAGES_DICT.values())) self.response_lang_combobox.set(response_lang) # self.response_lang_combobox.pack(side="left", padx=10) self.response_lang_combobox.grid(row=2, column=3, ipadx=60, padx=10, pady=3, sticky="ne") diff --git a/app/transcribe/conversation.py b/app/transcribe/conversation.py index d0b1e19..a932076 100644 --- a/app/transcribe/conversation.py +++ b/app/transcribe/conversation.py @@ -2,7 +2,7 @@ from heapq import merge import datetime import constants -from db import AppDB as appdb, conversation as convodb +from db import AppDB as appdb, conversation as convodb, llm_responses as llmrdb sys.path.append('../..') from tsutils import configuration # noqa: E402 pylint: disable=C0413 @@ -114,6 +114,30 @@ def update_conversation(self, persona: str, # print(f'Assistant Transcript length after completion: {len(transcript)}') self.last_update = datetime.datetime.utcnow() + def on_convo_select(self, input_text: str): + """Callback when a specific conversation is selected. + """ + print(f'convo: {input_text}') + end_speaker = input_text.find(':') + if end_speaker == -1: + return + persona = input_text[:end_speaker].strip() + print(persona) + transcript = self.transcript_data[persona] + for index, (first, _, third) in enumerate(transcript): + if first.strip() == input_text.strip(): + convo_id = third + print(convo_id) + if not convo_id: + return + + # Get LLM_response for this convo_id + # get_text_by_invocation_and_conversation + inv_id = appdb().get_invocation_id() + llmr_object: llmrdb.LLMResponses = appdb().get_object(llmrdb.TABLE_NAME) + response = llmr_object.get_text_by_invocation_and_conversation(inv_id, convo_id) + print(response) + def get_conversation(self, sources: list = None, length: int = 0) -> list: diff --git a/app/transcribe/db/llm_responses.py b/app/transcribe/db/llm_responses.py index cefc061..cbc6e6f 100644 --- a/app/transcribe/db/llm_responses.py +++ b/app/transcribe/db/llm_responses.py @@ -7,7 +7,7 @@ import datetime import sqlalchemy as sqldb -from sqlalchemy import Column, Integer, String, MetaData, DateTime, Engine, insert +from sqlalchemy import Column, Integer, String, MetaData, DateTime, Engine, insert, select from sqlalchemy.orm import Session, mapped_column, declarative_base, Mapped TABLE_NAME = 'LLMResponses' @@ -125,6 +125,27 @@ def insert_response(self, invocation_id: int, conversation_id: int, text: str) - return result.inserted_primary_key[0] + def get_text_by_invocation_and_conversation(self, invocation_id: int, conversation_id: int) -> str: + """ + Retrieves the text of a response based on the invocation_id and conversation_id. + + Args: + invocation_id (int): The ID of the related invocation. + conversation_id (int): The ID of the related conversation. + + Returns: + str: The text of the matching response or None if no match is found. + """ + stmt = select(self._db_table.c.Text).where( + self._db_table.c.InvocationId == invocation_id, + self._db_table.c.ConversationId == conversation_id + ) + + with Session(self.engine) as session: + result = session.execute(stmt).scalar() + + return result + def populate_data(self): """ Placeholder method for populating the table with initial data. diff --git a/app/transcribe/uicomp/selectable_text.py b/app/transcribe/uicomp/selectable_text.py index 39eacd3..72c7878 100644 --- a/app/transcribe/uicomp/selectable_text.py +++ b/app/transcribe/uicomp/selectable_text.py @@ -24,30 +24,42 @@ def __init__(self, master=None, **kwargs): # Handler for double click # self.text_widget.bind("", self.on_double_click) + # Define the tag for highlighting + self.text_widget.tag_configure("highlight", background="white") + self.on_text_click_cb = None + + def set_callbacks(self, onTextClick): + """Set callback handlers + """ + self.on_text_click_cb = onTextClick + def clear_all_text(self): + """Clear all text from the component + """ self.text_widget.configure(state="normal") self.text_widget.delete("1.0", END) self.text_widget.configure(state="disabled") - def on_text_select(self, event): - """Handler for left mouse click - """ - try: - selected_text = self.text_widget.get(SEL_FIRST, SEL_LAST) - print(f"Selected text: {selected_text}") + def on_text_click(self, event): + """Handle the click event on the Text widget.""" + # Remove the previous highlight + self.text_widget.tag_remove("highlight", "1.0", END) - index = self.text_widget.index("@%s,%s" % (event.x, event.y)) - line_number = int(index.split(".")[0]) + # Get the index of the clicked line + index = self.text_widget.index("@%s,%s" % (event.x, event.y)) + line_number = int(index.split(".")[0]) - # Get the text of the clicked line - line_start = f"{line_number}.0" - line_end = f"{line_number}.end" - line_text = self.text_widget.get(line_start, line_end).strip() + # Get the text of the clicked line + line_start = f"{line_number}.0" + line_end = f"{line_number}.end" + line_text = self.text_widget.get(line_start, line_end).strip() - # Trigger an event (print the line text) - print(f"Selected: {line_text}") - except: - pass # No selection + # Add the highlight tag to the clicked line + self.text_widget.tag_add("highlight", line_start, line_end) + self.on_text_click_cb(line_text) + + # Trigger an event (print the line text) + # print(f"Selected: {line_text}") def on_double_click(self, event): """Handler for double click @@ -60,25 +72,6 @@ def on_double_click(self, event): self.text_widget.see("insert") self.text_widget.focus() - def on_text_click(self, event): - """ - Handle the click event on the Text widget. - - Args: - event (tkinter.Event): The event object containing event details. - """ - # Get the index of the clicked line - index = self.text_widget.index("@%s,%s" % (event.x, event.y)) - line_number = int(index.split(".")[0]) - - # Get the text of the clicked line - line_start = f"{line_number}.0" - line_end = f"{line_number}.end" - line_text = self.text_widget.get(line_start, line_end).strip() - - # Trigger an event (print the line text) - print(f"Selected: {line_text}") - def scroll_to_top(self): """ Scroll the Text widget to the top. From 150f50b7ea0a13c5116b24a0a364597f1c2811e1 Mon Sep 17 00:00:00 2001 From: vivek Date: Sun, 26 May 2024 13:04:27 -0400 Subject: [PATCH 15/19] misc --- app/transcribe/appui.py | 16 +++++++++++----- app/transcribe/conversation.py | 12 ++++++++++-- app/transcribe/global_vars.py | 5 ++++- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/app/transcribe/appui.py b/app/transcribe/appui.py index 54a7005..5adb3d5 100644 --- a/app/transcribe/appui.py +++ b/app/transcribe/appui.py @@ -263,7 +263,6 @@ def show_context_menu(event): finally: m.grab_release() - # TODO: Ref to transcript_textbox self.transcript_text.bind("", show_context_menu) # self.grid_rowconfigure(0, weight=100) @@ -731,21 +730,28 @@ def update_response_ui(responder: gr.GPTResponder, if global_vars_module is None: global_vars_module = TranscriptionGlobals() + response = None # global_vars_module.responder.enabled --> This is continous response mode from LLM # global_vars_module.update_response_now --> Get Response now from LLM if global_vars_module.responder.enabled or global_vars_module.update_response_now: response = responder.response + if global_vars_module.previous_response is not None: + # User selection of previous response takes precedence over + # Automated ping of LLM Response + response = global_vars_module.previous_response + + if response: textbox.configure(state="normal") write_in_textbox(textbox, response) textbox.configure(state="disabled") textbox.see("end") - update_interval = int(update_interval_slider.get()) - responder.update_response_interval(update_interval) - update_interval_slider_label.configure(text=f'LLM Response interval: ' - f'{update_interval} seconds') + update_interval = int(update_interval_slider.get()) + responder.update_response_interval(update_interval) + update_interval_slider_label.configure(text=f'LLM Response interval: ' + f'{update_interval} seconds') textbox.after(300, update_response_ui, responder, textbox, update_interval_slider_label, update_interval_slider) diff --git a/app/transcribe/conversation.py b/app/transcribe/conversation.py index a932076..3910607 100644 --- a/app/transcribe/conversation.py +++ b/app/transcribe/conversation.py @@ -15,13 +15,14 @@ class Conversation: update_handler = None insert_handler = None - def __init__(self): + def __init__(self, context): self.transcript_data = {constants.PERSONA_SYSTEM: [], constants.PERSONA_YOU: [], constants.PERSONA_SPEAKER: [], constants.PERSONA_ASSISTANT: []} self.last_update: datetime.datetime = None self.initialize_conversation() + self.context = context def set_handlers(self, update, insert): self.update_handler = update @@ -120,15 +121,21 @@ def on_convo_select(self, input_text: str): print(f'convo: {input_text}') end_speaker = input_text.find(':') if end_speaker == -1: + self.context.previous_response = None return persona = input_text[:end_speaker].strip() print(persona) transcript = self.transcript_data[persona] - for index, (first, _, third) in enumerate(transcript): + bFound = False + for _, (first, _, third) in enumerate(transcript): if first.strip() == input_text.strip(): convo_id = third + bFound = True + break + print(convo_id) if not convo_id: + self.context.previous_response = None return # Get LLM_response for this convo_id @@ -136,6 +143,7 @@ def on_convo_select(self, input_text: str): inv_id = appdb().get_invocation_id() llmr_object: llmrdb.LLMResponses = appdb().get_object(llmrdb.TABLE_NAME) response = llmr_object.get_text_by_invocation_and_conversation(inv_id, convo_id) + self.context.previous_response = response if response else 'No LLM response corresponding to this row' print(response) def get_conversation(self, diff --git a/app/transcribe/global_vars.py b/app/transcribe/global_vars.py index 5120bc8..d918629 100644 --- a/app/transcribe/global_vars.py +++ b/app/transcribe/global_vars.py @@ -31,6 +31,9 @@ class TranscriptionGlobals(Singleton.Singleton): update_response_now: bool = False # Read response in voice read_response: bool = False + # LLM Response to an earlier conversation + # This is populated when user clicks on text in transcript textbox + previous_response: str = None # editmenu: tk.Menu = None # filemenu: tk.Menu = None # update_interval_slider_label: ctk.CTkLabel = None @@ -53,7 +56,7 @@ def __init__(self): return if self.audio_queue is None: self.audio_queue = queue.Queue() - self.convo = conversation.Conversation() + self.convo = conversation.Conversation(self) self.start = datetime.datetime.now() self.task_worker = task_queue.TaskQueue() self.data_dir = utilities.get_data_path(app_name='Transcribe') From 7693f2ad8992711fcc64cdf2487e0d324d643632 Mon Sep 17 00:00:00 2001 From: vivek Date: Sun, 26 May 2024 13:41:04 -0400 Subject: [PATCH 16/19] misc --- app/transcribe/appui.py | 48 ++------------------------- app/transcribe/conversation.py | 6 +--- app/transcribe/global_vars.py | 8 ----- app/transcribe/main.py | 60 ---------------------------------- 4 files changed, 3 insertions(+), 119 deletions(-) diff --git a/app/transcribe/appui.py b/app/transcribe/appui.py index 5adb3d5..dead4a0 100644 --- a/app/transcribe/appui.py +++ b/app/transcribe/appui.py @@ -76,8 +76,8 @@ def update_initial_transcripts(self): def clear_transcript(self): """Clear transcript from all places where it exists. """ - self.global_vars.transcriber.clear_transcriber_context(self.global_vars.audio_queue) self.transcript_text.clear_all_text() + self.global_vars.transcriber.clear_transcriber_context(self.global_vars.audio_queue) def create_ui_components(self, config: dict): """Create all UI components @@ -98,10 +98,6 @@ def create_ui_components(self, config: dict): self.transcript_text: SelectableText = SelectableText(self.main_frame) self.transcript_text.pack(side="left", fill="both", expand=True, padx=10, pady=10) self.transcript_text.set_callbacks(self.global_vars.convo.on_convo_select) - # self.transcript_text.grid(row=0, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") - # self.transcript_textbox = ctk.CTkTextbox(self.root, width=300, font=("Arial", UI_FONT_SIZE), - # text_color='#FFFCF2', wrap="word") - # self.transcript_textbox.grid(row=0, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") # Right side self.right_frame = ctk.CTkFrame(self.main_frame) @@ -113,13 +109,9 @@ def create_ui_components(self, config: dict): font=("Arial", UI_FONT_SIZE), text_color='#639cdc', wrap="word") - # self.response_textbox.grid(row=0, column=2, padx=10, pady=3, sticky="nsew") self.response_textbox.pack(fill="both", expand=True) self.response_textbox.insert("0.0", prompts.INITIAL_RESPONSE) - # Bind the event to enforce minimum width - # self.right_frame.bind("", self.enforce_minimum_width_of_response) - # Bottom Frame for buttons self.bottom_frame = ctk.CTkFrame(self, border_color="white", border_width=2) self.bottom_frame.pack(side="bottom", fill="both", pady=10) @@ -128,40 +120,28 @@ def create_ui_components(self, config: dict): b_text = "Suggest Responses Continuously" if not response_enabled else "Do Not Suggest Responses Continuously" self.continuous_response_button = ctk.CTkButton(self.bottom_frame, text=b_text) self.continuous_response_button.grid(row=0, column=4, padx=10, pady=3, sticky="nsew") - # self.continuous_response_button.grid(row=1, column=2, padx=10, pady=3, sticky="nsew") - # self.continuous_response_button.pack(side="left", padx=10) self.continuous_response_button.configure(command=self.freeze_unfreeze) self.response_now_button = ctk.CTkButton(self.bottom_frame, text="Suggest Response Now") self.response_now_button.grid(row=1, column=4, padx=10, pady=3, sticky="nsew") - # self.response_now_button.grid(row=2, column=2, padx=10, pady=3, sticky="nsew") - # self.response_now_button.pack(side="left", padx=10) self.response_now_button.configure(command=self.get_response_now) self.read_response_now_button = ctk.CTkButton(self.bottom_frame, text="Suggest Response and Read") - # self.read_response_now_button.pack(side="left", padx=10) self.read_response_now_button.grid(row=2, column=4, padx=10, pady=3, sticky="nsew") - # self.read_response_now_button.grid(row=3, column=2, padx=10, pady=3, sticky="nsew") self.read_response_now_button.configure(command=self.update_response_ui_and_read_now) self.summarize_button = ctk.CTkButton(self.bottom_frame, text="Summarize") - # self.summarize_button.pack(side="left", padx=10) self.summarize_button.grid(row=3, column=4, padx=10, pady=3, sticky="nsew") - # self.summarize_button.grid(row=4, column=2, padx=10, pady=3, sticky="nsew") self.summarize_button.configure(command=self.summarize) # Continuous LLM Response label, and slider self.update_interval_slider_label = ctk.CTkLabel(self.bottom_frame, text="", font=("Arial", 12), text_color="#FFFCF2") -# self.update_interval_slider_label.pack(side="left", padx=10) self.update_interval_slider_label.grid(row=0, column=0, columnspan=4, padx=10, pady=3, sticky="nsew") - # self.update_interval_slider_label.grid(row=1, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") self.update_interval_slider = ctk.CTkSlider(self.bottom_frame, from_=1, to=30, width=300, # height=5, number_of_steps=29) self.update_interval_slider.set(config['General']['llm_response_interval']) self.update_interval_slider.grid(row=1, column=0, columnspan=4, padx=10, pady=3, sticky="nsew") - # self.update_interval_slider.grid(row=2, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") - # self.update_interval_slider.pack(side="left", padx=10) self.update_interval_slider.configure(command=self.update_interval_slider_value) label_text = f'LLM Response interval: {int(self.update_interval_slider.get())} seconds' @@ -171,46 +151,35 @@ def create_ui_components(self, config: dict): audio_lang_label = ctk.CTkLabel(self.bottom_frame, text="Audio Lang: ", font=("Arial", 12), text_color="#FFFCF2") - # audio_lang_label.pack(side="left", padx=10) audio_lang_label.grid(row=2, column=0, padx=10, pady=3, sticky='nw') audio_lang = config['OpenAI']['audio_lang'] self.audio_lang_combobox = ctk.CTkOptionMenu(self.bottom_frame, width=15, values=list(LANGUAGES_DICT.values())) self.audio_lang_combobox.set(audio_lang) - # self.audio_lang_combobox.pack(side="left", padx=10) self.audio_lang_combobox.grid(row=2, column=1, ipadx=60, padx=10, pady=3, sticky="ne") - # self.audio_lang_combobox.grid(row=3, column=0, ipadx=60, padx=10, pady=3, sticky="ne") self.audio_lang_combobox.configure(command=self.set_audio_language) # LLM Response language selection label, dropdown response_lang_label = ctk.CTkLabel(self.bottom_frame, text="Response Lang: ", font=("Arial", 12), text_color="#FFFCF2") - # response_lang_label.pack(side="left", padx=10) response_lang_label.grid(row=2, column=2, padx=10, pady=3, sticky="nw") - # response_lang_label.grid(row=3, column=1, padx=10, pady=3, sticky="nw") response_lang = config['OpenAI']['response_lang'] self.response_lang_combobox = ctk.CTkOptionMenu(self.bottom_frame, width=15, values=list(LANGUAGES_DICT.values())) self.response_lang_combobox.set(response_lang) - # self.response_lang_combobox.pack(side="left", padx=10) self.response_lang_combobox.grid(row=2, column=3, ipadx=60, padx=10, pady=3, sticky="ne") - # self.response_lang_combobox.grid(row=3, column=1, ipadx=60, padx=10, pady=3, sticky="ne") self.response_lang_combobox.configure(command=self.set_response_language) self.github_link = ctk.CTkLabel(self.bottom_frame, text="Star the Github Repo", text_color="#639cdc", cursor="hand2") - # self.github_link.pack(side="left", padx=10) self.github_link.grid(row=3, column=0, padx=10, pady=3, sticky="wn") - # self.github_link.grid(row=4, column=0, padx=10, pady=3, sticky="wn") self.github_link.bind('', lambda e: self.open_link('https://github.com/vivekuppal/transcribe?referer=desktop')) self.issue_link = ctk.CTkLabel(self.bottom_frame, text="Report an issue", text_color="#639cdc", cursor="hand2") - # self.issue_link.pack(side="left", padx=10) self.issue_link.grid(row=3, column=1, padx=10, pady=3, sticky="wn") - # self.issue_link.grid(row=4, column=1, padx=10, pady=3, sticky="wn") self.issue_link.bind('', lambda e: self.open_link( 'https://github.com/vivekuppal/transcribe/issues/new?referer=desktop')) @@ -601,16 +570,6 @@ def set_response_language(self, lang: str): except Exception as e: logger.error(f"Error setting response language: {e}") - # def enforce_minimum_width_of_response(self, event): - # widthm = self.main_frame.winfo_width() - # widthr = self.right_frame.winfo_width() - # widthl = self.bottom_frame.winfo_width() - # # if self.response_textbox.winfo_width() < self.min_response_textbox_width: - # # self.response_textbox.configure(width=self.min_response_textbox_width) - # # self.right_frame.configure(width=self.min_response_textbox_width) - # # self.transcript_text.configure(width=widthm - self.min_response_textbox_width) - # print(f'Widths as observed: all {widthm}, right: {widthr}, left: {widthl}') - def popup_msg_no_close_threaded(title, msg): """Create a pop up with no close button. @@ -713,9 +672,6 @@ def update_transcript_ui(transcriber: AudioTranscriber, textbox: SelectableText) textbox.scroll_to_bottom() last_transcript_ui_update_time = datetime.datetime.utcnow() - # textbox.after(constants.TRANSCRIPT_UI_UPDATE_DELAY_DURATION_MS, - # update_transcript_ui, transcriber, textbox) - def update_response_ui(responder: gr.GPTResponder, textbox: ctk.CTkTextbox, @@ -751,7 +707,7 @@ def update_response_ui(responder: gr.GPTResponder, update_interval = int(update_interval_slider.get()) responder.update_response_interval(update_interval) update_interval_slider_label.configure(text=f'LLM Response interval: ' - f'{update_interval} seconds') + f'{update_interval} seconds') textbox.after(300, update_response_ui, responder, textbox, update_interval_slider_label, update_interval_slider) diff --git a/app/transcribe/conversation.py b/app/transcribe/conversation.py index 3910607..36563c7 100644 --- a/app/transcribe/conversation.py +++ b/app/transcribe/conversation.py @@ -102,7 +102,7 @@ def update_conversation(self, persona: str, if persona.lower() != 'assistant': self.update_handler(persona, ui_text) else: - if self._initialized: + if self._initialized and persona != constants.PERSONA_SYSTEM and persona != constants.PERSONA_ASSISTANT: # Insert in DB # print(f'Add to DB: {inv_id} - {time_spoken} - {persona} - {text}') convo_id = convo_object.insert_conversation(inv_id, time_spoken, persona, text) @@ -118,7 +118,6 @@ def update_conversation(self, persona: str, def on_convo_select(self, input_text: str): """Callback when a specific conversation is selected. """ - print(f'convo: {input_text}') end_speaker = input_text.find(':') if end_speaker == -1: self.context.previous_response = None @@ -126,14 +125,11 @@ def on_convo_select(self, input_text: str): persona = input_text[:end_speaker].strip() print(persona) transcript = self.transcript_data[persona] - bFound = False for _, (first, _, third) in enumerate(transcript): if first.strip() == input_text.strip(): convo_id = third - bFound = True break - print(convo_id) if not convo_id: self.context.previous_response = None return diff --git a/app/transcribe/global_vars.py b/app/transcribe/global_vars.py index d918629..601e599 100644 --- a/app/transcribe/global_vars.py +++ b/app/transcribe/global_vars.py @@ -4,8 +4,6 @@ import os import queue import datetime -import tkinter as tk -import customtkinter as ctk from audio_transcriber import AudioTranscriber import audio_player sys.path.append('../..') @@ -26,7 +24,6 @@ class TranscriptionGlobals(Singleton.Singleton): transcriber: AudioTranscriber = None # Global for responses from openAI API responder = None - # freeze_button: ctk.CTkButton = None # Update_response_now is true when we are waiting for a one time immediate response to query update_response_now: bool = False # Read response in voice @@ -34,11 +31,6 @@ class TranscriptionGlobals(Singleton.Singleton): # LLM Response to an earlier conversation # This is populated when user clicks on text in transcript textbox previous_response: str = None - # editmenu: tk.Menu = None - # filemenu: tk.Menu = None - # update_interval_slider_label: ctk.CTkLabel = None - # response_textbox: ctk.CTkTextbox = None - # transcript_textbox: ctk.CTkTextbox = None start: datetime.datetime = None task_worker = None main_window = None diff --git a/app/transcribe/main.py b/app/transcribe/main.py index 0749fd3..ada299a 100644 --- a/app/transcribe/main.py +++ b/app/transcribe/main.py @@ -2,12 +2,10 @@ import time import atexit import app_utils as au -# import customtkinter as ctk from args import create_args, update_args_config, handle_args_batch_tasks from global_vars import T_GLOBALS from appui import AppUI sys.path.append('../..') -# import ui # noqa: E402 pylint: disable=C0413 from tsutils import configuration # noqa: E402 pylint: disable=C0413 from tsutils import app_logging as al # noqa: E402 pylint: disable=C0413 from tsutils import utilities as u # noqa: E402 pylint: disable=C0413 @@ -44,8 +42,6 @@ def main(): f'{data_dir}/logs/mic.wav.bak']) # Convert raw audio files to real wav file format when program exits - # atexit.register(global_vars.user_audio_recorder.write_wav_data_to_file) - # atexit.register(global_vars.speaker_audio_recorder.write_wav_data_to_file) atexit.register(au.shutdown, global_vars) user_stop_func = global_vars.user_audio_recorder.record_audio(global_vars.audio_queue) @@ -56,8 +52,6 @@ def main(): speaker_stop_func = global_vars.speaker_audio_recorder.record_audio(global_vars.audio_queue) global_vars.speaker_audio_recorder.stop_record_func = speaker_stop_func -# update_audio_devices(global_vars, config) - # Transcriber needs to be created before handling batch tasks which include batch # transcription. This order of initialization results in initialization of Mic, Speaker # as well which is not necessary for some batch tasks. @@ -68,68 +62,14 @@ def main(): log_listener = al.initiate_log(config=config) aui = AppUI(config=config) - # root = ctk.CTk() - # T_GLOBALS.main_window = root - # ui_cb = ui.UICallbacks() - # ui_components = ui.create_ui_components(root, config=config) - # global_vars.transcript_textbox = ui_components[0] - # global_vars.response_textbox = ui_components[1] - # update_interval_slider = ui_components[2] - # global_vars.update_interval_slider_label = ui_components[3] - # global_vars.freeze_button = ui_components[4] - # audio_lang_combobox = ui_components[5] - # response_lang_combobox = ui_components[6] - # global_vars.filemenu = ui_components[7] - # response_now_button = ui_components[8] - # read_response_now_button = ui_components[9] - # global_vars.editmenu = ui_components[10] - # github_link = ui_components[11] - # issue_link = ui_components[12] - # summarize_button = ui_components[13] - - # disable speaker/microphone on startup - # if config['General']['disable_speaker']: - # print('[INFO] Disabling Speaker') - # ui_cb.enable_disable_speaker(global_vars.editmenu) - - # if config['General']['disable_mic']: - # print('[INFO] Disabling Microphone') - # ui_cb.enable_disable_microphone(global_vars.editmenu) - au.initiate_app_threads(global_vars=global_vars, config=config) print("READY") - # root.grid_rowconfigure(0, weight=100) - # root.grid_rowconfigure(1, weight=1) - # root.grid_rowconfigure(2, weight=1) - # root.grid_rowconfigure(3, weight=1) - # root.grid_columnconfigure(0, weight=2) - # root.grid_columnconfigure(1, weight=1) - - # global_vars.freeze_button.configure(command=ui_cb.freeze_unfreeze) - # response_now_button.configure(command=ui_cb.get_response_now) - # read_response_now_button.configure(command=ui_cb.update_response_ui_and_read_now) - # summarize_button.configure(command=ui_cb.summarize) - # update_interval_slider.configure(command=ui_cb.update_interval_slider_value) - # label_text = f'LLM Response interval: {int(update_interval_slider.get())} seconds' - # global_vars.update_interval_slider_label.configure(text=label_text) - # audio_lang_combobox.configure(command=ui_cb.set_audio_language) - # response_lang_combobox.configure(command=ui_cb.set_response_language) # Set the response lang in STT Model. global_vars.transcriber.stt_model.set_lang(config['OpenAI']['audio_lang']) - # github_link.bind('', lambda e: - # ui_cb.open_link('https://github.com/vivekuppal/transcribe?referer=desktop')) - # issue_link.bind('', lambda e: ui_cb.open_link( - # 'https://github.com/vivekuppal/transcribe/issues/new?referer=desktop')) - - # ui.update_transcript_ui(global_vars.transcriber, global_vars.transcript_textbox) - # ui.update_response_ui(global_vars.responder, global_vars.response_textbox, - # global_vars.update_interval_slider_label, update_interval_slider) - aui.update_initial_transcripts() aui.start() - # root.mainloop() log_listener.stop() From e1b49f98d553fe9db8048d61a83b596c97511625 Mon Sep 17 00:00:00 2001 From: vivek Date: Sun, 26 May 2024 13:42:50 -0400 Subject: [PATCH 17/19] misc --- app/transcribe/ui.py | 623 ------------------------------------------- 1 file changed, 623 deletions(-) delete mode 100644 app/transcribe/ui.py diff --git a/app/transcribe/ui.py b/app/transcribe/ui.py deleted file mode 100644 index 910c62e..0000000 --- a/app/transcribe/ui.py +++ /dev/null @@ -1,623 +0,0 @@ -# import threading -# import datetime -# import time -# import tkinter as tk -# import webbrowser -# import pyperclip -# import customtkinter as ctk -# from tktooltip import ToolTip -# from audio_transcriber import AudioTranscriber -# import prompts -# from global_vars import TranscriptionGlobals, T_GLOBALS -# import constants -# import gpt_responder as gr -# from tsutils.language import LANGUAGES_DICT -# from tsutils import utilities -# from tsutils import app_logging as al -# from tsutils import configuration - - -# logger = al.get_module_logger(al.UI_LOGGER) -# UI_FONT_SIZE = 20 -# Order of initialization can be unpredictable in python based on where imports are placed. -# Setting it to None so comparison is deterministic in update_transcript_ui method -# last_transcript_ui_update_time: datetime.datetime = None -# global_vars_module: TranscriptionGlobals = T_GLOBALS -# pop_up = None - - -# class UICallbacks: -# """All callbacks for UI""" - -# global_vars: TranscriptionGlobals -# ui_filename: str = None - -# def __init__(self): -# self.global_vars = TranscriptionGlobals() - -# def copy_to_clipboard(self): -# """Copy transcription text data to clipboard. -# Does not include responses from assistant. -# """ -# logger.info(UICallbacks.copy_to_clipboard.__name__) -# self.capture_action("Copy transcript to clipboard") -# try: -# pyperclip.copy(self.global_vars.transcriber.get_transcript()) -# except Exception as e: -# logger.error(f"Error copying to clipboard: {e}") - -# def save_file(self): -# """Save transcription text data to file. -# Does not include responses from assistant. -# """ -# logger.info(UICallbacks.save_file.__name__) -# filename = ctk.filedialog.asksaveasfilename(defaultextension='.txt', -# title='Save Transcription', -# filetypes=[("Text Files", "*.txt")]) -# self.capture_action(f'Save transcript to file:{filename}') -# if not filename: -# return -# try: -# with open(file=filename, mode="w", encoding='utf-8') as file_handle: -# file_handle.write(self.global_vars.transcriber.get_transcript()) -# except Exception as e: -# logger.error(f"Error saving file {filename}: {e}") - -# def freeze_unfreeze(self): -# """Respond to start / stop of seeking responses from openAI API -# """ -# logger.info(UICallbacks.freeze_unfreeze.__name__) -# try: -# # Invert the state -# self.global_vars.responder.enabled = not self.global_vars.responder.enabled -# self.capture_action(f'{"Enabled " if self.global_vars.responder.enabled else "Disabled "} continuous LLM responses') -# self.freeze_button.configure( -# text="Suggest Responses Continuously" if not self.global_vars.responder.enabled else "Do Not Suggest Responses Continuously" -# ) -# except Exception as e: -# logger.error(f"Error toggling responder state: {e}") - -# def enable_disable_speaker(self, editmenu): -# """Toggles the state of speaker -# """ -# try: -# self.global_vars.speaker_audio_recorder.enabled = not self.global_vars.speaker_audio_recorder.enabled -# editmenu.entryconfigure(2, label="Disable Speaker" if self.global_vars.speaker_audio_recorder.enabled else "Enable Speaker") -# self.capture_action(f'{"Enabled " if self.global_vars.speaker_audio_recorder.enabled else "Disabled "} speaker input') -# except Exception as e: -# logger.error(f"Error toggling speaker state: {e}") - -# def enable_disable_microphone(self, editmenu): -# """Toggles the state of microphone -# """ -# try: -# self.global_vars.user_audio_recorder.enabled = not self.global_vars.user_audio_recorder.enabled -# editmenu.entryconfigure(3, label="Disable Microphone" if self.global_vars.user_audio_recorder.enabled else "Enable Microphone") -# self.capture_action(f'{"Enabled " if self.global_vars.user_audio_recorder.enabled else "Disabled "} microphone input') -# except Exception as e: -# logger.error(f"Error toggling microphone state: {e}") - -# def update_interval_slider_value(self, slider_value): -# """Update interval slider label to match the slider value -# Update the config value -# """ -# try: -# config_obj = configuration.Config() -# # Save config -# altered_config = {'General': {'llm_response_interval': int(slider_value)}} -# config_obj.add_override_value(altered_config) - -# label_text = f'LLM Response interval: {int(slider_value)} seconds' -# self.global_vars.update_interval_slider_label.configure(text=label_text) -# self.capture_action(f'Update LLM response interval to {int(slider_value)}') -# except Exception as e: -# logger.error(f"Error updating slider value: {e}") - -# def get_response_now(self): -# """Get response from LLM right away -# Update the Response UI with the response -# """ -# if self.global_vars.update_response_now: -# # We are already in the middle of getting a response -# return -# # We need a separate thread to ensure UI is responsive as responses are -# # streamed back. Without the thread UI appears stuck as we stream the -# # responses back -# self.capture_action('Get LLM response now') -# response_ui_thread = threading.Thread(target=self.get_response_now_threaded, -# name='GetResponseNow') -# response_ui_thread.daemon = True -# response_ui_thread.start() - -# def get_response_selected_now_threaded(self, text: str): -# """Update response UI in a separate thread -# """ -# self.update_response_ui_threaded(lambda: self.global_vars.responder.generate_response_for_selected_text(text)) - -# def get_response_now_threaded(self): -# """Update response UI in a separate thread -# """ -# self.update_response_ui_threaded(self.global_vars.responder.generate_response_from_transcript_no_check) - -# def update_response_ui_threaded(self, response_generator): -# """Helper method to update response UI in a separate thread -# """ -# try: -# self.global_vars.update_response_now = True -# response_string = response_generator() -# self.global_vars.update_response_now = False -# # Set event to play the recording audio if required -# if self.global_vars.read_response: -# self.global_vars.audio_player_var.speech_text_available.set() -# self.global_vars.response_textbox.configure(state="normal") -# if response_string: -# write_in_textbox(self.global_vars.response_textbox, response_string) -# self.global_vars.response_textbox.configure(state="disabled") -# self.global_vars.response_textbox.see("end") -# except Exception as e: -# logger.error(f"Error in threaded response: {e}") - -# def get_response_selected_now(self): -# """Get response from LLM right away for selected_text -# Update the Response UI with the response -# """ -# if self.global_vars.update_response_now: -# # We are already in the middle of getting a response -# return -# # We need a separate thread to ensure UI is responsive as responses are -# # streamed back. Without the thread UI appears stuck as we stream the -# # responses back -# self.capture_action('Get LLM response selected now') -# selected_text = self.global_vars.transcript_textbox.selection_get() -# response_ui_thread = threading.Thread(target=self.get_response_selected_now_threaded, -# args=(selected_text,), -# name='GetResponseSelectedNow') -# response_ui_thread.daemon = True -# response_ui_thread.start() - -# def summarize_threaded(self): -# """Get summary from LLM in a separate thread""" -# global pop_up # pylint: disable=W0603 -# try: -# print('Summarizing...') -# popup_msg_no_close(title='Summary', msg='Creating a summary') -# summary = self.global_vars.responder.summarize() -# # When API key is not specified, give a chance for the thread to initialize - -# if pop_up is not None: -# try: -# pop_up.destroy() -# except Exception as e: -# # Somehow we get the exception -# # RuntimeError: main thread is not in main loop -# logger.info('Exception in summarize_threaded') -# logger.info(e) - -# pop_up = None -# if summary is None: -# popup_msg_close_button(title='Summary', -# msg='Failed to get summary. Please check you have a valid API key.') -# return - -# # Enhancement here would be to get a streaming summary -# popup_msg_close_button(title='Summary', msg=summary) -# except Exception as e: -# logger.error(f"Error in summarize_threaded: {e}") - -# def summarize(self): -# """Get summary response from LLM -# """ -# self.capture_action('Get summary from LLM') -# summarize_ui_thread = threading.Thread(target=self.summarize_threaded, -# name='Summarize') -# summarize_ui_thread.daemon = True -# summarize_ui_thread.start() - -# def update_response_ui_and_read_now(self): -# """Get response from LLM right away -# Update the Response UI with the response -# Read the response -# """ -# self.capture_action('Get LLM response now and read aloud') -# self.global_vars.set_read_response(True) -# self.get_response_now() - -# def set_transcript_state(self): -# """Enables, disables transcription. -# Text of menu item File -> Pause Transcription toggles accordingly -# """ -# logger.info(UICallbacks.set_transcript_state.__name__) -# try: -# self.global_vars.transcriber.transcribe = not self.global_vars.transcriber.transcribe -# self.capture_action(f'{"Enabled " if self.global_vars.transcriber.transcribe else "Disabled "} transcription.') -# if self.global_vars.transcriber.transcribe: -# self.global_vars.filemenu.entryconfigure(1, label="Pause Transcription") -# else: -# self.global_vars.filemenu.entryconfigure(1, label="Start Transcription") -# except Exception as e: -# logger.error(f"Error setting transcript state: {e}") - -# def open_link(self, url: str): -# """Open the link in a web browser -# """ -# self.capture_action(f'Navigate to {url}.') -# try: -# webbrowser.open(url=url, new=2) -# except Exception as e: -# logger.error(f"Error opening URL {url}: {e}") - -# def open_github(self): -# """Link to git repo main page -# """ -# self.capture_action('open_github.') -# self.open_link('https://github.com/vivekuppal/transcribe?referer=desktop') - -# def open_support(self): -# """Link to git repo issues page -# """ -# self.capture_action('open_support.') -# self.open_link('https://github.com/vivekuppal/transcribe/issues/new?referer=desktop') - -# def capture_action(self, action_text: str): -# """Write to file -# """ -# try: -# if not self.ui_filename: -# data_dir = utilities.get_data_path(app_name='Transcribe') -# self.ui_filename = utilities.incrementing_filename(filename=f'{data_dir}/logs/ui', extension='txt') -# with open(self.ui_filename, mode='a', encoding='utf-8') as ui_file: -# ui_file.write(f'{datetime.datetime.now()}: {action_text}\n') -# except Exception as e: -# logger.error(f"Error capturing action {action_text}: {e}") - -# def set_audio_language(self, lang: str): -# """Alter audio language in memory and persist it in config file -# """ -# try: -# self.global_vars.transcriber.stt_model.set_lang(lang) -# config_obj = configuration.Config() -# # Save config -# altered_config = {'OpenAI': {'audio_lang': lang}} -# config_obj.add_override_value(altered_config) -# except Exception as e: -# logger.error(f"Error setting audio language: {e}") - -# def set_response_language(self, lang: str): -# """Alter response language in memory and persist it in config file -# """ -# try: -# config_obj = configuration.Config() -# altered_config = {'OpenAI': {'response_lang': lang}} -# # Save config -# config_obj.add_override_value(altered_config) -# config_data = config_obj.data - -# # Create a new system prompt -# prompt = config_data["General"]["system_prompt"] -# response_lang = config_data["OpenAI"]["response_lang"] -# if response_lang is not None: -# prompt += f'. Respond exclusively in {response_lang}.' -# convo = self.global_vars.convo -# convo.update_conversation(persona=constants.PERSONA_SYSTEM, -# text=prompt, -# time_spoken=datetime.datetime.utcnow(), -# update_previous=True) -# except Exception as e: -# logger.error(f"Error setting response language: {e}") - - -# def popup_msg_no_close_threaded(title, msg): -# """Create a pop up with no close button. -# """ -# global pop_up # pylint: disable=W0603 -# try: -# popup = ctk.CTkToplevel(T_GLOBALS.main_window) -# popup.geometry("100x50") -# popup.title(title) -# label = ctk.CTkLabel(popup, text=msg, font=("Arial", 12), -# text_color="#FFFCF2") -# label.pack(side="top", fill="x", pady=10) -# pop_up = popup -# popup.lift() -# except Exception as e: -# # Sometimes get the error - calling Tcl from different apartment -# logger.info('Exception in popup_msg_no_close_threaded') -# logger.info(e) -# return - - -# def popup_msg_no_close(title: str, msg: str): -# """Create a popup that the caller is responsible for closing -# using the destroy method -# """ -# kwargs = {} -# kwargs['title'] = title -# kwargs['msg'] = msg -# pop_ui_thread = threading.Thread(target=popup_msg_no_close_threaded, -# name='Pop up thread', -# kwargs=kwargs) -# pop_ui_thread.daemon = True -# pop_ui_thread.start() -# # Give a chance for the thread to initialize -# # When API key is not specified, need the thread to initialize to -# # allow summarize window to show and ultimately be closed. -# time.sleep(0.1) - - -# def popup_msg_close_button(title: str, msg: str): -# """Create a popup that the caller is responsible for closing -# using the destroy method -# """ -# popup = ctk.CTkToplevel(T_GLOBALS.main_window) -# popup.geometry("380x710") -# popup.title(title) -# txtbox = ctk.CTkTextbox(popup, width=350, height=600, font=("Arial", UI_FONT_SIZE), -# text_color='#FFFCF2', wrap="word") -# txtbox.grid(row=0, column=0, padx=10, pady=10, sticky="nsew") -# txtbox.insert("0.0", msg) - -# def copy_summary_to_clipboard(): -# pyperclip.copy(txtbox.cget("text")) - -# copy_button = ctk.CTkButton(popup, text="Copy to Clipboard", command=copy_summary_to_clipboard) -# copy_button.grid(row=1, column=0, padx=10, pady=5, sticky="nsew") - -# close_button = ctk.CTkButton(popup, text="Close", command=popup.destroy) -# close_button.grid(row=2, column=0, padx=10, pady=5, sticky="nsew") -# popup.lift() - - -# def write_in_textbox(textbox: ctk.CTkTextbox, text: str): -# """Update the text of textbox with the given text -# Args: -# textbox: textbox to be updated -# text: updated text -# """ -# # Get current selection attributes, so they can be preserved after writing new text -# a: tuple = textbox.tag_ranges('sel') -# # (, ) -# textbox.delete("0.0", "end") -# textbox.insert("0.0", text) -# if len(a): -# textbox.tag_add('sel', a[0], a[1]) - - -# def update_transcript_ui(transcriber: AudioTranscriber, textbox: ctk.CTkTextbox): -# """Update the text of transcription textbox with the given text -# Args: -# transcriber: AudioTranscriber Object -# textbox: textbox to be updated -# """ - -# global last_transcript_ui_update_time # pylint: disable=W0603 -# global global_vars_module # pylint: disable=W0603 - -# if global_vars_module is None: -# global_vars_module = TranscriptionGlobals() - -# # None comparison is for initialization -# if last_transcript_ui_update_time is None or last_transcript_ui_update_time < global_vars_module.convo.last_update: -# transcript_string = transcriber.get_transcript() -# write_in_textbox(textbox, transcript_string) -# textbox.see("end") -# last_transcript_ui_update_time = datetime.datetime.utcnow() - -# textbox.after(constants.TRANSCRIPT_UI_UPDATE_DELAY_DURATION_MS, -# update_transcript_ui, transcriber, textbox) - - -# def update_response_ui(responder: gr.GPTResponder, -# textbox: ctk.CTkTextbox, -# update_interval_slider_label: ctk.CTkLabel, -# update_interval_slider: ctk.CTkSlider): -# """Update the text of response textbox with the given text -# Args: -# textbox: textbox to be updated -# text: updated text -# """ -# global global_vars_module # pylint: disable=W0603 - -# if global_vars_module is None: -# global_vars_module = TranscriptionGlobals() - -# # global_vars_module.responder.enabled --> This is continous response mode from LLM -# # global_vars_module.update_response_now --> Get Response now from LLM -# if global_vars_module.responder.enabled or global_vars_module.update_response_now: -# response = responder.response - -# textbox.configure(state="normal") -# write_in_textbox(textbox, response) -# textbox.configure(state="disabled") -# textbox.see("end") - -# update_interval = int(update_interval_slider.get()) -# # responder.update_response_interval(update_interval) -# update_interval_slider_label.configure(text=f'LLM Response interval: ' -# f'{update_interval} seconds') - -# textbox.after(300, update_response_ui, responder, textbox, -# update_interval_slider_label, update_interval_slider) - - -# def create_ui_components(root, config: dict): -# """Create UI for the application -# """ -# logger.info(create_ui_components.__name__) -# ctk.set_appearance_mode("dark") -# ctk.set_default_color_theme("dark-blue") -# root.title("Transcribe") -# root.configure(bg='#252422') -# root.geometry("1000x600") - -# ui_cb = UICallbacks() -# global_vars = TranscriptionGlobals() - -# # Create the menu bar -# menubar = tk.Menu(root) - -# # Create a file menu -# filemenu = tk.Menu(menubar, tearoff=False) - -# # Add a "Save" menu item to the file menu -# filemenu.add_command(label="Save Transcript to File", command=ui_cb.save_file) - -# # Add a "Pause" menu item to the file menu -# filemenu.add_command(label="Pause Transcription", command=ui_cb.set_transcript_state) - -# # Add a "Quit" menu item to the file menu -# filemenu.add_command(label="Quit", command=root.quit) - -# # Add the file menu to the menu bar -# menubar.add_cascade(label="File", menu=filemenu) - -# # Create an edit menu -# editmenu = tk.Menu(menubar, tearoff=False) - -# # Add a "Clear Audio Transcript" menu item to the file menu -# editmenu.add_command(label="Clear Audio Transcript", command=lambda: -# global_vars.transcriber.clear_transcriber_context(global_vars.audio_queue)) - -# # Add a "Copy To Clipboard" menu item to the file menu -# editmenu.add_command(label="Copy Transcript to Clipboard", command=ui_cb.copy_to_clipboard) - -# # Add "Disable Speaker" menu item to file menu -# editmenu.add_command(label="Disable Speaker", command=lambda: ui_cb.enable_disable_speaker(editmenu)) - -# # Add "Disable Microphone" menu item to file menu -# editmenu.add_command(label="Disable Microphone", command=lambda: ui_cb.enable_disable_microphone(editmenu)) - -# # Add the edit menu to the menu bar -# menubar.add_cascade(label="Edit", menu=editmenu) - -# # Create help menu, add items in help menu -# helpmenu = tk.Menu(menubar, tearoff=False) -# helpmenu.add_command(label="Github Repo", command=ui_cb.open_github) -# helpmenu.add_command(label="Star the Github repo", command=ui_cb.open_github) -# helpmenu.add_command(label="Report an Issue", command=ui_cb.open_support) -# menubar.add_cascade(label="Help", menu=helpmenu) - -# # Add the menu bar to the main window -# root.config(menu=menubar) - -# # Speech to Text textbox -# transcript_textbox = ctk.CTkTextbox(root, width=300, font=("Arial", UI_FONT_SIZE), -# text_color='#FFFCF2', wrap="word") -# transcript_textbox.grid(row=0, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") - -# # LLM Response textbox -# response_textbox = ctk.CTkTextbox(root, width=300, font=("Arial", UI_FONT_SIZE), -# text_color='#639cdc', wrap="word") -# response_textbox.grid(row=0, column=2, padx=10, pady=3, sticky="nsew") -# response_textbox.insert("0.0", prompts.INITIAL_RESPONSE) - -# response_enabled = bool(config['General']['continuous_response']) -# b_text = "Suggest Responses Continuously" if not response_enabled else "Do Not Suggest Responses Continuously" -# continuous_response_button = ctk.CTkButton(root, text=b_text) -# continuous_response_button.grid(row=1, column=2, padx=10, pady=3, sticky="nsew") - -# response_now_button = ctk.CTkButton(root, text="Suggest Response Now") -# response_now_button.grid(row=2, column=2, padx=10, pady=3, sticky="nsew") - -# read_response_now_button = ctk.CTkButton(root, text="Suggest Response and Read") -# read_response_now_button.grid(row=3, column=2, padx=10, pady=3, sticky="nsew") - -# summarize_button = ctk.CTkButton(root, text="Summarize") -# summarize_button.grid(row=4, column=2, padx=10, pady=3, sticky="nsew") - -# # Continuous LLM Response label, and slider -# update_interval_slider_label = ctk.CTkLabel(root, text="", font=("Arial", 12), -# text_color="#FFFCF2") -# update_interval_slider_label.grid(row=1, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") - -# update_interval_slider = ctk.CTkSlider(root, from_=1, to=30, width=300, # height=5, -# number_of_steps=29) -# update_interval_slider.set(config['General']['llm_response_interval']) -# update_interval_slider.grid(row=2, column=0, columnspan=2, padx=10, pady=3, sticky="nsew") - -# # Speech to text language selection label, dropdown -# audio_lang_label = ctk.CTkLabel(root, text="Audio Lang: ", -# font=("Arial", 12), -# text_color="#FFFCF2") -# audio_lang_label.grid(row=3, column=0, padx=10, pady=3, sticky="nw") - -# audio_lang = config['OpenAI']['audio_lang'] -# audio_lang_combobox = ctk.CTkOptionMenu(root, width=15, values=list(LANGUAGES_DICT.values())) -# audio_lang_combobox.set(audio_lang) -# audio_lang_combobox.grid(row=3, column=0, ipadx=60, padx=10, pady=3, sticky="ne") - -# # LLM Response language selection label, dropdown -# response_lang_label = ctk.CTkLabel(root, -# text="Response Lang: ", -# font=("Arial", 12), text_color="#FFFCF2") -# response_lang_label.grid(row=3, column=1, padx=10, pady=3, sticky="nw") - -# response_lang = config['OpenAI']['response_lang'] -# response_lang_combobox = ctk.CTkOptionMenu(root, width=15, values=list(LANGUAGES_DICT.values())) -# response_lang_combobox.set(response_lang) -# response_lang_combobox.grid(row=3, column=1, ipadx=60, padx=10, pady=3, sticky="ne") - -# github_link = ctk.CTkLabel(root, text="Star the Github Repo", -# text_color="#639cdc", cursor="hand2") -# github_link.grid(row=4, column=0, padx=10, pady=3, sticky="wn") - -# issue_link = ctk.CTkLabel(root, text="Report an issue", text_color="#639cdc", cursor="hand2") -# issue_link.grid(row=4, column=1, padx=10, pady=3, sticky="wn") - -# # Create right click menu for transcript textbox. -# # This displays only inside the speech to text textbox -# m = tk.Menu(root, tearoff=0) -# m.add_command(label="Generate response for selected text", -# command=ui_cb.get_response_selected_now) -# m.add_command(label="Save Transcript to File", command=ui_cb.save_file) -# m.add_command(label="Clear Audio Transcript", command=lambda: -# global_vars.transcriber.clear_transcriber_context(global_vars.audio_queue)) -# m.add_command(label="Copy Transcript to Clipboard", command=ui_cb.copy_to_clipboard) -# m.add_separator() -# m.add_command(label="Quit", command=root.quit) - -# chat_inference_provider = config['General']['chat_inference_provider'] -# if chat_inference_provider == 'openai': -# api_key = config['OpenAI']['api_key'] -# base_url = config['OpenAI']['base_url'] -# model = config['OpenAI']['ai_model'] -# elif chat_inference_provider == 'together': -# api_key = config['Together']['api_key'] -# base_url = config['Together']['base_url'] -# model = config['Together']['ai_model'] - -# if not utilities.is_api_key_valid(api_key=api_key, base_url=base_url, model=model): -# # Disable buttons that interact with backend services -# continuous_response_button.configure(state='disabled') -# response_now_button.configure(state='disabled') -# read_response_now_button.configure(state='disabled') -# summarize_button.configure(state='disabled') - -# tt_msg = 'Add API Key in override.yaml to enable button' -# # Add tooltips for disabled buttons -# ToolTip(continuous_response_button, msg=tt_msg, -# delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, -# padx=7, pady=7) -# ToolTip(response_now_button, msg=tt_msg, -# delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, -# padx=7, pady=7) -# ToolTip(read_response_now_button, msg=tt_msg, -# delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, -# padx=7, pady=7) -# ToolTip(summarize_button, msg=tt_msg, -# delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, -# padx=7, pady=7) - -# def show_context_menu(event): -# try: -# m.tk_popup(event.x_root, event.y_root) -# finally: -# m.grab_release() - -# transcript_textbox.bind("", show_context_menu) - -# # Order of returned components is important. -# # Add new components to the end -# return [transcript_textbox, response_textbox, update_interval_slider, -# update_interval_slider_label, continuous_response_button, -# audio_lang_combobox, response_lang_combobox, filemenu, response_now_button, -# read_response_now_button, editmenu, github_link, issue_link, summarize_button] From 540cd782e998ed61f8fb1de97ef78bad01f9ccca Mon Sep 17 00:00:00 2001 From: vivek Date: Sun, 26 May 2024 14:10:48 -0400 Subject: [PATCH 18/19] doc --- app/transcribe/conversation.py | 16 ++-- app/transcribe/uicomp/selectable_text.py | 93 ++++++++++++++++++++---- tsutils/task_queue.py | 2 + 3 files changed, 92 insertions(+), 19 deletions(-) diff --git a/app/transcribe/conversation.py b/app/transcribe/conversation.py index 36563c7..ff45b2c 100644 --- a/app/transcribe/conversation.py +++ b/app/transcribe/conversation.py @@ -2,7 +2,10 @@ from heapq import merge import datetime import constants -from db import AppDB as appdb, conversation as convodb, llm_responses as llmrdb +from db import ( + AppDB as appdb, + conversation as convodb, + llm_responses as llmrdb) sys.path.append('../..') from tsutils import configuration # noqa: E402 pylint: disable=C0413 @@ -25,6 +28,13 @@ def __init__(self, context): self.context = context def set_handlers(self, update, insert): + """Sets handlers to be called when a conversation is updated or + a new conversation is inserted. + + Args: + update: Handler for update update(persona, input_text) + insert: Handler for insert insert(input_text) + """ self.update_handler = update self.insert_handler = insert @@ -111,8 +121,6 @@ def update_conversation(self, persona: str, # print(f'Added: {time_spoken} - {new_element}') transcript.append((convo_text, time_spoken, convo_id)) - # if (persona.lower() == 'assistant'): - # print(f'Assistant Transcript length after completion: {len(transcript)}') self.last_update = datetime.datetime.utcnow() def on_convo_select(self, input_text: str): @@ -123,7 +131,6 @@ def on_convo_select(self, input_text: str): self.context.previous_response = None return persona = input_text[:end_speaker].strip() - print(persona) transcript = self.transcript_data[persona] for _, (first, _, third) in enumerate(transcript): if first.strip() == input_text.strip(): @@ -140,7 +147,6 @@ def on_convo_select(self, input_text: str): llmr_object: llmrdb.LLMResponses = appdb().get_object(llmrdb.TABLE_NAME) response = llmr_object.get_text_by_invocation_and_conversation(inv_id, convo_id) self.context.previous_response = response if response else 'No LLM response corresponding to this row' - print(response) def get_conversation(self, sources: list = None, diff --git a/app/transcribe/uicomp/selectable_text.py b/app/transcribe/uicomp/selectable_text.py index 72c7878..847a55e 100644 --- a/app/transcribe/uicomp/selectable_text.py +++ b/app/transcribe/uicomp/selectable_text.py @@ -1,4 +1,69 @@ -from tkinter import Text, Scrollbar, END, SEL_FIRST, SEL_LAST +""" +This module defines a custom Tkinter component, SelectableText, which extends the functionality +of a Tkinter Frame. The SelectableText component is designed to display multiple lines of text +with integrated scrolling and interactive features. + +Classes: + SelectableText: A custom Tkinter Frame component that allows for displaying, highlighting, + and interacting with lines of text within a Text widget. Supports adding + text to the top or bottom, clearing text, scrolling, deleting specific rows, + and more. + +Example Usage: + import customtkinter as ctk + from selectable_text import SelectableText + + if __name__ == "__main__": + ctk.set_appearance_mode("dark") + app = SelectableText() + + # Add a lot of lines of text + lines_of_text = [f"Line {i}: This is an example of a long line of text that should wrap around the Text widget." + for i in range(1, 101)] + for line in lines_of_text: + app.add_text_to_bottom(line) + + app.mainloop() + +Methods: + set_callbacks(self, onTextClick) + Set callback handlers for click events. + + clear_all_text(self) + Clear all text from the component. + + on_text_click(self, event) + Handle the click event on the Text widget, highlighting the clicked line. + + on_double_click(self, event) + Handle the double-click event on the Text widget. + + scroll_to_top(self) + Scroll the Text widget to the top. + + scroll_to_bottom(self) + Scroll the Text widget to the bottom. + + add_text_to_top(self, input_text: str) + Add text to the top of the Text widget. + + add_text_to_bottom(self, input_text: str) + Add text to the bottom of the Text widget. + + delete_row_starting_with(self, start_text: str) + Delete the row that starts with the given text. + + replace_multiple_newlines(self) + Replace multiple consecutive newline characters with a single newline character. + + delete_last_2_row(self) + Delete the last 2 rows of text. + + get_text_last_3_rows(self) -> str + Get the text of the last 3 rows. +""" + +from tkinter import Text, Scrollbar, END import customtkinter as ctk @@ -46,8 +111,8 @@ def on_text_click(self, event): self.text_widget.tag_remove("highlight", "1.0", END) # Get the index of the clicked line - index = self.text_widget.index("@%s,%s" % (event.x, event.y)) - line_number = int(index.split(".")[0]) + index = self.text_widget.index(f"@{event.x},{event.y}") + line_number = int(index.split(".", maxsplit=1)[0]) # Get the text of the clicked line line_start = f"{line_number}.0" @@ -58,15 +123,12 @@ def on_text_click(self, event): self.text_widget.tag_add("highlight", line_start, line_end) self.on_text_click_cb(line_text) - # Trigger an event (print the line text) - # print(f"Selected: {line_text}") - def on_double_click(self, event): """Handler for double click """ - index = self.text_widget.index("@%s,%s" % (event.x, event.y)) - line_start = self.text_widget.index("%s linestart" % index) - line_end = self.text_widget.index("%s lineend" % index) + index = self.text_widget.index(f"@{event.x},{event.y}") + line_start = self.text_widget.index(f"{index} linestart") + line_end = self.text_widget.index(f"{index} lineend") self.text_widget.tag_add('SEL', line_start, line_end) self.text_widget.mark_set("insert", line_end) self.text_widget.see("insert") @@ -103,7 +165,7 @@ def add_text_to_bottom(self, input_text: str): def delete_row_starting_with(self, start_text: str): """Delete the row that starts with the given text.""" self.text_widget.configure(state="normal") - last_line_index = int(self.text_widget.index('end-1c').split('.')[0]) + last_line_index = int(self.text_widget.index('end-1c').split('.', maxsplit=1)[0]) for line_number in range(last_line_index, 0, -1): line_start = f"{line_number}.0" @@ -116,7 +178,8 @@ def delete_row_starting_with(self, start_text: str): self.text_widget.configure(state="disabled") def replace_multiple_newlines(self): - """Replace multiple consecutive lines with only newline characters with a single newline character. + """Replace multiple consecutive lines with only newline characters + with a single newline character. """ self.text_widget.configure(state="normal") current_index = "1.0" @@ -133,14 +196,16 @@ def delete_last_2_row(self): """ self.text_widget.configure(state="normal") last_index = self.text_widget.index("end-1c linestart") - second_last_index = self.text_widget.index("%s -1 lines" % last_index) + second_last_index = self.text_widget.index(f"{last_index} -1 lines") self.text_widget.delete(second_last_index, "end-1c") self.text_widget.configure(state="disabled") def get_text_last_3_rows(self) -> str: + """Gets the text of last 3 rows + """ last_index = self.text_widget.index("end-1c linestart") - second_last_index = self.text_widget.index("%s -1 lines" % last_index) - third_last_index = self.text_widget.index("%s -1 lines" % second_last_index) + second_last_index = self.text_widget.index(f"{last_index} -1 lines") + third_last_index = self.text_widget.index(f"{second_last_index} -1 lines") line_text = self.text_widget.get(third_last_index, "end-1c") return line_text diff --git a/tsutils/task_queue.py b/tsutils/task_queue.py index e9e1e06..91faf5e 100644 --- a/tsutils/task_queue.py +++ b/tsutils/task_queue.py @@ -12,6 +12,8 @@ class TaskQueueEnum(Enum): DB_CLEAN = 2 +# Add a task to clean log files at regular intervals +# Add a task to purge DB at regular intervals class TaskQueue: def __init__(self): From 45ebf7bde20811960fd2dc4566a6307ca102f20c Mon Sep 17 00:00:00 2001 From: vivek Date: Thu, 30 May 2024 18:19:27 -0400 Subject: [PATCH 19/19] Edit Lines --- app/transcribe/appui.py | 83 +++++++++++++++++++----- app/transcribe/conversation.py | 56 +++++++++++++++- app/transcribe/db/conversation.py | 19 ++++++ app/transcribe/uicomp/selectable_text.py | 36 +++++++++- 4 files changed, 176 insertions(+), 18 deletions(-) diff --git a/app/transcribe/appui.py b/app/transcribe/appui.py index dead4a0..c31fb41 100644 --- a/app/transcribe/appui.py +++ b/app/transcribe/appui.py @@ -185,14 +185,14 @@ def create_ui_components(self, config: dict): # Create right click menu for transcript textbox. # This displays only inside the speech to text textbox - m = tk.Menu(self.main_frame, tearoff=0) - m.add_command(label="Generate response for selected text", - command=self.get_response_selected_now) - m.add_command(label="Save Transcript to File", command=self.save_file) - m.add_command(label="Clear Audio Transcript", command=self.clear_transcript) - m.add_command(label="Copy Transcript to Clipboard", command=self.copy_to_clipboard) - m.add_separator() - m.add_command(label="Quit", command=self.quit) + self.transcript_text.add_right_click_menu(label="Generate response for selected text", + command=self.get_response_selected_now) + self.transcript_text.add_right_click_menu(label="Save Transcript to File", command=self.save_file) + self.transcript_text.add_right_click_menu(label="Clear Audio Transcript", command=self.clear_transcript) + self.transcript_text.add_right_click_menu(label="Copy Transcript to Clipboard", command=self.copy_to_clipboard) + self.transcript_text.add_right_click_menu(label="Edit line", command=self.edit_current_line) + self.transcript_text.add_right_menu_separator() + self.transcript_text.add_right_click_menu(label="Quit", command=self.quit) chat_inference_provider = config['General']['chat_inference_provider'] if chat_inference_provider == 'openai': @@ -226,14 +226,6 @@ def create_ui_components(self, config: dict): delay=0.01, follow=True, parent_kwargs={"padx": 3, "pady": 3}, padx=7, pady=7) - def show_context_menu(event): - try: - m.tk_popup(event.x_root, event.y_root) - finally: - m.grab_release() - - self.transcript_text.bind("", show_context_menu) - # self.grid_rowconfigure(0, weight=100) # self.grid_rowconfigure(1, weight=1) # self.grid_rowconfigure(2, weight=1) @@ -241,7 +233,66 @@ def show_context_menu(event): # self.grid_columnconfigure(0, weight=1) # self.grid_columnconfigure(1, weight=1) + def edit_current_line(self): + """Edit the selected line of text required + """ + try: + ctk.set_appearance_mode("dark") + ctk.set_default_color_theme("dark-blue") + + current_line = self.transcript_text.text_widget.index("insert linestart") + current_line_text = self.transcript_text.text_widget.get(current_line, f"{current_line} lineend") + + edit_window = tk.Toplevel(self) + edit_window.title("Edit Line") + edit_window.configure(background='#252422') + + edit_text = tk.Text(edit_window, wrap="word", height=10, width=50, + bg='#252422', font=("Arial", 20), + foreground='#639cdc') + edit_text.pack(expand=True, fill='both') + # Separate Person, text in this line + end_speaker = current_line_text.find(':') + if end_speaker == -1: + # Could not determine speaker in text + return + speaker: str = current_line_text[:end_speaker].strip() + speaker_text: str = current_line_text[end_speaker+1:].strip() + if speaker_text[0] == '[': + speaker_text = speaker_text[1:] + if speaker_text[-1] == ']': + speaker_text = speaker_text[:-1] + edit_text.insert(tk.END, speaker_text) + + def save_edit(): + # Needs to do 3 things + # 1. Edit the text in SelectableText class + # 2. Edit the convo object + # 3. Save in DBs + new_text = edit_text.get("1.0", tk.END).strip() + self.transcript_text.text_widget.configure(state="normal") + self.transcript_text.text_widget.delete(current_line, f"{current_line} lineend") + self.transcript_text.text_widget.insert(current_line, f'{speaker}: {new_text}') + self.transcript_text.text_widget.configure(state="disabled") + # Separate persona, text + convo_id = self.global_vars.convo.get_convo_id(persona=speaker, input_text=speaker_text) + self.global_vars.convo.update_conversation_by_id(persona=speaker, convo_id=convo_id, text=new_text) + edit_window.destroy() + + def cancel_edit(): + edit_window.destroy() + save_button = ctk.CTkButton(edit_window, text="Save", command=save_edit) + save_button.pack(side=ctk.LEFT, padx=10, pady=10) + + cancel_button = ctk.CTkButton(edit_window, text="Cancel", command=cancel_edit) + cancel_button.pack(side=ctk.RIGHT, padx=10, pady=10) + + except tk.TclError: + pass # No text in the line + def create_menus(self): + """Create menus for the application + """ # Create the menu bar menubar = tk.Menu(self) diff --git a/app/transcribe/conversation.py b/app/transcribe/conversation.py index ff45b2c..3224f1b 100644 --- a/app/transcribe/conversation.py +++ b/app/transcribe/conversation.py @@ -12,7 +12,8 @@ class Conversation: """Encapsulates the complete conversation. - Has text from Speakers, Microphone, LLM, Instructions to LLM + The member transcript_data has separate lists for different personas. + Each list has a tuple of (ConversationText, time, conversation_id) """ _initialized: bool = False update_handler = None @@ -68,6 +69,34 @@ def clear_conversation_data(self): self.transcript_data[constants.PERSONA_ASSISTANT].clear() self.initialize_conversation() + def update_conversation_by_id(self, persona: str, convo_id: int, text: str): + """ + Update a conversation entry in the transcript_data list. + + Args: + persona (str): The persona whose conversation is to be updated. + convo_id (int): The ID of the conversation entry to update. + text (str): The new content of the conversation. + """ + transcript = self.transcript_data[persona] + + # Find the conversation with the given convo_id + for index, (_, time_spoken, current_convo_id) in enumerate(transcript): + if current_convo_id == convo_id: + # Update the conversation text + new_convo_text = f"{persona}: [{text}]\n\n" + transcript[index] = (new_convo_text, time_spoken, convo_id) + # Update the conversation in the database + if self._initialized: + # inv_id = appdb().get_invocation_id() + convo_object: convodb.Conversations = appdb().get_object(convodb.TABLE_NAME) + convo_object.update_conversation(convo_id, text) + # if persona.lower() != 'assistant': + # self.update_handler(persona, new_convo_text) + break + else: + print(f'Conversation with ID {convo_id} not found for persona {persona}.') + def update_conversation(self, persona: str, text: str, time_spoken, @@ -123,6 +152,31 @@ def update_conversation(self, persona: str, self.last_update = datetime.datetime.utcnow() + def get_convo_id(self, persona: str, input_text: str): + """ + Retrieves the ID of the conversation row that matches the given speaker and text. + + Args: + speaker (str): The name of the speaker. + text (str): The content of the conversation. + + Returns: + int: The ID of the matching conversation entry. + """ + if not self._initialized: + return + cleaned_text = input_text.strip() + if cleaned_text[0] == '[': + cleaned_text = cleaned_text[1:] + if cleaned_text[-1] == ']': + cleaned_text = cleaned_text[:-1] + inv_id = appdb().get_invocation_id() + convo_object: convodb.Conversations = appdb().get_object(convodb.TABLE_NAME) + convo_id = convo_object.get_convo_id_by_speaker_and_text(speaker=persona, + input_text=cleaned_text, + inv_id=inv_id) + return convo_id + def on_convo_select(self, input_text: str): """Callback when a specific conversation is selected. """ diff --git a/app/transcribe/db/conversation.py b/app/transcribe/db/conversation.py index 5e3049d..5648cea 100644 --- a/app/transcribe/db/conversation.py +++ b/app/transcribe/db/conversation.py @@ -147,6 +147,25 @@ def get_max_convo_id(self, speaker: str, inv_id: int) -> int: return convo_id + def get_convo_id_by_speaker_and_text(self, speaker: str, input_text: str, inv_id: int) -> int: + """ + Retrieves the ID of the conversation row that matches the given speaker and text. + + Args: + speaker (str): The name of the speaker. + text (str): The content of the conversation. + + Returns: + int: The ID of the matching conversation entry. + """ + stmt = text(f'SELECT Id FROM {self._table_name} WHERE Speaker = :speaker and Text = :text and InvocationId = :inv_id') + with Session(self.engine) as session: + result = session.execute(stmt, {'speaker': speaker, 'text': input_text, 'inv_id': inv_id}) + convo_id = result.scalar() + session.commit() + + return convo_id + def update_conversation(self, conversation_id: int, convo_text: str): """ Updates the text of a conversation entry in the Conversations table. diff --git a/app/transcribe/uicomp/selectable_text.py b/app/transcribe/uicomp/selectable_text.py index 847a55e..ef8468b 100644 --- a/app/transcribe/uicomp/selectable_text.py +++ b/app/transcribe/uicomp/selectable_text.py @@ -63,7 +63,7 @@ Get the text of the last 3 rows. """ -from tkinter import Text, Scrollbar, END +from tkinter import Text, Scrollbar, END, Menu, TclError import customtkinter as ctk @@ -71,6 +71,9 @@ class SelectableText(ctk.CTkFrame): """Custom TKinter Component to display multiple lines of text and support custom functionality on clicking a line of text. """ + + context_menu = None + def __init__(self, master=None, **kwargs): super().__init__(master, **kwargs) @@ -93,6 +96,37 @@ def __init__(self, master=None, **kwargs): self.text_widget.tag_configure("highlight", background="white") self.on_text_click_cb = None + # Bind right click menu + self.text_widget.bind("", self.show_context_menu) + + def add_right_click_menu(self, label, command): + """Add an entry to the right click menu. + """ + if not self.context_menu: + self.context_menu = Menu(self, tearoff=0) + self.context_menu.add_command(label=label, command=command) + + def get_selected_text(self): + self.text_widget.selection_get() + + def add_right_menu_separator(self): + """Add a separator to right click menu. + """ + self.context_menu.add_separator() + + def show_context_menu(self, event): + """Show the context menu. + """ + if self.context_menu: + self.context_menu.post(event.x_root, event.y_root) + + def copy_text(self): + try: + self.clipboard_clear() + self.clipboard_append(self.selection_get()) + except TclError: + pass # No text selected + def set_callbacks(self, onTextClick): """Set callback handlers """