{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://moji.kaitoranma.com/docs/schemas/subscribe-server-message.json",
  "title": "SubscribeServerMessage",
  "description": "Subscribe（/v1/subscribe）で文字おこしくんが送るメッセージを表す。",
  "oneOf": [
    {
      "$ref": "#/$defs/Welcome"
    },
    {
      "$ref": "#/$defs/SpeakerUpserted"
    },
    {
      "$ref": "#/$defs/SpeakerRemoved"
    },
    {
      "$ref": "#/$defs/TrackOpened"
    },
    {
      "$ref": "#/$defs/TrackClosed"
    },
    {
      "$ref": "#/$defs/SpeechStarted"
    },
    {
      "$ref": "#/$defs/SpeechEnded"
    },
    {
      "$ref": "#/$defs/Utterance"
    },
    {
      "$ref": "#/$defs/SessionEnded"
    },
    {
      "$ref": "#/$defs/Error"
    },
    {
      "$ref": "#/$defs/Pong"
    }
  ],
  "$defs": {
    "Welcome": {
      "title": "welcome",
      "description": "文字おこしくんは、接続した時点の状態のスナップショットを welcome で送る。受け手は状態を丸ごと置き換える（再接続のときも）。recent は、直近の発話の最新の改訂を startedAt の昇順に並べたものである（認識途中を含む）。",
      "type": "object",
      "required": [
        "type",
        "protocolVersion",
        "sessionId",
        "speakers",
        "tracks",
        "speakingTrackIds",
        "recent"
      ],
      "properties": {
        "type": {
          "const": "welcome"
        },
        "protocolVersion": {
          "$ref": "common.json#/$defs/ProtocolVersion"
        },
        "sessionId": {
          "$ref": "common.json#/$defs/ServerId"
        },
        "speakers": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/SpeakerEntry"
          }
        },
        "tracks": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/WireTrack"
          }
        },
        "speakingTrackIds": {
          "type": "array",
          "items": {
            "$ref": "common.json#/$defs/ServerId"
          },
          "description": "発声中のトラックを表す。"
        },
        "recent": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/WireUtterance"
          }
        }
      }
    },
    "SpeakerUpserted": {
      "title": "speaker_upserted",
      "description": "文字おこしくんは、話者の追加・更新を知らせる。inferred は、話者分離が推定した話者かどうかを表す。",
      "type": "object",
      "required": [
        "type",
        "speaker",
        "inferred"
      ],
      "properties": {
        "type": {
          "const": "speaker_upserted"
        },
        "speaker": {
          "$ref": "common.json#/$defs/ServerSpeaker"
        },
        "inferred": {
          "type": "boolean"
        }
      }
    },
    "SpeakerRemoved": {
      "title": "speaker_removed",
      "description": "文字おこしくんは、話者の削除を知らせる。受け手はその話者の過去の発話を消さない。",
      "type": "object",
      "required": [
        "type",
        "speakerId"
      ],
      "properties": {
        "type": {
          "const": "speaker_removed"
        },
        "speakerId": {
          "$ref": "common.json#/$defs/ServerId"
        }
      }
    },
    "TrackOpened": {
      "title": "track_opened",
      "description": "文字おこしくんは、トラックが開いたことを知らせる。",
      "type": "object",
      "required": [
        "type",
        "track"
      ],
      "properties": {
        "type": {
          "const": "track_opened"
        },
        "track": {
          "$ref": "#/$defs/WireTrack"
        }
      }
    },
    "TrackClosed": {
      "title": "track_closed",
      "description": "文字おこしくんは、トラックが閉じたことを知らせる。受け手はそのトラックの発話を消さない。",
      "type": "object",
      "required": [
        "type",
        "trackId"
      ],
      "properties": {
        "type": {
          "const": "track_closed"
        },
        "trackId": {
          "$ref": "common.json#/$defs/ServerId"
        }
      }
    },
    "SpeechStarted": {
      "title": "speech_started",
      "description": "文字おこしくんは、トラックが発声中になったことを知らせる（「話している」の表示に使う）。",
      "type": "object",
      "required": [
        "type",
        "trackId"
      ],
      "properties": {
        "type": {
          "const": "speech_started"
        },
        "trackId": {
          "$ref": "common.json#/$defs/ServerId"
        }
      }
    },
    "SpeechEnded": {
      "title": "speech_ended",
      "description": "文字おこしくんは、トラックの発声が終わったことを知らせる。",
      "type": "object",
      "required": [
        "type",
        "trackId"
      ],
      "properties": {
        "type": {
          "const": "speech_ended"
        },
        "trackId": {
          "$ref": "common.json#/$defs/ServerId"
        }
      }
    },
    "Utterance": {
      "title": "utterance",
      "description": "文字おこしくんは、発話の改訂を 1 件ずつ送る。受け手は utteranceId ごとに revision が最大のものだけを採用し、同じか小さい revision は捨てる（追記しない。置き換える）。",
      "type": "object",
      "required": [
        "type",
        "utterance"
      ],
      "properties": {
        "type": {
          "const": "utterance"
        },
        "utterance": {
          "$ref": "#/$defs/WireUtterance"
        }
      }
    },
    "SessionEnded": {
      "title": "session_ended",
      "description": "文字おこしくんは、文字起こしが終了したことを知らせる。この後、文字おこしくんはイベントを送らない。クライアントは再接続しない。",
      "type": "object",
      "required": [
        "type"
      ],
      "properties": {
        "type": {
          "const": "session_ended"
        }
      }
    },
    "Error": {
      "title": "error",
      "description": "文字おこしくんは、エラーを error で知らせる。error.fatal が true なら、文字おこしくんはこの後に接続を切る。",
      "type": "object",
      "required": [
        "type",
        "error"
      ],
      "properties": {
        "type": {
          "const": "error"
        },
        "error": {
          "$ref": "common.json#/$defs/WireError"
        }
      }
    },
    "Pong": {
      "title": "pong",
      "description": "文字おこしくんは、ping に pong で応える。",
      "type": "object",
      "required": [
        "type"
      ],
      "properties": {
        "type": {
          "const": "pong"
        }
      }
    },
    "SpeakerEntry": {
      "type": "object",
      "required": [
        "speaker",
        "inferred"
      ],
      "properties": {
        "speaker": {
          "$ref": "common.json#/$defs/ServerSpeaker"
        },
        "inferred": {
          "type": "boolean"
        }
      }
    },
    "WireTrack": {
      "description": "トラックを表す。single は 1 人の話者だけのトラックを、mixed は複数の話者の声が入ったトラックを表す。",
      "oneOf": [
        {
          "type": "object",
          "required": [
            "trackId",
            "mode",
            "speakerId"
          ],
          "properties": {
            "trackId": {
              "$ref": "common.json#/$defs/ServerId"
            },
            "mode": {
              "const": "single"
            },
            "speakerId": {
              "$ref": "common.json#/$defs/ServerId"
            }
          }
        },
        {
          "type": "object",
          "required": [
            "trackId",
            "mode"
          ],
          "properties": {
            "trackId": {
              "$ref": "common.json#/$defs/ServerId"
            },
            "mode": {
              "const": "mixed"
            }
          }
        }
      ]
    },
    "WireSpeakerAttribution": {
      "description": "発話の話者を表す。declared は入力元が宣言した話者を、inferred は話者分離が推定した話者を、unattributed は話者が分からないことを表す。inferred の発話は、後の改訂で別の speakerId の話者に付け替わることがある。受け手は unattributed の発話をトラック単位で表示する。",
      "oneOf": [
        {
          "type": "object",
          "required": [
            "kind",
            "speakerId"
          ],
          "properties": {
            "kind": {
              "enum": [
                "declared",
                "inferred"
              ]
            },
            "speakerId": {
              "$ref": "common.json#/$defs/ServerId"
            }
          }
        },
        {
          "type": "object",
          "required": [
            "kind"
          ],
          "properties": {
            "kind": {
              "const": "unattributed"
            }
          }
        }
      ]
    },
    "WireUtterance": {
      "type": "object",
      "description": "発話 1 件の、ある時点の改訂を表す。文字おこしくんが発話を partial（認識途中）→ final（確定）→ refined（LLM による整形）と改訂するたびに、revision が増える。",
      "required": [
        "utteranceId",
        "trackId",
        "speaker",
        "stage",
        "revision",
        "text",
        "startedAt"
      ],
      "properties": {
        "utteranceId": {
          "$ref": "common.json#/$defs/ServerId"
        },
        "trackId": {
          "$ref": "common.json#/$defs/ServerId"
        },
        "speaker": {
          "$ref": "#/$defs/WireSpeakerAttribution"
        },
        "stage": {
          "enum": [
            "partial",
            "final",
            "refined"
          ]
        },
        "revision": {
          "type": "integer",
          "minimum": 0,
          "maximum": 9007199254740991,
          "description": "同じ utteranceId では、大きい方が新しい。文字おこしくんが partial を間引くことがあるので、連番とは限らない。"
        },
        "text": {
          "type": "string",
          "description": "表示するテキストを表す（空文字のこともある）。受け手はテキストを HTML として埋め込まないこと。"
        },
        "startedAt": {
          "$ref": "common.json#/$defs/Timestamp",
          "description": "発話の最初の音声フレームを文字おこしくんが受け取った時刻（エポックミリ秒）を表す。受け手は話者をまたいで発話を並べるときに startedAt を使う。"
        }
      }
    }
  }
}
