Compare commits
2 Commits
07258bb46d
...
4954cc0741
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4954cc0741 | ||
|
|
1be261c95f |
556
docs/30_マスタードキュメント_Alexa_TTS_API編.md
Normal file
556
docs/30_マスタードキュメント_Alexa_TTS_API編.md
Normal 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)
|
||||
|
||||
**ローカル PC(Windows)でのみ実行可能**(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. 取得した Cookie(at-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 | 本マスタードキュメント作成 |
|
||||
@@ -121,17 +121,43 @@ Amazon の `/api/behaviors/preview` エンドポイントが、
|
||||
|
||||
---
|
||||
|
||||
## 次に試すべきこと
|
||||
## 試行ログ続き(2026-03-03 午後)
|
||||
|
||||
- [ ] `auth4.js` で Cookie を新規取得してテスト(セッションリセット)
|
||||
- [ ] `/api/behaviors/preview` 以外のエンドポイントを探す(例: `/api/ap/d-notification`など)
|
||||
- [ ] `Alexa.TextCommand` ノードタイプ(「テキストで命令を送る」別ルート)
|
||||
- [ ] ローカルブリッジ方式(ローカルPCをプロキシにしてAmazonにリクエストを転送する)
|
||||
- [ ] alexa-cookie / alexa-remote2 のソースコードから別APIを調査する
|
||||
#### 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`
|
||||
- 上記ファイルに記録されていた未解決事項がこのファイルに続く
|
||||
|
||||
36
docs/alexa-api/Alexa TTS API 実装記録 (2026-03-02)/!!!meta.json
Normal file
36
docs/alexa-api/Alexa TTS API 実装記録 (2026-03-02)/!!!meta.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
<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: 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</p>
|
||||
<h2>認証方法(auth4.js)</h2>
|
||||
<p>Amazon Japan OpenID フローを自前で実装。ローカル PC(Windows)でのみ実行する:</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>取得した Cookie(at-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>{ 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" }</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 が海外IP(keinafarm.net = 非日本IP)からのリクエストを IP ベースでフィルタリングし、textToSpeak
|
||||
の日本語文字を除去している。Alexa.TextCommand は同じ問題がない(異なる API パス)。</p>
|
||||
<p><strong>確認済み事実</strong>: alexa_api の server.js ログには日本語テキストが正しく届いている。除去は
|
||||
Amazon サーバー側で発生。</p>
|
||||
<p><strong>次の調査候補</strong>:</p>
|
||||
<ul>
|
||||
<li>SSML の <lang xml:lang="ja-JP"> タグで強制的に日本語 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) { 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();
|
||||
}</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>
|
||||
11
docs/alexa-api/Alexa TTS API 実装記録 (2026-03-02)/index.html
Normal file
11
docs/alexa-api/Alexa TTS API 実装記録 (2026-03-02)/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,16 @@
|
||||
<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>
|
||||
551
docs/alexa-api/Alexa TTS API 実装記録 (2026-03-02)/style.css
Normal file
551
docs/alexa-api/Alexa TTS API 実装記録 (2026-03-02)/style.css
Normal file
@@ -0,0 +1,551 @@
|
||||
/* !!!!!! 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user