From 5d7796f75f98868f8dcb3e6880332f0ea582b24e Mon Sep 17 00:00:00 2001 From: nadje Date: Thu, 3 Oct 2024 15:33:23 +0200 Subject: [PATCH] improve gui and log messages --- .../automate_custom_events/__main__.py | 474 +++++++++++++++--- .../cloud_interaction.py | 2 +- .../automate_custom_events/control_modules.py | 13 +- .../automate_custom_events/process_frames.py | 98 ++-- .../automate_custom_events/tk_utils.py | 26 +- 5 files changed, 472 insertions(+), 141 deletions(-) diff --git a/src/pupil_labs/automate_custom_events/__main__.py b/src/pupil_labs/automate_custom_events/__main__.py index 4ea1a67..4af88d6 100644 --- a/src/pupil_labs/automate_custom_events/__main__.py +++ b/src/pupil_labs/automate_custom_events/__main__.py @@ -1,51 +1,300 @@ +# from pathlib import Path +# import tkinter as tk +# from tkinter import ttk +# from tkinter.font import Font +# import sv_ttk +# import asyncio +# import threading +# import sys +# import logging +# import re +# from pupil_labs.automate_custom_events.tk_utils import TTKFormLayoutHelper +# from pupil_labs.automate_custom_events.control_modules import run_modules + +# def extract_ids(url): +# # Use regex to extract the workspace ID and recording ID +# workspace_pattern = r"workspaces/([a-f0-9\-]+)/" +# recording_pattern = r"id=([a-f0-9\-]+)&" + +# # Find matches using regex +# workspace_match = re.search(workspace_pattern, url) +# recording_match = re.search(recording_pattern, url) + +# # Extract the values if they exist +# workspace_id = workspace_match.group(1) if workspace_match else None +# recording_id = recording_match.group(1) if recording_match else None + +# return workspace_id, recording_id + +# # Function to toggle visibility of the general parameters frame +# def toggle_general_parameters(): +# if general_frame.winfo_viewable(): +# general_frame.grid_remove() # Hide the general parameters frame +# else: +# general_frame.grid(row=2, column=0, columnspan=2, sticky='ew') # Show the general parameters frame + +# async def run_task(): +# try: +# url = url_entry.get() +# cloud_token = cloud_token_entry.get() +# prompt_description = prompt_entry.get("1.0", "end-1c") +# event_code = prompt_event_entry.get("1.0", "end-1c") +# batch_size = batch_entry.get() +# start_time_seconds = start_entry.get() +# end_time_seconds = end_entry.get() +# openai_api_key = openai_key_entry.get() +# download_path = Path(download_path_entry.get()) +# workspace_id, rec_id = extract_ids(url) +# recpath = Path(download_path / rec_id) +# await run_modules(openai_api_key, workspace_id, rec_id, cloud_token, download_path, recpath, +# prompt_description, event_code, batch_size, +# start_time_seconds, end_time_seconds) +# finally: +# pass # Removed progress_bar.stop() and run_button.config(state="normal") + +# def clear_module_fields(): +# """Helper function to clear all general parameters and prompt fields.""" +# widgets = [ +# url_entry, +# cloud_token_entry, +# openai_key_entry, +# prompt_entry, +# prompt_event_entry, +# batch_entry, +# start_entry, +# end_entry +# ] + +# for widget in widgets: +# if isinstance(widget, tk.Text): +# widget.delete("1.0", tk.END) +# else: +# widget.delete(0, tk.END) + +# def on_run_click(): +# def task(): +# asyncio.run(run_task()) +# # Re-enable the run button and stop the progress bar in the main thread +# root.after(0, lambda: run_button.config(state="normal")) +# root.after(0, progress_bar.stop) + +# progress_bar.start() # Start progress bar +# run_button.config(state="disabled") # Disable run button to prevent multiple clicks +# threading.Thread(target=task).start() + +# # Create the main window +# root = tk.Tk() +# root.title("Module Controller") +# root.geometry("600x900") # Adjusted window size + +# # Center the main window +# root.update_idletasks() +# width = root.winfo_width() +# height = root.winfo_height() +# x = (root.winfo_screenwidth() // 2) - (width // 2) +# y = (root.winfo_screenheight() // 2) - (height // 2) +# root.geometry(f"+{x}+{y}") + +# # Set up the style +# sv_ttk.set_theme("dark") +# style = ttk.Style() + +# # Create custom styles +# style.configure('Compute.TButton', +# background='#6D7BE0', # Desired background color +# foreground='white', # Text color +# padding=6) + +# style.map('Compute.TButton', +# background=[('active', 'dark blue'), ('pressed', 'navy')], +# foreground=[('disabled', 'grey')]) + +# style.layout('Compute.TButton', [('Button.padding', {'children': [('Button.label', {'sticky': 'nswe'})], 'sticky': 'nswe'})]) + +# style.configure('Custom.Horizontal.TProgressbar', +# troughcolor='white', # Background area color +# background='#6D7BE0') # Optional: sets the border color to match + +# style.configure('Custom.TEntry', +# foreground='white', +# fieldbackground='#000000', +# background = '#000000', +# insertcolor='white') + +# heading_font = Font(font=style.lookup("TLabel", "font"), weight="bold") +# heading_font.configure(size=heading_font.cget("size")) +# style.configure("TLabel", padding=(10, 5)) +# style.configure("Heading.TLabel", font=heading_font, padding=(10, 10)) +# style.configure("Accent.TButton", foreground="blue") +# layout_helper = TTKFormLayoutHelper(root) + +# # Main frame to center content +# main_frame = ttk.Frame(root, padding=(20, 20, 20, 20)) +# main_frame.grid(column=0, row=0, sticky='nsew') +# root.columnconfigure(0, weight=1) +# root.rowconfigure(0, weight=1) + +# # Toggle Button for General Parameters at the top +# toggle_button = ttk.Button(main_frame, text="Select Recording", command=toggle_general_parameters) +# toggle_button.grid(row=0, column=0, columnspan=2, pady=(10, 10), sticky='ew') + +# # General parameters (in a frame) +# general_frame = ttk.Frame(main_frame) + +# # Create labeled entries for the general parameters using the helper functions +# # Set bg to '#000000' for input fields +# bg = '#000000' +# entry_fg = 'white' + +# url_entry = layout_helper.create_labeled_entry(general_frame, 'Recording Link', row=0, default_value='https://api.cloud.pupil-labs.com/v2/workspaces/d6bde22c-0c74-4d7d-8ab6-65b665c3cb4e/recordings.zip?id=bdd9d604-298a-4c20-889c-46d818a5c5ec&share-key=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJkNmJkZTIyYy0wYzc0LTRkN2QtOGFiNi02NWI2NjVjM2NiNGUiLCJtZXRob2QiOiJHRVQiLCJwYXRoIjoiL3YyL3dvcmtzcGFjZXMvZDZiZGUyMmMtMGM3NC00ZDdkLThhYjYtNjViNjY1YzNjYjRlL3JlY29yZGluZ3MuemlwIiwicXVlcnkiOiJpZD1iZGQ5ZDYwNC0yOThhLTRjMjAtODg5Yy00NmQ4MThhNWM1ZWMiLCJleHAiOjE3MjgzOTAzMzQuMjU0Mzc5fQ.VAhp3w_bMI2G5ErfywPVHA6p62yY8sPhA3wNJcaGWnE') +# cloud_token_entry = layout_helper.create_labeled_entry(general_frame, 'Cloud API Token', row=1, show='*', default_value='CgwCxAZy4weDcaxmzpsDjWKPKsqTYbUY4DmdgwrP8GTa') +# openai_key_entry = layout_helper.create_labeled_entry(general_frame, 'OpenAI API Key', row=2, show='*', default_value='sk-r6tWCqoKli236HVXvHvgUO9ZPCzTJWmE31zu8d4zhQT3BlbkFJ88iso4ktkTr-CvkJzofYGblisQPYZFFR-w3zqHX-4A') +# download_path_entry = layout_helper.create_labeled_folder_selector(general_frame, 'Download Path', row=3, default_path=Path.cwd()) +# batch_entry = layout_helper.create_labeled_entry(general_frame, 'Frame batch', row=4, default_value='30') +# start_entry = layout_helper.create_labeled_entry(general_frame, 'Start (s)', row=5, default_value='4') +# end_entry = layout_helper.create_labeled_entry(general_frame, 'End (s)', row=6, default_value='45') + +# # Initially hide the general parameters section +# general_frame.grid(row=2, column=0, columnspan=2, sticky='ew') +# general_frame.grid_remove() # Hide at start + +# # Layout helper reset for main_frame +# layout_helper = TTKFormLayoutHelper(main_frame) + +# # Prompts (always visible) +# layout_helper.row_idx = 3 # Start from row 3 to ensure correct placement + +# layout_helper.add_heading('Analyze this egocentric video. The red circle in the overlay indicates where the wearer is looking. Note the times when...') +# prompt_entry = tk.Text(main_frame, height=5, width=80, bg='#000000', fg='white', insertbackground='white') +# layout_helper.add_row('', prompt_entry, {'pady': 10}) +# layout_helper.add_heading('... and report them with the following event names.') +# prompt_event_entry = tk.Text(main_frame, height=5, width=80, bg='#000000', fg='white', insertbackground='white') +# layout_helper.add_row('', prompt_event_entry, {'pady': 10}) + +# clear_button = ttk.Button(main_frame, text="Reset Form", command=clear_module_fields, style='TButton') +# clear_button.grid(row=layout_helper.row_idx, column=0, columnspan=2, pady=(10, 0), sticky='ew') + +# run_button = ttk.Button(main_frame, text="Compute", command=on_run_click, style='Compute.TButton') +# run_button.grid(row=layout_helper.row_idx + 1, column=0, columnspan=2, pady=(10, 10), sticky='ew') + +# # Progress bar below the buttons +# progress_bar = ttk.Progressbar(main_frame, mode='indeterminate', style='Custom.Horizontal.TProgressbar') +# progress_bar.grid(row=layout_helper.row_idx + 2, column=0, columnspan=2, pady=(10, 10), sticky='ew') + +# # Console panel +# console_label = ttk.Label(main_frame, text="Console Output:") +# console_label.grid(row=layout_helper.row_idx + 3, column=0, columnspan=2, pady=(10, 0), sticky='w') + +# console_text = tk.Text(main_frame, height=10, width=80, state='disabled', bg='#000000', fg='white', wrap='word') +# console_text.grid(row=layout_helper.row_idx + 4, column=0, columnspan=2, pady=(5, 10), sticky='nsew') + +# # Configure row and column weights for console_text to expand +# main_frame.rowconfigure(layout_helper.row_idx + 4, weight=1) +# main_frame.columnconfigure(0, weight=1) +# main_frame.columnconfigure(1, weight=1) + +# # Set up logging +# logger = logging.getLogger() +# logger.setLevel(logging.DEBUG) # Set the root logger level + +# # Create formatters +# formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + +# # Remove any existing handlers +# logger.handlers = [] + +# # Create console handler for standard console output +# console_handler = logging.StreamHandler() +# console_handler.setLevel(logging.DEBUG) # Adjust as needed +# console_handler.setFormatter(formatter) + +# # Create GUI handler for the GUI console +# class TextHandler(logging.Handler): +# def __init__(self, text_widget): +# super().__init__() +# self.text_widget = text_widget +# self.ansi_escape = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]') + +# def emit(self, record): +# msg = self.format(record) +# # Clean ANSI escape codes +# msg = self.ansi_escape.sub('', msg) +# def append(): +# self.text_widget.configure(state='normal') +# self.text_widget.insert(tk.END, msg + '\n') +# self.text_widget.see(tk.END) +# self.text_widget.configure(state='disabled') +# self.text_widget.after(0, append) + +# gui_handler = TextHandler(console_text) +# gui_handler.setLevel(logging.INFO) # Adjust level as needed +# gui_handler.setFormatter(formatter) + +# # Add handlers to the logger +# logger.addHandler(console_handler) +# logger.addHandler(gui_handler) + +# # Start the GUI event loop +# root.mainloop() + from pathlib import Path import tkinter as tk from tkinter import ttk from tkinter.font import Font import sv_ttk import asyncio -from pupil_labs.automate_custom_events.tk_utils import TTKFormLayoutHelper, FolderSelector +import threading +import sys +import logging +import re +from pupil_labs.automate_custom_events.tk_utils import TTKFormLayoutHelper from pupil_labs.automate_custom_events.control_modules import run_modules +def extract_ids(url): + # Use regex to extract the workspace ID and recording ID + workspace_pattern = r"workspaces/([a-f0-9\-]+)/" + recording_pattern = r"id=([a-f0-9\-]+)&" + + # Find matches using regex + workspace_match = re.search(workspace_pattern, url) + recording_match = re.search(recording_pattern, url) + + # Extract the values if they exist + workspace_id = workspace_match.group(1) if workspace_match else None + recording_id = recording_match.group(1) if recording_match else None + + return workspace_id, recording_id # Function to toggle visibility of the general parameters frame def toggle_general_parameters(): if general_frame.winfo_viewable(): general_frame.grid_remove() # Hide the general parameters frame - general_title.grid_remove() # Hide the general parameters title else: - general_title.grid(row=layout_helper.row_idx) # Show the title - general_frame.grid(row=layout_helper.row_idx + 1, column=0, columnspan=2, sticky='ew') # Show the general parameters frame - + general_frame.grid(row=2, column=0, columnspan=2, sticky='ew') # Show the general parameters frame async def run_task(): try: - rec_id = rec_id_entry.get() - workspace_id = workspace_id_entry.get() + url = url_entry.get() cloud_token = cloud_token_entry.get() prompt_description = prompt_entry.get("1.0", "end-1c") event_code = prompt_event_entry.get("1.0", "end-1c") batch_size = batch_entry.get() start_time_seconds = start_entry.get() end_time_seconds = end_entry.get() - openai_api_key = openai_key_entry.get() download_path = Path(download_path_entry.get()) + workspace_id, rec_id = extract_ids(url) recpath = Path(download_path / rec_id) - await run_modules(openai_api_key, workspace_id, rec_id, cloud_token, download_path, recpath, prompt_description, event_code, batch_size, start_time_seconds, end_time_seconds) finally: - progress_bar.stop() - run_button.config(state="normal") - + pass # Removed progress_bar.stop() and run_button.config(state="normal") def clear_module_fields(): """Helper function to clear all general parameters and prompt fields.""" widgets = [ - rec_id_entry, - workspace_id_entry, + url_entry, cloud_token_entry, openai_key_entry, prompt_entry, @@ -61,80 +310,173 @@ def clear_module_fields(): else: widget.delete(0, tk.END) +def on_run_click(): + def task(): + asyncio.run(run_task()) + # Re-enable the run button and stop the progress bar in the main thread + root.after(0, lambda: run_button.config(state="normal")) + root.after(0, progress_bar.stop) -async def on_run_click(): progress_bar.start() # Start progress bar run_button.config(state="disabled") # Disable run button to prevent multiple clicks - await run_task() - progress_bar.stop() - + threading.Thread(target=task).start() # Create the main window root = tk.Tk() root.title("Module Controller") -root.geometry("600x1000") +root.geometry("600x1000") # Adjusted window size -root.columnconfigure(0, weight=0) -root.columnconfigure(1, weight=1) +# Center the main window +root.update_idletasks() +width = root.winfo_width() +height = root.winfo_height() +x = (root.winfo_screenwidth() // 2) - (width // 2) +y = (root.winfo_screenheight() // 2) - (height // 2) +root.geometry(f"+{x}+{y}") -# Set a built-in ttk theme +# Set up the style sv_ttk.set_theme("dark") -style = ttk.Style(root) +style = ttk.Style() + +# Create custom styles +style.configure('Compute.TButton', + background='#6D7BE0', # Desired background color + foreground='white', # Text color + padding=6) + +style.map('Compute.TButton', + background=[('active', 'dark blue'), ('pressed', 'navy')], + foreground=[('disabled', 'grey')]) + +style.layout('Compute.TButton', [('Button.padding', {'children': [('Button.label', {'sticky': 'nswe'})], 'sticky': 'nswe'})]) + +style.configure('Custom.Horizontal.TProgressbar', + troughcolor='white', # Background area color + background='#6D7BE0') # Optional: sets the border color to match -heading_font = Font(font=style.lookup("TLabel", "font"), weight="bold") +style.configure('Custom.TEntry', + foreground='white', + fieldbackground='#000000', + background='#000000', + insertcolor='white') + +heading_font = Font(font=style.lookup("TLabel", "font")) heading_font.configure(size=heading_font.cget("size")) -style.configure("TLabel", padding=(20, 5)) -style.configure("Heading.TLabel", font=heading_font, padding=(10, 20)) +style.configure("TLabel", padding=(10, 5)) +style.configure("Heading.TLabel", font=heading_font, padding=(10, 10)) style.configure("Accent.TButton", foreground="blue") layout_helper = TTKFormLayoutHelper(root) -# Title for General Parameters (initially hidden) -general_title = tk.Label(root, text="General Parameters", font=heading_font) +# Main frame to center content with consistent margins +main_frame = ttk.Frame(root, padding=(40, 20, 20, 20)) # Added left and right padding +main_frame.grid(column=0, row=0, sticky='nsew') +root.columnconfigure(0, weight=1) +root.rowconfigure(0, weight=1) + +# Center the main_frame contents by configuring row and column weights +for col in range(2): + main_frame.columnconfigure(col, weight=1) + +# Toggle Button for General Parameters at the top +toggle_button = ttk.Button(main_frame, text="Select Recording", command=toggle_general_parameters) +toggle_button.grid(row=0, column=0, columnspan=2, pady=(10, 10), sticky='ew') # General parameters (in a frame) -general_frame = ttk.Frame(root) +general_frame = ttk.Frame(main_frame) # Create labeled entries for the general parameters using the helper functions -rec_id_entry = layout_helper.create_labeled_entry(general_frame, 'Recording ID', row=0, default_value='006875b0-0b90-46f7-858d-c949495804fa') -workspace_id_entry = layout_helper.create_labeled_entry(general_frame, 'Workspace ID', row=1, default_value='d6bde22c-0c74-4d7d-8ab6-65b665c3cb4e') -cloud_token_entry = layout_helper.create_labeled_entry(general_frame, 'Cloud API Token', row=2, show='*', default_value='CgwCxAZy4weDcaxmzpsDjWKPKsqTYbUY4DmdgwrP8GTa') -openai_key_entry = layout_helper.create_labeled_entry(general_frame, 'OpenAI API Key', row=3, show='*', default_value='sk-r6tWCqoKli236HVXvHvgUO9ZPCzTJWmE31zu8d4zhQT3BlbkFJ88iso4ktkTr-CvkJzofYGblisQPYZFFR-w3zqHX-4A') -download_path_entry = layout_helper.create_labeled_folder_selector(general_frame, 'Download Path', row=4, default_path=Path.cwd()) -batch_entry = layout_helper.create_labeled_entry(general_frame, 'Frame batch', row=5, default_value='20') -start_entry = layout_helper.create_labeled_entry(general_frame, 'Start (s)', row=6, default_value='60') -end_entry = layout_helper.create_labeled_entry(general_frame, 'End (s)', row=7, default_value='125') - -# Toggle Button for General Parameters -toggle_button = ttk.Button(root, text="Select recording", command=toggle_general_parameters) -layout_helper.add_row('', toggle_button, {'pady': 10}) +bg = '#000000' +entry_fg = 'white' + +url_entry = layout_helper.create_labeled_entry(general_frame, 'Recording Link', row=0, default_value='https://api.cloud.pupil-labs.com/v2/workspaces/d6bde22c-0c74-4d7d-8ab6-65b665c3cb4e/recordings.zip?id=bdd9d604-298a-4c20-889c-46d818a5c5ec&share-key=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJkNmJkZTIyYy0wYzc0LTRkN2QtOGFiNi02NWI2NjY1YzNjYjRlIiwibWV0aG9kIjoiR0VUIiwicGF0aCI6Ii92Mi93b3Jrc3BhY2VzL2Q2YmRlMjJjLTBjNzQtNGQ3ZC04YWI2LTY1YjY2NWMzY2I0ZS9yZWNvcmRpbmdzLnppcCIsInF1ZXJ5IjoiaWQ9YmRkOWQ2MDQtMjk4YS00YzIwLTg4OWMtNDZkODE4YTVjNWVjIiwiZXhwIjoxNzI4MzkwMzM0LjI1NDM3OX0.VAhp3w_bMI2G5ErfywPVHA6p62yY8sPhA3wNJcaGWnE') +cloud_token_entry = layout_helper.create_labeled_entry(general_frame, 'Cloud API Token', row=1, show='*', default_value='CgwCxAZy4weDcaxmzpsDjWKPKsqTYbUY4DmdgwrP8GTa') +openai_key_entry = layout_helper.create_labeled_entry(general_frame, 'OpenAI API Key', row=2, show='*', default_value='sk-r6tWCqoKli236HVXvHvgUO9ZPCzTJWmE31zu8d4zhQT3BlbkFJ88iso4ktkTr-CvkJzofYGblisQPYZFFR-w3zqHX-4A') +download_path_entry = layout_helper.create_labeled_folder_selector(general_frame, 'Download Path', row=3, default_path=Path.cwd()) +batch_entry = layout_helper.create_labeled_entry(general_frame, 'Frame batch', row=4, default_value='30') +start_entry = layout_helper.create_labeled_entry(general_frame, 'Start (s)', row=5, default_value='4') +end_entry = layout_helper.create_labeled_entry(general_frame, 'End (s)', row=6, default_value='45') # Initially hide the general parameters section -general_frame.grid(row=layout_helper.row_idx, column=0, columnspan=2, sticky='ew') +general_frame.grid(row=2, column=0, columnspan=2, sticky='ew') general_frame.grid_remove() # Hide at start -general_title.grid_remove() # Hide title at start - -# Prompts (always visible) in a separate frame -prompts_frame = ttk.Frame(root) -layout_helper.add_heading('Analyze this egocentric video, the red circle in the overlay indicates where the wearer is looking. \nNote the times when...') -prompt_entry = tk.Text(root, height=5, width=50, bg='gray', fg='white') -layout_helper.add_row(' ', prompt_entry, {'pady': 10}) -layout_helper.add_heading('... and report them with the following event names.') -prompt_event_entry = tk.Text(root, height=5, width=50, bg='gray', fg='white') -layout_helper.add_row(' ', prompt_event_entry, {'pady': 10}) - -# Place the prompts frame initially below the general parameters -prompts_frame.grid(row=layout_helper.row_idx + 1, column=0, columnspan=2, sticky='ew') - -# Buttons -layout_helper.add_spacer_row() -clear_button = ttk.Button(root, text="Reset form", command=clear_module_fields, style='TButton') -layout_helper.add_row('', clear_button, {'pady': 10}) - -run_button = ttk.Button(root, text="Compute", command=lambda: asyncio.run(on_run_click()), style='Accent.TButton') -layout_helper.add_row('', run_button, {'pady': 10}) - -# Progress bar -progress_bar = layout_helper.add_row('', ttk.Progressbar(root, mode='indeterminate')) + +# Layout helper reset for main_frame +layout_helper = TTKFormLayoutHelper(main_frame) + +# Prompts (always visible) +layout_helper.row_idx = 3 # Start from row 3 to ensure correct placement + +layout_helper.add_heading_2('Analyze this egocentric video. The red circle in the overlay indicates where the wearer is looking. Note the times when...', heading_font) +# Insert background for prompt entry +prompt_entry = tk.Text(main_frame, height=5, width=80, bg='#000000', fg='white', insertbackground='white') +layout_helper.add_row('', prompt_entry, {'pady': 10, 'sticky': 'ew'}) # Added sticky='ew' to ensure text fills the width +layout_helper.add_heading('... and report them as the following events.') +prompt_event_entry = tk.Text(main_frame, height=5, width=80, bg='#000000', fg='white', insertbackground='white') +layout_helper.add_row('', prompt_event_entry, {'pady': 10, 'sticky': 'ew'}) # Added sticky='ew' to ensure text fills the width + +# Add buttons below the prompt entries +clear_button = ttk.Button(main_frame, text="Reset Form", command=clear_module_fields, style='TButton') +clear_button.grid(row=layout_helper.row_idx, column=0, columnspan=2, pady=(10, 0), sticky='ew') + +run_button = ttk.Button(main_frame, text="Compute", command=on_run_click, style='Compute.TButton') +run_button.grid(row=layout_helper.row_idx + 1, column=0, columnspan=2, pady=(10, 10), sticky='ew') + +# Progress bar below the buttons +progress_bar = ttk.Progressbar(main_frame, mode='indeterminate', style='Custom.Horizontal.TProgressbar') +progress_bar.grid(row=layout_helper.row_idx + 2, column=0, columnspan=2, pady=(10, 10), sticky='ew') + +# Console output label and text area +console_label = ttk.Label(main_frame, text="Console Output:",style="Heading.TLabel") +console_label.grid(row=layout_helper.row_idx + 3, column=0, columnspan=2, pady=(10, 0), sticky='w') + +console_text = tk.Text(main_frame, height=10, width=80, state='disabled', bg='#000000', fg='white', wrap='word') +console_text.grid(row=layout_helper.row_idx + 4, column=0, columnspan=2, pady=(5, 10), sticky='nsew') + +# Configure row and column weights for console_text to expand +main_frame.rowconfigure(layout_helper.row_idx + 4, weight=1) +main_frame.columnconfigure(0, weight=1) +main_frame.columnconfigure(1, weight=1) + +# Set up logging +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) # Set the root logger level + +# Create formatters +formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + +# Remove any existing handlers +logger.handlers = [] + +# Create console handler for standard console output +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) # Adjust as needed +console_handler.setFormatter(formatter) + +# Create GUI handler for the GUI console +class TextHandler(logging.Handler): + def __init__(self, text_widget): + super().__init__() + self.text_widget = text_widget + self.ansi_escape = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]') + + def emit(self, record): + msg = self.format(record) + # Clean ANSI escape codes + msg = self.ansi_escape.sub('', msg) + def append(): + self.text_widget.configure(state='normal') + self.text_widget.insert(tk.END, msg + '\n') + self.text_widget.see(tk.END) + self.text_widget.configure(state='disabled') + self.text_widget.after(0, append) + +gui_handler = TextHandler(console_text) +gui_handler.setLevel(logging.INFO) # Adjust level as needed +gui_handler.setFormatter(formatter) + +# Add handlers to the logger +logger.addHandler(console_handler) +logger.addHandler(gui_handler) # Start the GUI event loop root.mainloop() \ No newline at end of file diff --git a/src/pupil_labs/automate_custom_events/cloud_interaction.py b/src/pupil_labs/automate_custom_events/cloud_interaction.py index 6157315..60025bd 100644 --- a/src/pupil_labs/automate_custom_events/cloud_interaction.py +++ b/src/pupil_labs/automate_custom_events/cloud_interaction.py @@ -47,7 +47,7 @@ def send_event_to_cloud(workspace_id, recording_id, keyword, timestamp_sec, API_ data = {"name": keyword, "offset_s": timestamp_sec} response = requests.post(url, headers=headers, data=json.dumps(data)) if response.status_code == 200: - logging.info(f"Event sent successfully: {data}") + logging.debug(f"Event sent successfully: {data}") else: logging.error(f"Failed to send event: {response.status_code}, {response.text}") diff --git a/src/pupil_labs/automate_custom_events/control_modules.py b/src/pupil_labs/automate_custom_events/control_modules.py index 1ddc674..567095d 100644 --- a/src/pupil_labs/automate_custom_events/control_modules.py +++ b/src/pupil_labs/automate_custom_events/control_modules.py @@ -17,14 +17,14 @@ async def run_modules(OPENAI_API_KEY, worksp_id, rec_id, cloud_api_key, download ############################################################################# # 1. Download, read data, and create gaze overlay video to be sent to OpenAI ############################################################################# - logging.info("[white bold on #0d122a]◎ Let's start! ⚡️[/]", extra={"markup": True}) + logging.info("[white bold on #0d122a]◎ Getting the recording data from Pupil Cloud! ⚡️[/]", extra={"markup": True}) download_recording(rec_id, worksp_id, download_path, cloud_api_key) recpath = Path(download_path / rec_id) files = glob.glob(str(Path(recpath, "*.mp4"))) gaze_overlay_path = os.path.join(recpath, "gaze_overlay.mp4") if os.path.exists(gaze_overlay_path): - logging.info(f"{gaze_overlay_path} exists.") + logging.debug(f"{gaze_overlay_path} exists.") else: logging.warning(f"{gaze_overlay_path} does not exist.") @@ -37,11 +37,11 @@ async def run_modules(OPENAI_API_KEY, worksp_id, rec_id, cloud_api_key, download _, frames, pts, ts = read_video_ts(raw_video_path) # Read gaze data - logging.info("Reading gaze data...") + logging.debug("Reading gaze data...") gaze_df = pd.read_csv(Path(recpath, 'gaze.csv'), dtype=oftype) # Read the world timestamps (needed for gaze module) - logging.info("Reading world timestamps...") + logging.debug("Reading world timestamps...") world_timestamps_df = pd.read_csv( Path(recpath, "world_timestamps.csv"), dtype=oftype @@ -56,7 +56,7 @@ async def run_modules(OPENAI_API_KEY, worksp_id, rec_id, cloud_api_key, download "timestamp [ns]": ts_world, }) - logging.info("Merging video and gaze dfs..") + logging.debug("Merging video and gaze dfs..") selected_col = ["timestamp [ns]", "gaze x [px]", "gaze y [px]"] gaze_df = gaze_df[selected_col] gaze_df = gaze_df.sort_values(by="timestamp [ns]") @@ -84,6 +84,7 @@ async def run_modules(OPENAI_API_KEY, worksp_id, rec_id, cloud_api_key, download ############################################################################# # 3. Process Frames with GPT-v ############################################################################# + logging.info("Start processing the frames..") async_process_frames = ProcessFrames(baseframes_modules, video_df_for_modules, OPENAI_API_KEY, cloud_api_key, rec_id, worksp_id, description, event_code, int(batch_size), start_time_seconds, end_time_seconds @@ -93,6 +94,6 @@ async def run_modules(OPENAI_API_KEY, worksp_id, rec_id, cloud_api_key, download print(async_process_frames_output_events) final_output_path = pd.DataFrame(async_process_frames_output_events) final_output_path.to_csv(os.path.join(recpath, 'custom_events.csv'), index=False) - logging.info("[white bold on #0d122a]◎ Modules completed and events sent! ⚡️[/]", extra={"markup": True}) + logging.info("[white bold on #0d122a]◎ Activity recognition completed and events sent! ⚡️[/]", extra={"markup": True}) return final_output_path diff --git a/src/pupil_labs/automate_custom_events/process_frames.py b/src/pupil_labs/automate_custom_events/process_frames.py index 029d375..17867ae 100644 --- a/src/pupil_labs/automate_custom_events/process_frames.py +++ b/src/pupil_labs/automate_custom_events/process_frames.py @@ -6,7 +6,9 @@ import aiohttp from pupil_labs.automate_custom_events.cloud_interaction import send_event_to_cloud import asyncio +import logging +logger = logging.getLogger(__name__) class ProcessFrames: def __init__(self, base64Frames, vid_modules, OPENAI_API_KEY, cloudtoken, recID, workID, @@ -24,11 +26,6 @@ def __init__(self, base64Frames, vid_modules, OPENAI_API_KEY, cloudtoken, recID, self.session_cost = 0 self.batch_size = batch_size - # self.arm_activity = arm_activity1 - # # Flags for tracking activities - # self.gm_flag = False # Flag for gaze module - # self.arm_flag = False # Flag for arm activity - # Time range parameters self.start_time_seconds = int(start_time_seconds) self.end_time_seconds = int(end_time_seconds) @@ -43,9 +40,8 @@ def __init__(self, base64Frames, vid_modules, OPENAI_API_KEY, cloudtoken, recID, You are an experienced video annotator specialized in eye-tracking data analysis. **Task:** - - - Analyze each frame of the provided video sequence, which includes a gaze overlay (a red circle indicating where the user is looking). - - Identify when any of the specified activities **start** or **end** in the video based on the visual content and the gaze location. + - Analyze the frames of this egocentric video, the red circle in the overlay indicates where the wearer is looking. + - Identify when any of the specified activities happen in the video based on the visual content (video feed) and the gaze location (red circle). **Activities and Corresponding Codes:** @@ -59,33 +55,30 @@ def __init__(self, base64Frames, vid_modules, OPENAI_API_KEY, cloudtoken, recID, - For each frame: - Examine the visual elements and the position of the gaze overlay. - - Determine if any of the specified activities are **starting** or **ending**. - - If an activity is **starting**, record the following information: - - **Frame Number:** [frame number] - - **Timestamp:** [timestamp from the provided dataframe] - - **Code:** start_[corresponding activity code] - - If an activity is **ending**, record: + - Determine if any of the specified activities are detected in the frame. + - If an activity is detected, record the following information: - **Frame Number:** [frame number] - **Timestamp:** [timestamp from the provided dataframe] - - **Code:** end_[corresponding activity code] - - Only consider the activities listed above. + - **Code:** [corresponding activity code] + - If an activity is not detected, move to the next frame. + - Only consider the activities listed above. Be as precise as possible. - Ensure the output is accurate and formatted correctly. **Output Format:** ``` - Frame [frame number]: Timestamp - [timestamp], Code - [start/end]_[code] + Frame [frame number]: Timestamp - [timestamp], Code - [code] ``` **Examples:** - - If in frame 25 the user **starts** driving under a bridge and the timestamp is 65, the output should be: + - If in frame 25 the user is cutting a red pepper and the timestamp is 65, the output should be: ``` - Frame 25: Timestamp - 65, Code - start_driving_under_bridge + Frame 25: Timestamp - 65, Code - cutting_red_pper ``` - - If in frame 50 the user **stops** turning left, the output should be: + - If in frame 50 the user is looking at the rear mirror, the output should be: ``` - Frame 50: Timestamp - [timestamp], Code - end_turning_left + Frame 50: Timestamp - [timestamp], Code - looking_rear_mirror ``` """ @@ -103,7 +96,7 @@ async def query_frame(self, index, session): # Check if the frame's timestamp is within the specified time range timestamp = self.mydf.iloc[index]['timestamp [s]'] if not self.is_within_time_range(timestamp): - print(f"Timestamp {timestamp} is not within selected timerange") + #print(f"Timestamp {timestamp} is not within selected timerange") return None base64_frames_content = [{"image": self.base64Frames[index], "resize": 768}] @@ -152,44 +145,23 @@ async def query_frame(self, index, session): frame_number = int(match[0]) timestamp = float(match[1]) code = match[2] - - # Parse the code to get the prefix and activity code - code_match = re.match(r'(start|end)_(.+)', code) - if code_match: - prefix = code_match.group(1) # 'start' or 'end' - activity_code = code_match.group(2) - - # Check if the activity code is valid - if activity_code not in self.codes: - print(f"Invalid activity code: {activity_code}") - continue - - # Get the current state of the activity - activity_active = self.activity_states[activity_code] - - if prefix == 'start': - if not activity_active: - # Activity is starting - self.activity_states[activity_code] = True - send_event_to_cloud(self.workspaceid, self.recid, code, timestamp, self.cloud_token) - print(f"Sent start event for {activity_code}") - else: - # Start event already sent, ignore - print(f"Start event for {activity_code} already sent, ignoring.") - elif prefix == 'end': - if activity_active: - # Activity is ending - self.activity_states[activity_code] = False - send_event_to_cloud(self.workspaceid, self.recid, code, timestamp, self.cloud_token) - print(f"Sent end event for {activity_code}") - else: - # End event without a start, ignore - print(f"End event for {activity_code} received without a start, ignoring.") - else: - print(f"Unknown prefix in code: {code}") + # # Check if the activity code is valid + if code not in self.codes: + print("The activity was not detected") + continue + + # Get the current state of the activity + activity_active = self.activity_states[code] + + if not activity_active: + # Activity is starting or being detected for the first time + self.activity_states[code] = True + send_event_to_cloud(self.workspaceid, self.recid, code, timestamp, self.cloud_token) + logger.info(f"Activity detected: {code}") else: - print(f"Invalid code format: {code}") - + # Activity already detected, ignore + logger.debug(f"Event for {code} already sent - ignoring.") + return { "frame_id": frame_number, "timestamp [s]": timestamp, @@ -201,10 +173,10 @@ async def query_frame(self, index, session): elif response.status == 429: retry_count += 1 wait_time = 2 ** retry_count # Exponential backoff - print(f"Rate limit hit. Retrying in {wait_time} seconds...") + logger.warning(f"Rate limit hit. Retrying in {wait_time} seconds...") await asyncio.sleep(wait_time) else: - print(f"Error: {response.status}") + logger.warning(f"Error: {response.status}") return None print("Max retries reached. Exiting.") return None @@ -214,7 +186,7 @@ async def binary_search(self, session, start, end, identified_activities): return [] mid = (start + end) // 2 - print(f"Binary search range: {start}-{end}, mid: {mid}") + #print(f"Binary search range: {start}-{end}, mid: {mid}") results = [] # Process the mid frame and ensure both prompts are evaluated @@ -238,7 +210,7 @@ async def process_batches(self, session, batch_size): end = min(i + batch_size, len(self.base64Frames)) batch_results = await self.binary_search(session, i, end, identified_activities) all_results.extend(batch_results) - print(f"Processed batch {i} to {end}, results: {batch_results}") + #print(f"Processed batch {i} to {end}, results: {batch_results}") async def prompting(self, save_path, batch_size): async with aiohttp.ClientSession() as session: diff --git a/src/pupil_labs/automate_custom_events/tk_utils.py b/src/pupil_labs/automate_custom_events/tk_utils.py index 59c46d8..e781dfa 100644 --- a/src/pupil_labs/automate_custom_events/tk_utils.py +++ b/src/pupil_labs/automate_custom_events/tk_utils.py @@ -1,15 +1,27 @@ import tkinter as tk from tkinter import ttk, filedialog - class TTKFormLayoutHelper: def __init__(self, root): self.row_idx = 0 self.root = root + + def add_heading_2(self, text, heading_font): + # Use tk.Text to control line spacing + text_widget = tk.Text(self.root, height=3, wrap="word", borderwidth=0, font=heading_font) + text_widget.insert("1.0", text) + # text_widget.tag_configure("center", justify='full') # Optional centering + text_widget.tag_add("center", "1.0", "end") + + # Adjust line spacing with spacing2 (spacing between lines) + text_widget.configure(spacing2=5, state="disabled") # spacing2 adds space between lines + text_widget.grid(row=self.row_idx, column=0, columnspan=2, sticky="ew", pady=10) + + self.row_idx += 2 def add_heading(self, text): label = ttk.Label(self.root, text=text, wraplength=500, style="Heading.TLabel") - label.grid(row=self.row_idx, column=0, columnspan=2, sticky="w") + label.grid(row=self.row_idx, column=0, columnspan=2, sticky="w", pady=10) self.row_idx += 1 @@ -23,7 +35,8 @@ def add_row(self, text, widget=None, widget_grid_args=None): label = ttk.Label(self.root, text=text) label.grid(row=self.row_idx, column=0, sticky="w") - widget.grid(row=self.row_idx, column=1, sticky="ew", padx=10, **widget_grid_args) + # widget.grid(row=self.row_idx, column=1, sticky="ew", padx=10, **widget_grid_args) + widget.grid(row=self.row_idx, column=1, padx=10, **widget_grid_args) self.row_idx += 1 return widget @@ -37,8 +50,11 @@ def add_spacer_row(self): # Function to create a labeled entry def create_labeled_entry(self,parent, label_text, row, show=None, default_value=None): """Helper function to create a label and entry widget in a given parent.""" - label = ttk.Label(parent, text=label_text) + label = ttk.Label(parent, text=label_text,style="Heading.TLabel") label.grid(row=row, column=0, sticky='w', padx=5, pady=5) + entry_kwargs = {'style': 'Custom.TEntry'} + if show: + entry_kwargs['show'] = show entry = ttk.Entry(parent, show=show) if show else ttk.Entry(parent) entry.grid(row=row, column=1, sticky='ew', padx=5, pady=5) @@ -52,7 +68,7 @@ def create_labeled_entry(self,parent, label_text, row, show=None, default_value= # Function to create a labeled folder selector def create_labeled_folder_selector(self,parent, label_text, row, default_path): """Helper function to create a label and folder selector widget in a given parent.""" - label = ttk.Label(parent, text=label_text) + label = ttk.Label(parent, text=label_text,style="Heading.TLabel") label.grid(row=row, column=0, sticky='w', padx=5, pady=5) folder_selector = FolderSelector(parent, default_path)