Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edit Lines #226

Merged
merged 20 commits into from
May 30, 2024
85 changes: 68 additions & 17 deletions app/transcribe/appui.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,15 +184,14 @@ def create_ui_components(self, config: dict):
'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.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':
Expand Down Expand Up @@ -226,22 +225,74 @@ 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("<Button-3>", show_context_menu)

# 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 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)

Expand Down
56 changes: 55 additions & 1 deletion app/transcribe/conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
"""
Expand Down
19 changes: 19 additions & 0 deletions app/transcribe/db/conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
36 changes: 35 additions & 1 deletion app/transcribe/uicomp/selectable_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,17 @@
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


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)

Expand All @@ -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("<Button-3>", 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
"""
Expand Down