IO4IT 2.0.0__tar.gz → 2.1.0__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 (64) hide show
  1. io4it-2.1.0/PKG-INFO +7 -0
  2. io4it-2.1.0/io4it.egg-info/PKG-INFO +7 -0
  3. {io4it-2.0.0 → io4it-2.1.0}/io4it.egg-info/SOURCES.txt +2 -1
  4. io4it-2.1.0/io4it.egg-info/entry_points.txt +7 -0
  5. {io4it-2.0.0 → io4it-2.1.0}/io4it.egg-info/requires.txt +6 -1
  6. io4it-2.1.0/orangecontrib/IO4IT/widgets/OWChatGpt.py +201 -0
  7. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/OWDeep_Search.py +2 -1
  8. io4it-2.1.0/orangecontrib/IO4IT/widgets/OWExportMarkdown.py +216 -0
  9. io4it-2.1.0/orangecontrib/IO4IT/widgets/OWMarkdownizer.py +563 -0
  10. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/OWS3Uploader.py +1 -0
  11. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/OWS3downloader.py +1 -0
  12. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/OWS3list.py +1 -0
  13. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/OWSpeechToText.py +1 -0
  14. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/OWmailSender.py +1 -0
  15. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/OWwordpdf2docx.py +1 -0
  16. io4it-2.1.0/orangecontrib/IO4IT/widgets/designer/owchatgpt.ui +155 -0
  17. io4it-2.1.0/orangecontrib/IO4IT/widgets/designer/owmarkdownizer.ui +101 -0
  18. io4it-2.1.0/setup.py +74 -0
  19. io4it-2.0.0/PKG-INFO +0 -23
  20. io4it-2.0.0/io4it.egg-info/PKG-INFO +0 -23
  21. io4it-2.0.0/io4it.egg-info/entry_points.txt +0 -2
  22. io4it-2.0.0/orangecontrib/IO4IT/widgets/OWChatGpt.py +0 -121
  23. io4it-2.0.0/orangecontrib/IO4IT/widgets/OWExportMarkdown.py +0 -153
  24. io4it-2.0.0/orangecontrib/IO4IT/widgets/OWMarkdownizer.py +0 -234
  25. io4it-2.0.0/setup.py +0 -98
  26. {io4it-2.0.0 → io4it-2.1.0}/io4it.egg-info/dependency_links.txt +0 -0
  27. {io4it-2.0.0 → io4it-2.1.0}/io4it.egg-info/namespace_packages.txt +0 -0
  28. {io4it-2.0.0 → io4it-2.1.0}/io4it.egg-info/top_level.txt +0 -0
  29. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/__init__.py +0 -0
  30. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/ocr_function/__init__.py +0 -0
  31. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/ocr_function/word_converter.py +0 -0
  32. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/utils/__init__.py +0 -0
  33. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/utils/mail.py +0 -0
  34. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/utils/offuscation_basique.py +0 -0
  35. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/OWmailLoader.py +0 -0
  36. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/__init__.py +0 -0
  37. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/designer/__init__.py +0 -0
  38. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/designer/chart.html +0 -0
  39. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/designer/nogui.ui +0 -0
  40. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/designer/ow_file_ext_selector.ui +0 -0
  41. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/designer/owdeepsearch.ui +0 -0
  42. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/designer/owexportmarkdown.ui +0 -0
  43. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/designer/owmailloader.ui +0 -0
  44. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/designer/owmailsender.ui +0 -0
  45. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/designer/owspeechtotext.ui +0 -0
  46. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/designer/owvisualizationer.ui +0 -0
  47. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/designer/wordpdf2docx.ui +0 -0
  48. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/icons/__init__.py +0 -0
  49. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/icons/chatgpt.png +0 -0
  50. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/icons/deepsearch.svg +0 -0
  51. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/icons/download.png +0 -0
  52. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/icons/export_md.png +0 -0
  53. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/icons/file_extensor.png +0 -0
  54. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/icons/list_aws.png +0 -0
  55. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/icons/mail_loader.png +0 -0
  56. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/icons/mail_writer.png +0 -0
  57. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/icons/md.png +0 -0
  58. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/icons/speech_to_text.png +0 -0
  59. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/icons/upload.png +0 -0
  60. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/icons/visualizationer.png +0 -0
  61. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/icons/wordpdf2docx.png +0 -0
  62. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/IO4IT/widgets/icons_dev/__init__.py +0 -0
  63. {io4it-2.0.0 → io4it-2.1.0}/orangecontrib/__init__.py +0 -0
  64. {io4it-2.0.0 → io4it-2.1.0}/setup.cfg +0 -0
io4it-2.1.0/PKG-INFO ADDED
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.1
2
+ Name: io4it
3
+ Version: 2.1.0
4
+ Home-page:
5
+ Author:
6
+ Author-email:
7
+ Keywords: orange3 add-on
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.1
2
+ Name: io4it
3
+ Version: 2.1.0
4
+ Home-page:
5
+ Author:
6
+ Author-email:
7
+ Keywords: orange3 add-on
@@ -1,4 +1,3 @@
1
- setup.cfg
2
1
  setup.py
3
2
  io4it.egg-info/PKG-INFO
4
3
  io4it.egg-info/SOURCES.txt
@@ -30,10 +29,12 @@ orangecontrib/IO4IT/widgets/designer/__init__.py
30
29
  orangecontrib/IO4IT/widgets/designer/chart.html
31
30
  orangecontrib/IO4IT/widgets/designer/nogui.ui
32
31
  orangecontrib/IO4IT/widgets/designer/ow_file_ext_selector.ui
32
+ orangecontrib/IO4IT/widgets/designer/owchatgpt.ui
33
33
  orangecontrib/IO4IT/widgets/designer/owdeepsearch.ui
34
34
  orangecontrib/IO4IT/widgets/designer/owexportmarkdown.ui
35
35
  orangecontrib/IO4IT/widgets/designer/owmailloader.ui
36
36
  orangecontrib/IO4IT/widgets/designer/owmailsender.ui
37
+ orangecontrib/IO4IT/widgets/designer/owmarkdownizer.ui
37
38
  orangecontrib/IO4IT/widgets/designer/owspeechtotext.ui
38
39
  orangecontrib/IO4IT/widgets/designer/owvisualizationer.ui
39
40
  orangecontrib/IO4IT/widgets/designer/wordpdf2docx.ui
@@ -0,0 +1,7 @@
1
+ [orange.widgets]
2
+ AAIT - ALGORITHM = orangecontrib.ALGORITHM.widgets
3
+ AAIT - API = orangecontrib.API.widgets
4
+ AAIT - LLM INTEGRATION = orangecontrib.LLM_INTEGRATION.widgets
5
+ AAIT - MODELS = orangecontrib.LLM_MODELS.widgets
6
+ AAIT - TOOLBOX = orangecontrib.TOOLBOX.widgets
7
+ IO4IT = orangecontrib.IO4IT.widgets
@@ -11,6 +11,11 @@ pyannote.audio
11
11
  pyannote-core
12
12
  pypandoc
13
13
  pypandoc-binary
14
- wave==0.0.2
15
14
  scikit-learn
16
15
  openai
16
+ pip-system-certs==5.0
17
+ docx2pdf
18
+ doc2docx
19
+ msal
20
+ exchangelib
21
+ CATEGORIT
@@ -0,0 +1,201 @@
1
+ import os
2
+ import sys
3
+ import base64
4
+ import ast
5
+ import openai
6
+ import Orange
7
+ from Orange.data import StringVariable
8
+ from Orange.widgets.widget import OWWidget, Input, Output
9
+ from Orange.widgets.settings import Setting
10
+ from AnyQt.QtWidgets import QApplication, QLineEdit, QSpinBox, QDoubleSpinBox, QPushButton
11
+
12
+ if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
13
+ from Orange.widgets.orangecontrib.AAIT.utils import thread_management
14
+ from Orange.widgets.orangecontrib.AAIT.utils.import_uic import uic
15
+ from Orange.widgets.orangecontrib.IO4IT.utils import offuscation_basique
16
+ else:
17
+ from orangecontrib.AAIT.utils.import_uic import uic
18
+ from orangecontrib.AAIT.utils import thread_management
19
+ from orangecontrib.IO4IT.utils import offuscation_basique
20
+
21
+ class ChatGpt(OWWidget):
22
+ name = "CallChatGptApi"
23
+ description = "Call to chatgpt API. You need to provide a prompt and an api_keys. You call also add an image_paths and a system_prompt if you want."
24
+ icon = "icons/chatgpt.png"
25
+ if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
26
+ icon = "icons_dev/chatgpt.png"
27
+ priority = 3000
28
+ max_tokens = Setting(4096)
29
+ temperature = Setting(0)
30
+ model = Setting("gpt-4o")
31
+ gui = os.path.join(os.path.dirname(os.path.abspath(__file__)), "designer/owchatgpt.ui")
32
+ want_control_area = False
33
+ category = "AAIT - LLM INTEGRATION"
34
+
35
+ class Inputs:
36
+ data = Input("Data", Orange.data.Table)
37
+
38
+ @Inputs.data
39
+ def set_data(self, in_data):
40
+ if in_data is None:
41
+ return
42
+ if "prompt" not in in_data.domain:
43
+ self.error("input table need a prompt column")
44
+ return
45
+ self.prompt = in_data.get_column("prompt")[0]
46
+ if "image_paths" in in_data.domain:
47
+ self.image_paths = in_data.get_column("image_paths")[0]
48
+ if "api_keys" not in in_data.domain:
49
+ self.api_keys = offuscation_basique.lire_config_cli_api("CHAT_GPT")
50
+ if offuscation_basique.lire_config_cli_api("CHAT_GPT") is None:
51
+ self.error("input table need a api_keys column or a folder API with you CHAT_GPT keys in aait store")
52
+ if self.api_keys is None:
53
+ self.api_keys = in_data.get_column("api_keys")[0]
54
+ if "system_prompt" in in_data.domain:
55
+ self.system_prompt = in_data.get_column("system_prompt")[0]
56
+ self.data = in_data
57
+ self.run()
58
+
59
+ class Outputs:
60
+ data = Output("Data", Orange.data.Table)
61
+
62
+ def __init__(self):
63
+ super().__init__()
64
+ # Qt Management
65
+ self.setFixedWidth(400)
66
+ self.setFixedHeight(300)
67
+ uic.loadUi(self.gui, self)
68
+ self.edit_model = self.findChild(QLineEdit, 'modelName')
69
+ self.edit_model.setPlaceholderText("Model name")
70
+ self.edit_model.setText(self.model)
71
+ self.edit_model.editingFinished.connect(self.update_parameters)
72
+
73
+ self.edit_max_tokens = self.bind_spinbox("boxMaxTokens", self.max_tokens)
74
+ self.edit_temperature = self.bind_spinbox("boxTemperature", self.temperature, is_double=True)
75
+ self.pushButton_call_api =self.findChild(QPushButton, 'callApi')
76
+ self.pushButton_call_api.clicked.connect(self.run)
77
+
78
+ self.data = None
79
+ self.prompt = None
80
+ self.image_paths = None
81
+ self.api_keys = None
82
+ self.system_prompt = ""
83
+ self.text_response = None
84
+ self.thread = None
85
+ self.run()
86
+
87
+ def bind_spinbox(self, name, value, is_double=False):
88
+ widget_type = QDoubleSpinBox if is_double else QSpinBox
89
+ box = self.findChild(widget_type, name)
90
+ box.setValue(value)
91
+ box.editingFinished.connect(self.update_parameters)
92
+ return box
93
+
94
+ def update_parameters(self):
95
+ self.max_tokens = self.edit_max_tokens.value()
96
+ self.temperature = self.edit_temperature.value()
97
+ self.model = self.edit_model.text()
98
+
99
+ def generate_answers(self):
100
+ try:
101
+ openai.api_key = self.api_keys
102
+ response = openai.chat.completions.create(
103
+ model=self.model,
104
+ messages=[
105
+ {
106
+ "role": "system",
107
+ "content": self.system_prompt
108
+ },
109
+ {
110
+ "role": "user",
111
+ "content": self.prompt
112
+ }
113
+ ],
114
+ max_tokens=self.max_tokens,
115
+ temperature=self.temperature
116
+ )
117
+ self.text_response = response.choices[0].message.content
118
+ except Exception as e:
119
+ print(e)
120
+ self.error(f"Error: {e}")
121
+ return
122
+ if self.text_response is None:
123
+ self.error("No response from chatgpt.")
124
+
125
+
126
+ def run(self):
127
+ self.error("")
128
+ self.warning("")
129
+ if self.data is None:
130
+ return
131
+
132
+ if self.prompt == "" or self.prompt is None:
133
+ self.error("No prompt provided.")
134
+ return
135
+
136
+ if self.api_keys is None:
137
+ self.error("No api keys provided.")
138
+ return
139
+
140
+ # si on relance la génération par le bouton le prompt est déjà rempli (déjà une liste)
141
+ # mais on peut changer le model, la temp ou le nombre max de token
142
+ if isinstance(self.prompt, list):
143
+ self.prompt = [{"type": "text", "text": self.prompt}]
144
+ if self.image_paths is not None and self.image_paths != []:
145
+ if type(self.image_paths) == str:
146
+ self.image_paths = ast.literal_eval(self.image_paths)
147
+ for img_path in self.image_paths:
148
+ filename = os.path.basename(img_path)
149
+
150
+ # Ajoute une mention de l’image avant
151
+ self.prompt.append({
152
+ "type": "text",
153
+ "text": f"Photo : {filename}"
154
+ })
155
+
156
+ with open(img_path, "rb") as f:
157
+ b64_img = base64.b64encode(f.read()).decode("utf-8")
158
+ self.prompt.append({
159
+ "type": "image_url",
160
+ "image_url": {
161
+ "url": f"data:image/jpeg;base64,{b64_img}"
162
+ }
163
+ })
164
+ self.progressBarInit()
165
+ self.thread = thread_management.Thread(self.generate_answers)
166
+ self.thread.progress.connect(self.handle_progress)
167
+ self.thread.result.connect(self.handle_result)
168
+ self.thread.finish.connect(self.handle_finish)
169
+ self.thread.start()
170
+
171
+ def handle_progress(self, progress) -> None:
172
+ value = progress[0]
173
+ text = progress[1]
174
+ if value is not None:
175
+ self.progressBarSet(value)
176
+ if text is None:
177
+ self.textBrowser.setText("")
178
+ else:
179
+ self.textBrowser.insertPlainText(text)
180
+
181
+ def handle_result(self, result):
182
+ new_name_column = StringVariable("answer")
183
+ table = self.data.add_column(new_name_column, [self.text_response])
184
+ self.Outputs.data.send(table)
185
+
186
+ def handle_finish(self):
187
+ print("Generation finished")
188
+ self.progressBarFinished()
189
+
190
+ def post_initialized(self):
191
+ pass
192
+
193
+ if __name__ == "__main__":
194
+ app = QApplication(sys.argv)
195
+ my_widget = ChatGpt()
196
+ my_widget.show()
197
+
198
+ if hasattr(app, "exec"):
199
+ sys.exit(app.exec())
200
+ else:
201
+ sys.exit(app.exec_())
@@ -26,7 +26,8 @@ else:
26
26
  @apply_modification_from_python_file(filepath_original_widget=__file__)
27
27
  class OWDeep_Search(widget.OWWidget):
28
28
  name = "Deep Search"
29
- description = "Generate a response to a column 'prompt' with a LuuLM"
29
+ description = "Generate a response to a column 'prompt' with a LLM"
30
+ category = "AAIT - LLM INTEGRATION"
30
31
  icon = "icons/deepsearch.svg"
31
32
  if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
32
33
  icon = "icons_dev/owqueryllm.svg"
@@ -0,0 +1,216 @@
1
+ import os
2
+ import sys
3
+ import tempfile
4
+ import traceback
5
+
6
+ import Orange
7
+ from Orange.data import StringVariable
8
+ from Orange.widgets import widget
9
+ from Orange.widgets.widget import Input, Output
10
+ from AnyQt.QtWidgets import QMessageBox, QApplication
11
+
12
+ from docx import Document
13
+ from docx.shared import Pt as pt_docx
14
+ from pptx import Presentation
15
+ from pptx.util import Inches, Pt
16
+ import pypandoc
17
+ from docx2pdf import convert
18
+
19
+ # Chargement UI
20
+ if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
21
+ from Orange.widgets.orangecontrib.AAIT.utils.import_uic import uic
22
+ else:
23
+ from orangecontrib.AAIT.utils.import_uic import uic
24
+
25
+
26
+ class OWExportMarkdown(widget.OWWidget):
27
+ name = "OWExportMarkdown"
28
+ description = "Auto-exporte content→(docx,pptx,pdf) au même path (base + extensions)."
29
+ icon = "icons/export_md.png"
30
+ if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
31
+ icon = "icons_dev/export_md.png"
32
+ want_control_area = True
33
+ priority = 9999
34
+ category = "AAIT - TOOLBOX"
35
+
36
+ class Inputs:
37
+ data = Input("Data", Orange.data.Table)
38
+
39
+ class Outputs:
40
+ data = Output("Data", Orange.data.Table)
41
+
42
+ def __init__(self):
43
+ super().__init__()
44
+ self.data = None
45
+ ui_path = os.path.join(os.path.dirname(__file__), "designer", "owexportmarkdown.ui")
46
+ uic.loadUi(ui_path, baseinstance=self)
47
+
48
+ # -------- helpers headers/footers --------
49
+ def ajouter_en_tete_pied_docx(self, file_path, header_text, footer_text):
50
+ try:
51
+ doc = Document(file_path)
52
+ section = doc.sections[0]
53
+ header = section.header
54
+ p = header.paragraphs[0] if header.paragraphs else header.add_paragraph()
55
+ p.text = header_text
56
+ if p.runs:
57
+ p.runs[0].font.size = pt_docx(10)
58
+ footer = section.footer
59
+ p = footer.paragraphs[0] if footer.paragraphs else footer.add_paragraph()
60
+ p.text = footer_text
61
+ if p.runs:
62
+ p.runs[0].font.size = pt_docx(10)
63
+ doc.save(file_path)
64
+ except Exception:
65
+ # en-tête/pied non bloquants
66
+ pass
67
+
68
+ def ajouter_entete_pied_pptx(self, file_path, entete_text, pied_text):
69
+ try:
70
+ prs = Presentation(file_path)
71
+ for slide in prs.slides:
72
+ entete = slide.shapes.add_textbox(Inches(0.3), Inches(0.2), Inches(8), Inches(0.5))
73
+ tf_entete = entete.text_frame
74
+ tf_entete.text = entete_text
75
+ tf_entete.paragraphs[0].font.size = Pt(12)
76
+ tf_entete.paragraphs[0].font.bold = True
77
+
78
+ pied = slide.shapes.add_textbox(Inches(0.3), Inches(6.3), Inches(8), Inches(0.5))
79
+ tf_pied = pied.text_frame
80
+ tf_pied.text = pied_text
81
+ tf_pied.paragraphs[0].font.size = Pt(10)
82
+ prs.save(file_path)
83
+ except Exception:
84
+ pass
85
+
86
+ # -------------- input --------------
87
+ @Inputs.data
88
+ def set_data(self, in_data):
89
+ self.error("")
90
+ if in_data is None:
91
+ self.data = None
92
+ self.Outputs.data.send(None)
93
+ return
94
+
95
+ # On exige au moins 'path'
96
+ if "path" not in in_data.domain:
97
+ self.error("La table d'entrée doit contenir au moins la colonne 'path'.")
98
+ self.Outputs.data.send(None)
99
+ return
100
+
101
+ # Optionnellement 'content'
102
+ self.data = in_data
103
+ try:
104
+ table_out = self.export_all_rows()
105
+ self.Outputs.data.send(table_out)
106
+ except Exception as e:
107
+ tb = traceback.format_exc()
108
+ QMessageBox.critical(self, "Erreur d'export", f"{e}\n\n{tb}")
109
+ self.Outputs.data.send(None)
110
+
111
+ # -------------- core --------------
112
+ def export_all_rows(self):
113
+ base_paths = self.data.get_column("path")
114
+ has_content = "content" in self.data.domain
115
+ file_contents = self.data.get_column("content") if has_content else [None] * len(base_paths)
116
+
117
+ pdf_paths, docx_paths, pptx_paths = [], [], []
118
+
119
+ for i, (md_text, base_path) in enumerate(zip(file_contents, base_paths)):
120
+ base_path = str(base_path or "").strip()
121
+
122
+ # Lecture du contenu si 'content' absent et path en .md
123
+ if not has_content:
124
+ if base_path.lower().endswith(".md"):
125
+ try:
126
+ with open(base_path, "r", encoding="utf-8") as f:
127
+ md_text = f.read()
128
+ # on remplace base_path par le même (on garde la base pour sorties)
129
+ except Exception as e:
130
+ self.error(f"Impossible de lire le fichier : {base_path} ({e})")
131
+ pdf_paths.append("")
132
+ docx_paths.append("")
133
+ pptx_paths.append("")
134
+ continue
135
+ else:
136
+ # pas de content et path non .md -> rien à faire pour cette ligne
137
+ pdf_paths.append("")
138
+ docx_paths.append("")
139
+ pptx_paths.append("")
140
+ continue
141
+
142
+ md_text = (str(md_text or "")).strip()
143
+
144
+ if not md_text or not base_path:
145
+ pdf_paths.append("")
146
+ docx_paths.append("")
147
+ pptx_paths.append("")
148
+ continue
149
+
150
+ # Normaliser la base: enlever extension si présente
151
+ base_no_ext, _ = os.path.splitext(base_path)
152
+ # Créer dossier si nécessaire
153
+ out_dir = os.path.dirname(base_no_ext)
154
+ if out_dir and not os.path.isdir(out_dir):
155
+ os.makedirs(out_dir, exist_ok=True)
156
+
157
+ docx_out = base_no_ext + ".docx"
158
+ pptx_out = base_no_ext + ".pptx"
159
+ pdf_out = base_no_ext + ".pdf"
160
+
161
+ # MD temporaire
162
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False, encoding="utf-8") as tmp:
163
+ tmp.write(md_text)
164
+ tmp_md = tmp.name
165
+
166
+ try:
167
+ # DOCX
168
+ pypandoc.convert_file(tmp_md, to="docx", outputfile=docx_out)
169
+ self.ajouter_en_tete_pied_docx(
170
+ docx_out,
171
+ "Rapport - Orange AI",
172
+ "Page générée automatiquement - Ne pas diffuser"
173
+ )
174
+
175
+ # PPTX
176
+ pypandoc.convert_file(tmp_md, to="pptx", outputfile=pptx_out)
177
+ self.ajouter_entete_pied_pptx(
178
+ pptx_out,
179
+ "Orange AI – Présentation",
180
+ "Page générée automatiquement"
181
+ )
182
+
183
+ # PDF (docx -> pdf) avec chemin de sortie exact
184
+ try:
185
+ convert(docx_out, pdf_out)
186
+ except Exception:
187
+ # fallback pandoc->pdf (si LaTeX dispo)
188
+ try:
189
+ pypandoc.convert_file(tmp_md, to="pdf", outputfile=pdf_out)
190
+ except Exception:
191
+ self.error(f"Échec conversion PDF pour la ligne {i+1}.")
192
+ pdf_out = ""
193
+ finally:
194
+ try:
195
+ os.remove(tmp_md)
196
+ except Exception:
197
+ pass
198
+
199
+ pdf_paths.append(pdf_out if os.path.isfile(pdf_out) else "")
200
+ docx_paths.append(docx_out if os.path.isfile(docx_out) else "")
201
+ pptx_paths.append(pptx_out if os.path.isfile(pptx_out) else "")
202
+
203
+ # Ajouter colonnes sortie
204
+ table = self.data
205
+ table = table.add_column(StringVariable("output_pdf_path"), pdf_paths)
206
+ table = table.add_column(StringVariable("output_docx_path"), docx_paths)
207
+ table = table.add_column(StringVariable("output_pptx_path"), pptx_paths)
208
+
209
+ return table
210
+
211
+
212
+ if __name__ == "__main__":
213
+ app = QApplication(sys.argv)
214
+ w = OWExportMarkdown()
215
+ w.show()
216
+ sys.exit(app.exec() if hasattr(app, "exec") else app.exec_())