1+ import tkinter as tk
2+ from tkinter import messagebox
3+ from PIL import ImageGrab , Image
4+ import openai
5+ import base64
6+ import io
7+ from config import get_openai_api_key
8+
9+ class AIScreenReader :
10+
11+ def __init__ (self , parent_app , logger ):
12+ self .parent_app = parent_app
13+ self .logger = logger
14+ try :
15+ self .client = openai .OpenAI (api_key = get_openai_api_key ())
16+ except Exception as e :
17+ self .logger .error (f"Failed to initialize OpenAI client: { e } " )
18+ messagebox .showerror ("OpenAI Error" , f"Failed to initialize OpenAI client. Please check your API key.\n \n { e } " )
19+ self .client = None
20+
21+ def _analyze_image (self , image : Image .Image , model : str , prompt : str ):
22+ if not self .client :
23+ return "OpenAI client not initialized."
24+
25+ self .logger .info (f"Sending image to OpenAI for analysis using model: { model } ..." )
26+
27+ buffered = io .BytesIO ()
28+ image .save (buffered , format = "PNG" )
29+ base64_image = base64 .b64encode (buffered .getvalue ()).decode ('utf-8' )
30+
31+ try :
32+ response = self .client .chat .completions .create (
33+ model = model ,
34+ messages = [
35+ {
36+ "role" : "user" ,
37+ "content" : [
38+ {"type" : "text" , "text" : prompt },
39+ {"type" : "image_url" , "image_url" : {"url" : f"data:image/png;base64,{ base64_image } " }},
40+ ],
41+ }
42+ ],
43+ max_tokens = 1000 ,
44+ )
45+ self .logger .info ("Received analysis from OpenAI." )
46+ return response .choices [0 ].message .content
47+ except Exception as e :
48+ self .logger .error (f"OpenAI API call failed: { e } " )
49+ messagebox .showerror ("OpenAI Error" , f"API call failed: { e } " )
50+ return f"Error analyzing image: { e } "
51+
52+ def analyze_full_screen (self , model : str , prompt : str ):
53+ self .logger .info ("Capturing full screen..." )
54+
55+ self .parent_app .withdraw ()
56+ self .parent_app .update_idletasks ()
57+
58+ screenshot = ImageGrab .grab (all_screens = True )
59+
60+ self .parent_app .deiconify ()
61+
62+ return self ._analyze_image (screenshot , model , prompt )
63+
64+ def analyze_screen_area (self , model : str , prompt : str ):
65+ self .logger .info ("Starting screen area selection..." )
66+ selector = ScreenAreaSelector (self .parent_app )
67+
68+ self .parent_app .withdraw ()
69+ self .parent_app .wait_window (selector .master )
70+
71+ screenshot = None
72+ if selector .bbox :
73+ self .logger .info (f"Area selected: { selector .bbox } " )
74+
75+ screen_width = self .parent_app .winfo_screenwidth ()
76+ screen_height = self .parent_app .winfo_screenheight ()
77+
78+ x1 = max (0 , selector .bbox [0 ])
79+ y1 = max (0 , selector .bbox [1 ])
80+ x2 = min (screen_width , selector .bbox [2 ])
81+ y2 = min (screen_height , selector .bbox [3 ])
82+
83+ if x1 >= x2 or y1 >= y2 :
84+ self .logger .warning ("Invalid area selected (zero or negative size). Aborting." )
85+ else :
86+ clamped_bbox = (x1 , y1 , x2 , y2 )
87+ self .logger .info (f"Clamped bbox to: { clamped_bbox } " )
88+ full_screenshot = ImageGrab .grab (all_screens = True )
89+ screenshot = full_screenshot .crop (clamped_bbox )
90+
91+ self .parent_app .deiconify ()
92+
93+ if screenshot :
94+ return self ._analyze_image (screenshot , model , prompt )
95+ else :
96+ self .logger .info ("Area selection cancelled." )
97+ return None
98+
99+
100+ class ScreenAreaSelector :
101+ def __init__ (self , parent_app ):
102+ self .parent_app = parent_app
103+ self .master = tk .Toplevel (parent_app )
104+ self .master .title ("Screen Selector" )
105+ self .master .attributes ("-fullscreen" , True )
106+ self .master .attributes ("-alpha" , 0.3 )
107+ self .master .bind ("<ButtonPress-1>" , self .on_press )
108+ self .master .bind ("<B1-Motion>" , self .on_drag )
109+ self .master .bind ("<ButtonRelease-1>" , self .on_release )
110+
111+ self .canvas = tk .Canvas (self .master , cursor = "cross" , bg = "grey" )
112+ self .canvas .pack (fill = tk .BOTH , expand = True )
113+
114+ if hasattr (self .parent_app , 'apply_privacy_settings_to_window' ):
115+ self .parent_app .apply_privacy_settings_to_window (self .master , apply_transparency = False )
116+
117+ self .start_x = None
118+ self .start_y = None
119+ self .rect = None
120+ self .bbox = None
121+
122+ def on_press (self , event ):
123+ self .start_x = self .canvas .canvasx (event .x )
124+ self .start_y = self .canvas .canvasy (event .y )
125+ if self .rect :
126+ self .canvas .delete (self .rect )
127+ self .rect = self .canvas .create_rectangle (self .start_x , self .start_y , self .start_x , self .start_y , outline = 'red' , width = 2 )
128+
129+ def on_drag (self , event ):
130+ cur_x , cur_y = (self .canvas .canvasx (event .x ), self .canvas .canvasy (event .y ))
131+ self .canvas .coords (self .rect , self .start_x , self .start_y , cur_x , cur_y )
132+
133+ def on_release (self , event ):
134+ end_x , end_y = (self .canvas .canvasx (event .x ), self .canvas .canvasy (event .y ))
135+ self .bbox = (min (self .start_x , end_x ), min (self .start_y , end_y ),
136+ max (self .start_x , end_x ), max (self .start_y , end_y ))
137+ self .master .destroy ()
0 commit comments