WordWarp ができるまで — OPUS-MT をブラウザで動かして翻訳をローカル化する
@xenova/transformers と OPUS-MT (Helsinki-NLP) の量子化版で、 テキストをサーバーに送らずブラウザ内推論で翻訳する Webサービスの設計記録。 言語ペア切替、 段落分割、 IndexedDB キャッシュ、 8ペア対応の UX まで解説します。
WordWarp ができるまで — OPUS-MT をブラウザで動かして翻訳をローカル化する
WordWarp は、テキストを サーバーに送らずに 翻訳する Webサービスです。 @xenova/transformers (transformers.js) + OPUS-MT (Helsinki-NLP) の量子化済み ONNX モデルを WASM 上で動かし、 入力テキストは一切ブラウザ外に出ません。 メディア処理 9本柱に続く、 ラボ 10本目。 voice-scribe / text-pluck の出力を翻訳に流す Process Chain の起点でもあります。
なぜこの形にしたか
翻訳に入力する文章は、 メール下書き、 健康相談、 契約書ドラフト、 仕事の指示書、 機密情報を含むものが多い。 これらが Google翻訳 / DeepL / OpenAI API を経由する設計は、 一度立ち止まって考える価値があります。
OPUS-MT (Helsinki-NLP) は OSS の中で実用範囲に到達した機械翻訳モデル群で、 言語ペアごとに 80MB 程度の量子化版がブラウザで動かせる規模になりました。 transformers.js が WASM + ONNX で OPUS-MT を成立させた今、 「外に出したくない文書専用の翻訳器」 を一つの製品として出すタイミングです。
visual direction
ヴェラム + バーミリオン (vellum & vermilion)。
- 深い羊皮紙ブラウン (
#110a06) を背景に、 赤い朱インク (#dc2626) と古銅 (#fbbf24) のグラデーション - 「ロゼッタストーン」 「写本」 「朱書き校正」 のメタファー
- 既存9本 (mint / amber / violet / cobalt / lime / gold / rose / indigo / emerald) と完全に別の色相 + 温度感 (warm sepia + crimson)
実装の見どころ
1. ペアごとの軽量モデル
OPUS-MT は言語ペアごとにモデルが独立しています。 各 ~80MB。 v1 は 7ペアをスイッチャで切り替える形にしました。
export const PAIRS = {
"ja-en": { modelId: "Xenova/opus-mt-ja-en", ... },
"zh-en": { modelId: "Xenova/opus-mt-zh-en", ... },
"en-zh": { modelId: "Xenova/opus-mt-en-zh", ... },
"ko-en": { modelId: "Xenova/opus-mt-ko-en", ... },
"en-fr": { modelId: "Xenova/opus-mt-en-fr", ... },
"en-de": { modelId: "Xenova/opus-mt-en-de", ... },
"en-es": { modelId: "Xenova/opus-mt-en-es", ... },
}
切替時にモデルが動的ロードされ、 IndexedDB に自動キャッシュ。 2回目以降は即起動します。 「warm: 3/7」 とステータスバーに使用済みペア数を表示しているのは、 ダウンロード済みのモデルを意識させる UX の試みです。
英→日 (en→ja) を v1 から外した理由: Helsinki-NLP の opus-mt-en-jap モデルは学習データの大半が聖書コーパスで、 一般文に対して 「hello」 →「陰府は陰府に及ぶ」 のような意味不明な聖書的出力を返す既知の偏りがあります。 動くフリをして実用にならない結果を返すのは誠実ではないため除外。 NLLB-200 等の品質を担保できる多言語モデル (~400MB) への切替は IMPROVE 候補。
2. 1行ロード
pipeline("translation", modelId) でモデルとタスクパイプラインが返ってくるので、 そのまま await pipe(text) で翻訳できる。 高水準 API のおかげで runner はほぼ薄いラッパーです。
const pipe = await pipeline("translation", meta.modelId, {
quantized: true,
progress_callback: (e) => onProgress({ kind: "download", ratio: e.progress / 100 }),
})
const out = await pipe(text, { max_new_tokens: 512 })
3. 段落分割
OPUS-MT は 1回の推論で 512 トークン程度が上限です。 長文を一気に投げるとサイレントに truncate されてしまう。 WordWarp は入力を空行で段落分割し、 各段落を順に翻訳して結合します。
const paragraphs = text.split(/\n\s*\n/)
const results = []
for (const para of paragraphs) {
results.push(await pipe(para.trim()))
}
return results.join("\n\n")
これで 1万文字程度までは実用範囲。 構造を保ったままの翻訳が出ます。
4. swap ボタン
「日本語 → 英語」 で出た結果を 「英語 → 日本語」 に投げ直すケースは多いので、 ボタン一発でペアと入出力をひっくり返せるようにしました。 ja-en / en-ja、 zh-en / en-zh のように相互ペアがある場合のみ active。
5. 段落数の inference progress
「翻訳中…」 だけだと長文で固まったように見える。 段落数の中で何番目を訳しているかを paragraph 3/12 で見せると、 心理的待ち時間が大幅に短くなります。
苦労したところ
- Turbopack 経由の
await import("@xenova/transformers")がエラー: 開発・本番どちらでも Next.js 16 Turbopack の chunk loader が transformers.js モジュールを再エクスポートする際Object.keys(undefined)で死ぬ事象に遭遇 (Cannot convert undefined or null to object)。 同じパターンで使っている voice-scribe (Whisper) は load 時点で発火しないため気付かれていなかった可能性がある。 対処は CDN ESM を直接await import(url)する こと:https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2/dist/transformers.min.jsを side-load してバンドラを経由しない経路にした。 - en→ja の品質偏り: 上記の通り聖書コーパス問題で v1 から外した。 NLLB-200 を別ルートで足すのが筋。
- ja↔zh の直接ペアがない: Xenova/opus-mt-ja-zh も opus-mt-zh-ja も 401 で取れず。 「日本語 → 中国語」 は将来 「日 → 英 → 中」 の二段経由を runner 内で組む方針。 v1 は対応ペアを正直に表示します。
- 段落分割と空行: 空行で split すると、 入力末尾の改行が空文字段落として残る。 翻訳をスキップしつつ改行構造を保つために空段落をそのまま
""で push し、 join で復元しています。
今後の拡張
- NLLB-200 への切替モード: ~600MB だが 100言語対応。 重さと多言語の trade-off をユーザーが選べる構成
- voice-scribe / text-pluck 連携: 出力テキストを WordWarp に渡す導線
- 用語集 / 翻訳メモリ: ユーザー定義の置換ルールを翻訳前後に適用
- 二段経由翻訳: 直接ペアがない言語間 (ja-zh など) を 「英語経由」 で繋ぐ
- 文書アップロード: pdf / docx / srt から本文を抜き出して一括翻訳
このサービスから言える事
ラボの 10本柱:
- voice-scribe / clip-cast / bg-snap / text-pluck / pdf-anvil / pixel-lift / pic-flip / mind-cell / beam-drop / word-warp
メディア処理 7本 + 生成系 (mind-cell) + 転送系 (beam-drop) に続いて、 今度は変換系 (テキスト→別言語テキスト) もブラウザだけで揃った。 「外部 SaaS に文書を送らないと翻訳できない」 という常識が、 また一段更新時期に来ています。
Next Action
読んだあとに、そのまま使って確かめる。
この開発ログは WordWarp をどう育てているかの記録です。読んだらそのままサービス本体へ戻って、価値を確かめられるようにしています。