pdf-icon

StackFlow AI プラットフォーム

アプリケーション

CVビジョンアプリケーション

VLMマルチモーダル

大規模言語モデル (LLM)

音声アシスタント

AI Pyramid - Home Assistant

Home Assistant は、ローカライズされたデバイス管理と自動制御をサポートするオープンソースのスマートホームプラットフォームであり、プライバシー保護、安全で信頼性が高く、高度なカスタマイズが可能な特性を備えています。

1. 準備

メモリ構成
4GB メモリ版の AI Pyramid の場合、Home Assistant Docker イメージをインストールする前に、AI Pyramid 仮想メモリ調整チュートリアルを参考にメモリ割り当てポリシーを最適化する必要があります。

2. イメージのインストール

Home Assistant 公式ドキュメントを参照するか、以下の手順を実行して Docker コンテナをデプロイしてください。

  1. Home Assistant Docker イメージをプルする
  • /PATH_TO_YOUR_CONFIG は、設定を保存して実行したいフォルダを指します。:/config の部分は必ず残してください。
  • MY_TIME_ZONE は tz データベースの名前です(例:TZ=Asia/Tokyo)。
docker run -d \
  --name homeassistant \
  --privileged \
  --restart=unless-stopped \
  -e TZ=MY_TIME_ZONE \
  -v /PATH_TO_YOUR_CONFIG:/config \
  -v /run/dbus:/run/dbus:ro \
  --network=host \
  ghcr.io/home-assistant/home-assistant:stable

3. HAOS 初期化

  1. ブラウザから Home Assistant Web インターフェースにアクセスします:ローカルアクセスは http://homeassistant.local:8123/、リモートアクセスは http://デバイスIP:8123/
ネットワーク依存
Home Assistant OS の初回起動時には、ネットワークから必要なリソースをダウンロードする必要があり、このプロセスには数十分かかる場合があります。初期化がタイムアウトした場合は、接続を改善するためにプロキシ設定済みのネットワーク環境に切り替えることを推奨します。
  1. 画面の指示に従って管理者アカウントを作成し、システム初期設定を完了させます。

4. デバイスファームウェアのビルド

ESPHome に関する注意事項
AI Pyramid 上の Docker でデプロイされた Home Assistant の ESPHome プラグイン環境は不完全であり、ファームウェアのビルドや書き込み操作を直接実行することはできません。PC 側に独立した ESPHome ツールチェーンをインストールして、ファームウェアのビルドと書き込みを行うことを推奨します。以下では M5Stack CoreS3 を例に、ESPHome ファームウェアのビルドと書き込み手順を説明します。
  1. ESPHome 公式インストールガイドを参考に、開発用 PC に ESPHome 開発環境をデプロイします。

本書は ESPHome 2025.12.5 バージョンに基づいています。バージョン間で大きな差異がある場合があるため、プロジェクトの YAML 設定ファイルの要求に応じて適切なバージョンを選択してください。

pip install esphome==2025.12.5
  1. M5Stack ESPHome 設定ファイルリポジトリをクローンします。
git clone https://github.com/m5stack/esphome-yaml.git
  1. ESPHome Dashboard サービスを起動します。
esphome dashboard esphome-yaml/
  1. ブラウザで 127.0.0.1:6052 にアクセスします。
  1. Wi-Fi 接続パラメータを設定します。
# Your Wi-Fi SSID and password
wifi_ssid: "your_wifi_name"
wifi_password: "your_wifi_password"
  1. OpenSSL を使用して暗号化キーを生成します。
openssl rand -base64 32

キー生成の出力例:

(base) m5stack@MS-7E06:~$ openssl rand -base64 32
BUEzgskL8daDJ5rLD90Chq2M43jC0haA/vVxcULQAls=
  1. cores3-config-example.yaml 設定ファイルを編集し、生成した暗号化キーを該当フィールドに入力します。

左上の「INSTALL」ボタンをクリックしてビルドを開始します。

3 番目の項目を選択し、ターミナルでリアルタイムにビルド出力を確認します。

CoreS3 に対応するシリアルポートデバイスを選択します。

初回ビルド時には必要な依存関係が自動的にダウンロードされます。

ファームウェアのビルドおよび書き込みプロセスが完了するまで待ちます。

デバイスの再起動後、取得された IP アドレスを記録してください。これは後ほど Home Assistant でデバイスを統合する際に必要になります。

5. デバイスの追加

  1. Home Assistant の設定画面に入り、デバイスの追加を選択します。
  1. 統合リストで ESPHome を検索します。
  1. Host フィールドにデバイスの IP アドレスを入力し、Port フィールドには YAML 設定ファイルで定義されたポート番号を入力します。
  1. YAML 設定ファイルで定義された暗号化キーを入力します。
  1. 音声処理モードは、とりあえずクラウド処理(Cloud Processing)を選択します。
  1. 音声ウェイクワードおよび TTS エンジンのパラメータを設定します。
  1. 設定完了後、デバイスは Home Assistant の概要ページに表示されます。

6. ローカル音声アシスタントの設定

Wyoming Protocol を使用することで、Home Assistant にローカルの音声認識および合成機能を統合し、完全にオフラインな音声アシスタント体験を実現できます。

6.1 音声テキスト変換(ASR)の設定

ステップ 1:依存パッケージとモデルのインストール

システムに音声認識に必要な依存パッケージとモデルがインストールされていることを確認します:

apt install lib-llm llm-sys llm-asr llm-openai-api llm-model-sense-voice-small-10s-ax650
pip install openai wyoming

ステップ 2:Wyoming 音声テキスト変換サービスの作成

AI Pyramid 内で wyoming_whisper_service.py ファイルを新規作成し、以下のコードをコピーしてください:

#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
#
# SPDX-License-Identifier: MIT
"""
Wyoming protocol server for an OpenAI-compatible SenseVoice API.
Compatible with Wyoming protocol 1.8.0 for SenseVoice transcription.
"""

import argparse
import asyncio
import io
import logging
import wave
from functools import partial
from typing import Optional

from openai import OpenAI
from wyoming.asr import Transcribe, Transcript
from wyoming.audio import AudioChunk, AudioStart, AudioStop
from wyoming.event import Event
from wyoming.info import AsrModel, AsrProgram, Attribution, Info
from wyoming.server import AsyncServer, AsyncEventHandler

_LOGGER = logging.getLogger(__name__)


class SenseVoiceEventHandler(AsyncEventHandler):
    """Handle Wyoming protocol audio transcription requests."""

    def __init__(
        self,
        wyoming_info: Info,
        client: OpenAI,
        model: str,
        language: Optional[str] = None,
        *args,
        **kwargs,
    ) -> None:
        super().__init__(*args, **kwargs)

        self.client = client
        self.wyoming_info_event = wyoming_info.event()
        self.model = model
        self.language = language

        # Audio buffer state for a single transcription request.
        self.audio_buffer: Optional[io.BytesIO] = None
        self.wav_file: Optional[wave.Wave_write] = None

        _LOGGER.info("Handler initialized with model: %s", model)

    async def handle_event(self, event: Event) -> bool:
        """Handle Wyoming protocol events."""
        # Service info request.
        if event.type == "describe":
            _LOGGER.debug("Received describe request")
            await self.write_event(self.wyoming_info_event)
            _LOGGER.info("Sent info response")
            return True

        # Transcription request.
        if Transcribe.is_type(event.type):
            transcribe = Transcribe.from_event(event)
            _LOGGER.info("Transcribe request: language=%s", transcribe.language)

            # Reset audio buffers for the new request.
            self.audio_buffer = None
            self.wav_file = None
            return True

        # Audio stream starts.
        if AudioStart.is_type(event.type):
            _LOGGER.debug("Audio start")
            return True

        # Audio stream chunk.
        if AudioChunk.is_type(event.type):
            chunk = AudioChunk.from_event(event)

            # Initialize WAV writer on the first chunk.
            if self.wav_file is None:
                _LOGGER.debug("Creating WAV buffer")
                self.audio_buffer = io.BytesIO()
                self.wav_file = wave.open(self.audio_buffer, "wb")
                self.wav_file.setframerate(chunk.rate)
                self.wav_file.setsampwidth(chunk.width)
                self.wav_file.setnchannels(chunk.channels)

            # Append raw audio frames.
            self.wav_file.writeframes(chunk.audio)
            return True

        # Audio stream ends; perform transcription.
        if AudioStop.is_type(event.type):
            _LOGGER.info("Audio stop - starting transcription")

            if self.wav_file is None:
                _LOGGER.warning("No audio data received")
                return False

            try:
                # Finalize WAV payload.
                self.wav_file.close()

                # Extract audio bytes.
                self.audio_buffer.seek(0)
                audio_data = self.audio_buffer.getvalue()

                # Build in-memory file for the API client.
                audio_file = io.BytesIO(audio_data)
                audio_file.name = "audio.wav"

                # Call the transcription API.
                _LOGGER.info("Calling transcription API")

                transcription_params = {
                    "model": self.model,
                    "file": audio_file,
                }

                # Add language if explicitly set.
                if self.language:
                    transcription_params["language"] = self.language

                result = self.client.audio.transcriptions.create(**transcription_params)

                # Extract transcript text.
                if hasattr(result, "text"):
                    transcript_text = result.text
                else:
                    transcript_text = str(result)

                _LOGGER.info("Transcription result: %s", transcript_text)

                # Send transcript back to the client.
                await self.write_event(Transcript(text=transcript_text).event())

                _LOGGER.info("Sent transcript")
            except Exception as e:
                _LOGGER.error("Transcription error: %s", e, exc_info=True)
                # Send empty transcript on error to keep protocol flow.
                await self.write_event(Transcript(text="").event())
            finally:
                # Release buffers for the next request.
                self.audio_buffer = None
                self.wav_file = None

            return True

        return True


async def main() -> None:
    """Program entrypoint."""
    parser = argparse.ArgumentParser(
        description="Wyoming protocol server for OpenAI-compatible SenseVoice API"
    )
    parser.add_argument(
        "--uri",
        default="tcp://0.0.0.0:10300",
        help="URI to listen on (default: tcp://0.0.0.0:10300)",
    )
    parser.add_argument(
        "--api-key",
        default="sk-",
        help="OpenAI API key (default: sk-)",
    )
    parser.add_argument(
        "--base-url",
        default="http://127.0.0.1:8000/v1",
        help="API base URL (default: http://127.0.0.1:8000/v1)",
    )
    parser.add_argument(
        "--model",
        default="sense-voice-small-10s-ax650",
        help="Model name (default: sense-voice-small-10s-ax650)",
    )
    parser.add_argument(
        "--language",
        help="Language code (e.g., en, zh, auto)",
    )
    parser.add_argument(
        "--debug",
        action="store_true",
        help="Enable debug logging",
    )

    args = parser.parse_args()

    # Configure logging.
    logging.basicConfig(
        level=logging.DEBUG if args.debug else logging.INFO,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    )

    _LOGGER.info("Starting Wyoming SenseVoice service")
    _LOGGER.info("API Base URL: %s", args.base_url)
    _LOGGER.info("Model: %s", args.model)
    _LOGGER.info("Language: %s", args.language or "auto")

    # Initialize OpenAI client.
    client = OpenAI(
        api_key=args.api_key,
        base_url=args.base_url,
    )

    # Build Wyoming service metadata (protocol 1.8.0 compatible).
    wyoming_info = Info(
        asr=[
            AsrProgram(
                name=args.model,
                description=f"OpenAI-compatible SenseVoice API ({args.model})",
                attribution=Attribution(
                    name="SenseVoice",
                    url="https://github.com/FunAudioLLM/SenseVoice",
                ),
                version="1.0.0",
                installed=True,
                models=[
                    AsrModel(
                        name=args.model,
                        description=f"SenseVoice model: {args.model}",
                        attribution=Attribution(
                            name="SenseVoice",
                            url="https://github.com/FunAudioLLM/SenseVoice",
                        ),
                        installed=True,
                        languages=(
                            ["zh", "en", "yue", "ja", "ko"]
                            if not args.language
                            else [args.language]
                        ),
                        version="1.0.0",
                    )
                ],
            )
        ],
    )

    _LOGGER.info("Service info created")

    # Create server.
    server = AsyncServer.from_uri(args.uri)

    _LOGGER.info("Server listening on %s", args.uri)

    # Run server loop.
    try:
        await server.run(
            partial(
                SenseVoiceEventHandler,
                wyoming_info,
                client,
                args.model,
                args.language,
            )
        )
    except KeyboardInterrupt:
        _LOGGER.info("Server stopped by user")
    except Exception as e:
        _LOGGER.error("Server error: %s", e, exc_info=True)


if __name__ == "__main__":
    asyncio.run(main())

ステップ 3:音声テキスト変換サービスの起動

以下のコマンドを実行してサービスを起動します(IP アドレスは実際の AI Pyramid のアドレスに置き換えてください):

python wyoming_whisper_service.py --base-url http://192.168.20.138:8000/v1
IP アドレスのヒント
192.168.20.138 を実際に使用している AI Pyramid デバイスの IP アドレスに置き換えてください。

起動成功時の出力例:

root@m5stack-AI-Pyramid:~/wyoming-openai-stt# python wyoming_whisper_service.py --base-url http://192.168.20.138:8000/v1
2026-02-04 16:29:45,121 - __main__ - INFO - Starting Wyoming Whisper service
2026-02-04 16:29:45,122 - __main__ - INFO - API Base URL: http://192.168.20.138:8000/v1
2026-02-04 16:29:45,122 - __main__ - INFO - Model: sense-voice-small-10s-ax650
2026-02-04 16:29:45,123 - __main__ - INFO - Language: auto
2026-02-04 16:29:46,098 - __main__ - INFO - Service info created
2026-02-04 16:29:46,099 - __main__ - INFO - Server listening on tcp://0.0.0.0:10300

ステップ 4:Home Assistant に Wyoming Protocol を追加する

Home Assistant の設定画面に入り、"Wyoming Protocol" 統合を検索して追加します:

ステップ 5:接続パラメータの設定

Wyoming Protocol の接続パラメータを設定します:

  • ホスト:127.0.0.1
  • ポート:10300
ポートの説明
ポート番号は前のステップで起動した音声テキスト変換サービスと一致させる必要があります。

ステップ 6:音声アシスタントの作成

Home Assistant の設定で「音声アシスタント」モジュールに入り、「音声アシスタントを作成」をクリックします:

ステップ 7:ASR モデルの設定

音声認識モデルとして、先ほど追加した sense-voice-small-10s-ax650 を選択します。言語設定はデフォルトのままで問題ありません:

6.2 テキスト音声合成(TTS)の設定

ステップ 1:依存パッケージとモデルのインストール

システムに音声合成に必要な依存パッケージとモデルがインストールされていることを確認します:

apt install lib-llm llm-sys llm-melotts llm-openai-api llm-model-melotts-en-us-ax650
pip install openai wyoming
選択可能な言語
llm-model-melotts-zh-cn-ax650llm-model-melotts-ja-jp-ax650 など、多言語対応の MeloTTS モデルをサポートしており、必要に応じてインストールできます。

ステップ 2:Wyoming テキスト音声合成サービスの作成

AI Pyramid 内で wyoming_openai_tts.py ファイルを新規作成し、以下のコードをコピーしてください:

#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
#
# SPDX-License-Identifier: MIT
"""
Wyoming protocol server for OpenAI API TTS service.
Connects local OpenAI-compatible TTS API to Home Assistant.
"""

import argparse
import asyncio
import logging
import wave
import io
from pathlib import Path
from typing import Optional

from openai import OpenAI
from wyoming.audio import AudioChunk, AudioStart, AudioStop
from wyoming.event import Event
from wyoming.info import Attribution, Info, TtsProgram, TtsVoice
from wyoming.server import AsyncEventHandler, AsyncServer
from wyoming.tts import Synthesize

_LOGGER = logging.getLogger(__name__)

# Default configuration
DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 10200
DEFAULT_API_BASE_URL = "http://192.168.20.138:8000/v1"
DEFAULT_MODEL = "melotts-zh-cn-ax650"
DEFAULT_VOICE = "melotts-zh-cn-ax650"
DEFAULT_RESPONSE_FORMAT = "wav"

# Available voices for Wyoming protocol
AVAILABLE_VOICES = [
    TtsVoice(
        name="melotts-en-au-ax650",
        description="MeloTTS English (AU)",
        attribution=Attribution(
            name="MeloTTS",
            url="https://huggingface.co/myshell-ai/MeloTTS-English",
        ),
        version="1.0.0",
        installed=True,
        languages=["en-au"],
    ),
    TtsVoice(
        name="melotts-en-default-ax650",
        description="MeloTTS English (Default)",
        attribution=Attribution(
            name="MeloTTS",
            url="https://huggingface.co/myshell-ai/MeloTTS-English",
        ),
        version="1.0.0",
        installed=True,
        languages=["en"],
    ),
    TtsVoice(
        name="melotts-en-us-ax650",
        description="MeloTTS English (US)",
        attribution=Attribution(
            name="MeloTTS",
            url="https://huggingface.co/myshell-ai/MeloTTS-English",
        ),
        version="1.0.0",
        installed=True,
        languages=["en-us"],
    ),
    TtsVoice(
        name="melotts-en-br-ax650",
        description="MeloTTS English (BR)",
        attribution=Attribution(
            name="MeloTTS",
            url="https://huggingface.co/myshell-ai/MeloTTS-English",
        ),
        version="1.0.0",
        installed=True,
        languages=["en-br"],
    ),
    TtsVoice(
        name="melotts-en-india-ax650",
        description="MeloTTS English (India)",
        attribution=Attribution(
            name="MeloTTS",
            url="https://huggingface.co/myshell-ai/MeloTTS-English",
        ),
        version="1.0.0",
        installed=True,
        languages=["en-in"],
    ),
    TtsVoice(
        name="melotts-ja-jp-ax650",
        description="MeloTTS Japanese (JP)",
        attribution=Attribution(
            name="MeloTTS",
            url="https://huggingface.co/myshell-ai/MeloTTS-Japanese",
        ),
        version="1.0.0",
        installed=True,
        languages=["ja-jp"],
    ),
    TtsVoice(
        name="melotts-es-es-ax650",
        description="MeloTTS Spanish (ES)",
        attribution=Attribution(
            name="MeloTTS",
            url="https://huggingface.co/myshell-ai/MeloTTS-Spanish",
        ),
        version="1.0.0",
        installed=True,
        languages=["es-es"],
    ),
    TtsVoice(
        name="melotts-zh-cn-ax650",
        description="MeloTTS Chinese (CN)",
        attribution=Attribution(
            name="MeloTTS",
            url="https://huggingface.co/myshell-ai/MeloTTS-Chinese",
        ),
        version="1.0.0",
        installed=True,
        languages=["zh-cn"],
    ),
]

# Map voice name -> model name for automatic switching
VOICE_MODEL_MAP = {voice.name: voice.name for voice in AVAILABLE_VOICES}


class OpenAITTSEventHandler:
    """Event handler for Wyoming protocol with OpenAI TTS."""

    def __init__(
        self,
        api_key: str,
        base_url: str,
        model: str,
        default_voice: str,
        response_format: str,
    ):
        """Initialize the event handler."""
        self.api_key = api_key
        self.base_url = base_url
        self.model = model
        self.default_voice = default_voice
        self.response_format = response_format
        self.voice_model_map = VOICE_MODEL_MAP

        # Initialize OpenAI client
        self.client = OpenAI(
            api_key=api_key,
            base_url=base_url,
        )

        _LOGGER.info(
            "Initialized OpenAI TTS handler with base_url=%s, model=%s",
            base_url,
            model,
        )

    async def handle_event(self, event: Event) -> Optional[Event]:
        """Handle a Wyoming protocol event."""
        if Synthesize.is_type(event.type):
            synthesize = Synthesize.from_event(event)
            _LOGGER.info("Synthesizing text: %s", synthesize.text)

            # Use specified voice or default
            voice = synthesize.voice.name if synthesize.voice else self.default_voice
            model = self.voice_model_map.get(voice, self.model)

            try:
                # Generate speech using OpenAI API
                audio_data = await asyncio.to_thread(
                    self._synthesize_speech,
                    synthesize.text,
                    voice,
                    model,
                )

                # Read WAV file properties
                with wave.open(io.BytesIO(audio_data), "rb") as wav_file:
                    sample_rate = wav_file.getframerate()
                    sample_width = wav_file.getsampwidth()
                    channels = wav_file.getnchannels()
                    audio_bytes = wav_file.readframes(wav_file.getnframes())

                _LOGGER.info(
                    "Generated audio: %d bytes, %d Hz, %d channels",
                    len(audio_bytes),
                    sample_rate,
                    channels,
                )

                # Send audio start event
                yield AudioStart(
                    rate=sample_rate,
                    width=sample_width,
                    channels=channels,
                ).event()

                # Send audio in chunks
                chunk_size = 8192
                for i in range(0, len(audio_bytes), chunk_size):
                    chunk = audio_bytes[i:i + chunk_size]
                    yield AudioChunk(
                        audio=chunk,
                        rate=sample_rate,
                        width=sample_width,
                        channels=channels,
                    ).event()

                # Send audio stop event
                yield AudioStop().event()

            except Exception as err:
                _LOGGER.exception("Error during synthesis: %s", err)
                raise

    def _synthesize_speech(self, text: str, voice: str, model: str) -> bytes:
        """Synthesize speech using OpenAI API (blocking call)."""
        with self.client.audio.speech.with_streaming_response.create(
            model=model,
            voice=voice,
            response_format=self.response_format,
            input=text,
        ) as response:
            # Read all audio data
            audio_data = b""
            for chunk in response.iter_bytes(chunk_size=8192):
                audio_data += chunk
            return audio_data


async def main():
    """Run the Wyoming protocol server."""
    parser = argparse.ArgumentParser(description="Wyoming OpenAI TTS Server")
    parser.add_argument(
        "--uri",
        default=f"tcp://{DEFAULT_HOST}:{DEFAULT_PORT}",
        help="URI to bind the server (default: tcp://0.0.0.0:10200)",
    )
    parser.add_argument(
        "--api-key",
        default="sk-your-key",
        help="OpenAI API key (default: sk-your-key)",
    )
    parser.add_argument(
        "--base-url",
        default=DEFAULT_API_BASE_URL,
        help=f"OpenAI API base URL (default: {DEFAULT_API_BASE_URL})",
    )
    parser.add_argument(
        "--model",
        default=DEFAULT_MODEL,
        help=f"TTS model name (default: {DEFAULT_MODEL})",
    )
    parser.add_argument(
        "--voice",
        default=DEFAULT_VOICE,
        help=f"Default voice name (default: {DEFAULT_VOICE})",
    )
    parser.add_argument(
        "--response-format",
        default=DEFAULT_RESPONSE_FORMAT,
        choices=["mp3", "opus", "aac", "flac", "wav", "pcm"],
        help=f"Audio response format (default: {DEFAULT_RESPONSE_FORMAT})",
    )
    parser.add_argument(
        "--debug",
        action="store_true",
        help="Enable debug logging",
    )

    args = parser.parse_args()

    # Setup logging
    logging.basicConfig(
        level=logging.DEBUG if args.debug else logging.INFO,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    )

    _LOGGER.info("Starting Wyoming OpenAI TTS Server")
    _LOGGER.info("URI: %s", args.uri)
    _LOGGER.info("Model: %s", args.model)
    _LOGGER.info("Default voice: %s", args.voice)

    # Create Wyoming info
    wyoming_info = Info(
        tts=[
            TtsProgram(
                name="MeloTTS",
                description="OpenAI compatible TTS service",
                attribution=Attribution(
                    name="MeloTTS",
                    url="https://huggingface.co/myshell-ai/MeloTTS-English",
                ),
                version="1.0.0",
                installed=True,
                voices=AVAILABLE_VOICES,
            )
        ],
    )

    # Create event handler
    event_handler = OpenAITTSEventHandler(
        api_key=args.api_key,
        base_url=args.base_url,
        model=args.model,
        default_voice=args.voice,
        response_format=args.response_format,
    )

    # Start server
    server = AsyncServer.from_uri(args.uri)

    _LOGGER.info("Server started, waiting for connections...")

    await server.run(
        partial(
            OpenAITtsHandler,
            wyoming_info=wyoming_info,
            event_handler=event_handler,
        )
    )


class OpenAITtsHandler(AsyncEventHandler):
    """Wyoming async event handler for OpenAI TTS."""

    def __init__(
        self,
        reader: asyncio.StreamReader,
        writer: asyncio.StreamWriter,
        wyoming_info: Info,
        event_handler: OpenAITTSEventHandler,
    ) -> None:
        super().__init__(reader, writer)
        self._wyoming_info = wyoming_info
        self._event_handler = event_handler
        self._sent_info = False

    async def handle_event(self, event: Event) -> bool:
        if not self._sent_info:
            await self.write_event(self._wyoming_info.event())
            self._sent_info = True
            _LOGGER.info("Client connected")

        _LOGGER.debug("Received event: %s", event.type)

        try:
            async for response_event in self._event_handler.handle_event(event):
                await self.write_event(response_event)
        except Exception as err:
            _LOGGER.exception("Error handling connection: %s", err)
            return False

        return True

    async def disconnect(self) -> None:
        _LOGGER.info("Client disconnected")


if __name__ == "__main__":
    from functools import partial

    asyncio.run(main())

5. デバイスの追加

  1. Home Assistant の設定画面に入り、「デバイスの追加」を選択します。
  1. 統合リストで ESPHome を検索します。
  1. Host フィールドにデバイスの IP アドレスを、Port フィールドに YAML 設定ファイルで定義されたポート番号を入力します。
  1. YAML 設定ファイルで定義された暗号化キーを入力します。
  1. 音声処理モードは、一旦クラウド処理(Cloud Processing)を選択します。
  1. 音声ウェイクワードおよび TTS エンジンのパラメータを設定します。
  1. 設定完了後、デバイスは Home Assistant の概要ページに表示されます。

6. ローカル音声アシスタントの設定

Wyoming Protocol を使用することで、Home Assistant にローカルの音声認識および合成機能を統合し、完全にオフラインな音声アシスタント体験を実現できます。

6.1 音声テキスト変換(ASR)の設定

ステップ 1:依存パッケージとモデルのインストール

システムに音声認識に必要な依存パッケージとモデルがインストールされていることを確認します:

apt install lib-llm llm-sys llm-asr llm-openai-api llm-model-sense-voice-small-10s-ax650
pip install openai wyoming

ステップ 2:Wyoming 音声テキスト変換サービスの作成

AI Pyramid 内で wyoming_whisper_service.py ファイルを新規作成し、以下のコードをコピーしてください:

ステップ 3:音声テキスト変換サービスの起動

以下のコマンドを実行してサービスを起動します(IP アドレスは実際の AI Pyramid のアドレスに置き換えてください):

python wyoming_whisper_service.py --base-url http://192.168.20.138:8000/v1
IP アドレスのヒント
192.168.20.138 を実際に使用している AI Pyramid デバイスの IP アドレスに置き換えてください。

起動成功時の出力例:

root@m5stack-AI-Pyramid:~/wyoming-openai-stt# python wyoming_whisper_service.py --base-url http://192.168.20.138:8000/v1
2026-02-04 16:29:45,121 - __main__ - INFO - Starting Wyoming Whisper service
2026-02-04 16:29:45,122 - __main__ - INFO - API Base URL: http://192.168.20.138:8000/v1
2026-02-04 16:29:45,122 - __main__ - INFO - Model: sense-voice-small-10s-ax650
2026-02-04 16:29:45,123 - __main__ - INFO - Language: auto
2026-02-04 16:29:46,098 - __main__ - INFO - Service info created
2026-02-04 16:29:46,099 - __main__ - INFO - Server listening on tcp://0.0.0.0:10300

ステップ 4:Home Assistant に Wyoming Protocol を追加する

Home Assistant の設定画面に入り、「Wyoming Protocol」統合を検索して追加します:

ステップ 5:接続パラメータの設定

Wyoming Protocol の接続パラメータを設定します:

  • Host:127.0.0.1
  • Port:10300
ポートの説明
ポート番号は前のステップで起動した音声テキスト変換サービスと一致させる必要があります。

ステップ 6:音声アシスタントの作成

Home Assistant の設定で「音声アシスタント」モジュールに入り、「音声アシスタントを作成」をクリックします:

ステップ 7:ASR モデルの設定

音声認識モデルとして、先ほど追加した sense-voice-small-10s-ax650 を選択します。言語設定はデフォルトのままで問題ありません:

6.2 テキスト音声合成(TTS)の設定

ステップ 1:依存パッケージとモデルのインストール

システムに音声合成に必要な依存パッケージとモデルがインストールされていることを確認します:

apt install lib-llm llm-sys llm-melotts llm-openai-api llm-model-melotts-en-us-ax650
pip install openai wyoming
利用可能な言語
llm-model-melotts-zh-cn-ax650llm-model-melotts-ja-jp-ax650 など、複数の言語の MeloTTS モデルをサポートしており、必要に応じてインストール可能です。

ステップ 2:Wyoming テキスト音声合成サービスの作成

AI Pyramid 内で wyoming_openai_tts.py ファイルを新規作成し、以下のコードをコピーしてください:

ステップ 3:テキスト音声合成サービスの起動

以下のコマンドを使用して Wyoming テキスト音声合成サービスを起動します。AI Pyramid の IP アドレスに置き換えてください:

python wyoming_openai_tts.py --base-url=http://192.168.20.138:8000/v1
サービスの確認
以下の出力が表示されれば、サービスは正常に起動しています。
root@m5stack-AI-Pyramid:~/wyoming-openai-tts# python wyoming_openai_tts.py --base_url=http://192.168.20.138:8000/v1
2026-02-04 17:03:18,152 - __main__ - INFO - Starting Wyoming OpenAI TTS Server
2026-02-04 17:03:18,153 - __main__ - INFO - URI: tcp://0.0.0.0:10200
2026-02-04 17:03:18,153 - __main__ - INFO - Model: melotts-zh-cn-ax650
2026-02-04 17:03:18,153 - __main__ - INFO - Default voice: melotts-zh-cn-ax650
2026-02-04 17:03:19,081 - __main__ - INFO - Initialized OpenAI TTS handler with base_url=http://192.168.20.138:8000/v1, model=melotts-zh-cn-ax650
2026-02-04 17:03:19,082 - __main__ - INFO - Server started, waiting for connections...

ステップ 4:Home Assistant に Wyoming Protocol を追加する

Home Assistant の設定を開き、「Wyoming Protocol」統合を検索して追加します:

接続設定
Host を 127.0.0.1、Port を 10200(テキスト音声合成サービスの設定と一致させる必要があります)に設定します。

ステップ 5:音声アシスタントの設定

「設定 - 音声アシスタント」でアシスタントの設定を作成または編集します。テキスト音声合成(TTS)オプションを先ほど追加した「MeloTTS」に設定し、必要に応じて適切な言語と音声を選択します。対応する言語の TTS モデルがインストールされていることを確認してください。ここでは例として American English で説明します:

7. HACS の設定

  1. homeassistant コンテナに入ります。
docker exec -it homeassistant bash
  1. HACS をインストールします。
wget -O - https://get.hacs.xyz | bash -
  1. Ctrl + D でコンテナを抜け、homeassistant コンテナを再起動します。
docker restart homeassistant
  1. 「設定 -> デバイスとサービス -> 統合を追加」で HACS を検索します。
  1. すべてにチェックを入れます。
  1. https://github.com/login/device を開きます。
  1. 認証を完了させます。

8. Local LLM Conversation の設定

注意
アドレスは自身の HomeAssistant サーバー(AI Pyramid)のアドレスに変更してください。
  1. http://192.168.20.33:8123/hacs/repository?owner=acon96\&repository=home-llm\&category=Integration にアクセスしてプラグインを追加します。
  1. 右下の「ダウンロード」をクリックします。
  1. バージョンは最新版を選択します。
  1. Home Assistant を再起動します。
  1. 「設定 -> 統合を追加」で Local LLMs を検索して追加します。

Ollama 統合の設定

  1. Local LLMs の追加画面で、バックエンドに Ollama API を選択し、モデル言語はデフォルトで English を選択します。
  1. API ホストアドレスには Ollama サービスが動作しているホストを入力します。ブラウザからその IP で Ollama is running が確認できることを確実にしてください。
  1. Ollama に HomeAssistant 用に微調整(fine-tuning)されたモデルを追加します。
ollama run hf.co/acon96/Home-3B-v3-GGUF
  1. エージェントの追加で、先ほどプルしたモデルを選択します。
  1. 必ず AssistHome Assistant Services の 2 つの設定項目にチェックを入れてください。その他の項目については、詳しくない場合はデフォルトのままにしてください。
  1. システムプロンプトの設定は以下を参考にしてください。プロンプトをダウンロードできます。詳細な説明についてはこちらのドキュメントを参照してください。
  1. 音声アシスタントの設定で、対話エージェントを先ほど設定したモデルに変更します。
On This Page