Script python permettant de traduire un long texte
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  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
  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. from datetime import datetime
  13. # Configuration
  14. PDF_PATH = "TaniaBorecMemoir(Ukr).pdf" # Fichier original
  15. OLLAMA_MODEL = "traductionUkrainienVersFrancais:latest"
  16. OLLAMA_URL = "http://localhost:11434/api/generate" # URL par défaut d'Ollama
  17. TARGET_LANGUAGE = "français" # Langue cible (ex: "français", "anglais", "allemand", "espagnol", etc.)
  18. # Récupère la date et l'heure actuelles au format AAAMMJJ-HHMM
  19. current_datetime = datetime.now().strftime("%Y%m%d-%H%M")
  20. # Ajoute la date et la langue cible au nom du fichier PDF de sortie
  21. # OUTPUT_PDF_PATH = PDF_PATH.replace(".pdf",f" ({TARGET_LANGUAGE.upper()[:2]})_{current_datetime}.pdf")
  22. OUTPUT_PDF_PATH = PDF_PATH.replace(".pdf",f" ({TARGET_LANGUAGE.upper()[:2]})_V2.pdf")
  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 extract_text_from_pdf(pdf_path):
  122. """Extrait le texte page par page d'un PDF sans les numéros de pages."""
  123. import re
  124. text_by_page = []
  125. with open(pdf_path, "rb") as file:
  126. reader = PyPDF2.PdfReader(file)
  127. for page in reader.pages:
  128. text = page.extract_text()
  129. # Supprime les numéros de pages (nombres seuls en début/fin de ligne)
  130. text = re.sub(r'^\d+\s*\n', '', text, flags=re.MULTILINE)
  131. text = re.sub(r'\n\s*\d+\s*$', '', text, flags=re.MULTILINE)
  132. text_by_page.append(text)
  133. return text_by_page
  134. def split_pages_in_paragraphs(pages_text):
  135. """
  136. Divise le texte en paragraphes en détectant un point suivi d'un saut de ligne ou d'un retour à la ligne.
  137. Conserve les sauts de ligne à l'intérieur des paragraphes.
  138. """
  139. import re
  140. # Concatène tout le texte
  141. full_text = "\n".join(pages_text)
  142. # Remplace les sauts de ligne à l'intérieur des paragraphes par des espaces
  143. # (pour éviter les sauts de ligne intempestifs dans un même paragraphe)
  144. full_text = re.sub(r'(?<![.!?])\n+(?![.!?])', ' ', full_text)
  145. # Divise le texte en paragraphes : un point suivi d'un saut de ligne
  146. paragraphs = re.split(r'(?<=[.!?])\s*\n+', full_text.strip())
  147. # Nettoie chaque paragraphe : remplace les sauts de ligne restants par des espaces
  148. paragraphs = [re.sub(r'\s+', ' ', p).strip() for p in paragraphs if p.strip()]
  149. return paragraphs
  150. def send_to_ollama(text, target_lang=TARGET_LANGUAGE, model=OLLAMA_MODEL, context_size=128000):
  151. """Envoie une requête à Ollama et retourne la réponse traduite."""
  152. # Construit le prompt avec les instructions système et la demande de traduction
  153. full_prompt = f"\n\nTraduis le texte suivant de l'ukrainien vers le {target_lang} :\n{text}"
  154. payload = {
  155. "model": model,
  156. "prompt": full_prompt,
  157. "stream": False,
  158. "options": {"num_ctx": context_size}
  159. }
  160. response = requests.post(OLLAMA_URL, data=json.dumps(payload))
  161. if response.status_code == 200:
  162. return response.json()["response"]
  163. else:
  164. raise Exception(f"Erreur Ollama: {response.text}")
  165. def register_unicode_font():
  166. """Enregistre une police TrueType qui supporte le cyrilique."""
  167. # Recherche une police système qui supporte le cyrilique
  168. font_paths = [
  169. r"C:\Windows\Fonts\DejaVuSans.ttf",
  170. r"C:\Windows\Fonts\Calibri.ttf",
  171. r"C:\Windows\Fonts\arial.ttf",
  172. ]
  173. for font_path in font_paths:
  174. if os.path.exists(font_path):
  175. try:
  176. pdfmetrics.registerFont(TTFont('UnicodeFont', font_path))
  177. return 'UnicodeFont'
  178. except Exception as e:
  179. print(f"Erreur lors de l'enregistrement de {font_path}: {e}")
  180. # Si aucune police spéciale trouvée, utilise Helvetica par défaut
  181. print("Aucune police Unicode trouvée, utilisation d'Helvetica")
  182. return 'Helvetica'
  183. def create_pdf_from_results(results, output_path):
  184. """Crée un PDF à partir des résultats de traduction."""
  185. doc = SimpleDocTemplate(output_path, pagesize=letter, topMargin=inch, bottomMargin=inch)
  186. story = []
  187. # Enregistre une police qui supporte le cyrilique
  188. font_name = register_unicode_font()
  189. # Style personnalisé
  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. # Titre avec la langue cible
  218. story.append(Paragraph(f"Traduction - Ukrainien vers {TARGET_LANGUAGE.capitalize()}", title_style))
  219. story.append(Paragraph(f"Document : {PDF_PATH}", title_style))
  220. story.append(Spacer(1, 0.2*inch))
  221. # Contenu
  222. for page_num, translation in results.items():
  223. # Préserver la mise en page en convertissant les sauts de ligne
  224. formatted_text = translation.replace("\n", "<br/>")
  225. story.append(Paragraph(formatted_text, body_style))
  226. # story.append(Spacer(1, 0.1*inch))
  227. # Infos sur le LLM
  228. story.append(Spacer(1, 0.2*inch))
  229. story.append(Paragraph(display_llm_info(), page_style))
  230. # Construction du PDF
  231. doc.build(story)
  232. print(f"PDF généré avec succès : {output_path}")
  233. def create_txt_from_results(results, output_path):
  234. """Crée un fichier TXT à partir des résultats de traduction."""
  235. OUTPUT_TXT_PATH = output_path.replace(".pdf", f".txt") # Chemin du fichier TXT de sortie
  236. # Titre avec la langue cible
  237. title_text = f"Traduction - Ukrainien vers {TARGET_LANGUAGE.capitalize()}"
  238. with open(OUTPUT_TXT_PATH, 'w', encoding='utf-8') as txt_file:
  239. txt_file.write(title_text + "\n\n")
  240. # Contenu
  241. for page_num, translation in results.items():
  242. # Préserver la mise en page en convertissant les sauts de ligne
  243. txt_file.write(translation + "\n\n")
  244. # Infos sur le LLM
  245. txt_file.write("\n")
  246. txt_file.write(display_llm_info() + "\n")
  247. def main():
  248. # Affiche les informations du modèle LLM
  249. display_llm_info()
  250. # Extraction du texte page par page
  251. pages = extract_text_from_pdf(PDF_PATH)
  252. print(f"Nombre de pages extraites : {len(pages)}")
  253. # Fusion des paragraphes qui s'étendent sur plusieurs pages
  254. paragraphs = split_pages_in_paragraphs(pages)
  255. print(f"Nombre de paragraphes complets extraits : {len(paragraphs)}")
  256. # Dictionnaire pour stocker les résultats
  257. results = {}
  258. # Traitement des paragraphes complets
  259. # for i, paragraph_text in enumerate(paragraphs, start=1):
  260. nb_paragraph_cumul = 7
  261. for i in range(0, len(paragraphs), nb_paragraph_cumul):
  262. batch = paragraphs[i:i + nb_paragraph_cumul]
  263. paragraph_cumul = "\n".join(batch) # Concatène les paragraphes avec un saut de ligne
  264. start_idx = i + 1
  265. end_idx = min(i + nb_paragraph_cumul, len(paragraphs))
  266. print(f"{15 * '-'} Traduction des paragraphes {start_idx} à {end_idx} / {len(paragraphs)}...")
  267. try:
  268. result = send_to_ollama(paragraph_cumul, target_lang=TARGET_LANGUAGE)
  269. print(f"{result}")
  270. # Stocke le résultat pour chaque paragraphe du batch
  271. results[i] = result # Ou `results[idx] = f"Résultat pour {idx}"` si tu veux les séparer
  272. except Exception as e:
  273. print(f"Erreur lors du traitement des paragraphes {start_idx} à {end_idx} : {e}")
  274. results[i] = f"Erreur lors du traitement des paragraphes {start_idx} à {end_idx} : {e}"
  275. # Création du PDF avec tous les résultats
  276. create_pdf_from_results(results, OUTPUT_PDF_PATH)
  277. create_txt_from_results(results, OUTPUT_PDF_PATH)
  278. if __name__ == "__main__":
  279. main()