| import PyPDF2 | import PyPDF2 | ||||
| import requests | import requests | ||||
| import json | import json | ||||
| from reportlab.lib.pagesizes import letter | |||||
| from reportlab.lib.units import inch | |||||
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer | |||||
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |||||
| from reportlab.lib.enums import TA_JUSTIFY | |||||
| from reportlab.pdfbase import pdfmetrics | |||||
| from reportlab.pdfbase.ttfonts import TTFont | |||||
| import os | import os | ||||
| from datetime import datetime | from datetime import datetime | ||||
| # Configuration | # Configuration | ||||
| PDF_PATH = "TaniaBorecMemoir(Ukr).pdf" # Fichier original | |||||
| PDF_PATH = "TaniaBorecMemoir(Ukr).pdf" | |||||
| OLLAMA_MODEL = "traductionUkrainienVersFrancais:latest" | OLLAMA_MODEL = "traductionUkrainienVersFrancais:latest" | ||||
| OLLAMA_URL = "http://localhost:11434/api/generate" # URL par défaut d'Ollama | |||||
| TARGET_LANGUAGE = "français" # Langue cible (ex: "français", "anglais", "allemand", "espagnol", etc.) | |||||
| # Récupère la date et l'heure actuelles au format AAAMMJJ-HHMM | |||||
| current_datetime = datetime.now().strftime("%Y%m%d-%H%M") | |||||
| # Ajoute la date et la langue cible au nom du fichier PDF de sortie | |||||
| # OUTPUT_PDF_PATH = PDF_PATH.replace(".pdf",f" ({TARGET_LANGUAGE.upper()[:2]})_{current_datetime}.pdf") | |||||
| OUTPUT_PDF_PATH = PDF_PATH.replace(".pdf",f" ({TARGET_LANGUAGE.upper()[:2]})_V2.pdf") | |||||
| def extract_parameters_from_template(template_str): | |||||
| """Extrait les paramètres du modèle à partir du template.""" | |||||
| import re | |||||
| parameters = {} | |||||
| if not template_str or not isinstance(template_str, str): | |||||
| return parameters | |||||
| # Si la chaîne contient "parameters:", récupère ce qui suit | |||||
| if 'parameters:' in template_str: | |||||
| params_section = template_str.split('parameters:', 1)[1] | |||||
| else: | |||||
| # Sinon, utilise la chaîne directement (elle contient déjà les paramètres) | |||||
| params_section = template_str | |||||
| # Parse les lignes de paramètres | |||||
| # Format: "stop "<end_of_turn>"" | |||||
| # "temperature 0.1" | |||||
| lines = params_section.split('\n') | |||||
| for line in lines: | |||||
| if not line.strip(): | |||||
| continue | |||||
| # Divise par le premier groupe d'espaces blancs | |||||
| # Cela sépare la clé des valeurs avec leurs espaces | |||||
| parts = line.split(None, 1) # split() avec maxsplit=1 divise sur les espaces | |||||
| if len(parts) == 2: | |||||
| param_name = parts[0].strip() | |||||
| param_value = parts[1].strip() | |||||
| parameters[param_name] = param_value | |||||
| return parameters | |||||
| def get_llm_model_info(model=OLLAMA_MODEL): | |||||
| """ | |||||
| Extrait les informations du modèle LLM depuis Ollama, y compris le nom depuis la ligne FROM du Modelfile. | |||||
| @param model: Nom du modèle à interroger. | |||||
| @type model: str | |||||
| @return: Dictionnaire contenant les informations du modèle, ou None en cas d'erreur. | |||||
| @rtype: dict | None | |||||
| """ | |||||
| try: | |||||
| # Chemin vers le fichier Modelfile (supposé être dans le même répertoire que le script) | |||||
| modelfile_path = os.path.join(os.path.dirname(__file__), "Modelfile") | |||||
| # Initialisation de model_name | |||||
| model_name = "none" | |||||
| # Lecture du fichier Modelfile pour extraire le nom du modèle | |||||
| if os.path.exists(modelfile_path): | |||||
| with open(modelfile_path, 'r', encoding='utf-8') as file: | |||||
| for line in file: | |||||
| if line.strip().startswith('FROM '): | |||||
| model_name = line.strip().split('FROM ')[1].strip() | |||||
| break | |||||
| # URL pour obtenir les informations du modèle | |||||
| info_url = OLLAMA_URL.replace("/api/generate", "/api/show") | |||||
| payload = {"name": model} | |||||
| response = requests.post(info_url, json=payload) | |||||
| if response.status_code == 200: | |||||
| model_data = response.json() | |||||
| # Gère le cas où model_data est une chaîne | |||||
| if isinstance(model_data, str): | |||||
| model_data = json.loads(model_data) | |||||
| # Extrait les paramètres du template | |||||
| parameters = model_data.get('parameters', '') | |||||
| parsed_params = extract_parameters_from_template(parameters) | |||||
| # Extraction du nom depuis la ligne FROM | |||||
| modelfile_content = model_data.get('Modelfile', '') | |||||
| # Extraction des informations principales | |||||
| info = { | |||||
| "temperature": parsed_params.get('temperature', model_data.get("temperature", "Not available")), | |||||
| "name": model_name, | |||||
| "num_ctx": parsed_params.get('num_ctx', "Not available"), | |||||
| "top_k": parsed_params.get('top_k', "Not available"), | |||||
| "top_p": parsed_params.get('top_p', "Not available"), | |||||
| "system": model_data.get("system", "Not available"), | |||||
| "modified_at": model_data.get("modified_at", "Not available"), | |||||
| } | |||||
| return info | |||||
| else: | |||||
| print(f"Erreur lors de la récupération du modèle : {response.text}") | |||||
| return None | |||||
| except Exception as e: | |||||
| print(f"Erreur lors de l'accès aux informations du modèle : {e}") | |||||
| return None | |||||
| def display_llm_info(): | |||||
| """Retourne les informations du modèle LLM formatées.""" | |||||
| from datetime import datetime | |||||
| info = get_llm_model_info(OLLAMA_MODEL) | |||||
| if info: | |||||
| # Formate la date en jj/mm/AAAA | |||||
| modified_at = info.get('modified_at', 'Not available') | |||||
| if modified_at and modified_at != 'Not available': | |||||
| try: | |||||
| # Parse la date ISO | |||||
| date_obj = datetime.fromisoformat(modified_at.replace('Z', '+00:00')) | |||||
| # Formate en jj/mm/AAAA | |||||
| formatted_date = date_obj.strftime("%d/%m/%Y") | |||||
| except: | |||||
| formatted_date = modified_at | |||||
| else: | |||||
| formatted_date = 'Not available' | |||||
| return f"LLM Modèle: {info['name']}<br//>\nDate de modification: {formatted_date}<br//>\nSystem: {info['system']}<br//>\nTemperature: {info['temperature']}" | |||||
| else: | |||||
| return "Informations du modèle non disponibles" | |||||
| OLLAMA_URL = "http://localhost:11434/api/generate" | |||||
| TARGET_LANGUAGE = "français" | |||||
| CHECKPOINT_FILE = "checkpoint.json" | |||||
| TEMP_OUTPUT_TXT = "output_temp.txt" | |||||
| FINAL_OUTPUT_PDF = PDF_PATH.replace(".pdf",f" ({TARGET_LANGUAGE.upper()[:2]})_V2.pdf") | |||||
| FINAL_OUTPUT_TXT = PDF_PATH.replace(".pdf",f" ({TARGET_LANGUAGE.upper()[:2]})_V2.txt") | |||||
| # Charge ou initialise le checkpoint | |||||
| def load_checkpoint(): | |||||
| if os.path.exists(CHECKPOINT_FILE): | |||||
| with open(CHECKPOINT_FILE, "r") as f: | |||||
| return json.load(f) | |||||
| return {"last_processed_index": -1, "results": {}} | |||||
| # Sauvegarde le checkpoint | |||||
| def save_checkpoint(last_index, results): | |||||
| with open(CHECKPOINT_FILE, "w") as f: | |||||
| json.dump({"last_processed_index": last_index, "results": results}, f) | |||||
| # Sauvegarde les résultats temporaires dans un fichier TXT | |||||
| def save_temp_results(results): | |||||
| with open(TEMP_OUTPUT_TXT, "w", encoding="utf-8") as f: | |||||
| for idx, translation in results.items(): | |||||
| f.write(f"Paragraphe {idx}:\n{translation}\n\n") | |||||
| # Extraction du texte du PDF (inchangée) | |||||
| def extract_text_from_pdf(pdf_path): | def extract_text_from_pdf(pdf_path): | ||||
| """Extrait le texte page par page d'un PDF sans les numéros de pages.""" | |||||
| import re | |||||
| text_by_page = [] | text_by_page = [] | ||||
| with open(pdf_path, "rb") as file: | with open(pdf_path, "rb") as file: | ||||
| reader = PyPDF2.PdfReader(file) | reader = PyPDF2.PdfReader(file) | ||||
| for page in reader.pages: | for page in reader.pages: | ||||
| text = page.extract_text() | text = page.extract_text() | ||||
| # Supprime les numéros de pages (nombres seuls en début/fin de ligne) | |||||
| text = re.sub(r'^\d+\s*\n', '', text, flags=re.MULTILINE) | |||||
| text = re.sub(r'\n\s*\d+\s*$', '', text, flags=re.MULTILINE) | |||||
| text_by_page.append(text) | text_by_page.append(text) | ||||
| return text_by_page | return text_by_page | ||||
| # Découpage en paragraphes (inchangé) | |||||
| def split_pages_in_paragraphs(pages_text): | def split_pages_in_paragraphs(pages_text): | ||||
| """ | |||||
| Divise le texte en paragraphes en détectant un point suivi d'un saut de ligne ou d'un retour à la ligne. | |||||
| Conserve les sauts de ligne à l'intérieur des paragraphes. | |||||
| """ | |||||
| import re | import re | ||||
| # Concatène tout le texte | |||||
| full_text = "\n".join(pages_text) | full_text = "\n".join(pages_text) | ||||
| # Remplace les sauts de ligne à l'intérieur des paragraphes par des espaces | |||||
| # (pour éviter les sauts de ligne intempestifs dans un même paragraphe) | |||||
| full_text = re.sub(r'(?<![.!?])\n+(?![.!?])', ' ', full_text) | full_text = re.sub(r'(?<![.!?])\n+(?![.!?])', ' ', full_text) | ||||
| # Divise le texte en paragraphes : un point suivi d'un saut de ligne | |||||
| paragraphs = re.split(r'(?<=[.!?])\s*\n+', full_text.strip()) | paragraphs = re.split(r'(?<=[.!?])\s*\n+', full_text.strip()) | ||||
| # Nettoie chaque paragraphe : remplace les sauts de ligne restants par des espaces | |||||
| paragraphs = [re.sub(r'\s+', ' ', p).strip() for p in paragraphs if p.strip()] | paragraphs = [re.sub(r'\s+', ' ', p).strip() for p in paragraphs if p.strip()] | ||||
| return paragraphs | return paragraphs | ||||
| def send_to_ollama(text, target_lang=TARGET_LANGUAGE, model=OLLAMA_MODEL, context_size=128000): | |||||
| """Envoie une requête à Ollama et retourne la réponse traduite.""" | |||||
| # Construit le prompt avec les instructions système et la demande de traduction | |||||
| # Envoi à Ollama (inchangé) | |||||
| def send_to_ollama(text, target_lang=TARGET_LANGUAGE, model=OLLAMA_MODEL): | |||||
| full_prompt = f"\n\nTraduis le texte suivant de l'ukrainien vers le {target_lang} :\n{text}" | full_prompt = f"\n\nTraduis le texte suivant de l'ukrainien vers le {target_lang} :\n{text}" | ||||
| payload = { | |||||
| "model": model, | |||||
| "prompt": full_prompt, | |||||
| "stream": False, | |||||
| "options": {"num_ctx": context_size} | |||||
| } | |||||
| payload = {"model": model, "prompt": full_prompt, "stream": False} | |||||
| response = requests.post(OLLAMA_URL, data=json.dumps(payload)) | response = requests.post(OLLAMA_URL, data=json.dumps(payload)) | ||||
| if response.status_code == 200: | if response.status_code == 200: | ||||
| return response.json()["response"] | return response.json()["response"] | ||||
| else: | else: | ||||
| raise Exception(f"Erreur Ollama: {response.text}") | raise Exception(f"Erreur Ollama: {response.text}") | ||||
| def register_unicode_font(): | |||||
| """Enregistre une police TrueType qui supporte le cyrilique.""" | |||||
| # Recherche une police système qui supporte le cyrilique | |||||
| font_paths = [ | |||||
| r"C:\Windows\Fonts\DejaVuSans.ttf", | |||||
| r"C:\Windows\Fonts\Calibri.ttf", | |||||
| r"C:\Windows\Fonts\arial.ttf", | |||||
| ] | |||||
| for font_path in font_paths: | |||||
| if os.path.exists(font_path): | |||||
| try: | |||||
| pdfmetrics.registerFont(TTFont('UnicodeFont', font_path)) | |||||
| return 'UnicodeFont' | |||||
| except Exception as e: | |||||
| print(f"Erreur lors de l'enregistrement de {font_path}: {e}") | |||||
| # Si aucune police spéciale trouvée, utilise Helvetica par défaut | |||||
| print("Aucune police Unicode trouvée, utilisation d'Helvetica") | |||||
| return 'Helvetica' | |||||
| # Création du PDF final (inchangée) | |||||
| def create_pdf_from_results(results, output_path): | def create_pdf_from_results(results, output_path): | ||||
| """Crée un PDF à partir des résultats de traduction.""" | |||||
| doc = SimpleDocTemplate(output_path, pagesize=letter, topMargin=inch, bottomMargin=inch) | |||||
| from reportlab.lib.pagesizes import letter | |||||
| from reportlab.lib.units import inch | |||||
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer | |||||
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |||||
| from reportlab.lib.enums import TA_JUSTIFY | |||||
| from reportlab.pdfbase import pdfmetrics | |||||
| from reportlab.pdfbase.ttfonts import TTFont | |||||
| doc = SimpleDocTemplate(output_path, pagesize=letter) | |||||
| story = [] | story = [] | ||||
| # Enregistre une police qui supporte le cyrilique | |||||
| font_name = register_unicode_font() | |||||
| # Style personnalisé | |||||
| styles = getSampleStyleSheet() | styles = getSampleStyleSheet() | ||||
| title_style = ParagraphStyle( | |||||
| 'CustomTitle', | |||||
| parent=styles['Heading1'], | |||||
| fontSize=16, | |||||
| textColor='#1f4788', | |||||
| spaceAfter=0.3*inch, | |||||
| alignment=TA_JUSTIFY, | |||||
| fontName=font_name | |||||
| ) | |||||
| page_style = ParagraphStyle( | |||||
| 'PageHeading', | |||||
| parent=styles['Heading2'], | |||||
| fontSize=12, | |||||
| textColor='#1f4788', | |||||
| spaceAfter=0.2*inch, | |||||
| spaceBefore=0.2*inch, | |||||
| fontName=font_name | |||||
| ) | |||||
| body_style = ParagraphStyle( | |||||
| 'CustomBody', | |||||
| parent=styles['BodyText'], | |||||
| fontSize=11, | |||||
| alignment=TA_JUSTIFY, | |||||
| spaceAfter=0.2*inch, | |||||
| fontName=font_name | |||||
| ) | |||||
| # Titre avec la langue cible | |||||
| story.append(Paragraph(f"Traduction - Ukrainien vers {TARGET_LANGUAGE.capitalize()}", title_style)) | |||||
| story.append(Paragraph(f"Document : {PDF_PATH}", title_style)) | |||||
| story.append(Spacer(1, 0.2*inch)) | |||||
| # Contenu | |||||
| for page_num, translation in results.items(): | |||||
| # Préserver la mise en page en convertissant les sauts de ligne | |||||
| formatted_text = translation.replace("\n", "<br/>") | |||||
| story.append(Paragraph(formatted_text, body_style)) | |||||
| # story.append(Spacer(1, 0.1*inch)) | |||||
| # Infos sur le LLM | |||||
| story.append(Spacer(1, 0.2*inch)) | |||||
| story.append(Paragraph(display_llm_info(), page_style)) | |||||
| # Construction du PDF | |||||
| doc.build(story) | |||||
| print(f"PDF généré avec succès : {output_path}") | |||||
| body_style = styles["BodyText"] | |||||
| def create_txt_from_results(results, output_path): | |||||
| """Crée un fichier TXT à partir des résultats de traduction.""" | |||||
| OUTPUT_TXT_PATH = output_path.replace(".pdf", f".txt") # Chemin du fichier TXT de sortie | |||||
| # Titre avec la langue cible | |||||
| title_text = f"Traduction - Ukrainien vers {TARGET_LANGUAGE.capitalize()}" | |||||
| with open(OUTPUT_TXT_PATH, 'w', encoding='utf-8') as txt_file: | |||||
| txt_file.write(title_text + "\n\n") | |||||
| # Contenu | |||||
| for page_num, translation in results.items(): | |||||
| # Préserver la mise en page en convertissant les sauts de ligne | |||||
| txt_file.write(translation + "\n\n") | |||||
| # Infos sur le LLM | |||||
| txt_file.write("\n") | |||||
| txt_file.write(display_llm_info() + "\n") | |||||
| for idx, translation in results.items(): | |||||
| story.append(Paragraph(translation, body_style)) | |||||
| doc.build(story) | |||||
| print(f"PDF final généré : {output_path}") | |||||
| # Fonction principale | |||||
| def main(): | def main(): | ||||
| # Affiche les informations du modèle LLM | |||||
| display_llm_info() | |||||
| # Extraction du texte page par page | |||||
| # Charge le checkpoint | |||||
| checkpoint = load_checkpoint() | |||||
| last_index = checkpoint["last_processed_index"] | |||||
| results = checkpoint["results"] | |||||
| # Extraction des paragraphes | |||||
| pages = extract_text_from_pdf(PDF_PATH) | pages = extract_text_from_pdf(PDF_PATH) | ||||
| print(f"Nombre de pages extraites : {len(pages)}") | |||||
| # Fusion des paragraphes qui s'étendent sur plusieurs pages | |||||
| paragraphs = split_pages_in_paragraphs(pages) | paragraphs = split_pages_in_paragraphs(pages) | ||||
| print(f"Nombre de paragraphes complets extraits : {len(paragraphs)}") | |||||
| # Dictionnaire pour stocker les résultats | |||||
| results = {} | |||||
| # Traitement des paragraphes complets | |||||
| # for i, paragraph_text in enumerate(paragraphs, start=1): | |||||
| nb_paragraph_cumul = 7 | |||||
| for i in range(0, len(paragraphs), nb_paragraph_cumul): | |||||
| batch = paragraphs[i:i + nb_paragraph_cumul] | |||||
| paragraph_cumul = "\n".join(batch) # Concatène les paragraphes avec un saut de ligne | |||||
| # Traitement des paragraphes | |||||
| batch_size = 3 | |||||
| for i in range(last_index + 1, len(paragraphs), batch_size): | |||||
| batch = paragraphs[i:i + batch_size] | |||||
| paragraph_cumul = "\n".join(batch) | |||||
| start_idx = i + 1 | |||||
| end_idx = min(i + nb_paragraph_cumul, len(paragraphs)) | |||||
| print(f"{15 * '-'} Traduction des paragraphes {start_idx} à {end_idx} / {len(paragraphs)}...") | |||||
| print(f"{15 * '-'} Traduction des paragraphes {i+1} à {min(i + batch_size, len(paragraphs))} / {len(paragraphs)}") | |||||
| try: | try: | ||||
| result = send_to_ollama(paragraph_cumul, target_lang=TARGET_LANGUAGE) | |||||
| result = send_to_ollama(paragraph_cumul) | |||||
| print(f"{result}") | print(f"{result}") | ||||
| # Stocke le résultat pour chaque paragraphe du batch | |||||
| results[i] = result # Ou `results[idx] = f"Résultat pour {idx}"` si tu veux les séparer | |||||
| results[i] = result | |||||
| save_checkpoint(i, results) # Sauvegarde le checkpoint | |||||
| save_temp_results(results) # Sauvegarde les résultats temporaires | |||||
| except Exception as e: | except Exception as e: | ||||
| print(f"Erreur lors du traitement des paragraphes {start_idx} à {end_idx} : {e}") | |||||
| results[i] = f"Erreur lors du traitement des paragraphes {start_idx} à {end_idx} : {e}" | |||||
| print(f"Erreur : {e}") | |||||
| continue | |||||
| # Création du PDF avec tous les résultats | |||||
| create_pdf_from_results(results, OUTPUT_PDF_PATH) | |||||
| create_txt_from_results(results, OUTPUT_PDF_PATH) | |||||
| # Génération des fichiers finaux | |||||
| save_temp_results(results) | |||||
| create_pdf_from_results(results, FINAL_OUTPUT_PDF) | |||||
| os.rename(TEMP_OUTPUT_TXT, FINAL_OUTPUT_TXT) | |||||
| print("Traduction terminée !") | |||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||
| main() | main() |