利用 PLaMo 3 Translate 构建本地翻译 API 与游戏差分翻译工具
PLaMo 3 Translate でローカル翻訳APIとゲームの差分翻訳ツールを作った
我开发了一个可以在本地运行的PLaMo 3 Translate翻译API。使其能够从其他应用程序或脚本中调用,并制作了仅翻译游戏更新内容的工具,尝试实现了在保留手动修正译文的同时进行更新的功能。
TL;DR
- 我在 llama.cpp 中运行了 PLaMo 3 Translate 的0.8B模型,使其可以作为本地HTTP API调用。
- API和CLI使用Go实现。翻译推理由llama.cpp负责,获取模型后不会将原文或译文发送到外部服务。
- 针对游戏场景,我开发了另一个CLI,仅翻译新增或更改的文本。它保留手动修正的译文和已批准的译文,当原文发生变化时,会报告为需要确认。
- 除了检查占位符外,我还能够在Godot中使用实际字体和UI宽度来确认文字溢出。
はじめに
PFN が PLaMo 3 Translate のモデル重みを公開しました。日本語を含む翻訳を小さなモデルで動かせるなら、手元のツールから呼べる翻訳APIにすると使いやすそうだと思いました。
文章を1つ翻訳するだけなら、専用の画面があれば十分です。ただ、別のアプリに翻訳機能を加えたり、ファイルの文章をまとめて処理したりするときは、HTTPで呼べる方が組み込みやすくなります。日本語を扱うローカルAPIだけでも、使い道はありそうです。
自分はゲームを作っているので、ゲーム開発での使い道も考えてみました。例えば、日本語のメニューや説明文を追加したときに、追加した分だけ英語の訳を作る使い方です。以前に手動で修正した訳は残し、原文が変わったものだけを見直せれば、更新のたびに翻訳ファイル全体を確認する手間を減らせそうです。
そこで、汎用のローカル翻訳APIと、ゲームの差分翻訳を扱うCLIを分けて作りました。APIはゲーム以外からも使えます。実装は Codex と進め、Windows のCPUで実モデルを動かし、Godot のサンプルへ訳文を読み込むところまで試しています。
PLaMo 3 Translate は翻訳用のモデルです。0.8B、2B、8B、31Bの4サイズがあり、日本語と英語を含む53言語に対応しています。文脈や用語集を指定でき、日本語への翻訳では敬体と常体も選べます。
今回は最小の0.8Bを使いました。モデルを llama.cpp で読み込めるGGUF形式のうち、Q4_K_Mという4bit量子化のファイルです。手元で取得したファイルは約573MBでした。これはモデルファイルの大きさで、実行中に使うメモリ量とは別です。
クラウドの翻訳サービスと違い、初回にモデルと実行環境を取得した後は、自分のPCで翻訳できます。翻訳のたびに外部APIへ課金する構成ではありませんが、推論に使うCPUやメモリは手元のものです。
ソースコードはMITライセンスで公開しています。モデルは PLaMo Community License で、個人の非商用利用は登録不要で無償です。商用利用やモデル・訳文の公開条件はREADMEにまとめています(Built with PLaMo)。
翻訳APIの構成
APIの名前は local-jp-translate、実行ファイルは jptranslate です。HTTPを受ける部分と、モデルの推論を行う部分を別のプロセスにしました。

アプリからは同じ翻訳APIを呼び、推論はPC内の llama-server で実行する。
llama-server 是包含在 llama.cpp 中的推理服务器。jptranslate 可以作为子进程启动,也可以连接到已在本地运行的实例。两者均在回环地址上监听。
API 需要指定原文、源语言和目标翻译语言。如有需要,还可提供术语表和上下文,以及日语文体要求。服务器会将其转换为模型用于翻译的格式,并以 JSON 形式返回译文。
API 和 CLI 的实现选择了 Go 语言。因为可以使用标准库编写 HTTP、JSON、文件获取和子进程管理等功能,且便于分发各操作系统的可执行文件。这样用户无需在其运行环境中安装 Go 或 Python。
由于大部分翻译时间由 llama.cpp 承担,仅更改 API 的语言并不会使推理变快。本次优先降低了分发和启动的成本。面向 Windows 的 API ZIP 包约为 3.3MB,llama.cpp 和模型需单独获取。
使用 API
源代码及各操作系统的分发文件已汇总至以下仓库。
在Windows中解压ZIP文件并运行安装程序。
.\scripts\setup.ps1 -AcknowledgeModelLicense
获取适用于CPU的llama.cpp和模型,核对文件哈希值。完成后使用显示出的命令启动。如果从解压后的文件夹执行,格式如下。
.\jptranslate.exe serve `
--llama-server .\runtime\llama-b12144-cpu\llama-server.exe `
--model .\models\plamo-3-translate-0.8b-Q4_K_M.gguf
默认的监听地址是http://127.0.0.1:8787。可以从另一个终端向/translate发送文本。
$body = @{
text = "Please save your changes."
source = "en"
target = "ja"
style = "polite"
} | ConvertTo-Json
Invoke-RestMethod -Method Post `
-Uri http://127.0.0.1:8787/translate `
-ContentType 'application/json' `
-Body ([Text.Encoding]::UTF8.GetBytes($body))
来自PLaMo的译文如下。已从响应JSON中提取出译文部分。
{"translated_text":"変更内容を保存してください。"}
这段短文在Windows CPU上加载模型后,约197毫秒返回结果。若采用支持GPU的配置,有可能进一步提速。
也可以通过CLI仅翻译单句。虽然也准备了/v1/chat/completions,但它是专用于翻译且非流式的兼容端点,并非通用聊天API。
通过浏览器使用时,需明确指定允许连接的 Origin。即使是在本地运行,也不应允许打开的网页随意执行翻译,因此加入了 Host 和 Origin 的检查。
仅翻译游戏的更新部分
面向游戏的 CLI 是 game-l10n-updater,可执行文件是 gamel10n。原文数据及按语言区分的译文以 JSON 格式存储,仅将需要翻译的文本发送至前述 API。
启动 API 后,在解压分发文件的文件夹中创建配置文件和空的原文文件。
.\gamel10n.exe init -source-lang ja -languages en
在生成的 source/catalog.json 中写入原文。不仅要包含要显示的文本,还要为其提供用于标识该文本的键以及使用位置。例如,标题画面的问候语可以这样编写。
{
"schema_version": 1,
"entries": [
{
"key": "ui.greet",
"text": "こんにちは、{name}さん!",
"context": "title screen greeting",
"max_length": 24
}
]
}
context 是翻译时参考的上下文,max_length 是字符数的参考值。{name} 是游戏运行时替换为名称的部分,因此在翻译后仍需保留。
CLI 会将前一次的原文与译文哈希值记录到状态文件中。下次更新时,如果原文相同则不调用翻译,仅处理新增或修改过的文本。流程是指定配置文件以确认计划,然后进行更新。
.\gamel10n.exe plan -config .\gamel10n.json
.\gamel10n.exe sync -config .\gamel10n.json
我们确保人工修正的译文不会被自动翻译覆盖。通过比较前一次的译文与当前文件,若存在手动更改则予以保留。此后即使日文原文也发生了变化,也会将译文保留并报告为 needs_review。已批准和锁定的译文同样会被保留。
我们也尝试了日文菜单和说明文字。以下是 PLaMo 3 Translate 0.8B 返回的英文译文:
| 日文原文 | 模型返回的英文译文 |
|---|---|
| 保存 | 保存 |
| 冒险菜单 | 冒险菜单 |
| 在继续冒险之前,请保存当前的进度。 | 请在继续冒险之前保存当前进度。 |
| 欢迎再次光临。 | 请再次光临。 |
| 保存之前的冒险并返回标题画面 | 保存你之前的冒险并返回标题画面 |
“保存”以及说明保存进度的文案,翻译得符合意图。另一方面,“请再次光临”如果是希望玩家再玩一次游戏的场景,用“Come back and play again.”可能更易懂。仅看译文的话,也可能被理解为希望玩家重返某个地点。
较长的按钮文案“Save your previous adventures”作为保存游戏进度的操作略显不自然。在此用途下,建议修改为“Save your progress and return to the title screen”。此处提出的修正方案与模型的输出是分开的。我觉得虽然简短的操作名称易于使用,但无法自动确定其在游戏中的含义和语气。
因此,在确认译文并进行修正后,我仍继续推进更新。当我手动更改由实际模型生成的译文,并进一步更改日文原文以执行更新时,已修正的译文得以保留,并被报告为需确认状态。另一方面,未对自动译文进行更改的文本,在原文更改时会重新翻译。本次也仅翻译了新增部分和更改了原文的部分,未更改的文本未被重新翻译。
从原文中删除的键,默认情况下也不会从译文中消失,而是会进行报告。只有在希望反映删除时,才需要显式指定。
确认译文和画面
游戏文本中会混入用于嵌入名字的 {player_name}、显示数值的 %d、添加颜色的 [color=red] 等标记。即使作为文章读起来自然,缺少这些标记就无法在游戏中使用。
因此,我们会比较翻译前后占位符和标签的名称与数量。对于缺失或变成其他内容的译文,不予采用。至于含义和语气是否符合游戏,则需要由人来阅读确认。
我还想确认另一点:它是否能完整显示在画面内。在日语中即使按钮名称很短,翻译成英语后横向宽度也会变长。仅凭字符数无法判断字体及字符组合所造成的宽度差异。
这次添加了面向 Godot 的插件。想要确认的 Label、普通的 Button、RichTextLabel 指定翻译键,等布局确定后,再使用实际字体和显示区域进行测量。

加载了实际模型的英文翻译和手动修正后的翻译的示例。故意缩小的按钮被检测到了。
在此示例中,按钮内可用宽度为172px,而英文翻译后的字符串长度为467px。将Godot侧的报告导入CLI后,可以了解需要重新审视哪些键。可以在同一更新作业中确认仅通过JSON检查无法发现的屏幕侧问题。
它不会自动遍历游戏内的所有画面,而是针对指定的场景和UI部件。我们将其设计为按以下顺序使用的工具:首先更新翻译,确认格式,然后在实际画面上查看是否容纳得下。
结语
由于实现了能够通过HTTP调用小型翻译模型,因此可以从其他工具中使用。我们将API与游戏用CLI分离,因此既可以将翻译部分集成到现有的脚本中,也可以作为游戏的更新处理来使用。
在游戏应用中,除了生成译文外,还需要记录自上次以来发生了哪些变化、人工进行了哪些修改。通过将这部分逻辑与模型的响应分开,以代码形式处理,从而能够在保持未变更译文和已修正译文不变的同时进行更新。
本次我在本地尝试了菜单和说明文字的英译,以及在 Godot 示例中的显示确认。今后打算一边用于游戏的更新,一边打造便于确认和修正译文工具。
来源:marvelousu · LLM游戏功能与开发实践 · zenn.dev

