docs: アーカイブ後にAlexa中間文書を整理削除

This commit is contained in:
Akira
2026-03-04 12:31:42 +09:00
parent 70f842f00e
commit bb958b3554
9 changed files with 0 additions and 3477 deletions

View File

@@ -1,148 +0,0 @@
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 フローを自前で実装。ローカル PCWindowsでのみ実行する
# ローカル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 へのリダイレクトをたどる
取得した Cookieat-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 が海外IPkeinafarm.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

File diff suppressed because it is too large Load Diff

View File

@@ -1,163 +0,0 @@
# Alexa 日本語 TTS 問題 試行記録
最終更新: 2026-03-03
担当: akira + AI (Antigravity)
---
## 現在の問題
`/api/behaviors/preview` + `Alexa.Speak` を使って日本語テキストを TTSで発話させようとしているが、
**日本語Unicode文字だけが Amazon 側でフィルタリングされ、発話されない。**
ASCII文字英語は正常に発話される。
---
## 環境
- テスト用スクリプト: `alexa-api/test_tts.js`ローカルPCから直接 alexa.amazon.co.jp を叩く)
- 本番: `alexa-api/server.js`VPS上のDockerコンテナ
- テストデバイス: オフィスの右エコー (serial: G0922H08525302K5, type: A4ZXE0RM7LQ7A)
- Alexaアプリでデバイス言語設定: **日本語** に設定済み(確認済み)
- VPS IP: 162.43.33.56大阪・Xserver Inc. = 日本国内 ✅
---
## 試行ログ(時系列)
### 【サーバー側での試行】ChatGPT との会話ログより、2026-03-02〜03
#### ❌ `speakType: 'ssml'` を `operationPayload` に追加
```json
"type": "Alexa.Speak",
"operationPayload": { ..., "speakType": "ssml" }
```
→ 変化なし。`Alexa.Speak` はSSML非対応のため無効。
#### ❌ `type: 'Alexa.SpeakSsml'` に変更 + `textToSpeak` にSSMLなし
```json
"type": "Alexa.SpeakSsml",
"operationPayload": { ..., "textToSpeak": text }
```
→ 英語も含めて完全無音LEDも反応なし
#### ❌ `Alexa.SpeakSsml` + `textToSpeak: '<speak>'+text+'</speak>'`
→ 英語も無音。`Alexa.SpeakSsml``textToSpeak` ではなく別キーを要求する模様。
#### ❌ `Alexa.SpeakSsml` + `ssml: ssml`(キー名を変更)
→ 英語も発話せず。
**ChatGPTの最終見解:** `/api/behaviors/preview` では `Alexa.SpeakSsml` は動作しないAPIの癖`Alexa.Speak` に戻すしかない。
---
### 【ローカルPCでの試行】2026-03-03 午前)
#### ❌ `locale: 'ja-JP'` + 日本語テキストtest_tts.js デフォルト)
```js
locale: 'ja-JP',
textToSpeak: 'テストです。聞こえますか'
```
→ 「エ」だけ発話(最初の「テ」の母音のみ)。
#### ✅ `locale: ''` + ASCII: `'hello'`
→ 「ハロー」と正常発話。英語は問題なし。
#### ❌ `locale: ''` + 日本語: `'テストです。聞こえますか'`
→ 「エ」のみ。デバイス言語が英語設定ならこの動作になるが、日本語設定確認済みのため別原因。
#### ❌ `locale: 'ja-JP'` + 日本語: `'テストです。これは日本語のテストです'`
→ 「えんえ」のような音のみ(断片的な音)。
#### ❌ `locale: 'ja-JP'` + ひらがな: `'あいうえお'`
→ 無音LEDは点滅 = 通知は届いている)。
#### 🔍 `locale: 'ja-JP'` + 混在: `'あいうえおThis is Testあいうえお'`
→ 「ディスイズテスタ」のみ発話。
**重要: 日本語部分は無音、ASCII部分のみ日本語アクセントで読まれる。**
→ Amazon側で日本語Unicodeを除去している証拠。
#### ❌ `locale: 'ja-JP'` + Unicodeエスケープ: `'\u3053\u308c\u306f\u30c6\u30b9\u30c8\u3067\u3059'`
→ 無音。ファイルエンコード問題ではないUnicodeエスケープ = `これはテストです` と同一)。
**→ 文字コードの問題ではないことが確定。**
#### ❌ `type: 'AlexaAnnouncement'` + locale:`'ja-JP'` + content[].speak構造
```json
"type": "AlexaAnnouncement",
"operationPayload": {
"content": [{ "locale": "ja-JP", "speak": { "type": "text", "value": "日本語のテストです" } }],
"target": { "devices": [...] }
}
```
→ 「えんえせんと」("AlexaAnnouncement" を日本語発音で読んだもの)。
コンテンツではなくノード型名が読まれた → このノードタイプは別用途。
---
## 確定した事実
| 事実 | 根拠 |
|------|------|
| 通知自体は届いている | LEDが点滅する |
| 英語ASCIIは正常発話 | "hello" → 「ハロー」、"This is Test" → 「ディスイズテスタ」 |
| 日本語Unicodeのみ除去される | 混在テキストで確認。Unicodeエスケープでも同じ |
| デバイス言語設定は日本語 | Alexaアプリで確認済み |
| サーバーIPは日本大阪 | ipinfo.io で確認: Xserver Inc., JP |
| 文字コードは問題なし | Unicodeエスケープテストで確定 |
| `Alexa.SpeakSsml` 系は全て失敗 | 英語含め無音 |
| `AlexaAnnouncement` は別用途 | ノード型名が読まれた |
---
## 仮説(現在)
Amazon の `/api/behaviors/preview` エンドポイントが、
何らかの理由で `textToSpeak` 内の日本語Unicodeを除去している。
考えられる原因:
1. **セッション/Cookie が古くなりJapanese TTS権限が変わった**Cookie の再生成で解消する可能性)
2. **Amazonが API の挙動を変更した**非公開APIのためいつでも変更しうる
3. **別のAPIエンドポイントが必要**(未探索のルートがある可能性)
---
## 試行ログ続き2026-03-03 午後)
#### Cookie 新規取得auth4.js 再実行)
→ 変化なし。Cookie は原因ではなかった。
#### ❌ `AlexaAnnouncement` ノードタイプ
→ 「えんえせんと」("AlexaAnnouncement" を日本語で読んだ)。コンテンツではなくノード名が読まれた。別用途のノード。
#### ✅ **解決!** `sequenceJson` の non-ASCII を `\uXXXX` エスケープに変換
```javascript
var rawSequenceJson = JSON.stringify(sequenceObj).replace(
/[\u0080-\uffff]/g,
function(c) { return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); }
);
```
→ 「これは日本語のテストです」が完璧に発話された!
---
## ✅ 解決済み2026-03-03
**根本原因**: `sequenceJson` 内の日本語文字を raw UTF-8 のまま Amazon に送ると、Amazon 側のパーサーがそれをフィルタリングして無視する。
**解決策**: `JSON.stringify(sequenceObj)` 後に non-ASCII 文字(`\u0080` 以上)を `\uXXXX` 形式のJSONエスケープシーケンスに変換してから `sequenceJson` として送る。
**修正箇所**: `alexa-api/server.js``alexa-api/test_tts.js`
**確定したパラメータ**:
- `type: 'Alexa.Speak'`
- `locale: 'ja-JP'`
- `textToSpeak: <日本語テキスト>`
- `sequenceJson` は non-ASCII を `\uXXXX` エスケープして送る
---
## 参考
- 実装記録: `docs/alexa-api/10_Alexa TTS API 実装記録 (2026-03-02).md`

View File

@@ -1,36 +0,0 @@
{
"formatVersion": 2,
"appVersion": "0.63.7",
"files": [
{
"noteId": "IRnO9uub2Bwg",
"notePath": [
"IRnO9uub2Bwg"
],
"isClone": false,
"title": "Alexa TTS API 実装記録 (2026-03-02)",
"notePosition": 50,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"format": "html",
"dataFileName": "Alexa TTS API 実装記録 (2026-03-02.html",
"noImport": false,
"attributes": [],
"attachments": []
},
{
"noImport": true,
"dataFileName": "navigation.html"
},
{
"noImport": true,
"dataFileName": "index.html"
},
{
"noImport": true,
"dataFileName": "style.css"
}
]
}

View File

@@ -1,318 +0,0 @@
<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>

View File

@@ -1,11 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<frameset cols="25%,75%">
<frame name="navigation" src="navigation.html">
<frame name="detail" src="Alexa%20TTS%20API%20%E5%AE%9F%E8%A3%85%E8%A8%98%E9%8C%B2%20(2026-03-02.html">
</frameset>
</html>

View File

@@ -1,16 +0,0 @@
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<ul>
<li><a href="Alexa%20TTS%20API%20%E5%AE%9F%E8%A3%85%E8%A8%98%E9%8C%B2%20(2026-03-02.html"
target="detail">Alexa TTS API 実装記録 (2026-03-02)</a>
</li>
</ul>
</body>
</html>

View File

@@ -1,551 +0,0 @@
/* !!!!!! TRILIUM CUSTOM CHANGES !!!!!! */
.printed-content .ck-widget__selection-handle, .printed-content .ck-widget__type-around { /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */
display: none;
}
/*
* CKEditor 5 (v41.0.0) content styles.
* Generated on Fri, 26 Jan 2024 10:23:49 GMT.
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/content-styles.html
*/
:root {
--ck-color-image-caption-background: hsl(0, 0%, 97%);
--ck-color-image-caption-text: hsl(0, 0%, 20%);
--ck-color-mention-background: hsla(341, 100%, 30%, 0.1);
--ck-color-mention-text: hsl(341, 100%, 30%);
--ck-color-selector-caption-background: hsl(0, 0%, 97%);
--ck-color-selector-caption-text: hsl(0, 0%, 20%);
--ck-highlight-marker-blue: hsl(201, 97%, 72%);
--ck-highlight-marker-green: hsl(120, 93%, 68%);
--ck-highlight-marker-pink: hsl(345, 96%, 73%);
--ck-highlight-marker-yellow: hsl(60, 97%, 73%);
--ck-highlight-pen-green: hsl(112, 100%, 27%);
--ck-highlight-pen-red: hsl(0, 85%, 49%);
--ck-image-style-spacing: 1.5em;
--ck-inline-image-style-spacing: calc(var(--ck-image-style-spacing) / 2);
--ck-todo-list-checkmark-size: 16px;
}
/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table .ck-table-resized {
table-layout: fixed;
}
/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table table {
overflow: hidden;
}
/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table td,
.ck-content .table th {
overflow-wrap: break-word;
position: relative;
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table {
margin: 0.9em auto;
display: table;
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
height: 100%;
border: 1px double hsl(0, 0%, 70%);
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table table td,
.ck-content .table table th {
min-width: 2em;
padding: .4em;
border: 1px solid hsl(0, 0%, 75%);
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table table th {
font-weight: bold;
background: hsla(0, 0%, 0%, 5%);
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content[dir="rtl"] .table th {
text-align: right;
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content[dir="ltr"] .table th {
text-align: left;
}
/* @ckeditor/ckeditor5-table/theme/tablecaption.css */
.ck-content .table > figcaption {
display: table-caption;
caption-side: top;
word-break: break-word;
text-align: center;
color: var(--ck-color-selector-caption-text);
background-color: var(--ck-color-selector-caption-background);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
position: relative;
clear: both;
padding: 5px 0;
display: flex;
align-items: center;
justify-content: center;
}
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after {
content: '';
position: absolute;
border-bottom: 2px dashed hsl(0, 0%, 77%);
width: 100%;
}
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break__label {
position: relative;
z-index: 1;
padding: .3em .6em;
display: block;
text-transform: uppercase;
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
font-size: 0.75em;
font-weight: bold;
color: hsl(0, 0%, 20%);
background: hsl(0, 0%, 100%);
box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* @ckeditor/ckeditor5-media-embed/theme/mediaembed.css */
.ck-content .media {
clear: both;
margin: 0.9em 0;
display: block;
min-width: 15em;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list {
list-style: none;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list li {
position: relative;
margin-bottom: 5px;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list li .todo-list {
margin-top: 5px;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input {
-webkit-appearance: none;
display: inline-block;
position: relative;
width: var(--ck-todo-list-checkmark-size);
height: var(--ck-todo-list-checkmark-size);
vertical-align: middle;
border: 0;
left: -25px;
margin-right: -15px;
right: 0;
margin-left: 0;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content[dir=rtl] .todo-list .todo-list__label > input {
left: 0;
margin-right: 0;
right: -25px;
margin-left: -15px;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input::before {
display: block;
position: absolute;
box-sizing: border-box;
content: '';
width: 100%;
height: 100%;
border: 1px solid hsl(0, 0%, 20%);
border-radius: 2px;
transition: 250ms ease-in-out box-shadow;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input::after {
display: block;
position: absolute;
box-sizing: content-box;
pointer-events: none;
content: '';
left: calc( var(--ck-todo-list-checkmark-size) / 3 );
top: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
width: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
height: calc( var(--ck-todo-list-checkmark-size) / 2.6 );
border-style: solid;
border-color: transparent;
border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0;
transform: rotate(45deg);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input[checked]::before {
background: hsl(126, 64%, 41%);
border-color: hsl(126, 64%, 41%);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input[checked]::after {
border-color: hsl(0, 0%, 100%);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label .todo-list__label__description {
vertical-align: middle;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] {
position: absolute;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > input,
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input {
cursor: pointer;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > input:hover::before, .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input:hover::before {
box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input {
-webkit-appearance: none;
display: inline-block;
position: relative;
width: var(--ck-todo-list-checkmark-size);
height: var(--ck-todo-list-checkmark-size);
vertical-align: middle;
border: 0;
left: -25px;
margin-right: -15px;
right: 0;
margin-left: 0;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content[dir=rtl] .todo-list .todo-list__label > span[contenteditable=false] > input {
left: 0;
margin-right: 0;
right: -25px;
margin-left: -15px;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::before {
display: block;
position: absolute;
box-sizing: border-box;
content: '';
width: 100%;
height: 100%;
border: 1px solid hsl(0, 0%, 20%);
border-radius: 2px;
transition: 250ms ease-in-out box-shadow;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::after {
display: block;
position: absolute;
box-sizing: content-box;
pointer-events: none;
content: '';
left: calc( var(--ck-todo-list-checkmark-size) / 3 );
top: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
width: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
height: calc( var(--ck-todo-list-checkmark-size) / 2.6 );
border-style: solid;
border-color: transparent;
border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0;
transform: rotate(45deg);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::before {
background: hsl(126, 64%, 41%);
border-color: hsl(126, 64%, 41%);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::after {
border-color: hsl(0, 0%, 100%);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] {
position: absolute;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol {
list-style-type: decimal;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol {
list-style-type: lower-latin;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol {
list-style-type: lower-roman;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol ol {
list-style-type: upper-latin;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol ol ol {
list-style-type: upper-roman;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul {
list-style-type: disc;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul {
list-style-type: circle;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul ul {
list-style-type: square;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul ul ul {
list-style-type: square;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image {
display: table;
clear: both;
text-align: center;
margin: 0.9em auto;
min-width: 50px;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image img {
display: block;
margin: 0 auto;
max-width: 100%;
min-width: 100%;
height: auto;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image-inline {
/*
* Normally, the .image-inline would have "display: inline-block" and "img { width: 100% }" (to follow the wrapper while resizing).;
* Unfortunately, together with "srcset", it gets automatically stretched up to the width of the editing root.
* This strange behavior does not happen with inline-flex.
*/
display: inline-flex;
max-width: 100%;
align-items: flex-start;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image-inline picture {
display: flex;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image-inline picture,
.ck-content .image-inline img {
flex-grow: 1;
flex-shrink: 1;
max-width: 100%;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content img.image_resized {
height: auto;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized {
max-width: 100%;
display: block;
box-sizing: border-box;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized img {
width: 100%;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized > figcaption {
display: block;
}
/* @ckeditor/ckeditor5-image/theme/imagecaption.css */
.ck-content .image > figcaption {
display: table-caption;
caption-side: bottom;
word-break: break-word;
color: var(--ck-color-image-caption-text);
background-color: var(--ck-color-image-caption-background);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-left,
.ck-content .image-style-block-align-right {
max-width: calc(100% - var(--ck-image-style-spacing));
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-left,
.ck-content .image-style-align-right {
clear: none;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-side {
float: right;
margin-left: var(--ck-image-style-spacing);
max-width: 50%;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-left {
float: left;
margin-right: var(--ck-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-center {
margin-left: auto;
margin-right: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-right {
float: right;
margin-left: var(--ck-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-right {
margin-right: 0;
margin-left: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-left {
margin-left: 0;
margin-right: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content p + .image-style-align-left,
.ck-content p + .image-style-align-right,
.ck-content p + .image-style-side {
margin-top: 0;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-left,
.ck-content .image-inline.image-style-align-right {
margin-top: var(--ck-inline-image-style-spacing);
margin-bottom: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-left {
margin-right: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-right {
margin-left: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-yellow {
background-color: var(--ck-highlight-marker-yellow);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-green {
background-color: var(--ck-highlight-marker-green);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-pink {
background-color: var(--ck-highlight-marker-pink);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-blue {
background-color: var(--ck-highlight-marker-blue);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-red {
color: var(--ck-highlight-pen-red);
background-color: transparent;
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-green {
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */
.ck-content blockquote {
overflow: hidden;
padding-right: 1.5em;
padding-left: 1.5em;
margin-left: 0;
margin-right: 0;
font-style: italic;
border-left: solid 5px hsl(0, 0%, 80%);
}
/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */
.ck-content[dir="rtl"] blockquote {
border-left: 0;
border-right: solid 5px hsl(0, 0%, 80%);
}
/* @ckeditor/ckeditor5-basic-styles/theme/code.css */
.ck-content code {
background-color: hsla(0, 0%, 78%, 0.3);
padding: .15em;
border-radius: 2px;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-tiny {
font-size: .7em;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-small {
font-size: .85em;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-big {
font-size: 1.4em;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-huge {
font-size: 1.8em;
}
/* @ckeditor/ckeditor5-mention/theme/mention.css */
.ck-content .mention {
background: var(--ck-color-mention-background);
color: var(--ck-color-mention-text);
}
/* @ckeditor/ckeditor5-horizontal-line/theme/horizontalline.css */
.ck-content hr {
margin: 15px 0;
height: 4px;
background: hsl(0, 0%, 87%);
border: 0;
}
/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */
.ck-content pre {
padding: 1em;
color: hsl(0, 0%, 20.8%);
background: hsla(0, 0%, 78%, 0.3);
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
text-align: left;
direction: ltr;
tab-size: 4;
white-space: pre-wrap;
font-style: normal;
min-width: 200px;
}
/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */
.ck-content pre code {
background: unset;
padding: 0;
border-radius: 0;
}
@media print {
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
padding: 0;
}
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after {
display: none;
}
}

View File

@@ -1,105 +0,0 @@
# 引き継ぎ - `u/admin/alexa_speak` API反映後にUIドロップダウンが変わらない件
> **作成日**: 2026-03-04
> **対象**: `windmill.keinafarm.net` / workspace `admins`
> **対象スクリプト**: `u/admin/alexa_speak`
> **目的**: 別端末から同じ事象に遭遇しても、原因切り分けと復旧をすぐ実施できるようにする
---
## 1. 事象の概要
`u/admin/alexa_speak` を Windmill API`create-script`)で更新した直後、
- Scriptタブ上のコードは更新済み
- `schema` 上も `device``dynselect-device`
- しかし Inputフォームは `Device` がテキスト入力のまま(ドロップダウンにならない)
という状態になった。
---
## 2. 当日の時系列(要点)
1. 既存スクリプトを取得し、ローカル `scripts/alexa_speak.ts`Dynamic Select実装ありをAPIで反映
2. サーバー再取得で `content` 一致を確認(更新自体は成功)
3. UIを開くと `Device` が入力欄のままで、ドロップダウン化されていない
4. `schema.device``format: dynselect-device`, `originalType: DynSelect_device` に更新して再反映
5. それでも UI は直ちには変わらず
6. Windmill UIで `Edit` に入り、`Deploy` を1回実施
7. 直後にドロップダウン表示へ反映
---
## 3. 確認できた事実
- API反映は成功しているhash更新
- 中間: `a6010687183a199d`
- 最終: `318d78f45a084e32`
- 最終状態では以下がAPIで確認済み
- `schema.properties.device.format = "dynselect-device"`
- `schema.properties.device.originalType = "DynSelect_device"`
- UI反映は API反映だけでは即時にならず、`Edit -> Deploy` 後に反映された
---
## 4. 想定される原因
Windmill CE 側で、API経由更新時にフォームUIメタ情報入力ウィジェット解決の再計算または再適用が即時反映されないケースがある。
実務上は「API更新後にUIで1回Deploy」が回避策として有効。
---
## 5. 再現時の標準対応手順Runbook
### 5.1 APIでスクリプト更新
```bash
cd /home/akira/develop/windmill_workflow
./wm-api.sh get-script u/admin/alexa_speak > /tmp/remote_alexa_speak.json
# parent_hash を含む payload を作成して create-script
./wm-api.sh create-script /tmp/alexa_speak_push.json
```
### 5.2 APIで反映確認
```bash
./wm-api.sh get-script u/admin/alexa_speak
```
確認ポイント:
- `hash` が更新されている
- `content` が想定コードになっている
- `schema.properties.device.format``dynselect-device`
- `schema.properties.device.originalType``DynSelect_device`
### 5.3 UI反映されない場合
1. `u/admin/alexa_speak` を最新リビジョンで開く
2. ハードリロード(`Ctrl + Shift + R`
3. 変化がなければ `Edit -> Deploy` を1回実施
4. Inputフォームの `Device` がドロップダウン化されたことを確認
---
## 6. 補足(今回の最終状態)
- スクリプト: `u/admin/alexa_speak`
- 期待UI:
- `Device`: ドロップダウンdynselect
- `Text`: テキスト入力
- 前提:
- `alexa_api` コンテナが稼働
- `http://alexa_api:3500/devices` が取得可能
---
## 7. 引き継ぎメモ
- 「API反映成功」と「UIフォーム反映成功」は同時とは限らない
- 引き継ぎ時は、必ず以下をセットで確認する
1. APIレスポンスの `hash``schema`
2. UI表示必要なら `Edit -> Deploy`