Files
windmill_workflow/docs/30_マスタードキュメント_Alexa_TTS_API編.md
2026-03-03 13:09:11 +09:00

19 KiB
Raw Blame History

マスタードキュメント - Alexa TTS API 編

最終更新: 2026-03-03 対象システム: windmill.keinafarm.netワークスペース: admins 目的: このドキュメントだけで Alexa TTS API の全容を把握し、作業を継続できること


目次

  1. 機能概要
  2. システム構成
  3. ファイル構成
  4. Windmillスクリプト仕様
  5. APIサーバー仕様
  6. Alexa API の仕組み(重要な知識)
  7. 認証・Cookie管理
  8. デプロイ手順
  9. デバイス一覧
  10. 運用手順・コマンド集
  11. 既知の問題・落とし穴
  12. ソースファイル索引
  13. 実装の経緯(試行錯誤記録)
  14. 更新履歴

1. 機能概要

目的

Windmill のワークフローから、家の各部屋に設置した Amazon Echo デバイスに対して、任意の日本語テキストを読み上げさせる。

ユーザーフロー

Windmill ワークフロー
  └→ POST http://alexa_api:3500/speak
        └→ alexa-api サーバーDockerコンテナ
              └→ HTTPS: alexa.amazon.co.jp/api/behaviors/preview
                    └→ Amazon サーバーが Echo デバイスに指示
                          └→ Echo デバイスが日本語で読み上げる

現在の状態

完全動作中2026-03-03 解決済み)

  • ローカルPCからもサーバーのDockerコンテナからも、日本語テキストの読み上げが動作する
  • 解決の鍵: sequenceJson 内の日本語文字を \uXXXX 形式にエスケープして送信する

2. システム構成

[ローカルPC (Windows)]
  c:\Users\akira\Develop\windmill_workflow\alexa-api\
  ├── 開発・編集
  ├── auth4.js でCookie取得ローカルのみ実行可能
  └── gitea でサーバーと同期push は scp を使う)

[VPSサーバー (keinafarm.net)]
  /home/claude/alexa-api/  ← git とは別にコピーして管理
  ├── server.js
  ├── Dockerfile
  ├── docker-compose.yml
  └── .env  ALEXA_COOKIE を保管)

[Docker コンテナ: alexa_api]
  ├── ネットワーク: windmill_windmill-internal
  ├── ポート: 3500外部非公開
  └── Windmill ワーカーから http://alexa_api:3500 でアクセス

[Windmill]
  スクリプト: u/admin/alexa_speak
  └→ http://alexa_api:3500/speak を呼び出す

ネットワーク設計のポイント

  • alexa_api コンテナは外部に公開しない(セキュリティ)
  • Windmill ワーカーと同じ Docker 内部ネットワーク windmill_windmill-internal に接続
  • Windmill から http://alexa_api:3500 でアクセス可能

3. ファイル構成

ファイル 場所 役割 備考
server.js alexa-api/ Express API サーバー本体 本番コード。変更したらビルド・再デプロイが必要
Dockerfile alexa-api/ Docker イメージ定義 node:20-alpine ベース
docker-compose.yml alexa-api/ コンテナ起動設定 windmill_windmill-internal に接続
package.json alexa-api/ npm 依存関係 本番は express のみ
.env.example alexa-api/ 環境変数テンプレート ALEXA_COOKIE=xxx の形式
.env alexa-api/.gitignore 対象) 実際の Cookie 保管 Git にコミットしない
auth4.js alexa-api/ Amazon 認証・Cookie 取得スクリプト ローカルのみで実行Windowsブラウザ認証が必要
auth.js / auth2.js / auth3.js alexa-api/ auth4.js の旧バージョン 参考用。実際は auth4.js を使う
test_tts.js alexa-api/ ローカルテスト用スクリプト 直接 alexa.amazon.co.jp を叩いて動作確認

4. Windmillスクリプト仕様

スクリプトパス

u/admin/alexa_speak

スクリプト本体TypeScript / Bun

export async function main(device: string, text: string) {
  const res = await fetch("http://alexa_api:3500/speak", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ device, text }),
  });
  if (!res.ok) throw new Error("alexa-api error " + res.status);
  return res.json();
}

スキーマ

{
  "type": "object",
  "required": ["device", "text"],
  "properties": {
    "device": {
      "type": "string",
      "description": "デバイス名またはシリアル番号"
    },
    "text": {
      "type": "string",
      "description": "読み上げるテキスト"
    }
  }
}

呼び出し例

// デバイス名で指定
await main("オフィスの右エコー", "来客がありました");

// シリアル番号で指定(確実)
await main("G0922H08525302K5", "来客がありました");

5. APIサーバー仕様

エンドポイント一覧

メソッド パス 説明
POST /speak テキスト読み上げ
GET /devices デバイス一覧取得
GET /health ヘルスチェック

POST /speak

リクエスト:

{
  "device": "オフィスの右エコー",
  "text": "読み上げる日本語テキスト"
}
  • device: デバイス名(日本語)またはシリアル番号。部分一致も可能
  • text: 読み上げるテキスト日本語OK

レスポンス(成功):

{
  "ok": true,
  "device": "オフィスの右エコー",
  "text": "読み上げる日本語テキスト"
}

レスポンス(失敗):

{
  "error": "デバイス \"xxxxx\" が見つかりません",
  "available": "プレハブ, リビングエコー1, ..."
}

GET /health

{ "ok": true, "cookieLength": 1234 }

GET /devices

[
  { "name": "オフィスの右エコー", "type": "A4ZXE0RM7LQ7A", "serial": "G0922H08525302K5", "online": true, "family": "ECHO" },
  ...
]

6. Alexa API の仕組み(重要な知識)

直接 API 実装の理由

alexa-remote2 ライブラリは、取得済みの Cookie 文字列を受け付けず内部で再認証しようとして失敗するため、使用しない。すべて自前で HTTPS リクエストを組み立てる。

API 呼び出しシーケンス

1. GET /api/language
   → Set-Cookie: csrf=XXXXX を取得(毎リクエストごとに必要)

2. GET /api/bootstrap
   → customerId を取得(キャッシュ: サーバー起動中は永続)
   → customerId = "A1AE8HXD8IJ61L"

3. GET /api/devices-v2/device?cached=false
   → デバイス一覧取得5分キャッシュ

4. POST /api/behaviors/preview
   → シーケンス JSON を送信して読み上げ実行

POST /api/behaviors/preview のリクエスト構造

{
  "behaviorId": "PREVIEW",
  "sequenceJson": "<エスケープ済みJSON文字列>",
  "status": "ENABLED"
}

sequenceJson の中身JSON文字列化 + \uXXXX エスケープ後):

{
  "@type": "com.amazon.alexa.behaviors.model.Sequence",
  "startNode": {
    "@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode",
    "type": "Alexa.Speak",
    "operationPayload": {
      "deviceType": "A4ZXE0RM7LQ7A",
      "deviceSerialNumber": "G0922H08525302K5",
      "customerId": "A1AE8HXD8IJ61L",
      "locale": "ja-JP",
      "textToSpeak": "読み上げるテキスト"
    }
  }
}

⚠️ 最重要ポイント: \uXXXX エスケープ

// ★ これをしないと日本語が発話されない!
var rawSequenceJson = JSON.stringify(sequenceObj).replace(
  /[\u0080-\uffff]/g,
  function(c) { return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); }
);

なぜ必要か: sequenceJson に raw UTF-8 の日本語文字が含まれていると、Amazon のパーサーが日本語 Unicode 文字をフィルタリングして除去してしまい、発話されない。\uXXXX 形式の JSON エスケープシーケンスに変換することで回避できる。

ヘッダー要件

Content-Type: application/json
csrf: <CSRFトークン>           ← ヘッダーに必要
Referer: https://alexa.amazon.co.jp/spa/index.html
Origin: https://alexa.amazon.co.jp
Cookie: <ALEXA_COOKIE>; csrf=<CSRFトークン>  ← Cookieにも必要
  • CSRF トークンはヘッダー(csrf:)と Cookiecsrf=)の 両方に必要
  • Content-Length は不要Amazon が自動判定)

locale パラメータ

動作
"ja-JP" 日本語で発話(\uXXXX エスケープが前提)
"" (空文字) 英語のみ発話。日本語は除去される
locale なし 英語音声として扱われる

7. 認証・Cookie管理

Amazon Alexa の非公式 API は Cookie 認証を使用する。Alexa アプリのログイン状態を模倣する。

ローカル PCWindowsでのみ実行可能Amazon のログインフローにブラウザーリダイレクトが必要なため)。

# alexa-api ディレクトリで実行
cd alexa-api
AMAZON_EMAIL="メールアドレス" AMAZON_PASSWORD="パスワード" node auth4.js

成功すると alexa-api/.env が生成・更新される。

auth4.js のログインフロー

  1. GET https://www.amazon.co.jp/ap/signin?openid.assoc_handle=amzn_dp_project_dee_jp
  2. hidden フィールドanti-csrftoken-a2z, appActionToken, workflowState 等)を抽出
  3. POST でメール/パスワードを送信
  4. alexa.amazon.co.jp/api/apps/v1/token へのリダイレクトをたどる
  5. 取得した Cookieat-acbjp, sess-at-acbjp, sst-acbjp, session-token 等)を .env に保存

数日〜数週間で期限切れになる。期限切れの症状: /health を叩くと Cookie 長は正常だが、/speak が 400 や 403 を返す。


8. デプロイ手順

A. コード変更時のデプロイ(ビルドが必要)

server.js / Dockerfile / package.json を変更した場合:

# Step 1: ローカルで編集後、scp でサーバーに転送
scp alexa-api/server.js keinafarm-claude:/home/claude/alexa-api/server.js
scp alexa-api/Dockerfile keinafarm-claude:/home/claude/alexa-api/Dockerfile
scp alexa-api/package.json keinafarm-claude:/home/claude/alexa-api/package.json
scp alexa-api/package-lock.json keinafarm-claude:/home/claude/alexa-api/package-lock.json

# Step 2: サーバーでビルドして再起動
ssh keinafarm-claude
cd /home/claude/alexa-api
sudo docker compose build
sudo docker compose up -d

# Step 3: Traefik 再起動(コンテナ再作成後は必須)
sudo docker restart traefik

⚠️ 重要: docker compose restart はイメージをリビルドしない。コード変更は build + up -d が必要。

# 1. ローカルで auth4.js を実行して .env を更新
cd alexa-api
AMAZON_EMAIL="xxx" AMAZON_PASSWORD="xxx" node auth4.js

# 2. .env をサーバーに転送
scp alexa-api/.env keinafarm-claude:/home/claude/alexa-api/.env

# 3. コンテナを再起動restart で OK。Traefik 再起動不要)
ssh keinafarm-claude 'sudo docker compose -f /home/claude/alexa-api/docker-compose.yml restart'

Traefik 再起動が必要な理由

docker compose up -d はコンテナを「再作成」するため、Docker 内部 IP アドレスが変わる。Traefik が古い IP を参照したまま 502/504 エラーを返すため、sudo docker restart traefik で新しい IP を再検出させる。

docker compose restart はコンテナ再起動のみIP 不変)なので Traefik 再起動は不要。

docker-compose.yml

services:
  alexa-api:
    build: .
    container_name: alexa_api
    restart: unless-stopped
    env_file:
      - .env
    environment:
      - PORT=3500
    networks:
      - windmill_windmill-internal
    # デバッグ時は以下のコメントを外す:
    # ports:
    #   - "127.0.0.1:3500:3500"

networks:
  windmill_windmill-internal:
    external: true

9. デバイス一覧

名前 deviceType serialNumber
プレハブ A4ZXE0RM7LQ7A G0922H085165007R
リビングエコー1 ASQZWP4GPYUT7 G8M2DB08522600RL
リビングエコー2 ASQZWP4GPYUT7 G8M2DB08522503WF
オフィスの右エコー A4ZXE0RM7LQ7A G0922H08525302K5
オフィスの左エコー A4ZXE0RM7LQ7A G0922H08525302J9
寝室のエコー ASQZWP4GPYUT7 G8M2HN08534302XH

Windmill スクリプトから device パラメータに名前またはシリアル番号を渡す。


10. 運用手順・コマンド集

サーバー上での確認コマンド

# コンテナ状態確認
sudo docker ps | grep alexa

# リアルタイムログ確認
sudo docker logs alexa_api -f

# コンテナ停止
sudo docker compose -f /home/claude/alexa-api/docker-compose.yml stop

# ビルド+起動(コード変更後)
cd /home/claude/alexa-api
sudo docker compose build
sudo docker compose up -d
sudo docker restart traefik

# Cookie 更新時(再起動のみ)
sudo docker compose -f /home/claude/alexa-api/docker-compose.yml restart

動作確認Windmill ワーカーコンテナ内から)

# ヘルスチェック
curl http://alexa_api:3500/health

# デバイス一覧確認
curl http://alexa_api:3500/devices

# TTS テスト
curl -X POST http://alexa_api:3500/speak \
  -H "Content-Type: application/json" \
  -d '{"device":"オフィスの右エコー","text":"テストです"}'

Windmill からスクリプト実行

curl -X POST \
  -H "Authorization: Bearer <WindmillトークンWIND>" \
  -H "Content-Type: application/json" \
  -d '{"device":"オフィスの右エコー","text":"テストです"}' \
  "https://windmill.keinafarm.net/api/w/admins/jobs/run_wait_result/p/u/admin/alexa_speak"

11. 既知の問題・落とし穴

問題 原因・対処
docker compose restart してもコードが古い restart はリビルドしない。build + up -d を使う
コンテナ再作成後に 502/504 エラー Traefik が古い IP を参照。sudo docker restart traefik で解消
alexa-remote2 は使えない 取得済み Cookie を受け付けず内部再認証で失敗。直接 API 実装が必要
CSRF トークンはヘッダーと Cookie の両方に必要 片方だけでは 401 になる
operationPayloadcustomerId が必須 なければ 400 エラー
sequenceJson の日本語を \uXXXX エスケープしないと無音 Amazon パーサーが raw UTF-8 の日本語をフィルタリングする
Alexa.SpeakSsml は動作しない /api/behaviors/preview では使えない。Alexa.Speak のみ有効
AlexaAnnouncement は別用途 コンテンツでなくノード名が読まれる
レート制限 短時間連続リクエストで HTTP 429 または無音。通知用途では問題なし
Gitea push がブロックされる pre-receive フックでエラー。ファイル転送は scp を使う

12. ソースファイル索引

コアコード

ファイル 説明
alexa-api/server.js Express API サーバー。Alexa への直接 HTTPS 実装
alexa-api/Dockerfile node:20-alpine ベース
alexa-api/docker-compose.yml windmill_windmill-internal ネットワーク接続設定
alexa-api/auth4.js Amazon 認証・Cookie 取得(ローカルのみ)
alexa-api/test_tts.js ローカルテスト用スクリプト
alexa-api/.env.example 環境変数テンプレート

ドキュメント

ファイル 説明
docs/alexa-api/10_Alexa TTS API 実装記録 (2026-03-02).md Claude Code による実装記録Triliumからコピー
docs/alexa-api/11_色々やってダメだった.txt ChatGPT との試行錯誤チャットログ
docs/alexa-api/12_ローカルで試したこと.md 日本語TTS問題の調査記録解決過程
docs/30_マスタードキュメント_Alexa_TTS_API編.md 本ドキュメント

13. 実装の経緯(試行錯誤記録)

フェーズ1: alexa-remote2 の断念2026-03-02 以前)

当初は alexa-remote2 ライブラリを使用しようとしたが、取得済みの Cookie 文字列を渡しても内部で再認証を試みて失敗することがわかり、断念。Amazon Alexa API への直接 HTTPS 実装に切り替えた。

フェーズ2: 英語は動くが日本語が出ない2026-03-02〜03

Alexa.Speak で英語は正常に発話されるが、日本語テキストが発話されない問題が発生。試行した内容:

試行内容 結果
speakType: 'ssml' を operationPayload に追加 変化なし(このフィールドは無効)
type: 'Alexa.SpeakSsml' に変更 英語も含め完全無音
<lang xml:lang="ja-JP"> SSML タグを text に含める 英語のみ発話(日本語部分は無音)
locale: '' (空文字) 英語は読めるが日本語は除去
locale: 'ja-JP' 日本語が除去されるVPSから
Cookie 新規取得 変化なしCookie は原因ではなかった)
AlexaAnnouncement ノード ノード名自体が読まれる(別用途)
Unicodeエスケープ \u3053\u308c... をテキストに 変化なし

フェーズ3: 根本原因の特定と解決2026-03-03

決定的な観察: 日本語と英語が混在したテキスト 'あいうえおThis is Testあいうえお' を送ると、英語部分(This is Test)のみが読まれ、日本語部分(あいうえお)は完全に無視された。

根本原因: sequenceJson パラメータに raw UTF-8 の日本語文字が含まれていると、Amazon のパーサーがそれをフィルタリングして除去する。文字コードの問題ではなく(\u3053\u308c... でも同じ結果、JSON の文字列値の中の非 ASCII 文字の扱いの問題だった。

解決策: JSON.stringify() 後に non-ASCII 文字を \uXXXX 形式の JSON エスケープシーケンスに変換する。

var rawSequenceJson = JSON.stringify(sequenceObj).replace(
  /[\u0080-\uffff]/g,
  function(c) { return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); }
);

この修正により「これは日本語のテストです」が完璧に発話されることを確認。server.jstest_tts.js の両方に適用済み。


14. 更新履歴

日付 変更内容
2026-03-02 alexa-remote2 断念、直接 API 実装開始
2026-03-02〜03 日本語TTS問題の調査・試行錯誤
2026-03-03 \uXXXX エスケープで日本語TTS完全解決。server.js・test_tts.js に反映
2026-03-03 本マスタードキュメント作成