io4it 0.0.0.12.10__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.
- io4it-0.0.0.12.10-py3.11-nspkg.pth +1 -0
- io4it-0.0.0.12.10.dist-info/License.txt +6 -0
- io4it-0.0.0.12.10.dist-info/METADATA +23 -0
- io4it-0.0.0.12.10.dist-info/RECORD +30 -0
- io4it-0.0.0.12.10.dist-info/WHEEL +5 -0
- io4it-0.0.0.12.10.dist-info/entry_points.txt +2 -0
- io4it-0.0.0.12.10.dist-info/namespace_packages.txt +1 -0
- io4it-0.0.0.12.10.dist-info/top_level.txt +1 -0
- orangecontrib/IO4IT/__init__.py +0 -0
- orangecontrib/IO4IT/ocr_function/__init__.py +0 -0
- orangecontrib/IO4IT/ocr_function/word_converter.py +327 -0
- orangecontrib/IO4IT/widgets/OWMarkdownizer.py +202 -0
- orangecontrib/IO4IT/widgets/OWPathPropagator.py +123 -0
- orangecontrib/IO4IT/widgets/OWS3Uploader.py +92 -0
- orangecontrib/IO4IT/widgets/OWS3downloader.py +94 -0
- orangecontrib/IO4IT/widgets/OWS3list.py +107 -0
- orangecontrib/IO4IT/widgets/OWSpeechToText.py +362 -0
- orangecontrib/IO4IT/widgets/OWwordpdf2docx.py +129 -0
- orangecontrib/IO4IT/widgets/__init__.py +19 -0
- orangecontrib/IO4IT/widgets/designer/ow_in_or_out_path.ui +85 -0
- orangecontrib/IO4IT/widgets/designer/owspeechtotext.ui +104 -0
- orangecontrib/IO4IT/widgets/designer/wordpdf2docx.ui +57 -0
- orangecontrib/IO4IT/widgets/icons/category.svg +50 -0
- orangecontrib/IO4IT/widgets/icons/download.png +0 -0
- orangecontrib/IO4IT/widgets/icons/in_or_out.png +0 -0
- orangecontrib/IO4IT/widgets/icons/list_aws.png +0 -0
- orangecontrib/IO4IT/widgets/icons/md.png +0 -0
- orangecontrib/IO4IT/widgets/icons/speech_to_text.png +0 -0
- orangecontrib/IO4IT/widgets/icons/upload.png +0 -0
- orangecontrib/IO4IT/widgets/icons/wordpdf2docx.png +0 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import ntpath
|
|
3
|
+
import os
|
|
4
|
+
import wave
|
|
5
|
+
import tempfile
|
|
6
|
+
import shutil
|
|
7
|
+
import os
|
|
8
|
+
from PyQt5.QtCore import QThread, pyqtSignal
|
|
9
|
+
from PyQt5.QtWidgets import QApplication, QTextEdit, QPushButton, QSpinBox
|
|
10
|
+
from pyannote.audio import Audio
|
|
11
|
+
from pyannote.core import Segment
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import torch
|
|
15
|
+
import whisper
|
|
16
|
+
from Orange.data import Table, Domain, StringVariable
|
|
17
|
+
from Orange.widgets import widget
|
|
18
|
+
from Orange.widgets.utils.signals import Output
|
|
19
|
+
from sklearn.cluster import KMeans
|
|
20
|
+
from speechbrain.inference.speaker import EncoderClassifier
|
|
21
|
+
|
|
22
|
+
if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
|
|
23
|
+
from orangecontrib.AAIT.utils.import_uic import uic
|
|
24
|
+
from orangecontrib.AAIT.utils import SimpleDialogQt
|
|
25
|
+
from orangecontrib.AAIT.utils.MetManagement import get_local_store_path, GetFromRemote
|
|
26
|
+
else:
|
|
27
|
+
from orangecontrib.AAIT.utils.import_uic import uic
|
|
28
|
+
from orangecontrib.AAIT.utils import SimpleDialogQt
|
|
29
|
+
from orangecontrib.AAIT.utils.MetManagement import get_local_store_path, GetFromRemote
|
|
30
|
+
|
|
31
|
+
import subprocess
|
|
32
|
+
|
|
33
|
+
def convert_audio_to_pcm(file_path, ffmpeg_path):
|
|
34
|
+
try:
|
|
35
|
+
import subprocess
|
|
36
|
+
|
|
37
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
38
|
+
if ext not in [".mp3", ".wav"]:
|
|
39
|
+
print(f"[ERREUR] Type de fichier non supporté : {ext}")
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
pcm_wav_path = file_path.replace(ext, "_pcm.wav") # pas de nom compliqué ici
|
|
43
|
+
|
|
44
|
+
ffmpeg_cmd = [
|
|
45
|
+
ffmpeg_path, "-y",
|
|
46
|
+
"-i", file_path,
|
|
47
|
+
"-acodec", "pcm_s16le",
|
|
48
|
+
"-ar", "16000",
|
|
49
|
+
"-ac", "1",
|
|
50
|
+
pcm_wav_path
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
print(f"[INFO] Lancement de ffmpeg : {' '.join(ffmpeg_cmd)}")
|
|
54
|
+
result = subprocess.run(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
55
|
+
|
|
56
|
+
if result.returncode != 0:
|
|
57
|
+
print(f"[ERREUR] ffmpeg a échoué avec le code {result.returncode}")
|
|
58
|
+
print("[STDERR]", result.stderr)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
if os.path.exists(pcm_wav_path):
|
|
62
|
+
print(f"[INFO] Fichier converti : {pcm_wav_path}")
|
|
63
|
+
return pcm_wav_path
|
|
64
|
+
else:
|
|
65
|
+
print("[ERREUR] Le fichier converti n’a pas été trouvé.")
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
except Exception as e:
|
|
69
|
+
print("❌ Exception pendant la conversion audio :", e)
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_wav_duration(wav_path):
|
|
74
|
+
try:
|
|
75
|
+
with wave.open(wav_path, "rb") as wav_file:
|
|
76
|
+
frames = wav_file.getnframes()
|
|
77
|
+
rate = wav_file.getframerate()
|
|
78
|
+
return frames / float(rate)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
# and print the error message
|
|
81
|
+
print("An error occurred when getting the wav duration : ", e)
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TranscriptionThread(QThread):
|
|
86
|
+
result_signal = pyqtSignal(str, list, float)
|
|
87
|
+
|
|
88
|
+
def __init__(self, file_path, model, embedding_model, audio_helper, num_speakers=2):
|
|
89
|
+
super().__init__()
|
|
90
|
+
self.file_path = file_path
|
|
91
|
+
self.model = model
|
|
92
|
+
self.embedding_model = embedding_model
|
|
93
|
+
self.audio_helper = audio_helper
|
|
94
|
+
self.num_speakers = num_speakers
|
|
95
|
+
|
|
96
|
+
def run(self):
|
|
97
|
+
try:
|
|
98
|
+
print("[THREAD] Démarrage du thread de transcription")
|
|
99
|
+
print(f"[THREAD] Fichier à traiter : {self.file_path}")
|
|
100
|
+
|
|
101
|
+
if not os.path.exists(self.file_path):
|
|
102
|
+
print(f"[ERREUR] Le fichier n'existe pas : {self.file_path}")
|
|
103
|
+
self.result_signal.emit("Erreur : fichier introuvable.", [], 0.0)
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
file_duration = get_wav_duration(self.file_path)
|
|
107
|
+
print(f"[INFO] Durée du fichier : {file_duration:.2f} sec" if file_duration else "[ERREUR] Durée inconnue")
|
|
108
|
+
|
|
109
|
+
if file_duration is None:
|
|
110
|
+
self.result_signal.emit("Error: File duration unknown.", [], 0.0)
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
print("[INFO] Début de la transcription avec Whisper")
|
|
114
|
+
start_time = datetime.datetime.now()
|
|
115
|
+
|
|
116
|
+
result = self.model.transcribe(
|
|
117
|
+
self.file_path,
|
|
118
|
+
language="fr",
|
|
119
|
+
without_timestamps=False,
|
|
120
|
+
temperature=0
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
transcription_time = (datetime.datetime.now() - start_time).total_seconds() / 60
|
|
124
|
+
print(f"[INFO] Transcription terminée en {transcription_time:.2f} min")
|
|
125
|
+
|
|
126
|
+
if not result or "segments" not in result or not result["segments"]:
|
|
127
|
+
print("[ERREUR] Aucun segment détecté")
|
|
128
|
+
self.result_signal.emit("Error: No speech detected.", [], transcription_time)
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
segments = result["segments"]
|
|
132
|
+
print(f"[INFO] Nombre de segments détectés : {len(segments)}")
|
|
133
|
+
|
|
134
|
+
embeddings = None
|
|
135
|
+
|
|
136
|
+
for i, segment in enumerate(segments):
|
|
137
|
+
start, end = segment["start"], segment["end"]
|
|
138
|
+
|
|
139
|
+
if end > file_duration:
|
|
140
|
+
print(f"[AVERTISSEMENT] Segment {i} ignoré (fin hors durée)")
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
waveform, _ = self.audio_helper.crop(self.file_path, Segment(start, end))
|
|
145
|
+
if waveform.ndim == 1:
|
|
146
|
+
waveform = waveform.unsqueeze(0)
|
|
147
|
+
with torch.no_grad():
|
|
148
|
+
embedding = self.embedding_model.encode_batch(waveform).squeeze().cpu().numpy()
|
|
149
|
+
if embeddings is None:
|
|
150
|
+
embeddings = np.zeros((len(segments), embedding.shape[0]))
|
|
151
|
+
embeddings[i] = embedding
|
|
152
|
+
except Exception as crop_err:
|
|
153
|
+
print(f"[ERREUR] Erreur lors du crop ou de l'embedding pour le segment {i} : {crop_err}")
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
print("[INFO] Clustering des embeddings avec KMeans")
|
|
157
|
+
clustering = KMeans(n_clusters=min(self.num_speakers, len(segments)), random_state=42).fit(embeddings)
|
|
158
|
+
labels = clustering.labels_
|
|
159
|
+
|
|
160
|
+
speaker_map = {}
|
|
161
|
+
merged_segments = []
|
|
162
|
+
current_speaker = None
|
|
163
|
+
current_text = ""
|
|
164
|
+
current_start = None
|
|
165
|
+
table_output = []
|
|
166
|
+
|
|
167
|
+
print("[INFO] Regroupement par locuteur")
|
|
168
|
+
for i, segment in enumerate(segments):
|
|
169
|
+
speaker_id = labels[i]
|
|
170
|
+
if speaker_id not in speaker_map:
|
|
171
|
+
speaker_map[speaker_id] = f"SPEAKER {len(speaker_map) + 1}"
|
|
172
|
+
speaker = speaker_map[speaker_id]
|
|
173
|
+
|
|
174
|
+
if current_speaker == speaker:
|
|
175
|
+
current_text += f" {segment['text']}"
|
|
176
|
+
else:
|
|
177
|
+
if current_speaker is not None:
|
|
178
|
+
timestamp = str(datetime.timedelta(seconds=round(current_start)))
|
|
179
|
+
merged_segments.append(f"{current_speaker} {timestamp}: {current_text}")
|
|
180
|
+
table_output.append([current_speaker, timestamp, current_text])
|
|
181
|
+
current_speaker = speaker
|
|
182
|
+
current_text = segment["text"]
|
|
183
|
+
current_start = segment["start"]
|
|
184
|
+
|
|
185
|
+
print("[INFO] Finalisation des résultats")
|
|
186
|
+
speaker_text_output = "\n".join(merged_segments)
|
|
187
|
+
self.result_signal.emit(speaker_text_output, table_output, transcription_time)
|
|
188
|
+
print("[THREAD] Transcription terminée et signal émis")
|
|
189
|
+
|
|
190
|
+
except Exception as e:
|
|
191
|
+
print("❌ An error occurred during transcription:", e)
|
|
192
|
+
import traceback
|
|
193
|
+
traceback.print_exc()
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class OWSpeech_To_Text(widget.OWWidget):
|
|
198
|
+
name = "Speech To Text"
|
|
199
|
+
description = "Convert audio to text with speaker recognition"
|
|
200
|
+
priority = 1111
|
|
201
|
+
category = "Advanced Artificial Intelligence Tools"
|
|
202
|
+
icon = "icons/speech_to_text.png"
|
|
203
|
+
if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
|
|
204
|
+
icon = "icons_dev/speech_to_text.png"
|
|
205
|
+
gui = os.path.join(os.path.dirname(os.path.abspath(__file__)), "designer/owspeechtotext.ui")
|
|
206
|
+
|
|
207
|
+
class Outputs:
|
|
208
|
+
data = Output("Data", Table)
|
|
209
|
+
global_transcription = Output("Global Transcription", Table)
|
|
210
|
+
|
|
211
|
+
def __init__(self):
|
|
212
|
+
super().__init__()
|
|
213
|
+
self.file_path = ""
|
|
214
|
+
self.num_speakers = 4 # spin box a defaut 4
|
|
215
|
+
|
|
216
|
+
self.local_store_path = get_local_store_path()
|
|
217
|
+
model_name = "large-v3-turbo.pt"
|
|
218
|
+
self.embedding_model_name = "spkrec-ecapa-voxceleb"
|
|
219
|
+
self.model_path = os.path.join(self.local_store_path, "Models", "S2T", model_name)
|
|
220
|
+
self.embedding_model_path = os.path.join(self.local_store_path, "Models", "S2T", self.embedding_model_name)
|
|
221
|
+
|
|
222
|
+
self.ffmpeg_path = os.path.join(self.local_store_path, "Models", "S2T", "ffmpeg", "bin", "ffmpeg.exe")
|
|
223
|
+
# Extraire le dossier de ffmpeg.exe
|
|
224
|
+
ffmpeg_bin_dir = os.path.dirname(self.ffmpeg_path)
|
|
225
|
+
|
|
226
|
+
# Ajouter ffmpeg au PATH pour whisper/ffmpeg-python
|
|
227
|
+
if ffmpeg_bin_dir not in os.environ["PATH"]:
|
|
228
|
+
os.environ["PATH"] = ffmpeg_bin_dir + os.pathsep + os.environ["PATH"]
|
|
229
|
+
print(f"[INFO] Dossier ffmpeg ajouté au PATH Python : {ffmpeg_bin_dir}")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if not os.path.exists(self.model_path):
|
|
233
|
+
if not SimpleDialogQt.BoxYesNo("Whisper turbo Transcription Model isn't in your computer. Do you want to download it from AAIT store?"):
|
|
234
|
+
return
|
|
235
|
+
try:
|
|
236
|
+
if 0 != GetFromRemote("Whisper turbo"):
|
|
237
|
+
return
|
|
238
|
+
except Exception as e:
|
|
239
|
+
print(e)
|
|
240
|
+
SimpleDialogQt.BoxError("Unable to get the Whisper turbo.")
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
if not os.path.exists(self.embedding_model_path):
|
|
244
|
+
if not SimpleDialogQt.BoxYesNo("Voxceleb Embedding Model isn't in your computer. Do you want to download it from AAIT store?"):
|
|
245
|
+
return
|
|
246
|
+
try:
|
|
247
|
+
if 0 != GetFromRemote("Voxceleb"):
|
|
248
|
+
return
|
|
249
|
+
except Exception as e:
|
|
250
|
+
print(e)
|
|
251
|
+
SimpleDialogQt.BoxError("Unable to get the Voxceleb.")
|
|
252
|
+
return
|
|
253
|
+
if not os.path.exists(self.ffmpeg_path):
|
|
254
|
+
if not SimpleDialogQt.BoxYesNo("FFMPEG isn't in your computer. Do you want to download it from AAIT store?"):
|
|
255
|
+
return
|
|
256
|
+
try:
|
|
257
|
+
if 0 != GetFromRemote("FFMPEG"):
|
|
258
|
+
return
|
|
259
|
+
except Exception as e:
|
|
260
|
+
print(e)
|
|
261
|
+
SimpleDialogQt.BoxError("Unable to get the ffmpeg.")
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
self.model = whisper.load_model(self.model_path)
|
|
265
|
+
print("Version of PyTorch :", torch.__version__)
|
|
266
|
+
print("Used cuda version :", torch.version.cuda)
|
|
267
|
+
print("CUDA available ? :", torch.cuda.is_available())
|
|
268
|
+
if torch.cuda.is_available():
|
|
269
|
+
print("Number of GPU :", torch.cuda.device_count())
|
|
270
|
+
print("GPU's name :", torch.cuda.get_device_name(0))
|
|
271
|
+
# Définition d'un model d'embedding (hard codé pour l'instant)
|
|
272
|
+
|
|
273
|
+
self.embedding_model = EncoderClassifier.from_hparams(source=self.embedding_model_path)
|
|
274
|
+
self.audio_helper = Audio()
|
|
275
|
+
uic.loadUi(self.gui, self)
|
|
276
|
+
|
|
277
|
+
self.file_button = self.findChild(QPushButton,
|
|
278
|
+
'fileButton')
|
|
279
|
+
self.file_button.clicked.connect(self.select_file)
|
|
280
|
+
self.process_button = self.findChild(QPushButton,
|
|
281
|
+
'processButton')
|
|
282
|
+
self.process_button.clicked.connect(self.process_recording)
|
|
283
|
+
self.text_area = self.findChild(QTextEdit, 'textArea')
|
|
284
|
+
|
|
285
|
+
self.spinBox_nb_people=self.findChild(QSpinBox,'spinBox_nb_people')
|
|
286
|
+
self.spinBox_nb_people.setValue(int(self.num_speakers))
|
|
287
|
+
self.spinBox_nb_people.valueChanged.connect(self.spinbox_value_changed)
|
|
288
|
+
self.process_button.setEnabled(False)
|
|
289
|
+
|
|
290
|
+
def spinbox_value_changed(self, value):
|
|
291
|
+
self.num_speakers = value
|
|
292
|
+
|
|
293
|
+
def select_file(self):
|
|
294
|
+
file_path = SimpleDialogQt.BoxSelectExistingFile(self, extention="Audio files (*.wav *.mp3)")
|
|
295
|
+
|
|
296
|
+
if file_path:
|
|
297
|
+
# 🔃 Copie dans un chemin sans accents ni caractères spéciaux
|
|
298
|
+
temp_dir = tempfile.gettempdir()
|
|
299
|
+
base_ext = os.path.splitext(file_path)[1]
|
|
300
|
+
clean_copy = os.path.join(temp_dir, "input_audio" + base_ext)
|
|
301
|
+
shutil.copy(file_path, clean_copy)
|
|
302
|
+
print(f"[INFO] Copie vers fichier temporaire sans accents : {clean_copy}")
|
|
303
|
+
|
|
304
|
+
# 🔁 Conversion dans ce dossier temporaire
|
|
305
|
+
pcm_path = convert_audio_to_pcm(clean_copy, self.ffmpeg_path)
|
|
306
|
+
print(f"[DEBUG] pcm_path: {pcm_path}")
|
|
307
|
+
|
|
308
|
+
if pcm_path:
|
|
309
|
+
self.file_path = pcm_path
|
|
310
|
+
self.temp_pcm_path = pcm_path
|
|
311
|
+
self.process_button.setEnabled(True)
|
|
312
|
+
else:
|
|
313
|
+
SimpleDialogQt.BoxError("Erreur : La conversion audio a échoué.")
|
|
314
|
+
else:
|
|
315
|
+
print("[ERREUR] Aucun fichier sélectionné.")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def process_recording(self):
|
|
319
|
+
self.process_button.setEnabled(False)
|
|
320
|
+
if not self.file_path:
|
|
321
|
+
SimpleDialogQt.BoxError(
|
|
322
|
+
"Aucun fichier sélectionné. Veuillez choisir un fichier audio avant de lancer la transcription.")
|
|
323
|
+
return
|
|
324
|
+
self.num_speakers = self.spinBox_nb_people.value()
|
|
325
|
+
self.text_area.setText("Transcription in progress...")
|
|
326
|
+
self.thread = TranscriptionThread(
|
|
327
|
+
self.file_path, self.model, self.embedding_model, self.audio_helper, self.num_speakers
|
|
328
|
+
)
|
|
329
|
+
self.thread.result_signal.connect(self.display_text)
|
|
330
|
+
self.thread.start()
|
|
331
|
+
self.progressBarInit() # Ajout de la barre de progression
|
|
332
|
+
|
|
333
|
+
def display_text(self, text, table_output, transcription_time):
|
|
334
|
+
self.text_area.setText(f"{text}\n\n⏳ Temps de transcription: {transcription_time:.2f} minutes")
|
|
335
|
+
|
|
336
|
+
# Sortie 1 : tableau par speaker
|
|
337
|
+
domain = Domain([],
|
|
338
|
+
metas=[StringVariable("Speaker"), StringVariable("Timestamp"), StringVariable("Transcription")])
|
|
339
|
+
metas = [[row[0], row[1], row[2]] for row in table_output] if table_output else [["", "", ""]]
|
|
340
|
+
out_data = Table(domain, [[] for _ in metas])
|
|
341
|
+
for i, meta in enumerate(metas):
|
|
342
|
+
out_data.metas[i] = meta
|
|
343
|
+
self.Outputs.data.send(out_data)
|
|
344
|
+
|
|
345
|
+
# Sortie 2 : une seule ligne avec toutes les infos
|
|
346
|
+
global_domain = Domain([],
|
|
347
|
+
metas=[StringVariable("Nom du fichier"),
|
|
348
|
+
StringVariable("Transcription"),
|
|
349
|
+
StringVariable("Temps de transcription (min)")])
|
|
350
|
+
filename = ntpath.basename(self.file_path) # Pour ne garder que le nom du fichier
|
|
351
|
+
global_metas = [[filename, text, f"{transcription_time:.2f}"]]
|
|
352
|
+
global_table = Table(global_domain, [[]])
|
|
353
|
+
global_table.metas[0] = global_metas[0]
|
|
354
|
+
self.Outputs.global_transcription.send(global_table)
|
|
355
|
+
self.progressBarFinished()
|
|
356
|
+
|
|
357
|
+
if __name__ == "__main__":
|
|
358
|
+
import sys
|
|
359
|
+
app = QApplication(sys.argv)
|
|
360
|
+
window = OWSpeech_To_Text()
|
|
361
|
+
window.show()
|
|
362
|
+
sys.exit(app.exec_())
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import Orange.data
|
|
5
|
+
from AnyQt.QtWidgets import QApplication,QCheckBox
|
|
6
|
+
|
|
7
|
+
from Orange.widgets import widget
|
|
8
|
+
from Orange.widgets.utils.signals import Input, Output
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
|
|
12
|
+
from Orange.widgets.orangecontrib.AAIT.utils import thread_management
|
|
13
|
+
from Orange.widgets.orangecontrib.AAIT.utils.import_uic import uic
|
|
14
|
+
from Orange.widgets.orangecontrib.IO4IT.ocr_function import word_converter
|
|
15
|
+
else:
|
|
16
|
+
from orangecontrib.AAIT.utils import thread_management
|
|
17
|
+
from orangecontrib.AAIT.utils.import_uic import uic
|
|
18
|
+
from orangecontrib.IO4IT.ocr_function import word_converter
|
|
19
|
+
|
|
20
|
+
class OWwordpdf2docx(widget.OWWidget):
|
|
21
|
+
name = "WordPdf2Docx"
|
|
22
|
+
description = "Convert pdf from a directory to docx using word"
|
|
23
|
+
icon = "icons/wordpdf2docx.png"
|
|
24
|
+
if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
|
|
25
|
+
icon = "icons_dev/wordpdf2docx.png"
|
|
26
|
+
gui = os.path.join(os.path.dirname(os.path.abspath(__file__)), "designer/wordpdf2docx.ui")
|
|
27
|
+
want_control_area = False
|
|
28
|
+
priority = 3000
|
|
29
|
+
|
|
30
|
+
class Inputs:
|
|
31
|
+
data = Input("Data", Orange.data.Table)
|
|
32
|
+
|
|
33
|
+
class Outputs:
|
|
34
|
+
data = Output("Data", Orange.data.Table)
|
|
35
|
+
|
|
36
|
+
@Inputs.data
|
|
37
|
+
def set_data(self, in_data):
|
|
38
|
+
self.data = in_data
|
|
39
|
+
if self.autorun:
|
|
40
|
+
self.run()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
super().__init__()
|
|
45
|
+
# Qt Management
|
|
46
|
+
self.setFixedWidth(470)
|
|
47
|
+
self.setFixedHeight(300)
|
|
48
|
+
uic.loadUi(self.gui, self)
|
|
49
|
+
self.check_box= self.findChild(QCheckBox, 'checkBox')
|
|
50
|
+
# Data Management
|
|
51
|
+
self.data = None
|
|
52
|
+
self.thread = None
|
|
53
|
+
self.autorun = True
|
|
54
|
+
self.result = None
|
|
55
|
+
self.post_initialized()
|
|
56
|
+
|
|
57
|
+
def run(self):
|
|
58
|
+
self.error("")
|
|
59
|
+
# if thread is running quit
|
|
60
|
+
if self.thread is not None:
|
|
61
|
+
self.thread.safe_quit()
|
|
62
|
+
|
|
63
|
+
if self.data is None:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Verification of in_data
|
|
68
|
+
self.error("")
|
|
69
|
+
try:
|
|
70
|
+
self.data.domain["input_dir"]
|
|
71
|
+
except KeyError:
|
|
72
|
+
self.error('You need a "input_dir" column in input data')
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
if type(self.data.domain["input_dir"]).__name__ != 'StringVariable':
|
|
76
|
+
self.error('"input_dir" column needs to be a Text')
|
|
77
|
+
return
|
|
78
|
+
try:
|
|
79
|
+
self.data.domain["output_dir"]
|
|
80
|
+
except KeyError:
|
|
81
|
+
self.error('You need a "output_dir" column in input data')
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
if type(self.data.domain["output_dir"]).__name__ != 'StringVariable':
|
|
85
|
+
self.error('"output_dir" column needs to be a Text')
|
|
86
|
+
return
|
|
87
|
+
input_dir = self.data.get_column("input_dir")
|
|
88
|
+
output_dir = self.data.get_column("output_dir")
|
|
89
|
+
|
|
90
|
+
# Start progress bar
|
|
91
|
+
self.progressBarInit()
|
|
92
|
+
# Connect and start thread : main function, progress, result and finish
|
|
93
|
+
# --> progress is used in the main function to track progress (with a callback)
|
|
94
|
+
# --> result is used to collect the result from main function
|
|
95
|
+
# --> finish is just an empty signal to indicate that the thread is finished
|
|
96
|
+
ignore_existing_docx=False
|
|
97
|
+
if self.check_box.isChecked():
|
|
98
|
+
ignore_existing_docx=True
|
|
99
|
+
self.thread = thread_management.Thread(word_converter.convert_pdf_structure, input_dir, output_dir,ignore_exsting_out_put=ignore_existing_docx)
|
|
100
|
+
self.thread.progress.connect(self.handle_progress)
|
|
101
|
+
self.thread.result.connect(self.handle_result)
|
|
102
|
+
self.thread.finish.connect(self.handle_finish)
|
|
103
|
+
self.thread.start()
|
|
104
|
+
|
|
105
|
+
def handle_progress(self, value: float) -> None:
|
|
106
|
+
self.progressBarSet(value)
|
|
107
|
+
|
|
108
|
+
def handle_result(self, result):
|
|
109
|
+
try:
|
|
110
|
+
self.result = result
|
|
111
|
+
self.error(result)
|
|
112
|
+
self.Outputs.data.send(self.data)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
print("An error occurred when sending out_data:", e)
|
|
115
|
+
self.Outputs.data.send(None)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
def handle_finish(self):
|
|
119
|
+
print("conversion finished")
|
|
120
|
+
self.progressBarFinished()
|
|
121
|
+
|
|
122
|
+
def post_initialized(self):
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
app = QApplication(sys.argv)
|
|
127
|
+
my_widget = OWwordpdf2docx()
|
|
128
|
+
my_widget.show()
|
|
129
|
+
app.exec_()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Widgets from Development workflow category
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
# ID = "orangecontrib.AAIT"
|
|
6
|
+
|
|
7
|
+
NAME = "Advanced Artificial Intelligence Tools"
|
|
8
|
+
|
|
9
|
+
# Category icon show in the menu
|
|
10
|
+
ICON = "icons/category.svg"
|
|
11
|
+
|
|
12
|
+
BACKGROUND = "light-green"
|
|
13
|
+
|
|
14
|
+
DESCRIPTION = ("Advanced Artificial Intelligence Tools is a package meant to develop and enable advanced AI "
|
|
15
|
+
"functionalities in Orange Data Mining.")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# PRIORITY = 6
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<ui version="4.0">
|
|
3
|
+
<class>Form</class>
|
|
4
|
+
<widget class="QWidget" name="Form">
|
|
5
|
+
<property name="geometry">
|
|
6
|
+
<rect>
|
|
7
|
+
<x>0</x>
|
|
8
|
+
<y>0</y>
|
|
9
|
+
<width>727</width>
|
|
10
|
+
<height>151</height>
|
|
11
|
+
</rect>
|
|
12
|
+
</property>
|
|
13
|
+
<property name="windowTitle">
|
|
14
|
+
<string>Input_dir or Output_dir</string>
|
|
15
|
+
</property>
|
|
16
|
+
<widget class="QLabel" name="Description">
|
|
17
|
+
<property name="geometry">
|
|
18
|
+
<rect>
|
|
19
|
+
<x>40</x>
|
|
20
|
+
<y>10</y>
|
|
21
|
+
<width>641</width>
|
|
22
|
+
<height>41</height>
|
|
23
|
+
</rect>
|
|
24
|
+
</property>
|
|
25
|
+
<property name="sizePolicy">
|
|
26
|
+
<sizepolicy hsizetype="Maximum" vsizetype="Expanding">
|
|
27
|
+
<horstretch>0</horstretch>
|
|
28
|
+
<verstretch>0</verstretch>
|
|
29
|
+
</sizepolicy>
|
|
30
|
+
</property>
|
|
31
|
+
<property name="font">
|
|
32
|
+
<font>
|
|
33
|
+
<pointsize>8</pointsize>
|
|
34
|
+
</font>
|
|
35
|
+
</property>
|
|
36
|
+
<property name="text">
|
|
37
|
+
<string>This widget get a folder path to propagate it as a path , either as input or output path</string>
|
|
38
|
+
</property>
|
|
39
|
+
<property name="wordWrap">
|
|
40
|
+
<bool>true</bool>
|
|
41
|
+
</property>
|
|
42
|
+
</widget>
|
|
43
|
+
<widget class="QPushButton" name="fileButton">
|
|
44
|
+
<property name="geometry">
|
|
45
|
+
<rect>
|
|
46
|
+
<x>60</x>
|
|
47
|
+
<y>70</y>
|
|
48
|
+
<width>291</width>
|
|
49
|
+
<height>51</height>
|
|
50
|
+
</rect>
|
|
51
|
+
</property>
|
|
52
|
+
<property name="text">
|
|
53
|
+
<string>📂 Select a fodler</string>
|
|
54
|
+
</property>
|
|
55
|
+
</widget>
|
|
56
|
+
<widget class="QRadioButton" name="input_dir">
|
|
57
|
+
<property name="geometry">
|
|
58
|
+
<rect>
|
|
59
|
+
<x>470</x>
|
|
60
|
+
<y>60</y>
|
|
61
|
+
<width>131</width>
|
|
62
|
+
<height>31</height>
|
|
63
|
+
</rect>
|
|
64
|
+
</property>
|
|
65
|
+
<property name="text">
|
|
66
|
+
<string>input_dir</string>
|
|
67
|
+
</property>
|
|
68
|
+
</widget>
|
|
69
|
+
<widget class="QRadioButton" name="output_dir">
|
|
70
|
+
<property name="geometry">
|
|
71
|
+
<rect>
|
|
72
|
+
<x>470</x>
|
|
73
|
+
<y>100</y>
|
|
74
|
+
<width>131</width>
|
|
75
|
+
<height>31</height>
|
|
76
|
+
</rect>
|
|
77
|
+
</property>
|
|
78
|
+
<property name="text">
|
|
79
|
+
<string>output_dir</string>
|
|
80
|
+
</property>
|
|
81
|
+
</widget>
|
|
82
|
+
</widget>
|
|
83
|
+
<resources/>
|
|
84
|
+
<connections/>
|
|
85
|
+
</ui>
|