見切り発車で平仮名の学習アプリを作る【7日目】

**2024年のアドベントカレンダー7日目の記事です。**

https://qiita.com/advent-calendar/2024/yaikawa_memory_2024

### わからないボタンクリックで問題文を読み上げたい

よめないひらがなが出てきたときに確認できるように読み上げ機能を作ります。
TTSの機能についてもweb speech apiの[SpeechSynthesisUtterance](https://developer.mozilla.org/ja/docs/Web/API/SpeechSynthesisUtterance)で用意されているため、こちらを利用することにしました。

わからないボタンをクリックしたときに呼び出せるように以下のようなメソッドを作成します。

```javascript:App.js
  const speakText = (text) => {
    if (!('speechSynthesis' in window)) {
      alert('なんかだめみたい。おじさんにきいてみて');
      return;
    }
    
    const utterance = new SpeechSynthesisUtterance(text);
    utterance.lang = 'ja-JP';
    utterance.rate = 2;
    window.speechSynthesis.speak(utterance);
  }
```

`utterance.lang = 'ja-JP';`と`utterance.rate = 2;`で言語、読み上げ速度の指定を入れた以外あまり労力なく読み上げ機能の作成ができました。

問題文自体はstate管理しているので上記で作成したメソッドに対してstateを渡すだけで読み上げが可能になりました。


```javascript:App.js
import './App.css';
import { Provider } from './components/ui/provider';
import { Box, AbsoluteCenter, Heading, VStack, HStack } from '@chakra-ui/react';
import { useState, useRef } from 'react';
import { Button } from './components/ui/button';

function App() {
  const [question, setQuestion] = useState('あとですぷしからしゅとくするようにします');
  const [answer, setAnswer] = useState('ここがおんせいにゅうりょくでかわるところ');
  const [isRecording, setIsRecording] = useState(false);
  const recognitionRef = useRef(null);

  const startRecording = () => {
    if (!('webkitSpeechRecognition' in window)) {
      alert('なんかだめみたい。おじさんにきいてみて');
      return;
    }

    recognitionRef.current = new window.webkitSpeechRecognition();
    recognitionRef.current.lang = 'ja-JP';
    recognitionRef.current.continuous = false;
    recognitionRef.current.interimResults = false;

    recognitionRef.current.onstart = () => {
      setIsRecording(true);
    };

    recognitionRef.current.onresult = (event) => {
      const transcript = event.results[0][0].transcript;
      setAnswer(transcript);
    };

    recognitionRef.current.onerror = (event) => {
      console.error(event.error);
    };

    recognitionRef.current.onend = () => {
      setIsRecording(false);
    };

    recognitionRef.current.start();
  };

  const stopRecording = () => {
    if (recognitionRef.current) {
      recognitionRef.current.stop();
    }
  };

  const speakText = (text) => {
    if (!('speechSynthesis' in window)) {
      alert('なんかだめみたい。おじさんにきいてみて');
      return;
    }
    
    const utterance = new SpeechSynthesisUtterance(text);
    utterance.lang = 'ja-JP';
    utterance.rate = 2;
    window.speechSynthesis.speak(utterance);
  }

  return (
    <Provider>
      <Box bg="blue.100" w="100vw" h="100vh">
        <AbsoluteCenter>
          <VStack gap="20">
            <Heading size="5xl">
              {question}
            </Heading>
            <HStack gap="20">
              <Button size="2xl" variant="outline" borderColor="green.600" color="green.600" onClick={isRecording ? stopRecording : startRecording}>
                {isRecording ? "こたえた" : "こたえる"}
              </Button>
              <Button size="2xl" variant="outline" borderColor="orange.600" color="orange.600" onClick={() => speakText(question)}>
                わからない
              </Button>
            </HStack>
            <Heading size="5xl">
              {answer}
            </Heading>
          </VStack>
        </AbsoluteCenter>
      </Box>
    </Provider>
  );
}

export default App;
```

ファイルを分けずにApp.jsに詰め込んでいるのでどこかのタイミングで整理したいと思います。