제 AI-Powered Markdown Translator 스크립트의 버전 1.5 출시를 기쁘게 알려드립니다. 이번 업데이트는 기본 모델의 업데이트, 번역 프롬프트 최적화, 코드 리팩토링 및 출력 파일 관리 개선 등 여러 주요 개선 사항을 포함합니다.
버전 1.5의 새로운 기능
기본 모델 및 API 키 업데이트
이번 버전에서는 최신 AI 모델 버전을 사용하기 위해 기본 모델과 API 키를 업데이트했습니다:
- OpenAI:
gpt-4o
로 업데이트. - Mistral AI:
mistral-large-latest
로 변경. - Anthropic의 Claude:
claude-3-5-sonnet-20240620
로 업데이트하고DEFAULT_ANTHROPIC_API_KEY
추가.
이러한 업데이트를 통해 스크립트는 가장 최신의 성능이 좋은 모델을 사용하여 더 높은 품질의 번역을 제공합니다.
번역 프롬프트 최적화
AI 모델과 상호 작용하는 프롬프트를 개선하여 번역의 명확성과 효율성을 높였습니다:
- 상세 지침: 프롬프트에는 메타데이터, URL, 이미지 경로, 코드 블록 및 Markdown 파일의 ‘front matter’ 등 형식 요소를 보존하기 위한 구체적인 지침이 포함됩니다.
- 형식 보존: 프롬프트에서 형식 요소와 코드를 번역하지 않도록 강조하여 번역된 문서의 원래 구조를 보다 잘 유지합니다.
코드 리팩토링 및 개선
스크립트의 유지보수성과 성능을 향상시키기 위해 여러 조정이 이루어졌습니다:
MistralClient
를Mistral
로 대체: Mistral AI 클라이언트 초기화를Mistral
클래스를 사용하도록 업데이트하여 API의 새로운 버전과의 호환성을 높였습니다.- 임포트 재구성: 임포트를 재배치하여 코드의 가독성과 유지보수를 용이하게 했습니다.
- 텍스트 세분화 개선: 세분화 함수가 긴 텍스트를 더 잘 처리하고 자연스러운 분할 지점을 유지하여 번역의 일관성과 유연성을 높입니다.
- 코드 블록 관리: 번역 시 코드 블록을 보존하여 원래의 형식을 유지하고 잠재적인 코드 오류를 방지합니다.
출력 파일 관리 개선
- 파일 이름 지정: 파일 이름에서 모델과 언어의 순서를 반대로 하여 번역을 보다 쉽게 정리하고 검색할 수 있도록 했습니다. 예:
mon_article-en-gpt-4o.md
. - 기존 번역 확인: 스크립트는 특정 파일에 대한 번역이 이미 존재하는지 모델에 관계없이 확인하여 불필요한 중복을 방지합니다. 필요시
--force
옵션을 사용하여 번역을 강제할 수 있습니다.
기타 개선 사항
- 코드 정리: 불필요한 빈 줄 삭제 및 경미한 조정을 통해 스크립트의 구조와 가독성을 개선했습니다.
- 유연성 증가: 스크립트는 미래에 새로운 기능 추가나 Anthropic 및 Mistral AI와 같은 새로운 AI 모델 지원을 쉽게 할 수 있도록 설계되었습니다.
업데이트된 코드
여기 새로운 스크립트가 있습니다:
#!/usr/bin/env python3
import os
import argparse
import time
import re
import glob
from openai import OpenAI
import anthropic
from mistralai import Mistral
EXCLUDE_PATTERNS = ["traductions_"]
# Initialisation de la configuration avec les valeurs par défaut
DEFAULT_OPENAI_API_KEY = "votre-cle-api-openai-par-defaut"
DEFAULT_MISTRAL_API_KEY = "votre-cle-api-mistral-par-defaut"
DEFAULT_ANTHROPIC_API_KEY = "votre-cle-api-anthropic-par-defaut"
DEFAULT_MODEL_OPENAI = "gpt-4o"
DEFAULT_MODEL_MISTRAL = "mistral-large-latest"
DEFAULT_MODEL_CLAUDE = "claude-3-5-sonnet-20240620"
DEFAULT_SOURCE_LANG = "fr"
DEFAULT_TARGET_LANG = "en"
DEFAULT_SOURCE_DIR = "content/posts"
DEFAULT_TARGET_DIR = "traductions_en"
MODEL_TOKEN_LIMITS = {
"gpt-4o": 4096,
"gpt-4": 8192,
"gpt-4-32k": 32768,
"gpt-4-0613": 8192,
"gpt-4-32k-0613": 32768,
"mistral-large-latest": 4096,
"claude-3-5-sonnet-20240620": 8192,
}
def segment_text(text, max_length):
"""
Divise un texte Markdown en segments ne dépassant pas la longueur maximale spécifiée,
en essayant de conserver des points de coupure naturels.
Args:
text (str): Texte Markdown à diviser.
max_length (int): Longueur maximale de chaque segment.
Returns:
list[str]: Liste des segments de texte Markdown.
"""
segments = []
while text:
if len(text) <= max_length:
segments.append(text)
break
segment = text[:max_length]
next_index = max_length
# Recherche de points de coupure naturels (fin de phrase, fin de paragraphe, fin de titre)
last_good_break = max(
segment.rfind(". "), segment.rfind("\n\n"), segment.rfind("\n#")
)
if last_good_break != -1:
next_index = last_good_break + 1
segments.append(text[:next_index])
text = text[next_index:]
return segments
def translate(
text, client, args, use_mistral=False, use_claude=False, is_translation_note=False
):
"""
Traduit un texte à l'aide de l'API OpenAI, Mistral AI ou Claude, selon les paramètres spécifiés.
Cette fonction segmente d'abord le texte pour s'assurer qu'il respecte la limite de tokens du modèle.
Elle utilise un argument optionnel 'is_translation_note' pour gérer différemment les notes de traduction.
Args:
text (str): Texte à traduire.
client: Objet client de l'API de traduction (OpenAI, Mistral AI ou Claude).
args: Objet argparse contenant les arguments de la ligne de commande.
use_mistral (bool): Si True, utilise l'API Mistral AI pour la traduction.
use_claude (bool): Si True, utilise l'API Claude pour la traduction.
is_translation_note (bool): Si True, le texte est une note de traduction.
Returns:
str: Texte traduit.
"""
model_limit = MODEL_TOKEN_LIMITS.get(args.model, 4096)
segments = segment_text(text, model_limit)
translated_segments = []
for segment in segments:
try:
prompt_message = ""
if is_translation_note:
prompt_message = (
f"Directly translate to {args.target_lang} without any additions, ensuring that elements such as URLs, image paths, code blocks, "
"and specifically 'front matter' metadata (like 'title', 'date', 'categories', 'tags', 'draft') are not translated. "
"The 'front matter' is a block of metadata used at the beginning of some file formats like Markdown for static site generators "
"such as Hugo. These metadata should remain unchanged. Additionally, any specific file formatting or markup language elements "
"(e.g., special tags, preprocessing directives) should also be left unchanged. Here is the text to translate: "
f"'{segment}'."
)
else:
prompt_message = (
f"Perform a direct translation from {args.source_lang} to {args.target_lang}, without altering URLs. "
f"Begin the translation immediately without any introduction or added notes, and ensure not to include any additional "
f"information or context beyond the requested translation: '{segment}'. Strictly follow the source text without adding, "
f"modifying, or omitting elements that are not explicitly present."
)
if use_mistral:
messages = [{"role": "user", "content": prompt_message}]
response = client.chat.complete(model=args.model, messages=messages)
translated_text = response.choices[0].message.content.strip()
elif use_claude:
messages = [{"role": "user", "content": prompt_message}]
response = client.messages.create(
model=args.model, max_tokens=4096, messages=messages
)
# Extraire le texte de chaque ContentBlock dans la liste de réponses
translated_texts = [
block.text.strip() for block in response.content
] # Assurez-vous que .content est la liste des ContentBlock
translated_text = " ".join(translated_texts)
else:
messages = [
{"role": "system", "content": prompt_message},
{"role": "user", "content": segment},
]
response = client.chat.completions.create(
model=args.model, messages=messages
)
translated_text = response.choices[0].message.content.strip()
except Exception as e:
raise RuntimeError(f"Erreur lors de la traduction : {e}")
translated_segments.append(translated_text)
return " ".join(translated_segments)
def translate_markdown_file(
file_path,
output_path,
client,
args,
use_mistral,
use_claude,
add_translation_note=False,
force=False,
):
"""
Traduit un fichier Markdown en utilisant les modèles de traitement du langage naturel de OpenAI, Mistral AI ou Claude.
Args:
file_path (str): Chemin complet vers le fichier d'entrée.
output_path (str): Chemin complet vers le fichier de sortie.
client: Objet client de traduction.
args: Arguments supplémentaires pour la traduction.
use_mistral (bool): Indique si l'API Mistral AI doit être utilisée pour la traduction.
use_claude (bool): Indique si l'API Claude doit être utilisée pour la traduction.
add_translation_note (bool): Indique si une note de traduction doit être ajoutée.
force (bool): Indique si la traduction doit être forcée même si une traduction existe déjà.
Returns:
None. Le résultat de la traduction est écrit dans le fichier de sortie spécifié.
En cas d'échec, un message est imprimé pour indiquer une erreur et suggérer de relancer le traitement.
"""
try:
# Calcul des chemins relatifs pour un affichage plus lisible
relative_file_path = os.path.join(
args.source_dir, os.path.relpath(file_path, start=args.source_dir)
)
relative_output_path = os.path.join(
args.target_dir, os.path.relpath(output_path, start=args.target_dir)
)
print(f"Traitement du fichier : {relative_file_path}")
start_time = time.time()
# Lecture du contenu du fichier
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
if not content:
print(
f"Le fichier '{relative_file_path}' est vide, aucune traduction n'est effectuée."
)
return
# Extraction et remplacement des blocs de code pour les préserver pendant la traduction
regex = re.compile(
r"(?P<start>^```(?P<block_language>(\w|-)+)\n)(?P<code>.*?\n)(?P<end>```)",
re.DOTALL | re.MULTILINE,
)
code_blocks = [match.group("code") for match in regex.finditer(content)]
placeholders = [f"#CODEBLOCK{index}#" for index, _ in enumerate(code_blocks)]
for placeholder, code_block in zip(placeholders, code_blocks):
content = content.replace(code_block, placeholder)
# Traduction du contenu
translated_content = translate(content, client, args, use_mistral, use_claude)
# Restauration des blocs de code dans le contenu traduit
for placeholder, code_block in zip(placeholders, code_blocks):
translated_content = translated_content.replace(placeholder, code_block)
# Ajout de la note de traduction si nécessaire
if add_translation_note:
translation_note = translate(
"Ce document a été traduit de la version "
+ args.source_lang
+ " vers la langue "
+ args.target_lang
+ " en utilisant le modèle "
+ args.model
+ ". Pour plus d'informations sur le processus de traduction, consultez https://gitlab.com/jls42/ai-powered-markdown-translator",
client,
args,
use_mistral,
use_claude,
True,
)
translated_content += "\n\n**" + translation_note + "**\n\n"
# Écriture du contenu traduit dans le fichier de sortie
clean_output_path = os.path.normpath(output_path)
if os.path.exists(clean_output_path) and not force:
print(
f"Le fichier '{relative_output_path}' existe déjà, aucune traduction n'est effectuée."
)
return
with open(clean_output_path, "w", encoding="utf-8") as f:
f.write(translated_content)
end_time = time.time()
print(
f"Fichier '{relative_file_path}' traduit en {end_time - start_time:.2f} secondes et enregistré sous : {relative_output_path}"
)
except IOError as e:
print(f"Erreur lors du traitement du fichier '{relative_file_path}': {e}")
except Exception as e:
print(
f"Une erreur inattendue est survenue lors de la traduction du fichier '{relative_file_path}': {e}\n"
"Veuillez relancer le traitement pour ce fichier."
)
def is_excluded(path):
"""
Vérifie si le chemin donné correspond à l'un des motifs d'exclusion.
Cette fonction parcourt la liste des motifs d'exclusion définis dans EXCLUDE_PATTERNS.
Si l'un de ces motifs est trouvé dans le chemin fourni, la fonction renvoie True,
indiquant que le chemin doit être exclu du processus de traduction.
Args:
path (str): Le chemin du fichier ou du répertoire à vérifier.
Returns:
bool: True si le chemin correspond à l'un des motifs d'exclusion, False sinon.
"""
for pattern in EXCLUDE_PATTERNS:
if pattern in path:
return True
return False
def translate_directory(
input_dir,
output_dir,
client,
args,
use_mistral,
use_claude,
add_translation_note,
force,
):
"""
Traduit tous les fichiers markdown dans le répertoire d'entrée et ses sous-répertoires.
Args:
input_dir (str): Chemin vers le répertoire d'entrée.
output_dir (str): Chemin vers le répertoire de sortie.
client: Objet client de traduction.
args: Arguments supplémentaires pour la traduction.
use_mistral (bool): Indique si l'API Mistral AI doit être utilisée pour la traduction.
use_claude (bool): Indique si l'API Claude doit être utilisée pour la traduction.
add_translation_note (bool): Indique si une note de traduction doit être ajoutée.
force (bool): Indique si la traduction doit être forcée même si une traduction existe déjà.
Returns:
None
"""
input_dir = os.path.abspath(input_dir)
output_dir = os.path.abspath(output_dir)
if not os.path.exists(output_dir):
os.makedirs(output_dir)
output_base_dir = os.path.basename(output_dir)
for root, dirs, files in os.walk(input_dir, topdown=True):
if is_excluded(root) or root.startswith(output_dir):
continue
if (
os.path.basename(root) == output_base_dir
and os.path.abspath(os.path.join(root, "..")) == input_dir
):
continue
for file in files:
if file.endswith(".md") and not is_excluded(file):
file_path = os.path.join(root, file)
base, _ = os.path.splitext(file)
output_file = f"{base}-{args.target_lang}-{args.model}.md" # Inversion du modèle et de la langue
relative_path = os.path.relpath(root, input_dir)
output_path = os.path.join(output_dir, relative_path, output_file)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# Vérification si une traduction existe déjà, peu importe le modèle
target_language_files = glob.glob(
f"{output_dir}/**/{base}-{args.target_lang}*.md", recursive=True
) + glob.glob(
f"{output_dir}/**/{base}-*{args.target_lang}.md", recursive=True
)
existing_translation = any(
[os.path.exists(file) for file in target_language_files]
)
if not existing_translation or force:
translate_markdown_file(
file_path,
output_path,
client,
args,
use_mistral,
use_claude,
add_translation_note,
force,
)
print(f"Fichier '{file}' traité.")
elif not force:
print(
f"La traduction de '{file}' existe déjà, aucune action effectuée."
)
def main():
"""
Point d'entrée principal du script de traduction de fichiers Markdown.
Ce script traduit des fichiers Markdown d'une langue source à une langue cible en utilisant
les services de traduction de l'API OpenAI, Mistral AI ou Claude. Il prend en charge la segmentation
des textes longs et peut également ajouter une note de traduction en fin de document.
Arguments du script:
--source_dir: Répertoire contenant les fichiers Markdown à traduire.
--target_dir: Répertoire de destination pour les fichiers traduits.
--model: Modèle de traduction GPT à utiliser.
--target_lang: Langue cible pour la traduction.
--source_lang: Langue source des documents.
--use_mistral: Indicateur pour utiliser l'API Mistral AI pour la traduction.
--use_claude: Indicateur pour utiliser l'API Claude pour la traduction.
--add_translation_note: Indicateur pour ajouter une note de traduction au contenu traduit.
"""
parser = argparse.ArgumentParser(description="Traduit les fichiers Markdown.")
parser.add_argument(
"--force",
action="store_true",
help="Forcer la traduction même si une traduction existe déjà",
)
parser.add_argument(
"--source_dir",
type=str,
default=DEFAULT_SOURCE_DIR,
help="Répertoire source contenant les fichiers Markdown",
)
parser.add_argument(
"--target_dir",
type=str,
default=DEFAULT_TARGET_DIR,
help="Répertoire cible pour sauvegarder les traductions",
)
parser.add_argument(
"--model",
type=str,
help="Modèle GPT à utiliser pour la traduction, la valeur par défaut dépend de l'API sélectionnée",
)
parser.add_argument(
"--target_lang",
type=str,
default=DEFAULT_TARGET_LANG,
help="Langue cible pour la traduction",
)
parser.add_argument(
"--source_lang",
type=str,
default=DEFAULT_SOURCE_LANG,
help="Langue source pour la traduction",
)
parser.add_argument(
"--use_mistral",
action="store_true",
help="Utiliser l'API Mistral AI pour la traduction",
)
parser.add_argument(
"--use_claude",
action="store_true",
help="Utiliser l'API Claude d'Anthropic pour la traduction",
)
parser.add_argument(
"--add_translation_note",
action="store_true",
help="Ajouter une note de traduction au contenu traduit",
)
args = parser.parse_args()
if not os.path.isdir(args.source_dir):
raise ValueError(
f"Le répertoire source spécifié n'existe pas : {args.source_dir}"
)
if not os.path.exists(args.target_dir):
os.makedirs(args.target_dir)
if args.use_mistral:
args.model = args.model if args.model else DEFAULT_MODEL_MISTRAL
api_key = os.getenv("MISTRAL_API_KEY", DEFAULT_MISTRAL_API_KEY)
if not api_key:
raise ValueError("Clé API Mistral non spécifiée.")
client = Mistral(api_key=api_key)
elif args.use_claude:
args.model = args.model if args.model else DEFAULT_MODEL_CLAUDE
api_key = os.getenv("ANTHROPIC_API_KEY", DEFAULT_ANTHROPIC_API_KEY)
if not api_key:
raise ValueError("Clé API Claude non spécifiée.")
client = anthropic.Anthropic(api_key=api_key)
else:
args.model = args.model if args.model else DEFAULT_MODEL_OPENAI
openai_api_key = os.getenv("OPENAI_API_KEY", DEFAULT_OPENAI_API_KEY)
if not openai_api_key:
raise ValueError("Clé API OpenAI non spécifiée.")
client = OpenAI(api_key=openai_api_key)
translate_directory(
args.source_dir,
args.target_dir,
client,
args,
args.use_mistral,
args.use_claude,
args.add_translation_note,
args.force,
)
if args.use_mistral or args.use_claude:
try:
del client
except TypeError:
pass
if __name__ == "__main__":
main()
소스 코드 접근
모든 새로운 기능이 포함된 전체 소스 코드는 GitLab에서 확인할 수 있습니다. 이를 확인하고 자신의 프로젝트에 사용하거나 기여해 주세요. 당신의 피드백은 이 도구를 지속적으로 개선하는 데 매우 중요합니다.
👉 AI-Powered Markdown Translator sur GitLab
결론
이 버전 1.5 업데이트는 제 자동 번역 스크립트 AI-Powered Markdown Translator의 진화에서 중요한 단계입니다. 기본 모델을 업데이트하고 코드를 최적화하여, 여러분의 번역 요구를 충족하기 위해 더욱 강력하고 유연한 도구를 제공하고자 합니다. 이러한 개선 사항이 유용하길 바라며 여러분의 의견과 제안을 환영합니다.
이 문서는 gpt-4o 모델을 사용하여 fr 버전에서 ko 언어로 번역되었습니다. 번역 과정에 대한 자세한 내용은 https://gitlab.com/jls42/ai-powered-markdown-translator를 참조하세요.