はじめに
機密情報や社内資料を含むプレゼンテーションを外部のクラウドにアップロードするのは避けたいというニーズは多い。特に翻訳サービスは利便性と引き換えに情報漏えいのリスクがあり、企業では利用を制限しているケースもある。本記事では、PowerPointファイルを丸ごと翻訳できるツールとウェブサービスを自前で用意する方法を解説する。
GitHubで pptx‑slide‑translator を公開した。ソースコードを読み解きながら、機能概要や使い方、Webサービスの構成、メトリクスの確認方法まで、セキュアな翻訳環境の構築手順を紹介する。
pptx‑slide‑translator とは
このリポジトリにはコマンドラインツールとWebサービスの二つの実装が含まれています。主な特徴は次の通りです。
- PPTXファイル内のテキストを一括で翻訳する
- OpenAI GPT-4.1-miniなどのAPIを利用しつつ、OpenAI互換APIやローカルLLMにも対応
- 日本語と英語への翻訳に対応 (多言語対応はLLMに依存)
- グループ化されたオブジェクトやテーブル内のテキストも検出し翻訳
ツールはPython 3.13以上が必要で、langchainやpython-pptx、tiktokenといったライブラリを利用しています。インストールは uv tool から行う方法が推奨されており、pptx-translate というコマンドが提供されます。
コマンドラインツールの使い方
コマンドライン版は pptx-translate としてインストールされます。基本的な使用方法は以下の通りです。
# 英語へ翻訳(デフォルト)
pptx-translate input.pptx
# 日本語へ翻訳
pptx-translate input.pptx -l ja
# 出力ファイル名を指定
pptx-translate input.pptx -o translated.pptx
# 特定のモデルやAPIエンドポイントを指定
pptx-translate input.pptx -m gpt-4.1 -u http://localhost:11434/v1
利用前には必ず環境変数を設定する必要があります。OpenAIのAPIを利用する場合は、自身のアカウントから発行したAPIキーを OPENAI_API_KEY に設定しないと翻訳リクエストが失敗します。環境変数の設定例は以下のようになります。
# bash/zshの場合
export OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# fishシェルの場合
set -x OPENAI_API_KEY sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
OpenAI互換APIやローカルLLMを使用する場合でも OPENAI_API_KEY は空文字にできないため、ダミー文字列を設定します。また、使用するモデル名は OPENAI_MODEL に、APIのベースURLは OPENAI_BASEURL に指定します。例えば、Ollamaサーバーで動作している gemma3:12b モデルを使用する場合は次のように設定します。
export OPENAI_API_KEY="dummy" # ダミー値でも空でなければよい
export OPENAI_MODEL="gemma3:12b" # ローカルLLMのモデル名
export OPENAI_BASEURL="http://localhost:11434/v1" # Ollamaのエンドポイント
OPENAI_MODEL にはGPT‑4.1-miniのようなOpenAIモデル名や gemma3:12b のようなローカルモデル名を指定できます。OPENAI_BASEURL を指定しない場合はOpenAIのデフォルトAPIエンドポイントが使用されますが、ローカルLLMを利用する場合は必ずエンドポイントURLを指定してください。これらの環境変数はターミナルで設定するか、.env ファイルに記述しておくと、ツールやWebサービスから自動的に読み込まれます。
コマンドラインオプション
pptx-translate には以下のオプションが用意されています。
| オプション | 意味 |
|---|---|
-o OUTPUT |
出力PPTXファイルのパス (デフォルトは output_translated.pptx) |
-l {ja,en} |
翻訳先言語。jaは日本語、enは英語 |
-m MODEL |
使用するモデル名。例: gpt-4.1-mini、gemma3:12b
|
-u BASEURL |
OpenAI互換APIのベースURL。ローカルLLMのエンドポイント指定に使う |
翻訳ロジックの概要
翻訳処理の中心は pptx_slide_translator/main.py に実装されています。主要な流れは次の通りです。
-
環境変数の確認:
OPENAI_API_KEYが設定されていない場合はエラーを出して終了します。 - モデル設定の取得:環境変数からモデル名とAPIベースURLを読み取り、OSSモデルの場合はローカルエンドポイントを自動設定します。
- テキストの収集:PowerPointファイルを読み込み、各スライド内の図形・テーブル・グループ化オブジェクトからテキストを再帰的に収集します。空白文字のみのテキストは除外され、収集したテキストとオブジェクト参照がリストに格納されます。
-
バッチ翻訳:収集したテキストを10件ずつのバッチに分け、
LangChainのChatOpenAIクラスを使って非同期で翻訳を実行します。プロンプトではJSON形式で結果を返すようモデルに指示し、JsonOutputParserで安全に構造化データを解析します。 - 結果の適用:翻訳が完了したら、元のテキストオブジェクトに順番に書き戻し、スライドのレイアウトやスタイルを維持したまま翻訳済みファイルを保存します。
-
メトリクス取得:
backend/token_counter.pyのTiktokenCountCallbackを利用し、翻訳時の入力・出力トークン数や推定コストを取得します。メトリクスは標準出力に表示されるだけでなく、Webサービスではファイルとして記録されます。
翻訳ロジックのコード
以下は pptx_slide_translator/main.py の主要部分のサンプルコードです。ここでは流れを追いやすいように要点だけを記載しています。
from pptx import Presentation
import asyncio
# ヘルパー関数: シェイプからテキストを収集する
def collect_texts_from_shape(shape, texts_to_translate, text_objects):
"""
各スライド内のシェイプを走査してテキストを集める関数
グループ化されたオブジェクトやテーブルは内部に複数のシェイプやセルを持つため、
再帰的に探索しないと深い階層のテキストが取りこぼされてしまう
再帰処理を行わない場合、グループやテーブル内のテキストが翻訳されず
元の言語のまま残ってしまう
"""
# 例: グループの処理
if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
for child in shape.shapes:
collect_texts_from_shape(child, texts_to_translate, text_objects)
return
# 例: テーブルの処理
if shape.shape_type == MSO_SHAPE_TYPE.TABLE:
for row in shape.table.rows:
for cell in row.cells:
if cell.text.strip():
texts_to_translate.append(cell.text)
text_objects.append(cell)
return
# 通常のテキストフレーム
if hasattr(shape, "text_frame") and shape.text_frame:
for paragraph in shape.text_frame.paragraphs:
for run in paragraph.runs:
if run.text.strip():
texts_to_translate.append(run.text)
text_objects.append(run)
# メインの翻訳関数
async def translate_texts_openai_async(texts, target_lang="en", batch_size=10):
"""
LangChainを用いてテキストリストをバッチごとに並列翻訳する
バッチサイズを大きくすると速度は上がるが、短時間にAPIを呼び出す回数が増えるため
レート制限に抵触するリスクが高まる。適切なバッチサイズを選択することが重要
"""
# ... 実際の実装ではChatOpenAIとJsonOutputParserを使用して翻訳処理を行う ...
pass
def translate_pptx(input_path, output_path, target_lang="en"):
# PPTX読み込み
prs = Presentation(input_path)
# テキストとそのオブジェクトを収集
texts_to_translate = []
text_objects = []
for slide in prs.slides:
for shape in slide.shapes:
collect_texts_from_shape(shape, texts_to_translate, text_objects)
# 非同期バッチ翻訳を実行
translated_texts, _ = asyncio.run(
translate_texts_openai_async(texts_to_translate, target_lang)
)
# 翻訳結果をオブジェクトに適用
for obj, translated in zip(text_objects, translated_texts):
obj.text = translated
# ファイル保存
prs.save(output_path)
return
このコードでは、collect_texts_from_shape 関数がグループ化されたシェイプやテーブル内のセルを再帰的に探索し、取りこぼしなくテキストを集めています。再帰処理を行わない場合、グループやテーブルの内部にあるテキストが検出されず、翻訳されないまま残ってしまいます。
翻訳部分では、translate_texts_openai_async がテキストを指定したバッチサイズで並列翻訳します。並列化によって速度を向上させていますが、バッチサイズを大きくしすぎると短時間で多数のAPI呼び出しが発生し、レート制限に抵触する可能性があります。実運用ではAPIのレート制限や費用を考慮し、適切なバッチサイズと並列度を設定することが重要です。
PPTX翻訳Webサービス
翻訳サービスの概要
リポジトリにはFastAPIとReactで構築されたWebサービスも用意されています。これはブラウザからPPTXファイルをアップロードするだけで翻訳を行い、ダウンロードできるようにするものです。主な特徴は次の通りです。
- ドラッグ&ドロップによるアップロード:ユーザーはブラウザからPPTXファイルを簡単に送信できる。
- 高品質な翻訳:デフォルトでGPT‑4.1‑miniを使用し、ローカルLLMにも切り替え可能。
- プライバシー保護:翻訳済みファイルはダウンロード後すぐに削除され、未ダウンロードの場合でも10分後に自動削除される。
- 進捗表示とログ:翻訳キューの状況や進捗をリアルタイムで表示し、ログやメトリクスを記録する。
-
Docker対応:
docker-compose upだけで必要なサービスが一括起動できる。
使用方法
Webサービスを試すには Docker と Docker Compose が必要です。以下の手順でセットアップできます。
# リポジトリをクローン
git clone https://github.com/aRaikoFunakami/pptx-slide-translator.git
cd pptx-slide-translator
# 環境変数ファイルを用意
cp .env.example .env
# .envを編集してAPIキーやGrafanaの管理者パスワードを設定する
# サービスを起動
docker-compose up -d
# ブラウザからアクセス
# 翻訳サービス: http://localhost/
# Grafanaダッシュボード: http://localhost:3000/
環境変数ではOpenAIのAPIキーのほか、使用するモデル名や同時処理数などを指定できます。OPENAI_BASEURLにローカルLLMのエンドポイントを設定すれば自前モデルで翻訳が行われます。デフォルトでは同時翻訳数は1件ですが、MAX_CONCURRENT_TRANSLATIONSで変更可能です。
サービス構成図
下図はWebサービスの構成を示した概念図をMermaid記法で表したものです。ブラウザからReactフロントエンドへアップロードしたPPTXファイルはFastAPIバックエンドで受け取り、翻訳エンジン(OpenAI APIやローカルLLM)で処理されます。メトリクスはGrafanaに送られ、フロントエンド経由でユーザーに結果が返されます。
メトリクスの確認方法
Webサービスでは処理ごとにメトリクスを記録しており、Grafanaダッシュボードで可視化できます。
| 指標 | 内容 |
|---|---|
| 総トークン使用量・総コスト | 翻訳済みすべてのリクエストに対するトークン数と料金を集計 |
| 時系列グラフ | 5分単位でトークン使用量やAPI費用をグラフ表示する |
| IPアドレス別統計 | どのクライアントがどれだけ利用しているかを累積で表示 |
ログファイルはlogs/app.logとlogs/metrics.jsonlに保存され、jqやawkを使って統計を集計できます。例えば翻訳成功率を調べる場合は次のように実行します。
# 翻訳成功率
cat logs/metrics.jsonl | jq -r 'select(.status) | .status' | sort | uniq -c
# IPアドレス別の使用状況
cat logs/metrics.jsonl | jq -r 'select(.ip_address) | .ip_address' | sort | uniq -c
翻訳Webサービスを起動するコマンド
バックエンドだけを起動したい場合は backend/main.py を実行し、開発者がフロントエンドをカスタマイズする際は frontend ディレクトリで npm start を実行します。ファイルサイズ制限や同時処理数の変更はソースコードや環境変数で調整できます。
Webサービスの制限事項とセキュリティ
アップロードされたファイルは翻訳ファイルをユーザーがダウンロードした直後に削除されます。ユーザーが翻訳ファイルをダウンロードし忘れた場合でも10分後には自動削除されます。
現状の実装ではユーザー認証がなく、誰でもアクセスできる点に注意が必要です。また、アップロード可能なファイルは最大500MBまでで、翻訳処理は5分でタイムアウトします。本番運用を考える場合、以下の対策を検討するとよいでしょう。
- Grafanaのパスワード変更:デフォルトの管理者パスワードは必ず変更する
- 認証導入:FastAPIのミドルウェアやプロキシでベーシック認証やJWT認証を追加する
- SSL対応:リバースプロキシやLet's EncryptでHTTPSを有効にし、安全な通信を確保する
- ログの保全:ログファイルにはIPアドレスやファイル名が記録されるため、適切なアクセス権限と保管期間の設定が必要
ローカルLLMを翻訳エンジンに指定する
以前のブログ記事では、ローカルで大規模言語モデルを動かし、API互換インターフェース経由で利用する方法を紹介しました(この記事を参照)。本ツールも OPENAI_MODEL と OPENAI_BASEURL を指定するだけでローカルLLMに切り替えられます。例えばOllamaで gemma3:12b モデルを動かす場合は以下のように設定します。
OPENAI_API_KEY=dummy
OPENAI_MODEL=gemma3:12b
OPENAI_BASEURL=http://localhost:11434/v1
OpenAI APIを使用すれば応答は高速ですが、外部サービスにデータを送る必要があります。ローカルLLMに切り替えることで翻訳速度は遅くなるものの、完全に社内ネットワーク内で処理が完結するため、機密情報を扱う際のセキュリティが確保できます。
まとめ
本記事では、PowerPointファイルを安全に翻訳するための自前ツール「pptx‑slide‑translator」とそのWebサービスについて解説しました。CLIツールではスライド構造を保ったまま高速に翻訳でき、Webサービスではドラッグ&ドロップで簡単に利用できます。さらにGrafanaによるメトリクス監視やログ管理、ローカルLLMへの切り替え機能も備えており、セキュリティと利便性を両立しています。
自社やチームの環境に合わせてカスタマイズし、外部サービスに頼らずに安全な翻訳環境を整えてみてください。

