イントロダクション
概要
英語リスニング練習用のWebアプリを作成しました.
- トピックを選択
- 音声を再生
- 聞きとった内容をテキストボックスに入力
- 聞き取れない部分はリピートしながら回答
動機
TOEICのリスニング勉強の一環でディクテーションをしていた時,
”聞き取りづらいとこだけ何回も繰り返す方法ないかな~”と思ったのがきっかけ.
ユーザが指定した部分をリピートし続けるプレイヤーを作る目的で作成しました.
要件
機能要件
ディクテーションするにあたって必要と考えた機能は下記の通り.
- 音声プレイヤーと回答テキストボックス
- 音声プレイヤー
- 基本的な機能(再生/停止,シークバー,数秒戻る/進む…など)
- 開始/終了時間を指定し,対象区間をループする機能
- 回答テキストボックス
- テキスト入力機能
- 解答の表示機能
- (できれば)回答の正解/不正解表示機能
- 音声プレイヤー
その他の要件
音声ファイルと原稿となるテキストファイルが必要.
ずっと同じ問題では困るので,原稿&音声の内容は定期的に更新されるようにしたい.
トピックについてはとくにこだわりないので,数種類から選べればOK.
使用技術・API
- React
- 機能要件を満たす音声プレイヤー,回答テキストボックスのコンポーネントを作成
- Google Cloud
- Cloud Run
- バックエンドAPIの実行に利用
- Gemini
- 原稿テキスト作成に利用
- Text-to-Speech API(Vertex AI)
- 原稿テキストの音声化に利用
- Cloud Storage
- 音声&原稿テキストの保存に利用
- Cloud Scheduler
- 原稿&音声の定期更新に利用
- Cloud Run
最終的に下記のような構成としました.
- ページアクセス時のフロー
バックエンド
API
下記のAPIを用意しました.
名称 | 説明 |
---|---|
音声&原稿URL取得API | 引数:ファイル保存日,トピック 対象トピックの音声&原稿ファイルのURLを取得するAPI 原稿&音声を毎日更新しているため,日付も引数としています |
音声&原稿ファイル生成API | 引数:トピック GeminiとText-To-Speech APIにアクセスし,音声と原稿ファイルを生成するAPI Storageに保存するAPICloud Schedulerによる定期実行用 OIDCトークンによって認証し,Cloud Scheduler以外からの実行を拒否 |
フロントエンド
ランディングページと問題ページの2つで構成しました.
ランディングページ
トピックを選択して,対応する問題ページに遷移します.
問題ページ
アクセス時にバックエンドの音声&原稿URL取得APIを実行.
取得したURLにアクセスして音声&原稿データを取得し,各コンポーネントに渡します.
Reactで機能要件を満たす音声プレイヤー,回答テキストボックスのコンポーネントを作成しました.
音声プレイヤーコンポーネント
基本的な機能に加え,下のスライダーからループ開始/終了時間を指定できます.
回答テキストボックスコンポーネント
各単語ごとに入力ボックスを分けています.
解答表示はボタンでON/OFF可能.
回答の正解/不正解表示機能については単語ごとに区切って判定しています.
音声/原稿の自動更新機能
バックエンドの音声&原稿ファイル生成APIを1日1回実行し,新しい音声&原稿ファイルを作成しています.
Cloud Scheduler側の対応
ターゲットタイプ”HTTP”を選択し,音声&原稿ファイル生成APIのURLを対象URLに設定します.
また,今回は認証を行いたいのでAuthヘッダーに”OIDCトークンを追加”を設定します.
サービスアカウントにはCloud Scheduler Job実行者のロールを設定する必要があります.
必要に応じてIAMと管理からロールを割り当てましょう.
バックエンド(API)側の対応
バックエンド側では認証を行うための対応が必要です.
今回は,OIDCトークンのペイロードに含まれるemailを利用して下記のようなフローでアクセス制御を行います.
OIDCトークンには設定したサービスアカウントのメールアドレスが含まれるので,これを用いて認証を行います.
- [Cloud Scheduler側処理] OIDCトークンを含んだリクエストをバックエンドAPIに投げる
- [バックエンド側処理] OIDCトークンのペイロードに含まれるemailと,事前に設定しておいたメールアドレスを比較
- [バックエンド側処理] メールアドレスが一致していればファイル生成処理継続,一致していなければ拒否する
実装は下記のようになります.
- 環境変数
ENDPOINT={APIのURL}
SERVICE_ACCOUNT_EMAIL={サービスアカウントのメールアドレス}
- 認証処理のコード
import { OAuth2Client } from 'google-auth-library';
import dotenv from 'dotenv';
//環境変数にENDPOINT={APIのURL}を設定しておく
const client = new OAuth2Client(process.env.ENDPOINT);
export const authenticate = async (req, res, next) => {
try {
//authorizationヘッダーがないなら拒否
if (!req.headers['authorization']) {
throw new Error('Forbidden - No token provided');
}
//OIDCトークンからペイロードを取り出す
const idToken = req.headers['authorization'].split(' ')[1];
const ticket = await client.verifyIdToken({
idToken,
audience: process.env.ENDPOINT,
});
const payload = ticket.getPayload();
if (payload) {
// サービスアカウントのメールアドレスを検証
if (payload.email === process.env.SERVICE_ACCOUNT_EMAIL) {
//一致していれば次の処理
next();
} else {
//不一致なので拒否
throw new Error('Forbidden - Invalid service account');
}
} else {
throw new Error('Forbidden');
}
} catch (error) {
console.error(error);
res.status(403).send('Forbidden');
}
};
- サーバのコード
import express from 'express';
const app = express();
const port = 8080;
app.post('/{エンドポイントの名前(任意)}', authenticate, async (_, res) => { //引数に↑で書いたauthenticateを追加
/*
ファイル生成処理のコード
*/
});
デプロイ方法
バックエンド
バックエンドのコードはCloud Runでデプロイしました.
GitHubでリポジトリを作成し,Cloud Runの”リポジトリを接続”からデプロイできます.
フロントエンドを後述のようにVercelにデプロイしているので,”未認証の呼び出しを許可”を選択します.
環境変数にURLとサービスアカウントのメールアドレスを設定しておきます.
設定ページ下部の”コンテナ,ボリューム…”の”変数とシークレット”から設定できます.
フロントエンド
ホスティングサービスはVercelを利用しました.
GitHubでリポジトリを作成し,Vercelのページから連携すれば完了です.
まとめ
ディクテーション用アプリの開発内容についてまとめました.
補足
- 原稿テキストは下記のようなニュースAPIから取得しても良いかもしれません
https://www.microsoft.com/en-us/bing/apis/bing-news-search-api - Vercel cronを使えばバックエンドのAPIをフロント側に統合できそう?
https://vercel.com/docs/cron-jobs/manage-cron-jobs
参考リンク
バックエンド