OpenAIのChatAPIを利用した「ふりがな生成」サービスを作った
こんにちは、フットボール・テクノロジーズ社でフロントエンジニアとして働いているpeko-pekoです。Zennでの初めての投稿になります😆
今回、「kanamore」(カナモレ)というサービスを作ったので、下記の目的で記事を書いてみました。
まだ、β版の段階なので、ドメインは設定していません
記事を書いた目的
- OpenAIのAPIなど、開発を通して学んだ技術を整理したかった
- いろんなエンジニアの人に見てもらい、フィードバックが欲しかった。(自分の実装方法が正しいのか自信が無いので😭。もっと改善したい💪)
- 同じ分野の興味があるエンジニアと繋がりたい
tl;dr カナモレとは?
今回紹介するカナモレは、3行で紹介すると下記になります。
- ホームページの漢字表記に、ふりがなが自動追加できるサービス
- 20カ国以上の翻訳言語も自動追加できる
- 1行のscriptタグを貼り付けるだけで利用できるノーコードアプリ
日本に在住の外国の方など、ひらがなは読めるが漢字が読めない人は300万人程度が日本にいると想定されています。そのような人に向けに、日本語HPの漢字表記に、自動でふりがなのルビが振られるのが特徴です。
技術スタック
フロントエンド:Next@13.2、ChakraUI、
バックエンド:Firebase firestore、Firebase functions(第二世代)
ホスティング:Vercel
その他:OpenAI ChatAPI、Google Translate API
日本語の文章から「ふりがな」を生成するために、OpenAIのChatAPIを利用。日本語から英語などの翻訳を生成するには、Google Translate APIを利用。本来なら、全てをOpenAIのChatAPIを利用してもいいのですが、生成速度、価格を考慮して上記で決定。(詳しくは、後述)
カナモレの実行フロー
カナモレを利用すると、HPの漢字表記にふりがなが自動で付与されるのですが、裏側では下記の流れで実行されます。
- 指定されたURLから、表示されている日本語の文章を取得
- 文章を単語単位に分割
- 漢字が含まれる単語には、ふりがなを追加
- 漢字、ふりがなの情報を辞書としてfirestoreに保存
- 利用者のHPに貼り付けらたScriptタグから、4で生成した辞書を呼び出し、表示されている日本語の文章を、ふりがなのルビが追加されたDOMに差し替える。
ここで技術的に難しかったのが、3になります。
具体的には、日本語の文章(例:今日の東京は、とても蒸し暑い)をOpenAIのAPIにinputして、下記のようなoutputを取得するまでが大変でした。
input:
今日の東京は、とても蒸し暑い。
output:
const text = [
{ t: "今日", r: "きょう" },
{ t: "東京", r: "東京" },
{ t: "蒸し暑い", r: "むしあつい" }
]
いろいろと試行錯誤して作成したコードが下記になります。
const { Configuration, OpenAIApi } = require("openai");
const configuration = new Configuration({
apiKey: process.env.OPENAI_API_KEY,
});
const openai = new OpenAIApi(configuration);
const sentryWrapper = require("../../common/sentryErrorWrapper");
module.exports = sentryWrapper(async function ({ texts }) {
const sleep = (msec) => new Promise((resolve) => setTimeout(resolve, msec));
let dictionaries = [];
let totalTokens = 0
const ngWords = [
{
t: "停止",
r: "ていし",
},
{
t: "戻る",
r: "もどる",
},
{
t: "その他",
r: "そのほか",
},
{
t: "一覧",
r: "いちらん",
},
{
t: "変更",
r: "へんこう",
},
{
t: "表示",
r: "ひょうじ",
},
{
t: "中文",
r: "ちゅうごくご",
},
{
t: "読み上げ",
r: "よみあげ",
},
{
t: "関連",
r: "かんれん",
},
];
// texts内のtextが、ngWordsのtと一致した場合は、textを""に置き換える
const filteredTexts = texts.map((text) => {
const filteredText = ngWords.reduce((acc, ngWord) => {
return acc.replace(ngWord.t, "");
}, text);
return filteredText;
});
for (const text of filteredTexts) {
await sleep(10);
let kana = "";
let tokens = 0;
try {
const response = await openai.createChatCompletion({
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "[今日は暑い]\n\n Divide the sentence into words.",
},
{
role: "assistant",
content:
'[{"t":"今日", "r":"きょう"},{ "t":"暑い","r":"あつい"}]\n\n',
},
{
role: "user",
content: `[${text}]\n\n`,
},
],
temperature: 0.0,
max_tokens: 1000,
n: 1,
});
kana = response.data.choices[0].message?.content.trim() || "";
tokens = response.data.usage?.total_tokens || 0;
totalTokens += tokens;
} catch (error) {
console.log(">>>>>>>>>>>>> error", error);
sleep(1000);
}
try {
const parsedWord = JSON.parse(kana);
dictionaries = dictionaries.concat(parsedWord);
} catch (error) {
console.log(">>>>>>>>>>>>> error", error);
}
}
dictionaries.concat(ngWords);
const regex = /[\u4E00-\u9FFF]/; // 漢字が含まれるかどうかをチェックする正規表現
let kanjiDictionaries = dictionaries.reduce((acc, cur) => {
if (regex.test(cur.t) && !acc.map((row) => row.t).includes(cur.t)) {
acc.push(cur);
}
return acc;
}, []);
return {
dictionary: kanjiDictionaries,
totalTokens,
}
})
このコードに行き着くまでに、かなりの試行錯誤をしました。
その辺りの苦労を、今後のために残しておきたいと思います!
ポイント1:promptで適切なassistantを挟んであげる
思ったようなoutputを取得するためには、下記のようにassistantを挟んで期待する行動を教えてあげるのが有効的です。また、英語の方が思ったような結果を出力してくれることも判明(一般的には英語の方がtoken数が少なくなる傾向)。
{
role: "user",
content: "[今日は暑い]\n\n Divide the sentence into words.",
},
{
role: "assistant",
content:
'[{"t":"今日", "r":"きょう"},{ "t":"暑い","r":"あつい"}]\n\n',
},
参考までに、他のエンジニアが書いたpromptの記事などを見ると、最初のinput部分に「分からない場合は不明と回答すること。」などの追加をするtipsもありました。
また、試しに「漢字が含まない単語は出力しない」という命令を追加したのですが、思ったようなoutputが得られなかったです😭 ひょっとしたら、user, assistantの会話回数を増やしたら可能かもしれませんが、その分、token数が増えるので上記のシンプルな形にしています。
ポイント2:promptが勘違いするような単語は事前に取り除く
HPに表示されている日本語の文章が${text}に入るのですが、サイトによっては思わぬ文章が入ることがあります。例えば、「停止」「変更」などの単語の場合、promptが新たな命令と勘違いしてしまうことがあります。
{
role: "user",
content: `[${text}]\n\n`,
},
なので、事前にNG Wordsのリストを作って、${text}には代入されないようにしています。
これは、運用しながら追加するのを想定しています。
const ngWords = [
{
t: "停止",
r: "ていし",
},
{
t: "戻る",
r: "もどる",
},
{
t: "その他",
r: "そのほか",
},
{
t: "一覧",
r: "いちらん",
},
{
t: "変更",
r: "へんこう",
},
{
t: "表示",
r: "ひょうじ",
},
{
t: "中文",
r: "ちゅうごくご",
},
{
t: "読み上げ",
r: "よみあげ",
},
{
t: "関連",
r: "かんれん",
},
];
// texts内のtextが、ngWordsのtと一致した場合は、textを""に置き換える
const filteredTexts = texts.map((text) => {
const filteredText = ngWords.reduce((acc, ngWord) => {
return acc.replace(ngWord.t, "");
}, text);
return filteredText;
});
ポイント3:ChatGPTのAPI価格は高い。処理速度が遅い。
今回は、
model: "gpt-3.5-turbo"
を利用しているのですが、APIの利用価格が高いです。例えば、下記のスターバックスのTOPページをカナモレでふりがなを追加した場合、
対象RUL:https://www.starbucks.co.jp/
OpenAI API利用料金:5円程度
処理速度:300秒程度
のコストが発生します😭
サービスによっては無視できるかもしれませんが、kanamoreでは大きなボトルネックになります。そのため、GoogleのNatural Language APIで代替できないか試してみたのですが、outputの精度がOpenAIの方が圧倒的に良かったです。(ひょっとしたら、確認方法が正しくなかった可能性はあります🙇♂️ )
OpenAI: { t: "10本", r: "じゅっぽん" }
Google Natural Language API: { t: "10本", r: "じゅうほん" }
なので、このOpenAIのAPIを利用するコスト、処理時間を考慮するサービス設計が必要になると思います。例えば、「以前に作った辞書を参照して、存在しない文章のみをGPTに投げる」「処理が完了したらメールでお知らせする」など。
この辺りはサービスの特性に合わせるのがいいと思います。
今後の改善タスク
カナモレは、まだβ版なのでこれから改善を継続していきます。
具体的には、下記のような機能を計画中。
1. サイトの画像上で表示されているテキストに、ふりがな、翻訳を自動で追加
2. ページ内の情報が更新された場合に、機械的に辞書にふりがな、翻訳を追加
3. Stripeを利用した電子マネー決済、コンビニ決済などにも対応
「言葉の壁を越え、世界とつながる」をコンセプトに、これからも突き進んでいきたいと思います💪
業務委託もやっています💪
また、株式会社フットボール・テクノロジーズでは、カナモレのようなChatAPIを利用したサービスを業務委託として開発もしています。
興味がある方がいましたら、下記よりコンタクトをお願いします🙇♂️
最後に。。。
今後、ChatGPTのような自然言語処理を絡めたサービスが増えてくると思います。
今回、OpenAIのAPIを利用してサービス開発をしましたが、新しい分野であるがゆえに、実際にコードを書いてみるとハマるポイントが多くあり、想定よりも実装時間がかかりました。
こうやって、学んだことを共有して、これからOpenAIのAPIを使ってサービスを作ろうとしている方の、少しでも役に立てたら嬉しいです!
今日もお腹がペコペコ。
Discussion