# マスタードキュメント - Alexa TTS API 編 > **最終更新**: 2026-03-03 > **対象システム**: windmill.keinafarm.net(ワークスペース: admins) > **目的**: このドキュメントだけで Alexa TTS API の全容を把握し、作業を継続できること --- ## 目次 1. [機能概要](#1-機能概要) 2. [システム構成](#2-システム構成) 3. [ファイル構成](#3-ファイル構成) 4. [Windmillスクリプト仕様](#4-windmillスクリプト仕様) 5. [APIサーバー仕様](#5-apiサーバー仕様) 6. [Alexa API の仕組み(重要な知識)](#6-alexa-api-の仕組み重要な知識) 7. [認証・Cookie管理](#7-認証cookie管理) 8. [デプロイ手順](#8-デプロイ手順) 9. [デバイス一覧](#9-デバイス一覧) 10. [運用手順・コマンド集](#10-運用手順コマンド集) 11. [既知の問題・落とし穴](#11-既知の問題落とし穴) 12. [ソースファイル索引](#12-ソースファイル索引) 13. [実装の経緯(試行錯誤記録)](#13-実装の経緯試行錯誤記録) 14. [更新履歴](#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/` | ローカルテスト用スクリプト | `.env` を読んで直接 alexa.amazon.co.jp を叩く。テスト対象デバイスはシリアル `G0922H08525302K5`(オフィスの右エコー)にハードコード。TABLET は一覧表示から除外。 | --- ## 4. Windmillスクリプト仕様 ### スクリプトパス ``` u/admin/alexa_speak ``` ### スクリプト本体(TypeScript / Bun) ```typescript 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(); } ``` ### スキーマ ```json { "type": "object", "required": ["device", "text"], "properties": { "device": { "type": "string", "description": "デバイス名またはシリアル番号" }, "text": { "type": "string", "description": "読み上げるテキスト" } } } ``` ### 呼び出し例 ```typescript // デバイス名で指定 await main("オフィスの右エコー", "来客がありました"); // シリアル番号で指定(確実) await main("G0922H08525302K5", "来客がありました"); ``` --- ## 5. APIサーバー仕様 ### エンドポイント一覧 | メソッド | パス | 説明 | |---------|------|------| | `POST` | `/speak` | テキスト読み上げ | | `GET` | `/devices` | デバイス一覧取得 | | `GET` | `/health` | ヘルスチェック | ### POST /speak **リクエスト**: ```json { "device": "オフィスの右エコー", "text": "読み上げる日本語テキスト" } ``` - `device`: デバイス名(日本語)またはシリアル番号。部分一致も可能 - `text`: 読み上げるテキスト(日本語OK) **レスポンス(成功)**: ```json { "ok": true, "device": "オフィスの右エコー", "text": "読み上げる日本語テキスト" } ``` - Amazon は **200 または 202** を返す(どちらも成功として扱う。202 は非同期処理を示す) **レスポンス(失敗)**: ```json { "error": "デバイス \"xxxxx\" が見つかりません", "available": "プレハブ, リビングエコー1, ..." } ``` ### GET /health ```json { "ok": true, "cookieLength": 1234 } ``` ### GET /devices ```json [ { "name": "オフィスの右エコー", "type": "A4ZXE0RM7LQ7A", "serial": "G0922H08525302K5", "online": true, "family": "ECHO" }, ... ] ``` - `force=true` で**常にキャッシュを無効化**して最新一覧を取得する(`/speak` の5分キャッシュとは独立) - TABLET や Alexa アプリなど、Echo 以外のデバイスも含む全デバイスを返す ### デバイス検索ロジック(findDevice) `/speak` の `device` パラメータは以下の優先順位で検索する: 1. **シリアル番号完全一致**(`serialNumber === device`) 2. **アカウント名完全一致**(大文字小文字を無視) 3. **アカウント名部分一致**(`includes()` 、大文字小文字を無視) ```javascript // 例: "右エコー" でも "オフィスの右エコー" を見つけられる ``` ### キャッシュ仕様 | 対象 | TTL | 備考 | |------|-----|------| | `customerId` | サーバー再起動まで永続 | Bootstrap API から取得 | | デバイス一覧(`/speak` 経由) | 5分 | 期限切れ後は自動更新 | | デバイス一覧(`/devices` 経由) | なし(毎回強制取得) | 診断・確認用途 | --- ## 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 を送信して読み上げ実行 ``` ### サーバー起動時の自動初期化 `app.listen()` の直後に非同期で初期化処理を実行する: 1. `getCustomerId()` を呼び出して `customerId` をキャッシュ(成功すると `[INFO] Customer ID: xxx` をログ出力) 2. `getDevices()` を呼び出してデバイス一覧をキャッシュ(5分 TTL) 3. `deviceType` が `A4ZXE` または `ASQZWP` で始まるデバイス(Echo 系)のみをログ出力 **失敗してもサーバーは起動し続ける**(`[WARN] Startup init failed:` と出力して続行)。ただし Cookie が無効な場合、その後の `/speak` リクエストも全て失敗する。 ### POST /api/behaviors/preview のリクエスト構造 ```json { "behaviorId": "PREVIEW", "sequenceJson": "<エスケープ済みJSON文字列>", "status": "ENABLED" } ``` **`sequenceJson` の中身**(JSON文字列化 + `\uXXXX` エスケープ後): ```json { "@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` エスケープ ```javascript // ★ これをしないと日本語が発話されない! 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: ← ヘッダーに必要 Referer: https://alexa.amazon.co.jp/spa/index.html Origin: https://alexa.amazon.co.jp Cookie: ; csrf= ← Cookieにも必要 ``` - CSRF トークンはヘッダー(`csrf:`)と Cookie(`csrf=`)の **両方に必要** - `Content-Length` は不要(Amazon が自動判定) ### locale パラメータ | 値 | 動作 | |----|------| | `"ja-JP"` | ✅ 日本語で発話(`\uXXXX` エスケープが前提) | | `""` (空文字) | 英語のみ発話。日本語は除去される | | locale なし | 英語音声として扱われる | --- ## 7. 認証・Cookie管理 ### Cookie の役割 Amazon Alexa の非公式 API は Cookie 認証を使用する。Alexa アプリのログイン状態を模倣する。 ### Cookie の取得方法(auth4.js) **ローカル PC(Windows)でのみ実行可能**(Amazon のログインフローにブラウザーリダイレクトが必要なため)。 ```bash # 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` - `openid.assoc_handle: 'amzn_dp_project_dee_jp'` が **Alexa Japan 専用のハンドル**(他のAmazonサービスとは異なる) - `openid.return_to: 'https://alexa.amazon.co.jp/api/apps/v1/token'` にリダイレクト先を指定 2. hidden フィールド(anti-csrftoken-a2z, appActionToken, workflowState 等)を HTML から抽出 3. POST でメール/パスワードを `rememberMe: 'true'` と一緒に送信(長期Cookie取得のため重要) 4. 3xx リダイレクトを最大10回たどる(`alexa.amazon.co.jp/api/apps/v1/token` 等) 5. 取得した Cookie(at-acbjp, sess-at-acbjp, sst-acbjp, session-token 等)を `.env` に保存 **成功判定**: Cookie に `at-acbjp` または `session-token` が含まれているかで判定。 **失敗時のエラー検出**: - CAPTCHA が要求されている場合: `※ CAPTCHA が要求されています。しばらく待ってから再試行してください。` - パスワードが間違っている場合: `※ パスワードが間違っている可能性があります。` ### Cookie の有効期限 数日〜数週間で期限切れになる。期限切れの症状: `/health` を叩くと Cookie 長は正常だが、`/speak` が 400 や 403 を返す。 --- ## 8. デプロイ手順 ### A. コード変更時のデプロイ(ビルドが必要) `server.js` / `Dockerfile` / `package.json` を変更した場合: ```bash # 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` が必要。 ### B. Cookie 更新時のデプロイ(ビルド不要) ```bash # 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 ```yaml 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. 運用手順・コマンド集 ### サーバー上での確認コマンド ```bash # コンテナ状態確認 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 ワーカーコンテナ内から) ```bash # ヘルスチェック 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 からスクリプト実行 ```bash curl -X POST \ -H "Authorization: Bearer " \ -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 になる | | `operationPayload` に `customerId` が必須 | なければ 400 エラー | | `sequenceJson` の日本語を `\uXXXX` エスケープしないと無音 | Amazon パーサーが raw UTF-8 の日本語をフィルタリングする | | `Alexa.SpeakSsml` は動作しない | `/api/behaviors/preview` では使えない。`Alexa.Speak` のみ有効 | | `AlexaAnnouncement` は別用途 | コンテンツでなくノード名が読まれる | | レート制限 | 短時間連続リクエストで HTTP 429 または無音。通知用途では問題なし | | Gitea push がブロックされる | pre-receive フックでエラー。ファイル転送は scp を使う | | 起動ログに Echo デバイスが出ない | deviceType が `A4ZXE` or `ASQZWP` で始まるもののみ表示。新デバイス追加時は確認を | | test_tts.js のテスト対象が固定 | シリアル `G0922H08525302K5`(オフィスの右エコー)にハードコード。他デバイスでテストする場合は一時的に書き換える | | auth4.js で CAPTCHA が出る | Amazon のレート制限。しばらく時間を置いてから再実行 | | `/devices` と `/speak` のキャッシュが異なる | `/devices` は毎回最新取得、`/speak` は5分キャッシュ。新しいデバイス追加直後に `/speak` が失敗する場合、コンテナ再起動でキャッシュクリア | --- ## 12. ソースファイル索引 ### コアコード | ファイル | 説明 | |---------|------| | `alexa-api/server.js` | Express API サーバー。Alexa への直接 HTTPS 実装 | | `alexa-api/Dockerfile` | node:20-alpine ベース。**`npm install --omit=dev`** で devDependencies(alexa-remote2, alexa-cookie2)を除外してビルド。コピーするのは `server.js` のみ(auth4.js 等はコンテナに含まれない) | | `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'` に変更 | 英語も含め完全無音 | | `` 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 エスケープシーケンスに変換する。 ```javascript var rawSequenceJson = JSON.stringify(sequenceObj).replace( /[\u0080-\uffff]/g, function(c) { return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); } ); ``` この修正により「これは日本語のテストです」が完璧に発話されることを確認。`server.js` と `test_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 | 本マスタードキュメント作成 | | 2026-03-03 | findDevice 検索ロジック、キャッシュ仕様、起動初期化、auth4.js 詳細、Dockerfile 仕様を追記 |