IO4IT 4.0.24__tar.gz → 4.0.26__tar.gz

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.
Files changed (115) hide show
  1. {io4it-4.0.24 → io4it-4.0.26}/PKG-INFO +1 -1
  2. {io4it-4.0.24 → io4it-4.0.26}/io4it.egg-info/PKG-INFO +1 -1
  3. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/ocr_function/word_converter.py +156 -40
  4. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/toolViews/linkHTLMWorkflow_manager_ui.py +2 -2
  5. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/custom_api.py +84 -43
  6. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWCustomLLMApi.py +15 -3
  7. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWExportMarkdown.py +2 -2
  8. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWJsonToDataTable.py +2 -2
  9. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWOfficeNormalizer.py +61 -20
  10. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWParserHTML.py +2 -2
  11. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWProcessPoolExecutor.py +44 -21
  12. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWWebSearch.py +2 -2
  13. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owofficenormalizer.ui +18 -2
  14. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owprocesspoolexecutor.ui +17 -30
  15. {io4it-4.0.24 → io4it-4.0.26}/setup.py +1 -1
  16. {io4it-4.0.24 → io4it-4.0.26}/io4it.egg-info/SOURCES.txt +0 -0
  17. {io4it-4.0.24 → io4it-4.0.26}/io4it.egg-info/dependency_links.txt +0 -0
  18. {io4it-4.0.24 → io4it-4.0.26}/io4it.egg-info/entry_points.txt +0 -0
  19. {io4it-4.0.24 → io4it-4.0.26}/io4it.egg-info/namespace_packages.txt +0 -0
  20. {io4it-4.0.24 → io4it-4.0.26}/io4it.egg-info/requires.txt +0 -0
  21. {io4it-4.0.24 → io4it-4.0.26}/io4it.egg-info/top_level.txt +0 -0
  22. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/__init__.py +0 -0
  23. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/ocr_function/__init__.py +0 -0
  24. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/toolViews/designer/key_manager_ui.ui +0 -0
  25. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/toolViews/designer/link_workflow_manager_ui.ui +0 -0
  26. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/toolViews/key_manager_ui.py +0 -0
  27. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/__init__.py +0 -0
  28. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/environment_collector.py +0 -0
  29. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/file2images.py +0 -0
  30. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/keys_manager.py +0 -0
  31. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/mail.py +0 -0
  32. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/pool_exec_utils.py +0 -0
  33. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/pst_extractor/TigerODMPST.dll +0 -0
  34. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/pst_extractor/__init__.py +0 -0
  35. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/pst_extractor/export_pst.py +0 -0
  36. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/pst_extractor/libpff.dll +0 -0
  37. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/pst_extractor/z.dll +0 -0
  38. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/secret_manager.py +0 -0
  39. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/utils_md.py +0 -0
  40. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWChatGpt.py +0 -0
  41. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWDeep_Search.py +0 -0
  42. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWDoclingToMarkdown.py +0 -0
  43. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWExtractTablesDocxToXlsx.py +0 -0
  44. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWInboxMailMonitoring.py +0 -0
  45. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWMD2HTML.py +0 -0
  46. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWMarkdownLoader.py +0 -0
  47. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWMarkdownizer.py +0 -0
  48. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWPPTX2JPG.py +0 -0
  49. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWPdfType.py +0 -0
  50. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWPipAuditor.py +0 -0
  51. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWS3Uploader.py +0 -0
  52. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWS3downloader.py +0 -0
  53. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWS3list.py +0 -0
  54. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWXLSX_insert.py +0 -0
  55. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWmailLoader.py +0 -0
  56. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWmailSender.py +0 -0
  57. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWwordpdf2docx.py +0 -0
  58. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/__init__.py +0 -0
  59. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/__init__.py +0 -0
  60. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/nogui.ui +0 -0
  61. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/ow_extract_tables_docx_to_xlsx.ui +0 -0
  62. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/ow_file_ext_selector.ui +0 -0
  63. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/ow_json_to_data_table.ui +0 -0
  64. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owchatgpt.ui +0 -0
  65. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owcustomllmapi.ui +0 -0
  66. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owdeepsearch.ui +0 -0
  67. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owdoclingtomarkdown.ui +0 -0
  68. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owexportmarkdown.ui +0 -0
  69. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owfile2images.ui +0 -0
  70. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owinboxmailmonitoring.ui +0 -0
  71. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owinsertdatatoexcel.ui +0 -0
  72. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owmailloader.ui +0 -0
  73. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owmailsender.ui +0 -0
  74. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owmarkdownizer.ui +0 -0
  75. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owmarkdownloader.ui +0 -0
  76. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owmd2html.ui +0 -0
  77. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owparserhtml.ui +0 -0
  78. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owpdftype.ui +0 -0
  79. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owpipauditor.ui +0 -0
  80. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owspeechtotext.ui +0 -0
  81. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owvisualizationer.ui +0 -0
  82. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owwebsearch.ui +0 -0
  83. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/wordpdf2docx.ui +0 -0
  84. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/__init__.py +0 -0
  85. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/chatgpt.png +0 -0
  86. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/check_pdf.png +0 -0
  87. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/deepsearch.svg +0 -0
  88. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/dep_md_old.png +0 -0
  89. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/download.png +0 -0
  90. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/export_md.png +0 -0
  91. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/extract_table.png +0 -0
  92. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/file2images.png +0 -0
  93. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/file_extensor.png +0 -0
  94. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/html.png +0 -0
  95. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/json-file.png +0 -0
  96. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/list_aws.png +0 -0
  97. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/load_md.png +0 -0
  98. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/mail_loader.png +0 -0
  99. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/mail_writer.png +0 -0
  100. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/md.png +0 -0
  101. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/monitor-email.svg +0 -0
  102. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/office_normalizer.png +0 -0
  103. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/owcustomllmapi.svg +0 -0
  104. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/owmd2html.svg +0 -0
  105. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/pip_auditor.png +0 -0
  106. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/process_pool_executor.png +0 -0
  107. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/speech_to_text.png +0 -0
  108. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/upload.png +0 -0
  109. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/visualizationer.png +0 -0
  110. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/websearch.png +0 -0
  111. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/wordpdf2docx.png +0 -0
  112. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/xlsx_insert.png +0 -0
  113. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons_dev/__init__.py +0 -0
  114. {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/__init__.py +0 -0
  115. {io4it-4.0.24 → io4it-4.0.26}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: io4it
3
- Version: 4.0.24
3
+ Version: 4.0.26
4
4
  Home-page:
5
5
  Author:
6
6
  Author-email:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: io4it
3
- Version: 4.0.24
3
+ Version: 4.0.26
4
4
  Home-page:
5
5
  Author:
6
6
  Author-email:
@@ -16,6 +16,81 @@ def enable_long_path(path):
16
16
  """Simplifie la gestion des chemins longs sous Windows."""
17
17
  return pathlib.Path(r"\\?\\" + str(path))
18
18
 
19
+ def get_word_application():
20
+ """
21
+ Retourne une instance Word DÉDIÉE au script, dans un processus séparé.
22
+
23
+ Point clé : on utilise DispatchEx et NON Dispatch.
24
+ - Dispatch("Word.Application") -> se raccroche au Word déjà ouvert par
25
+ l'utilisateur (Running Object Table). Un Quit() fermerait alors TOUTE
26
+ l'application, donc le document de l'utilisateur.
27
+ - DispatchEx("Word.Application") -> force la création d'un NOUVEAU
28
+ processus WINWORD.EXE, isolé. Notre Quit() ne fermera que cette
29
+ instance ; le document ouvert par l'utilisateur n'est jamais touché.
30
+
31
+ L'instance est rendue invisible pour ne pas polluer l'écran de l'utilisateur.
32
+ """
33
+ word = win32com.client.DispatchEx("Word.Application")
34
+ try:
35
+ word.Visible = False # mettre True uniquement pour débogage
36
+ except Exception:
37
+ pass
38
+ try:
39
+ word.DisplayAlerts = 0 # pas de pop-up (sur NOTRE instance uniquement)
40
+ except Exception:
41
+ pass
42
+ return word
43
+
44
+
45
+ def safe_quit_word(word):
46
+ """
47
+ Ferme notre instance Word dédiée SANS jamais faire perdre de données.
48
+
49
+ Cas normal (DispatchEx a bien créé un processus séparé) : après nos
50
+ conversions, l'instance ne contient plus aucun document -> on quitte
51
+ proprement.
52
+
53
+ Cas anormal (DispatchEx est retombé sur le Word de l'utilisateur) :
54
+ il reste des documents ouverts que nous n'avons pas ouverts. On REFUSE
55
+ alors de forcer la fermeture :
56
+ - on tente de sauvegarder EN PLACE, sans dialogue bloquant, les
57
+ documents déjà associés à un fichier (doc.Save() est non destructif :
58
+ il réécrit le fichier existant, il ne crée pas de "Enregistrer sous") ;
59
+ - les documents jamais enregistrés (sans chemin) sont laissés tels
60
+ quels, en mémoire ;
61
+ - on NE quitte PAS : l'utilisateur reprend la main et sauvegarde
62
+ lui-même comme il l'entend.
63
+ """
64
+ if word is None:
65
+ return
66
+ try:
67
+ count = word.Documents.Count
68
+ except Exception:
69
+ count = 0
70
+
71
+ if count > 0:
72
+ # Instance non vide -> on ne force rien.
73
+ try:
74
+ for i in range(1, count + 1):
75
+ try:
76
+ d = word.Documents.Item(i)
77
+ # d.Saved == False -> modifications non enregistrées
78
+ # d.Path non vide -> le document a déjà un fichier associé
79
+ if (not d.Saved) and d.Path:
80
+ d.Save() # réécrit le fichier existant, aucune boîte de dialogue
81
+ except Exception:
82
+ pass
83
+ except Exception:
84
+ pass
85
+ return # surtout pas de Quit : on préserve le travail de l'utilisateur
86
+
87
+ # Instance vide : c'est bien la nôtre, on peut la fermer.
88
+ try:
89
+ word.Quit()
90
+ except Exception:
91
+ pass
92
+
93
+
19
94
  def convert_pdf_structure(input_dir: str, output_dir: str,ignore_exsting_out_put=False,forceBasicConvertion=False,progress_callback=None):
20
95
  """
21
96
  return a string with log in case of error
@@ -37,38 +112,52 @@ def convert_pdf_structure(input_dir: str, output_dir: str,ignore_exsting_out_put
37
112
  input_path = Path(str(input_dir[i]))
38
113
  for pdf_file in input_path.rglob("*.pdf"):
39
114
  nbre_file += 1
115
+
116
+ pythoncom.CoInitialize()
117
+ word = None
118
+ try:
119
+ word = get_word_application()
120
+ except Exception as e:
121
+ pythoncom.CoUninitialize()
122
+ return f"Impossible de démarrer Microsoft Word : {e}"
40
123
 
41
124
  k = 1
42
- for i, data in enumerate(input_dir):
43
- input_path = Path(str(input_dir[i]))
44
- output_path = Path(str(output_dir[i]))
125
+ try:
126
+ for i, data in enumerate(input_dir):
127
+ input_path = Path(str(input_dir[i]))
128
+ output_path = Path(str(output_dir[i]))
45
129
 
46
- if not input_path.exists() or not input_path.is_dir():
47
- print(f"Error: The input directory '{input_dir}' does not exist or is not a directory.")
48
- return f"Error: The input directory '{input_dir}' does not exist or is not a directory. "
130
+ if not input_path.exists() or not input_path.is_dir():
131
+ print(f"Error: The input directory '{input_dir}' does not exist or is not a directory.")
132
+ return f"Error: The input directory '{input_dir}' does not exist or is not a directory. "
49
133
 
50
- for pdf_file in input_path.rglob("*.pdf"): # Recursively search for .pdf and .PDF files
51
- print("traitement de ",str(pdf_file))
52
- relative_path = pdf_file.relative_to(input_path) # Get relative path from input root
53
- new_file_path = output_path / relative_path.with_suffix(".docx") # Change extension to .docx
134
+ for pdf_file in input_path.rglob("*.pdf"): # Recursively search for .pdf and .PDF files
135
+ print("traitement de ",str(pdf_file))
136
+ relative_path = pdf_file.relative_to(input_path) # Get relative path from input root
137
+ new_file_path = output_path / relative_path.with_suffix(".docx") # Change extension to .docx
54
138
 
55
139
 
56
140
 
57
- if ignore_exsting_out_put:
58
- if os.path.exists(enable_long_path(str(new_file_path))):
59
- print("ignoring",enable_long_path(str(new_file_path)))
60
- continue
141
+ if ignore_exsting_out_put:
142
+ if os.path.exists(enable_long_path(str(new_file_path))):
143
+ print("ignoring",enable_long_path(str(new_file_path)))
144
+ continue
61
145
 
62
146
 
63
- if 0!= convert_pdf_with_temp(str(pdf_file),str(new_file_path),forceBasicConvertion):#convert_pdf_with_temp #convert_pdf_to_docx
64
- if error_log!="":
65
- error_log+="\n"
66
- error_log+="error -> "+str(pdf_file)
67
- return error_log # a supprimer
68
- if progress_callback is not None:
69
- progress_value = float(100 * (k) / nbre_file)
70
- k += 1
71
- progress_callback(progress_value)
147
+ if 0!= convert_pdf_with_temp(str(pdf_file),str(new_file_path),forceBasicConvertion,word=word):#convert_pdf_with_temp #convert_pdf_to_docx
148
+ if error_log!="":
149
+ error_log+="\n"
150
+ error_log+="error -> "+str(pdf_file)
151
+ return error_log # a supprimer
152
+ if progress_callback is not None:
153
+ progress_value = float(100 * (k) / nbre_file)
154
+ k += 1
155
+ progress_callback(progress_value)
156
+ finally:
157
+ # On ne ferme QUE notre instance dédiée (processus séparé), et jamais
158
+ # si elle contient encore des documents de l'utilisateur.
159
+ safe_quit_word(word)
160
+ pythoncom.CoUninitialize()
72
161
  # purge temp dir if everithing is ok
73
162
  if error_log=="":
74
163
  reset_folder(get_local_store_path() + "temp_word_conversion/", attempts=10, delay=0.05, recreate=False)
@@ -77,13 +166,22 @@ def convert_pdf_structure(input_dir: str, output_dir: str,ignore_exsting_out_put
77
166
 
78
167
 
79
168
 
80
- def convert_pdf_to_docx(pdf_path, docx_path):
169
+ def convert_pdf_to_docx(pdf_path, docx_path, word=None):
81
170
  """
82
171
  Convertit un fichier PDF en DOCX en utilisant Microsoft Word.
83
172
 
84
173
  Args:
85
174
  pdf_path (str): Chemin du fichier PDF source.
86
175
  docx_path (str): Chemin du fichier DOCX de destination.
176
+ word: instance Word.Application à réutiliser. Si None, une instance
177
+ DÉDIÉE (processus séparé via DispatchEx) est créée puis fermée
178
+ à la fin de l'appel.
179
+
180
+ Cette fonction ne ferme JAMAIS un Word ouvert par l'utilisateur :
181
+ - si `word` est fourni, elle ne fait que fermer le document qu'elle a
182
+ ouvert (doc.Close), sans jamais appeler Quit ;
183
+ - si `word` est None, elle crée sa propre instance isolée et ne ferme
184
+ que celle-ci.
87
185
 
88
186
  Returns:
89
187
  int: 0 si la conversion a réussi, 1 en cas d'échec.
@@ -92,14 +190,14 @@ def convert_pdf_to_docx(pdf_path, docx_path):
92
190
  print(f"Erreur : Le fichier {pdf_path} n'existe pas.")
93
191
  return 1
94
192
 
193
+ own_instance = word is None
194
+ doc = None
95
195
  try:
96
- # Initialiser COM
97
- pythoncom.CoInitialize()
196
+ if own_instance:
197
+ # Initialiser COM et créer une instance dédiée isolée
198
+ pythoncom.CoInitialize()
199
+ word = get_word_application()
98
200
 
99
- # Lancer Word
100
- word = win32com.client.Dispatch("Word.Application")
101
- word.DisplayAlerts = 0 # Désactiver les alertes
102
- word.Visible = True # Mettre à True pour voir Word en action
103
201
  print(f"Conversion de {pdf_path} en {docx_path}...")
104
202
 
105
203
  # Ouvrir le PDF en lecture seule
@@ -108,20 +206,26 @@ def convert_pdf_to_docx(pdf_path, docx_path):
108
206
  # Sauvegarder en DOCX
109
207
  doc.SaveAs(docx_path, FileFormat=16) # 16 = wdFormatDocumentDefault
110
208
  doc.Close(False)
209
+ doc = None
111
210
 
112
211
  print(f"Conversion réussie : {docx_path}")
113
212
  return 0
114
213
 
115
214
  except Exception as e:
116
215
  print(f"Erreur lors de la conversion : {e}")
216
+ # Toujours refermer le document qu'on a ouvert, jamais l'application
217
+ if doc is not None:
218
+ try:
219
+ doc.Close(False)
220
+ except Exception:
221
+ pass
117
222
  return 1
118
223
 
119
224
  finally:
120
- if 'word' in locals():
121
- word.Quit()
122
-
123
- # Libérer COM
124
- pythoncom.CoUninitialize()
225
+ # On ne quitte Word QUE si c'est nous qui avons créé l'instance.
226
+ if own_instance and word is not None:
227
+ safe_quit_word(word)
228
+ pythoncom.CoUninitialize()
125
229
 
126
230
 
127
231
  def wait_for_file_access(file_path, timeout=10, interval=0.5):
@@ -259,11 +363,16 @@ def write_two_strings_to_file(file_path: str,string1: str, string2: str):
259
363
  return 0
260
364
 
261
365
 
262
- def convert_pdf_with_temp(temp_pdf, output_path,forceBasicConvertion=False):
366
+ def convert_pdf_with_temp(temp_pdf, output_path,forceBasicConvertion=False, word=None):
263
367
  """
264
368
  Copie le PDF source dans un dossier temporaire, le convertit en DOCX,
265
369
  puis copie le fichier résultant vers le chemin de sortie spécifié,
266
370
  en gérant les chemins longs.
371
+
372
+ word : instance Word.Application à réutiliser pour toutes les tentatives.
373
+ Si None, une instance DÉDIÉE (processus séparé) est créée puis
374
+ fermée à la fin de l'appel. Dans tous les cas, un Word ouvert par
375
+ l'utilisateur n'est jamais fermé.
267
376
  """
268
377
  pdf_path = enable_long_path(os.path.abspath(temp_pdf))
269
378
  output_path = enable_long_path(os.path.abspath(output_path))
@@ -277,7 +386,11 @@ def convert_pdf_with_temp(temp_pdf, output_path,forceBasicConvertion=False):
277
386
  if not output_dir.exists():
278
387
  output_dir.mkdir(parents=True, exist_ok=True)
279
388
 
389
+ own_instance = word is None
280
390
  try:
391
+ if own_instance:
392
+ pythoncom.CoInitialize()
393
+ word = get_word_application()
281
394
  dest_dir = get_local_store_path() + "temp_word_conversion/"
282
395
  if 0 != reset_folder(dest_dir, attempts=10, delay=0.05):
283
396
  print("impossible to reset " + dest_dir)
@@ -304,10 +417,10 @@ def convert_pdf_with_temp(temp_pdf, output_path,forceBasicConvertion=False):
304
417
  wait_for_file_access(temp_pdf)
305
418
  time.sleep(1)
306
419
  result=0
307
- # Conversion du PDF en DOCX
420
+ # Conversion du PDF en DOCX (on réutilise la même instance Word dédiée)
308
421
  for _ in range(4):
309
422
  time.sleep(1)
310
- result = convert_pdf_to_docx(str(temp_pdf), str(temp_docx))
423
+ result = convert_pdf_to_docx(str(temp_pdf), str(temp_docx), word=word)
311
424
  if result==0:
312
425
  break
313
426
  if result == 0:
@@ -329,5 +442,8 @@ def convert_pdf_with_temp(temp_pdf, output_path,forceBasicConvertion=False):
329
442
  print(f"Erreur : {e}")
330
443
  return 1
331
444
 
332
-
333
-
445
+ finally:
446
+ # On ne quitte Word QUE si c'est nous qui avons créé l'instance ici.
447
+ if own_instance and word is not None:
448
+ safe_quit_word(word)
449
+ pythoncom.CoUninitialize()
@@ -7,11 +7,11 @@ import ast
7
7
  if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
8
8
  from Orange.widgets.orangecontrib.AAIT.utils import MetManagement
9
9
  from Orange.widgets.orangecontrib.AAIT.utils.import_uic import uic
10
- from Orange.widgets.orangecontrib.HLIT_dev.remote_server_smb import convert, hlit_workflow_management
10
+ from Orange.widgets.orangecontrib.HLIT.remote_server_smb import convert, hlit_workflow_management
11
11
  else:
12
12
  from orangecontrib.AAIT.utils import MetManagement
13
13
  from orangecontrib.AAIT.utils.import_uic import uic
14
- from orangecontrib.HLIT_dev.remote_server_smb import convert, hlit_workflow_management
14
+ from orangecontrib.HLIT.remote_server_smb import convert, hlit_workflow_management
15
15
 
16
16
  FUNCTION_NAMES = [
17
17
  "Create",
@@ -1,11 +1,18 @@
1
- import base64
2
- import mimetypes
3
1
  import os
2
+ import re
3
+ import json
4
+ import copy
5
+ import numpy as np
4
6
 
5
7
  import requests
6
8
 
7
- import Orange.data
8
- from Orange.data import StringVariable
9
+ from Orange.data import StringVariable, Table
10
+
11
+
12
+ if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
13
+ from Orange.widgets.orangecontrib.AAIT.llm.answers_llama import write_tokens_to_file, prompt_to_messages, conv_to_messages
14
+ else:
15
+ from orangecontrib.AAIT.llm.answers_llama import write_tokens_to_file, prompt_to_messages, conv_to_messages
9
16
 
10
17
 
11
18
  def build_headers(header_name, auth_scheme, api_key):
@@ -15,37 +22,25 @@ def build_headers(header_name, auth_scheme, api_key):
15
22
  return {"Content-Type": "application/json", header_name: value}
16
23
 
17
24
 
18
- def image_to_data_uri(file_path):
19
- """Encode a local image file as a base64 data URI."""
20
- mime_type, _ = mimetypes.guess_type(file_path)
21
- mime_type = mime_type or "application/octet-stream"
22
- with open(file_path, "rb") as f:
23
- encoded = base64.b64encode(f.read()).decode("utf-8")
24
- return f"data:{mime_type};base64,{encoded}"
25
-
26
-
27
- def build_user_content(prompt, image_paths):
28
- """Build the 'content' field of a user message, attaching images when provided."""
29
- if not image_paths:
30
- return str(prompt)
31
-
32
- content = []
33
- for image_path in image_paths:
34
- image_path = image_path.strip().strip("'").strip('"')
35
- if not image_path:
36
- continue
37
- if not os.path.exists(image_path):
38
- content.append({"type": "text", "text": f"[ERROR] Image not found: {image_path}"})
39
- continue
40
- try:
41
- content.append({"type": "image_url", "image_url": {"url": image_to_data_uri(image_path)}})
42
- except Exception as e:
43
- content.append({"type": "text", "text": f"[ERROR] Unable to read image {image_path}: {e}"})
44
- content.append({"type": "text", "text": str(prompt)})
45
- return content
25
+ def search_for_streamed_token(decoded_line):
26
+ r"""
27
+ /!\ This function is meant to be updated whenever a new response format is identified /!\
46
28
 
47
-
48
- def call_chat_completion(prompt, system_prompt, config, image_paths=None):
29
+ Parse a decoded stream response "line" to find the token generated by a LLM.
30
+ """
31
+ known_keys = ["content", "reasoning"]
32
+ dict_regex = r"\{[\s\S]*\}"
33
+ match = re.search(dict_regex, decoded_line)
34
+ if not match:
35
+ return "Token not found"
36
+
37
+ json_data = json.loads(match.group())
38
+ delta = json_data["choices"][0]["delta"]
39
+ token = next((delta[key] for key in known_keys if key in delta), "Token not found")
40
+ return token
41
+
42
+
43
+ def call_chat_completion(messages, config, stream=False, workflow_id="Request_RAG"):
49
44
  """
50
45
  Call a /chat/completions endpoint using the widely adopted chat-completions
51
46
  message format. Vision-capable models can be reached by passing image_paths,
@@ -58,11 +53,6 @@ def call_chat_completion(prompt, system_prompt, config, image_paths=None):
58
53
  url = config["base_url"].rstrip("/") + config["route"]
59
54
  headers = build_headers(config["header_name"], config["auth_scheme"], config["api_key"])
60
55
 
61
- messages = []
62
- if system_prompt:
63
- messages.append({"role": "system", "content": str(system_prompt)})
64
- messages.append({"role": "user", "content": build_user_content(prompt, image_paths)})
65
-
66
56
  payload = {
67
57
  "model": config["model"],
68
58
  "messages": messages,
@@ -86,10 +76,25 @@ def call_chat_completion(prompt, system_prompt, config, image_paths=None):
86
76
  proxy_url = config.get("proxy_url") or ""
87
77
  kwargs["proxies"] = {"http": proxy_url, "https": proxy_url}
88
78
 
89
- response = requests.post(url, headers=headers, json=payload, **kwargs)
79
+ response = requests.post(url, headers=headers, json=payload, stream=stream, **kwargs)
90
80
  response.raise_for_status()
91
- data = response.json()
92
- return data["choices"][0]["message"]["content"]
81
+
82
+ if not stream:
83
+ data = response.json()
84
+ return data["choices"][0]["message"]["content"]
85
+ else:
86
+ answer = ""
87
+ for line in response.iter_lines():
88
+ if line:
89
+ decoded_line = line.decode("utf-8")
90
+ try:
91
+ token = search_for_streamed_token(decoded_line)
92
+ except Exception as e:
93
+ token = "Error"
94
+ print("An error occured when trying to get token from stream response:", e)
95
+ answer += token
96
+ write_tokens_to_file(token, workflow_id=workflow_id)
97
+ return answer
93
98
 
94
99
 
95
100
  def generate_answers(data, config, progress_callback=None):
@@ -116,8 +121,9 @@ def generate_answers(data, config, progress_callback=None):
116
121
  prompt = prompts[i]
117
122
  system_prompt = system_prompts[i] if system_prompts is not None else None
118
123
  image_paths = image_paths_column[i].split(";") if image_paths_column is not None else None
124
+ messages = prompt_to_messages(prompt, system_prompt, image_paths)
119
125
  try:
120
- answer = call_chat_completion(prompt, system_prompt, config, image_paths)
126
+ answer = call_chat_completion(messages, config, stream=True)
121
127
  except requests.HTTPError as e:
122
128
  body = e.response.text if e.response is not None else str(e)
123
129
  answer = f"[ERROR] HTTP {e.response.status_code if e.response is not None else '?'}: {body}"
@@ -130,3 +136,38 @@ def generate_answers(data, config, progress_callback=None):
130
136
 
131
137
  answer_var = StringVariable("Answer")
132
138
  return data.add_column(answer_var, answers)
139
+
140
+
141
+
142
+ def continue_conversation(table, config, progress_callback=None):
143
+ """
144
+ Runs call_chat_completion considering the table as a conversation (list of messages).
145
+ """
146
+ data = copy.deepcopy(table)
147
+ messages = conv_to_messages(data)
148
+ if not messages:
149
+ return data
150
+
151
+ try:
152
+ answer = call_chat_completion(messages, config, stream=True)
153
+ except requests.HTTPError as e:
154
+ body = e.response.text if e.response is not None else str(e)
155
+ answer = f"[ERROR] HTTP {e.response.status_code if e.response is not None else '?'}: {body}"
156
+ except Exception as e:
157
+ answer = f"[ERROR] {e}"
158
+
159
+ # Create output table
160
+ meta = data.metas[-1].copy()
161
+ meta_names = [m.name for m in data.domain.metas]
162
+ meta[meta_names.index("role")] = "assistant"
163
+ meta[meta_names.index("type")] = "text"
164
+ meta[meta_names.index("content")] = answer
165
+ empty_x = np.zeros((1, len(data.domain.attributes)))
166
+
167
+ new_row = Table.from_numpy(
168
+ data.domain,
169
+ X=empty_x,
170
+ metas=np.array([meta], dtype=object)
171
+ )
172
+ out_data = Table.concatenate([data, new_row])
173
+ return out_data
@@ -13,11 +13,13 @@ from AnyQt.QtWidgets import (
13
13
  if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
14
14
  from Orange.widgets.orangecontrib.IO4IT.utils import custom_api
15
15
  from Orange.widgets.orangecontrib.AAIT.utils import thread_management
16
+ from Orange.widgets.orangecontrib.AAIT.llm.answers_llama import identify_table_type
16
17
  from Orange.widgets.orangecontrib.AAIT.utils.import_uic import uic
17
18
  from Orange.widgets.orangecontrib.AAIT.utils.initialize_from_ini import apply_modification_from_python_file
18
19
  else:
19
20
  from orangecontrib.IO4IT.utils import custom_api
20
21
  from orangecontrib.AAIT.utils import thread_management
22
+ from orangecontrib.AAIT.llm.answers_llama import identify_table_type
21
23
  from orangecontrib.AAIT.utils.import_uic import uic
22
24
  from orangecontrib.AAIT.utils.initialize_from_ini import apply_modification_from_python_file
23
25
 
@@ -192,8 +194,14 @@ class OWCustomLLMApi(widget.OWWidget):
192
194
  if self.data is None:
193
195
  self.Outputs.data.send(None)
194
196
  return
195
- if "prompt" not in self.data.domain:
196
- self.error("Input table needs a 'prompt' column.")
197
+
198
+ table_type = identify_table_type(self.data)
199
+ if table_type == "error":
200
+ self.error("The table must contain either (StringVariable):\n- role + type + content\nor:\n- prompt (image paths)")
201
+ self.Outputs.data.send(None)
202
+ return
203
+ elif table_type == "multiple":
204
+ self.error("Cannot have role/type/content and prompt simultaneously.")
197
205
  self.Outputs.data.send(None)
198
206
  return
199
207
 
@@ -213,7 +221,11 @@ class OWCustomLLMApi(widget.OWWidget):
213
221
  self.progressBarInit()
214
222
  self.textBrowser.setText("")
215
223
 
216
- self.thread = thread_management.Thread(custom_api.generate_answers, self.data, self.build_config())
224
+
225
+ if table_type == "batch":
226
+ self.thread = thread_management.Thread(custom_api.generate_answers, self.data, self.build_config())
227
+ elif table_type == "conversation":
228
+ self.thread = thread_management.Thread(custom_api.continue_conversation, self.data, self.build_config())
217
229
  self.thread.progress.connect(self.handle_progress)
218
230
  self.thread.result.connect(self.handle_result)
219
231
  self.thread.finish.connect(self.handle_finish)
@@ -265,8 +265,8 @@ class OWExportMarkdown(widget.OWWidget):
265
265
  )
266
266
  self.ajouter_entete_pied_pptx(
267
267
  pptx_out,
268
- "Orange AI – Présentation",
269
- "Page générée automatiquement"
268
+ "TigerODM – Présentation",
269
+ "Page générée automatiquement par IA"
270
270
  )
271
271
  produced_pptx = pptx_out if os.path.isfile(pptx_out) else ""
272
272
 
@@ -8,10 +8,10 @@ from Orange.widgets.settings import Setting
8
8
  from AnyQt.QtWidgets import QCheckBox
9
9
  from Orange.data import Table, Domain, StringVariable
10
10
  if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
11
- from Orange.widgets.orangecontrib.HLIT_dev.remote_server_smb import convert
11
+ from Orange.widgets.orangecontrib.HLIT.remote_server_smb import convert
12
12
  from Orange.widgets.orangecontrib.AAIT.utils import base_widget
13
13
  else:
14
- from orangecontrib.HLIT_dev.remote_server_smb import convert
14
+ from orangecontrib.HLIT.remote_server_smb import convert
15
15
  from orangecontrib.AAIT.utils import base_widget
16
16
 
17
17
  class OWJsonToDataTable(base_widget.BaseListWidget):