Files
windmill_workflow/docs/alexa-api/Alexa TTS API 実装記録 (2026-03-02)/Alexa TTS API 実装記録 (2026-03-02.html
2026-03-03 12:56:26 +09:00

318 lines
19 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<base target="_parent">
<title data-trilium-title>Alexa TTS API 実装記録 (2026-03-02)</title>
</head>
<body>
<div class="content">
<h1 data-trilium-h1>Alexa TTS API 実装記録 (2026-03-02)</h1>
<div class="ck-content">
<p>Alexa TTS API マスタードキュメント</p>
<p><strong>最終更新</strong>: 2026-03-03 <strong>状態</strong>: サーバーからの日本語TTS未解決調査中
<br>------------
<br>2026/03/03 10:24 akira記録
<br>akiraが下記の変更をしましたので、内容を読んでください。
<br>
<br>1) 構成とサーバーへのファイル受け渡し方法を変更しました
<br>/home/claude/windmill_workflow
<br>に、https://gitea.keinafarm.net/akira/windmill_workflow.gitをcloneしました
<br>これにより、
<br>C:\Users\akira\Develop\windmill_workflow
<br>とのやり取りはgiteaを使って出来るようになります。
<br>
<br>2) docker compose up -dで、「 Docker 内部の IP アドレスが変わり、Traefik のルーティングが古い IP
を参照したまま 502/504 エラーになる」のは、原因があるはずです。(他のコンテナで、問題になっていないので)
<br>調査して、Traefik 再起動が不必要になるようにしたいです
<br>
<br>
<br>
<br>------------</p>
<hr>
<h2>概要</h2>
<p>Windmill から家中の Echo デバイスに任意のテキストを読み上げさせる API サーバー。Node.js + Express で実装し、Docker
コンテナとして Windmill サーバー上で稼働する。</p>
<p><strong>⚠ 現在の問題</strong>: ローカル PC からは日本語TTS動作確認済み。しかしサーバーkeinafarm.netのコンテナからリクエストすると日本語文字が
Amazon 側で除去されて発話されない。原因は Amazon の IP ベースフィルタリング海外IPからは日本語 textToSpeak
を無視する模様)。調査継続中。</p>
<h2>ファイル構成</h2>
<figure class="table">
<table>
<thead>
<tr>
<th>ファイル</th>
<th>場所</th>
<th>役割</th>
<th>備考</th>
</tr>
</thead>
<tbody>
<tr>
<td>server.js</td>
<td>alexa-api/(リポジトリ)</td>
<td>Express API サーバー本体</td>
<td>本番コード。変更したらビルド・再デプロイが必要</td>
</tr>
<tr>
<td>Dockerfile</td>
<td>alexa-api/(リポジトリ)</td>
<td>Docker イメージ定義</td>
<td>node:20-alpine ベース。server.js と package*.json をコピー</td>
</tr>
<tr>
<td>docker-compose.yml</td>
<td>alexa-api/(リポジトリ)</td>
<td>コンテナ起動設定</td>
<td>windmill_windmill-internal ネットワーク接続。外部ポート非公開</td>
</tr>
<tr>
<td>package.json / package-lock.json</td>
<td>alexa-api/(リポジトリ)</td>
<td>npm 依存関係</td>
<td>本番: express のみ。devDeps に alexa-remote2不使用</td>
</tr>
<tr>
<td>.env.example</td>
<td>alexa-api/(リポジトリ)</td>
<td>環境変数テンプレート</td>
<td>ALEXA_COOKIE=xxx の形式</td>
</tr>
<tr>
<td>.env</td>
<td>alexa-api/(リポジトリ、.gitignore 対象)</td>
<td>実際の Cookie 保管</td>
<td>Git にコミットしない。ローカル作業後に scp でサーバーへ転送</td>
</tr>
<tr>
<td>auth4.js</td>
<td>alexa-api/(リポジトリ)</td>
<td>Amazon 認証・Cookie 取得スクリプト</td>
<td>ローカルのみで実行Windows PC</td>
</tr>
<tr>
<td>auth.js / auth2.js / auth3.js</td>
<td>alexa-api/(リポジトリ)</td>
<td>auth4.js の旧バージョン</td>
<td>参考用。実際は auth4.js を使う</td>
</tr>
<tr>
<td>test_tts.js</td>
<td>alexa-api/(リポジトリ)</td>
<td>ローカルテスト用スクリプト</td>
<td>直接 alexa.amazon.co.jp を叩いて動作確認</td>
</tr>
</tbody>
</table>
</figure>
<p><strong>サーバー上のファイル場所</strong>: <code>/home/claude/alexa-api/</code>git
リポジトリとは別にコピーして管理)</p>
<h2>サーバーへのデプロイ手順</h2>
<p>server.js や Dockerfile、package.json を変更した場合は以下の手順でサーバーに反映する。</p>
<h3>Step 1: ローカルでファイルを編集</h3>
<p>リポジトリc:\Users\akira\Develop\windmill_workflow\alexa-api\)でファイルを編集する。</p>
<h3>Step 2: scp でサーバーに転送</h3>
<p>変更したファイルをサーバーに scp で転送する:</p>
<p># server.js を変更した場合 scp alexa-api/server.js keinafarm-claude:/home/claude/alexa-api/server.js
# Dockerfile や package.json を変更した場合 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
# .env を更新した場合Cookie 更新時など) scp alexa-api/.env keinafarm-claude:/home/claude/alexa-api/.env</p>
<h3>Step 3: サーバーでビルドして再起動</h3>
<p><strong>⚠ 重要</strong>: <code>docker compose restart</code> はイメージをリビルドしない。server.js
等を変更した場合は必ず <code>build + up -d</code> を実行すること。</p>
<p># SSH でサーバーに接続してビルド+起動 ssh keinafarm-claude cd /home/claude/alexa-api
# イメージをビルドserver.js 等の変更を反映) sudo docker compose build # コンテナを再作成して起動
sudo docker compose up -d # Traefik を再起動(コンテナ再作成後は必須) sudo docker restart
traefik</p>
<h3>Step 4: 動作確認</h3>
<p># ヘルスチェックWindmill ワーカーコンテナ内から) curl http://alexa_api:3500/health # ログ確認
sudo docker logs alexa_api -f # デバイス一覧確認 curl http://alexa_api:3500/devices</p>
<h3>Cookie だけ更新する場合server.js 変更なし)</h3>
<p># .env をサーバーに転送 scp alexa-api/.env keinafarm-claude:/home/claude/alexa-api/.env
# コンテナを再起動restart で OK → イメージのリビルド不要) ssh keinafarm-claude 'sudo docker
compose -f /home/claude/alexa-api/docker-compose.yml restart' # ※ Traefik
の再起動は不要(コンテナ再作成しないため)</p>
<h2>Traefik 再起動が必要な理由</h2>
<p>docker compose up -d はコンテナを「再作成」するdocker compose restart は既存コンテナを再起動するだけ)。コンテナが再作成されると
Docker 内部の IP アドレスが変わり、Traefik のルーティングが古い IP を参照したまま 502/504 エラーになる。</p>
<p><strong>対処</strong>: <code>sudo docker restart traefik</code> で Traefik
に新しい IP を再検出させる。</p>
<p>この問題は Traefik の設定で <code>watch: true</code> にすれば自動解消できるが、現状はコンテナ再作成のたびに手動で
Traefik を再起動する運用としている。</p>
<h2>docker-compose.yml の内容</h2>
<p>services: &nbsp;alexa-api: &nbsp; &nbsp;build: . &nbsp; &nbsp;container_name:
alexa_api &nbsp; &nbsp;restart: unless-stopped &nbsp; &nbsp;env_file: &nbsp;
&nbsp; &nbsp;- .env &nbsp; &nbsp;environment: &nbsp; &nbsp; &nbsp;- PORT=3500
&nbsp; &nbsp;networks: &nbsp; &nbsp; &nbsp;- windmill_windmill-internal
&nbsp; &nbsp;# 外部には公開しないWindmill ワーカーから内部ネットワーク経由でのみアクセス) &nbsp; &nbsp;#
デバッグ時は以下のコメントを外す: &nbsp; &nbsp;# ports: &nbsp; &nbsp;# &nbsp; - "127.0.0.1:3500:3500"
networks: &nbsp;windmill_windmill-internal: &nbsp; &nbsp;external: true</p>
<h2>認証方法auth4.js</h2>
<p>Amazon Japan OpenID フローを自前で実装。ローカル PCWindowsでのみ実行する</p>
<p># ローカルPC の alexa-api ディレクトリで実行 cd alexa-api AMAZON_EMAIL="メールアドレス" AMAZON_PASSWORD="パスワード"
node auth4.js</p>
<p>成功すると alexa-api/.env が生成または更新される。</p>
<p>ログインフローの概要:</p>
<ol>
<li>GET https://www.amazon.co.jp/ap/signin?openid.assoc_handle=amzn_dp_project_dee_jp</li>
<li>hidden フィールドanti-csrftoken-a2z, appActionToken, workflowState 等)を抽出</li>
<li>POST でメール/パスワードを送信</li>
<li>alexa.amazon.co.jp/api/apps/v1/token へのリダイレクトをたどる</li>
<li>取得した Cookieat-acbjp, sess-at-acbjp, sst-acbjp, session-token 等)を .env
に保存</li>
</ol>
<h2>TTS の仕組みserver.js</h2>
<p>alexa-remote2 は使わない直接 API 実装。Endpoints:</p>
<ul>
<li>POST /speak — { device: "デバイス名 or serial", text: "しゃべる内容" }</li>
<li>GET /devices — デバイス一覧</li>
<li>GET /health — ヘルスチェック</li>
</ul>
<p>内部の API 呼び出し順序:</p>
<ol>
<li>GET /api/language → Set-Cookie: csrf=XXXXX を取得(毎リクエストごと)</li>
<li>GET /api/bootstrap → customerId を取得(キャッシュ: 永続A1AE8HXD8IJ61L</li>
<li>GET /api/devices-v2/device → デバイス一覧5分キャッシュ</li>
<li>POST /api/behaviors/preview にシーケンス JSON を送信</li>
</ol>
<p>POST /api/behaviors/preview のボディ構造:</p>
<p>{ &nbsp;behaviorId: "PREVIEW", &nbsp;sequenceJson: JSON.stringify({ &nbsp;
&nbsp;"@type": "com.amazon.alexa.behaviors.model.Sequence", &nbsp; &nbsp;startNode:
{ &nbsp; &nbsp; &nbsp;"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode",
&nbsp; &nbsp; &nbsp;type: "Alexa.Speak", &nbsp; &nbsp; &nbsp;operationPayload:
{ &nbsp; &nbsp; &nbsp; &nbsp;deviceType: "...", &nbsp; &nbsp; &nbsp; &nbsp;deviceSerialNumber:
"...", &nbsp; &nbsp; &nbsp; &nbsp;customerId: "A1AE8HXD8IJ61L", &nbsp;
&nbsp; &nbsp; &nbsp;locale: "ja-JP", &nbsp; &nbsp;// ← 重要(下記参照) &nbsp;
&nbsp; &nbsp; &nbsp;textToSpeak: "発話内容" &nbsp; &nbsp; &nbsp;} &nbsp; &nbsp;}
&nbsp;}), &nbsp;status: "ENABLED" }</p>
<p>ヘッダーに <code>csrf: XXXXX</code> と Cookie に <code>csrf=XXXXX</code> の両方が必要。Content-Length
は Buffer.byteLength で計算(マルチバイト文字対応)。</p>
<h2>⚠ locale パラメータについて(重要・未解決)</h2>
<figure class="table">
<table>
<thead>
<tr>
<th>locale 値</th>
<th>ローカル PC から</th>
<th>サーバーkeinafarm.netから</th>
</tr>
</thead>
<tbody>
<tr>
<td>""(空文字)</td>
<td>✅ 日本語・英語・漢字全て発話</td>
<td>❌ 英語TTSになり日本語部分が発話されない</td>
</tr>
<tr>
<td>"ja-JP"</td>
<td>❌ 一瞬音が出るだけ(失敗)</td>
<td>❌ 日本語文字が Amazon 側で除去され英字のみ発話</td>
</tr>
</tbody>
</table>
</figure>
<p>現在 server.js では <code>locale: "ja-JP"</code> に設定している。</p>
<p><strong>仮説</strong>: Amazon が海外IPkeinafarm.net = 非日本IPからのリクエストを IP ベースでフィルタリングし、textToSpeak
の日本語文字を除去している。Alexa.TextCommand は同じ問題がない(異なる API パス)。</p>
<p><strong>確認済み事実</strong>: alexa_api の server.js ログには日本語テキストが正しく届いている。除去は
Amazon サーバー側で発生。</p>
<p><strong>次の調査候補</strong>:</p>
<ul>
<li>SSML の &lt;lang xml:lang="ja-JP"&gt; タグで強制的に日本語 TTS を指定できるか</li>
<li>Alexa.TextCommand で「次を読み上げて:{text}」形式が使えるか</li>
<li>ローカルブリッジ方式(ユーザーのローカル PC で小さなプロキシサーバーを動かし、クラウドサーバーからローカル経由で alexa.amazon.co.jp
を叩く)</li>
</ul>
<h2>デバイス一覧Echo デバイスのみ)</h2>
<figure class="table">
<table>
<thead>
<tr>
<th>名前</th>
<th>deviceType</th>
<th>serialNumber</th>
</tr>
</thead>
<tbody>
<tr>
<td>プレハブ</td>
<td>A4ZXE0RM7LQ7A</td>
<td>G0922H085165007R</td>
</tr>
<tr>
<td>リビングエコー1</td>
<td>ASQZWP4GPYUT7</td>
<td>G8M2DB08522600RL</td>
</tr>
<tr>
<td>リビングエコー2</td>
<td>ASQZWP4GPYUT7</td>
<td>G8M2DB08522503WF</td>
</tr>
<tr>
<td>オフィスの右エコー</td>
<td>A4ZXE0RM7LQ7A</td>
<td>G0922H08525302K5</td>
</tr>
<tr>
<td>オフィスの左エコー</td>
<td>A4ZXE0RM7LQ7A</td>
<td>G0922H08525302J9</td>
</tr>
<tr>
<td>寝室のエコー</td>
<td>ASQZWP4GPYUT7</td>
<td>G8M2HN08534302XH</td>
</tr>
</tbody>
</table>
</figure>
<h2>Windmill スクリプトu/admin/alexa_speak</h2>
<p>export async function main(device: string, text: string) { &nbsp;const
res = await fetch("http://alexa_api:3500/speak", { &nbsp; &nbsp;method:
"POST", &nbsp; &nbsp;headers: { "Content-Type": "application/json" }, &nbsp;
&nbsp;body: JSON.stringify({ device, text }), &nbsp;}); &nbsp;if (!res.ok)
throw new Error("alexa-api error " + res.status); &nbsp;return res.json();
}</p>
<p>device はデバイス名日本語またはシリアル番号で指定可能。Windmill ワーカーから <code>http://alexa_api:3500</code> でアクセスwindmill_windmill-internal
ネットワーク経由)。</p>
<h2>Cookie の更新手順</h2>
<p>Cookie は数日〜数週間で期限切れ。切れたら:</p>
<p># 1. ローカル PC で Cookie を取得 cd alexa-api AMAZON_EMAIL="メールアドレス" AMAZON_PASSWORD="パスワード"
node auth4.js # → alexa-api/.env が更新される # 2. サーバーに .env を転送 scp alexa-api/.env
keinafarm-claude:/home/claude/alexa-api/.env # 3. コンテナを再起動restart で OK、リビルド不要
ssh keinafarm-claude 'sudo docker compose -f /home/claude/alexa-api/docker-compose.yml
restart' # ※ Traefik 再起動は不要(コンテナ再作成なし)</p>
<h2>既知の問題・落とし穴</h2>
<ul>
<li><strong>docker compose restart ≠ リビルド</strong>: server.js を変更しても restart
ではコンテナ内のコードは古いまま。build + up -d が必要。</li>
<li><strong>コンテナ再作成後は Traefik 再起動必須</strong>: up -d でコンテナ再作成すると Docker 内部
IP が変わり Traefik が 502/504 を返す。<code>sudo docker restart traefik</code> で解消。</li>
<li><strong>alexa-remote2 は使えない</strong>: 取得した Cookie 文字列を受け付けない(内部で再認証しようとして失敗)。直接
API 実装が必要。</li>
<li><strong>CSRF トークンは Cookie と ヘッダーの両方に必要</strong>: csrf ヘッダーだけ、または Cookie
だけでは認証失敗。</li>
<li><strong>operationPayload に customerId 必須</strong>: ないと 400 エラー。</li>
<li><strong>レート制限</strong>: 短時間に連続リクエストすると HTTP 429 または 200 で音が出ない。通常の通知用途では問題なし。</li>
<li><strong>git push がブロックされる</strong>: Gitea の pre-receive フック(<code>remote: Gitea: User permission denied for writing</code>)で
push が失敗する。根本原因は未調査。ファイル転送は scp で行っている。</li>
<li><strong>firstRunCompleted: false はデバイス設定の未完了フラグ</strong>: TTS には直接影響しないroot
cause ではなかった)。</li>
</ul>
<h2>サーバー上の運用コマンド一覧</h2>
<p># コンテナ状態確認 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</p>
</div>
</div>
</body>
</html>