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.
- {io4it-4.0.24 → io4it-4.0.26}/PKG-INFO +1 -1
- {io4it-4.0.24 → io4it-4.0.26}/io4it.egg-info/PKG-INFO +1 -1
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/ocr_function/word_converter.py +156 -40
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/toolViews/linkHTLMWorkflow_manager_ui.py +2 -2
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/custom_api.py +84 -43
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWCustomLLMApi.py +15 -3
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWExportMarkdown.py +2 -2
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWJsonToDataTable.py +2 -2
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWOfficeNormalizer.py +61 -20
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWParserHTML.py +2 -2
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWProcessPoolExecutor.py +44 -21
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWWebSearch.py +2 -2
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owofficenormalizer.ui +18 -2
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owprocesspoolexecutor.ui +17 -30
- {io4it-4.0.24 → io4it-4.0.26}/setup.py +1 -1
- {io4it-4.0.24 → io4it-4.0.26}/io4it.egg-info/SOURCES.txt +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/io4it.egg-info/dependency_links.txt +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/io4it.egg-info/entry_points.txt +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/io4it.egg-info/namespace_packages.txt +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/io4it.egg-info/requires.txt +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/io4it.egg-info/top_level.txt +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/__init__.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/ocr_function/__init__.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/toolViews/designer/key_manager_ui.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/toolViews/designer/link_workflow_manager_ui.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/toolViews/key_manager_ui.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/__init__.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/environment_collector.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/file2images.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/keys_manager.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/mail.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/pool_exec_utils.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/pst_extractor/TigerODMPST.dll +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/pst_extractor/__init__.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/pst_extractor/export_pst.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/pst_extractor/libpff.dll +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/pst_extractor/z.dll +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/secret_manager.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/utils/utils_md.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWChatGpt.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWDeep_Search.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWDoclingToMarkdown.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWExtractTablesDocxToXlsx.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWInboxMailMonitoring.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWMD2HTML.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWMarkdownLoader.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWMarkdownizer.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWPPTX2JPG.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWPdfType.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWPipAuditor.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWS3Uploader.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWS3downloader.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWS3list.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWXLSX_insert.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWmailLoader.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWmailSender.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/OWwordpdf2docx.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/__init__.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/__init__.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/nogui.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/ow_extract_tables_docx_to_xlsx.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/ow_file_ext_selector.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/ow_json_to_data_table.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owchatgpt.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owcustomllmapi.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owdeepsearch.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owdoclingtomarkdown.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owexportmarkdown.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owfile2images.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owinboxmailmonitoring.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owinsertdatatoexcel.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owmailloader.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owmailsender.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owmarkdownizer.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owmarkdownloader.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owmd2html.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owparserhtml.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owpdftype.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owpipauditor.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owspeechtotext.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owvisualizationer.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/owwebsearch.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/designer/wordpdf2docx.ui +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/__init__.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/chatgpt.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/check_pdf.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/deepsearch.svg +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/dep_md_old.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/download.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/export_md.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/extract_table.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/file2images.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/file_extensor.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/html.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/json-file.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/list_aws.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/load_md.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/mail_loader.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/mail_writer.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/md.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/monitor-email.svg +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/office_normalizer.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/owcustomllmapi.svg +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/owmd2html.svg +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/pip_auditor.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/process_pool_executor.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/speech_to_text.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/upload.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/visualizationer.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/websearch.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/wordpdf2docx.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons/xlsx_insert.png +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/IO4IT/widgets/icons_dev/__init__.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/orangecontrib/__init__.py +0 -0
- {io4it-4.0.24 → io4it-4.0.26}/setup.cfg +0 -0
|
@@ -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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
8
|
-
|
|
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
|
|
19
|
-
"""
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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(
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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.
|
|
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.
|
|
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):
|