跳到正文
marvelousu · LLM游戏功能与开发实践· 来源· Tom·· 3 小时前AI 评分72

利用 PLaMo 3 Translate 构建本地翻译 API 与游戏差分翻译工具

PLaMo 3 Translate でローカル翻訳APIとゲームの差分翻訳ツールを作った

正文 · AI 翻译

我开发了一个可以在本地运行的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の構成。アプリとゲーム用CLIからjptranslateを呼び、llama-serverがPLaMoで翻訳する
アプリからは同じ翻訳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

源代码及各操作系统的分发文件已汇总至以下仓库。

GitHub: marvelousu/local-jp-translate

在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。

GitHub: marvelousu/game-l10n-updater

启动 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 指定翻译键,等布局确定后,再使用实际字体和显示区域进行测量。

翻訳した文章をGodotへ読み込み、狭いボタンの文字あふれを検出した画面
加载了实际模型的英文翻译和手动修正后的翻译的示例。故意缩小的按钮被检测到了。

在此示例中,按钮内可用宽度为172px,而英文翻译后的字符串长度为467px。将Godot侧的报告导入CLI后,可以了解需要重新审视哪些键。可以在同一更新作业中确认仅通过JSON检查无法发现的屏幕侧问题。

它不会自动遍历游戏内的所有画面,而是针对指定的场景和UI部件。我们将其设计为按以下顺序使用的工具:首先更新翻译,确认格式,然后在实际画面上查看是否容纳得下。

结语

由于实现了能够通过HTTP调用小型翻译模型,因此可以从其他工具中使用。我们将API与游戏用CLI分离,因此既可以将翻译部分集成到现有的脚本中,也可以作为游戏的更新处理来使用。

在游戏应用中,除了生成译文外,还需要记录自上次以来发生了哪些变化、人工进行了哪些修改。通过将这部分逻辑与模型的响应分开,以代码形式处理,从而能够在保持未变更译文和已修正译文不变的同时进行更新。

本次我在本地尝试了菜单和说明文字的英译,以及在 Godot 示例中的显示确认。今后打算一边用于游戏的更新,一边打造便于确认和修正译文工具。

来源:marvelousu · LLM游戏功能与开发实践 · zenn.dev