ルビと格闘した話

この記事は [Medley（メドレー） Advent Calendar 2024](https://qiita.com/advent-calendar/2024/medley) の 21 日目の記事です。

# はじめに
メドレーのエンジニアのSongです。[ジョブメドレーアカデミー](https://jm-academy.jp/?utm_source=google&utm_medium=cpc&gad_source=1&gclid=Cj0KCQiAvP-6BhDyARIsAJ3uv7b8HOYHHcX11HT6gg3UQi1MhXK_ZScMgbdJcVA-rohRinYEj_6R1CgaAl88EALw_wcB)という、介護や障がい福祉、在宅医療などの各業種に特化したオンライン動画研修サービスの開発に携わっています。その中で「介護福祉士国家試験」の過去問を解けるようにするという機能開発で、すべての漢字にルビを表示する対応をしました。
その時の学びをまとめていきたいと思います。

# この記事で書いたこと
- ルビを表示する方法がわかる
- 効率的なルビの生成・管理方法がわかる

# 前提
介護福祉士国家試験では、外国人を対象に、ふりがな付き問題用紙を選ぶことができます。近年は外国人の受験者も増加していて、ジョブメドレーアカデミーでも問題文や選択肢文のすべての漢字にルビを表示することになりました。
試験問題の過去問は、社会福祉復興・試験センター様に許可をいただき、[サイト](https://www.sssc.or.jp/kaigo/past_exam/index_epa.html)からダウンロードして、こちらを使用しました。

# ルビとは
ルビとは、ふりがなのことです。
以下のように漢字などの上にふりがなを振っているものをルビと言っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736722/f475a69d-2c85-af61-d468-e3c5dfcdf94c.png)


画面表示する時には、以下のようなHTMLの形式を使用することでルビを表示できます。

```html
<ruby>介護<rp>(</rp><rt>かいご</rt><rp>)</rp></ruby>
```
rubyタグは基本的にどのブラウザでも使えますが、とても古いブラウザの中には未対応なブラウザがあるそうです。(参照：https://caniuse.com/ruby)

未対応のブラウザの場合は、ルビとしては表示されず、rpタグ内の記述が表示されるようになります。
例えば、上記記述のようにrpタグ内に()を記述した場合は、括弧の中に振り仮名が表示される形で表示されます。

![スクリーンショット 2024-12-13 1.55.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736722/ed5e96f0-be80-0dc3-46e7-fd3e69d1b2c1.png)


その他は、rubyタグの[ドキュメント](https://developer.mozilla.org/ja/docs/Web/HTML/Element/ruby)を参照して下さい。

# 作業の流れ
過去問機能の提供に対して発生した作業は以下です。
- 過去問のPDFから、問題文などのテキストデータを取得
- 取得したデータをDBで永続化する
- DBから取得したデータをAPIでフロントエンドに渡す
- APIで取得したデータを画面表示する

これらの作業のうち、ルビが関わる部分に関して、以下で触れていきます。


# ルビデータの用意
試験の過去問はPDFだったため、[pdfminer](https://github.com/pdfminer/pdfminer.six)を使用して、PDFから問題文などのテキストデータを取得しました。しかし、ルビの部分は文字化けして取得することができませんでした。そのため、自分たちで問題文にルビを振っていく必要がありました。

### 試したこと①  GPTに頑張ってもらう
最初は、データをHTML形式のままDBに保存しようとしていたこともあり、GPTに問題文などのテキストデータを渡して、「文章にふりがなを振って、それをHTMLのrubyタグ形式にして」とお願いして返してもらおうとしました。しかし、勝手に文章を変えたり、漢字以外にもふりがなを振り出してしまったりと、いい感じにセットアップしていくのが大変でした。また、125問×数実施回分で量が膨大だったので、データを渡していくのも現実的ではありませんでした。（試験問題の中には事例問題など、長い問題もあり）

※メドレーでは生成AI利用のガイドラインが社内で展開されており、各部門の業務ではそのガイドラインに沿って利用をしています。


### 試したこと② Yahooのルビ振り用APIの活用
Yahoo!デベロッパーネットワークで提供されているAPIの1つに[「ルビ振り」](https://developer.yahoo.co.jp/webapi/jlp/furigana/v2/furigana.html)というAPIがありました。これが作業をとても楽にしてくれました。

このAPIは、文章を「日本語の意味の単位」で切り、さらにその中に漢字が含まれている場合には、その漢字のふりがなも一緒に返してくれます。

例えば以下のようなパラメータでリクエストすると、
```json
{
	"id": "841",
	"jsonrpc": "2.0",
	"method": "jlp.furiganaservice.furigana",
	"params": {
		"q": "著書『ケアの本質−生きることの意味』の中で、「一人の人格をケアするとは、最も深い意味で、その人が成長すること、自己実現することをたすけることである」と述べた人物として、正しいものを1つ選びなさい。",
		"grade": 1
	}
}
```

以下のようなレスポンスを返してくれます。

```json
{
	"id": "841",
	"jsonrpc": "2.0",
	"result": {
		"word": [
			{
				"furigana": "ちょしょ",
				"roman": "tyosyo",
				"surface": "著書"
			},
			{
				"surface": "『"
			},
			{
				"surface": "ケア"
			},
			{
				"surface": "の"
			},
			{
				"furigana": "ほんしつ",
				"roman": "honsitu",
				"surface": "本質"
			},
			{
				"surface": "－"
			},
			{
				"furigana": "いきる",
				"roman": "ikiru",
				"subword": [
					{
						"furigana": "い",
						"roman": "i",
						"surface": "生"
					},
					{
						"furigana": "きる",
						"roman": "kiru",
						"surface": "きる"
					}
				],
				"surface": "生きる"
			},
			{
				"surface": "こと"
			}
           〜長いので省略〜
		]
	}
}
```

ただし、ふりがなに関しては、完璧というわけではなく、"年々"を"としとし"にしてしまうなど、たまーにミスります。そのため、ふりがなの振り間違えがないか、目視でのテストは必要です。


余談ですが、リクエストパラメータのgradeで、学年を選ぶことができます。例えば、2でリクエストすると、小学2年生向けのルビが振られ、1年生で学習済みの漢字にはふりがなが振られないようにできるそうです。子供向け学習アプリとかで活用できそう！

## 最終的に採用した方法
最終的に、試したこと②の「Yahooのルビ振り用API」を採用しました。理由としては、作業時間の短さです。最初にAPIを呼び出すスクリプトを実装するという手間はあるものの、1度実装してしまえば、あとは一瞬でルビを振ることができます。また、テストも、ルビの振り間違いだけ確認すれば良く、文章の区切り方がおかしいか・文章が変わってないかなどを確認する必要がなかったので、時短になりました。

# DBでの管理方法
ルビをどうやってDBで管理していくかも悩みました。

### 試したこと① HTML形式のまま保存

| id | description | description_ruby |  
| ---- | ---- | ---- | 
| 1  |  Ａさん（76歳、女性、要支援1）は、一人暮らしである。週1回介護予防通所.... | ```Ａさん（76<ruby>歳<rp>(</rp><rt>さい</rt><rp>)</rp>、<ruby>女性<rp>(</rp><rt>じょせい</rt><rp>)</rp>、<ruby>要支援<rp>(</rp><rt>ようしえん</rt><rp>)</rp>1）は、<ruby>一人<rp>(</rp><rt>ひとり</rt><rp>)</rp><ruby>暮<rp>(</rp><rt>ぐ</rt><rp>)</rp>らしである。<ruby>週<rp>(</rp><rt>しゅう</rt><rp>)</rp>1<ruby>回<rp>(</rp><rt>かい</rt><rp>)</rp><ruby>介護<rp>(</rp><rt>かいご</rt><rp>)</rp><ruby>予防<rp>(</rp><rt>よぼう</rt><rp>)</rp><ruby>通所<rp>(</rp><rt>つうしょ</rt><rp>)</rp>```  | 

文章をrubyタグ付きのHTML形式のデータにして、それをそのままテーブルに格納するという方法です。フロントエンドには、APIで、description_rubyをstring型でそのまま返します。

- メリット
    - DBに入っているデータをそのままパースして画面に表示するだけ
- デメリット
    - rubyタグをつけたことにより、データがとても長くなるので、ルビの振り忘れやふりがな間違いを発見しにくい
    - データがとても長いので、データ修正時に、修正箇所を見つけるのが大変
    - 問題文中の単語をクリックできるようにするなど、仕様追加があった時に対応しにくい

### 試したこと② ルビを振る単位で分割して保存

| id | question_id | word | ruby | order_number | keyword_id | 
| ---- | ---- | ---- | ---- | ---- | ---- |
| 1  |  1 | NPO  | null | 1 | 1 |
| 2  |  1 | 法人 | ほうじん | 2 |  1 |
| 3  |  1 | に | null | 3 |  null |
| 4  |  1 | 対 | たい | 4 | null |
| 5  |  1 | して | null | 5 | null |

Questionテーブルとは別に、ルビ管理用のテーブルを用意し、文章を「ルビを振る単位」で分割してテーブルに入れていく方法です。同じquestion_idのwordをorder_numberの順番で並べると、元の問題文になります。APIではテーブルの形のまま返して、フロントエンドで、rubyタグを適用します。

- メリット
    - rubyタグによる複雑さが消えたので、テストしやすい・修正しやすい
    - スプレッドシートなどを使用して管理しやすい
        - テーブルの形のまま、スプレッドシートで管理するようにしました。これにより、条件付き書式設定を使って、ルビの振り忘れチェックなどもできるようになりました
        - また、スプレッドシートなのでエンジニア以外もテスト→修正と、管理できるようになりました
    - 問題文中の単語に対する新しい仕様追加をしやすい
        - 例えば、このテーブルの場合だと、keyword_idがあるwordの時には、画面でクリックできるようにするといったことができるようになります
- デメリット
    - フロントエンド側で、「ルビがある場合はrubyタグで挟む」という実装をしないといけないので少し手間

## 最終的に採用した方法
最終的に、試したこと②の「ルビを振る単位で分割してDBに保存する」方法を採用しました。
その理由は、「問題文中の特定の単語のみをクリックしたときに、UIライブラリのpopoverを使用して、その翻訳語を表示する」という要件を実現する必要があったためです。試したこと①のようにHTMLで持ってしまうと、 UIライブラリを使えない・スタイルの適用がしにくいなど、フロントエンド側の自由が利かないケースがあるので、今後の拡張性を考えるとやはり、試したこと②の方が良いです。
また、125問×数実施回分の問題文・選択肢文のルビの振り間違いを「目視」でテストする必要があったので、エンジニア以外もデータを修正できる「試したこと②」が、テスト人員の確保という点でも最適でした。

# 完成した画面
全ての漢字にルビが振られ、また、文中のキーワードもクリックできるようになりました。

![screen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736722/3c2341f7-00d4-9385-d64a-474645a8c77c.png)


# 最後に
最後に、rubyタグを使用した際は必ず実機での確認をしてください。
Web開発するにあたり、chromeの開発者コンソールでPCおよびスマートフォンのサイズを見ながら問題なく開発してましたが、いざスマートフォンの実機で触ってみると、不具合がいくつか出てきました。OSやブラウザによってスタイルの効き方が違かったので、様々なデバイスで実機確認することをお勧めします。


 [Medley（メドレー） Advent Calendar 2024](https://qiita.com/advent-calendar/2024/medley)、明日は [@ymzkmct](https://qiita.com/ymzkmct) さんです！