JA ▾

Chinese LLM API翻訳

API キーを取得

更新日

チャットAPIによる中国語↔英語翻訳とローカライズ

チャットモデルでUI文字列やドキュメントを翻訳するのはデモでは簡単ですが、本番環境への展開は困難です。失敗はありふれたものです:プレースホルダの名前変更、用語集の用語が3つの異なる方法で翻訳される、JSON応答が文章に包まれることなど。このガイドでは、OpenAI互換のチャット完了エンドポイントを使用して、各段階にチェックを組み込んだパイプラインとして翻訳を扱います。

1つのプロンプトではなく、段階的に考える

本番環境の翻訳ジョブには4つの段階があります:文字列の準備、モデルへの呼び出し、結果の検証、そして戻してマージです。品質問題のほとんどは検証段階を省略することに起因します。ローカライゼーションエンジニアはプレースホルダリンターを実行せずに翻訳ファイルを納品することはありませんし、翻訳者がモデルである場合にも同じ規律が適用されます。

エンドポイントはhttps://api.chinesellmapi.com/v1/chat/completions、モデルIDはuncensored、ベアラー認証です。これは両方向、英語から中国語へ、および中国語から英語への処理を扱いますが、以下の例では簡体中国語への英語ソース文字列を使用しています。逆方向の場合はシステムプロンプトで方向を切り替えてください。

共有される100,000トークンのコンテキストウィンドウ、デフォルトのmax_tokens値の2,048(長いドキュメントの場合は引き上げ、最大32,000まで)、および8 MBのリクエストボディ制限を念頭に置いてください。これらはいずれもUI文字列には影響しませんが、ドキュメント全体にはすべて関係します。

グロサリーの注入

ブランド名、製品名、法的に敏感な用語には固定された表記が必要です。最も手頃な方法は、システムプロンプトにグロサリーブロックを配置し、ソース用語と必要なターゲットをリストすることです。ルールを明示的に指定します:ソース用語の語尾変化に関係なく、グロサリーをそのまま使用してください。グロサリーは短く保つ必要があります。各行はすべての呼び出しで入力として課金されるためです。数十件のエントリが通常で、数千件は通常ではありません。

import os
from openai import OpenAI

client = OpenAI(base_url="https://api.chinesellmapi.com/v1", api_key=os.environ["API_KEY"])

GLOSSARY = {
    "workspace": "工作区",
    "pull request": "合并请求",
    "seat": "席位",
    "billing cycle": "计费周期",
}

def build_system(glossary, target="Simplified Chinese"):
    rows = "\n".join(f"- {src} => {dst}" for src, dst in glossary.items())
    return (
        f"You are a software localization translator. Translate English UI strings into {target}.\n"
        "Rules:\n"
        "1. Use the glossary below verbatim whenever the source term appears, in any inflection.\n"
        "2. Copy placeholders such as {name}, %s, %d and {{count}} exactly, character for character.\n"
        "3. Copy HTML tags and attributes exactly; translate only the text between tags.\n"
        "4. Output the translation only, with no notes.\n\n"
        "Glossary:\n" + rows
    )

def translate(text):
    r = client.chat.completions.create(
        model="uncensored",
        messages=[
            {"role": "system", "content": build_system(GLOSSARY)},
            {"role": "user", "content": text},
        ],
        temperature=0.2,
        max_tokens=400,
    )
    return r.choices[0].message.content.strip()

print(translate("Invite {name} to the workspace before the next billing cycle."))

用語集が大きい場合は、リクエストごとにフィルタリングしてください:バッチに含まれるソース用語のみを持つエントリを含めます。小文字のテキストに対する単純な部分文字列テストは、最初のバージョンには十分であり、プロンプトを小さく保ちます。温度を0.2付近に設定してください。UIコピーでは創造的な変化はバグです。

また、ターゲット言語のスタイルガイドを事前に決定してください。敬語かカジュアルか、漢字と埋め込まれたラテン語や数字の間にスペースを入れるか、どの句読点セットを使用するかを指定します。これらの決定をシステムプロンプトに含め、すべてのバッチがそれらを継承するようにしてください。

プレースホルダとマークアップの維持

プレースホルダは予測可能な方法で壊れます:モデルが変数名を翻訳する、末尾の%sをドロップする、または中括弧内にスペースを追加するなどです。プロンプトルールはこれらの失敗を減らしますが、完全に排除することはできないため、コードで検証してください。ソースとターゲットからプレースホルダとタグを正規表現で抽出し、ソートされたリストとして比較します。言語間で順序が変更されることは正当なため、シーケンスではなくマルチセットとして比較してください。

import re

PLACEHOLDER = re.compile(r"\{\{?\w+\}?\}|%[sd]|</?[a-zA-Z][^>]*>")

def placeholders_ok(source: str, target: str) -> bool:
    # Same multiset of placeholders and tags, regardless of order.
    return sorted(PLACEHOLDER.findall(source)) == sorted(PLACEHOLDER.findall(target))

src = 'You have <b>{count}</b> unread messages in %s.'
bad = '你在 %s 中有 <b>{数量}</b> 条未读消息。'
good = '你在 %s 中有 <b>{count}</b> 条未读消息。'
print(placeholders_ok(src, bad), placeholders_ok(src, good))   # False True

チェックに失敗した場合は、失敗したペアをプロンプトに引用して1回だけ再試行してください。例えば、プレースホルダーが変更されたため修正が必要であるというユーザーメッセージを送信します。それでも失敗した場合は、ループするのではなく、人間のレビューのために文字列にフラグを付けます。リッチテキストには特に注意が必要です。テキストノードを翻訳し、マークアップを自分で再構築することを推奨します。これにより、タグの破損が構造的に不可能になります。

指示による構造化出力

1つのリクエストで文字列をバッチ処理するには、機械可読な返信が必要です。APIは標準的なチャット完了フィールドを受け付けます。ここではスキーマ強制スイッチではなく明確な指示によって構造化出力が得られるため、プロンプトに契約を記述し、結果を再アラインできるようにIDを含め、防御的に解析します。モデルはJSONをコードフェンスで囲んだり、友好的な文を追加したりすることがあるため、解析前にフェンスを削除し、解析エラーを再試行可能なイベントとして扱います。

import json
import os
import re
from openai import OpenAI

client = OpenAI(base_url="https://api.chinesellmapi.com/v1", api_key=os.environ["API_KEY"])

SYSTEM = (
    "Translate each item from English to Simplified Chinese. "
    "Reply with a JSON object only, no code fences, no commentary. "
    'Shape: {"items": [{"id": <number>, "zh": "<translation>"}]}. '
    "Keep the same ids, keep every placeholder and HTML tag unchanged."
)

def translate_batch(strings):
    payload = [{"id": i, "en": s} for i, s in enumerate(strings)]
    r = client.chat.completions.create(
        model="uncensored",
        messages=[
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
        ],
        temperature=0.2,
        max_tokens=2000,
    )
    raw = r.choices[0].message.content.strip()
    raw = re.sub(r"^```(?:json)?|```$", "", raw, flags=re.M).strip()   # tolerate stray fences
    data = json.loads(raw)
    by_id = {item["id"]: item["zh"] for item in data["items"]}
    return [by_id[i] for i in range(len(strings))]

print(translate_batch(["Save changes", "Delete {count} files?", "Welcome back, <b>{name}</b>"]))

解析後には常に検証してください。IDが一致し、カウントが一致し、各項目が前のセクションのプレースホルダーチェックに合格する必要があります。短いUIテキストの場合、1リクエストあたり50文字列が適切なバッチサイズです。検証パス率が高い間のみ、バッチサイズを増やしてください。

レート制限内のバッチ翻訳

import asyncio
import os
import time
from openai import AsyncOpenAI, APIStatusError

client = AsyncOpenAI(
    base_url="https://api.chinesellmapi.com/v1",
    api_key=os.environ["API_KEY"],
    max_retries=0,
    timeout=90,
)

RATE = 240 / 60            # requests per second, below the 300/min cap
gate = asyncio.Lock()
next_slot = 0.0
slots = asyncio.Semaphore(6)

async def pace():
    global next_slot
    async with gate:
        now = time.monotonic()
        if next_slot > now:
            await asyncio.sleep(next_slot - now)
        next_slot = max(now, next_slot) + 1 / RATE

async def call(batch, attempt=0):
    async with slots:
        await pace()
        try:
            r = await client.chat.completions.create(
                model="uncensored",
                messages=[
                    {"role": "system", "content": "Translate to Simplified Chinese. Keep placeholders unchanged. One line per input line."},
                    {"role": "user", "content": "\n".join(batch)},
                ],
                max_tokens=1500,
                temperature=0.2,
            )
            return r.choices[0].message.content.splitlines()
        except APIStatusError as e:
            if e.status_code in (429, 503) and attempt < 4:
                await asyncio.sleep(2 ** attempt + 1)     # 1s, 3s, 5s, 9s
                return await call(batch, attempt + 1)
            raise

async def run(all_strings, size=20):
    chunks = [all_strings[i:i + size] for i in range(0, len(all_strings), size)]
    results = await asyncio.gather(*(call(c) for c in chunks))
    return [line for part in results for line in part]

if __name__ == "__main__":
    strings = [f"Item {n} was updated by {{user}}" for n in range(60)]
    out = asyncio.run(run(strings))
    print(len(out), out[0])

このバージョンは簡潔さのために行で分割しますが、これは単一行の文字列には適しています。複数行の文字列の場合、前のセクションのJSONバリアントを使用して、文字列内の改行が文字列の境界と誤解されないようにしてください。

逆方向:中国語ソーステキスト

中国語から英語への翻訳には固有の失敗モードがあります。中国語は主語や複数形を自由に省略するため、モデルはそれらを推測する必要があります。文脈を提供してください。「已发送」という文字列は、それがボタンのラベルか、ステータバッジか、トーストかによって、「Sent」、「Has been sent」、「You sent it」のいずれにもなり得ます。解決策は、各文字列の隣にコンテキストフィールドを設け、開発者によって提供され、JSONバッチで渡すことです。その文字列が表示される場所、その最大長、それがラベルか文かを示します。

人名や地名を含む中国語のソースドキュメントについては、声調記号なしの漢語拼音などのローマ字表記規則を事前に指定し、頻出する名前をグロサリーに追加してください。これを行わないと、同じ人物が1つのドキュメント内で2つの異なる綴りで表示され、レビュアーが最初に気づく不整合の原因となります。

自動化されたチェックは構造的なエラーを検出しますが、誤訳は検出しません。軽量な人間のステップを追加してください。各バッチの固定された割合(例えば20文字列に1つ)と、再試行が必要だったすべての文字列をサンプリングし、バイリンガルのレビュアーに送ってください。バッチごとのレビュアーの編集率を追跡してください。それが上昇した場合は、プロンプトを強化するか、バッチサイズを縮小するか、修正されている用語に対してグロサリーエントリを追加してください。

マージ前のサンプリングとレビュー

自動化されたチェックは構造的なエラーを検出しますが、誤訳は検出しません。軽量な人間のステップを追加してください:各バッチの固定された割合(例えば20文字に1回など)、および再試行が必要だったすべての文字列をサンプリングし、バイリンガルのレビュアーに送ってください。バッチごとのレビュアーの編集率を追跡してください。それが上昇する場合、プロンプトを強化するか、バッチサイズを縮小するか、修正されている用語に対して用語集のエントリを追加してください。

ローカライゼーション実行のコスト見積もり

質問と回答

APIは双方向に翻訳できますか?

はい。中国語から英語、英語から中国語の両方が同じチャットエンドポイントで動作します。システムプロンプトで方向を指定してください。

モデルが{name}のようなプレースホルダを変更しないようにするにはどうすればよいですか?

プロンプトでルールを指定し、ソースとターゲットのプレースホルダリストをコードで比較して検証します。不一致の場合は再試行またはフラグを立てます。

JSONモードのスイッチはありますか?

指示による構造化出力を取得します。プロンプトで正確な形状を指定し、不要なコードフェンスを削除してから結果を解析・検証します。

大規模なバッチをどれくらいの速さで実行できますか?

各キーは1分あたり300リクエストまで許可されます。リクエストを均等に配分し、429または503応答ではバックオフしてください。

キーはフォーム 1 つで手に入ります

アカウントを作成し、キーをコピーし、ベースURLを変更します。これでセットアップは完了です。

API キーを取得