25 KiB
マスタードキュメント - Alexa TTS API 編
最終更新: 2026-03-04 対象システム: windmill.keinafarm.net(ワークスペース: admins) 目的: このドキュメントだけで Alexa TTS API の全容を把握し、作業を継続できること
目次
- 機能概要
- システム構成
- ファイル構成
- Windmillスクリプト仕様
- APIサーバー仕様
- Alexa API の仕組み(重要な知識)
- 認証・Cookie管理
- デプロイ手順
- デバイス一覧
- 運用手順・コマンド集
- 既知の問題・落とし穴
- ソースファイル索引
- 実装の経緯(試行錯誤記録)
- 更新履歴
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-04 時点)
- ローカルPCからもサーバーのDockerコンテナからも、日本語テキストの読み上げが動作する
- 解決の鍵:
sequenceJson内の日本語文字を\uXXXX形式にエスケープして送信する - 補足:
u/admin/alexa_speakを API 更新した直後、Windmill UI の入力欄が即時更新されない場合がある(後述の運用回避策を適用)
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)
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": "読み上げる日本語テキスト"
}
- Amazon は 200 または 202 を返す(どちらも成功として扱う。202 は非同期処理を示す)
レスポンス(失敗):
{
"error": "デバイス \"xxxxx\" が見つかりません",
"available": "プレハブ, リビングエコー1, ..."
}
GET /health
{ "ok": true, "cookieLength": 1234 }
GET /devices
[
{ "name": "オフィスの右エコー", "type": "A4ZXE0RM7LQ7A", "serial": "G0922H08525302K5", "online": true, "family": "ECHO" },
...
]
force=trueで常にキャッシュを無効化して最新一覧を取得する(/speakの5分キャッシュとは独立)- TABLET や Alexa アプリなど、Echo 以外のデバイスも含む全デバイスを返す
デバイス検索ロジック(findDevice)
/speak の device パラメータは以下の優先順位で検索する:
- シリアル番号完全一致(
serialNumber === device) - アカウント名完全一致(大文字小文字を無視)
- アカウント名部分一致(
includes()、大文字小文字を無視)
// 例: "右エコー" でも "オフィスの右エコー" を見つけられる
キャッシュ仕様
| 対象 | 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() の直後に非同期で初期化処理を実行する:
getCustomerId()を呼び出してcustomerIdをキャッシュ(成功すると[INFO] Customer ID: xxxをログ出力)getDevices()を呼び出してデバイス一覧をキャッシュ(5分 TTL)deviceTypeがA4ZXEまたはASQZWPで始まるデバイス(Echo 系)のみをログ出力
失敗してもサーバーは起動し続ける([WARN] Startup init failed: と出力して続行)。ただし Cookie が無効な場合、その後の /speak リクエストも全て失敗する。
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:)と Cookie(csrf=)の 両方に必要 Content-Lengthは不要(Amazon が自動判定)
locale パラメータ
| 値 | 動作 |
|---|---|
"ja-JP" |
✅ 日本語で発話(\uXXXX エスケープが前提) |
"" (空文字) |
英語のみ発話。日本語は除去される |
| locale なし | 英語音声として扱われる |
7. 認証・Cookie管理
Cookie の役割
Amazon Alexa の非公式 API は Cookie 認証を使用する。Alexa アプリのログイン状態を模倣する。
Cookie の取得方法(auth4.js)
ローカル PC(Windows)でのみ実行可能(Amazon のログインフローにブラウザーリダイレクトが必要なため)。
# alexa-api ディレクトリで実行
cd alexa-api
AMAZON_EMAIL="メールアドレス" AMAZON_PASSWORD="パスワード" node auth4.js
成功すると alexa-api/.env が生成・更新される。
auth4.js のログインフロー
GET https://www.amazon.co.jp/ap/signin?openid.assoc_handle=amzn_dp_project_dee_jpopenid.assoc_handle: 'amzn_dp_project_dee_jp'が Alexa Japan 専用のハンドル(他のAmazonサービスとは異なる)openid.return_to: 'https://alexa.amazon.co.jp/api/apps/v1/token'にリダイレクト先を指定
- hidden フィールド(anti-csrftoken-a2z, appActionToken, workflowState 等)を HTML から抽出
- POST でメール/パスワードを
rememberMe: 'true'と一緒に送信(長期Cookie取得のため重要) - 3xx リダイレクトを最大10回たどる(
alexa.amazon.co.jp/api/apps/v1/token等) - 取得した 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 を変更した場合:
# 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 更新時のデプロイ(ビルド不要)
# 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"
API反映後にUI入力欄が変わらない場合(2026-03-04 追記)
u/admin/alexa_speak を create-script で更新後、API上の schema は更新済みでも、Input フォームが旧表示のまま残ることがある。
対応手順:
- APIで最新状態を確認する
hash更新とschema.properties.deviceの以下2項目を確認するformat = "dynselect-device"originalType = "DynSelect_device"
- Windmill UI を
Ctrl + Shift + Rでハードリロードする - 反映されない場合は
Edit -> Deployを1回実行する - Input フォームで
Deviceがドロップダウン表示になったことを確認する
実運用上は「API反映成功」と「UIフォーム反映成功」を別チェックとして扱う。
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/archive/alexa-tts/10_Alexa TTS API 実装記録 (2026-03-02).md |
Claude Code による実装記録(アーカイブ) |
docs/archive/alexa-tts/11_色々やってダメだった.txt |
ChatGPT との試行錯誤チャットログ(アーカイブ) |
docs/archive/alexa-tts/12_ローカルで試したこと.md |
日本語TTS問題の調査記録(アーカイブ) |
docs/archive/alexa-tts/21_引き継ぎ_alexa_speak_API反映後にUIドロップダウンが変わらない件.md |
API反映後のUI未反映事象の切り分けと回避策(アーカイブ) |
docs/archive/alexa-tts/README.md |
Alexa 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.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 仕様を追記 |
| 2026-03-04 | u/admin/alexa_speak の API 反映後にUIドロップダウンが即時反映されない事象と標準対応(Edit -> Deploy)を統合。中間資料のアーカイブ索引を追加 |