Script python permettant de traduire un long texte
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

main.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. import PyPDF2
  2. import requests
  3. import json
  4. from reportlab.lib.pagesizes import letter
  5. from reportlab.lib.units import inch
  6. from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Flowable
  7. from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
  8. from reportlab.lib.enums import TA_JUSTIFY
  9. from reportlab.pdfbase import pdfmetrics
  10. from reportlab.pdfbase.ttfonts import TTFont
  11. import os
  12. # Configuration
  13. DEBUG = True
  14. PDF_PATH = "TaniaBorecMemoir(Ukr).pdf"
  15. OLLAMA_MODEL = "traductionUkrainienVersFrancais:latest"
  16. OLLAMA_URL = "http://localhost:11434/api/generate"
  17. TARGET_LANGUAGE = "français"
  18. CHECKPOINT_FILE = "checkpoint.json"
  19. TEMP_OUTPUT_TXT = "output_temp.txt"
  20. FINAL_OUTPUT_PDF = PDF_PATH.replace(".pdf",f"({TARGET_LANGUAGE.upper()[:2]})_V7.pdf")
  21. FINAL_OUTPUT_TXT = PDF_PATH.replace(".pdf",f"({TARGET_LANGUAGE.upper()[:2]})_V7.txt")
  22. DEBUG = True
  23. def extract_parameters_from_template(template_str):
  24. """Extrait les paramètres du modèle à partir du template."""
  25. import re
  26. parameters = {}
  27. if not template_str or not isinstance(template_str, str):
  28. return parameters
  29. # Si la chaîne contient "parameters:", récupère ce qui suit
  30. if 'parameters:' in template_str:
  31. params_section = template_str.split('parameters:', 1)[1]
  32. else:
  33. # Sinon, utilise la chaîne directement (elle contient déjà les paramètres)
  34. params_section = template_str
  35. # Parse les lignes de paramètres
  36. # Format: "stop "<end_of_turn>""
  37. # "temperature 0.1"
  38. lines = params_section.split('\n')
  39. for line in lines:
  40. if not line.strip():
  41. continue
  42. # Divise par le premier groupe d'espaces blancs
  43. # Cela sépare la clé des valeurs avec leurs espaces
  44. parts = line.split(None, 1) # split() avec maxsplit=1 divise sur les espaces
  45. if len(parts) == 2:
  46. param_name = parts[0].strip()
  47. param_value = parts[1].strip()
  48. parameters[param_name] = param_value
  49. return parameters
  50. def get_llm_model_info(model=OLLAMA_MODEL):
  51. """
  52. Extrait les informations du modèle LLM depuis Ollama, y compris le nom depuis la ligne FROM du Modelfile.
  53. @param model: Nom du modèle à interroger.
  54. @type model: str
  55. @return: Dictionnaire contenant les informations du modèle, ou None en cas d'erreur.
  56. @rtype: dict | None
  57. """
  58. try:
  59. # Chemin vers le fichier Modelfile (supposé être dans le même répertoire que le script)
  60. modelfile_path = os.path.join(os.path.dirname(__file__), "Modelfile")
  61. # Initialisation de model_name
  62. model_name = "none"
  63. # Lecture du fichier Modelfile pour extraire le nom du modèle
  64. if os.path.exists(modelfile_path):
  65. with open(modelfile_path, 'r', encoding='utf-8') as file:
  66. for line in file:
  67. if line.strip().startswith('FROM '):
  68. model_name = line.strip().split('FROM ')[1].strip()
  69. break
  70. # URL pour obtenir les informations du modèle
  71. info_url = OLLAMA_URL.replace("/api/generate", "/api/show")
  72. payload = {"name": model}
  73. response = requests.post(info_url, json=payload)
  74. if response.status_code == 200:
  75. model_data = response.json()
  76. # Gère le cas où model_data est une chaîne
  77. if isinstance(model_data, str):
  78. model_data = json.loads(model_data)
  79. # Extrait les paramètres du template
  80. parameters = model_data.get('parameters', '')
  81. parsed_params = extract_parameters_from_template(parameters)
  82. # Extraction du nom depuis la ligne FROM
  83. modelfile_content = model_data.get('Modelfile', '')
  84. # Extraction des informations principales
  85. info = {
  86. "temperature": parsed_params.get('temperature', model_data.get("temperature", "Not available")),
  87. "name": model_name,
  88. "num_ctx": parsed_params.get('num_ctx', "Not available"),
  89. "top_k": parsed_params.get('top_k', "Not available"),
  90. "top_p": parsed_params.get('top_p', "Not available"),
  91. "system": model_data.get("system", "Not available"),
  92. "modified_at": model_data.get("modified_at", "Not available"),
  93. }
  94. return info
  95. else:
  96. print(f"Erreur lors de la récupération du modèle : {response.text}")
  97. return None
  98. except Exception as e:
  99. print(f"Erreur lors de l'accès aux informations du modèle : {e}")
  100. return None
  101. def display_llm_info():
  102. """Retourne les informations du modèle LLM formatées."""
  103. from datetime import datetime
  104. info = get_llm_model_info(OLLAMA_MODEL)
  105. if info:
  106. # Formate la date en jj/mm/AAAA
  107. modified_at = info.get('modified_at', 'Not available')
  108. if modified_at and modified_at != 'Not available':
  109. try:
  110. # Parse la date ISO
  111. date_obj = datetime.fromisoformat(modified_at.replace('Z', '+00:00'))
  112. # Formate en jj/mm/AAAA
  113. formatted_date = date_obj.strftime("%d/%m/%Y")
  114. except:
  115. formatted_date = modified_at
  116. else:
  117. formatted_date = 'Not available'
  118. return f"LLM Modèle: {info['name']}<br//>\nDate de modification: {formatted_date}<br//>\nSystem: {info['system']}<br//>\nTemperature: {info['temperature']}"
  119. else:
  120. return "Informations du modèle non disponibles"
  121. def register_unicode_font():
  122. """Enregistre une police TrueType qui supporte le cyrillique."""
  123. # Recherche une police système qui supporte le cyrillique
  124. font_paths = [
  125. r"C:\Windows\Fonts\DejaVuSans.ttf",
  126. r"C:\Windows\Fonts\Calibri.ttf",
  127. r"C:\Windows\Fonts\arial.ttf",
  128. ]
  129. for font_path in font_paths:
  130. if os.path.exists(font_path):
  131. try:
  132. pdfmetrics.registerFont(TTFont('UnicodeFont', font_path))
  133. return 'UnicodeFont'
  134. except Exception as e:
  135. print(f"Erreur lors de l'enregistrement de {font_path}: {e}")
  136. # Si aucune police spéciale trouvée, utilise Helvetica par défaut
  137. print("Aucune police Unicode trouvée, utilisation d'Helvetica")
  138. return 'Helvetica'
  139. # Charge ou initialise le checkpoint
  140. def load_checkpoint():
  141. if os.path.exists(CHECKPOINT_FILE):
  142. with open(CHECKPOINT_FILE, "r") as f:
  143. return json.load(f)
  144. return {"last_processed_index": -1, "results": {}}
  145. # Sauvegarde le checkpoint
  146. # Sauvegarde le checkpoint
  147. def save_checkpoint(last_index, results):
  148. # Trier les clés du dictionnaire results
  149. sorted_results = {key: results[key] for key in sorted(results.keys(), key=int)}
  150. with open(CHECKPOINT_FILE, "w") as f:
  151. # Utiliser un espace d'indentation de 4 espaces pour rendre le JSON plus lisible
  152. json.dump({"last_processed_index": last_index, "results": sorted_results}, f, indent=4)
  153. # Sauvegarde les résultats temporaires dans un fichier TXT
  154. def save_temp_results(results):
  155. with open(TEMP_OUTPUT_TXT, "w", encoding="utf-8") as f:
  156. for idx, translation in results.items():
  157. f.write(f"Paragraphe {idx}:\n{translation}\n\n")
  158. # Extraction du texte du PDF (inchangée)
  159. def extract_text_from_pdf(pdf_path):
  160. text_by_page = []
  161. with open(pdf_path, "rb") as file:
  162. reader = PyPDF2.PdfReader(file)
  163. for page in reader.pages:
  164. text = page.extract_text()
  165. text_by_page.append(text)
  166. return text_by_page
  167. # Découpage en paragraphes (inchangé)
  168. def split_pages_in_paragraphs(pages_text):
  169. import re
  170. full_text = "\n".join(pages_text)
  171. full_text = re.sub(r'(?<![.!?])\n+(?![.!?])', ' ', full_text)
  172. paragraphs = re.split(r'(?<=[.!?])\s*\n+', full_text.strip())
  173. paragraphs = [re.sub(r'\s+', ' ', p).strip() for p in paragraphs if p.strip()]
  174. return paragraphs
  175. # Envoi à Ollama (inchangé)
  176. def send_to_ollama(text, target_lang=TARGET_LANGUAGE, model=OLLAMA_MODEL):
  177. full_prompt = f"\n\nTraduis le texte suivant de l'ukrainien vers le {target_lang} :\n{text}"
  178. payload = {"model": model, "prompt": full_prompt, "stream": False}
  179. response = requests.post(OLLAMA_URL, data=json.dumps(payload))
  180. if response.status_code == 200:
  181. return response.json()["response"]
  182. else:
  183. raise Exception(f"Erreur Ollama: {response.text}")
  184. # Création du PDF final avec numéros de chapitres dans la marge
  185. def create_pdf_from_results(results, output_path):
  186. """Crée un PDF à partir des résultats de traduction, avec des notes dans la marge et un numéro de page."""
  187. story = []
  188. font_name = register_unicode_font()
  189. # Styles personnalisés
  190. styles = getSampleStyleSheet()
  191. title_style = ParagraphStyle(
  192. 'CustomTitle',
  193. parent=styles['Heading1'],
  194. fontSize=16,
  195. textColor='#1f4788',
  196. spaceAfter=0.3*inch,
  197. alignment=TA_JUSTIFY,
  198. fontName=font_name
  199. )
  200. page_style = ParagraphStyle(
  201. 'PageHeading',
  202. parent=styles['Heading2'],
  203. fontSize=12,
  204. textColor='#1f4788',
  205. spaceAfter=0.2*inch,
  206. spaceBefore=0.2*inch,
  207. fontName=font_name
  208. )
  209. body_style = ParagraphStyle(
  210. 'CustomBody',
  211. parent=styles['BodyText'],
  212. fontSize=11,
  213. alignment=TA_JUSTIFY,
  214. spaceAfter=0.2*inch,
  215. fontName=font_name
  216. )
  217. note_style = ParagraphStyle(
  218. 'CustomBody',
  219. parent=styles['BodyText'],
  220. fontSize=8,
  221. alignment=TA_JUSTIFY,
  222. spaceAfter=0,
  223. fontName=font_name
  224. )
  225. # Création du document avec les callbacks pour les notes et le numéro de page
  226. doc = SimpleDocTemplate(
  227. output_path,
  228. pagesize=letter,
  229. topMargin=inch,
  230. bottomMargin=inch,
  231. )
  232. # Titre avec la langue cible
  233. story.append(Paragraph(f"Traduction - Ukrainien vers {TARGET_LANGUAGE.capitalize()}", title_style))
  234. story.append(Paragraph(f"Document : {PDF_PATH}", title_style))
  235. story.append(Spacer(1, 0.2*inch))
  236. # Contenu
  237. for paragraph_num, translation in results.items():
  238. formatted_text = translation.replace("\n", "<br/>")
  239. if DEBUG:
  240. # Ajoute le paragraphe avec sa note
  241. story.append(Paragraph(paragraph_num, note_style))
  242. story.append(Paragraph(formatted_text, body_style))
  243. # Infos sur le LLM
  244. story.append(Spacer(1, 0.2*inch))
  245. story.append(Paragraph(display_llm_info(), page_style))
  246. # Construction du PDF
  247. doc.build(story)
  248. print(f"PDF généré avec succès : {output_path}")
  249. def create_txt_from_results(results, output_path):
  250. """Crée un fichier TXT à partir des résultats de traduction."""
  251. OUTPUT_TXT_PATH = output_path.replace(".pdf", f".txt") # Chemin du fichier TXT de sortie
  252. # Titre avec la langue cible
  253. title_text = f"Traduction - Ukrainien vers {TARGET_LANGUAGE.capitalize()}"
  254. with open(OUTPUT_TXT_PATH, 'w', encoding='utf-8') as txt_file:
  255. txt_file.write(title_text + "\n\n")
  256. # Contenu
  257. for paragraph_num, translation in results.items():
  258. # Ajoute les numéro de paragraphe et chapitre
  259. if(DEBUG): txt_file.write(f"{paragraph_num}\n")
  260. # Préserver la mise en page en convertissant les sauts de ligne
  261. txt_file.write(translation + "\n\n")
  262. # Infos sur le LLM
  263. txt_file.write("\n")
  264. txt_file.write(display_llm_info() + "\n")
  265. # Fonction principale
  266. def main():
  267. checkpoint = load_checkpoint()
  268. last_index = checkpoint["last_processed_index"]
  269. results = checkpoint["results"]
  270. pages = extract_text_from_pdf(PDF_PATH)
  271. paragraphs = split_pages_in_paragraphs(pages)
  272. # Liste de tous les indices de batches attendus (par pas de batch_size)
  273. batch_size = 5
  274. expected_batch_indices = list(range(0, len(paragraphs), batch_size))
  275. # Liste des indices de batches déjà présents dans results
  276. present_batch_indices = set()
  277. for key in results.keys():
  278. batch_start = int(int(key) // batch_size * batch_size) # Arrondit à l'indice de début de batch
  279. present_batch_indices.add(batch_start)
  280. # Trouve les batches manquants
  281. missing_batches = [i for i in expected_batch_indices if i not in present_batch_indices and i <= last_index]
  282. # Affichage des batches manquants (pour débogage)
  283. print(f"Batches manquants détectés : {missing_batches}")
  284. # Traduction des paragraphes manquants
  285. for i in missing_batches:
  286. batch = paragraphs[i:i + batch_size]
  287. paragraph_cumul = "\n".join(batch)
  288. print(f"{15 * '-'} Traduction des paragraphes manquants {i+1} à {min(i + batch_size, len(paragraphs))} / {len(paragraphs)}")
  289. try:
  290. result = send_to_ollama(paragraph_cumul)
  291. print(f"{result}")
  292. results[str(i)] = result
  293. save_checkpoint(len(paragraphs), results) # Met à jour le dernier indice du batch
  294. save_temp_results(results)
  295. except Exception as e:
  296. print(f"Erreur lors de la traduction du paragraphe {i}: {e}")
  297. # Traitement des paragraphes suivants
  298. for i in range(last_index + 1, len(paragraphs), batch_size):
  299. batch = paragraphs[i:i + batch_size]
  300. paragraph_cumul = "\n".join(batch)
  301. print(f"{15 * '-'} Traduction des paragraphes {i+1} à {min(i + batch_size, len(paragraphs))} / {len(paragraphs)}")
  302. try:
  303. result = send_to_ollama(paragraph_cumul)
  304. print(f"{result}")
  305. results[str(i)] = result
  306. save_checkpoint(i + batch_size - 1, results)
  307. save_temp_results(results)
  308. except Exception as e:
  309. print(f"Erreur : {e}")
  310. continue
  311. save_temp_results(results)
  312. create_pdf_from_results(results, FINAL_OUTPUT_PDF)
  313. create_txt_from_results(results, FINAL_OUTPUT_TXT)
  314. print("Traduction terminée !")
  315. if __name__ == "__main__":
  316. main()