OpenAIのAPIを使って日本語のワード文章をスタイルを保持しながら英訳する

実現したこと

とある会議にて。日本語で書いてある説明書を英訳しなきゃねぇみたいな話になった。人力で頑張りましょう見たいな雰囲気が漂って来たので、無敵のPythonでやる仕組みを構築したので紹介します。

このスクリプトは元のワードファイルの形式を取得し、日本語をOpenAIのAPIを使って英語にし、形式を保持しながらワードファイルを生成する感じです。

スクリプトの概要

  • Word文書の翻訳: 入力されたWord文書を読み込み、内容を翻訳し、新しいWord文書として保存します。

  • フォーマット保持: 元の文書のフォントスタイル、色、サイズ、配置などのフォーマットを保持します。

  • チャンク処理: 長い文書を適切なサイズのチャンクに分割して処理することで、APIの制限を回避します。

  • カスタマイズ可能: コマンドラインから異なるOpenAIモデルや温度設定を指定できます。

  • 進行状況表示: tqdmライブラリを使用して、翻訳の進行状況をリアルタイムで表示します。

環境構築

OpanAI APIキー取得

OpenAIのウェブサイトでアカウントを作成し、APIキーを取得します。詳細な手順はこちらの記事を参照してください。

conda環境構築

# environment_word_translator.yml
name: word-translator
channels:
  - conda-forge
  - defaults
dependencies:
  - python=3.12
  - python-dotenv
  - openai
  - python-docx
  - tqdm
conda env create -f environment_word_translator.yml

スクリプト

import os
import argparse
from dotenv import load_dotenv
from typing import List, Tuple, Optional
from docx import Document
from docx.shared import RGBColor, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.text.paragraph import Paragraph
from openai import OpenAI
from tqdm import tqdm

# Load environment variables from .env file
load_dotenv()

# OpenAI API key configuration
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
MAX_TOKENS = 4000  # Adjusted for GPT-4's capacity

def split_text(text: str, max_tokens: int) -> List[str]:
    """Split text into chunks that don't exceed max_tokens."""
    words = text.split()
    chunks = []
    current_chunk = []

    for word in words:
        if len(" ".join(current_chunk + [word])) > max_tokens:
            chunks.append(" ".join(current_chunk))
            current_chunk = [word]
        else:
            current_chunk.append(word)

    if current_chunk:
        chunks.append(" ".join(current_chunk))

    return chunks

def translate_text(client: OpenAI, text: str, model: str, temperature: float) -> str:
    """Translate text to B2 level academic English using OpenAI's GPT-4 model."""
    chunks = split_text(text, MAX_TOKENS)
    translated_chunks = []

    for chunk in tqdm(chunks, desc="Translating chunks", unit="chunk"):
        messages = [
            {"role": "system", "content": """You are a professional translator specializing in academic writing. Your task is to translate the following text to English at a B2 level (upper intermediate) with an academic tone. Follow these guidelines:

1. Maintain the original meaning and style of the text.
2. Use appropriate academic vocabulary and structures, but avoid overly complex or obscure terms.
3. Ensure diversity in word choice and sentence structures. Avoid repetitive use of certain words or phrases.
4. Do not use overly formal or archaic language. Aim for clarity and readability.
5. Preserve the original tone and intent of the text, whether it's persuasive, informative, or analytical.
6. Be aware of context and field-specific terminology, translating them accurately.
7. Avoid using cliché phrases or expressions that are overused in AI-generated text.
8. If the original text includes colloquialisms or idioms, translate them into appropriate English equivalents that maintain the intended meaning and tone.
9. Pay attention to nuances and connotations in the original text, and strive to convey these in the translation.
10. If you encounter any ambiguities in the original text, translate in a way that preserves that ambiguity rather than making assumptions.

Translate the following text, keeping these guidelines in mind:"""},
            {"role": "user", "content": chunk}
        ]
        
        try:
            response = client.chat.completions.create(
                messages=messages,
                model=model,
                max_tokens=MAX_TOKENS,
                n=1,
                stop=None,
                temperature=temperature,
            )
            translated_chunks.append(response.choices[0].message.content.strip())
        except Exception as e:
            print(f"Translation error: {e}")
            translated_chunks.append(chunk)

    return " ".join(translated_chunks)

def process_paragraph(client: OpenAI, paragraph: Paragraph, model: str, temperature: float) -> Tuple[str, List[Tuple]]:
    """Process a paragraph and its runs, returning translated text and formatting info."""
    translated_text = translate_text(client, paragraph.text, model, temperature)
    
    formatting = []
    for run in paragraph.runs:
        formatting.append((
            run.bold, run.italic, run.underline,
            run.font.name or "Default", run.font.size,
            run.font.color.rgb if run.font.color.rgb else RGBColor(0, 0, 0)
        ))
    
    return translated_text, formatting

def apply_formatting(paragraph: Paragraph, text: str, formatting: List[Tuple]) -> None:
    """Apply formatting to a paragraph based on the original formatting."""
    paragraph.text = ""
    words = text.split()
    format_index = 0
    current_run = paragraph.add_run()

    for word in words:
        if format_index < len(formatting):
            bold, italic, underline, font_name, font_size, color = formatting[format_index]
            current_run = paragraph.add_run(word + " ")
            current_run.bold = bold
            current_run.italic = italic
            current_run.underline = underline
            current_run.font.name = font_name
            if font_size:
                current_run.font.size = font_size
            current_run.font.color.rgb = color

            # Move to the next format if the current run is longer than the original
            if len(current_run.text) >= len(formatting[format_index][3]):
                format_index += 1
        else:
            current_run.add_text(word + " ")

def get_document_margins(doc: Document) -> Tuple[float, float, float, float]:
    """Get the margins of the document in inches."""
    section = doc.sections[0]
    return (
        section.top_margin.inches,
        section.bottom_margin.inches,
        section.left_margin.inches,
        section.right_margin.inches
    )

def set_document_margins(doc: Document, margins: Tuple[float, float, float, float]) -> None:
    """Set the margins of the document in inches."""
    section = doc.sections[0]
    section.top_margin = Inches(margins[0])
    section.bottom_margin = Inches(margins[1])
    section.left_margin = Inches(margins[2])
    section.right_margin = Inches(margins[3])

def translate_word_document(input_path: str, output_path: str, api_key: str, model: str, temperature: float) -> None:
    """Translate a Word document to B2 level academic English, preserving formatting."""
    client = OpenAI(api_key=OPENAI_API_KEY)
    
    doc = Document(input_path)
    translated_doc = Document()

    # Get and set margins
    original_margins = get_document_margins(doc)
    set_document_margins(translated_doc, original_margins)

    for paragraph in tqdm(doc.paragraphs, desc="Processing paragraphs", unit="paragraph"):
        translated_text, formatting = process_paragraph(client, paragraph, model, temperature)
        translated_paragraph = translated_doc.add_paragraph()
        apply_formatting(translated_paragraph, translated_text, formatting)
        translated_paragraph.alignment = paragraph.alignment
        translated_paragraph.style = paragraph.style

    translated_doc.save(output_path)
    print(f"Translated Word document saved to {output_path}")
    print(f"Original margins (top, bottom, left, right): {original_margins}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Translate Word documents to English.")
    parser.add_argument("input_file", help="Path to the input file to be translated")
    parser.add_argument("--model", default="gpt-4", help="OpenAI model to use (default: gpt-4)")
    parser.add_argument("--temperature", type=float, default=0.5, help="Generation temperature (default: 0.5)")
    args = parser.parse_args()

    api_key = OPENAI_API_KEY
    if not api_key:
        raise ValueError("OPENAI_API_KEY is not set.")

    input_file = args.input_file
    output_dir = os.path.dirname(input_file)
    output_file = os.path.join(output_dir, f"translated_{os.path.basename(input_file)}")

    translate_word_document(input_file, output_file, api_key, args.model, args.temperature)

.env

# .env
OPENAI_API_KEY=sk-your_key

使用方法

ヘルプ

Japanease-word-file-translate-to-English.py --help
usage: Japanease-word-file-translate-to-English.py [-h] [--model MODEL] [--temperature TEMPERATURE] input_file

Translate Word documents to English.

positional arguments:
  input_file            Path to the input file to be translated

options:
  -h, --help            show this help message and exit
  --model MODEL         OpenAI model to use (default: gpt-4o)
  --temperature TEMPERATURE
                        Generation temperature (default: 0.5)#

使用例

# ファイルあるいはファイルパスに空白がある場合は引用符で囲うこと。
python Japanease-word-file-translate-to-English.py "Path/to/your/file/sss sss.docx"

まとめ

このPythonスクリプトは長い文章でも100円ぐらい?で高品質なワードファイルの英訳が一瞬で可能になります。(自腹しました💦)

これで教育・研究活動に時間が使えるようになりハッピーになると良いですね。

AIの進歩は目覚ましいものがあります。皆さんも、このようなAI技術を活用して、より効率的な作業環境を作り上げていってください!

質問や改善点があれば、ぜひコメントで教えてください。

この記事が気に入ったらサポートをしてみませんか?