Raspberry Pi 5 で無課金、モニター不要、自動で会話を記録するスマートスピーカーを作成

大学の講義でICT実践なるものがあり、スマートスピーカーを作ったのでそのプロセスを記録しておこうと思います。

systemdでの自動実行やOpenJTalkのインストールなどを行ったため、linux周りについての知識も身についたと思います。

本記事ではpythonコードの解説はほぼ行わず、

環境構築やライブラリのインストール、systemdの設定などをメインに扱っています。

pythonのコードについてはgithubのコードを見ればおおよそわかります。

`GPIO_○○`の記述を全削除すれば、その処理過程や内容はかなりわかりやすくなるはずです。

**主な目次**
- [１．環境構築](#１環境構築)
- [２．必要なライブラリのインストール](#２必要なライブラリのインストール)
- [３．Main.py実行のために必要な設定](#３mainpy実行のために必要な設定)
- [４．systemdを使った自動実行とその他の実装](#４systemdを使った自動実行とその他の実装)
- [使い方メモ](#使い方メモ)
    - [自分でダウンロードしたhtsを使いたい](#自分でダウンロードしたhtsを使いたい)
- [おまけ](#おまけ)
    - [RaspberryPi-5で直接文字起こししたときの速度](#raspberrypi-5で直接文字起こししたときの速度)
    - [systemdの`ENVIRONMENT="XDG_RUNTIME_DIR"="/run/user/***自分のユーザーid***"`とは何？](#systemdのenvironmentxdg_runtime_dirrunuser自分のユーザーidとは何)


## 何をつくるの？

![スクリーンショット 2025-01-18 175309.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3984207/071c1138-6224-0b8f-a758-56b9953830f7.png)


Geminiとfaster-whisperを使って、無課金のスマートスピーカーを作ります。

もともとChatGPTと通信したかったのですが、ChatGPT apiのクレジット購入ができませんでした。
そのためGeminiの無料枠を利用します。

ちなみに、ChatGPT apiの無料クレジット付与については2025年1月17日現在、すでに終了しています。

ChatGPT apiの使用を想定していたため、入力内容をDeepLで英訳してからAPIに送信したりする機能もあります。
（元々ChatGPTと通信する場合にトークン数をなるべく節約したかった）

そのため、ChatGPTと通信する処理については中途半端に終わっています。

（ちゃんと通信が出来ていることは確認したが、応答の読み上げやGPTに渡す会話履歴の機能などについては動作未確認）

スマートスピーカーとの会話はスプレッドシートに記録され、設定したメールアドレスに送信されます。

全体の処理内容はこんな感じ。

![スクリーンショット 2025-01-18 172002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3984207/86f30ca3-dfa5-8ec3-484b-64b3d22a4535.png)


今回作成したプログラムはすべてgithubに公開されています。

GPTに聞いたらライセンスを書いておけと言われたのでひとまずMITライセンスにしておきます。

https://github.com/narikyow/RaspGPT

これをダウンロードして、お手元のRaspberryPi 5 で実行できるようにします。

### 参考元

https://qiita.com/nijigen_plot/items/5f5299af6aebc54b42d3

https://qiita.com/Nekonun/items/2de0d5b3c77206c5ba31


## 使用機器
Raspberry Pi 5 8GBモデル

USBスピーカー：　サンワサプライ　MM-SPU218K

https://www.amazon.co.jp/%E3%82%B5%E3%83%B3%E3%83%AF%E3%82%B5%E3%83%97%E3%83%A9%E3%82%A4-PC%E3%82%B9%E3%83%94%E3%83%BC%E3%82%AB%E3%83%BC-%E3%83%91%E3%83%83%E3%82%B7%E3%83%96%E3%83%A9%E3%82%B8%E3%82%A8%E3%83%BC%E3%82%BF%E3%83%BC%E6%90%AD%E8%BC%89-%E9%9B%BB%E6%BA%90%E3%83%9C%E3%82%BF%E3%83%B3%E4%BB%98%E3%81%8D-MM-SPU21BK/dp/B0CKKW6Z32

USBマイク：　MillSO 

https://www.amazon.co.jp/MillSO-360%C2%B0%E9%9B%86%E9%9F%B3-%E8%A7%92%E5%BA%A6%E8%AA%BF%E6%95%B4%E5%8F%AF%E8%83%BD-%E3%83%95%E3%83%AC%E3%82%AD%E3%82%B7%E3%83%96%E3%83%AB%E3%82%A2%E3%83%BC%E3%83%A0-%E5%A4%96%E4%BB%98%E3%81%91%E3%83%9E%E3%82%A4%E3%82%AF/dp/B0CHTVQK9D/355-5686865-6039017?pd_rd_w=1g2lt&content-id=amzn1.sym.4343d45b-f176-499d-8a3f-cf22b2eee6a1&pf_rd_p=4343d45b-f176-499d-8a3f-cf22b2eee6a1&pf_rd_r=4ZMV8DJBW15P6CDGGHHM&pd_rd_wg=5B9BV&pd_rd_r=af3fc44c-032a-406a-9abf-d57730d4ca7c&pd_rd_i=B0CHTVQK9D&psc=1

## 実行環境
Raspberry Pi OS 64bit

python 3.9.21

pyenvを利用して、仮想環境上ですべてのプログラムを実行しています。

## １．環境構築
### pyenvのインストール、パス設定
brewの方が良いと聞きますが、自分の場合curlで行いました。

GPTにお願いして出てきたコマンドをそのまま実行してます。

```bash
curl https://pyenv.run | bash
```

ホームディレクトリ直下にある.`bashrc`ファイルに以下の内容を末尾に追加し、pyenvのパスを通します。

```bash: .bashsrc
# 他のいろんな記述...

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)"
eval "$(pyenv init -)"
```

`.bashrc`の変更内容を反映

```bash
source ~/.bashrc
```
これでpyenvコマンドが使えるようになります。

以下、pythonのインストールです。

`pyenv install --list`で利用可能なpythonのバージョンを確認できます。

今回はpython 3.9.21をインストールしていきます。

```bash
pyenv install 3.9.21
```

インストールが終わったら、今回使用する仮想環境を構築していきます。

`RaspGPT`を仮想環境の名前とします。

```bash
pyenv virtualenv 3.9.21 RaspGPT
```

これで仮想環境を構築することができます。
次は仮想環境を有効にします。

```bash
pyenv activate RaspGPT
```

これで仮想環境に入れます。

## ２．必要なライブラリのインストール
### sudo apt install 編
忙しい方向けに。まとめた記述はこれ
```bash
sudo apt install portaudio19-dev
sudo apt-get install open-jtalk
sudo apt-get install open-jtalk-mecab-naist-jdic hts-voice-nitech-jp-atr503-m001
```


pyaudioのインストールに失敗しないようにインストール
```bash
sudo apt install portaudio19-dev
```

OpenJTalkのインストール
```bash
sudo apt-get install open-jtalk
sudo apt-get install open-jtalk-mecab-naist-jdic hts-voice-nitech-jp-atr503-m001
```

英語ならこれもある。（今回は使わなかった。）
```bash
sudo apt install espeak-ng espeak-ng-data
espeak-ng "Hello, this is a test of the eSpeak-ng engine."
```

特に`portaudio19-dev`は、古い方の名前（たしか`portaudio-dev`だったか……？）がよく出てくるので、注意です。

他にも色々インストールはしましたが、そのへんはエラーが出たら適宜インストールをお願いします。

ChatGPTに投げたり、ググれば出てくるはずです。

### OpenJTalkの準備
OpenJTalkとは、
先程のコマンドを実行すれば、
`/usr/share/hts-voice/nitech-jp-atr503-m001/`に`nitech_jp_atr503_m001.htsvoice`
というファイルがダウンロードされます。

これが音声モデルです。
また、
`/var/lib/mecab/dic/open-jtalk/`に`naist-jdic`という辞書がダウンロードされます。

デフォルト音声モデルでも良いのですが、せっかくなので他のモデルもダウンロードします。

調べたところ初音ミクモデルなんかも有志の方が作られているとか。

ですが、今回はいろんなところで挙げられている「MEI」というhtsファイルをダウンロードします。

https://sourceforge.net/projects/mmdagent/files/MMDAgent_Example/MMDAgent_Example-1.6/MMDAgent_Example-1.6.zip/download

ここからダウンロードできるので、ダウンロードしたら解凍して、

`MMDAgent_Example-1.8/Voice/mei`

にある.htsファイルを、

`/usr/share/hts-voice/`

以下に移動させます。

githubに上げたコードはデフォルトでMEIのボイスを設定してあるので注意してください。

### pip install 編
これをreqirements.txtに保存します。
```txt:reqirements.txt
ctranslate2
faster-whisper
openai
gpiozero
requests
pyaudio
pyttsx3
google-generativeai
```
次のコマンドを実行します。
```
pip install -r reqirements.txt
```
これで必要なものはまとめてインストールできます。

覚えておくと便利です。

ディレクトリはこんな配置になっています。

![スクリーンショット 2025-01-18 172522.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3984207/075fd183-8b25-bd6c-6596-e04183bd08e2.png)


## ３．Main.py実行のために必要な設定
ソースコードはgithubに上げているので、そこからダウンロードしてください。

https://github.com/narikyow/RaspGPT

以降はホームディレクトリ直下にクローンしたものと仮定して、すなわち

`~/RaspGPT`

の形でダウンロードされているという前提で説明を進めます

### GPIOと回路周り
こんな感じの回路を想定しています。


![スクリーンショット 2025-01-18 172510.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3984207/c350677e-37b3-f7d8-438c-bd7d9e477b33.png)


### faster-whisper-smallのダウンロード
```
cd ~/RaspGPT/modules
git clone https://huggingface.co/Systran/faster-whisper-small
```
git LFSの設定は別にいらないです。

ファイルサイズの関係上、「model.bin」をダウンロードできなかったりするので、

https://huggingface.co/Systran/faster-whisper-small/tree/main

から手動でダウンロードし、`modules/faster-whisper-small`のmodel.binに上書きします。

mediumモデルやlargeモデルも同じやりかたでダウンロードできます。


### Main.pyにapiキーを設定。
自分で取得をお願いします。
Main.pyを開くとmain関数内冒頭のほうにAPIキーを格納するための変数があるのでそこに記述をお願いします。

`GAS_URL`は後ほど。

```python:Main.py
main():
    # ....
    
    API_KEY_DEEPL = "hogehoge_DEEPL"
    API_KEY_GPT = None
    API_KEY_GEMINI = "hogehoge_GEMINI"
    GAS_URL = "hogehoge_GAS_URL"
    # ....
```

### USBマイクの設定
`modules/Record.py`では、録音を行う際にUSBデバイスのインデックスが必要。
そのために使用するマイクのデバイス名を調べます。

マイクを抜き差ししながら
```
lsusb
```
とするとマイクのデバイス名が大体わかります。自分の場合は

```
Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 003 Device 002: ID 1a81:1004 Holtek Semiconductor, Inc. Wireless Dongle 2.4 GHZ HT82D40REW
Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 002: ID 1b3f:5580 Generalplus Technology Inc. SF-558
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
```
こんな感じで表示されて、抜き差しの結果
```
Bus 001 Device 002: ID 1b3f:5580 Generalplus Technology Inc. SF-558
```
SF-558というのがマイクの名前だとわかりましたので、`Main.py`の`INPUT_DEVICE_INDEX`を
```
def main():
    # ...
    
    INPUT_DEVICE_INDEX = 'SF-558'
    # ...
```
と書き換えます。

ハードウェア名がかぶってたりすると不具合を起こす可能性があるので注意をお願いします。

不具合が起きた場合pyaudio側で認識されているデバイス名をフルで入力すれば確実なので、

```bash:~/RaspGPT
python modules/Record.py
```
としてデバイス名の一覧を取得し、コピペすればうまく行くはずです。（多分エラーが出ますが）

### Google SpreadSheetの設定

Google SpreadSheetにアクセス。適当な名前のスプレッドシートを作り、

**1行目、A列から順に　Input | TranslatedInput | Response | DateTime** 

と記述します。

記述したら、 **「拡張機能」＞「AppsScript」**

として、
以下のGASを記述します。

**メールアドレス**については自分で記述をお願いします。

```javascript:RaspGPTdataRecord
function doPost(e) {
  //data = {"Input":"test", "TranslatedInput":"test", "Response":"test"}
  const timestamp = new Date();
  const mailAddress = "＜任意のメールアドレス＞";
  const mailSubject = `RaspGPT@${timestamp}`;

  try {
    // 受信データをパース
    Logger.log(e)
    const data = JSON.parse(e.postData.contents);

    // スプレッドシートを取得
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1");
    Logger.log(sheet)

    // スプレッドシートのカラム名を取得（1行目がカラム名と仮定）
    const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
    Logger.log(headers)

    // データのキーを確認して不一致がある場合はエラーを返す
    const dataKeys = Object.keys(data);
    const invalidKeys = dataKeys.filter(key => !headers.includes(key));
    if (invalidKeys.length > 0) {
      throw new Error(`Invalid keys detected: ${invalidKeys.join(", ")}. Allowed keys are: ${headers.join(", ")}.`);
    }

    // データを追加する行を作成
    const row = headers.map(header => (header in data ? data[header] : "")); // マッチするデータがあれば値を、それ以外は空白に
    row[row.length-1] = timestamp; // 最後の列にタイムスタンプを追加


    // 新しい行をスプレッドシートに追記
    sheet.appendRow(row);

    // JSONの内容を改行区切りで文字列に直す
    const mailMassage = row.join("\n\n");

    // メールを送信
    eMailSend(mailAddress, mailSubject, mailMassage);

    // 成功レスポンスを返す
    return ContentService.createTextOutput(
      JSON.stringify({ status: "success", message: "Data appended successfully." })
    ).setMimeType(ContentService.MimeType.JSON);

  } catch (error) {
    // エラーレスポンスを返す
    Logger.log(error)
    return ContentService.createTextOutput(
      JSON.stringify({ status: "error", message: error.message+" __mail send error__ " })
    ).setMimeType(ContentService.MimeType.JSON);
  }
}

function eMailSend(mailAddress, mailSubject, mailMassage) {
  try {
    // メール送信
    GmailApp.sendEmail(mailAddress, mailSubject, mailMassage);
  } catch (error) {
    Logger.log(error)
    // エラー時のレスポンスを返す
    return ContentService.createTextOutput(
      JSON.stringify({ status: "error", message: error.message })
    ).setMimeType(ContentService.MimeType.JSON);
  }
}
```

記述したら、ウェブアプリケーションとしてデプロイします。

**アクセスできるユーザーは「全員」** とします。

デプロイしたら、GASを実行するためのURLが表示されるので、それをコピー。

Main.pyの **GAS_URL** に貼り付けます。

これで、SpreadSheet.pyから先程記述したGASを実行し、スプレッドシートへの記録とメール送信を行うことができます。

一応、ここまでの設定を行えば

```bash
cd ~/RaspGPT
python Main.py
```
とするだけで実行はできるはずです。

## ４．systemdを使った自動実行とその他の実装
ここからはsystemdを使って機能を追加していきます。

RaspberryPi5では、/etc/rc.localを使った自動起動ができないと思われます。

また、今回は

* Main.pyがUSB接続デバイスを取り扱う
* pyenvの仮想環境からプログラムを自動で実行する

という２点があるため、一部bashファイルの記述やserviceファイルの記述内容にも注意が必要です。

これから、以下の機能の実装手順を解説していきます。

1. プログラム（Main.py）の自動実行
2. 特定のSSIDに対するwifi自動接続
3. シャットダウンボタンの実装

### 共通手順

以下のコードを実行して、`start_raspgpt.sh`, `wifi_reconnect.sh`, `button_shutdown.sh` すべてに実行権限を付与します。

これがないとsystemdからバッシュファイルを実行できません。

```bash: ~/RaspGPT/
chmod +x *.sh
```


あとで使うので、自分のユーザーidを確認してください。

```bash
id
```

一番最初にでてくるuidという番号を使います。

もし登録しているユーザが複数なら、

```bash
logname
```
で自分のユーザー名を確認して、どれがidの出力結果から自分のidを特定してください。

以下、それぞれの機能について説明します。


### 1. プログラム（Main.py）の自動実行

#### start_raspgpt.shの編集

まず、start_raspgpt.shを開いて、\*\*\*で囲ってある部分を編集します。

```bash:start_raspgpt.sh
#!/bin/bash

# pyenv の環境変数とパスを設定
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"

# pyenv を初期化
eval "$(pyenv init --path)"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"

# スピーカーの音量調整
amixer sset Master 80%

# 指定の仮想環境を有効化
pyenv shell RaspGPT

# スクリプト実行ディレクトリに移動
cd /home/***自分のユーザー名***/RaspGPT

# Python スクリプトを実行
python Main.py

```
#### raspgpt.serviceの編集

`_service`ディレクトリ以下に`service`ファイルが格納されているので、これを編集します。

```bash
cd ~/RaspGPT/_service
sudo nano raspgpt.service
```

この記述内容のうち、\*\*\*で囲ってある箇所を編集してください。
```service:raspgpt.service
[Unit]
Description=Raspgpt Auto Start Service
After=network.target

[Service]
Type=simple
User=***自分のユーザー名***
WorkingDirectory=/home/***自分のユーザー名***/RaspGPT
ExecStart=/home/***自分のユーザー名***/RaspGPT/start_raspgpt.sh
Environment="XDG_RUNTIME_DIR"=/run/user/***自分のユーザーid***
Restart=always
RestartSec=7

[Install]
WantedBy=multi-user.target
```

記述したら、`Ctrl + S`で保存し、`Ctrl + X`でエディタを閉じて、

```bash:~/RaspGPT/_service/
sudo cp raspgpt.service /etc/systemd/system/raspgpt.service
sudo systemctl daemon-reload
sudo systemctl enable raspgpt.service
sudo systemctl start raspgpt.service
```

としてやれば、systemdがラズパイ起動時に毎回

`/home/***自分のユーザー名***/RaspGPT/start_raspgpt.sh`

を実行し、その結果Main.pyが実行されるようになります。

もしこれでMain.pyがうまく動かない場合は、

```bash
sudo journalctl -u raspgpt.service
```

としてやればサービスのログを確認できるのでここから原因を探りましょう。

2\. と 3. のおおまかな流れは`start_raspgpt.service`と一緒なので、同じ箇所は説明を省きます。

### 2. 特定のSSIDに対するwifi自動接続
wifiが切れたりしても勝手に再接続を試みるバッシュファイル。

時々このバッシュファイルが動いていてもGUI側に接続するか否かのウィンドウが表示されることがある。

なんとなく、wifiのSSIDを変えると次回起動時に聞いてくる印象です。

実際に使う場合はそこに気をつければ大丈夫。
#### wifi_reconnect.shの編集
SSIDとネットワークインターフェースを記述
```bash:wifi_reconnect.sh
#!/bin/bash

# 再接続を試みるSSIDのリスト（必要に応じて追加・変更）
TARGET_SSIDS=("***SSIDを設定***")

# チェックするネットワークインターフェース名（例: wlan0）
WIFI_INTERFACE="***ネットワークインターフェースを設定***"

# 接続確認と再接続試行のループ
while true; do
    # 現在の接続状態をチェック
    if nmcli -t -f DEVICE,STATE device status | grep "^${WIFI_INTERFACE}:connected" > /dev/null; then
        echo "Wi‑Fiは接続済みです。"
	break
    else
        echo "Wi‑Fi接続がありません。再接続を試みます..."

        # 各SSIDに対して再接続を試みる
        for ssid in "${TARGET_SSIDS[@]}"; do
            echo "SSID: $ssid への接続を試行中..."
            nmcli device wifi connect "$ssid" ifname "$WIFI_INTERFACE"
            # 接続に成功したらループを抜ける
            if nmcli -t -f DEVICE,STATE device status | grep "^${WIFI_INTERFACE}:connected" > /dev/null; then
                echo "$ssid に接続しました。"
                break
            fi
        done
    fi

    # 一定間隔をおいて再チェック（例: 60秒）
    sleep 3
done

```

#### wifi_reconnect.serviceの編集

```bash
cd ~/RaspGPT/_service
sudo nano wifi_reconnect.service
```

ユーザー名を記述

```service:wifi_reconnect.service
[Unit]
Description=Wi-Fi Reconnect Service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart= /home/***ユーザー名***/RaspGPT/wifi_reconnect.sh
Restart=on-failure
User=***ユーザー名***
WorkingDirectory=/home/***ユーザー名***/RaspGPT

[Install]
WantedBy=multi-user.target

```

以下のコマンドを実行

```bash:~/RaspGPT/_service/
sudo cp wifi_reconnect.service /etc/systemd/system/wifi_reconnect.service
sudo systemctl daemon-reload
sudo systemctl enable wifi_reconnect.service
sudo systemctl start wifi_reconnect.service
```

#### NetworkManagerのディスパッチャディレクトリを編集
ネットワークの状態が変化した時に自動で実行される。

以下のコマンドを実行。

```bash
sudo nano /etc/NetworkManager/dispatcher.d/99-restart-wifi-reconnect
```

次の内容を記述

```bash:99-restart-wifi-reconnect
#!/bin/bash

INTERFACE="$1"
STATUS="$2"

# 特定のWi-Fiインターフェースと切断イベントを検知
if [ "$INTERFACE" = "wlan0" ] && [ "$STATUS" = "down" ]; then
    systemctl restart wifi_reconnect.service
fi
```

### 3. シャットダウンボタンの実装
RaspberryPi5にデフォルトで付いている電源ボタンを長押しした場合、正常終了しているのか怪しかったためシャットダウンボタンも一応実装。

ボタンが押されるとコマンド

`sudo shutdown -h now`

が実行される。

3番ピンと6番ピンをつないでシャットダウンボタンを作る記事もあったが、

RaspberryPi5でこの機能は排除されたと思われる。

#### button_shutdown.shの編集
ユーザー名を記述
```bash:button_shutdown.sh
#!/bin/bash

# pyenv の環境変数とパスを設定
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"

# pyenv を初期化
eval "$(pyenv init --path)"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"

# 指定の仮想環境を有効化
pyenv shell RaspGPT

# スクリプト実行ディレクトリに移動
cd /home/***自分のユーザー名***/RaspGPT

# Python スクリプトを実行
python button_shutdown.py

```

#### button_shutdown.serviceの編集

```bash
cd ~/RaspGPT/_service
sudo nano button_shutdown.service
```
ユーザー名を記述

```service:button_shutdown.service
[Unit]
Description=Button Triggered Shutdown
After=multi-user.target

[Service]
ExecStart=/home/***自分のユーザー名***/RaspGPT/button_shutdown.sh
Environment="XDG_RUNTIME_DIR"=/run/user/***ユーザーid***
Restart=always
RestartSec=7
User=***自分のユーザー名***

[Install]
WantedBy=multi-user.target
```

以下のコマンドを実行

```bash:~/RaspGPT/_service/
sudo cp button_shutdown.service /etc/systemd/system/button_shutdown.service
sudo systemctl daemon-reload
sudo systemctl enable button_shutdown.service
sudo systemctl start button_shutdown.service
```

## これで完成（のはず）



以上の手順を踏めば、RaspberryPi起動時にMain.pyが実行され、そのほか諸々の機能も正常に実行されるはずです。

## 今回目指したこと
今回作るスマートスピーカーは、このような特徴があります。

* ローカルで文字起こし
* DeepLと通信して翻訳
* Geminiと通信（何気にあんまり見かけない）
* 送られてきた内容を逐次読み上げる（レスポンスの高速化）
* スプレッドシートに内容を記録、メール送信
* GPIOピンからのボタン入力で処理を行うため、モニター不要
* シャットダウンボタンを実装することで安全に終了可能
* ラズパイ起動時に自動でプログラムが起動（プログラムが終了してもすぐに再度実行される）
* 指定したSSIDと接続できない場合、再接続を試みる

さっくり言うと

**実用的なスマートスピーカーを目指して作りました。**

かなり多機能になっているかと思われます。
尚の事、ChatGPT APIを使えなかったことが悔やまれます。


## 使い方メモ
### 自分でダウンロードしたhtsを使いたい
自分で追加したいモデルがあれば適宜`.hts`ファイルをダウンロードして、

`/usr/share/hts-voice/`

に追加して、`modules`ディレクトリ内の`VoReading.py`の`voice_paths`を編集して、
```python:RaspGPT/modules/VoReading.py
        # Set default paths
        voice_paths = {
            "DEFAULT": "/usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice",
            "MEI_ANG": "/usr/share/hts-voice/mei_angry.htsvoice",
            "MEI_BAS": "/usr/share/hts-voice/mei_angry.htsvoice",
            "MEI_HAP": "/usr/share/hts-voice/mei_angry.htsvoice",
            "MEI_NOR": "/usr/share/hts-voice/mei_angry.htsvoice",
            "MEI_SAD": "/usr/share/hts-voice/mei_angry.htsvoice",
            "新しく追加する音声の名前": "usr/share/hts-voice/追加したhtsのファイル名"
        }
```
`Main.py`の
```python: RaspGPT/Main.py
    #=============================
    # OpenJTalkの設定
    #=============================

    # 話すスピード
    SPEED = 1.3

    # 声の種類
    VOICE_TYPE = "新しく追加する音声の名前"

    # 辞書のパス
    DIC_PATH = None
```
`VOICE_TYPE`を変更してください。これでモデルの変更が可能です。

## おまけ

### RaspberryPi 5で直接文字起こししたときの速度

`faster-whisper`INT8量子化モデルを使用しています。

smallモデルでは`batch_size=3`, `beam_size=1`

mediumモデルでは`batch_size=1`, `beam_size=1`

デフォルトで設定しています。

使用するモデルについては、

`WHISPER_MODEL`を変更することで変えられます。



#### 使ってみた感じ

- - -

##### faster-whisper-small

普通にスマートスピーカーとして使うならこちらが推奨。

短い音声（4〜8秒）ならほぼ同じ秒数で処理可能。（9秒前後のことが多い）

長い音声（48秒とか）ならそれより短い時間（28秒前後）で処理可能。

`beam_size`が大きいと逆に精度が落ちることもあり、なんともいえない。

デフォルトでは 1 に設定して、応答速度を上げている。

`batch_size`は何度か試した結果、3がベストだった。

ただし精度に不安あり。

そのため、Main.pyでは`initial_prompt`を設定することで文字起こしの文脈が通りやすくなるよう調整している。

また、
* Geminiの最新の応答内容を文ごとに分割し、ユーザが設定した数だけランダムにサンプリングして文章を記録
* ユーザが最初に設定した`WHISPER_MODEL_CONTEXT_DEFAULT` に先程サンプリングした文章を加えて、それを`initial_prompt`に渡す

という処理を行っている。

これにより、会話の文脈にあった文字起こしをしやすくなるよう調整している。

が、それでもおかしな出力が返ってくることがある。

- - -
##### faster-whisper-medium
精度はsmallモデルよりも明らかに高い。

安定度が違う。

smallとmediumの間に超えられない壁があるんじゃないかと思うくらい差が歴然。

しかし、文字起こしにかかる時間が長い。

短い音声（4〜8秒）でも２０秒以上かかることがザラ。

長い音声（48秒）なら７０秒程度。

`batch_size` や `beam_size` も調整したが無理。

応答が遅くてもいいやって人はこっちを使うのがいいかも。

- - -
##### 結論
* **faster-whisper-smallがスマートスピーカーとして向いている**
* **faster-whisper-mediumは応答が遅くても良い、正確性を重視したい場合に向いている**

modules以下のpythonファイルからいじくれるので、興味がある方は試してみてください。



### systemdの`ENVIRONMENT="XDG_RUNTIME_DIR"="/run/user/***自分のユーザーid***"`とは何？

systemdは、システム環境でserviceファイル内で指定した内容を実行します。

この際ユーザーは指定されておらず、USBデバイスへのアクセスなどができません。

そのため、

```
Environment="XDG_RUNTIME_DIR"=/run/user/***自分のユーザーid***
```

として`XDG_RUNTIME_DIR`に`/run/user/***自分のユーザーid***`を指定してやることでserviceファイルの内容をどのユーザーとして実行するのかを指定しています。

エラーの原因がわからず調べていたところ、他の方がこの記述でうまくいったと報告していたので私も試してみたところうまくいきました。

よくわからなかったのでXDG_RUNTIME_DIRについてドキュメントを調べたところ、こんな内容が書かれていました。

https://www.freedesktop.org/software/systemd/man/latest/pam_systemd.html

以下ChatGPTの翻訳

*`pam_systemd`のDescription部分の翻訳*
>`pam_systemd` は、ユーザーのセッションを `systemd` のログインマネージャである `systemd-logind.service(8)` に登録し、それによって `systemd` のコントロールグループ階層に統合します。
>
>このモジュールは、ユーザーの JSON ユーザーレコード（定義されている場合）に基づき、セッションに対してさまざまなリソース管理やランタイムパラメータを適用します。
>
>---
>
>### **ログイン時の動作**
>
>`pam_systemd` は `systemd-logind.service` と連携して以下を実行します：
>
>1. **ユーザーのランタイムディレクトリの作成**:
>   - `/run/user/$UID` がまだ存在しない場合、新しい `tmpfs` ファイルシステムとしてディレクトリを作成またはマウントし、クォータを適用します。その所有権はログインしたユーザーに変更されます。
>
>2. **環境変数 `$XDG_SESSION_ID` の初期化**:
>   - 監査機能が利用可能で、`pam_loginuid.so` がこのモジュールの前に実行されている場合、監査セッションID（`/proc/self/sessionid`）から初期化されます。そうでない場合、独立したセッションカウンタが使用されます。
>
>3. **新しいシステムスコープユニットの作成**:
>   - セッション用に新しい `systemd` スコープユニットが作成されます。
>   - これがユーザーの最初の同時セッションである場合、自動的にユーザー専用のスライスユニットが `user.slice` 配下に作成され、その中にスコープが配置されます。
>   - また、`systemd` のユーザーマネージャインスタンスを実行する `user@.service` のインスタンスが開始されます。
>
>4. **ユーザー環境変数の設定**:
>   - ユーザーの JSON レコードに基づき、`$TZ`（タイムゾーン）、`$EMAIL`（メールアドレス）、`$LANG`（ロケール）の環境変数を設定します。
>   - また、ユーザーレコードで明示的に設定された環境変数をインポートし、`umask`、`nice` レベル、リソース制限を初期化します。
>
>---
>
>### **ログアウト時の動作**
>
>1. **プロセスの終了**:
>   - `logind.conf(5)` の設定で `KillUserProcesses=` が有効になっている場合、セッション内のすべてのプロセスが終了します。
>   - ユーザーの最後の同時セッションが終了した場合、`systemd` のユーザーインスタンスとユーザーのスライスユニットも終了します。
>
>2. **ランタイムディレクトリの削除**:
>   - ユーザーの最後の同時セッションが終了すると、`/run/user/$UID` とその内容も削除されます。
>
>---
>
>### **例外**
>
>- システムが `systemd` を初期化システムとして起動していない場合、このモジュールは何もせず、すぐに `PAM_SUCCESS` を返します。
>
>---
>
>この仕組みによって、ユーザーごとのセッション管理、プロセスのクリーンアップ、リソースの分離が適切に行われ、システムの安定性とセキュリティが向上します。


*ドキュメントのEnvironment `$XDG_RUNTIME_DIR`について*
>ユーザープライベートで書き込み可能なディレクトリへのパスであり、マシン上でユーザーがログインしている間に関連付けられるものです。このディレクトリは、ユーザーが初めてログインしたときに自動的に作成され、ユーザーが最後にログアウトしたときに削除されます。
>
>ユーザーが同時に2回ログインした場合、両方のセッションで同じ `$XDG_RUNTIME_DIR` とその内容が共有されます。一方、ユーザーが1回ログインし、その後ログアウトし、再びログインした場合、このディレクトリの内容はその間に消失します。ただし、アプリケーションはこの動作に依存せず、古いファイル（スタイルファイル）を扱えるようにする必要があります。
>
>このディレクトリにセッションごとのプライベートデータを保存するには、ファイル名に `$XDG_SESSION_ID` の値を含めるべきです。このディレクトリは、AF_UNIXソケット、FIFO、PIDファイルなど、ランタイムファイルシステムオブジェクトに使用されるべきです。また、このディレクトリはローカルであることが保証され、オペレーティングシステムが提供する可能な限りのファイルシステム機能セットを提供します。
>
>詳細については、[XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) を参照してください。なお、現在のユーザーがセッションの元のユーザーでない場合、`$XDG_RUNTIME_DIR` は設定されません。

わからなかったのでさらに調べる

https://specifications.freedesktop.org/basedir-spec/latest/

このサイトの `$XDG_RUNTIME_DIR` についての記述をChatGPTに翻訳させる。

*`$XDG_RUNTIME_DIR` 関連の記述について要約*

>`$XDG_RUNTIME_DIR` は、ユーザー固有の一時的なランタイムファイルやその他のファイルオブジェクト（例: ソケット、名前付きパイプなど）を保存するためのベースディレクトリを定義する環境変数です。 
>
>**主なポイント:**
>
>- **所有権とアクセス権**: このディレクトリはユーザー自身が所有し、ユーザーのみが読み書きアクセスできる必要があります。Unix のアクセスモードは `0700` に設定されるべきです。 
>
>- **ライフサイクル**: ディレクトリの寿命はユーザーのログインセッションに関連付けられています。ユーザーが最初にログインしたときに作成され、完全にログアウトしたときに削除される必要があります。 
>
>- **一貫性**: ユーザーが複数回ログインした場合、同じディレクトリが指し示され、最初のログインから最後のログアウトまで存在し続ける必要があります。 
>
>- **持続性**: このディレクトリ内のファイルは、システムの再起動や完全なログアウト/ログインサイクルを超えて持続しない必要があります。 
>
>- **ローカルファイルシステム**: ディレクトリはローカルファイルシステム上にあり、他のシステムと共有されない必要があります。また、オペレーティングシステムの標準に従った完全な機能を備えている必要があります。 
>
>これらの要件により、`$XDG_RUNTIME_DIR` はユーザーのランタイムデータの安全で一時的な保存場所として機能します。 


「`/run/user/$UID` がまだ存在しない場合、新しい `tmpfs` ファイルシステムとしてディレクトリを作成またはマウントし、クォータを適用します。その所有権はログインしたユーザーに変更されます。」

「なお、現在のユーザーがセッションの元のユーザーでない場合、`$XDG_RUNTIME_DIR` は設定されません。」


こうした記述から、systemd側でログインユーザーを設定するために`XDG_RUNTIME_DIR`を参照していると解釈しています。

ちなみに、systemdのserviceファイル内では
```
Environment=TEST_VALUE="hogehoge"
```
と記述することで変数を代入できたりするとか。

詳しく知りたい方はこちらを参考にどうぞ

https://qiita.com/yasushi-jp/items/97057509e96919c35f64


systemdのパラメータ（INSTALL, UNITなど）についてはこちらが参考になる

https://docs.redhat.com/ja/documentation/red_hat_enterprise_linux/7/html/system_administrators_guide/sect-Managing_Services_with_systemd-Unit_Files#sect-Managing_Services_with_systemd-Unit_File_Structure