0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Raspberry Pi5 を話し相手にしてみる

0
Posted at

背景

最近リモートワークをしがちなのですが、便利な一方で、「ちょっとした雑談」や「声に出して相談しながら考えを整理する時間」が、ほとんどなくなってしまいました。作業に詰まったとき、隣の人に話しかけているうちに自分で答えに気づく、ということがよくありましたが、一人で黙々と作業しているとそのきっかけがありません。
そこで、前から試してみたかった Raspberry Pi 5 でのローカルLLMと組み合わせて、机の上にいる、気軽に話しかけられる相棒を作ってみることにしました。
この記事では、その相棒「ラズ」ができるまでと、作ってみて分かったことを紹介します。

概要

作ったのは、「ヘイ、ラズ」と呼びかけると返事をして、語尾に「〜だっぴ」をつけて話す音声アシスタント「ラズ」です。
会話はファイルに保存されるので、ラズパイを再起動しても前に話したことを覚えています。

話しかけ方 ラズの動き
「ヘイ、ラズ」 「なんだっぴ?」と返事をして質問を待つ
「ヘイ、ラズ、〇〇は?」 続けて言った質問にそのまま答える
「ヘイ、ラズ、調子は?」 CPU温度やメモリ、ファンの回転数を調べて、自分の体調として答える
「ヘイ、ラズ、記憶を消して」 覚えたことを全部忘れる

使ったもの (ハードウェア)

以下のものを使用しました。

もの 用途・メモ
Raspberry Pi 5 本体。LLMを動かすのでメモリは8GB以上がおすすめ
公式 Active Cooler(ファン) 必須。LLMの推論中はCPUがかなり熱くなる
公式電源(5V/5A, 27W) 一般的なUSB-C充電器だと電力不足で起動しない
USB小型マイク 声の入力。USB 2.0ポートに接続
モニター(スピーカー内蔵) 画面表示と読み上げ音声の出力(HDMI経由)

使ったもの (ソフトウェア)

以下のものを使用しました。

ソフト 役割
Raspberry Pi OS (64-bit) OS
whisper.cpp(base モデル) 音声を文字に変換
Ollama + gemma3:1b 返事を生成するローカルLLM
Open JTalk + MMDAgent「メイ」の声 文章を音声で読み上げ
Python 3(標準ライブラリのみ) 全体をつなぐスクリプト

仕組み

マイクは常に音を聞いていて、「ヘイ、ラズ」で始まる言葉だけを LLM に渡します。返事は語尾を整えてから読み上げ、会話は記憶として保存します。

工夫した点

1. ボタンなしで話しかけられるようにした
マイクの音量を常に見て「声が始まった・終わった」を自動で判定し、聞き取った文の先頭に「ヘイ、ラズ」があるときだけ反応するようにしました。起動時に周りの雑音を測ってしきい値を決めるので、部屋の環境が変わっても調整しやすくしています。

2. 語尾「だっぴ」でキャラクターをつけた
システムプロンプトで語尾を指定していますが、小さいモデルはよく指示を忘れます。そこで、返ってきた文の語尾が「だっぴ」になっていなければ、プログラム側で「です」→「だっぴ」のように直す保険を入れました。

3. 自分の体調として状態を答える
「調子」「温度」などの言葉が入っていたら、CPU温度・メモリ・ファンの回転数・電圧不足の有無を実際に読み取り、LLMに渡しています。「CPUが熱い → 少し熱っぽいだっぴ」のように、ラズパイ自身の体調として話させることで、ラズパイで動かしている意味が出るようにしました。

4. 長く使っても重くならない記憶
直近の会話はそのまま保存し、古くなった分はLLMに要約させて「覚えていること」として残します。要約は返事を読み上げた後に行うので、会話の待ち時間は増えません。

5. 声を可愛くした
Open JTalk の標準の男性の声から、MMDAgent の「メイ」の明るい声に変え、少し高く・速くしました。声が変わるだけで、話しかけたくなる気持ちがかなり変わりました。

ソースコードの一部

スクリプトは Python の標準ライブラリだけで書いていて、外部のプログラムは subprocess で呼び出しています。

声の始まりと終わりを判定する

30ミリ秒ごとに音の大きさ(RMS)を計算し、しきい値を3回続けて超えたら話し始め、0.8秒静かになったら話し終わりと判断します。

raz.py
while True:
    frame = proc.stdout.read(FRAME_BYTES)
    loud = rms(frame) > self.threshold
    if not started:
        preroll.append(frame) 
        loud_run = loud_run + 1 if loud else 0
        if loud_run >= 3:
            started = True
            voiced = list(preroll)
    else:
        voiced.append(frame)
        silent = 0 if loud else silent + 1
        if silent >= end_silence or len(voiced) >= max_frames:
            break

語尾を「だっぴ」に直す

raz.py
ENDINGS = (("でしょう", "だっぴ"), ("ですね", "だっぴね"), ("ですよ", "だっぴよ"),
           ("です", "だっぴ"), ("だよ", "だっぴよ"), ("だね", "だっぴね"), ("だ", "だっぴ"))

if body and not body.endswith("っぴ"):
    for old, new in ENDINGS:
        if body.endswith(old):
            body = body[: -len(old)] + new
            break
    else:
        body += "だっぴ"

体調の情報をLLMに渡す

get_status() では /sys/class/thermal/thermal_zone0/temp(CPU温度)や /proc/meminfo(メモリ)、vcgencmd get_throttled(電圧不足や速度低下の有無)を読んでいます。

raz.py
status = get_status() if any(w in question for w in STATUS_WORDS) else None
if status:
    content = f"【ラズパイの状態】\n{status}\n\n【質問】\n{question}"

振り返り

今回は Raspberry Pi 5 の上で、音声認識(whisper.cpp)・ローカルLLM(Ollama + gemma3)・音声合成(Open JTalk)を組み合わせてみました。その結果、クラウドのAPIを一切使わずに、「ヘイ、ラズ」と呼ぶだけで返事をしてくれて、自分の体調を話したり、前に話した内容を覚えていたりする話し相手を、手のひらサイズのラズパイだけで作ることができました。
一番難しいと感じたのは、「いつでも話しかけたくなる」ような愛着を持たせる工夫です。話し相手である以上、まずは質問にきちんと答えられていることが前提です。そのうえで、返事が返ってくるまでのテンポや、喋り方・声の雰囲気によって、話しかけたくなるかどうかが大きく変わると感じました。語尾や声を変えるだけでも印象はかなり変わったので、このあたりはまだまだ工夫の余地がありそうです。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?