server.js を3点変更しました:
locale: 'ja-JP' → locale: ''(ローカルPCで成功していた設定) Content-Length ヘッダーを除去(test_tts.js では送っていなかった、これが差異の一つ) デバッグログを追加(Amazonへのリクエスト内容とレスポンスをログ出力) 認証問題について test_tts.js(ローカルで成功)と server.js(サーバーで失敗)を比較した結果、実は使っているCookieは全く同じもの(.env から読み込んでいる)なので、認証情報自体の差は本来ないはずです。 ただし、気になる点が1つあります: test_tts.js は locale: 'ja-JP' でローカルから成功しているのに、server.js は locale: 'ja-JP' でサーバーから失敗している これは実は「どこから接続しているか(IPアドレス) で Amazon 側の挙動が変わっている」可能性を示唆します。ただし、先ほど確認したように keinafarm.net は大阪のIPなので、この説明も矛盾します。 デプロイ手順: bash # ローカルから scp でサーバーへ転送 scp alexa-api/server.js keinafarm-claude:/home/claude/alexa-api/server.js # サーバーへSSHしてビルド&再起動 ssh keinafarm-claude 'cd /home/claude/alexa-api && sudo docker compose build && sudo docker compose up -d && sudo docker restart traefik' デプロイ後、sudo docker logs alexa_api -f でログを確認して、[DEBUG] 行の内容を教えてください。どんな JSON が Amazon に送られているか、Amazon が何を返しているかが見えてきます。
This commit is contained in:
@@ -37,6 +37,7 @@ function httpsRequest(path, options, extraCookies) {
|
|||||||
extraCookies = extraCookies || '';
|
extraCookies = extraCookies || '';
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
var allCookies = ALEXA_COOKIE + (extraCookies ? '; ' + extraCookies : '');
|
var allCookies = ALEXA_COOKIE + (extraCookies ? '; ' + extraCookies : '');
|
||||||
|
// bodyBuf はバイト列変換(マルチバイト文字に対応)
|
||||||
var bodyBuf = options.body ? Buffer.from(options.body, 'utf8') : null;
|
var bodyBuf = options.body ? Buffer.from(options.body, 'utf8') : null;
|
||||||
var reqOpts = {
|
var reqOpts = {
|
||||||
hostname: ALEXA_HOST,
|
hostname: ALEXA_HOST,
|
||||||
@@ -47,7 +48,8 @@ function httpsRequest(path, options, extraCookies) {
|
|||||||
'Accept': 'application/json, text/plain, */*',
|
'Accept': 'application/json, text/plain, */*',
|
||||||
'Accept-Language': 'ja-JP,ja;q=0.9',
|
'Accept-Language': 'ja-JP,ja;q=0.9',
|
||||||
'Cookie': allCookies,
|
'Cookie': allCookies,
|
||||||
}, bodyBuf ? { 'Content-Length': bodyBuf.length } : {}, options.headers || {}),
|
// Content-Length は送らない(test_tts.js で動作実績あり、Amazonが自動判定)
|
||||||
|
}, options.headers || {}),
|
||||||
};
|
};
|
||||||
var req = https.request(reqOpts, function(res) {
|
var req = https.request(reqOpts, function(res) {
|
||||||
var body = '';
|
var body = '';
|
||||||
@@ -128,8 +130,8 @@ app.post('/speak', async function(req, res) {
|
|||||||
|
|
||||||
console.log(' -> ' + target.accountName + ' (type=' + target.deviceType + ', serial=' + target.serialNumber + ')');
|
console.log(' -> ' + target.accountName + ' (type=' + target.deviceType + ', serial=' + target.serialNumber + ')');
|
||||||
|
|
||||||
var ssml = '<speak>' + text + '</speak>';
|
// locale: '' はローカルPCでは日本語発話成功(サーバーからは要検証)
|
||||||
|
// Alexa.SpeakSsml 系は全滅のため Alexa.Speak に戻す
|
||||||
var sequenceObj = {
|
var sequenceObj = {
|
||||||
'@type': 'com.amazon.alexa.behaviors.model.Sequence',
|
'@type': 'com.amazon.alexa.behaviors.model.Sequence',
|
||||||
startNode: {
|
startNode: {
|
||||||
@@ -139,7 +141,7 @@ var sequenceObj = {
|
|||||||
deviceType: target.deviceType,
|
deviceType: target.deviceType,
|
||||||
deviceSerialNumber: target.serialNumber,
|
deviceSerialNumber: target.serialNumber,
|
||||||
customerId: customerId,
|
customerId: customerId,
|
||||||
locale: 'ja-JP',
|
locale: '',
|
||||||
textToSpeak: text
|
textToSpeak: text
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -151,6 +153,9 @@ var sequenceObj = {
|
|||||||
status: 'ENABLED',
|
status: 'ENABLED',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// リクエストボディをログ出力(認証・パラメータ確認用)
|
||||||
|
console.log('[DEBUG] sequenceJson:', JSON.stringify(sequenceObj, null, 2));
|
||||||
|
|
||||||
var ttsRes = await httpsRequest('/api/behaviors/preview', {
|
var ttsRes = await httpsRequest('/api/behaviors/preview', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -162,6 +167,9 @@ var sequenceObj = {
|
|||||||
body: bodyStr,
|
body: bodyStr,
|
||||||
}, 'csrf=' + csrfToken);
|
}, 'csrf=' + csrfToken);
|
||||||
|
|
||||||
|
// Amazonからのレスポンスをログ出力
|
||||||
|
console.log('[DEBUG] Alexa API response: ' + ttsRes.status + ' body=' + ttsRes.body.substring(0, 200));
|
||||||
|
|
||||||
if (ttsRes.status === 200 || ttsRes.status === 202) {
|
if (ttsRes.status === 200 || ttsRes.status === 202) {
|
||||||
console.log(' [OK] TTS sent to ' + target.accountName);
|
console.log(' [OK] TTS sent to ' + target.accountName);
|
||||||
res.json({ ok: true, device: target.accountName, text: text });
|
res.json({ ok: true, device: target.accountName, text: text });
|
||||||
|
|||||||
148
docs/10_Alexa TTS API 実装記録 (2026-03-02).md
Normal file
148
docs/10_Alexa TTS API 実装記録 (2026-03-02).md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
Alexa TTS API マスタードキュメント
|
||||||
|
|
||||||
|
最終更新: 2026-03-03 状態: サーバーからの日本語TTS未解決(調査中)
|
||||||
|
------------
|
||||||
|
2026/03/03 10:24 akira記録
|
||||||
|
akiraが下記の変更をしましたので、内容を読んでください。
|
||||||
|
|
||||||
|
1) 構成とサーバーへのファイル受け渡し方法を変更しました
|
||||||
|
/home/claude/windmill_workflow
|
||||||
|
に、https://gitea.keinafarm.net/akira/windmill_workflow.gitをcloneしました
|
||||||
|
これにより、
|
||||||
|
C:\Users\akira\Develop\windmill_workflow
|
||||||
|
とのやり取りはgiteaを使って出来るようになります。
|
||||||
|
|
||||||
|
2) docker compose up -dで、「 Docker 内部の IP アドレスが変わり、Traefik のルーティングが古い IP を参照したまま 502/504 エラーになる」のは、原因があるはずです。(他のコンテナで、問題になっていないので)
|
||||||
|
調査して、Traefik 再起動が不必要になるようにしたいです
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
------------
|
||||||
|
|
||||||
|
概要
|
||||||
|
Windmill から家中の Echo デバイスに任意のテキストを読み上げさせる API サーバー。Node.js + Express で実装し、Docker コンテナとして Windmill サーバー上で稼働する。
|
||||||
|
|
||||||
|
⚠ 現在の問題: ローカル PC からは日本語TTS動作確認済み。しかしサーバー(keinafarm.net)のコンテナからリクエストすると日本語文字が Amazon 側で除去されて発話されない。原因は Amazon の IP ベースフィルタリング(海外IPからは日本語 textToSpeak を無視する模様)。調査継続中。
|
||||||
|
|
||||||
|
ファイル構成
|
||||||
|
ファイル 場所 役割 備考
|
||||||
|
server.js alexa-api/(リポジトリ) Express API サーバー本体 本番コード。変更したらビルド・再デプロイが必要
|
||||||
|
Dockerfile alexa-api/(リポジトリ) Docker イメージ定義 node:20-alpine ベース。server.js と package*.json をコピー
|
||||||
|
docker-compose.yml alexa-api/(リポジトリ) コンテナ起動設定 windmill_windmill-internal ネットワーク接続。外部ポート非公開
|
||||||
|
package.json / package-lock.json alexa-api/(リポジトリ) npm 依存関係 本番: express のみ。devDeps に alexa-remote2(不使用)
|
||||||
|
.env.example alexa-api/(リポジトリ) 環境変数テンプレート ALEXA_COOKIE=xxx の形式
|
||||||
|
.env alexa-api/(リポジトリ、.gitignore 対象) 実際の Cookie 保管 Git にコミットしない。ローカル作業後に scp でサーバーへ転送
|
||||||
|
auth4.js alexa-api/(リポジトリ) Amazon 認証・Cookie 取得スクリプト ローカルのみで実行(Windows PC)
|
||||||
|
auth.js / auth2.js / auth3.js alexa-api/(リポジトリ) auth4.js の旧バージョン 参考用。実際は auth4.js を使う
|
||||||
|
test_tts.js alexa-api/(リポジトリ) ローカルテスト用スクリプト 直接 alexa.amazon.co.jp を叩いて動作確認
|
||||||
|
サーバー上のファイル場所: /home/claude/alexa-api/(git リポジトリとは別にコピーして管理)
|
||||||
|
|
||||||
|
サーバーへのデプロイ手順
|
||||||
|
server.js や Dockerfile、package.json を変更した場合は以下の手順でサーバーに反映する。
|
||||||
|
|
||||||
|
Step 1: ローカルでファイルを編集
|
||||||
|
リポジトリ(c:\Users\akira\Develop\windmill_workflow\alexa-api\)でファイルを編集する。
|
||||||
|
|
||||||
|
Step 2: scp でサーバーに転送
|
||||||
|
変更したファイルをサーバーに scp で転送する:
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
Step 3: サーバーでビルドして再起動
|
||||||
|
⚠ 重要: docker compose restart はイメージをリビルドしない。server.js 等を変更した場合は必ず build + up -d を実行すること。
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
Step 4: 動作確認
|
||||||
|
# ヘルスチェック(Windmill ワーカーコンテナ内から) curl http://alexa_api:3500/health # ログ確認 sudo docker logs alexa_api -f # デバイス一覧確認 curl http://alexa_api:3500/devices
|
||||||
|
|
||||||
|
Cookie だけ更新する場合(server.js 変更なし)
|
||||||
|
# .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 の再起動は不要(コンテナ再作成しないため)
|
||||||
|
|
||||||
|
Traefik 再起動が必要な理由
|
||||||
|
docker compose up -d はコンテナを「再作成」する(docker compose restart は既存コンテナを再起動するだけ)。コンテナが再作成されると Docker 内部の IP アドレスが変わり、Traefik のルーティングが古い IP を参照したまま 502/504 エラーになる。
|
||||||
|
|
||||||
|
対処: sudo docker restart traefik で Traefik に新しい IP を再検出させる。
|
||||||
|
|
||||||
|
この問題は Traefik の設定で watch: true にすれば自動解消できるが、現状はコンテナ再作成のたびに手動で 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 # 外部には公開しない(Windmill ワーカーから内部ネットワーク経由でのみアクセス) # デバッグ時は以下のコメントを外す: # ports: # - "127.0.0.1:3500:3500" networks: windmill_windmill-internal: external: true
|
||||||
|
|
||||||
|
認証方法(auth4.js)
|
||||||
|
Amazon Japan OpenID フローを自前で実装。ローカル PC(Windows)でのみ実行する:
|
||||||
|
|
||||||
|
# ローカルPC の alexa-api ディレクトリで実行 cd alexa-api AMAZON_EMAIL="メールアドレス" AMAZON_PASSWORD="パスワード" node auth4.js
|
||||||
|
|
||||||
|
成功すると alexa-api/.env が生成または更新される。
|
||||||
|
|
||||||
|
ログインフローの概要:
|
||||||
|
|
||||||
|
GET https://www.amazon.co.jp/ap/signin?openid.assoc_handle=amzn_dp_project_dee_jp
|
||||||
|
hidden フィールド(anti-csrftoken-a2z, appActionToken, workflowState 等)を抽出
|
||||||
|
POST でメール/パスワードを送信
|
||||||
|
alexa.amazon.co.jp/api/apps/v1/token へのリダイレクトをたどる
|
||||||
|
取得した Cookie(at-acbjp, sess-at-acbjp, sst-acbjp, session-token 等)を .env に保存
|
||||||
|
TTS の仕組み(server.js)
|
||||||
|
alexa-remote2 は使わない直接 API 実装。Endpoints:
|
||||||
|
|
||||||
|
POST /speak — { device: "デバイス名 or serial", text: "しゃべる内容" }
|
||||||
|
GET /devices — デバイス一覧
|
||||||
|
GET /health — ヘルスチェック
|
||||||
|
内部の API 呼び出し順序:
|
||||||
|
|
||||||
|
GET /api/language → Set-Cookie: csrf=XXXXX を取得(毎リクエストごと)
|
||||||
|
GET /api/bootstrap → customerId を取得(キャッシュ: 永続)(A1AE8HXD8IJ61L)
|
||||||
|
GET /api/devices-v2/device → デバイス一覧(5分キャッシュ)
|
||||||
|
POST /api/behaviors/preview にシーケンス JSON を送信
|
||||||
|
POST /api/behaviors/preview のボディ構造:
|
||||||
|
|
||||||
|
{ behaviorId: "PREVIEW", sequenceJson: JSON.stringify({ "@type": "com.amazon.alexa.behaviors.model.Sequence", startNode: { "@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", type: "Alexa.Speak", operationPayload: { deviceType: "...", deviceSerialNumber: "...", customerId: "A1AE8HXD8IJ61L", locale: "ja-JP", // ← 重要(下記参照) textToSpeak: "発話内容" } } }), status: "ENABLED" }
|
||||||
|
|
||||||
|
ヘッダーに csrf: XXXXX と Cookie に csrf=XXXXX の両方が必要。Content-Length は Buffer.byteLength で計算(マルチバイト文字対応)。
|
||||||
|
|
||||||
|
⚠ locale パラメータについて(重要・未解決)
|
||||||
|
locale 値 ローカル PC から サーバー(keinafarm.net)から
|
||||||
|
""(空文字) ✅ 日本語・英語・漢字全て発話 ❌ 英語TTSになり日本語部分が発話されない
|
||||||
|
"ja-JP" ❌ 一瞬音が出るだけ(失敗) ❌ 日本語文字が Amazon 側で除去され英字のみ発話
|
||||||
|
現在 server.js では locale: "ja-JP" に設定している。
|
||||||
|
|
||||||
|
仮説: Amazon が海外IP(keinafarm.net = 非日本IP)からのリクエストを IP ベースでフィルタリングし、textToSpeak の日本語文字を除去している。Alexa.TextCommand は同じ問題がない(異なる API パス)。
|
||||||
|
|
||||||
|
確認済み事実: alexa_api の server.js ログには日本語テキストが正しく届いている。除去は Amazon サーバー側で発生。
|
||||||
|
|
||||||
|
次の調査候補:
|
||||||
|
|
||||||
|
SSML の <lang xml:lang="ja-JP"> タグで強制的に日本語 TTS を指定できるか
|
||||||
|
Alexa.TextCommand で「次を読み上げて:{text}」形式が使えるか
|
||||||
|
ローカルブリッジ方式(ユーザーのローカル PC で小さなプロキシサーバーを動かし、クラウドサーバーからローカル経由で alexa.amazon.co.jp を叩く)
|
||||||
|
デバイス一覧(Echo デバイスのみ)
|
||||||
|
名前 deviceType serialNumber
|
||||||
|
プレハブ A4ZXE0RM7LQ7A G0922H085165007R
|
||||||
|
リビングエコー1 ASQZWP4GPYUT7 G8M2DB08522600RL
|
||||||
|
リビングエコー2 ASQZWP4GPYUT7 G8M2DB08522503WF
|
||||||
|
オフィスの右エコー A4ZXE0RM7LQ7A G0922H08525302K5
|
||||||
|
オフィスの左エコー A4ZXE0RM7LQ7A G0922H08525302J9
|
||||||
|
寝室のエコー ASQZWP4GPYUT7 G8M2HN08534302XH
|
||||||
|
Windmill スクリプト(u/admin/alexa_speak)
|
||||||
|
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(); }
|
||||||
|
|
||||||
|
device はデバイス名(日本語)またはシリアル番号で指定可能。Windmill ワーカーから http://alexa_api:3500 でアクセス(windmill_windmill-internal ネットワーク経由)。
|
||||||
|
|
||||||
|
Cookie の更新手順
|
||||||
|
Cookie は数日〜数週間で期限切れ。切れたら:
|
||||||
|
|
||||||
|
# 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 再起動は不要(コンテナ再作成なし)
|
||||||
|
|
||||||
|
既知の問題・落とし穴
|
||||||
|
docker compose restart ≠ リビルド: server.js を変更しても restart ではコンテナ内のコードは古いまま。build + up -d が必要。
|
||||||
|
コンテナ再作成後は Traefik 再起動必須: up -d でコンテナ再作成すると Docker 内部 IP が変わり Traefik が 502/504 を返す。sudo docker restart traefik で解消。
|
||||||
|
alexa-remote2 は使えない: 取得した Cookie 文字列を受け付けない(内部で再認証しようとして失敗)。直接 API 実装が必要。
|
||||||
|
CSRF トークンは Cookie と ヘッダーの両方に必要: csrf ヘッダーだけ、または Cookie だけでは認証失敗。
|
||||||
|
operationPayload に customerId 必須: ないと 400 エラー。
|
||||||
|
レート制限: 短時間に連続リクエストすると HTTP 429 または 200 で音が出ない。通常の通知用途では問題なし。
|
||||||
|
git push がブロックされる: Gitea の pre-receive フック(remote: Gitea: User permission denied for writing)で push が失敗する。根本原因は未調査。ファイル転送は scp で行っている。
|
||||||
|
firstRunCompleted: false はデバイス設定の未完了フラグ: TTS には直接影響しない(root cause ではなかった)。
|
||||||
|
サーバー上の運用コマンド一覧
|
||||||
|
# コンテナ状態確認 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
|
||||||
|
|
||||||
2129
docs/shiraou/11_色々やってダメだった.txt
Normal file
2129
docs/shiraou/11_色々やってダメだった.txt
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user