from __future__ import annotations

import ssl
from dataclasses import asdict
from dataclasses import dataclass
from dataclasses import field
from enum import Enum
from typing import Any
from typing import Optional
from typing import cast
from warnings import warn

from typing_extensions import deprecated


class AudioEncoding(str, Enum):
    """
    Supported audio encoding formats for real-time transcription.

    The Speechmatics RT API supports several audio encoding formats for
    optimal compatibility with different audio sources and quality requirements.

    Attributes:
        PCM_F32LE: 32-bit float PCM used in the WAV audio format, little-endian architecture. 4 bytes per sample.
        PCM_S16LE: 16-bit signed integer PCM used in the WAV audio format, little-endian architecture. 2 bytes per sample.
        MULAW: 8 bit μ-law (mu-law) encoding. 1 byte per sample.

    Examples:
        >>> encoding = AudioEncoding.PCM_S16LE
    """

    PCM_F32LE = "pcm_f32le"
    PCM_S16LE = "pcm_s16le"
    MULAW = "mulaw"


@deprecated("Use Model instead")
class OperatingPoint(str, Enum):
    """Operating point options for transcription."""

    ENHANCED = "enhanced"
    STANDARD = "standard"


class Model(str, Enum):
    """Which model to use for transcription."""

    ENHANCED = "enhanced"
    STANDARD = "standard"


@dataclass
class AudioEventsConfig:
    types: Optional[list[str]] = None
    """Optional list of audio event types to detect."""

    def to_dict(self) -> dict[str, Any]:
        if self.types is None:
            return {}
        return asdict(self)


class ClientMessageType(str, Enum):
    """
    Message types that can be sent from client to server.

    These enum values represent the different types of messages that the
    client can send to the Speechmatics RT API during a transcription session.

    Attributes:
        StartRecognition: Initiates a new transcription session with
            configuration parameters.
        AddAudio: Indicates that audio data follows (not used in message
            headers, audio is sent as binary data).
        EndOfStream: Signals that no more audio data will be sent.
        SetRecognitionConfig: Updates transcription configuration during
            an active session (advanced use).
        GetSpeakers: Allows the client to request speaker data.
        AddChannelAudio: Indicates that audio data follows (not used in message
            headers, audio is sent as binary data).
        EndOfChannel: Signals that no more audio data will be sent.
        ForceEndOfUtterance: Signals that the end of an utterance has been reached.

    Examples:
        >>> # Starting a recognition session
        >>> message = {
        ...     "message": ClientMessageType.StartRecognition,
        ...     "audio_format": audio_format.to_dict(),
        ...     "transcription_config": config.to_dict()
        ... }
        >>>
        >>> # Ending the session
        >>> end_message = {
        ...     "message": ClientMessageType.END_OF_STREAM,
        ...     "last_seq_no": sequence_number
        ... }
    """

    START_RECOGNITION = "StartRecognition"
    ADD_AUDIO = "AddAudio"
    END_OF_STREAM = "EndOfStream"
    SET_RECOGNITION_CONFIG = "SetRecognitionConfig"
    GET_SPEAKERS = "GetSpeakers"
    ADD_CHANNEL_AUDIO = "AddChannelAudio"
    END_OF_CHANNEL = "EndOfChannel"
    FORCE_END_OF_UTTERANCE = "ForceEndOfUtterance"


class ServerMessageType(str, Enum):
    """
    Message types that can be received from the server.

    These enum values represent the different types of messages that the
    Speechmatics RT API can send to the client.

    Attributes:
        RecognitionStarted: Server response to 'StartRecognition',
            acknowledging that a recognition session has started.
        AudioAdded: Server response to 'AddAudio', indicating
            that audio has been added successfully.
        AddPartialTranscript: Indicates a partial transcript, which is an incomplete transcript that
            is immediately produced and may change as more context becomes available.
        AddTranscript: Indicates the final transcript that will not change for the given audio segment.
        EndOfTranscript: Indicates the server has finished sending all messages.
        EndOfUtterance: Signals that an utterance has ended, based on silence.
        AudioEventStarted: Signals the start of an audio event.
        AudioEventEnded: Signals the end of an audio event.
        AddTranslation: Provides final translation results that will not
            change for the given audio segment.
        AddPartialTranslation: Provides interim translation results that
            may change as more context becomes available.
        SpeakerResult: Provides the speaker identification data.
        Info: Informational messages from the server.
        Warning: Warning messages that don't stop transcription.
        Error: Error messages indicating transcription failure.

    Examples:
        >>> # Register event handlers for different message types
        >>> @client.on(ServerMessageType.ADD_TRANSCRIPT)
        >>> def handle_final(message):
        ...     print(f"Final: {message['metadata']['transcript']}")
        >>>
        >>> @client.on(ServerMessageType.ADD_PARTIAL_TRANSCRIPT)
        >>> def handle_partial(message):
        ...     print(f"Partial: {message['metadata']['transcript']}")
        >>>
        >>> @client.on(ServerMessageType.ERROR)
        >>> def handle_error(message):
        ...     print(f"Error: {message['reason']}")
    """

    RECOGNITION_STARTED = "RecognitionStarted"
    AUDIO_ADDED = "AudioAdded"
    ADD_PARTIAL_TRANSCRIPT = "AddPartialTranscript"
    ADD_TRANSCRIPT = "AddTranscript"
    END_OF_TRANSCRIPT = "EndOfTranscript"
    END_OF_UTTERANCE = "EndOfUtterance"
    AUDIO_EVENT_STARTED = "AudioEventStarted"
    AUDIO_EVENT_ENDED = "AudioEventEnded"
    ADD_TRANSLATION = "AddTranslation"
    ADD_PARTIAL_TRANSLATION = "AddPartialTranslation"
    SPEAKERS_RESULT = "SpeakersResult"
    INFO = "Info"
    WARNING = "Warning"
    ERROR = "Error"
    CHANNEL_AUDIO_ADDED = "ChannelAudioAdded"


@dataclass
class AudioFormat:
    """
    Configuration for audio format parameters.

    This class defines the audio format specification including encoding type,
    sample rate, and chunk size for streaming. These parameters must match
    the actual audio data being sent to ensure proper transcription.

    Attributes:
        encoding: Audio encoding format. Defaults to PCM_S16LE.
        sample_rate: Sample rate in Hz. Defaults to 44100 Hz.
        chunk_size: Size of audio chunks in bytes for streaming. Defaults to 4096.

    Examples:
        File:
            >>> audio_format = AudioFormat()

        Raw:
            >>> audio_format = AudioFormat(
            ...     encoding=AudioEncoding.PCM_S16LE,
            ...     sample_rate=16000,
            ... )
    """

    encoding: Optional[AudioEncoding] = None
    sample_rate: int = 44100
    chunk_size: int = 4096

    _BYTES_PER_SAMPLE = {
        AudioEncoding.PCM_F32LE: 4,
        AudioEncoding.PCM_S16LE: 2,
        AudioEncoding.MULAW: 1,
    }

    @property
    def bytes_per_sample(self) -> int:
        """Number of bytes per audio sample based on encoding.

        Raises:
            ValueError: If encoding is None (file type) or unrecognized.
        """
        if self.encoding is None:
            raise ValueError(
                "Cannot determine bytes per sample for file-type audio format. "
                "Set an explicit encoding on AudioFormat."
            )
        try:
            return self._BYTES_PER_SAMPLE[self.encoding]
        except KeyError:
            raise ValueError(f"Unknown encoding: {self.encoding}")

    def to_dict(self) -> dict[str, Any]:
        """
        Convert audio format to dictionary.

        Returns:
            Dictionary containing audio format specification with keys:
            - type: "raw" for raw audio data | "file" for file data
            - encoding: String encoding value
            - sample_rate: Integer sample rate in Hz

        Examples:
            >>> audio_format = AudioFormat()
            >>> audio_format.to_dict()
            >>> # Returns: {
            >>> #     "type": "file",
            >>> # }

            >>> audio_format = AudioFormat(
            ...     encoding=AudioEncoding.PCM_S16LE,
            ...     sample_rate=16000
            ... )
            >>> audio_format.to_dict()
            >>> # Returns: {
            >>> #     "type": "raw",
            >>> #     "encoding": "pcm_s16le",
            >>> #     "sample_rate": 16000
            >>> # }
        """

        if self.encoding:
            return {
                "type": "raw",
                "encoding": self.encoding,
                "sample_rate": self.sample_rate,
            }

        return {
            "type": "file",
        }


@dataclass
class ConversationConfig:
    """Conversation configuration for end-of-utterance detection.

    Attributes:
        end_of_utterance_silence_trigger: (Optional) How much silence in seconds is required to trigger end of utterance detection.

    Examples:
        >>> config = ConversationConfig(end_of_utterance_silence_trigger=0.5)
    """

    end_of_utterance_silence_trigger: Optional[float] = None


@dataclass
class SpeakerDiarizationConfig:
    """Speaker diarization configuration.

    Attributes:
        max_speakers: (Optional) This enforces the maximum number of speakers allowed in a single audio stream.
        speaker_sensitivity: (Optional) The sensitivity of the speaker detection.
            This is a number between 0 and 1, where 0 means least sensitive and 1 means
            most sensitive.
        prefer_current_speaker: (Optional) Whether to prefer the current speaker when assigning speaker labels.
            If true, the algorithm will prefer to stay with the current active speaker if it
            is a close enough match, even if other speakers may be closer. This is useful
            for cases where we can flip incorrectly between similar speakers during a single
            speaker section.
        speakers: (Optional) Add speaker identifiers to your session to identify specific speakers.
            This is a list of SpeakerIdentifier objects generated in previous transcription sessions.
            You can provide multiple identifiers for a single speaker to help the engine identify
            the speaker more accurately.

    Examples:
        >>> config = SpeakerDiarizationConfig(
            max_speakers=2,
            speaker_sensitivity=0.8,
            prefer_current_speaker=True,
            speakers=[
                SpeakerIdentifier(label="Agent", speaker_identifiers=["agent_1"]),
                SpeakerIdentifier(label="Customer", speaker_identifiers=["cust_1"]),
            ],
        )

    """

    max_speakers: Optional[int] = None
    speaker_sensitivity: Optional[float] = None
    prefer_current_speaker: Optional[bool] = None
    speakers: Optional[list[SpeakerIdentifier]] = None


@dataclass
class SpeakerIdentifier:
    """Labeled speaker identifier for guided speaker diarization.

    Use this to map one or more known speaker identifiers to a human-readable
    label. When provided in `SpeakerDiarizationConfig.speakers`, the engine can
    use these identifiers as hints to consistently assign the specified label.

    Attributes:
        label: Human-readable label to assign to this speaker or group
            (e.g., "Agent", "Customer", "Alice").
        speaker_identifiers: A list of string identifiers associated with this
            speaker. These can be any stable identifiers relevant to your
            application (for example device IDs, prior session speaker IDs,
            channel tags, etc.).

    Examples:
        >>> config = SpeakerDiarizationConfig(
        ...     max_speakers=2,
        ...     speakers=[
        ...         SpeakerIdentifier(label="Agent", speaker_identifiers=["agent_1"]),
        ...         SpeakerIdentifier(label="Customer", speaker_identifiers=["cust_1"]),
        ...     ],
        ... )
    """

    label: str = ""
    speaker_identifiers: list[str] = field(default_factory=list)


_UNSET = cast(Model, object())


@dataclass
class TranscriptionConfig:
    """
    Configuration for transcription behavior and features.

    Attributes:
        language: (Optional) ISO 639-1 language code (e.g., "en", "es", "fr").
            Defaults to "en".
        model: (Optional) Which acoustic model to use.
            Defaults to "enhanced".
        output_locale: (Optional) RFC-5646 language code for transcript output (eg. "en-US").
            Defaults to None.
        diarization: Type of diarization to use. Options: "none", "channel", "speaker", "channel_and_speaker".
            Defaults to None.
        additional_vocab: (Optional) Additional vocabulary that is not part of the standard language.
            Defaults to None.
        punctuation_overrides: (Optional) Permitted punctuation marks for advanced punctuation.
            Defaults to None.
        domain: (Optional) Request a language pack optimized for a specific domain (e.g. 'medical').
            Defaults to None.
        enable_entities: (Optional) Whether to enable entity detection/recognition.
            Defaults to None.
        enable_partials: (Optional) Whether to receive partial transcription results.
            Defaults to None.
        max_delay: (Optional) Maximum delay in seconds for transcript delivery.
            Defaults to None.
        max_delay_mode: (Optional) Determines whether the threshold specified in max_delay can be exceeded
            if a potential entity is detected. Flexible means if a potential entity
            is detected, then the max_delay can be overriden until the end of that
            entity. Fixed means that max_delay specified ignores any potential
            entity that would not be completed within that threshold.
        speaker_diarization_config: (Optional) Configuration for speaker diarization.
            Defaults to None.
        streaming_mode: (Optional) Indicates if we run the engine in streaming mode, or regular RT mode.
        audio_filtering_config: (Optional) Configuration for limiting the transcription of quiet audio.
            Defaults to None.
        transcript_filtering_config: (Optional) Configuration for applying filtering to the transcription.
            Defaults to None.
        conversation_config: (Optional) Configuration for end-of-utterance detection.
            Defaults to None.
        ctrl: (Optional) Configuration for controlling the engine.
            Defaults to None.
        channel_diarization_labels: (Optional) Configuration for channel diarization.
            Defaults to None.
        operating_point: (Deprecated) Legacy argument for specifying the operating point. Use `model` instead going forward
            Defaults to None.

    Examples:
        Basic English transcription:
            >>> config = TranscriptionConfig(language="en")

        Spanish with partials enabled:
            >>> config = TranscriptionConfig(
            ...     language="es",
            ...     model="enhanced",
            ...     enable_partials=True
            ... )

        Advanced configuration with speaker diarization:
            >>> config = TranscriptionConfig(
            ...     language="en",
            ...     enable_partials=True,
            ...     max_delay=5.0,
            ...     speaker_diarization_config={
            ...         "speaker_sensitivity": 0.7,
            ...         "max_speakers": 4
            ...     }
            ... )
    """

    language: str = "en"
    model: Model = _UNSET
    output_locale: Optional[str] = None
    diarization: Optional[str] = None
    additional_vocab: Optional[list[dict[str, Any]]] = None
    punctuation_overrides: Optional[dict] = None
    domain: Optional[str] = None
    enable_entities: Optional[bool] = None
    audio_filtering_config: Optional[dict] = None
    transcript_filtering_config: Optional[dict] = None
    max_delay: Optional[float] = None
    max_delay_mode: Optional[str] = None
    enable_partials: Optional[bool] = None
    speaker_diarization_config: Optional[SpeakerDiarizationConfig] = None
    streaming_mode: Optional[bool] = None
    conversation_config: Optional[ConversationConfig] = None
    ctrl: Optional[dict] = None
    channel_diarization_labels: Optional[list[str]] = None
    operating_point: Optional[OperatingPoint] = None

    def __post_init__(self) -> None:
        if self.model is not _UNSET and self.operating_point is not None:
            raise ValueError("Cannot specify both 'model' and 'operating_point'. Use 'model' instead.")
        if self.model is _UNSET and self.operating_point is None:
            self.model = Model.ENHANCED
        if self.operating_point is not None:
            warn(
                "'operating_point' is deprecated, use 'model' instead.",
                DeprecationWarning,
                stacklevel=2,
            )

    def to_dict(self) -> dict[str, Any]:
        """
        Convert transcription parameters to dictionary.

        Returns:
        Transcription configuration as a dict while excluding None values.

        Examples:
            >>> config = TranscriptionConfig(
            ...     language="en",
            ...     enable_partials=True,
            ...     max_delay=5.0,
            ...     enable_entities=True
            ... )
            >>> api_dict = config.to_dict()
            >>> # Returns: {
            >>> #     "language": "en",
            >>> #     "enable_partials": True,
            >>> #     "max_delay_mode": "flexible",
            >>> #     "enable_entities": True,
            >>> #     "max_delay": 5.0
            >>> # }
        """
        result = asdict(self, dict_factory=lambda x: {k: v for (k, v) in x if v is not None})
        if self.model is _UNSET:
            result.pop("model", None)
        return result


@dataclass
class TranslationConfig:
    """Translation config.

    Attributes:
        target_languages: Target languages for which translation should be produced.
        enable_partials: Indicates if partial translation, where sentences are produced
            immediately, is enabled.

    Examples:
        >>> config = TranslationConfig(
        ...     target_languages=["fr", "es"],
        ...     enable_partials=True
        ... )
        >>> config_dict = config.to_dict()
        >>> # Returns: {
        >>> #     "target_languages": ["fr", "es"],
        >>> #     "enable_partials": True
        >>> # }
    """

    target_languages: list[str]
    enable_partials: Optional[bool] = None

    def to_dict(self) -> dict[str, Any]:
        return asdict(self, dict_factory=lambda x: {k: v for (k, v) in x if v is not None})


@dataclass
class ConnectionConfig:
    """
    Configuration for WebSocket connection parameters.

    This class defines WebSocket-specific settings like ping intervals,
    message sizes, and connection timeouts.

    Attributes:
        open_timeout: Timeout for establishing WebSocket connection.
        ping_interval: Interval for WebSocket ping frames.
        ping_timeout: Timeout waiting for pong response.
        close_timeout: Timeout for closing WebSocket connection.
        max_size: Maximum message size in bytes.
        max_queue: Maximum number of messages in receive queue.
        read_limit: Maximum number of bytes to read from WebSocket (legacy websockets only).
        write_limit: Maximum number of bytes to write to WebSocket (legacy websockets only).
        ssl_context: SSL context for the WebSocket connection.
    Returns:
        Websocket connection configuration as a dict while excluding None values.
    """

    open_timeout: Optional[float] = None
    ping_interval: Optional[float] = None
    ping_timeout: Optional[float] = 60
    close_timeout: Optional[float] = None
    max_size: Optional[int] = None
    max_queue: Optional[int] = None
    read_limit: Optional[int] = None
    write_limit: Optional[int] = None
    ssl_context: ssl.SSLContext = field(default_factory=ssl.create_default_context)

    def to_dict(self) -> dict[str, Any]:
        """Convert to dict, excluding ssl field to avoid pickle errors."""
        result = {}
        if self.open_timeout is not None:
            result["open_timeout"] = self.open_timeout
        if self.ping_interval is not None:
            result["ping_interval"] = self.ping_interval
        if self.ping_timeout is not None:
            result["ping_timeout"] = self.ping_timeout
        if self.close_timeout is not None:
            result["close_timeout"] = self.close_timeout
        if self.max_size is not None:
            result["max_size"] = self.max_size
        if self.max_queue is not None:
            result["max_queue"] = self.max_queue
        if self.read_limit is not None:
            result["read_limit"] = self.read_limit
        if self.write_limit is not None:
            result["write_limit"] = self.write_limit

        return result


@dataclass
class SessionInfo:
    """
    Information about the current transcription session.

    This class tracks session state and metadata throughout the lifecycle
    of a real-time transcription session, providing unique identifiers
    and sequence tracking.

    Attributes:
        request_id: Unique identifier for this client request/session.
        session_id: Server-assigned session ID (set after recognition starts).
        sequence_number: Current sequence number for message ordering.
        is_running: Whether the transcription session is currently active.

    Examples:
        Creating session info:
            >>> import uuid
            >>> session = SessionInfo(request_id=str(uuid.uuid4()))

        Checking session state:
            >>> if session.is_running:
            ...     # Session is active, can send audio
            ...     pass
    """

    request_id: str
    session_id: Optional[str] = None
    sequence_number: int = 0
    is_running: bool = False


@dataclass
class RecognitionDisplay:
    """
    Display information for recognition results.

    Attributes:
        direction: Display direction (ltr/rtl).
    """

    direction: str

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> RecognitionDisplay:
        """Create RecognitionDisplay from dictionary."""
        return cls(direction=data.get("direction", "ltr"))


@dataclass
class RecognitionAlternative:
    """
    Alternative transcription result for a single recognition segment.

    This class represents a single alternative transcription with confidence,
    language, and speaker information, matching the OpenAPI specification exactly.

    Attributes:
        content: A word or punctuation mark.
        confidence: Confidence score assigned to the alternative (0.0 to 1.0).
        language: Language that the alternative word is assumed to be spoken in.
        display: Information about how the word/symbol should be displayed.
        speaker: Label indicating who said that word (only set if diarization enabled).
        tags: List of profanities and disfluencies tags.
    """

    content: str
    confidence: float
    language: Optional[str] = None
    display: Optional[RecognitionDisplay] = None
    speaker: Optional[str] = None
    tags: Optional[list[str]] = None

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> RecognitionAlternative:
        """Create RecognitionAlternative from dictionary."""
        display = None
        if "display" in data and data["display"]:
            display = RecognitionDisplay.from_dict(data["display"])

        return cls(
            content=data["content"],
            confidence=data["confidence"],
            language=data.get("language"),
            display=display,
            speaker=data.get("speaker"),
            tags=data.get("tags"),
        )


@dataclass
class RecognitionResult:
    """
    Individual recognition result with timing and alternatives.

    This class represents a single recognition result segment, matching
    the OpenAPI specification exactly.

    Attributes:
        type: Type of result (word, punctuation).
        start_time: Start time of this segment in seconds.
        end_time: End time of this segment in seconds.
        channel: Channel identifier for multi-channel audio.
        attaches_to: Attachment information for punctuation.
        is_eos: Whether this is end of sentence.
        alternatives: List of alternative transcriptions for this segment.
        score: Score for this result (0.0 to 1.0).
        volume: Volume level (0.0 to 100.0).
    """

    type: str
    start_time: float
    end_time: float
    channel: Optional[str] = None
    attaches_to: Optional[str] = None
    is_eos: Optional[bool] = None
    alternatives: Optional[list[RecognitionAlternative]] = None
    score: Optional[float] = None
    volume: Optional[float] = None

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> RecognitionResult:
        """Create RecognitionResult from dictionary."""
        alternatives = None
        if "alternatives" in data and data["alternatives"]:
            alternatives = [RecognitionAlternative.from_dict(alt) for alt in data["alternatives"]]

        return cls(
            type=data["type"],
            start_time=data["start_time"],
            end_time=data["end_time"],
            channel=data.get("channel"),
            attaches_to=data.get("attaches_to"),
            is_eos=data.get("is_eos"),
            alternatives=alternatives,
            score=data.get("score"),
            volume=data.get("volume"),
        )


@dataclass
class RecognitionMetadata:
    """
    Metadata for recognition results.

    This class represents the metadata included in AddTranscript messages,
    matching the OpenAPI specification exactly.

    Attributes:
        start_time: Start time of the audio segment in seconds.
        end_time: End time of the audio segment in seconds.
        transcript: The entire transcript contained in the segment in text format.
    """

    start_time: float
    end_time: float
    transcript: str

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> RecognitionMetadata:
        """Create RecognitionMetadata from dictionary."""
        return cls(
            start_time=data["start_time"],
            end_time=data["end_time"],
            transcript=data["transcript"],
        )


@dataclass
class TranscriptResult:
    """
    Structured representation of transcription results.

    This class provides a convenient way to work with transcription results
    from the Speechmatics RT API, extracting and organizing the relevant
    information from server messages.

    Attributes:
        format: Speechmatics JSON output format version number.
        metadata: Recognition metadata with transcript, start_time, end_time.
        results: List of detailed recognition results with timing and alternatives.
        is_final: Whether this is a final result (True) or partial (False).

    Examples:
        Creating from server message:
            >>> @client.on(ServerMessageType.ADD_TRANSCRIPT)
            >>> def handle_transcript(message):
            ...     result = TranscriptResult.from_message(message)
            ...     print(f"Final: {result.metadata.transcript}")

        Working with timing information:
            >>> duration = result.metadata.end_time - result.metadata.start_time
            >>> print(f"Segment duration: {duration:.2f}s")

        Word-level details:
            >>> for word_result in result.results:
            ...     if word_result.alternatives:
            ...         word = word_result.alternatives[0].content
            ...         confidence = word_result.alternatives[0].confidence
            ...         print(f"{word} ({confidence:.2f})")
    """

    metadata: RecognitionMetadata
    results: list[RecognitionResult]
    format: Optional[str] = None
    is_final: bool = False

    @classmethod
    def from_message(cls, message: dict[str, Any]) -> TranscriptResult:
        """
        Create a TranscriptResult from a server message.

        Extracts and organizes transcription information from the raw
        server message format into a structured, easy-to-use object.

        Args:
            message: Raw message dictionary from the Speechmatics RT API
                    containing transcription results.

        Returns:
            A TranscriptResult object with extracted information.

        Examples:
            >>> # Typical server message structure:
            >>> message = {
            ...     "message": "AddTranscript",
            ...     "format": "2.1",
            ...     "metadata": {
            ...         "transcript": "Hello world",
            ...         "start_time": 1.5,
            ...         "end_time": 2.8
            ...     },
            ...     "results": [
            ...         {
            ...             "type": "word",
            ...             "start_time": 1.5,
            ...             "end_time": 1.8,
            ...             "alternatives": [
            ...                 {
            ...                     "content": "Hello",
            ...                     "confidence": 0.98,
            ...                     "language": "en"
            ...                 }
            ...             ]
            ...         }
            ...     ]
            ... }
            >>> result = TranscriptResult.from_message(message)
            >>> # result.metadata.transcript == "Hello world"
            >>> # result.is_final == True
            >>> # result.format == "2.1"
        """
        metadata_data = message["metadata"]
        metadata = RecognitionMetadata.from_dict(metadata_data)

        results_data = message["results"]
        results = [RecognitionResult.from_dict(result) for result in results_data]

        return cls(
            format=message.get("format"),
            metadata=metadata,
            results=results,
            is_final=message["message"] == ServerMessageType.ADD_TRANSCRIPT,
        )
