CosenseのTranslation modeの設計と実装 (JSConf 2024)
CosenseのTranslation modeに実装において、
いかにユーザー体験を良くしつつリソースの消費を抑えるか
ということを紹介したいと思います
よろしくお願いします
株式会社Helpfeel 開発部のohnishiakiraohnishiakira.iconです
shokai.iconbalar.iconと一緒にCosenseの開発をしています Cosenseとは
https://nota.gyazo.com/0ebca5f67abb77253f2ed29f64f7b821
リアルタイムで共同編集できるwiki
みんなでチャットのように議論してもいい
ノートを取るに使ってもいい
リンクが特徴
https://nota.gyazo.com/be5dfcdf735b7625b983bfb6cefc37da
単語をリンク([リンク])にすると、リンクを介して関連するページがつながっていく
https://nota.gyazo.com/3e1eb3400f6f0eb9d0e68ee61dc75eda
Page MenuからStart translationする
https://nota.gyazo.com/c3da67f6ef441d370040d046528baed9
これが
https://nota.gyazo.com/359b540ed6f5dc17c2a9f0d8cc48c9f2
こうなる
https://nota.gyazo.com/c66dd1faa590581e3acb3ace5a0fbf84
Translation mode
エディタ右側のメニューからStart translationするだけ
Cosenseにログインしていれば誰でも使える
日本語、英語以外の言語でも翻訳できる
言語を超えて思考力MAXで対話する
テキストを書く、テキストを翻訳する、がエディタ内で完結
テキストの下に翻訳が表示されるので、
相手の言語が分からなくても、自分の言語に翻訳して表示できる
自分の言語で書いていても、相手の言語で翻訳されて表示される
お互い相手の言語が分からなくても、それぞれ自分の言語で対話できる
簡潔で分かりやすい文章を書こう
句読点なしで書かれた超長い文章は日本語でも読みにくい
翻訳しても読みにくい
ハイコンテキストな文ではなく、平易な文を書く
ローコンテキストな文はちゃんと翻訳してくれる
箇条書き
シンプルで分かりやすい文章を書きやすい
一文が短くなって、主語や目的が明確になる
分かりやすい文章は、言語が異なる相手にも伝りやすい
Translation modeの工夫
人間が読んでいるところを翻訳する
画面内に表示されたテキストだけ翻訳している
高速でスクロールして飛ばした箇所は翻訳しない
実際の様子
https://nota.gyazo.com/1fbed0328288cb38bb98a325c138a0dd
高速でスクロールしている間に画面から通り過ぎていった行は翻訳されず、
速度がゆっくりになってくると翻訳され始める
人間が読んでいるところを翻訳している
code:intersection-translate.js
const observer = new IntersectionObserver(
if (entry?.isIntersecting) {
enableTimerRef.current = setTimeout(
() => setEnableTranslate(true),
200
);
} else {
if (enableTimerRef.current) {
clearTimeout(enableTimerRef.current);
enableTimerRef.current = null;
}
setEnableTranslate(false);
}
},
{
rootMargin: '0px 0px 200px 0px'
}
);
ポイント
setTimeout()がポイント
setEnableTranslate(true)すると後続の処理で翻訳リクエストが送られるが、
200ms以内に行が画面外に行ったらclearTimeout()でキャンセルする
高速でスクロールして飛ばしたところは翻訳リクエストが送られない
全部翻訳しないの?
1ページ丸ごと一気に翻訳しようとするとどうなるか
短時間で大量にリクエストが送られてしまう
ブラウザ側も詰まるし、
サーバ側もリクエストが大量に来てセルフDDoSになる
人間が読んでいるところだけを優先して翻訳することによって
人間は快適に読めるし、
APIリクエストの数も減らせる
Cosenseはリアルタイムで共同編集できるwiki
同じページで、
同時に、
複数の人たちが、
色々なところを編集する
複数人が同じ箇所を同時に翻訳することもある
100人がTranslation modeを使うとどうなるか?
100回リクエストが送られる
code:mmd
sequenceDiagram
participant a as User x 100
participant b as Cosense
participant c as DeepL
a->>b: こんにちは x 100
b->>c: こんにちは x 100
c->>b: Hello x 100
b->>a: Hello x 100
DeepL APIは従量課金制
リクエストする文字数が増えれば増えるほど、コストがかかる
同じリクエストが100回DeepL APIに送られると、その分コストも嵩む
もったいない
同時編集でもキャッシュできる機構
同じリクエストは最初の1つだけDeepLへリクエストする
2つ目以降のリクエストはキャッシュが温まるのを待つ
code:deepl-api-cache.js
const schema = mongoose.Schema(
{
text: { type: String, required: true }, // 元テキスト
targetLang: { type: String, required: true }, // 翻訳先の言語
responseData: { type: Object, required: true }, // DeepL APIのレスポンス
waitingResponse: { type: Boolean, required: true }
}
);
DeepL APIのレスポンスをキャッシュするモデル
.waitResponse: true
「今からこのキャッシュを温めるから、しばらく待ってね」というフラグ
他のユーザーからのリクエストは、
この値がtrueならロックして、しばらく待ってからキャッシュを取り直す
キャッシュを取得できたらレスポンスを返す
しばらく待ってもキャッシュが取れなかったら自分でDeepLに取りに行く
code:mmd
sequenceDiagram
participant u1 as Aさん
participant u2 as Bさん
participant b as Cosense
participant c as DeepL
u1->>b: こんにちは
Note over b: DeepLへリクエストする(.waitingResponse=true)
b->>c: こんにちは
u2->>b: こんにちは
Note over b: キャッシュが温まるのを待つ
c->>b: hello
Note over b: 翻訳結果をキャッシュに保存 (.waitingResponse=false)
b->>u1: Hello
b->>u2: Hello
Aさん側
Aさん:「こんにちは」を翻訳して
Cosense:
キャッシュがない
DeepL APIへリクエストする
キャッシュオブジェクトを作成して.waitingResponse = trueを設定する
DeepL APIからレスポンスを受け取る
キャッシュオブジェクトに.waitingResponse = falseを設定する
Aさんにレスポンス
Bさん側
Bさん:「こんにちは」を翻訳して
Cosense:
キャッシュ取得中、しばらく待つ(.waitingResponseがtrue)
キャッシュが見つかった(.waitingResponseがfalse)
Bさんにレスポンス
まだキャッシュ取得中(.waitingResponseがtrue)
またしばらく待つ
待ってもキャッシュがない
自分で取りに行く
キャッシュ機構を自前で持っている理由
APIリクエストをキーにして、レスポンスをキャッシュしてくれるproxy
複数の人が同時に編集するCosenseでは、キャッシュが効く前にリクエストが飛んでしまう
2人以上のユーザーが同時に翻訳リクエストを投げて、キャッシュされる前にDeepLへリクエストが飛ぶ
100人で同時編集していたら100回同じリクエストがDeepLへ飛ぶ
もったいない
自前でロック機構を実装して、DeepLへのリクエストを減らしている
あえてコストをかける
ページ内に3つ以上の異なる言語があっても、自分の母国語に翻訳できる
https://nota.gyazo.com/1f9aa0a95379ac06a5dafbf0b9c0325f
英語もドイツ語も日本語に翻訳できる
どうなっているのか
2回リクエストして、リクエストの文字列と異なる方を返している
同じページ内に複数の異なる言語があっても、うまく母国語に翻訳できるようになっている
Cosenseの設定
https://nota.gyazo.com/d10bbfb0833a64aa84d1f8744b9caa0b
自分の母国語(Your native language)と、
何語に翻訳したいか(Translate to)
を設定できる
この2つがtarget_lang(後述)に指定される
code:mmd
sequenceDiagram
participant a as User
participant b as Cosense
participant c as DeepL
a->>b: こんにちは
b->>c: こんにちは (target_lang=ja)
b->>c: こんにちは (target_lang=en)
c->>b: こんにちは (target_lang=ja)
c->>b: Hello (target_lang=en)
note over b:DeepLからのレスポンスのうち、リクエストと異なる方を返す
b->>a: Hello
「こんにちは」という日本語を翻訳する時、CosenseからDeepLへ2つのリクエストが送られる
言語設定の「Your native language」と「Translate to」それぞれに翻訳しようと試みる
Your native language = ja, Translate to = en なら
「こんにちは」(target_lang=ja)→「こんにちは」
「こんにちは」(target_lang=en)→「hello」
レスポンスのうち、原文と異なるものをユーザーへ返す
結果比較翻訳の理由
ページ本文に特定の言語の文章だけが書かれるわけではない
色々な言語のユーザーが、同時に同じページで議論することもある
その文章が何語で書かれているかは自動判定に任せる
自由に母国語で書いて自由に母国語で読む
DeepL APIに同じ文章を2回翻訳リクエストすることにはなるが、
自分の言語で書けば相手には相手の言語で翻訳して表示されるし、
相手の言語も自分の言語に翻訳して表示される
というユーザー体験上のメリットがある
ページ本文以外の翻訳
ユーザーのコンテンツが表示される部分はだいたい翻訳できる
ページリスト画面、関連ページリスト、全文検索結果、infobox、文芸的データベース
(CosenseそのもののUIの文言は、人手できちんと用語を整理して翻訳する予定)
残りはQuickSearch
QuickSearchのUIは狭い
https://nota.gyazo.com/d2e6b6536ba897f6fb9077c42736be54
このポップアップの中だけ
どう翻訳テキストを出すか
本文と同じようにテキストの下に翻訳を表示する?
スクロールする度にガタガタして使いにくくなるだろう
ユーザー体験が悪い
QuickSearchの辞書を全部翻訳する
ここに限ってはあらかじめ翻訳しておいてはどうか?
DeepL APIで翻訳する
これを辞書の全候補分繰り返す
DeepL APIのQuotaを使い切ってしまった...
コストを試算
ローカル開発環境にコピーした社内のプロジェクト
ページ数74074
QuickSearchの候補数114,546件
文字数 2,183,795
料金で計算
1,000,000文字あたり2,500円(+月額630円)
(2,183,795 / 1,000,000) * 2,500 = 5,459.4875円
これが1プロジェクトにかかるコスト
結構かかる
ユーザーが見ている全プロジェクトで行うのは現実的ではない
断念...
QuickSearchの辞書を全部翻訳するのはコストがかかる
全部翻訳すればいけるか!?と思っていたが厳しかった
早くて安い方法、
ユーザー体験を損ねずに翻訳する方法、
あるかはわからない
を探したい
まとめ
CosenseのTranslation modeは、ユーザー体験を良くするための細かい意思決定を積み上げて作られています
人間が読むところを優先して翻訳する
同時編集にも対応できるキャッシュ機構
結果比較翻訳
あえてコストをかけてユーザー体験を向上させる
うまくいかなかったQuickSearchの翻訳
様々な工夫をして、ユーザー体験を良くしつつ、リソース消費を抑えています
Helpfeelではエンジニアを募集しています
地道に改善を積み重ねてユーザー体験を向上させるのが好きな方
Helpfeelのプロダクトに興味がある方
ブースにも来てね
Translation modeは今日発表した以外にも様々な細かい積み重ねを行なってます
もし興味があったら聞きに来てね