マスタードキュメント

This commit is contained in:
Akira
2026-03-03 13:09:11 +09:00
parent 1be261c95f
commit 4954cc0741

View File

@@ -0,0 +1,556 @@
# マスタードキュメント - 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/` | ローカルテスト用スクリプト | 直接 alexa.amazon.co.jp を叩いて動作確認 |
---
## 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": "読み上げる日本語テキスト"
}
```
**レスポンス(失敗)**:
```json
{
"error": "デバイス \"xxxxx\" が見つかりません",
"available": "プレハブ, リビングエコー1, ..."
}
```
### GET /health
```json
{ "ok": true, "cookieLength": 1234 }
```
### GET /devices
```json
[
{ "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 のリクエスト構造
```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: <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
**ローカル PCWindowsでのみ実行可能**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`
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` に保存
### 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 <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 になる |
| `operationPayload``customerId` が必須 | なければ 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 エスケープシーケンスに変換する。
```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 | 本マスタードキュメント作成 |