メインコンテンツまでスキップ

一括翻訳

ドキュメント一式の翻訳には、/v1/chat/completionsへの数百回の呼び出しが必要です。呼び出し自体は簡単です。このページでは、結果が実用に耐えるかどうかを左右する3つの点、すなわち、利用前にコストを把握すること、Markdownの構造が維持されていることを確認すること、そしてモデルが実際に何かを翻訳したことを確認することについて説明します。

ここに記載した数値はすべて、2026-09-23にこのサイト自身のドキュメントツリー(17ファイル、134,713文字)を対象として測定したものです。完全なスクリプトは実際に私たちが使用しているもので、ダウンロードできます:translate-docs.py(MIT形式、標準ライブラリ以外の依存関係なし)。

コストと実際の結果​

2026-09-23に一式すべてを実行しました。これは見積もりではなく、実際の出力です:

ソース17ファイル / 134,713文字
モデルgpt-4o-mini
測定レート2,500文字あたり18 AP
一式全体、1言語約970 AP(約US$0.10)
書き込まれたファイル15
チェックで拒否されたファイル2

17ファイル中2ファイルが破損した状態で返され、書き込まれませんでした。どちらも完全に正常に見えました。先頭も末尾も正しく、finish_reason: stopで、いかなる種類のエラーもありませんでした。このページが呼び出しそのものより検証を中心に説明しているのは、そのためです。

呼び出し​

1ファイルにつき1リクエストです。ストリーミングも分割も行いません。ドキュメントページはコンテキストウィンドウに収まり、分割すると境界部分で見出しレベルや表の列が失われるためです。

curl -X POST https://api.aicu.ai/v1/chat/completions \
-H "Authorization: Bearer aicu_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"temperature": 0,
"max_tokens": 8000,
"messages": [
{"role": "system", "content": "You translate technical API documentation into Korean. Translate prose only. Reproduce byte-for-byte: YAML frontmatter, anything inside ``` fences, URLs, file paths, HTTP methods and status codes, and API names in backticks. Keep the Markdown structure identical. Output the translated document only."},
{"role": "user", "content": "<the whole .md file>"}
]
}'

ここでは、temperature: 0が通常の用途以上に重要です。同じ入力から同じ出力を得られるようにする必要があります。そうすれば、ジョブを再実行しても同義語の違いだけで差分が生じることはありません。

呼び出しにかかったAPはレスポンスに記載されるため、事後に見積もる必要はありません:

X-AICU-AP-Cost: 18
X-AICU-AP-Balance: 6263161

構造が維持されていることを確認する​

不良な翻訳は、文章の悪さとして現れるより先に、壊れたMarkdownとして現れます。フェンスが1つ欠けるとページの残り全体が飲み込まれ、区切り行を失った表はパイプ文字が並ぶ段落としてレンダリングされます。

そのため、出力を入力と機械的に比較し、失敗した場合はファイルへの書き込みを拒否します:

def structure_check(src: str, out: str) -> list[str]:
bad = []
if src.count("```") != out.count("```"):
bad.append("code fences")
if src.count("|---") != out.count("|---"):
bad.append("tables")
for level in ("\n## ", "\n### "):
if src.count(level) != out.count(level):
bad.append(f"headings {level.strip()}")
if src.startswith("---") and not out.startswith("---"):
bad.append("frontmatter")
for url in set(re.findall(r"https?://[^\s)\"'`]+", src)):
if url not in out:
bad.append(f"lost URL {url}")
break
if out.strip().startswith("```"):
bad.append("whole document wrapped in a fence")
return bad

失敗したファイルは報告され、書き込まれないまま残ります。ページが翻訳されないことは復旧できますが、壊れたページを公開してしまうと復旧できません。

実際に検出されたもの​

失敗の原因は切り捨てではありませんでした。どちらのドキュメントも最終行まで翻訳され、正常に停止していました。モデルが途中の要素を単純に削除していたのです。

表全体が消失。 pricing.mdは、表1つ分の172行が少ない状態で返されました。最後の「見積もりの算出方法」の表がなくなっていましたが、その後の段落は翻訳され、残っていました。区切り行は16 → 14、表の行は42 → 38でした。

リンクがプレーンテキストに変換。 opencode.mdでは、すべての表、すべての行、すべての見出しが維持されていました。ある行の中にあった次の内容が、

| API key | `aicu_live_xxx` (issue one in the [dashboard](https://api.aicu.ai/dashboard/keys) — **required**) |

次のようになって返されました:

| API 키 | `aicu_live_xxx` (대시보드에서 발급 — **필수**) |

文面は依然として、ダッシュボードからキーを取得するよう読者に伝えています。しかし、ダッシュボードへのリンクが消えています。行数、見出し数、フェンス数、長さはすべて変わっていません。これを検出できるのはURL維持チェックだけです。

これは、賢いチェックを1つ実行するのではなく、安価なチェックを複数実行すべき理由を示しています。このサイトで発生した3つの失敗は、それぞれ異なるチェックによって検出され、複数の失敗を検出できた単一のチェックはありませんでした。

私たち自身のチェックにも誤りがあった​

これは同じ教訓のもう一面なので、触れておく価値があります。最初の実行では、3つ目のファイルがlost URL https://api.aicu.ai/dashboard/keys.で失敗しました。末尾のピリオドに注目してください。ソースではURLが文末にあり、正規表現が句点をURLの一部として取り込んでいました。翻訳文は独自の句読点で文を終えていたため、そのリテラル文字列が存在せず、完全に問題のないファイルが拒否されました。

url = url.rstrip(".,;:!?、。)」") # the sentence's punctuation is not part of the URL

正常な出力を不合格にするチェックは無効化します。 誤検出がゼロになるよう調整し、見逃しが発生することは受け入れてください。ここで紹介したチェックは、15件の正常と判明している翻訳に対して検証された後に、出力を拒否する目的で信頼されるようになりました。

チェックすべきでないもの​

長さです。これは誰もが最初に思いつく方法ですが、Markdownでは機能しません。15件の正常な翻訳では、行数の比率が0.934から1.000の範囲でした。これは、英語でハードラップされた段落が、対象言語では1行に結合されるためです。表が欠落したファイルは0.820で、検出可能でした。しかし、リンクが削除されたファイルは0.974で、通常の範囲に十分収まっていました。後者を検出できるほど厳しいしきい値にすると、複数の正常なファイルが拒否されます。

代わりに、構造要素を数えてください。15件の正常な翻訳すべてで、表の行、箇条書き、見出しが完全に一致していました。そのため、少しでも差があれば実際のシグナルになります:

for label, count in (("table rows", lambda t: sum(1 for l in t.splitlines() if l.lstrip().startswith("|"))),
("bullets", lambda t: sum(1 for l in t.splitlines() if re.match(r"\s*[-*] ", l))),
("headings", lambda t: sum(1 for l in t.splitlines() if l.startswith("#")))):
if count(src) != count(out):
bad.append(f"{label} {count(src)}→{count(out)}")

失敗ではないもの​

モデルはコードブロック内の人間向け言語の文字列(プロンプト値やコメント)も翻訳する一方で、識別子、パラメータ名、モデル名は変更しません:

- "prompt": "a red apple on a white table, product photo",
+ "prompt": "하얀 테이블 위에 빨간 사과, 제품 사진",

システムプロンプトは、フェンスで囲まれたブロックをバイト単位で同一にするよう求めているため、厳密に言えばこれは指示違反です。しかし同時に、韓国語の読者がサンプルに求めるものでもあります。私たちはこれを許容し、実際に破損につながるもの、つまりフェンス数と、"character": "SakiNoir"およびgpt-4o-miniが変更されずに返されることを確認します。どちらを選ぶかを決め、それに合うチェックを作成してください。レンダリングされたページで後から発見するのではなく、事前に決めることが重要です。

実際に翻訳されたことを確認する​

これは省略しがちなチェックであり、私たちが最も高い代償を払って学んだものでもあります。

英語 → 韓国語で4つのモデルをベンチマークしたところ、他のモデルが約1.85 / 1.55 / 0.27を記録したのに対し、gpt-5-miniは1.34 / 0.49 / 0.69でした。これは文章の質が悪かったように見えます。しかし、そうではありませんでした。出力にはハングル文字が1文字も含まれておらず、2,500文字の入力に対して2,499文字でした。英語のソースを変更せず、そのまま返していたのです。

その出力はstructure_checkを完全に通過します。ドキュメントそのものが原文なので、構造が同一なのは当然です。これを書き込むと、i18n/ko/は韓国語のファイル名を付けた英語で埋め尽くされます。後続処理は何も異常を報告しません。

2つの安価なテストでこれを検出できます:

def translated_check(src: str, out: str, locale: str) -> list[str]:
bad = []
# 1. Is the prose simply the source? (language-independent)
a, b = strip_code_and_urls(src), strip_code_and_urls(out)
same = sum(1 for x, y in zip(a, b) if x == y) / max(len(a), len(b))
if same > 0.9:
bad.append(f"source returned unchanged ({same:.0%} identical)")

# 2. Does the target script actually appear?
rng = TARGET_SCRIPT.get(locale) # ko: Hangul, zh: Han, ja: kana
if rng and not bad:
lo, hi = rng
n = sum(1 for c in out if lo <= ord(c) <= hi)
if n / max(len(out), 1) < 0.03:
bad.append(f"almost no target-script characters ({n})")
return bad

1つ目のテストは、どの言語ペアでも機能します。2つ目は、対象言語が独自の文字体系を持つ場合にのみ機能します。フランス語、スペイン語、ポルトガル語はソースとラテン文字を共有しているため、その場合に利用できるのは同一性テストだけです。それで十分です。翻訳に失敗したモデルは入力をそのまま返すことで失敗し、その繰り返しをテスト1が検出します。

この教訓の一般形は、言語モデルに対して実行するあらゆるバッチジョブに当てはまるため、明確に述べておく価値があります:

明確に失敗するジョブは安く済みます。誤って成功したように見えるジョブは高くつきます。 成功したように見える出力の検証に労力を割いてください。

変更されていないファイルに二度支払わない​

各ソースファイルのSHA-256を翻訳先の言語とともに保存し、一致するものはすべてスキップします:

digest = hashlib.sha256(src.encode()).hexdigest()
if manifest.get(str(path)) == digest and dest.exists():
continue # unchanged since the last run — skip

変更のないツリーを2回目に実行した場合のコストは0 APです。これにより、翻訳は一度きりのイベントではなく習慣になります。3ページを編集し、再実行し、3ページ分だけ支払います。

マニフェストはバージョン管理に含めてください。これは、どの翻訳がソースのどのリビジョンに対応しているかを示す記録です。6か月後に実際に知りたくなるのは、この情報です。

モデルの選択​

自分のドキュメントで測定してください。4つのモデルに同じ4つのファイルを与え、英語 → 韓国語の結果をJevで採点しました:

モデル忠実さ自然さ未翻訳AP / 2,500文字秒
gpt-4o-mini1.831.550.26186.9
gpt-4o1.841.560.302789.9
gpt-5.6-sol1.871.660.258499.2
gpt-5-mini1.340.490.69469.6

gpt-5.6-solは3つの評価軸すべてで最高です。同時に、差が+0.04、+0.11、−0.01であるのに対し、gpt-4o-miniの47倍の価格です。一式全体で970 APに対して45,700 APです。

gpt-5-miniの行は、上のセクションで説明したものです。これらは文章の評価ではなく、ソースをそのまま返した結果の評価です。

私たちはデフォルトでgpt-4o-miniを使用し、表現が重要な個別ページではgpt-5.6-solを使用します。追加コストで得られる違いは実在しますが小さく、正確さより文体に現れます。英語で省略された主語を補う、ダッシュでつながれた長い文を2つに分ける、自然な助詞を選ぶ、といった違いです。

出力の採点​

上記の3つのスコアは/v1/jevから取得しています。これは、後から解析する必要がある文章を返すのではなく、型付きの質問に回答します:

{
"state": {"source": "...", "translation": "..."},
"questions": {
"faithful": {
"type": "score",
"instructions": "Does the translation preserve the meaning of the source, including technical details?",
"criteria": ["Meaning is distorted", "Mostly faithful", "Faithful throughout"]
},
"natural": {
"type": "score",
"instructions": "Does the translation read naturally to a developer who speaks that language?",
"criteria": ["Stilted", "Readable but awkward", "Natural"]
},
"untranslated": {
"type": "noul",
"instructions": "Is there source-language prose left untranslated that should have been translated? (Code, URLs and parameter names are correctly left as-is.)"
}
}
}

1ファイルあたり約8 APなので、一式全体の採点にかかるコストは、1ページを翻訳するコストより少なくなります。

これはモデルの比較や外れ値の発見に使用し、ゲートとしては使用しないでください。機械的なチェックがファイルを書き込むかどうかを決定し、スコアは後で確認するための数値です。ソースを変更せずに返すモデルは、別のモデルに問題がないか尋ねるのではなく、4行の文字カウントで検出します。

実行方法​

**translate-docs.py**をダウンロードしてください。1ファイルで、標準ライブラリのみを使用します。

export AICU_API_KEY=aicu_live_... # llm scope

python3 scripts/translate-docs.py --to ko # dry-run: cost estimate only
python3 scripts/translate-docs.py --to ko --apply # translate and write
python3 scripts/translate-docs.py --to ko --apply --judge # ... and score each file
python3 scripts/translate-docs.py --to ko --apply --only pricing.md
python3 scripts/translate-docs.py --to ko --apply --model gpt-5.6-sol --force

デフォルトはドライランです。ファイル数、文字数、推定APを表示し、何も呼び出さずに終了します。有料APIに対して作成するすべてのバッチスクリプトは、この方法で開始すべきです。

出力先はi18n/<locale>/docusaurus-plugin-content-docs/current/で、Docusaurusが期待する場所です。パスを調整すれば、どのMarkdownツリーでも同じジョブを実行できます。

あらゆるバッチジョブに応用できること​

翻訳は一例にすぎず、その構造は一般化できます。

  1. 利用前に見積もる。 デフォルトはドライランとし、実行には--applyを使用します。
  2. 出力を機械的に検証し、失敗したものは書き込まない。 間違って実行するより、実行しない方がよい結果になります。
  3. エラーだけでなく、偽の成功もテストする。 すべてのチェックを通過しても間違っている出力こそ、本番環境に到達するものです。
  4. 作業を増分化する。 入力をハッシュ化し、変更がなければ無料にします。
  5. 自分のデータで測定してモデルを選ぶ。 すべての評価軸で最高だったモデルは、丸め誤差ほどの差に対して47倍の価格でした。

関連項目​

  • Chat (LLM) API — モデル、ヘッダー、制限
  • Jev — 文章を解析せずに行う型付き採点
  • 料金 — APの課金方法とレスポンスヘッダーからの確認方法