robotframework-aivision 0.2.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
AIVision/__init__.py ADDED
@@ -0,0 +1,29 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2025 Róbert Malovec
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ """
24
+ Main Robot Framework AI Library plugin entrypoint
25
+ """
26
+
27
+ from .library import AIVision
28
+
29
+ __all__ = ["AIVision"]
Binary file
AIVision/font/OFL.txt ADDED
@@ -0,0 +1,93 @@
1
+ Copyright 2020 The Anton Project Authors (https://github.com/googlefonts/AntonFont.git)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
AIVision/genai.py ADDED
@@ -0,0 +1,337 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2025 Róbert Malovec
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ from .platforms import Platforms
24
+ from openai import OpenAI
25
+ import os
26
+ import base64
27
+
28
+
29
+ class AIPlatform:
30
+ """Configuration class for AI platform settings."""
31
+ DEFAULT_IMG_DETAIL = "high"
32
+
33
+ def __init__(self, platform: Platforms = None, base_url: str = None,
34
+ api_key: str = None, model: str = None, image_detail: str = DEFAULT_IMG_DETAIL):
35
+ """
36
+ Initialize AI platform configuration.
37
+
38
+ Args:
39
+ platform: Platform enum value
40
+ base_url: Custom base URL (overrides platform default)
41
+ api_key: API key for authentication
42
+ model: Model name (overrides platform default)
43
+ image_detail: Image detail level for vision models
44
+ """
45
+ self.platform = platform
46
+ self.base_url = base_url or (platform.value["default_base_url"] if platform else None)
47
+ self.model = model or (platform.value["default_model"] if platform else None)
48
+ self.detail = image_detail
49
+ self.api_key = api_key
50
+ self.supports_vision = platform.value.get("supports_vision", False) if platform else False
51
+
52
+ # Validate API key requirement
53
+ if platform and platform.value.get("api_key_required", False) and not api_key:
54
+ raise ValueError(f"{platform.name} requires an API key")
55
+
56
+
57
+ class GenAI:
58
+ """
59
+ GenAI class for interacting with multiple AI platforms using OpenAI-compatible API.
60
+ Supports Ollama, Perplexity, and is easily extensible for other providers.
61
+ """
62
+
63
+ AUTOMATOR_INSTRUCTION = """
64
+ You are a response system for Robot Framework, specialized in test automation.
65
+ Your task is to evaluate an input instruction (assertion) against one or more provided images.
66
+ You must verify whether the assertion holds true based on the visual content of the images.
67
+ Make sure you observe images in every detail - all the logos, texts, titles, buttons, elements, inputs.
68
+
69
+ Your response must be strictly formatted like this:
70
+
71
+ RESULT: // PASS if assertion is verified, FAIL if not
72
+ EXPLANATION:
73
+ <brief explanation if TRUE, detailed explanation if FALSE>
74
+
75
+
76
+ When the assertion is TRUE:
77
+ Confirm the assertion and provide a brief explanation of why it was verified successfully.
78
+
79
+ When the assertion is FALSE:
80
+ Explain in detail what was wrong and why the assertion could not be verified.
81
+
82
+ Highlight any visual discrepancies, missing elements, or mismatches.
83
+
84
+ Example Inputs and Outputs:
85
+
86
+ Input Instruction: "The login button is visible and labeled 'Sign In'"
87
+ Provided Image: [screenshot of a login form]
88
+
89
+ Response when TRUE:
90
+
91
+ RESULT: pass
92
+ EXPLANATION:
93
+ 1. The login button is clearly visible
94
+ 2. The login button is labeled 'Sign In' as expected.
95
+
96
+
97
+ Response when FALSE:
98
+
99
+ RESULT: fail
100
+ EXPLANATION:
101
+ 1. The login button is either not visible or not labeled 'Sign In'.
102
+ 2. The visible button is labeled 'Log In' instead.
103
+
104
+
105
+ Ensure no other text is provided in the response.
106
+ """
107
+
108
+ def __init__(self, platform: Platforms = Platforms.Ollama, base_url: str = None,
109
+ api_key: str = None, model: str = None, image_detail: str = None,
110
+ simple_response: bool = True, initialize: bool = True,
111
+ system_prompt: str = AUTOMATOR_INSTRUCTION):
112
+ """
113
+ Initialize GenAI instance.
114
+
115
+ Args:
116
+ platform: AI platform to use (default: Ollama)
117
+ base_url: Custom base URL for API endpoint
118
+ api_key: API key for authentication
119
+ model: Model name to use
120
+ image_detail: Detail level for image processing
121
+ simple_response: Return simplified responses
122
+ initialize: Initialize client immediately
123
+ system_prompt: Main AI System prompt specifying Gen AI behavior
124
+ """
125
+ self.client = None
126
+ self.simple_response = simple_response
127
+ self.system_prompt = system_prompt or self.AUTOMATOR_INSTRUCTION
128
+
129
+ # Set default API key for platforms that don't require real keys
130
+ if platform == Platforms.Ollama and not api_key:
131
+ api_key = "ollama" # Required by OpenAI client but ignored by Ollama
132
+
133
+ self.ai_platform = AIPlatform(
134
+ platform=platform,
135
+ base_url=base_url,
136
+ api_key=api_key,
137
+ model=model,
138
+ image_detail=image_detail
139
+ )
140
+
141
+ if initialize:
142
+ self.initialize_genai(ai_platform=self.ai_platform)
143
+
144
+ def initialize_genai(self, ai_platform: AIPlatform = None):
145
+ """
146
+ Initialize the OpenAI client with platform-specific configuration.
147
+
148
+ Args:
149
+ ai_platform: AIPlatform configuration object
150
+ """
151
+ if not ai_platform:
152
+ raise ValueError("AI platform not specified")
153
+
154
+ if not ai_platform.base_url:
155
+ raise ValueError("Base URL is required")
156
+
157
+ # Initialize OpenAI client with platform-specific settings
158
+ self.client = OpenAI(
159
+ base_url=ai_platform.base_url,
160
+ api_key=ai_platform.api_key or "default"
161
+ )
162
+
163
+ self.ai_platform = ai_platform
164
+
165
+ def generate_ai_response(self, instructions: str, image_paths: list):
166
+ """
167
+ Generate AI response for test automation assertions with images.
168
+
169
+ Args:
170
+ instructions: Test assertion instructions
171
+ image_paths: List of image file paths to analyze
172
+
173
+ Returns:
174
+ AI-generated response
175
+ """
176
+ prompt = self._prepare_prompt(instructions, image_paths)
177
+ return self.chat_completion(prompt)
178
+
179
+ def chat_completion(self, messages):
180
+ """
181
+ Execute chat completion request.
182
+
183
+ Args:
184
+ messages: List of message dictionaries in OpenAI format
185
+
186
+ Returns:
187
+ Response content (simplified or full based on simple_response setting)
188
+ """
189
+ if not self.client:
190
+ raise RuntimeError("GenAI client not initialized. Call initialize_genai() first.")
191
+
192
+ # Convert messages format if needed (handle custom image format)
193
+ formatted_messages = self._format_messages_for_openai(messages)
194
+
195
+ try:
196
+ response = self.client.chat.completions.create(
197
+ model=self.ai_platform.model,
198
+ messages=formatted_messages
199
+ )
200
+
201
+ if self.simple_response:
202
+ return response.choices[0].message.content
203
+ else:
204
+ return response
205
+
206
+ except Exception as e:
207
+ raise Exception(f"Error during chat completion: {str(e)}")
208
+
209
+ def _prepare_prompt(self, instruction: str, image_paths: list = None):
210
+ """
211
+ Prepare prompt with instructions and images for test automation.
212
+
213
+ Args:
214
+ instruction: Test instruction/assertion
215
+ image_paths: List of image file paths
216
+
217
+ Returns:
218
+ Formatted prompt as list of messages
219
+ """
220
+ content = [
221
+ {
222
+ "type": "text",
223
+ "text": self.system_prompt
224
+ },
225
+ {
226
+ "type": "text",
227
+ "text": instruction
228
+ }
229
+ ]
230
+
231
+ # Add images if vision is supported
232
+ if image_paths and self.ai_platform.supports_vision:
233
+ for img_path in image_paths:
234
+ if not os.path.isfile(img_path):
235
+ raise FileNotFoundError(f"Image not found: {img_path}")
236
+
237
+ content.append({
238
+ "type": "image",
239
+ "image_path": img_path
240
+ })
241
+
242
+ return [{"role": "user", "content": content}]
243
+
244
+ def _format_messages_for_openai(self, messages):
245
+ """
246
+ Convert custom message format to OpenAI-compatible format.
247
+ Handles image paths by converting them to base64 data URIs.
248
+
249
+ Args:
250
+ messages: List of messages in custom format
251
+
252
+ Returns:
253
+ List of messages in OpenAI format
254
+ """
255
+ formatted_messages = []
256
+
257
+ for message in messages:
258
+ formatted_content = []
259
+
260
+ for item in message.get("content", []):
261
+ if item.get("type") == "image":
262
+ # Convert image file to base64 data URI
263
+ image_path = item.get("image_path")
264
+ if image_path and self.ai_platform.supports_vision:
265
+ image_data = self._encode_image_to_base64(image_path)
266
+ formatted_content.append({
267
+ "type": "image_url",
268
+ "image_url": {
269
+ "url": image_data,
270
+ "detail": self.ai_platform.detail
271
+ }
272
+ })
273
+ elif item.get("type") == "text":
274
+ formatted_content.append({
275
+ "type": "text",
276
+ "text": item.get("text", "")
277
+ })
278
+
279
+ formatted_messages.append({
280
+ "role": message.get("role"),
281
+ "content": formatted_content
282
+ })
283
+
284
+ return formatted_messages
285
+
286
+ @staticmethod
287
+ def _encode_image_to_base64(image_path: str) -> str:
288
+ """
289
+ Encode image file to base64 data URI.
290
+
291
+ Args:
292
+ image_path: Path to image file
293
+
294
+ Returns:
295
+ Base64-encoded data URI string
296
+ """
297
+ with open(image_path, "rb") as image_file:
298
+ image_data = base64.b64encode(image_file.read()).decode('utf-8')
299
+
300
+ # Detect image format from file extension
301
+ ext = os.path.splitext(image_path)[1].lower()
302
+ mime_types = {
303
+ '.png': 'image/png',
304
+ '.jpg': 'image/jpeg',
305
+ '.jpeg': 'image/jpeg',
306
+ '.gif': 'image/gif',
307
+ '.webp': 'image/webp'
308
+ }
309
+ mime_type = mime_types.get(ext, 'image/png')
310
+
311
+ return f"data:{mime_type};base64,{image_data}"
312
+
313
+ @staticmethod
314
+ def extract_result_and_explanation_from_response(response: str):
315
+ """
316
+ Extract RESULT and EXPLANATION from formatted response.
317
+
318
+ Args:
319
+ response: AI response string
320
+
321
+ Returns:
322
+ Tuple of (result, explanation)
323
+ """
324
+ parts = response.split("RESULT:", 1)
325
+ if len(parts) < 2:
326
+ return "fail", response
327
+
328
+ result_and_explanation = parts[1].strip()
329
+
330
+ parts = result_and_explanation.split("EXPLANATION:", 1)
331
+ if len(parts) < 2:
332
+ return parts[0].strip(), response
333
+
334
+ result = parts[0].strip()
335
+ explanation = parts[1].strip()
336
+
337
+ return result, explanation
AIVision/library.py ADDED
@@ -0,0 +1,437 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2025 Róbert Malovec
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ from .genai import GenAI
24
+ from .genai import Platforms
25
+ from PIL import Image, ImageDraw, ImageFont
26
+ from robot.api.deco import keyword
27
+ from robot.api import logger
28
+ from robot.libraries.BuiltIn import BuiltIn, RobotNotRunningError
29
+ from datetime import datetime
30
+ import os
31
+
32
+ """
33
+ GenAI Testing library module for Robot Framework
34
+ """
35
+
36
+
37
+ def _get_rf_output_dir():
38
+ """Returns Robot Framework output directory path"""
39
+ try:
40
+ output_dir = BuiltIn().get_variable_value("${OUTPUT_DIR}")
41
+ except RobotNotRunningError:
42
+ output_dir = os.getcwd()
43
+
44
+ return output_dir
45
+
46
+
47
+ class AIVision:
48
+ """
49
+ AI Vision Library module for Robot Framework
50
+
51
+ This RF library provides GenAI enabled front-end, UI and visual templates testing capabilities
52
+
53
+ """
54
+ ROBOT_LIBRARY_SCOPE = "GLOBAL"
55
+ FONT = os.path.join(
56
+ os.path.dirname(os.path.realpath(__file__)), "font", "Anton-Regular.ttf"
57
+ )
58
+ OUTPUT_DIR = _get_rf_output_dir()
59
+
60
+ def __init__(self, base_url: str = None, api_key: str = None, platform: Platforms = Platforms.Ollama,
61
+ model: str = None, image_detail: str = None, simple_response: bool = True,
62
+ initialize: bool = True, system_prompt: str = None):
63
+
64
+ self.genai = GenAI(base_url=base_url, api_key=api_key, platform=platform,
65
+ model=model, image_detail=image_detail,
66
+ simple_response=simple_response, initialize=initialize, system_prompt=system_prompt)
67
+
68
+ @keyword
69
+ def verify_that(self, screenshot_paths, instructions):
70
+ """Verifies that the image matches the instructions
71
+
72
+ Input parameters:
73
+
74
+ ``image_path``: (required) Path(s) to the image. Can be a single path or a list of paths
75
+
76
+ ``instructions``: (required) Instructions to be verified
77
+
78
+ *Examples*:
79
+
80
+ | Verify That | /path/to/image.png | Contains green logo in top right corner |
81
+
82
+ | @{img_paths} = | Create List | /path/to/image1.png | /path/to/image2.png |
83
+ | Verify That | ${img_paths} | First image contains logo as referenced on 2nd image. |
84
+ """
85
+ screenshot_paths = [screenshot_paths] if isinstance(screenshot_paths, str) else screenshot_paths
86
+ response = self.genai.generate_ai_response(instructions=f"Verify that: {instructions}", image_paths=screenshot_paths)
87
+ logger.debug(response)
88
+
89
+ self._assert_result(response)
90
+
91
+ @keyword
92
+ def verify_screenshot_matches_look_and_feel_template(self, screenshot_path, template_path,
93
+ override_instructions: str = None,
94
+ create_combined_image: bool = True):
95
+ """Verifies that the screenshot matches the look and feel template
96
+
97
+ Input parameters:
98
+
99
+ ``screenshot_path``: (required) Path to the screenshot image
100
+
101
+ ``template_path``: (required) Path to the template image
102
+
103
+ ``override_instructions``: (optional) If specified, it will override the built-in assertion instructions
104
+
105
+ ``create_combined_image``: (optional) default is _True_. If _True_, combined image will be created and saved
106
+
107
+ _Return Value_ is the path of the saved image
108
+
109
+ *Examples*:
110
+ | Verify Screenshot Matches Look And Feel Template | /path/to/screenshot.png | /path/to/template.png |
111
+ | Verify Screenshot Matches Look And Feel Template | /path/to/screenshot.png | /path/to/template.png | override_instructions=Custom instructions |
112
+ """
113
+ if create_combined_image:
114
+ try:
115
+ self.combine_images_on_paths_side_by_side(screenshot_path, template_path, "Actual", "Expected",
116
+ save=True)
117
+ except Exception as e:
118
+ logger.warn(f"Could not create combined image: {e}")
119
+
120
+ instructions = """First image is showing actual application view.
121
+ Second image is reference design template.
122
+ Verify screenshot matches look and feel template. Pay attention to details, design is important.
123
+ Make sure to check also all the visible logos, titles, labels, spelling, texts, links, menus, banners
124
+ and any available graphics. Always doublecheck the reference image in case you think some
125
+ text, label, logo or element is overlapping or containing typo.
126
+ """
127
+ if override_instructions:
128
+ instructions = override_instructions
129
+ response = self.genai.generate_ai_response(
130
+ instructions=instructions,
131
+ image_paths=[screenshot_path, template_path])
132
+
133
+ self._assert_result(response)
134
+
135
+ @staticmethod
136
+ @keyword
137
+ def open_image(image_path, mode="RGB"):
138
+ """Opens image from provided path
139
+
140
+ Input parameters:
141
+
142
+ ``image_path``: (required) Path to the image
143
+
144
+ ``mode``: (optional) default is _RGB_.
145
+ Defines type and depth of a pixel to which the opened image will be converted.
146
+ Supported modes can be seen
147
+ [https://pillow.readthedocs.io/en/3.0.x/handbook/concepts.html#modes|here].
148
+
149
+ _Return value_ is the PIL Image object
150
+
151
+ *Example*:
152
+ | ${image} = | Open Image | /path/to/image.png |
153
+ | ${image} = | Open Image | /path/to/image.png | RGBA |
154
+ """
155
+
156
+ try:
157
+ image = Image.open(image_path)
158
+ logger.debug(f"Image '{image_path}' was opened successfully")
159
+ except Exception as err:
160
+ raise AssertionError(
161
+ f"Could not open image on provided path:\n{type(err).__name__}: {err}"
162
+ )
163
+
164
+ if image.mode != mode:
165
+ logger.debug(
166
+ f"Image is in mode '{image.mode}' but desired is '{mode}'. Starting conversion."
167
+ )
168
+ try:
169
+ image = image.convert(mode=mode)
170
+ logger.debug(f"Image successfully converted to mode '{mode}'")
171
+ except Exception as err:
172
+ raise AssertionError(
173
+ f"Could not convert image to provided mode:\n{type(err).__name__}: {err}"
174
+ )
175
+
176
+ return image
177
+
178
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
179
+ @keyword
180
+ def save_image(
181
+ self,
182
+ image,
183
+ image_name=None,
184
+ image_format=None,
185
+ watermark=None,
186
+ image_path=OUTPUT_DIR,
187
+ ):
188
+ """Saves image to provided path and name
189
+
190
+ Input parameters:
191
+
192
+ ``image``: (required) PIL image object to save
193
+
194
+ ``image_name``: (optional) Name of the image.
195
+ If empty image name will be auto-generated
196
+
197
+ ``image_format``: (optional) If not set the image format will be determined from the _image_name_ extension
198
+ if specified there
199
+
200
+ ``watermark``: (optional) If the specified image will be watermarked with the specified string in top left corner
201
+
202
+ ``image_path``: (optional) Path to image to save.
203
+ If not specified images are being stored to the Robot Framework output directory
204
+
205
+ _Return Value_: Path of the saved image
206
+
207
+ *Examples*:
208
+ | Save Image | ${image}| my_image.png |
209
+ | Save Image | ${image}| my_image | png |
210
+ | Save Image | ${image}| my_image.png | watermark=My Label |
211
+ | Save Image | ${image}| my_image.png | image_path=/path/to/my/image/directory |
212
+ """
213
+ try:
214
+ if not image_name:
215
+ if image_format:
216
+ image_name = self.generate_image_name(extension=image_format)
217
+ else:
218
+ image_name = self.generate_image_name()
219
+
220
+ dest = os.path.join(image_path, image_name)
221
+
222
+ if image_format:
223
+ dest = os.path.join(dest, ".", image_format.lower())
224
+
225
+ logger.debug(f"Image will be saved to '{dest}'")
226
+
227
+ if watermark:
228
+ logger.debug(f"Adding watermark '{watermark}' to image")
229
+ image = self.add_watermark_to_image(image, watermark)
230
+
231
+ image.save(dest)
232
+
233
+ except Exception as err:
234
+ raise AssertionError(f"Could not save image:\n{type(err).__name__}: {err}")
235
+
236
+ logger.info(
237
+ f"<img width='800' src='{os.path.relpath(dest, image_path)}'/>", html=True
238
+ )
239
+
240
+ return dest
241
+
242
+ @staticmethod
243
+ @keyword
244
+ def generate_image_name(prefix="Snap", extension="png"):
245
+ """Generates unique image name with the specified prefix and optional image extension
246
+
247
+ Input parameters:
248
+
249
+ ``prefix``: (optional) default is "Image"
250
+
251
+ ``extension``: (optional) default is _png_
252
+
253
+ _Return Value_ is generated string representing unique image name
254
+
255
+ *Examples*:
256
+ | ${img_name} = | Generate Image Name |
257
+ | ${img_name} = | Generate Image Name | My-Image |
258
+ | ${img_name} = | Generate Image Name | My-Image | jpg |
259
+ """
260
+ if not prefix:
261
+ prefix = ""
262
+ name_template = "%s%s"
263
+ else:
264
+ name_template = "%s-%s"
265
+
266
+ image_name = name_template % (
267
+ prefix,
268
+ datetime.now().strftime("%m-%d-%Y_%H-%M-%S-%f")[:-3],
269
+ )
270
+ if extension:
271
+ image_name = f"{image_name}.{extension.lower()}"
272
+
273
+ logger.debug(f"Generated image name is: {image_name}")
274
+
275
+ return image_name
276
+
277
+ @keyword
278
+ def combine_images_on_paths_side_by_side(self, image_path1, image_path2, watermark1=None, watermark2=None,
279
+ mode="RGB", save=True):
280
+ """Combines two images specified by file path to one big image side-by-side
281
+
282
+ Input parameters:
283
+
284
+ ``image_path1``: (required) Path to the first image
285
+
286
+ ``image_path2``: (required) Path to the second image
287
+
288
+ ``watermark1``: (optional) If specified image1 will be watermarked with the specified string in top left corner
289
+
290
+ ``watermark2``: (optional) If specified image2 will be watermarked with the specified string in top left corner
291
+
292
+ ``mode``: (optional) default is _RGB_.
293
+
294
+ _Return Value_ is combined image as PIL Image format
295
+
296
+ *Examples*:
297
+ | ${image} = | Combine Images On Paths Side By Side | /path/to/image1.png | /path/to/image2.png |
298
+ | ${image} = | Combine Images On Paths Side By Side | /path/to/image1.png | /path/to/image2.png | Expected | Actual |
299
+
300
+ """
301
+ img1 = self.open_image(image_path1, mode=mode)
302
+ img2 = self.open_image(image_path2, mode=mode)
303
+
304
+ combined_img = self.combine_images_side_by_side(img1, img2, watermark1=watermark1, watermark2=watermark2,
305
+ mode=mode)
306
+
307
+ if save:
308
+ self.save_image(combined_img)
309
+
310
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
311
+ @keyword
312
+ def combine_images_side_by_side(
313
+ self, image1, image2, watermark1=None, watermark2=None, mode="RGB"
314
+ ):
315
+ """Combines two images to one big image side-by-side
316
+
317
+ Input parameters:
318
+
319
+ ``image1``: (required) Image one (PIL Image object) to combine
320
+
321
+ ``image2``: (required) Image two (PIL Image object) to combine
322
+
323
+ ``watermark1``: (optional) If specified image1 will be watermarked with the specified string in top left corner
324
+
325
+ ``watermark2``: (optional) If specified image2 will be watermarked with the specified string in top left corner
326
+
327
+ ``mode``: (optional) default is _RGB_.
328
+ Defines type and depth of a pixel which will be used for watermark layer.
329
+ You do not need typically change this value.
330
+ Supported modes can be seen
331
+ [https://pillow.readthedocs.io/en/3.0.x/handbook/concepts.html#modes|here].
332
+
333
+ _Return Value_ is combined image as PIL Image format
334
+
335
+ *Examples*:
336
+ | ${image} = | Combine Images Side By Side | ${image1} | ${image2} |
337
+ | ${image} = | Combine Images Side By Side | ${image1} | ${image2} |RGBA |
338
+ """
339
+ try:
340
+ # Create empty image for both images to fit
341
+ combined_image = Image.new(
342
+ mode,
343
+ (
344
+ image1.size[0] + image2.size[0] + 1,
345
+ max(image1.size[1], image2.size[1]),
346
+ ),
347
+ )
348
+
349
+ if watermark1:
350
+ logger.debug(f"Adding watermark '{watermark1}' to image1")
351
+ image1 = self.add_watermark_to_image(image1, watermark1)
352
+
353
+ if watermark2:
354
+ logger.debug(f"Adding watermark '{watermark2}' to image2")
355
+ image2 = self.add_watermark_to_image(image2, watermark2)
356
+
357
+ # Concatenate both images to one big image
358
+ combined_image.paste(image1, (0, 0))
359
+ combined_image.paste(image2, (image1.size[0] + 1, 0))
360
+
361
+ except Exception as err:
362
+ raise AssertionError(
363
+ f"Could not create combined image:\n{type(err).__name__}: {err}"
364
+ )
365
+
366
+ return combined_image
367
+
368
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
369
+ @keyword
370
+ def add_watermark_to_image(
371
+ self, image, text, color="red", text_size=50, text_position=(0, 0), mode="RGB"
372
+ ):
373
+ """Adds watermark text to the image
374
+
375
+ Input parameters:
376
+
377
+ ``image``: (required) PIL image object to add watermark to
378
+
379
+ ``text``: (required) Text string which will be added to the image
380
+
381
+ ``color``: (optional) default is _red_.
382
+ Text color of the watermark
383
+
384
+ ``text_size``: (optional) default is _50_.
385
+ Text size of the watermark
386
+
387
+ ``text_position``: (optional) default is _(0, 0)_.
388
+ Represents X,Y coordinates in the image where to add watermark
389
+
390
+ ``mode``: (optional) default is _RGB_.
391
+ Defines type and depth of a pixel which will be used for watermark layer.
392
+ You do not need typically change this value.
393
+ Supported modes can be seen
394
+ [https://pillow.readthedocs.io/en/3.0.x/handbook/concepts.html#modes|here].
395
+
396
+ _Return Value_ represents PIL Image object
397
+
398
+ *Examples*:
399
+ | ${w_image} = | Add Watermark To Image | ${image} | Original |
400
+ | ${w_image} = | Add Watermark To Image | ${image} | Original | blue |
401
+ | ${w_image} = | Add Watermark To Image | ${image} | text=Original | color=blue |
402
+ """
403
+ font_path = self.FONT
404
+
405
+ try:
406
+ font = ImageFont.truetype(font_path, text_size)
407
+ except Exception as err:
408
+ raise AssertionError(
409
+ f"Could not set watermark font:\n{type(err).__name__}: {err}"
410
+ )
411
+
412
+ try:
413
+ # Create watermark drawing canvas object
414
+ watermark = Image.new(mode, image.size)
415
+ draw = ImageDraw.ImageDraw(watermark, mode)
416
+
417
+ # Create semi-transparent watermark text
418
+ draw.text(text_position, text, fill=color, font=font)
419
+ mask = watermark.convert("L").point(lambda x: min(x, 100))
420
+ watermark.putalpha(mask)
421
+
422
+ # Create new image with watermark
423
+ w_image = image.copy()
424
+ w_image.paste(watermark, None, watermark)
425
+ except Exception as err:
426
+ raise AssertionError(
427
+ f"Could not create watermark:\n{type(err).__name__}: {err}"
428
+ )
429
+
430
+ return w_image
431
+
432
+ def _assert_result(self, response):
433
+ result, explanation = self.genai.extract_result_and_explanation_from_response(response)
434
+ if result and result.lower() == "pass":
435
+ logger.info(f"Verification passed:\n{explanation}")
436
+ else:
437
+ raise AssertionError(f"Verification failed:\n{explanation}")
AIVision/platforms.py ADDED
@@ -0,0 +1,68 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2025 Róbert Malovec
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ from enum import Enum
24
+
25
+
26
+ class Platforms(Enum):
27
+ """Enum defining supported AI platforms with their default configurations."""
28
+ Ollama = {
29
+ "default_model": "qwen3-coder:480b-cloud",
30
+ "default_base_url": "http://localhost:11434/v1",
31
+ "api_key_required": False,
32
+ "supports_vision": True
33
+ }
34
+
35
+ DockerModel = {
36
+ "default_model": "ai/qwen3-vl:8B-Q8_K_XL",
37
+ "default_base_url": "http://localhost:12434/engines/v1",
38
+ "api_key_required": False,
39
+ "supports_vision": True
40
+ }
41
+
42
+ OpenAI = {
43
+ "default_model": "gpt-5.2",
44
+ "default_base_url": "https://api.openai.com/v1",
45
+ "api_key_required": True,
46
+ "supports_vision": True
47
+ }
48
+
49
+ Perplexity = {
50
+ "default_model": "sonar-pro",
51
+ "default_base_url": "https://api.perplexity.ai",
52
+ "api_key_required": True,
53
+ "supports_vision": True
54
+ }
55
+
56
+ Gemini = {
57
+ "default_model": "gemini-2.5-flash",
58
+ "default_base_url": "https://generativelanguage.googleapis.com/v1beta/openai/",
59
+ "api_key_required": True,
60
+ "supports_vision": True
61
+ }
62
+
63
+ Manual = {
64
+ "default_model": None,
65
+ "default_base_url": None,
66
+ "api_key_required": True,
67
+ "supports_vision": True
68
+ }
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: robotframework_aivision
3
+ Version: 0.2.0a1
4
+ Summary: AI Vision library for Robot Framework
5
+ Home-page: https://github.com/robco/robotframework-aivision.git
6
+ Author: Róbert Malovec
7
+ Author-email: robert@malovec.sk
8
+ License: MIT License
9
+
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ License-File: LICENSE.txt
13
+ Requires-Dist: robotframework
14
+ Requires-Dist: pillow
15
+ Requires-Dist: openai
16
+ Dynamic: author
17
+ Dynamic: author-email
18
+ Dynamic: description
19
+ Dynamic: description-content-type
20
+ Dynamic: home-page
21
+ Dynamic: license
22
+ Dynamic: license-file
23
+ Dynamic: requires-dist
24
+ Dynamic: summary
25
+
26
+ [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/robco)
27
+ # Robot Framework AI Vision Library
28
+
29
+ AI VIsion Library for Robot Framework that verifies UI/screenshots (including template “look & feel”) by sending instructions plus one or more images to an OpenAI-compatible API (Ollama, OpenAI, Perplexity, Gemini, etc.).
30
+
31
+ The main keyword (`Verify That`) expects the model to return a strict `RESULT:` / `EXPLANATION:` format and will fail the test if the result is not `pass`.
32
+
33
+ ## Features
34
+
35
+ - Visual assertions on one or more screenshots using natural-language instructions.
36
+ - Template comparison keyword to validate “actual vs expected” look & feel (optionally creates a side-by-side image).
37
+ - Image utilities built on Pillow: open/convert, watermark, combine images, auto-generate names, save into Robot Framework output directory.
38
+ - Works with multiple providers via the `openai` Python client and OpenAI-compatible endpoints (`base_url`).
39
+
40
+ ## Installation
41
+
42
+ **Install from PyPI (once published):**
43
+ ```bash
44
+ pip install -U robotframework-aivision
45
+ ```
46
+
47
+ Runtime dependencies include Robot Framework, Pillow, and the `openai` Python client.
48
+
49
+ ## Configuration
50
+
51
+ Import the library in Robot Framework and choose a provider using `platform` plus optional overrides (`base_url`, `api_key`, `model`, `image_detail`).
52
+
53
+ ### Robot Framework import examples
54
+
55
+ **Default (Ollama-like local setup):**
56
+ ```robotframework
57
+ *** Settings ***
58
+ Library AIVision
59
+ ```
60
+
61
+ **OpenAI (API key required):**
62
+ ```robotframework
63
+ *** Settings ***
64
+ Library AIVision
65
+ ... platform=OpenAI
66
+ ... api_key=%{OPENAI_API_KEY}
67
+ ... model=gpt-5.2
68
+ ```
69
+
70
+ **Perplexity:**
71
+ ```robotframework
72
+ *** Settings ***
73
+ Library AIVision
74
+ ... platform=Perplexity
75
+ ... api_key=%{PPLX_API_KEY}
76
+ ... model=sonar-pro
77
+ ```
78
+
79
+ **Gemini (OpenAI-compatible endpoint):**
80
+ ```robotframework
81
+ *** Settings ***
82
+ Library AIVision
83
+ ... platform=Gemini
84
+ ... api_key=%{GEMINI_API_KEY}
85
+ ... model=gemini-2.5-flash
86
+ ```
87
+
88
+ ### Supported platforms (defaults)
89
+
90
+ The library defines these platform presets (model and `base_url`) which you can override via import arguments.
91
+
92
+ | Platform | Default `base_url` | Default model | API key |
93
+ |---|---|---|---|
94
+ | Ollama | `http://localhost:11434/v1` | `qwen3-coder:480b-cloud` | Not required |
95
+ | DockerModel | `http://localhost:12434/engines/v1` | `ai/qwen3-vl:8B-Q8_K_XL` | Not required. |
96
+ | OpenAI | `https://api.openai.com/v1` | `gpt-5.2` | Required. |
97
+ | Perplexity | `https://api.perplexity.ai` | `sonar-pro` | Required. |
98
+ | Gemini | `https://generativelanguage.googleapis.com/v1beta/openai/` | `gemini-2.5-flash` | Required. |
99
+ | Manual | `None` | `None` | Required. |
100
+
101
+ ## Keywords
102
+
103
+ All keywords below are implemented in `AIVision` and are available after importing the library.
104
+
105
+ | Keyword | Purpose |
106
+ |---|---|
107
+ | `Verify That` | Send one or more screenshots + instructions to the model, parse the `RESULT` and raise `AssertionError` on failure. |
108
+ | `Verify Screenshot Matches Look And Feel Template` | Compare a screenshot against a reference template with a built-in instruction set; optional combined image creation. |
109
+ | `Open Image` | Open an image (and optionally convert mode, default `RGB`). |
110
+ | `Save Image` | Save a PIL image to a path (defaults to RF output directory) with optional watermark. |
111
+ | `Generate Image Name` | Create a unique timestamp-based filename with prefix/extension. |
112
+ | `Combine Images On Paths Side By Side` | Combine two image files side-by-side (optionally watermark) and optionally save. |
113
+ | `Combine Images Side By Side` | Combine two in-memory PIL images side-by-side (optionally watermark). |
114
+ | `Add Watermark To Image` | Add watermark text using the included font file. |
115
+
116
+ ## Usage examples
117
+
118
+ ### Simple visual assertion
119
+
120
+ ```robotframework
121
+ *** Settings ***
122
+ Library AIVision platform=Ollama
123
+
124
+ *** Test Cases ***
125
+ Login button is correct
126
+ Verify That ${CURDIR}/screens/login.png Login button is visible and labeled as 'Sign In'
127
+ ```
128
+
129
+ ### Compare screenshot to design template
130
+
131
+ ```robotframework
132
+ *** Settings ***
133
+ Library AIVision
134
+
135
+ *** Test Cases ***
136
+ Home page matches template
137
+ Verify Screenshot Matches Look And Feel Template
138
+ ... ${CURDIR}/screens/home_actual.png
139
+ ... ${CURDIR}/templates/home_expected.png
140
+ ```
141
+
142
+ ### Override template instructions
143
+
144
+ ```robotframework
145
+ *** Settings ***
146
+ Library AIVision
147
+
148
+ *** Test Cases ***
149
+ Home page matches template - custom rules
150
+ Verify Screenshot Matches Look And Feel Template
151
+ ... ${CURDIR}/screens/home_actual.png
152
+ ... ${CURDIR}/templates/home_expected.png
153
+ ... override_instructions=Verify layout, spacing, typography, and brand colors match the template exactly.
154
+ ```
155
+
156
+
157
+ Version history
158
+ ----------------
159
+
160
+ 0.2.0a1, 2026-02-02 -- Alpha1 version
161
+ 0.2.0, 2026-01-29 -- AI System Prompt is configurable
162
+ 0.1.0, 2025-12-19 -- Additional GenAI Providers added
163
+ 0.0.1, 2024-05-11 -- Initial version
@@ -0,0 +1,12 @@
1
+ AIVision/__init__.py,sha256=Dp_ukmv2lgMOTdS-8xzUokr4IfC_aB91cGPfrinbHUA,1224
2
+ AIVision/genai.py,sha256=Na_EYVSGkir_1xqdpJocG5MVZUw9tSc9CV0JBEwxdzY,11678
3
+ AIVision/library.py,sha256=MZ-JDEC6rsuUuVscIijDtJcYgiiZEARdI94b_qLQ8Wc,16952
4
+ AIVision/platforms.py,sha256=vuozdhmHrhLlM85z4mHTlCiY2STvU_cgDyc0XD441vs,2388
5
+ AIVision/font/Anton-Regular.ttf,sha256=wkY8RDEDa8ak_mppboCdPAo9E3Md1h08n4wD9GYHpjE,161212
6
+ AIVision/font/OFL.txt,sha256=7mfm7iJ5C3kp8aN2nKKAHVZcZLWpCWlCwa31WW3pyeQ,4484
7
+ robotframework_aivision-0.2.0a1.dist-info/licenses/LICENSE,sha256=GnlimnQtg17QJ712UYWtmfPQFsme_q4cUxAO79yvkZ0,1072
8
+ robotframework_aivision-0.2.0a1.dist-info/licenses/LICENSE.txt,sha256=GnlimnQtg17QJ712UYWtmfPQFsme_q4cUxAO79yvkZ0,1072
9
+ robotframework_aivision-0.2.0a1.dist-info/METADATA,sha256=h0OhQJw3O_xFkLPf4IipgujylcwZSgXN-MvVydsAw4E,5529
10
+ robotframework_aivision-0.2.0a1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
11
+ robotframework_aivision-0.2.0a1.dist-info/top_level.txt,sha256=Z-P4V4dmnDuQoeciyqyQzGjkPLxcj924T4zKr5fNEuk,9
12
+ robotframework_aivision-0.2.0a1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Róbert Malovec
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Róbert Malovec
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ AIVision