・・・じゃないですか！？ というゴミのような口癖を治したい！

気になる口癖ありませんか？
私はしばしば講義やプレゼンの際、語尾で「・・・じゃないですか！？」と言ってしまいます；；

例）

- みなさんJavaScriptでコード書いてるじゃないですか！？
- Pythonって初心者向けといいつつ環境構築難しいじゃないですか！？

普段の話し言葉であれば問題ないですが、公式の場で私の話を聞いている立場からすると、「いや、それを聞きたいと思っているのだからいきなり同意を求められても知らんがな・・・」という話ですね

## 自覚症状がないからテクノロジーで解決する

最初の内は言わないように気を付けようと思っていたのですが、困ったことに自分自身でも発言している自覚症状がありませんでした。

仕方がないのでテクノロジーの力で解決することにしました。作成したwebアプリケーションが次のリンク先のものとなります

> <a href="https://word-checker.netlify.app/">
>   <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/3fbdda4e-dd2f-f6ad-ed8b-531966f05f48.png" width="50%">
> </a>
> 
> アクセスして使えるかと思います

https://word-checker.netlify.app/

### 使い方

- お使いのPCもしくはスマートフォンでアクセスします
- `start`を押すと許可を求められるので許可してください
- ・・・じゃないですか？という語尾を検知して**リアルタイムに音がなります**
- 発言した回数がカウントされているので、最後に確認して反省します

スマートフォンでご利用いただいてIoT的に使っていただいても良いかと思いますし、web会議であればご利用しているPCのブラウザからアクセスいただいても良いかと思います

但し、PCからのアクセスだとたまに非アクティブになってしまうので、安定してずっと使いたいのであればスマートフォンをIoT的に使うのが良いですね（~~わずかながらのIoT要素アピール~~）

## 実際に使ってみた

昨日web会議で実際に使ってみました！

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/b46ff07f-16f7-2aa2-2ea8-17221003f873.png" width=70%>
> 
> 冒頭で少し説明してから使ってみました

想定以上に精度よく音声をとってくれて、リアルタイムでフィードバックしてもらえるのはありがたかったです！
このタイミングで自分はこの発言をしているんだなというのが把握できましたね。

また、実際に違う言葉で使ってみたい（このときは「じゃないですか」のみ対応していた）という意見もいただいたので、その部分を反映させて言葉を入力できるように改修してみました。

**複数の言葉に対して判定するところは作成していません**ので、次に機能追加するとすればその部分でしょうか
 

## 開発環境

> GitHub
> 
https://github.com/watanabe-tsubasa/word_checker/

<details><summary>Package.json</summary>

```json
{
  "name": "app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "@types/dom-speech-recognition": "^0.0.4",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.43",
    "@types/react-dom": "^18.2.17",
    "@typescript-eslint/eslint-plugin": "^6.14.0",
    "@typescript-eslint/parser": "^6.14.0",
    "@vitejs/plugin-react-swc": "^3.5.0",
    "eslint": "^8.55.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.5",
    "typescript": "^5.2.2",
    "vite": "^5.0.8"
  }
}
```
</details>

ランタイムとして最速の肉まん

https://bun.sh/

## 実行環境

- ホスティング
  - Netlify
- ブラウザ
  - Google Chrome ver. 120.0.6099.110
  - iOSの`safari` `Chrome`

Braveのようなシールドが強いブラウザではWebSpeechAPIでエラーがでてしまいます。
実行して`start`ボタンが`stop`に変わっていれば実行できているかと思います

### 開発上のお話

UI要素（というほどのUIでもありませんが）はReactで作成し、面倒だったのでもとのCSSをほぼほぼ活かしました。変更したのはinput要素のスタイル程度です。

#### 音声認識の部分

whisperAPIなども検討しましたが、リアルタイムでのフィードバックという点を重要視してWeb Speech APIを利用しました

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

これをReactで使いやすいようにHooksの形式にラップし、いろいろなアプリケーションで使いまわせるようにしています

<details><summary>Speech Recognition の Hook</summary>

```ts
import { useState, useEffect } from 'react';

export const useSpeechRecognition = () => {
  const [transcript, setTranscript] = useState<string>('');
  const [isListening, setIsListening] = useState<boolean>(false);
  const [error, setError] = useState<string>('');

  const resetTrascript = () => {
    setTranscript('');
  }

  useEffect(() => {
    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SpeechRecognition) {
      setError('Your browser does not support Speech Recognition.');
      return;
    }

    const recognition = new SpeechRecognition();
    recognition.continuous = true;
    recognition.interimResults = true;
    recognition.lang = 'ja-JP'; // 日本語に設定

    recognition.onresult = (event) => {
      for (let i = event.resultIndex; i < event.results.length; i++) {
        const transcript = event.results[i][0].transcript;
        if (event.results[i].isFinal) {
          setTranscript((prevTranscript) => prevTranscript + transcript + ' ');
        }
      }
    };

    recognition.onerror = (event) => {
      setIsListening(false);
      setError('Speech recognition error: ' + event.error);
    };

    if (isListening) {
      recognition.start();
    } else {
      recognition.stop();
    }

    return () => {
      recognition.stop();
    };
  }, [isListening]);

  return {
    transcript,
    isListening,
    error,
    startListening: () => setIsListening(true),
    stopListening: () => setIsListening(false),
    setTranscript,
    resetTrascript
  };
};
```

</details>

#### ブザーの音声

音声ファイル自体はネットの無料の物を利用しました。これを同じくAudioPlayerのAPIをHooksとしてまとめています

### 使いたい！！という声があったので

最初は「・・・じゃないですか」という部分だけを拾っていたのですが、使ってみたいという声がありましたので、入力した言葉を拾えるような感じにしてみました

具体的には`inputタグ`を追加してその中の言葉をstateで管理（デフォルトはじゃないですか） 。stateの文字列に対して判定をおこなうという改修を行いました。まだうまくいくか試していませんが、エラーも出ず問題なくデプロイできたので大丈夫だと思います。

### とても細かいuseEffectの話（React興味ある方以外は読み飛ばしてください）

App.tsxの中で発した言葉のトラッキングとその言葉を配列に追加するロジックを`useEffect`の中で実装していますが、これは公式のアンチパターンとなるため非常に良くないです

```tsx
  useEffect(() => {
    if (transcript.length > 0){
      setTranscripts(transcripts => {
        const newTranscripts = [...transcripts, transcript];
        return newTranscripts.length > 5
        ? newTranscripts.slice(-5)
        : newTranscripts;
      })
      if (transcript.includes(ngWord)) {
        setCount(current => current + 1);
        play();
      }
      reseter();
    }
    console.log(transcripts)
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [transcript, reseter, transcripts])
```

具体的な変更としては

- useSpeechRecognition の戻り値としてtranscripts(話した言葉の配列）を返すようにする
- App.tsxにあるuseEffect内の配列変更、トラッキングの部分はuseSpeechRecognition内にすべて記述し、依存配列として`transcripts`のみを指定する
- App.tsxのuseEffectはtranscriptsの配列の最後の要素を見て、音声を再生する機能のみを実装する

つまり、

```diff_tsx
useEffect(() => {
-    if (transcript.length > 0){
-      setTranscripts(transcripts => {
-        const newTranscripts = [...transcripts, transcript];
-        return newTranscripts.length > 5
-        ? newTranscripts.slice(-5)
-        : newTranscripts;
-      })
      if (transcript.includes(ngWord)) {
        setCount(current => current + 1);
        play();
      }
^      reseter();
    }
^    console.log(transcripts)
  // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [transcript, reseter, transcripts])
+  }, [transcripts])
```

こんな感じで残りは`useSpeechRecognition.ts`で完結させた方がよいです。まあ、動いているのでとりあえず良いかということで回収予定はありません

## 出ない壮大な記事より出せる記事

当初想定していたアドベントカレンダーの方向性とは異なる記事となりましたが、出せない記事よりは出せるアウトプットをさっさとしてしまおうということでこの記事を書かせていただきました。

カスタマーアナライザーは全くノータッチというわけではなく、**Atom Cam2をハックする途中まではいけている**ので、近日中には続報を出せると思っています。こちらも実装でき次第LTさせていただき、いろんなお店に取り付けられたらなと思っています