OpenAIのChatAPIを利用した「ふりがな生成」サービスを作った

2023/06/01に公開

こんにちは、フットボール・テクノロジーズ社でフロントエンジニアとして働いているpeko-pekoです。Zennでの初めての投稿になります😆
今回、「kanamore」(カナモレ)というサービスを作ったので、下記の目的で記事を書いてみました。

https://kanamore.vercel.app/
まだ、β版の段階なので、ドメインは設定していません

記事を書いた目的

  1. OpenAIのAPIなど、開発を通して学んだ技術を整理したかった
  2. いろんなエンジニアの人に見てもらい、フィードバックが欲しかった。(自分の実装方法が正しいのか自信が無いので😭。もっと改善したい💪)
  3. 同じ分野の興味があるエンジニアと繋がりたい

tl;dr カナモレとは?

今回紹介するカナモレは、3行で紹介すると下記になります。

  1. ホームページの漢字表記に、ふりがなが自動追加できるサービス
  2. 20カ国以上の翻訳言語も自動追加できる
  3. 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の漢字表記にふりがなが自動で付与されるのですが、裏側では下記の流れで実行されます。

  1. 指定されたURLから、表示されている日本語の文章を取得
  2. 文章を単語単位に分割
  3. 漢字が含まれる単語には、ふりがなを追加
  4. 漢字、ふりがなの情報を辞書としてfirestoreに保存
  5. 利用者の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を利用したサービスを業務委託として開発もしています。
興味がある方がいましたら、下記よりコンタクトをお願いします🙇‍♂️
https://delicious-mist-3a0.notion.site/4a54c218dd37480483d09af8dc1a9e69

最後に。。。

今後、ChatGPTのような自然言語処理を絡めたサービスが増えてくると思います。
今回、OpenAIのAPIを利用してサービス開発をしましたが、新しい分野であるがゆえに、実際にコードを書いてみるとハマるポイントが多くあり、想定よりも実装時間がかかりました。
こうやって、学んだことを共有して、これからOpenAIのAPIを使ってサービスを作ろうとしている方の、少しでも役に立てたら嬉しいです!

今日もお腹がペコペコ。

Discussion