# WebSocket API リファレンス

| エンドポイント | 用途 | トークンの権限 | protocolVersion |
|---|---|---|---|
| `wss://<host>/v1/subscribe` | 文字起こし結果を受け取る | `transcript:read` | 1 |
| `wss://<host>/v1/ingest` | 音声を送る | `ingest:write` | 1 |

機械可読な版は [asyncapi.yaml](asyncapi.yaml)（AsyncAPI 3.1）と [schemas/](schemas/common.json)（JSON Schema 2020-12）にあります。

## 共通の決まり

- クライアントと文字おこしくんは、制御メッセージを、WebSocket のテキストフレーム 1 個に UTF-8 の JSON オブジェクトを 1 個ずつ入れて送る。`type` がメッセージの種類を表す。
- **クライアントは、認証のトークンを最初の `hello` メッセージで送る。** トークンを URL・ヘッダー・サブプロトコルには載せない。トークンはセッション単位の短期のもので、API キーではない。
- 受け手は未知のフィールドを無視する（前方互換）。送り手は任意フィールドを省くか `null` にする。受け手は未知の `type` のメッセージを不正なメッセージとして扱う。
- 文字おこしくんは、文字列の長さを、Unicode のコードポイントの数で数える。クライアントと文字おこしくんは、1 メッセージを 64 KiB までにし、permessage-deflate を使わない。
- 文字おこしくんは、エラーを `{"type":"error","error":{"code","message","fatal"}}` の形で送る。`fatal: true` なら、文字おこしくんはこの後に接続を閉じる。文字おこしくんは WebSocket の close code を規定しない。クライアントは、切れた理由を、直前に受け取った `error.code` で判断する。
- 文字おこしくんは、非 fatal な `error` を送った後に接続を切ることがある。たとえば、hello のタイムアウトの後と、受信が文字おこしくんからの送信に追いつかないクライアントへの `rate_limited` の後に、文字おこしくんは接続を切る。クライアントは、この切断を予期しない切断として扱い、短いバックオフで再接続する。

クライアントがいつ再接続し、いつ再接続せずに終えるかは、[再接続とエラーの扱い](reconnection.md)にあります。

## Subscribe（`/v1/subscribe`）

接続の流れ: `hello` → `welcome`（スナップショット）→ 差分のイベント → `session_ended`。受け手は `utteranceId` ごとに `revision` が最大の発話だけを採用します（[改訂モデル](revisions.md)）。

### クライアント → サーバー

#### `hello`

クライアントは、接続の直後に hello を 1 回だけ送る。

スキーマ: [`schemas/subscribe-client-message.json#/$defs/Hello`](schemas/subscribe-client-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"hello"` | ✓ |  |
| `protocolVersion` | [ProtocolVersion](#common-protocolversion) | ✓ |  |
| `token` | [Token](#common-token) | ✓ |  |

例: 接続直後の名乗り

```json
{"type":"hello","protocolVersion":1,"token":"eyJ2IjoxLCJzaWQiOiJzZXNfMDEiLCJzY29wZSI6InRyYW5zY3JpcHQ6cmVhZCJ9.c2lnbmF0dXJl"}
```

#### `ping`

ブラウザは WebSocket の ping フレームを扱えないので、クライアントは、生存確認とプロキシのアイドル切断の防止のために ping を送る。文字おこしくんは pong を返す。

スキーマ: [`schemas/subscribe-client-message.json#/$defs/Ping`](schemas/subscribe-client-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"ping"` | ✓ |  |

例: 生存確認（15 秒ごとなど）

```json
{"type":"ping"}
```

### サーバー → クライアント

#### `welcome`

文字おこしくんは、接続した時点の状態のスナップショットを welcome で送る。受け手は状態を丸ごと置き換える（再接続のときも）。recent は、直近の発話の最新の改訂を startedAt の昇順に並べたものである（認識途中を含む）。

スキーマ: [`schemas/subscribe-server-message.json#/$defs/Welcome`](schemas/subscribe-server-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"welcome"` | ✓ |  |
| `protocolVersion` | [ProtocolVersion](#common-protocolversion) | ✓ |  |
| `sessionId` | [ServerId](#common-serverid) | ✓ |  |
| `speakers` | array<object> | ✓ |  |
| `speakers[].speaker` | object | ✓ | Subscribe で文字おこしくんが送る話者を表す。 |
| `speakers[].speaker.speakerId` | [ServerId](#common-serverid) | ✓ |  |
| `speakers[].speaker.displayName` | [DisplayName](#common-displayname) | ✓ |  |
| `speakers[].speaker.avatarUrl` | [AvatarUrl](#common-avatarurl) |  |  |
| `speakers[].inferred` | boolean | ✓ |  |
| `tracks` | array<object> | ✓ |  |
| `tracks[]` | object（下のいずれか） | ✓ | トラックを表す。single は 1 人の話者だけのトラックを、mixed は複数の話者の声が入ったトラックを表す。 |
| `tracks[].trackId` | [ServerId](#common-serverid) | ✓ | mode が "single" のとき |
| `tracks[].mode` | `"single"` | ✓ | mode が "single" のとき |
| `tracks[].speakerId` | [ServerId](#common-serverid) | ✓ | mode が "single" のとき |
| `tracks[].trackId` | [ServerId](#common-serverid) | ✓ | mode が "mixed" のとき |
| `tracks[].mode` | `"mixed"` | ✓ | mode が "mixed" のとき |
| `speakingTrackIds` | array<[ServerId](#common-serverid)> | ✓ | 発声中のトラックを表す。 |
| `recent` | array<object> | ✓ |  |
| `recent[].utteranceId` | [ServerId](#common-serverid) | ✓ |  |
| `recent[].trackId` | [ServerId](#common-serverid) | ✓ |  |
| `recent[].speaker` | object（下のいずれか） | ✓ | 発話の話者を表す。declared は入力元が宣言した話者を、inferred は話者分離が推定した話者を、unattributed は話者が分からないことを表す。inferred の発話は、後の改訂で別の speakerId の話者に付け替わることがある。受け手は unattributed の発話をトラック単位で表示する。 |
| `recent[].speaker.kind` | `"declared"` \| `"inferred"` | ✓ | kind が "declared" か "inferred" のとき |
| `recent[].speaker.speakerId` | [ServerId](#common-serverid) | ✓ | kind が "declared" か "inferred" のとき |
| `recent[].speaker.kind` | `"unattributed"` | ✓ | kind が "unattributed" のとき |
| `recent[].stage` | `"partial"` \| `"final"` \| `"refined"` | ✓ |  |
| `recent[].revision` | integer（≥ 0） | ✓ | 同じ utteranceId では、大きい方が新しい。文字おこしくんが partial を間引くことがあるので、連番とは限らない。 |
| `recent[].text` | string | ✓ | 表示するテキストを表す（空文字のこともある）。受け手はテキストを HTML として埋め込まないこと。 |
| `recent[].startedAt` | [Timestamp](#common-timestamp) | ✓ | 発話の最初の音声フレームを文字おこしくんが受け取った時刻（エポックミリ秒）を表す。受け手は話者をまたいで発話を並べるときに startedAt を使う。 |

例: 接続時点のスナップショット

```json
{"type":"welcome","protocolVersion":1,"sessionId":"ses_01J8Z6Q3M4","speakers":[{"speaker":{"speakerId":"alice","displayName":"たろう","avatarUrl":"https://cdn.example.com/avatars/alice.png"},"inferred":false},{"speaker":{"speakerId":"room-mic#1","displayName":"話者 1"},"inferred":true}],"tracks":[{"trackId":"alice-mic","mode":"single","speakerId":"alice"},{"trackId":"room-mic","mode":"mixed"}],"speakingTrackIds":["alice-mic"],"recent":[{"utteranceId":"utt_01","trackId":"alice-mic","speaker":{"kind":"declared","speakerId":"alice"},"stage":"refined","revision":5,"text":"こんにちは、今日はよろしくお願いします。","startedAt":1790000000000}]}
```

#### `speaker_upserted`

文字おこしくんは、話者の追加・更新を知らせる。inferred は、話者分離が推定した話者かどうかを表す。

スキーマ: [`schemas/subscribe-server-message.json#/$defs/SpeakerUpserted`](schemas/subscribe-server-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"speaker_upserted"` | ✓ |  |
| `speaker` | object | ✓ | Subscribe で文字おこしくんが送る話者を表す。 |
| `speaker.speakerId` | [ServerId](#common-serverid) | ✓ |  |
| `speaker.displayName` | [DisplayName](#common-displayname) | ✓ |  |
| `speaker.avatarUrl` | [AvatarUrl](#common-avatarurl) |  |  |
| `inferred` | boolean | ✓ |  |

例: 話者の追加・更新

```json
{"type":"speaker_upserted","speaker":{"speakerId":"bob","displayName":"はなこ"},"inferred":false}
```

#### `speaker_removed`

文字おこしくんは、話者の削除を知らせる。受け手はその話者の過去の発話を消さない。

スキーマ: [`schemas/subscribe-server-message.json#/$defs/SpeakerRemoved`](schemas/subscribe-server-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"speaker_removed"` | ✓ |  |
| `speakerId` | [ServerId](#common-serverid) | ✓ |  |

例: 話者の削除

```json
{"type":"speaker_removed","speakerId":"bob"}
```

#### `track_opened`

文字おこしくんは、トラックが開いたことを知らせる。

スキーマ: [`schemas/subscribe-server-message.json#/$defs/TrackOpened`](schemas/subscribe-server-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"track_opened"` | ✓ |  |
| `track` | object（下のいずれか） | ✓ | トラックを表す。single は 1 人の話者だけのトラックを、mixed は複数の話者の声が入ったトラックを表す。 |
| `track.trackId` | [ServerId](#common-serverid) | ✓ | mode が "single" のとき |
| `track.mode` | `"single"` | ✓ | mode が "single" のとき |
| `track.speakerId` | [ServerId](#common-serverid) | ✓ | mode が "single" のとき |
| `track.trackId` | [ServerId](#common-serverid) | ✓ | mode が "mixed" のとき |
| `track.mode` | `"mixed"` | ✓ | mode が "mixed" のとき |

例: トラックが開いた

```json
{"type":"track_opened","track":{"trackId":"bob-mic","mode":"single","speakerId":"bob"}}
```

#### `track_closed`

文字おこしくんは、トラックが閉じたことを知らせる。受け手はそのトラックの発話を消さない。

スキーマ: [`schemas/subscribe-server-message.json#/$defs/TrackClosed`](schemas/subscribe-server-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"track_closed"` | ✓ |  |
| `trackId` | [ServerId](#common-serverid) | ✓ |  |

例: トラックが閉じた

```json
{"type":"track_closed","trackId":"bob-mic"}
```

#### `speech_started`

文字おこしくんは、トラックが発声中になったことを知らせる（「話している」の表示に使う）。

スキーマ: [`schemas/subscribe-server-message.json#/$defs/SpeechStarted`](schemas/subscribe-server-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"speech_started"` | ✓ |  |
| `trackId` | [ServerId](#common-serverid) | ✓ |  |

例: 発声中になった

```json
{"type":"speech_started","trackId":"alice-mic"}
```

#### `speech_ended`

文字おこしくんは、トラックの発声が終わったことを知らせる。

スキーマ: [`schemas/subscribe-server-message.json#/$defs/SpeechEnded`](schemas/subscribe-server-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"speech_ended"` | ✓ |  |
| `trackId` | [ServerId](#common-serverid) | ✓ |  |

例: 発声が終わった

```json
{"type":"speech_ended","trackId":"alice-mic"}
```

#### `utterance`

文字おこしくんは、発話の改訂を 1 件ずつ送る。受け手は utteranceId ごとに revision が最大のものだけを採用し、同じか小さい revision は捨てる（追記しない。置き換える）。

スキーマ: [`schemas/subscribe-server-message.json#/$defs/Utterance`](schemas/subscribe-server-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"utterance"` | ✓ |  |
| `utterance` | object | ✓ | 発話 1 件の、ある時点の改訂を表す。文字おこしくんが発話を partial（認識途中）→ final（確定）→ refined（LLM による整形）と改訂するたびに、revision が増える。 |
| `utterance.utteranceId` | [ServerId](#common-serverid) | ✓ |  |
| `utterance.trackId` | [ServerId](#common-serverid) | ✓ |  |
| `utterance.speaker` | object（下のいずれか） | ✓ | 発話の話者を表す。declared は入力元が宣言した話者を、inferred は話者分離が推定した話者を、unattributed は話者が分からないことを表す。inferred の発話は、後の改訂で別の speakerId の話者に付け替わることがある。受け手は unattributed の発話をトラック単位で表示する。 |
| `utterance.speaker.kind` | `"declared"` \| `"inferred"` | ✓ | kind が "declared" か "inferred" のとき |
| `utterance.speaker.speakerId` | [ServerId](#common-serverid) | ✓ | kind が "declared" か "inferred" のとき |
| `utterance.speaker.kind` | `"unattributed"` | ✓ | kind が "unattributed" のとき |
| `utterance.stage` | `"partial"` \| `"final"` \| `"refined"` | ✓ |  |
| `utterance.revision` | integer（≥ 0） | ✓ | 同じ utteranceId では、大きい方が新しい。文字おこしくんが partial を間引くことがあるので、連番とは限らない。 |
| `utterance.text` | string | ✓ | 表示するテキストを表す（空文字のこともある）。受け手はテキストを HTML として埋め込まないこと。 |
| `utterance.startedAt` | [Timestamp](#common-timestamp) | ✓ | 発話の最初の音声フレームを文字おこしくんが受け取った時刻（エポックミリ秒）を表す。受け手は話者をまたいで発話を並べるときに startedAt を使う。 |

例: 認識途中（partial）。同じ utteranceId の後の改訂で置き換わる

```json
{"type":"utterance","utterance":{"utteranceId":"utt_02","trackId":"alice-mic","speaker":{"kind":"declared","speakerId":"alice"},"stage":"partial","revision":1,"text":"それでは始め","startedAt":1790000004000}}
```

例: 確定（final）

```json
{"type":"utterance","utterance":{"utteranceId":"utt_02","trackId":"alice-mic","speaker":{"kind":"declared","speakerId":"alice"},"stage":"final","revision":3,"text":"それでは始めましょう","startedAt":1790000004000}}
```

例: 整形済み（refined）。final の後に非同期で届く

```json
{"type":"utterance","utterance":{"utteranceId":"utt_02","trackId":"alice-mic","speaker":{"kind":"declared","speakerId":"alice"},"stage":"refined","revision":4,"text":"それでは始めましょう。","startedAt":1790000004000}}
```

例: 話者分離による推定の話者（mixed トラック）

```json
{"type":"utterance","utterance":{"utteranceId":"utt_03","trackId":"room-mic","speaker":{"kind":"inferred","speakerId":"room-mic#1"},"stage":"final","revision":2,"text":"よろしくお願いします","startedAt":1790000006500}}
```

例: 話者不明（トラック単位で表示する）

```json
{"type":"utterance","utterance":{"utteranceId":"utt_04","trackId":"room-mic","speaker":{"kind":"unattributed"},"stage":"partial","revision":0,"text":"えっと","startedAt":1790000009120}}
```

#### `session_ended`

文字おこしくんは、文字起こしが終了したことを知らせる。この後、文字おこしくんはイベントを送らない。クライアントは再接続しない。

スキーマ: [`schemas/subscribe-server-message.json#/$defs/SessionEnded`](schemas/subscribe-server-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"session_ended"` | ✓ |  |

例: 文字起こしの終了

```json
{"type":"session_ended"}
```

#### `error`

文字おこしくんは、エラーを error で知らせる。error.fatal が true なら、文字おこしくんはこの後に接続を切る。

スキーマ: [`schemas/subscribe-server-message.json#/$defs/Error`](schemas/subscribe-server-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"error"` | ✓ |  |
| `error` | object | ✓ | エラーの内容を表す。fatal が true なら、文字おこしくんはこの後に接続を閉じる。 |
| `error.code` | [ErrorCode](#common-errorcode) | ✓ |  |
| `error.message` | string | ✓ | 人が読むための説明（英語）を表す。文言は互換性の対象外である。 |
| `error.fatal` | boolean | ✓ |  |

例: 終了済み・存在しないセッション（fatal。再接続しない）

```json
{"type":"error","error":{"code":"session_ended","message":"session not found","fatal":true}}
```

例: クライアントの受信が、文字おこしくんからの送信に追いつかない（文字おこしくんは非 fatal の error を送った後に接続を切る。クライアントは短いバックオフで再接続する）

```json
{"type":"error","error":{"code":"rate_limited","message":"the client is too slow to keep up","fatal":false}}
```

#### `pong`

文字おこしくんは、ping に pong で応える。

スキーマ: [`schemas/subscribe-server-message.json#/$defs/Pong`](schemas/subscribe-server-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"pong"` | ✓ |  |

例: ping への応答

```json
{"type":"pong"}
```

## Ingest（`/v1/ingest`）

接続の流れ: `hello` → `welcome` を待つ → 全話者の `speaker_upsert` → 開いている全トラックの `track_open` → `state_synced` → 音声のバイナリフレームと `speech_start` / `speech_end`。詳しい手順は[音声を送る](ingest.md)にあります。

<h3 id="audio-frame">音声バイナリフレーム</h3>

| offset | size | type | field | 内容 |
|---|---|---|---|---|
| 0 | 1 | u8 | frame type | 常に 0x01（audio）にする。 |
| 1 | 2 | u16 BE | channel | track_open で割り当てた番号（0〜65535）を入れる。 |
| 3 | 4 | u32 BE | sequence | トラックごとに 1 ずつ増やす。2^32 で 0 に折り返す。再接続の後は 0 から数え直してよい。 |
| 7 | 残り | bytes | payload | opus では、Opus のパケットを 1 個入れる（Ogg などのコンテナに入れない）。pcm_s16le では、符号付き 16 bit リトルエンディアンの PCM を入れる（2ch はインターリーブする）。 |

例（channel = 1、sequence = 42、payload = `f8 ff fe`）:

```text
01 00 01 00 00 00 2a f8 ff fe
```

- `opus` のトラックで payload が Discord の無音フレーム（`f8 ff fe` の 3 バイト）のとき、文字おこしくんは無音フレームをデコーダーに通すだけで、音声認識にも利用量にも入れない。デコーダーは次の音声との継ぎ目に無音フレームを使うので、クライアントは無音フレームを省かずに送る。無音フレームを送らない場合も、クライアントは `sequence` を飛ばさない（文字おこしくんは、飛ばした番号を欠損として補間する）。文字おこしくんは、無音フレームの直後の欠番を補間しない（補間しても無音になるため）。

### クライアント → サーバー

#### `hello`

クライアントは、接続の直後に hello を 1 回だけ送る。welcome を受け取るまで、ほかのメッセージを送らない。

スキーマ: [`schemas/ingest-client-message.json#/$defs/Hello`](schemas/ingest-client-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"hello"` | ✓ |  |
| `protocolVersion` | [ProtocolVersion](#common-protocolversion) | ✓ |  |
| `token` | [Token](#common-token) | ✓ |  |
| `clientName` | [ClientName](#common-clientname) |  |  |

例: 接続直後の名乗り

```json
{"type":"hello","protocolVersion":1,"token":"eyJ2IjoxLCJzaWQiOiJzZXNfMDEiLCJzY29wZSI6InRyYW5zY3JpcHQ6cmVhZCJ9.c2lnbmF0dXJl","clientName":"my-recorder/1.0"}
```

#### `speaker_upsert`

クライアントは、このメッセージで話者を追加・更新する。single トラックの話者の speaker_upsert は、そのトラックの track_open より前に送る。

スキーマ: [`schemas/ingest-client-message.json#/$defs/SpeakerUpsert`](schemas/ingest-client-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"speaker_upsert"` | ✓ |  |
| `speaker` | object | ✓ | Ingest でクライアントが宣言する話者を表す。 |
| `speaker.speakerId` | [ClientId](#common-clientid) | ✓ |  |
| `speaker.displayName` | [DisplayName](#common-displayname) | ✓ |  |
| `speaker.avatarUrl` | [AvatarUrl](#common-avatarurl) |  |  |

例: 話者の宣言

```json
{"type":"speaker_upsert","speaker":{"speakerId":"alice","displayName":"たろう","avatarUrl":"https://cdn.example.com/avatars/alice.png"}}
```

#### `speaker_remove`

クライアントは、このメッセージで話者を削除する。その話者の single トラックは、先に track_close しておく。

スキーマ: [`schemas/ingest-client-message.json#/$defs/SpeakerRemove`](schemas/ingest-client-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"speaker_remove"` | ✓ |  |
| `speakerId` | [ClientId](#common-clientid) | ✓ |  |

例: 話者の削除

```json
{"type":"speaker_remove","speakerId":"alice"}
```

#### `track_open`

クライアントは、このメッセージでトラックを開き、バイナリフレームで使う channel 番号を割り当てる。

スキーマ: [`schemas/ingest-client-message.json#/$defs/TrackOpen`](schemas/ingest-client-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"track_open"` | ✓ |  |
| `trackId` | [ClientId](#common-clientid) | ✓ |  |
| `channel` | integer（0〜65535） | ✓ | バイナリフレームのヘッダで、このトラックを表す番号である。クライアントは、番号を接続の中で一意にする（track_close の後は再利用してよい）。 |
| `format` | object | ✓ | 音声の形式を表す。opus では、sampleRate に 48000 だけを使える（1/2ch）。pcm_s16le は、符号付き 16 bit リトルエンディアンの PCM を表す。公式の文字おこしくん（moji.kaitoranma.com）は、pcm_s16le で 8000〜48000 Hz と 1/2ch を受け付ける（2ch はインターリーブする）。扱えない組み合わせには、文字おこしくんが unsupported_format を返す。 |
| `format.codec` | `"opus"` \| `"pcm_s16le"` | ✓ |  |
| `format.sampleRate` | integer（≥ 1） | ✓ | サンプリングレート（Hz）を表す。 |
| `format.channels` | `1` \| `2` | ✓ |  |
| `track` | object（下のいずれか） | ✓ | single は、1 人の話者だけのトラックを表す（クライアントは、speakerId の話者を先に宣言しておく）。mixed は、複数の話者の声が入ったトラックを表す（diarize が true なら、文字おこしくんが話者を分離する）。 |
| `track.mode` | `"single"` | ✓ | mode が "single" のとき |
| `track.speakerId` | [ClientId](#common-clientid) | ✓ | mode が "single" のとき |
| `track.mode` | `"mixed"` | ✓ | mode が "mixed" のとき |
| `track.diarize` | boolean | ✓ | mode が "mixed" のとき |

例: 1 人の話者の Opus（48kHz・2ch）のトラック

```json
{"type":"track_open","trackId":"alice-mic","channel":1,"format":{"codec":"opus","sampleRate":48000,"channels":2},"track":{"mode":"single","speakerId":"alice"}}
```

例: 複数人の声が入った PCM 16kHz モノラルのトラック（話者分離あり）

```json
{"type":"track_open","trackId":"room-mic","channel":2,"format":{"codec":"pcm_s16le","sampleRate":16000,"channels":1},"track":{"mode":"mixed","diarize":true}}
```

#### `track_close`

クライアントは、このメッセージでトラックを閉じる。

スキーマ: [`schemas/ingest-client-message.json#/$defs/TrackClose`](schemas/ingest-client-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"track_close"` | ✓ |  |
| `trackId` | [ClientId](#common-clientid) | ✓ |  |

例: トラックを閉じる

```json
{"type":"track_close","trackId":"room-mic"}
```

#### `state_synced`

クライアントは、接続の直後の状態の送り直し（全話者の speaker_upsert と全トラックの track_open）が終わったことを、このメッセージで知らせる。文字おこしくんは、クライアントが state_synced までに宣言しなかったトラックを閉じる。

スキーマ: [`schemas/ingest-client-message.json#/$defs/StateSynced`](schemas/ingest-client-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"state_synced"` | ✓ |  |

例: 状態の送り直しの完了

```json
{"type":"state_synced"}
```

#### `speech_start`

クライアントは、このメッセージで発話区間の開始の目安を知らせる。capturedAt は、送信側の時計による参考値である。

スキーマ: [`schemas/ingest-client-message.json#/$defs/SpeechStart`](schemas/ingest-client-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"speech_start"` | ✓ |  |
| `trackId` | [ClientId](#common-clientid) | ✓ |  |
| `capturedAt` | [Timestamp](#common-timestamp) |  |  |

例: 発話の開始

```json
{"type":"speech_start","trackId":"alice-mic","capturedAt":1790000000000}
```

#### `speech_end`

クライアントは、このメッセージで発話区間の終わりの目安を知らせる。文字おこしくんに speech_end が届くと、確定（final）が早く出る。

スキーマ: [`schemas/ingest-client-message.json#/$defs/SpeechEnd`](schemas/ingest-client-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"speech_end"` | ✓ |  |
| `trackId` | [ClientId](#common-clientid) | ✓ |  |
| `capturedAt` | [Timestamp](#common-timestamp) |  |  |

例: 発話の終わり

```json
{"type":"speech_end","trackId":"alice-mic"}
```

#### `ping`

クライアントは、生存確認のために ping を送る。文字おこしくんは pong を返す。

スキーマ: [`schemas/ingest-client-message.json#/$defs/Ping`](schemas/ingest-client-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"ping"` | ✓ |  |

例: 生存確認

```json
{"type":"ping"}
```

#### `audio_frame`

クライアントは、音声をバイナリフレーム（`application/octet-stream`）で送ります。レイアウトは[音声バイナリフレーム](#audio-frame)にあります。

### サーバー → クライアント

#### `welcome`

文字おこしくんは、hello を受け付けたことを welcome で知らせる。protocolVersion は、この接続で使うバージョンである。

スキーマ: [`schemas/ingest-server-message.json#/$defs/Welcome`](schemas/ingest-server-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"welcome"` | ✓ |  |
| `protocolVersion` | [ProtocolVersion](#common-protocolversion) | ✓ |  |
| `sessionId` | [ServerId](#common-serverid) | ✓ |  |

例: hello の受け付け

```json
{"type":"welcome","protocolVersion":1,"sessionId":"ses_01J8Z6Q3M4"}
```

#### `error`

文字おこしくんは、エラーを error で知らせる。trackId は、特定のトラックに関するエラーのときだけ付く。error.fatal が true なら、文字おこしくんはこの後に接続を切る。

スキーマ: [`schemas/ingest-server-message.json#/$defs/Error`](schemas/ingest-server-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"error"` | ✓ |  |
| `error` | object | ✓ | エラーの内容を表す。fatal が true なら、文字おこしくんはこの後に接続を閉じる。 |
| `error.code` | [ErrorCode](#common-errorcode) | ✓ |  |
| `error.message` | string | ✓ | 人が読むための説明（英語）を表す。文言は互換性の対象外である。 |
| `error.fatal` | boolean | ✓ |  |
| `trackId` | [ServerId](#common-serverid) |  |  |

例: 宣言していない話者を参照した（非 fatal。送信内容を直す）

```json
{"type":"error","error":{"code":"unknown_speaker","message":"speaker bob is not declared","fatal":false},"trackId":"bob-mic"}
```

例: トークンが不正・期限切れ（fatal。再接続しない）

```json
{"type":"error","error":{"code":"unauthorized","message":"the token is invalid, expired, or lacks ingest:write","fatal":true}}
```

#### `pong`

文字おこしくんは、ping に pong で応える。

スキーマ: [`schemas/ingest-server-message.json#/$defs/Pong`](schemas/ingest-server-message.json)

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `type` | `"pong"` | ✓ |  |

例: ping への応答

```json
{"type":"pong"}
```

## 共通の型

次の型は `schemas/common.json` の `$defs` にあります。

| 名前 | 型 | 説明 |
|---|---|---|
| <span id="common-clientid"></span>`ClientId` | string（1〜256 文字） | クライアントが送る ID（Ingest の trackId / speakerId）を表す。 |
| <span id="common-serverid"></span>`ServerId` | string（1〜1024 文字） | 文字おこしくんが送る ID（sessionId / trackId / speakerId / utteranceId）を表す。文字おこしくんはクライアントの ID から ID を派生させるので、ServerId は ClientId より長くなりうる。 |
| <span id="common-token"></span>`Token` | string（1〜4096 文字） | セッション単位のトークン（Ingest は ingest:write、Subscribe は transcript:read）を表す。トークンは API キーではない。 |
| <span id="common-protocolversion"></span>`ProtocolVersion` | integer（≥ 1） | プロトコルのバージョンを表す。現行のバージョンは、Ingest が 1、Subscribe が 1 である。 |
| <span id="common-timestamp"></span>`Timestamp` | number（≥ 0） | エポックミリ秒（UTC）の時刻を表す。 |
| <span id="common-displayname"></span>`DisplayName` | string（100 文字以下） | 表示名を表す。表示名は空文字のこともある（表示側が代替名にする）。表示名は利用者が入力した文字列なので、受け手は、表示名を HTML として埋め込まないこと。 |
| <span id="common-avatarurl"></span>`AvatarUrl` | string（2048 文字以下） | アバター画像の絶対 URL を表す。スキームには http: か https: だけを使える（javascript: や data: は使えない）。受け手の検証（WHATWG URL の解析）は少し広い形も受け付けるが、送る側は http(s)://host/... の形にすること。 |
| <span id="common-clientname"></span>`ClientName` | string（100 文字以下） | クライアントの識別名（ログ用）を表す。 |
| <span id="common-errorcode"></span>`ErrorCode` | string（1〜64 文字） | エラーコードを表す。既知のコードは errors.json にある。文字おこしくんはコードを互換性のある変更として追加するので、受け手は未知のコードも受け入れ、fatal に従って扱うこと。 |

## 値の上限

| 名前 | 値 |
|---|---|
| `maxIdLength` | 256 |
| `maxServerIdLength` | 1024 |
| `maxDisplayNameLength` | 100 |
| `maxAvatarUrlLength` | 2048 |
| `maxTokenLength` | 4096 |
| `maxClientNameLength` | 100 |
| `maxErrorCodeLength` | 64 |
| `maxChannel` | 65535 |

## 不正なメッセージの例

次の表に、文字おこしくんのサーバーの検証関数（`parse*`）と JSON Schema のどちらも拒否する、不正なメッセージの代表例を並べます。

| 方向 | 例 | 理由 |
|---|---|---|
| Ingest（client → server） | `{"type":"audio","trackId":"a"}` | type が未知の値 |
| Ingest（client → server） | `{"protocolVersion":1,"token":"t"}` | type が無い |
| Ingest（client → server） | `{"type":"hello","protocolVersion":1,"token":""}` | token が空 |
| Ingest（client → server） | `{"type":"hello","protocolVersion":0,"token":"t"}` | protocolVersion が 1 未満 |
| Ingest（client → server） | `{"type":"hello","protocolVersion":"1","token":"t"}` | protocolVersion が文字列 |
| Ingest（client → server） | `{"type":"track_open","trackId":"a","channel":65536,"format":{"codec":"opus","sampleRate":…` | channel が 65535 を超える |
| Ingest（client → server） | `{"type":"track_open","trackId":"a","channel":0,"format":{"codec":"pcm_s16le","sampleRate"…` | channels が 1 でも 2 でもない |
| Ingest（client → server） | `{"type":"track_open","trackId":"a","channel":0,"format":{"codec":"opus","sampleRate":4800…` | single に speakerId が無い |
| Ingest（client → server） | `{"type":"track_open","trackId":"a","channel":0,"format":{"codec":"aac","sampleRate":48000…` | codec が opus でも pcm_s16le でもない |
| Ingest（client → server） | `{"type":"speaker_upsert","speaker":{"speakerId":"a","displayName":"A","avatarUrl":"javasc…` | avatarUrl が http(s) の URL でない |
| Ingest（client → server） | `{"type":"speaker_upsert","speaker":{"speakerId":"a","displayName":"ああああああああああああああああああああああ…` | displayName が 100 文字を超える |
| Ingest（client → server） | `{"type":"speech_start","trackId":"a","capturedAt":-1}` | capturedAt が負 |
| Ingest（client → server） | `{"type":"track_close","trackId":""}` | trackId が空 |
| Ingest（client → server） | `["hello"]` | メッセージが JSON オブジェクトでない |
| Subscribe（server → client） | `{"type":"utterance","utterance":{"utteranceId":"u","trackId":"t","speaker":{"kind":"unatt…` | revision が負 |
| Subscribe（server → client） | `{"type":"utterance","utterance":{"utteranceId":"u","trackId":"t","speaker":{"kind":"unatt…` | revision が整数でない |
| Subscribe（server → client） | `{"type":"utterance","utterance":{"utteranceId":"u","trackId":"t","speaker":{"kind":"unatt…` | stage が partial・final・refined のどれでもない |
| Subscribe（server → client） | `{"type":"utterance","utterance":{"utteranceId":"u","trackId":"t","speaker":{"kind":"decla…` | declared に speakerId が無い |
| Subscribe（server → client） | `{"type":"welcome","protocolVersion":1,"sessionId":"s","speakers":[],"tracks":[],"speaking…` | welcome に recent が無い |
| Subscribe（server → client） | `{"type":"track_opened","track":"mixed"}` | track がオブジェクトでない |
