lybic-guiagents 0.1.0__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.

Potentially problematic release.


This version of lybic-guiagents might be problematic. Click here for more details.

Files changed (85) hide show
  1. desktop_env/__init__.py +1 -0
  2. desktop_env/actions.py +203 -0
  3. desktop_env/controllers/__init__.py +0 -0
  4. desktop_env/controllers/python.py +471 -0
  5. desktop_env/controllers/setup.py +882 -0
  6. desktop_env/desktop_env.py +509 -0
  7. desktop_env/evaluators/__init__.py +5 -0
  8. desktop_env/evaluators/getters/__init__.py +41 -0
  9. desktop_env/evaluators/getters/calc.py +15 -0
  10. desktop_env/evaluators/getters/chrome.py +1774 -0
  11. desktop_env/evaluators/getters/file.py +154 -0
  12. desktop_env/evaluators/getters/general.py +42 -0
  13. desktop_env/evaluators/getters/gimp.py +38 -0
  14. desktop_env/evaluators/getters/impress.py +126 -0
  15. desktop_env/evaluators/getters/info.py +24 -0
  16. desktop_env/evaluators/getters/misc.py +406 -0
  17. desktop_env/evaluators/getters/replay.py +20 -0
  18. desktop_env/evaluators/getters/vlc.py +86 -0
  19. desktop_env/evaluators/getters/vscode.py +35 -0
  20. desktop_env/evaluators/metrics/__init__.py +160 -0
  21. desktop_env/evaluators/metrics/basic_os.py +68 -0
  22. desktop_env/evaluators/metrics/chrome.py +493 -0
  23. desktop_env/evaluators/metrics/docs.py +1011 -0
  24. desktop_env/evaluators/metrics/general.py +665 -0
  25. desktop_env/evaluators/metrics/gimp.py +637 -0
  26. desktop_env/evaluators/metrics/libreoffice.py +28 -0
  27. desktop_env/evaluators/metrics/others.py +92 -0
  28. desktop_env/evaluators/metrics/pdf.py +31 -0
  29. desktop_env/evaluators/metrics/slides.py +957 -0
  30. desktop_env/evaluators/metrics/table.py +585 -0
  31. desktop_env/evaluators/metrics/thunderbird.py +176 -0
  32. desktop_env/evaluators/metrics/utils.py +719 -0
  33. desktop_env/evaluators/metrics/vlc.py +524 -0
  34. desktop_env/evaluators/metrics/vscode.py +283 -0
  35. desktop_env/providers/__init__.py +35 -0
  36. desktop_env/providers/aws/__init__.py +0 -0
  37. desktop_env/providers/aws/manager.py +278 -0
  38. desktop_env/providers/aws/provider.py +186 -0
  39. desktop_env/providers/aws/provider_with_proxy.py +315 -0
  40. desktop_env/providers/aws/proxy_pool.py +193 -0
  41. desktop_env/providers/azure/__init__.py +0 -0
  42. desktop_env/providers/azure/manager.py +87 -0
  43. desktop_env/providers/azure/provider.py +207 -0
  44. desktop_env/providers/base.py +97 -0
  45. desktop_env/providers/gcp/__init__.py +0 -0
  46. desktop_env/providers/gcp/manager.py +0 -0
  47. desktop_env/providers/gcp/provider.py +0 -0
  48. desktop_env/providers/virtualbox/__init__.py +0 -0
  49. desktop_env/providers/virtualbox/manager.py +463 -0
  50. desktop_env/providers/virtualbox/provider.py +124 -0
  51. desktop_env/providers/vmware/__init__.py +0 -0
  52. desktop_env/providers/vmware/manager.py +455 -0
  53. desktop_env/providers/vmware/provider.py +105 -0
  54. gui_agents/__init__.py +0 -0
  55. gui_agents/agents/Action.py +209 -0
  56. gui_agents/agents/__init__.py +0 -0
  57. gui_agents/agents/agent_s.py +832 -0
  58. gui_agents/agents/global_state.py +610 -0
  59. gui_agents/agents/grounding.py +651 -0
  60. gui_agents/agents/hardware_interface.py +129 -0
  61. gui_agents/agents/manager.py +568 -0
  62. gui_agents/agents/translator.py +132 -0
  63. gui_agents/agents/worker.py +355 -0
  64. gui_agents/cli_app.py +560 -0
  65. gui_agents/core/__init__.py +0 -0
  66. gui_agents/core/engine.py +1496 -0
  67. gui_agents/core/knowledge.py +449 -0
  68. gui_agents/core/mllm.py +555 -0
  69. gui_agents/tools/__init__.py +0 -0
  70. gui_agents/tools/tools.py +727 -0
  71. gui_agents/unit_test/__init__.py +0 -0
  72. gui_agents/unit_test/run_tests.py +65 -0
  73. gui_agents/unit_test/test_manager.py +330 -0
  74. gui_agents/unit_test/test_worker.py +269 -0
  75. gui_agents/utils/__init__.py +0 -0
  76. gui_agents/utils/analyze_display.py +301 -0
  77. gui_agents/utils/common_utils.py +263 -0
  78. gui_agents/utils/display_viewer.py +281 -0
  79. gui_agents/utils/embedding_manager.py +53 -0
  80. gui_agents/utils/image_axis_utils.py +27 -0
  81. lybic_guiagents-0.1.0.dist-info/METADATA +416 -0
  82. lybic_guiagents-0.1.0.dist-info/RECORD +85 -0
  83. lybic_guiagents-0.1.0.dist-info/WHEEL +5 -0
  84. lybic_guiagents-0.1.0.dist-info/licenses/LICENSE +201 -0
  85. lybic_guiagents-0.1.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,1011 @@
1
+ import logging
2
+ import os
3
+ import re
4
+ import xml.etree.ElementTree as ET
5
+ import zipfile
6
+ from io import BytesIO
7
+ from typing import List, Dict, Any
8
+
9
+ import easyocr
10
+ from PIL import Image
11
+ from docx import Document
12
+ from docx.enum.text import WD_PARAGRAPH_ALIGNMENT, WD_TAB_ALIGNMENT
13
+ from docx.shared import RGBColor
14
+ from odf.opendocument import load
15
+ from odf.text import P
16
+ from odf.text import Span
17
+ from rapidfuzz import fuzz
18
+ from skimage.color import deltaE_ciede2000
19
+ from skimage.color import rgb2lab
20
+
21
+ logger = logging.getLogger("desktopenv.metric.docs")
22
+
23
+
24
+ def find_default_font(config_file_path, rules):
25
+ """Find the default font in LibreOffice Writer."""
26
+ default_font = None
27
+ expected_font = rules["font_name"]
28
+
29
+ if not config_file_path:
30
+ return 0
31
+
32
+ try:
33
+ tree = ET.parse(config_file_path)
34
+ root = tree.getroot()
35
+
36
+ # Define the XML namespace used in the file
37
+ namespace = {'oor': 'http://openoffice.org/2001/registry'}
38
+
39
+ # Search for the node containing the default font setting for LibreOffice Writer
40
+ for elem in root.findall('.//item[@oor:path="/org.openoffice.Office.Writer/DefaultFont"]', namespace):
41
+ for prop in elem.findall('.//prop[@oor:name="Standard"]', namespace):
42
+ for value in prop.findall('value', namespace):
43
+ default_font = value.text
44
+ except Exception as e:
45
+ logger.error(f"Error: {e}")
46
+
47
+ return 1 if default_font == expected_font else 0
48
+
49
+
50
+ def contains_page_break(docx_file, rules):
51
+ if not docx_file:
52
+ return 0
53
+
54
+ try:
55
+ doc = Document(docx_file)
56
+ except Exception as e:
57
+ logger.error(f"Error: {e}")
58
+ return 0
59
+
60
+ try:
61
+ expected_page_break_count = rules["page_break_count"]
62
+ except Exception as e:
63
+ expected_page_break_count = None
64
+
65
+ namespaces = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
66
+
67
+ page_break_count = 0
68
+ for paragraph in doc.paragraphs:
69
+ for run in paragraph.runs:
70
+ br_elems = run.element.findall('.//w:br', namespaces)
71
+ for br in br_elems:
72
+ if br is not None and '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}type' in br.attrib and \
73
+ br.attrib['{http://schemas.openxmlformats.org/wordprocessingml/2006/main}type'] == 'page':
74
+ page_break_count += 1
75
+
76
+ if expected_page_break_count is not None and page_break_count != expected_page_break_count:
77
+ return 0
78
+
79
+ if page_break_count > 0:
80
+ return 1
81
+ else:
82
+ return 0
83
+
84
+ def compare_docx_files(file1, file2, **options):
85
+ ignore_blanks = options.get('ignore_blanks', True)
86
+ ignore_case = options.get('ignore_case', False)
87
+ ignore_order = options.get('ignore_order', False)
88
+ content_only = options.get('content_only', False)
89
+ fuzzy_match = options.get('fuzzy_match', False)
90
+ delete_empty_lines = options.get('delete_empty_lines', False)
91
+
92
+ if not file1 or not file2:
93
+ return 0
94
+
95
+ def get_paragraph_texts_odt(document):
96
+ paragraphs = document.getElementsByType(P)
97
+ paragraph_texts = []
98
+ for paragraph in paragraphs:
99
+ text_parts = []
100
+ for node in paragraph.childNodes:
101
+ if node.nodeType == node.TEXT_NODE:
102
+ text_parts.append(node.data)
103
+ elif node.nodeType == node.ELEMENT_NODE and node.tagName == 'text:span':
104
+ # Assuming direct text content in <text:span>, for simplicity
105
+ for child in node.childNodes:
106
+ if child.nodeType == child.TEXT_NODE:
107
+ text_parts.append(child.data)
108
+ paragraph_texts.append(''.join(text_parts))
109
+ return paragraph_texts
110
+
111
+ # Determine file types and load documents
112
+ if file1.endswith('.docx') and file2.endswith('.docx'):
113
+ try:
114
+ doc1 = Document(file1)
115
+ doc2 = Document(file2)
116
+ except Exception as e:
117
+ logger.error(f"Error: {e}")
118
+ return 0
119
+ doc1_paragraphs = [p.text for p in doc1.paragraphs]
120
+ doc2_paragraphs = [p.text for p in doc2.paragraphs]
121
+ if ignore_order:
122
+ doc1_paragraphs = sorted(doc1_paragraphs)
123
+ doc2_paragraphs = sorted(doc2_paragraphs)
124
+ if delete_empty_lines:
125
+ doc1_paragraphs = [p for p in doc1_paragraphs if p.strip()]
126
+ doc2_paragraphs = [p for p in doc2_paragraphs if p.strip()]
127
+ elif file1.endswith('.odt') and file2.endswith('.odt'):
128
+ try:
129
+ doc1 = load(file1)
130
+ doc2 = load(file2)
131
+ except Exception as e:
132
+ logger.error(f"Error: {e}")
133
+ return 0
134
+ doc1_paragraphs = get_paragraph_texts_odt(doc1)
135
+ doc2_paragraphs = get_paragraph_texts_odt(doc2)
136
+ if ignore_order:
137
+ doc1_paragraphs = sorted(doc1_paragraphs)
138
+ doc2_paragraphs = sorted(doc2_paragraphs)
139
+ if delete_empty_lines:
140
+ doc1_paragraphs = [p for p in doc1_paragraphs if p.strip()]
141
+ doc2_paragraphs = [p for p in doc2_paragraphs if p.strip()]
142
+ else:
143
+ # Unsupported file types or mismatch
144
+ print("Unsupported file types or mismatch between file types.")
145
+ return 0
146
+
147
+ if content_only:
148
+ # Compare the content of the documents
149
+ text1 = re.sub(r'\s+', ' ', '\n'.join(doc1_paragraphs)).strip()
150
+ text2 = re.sub(r'\s+', ' ', '\n'.join(doc2_paragraphs)).strip()
151
+ if ignore_case:
152
+ text1, text2 = text1.lower(), text2.lower()
153
+ similarity = fuzz.ratio(text1, text2) / 100.0
154
+ return similarity
155
+
156
+ # Process and compare documents
157
+ if ignore_blanks:
158
+ text1 = re.sub(r'\s+', ' ', '\n'.join(doc1_paragraphs)).strip()
159
+ text2 = re.sub(r'\s+', ' ', '\n'.join(doc2_paragraphs)).strip()
160
+ if ignore_case:
161
+ text1, text2 = text1.lower(), text2.lower()
162
+
163
+ if fuzzy_match:
164
+ similarity = fuzz.ratio(text1, text2) / 100.0
165
+ return similarity
166
+ else:
167
+ if text1 != text2:
168
+ return 0
169
+ else:
170
+ if len(doc1_paragraphs) != len(doc2_paragraphs):
171
+ print(doc1_paragraphs)
172
+ print(doc2_paragraphs)
173
+ print(len(doc1_paragraphs))
174
+ print(len(doc2_paragraphs))
175
+ return 0
176
+
177
+ if fuzzy_match:
178
+ total_similarity = 0
179
+ if not doc1_paragraphs:
180
+ return 1.0
181
+ for p1, p2 in zip(doc1_paragraphs, doc2_paragraphs):
182
+ if ignore_case:
183
+ p1, p2 = p1.lower(), p2.lower()
184
+ total_similarity += fuzz.ratio(p1, p2) / 100.0
185
+
186
+ if len(doc1_paragraphs) == 0:
187
+ return 1.0 if len(doc2_paragraphs) == 0 else 0.0
188
+
189
+ avg_similarity = total_similarity / len(doc1_paragraphs)
190
+ return avg_similarity
191
+ else:
192
+ # Compare each paragraph
193
+ for p1, p2 in zip(doc1_paragraphs, doc2_paragraphs):
194
+ if ignore_case:
195
+ p1, p2 = p1.lower(), p2.lower()
196
+ if p1 != p2:
197
+ # show the difference
198
+ print("=== First Paragraph ===")
199
+ print(f"\033[92m{repr(p1)}\033[0m") # Green color for p1, repr() shows hidden chars
200
+ print("=== Second Paragraph ===")
201
+ print(f"\033[91m{repr(p2)}\033[0m") # Red color for p2, repr() shows hidden chars
202
+ print("=" * 50) # Clear boundary
203
+ return 0
204
+
205
+ return 1
206
+
207
+
208
+ def compare_init_lines(file1, file2):
209
+ if not file1 or not file2:
210
+ return 0
211
+
212
+ try:
213
+ doc1 = Document(file1)
214
+ doc2 = Document(file2)
215
+ except Exception as e:
216
+ logger.error(f"Error: {e}")
217
+ return 0
218
+
219
+ doc1_paragraphs = [p.text for p in doc1.paragraphs]
220
+ doc2_paragraphs = [p.text for p in doc2.paragraphs]
221
+
222
+ # Compare each paragraph
223
+ for p1, p2 in zip(doc1_paragraphs, doc2_paragraphs):
224
+ if p1 != p2:
225
+ # print(p1)
226
+ # print(p2)
227
+ return 0
228
+
229
+ return 1
230
+
231
+
232
+ def compare_docx_tables(docx_file1, docx_file2):
233
+ if not docx_file1 or not docx_file2:
234
+ return 0
235
+
236
+ try:
237
+ doc1 = Document(docx_file1)
238
+ doc2 = Document(docx_file2)
239
+ except Exception as e:
240
+ logger.error(f"Error: {e}")
241
+ return 0
242
+
243
+ # get list of tables in docx
244
+ tables1 = doc1.tables
245
+ tables2 = doc2.tables
246
+
247
+ if len(tables1) != len(tables2):
248
+ return 0
249
+
250
+ # Compare each table content
251
+ for table1, table2 in zip(tables1, tables2):
252
+
253
+ if len(table1.rows) != len(table2.rows) or len(table1.columns) != len(table2.columns):
254
+ return 0
255
+
256
+ # Compare each cell
257
+ for i in range(len(table1.rows)):
258
+ for j in range(len(table1.columns)):
259
+ if table1.cell(i, j).text.strip() != table2.cell(i, j).text.strip():
260
+ return 0
261
+
262
+ return 1
263
+
264
+
265
+ def compare_docx_images(docx_file1, docx_file2):
266
+ if not docx_file1 or not docx_file2:
267
+ return 0
268
+
269
+ try:
270
+ doc1 = Document(docx_file1)
271
+ doc2 = Document(docx_file2)
272
+ except Exception as e:
273
+ logger.error(f"Error: {e}")
274
+ return 0
275
+
276
+ def extract_images(doc):
277
+ images = []
278
+ for rel in doc.part.rels.values():
279
+ if "image" in rel.reltype:
280
+ img_data = rel.target_part.blob
281
+ images.append(BytesIO(img_data))
282
+ return images
283
+
284
+ images1 = extract_images(doc1)
285
+ images2 = extract_images(doc2)
286
+ if len(images1) != len(images2):
287
+ return 0
288
+ for img1, img2 in zip(images1, images2):
289
+ if Image.open(img1).tobytes() != Image.open(img2).tobytes():
290
+ return 0
291
+ return 1
292
+
293
+
294
+ def compare_image_text(image_path, rule):
295
+ if not image_path:
296
+ return 0
297
+ reader = easyocr.Reader(['en'])
298
+ result = reader.readtext(image_path)
299
+ extracted_text = ' '.join([entry[1] for entry in result])
300
+
301
+ # Log OCR results
302
+ logger.info(f"OCR extracted texts: {[entry[1] for entry in result]}")
303
+ logger.info(f"Combined extracted text: {extracted_text}")
304
+
305
+ if rule['type'] == 'text':
306
+ target_text = rule['text']
307
+ match_found = target_text in extracted_text
308
+
309
+ # Log matching results
310
+ logger.info(f"Target text: '{target_text}'")
311
+ logger.info(f"Match found: {match_found}")
312
+ if match_found:
313
+ logger.info("✅ Text matching successful!")
314
+ else:
315
+ logger.info("❌ Text matching failed!")
316
+
317
+ return 1 if match_found else 0
318
+ else:
319
+ raise ValueError("Unsupported rule type")
320
+
321
+
322
+ def compare_line_spacing(docx_file1, docx_file2):
323
+ if not docx_file1 or not docx_file2:
324
+ return 0
325
+
326
+ if not compare_docx_files(docx_file1, docx_file2):
327
+ return 0
328
+
329
+ try:
330
+ doc1 = Document(docx_file1)
331
+ doc2 = Document(docx_file2)
332
+ except Exception as e:
333
+ logger.error(f"Error: {e}")
334
+ return 0
335
+
336
+ if len(doc1.paragraphs) != len(doc2.paragraphs):
337
+ return 0
338
+
339
+ # Compare each paragraph line spacing
340
+ for para1, para2 in zip(doc1.paragraphs, doc2.paragraphs):
341
+
342
+ spacing1 = para1.paragraph_format.line_spacing
343
+ spacing2 = para2.paragraph_format.line_spacing
344
+
345
+ if spacing1 != spacing2:
346
+ return 0
347
+
348
+ return 1
349
+
350
+
351
+ def compare_insert_equation(docx_file1, docx_file2):
352
+ if not docx_file1 or not docx_file2:
353
+ return 0
354
+
355
+ if not compare_docx_files(docx_file1, docx_file2):
356
+ return 0
357
+
358
+ try:
359
+ doc1 = Document(docx_file1)
360
+ doc2 = Document(docx_file2)
361
+ except Exception as e:
362
+ logger.error(f"Error: {e}")
363
+ return 0
364
+
365
+ # Compare each paragraph if it contains equation
366
+ for para1, para2 in zip(doc1.paragraphs, doc2.paragraphs):
367
+ for run1, run2 in zip(para1.runs, para2.runs):
368
+ if run1.element.xpath('.//w:object') and run2.element.xpath('.//w:object'):
369
+ return 1
370
+ return 0
371
+
372
+
373
+ def compare_font_names(docx_file, rules: List[Dict[str, Any]]):
374
+ if not docx_file:
375
+ return 0
376
+
377
+ try:
378
+ doc = Document(docx_file)
379
+ except Exception as e:
380
+ logger.error(f"Error: {e}")
381
+ return 0
382
+
383
+ expected_font = rules["font_name"]
384
+
385
+ for paragraph in doc.paragraphs:
386
+ for run in paragraph.runs:
387
+ font_name = run.font.name
388
+ if font_name != expected_font:
389
+ return 0
390
+ return 1
391
+
392
+
393
+ def compare_subscript_contains(docx_file1, docx_file2):
394
+ if not docx_file1 or not docx_file2:
395
+ return 0
396
+
397
+ try:
398
+ doc1 = Document(docx_file1)
399
+ doc2 = Document(docx_file2)
400
+ except Exception as e:
401
+ logger.error(f"Error: {e}")
402
+ return 0
403
+
404
+ for para1, para2 in zip(doc1.paragraphs, doc2.paragraphs):
405
+ for run1, run2 in zip(para1.runs, para2.runs):
406
+ # check if two paras both contain subscript
407
+ if run1.font.subscript and run2.font.subscript:
408
+ return 1
409
+ return 0
410
+
411
+
412
+ def has_page_numbers_in_footers(docx_file):
413
+ if not docx_file:
414
+ return 0
415
+
416
+ try:
417
+ doc = Document(docx_file)
418
+ except Exception as e:
419
+ logger.error(f"Error: {e}")
420
+ return 0
421
+
422
+ for section in doc.sections:
423
+ footer = section.footer
424
+ if footer is None:
425
+ return 0
426
+ footer_text = footer.paragraphs[0].text if footer.paragraphs else ''
427
+ if not any(char.isdigit() for char in footer_text):
428
+ # if no digit in footer, then no page number
429
+ return 0
430
+ return 1
431
+
432
+
433
+ def is_first_line_centered(docx_file):
434
+ if not docx_file:
435
+ return 0
436
+
437
+ try:
438
+ doc = Document(docx_file)
439
+ except Exception as e:
440
+ logger.error(f"Error: {e}")
441
+ return 0
442
+
443
+ first_paragraph = doc.paragraphs[0]
444
+
445
+ # check if the first line is center justified
446
+ return 1 if first_paragraph.paragraph_format.alignment == WD_PARAGRAPH_ALIGNMENT.CENTER else 0
447
+
448
+
449
+ def check_file_exists(directory, filename):
450
+ if not directory or not filename:
451
+ return 0
452
+ file_path = os.path.join(directory, filename)
453
+ return 1 if os.path.isfile(file_path) else 0
454
+
455
+
456
+ def check_tabstops(docx_file1, docx_file2, **kwargs) -> float:
457
+ if not docx_file1 or not docx_file2:
458
+ return .0
459
+
460
+ try:
461
+ doc1: Document = Document(docx_file1)
462
+ doc2: Document = Document(docx_file2)
463
+ except Exception as e:
464
+ logger.error(f"Error: {e}")
465
+ return .0
466
+
467
+ para1 = [p for p in doc1.paragraphs if p.text.strip()]
468
+ para2 = [p for p in doc2.paragraphs if p.text.strip()]
469
+ if len(para1) != len(para2): return .0
470
+
471
+ if kwargs.get('word_number_split_by_tabstop', None) is not None:
472
+ number = kwargs['word_number_split_by_tabstop']
473
+ index = kwargs.get('index', 0)
474
+ for p1 in para1:
475
+ splits = p1.text.split('\t')
476
+ if len(splits) == 0: return .0
477
+ words = list(filter(lambda x: x.strip(), re.split(r'\s', splits[index])))
478
+ if len(words) != number: return .0
479
+
480
+ section = doc2.sections[0]
481
+ paragraph_width = section.page_width - section.left_margin - section.right_margin
482
+ ignore_tabs = lambda x: x.alignment == WD_TAB_ALIGNMENT.CLEAR or (
483
+ x.alignment == WD_TAB_ALIGNMENT.LEFT and x.position == 0)
484
+ minus = .0
485
+ for p1, p2 in zip(para1, para2):
486
+ # filter CLEAR tabstop and default left-0 tabstop
487
+ tabs1 = [tst for tst in p1.paragraph_format.tab_stops if not ignore_tabs(tst)]
488
+ tabs2 = [tst for tst in p2.paragraph_format.tab_stops if not ignore_tabs(tst)]
489
+ if len(tabs1) != len(tabs2): return .0
490
+ difference = .0
491
+ for t1, t2 in zip(tabs1, tabs2):
492
+ if t1.alignment != t2.alignment: return .0
493
+ difference += abs(t1.position - t2.position)
494
+ minus += difference / paragraph_width
495
+ score = 1 - (minus / len(para1))
496
+ return score
497
+
498
+
499
+ def compare_contains_image(docx_file1, docx_file2):
500
+ if not docx_file1 or not docx_file2:
501
+ return 0
502
+
503
+ try:
504
+ doc1 = Document(docx_file1)
505
+ doc2 = Document(docx_file2)
506
+ except Exception as e:
507
+ logger.error(f"Error: {e}")
508
+ return 0
509
+
510
+ for para1, para2 in zip(doc1.paragraphs, doc2.paragraphs):
511
+ for run1, run2 in zip(para1.runs, para2.runs):
512
+ if ('graphicData' in run1._element.xml and 'graphicData' not in run2._element.xml) or (
513
+ 'graphicData' not in run1._element.xml and 'graphicData' in run2._element.xml):
514
+ return 0
515
+ return 1
516
+
517
+
518
+ def evaluate_colored_words_in_tables(file_path1, file_path2, **kwargs):
519
+ if not file_path1 or not file_path2:
520
+ return 0
521
+
522
+ if not compare_docx_files(file_path1, file_path2):
523
+ return 0
524
+
525
+ try:
526
+ document = Document(file_path1)
527
+ except Exception as e:
528
+ logger.error(f"Error: {e}")
529
+ return 0
530
+
531
+ threshold = kwargs.get('threshold', 3.5)
532
+
533
+ def _calculate_color_difference(rgb1, rgb2):
534
+ srgb1 = [rgb1[0] / 255.0, rgb1[1] / 255.0, rgb1[2] / 255.0]
535
+ srgb2 = [rgb2[0] / 255.0, rgb2[1] / 255.0, rgb2[2] / 255.0]
536
+ lab1, lab2 = rgb2lab(srgb1), rgb2lab(srgb2)
537
+ delta_e = deltaE_ciede2000(lab1, lab2)
538
+ return delta_e
539
+
540
+ for table in document.tables:
541
+ # Iterate through rows and cells in the table
542
+ for row in table.rows:
543
+ for cell in row.cells:
544
+ for paragraph in cell.paragraphs:
545
+ for run in paragraph.runs:
546
+ word = run.text
547
+ if word:
548
+ first_letter = word[0].lower()
549
+
550
+ if first_letter in 'aeiou' and _calculate_color_difference(run.font.color.rgb,
551
+ RGBColor(255, 0, 0)) > threshold:
552
+ return 0 # Vowel-colored words should be red
553
+ elif first_letter not in 'aeiou' and _calculate_color_difference(run.font.color.rgb,
554
+ RGBColor(0, 0,
555
+ 255)) > threshold:
556
+ return 0 # Non-vowel-colored words should be blue
557
+
558
+ return 1 # All words in tables are correctly colored
559
+
560
+
561
+ def check_highlighted_words(file_path1, file_path2):
562
+ if not file_path1 or not file_path2:
563
+ return 0
564
+
565
+ if not compare_docx_files(file_path1, file_path2):
566
+ return 0
567
+
568
+ doc = load(file_path1)
569
+ highlighted = False
570
+
571
+ for span in doc.getElementsByType(Span):
572
+ style_name = span.getAttribute('stylename')
573
+ if style_name:
574
+ for automatic_style in doc.automaticstyles.childNodes:
575
+ if automatic_style.getAttribute('name') == style_name:
576
+ for property in automatic_style.childNodes:
577
+ if property.getAttribute('backgroundcolor') == '#ffff00':
578
+ highlighted = True
579
+ break
580
+ if highlighted:
581
+ break
582
+
583
+ return 0 if highlighted else 1
584
+
585
+
586
+ def evaluate_strike_through_last_paragraph(file_path1, file_path2):
587
+ if not file_path1 or not file_path2:
588
+ return 0
589
+
590
+ if not compare_docx_files(file_path1, file_path2):
591
+ return 0
592
+
593
+ try:
594
+ document = Document(file_path1)
595
+ except Exception as e:
596
+ logger.error(f"Error: {e}")
597
+ return 0
598
+
599
+ # Get the last paragraph
600
+ last_paragraph = document.paragraphs[-1]
601
+
602
+ # Check if any run in the last paragraph has strike-through formatting
603
+ for run in last_paragraph.runs:
604
+ if not run.font.strike:
605
+ return 0 # At least one word does not have strike-through formatting
606
+
607
+ return 1 # All words in the last paragraph have strike-through formatting
608
+
609
+
610
+ def evaluate_conversion(file_path):
611
+ if not file_path:
612
+ return 0
613
+
614
+ try:
615
+ document = Document(file_path)
616
+ except Exception as e:
617
+ logger.error(f"Error: {e}")
618
+ return 0
619
+
620
+ for table in document.tables:
621
+ for row in table.rows:
622
+ for cell in row.cells:
623
+ for paragraph in cell.paragraphs:
624
+ for run in paragraph.runs:
625
+ if run.text.isupper():
626
+ return 0 # Uppercase text should be converted to lowercase
627
+
628
+ for paragraph in document.paragraphs:
629
+ for run in paragraph.runs:
630
+ if run.text.isupper():
631
+ return 0 # Uppercase text should be converted to lowercase
632
+
633
+ return 1 # All uppercase text has been successfully converted
634
+
635
+
636
+ def evaluate_spacing(file_path):
637
+ if not file_path:
638
+ return 0
639
+
640
+ try:
641
+ document = Document(file_path)
642
+ except Exception as e:
643
+ logger.error(f"Error: {e}")
644
+ return 0
645
+
646
+ # Check line spacing for introduction, body, and conclusion
647
+ introduction_spacing = document.paragraphs[0].paragraph_format.line_spacing
648
+ body_spacing = document.paragraphs[1].paragraph_format.line_spacing
649
+ conclusion_spacing = document.paragraphs[2].paragraph_format.line_spacing
650
+ if (introduction_spacing == 1.0 and body_spacing == 2.0 and conclusion_spacing == 1.5):
651
+ return 1
652
+ else:
653
+ return 0
654
+
655
+
656
+ def check_italic_font_size_14(path1, path2):
657
+ if not path1 or not path2:
658
+ return 0
659
+
660
+ if not compare_docx_files(path1, path2):
661
+ return 0
662
+
663
+ try:
664
+ document = Document(path1)
665
+ except Exception as e:
666
+ logger.error(f"Error: {e}")
667
+ return 0
668
+
669
+ for paragraph in document.paragraphs:
670
+ for run in paragraph.runs:
671
+ if run.italic:
672
+ # Check if font size is 14
673
+ if run.font.size is None or run.font.size.pt != 14:
674
+ return 0
675
+ return 1
676
+
677
+
678
+ def evaluate_alignment(docx_path):
679
+ if not docx_path:
680
+ return 0
681
+
682
+ # Load the document
683
+ try:
684
+ doc = Document(docx_path)
685
+ except Exception as e:
686
+ logger.error(f"Error: {e}")
687
+ return 0
688
+
689
+ # Iterate through each paragraph in the document
690
+ for para in doc.paragraphs:
691
+ # Split the paragraph into individual sentences
692
+ sentences = para.text.split('.')
693
+
694
+ for sentence in sentences:
695
+ # Split the sentence into words
696
+ words = sentence.strip().split()
697
+
698
+ # Check if the sentence has at least three words
699
+ if len(words) < 3:
700
+ continue # Skip sentences with less than three words
701
+
702
+ # The first three words should be separated from the rest
703
+ first_part = ' '.join(words[:3])
704
+ second_part = ' '.join(words[3:])
705
+
706
+ # Check if the sentence structure matches the pattern: first part + large space/tab + second part
707
+ if not (first_part in sentence and second_part in sentence and sentence.find(first_part) < sentence.find(
708
+ second_part)):
709
+ return 0 # The sentence does not meet the alignment criteria
710
+
711
+ return 1 # All sentences meet the alignment criteria
712
+
713
+
714
+ def get_unique_train_ids(initial_file): # fixed standard
715
+ if not initial_file:
716
+ return set(), 0
717
+
718
+ try:
719
+ doc = Document(initial_file)
720
+ except Exception as e:
721
+ logger.error(f"Error: {e}")
722
+ return set(), 0
723
+
724
+ train_ids = set()
725
+ processed_lines = 0
726
+
727
+ for para in doc.paragraphs:
728
+ line_parts = para.text.split(',')
729
+ if len(line_parts) == 4:
730
+ train_id = line_parts[1].strip()
731
+ if train_id not in train_ids:
732
+ train_ids.add(train_id)
733
+ processed_lines += 1
734
+
735
+ return train_ids, processed_lines
736
+
737
+
738
+ def check_no_duplicates(initial_file, processed_file):
739
+ if not initial_file or not processed_file:
740
+ return 0
741
+
742
+ # Open the document
743
+ train_ids_ini, ini_lines = get_unique_train_ids(initial_file)
744
+
745
+ try:
746
+ doc_processed = Document(processed_file)
747
+ except Exception as e:
748
+ logger.error(f"Error: {e}")
749
+ return 0
750
+
751
+ train_ids_pro = set()
752
+ processed_lines = 0 # Counter for valid lines processed
753
+
754
+ # processed
755
+ for para in doc_processed.paragraphs:
756
+ # Each line has the format: time_HH:MM:SS, train_id, station_id, platform_no
757
+ line_parts = para.text.split(',')
758
+ # Ensure the line has the correct format
759
+ if len(line_parts) == 4:
760
+ train_id = line_parts[1].strip()
761
+ # If train_id is already in the set, it's a duplicate
762
+ if train_id in train_ids_pro:
763
+ return 0 # Duplicate found
764
+ train_ids_pro.add(train_id)
765
+ processed_lines += 1 # Increment valid lines counter
766
+
767
+ if train_ids_pro != train_ids_ini or processed_lines != ini_lines:
768
+ return 0
769
+
770
+ # No duplicates found and at least one valid line was processed
771
+ return 1
772
+
773
+
774
+ def compare_docx_lines(file1, file2):
775
+ if not file1 or not file2:
776
+ return 0
777
+
778
+ # Read the text of the document, line by line
779
+ try:
780
+ doc1 = Document(file1)
781
+ doc2 = Document(file2)
782
+ except Exception as e:
783
+ logger.error(f"Error: {e}")
784
+ return 0
785
+
786
+ doc1_lines = [p.text.strip() for p in doc1.paragraphs if p.text.strip()]
787
+ doc2_lines = [p.text.strip() for p in doc2.paragraphs if p.text.strip()]
788
+ # print(doc1_lines)
789
+ # print(doc2_lines)
790
+
791
+ # Convert the list of lines to sets and compare
792
+ if set(doc1_lines) == set(doc2_lines):
793
+ return 1
794
+ else:
795
+ return 0
796
+
797
+
798
+ def compare_docx_files_and_ignore_new_lines(file1, file2, **options):
799
+ ignore_blanks = options.get('ignore_blanks', True)
800
+
801
+ if not file1 or not file2:
802
+ return 0
803
+
804
+ # Determine file types and load documents
805
+ if file1.endswith('.docx') and file2.endswith('.docx'):
806
+ try:
807
+ doc1 = Document(file1)
808
+ doc2 = Document(file2)
809
+ except Exception as e:
810
+ logger.error(f"Error: {e}")
811
+ return 0
812
+
813
+ # First, delete all the blank in paragraphs
814
+ doc1 = [p for p in doc1.paragraphs if p.text != '']
815
+ doc2 = [p for p in doc2.paragraphs if p.text != '']
816
+ doc1_paragraphs = [p.text for p in doc1]
817
+ doc2_paragraphs = [p.text for p in doc2]
818
+ else:
819
+ # Unsupported file types or mismatch
820
+ print("Unsupported file types or mismatch between file types.")
821
+ return 0
822
+
823
+ # Process and compare documents
824
+ if ignore_blanks:
825
+ text1 = re.sub(r'\s+', ' ', '\n'.join(doc1_paragraphs)).strip()
826
+ text2 = re.sub(r'\s+', ' ', '\n'.join(doc2_paragraphs)).strip()
827
+ if text1 != text2:
828
+ return 0
829
+ else:
830
+ if len(doc1_paragraphs) != len(doc2_paragraphs):
831
+ return 0
832
+ # Compare each paragraph
833
+ for p1, p2 in zip(doc1_paragraphs, doc2_paragraphs):
834
+ if p1 != p2:
835
+ return 0
836
+ return 1
837
+
838
+
839
+ # Docx file saved in the ubuntu cannot use this function to compare highlight, don't know why, deprecated
840
+ def compare_highlighted_text(file1, file2):
841
+ if not file1 or not file2:
842
+ return 0
843
+
844
+ def extract_highlighted_text(file_path):
845
+ highlighted_texts = []
846
+
847
+ # Open the .docx file as a zip file and read the document.xml
848
+ with zipfile.ZipFile(file_path, 'r') as docx:
849
+ with docx.open('word/document.xml') as document_xml:
850
+ tree = ET.parse(document_xml)
851
+ root = tree.getroot()
852
+
853
+ # Define the namespaces
854
+ namespaces = {
855
+ 'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
856
+ }
857
+
858
+ # Find all runs with highlight property
859
+ for run in root.findall('.//w:r', namespaces):
860
+ highlight = run.find('.//w:highlight', namespaces)
861
+ if highlight is not None and highlight.get(
862
+ '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val') != 'none':
863
+ text = run.find('.//w:t', namespaces)
864
+ if text is not None:
865
+ highlighted_texts.append(text.text)
866
+
867
+ return highlighted_texts
868
+
869
+ # Read the highlighted text from both documents
870
+ doc1_highlighted = extract_highlighted_text(file1)
871
+ doc2_highlighted = extract_highlighted_text(file2)
872
+
873
+ # Compare the sets of highlighted text to check if they are the same
874
+ if set(doc1_highlighted) == set(doc2_highlighted):
875
+ return 1
876
+ else:
877
+ return 0
878
+
879
+
880
+ def compare_references(file1, file2, **options):
881
+ if not file1 or not file2:
882
+ return 0
883
+
884
+ reference_indicator = options.get('reference_indicator', 'References')
885
+ reference_base_result = options.get('reference_base_result', 0.5)
886
+
887
+ # Determine file types and load documents
888
+ if file1.endswith('.docx') and file2.endswith('.docx'):
889
+ try:
890
+ doc1 = Document(file1)
891
+ doc2 = Document(file2)
892
+ except Exception as e:
893
+ logger.error(f"Error: {e}")
894
+ return 0
895
+
896
+ doc1_paragraphs = [p.text for p in doc1.paragraphs]
897
+ doc2_paragraphs = [p.text for p in doc2.paragraphs]
898
+ else:
899
+ # Unsupported file types or mismatch
900
+ print("Unsupported file types or mismatch between file types.")
901
+ return 0
902
+
903
+ # Find the references section in the paragraphs, find the idx of the last reference_indicator in the paragraph list
904
+ ref1_idx = doc1_paragraphs.index(reference_indicator) if reference_indicator in doc1_paragraphs else -1
905
+ ref2_idx = doc2_paragraphs.index(reference_indicator) if reference_indicator in doc2_paragraphs else -1
906
+
907
+ if ref1_idx == -1 and ref2_idx == -1:
908
+ return 1
909
+
910
+ if ref1_idx == -1 or ref2_idx == -1:
911
+ return 0
912
+
913
+ # split the reference section into reference items, and remove the empty string items
914
+ ref1 = [p for p in doc1_paragraphs[ref1_idx + 1:] if p.strip()]
915
+ ref2 = [p for p in doc2_paragraphs[ref2_idx + 1:] if p.strip()]
916
+
917
+ # Compare the references
918
+
919
+ if len(ref1) != len(ref2):
920
+ return 0
921
+
922
+ total_similarity = 0
923
+ for r1, r2 in zip(ref1, ref2):
924
+ # fuzzy match the references
925
+ similarity = fuzz.ratio(r1, r2) / 100.0
926
+ total_similarity += similarity
927
+
928
+ result = total_similarity / len(ref1)
929
+
930
+ epsilon = 0.01
931
+
932
+ if result >= reference_base_result + epsilon:
933
+ return (result - reference_base_result) / (1 - reference_base_result)
934
+ else:
935
+ return 0
936
+
937
+
938
+ def compare_unique_train_records(processed_file, expected_files, **kwargs):
939
+ """
940
+ Compares the processed file with a list of expected files containing the
941
+ gold standard and the initial document.
942
+ expected_files[0] should be the gold standard file.
943
+ expected_files[1] should be the initial file.
944
+ """
945
+ # Debug logging to understand what we're actually receiving
946
+ logger.info(f"DEBUG: processed_file type: {type(processed_file)}, value: {processed_file}")
947
+ logger.info(f"DEBUG: expected_files type: {type(expected_files)}, value: {expected_files}")
948
+ logger.info(f"DEBUG: kwargs: {kwargs}")
949
+
950
+ if not processed_file or not isinstance(expected_files, list) or len(expected_files) < 2:
951
+ logger.error("Invalid arguments: processed_file and a list of 2 expected_files are required.")
952
+ return 0
953
+
954
+ gold_file = expected_files[0]
955
+ initial_file = expected_files[1]
956
+
957
+ if not gold_file or not initial_file:
958
+ logger.error("Gold file or initial file path is missing from expected_files list.")
959
+ return 0
960
+
961
+ # Helper function to get lines and IDs from a file
962
+ def get_lines_and_ids_from_file(file_path):
963
+ try:
964
+ doc = Document(file_path)
965
+ lines = [p.text.strip() for p in doc.paragraphs if p.text.strip()]
966
+ train_ids = [line.split(',')[1].strip() for line in lines if len(line.split(',')) == 4]
967
+ return lines, train_ids
968
+ except Exception as e:
969
+ logger.error(f"Error opening or parsing file {file_path}: {e}")
970
+ return None, None
971
+
972
+ # Get data from all three files
973
+ processed_lines, processed_train_ids = get_lines_and_ids_from_file(processed_file)
974
+ if processed_lines is None: return 0
975
+
976
+ gold_lines, gold_train_ids = get_lines_and_ids_from_file(gold_file)
977
+ if gold_lines is None: return 0
978
+
979
+ initial_lines, _ = get_lines_and_ids_from_file(initial_file)
980
+ if initial_lines is None: return 0
981
+ initial_lines_set = set(initial_lines)
982
+
983
+ # 1. Subset Check: Ensure every processed line was in the initial file
984
+ if not set(processed_lines).issubset(initial_lines_set):
985
+ logger.error("Processed file contains lines not present in the initial file.")
986
+ logger.error(f"Extra lines: {set(processed_lines) - initial_lines_set}")
987
+ return 0
988
+
989
+ # 2. Uniqueness Check: Check for duplicates within the processed file
990
+ if len(processed_train_ids) != len(set(processed_train_ids)):
991
+ logger.error("Duplicate train_ids found in the processed file.")
992
+ return 0
993
+
994
+ # 3. Correctness Check: Compare the set of train_ids
995
+ if set(processed_train_ids) != set(gold_train_ids):
996
+ logger.error("Set of train_ids does not match between processed file and gold file.")
997
+ return 0
998
+
999
+ # 4. Line count check
1000
+ if len(processed_lines) != len(gold_lines):
1001
+ logger.error("Number of lines does not match between processed file and gold file.")
1002
+ return 0
1003
+
1004
+ return 1
1005
+
1006
+ if __name__ == "__main__":
1007
+ image_path = "/home/ubuntu/OSWorld/cache/02ce9a50-7af2-47ed-8596-af0c230501f8/ls.png"
1008
+ print(compare_image_text(image_path, {
1009
+ "type": "text",
1010
+ "text": "ls"
1011
+ }))