🚀

【開発ログ】TTS機能の実装及び学習開始・終了のタイミング

に公開

正確な学習時間の取得とスムーズに学習ができるように日々工夫して修正を重ねています。

前回

  • 学習イベントの保存
     - action, lang, wordId を記録
     - 例:{ action: "learn", lang: "ja", wordId: "xxxx" }

今回

  • TTS機能
     - 韓国語・日本語の発音再生機能を追加。
     - 各カードで「はつおん」ボタンを押すと、順番に日韓両方の発音が再生されます。

  • 勉強開始ボタン
     - 正確なレッスン時間を取得するために、レッスン前に「勉強開始」ボタンを押すように変更。

  • 学習終了タイミングの修正
     - 最初は「はつおん」ボタンを押すと、TTS再生後に actionlearned に変更していました。
     - 今回からは、TTSで何度も発音練習を行った後、テスト(クイズ)を通過した時点で学習完了とするように仕様を変更しました。


① TTS機能

単語カードでTTSを使って、韓国語と日本語の発音を交互に再生するようにしました。
一度再生が終わると次の単語を自動で読み上げるため、連続した発音練習が可能です。

TTS使用例

// TTS 定義
const speakTextsSequentially = (items : { word : string; lang : string }[]) => {
  if (items.length === 0) return;
  window.speechSynthesis.cancel();

  const utter = new SpeechSynthesisUtterance(items[0].word);
  utter.lang = items[0].lang;
  utter.rate = 0.5; 
  utter.pitch = 1; 

  utter.onend = () => {
    speakTextsSequentially(items.slice(1));
  };

  window.speechSynthesis.speak(utter);
};

// TTS再生
const handleSpeak = (cardItem: LearningCardData) => {
  const ttsItems = [
    { word: cardItem.ja, lang: "ja" },
    { word: cardItem.ko, lang: "ko" },
  ];
  speakTextsSequentially(ttsItems); 
};

適用例

{card.map((cardItem: LearningCardData) => (
  <article 
    key={cardItem.id}
    className="rounded-2xl border p-4 shadow-sm hover:shadow-md transition"
  >
    {/** 発音ボタン(TTS) */}
    <div className="mt-4 flex gap-2">
      <button 
        type="button"
        onClick={() => handleSpeak(cardItem)}
        className="w-full rounded bg-blue-500 px-4 py-2 text-white"
      >
        はつおん
      </button>
    </div>
  </article>
))}

② 勉強開始ボタン

学習時間の正確な記録を行うため、学習を始める前に「勉強開始」ボタンを押す設計にしました。
まず、モーダルを表示してユーザーに学習開始を促し、ボタンを押すと学習画面へ進みます。

抜粋

LessonsPage
"use client";
import { useState } from "react";
import StudyStartButton from "@/components/timer/StudyStartButton";

export default function LessonsPage() {
  const [hasStarted, setHasStarted] = useState(false);
  const [showModal, setShowModal] = useState(true);

  const handleStart = () => {
    setHasStarted(true);
    setShowModal(false);
  };

  return (
    <main className="min-h-screen p-6">
      {/* 勉強開始モーダル */}
      {showModal && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
          <div className="w-full max-w-sm rounded-2xl bg-white p-6 shadow-lg">
            <h2 className="text-xl font-bold mb-2">学習を始めますか?</h2>
            <StudyStartButton onStart={handleStart} />
          </div>
        </div>
      )}
    </main>
  );
}

StudyStartButton.tsx
export default function StudyStartButton({ onStart }: { onStart: () => void }) {
  return (
    <button
      onClick={onStart}
      className="w-full rounded-lg bg-blue-600 py-2 text-white font-bold hover:bg-blue-700 transition"
    >
      勉強開始
    </button>
  );
}

解説

項目 説明
useState hasStarted(学習開始フラグ)と showModal(モーダル表示制御)を管理。
handleStart() 「勉強開始」ボタン押下時に学習を開始し、モーダルを閉じる。
StudyStartButton 「勉強開始」ボタンのUIコンポーネント。onStartを受け取って動作。
モーダル表示 背景を半透明にして中央にダイアログを表示し、ユーザー操作を促す。

③ 学習終了タイミングの修正

最初はTTS再生後すぐに learned 状態にしていましたが、実際には「聞いただけ」で完了とするのは早すぎると感じました。

そのため次回からは、

「TTSで発音を練習 → TEST(クイズ)で正答 → 学習完了」
という流れに変更しました。

これにより、理解度を確認した上で完了判定できるようになると思います。


悩んだこと・学んだこと

  • TTSが一度しか再生できず、同じ単語を繰り返し再生できなかった問題がありました。
    原因は、再生前に「既に学習済み」のwordIdをブロックしていたことでした。
    再生後に学習チェックを行うよう順序を修正することで解決しました。

  • 学習完了の定義をどこに置くか悩みました。
    最終的には クイズ(テスト)を通過したら完了とする仕様に落ち着きました。
    これで「覚えたかどうか」をより正確に確認できます。


次回やること

  • Quiz実装
     - 音声を聞いて、正しい絵を選ぶゲーム形式にする予定。
     - 「リスニング × 選択」

  • 学習時間保存
      - 開始時間・終了時間をサーバーで記録し、学習データの取得。

  • Firebaseセッションの安定化
     - Cookieの有効期限切れ時に自動再認証するよう、ロジックを再確認。

  • UI/UX改善
     - ダッシュボードとエディタのデザイン統一。
     - スマホ表示時の余白やカード間レイアウトの見直し。

Discussion