はじめに
英語を学習中「あれ、英語でなんて言うんだっけ……」と言葉に詰まってしまうことがよくあります。
この「とっさに英単語が出てこない」状態を解決するため、Gemini APIを使ったシンプルな英語学習アプリ「Word Sprint」を作成しました。
背景
アプリのアイデアは、認知症予防のトレーニングとして知られる「30秒で10種の花を言う」という手法から着想を得ました。
「あの単語が出てこない」と悩む英語学習の悩みと、この認知症の症状は構造的に似ていると感じました。
つまり、制限時間内に特定のカテゴリーの単語をひたすら思い出す「カテゴリー想起(categorical fluency)」のトレーニングを繰り返せば、英語の瞬発力も鍛えられるのではないかと考えたのが開発のきっかけです。
最終的に、クイズメーカーと英語学習アプリを掛け合わせたようなツールになりました。
アプリの機能と技術構成
何ができるか
スマートフォンから利用できるシンプルなSPA(Single Page Application)です。
- 設問モード:自分でカテゴリーを指定するモード、Geminiに目的(「ITエンジニア」「映画視聴」など)を伝えてお題を作ってもらうモード、完全にお任せするモードの3種類。
- 入力モード:マイクで録音する「Speaking」と、カンマ区切りで入力する「Writing」。
- 制限時間:デフォルト30秒(変更可)。時間内にひたすら単語を出します。
- 採点とフィードバック:Geminiが正解・惜しかった単語を判定し、日本語で講評をしてくれます。
待機時間を減らすため、お題は複数件を事前にまとめて取得し、キャッシュするキュー構造にしています。これは地味ですが、操作感を良くする上で助かりました。
技術スタック
スピードを重視し、以下のようなシンプルな構成にしました。
- ホスティング: GitHub Pages
- フロントエンド: Vanilla JavaScript(ES Modulesによるファイル分割のみ)
-
AI: Google Gemini API(
gemini-flash-latestエイリアスを使用)
Gemini APIは、お題の生成、Writingの採点、Speakingの採点(音声データをそのままマルチモーダル入力として送信)の3つの用途で呼び出しています。パース事故を防ぐため、generationConfig.responseMimeType: "application/json" を指定して構造化出力を強制しています。
開発時の設計判断
AIエージェントと一つずつ確認しながら、複雑になりすぎないよう以下の判断をしました。
| 論点 | 決定した方針 | 理由 |
|---|---|---|
| APIキーの扱い | ユーザーが画面から入力し、localStorageに保存する |
静的ホスティングではサーバーサイドに鍵を隠せないため。インフラ管理をゼロにする目的。 |
| 音声認識・採点 | 録音データをGeminiのマルチモーダル入力に直接渡す | 文字起こし・語彙判定・フィードバックを1回のAPI呼び出しで任せられるため。 |
| 録音の聞き直し | その場(ページ内メモリ)のみ。リロードで消去 | 永続化(IndexedDB等)のニーズは薄いと判断しスコープ外とした。 |
| 学習履歴の蓄積 | 実装しない | 単発のクイズツールとしての利用を優先し、複雑化を避けるため。 |
| 回答形式 | 単語の列挙のみ(自由作文は不可) | 「単語をどれだけ出せるか」という瞬発力の訓練に特化するため。 |
ハマったところ / 注意点
開発から公開の過程で、いくつか想定外の事態に直面しました。
1. 外部APIのモデル廃止による突然のエラー
公開直後、アプリを触ってみると以下のような404エラーが発生しました。
{
"error": {
"code": 404,
"message": "This model models/gemini-2.5-flash is no longer available to new users. ...",
"status": "NOT_FOUND"
}
}
実装時に指定していた gemini-2.5-flash が、直後に「新規ユーザー向けには提供終了」になっていたのが原因でした。
これに対応するため、常に現在推奨されるFlashモデルを指すエイリアス gemini-flash-latest に切り替えました。動くコードを書いて終わりではなく、外部AIサービスのモデルライフサイクルという「目に見えない依存関係」に注意が必要だと実感しました。
2. CSSの hidden 属性の罠
Claude Codeにブラウザ操作までさせて実機検証をしたところ、非表示のはずの入力欄が表示されたままになるバグが見つかりました。
HTML標準の hidden 属性は本来 display: none になるはずですが、.field { display: flex } のような独自CSSクラスが同じ要素に指定されていると、そちらが優先されてしまうというCSSの仕様によるものでした。
[hidden] { display: none !important; } を1行追加することで解決しました。コードレビューだけでは気づきにくい、UI特有の罠でした。
3. デッドコードの発見
「Geminiへのプロンプト部分を別ファイルに分離したい」とリファクタリングをしている過程で、不思議なことに気づきました。
「指定モード(自分でカテゴリーを入力するモード)」なのに、Gemini経由でお題を作る分岐コードが残っていたのです。遷移ロジックをよく読むと、指定モードの際は元々Geminiを呼ばず、ユーザー入力をそのまま使っていました。
つまり、見た目上はバグのように見えて、実際の挙動には影響しない「未使用のデッドコード」でした。AIが書いたコードでも、後から人間が中身を整理して問い直すことで、コードの整合性をより高められる良い経験になりました。
おわりに
今回は、AIコーディングエージェント(Claude Code)との対話だけで、要件定義から実装、デプロイ、バグ修正までを一気通貫で進めることができました。
サーバーを一切持たず、クライアントサイドからのAPI呼び出しとGitHub Pagesだけで、実用的なAIアプリが作れるのは非常に便利です。
プロンプトのチューニングや詳細なフルフロー検証など、細かい改善点はまだありますが、まずは自分が普段の学習で使いながら調整していこうと思います。
個人開発のアイデアや、外部APIを使ったシンプルなアプリ構成の参考になれば幸いです。

