今日の天気とアニメは？？音声認識で天気予報とEテレ番組を教えて貰ってみた

## 音声認識と発話ができるアプリを作りたい！
音声認識と発話を利用したWebアプリを作成した。
今回は前回の[天気予報アプリ（ライブカメラで渋谷を見てみた）](https://qiita.com/kosuke0517lab/items/be242454c517b10a176b)をもとに作成している。前回と違う点としては**音声認識と発話の技術** を用いており、視覚だけではなく聴覚からも天気予報を知ることができる。
それに加え、NHK番組表APIという面白そうなAPIを見つけたので取り入れてみることにした。ジャンルをEテレに絞っているのでおかあさんといっしょやおじゃる丸、忍たま乱太郎といったアニメが好きな**子供たちにも楽しんでもらえるはず**なのでぜひ使ってみてほしい。

## どんなアプリ
・[前回](https://qiita.com/kosuke0517lab/items/be242454c517b10a176b)と同様に今日、明日、明後日の天気を画像付きで表示をする。
・『～の天気は』と訊くとその日の天気を教えてくれる。もしその日が雨なら傘を持って行ってと提案してくれる。
・指定日のＥテレの好きなアニメ番組名のタイトルとサブタイトル（アニメの概要）を音声で教えてくれる（当日から1週間後まで可）。
・指定日のＥテレで放送予定のアニメを音声と文字で教えてくれる。
↓完成品

https://weathernhk.netlify.app/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618860/6bcd10ae-ed27-796c-2d40-16314a367d67.png)


## 使い方
### 天気予報or指定したアニメの放送の有無と内容を知りたいとき
1.入力フォームにＥテレの好きなアニメを入力する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618860/c1f2715a-16aa-a5dc-56fc-4d6584ead41d.png)
2.スタートボタンを押して、取得したい日付を発話する。
　発話例)明日の天気は？
　※２日後まで取得可能

3.指定した日にちの天気予報と入力したアニメのタイトルとサブタイトルが発話される。

### 放映予定のアニメが知りたいとき
1.入力フォームに知りたい日にちを『YYYY-MM-DD』形式で入力をする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618860/5cefb39b-0654-70d6-5505-f8beb328d7a8.png)
2.指定した日にちに放映予定のアニメ一覧が表示、発話される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618860/17411796-b603-fbb1-bd86-ee42b524d057.png)

## 解説
### 音声認識を行う
発話をした際には下記のコードが呼び出されて処理が行われる。
まずは、発話された内容を[WebSpeechAPI](https://developer.mozilla.org/ja/docs/Web/API/Web_Speech_API)にてテキスト化する。
次にそのテキストの中に『今日』『明日』『明後日』という単語があれば後続の処理で傘が必要かの判定と発話処理を行い、もし無いようだったらもう一度発話をしてもらうように促す。

```javascript:音声認識
    // 音声を認識できたら
    this.recog.onresult = (event) => {
      // 認識されしだい、this.resultにその文字をいれる
      // Vueなので、文字をいれただけで画面表示も更新される
      this.result = event.results[0][0].transcript;
      let error = 0;
      let day = 0;
      console.log("発話内容：",this.result);
      if(this.result.indexOf("今日")>=0){
        day = 0;
        // window.location.href = "https://weather.yahoo.co.jp/weather/jp/13/4410.html";
      }else if(this.result.indexOf("明日")>=0){
        day = 1;        
      }else if(this.result.indexOf("明後日")>=0){
        day = 2;        
      }else{
        let say_message = "もう一度しゃべって下さい。";
        console.log("say_message:",say_message);
        // 発話をさせる
        this.say(say_message);     
        error = 1;
      }
      // 傘が必要かを判定
      if(error == 0){
        this.judgeUmbrella(day);
      }
      this.target_word = document.getElementById('anime').value;
      this.getNHK(this.target_day,this.target_word);
    };
```

### アニメのタイトルとサブタイトルを取得する
[NHK番組表API](https://api-portal.nhk.or.jp/)には複数のAPIがあるのだが、今回は該当する条件の番組リストを取得することができる、Program List APIを用いる。APIリクエストに使用するパラメータは以下の通りである。

 **APIリクエストの内容**
|パラメータ名| 今回の設定 |説明|
|:-:|:-:|:-:|
|area   |130（東京） 　 　　  |地域を設定   |
|service|e1（NHKEテレ）  　　  　 | サービスID  |
|genre  |0700（アニメ/特撮）   　　　| ジャンルID  |
|date   |音声入力の結果（YYYY-MM-DD）| 日付   |
|apikey |-　　　    |自分のAPIキー   |

```:Resource URL
https://api.nhk.or.jp/v2/pg/genre/{area}/{service}/{genre}/{date}.json?key={apikey}
```
今回のWebアプリでは下記のようにして指定したアニメのタイトルとサブタイトルを取得している。
```javascript:指定したアニメのタイトルとサブタイトルを取得
    // NHK APIを呼び出す
    getNHK: async function(date,target_word) {
      console.log('getNHKが呼び出されました');
      let say_message01 = date+"は"+target_word+"はありません。";
      let say_message02 = "";    
      let response;
      try {
        response = await axios.get('https://api.nhk.or.jp/v2/pg/genre/130/e1/0700/'+date+'apikey');
        let nhk_json = response.data.list.e1;
        // 指定したアニメが放映されているかチェック
        for(let i = 0;i<nhk_json.length;i++){
          if(nhk_json[i].title.indexOf(target_word)>0){
            // タイトルの先頭にアニメorミニアニメとついてしまうので取り除く
            say_message01 = nhk_json[i].title.replace(/アニメ|ミニアニメ/g, '');
            say_message02 = nhk_json[i].subtitle;  
          }
        }
        console.log(say_message01 + say_message02)
        this.say(say_message01 + say_message02);
      } catch (error) {
        console.error(error);
      }
    },
```

## 今後やってみたいこと
今回は音声認識技術で日にちを認識して、天気予報とEテレの番組を発話させるWebアプリを作成したのだが、この技術とIotやAI等を組み合わせればもっと便利で面白いアプリやプロダクトが作成できそうな気がする。今後もいろんな技術を学んで面白いプロダクトを皆さんに公開していきたい。

## ソースコード
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="yLvjNeq" data-user="kosuke05" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/kosuke05/pen/yLvjNeq">
  天気＆Eテレ 検索.js</a> by kosuke05 (<a href="https://codepen.io/kosuke05">@kosuke05</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<details><summary><b>HTML</b></summary><div>

```html:html
<!-- 全体をVue.js有効にする -->
<div class="container text-center p-3 mb-2 bg-dark text-white" id="app">
    <!-- タイトル行 -->
  <div class="row my-3">
    <div class="col-sm-6 mx-auto"><h1>Weather＆Eテレ検索アプリ</h1></div>
  </div>
  
  <div class="info">
    <p>
      1.ＮＨＫＥテレの好きなアニメを入力してね！<br>
      2.スタートボタンを押して、天気について話してみて<br>
      　発話例)明日の天気は？<br>
      　※２日後まで取得可能<br>
    </p>
  </div>
  
<!--   アニメのタイトルを入力 -->
  <div class="input-group mb-3 " id= "search-box" >
    <input type="text" class="form-control" id="anime" placeholder="おじゃる丸" aria-label="Recipient's username" aria-describedby="button-addon2" >
    <div class="input-group-append">
      <button @click="listen"　class="btn btn-outline-primary" type="button" id="button-addon2">{{ recogButton }}</button>
    </div>
  </div>
  
<!--   指定した日に放映予定のアニメ情報を取得 -->
  <div class="info">
    <p>
      指定した日に放映予定のアニメを教えるよ！<br>
      　※１週間後まで取得可能
    </p>
  </div>
  <div class="input-group mb-3 " id= "search-box" >
    <input type="text" class="form-control" id="program" placeholder="YYYY-MM-DD" aria-label="Recipient's username" aria-describedby="button-addon2" >
    <div class="input-group-append">
      <button @click="listen02"　class="btn btn-outline-primary" type="button" id="button-addon2">取得</button>
    </div>
  </div>
  
<!--   取得したアニメ情報を表示 -->
  <div class="col-12 clearfix" id="anime-info" >
  <dl class = "float-right test-box" v-for="anime in all_anime">
    {{ anime }}<br>
  </div>
  
<!--今日を含め3日間の天気を表示 -->
  <div class="col-sm-6 mx-auto" id="wether" >
  場所：{{ prace }}<br><br>
  <dl v-for="obj01 in object">
    {{ obj01.date }}：{{ obj01.weather }}<br>
    {{ obj01.min_temperature }}℃ ~ {{ obj01.max_temperature }}℃<br><br>
    <img v-bind:src=obj01.image>
  </div>
</div>
```
</div></details>

<details><summary><b>CSS</b></summary><div>

```css:css
.info{
  width:40%;
  padding:5px 0;
  margin: 0 auto;
  text-align:left;
}
#search-box{
  width:30%;
  padding:5px 0;
  margin: 0 auto;
}
.col-12 {
  padding:10px 0;
  margin:0 auto;
  width:40%;
}
.test-box {
  width:100%; 
  text-align:left;
}
```
</div></details>

<details><summary><b>Vue.js</b></summary><div>

```javascript:Vue.js
const app = new Vue({
  el: '#app',
  
  data: {
    recogButton: 'スタート！',
    recog: null,
    result: '',
    speech: null,
    message: '',
    result:'',
    prace: '',
    target_day:'',
    target_word:'おじゃる丸',
    all_anime:[],
    // 天気予報のためのオブジェクトを定義
    object:[{
      date:'',
      weather: '',
      min_temperature:'',
      max_temperature:'',
      image:''
    }],
  },
  created: async function() {
    this.getWeather();
  },  
  mounted() {
    // 音声認識の準備
    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition || window.mozSpeechRecognition || window.msSpeechRecognition || null;
    this.recog = new SpeechRecognition();
    this.recog.lang = 'ja-JP';
    this.recog.interimResults = false;
    this.recog.continuous = false;
    
    // 音声認識が開始されたら
    this.recog.onstart = () => {
      this.result = '';
      this.recogButton = 'ききとりちゅう…';
    };
    
    // 音声を認識できたら
    this.recog.onresult = (event) => {
      // 認識されしだい、this.resultにその文字をいれる
      // Vueなので、文字をいれただけで画面表示も更新される
      this.result = event.results[0][0].transcript;
      let error = 0;
      let day = 0;
      console.log("発話内容：",this.result);
      if(this.result.indexOf("今日")>=0){
        day = 0;
        // window.location.href = "https://weather.yahoo.co.jp/weather/jp/13/4410.html";
      }else if(this.result.indexOf("明日")>=0){
        day = 1;        
      }else if(this.result.indexOf("明後日")>=0){
        day = 2;        
      }else{
        let say_message = "もう一度しゃべって下さい。";
        console.log("say_message:",say_message);
        // 発話をさせる
        this.say(say_message);     
        error = 1;
      }
      // 傘が必要かを判定
      if(error == 0){
        this.judgeUmbrella(day);
      }
      this.target_word = document.getElementById('anime').value;
      this.getNHK(this.target_day,this.target_word);
    };
    
    // 音声認識が終了したら
    this.recog.onspeechend = () => {
      this.recog.stop();
      this.recogButton = 'スタート！';
    };
    
    // 認識できなかったら
    this.recog.onerror = () => {
      this.result = '（認識できませんでした）';
      this.recog.stop();
      this.recogButton = 'もう一度';
    };
    
    // 発話の準備
    this.speech = new window.SpeechSynthesisUtterance();
    this.speech.lang = 'ja-JP';
    // または、ブラウザが対応している言語のうちja-JPな最初のボイスを使う
    window.speechSynthesis.onvoiceschanged = () => {
      const voices = window.speechSynthesis.getVoices();
      this.speech.voice = voices.find(voice => voice.lang == 'ja-JP');
      console.log(this.speech.voice);
    };
  },
  
  methods: {
    // 認識（聞き取り）
    listen() {
      this.recog.start();
    },
    
    // YYYY-MM-DDで取得
    listen02() {
      let target_date = document.getElementById('program').value;
      console.log(target_date)
      this.getNHKAll(target_date);
    },
    
    // なんかしゃべる（いちおう非同期対応）
    async say(say_message) {
      return new Promise((res) => {
        // this.speech.text = this.message;
        this.speech.text = say_message
        this.speech.onend = () => res();
        speechSynthesis.speak(this.speech);
      });
    },
    // 天気を呼び出す関数はここ
    getWeather: async function(day) {
      console.log('getWeatherが呼び出されました');
      let response;
      try {
        // 天気のデータを取得
        response = await axios.get('https://weather.tsukumijima.net/api/forecast/city/130010');
        // 場所のデータを取得する
        this.prace = response.data.title;
        w_data = response.data.forecasts;
        this.object.shift();
        // プロパティーとして日にち、天気、気温、画像を追加する
        for(let i=0;i<3;i++){
          this.object.push({date: w_data[i].date,weather : w_data[i].telop,min_temperature:w_data[i].temperature.min.celsius,max_temperature:w_data[i].temperature.max.celsius,image:w_data[i].image.url});
        } 
      } catch (error) {
        console.error(error);
      }
    },
    // 傘が必要か判定をする関数
    judgeUmbrella: async function(day) {
        // 雨だったら傘を忘れないように注意喚起
        console.log("judgeUmbrellaが呼び出されました");
        let umbrella = "";
        if(w_data[day].telop.indexOf( '雨' )>0){
          umbrella = "雨が降るので傘を忘れずに";
        }
        console.log("umbrella:",umbrella);
        let say_message =  w_data[day].date +'の天気は' + w_data[day].telop + "です。"+umbrella;
        console.log("say_message:",say_message);
        // 発話をさせる
        this.say(say_message);  
        console.log("w_data[day].date:",w_data[day].date);
        this.target_day = w_data[day].date;
    },
    // NHK APIを呼び出す
    getNHK: async function(date,target_word) {
      console.log('getNHKが呼び出されました');
      let say_message01 = date+"は"+target_word+"はありません。";
      let say_message02 = "";    
      let response;
      try {
        response = await axios.get('https://api.nhk.or.jp/v2/pg/genre/130/e1/0700/'+date+'.json?key=38K97eAbINCjdtGA6HPYoGGVKhVXEZTb');
        let nhk_json = response.data.list.e1;
        // 指定したアニメが放映されているかチェック
        for(let i = 0;i<nhk_json.length;i++){
          if(nhk_json[i].title.indexOf(target_word)>0){
            // タイトルの先頭にアニメorミニアニメとついてしまうので取り除く
            say_message01 = nhk_json[i].title.replace(/アニメ|ミニアニメ/g, '');
            say_message02 = nhk_json[i].subtitle;  
          }
        }
        console.log(say_message01 + say_message02)
        this.say(say_message01 + say_message02);
      } catch (error) {
        console.error(error);
      }
    },
    // NHK APIを呼び出す
    getNHKAll: async function(date) {
      console.log('getNHKAllが呼び出されました');
      let say_message01 = [date +"に放映予定のアニメは"];
      let say_message02 = "となっています。たのしみですね。";    
      let response;
      try {
        response = await axios.get('https://api.nhk.or.jp/v2/pg/genre/130/e1/0700/'+date+'.json?key=38K97eAbINCjdtGA6HPYoGGVKhVXEZTb');
        let nhk_json = response.data.list.e1;
        console.log(nhk_json);
        // 指定したアニメが放映されているかチェック
        for(let i = 0;i<nhk_json.length;i++){
          console.log(nhk_json[i].title);
          this.all_anime.push(nhk_json[i].title.replace(/アニメ|ミニアニメ/g, '・'));
          say_message01.push(nhk_json[i].title.replace(/アニメ|ミニアニメ/g, '・')+"と");
        }
        console.log(say_message01 + say_message02)
        this.say(say_message01 + say_message02);
      } catch (error) {
        console.error(error);
      }
    },
  },
});
```
</div></details>

## 参考サイト

https://weather.tsukumijima.net

https://api-portal.nhk.or.jp/

https://developer.mozilla.org/ja/docs/Web/API/Web_Speech_API