(GAS&JS)ブラウザを使って音声の文字起こし

## やったこと

PCに向かってしゃべった内容を認識しブラウザに書き出します。

`エンター`という言葉を認識すると改行し、また`要点`という言葉を認識すると`■要点`の下に書き込み位置を移動します。

※音声入力で改行、書き込み位置の移動をしてる様子です。
![GAS-record-qiita.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217395/d9f2b8bc-a1d1-0a2a-8b3d-6fa6867947d9.gif)


### 背景

考えをまとめるとき、思いつくことをPCで書き出して(タイピングして)してまとめることが多いので、ハンズフリーでできないものかとGASで作ってみました。
茶碗洗いしてるときや、アイロンかけてるときに人と話してる感覚で考えを書き出せたらなと。

### 結果

実用的なレベルには至りませんでした。

- タイピングに比べ入力に時間がかかる
- 誤変換が多い

といった理由で、タイピングに比べ非常にストレスでした。

ただ、

- 音声の認識速度を上げる(前後の文脈をあまり考慮させず、認識した言葉をすぐに確定させる)
- Cloud Speech-to-Textとか使ってもう少し変換精度を上げる

などができれば、ある程度使えるかもしれません。

## 環境

- 実行環境
  - chrome (PCで試しましたが多分スマホでも使えます)
- プログラム
  - GAS
  - javascript
    - Web Speech API 

## コード

手順は以下です

1. googleスプレッドシートなどからスクリプトを作成する
2. コード.gsを編集し、index.htmlを新規作成する
3. 下記コードをコピペ
4. 公開>アプリケーションとして導入>更新
5. 表示されたURLにアクセス
6. startボタンを押す

特定の言葉を認識した場合、以下の処理をします。
`エンター`・・・改行
`ブレスト`・・・■ブレスト の下に書き込み
`要点`・・・■要点 の下に書き込み
`まとめ`・・・■まとめ の下に書き込み

```javascript:コード.gs
var spreadsheet = SpreadsheetApp.getActive(); //シートを取得
var sheet_rec = spreadsheet.getSheetByName("rec");
var last_row = 1;

/** include(html名)でhtmlファイルを読み込む */
function include(filename) {

    return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

/** index.htmlを開く */
function doGet() {
    var template = "index";

    return HtmlService.createTemplateFromFile(template)
        .evaluate()
        .setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
```

```html:index.html
<button id="start-btn">start</button>
<button id="stop-btn">stop</button>
<div id="write-sheet">
  <p>■ブレスト</p>
  <div id="brest"></div>
  <p>■要点</p>
  <div id="point"></div>
  <p>■まとめ</p>
  <div id="summary"></div>
</div>

<script>
  const startBtn = document.querySelector('#start-btn');
  const stopBtn = document.querySelector('#stop-btn');
  const writeDiv = document.querySelector('#write-sheet');
  const brestDiv = document.querySelector('#brest');
  const pointDiv = document.querySelector('#point');
  const summaryDiv = document.querySelector('#summary');

  SpeechRecognition = webkitSpeechRecognition || SpeechRecognition;
  let recognition = new SpeechRecognition();

  recognition.lang = 'ja-JP';
  recognition.interimResults = true;
  recognition.continuous = true;

  let finalTranscript = ''; // 確定した(黒の)認識結果
  let writePosition = brestDiv;

  recognition.onresult = (event) => {
    let interimTranscript = ''; // 暫定(灰色)の認識結果
    for (let i = event.resultIndex; i < event.results.length; i++) {
      let transcript = event.results[i][0].transcript;
      if (event.results[i].isFinal) {
        finalTranscript += transcript;
      } else {
        interimTranscript = transcript;
      }
    }
    finalTranscript = finalTranscript.replace('エンター', '<br>');

    // TODO:1回の書き込みで2箇所への移動はできない問題解決
    if (finalTranscript.indexOf('ブレスト') != -1) {
      // 移動前の書き込み
      writePosition.innerHTML = finalTranscript.substr(0, (finalTranscript.indexOf('ブレスト')));
      // 位置フラグ変更
      writePosition = brestDiv;
      // 移動後の書き込み
      finalTranscript = writePosition.innerHTML + finalTranscript.substr((finalTranscript.lastIndexOf('ブレスト')));
      finalTranscript = finalTranscript.replace('ブレスト', '');
    } else if (finalTranscript.indexOf('要点') != -1) {
      writePosition.innerHTML = finalTranscript.substr(0, (finalTranscript.indexOf('要点')) );
      writePosition = pointDiv;
      finalTranscript = writePosition.innerHTML + finalTranscript.substr((finalTranscript.lastIndexOf('要点')));
      finalTranscript = finalTranscript.replace('要点', '');
    } else if (finalTranscript.indexOf('まとめ') != -1) {
      writePosition.innerHTML = finalTranscript.substr(0, (finalTranscript.indexOf('まとめ')));
      writePosition = summaryDiv;
      finalTranscript = writePosition.innerHTML + finalTranscript.substr((finalTranscript.lastIndexOf('まとめ')));
      finalTranscript = finalTranscript.replace('まとめ', '');
    }

    writePosition.innerHTML = finalTranscript + '<i style="color:#ddd;">' + interimTranscript + '</i>';

  }

  startBtn.onclick = () => {
    recognition.start();
  }
  stopBtn.onclick = () => {
    recognition.stop();
    alert("読み取り終わり");
  }
</script>
```

以上です。
何か気づいた点あればコメントください。

---

### メモ

このプログラムだと、ページを更新すると記入内容がリセットされてしまうので、スプレッドシートに結果を記入できると便利かもしれません。

下記のようなコードで行けると思ったのですが、htmlからgasの関数を呼び出せずエラーとなりました。
暇なときに解決しようと思います...。

```javascript:コード.gs
var spreadsheet = SpreadsheetApp.getActive(); //シートを取得
var sheet_rec = spreadsheet.getSheetByName("rec");
var last_row = 1;

function writeHTML(HTMLcontent) {
    last_row = sheet_rec.getLastRow(); // 空白でない最終行の位置を取得
    sheet_rec.getRange(last_row + 1, 2).setValue(HTMLcontent);
}
```

```javascript:index.html
   stopBtn.onclick = () => {
     recognition.stop();
     google.script.run.withSuccessHandler(function () {
       alert("読み取り終わり");
     }).writeHTML(writeDiv.innerHTML);
   }

```