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

27 KiB
Raw Permalink Blame History

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

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

関連ドキュメント: docs/31_Alexa_Cookie更新GUI運用.md


目次

  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-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 でアクセス可能
  • akirawindmill への自由な切り替え権限は与えない
  • 代わりに sudoers/usr/local/bin/alexa-cookie-deploy.sh /tmp/alexa-api.env だけを許可する
  • ローカル GUI は .env/tmp/alexa-api.env へ転送したあと、この専用スクリプトだけを sudo で実行する
  • 専用スクリプト自身は root で .env 反映と docker compose restart を完了する
  • これにより Cookie 更新だけを安全寄りに GUI 化できる

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 取得スクリプト CLI版。ローカルのみで実行
auth4-web.js alexa-api/ Cookie 更新 GUI ブラウザで認証し、サーバー反映まで実行可能
auth4-core.js alexa-api/ Cookie 更新の共通ロジック CLI版とGUI版で共通利用
alexa-cookie-deploy.sh alexa-api/ サーバー側専用反映スクリプト /tmp/alexa-api.env を本番 .env に反映し、root で再起動
alexa-cookie-deploy.sudoers alexa-api/ sudoers 設定例 akira から専用反映スクリプトだけ実行許可
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

/speakdevice パラメータは以下の優先順位で検索する:

  1. シリアル番号完全一致serialNumber === device
  2. アカウント名完全一致(大文字小文字を無視)
  3. アカウント名部分一致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() の直後に非同期で初期化処理を実行する:

  1. getCustomerId() を呼び出して customerId をキャッシュ(成功すると [INFO] Customer ID: xxx をログ出力)
  2. getDevices() を呼び出してデバイス一覧をキャッシュ5分 TTL
  3. deviceTypeA4ZXE または 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:)と 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
    • 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. 取得した Cookieat-acbjp, sess-at-acbjp, sst-acbjp, session-token 等)を .env に保存

成功判定: Cookie に at-acbjp または session-token が含まれているかで判定。

失敗時のエラー検出:

  • CAPTCHA が要求されている場合: ※ CAPTCHA が要求されています。しばらく待ってから再試行してください。
  • パスワードが間違っている場合: ※ パスワードが間違っている可能性があります。

数日〜数週間で期限切れになる。期限切れの症状: /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. ローカルで GUI を起動
cd /home/akira/develop/windmill_workflow/alexa-api
npm run auth:web

# 2. ブラウザで http://127.0.0.1:3678 を開き、Amazon の認証情報を入力

# 3. 同じ画面の「サーバーへ反映する」を実行
#    デフォルト値:
#    SSH 接続先: keinafarm
#    リモート一時アップロード先: /tmp/alexa-api.env
#    実行する専用コマンド: sudo /usr/local/bin/alexa-cookie-deploy.sh /tmp/alexa-api.env

サーバー側で一度だけ以下を実施する:

# 1. 専用反映スクリプトを配置
scp alexa-api/alexa-cookie-deploy.sh keinafarm:/tmp/alexa-cookie-deploy.sh
ssh keinafarm 'sudo install -m 755 /tmp/alexa-cookie-deploy.sh /usr/local/bin/alexa-cookie-deploy.sh'

# 2. sudoers を配置
scp alexa-api/alexa-cookie-deploy.sudoers keinafarm:/tmp/alexa-cookie-deploy.sudoers
ssh keinafarm 'sudo install -m 440 /tmp/alexa-cookie-deploy.sudoers /etc/sudoers.d/alexa-cookie-deploy'

# 3. 動作確認
ssh keinafarm 'sudo /usr/local/bin/alexa-cookie-deploy.sh /tmp/not-found.env' || true
  • akira から許可するのは sudo /usr/local/bin/alexa-cookie-deploy.sh /tmp/alexa-api.env だけ
  • windmill への自由な su や広い sudo 権限は与えない
  • 専用スクリプトは /tmp/alexa-api.env/home/claude/alexa-api/.env に反映し、root で docker compose 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_speakcreate-script で更新後、API上の schema は更新済みでも、Input フォームが旧表示のまま残ることがある。

対応手順:

  1. APIで最新状態を確認する
  2. hash 更新と schema.properties.device の以下2項目を確認する
    • format = "dynselect-device"
    • originalType = "DynSelect_device"
  3. Windmill UI を Ctrl + Shift + R でハードリロードする
  4. 反映されない場合は Edit -> Deploy を1回実行する
  5. 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 になる
operationPayloadcustomerId が必須 なければ 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 で devDependenciesalexa-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.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 本マスタードキュメント作成
2026-03-03 findDevice 検索ロジック、キャッシュ仕様、起動初期化、auth4.js 詳細、Dockerfile 仕様を追記
2026-03-04 u/admin/alexa_speak の API 反映後にUIドロップダウンが即時反映されない事象と標準対応Edit -> Deploy)を統合。中間資料のアーカイブ索引を追加