"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""

from .basesdk import BaseSDK
from enum import Enum
from supertone import errors, models, utils
from supertone._hooks import HookContext
from supertone.types import OptionalNullable, UNSET
from supertone.utils.unmarshal_json_response import unmarshal_json_response
from typing import Any, Mapping, Optional, Union

# region imports
import httpx
import asyncio
import base64
import copy
import json
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List

from supertone.custom_utils import (
    # Constants
    WAV_HEADER_SIZE,
    MP3_ID3V2_HEADER_SIZE,
    DEFAULT_STREAM_CHUNK_SIZE,
    DEFAULT_MAX_TEXT_LENGTH,
    MAX_PARALLEL_WORKERS,
    # Text utilities
    chunk_text,
    extract_audio_from_ndjson,
    # Pronunciation utilities
    apply_pronunciation_dictionary,
    # Audio utilities
    merge_wav_binary,
    merge_mp3_binary,
    remove_wav_header,
    remove_mp3_header,
    detect_audio_format,
    extract_audio_from_response,
    extract_audio_from_response_async,
    extract_audio_from_responses,
    extract_audio_from_responses_async,
    # Phoneme utilities
    merge_phoneme_data,
    adjust_phoneme_timing,
    create_empty_phoneme_dict,
    # Logger utilities
    get_logger,
)

# Initialize logger for this module
logger = get_logger(__name__)

# endregion imports


class CreateSpeechAcceptEnum(str, Enum):
    APPLICATION_JSON = "application/json"
    AUDIO_MPEG = "audio/mpeg"
    AUDIO_WAV = "audio/wav"


class StreamSpeechAcceptEnum(str, Enum):
    APPLICATION_X_NDJSON = "application/x-ndjson"
    AUDIO_MPEG = "audio/mpeg"
    AUDIO_WAV = "audio/wav"


class TextToSpeech(BaseSDK):
    r"""Text-to-Speech API endpoints"""

    # region sdk-class-body
    def __init__(self, *args, **kwargs):
        """Initialize TextToSpeech with automatic text chunking support"""
        super().__init__(*args, **kwargs)

        # create_speech: merge mode (generates complete audio file)
        self.create_speech = self._auto_chunk_text(max_length=DEFAULT_MAX_TEXT_LENGTH)(
            self.create_speech
        )
        self.create_speech_async = self._auto_chunk_text_async(
            max_length=DEFAULT_MAX_TEXT_LENGTH
        )(self.create_speech_async)

        # stream_speech: sequential streaming mode (immediate delivery)
        self.stream_speech = self._auto_chunk_text_streaming(
            max_length=DEFAULT_MAX_TEXT_LENGTH
        )(self.stream_speech)
        self.stream_speech_async = self._auto_chunk_text_streaming_async(
            max_length=DEFAULT_MAX_TEXT_LENGTH
        )(self.stream_speech_async)

    def _apply_pronunciation_dictionary(
        self,
        text: str,
        pronunciation_dictionary: Optional[List[Mapping[str, Any]]] = None,
    ) -> str:
        """
        Apply pronunciation dictionary substitutions when provided.

        This is a lightweight wrapper around the custom_utils helper to keep
        decorator wrappers concise.
        """
        if pronunciation_dictionary is None:
            return text
        return apply_pronunciation_dictionary(text, pronunciation_dictionary)

    def _chunk_text(
        self, text: str, max_length: int = DEFAULT_MAX_TEXT_LENGTH
    ) -> List[str]:
        """
        Split input text into sentence chunks suitable for TTS processing.

        Wrapper for the chunk_text utility function from custom_utils.

        :param text: Input text to be segmented
        :param max_length: Maximum length of each chunk
        :return: List of text chunks
        """
        return chunk_text(text, max_length)

    def _should_chunk_text(self, text: str, max_length: int) -> bool:
        """
        Check if text needs to be chunked.

        :param text: Input text
        :param max_length: Maximum text length
        :return: True if text needs chunking, False otherwise
        """
        return len(text) > max_length

    def _process_chunks_parallel(
        self, text_chunks: List[str], original_method, args, kwargs
    ) -> List[Any]:
        """
        Process text chunks in parallel using ThreadPoolExecutor.

        :param text_chunks: List of text chunks to process
        :param original_method: Original method to call for each chunk
        :param args: Positional arguments for the method
        :param kwargs: Keyword arguments for the method
        :return: List of responses in order
        """

        with ThreadPoolExecutor(max_workers=MAX_PARALLEL_WORKERS) as executor:
            # Submit all chunks
            future_to_chunk = {}
            for i, chunk in enumerate(text_chunks):
                chunk_kwargs = kwargs.copy()
                chunk_kwargs["text"] = chunk
                future = executor.submit(original_method, *args, **chunk_kwargs)
                future_to_chunk[future] = i

            # Collect results in order
            chunk_responses = [None] * len(text_chunks)
            for future in as_completed(future_to_chunk):
                chunk_index = future_to_chunk[future]
                try:
                    response = future.result()
                    chunk_responses[chunk_index] = response
                except Exception as exc:
                    logger.error(f"Chunk {chunk_index} failed: {exc}", exc_info=True)
                    raise exc

        responses = [r for r in chunk_responses if r is not None]

        if not responses:
            raise RuntimeError("No successful responses from chunked requests")

        return responses

    async def _process_chunks_parallel_async(
        self, text_chunks: List[str], original_method, args, kwargs
    ) -> List[Any]:
        """
        Process text chunks in parallel using asyncio.gather.

        :param text_chunks: List of text chunks to process
        :param original_method: Original async method to call for each chunk
        :param args: Positional arguments for the method
        :param kwargs: Keyword arguments for the method
        :return: List of responses in order
        """

        async def process_chunk(chunk: str):
            chunk_kwargs = kwargs.copy()
            chunk_kwargs["text"] = chunk
            return await original_method(*args, **chunk_kwargs)

        # Process all chunks in parallel
        chunk_tasks = [process_chunk(chunk) for chunk in text_chunks]
        responses = await asyncio.gather(*chunk_tasks)

        if not responses:
            raise errors.SupertoneDefaultError(
                "No successful responses from chunked requests",
                httpx.Response(status_code=400, content="Bad Request"),
            )

        return list(responses)

    async def _merge_audio_async(
        self, audio_responses: List[models.CreateSpeechResponse]
    ) -> bytes:
        """Async version: Merge multiple audio responses into one (supports WAV/MP3)"""
        if not audio_responses:
            return b""

        # Extract audio data from first response
        first_audio_data = await extract_audio_from_response_async(audio_responses[0])

        # Detect format and merge accordingly
        audio_format = detect_audio_format(first_audio_data)
        if audio_format == "wav":
            return await self._merge_wav_files_async(audio_responses)
        else:
            # MP3 or other formats - simple concatenation
            return await self._merge_mp3_files_async(audio_responses)

    def _merge_audio(self, audio_responses: List[models.CreateSpeechResponse]) -> bytes:
        """Merge multiple audio responses into one (supports WAV/MP3)"""
        if not audio_responses:
            return b""

        # Extract audio data from first response
        first_audio_data = extract_audio_from_response(audio_responses[0])

        # Detect format and merge accordingly
        audio_format = detect_audio_format(first_audio_data)
        if audio_format == "wav":
            return self._merge_wav_files(audio_responses)
        else:
            # MP3 or other formats - simple concatenation
            return self._merge_mp3_files(audio_responses)

    def _merge_wav_binary(self, audio_chunks: List[bytes]) -> bytes:
        """
        Merge binary WAV data chunks.

        Wrapper for the merge_wav_binary utility function from custom_utils.

        :param audio_chunks: List of binary WAV data to merge
        :return: Merged WAV file as binary data
        """
        return merge_wav_binary(audio_chunks)

    async def _merge_wav_files_async(
        self, audio_responses: List[models.CreateSpeechResponse]
    ) -> bytes:
        """Async version: Merge WAV files (response object version)"""
        audio_chunks = await extract_audio_from_responses_async(audio_responses)
        return self._merge_wav_binary(audio_chunks)

    async def _merge_mp3_files_async(
        self, audio_responses: List[models.CreateSpeechResponse]
    ) -> bytes:
        """Async version: Merge MP3 files using utility function"""
        audio_chunks = await extract_audio_from_responses_async(audio_responses)
        return merge_mp3_binary(audio_chunks)

    def _merge_wav_files(
        self, audio_responses: List[models.CreateSpeechResponse]
    ) -> bytes:
        """Merge WAV files (response object version)"""
        audio_chunks = extract_audio_from_responses(audio_responses)
        return self._merge_wav_binary(audio_chunks)

    def _merge_mp3_files(
        self, audio_responses: List[models.CreateSpeechResponse]
    ) -> bytes:
        """Merge MP3 files using utility function"""
        audio_chunks = extract_audio_from_responses(audio_responses)
        return merge_mp3_binary(audio_chunks)

    def _merge_json_responses(
        self, json_responses: List[models.CreateSpeechResponse]
    ) -> models.CreateSpeechResponseBody:
        """Merge multiple JSON responses into one (includes phoneme data)"""
        if not json_responses:
            return models.CreateSpeechResponseBody(audio_base64="")

        audio_chunks = []  # Collect binary audio data
        phoneme_chunks = []  # Collect phoneme data for merging

        for response in json_responses:
            result = response.result

            # Type guard: only process CreateSpeechResponseBody
            if not isinstance(result, models.CreateSpeechResponseBody):
                continue

            # Convert audio_base64 to binary and collect
            if result.audio_base64:
                audio_data = base64.b64decode(result.audio_base64)
                audio_chunks.append(audio_data)

            # Collect phoneme data
            if result.phonemes:
                phonemes = result.phonemes
                phoneme_dict: dict = {}

                if hasattr(phonemes, "symbols") and phonemes.symbols:
                    phoneme_dict["symbols"] = phonemes.symbols

                if (
                    hasattr(phonemes, "durations_seconds")
                    and phonemes.durations_seconds
                ):
                    phoneme_dict["durations_seconds"] = phonemes.durations_seconds

                if (
                    hasattr(phonemes, "start_times_seconds")
                    and phonemes.start_times_seconds
                ):
                    phoneme_dict["start_times_seconds"] = phonemes.start_times_seconds

                if phoneme_dict:
                    phoneme_chunks.append(phoneme_dict)

        # Merge phoneme data using utility function
        merged_phonemes = merge_phoneme_data(phoneme_chunks)

        # Merge audio data (handle WAV/MP3 headers)
        merged_audio_base64 = None
        if audio_chunks:
            # Directly merge binary data (handle WAV headers)
            if (
                len(audio_chunks[0]) >= WAV_HEADER_SIZE
                and audio_chunks[0][:4] == b"RIFF"
            ):
                # Merge WAV files
                merged_audio_data = self._merge_wav_binary(audio_chunks)
            else:
                # MP3 or other formats - simple concatenation
                merged_audio_data = b"".join(audio_chunks)
            merged_audio_base64 = base64.b64encode(merged_audio_data).decode("utf-8")

        return models.CreateSpeechResponseBody(
            audio_base64=merged_audio_base64 if merged_audio_base64 is not None else "",
            phonemes=(
                models.Phonemes(
                    symbols=merged_phonemes["symbols"],
                    durations_seconds=merged_phonemes["durations_seconds"],
                    start_times_seconds=merged_phonemes["start_times_seconds"],
                )
                if merged_phonemes["symbols"]
                else None
            ),
        )

    def _auto_chunk_text(self, max_length: int = DEFAULT_MAX_TEXT_LENGTH):
        """
        Decorator that automatically chunks long text and merges TTS responses for sync methods.

        :param max_length: Maximum text length before chunking (default: DEFAULT_MAX_TEXT_LENGTH)
        """

        def decorator(original_method):
            def wrapper(*args, **kwargs):
                text = kwargs.get("text", "")
                pronunciation_dictionary = kwargs.pop("pronunciation_dictionary", None)
                text = self._apply_pronunciation_dictionary(
                    text, pronunciation_dictionary
                )
                kwargs["text"] = text

                # Short text: call original method directly
                if not self._should_chunk_text(text, max_length):
                    return original_method(*args, **kwargs)

                # Long text: chunk, process in parallel, and merge
                text_chunks = self._chunk_text(text, max_length=max_length)
                responses = self._process_chunks_parallel(
                    text_chunks, original_method, args, kwargs
                )
                return self._merge_responses(responses)

            return wrapper

        return decorator

    def _auto_chunk_text_async(self, max_length: int = DEFAULT_MAX_TEXT_LENGTH):
        """
        Decorator that automatically chunks long text and merges TTS responses for async methods.

        :param max_length: Maximum text length before chunking (default: DEFAULT_MAX_TEXT_LENGTH)
        """

        def decorator(original_method):
            async def wrapper(*args, **kwargs):
                text = kwargs.get("text", "")
                pronunciation_dictionary = kwargs.pop("pronunciation_dictionary", None)
                text = self._apply_pronunciation_dictionary(
                    text, pronunciation_dictionary
                )
                kwargs["text"] = text

                # Short text: call original method and cache content
                if not self._should_chunk_text(text, max_length):
                    response = await original_method(*args, **kwargs)
                    # Cache async response content to prevent sync read errors
                    if hasattr(response, "result") and hasattr(
                        response.result, "aiter_bytes"
                    ):
                        audio_data = await extract_audio_from_response_async(response)
                        response.result._content = audio_data
                    return response

                # Long text: chunk, process in parallel, and merge
                text_chunks = self._chunk_text(text, max_length=max_length)
                responses = await self._process_chunks_parallel_async(
                    text_chunks, original_method, args, kwargs
                )
                return await self._merge_responses_async(responses)

            return wrapper

        return decorator

    def _auto_chunk_text_streaming(self, max_length: int = DEFAULT_MAX_TEXT_LENGTH):
        """
        Decorator that automatically chunks long text for streaming TTS methods.

        Uses sequential streaming for real-time streaming.

        :param max_length: Maximum text length before chunking (default: DEFAULT_MAX_TEXT_LENGTH)
        """

        def decorator(original_method):
            def wrapper(*args, **kwargs):
                text = kwargs.get("text", "")
                pronunciation_dictionary = kwargs.pop("pronunciation_dictionary", None)
                text = self._apply_pronunciation_dictionary(
                    text, pronunciation_dictionary
                )
                kwargs["text"] = text

                # Short text: call original method directly
                if not self._should_chunk_text(text, max_length):
                    return original_method(*args, **kwargs)

                # Long text: chunk and process
                text_chunks = self._chunk_text(text, max_length=max_length)

                # Get first response to determine format
                first_chunk_kwargs = kwargs.copy()
                first_chunk_kwargs["text"] = text_chunks[0]
                first_response = original_method(*args, **first_chunk_kwargs)

                # Single chunk: return as-is
                if len(text_chunks) == 1:
                    return first_response

                # Multiple chunks: handle based on response type
                remaining_chunks = text_chunks[1:]

                if isinstance(first_response.result, str):
                    # JSON/NDJSON format: parallel processing and merge
                    responses = self._process_chunks_parallel(
                        text_chunks, original_method, args, kwargs
                    )
                    return self._merge_streaming_responses(responses)
                else:
                    # Binary format: sequential streaming
                    extended_stream = self._create_extended_streaming_response(
                        first_response.result,
                        remaining_chunks,
                        original_method,
                        args,
                        kwargs,
                    )
                    return models.StreamSpeechResponse.model_construct(
                        result=extended_stream, headers=first_response.headers
                    )

            return wrapper

        return decorator

    def _auto_chunk_text_streaming_async(
        self, max_length: int = DEFAULT_MAX_TEXT_LENGTH
    ):
        """
        Decorator that automatically chunks long text for async streaming TTS methods.

        Uses sequential streaming for real-time streaming.

        :param max_length: Maximum text length before chunking (default: DEFAULT_MAX_TEXT_LENGTH)
        """

        def decorator(original_method):
            async def wrapper(*args, **kwargs):
                text = kwargs.get("text", "")
                pronunciation_dictionary = kwargs.pop("pronunciation_dictionary", None)
                text = self._apply_pronunciation_dictionary(
                    text, pronunciation_dictionary
                )
                kwargs["text"] = text

                # Short text: call original method directly
                if not self._should_chunk_text(text, max_length):
                    return await original_method(*args, **kwargs)

                # Long text: chunk and process
                text_chunks = self._chunk_text(text, max_length=max_length)

                # Get first response to determine format
                first_chunk_kwargs = kwargs.copy()
                first_chunk_kwargs["text"] = text_chunks[0]
                first_response = await original_method(*args, **first_chunk_kwargs)

                # Single chunk: return as-is
                if len(text_chunks) == 1:
                    return first_response

                # Multiple chunks: handle based on response type
                remaining_chunks = text_chunks[1:]

                if isinstance(first_response.result, str):
                    # JSON/NDJSON format: parallel processing and merge
                    responses = await self._process_chunks_parallel_async(
                        text_chunks, original_method, args, kwargs
                    )
                    return await self._merge_streaming_responses_async(responses)
                else:
                    # Binary format: sequential streaming
                    extended_stream = self._create_extended_streaming_response_async(
                        first_response.result,
                        remaining_chunks,
                        original_method,
                        args,
                        kwargs,
                    )
                    return models.StreamSpeechResponse.model_construct(
                        result=extended_stream, headers=first_response.headers
                    )

            return wrapper

        return decorator

    async def _merge_streaming_responses_async(
        self, responses: List[models.StreamSpeechResponse]
    ) -> models.StreamSpeechResponse:
        """Async version: Merge streaming responses and return StreamSpeechResponse"""
        if not responses:
            raise RuntimeError("No responses to merge")

        # Use first response headers as default
        first_response = responses[0]
        merged_headers = first_response.headers

        # Collect audio data and phoneme data
        audio_data = b""
        phoneme_chunks = []

        for i, response in enumerate(responses):
            try:
                # Extract audio data from response
                if isinstance(response.result, str):
                    # NDJSON response
                    chunk_audio = self._extract_audio_from_ndjson(response.result)
                else:
                    # Binary response
                    chunk_audio = await extract_audio_from_response_async(response)

                # Remove WAV header for non-first chunks
                if i > 0 and chunk_audio:
                    chunk_audio = self._remove_wav_header(chunk_audio)

                audio_data += chunk_audio

                # Extract and collect phoneme data
                phoneme_data = self._extract_phonemes_from_stream_response(response)
                if phoneme_data and isinstance(phoneme_data, dict):
                    phoneme_chunks.append(phoneme_data)

            except Exception as e:
                logger.warning(f"Failed to process response {i}: {e}")
                continue

        # Merge phoneme data using utility function
        merged_phonemes = merge_phoneme_data(phoneme_chunks)

        # Generate merged result as JSON string
        merged_result = {
            "audio_base64": base64.b64encode(audio_data).decode("utf-8"),
            "phonemes": merged_phonemes if merged_phonemes["symbols"] else None,
        }

        result_json = json.dumps(merged_result)

        return models.StreamSpeechResponse(
            headers=merged_headers,
            result=result_json,
        )

    def _merge_streaming_responses(
        self, responses: List[models.StreamSpeechResponse]
    ) -> models.StreamSpeechResponse:
        """Merge streaming responses and return StreamSpeechResponse"""
        if not responses:
            raise RuntimeError("No responses to merge")

        # Use first response headers as default
        first_response = responses[0]
        merged_headers = first_response.headers

        # Collect audio data and phoneme data
        audio_data = b""
        phoneme_chunks = []

        for i, response in enumerate(responses):
            try:
                # Extract audio data from response
                if isinstance(response.result, str):
                    # NDJSON response
                    chunk_audio = self._extract_audio_from_ndjson(response.result)
                else:
                    # Binary response
                    chunk_audio = extract_audio_from_response(response)

                # Remove WAV header for non-first chunks
                if i > 0 and chunk_audio:
                    chunk_audio = self._remove_wav_header(chunk_audio)

                audio_data += chunk_audio

                # Extract and collect phoneme data
                phoneme_data = self._extract_phonemes_from_stream_response(response)
                if phoneme_data and isinstance(phoneme_data, dict):
                    phoneme_chunks.append(phoneme_data)

            except Exception as e:
                logger.warning(f"Failed to process response {i}: {e}")
                continue

        # Merge phoneme data using utility function
        merged_phonemes = merge_phoneme_data(phoneme_chunks)

        # Generate merged result as JSON string (StreamSpeechResponse compatible)
        merged_result = {
            "audio_base64": base64.b64encode(audio_data).decode("utf-8"),
            "phonemes": merged_phonemes if merged_phonemes["symbols"] else None,
        }

        # Return in NDJSON format (application/x-ndjson compatible)
        result_json = json.dumps(merged_result)

        # Return StreamSpeechResponse (as str type)
        return models.StreamSpeechResponse(
            headers=merged_headers,
            result=result_json,
        )

    def _extract_phonemes_from_stream_response(self, response):
        """Extract phoneme data from stream response"""
        # Process NDJSON response
        if isinstance(response.result, str):
            import json

            # Check if it's a single JSON object
            try:
                data = json.loads(response.result)
                if "phonemes" in data:
                    return data["phonemes"]
            except json.JSONDecodeError:
                pass

            # Process NDJSON (multiple lines)
            lines = response.result.strip().split("\n")

            # Collect phoneme data from all lines
            # Note: NDJSON lines within a single response already have
            # continuous timestamps from the API, so we just concatenate them
            all_phonemes = create_empty_phoneme_dict()

            for i, line in enumerate(lines):
                if line.strip():
                    try:
                        data = json.loads(line)
                        if "phonemes" in data and data["phonemes"]:
                            phonemes = data["phonemes"]
                            # Directly extend without time adjustment
                            # (API provides continuous timestamps within NDJSON)
                            all_phonemes["symbols"].extend(phonemes.get("symbols", []))
                            all_phonemes["durations_seconds"].extend(
                                phonemes.get("durations_seconds", [])
                            )
                            all_phonemes["start_times_seconds"].extend(
                                phonemes.get("start_times_seconds", [])
                            )
                    except json.JSONDecodeError:
                        logger.debug(f"Failed to parse NDJSON line {i}")
                        continue

            # Return collected phoneme data if any
            if all_phonemes["symbols"]:
                return all_phonemes
        return None

    def _extract_audio_from_ndjson(self, ndjson_str):
        """
        Extract audio data from NDJSON response.

        Wrapper for the extract_audio_from_ndjson utility function from custom_utils.

        :param ndjson_str: NDJSON string containing audio_base64 field
        :return: Decoded binary audio data
        """
        return extract_audio_from_ndjson(ndjson_str)

    def _remove_wav_header(self, audio_data):
        """
        Remove WAV header (for intermediate chunks).

        Wrapper for the remove_wav_header utility function from custom_utils.

        :param audio_data: Binary WAV data with header
        :return: Binary audio data without header
        """
        return remove_wav_header(audio_data)

    def _create_sequential_streaming_response(
        self, text_chunks, original_method, args, kwargs
    ):
        """Create sequential streaming response with header handling"""

        class HeaderAwareStreamingResponse:
            def __init__(self, chunks, method, args, kwargs):
                self.text_chunks = chunks
                self.original_method = method
                self.args = args
                self.kwargs = kwargs
                self.is_first_chunk = True
                self.audio_format = None  # 'wav' or 'mp3'

                # Attributes for httpx.Response interface compatibility
                self.status_code = 200
                self.headers = {}
                self.url = None
                self.request = None

            def iter_bytes(self, chunk_size=DEFAULT_STREAM_CHUNK_SIZE):
                """Sequential streaming with header handling"""
                for i, text_chunk in enumerate(self.text_chunks):
                    # Stream request with current text chunk
                    chunk_kwargs = self.kwargs.copy()
                    chunk_kwargs["text"] = text_chunk

                    response = self.original_method(*self.args, **chunk_kwargs)

                    if hasattr(response.result, "iter_bytes"):
                        # Process streaming data in real-time
                        yield from self._process_streaming_chunk(
                            response.result.iter_bytes(), i == 0
                        )

            def _process_streaming_chunk(self, chunk_iterator, is_first_chunk):
                """Process streaming data of individual chunk with header handling"""
                accumulated_data = b""
                header_processed = False

                for raw_chunk in chunk_iterator:
                    accumulated_data += raw_chunk

                    # If first chunk or header not yet processed
                    if is_first_chunk and not header_processed:
                        # First chunk: send with header and detect format
                        if len(accumulated_data) >= WAV_HEADER_SIZE:
                            self.audio_format = self._detect_audio_format(
                                accumulated_data
                            )
                            header_processed = True
                            yield accumulated_data
                            accumulated_data = b""
                        # Continue accumulating if not enough data for header analysis

                    elif not is_first_chunk and not header_processed:
                        # From second chunk: remove header and send audio data only
                        if self._has_enough_data_for_header_removal(accumulated_data):
                            audio_only_data = self._remove_audio_header(
                                accumulated_data
                            )
                            header_processed = True
                            if audio_only_data:
                                yield audio_only_data
                            accumulated_data = b""
                        # Continue accumulating if not enough data for header removal

                    else:
                        # After header processing: send pure audio data
                        yield accumulated_data
                        accumulated_data = b""

                # Send remaining data if any
                if accumulated_data:
                    if not is_first_chunk and not header_processed:
                        # Try to remove header at the end
                        audio_only_data = self._remove_audio_header(accumulated_data)
                        if audio_only_data:
                            yield audio_only_data
                    else:
                        yield accumulated_data

            def _detect_audio_format(self, data):
                """Detect audio format using utility function"""
                return detect_audio_format(data)

            def _has_enough_data_for_header_removal(self, data):
                """Check if there's enough data for header removal"""
                if self.audio_format == "wav":
                    return len(data) >= WAV_HEADER_SIZE
                elif self.audio_format == "mp3":
                    return len(data) >= MP3_ID3V2_HEADER_SIZE
                return len(data) >= WAV_HEADER_SIZE  # Default

            def _remove_audio_header(self, data):
                """Remove audio header using utility functions"""
                if self.audio_format == "wav":
                    return remove_wav_header(data)
                elif self.audio_format == "mp3":
                    return remove_mp3_header(data)
                else:
                    # Return as-is for unknown format (safety)
                    return data

            def read(self):
                """Read all data (fallback)"""
                all_data = b""
                for chunk in self.iter_bytes():
                    all_data += chunk
                return all_data

            # Additional methods for httpx.Response compatibility
            @property
            def content(self):
                """httpx.Response.content compatibility"""
                return self.read()

            @property
            def text(self):
                """httpx.Response.text compatibility"""
                return self.read().decode("utf-8", errors="ignore")

            def json(self):
                """httpx.Response.json() compatibility"""
                import json

                return json.loads(self.text)

            def __getattr__(self, name):
                """Provide default values for missing attributes"""
                if name in ["encoding", "reason_phrase", "http_version", "elapsed"]:
                    return None
                raise AttributeError(
                    f"'{self.__class__.__name__}' object has no attribute '{name}'"
                )

        # Get header info from first response
        first_chunk_kwargs = kwargs.copy()
        first_chunk_kwargs["text"] = text_chunks[0]
        first_response = original_method(*args, **first_chunk_kwargs)

        # Create header-aware streaming response
        header_aware_stream = HeaderAwareStreamingResponse(
            text_chunks, original_method, args, kwargs
        )

        # Copy header info from first response
        if hasattr(first_response, "headers"):
            header_aware_stream.headers = first_response.headers

        return models.StreamSpeechResponse(
            result=header_aware_stream,  # type: ignore[arg-type]
            headers=first_response.headers,
        )

    async def _create_sequential_streaming_response_async(
        self, text_chunks, original_method, args, kwargs
    ):
        """Create async sequential streaming response with header handling"""

        class AsyncHeaderAwareStreamingResponse:
            def __init__(self, chunks, method, args, kwargs):
                self.text_chunks = chunks
                self.original_method = method
                self.args = args
                self.kwargs = kwargs
                self.is_first_chunk = True
                self.audio_format = None  # 'wav' or 'mp3'

            async def iter_bytes(self, chunk_size=DEFAULT_STREAM_CHUNK_SIZE):
                """Async sequential streaming with header handling"""
                for i, text_chunk in enumerate(self.text_chunks):
                    # Async streaming request with current text chunk
                    chunk_kwargs = self.kwargs.copy()
                    chunk_kwargs["text"] = text_chunk

                    response = await self.original_method(*self.args, **chunk_kwargs)

                    if hasattr(response.result, "aiter_bytes"):
                        # Process streaming data in real-time (async)
                        async for chunk in self._process_streaming_chunk_async(
                            response.result.aiter_bytes(), i == 0
                        ):
                            yield chunk
                    elif hasattr(response.result, "iter_bytes"):
                        # Fallback: sync iterator (shouldn't happen in async context)
                        logger.warning("Using sync iterator in async context")
                        async for chunk in self._process_streaming_chunk_async(
                            response.result.iter_bytes(), i == 0
                        ):
                            yield chunk

            async def _process_streaming_chunk_async(
                self, chunk_iterator, is_first_chunk
            ):
                """Process async streaming data of individual chunk with header handling"""
                accumulated_data = b""
                header_processed = False

                # Check if chunk_iterator is async iterator
                if hasattr(chunk_iterator, "__aiter__"):
                    # Async iterator
                    async for raw_chunk in chunk_iterator:
                        accumulated_data += raw_chunk

                        # If first chunk or header not yet processed
                        if is_first_chunk and not header_processed:
                            # First chunk: send with header and detect format
                            if len(accumulated_data) >= WAV_HEADER_SIZE:
                                self.audio_format = self._detect_audio_format(
                                    accumulated_data
                                )
                                header_processed = True
                                yield accumulated_data
                                accumulated_data = b""
                            # Continue accumulating if not enough data for header analysis

                        elif not is_first_chunk and not header_processed:
                            # From second chunk: remove header and send audio data only
                            if self._has_enough_data_for_header_removal(
                                accumulated_data
                            ):
                                audio_only_data = self._remove_audio_header(
                                    accumulated_data
                                )
                                header_processed = True
                                if audio_only_data:
                                    yield audio_only_data
                                accumulated_data = b""
                            # Continue accumulating if not enough data for header removal

                        else:
                            # After header processing: send pure audio data
                            yield accumulated_data
                            accumulated_data = b""

                else:
                    # Sync iterator case
                    for raw_chunk in chunk_iterator:
                        accumulated_data += raw_chunk

                        # If first chunk or header not yet processed
                        if is_first_chunk and not header_processed:
                            if len(accumulated_data) >= WAV_HEADER_SIZE:
                                self.audio_format = self._detect_audio_format(
                                    accumulated_data
                                )
                                header_processed = True
                                yield accumulated_data
                                accumulated_data = b""

                        elif not is_first_chunk and not header_processed:
                            if self._has_enough_data_for_header_removal(
                                accumulated_data
                            ):
                                audio_only_data = self._remove_audio_header(
                                    accumulated_data
                                )
                                header_processed = True
                                if audio_only_data:
                                    yield audio_only_data
                                accumulated_data = b""

                        else:
                            yield accumulated_data
                            accumulated_data = b""

                # Send remaining data if any
                if accumulated_data:
                    if not is_first_chunk and not header_processed:
                        # Try to remove header at the end
                        audio_only_data = self._remove_audio_header(accumulated_data)
                        if audio_only_data:
                            yield audio_only_data
                    else:
                        yield accumulated_data

            def _detect_audio_format(self, data):
                """Detect audio format using utility function"""
                return detect_audio_format(data)

            def _has_enough_data_for_header_removal(self, data):
                """Check if there's enough data for header removal"""
                if self.audio_format == "wav":
                    return len(data) >= WAV_HEADER_SIZE
                elif self.audio_format == "mp3":
                    return len(data) >= MP3_ID3V2_HEADER_SIZE
                return len(data) >= WAV_HEADER_SIZE  # Default

            def _remove_audio_header(self, data):
                """Remove audio header using utility functions"""
                if self.audio_format == "wav":
                    return remove_wav_header(data)
                elif self.audio_format == "mp3":
                    return remove_mp3_header(data)
                else:
                    # Return as-is for unknown format (safety)
                    return data

            async def read(self):
                """Read all data (fallback)"""
                all_data = b""
                async for chunk in self.iter_bytes():
                    all_data += chunk
                return all_data

        # Get header info from first response
        first_chunk_kwargs = kwargs.copy()
        first_chunk_kwargs["text"] = text_chunks[0]
        first_response = await original_method(*args, **first_chunk_kwargs)

        # Create async header-aware streaming response
        async_header_aware_stream = AsyncHeaderAwareStreamingResponse(
            text_chunks, original_method, args, kwargs
        )

        return models.StreamSpeechResponse(
            result=async_header_aware_stream,  # type: ignore[arg-type]
            headers=first_response.headers,
        )

    async def _merge_responses_async(
        self, responses: List[models.CreateSpeechResponse]
    ) -> models.CreateSpeechResponse:
        """Async version: Helper method that selects appropriate merge strategy based on response type"""
        if not responses:
            raise RuntimeError("No responses to merge")

        # Use first response headers as default
        merged_headers = responses[0].headers
        first_result = responses[0].result

        # Merge based on response type
        if (
            hasattr(first_result, "read")
            or hasattr(first_result, "aread")
            or hasattr(first_result, "content")
        ):
            # audio/wav or audio/mpeg response
            merged_audio = await self._merge_audio_async(responses)

            # Store merged audio in first response's _content
            # Reuse original response object (passes pydantic validation)
            first_result._content = merged_audio  # type: ignore[attr-defined,union-attr]

            return models.CreateSpeechResponse(
                result=first_result, headers=merged_headers
            )

        elif isinstance(first_result, models.CreateSpeechResponseBody):
            # application/json response (includes phoneme data)
            json_responses = [
                r
                for r in responses
                if isinstance(r.result, models.CreateSpeechResponseBody)
            ]
            merged_result = self._merge_json_responses(json_responses)

            return models.CreateSpeechResponse(
                result=merged_result, headers=merged_headers
            )

        else:
            raise RuntimeError("Unexpected response type for chunked processing")

    def _merge_responses(
        self, responses: List[models.CreateSpeechResponse]
    ) -> models.CreateSpeechResponse:
        """Helper method that selects appropriate merge strategy based on response type"""
        if not responses:
            raise RuntimeError("No responses to merge")

        # Use first response headers as default
        merged_headers = responses[0].headers
        first_result = responses[0].result

        # Merge based on response type
        if hasattr(first_result, "read") or hasattr(first_result, "content"):
            # audio/wav or audio/mpeg response
            # Pass response object as-is (binary extraction handled in _merge_wav_files)
            merged_audio = self._merge_audio(responses)

            # Create new response with merged audio
            merged_response = copy.deepcopy(first_result)

            # Set to _content attribute for httpx.Response
            if hasattr(merged_response, "_content"):
                merged_response._content = merged_audio  # type: ignore[attr-defined]
            elif hasattr(merged_response, "content"):
                merged_response.content = merged_audio  # type: ignore[misc]
            else:
                # Need to create new object for other types
                class MockResponse:
                    def __init__(self, content):
                        self.content = content

                    def read(self):
                        return self.content

                merged_response = MockResponse(merged_audio)  # type: ignore[assignment]

            return models.CreateSpeechResponse(
                result=merged_response,  # type: ignore[arg-type]
                headers=merged_headers,  # type: ignore[arg-type]
            )

        elif isinstance(first_result, models.CreateSpeechResponseBody):
            # application/json response (includes phoneme data)
            json_responses = [
                r
                for r in responses
                if isinstance(r.result, models.CreateSpeechResponseBody)
            ]
            merged_result = self._merge_json_responses(json_responses)

            return models.CreateSpeechResponse(
                result=merged_result, headers=merged_headers
            )

        else:
            raise RuntimeError("Unexpected response type for chunked processing")

    def _create_extended_streaming_response(
        self, original_response, remaining_chunks, original_method, args, kwargs
    ):
        """Extend original response to stream remaining chunks sequentially (includes phoneme merging)"""

        class ExtendedStreamingWrapper:
            def __init__(self, original_resp, chunks, method, args, kwargs):
                self.original_response = original_resp
                self.remaining_chunks = chunks
                self.original_method = method
                self.args = args
                self.kwargs = kwargs
                self._first_chunk_consumed = False
                self._phoneme_data = []
                self._total_duration_offset = 0.0  # Accumulated duration

            def iter_bytes(self, chunk_size=DEFAULT_STREAM_CHUNK_SIZE):
                # Process first chunk
                if not self._first_chunk_consumed:
                    first_phoneme = self._extract_phoneme_from_response(
                        self.original_response
                    )
                    if first_phoneme:
                        # Add first chunk without offset
                        self._phoneme_data.append(first_phoneme)
                        # Update offset with sum of total duration
                        if "durations_seconds" in first_phoneme:
                            self._total_duration_offset = sum(
                                first_phoneme["durations_seconds"]
                            )

                    for chunk in self.original_response.iter_bytes(chunk_size):
                        yield self._remove_audio_trailer_if_needed(chunk)
                    self._first_chunk_consumed = True

                # Process remaining chunks
                for text_chunk in self.remaining_chunks:
                    chunk_kwargs = self.kwargs.copy()
                    chunk_kwargs["text"] = text_chunk

                    try:
                        chunk_response = self.original_method(
                            *self.args, **chunk_kwargs
                        )

                        chunk_phoneme = self._extract_phoneme_from_response(
                            chunk_response
                        )
                        if chunk_phoneme:
                            # Adjust time by applying current offset
                            adjusted_phoneme = adjust_phoneme_timing(
                                chunk_phoneme, self._total_duration_offset
                            )
                            self._phoneme_data.append(adjusted_phoneme)

                            # Accumulate this chunk's total duration to offset
                            if "durations_seconds" in chunk_phoneme:
                                self._total_duration_offset += sum(
                                    chunk_phoneme["durations_seconds"]
                                )

                        for chunk in chunk_response.result.iter_bytes(chunk_size):
                            yield self._remove_audio_header_if_needed(chunk)
                    except Exception as e:
                        logger.warning(f"Failed to process chunk: {e}")
                        continue

            def _extract_phoneme_from_response(self, response):
                """Extract phoneme data from response"""
                # Parse phoneme info from NDJSON format response
                if hasattr(response, "result") and isinstance(response.result, str):
                    try:
                        import json

                        # Parse NDJSON (each line is separate JSON)
                        lines = response.result.strip().split("\n")
                        for line in lines:
                            if line.strip():
                                data = json.loads(line)
                                if "phonemes" in data:
                                    return data["phonemes"]
                    except Exception:
                        pass
                return None

            def get_merged_phonemes(self):
                """Return merged phoneme data"""
                if not self._phoneme_data:
                    return None

                merged = {
                    "symbols": [],
                    "durations_seconds": [],
                    "start_times_seconds": [],
                }
                for phoneme in self._phoneme_data:
                    merged["symbols"].extend(phoneme.get("symbols", []))
                    merged["durations_seconds"].extend(
                        phoneme.get("durations_seconds", [])
                    )
                    merged["start_times_seconds"].extend(
                        phoneme.get("start_times_seconds", [])
                    )

                return merged

            def _remove_audio_trailer_if_needed(self, chunk):
                """Remove trailing info from first chunk (if needed)"""
                return chunk

            def _remove_audio_header_if_needed(self, chunk):
                """Remove audio header from intermediate chunks using utility function"""
                return remove_wav_header(chunk)

            def __getattr__(self, name):
                """Get missing attributes from original response"""
                return getattr(self.original_response, name)

        return ExtendedStreamingWrapper(
            original_response, remaining_chunks, original_method, args, kwargs
        )

    def _create_extended_streaming_response_async(
        self, original_response, remaining_chunks, original_method, args, kwargs
    ):
        """Async version: Extend original response to stream remaining chunks sequentially"""

        class AsyncExtendedStreamingWrapper:
            def __init__(self, original_resp, chunks, method, args, kwargs):
                self.original_response = original_resp
                self.remaining_chunks = chunks
                self.original_method = method
                self.args = args
                self.kwargs = kwargs
                self._first_chunk_consumed = False

            async def aiter_bytes(self, chunk_size=DEFAULT_STREAM_CHUNK_SIZE):
                """Extended async streaming - first response + remaining chunks"""
                # Stream first chunk
                if not self._first_chunk_consumed:
                    if hasattr(self.original_response, "aiter_bytes"):
                        # Use async stream
                        async for chunk in self.original_response.aiter_bytes(
                            chunk_size
                        ):
                            yield self._remove_audio_trailer_if_needed(chunk)
                    elif hasattr(self.original_response, "iter_bytes"):
                        # Fallback to sync stream
                        for chunk in self.original_response.iter_bytes(chunk_size):
                            yield self._remove_audio_trailer_if_needed(chunk)
                    self._first_chunk_consumed = True

                # Process remaining chunks sequentially
                for text_chunk in self.remaining_chunks:
                    chunk_kwargs = self.kwargs.copy()
                    chunk_kwargs["text"] = text_chunk

                    try:
                        chunk_response = await self.original_method(
                            *self.args, **chunk_kwargs
                        )
                        if hasattr(chunk_response.result, "aiter_bytes"):
                            # Use async stream
                            async for chunk in chunk_response.result.aiter_bytes(
                                chunk_size
                            ):
                                yield self._remove_audio_header_if_needed(chunk)
                        elif hasattr(chunk_response.result, "iter_bytes"):
                            # Fallback to sync stream
                            for chunk in chunk_response.result.iter_bytes(chunk_size):
                                yield self._remove_audio_header_if_needed(chunk)
                    except Exception as e:
                        logger.warning(f"Failed to process chunk: {e}")
                        continue

            def _remove_audio_header_if_needed(self, chunk):
                """Remove audio header from intermediate chunks using utility function"""
                return remove_wav_header(chunk)

            def _remove_audio_trailer_if_needed(self, chunk):
                """Remove trailing info from first chunk (if needed)"""
                return chunk

            def __getattr__(self, name):
                """Get missing attributes from original response"""
                return getattr(self.original_response, name)

        return AsyncExtendedStreamingWrapper(
            original_response, remaining_chunks, original_method, args, kwargs
        )

    # endregion sdk-class-body

    def create_speech(
        self,
        *,
        voice_id: str,
        text: str,
        language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage,
        style: Optional[str] = None,
        model: Optional[
            models.APIConvertTextToSpeechUsingCharacterRequestModel
        ] = models.APIConvertTextToSpeechUsingCharacterRequestModel.SONA_SPEECH_1,
        output_format: Optional[
            models.APIConvertTextToSpeechUsingCharacterRequestOutputFormat
        ] = models.APIConvertTextToSpeechUsingCharacterRequestOutputFormat.WAV,
        voice_settings: Optional[
            Union[
                models.ConvertTextToSpeechParameters,
                models.ConvertTextToSpeechParametersTypedDict,
            ]
        ] = None,
        include_phonemes: Optional[bool] = False,
        normalized_text: Optional[str] = None,
        retries: OptionalNullable[utils.RetryConfig] = UNSET,
        server_url: Optional[str] = None,
        timeout_ms: Optional[int] = None,
        accept_header_override: Optional[CreateSpeechAcceptEnum] = None,
        http_headers: Optional[Mapping[str, str]] = None,
    ) -> models.CreateSpeechResponse:
        r"""Convert text to speech

        Convert text to speech using the specified voice

        :param voice_id:
        :param text: The text to convert to speech
        :param language: The language code of the text
        :param style: The style of character to use for the text-to-speech conversion
        :param model: The model type to use for the text-to-speech conversion
        :param output_format: The desired output format of the audio file (wav, mp3). Default is wav.
        :param voice_settings:
        :param include_phonemes: Return phoneme timing data with the audio
        :param normalized_text: Pre-normalized text for TTS. Only used with sona_speech_2 and sona_speech_2_flash models.
        :param retries: Override the default retry configuration for this method
        :param server_url: Override the default server URL for this method
        :param timeout_ms: Override the default request timeout configuration for this method in milliseconds
        :param accept_header_override: Override the default accept header for this method
        :param http_headers: Additional headers to set or replace on requests.
        """
        base_url = None
        url_variables = None
        if timeout_ms is None:
            timeout_ms = self.sdk_configuration.timeout_ms

        if server_url is not None:
            base_url = server_url
        else:
            base_url = self._get_url(base_url, url_variables)

        request = models.CreateSpeechRequest(
            voice_id=voice_id,
            api_convert_text_to_speech_using_character_request=models.APIConvertTextToSpeechUsingCharacterRequest(
                text=text,
                language=language,
                style=style,
                model=model,
                output_format=output_format,
                voice_settings=utils.get_pydantic_model(
                    voice_settings, Optional[models.ConvertTextToSpeechParameters]
                ),
                include_phonemes=include_phonemes,
                normalized_text=normalized_text,
            ),
        )

        req = self._build_request(
            method="POST",
            path="/v1/text-to-speech/{voice_id}",
            base_url=base_url,
            url_variables=url_variables,
            request=request,
            request_body_required=True,
            request_has_path_params=True,
            request_has_query_params=True,
            user_agent_header="user-agent",
            accept_header_value=accept_header_override.value
            if accept_header_override is not None
            else "application/json;q=1, audio/mpeg;q=0.7, audio/wav;q=0",
            http_headers=http_headers,
            security=self.sdk_configuration.security,
            get_serialized_body=lambda: utils.serialize_request_body(
                request.api_convert_text_to_speech_using_character_request,
                False,
                False,
                "json",
                models.APIConvertTextToSpeechUsingCharacterRequest,
            ),
            timeout_ms=timeout_ms,
        )

        if retries == UNSET:
            if self.sdk_configuration.retry_config is not UNSET:
                retries = self.sdk_configuration.retry_config

        retry_config = None
        if isinstance(retries, utils.RetryConfig):
            retry_config = (retries, ["429", "500", "502", "503", "504"])

        http_res = self.do_request(
            hook_ctx=HookContext(
                config=self.sdk_configuration,
                base_url=base_url or "",
                operation_id="create_speech",
                oauth2_scopes=[],
                security_source=self.sdk_configuration.security,
            ),
            request=req,
            error_status_codes=[
                "400",
                "401",
                "402",
                "403",
                "404",
                "408",
                "429",
                "4XX",
                "500",
                "5XX",
            ],
            stream=True,
            retry_config=retry_config,
        )

        response_data: Any = None
        if utils.match_response(http_res, "200", "audio/wav"):
            return models.CreateSpeechResponse(
                result=http_res, headers=utils.get_response_headers(http_res.headers)
            )
        if utils.match_response(http_res, "200", "audio/mpeg"):
            return models.CreateSpeechResponse(
                result=http_res, headers=utils.get_response_headers(http_res.headers)
            )
        if utils.match_response(http_res, "200", "application/json"):
            http_res_text = utils.stream_to_text(http_res)
            return models.CreateSpeechResponse(
                result=unmarshal_json_response(
                    models.CreateSpeechResponseBody, http_res, http_res_text
                ),
                headers=utils.get_response_headers(http_res.headers),
            )
        if utils.match_response(http_res, "400", "application/json"):
            http_res_text = utils.stream_to_text(http_res)
            response_data = unmarshal_json_response(
                errors.BadRequestErrorResponseData, http_res, http_res_text
            )
            raise errors.BadRequestErrorResponse(response_data, http_res, http_res_text)
        if utils.match_response(http_res, "401", "application/json"):
            http_res_text = utils.stream_to_text(http_res)
            response_data = unmarshal_json_response(
                errors.UnauthorizedErrorResponseData, http_res, http_res_text
            )
            raise errors.UnauthorizedErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "402", "application/json"):
            http_res_text = utils.stream_to_text(http_res)
            response_data = unmarshal_json_response(
                errors.PaymentRequiredErrorResponseData, http_res, http_res_text
            )
            raise errors.PaymentRequiredErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "403", "application/json"):
            http_res_text = utils.stream_to_text(http_res)
            response_data = unmarshal_json_response(
                errors.ForbiddenErrorResponseData, http_res, http_res_text
            )
            raise errors.ForbiddenErrorResponse(response_data, http_res, http_res_text)
        if utils.match_response(http_res, "404", "application/json"):
            http_res_text = utils.stream_to_text(http_res)
            response_data = unmarshal_json_response(
                errors.NotFoundErrorResponseData, http_res, http_res_text
            )
            raise errors.NotFoundErrorResponse(response_data, http_res, http_res_text)
        if utils.match_response(http_res, "408", "application/json"):
            http_res_text = utils.stream_to_text(http_res)
            response_data = unmarshal_json_response(
                errors.RequestTimeoutErrorResponseData, http_res, http_res_text
            )
            raise errors.RequestTimeoutErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "429", "application/json"):
            http_res_text = utils.stream_to_text(http_res)
            response_data = unmarshal_json_response(
                errors.TooManyRequestsErrorResponseData, http_res, http_res_text
            )
            raise errors.TooManyRequestsErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "500", "application/json"):
            http_res_text = utils.stream_to_text(http_res)
            response_data = unmarshal_json_response(
                errors.InternalServerErrorResponseData, http_res, http_res_text
            )
            raise errors.InternalServerErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "4XX", "*"):
            http_res_text = utils.stream_to_text(http_res)
            raise errors.SupertoneDefaultError(
                "API error occurred", http_res, http_res_text
            )
        if utils.match_response(http_res, "5XX", "*"):
            http_res_text = utils.stream_to_text(http_res)
            raise errors.SupertoneDefaultError(
                "API error occurred", http_res, http_res_text
            )

        http_res_text = utils.stream_to_text(http_res)
        raise errors.SupertoneDefaultError(
            "Unexpected response received", http_res, http_res_text
        )

    async def create_speech_async(
        self,
        *,
        voice_id: str,
        text: str,
        language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage,
        style: Optional[str] = None,
        model: Optional[
            models.APIConvertTextToSpeechUsingCharacterRequestModel
        ] = models.APIConvertTextToSpeechUsingCharacterRequestModel.SONA_SPEECH_1,
        output_format: Optional[
            models.APIConvertTextToSpeechUsingCharacterRequestOutputFormat
        ] = models.APIConvertTextToSpeechUsingCharacterRequestOutputFormat.WAV,
        voice_settings: Optional[
            Union[
                models.ConvertTextToSpeechParameters,
                models.ConvertTextToSpeechParametersTypedDict,
            ]
        ] = None,
        include_phonemes: Optional[bool] = False,
        normalized_text: Optional[str] = None,
        retries: OptionalNullable[utils.RetryConfig] = UNSET,
        server_url: Optional[str] = None,
        timeout_ms: Optional[int] = None,
        accept_header_override: Optional[CreateSpeechAcceptEnum] = None,
        http_headers: Optional[Mapping[str, str]] = None,
    ) -> models.CreateSpeechResponse:
        r"""Convert text to speech

        Convert text to speech using the specified voice

        :param voice_id:
        :param text: The text to convert to speech
        :param language: The language code of the text
        :param style: The style of character to use for the text-to-speech conversion
        :param model: The model type to use for the text-to-speech conversion
        :param output_format: The desired output format of the audio file (wav, mp3). Default is wav.
        :param voice_settings:
        :param include_phonemes: Return phoneme timing data with the audio
        :param normalized_text: Pre-normalized text for TTS. Only used with sona_speech_2 and sona_speech_2_flash models.
        :param retries: Override the default retry configuration for this method
        :param server_url: Override the default server URL for this method
        :param timeout_ms: Override the default request timeout configuration for this method in milliseconds
        :param accept_header_override: Override the default accept header for this method
        :param http_headers: Additional headers to set or replace on requests.
        """
        base_url = None
        url_variables = None
        if timeout_ms is None:
            timeout_ms = self.sdk_configuration.timeout_ms

        if server_url is not None:
            base_url = server_url
        else:
            base_url = self._get_url(base_url, url_variables)

        request = models.CreateSpeechRequest(
            voice_id=voice_id,
            api_convert_text_to_speech_using_character_request=models.APIConvertTextToSpeechUsingCharacterRequest(
                text=text,
                language=language,
                style=style,
                model=model,
                output_format=output_format,
                voice_settings=utils.get_pydantic_model(
                    voice_settings, Optional[models.ConvertTextToSpeechParameters]
                ),
                include_phonemes=include_phonemes,
                normalized_text=normalized_text,
            ),
        )

        req = self._build_request_async(
            method="POST",
            path="/v1/text-to-speech/{voice_id}",
            base_url=base_url,
            url_variables=url_variables,
            request=request,
            request_body_required=True,
            request_has_path_params=True,
            request_has_query_params=True,
            user_agent_header="user-agent",
            accept_header_value=accept_header_override.value
            if accept_header_override is not None
            else "application/json;q=1, audio/mpeg;q=0.7, audio/wav;q=0",
            http_headers=http_headers,
            security=self.sdk_configuration.security,
            get_serialized_body=lambda: utils.serialize_request_body(
                request.api_convert_text_to_speech_using_character_request,
                False,
                False,
                "json",
                models.APIConvertTextToSpeechUsingCharacterRequest,
            ),
            timeout_ms=timeout_ms,
        )

        if retries == UNSET:
            if self.sdk_configuration.retry_config is not UNSET:
                retries = self.sdk_configuration.retry_config

        retry_config = None
        if isinstance(retries, utils.RetryConfig):
            retry_config = (retries, ["429", "500", "502", "503", "504"])

        http_res = await self.do_request_async(
            hook_ctx=HookContext(
                config=self.sdk_configuration,
                base_url=base_url or "",
                operation_id="create_speech",
                oauth2_scopes=[],
                security_source=self.sdk_configuration.security,
            ),
            request=req,
            error_status_codes=[
                "400",
                "401",
                "402",
                "403",
                "404",
                "408",
                "429",
                "4XX",
                "500",
                "5XX",
            ],
            stream=True,
            retry_config=retry_config,
        )

        response_data: Any = None
        if utils.match_response(http_res, "200", "audio/wav"):
            return models.CreateSpeechResponse(
                result=http_res, headers=utils.get_response_headers(http_res.headers)
            )
        if utils.match_response(http_res, "200", "audio/mpeg"):
            return models.CreateSpeechResponse(
                result=http_res, headers=utils.get_response_headers(http_res.headers)
            )
        if utils.match_response(http_res, "200", "application/json"):
            http_res_text = await utils.stream_to_text_async(http_res)
            return models.CreateSpeechResponse(
                result=unmarshal_json_response(
                    models.CreateSpeechResponseBody, http_res, http_res_text
                ),
                headers=utils.get_response_headers(http_res.headers),
            )
        if utils.match_response(http_res, "400", "application/json"):
            http_res_text = await utils.stream_to_text_async(http_res)
            response_data = unmarshal_json_response(
                errors.BadRequestErrorResponseData, http_res, http_res_text
            )
            raise errors.BadRequestErrorResponse(response_data, http_res, http_res_text)
        if utils.match_response(http_res, "401", "application/json"):
            http_res_text = await utils.stream_to_text_async(http_res)
            response_data = unmarshal_json_response(
                errors.UnauthorizedErrorResponseData, http_res, http_res_text
            )
            raise errors.UnauthorizedErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "402", "application/json"):
            http_res_text = await utils.stream_to_text_async(http_res)
            response_data = unmarshal_json_response(
                errors.PaymentRequiredErrorResponseData, http_res, http_res_text
            )
            raise errors.PaymentRequiredErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "403", "application/json"):
            http_res_text = await utils.stream_to_text_async(http_res)
            response_data = unmarshal_json_response(
                errors.ForbiddenErrorResponseData, http_res, http_res_text
            )
            raise errors.ForbiddenErrorResponse(response_data, http_res, http_res_text)
        if utils.match_response(http_res, "404", "application/json"):
            http_res_text = await utils.stream_to_text_async(http_res)
            response_data = unmarshal_json_response(
                errors.NotFoundErrorResponseData, http_res, http_res_text
            )
            raise errors.NotFoundErrorResponse(response_data, http_res, http_res_text)
        if utils.match_response(http_res, "408", "application/json"):
            http_res_text = await utils.stream_to_text_async(http_res)
            response_data = unmarshal_json_response(
                errors.RequestTimeoutErrorResponseData, http_res, http_res_text
            )
            raise errors.RequestTimeoutErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "429", "application/json"):
            http_res_text = await utils.stream_to_text_async(http_res)
            response_data = unmarshal_json_response(
                errors.TooManyRequestsErrorResponseData, http_res, http_res_text
            )
            raise errors.TooManyRequestsErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "500", "application/json"):
            http_res_text = await utils.stream_to_text_async(http_res)
            response_data = unmarshal_json_response(
                errors.InternalServerErrorResponseData, http_res, http_res_text
            )
            raise errors.InternalServerErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "4XX", "*"):
            http_res_text = await utils.stream_to_text_async(http_res)
            raise errors.SupertoneDefaultError(
                "API error occurred", http_res, http_res_text
            )
        if utils.match_response(http_res, "5XX", "*"):
            http_res_text = await utils.stream_to_text_async(http_res)
            raise errors.SupertoneDefaultError(
                "API error occurred", http_res, http_res_text
            )

        http_res_text = await utils.stream_to_text_async(http_res)
        raise errors.SupertoneDefaultError(
            "Unexpected response received", http_res, http_res_text
        )

    def stream_speech(
        self,
        *,
        voice_id: str,
        text: str,
        language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage,
        style: Optional[str] = None,
        model: Optional[
            models.APIConvertTextToSpeechUsingCharacterRequestModel
        ] = models.APIConvertTextToSpeechUsingCharacterRequestModel.SONA_SPEECH_1,
        output_format: Optional[
            models.APIConvertTextToSpeechUsingCharacterRequestOutputFormat
        ] = models.APIConvertTextToSpeechUsingCharacterRequestOutputFormat.WAV,
        voice_settings: Optional[
            Union[
                models.ConvertTextToSpeechParameters,
                models.ConvertTextToSpeechParametersTypedDict,
            ]
        ] = None,
        include_phonemes: Optional[bool] = False,
        normalized_text: Optional[str] = None,
        retries: OptionalNullable[utils.RetryConfig] = UNSET,
        server_url: Optional[str] = None,
        timeout_ms: Optional[int] = None,
        accept_header_override: Optional[StreamSpeechAcceptEnum] = None,
        http_headers: Optional[Mapping[str, str]] = None,
    ) -> models.StreamSpeechResponse:
        r"""Convert text to speech with streaming response

        Convert text to speech using the specified voice with streaming response. Returns binary audio stream.

        :param voice_id:
        :param text: The text to convert to speech
        :param language: The language code of the text
        :param style: The style of character to use for the text-to-speech conversion
        :param model: The model type to use for the text-to-speech conversion
        :param output_format: The desired output format of the audio file (wav, mp3). Default is wav.
        :param voice_settings:
        :param include_phonemes: Return phoneme timing data with the audio
        :param normalized_text: Pre-normalized text for TTS. Only used with sona_speech_2 and sona_speech_2_flash models.
        :param retries: Override the default retry configuration for this method
        :param server_url: Override the default server URL for this method
        :param timeout_ms: Override the default request timeout configuration for this method in milliseconds
        :param accept_header_override: Override the default accept header for this method
        :param http_headers: Additional headers to set or replace on requests.
        """
        base_url = None
        url_variables = None
        if timeout_ms is None:
            timeout_ms = self.sdk_configuration.timeout_ms

        if server_url is not None:
            base_url = server_url
        else:
            base_url = self._get_url(base_url, url_variables)

        request = models.StreamSpeechRequest(
            voice_id=voice_id,
            api_convert_text_to_speech_using_character_request=models.APIConvertTextToSpeechUsingCharacterRequest(
                text=text,
                language=language,
                style=style,
                model=model,
                output_format=output_format,
                voice_settings=utils.get_pydantic_model(
                    voice_settings, Optional[models.ConvertTextToSpeechParameters]
                ),
                include_phonemes=include_phonemes,
                normalized_text=normalized_text,
            ),
        )

        req = self._build_request(
            method="POST",
            path="/v1/text-to-speech/{voice_id}/stream",
            base_url=base_url,
            url_variables=url_variables,
            request=request,
            request_body_required=True,
            request_has_path_params=True,
            request_has_query_params=True,
            user_agent_header="user-agent",
            accept_header_value=accept_header_override.value
            if accept_header_override is not None
            else "application/x-ndjson;q=1, audio/mpeg;q=0.7, audio/wav;q=0",
            http_headers=http_headers,
            security=self.sdk_configuration.security,
            get_serialized_body=lambda: utils.serialize_request_body(
                request.api_convert_text_to_speech_using_character_request,
                False,
                False,
                "json",
                models.APIConvertTextToSpeechUsingCharacterRequest,
            ),
            timeout_ms=timeout_ms,
        )

        if retries == UNSET:
            if self.sdk_configuration.retry_config is not UNSET:
                retries = self.sdk_configuration.retry_config

        retry_config = None
        if isinstance(retries, utils.RetryConfig):
            retry_config = (retries, ["429", "500", "502", "503", "504"])

        http_res = self.do_request(
            hook_ctx=HookContext(
                config=self.sdk_configuration,
                base_url=base_url or "",
                operation_id="stream_speech",
                oauth2_scopes=[],
                security_source=self.sdk_configuration.security,
            ),
            request=req,
            error_status_codes=[
                "400",
                "401",
                "402",
                "403",
                "404",
                "408",
                "429",
                "4XX",
                "500",
                "5XX",
            ],
            stream=True,
            retry_config=retry_config,
        )

        response_data: Any = None
        if utils.match_response(http_res, "200", "audio/wav"):
            return models.StreamSpeechResponse(
                result=http_res, headers=utils.get_response_headers(http_res.headers)
            )
        if utils.match_response(http_res, "200", "audio/mpeg"):
            return models.StreamSpeechResponse(
                result=http_res, headers=utils.get_response_headers(http_res.headers)
            )
        if utils.match_response(http_res, "200", "application/x-ndjson"):
            http_res_text = utils.stream_to_text(http_res)
            return models.StreamSpeechResponse(
                result=http_res_text,
                headers=utils.get_response_headers(http_res.headers),
            )
        if utils.match_response(http_res, "400", "application/json"):
            http_res_text = utils.stream_to_text(http_res)
            response_data = unmarshal_json_response(
                errors.BadRequestErrorResponseData, http_res, http_res_text
            )
            raise errors.BadRequestErrorResponse(response_data, http_res, http_res_text)
        if utils.match_response(http_res, "401", "application/json"):
            http_res_text = utils.stream_to_text(http_res)
            response_data = unmarshal_json_response(
                errors.UnauthorizedErrorResponseData, http_res, http_res_text
            )
            raise errors.UnauthorizedErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "402", "application/json"):
            http_res_text = utils.stream_to_text(http_res)
            response_data = unmarshal_json_response(
                errors.PaymentRequiredErrorResponseData, http_res, http_res_text
            )
            raise errors.PaymentRequiredErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "403", "application/json"):
            http_res_text = utils.stream_to_text(http_res)
            response_data = unmarshal_json_response(
                errors.ForbiddenErrorResponseData, http_res, http_res_text
            )
            raise errors.ForbiddenErrorResponse(response_data, http_res, http_res_text)
        if utils.match_response(http_res, "404", "application/json"):
            http_res_text = utils.stream_to_text(http_res)
            response_data = unmarshal_json_response(
                errors.NotFoundErrorResponseData, http_res, http_res_text
            )
            raise errors.NotFoundErrorResponse(response_data, http_res, http_res_text)
        if utils.match_response(http_res, "408", "application/json"):
            http_res_text = utils.stream_to_text(http_res)
            response_data = unmarshal_json_response(
                errors.RequestTimeoutErrorResponseData, http_res, http_res_text
            )
            raise errors.RequestTimeoutErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "429", "application/json"):
            http_res_text = utils.stream_to_text(http_res)
            response_data = unmarshal_json_response(
                errors.TooManyRequestsErrorResponseData, http_res, http_res_text
            )
            raise errors.TooManyRequestsErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "500", "application/json"):
            http_res_text = utils.stream_to_text(http_res)
            response_data = unmarshal_json_response(
                errors.InternalServerErrorResponseData, http_res, http_res_text
            )
            raise errors.InternalServerErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "4XX", "*"):
            http_res_text = utils.stream_to_text(http_res)
            raise errors.SupertoneDefaultError(
                "API error occurred", http_res, http_res_text
            )
        if utils.match_response(http_res, "5XX", "*"):
            http_res_text = utils.stream_to_text(http_res)
            raise errors.SupertoneDefaultError(
                "API error occurred", http_res, http_res_text
            )

        http_res_text = utils.stream_to_text(http_res)
        raise errors.SupertoneDefaultError(
            "Unexpected response received", http_res, http_res_text
        )

    async def stream_speech_async(
        self,
        *,
        voice_id: str,
        text: str,
        language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage,
        style: Optional[str] = None,
        model: Optional[
            models.APIConvertTextToSpeechUsingCharacterRequestModel
        ] = models.APIConvertTextToSpeechUsingCharacterRequestModel.SONA_SPEECH_1,
        output_format: Optional[
            models.APIConvertTextToSpeechUsingCharacterRequestOutputFormat
        ] = models.APIConvertTextToSpeechUsingCharacterRequestOutputFormat.WAV,
        voice_settings: Optional[
            Union[
                models.ConvertTextToSpeechParameters,
                models.ConvertTextToSpeechParametersTypedDict,
            ]
        ] = None,
        include_phonemes: Optional[bool] = False,
        normalized_text: Optional[str] = None,
        retries: OptionalNullable[utils.RetryConfig] = UNSET,
        server_url: Optional[str] = None,
        timeout_ms: Optional[int] = None,
        accept_header_override: Optional[StreamSpeechAcceptEnum] = None,
        http_headers: Optional[Mapping[str, str]] = None,
    ) -> models.StreamSpeechResponse:
        r"""Convert text to speech with streaming response

        Convert text to speech using the specified voice with streaming response. Returns binary audio stream.

        :param voice_id:
        :param text: The text to convert to speech
        :param language: The language code of the text
        :param style: The style of character to use for the text-to-speech conversion
        :param model: The model type to use for the text-to-speech conversion
        :param output_format: The desired output format of the audio file (wav, mp3). Default is wav.
        :param voice_settings:
        :param include_phonemes: Return phoneme timing data with the audio
        :param normalized_text: Pre-normalized text for TTS. Only used with sona_speech_2 and sona_speech_2_flash models.
        :param retries: Override the default retry configuration for this method
        :param server_url: Override the default server URL for this method
        :param timeout_ms: Override the default request timeout configuration for this method in milliseconds
        :param accept_header_override: Override the default accept header for this method
        :param http_headers: Additional headers to set or replace on requests.
        """
        base_url = None
        url_variables = None
        if timeout_ms is None:
            timeout_ms = self.sdk_configuration.timeout_ms

        if server_url is not None:
            base_url = server_url
        else:
            base_url = self._get_url(base_url, url_variables)

        request = models.StreamSpeechRequest(
            voice_id=voice_id,
            api_convert_text_to_speech_using_character_request=models.APIConvertTextToSpeechUsingCharacterRequest(
                text=text,
                language=language,
                style=style,
                model=model,
                output_format=output_format,
                voice_settings=utils.get_pydantic_model(
                    voice_settings, Optional[models.ConvertTextToSpeechParameters]
                ),
                include_phonemes=include_phonemes,
                normalized_text=normalized_text,
            ),
        )

        req = self._build_request_async(
            method="POST",
            path="/v1/text-to-speech/{voice_id}/stream",
            base_url=base_url,
            url_variables=url_variables,
            request=request,
            request_body_required=True,
            request_has_path_params=True,
            request_has_query_params=True,
            user_agent_header="user-agent",
            accept_header_value=accept_header_override.value
            if accept_header_override is not None
            else "application/x-ndjson;q=1, audio/mpeg;q=0.7, audio/wav;q=0",
            http_headers=http_headers,
            security=self.sdk_configuration.security,
            get_serialized_body=lambda: utils.serialize_request_body(
                request.api_convert_text_to_speech_using_character_request,
                False,
                False,
                "json",
                models.APIConvertTextToSpeechUsingCharacterRequest,
            ),
            timeout_ms=timeout_ms,
        )

        if retries == UNSET:
            if self.sdk_configuration.retry_config is not UNSET:
                retries = self.sdk_configuration.retry_config

        retry_config = None
        if isinstance(retries, utils.RetryConfig):
            retry_config = (retries, ["429", "500", "502", "503", "504"])

        http_res = await self.do_request_async(
            hook_ctx=HookContext(
                config=self.sdk_configuration,
                base_url=base_url or "",
                operation_id="stream_speech",
                oauth2_scopes=[],
                security_source=self.sdk_configuration.security,
            ),
            request=req,
            error_status_codes=[
                "400",
                "401",
                "402",
                "403",
                "404",
                "408",
                "429",
                "4XX",
                "500",
                "5XX",
            ],
            stream=True,
            retry_config=retry_config,
        )

        response_data: Any = None
        if utils.match_response(http_res, "200", "audio/wav"):
            return models.StreamSpeechResponse(
                result=http_res, headers=utils.get_response_headers(http_res.headers)
            )
        if utils.match_response(http_res, "200", "audio/mpeg"):
            return models.StreamSpeechResponse(
                result=http_res, headers=utils.get_response_headers(http_res.headers)
            )
        if utils.match_response(http_res, "200", "application/x-ndjson"):
            http_res_text = await utils.stream_to_text_async(http_res)
            return models.StreamSpeechResponse(
                result=http_res_text,
                headers=utils.get_response_headers(http_res.headers),
            )
        if utils.match_response(http_res, "400", "application/json"):
            http_res_text = await utils.stream_to_text_async(http_res)
            response_data = unmarshal_json_response(
                errors.BadRequestErrorResponseData, http_res, http_res_text
            )
            raise errors.BadRequestErrorResponse(response_data, http_res, http_res_text)
        if utils.match_response(http_res, "401", "application/json"):
            http_res_text = await utils.stream_to_text_async(http_res)
            response_data = unmarshal_json_response(
                errors.UnauthorizedErrorResponseData, http_res, http_res_text
            )
            raise errors.UnauthorizedErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "402", "application/json"):
            http_res_text = await utils.stream_to_text_async(http_res)
            response_data = unmarshal_json_response(
                errors.PaymentRequiredErrorResponseData, http_res, http_res_text
            )
            raise errors.PaymentRequiredErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "403", "application/json"):
            http_res_text = await utils.stream_to_text_async(http_res)
            response_data = unmarshal_json_response(
                errors.ForbiddenErrorResponseData, http_res, http_res_text
            )
            raise errors.ForbiddenErrorResponse(response_data, http_res, http_res_text)
        if utils.match_response(http_res, "404", "application/json"):
            http_res_text = await utils.stream_to_text_async(http_res)
            response_data = unmarshal_json_response(
                errors.NotFoundErrorResponseData, http_res, http_res_text
            )
            raise errors.NotFoundErrorResponse(response_data, http_res, http_res_text)
        if utils.match_response(http_res, "408", "application/json"):
            http_res_text = await utils.stream_to_text_async(http_res)
            response_data = unmarshal_json_response(
                errors.RequestTimeoutErrorResponseData, http_res, http_res_text
            )
            raise errors.RequestTimeoutErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "429", "application/json"):
            http_res_text = await utils.stream_to_text_async(http_res)
            response_data = unmarshal_json_response(
                errors.TooManyRequestsErrorResponseData, http_res, http_res_text
            )
            raise errors.TooManyRequestsErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "500", "application/json"):
            http_res_text = await utils.stream_to_text_async(http_res)
            response_data = unmarshal_json_response(
                errors.InternalServerErrorResponseData, http_res, http_res_text
            )
            raise errors.InternalServerErrorResponse(
                response_data, http_res, http_res_text
            )
        if utils.match_response(http_res, "4XX", "*"):
            http_res_text = await utils.stream_to_text_async(http_res)
            raise errors.SupertoneDefaultError(
                "API error occurred", http_res, http_res_text
            )
        if utils.match_response(http_res, "5XX", "*"):
            http_res_text = await utils.stream_to_text_async(http_res)
            raise errors.SupertoneDefaultError(
                "API error occurred", http_res, http_res_text
            )

        http_res_text = await utils.stream_to_text_async(http_res)
        raise errors.SupertoneDefaultError(
            "Unexpected response received", http_res, http_res_text
        )

    def predict_duration(
        self,
        *,
        voice_id: str,
        text: str,
        language: models.PredictTTSDurationRequestLanguage,
        style: Optional[str] = None,
        model: Optional[
            models.PredictTTSDurationRequestModel
        ] = models.PredictTTSDurationRequestModel.SONA_SPEECH_1,
        output_format: Optional[
            models.PredictTTSDurationRequestOutputFormat
        ] = models.PredictTTSDurationRequestOutputFormat.WAV,
        voice_settings: Optional[
            Union[
                models.ConvertTextToSpeechParameters,
                models.ConvertTextToSpeechParametersTypedDict,
            ]
        ] = None,
        retries: OptionalNullable[utils.RetryConfig] = UNSET,
        server_url: Optional[str] = None,
        timeout_ms: Optional[int] = None,
        http_headers: Optional[Mapping[str, str]] = None,
    ) -> models.PredictDurationResponse:
        r"""Predict text-to-speech duration

        Predict the duration of text-to-speech conversion without generating audio

        :param voice_id:
        :param text: The text to convert to speech. Max length is 300 characters.
        :param language: Language code of the voice
        :param style: The style of character to use for the text-to-speech conversion
        :param model: The model type to use for the text-to-speech conversion
        :param output_format: The desired output format of the audio file (wav, mp3). Default is wav.
        :param voice_settings:
        :param retries: Override the default retry configuration for this method
        :param server_url: Override the default server URL for this method
        :param timeout_ms: Override the default request timeout configuration for this method in milliseconds
        :param http_headers: Additional headers to set or replace on requests.
        """
        base_url = None
        url_variables = None
        if timeout_ms is None:
            timeout_ms = self.sdk_configuration.timeout_ms

        if server_url is not None:
            base_url = server_url
        else:
            base_url = self._get_url(base_url, url_variables)

        request = models.PredictDurationRequest(
            voice_id=voice_id,
            predict_tts_duration_request=models.PredictTTSDurationRequest(
                text=text,
                language=language,
                style=style,
                model=model,
                output_format=output_format,
                voice_settings=utils.get_pydantic_model(
                    voice_settings, Optional[models.ConvertTextToSpeechParameters]
                ),
            ),
        )

        req = self._build_request(
            method="POST",
            path="/v1/predict-duration/{voice_id}",
            base_url=base_url,
            url_variables=url_variables,
            request=request,
            request_body_required=True,
            request_has_path_params=True,
            request_has_query_params=True,
            user_agent_header="user-agent",
            accept_header_value="application/json",
            http_headers=http_headers,
            security=self.sdk_configuration.security,
            get_serialized_body=lambda: utils.serialize_request_body(
                request.predict_tts_duration_request,
                False,
                False,
                "json",
                models.PredictTTSDurationRequest,
            ),
            timeout_ms=timeout_ms,
        )

        if retries == UNSET:
            if self.sdk_configuration.retry_config is not UNSET:
                retries = self.sdk_configuration.retry_config

        retry_config = None
        if isinstance(retries, utils.RetryConfig):
            retry_config = (retries, ["429", "500", "502", "503", "504"])

        http_res = self.do_request(
            hook_ctx=HookContext(
                config=self.sdk_configuration,
                base_url=base_url or "",
                operation_id="predict_duration",
                oauth2_scopes=[],
                security_source=self.sdk_configuration.security,
            ),
            request=req,
            error_status_codes=[
                "400",
                "401",
                "402",
                "403",
                "404",
                "408",
                "429",
                "4XX",
                "500",
                "5XX",
            ],
            retry_config=retry_config,
        )

        response_data: Any = None
        if utils.match_response(http_res, "200", "application/json"):
            return unmarshal_json_response(models.PredictDurationResponse, http_res)
        if utils.match_response(http_res, "400", "application/json"):
            response_data = unmarshal_json_response(
                errors.BadRequestErrorResponseData, http_res
            )
            raise errors.BadRequestErrorResponse(response_data, http_res)
        if utils.match_response(http_res, "401", "application/json"):
            response_data = unmarshal_json_response(
                errors.UnauthorizedErrorResponseData, http_res
            )
            raise errors.UnauthorizedErrorResponse(response_data, http_res)
        if utils.match_response(http_res, "402", "application/json"):
            response_data = unmarshal_json_response(
                errors.PaymentRequiredErrorResponseData, http_res
            )
            raise errors.PaymentRequiredErrorResponse(response_data, http_res)
        if utils.match_response(http_res, "403", "application/json"):
            response_data = unmarshal_json_response(
                errors.ForbiddenErrorResponseData, http_res
            )
            raise errors.ForbiddenErrorResponse(response_data, http_res)
        if utils.match_response(http_res, "404", "application/json"):
            response_data = unmarshal_json_response(
                errors.NotFoundErrorResponseData, http_res
            )
            raise errors.NotFoundErrorResponse(response_data, http_res)
        if utils.match_response(http_res, "408", "application/json"):
            response_data = unmarshal_json_response(
                errors.RequestTimeoutErrorResponseData, http_res
            )
            raise errors.RequestTimeoutErrorResponse(response_data, http_res)
        if utils.match_response(http_res, "429", "application/json"):
            response_data = unmarshal_json_response(
                errors.TooManyRequestsErrorResponseData, http_res
            )
            raise errors.TooManyRequestsErrorResponse(response_data, http_res)
        if utils.match_response(http_res, "500", "application/json"):
            response_data = unmarshal_json_response(
                errors.InternalServerErrorResponseData, http_res
            )
            raise errors.InternalServerErrorResponse(response_data, http_res)
        if utils.match_response(http_res, "4XX", "*"):
            http_res_text = utils.stream_to_text(http_res)
            raise errors.SupertoneDefaultError(
                "API error occurred", http_res, http_res_text
            )
        if utils.match_response(http_res, "5XX", "*"):
            http_res_text = utils.stream_to_text(http_res)
            raise errors.SupertoneDefaultError(
                "API error occurred", http_res, http_res_text
            )

        raise errors.SupertoneDefaultError("Unexpected response received", http_res)

    async def predict_duration_async(
        self,
        *,
        voice_id: str,
        text: str,
        language: models.PredictTTSDurationRequestLanguage,
        style: Optional[str] = None,
        model: Optional[
            models.PredictTTSDurationRequestModel
        ] = models.PredictTTSDurationRequestModel.SONA_SPEECH_1,
        output_format: Optional[
            models.PredictTTSDurationRequestOutputFormat
        ] = models.PredictTTSDurationRequestOutputFormat.WAV,
        voice_settings: Optional[
            Union[
                models.ConvertTextToSpeechParameters,
                models.ConvertTextToSpeechParametersTypedDict,
            ]
        ] = None,
        retries: OptionalNullable[utils.RetryConfig] = UNSET,
        server_url: Optional[str] = None,
        timeout_ms: Optional[int] = None,
        http_headers: Optional[Mapping[str, str]] = None,
    ) -> models.PredictDurationResponse:
        r"""Predict text-to-speech duration

        Predict the duration of text-to-speech conversion without generating audio

        :param voice_id:
        :param text: The text to convert to speech. Max length is 300 characters.
        :param language: Language code of the voice
        :param style: The style of character to use for the text-to-speech conversion
        :param model: The model type to use for the text-to-speech conversion
        :param output_format: The desired output format of the audio file (wav, mp3). Default is wav.
        :param voice_settings:
        :param retries: Override the default retry configuration for this method
        :param server_url: Override the default server URL for this method
        :param timeout_ms: Override the default request timeout configuration for this method in milliseconds
        :param http_headers: Additional headers to set or replace on requests.
        """
        base_url = None
        url_variables = None
        if timeout_ms is None:
            timeout_ms = self.sdk_configuration.timeout_ms

        if server_url is not None:
            base_url = server_url
        else:
            base_url = self._get_url(base_url, url_variables)

        request = models.PredictDurationRequest(
            voice_id=voice_id,
            predict_tts_duration_request=models.PredictTTSDurationRequest(
                text=text,
                language=language,
                style=style,
                model=model,
                output_format=output_format,
                voice_settings=utils.get_pydantic_model(
                    voice_settings, Optional[models.ConvertTextToSpeechParameters]
                ),
            ),
        )

        req = self._build_request_async(
            method="POST",
            path="/v1/predict-duration/{voice_id}",
            base_url=base_url,
            url_variables=url_variables,
            request=request,
            request_body_required=True,
            request_has_path_params=True,
            request_has_query_params=True,
            user_agent_header="user-agent",
            accept_header_value="application/json",
            http_headers=http_headers,
            security=self.sdk_configuration.security,
            get_serialized_body=lambda: utils.serialize_request_body(
                request.predict_tts_duration_request,
                False,
                False,
                "json",
                models.PredictTTSDurationRequest,
            ),
            timeout_ms=timeout_ms,
        )

        if retries == UNSET:
            if self.sdk_configuration.retry_config is not UNSET:
                retries = self.sdk_configuration.retry_config

        retry_config = None
        if isinstance(retries, utils.RetryConfig):
            retry_config = (retries, ["429", "500", "502", "503", "504"])

        http_res = await self.do_request_async(
            hook_ctx=HookContext(
                config=self.sdk_configuration,
                base_url=base_url or "",
                operation_id="predict_duration",
                oauth2_scopes=[],
                security_source=self.sdk_configuration.security,
            ),
            request=req,
            error_status_codes=[
                "400",
                "401",
                "402",
                "403",
                "404",
                "408",
                "429",
                "4XX",
                "500",
                "5XX",
            ],
            retry_config=retry_config,
        )

        response_data: Any = None
        if utils.match_response(http_res, "200", "application/json"):
            return unmarshal_json_response(models.PredictDurationResponse, http_res)
        if utils.match_response(http_res, "400", "application/json"):
            response_data = unmarshal_json_response(
                errors.BadRequestErrorResponseData, http_res
            )
            raise errors.BadRequestErrorResponse(response_data, http_res)
        if utils.match_response(http_res, "401", "application/json"):
            response_data = unmarshal_json_response(
                errors.UnauthorizedErrorResponseData, http_res
            )
            raise errors.UnauthorizedErrorResponse(response_data, http_res)
        if utils.match_response(http_res, "402", "application/json"):
            response_data = unmarshal_json_response(
                errors.PaymentRequiredErrorResponseData, http_res
            )
            raise errors.PaymentRequiredErrorResponse(response_data, http_res)
        if utils.match_response(http_res, "403", "application/json"):
            response_data = unmarshal_json_response(
                errors.ForbiddenErrorResponseData, http_res
            )
            raise errors.ForbiddenErrorResponse(response_data, http_res)
        if utils.match_response(http_res, "404", "application/json"):
            response_data = unmarshal_json_response(
                errors.NotFoundErrorResponseData, http_res
            )
            raise errors.NotFoundErrorResponse(response_data, http_res)
        if utils.match_response(http_res, "408", "application/json"):
            response_data = unmarshal_json_response(
                errors.RequestTimeoutErrorResponseData, http_res
            )
            raise errors.RequestTimeoutErrorResponse(response_data, http_res)
        if utils.match_response(http_res, "429", "application/json"):
            response_data = unmarshal_json_response(
                errors.TooManyRequestsErrorResponseData, http_res
            )
            raise errors.TooManyRequestsErrorResponse(response_data, http_res)
        if utils.match_response(http_res, "500", "application/json"):
            response_data = unmarshal_json_response(
                errors.InternalServerErrorResponseData, http_res
            )
            raise errors.InternalServerErrorResponse(response_data, http_res)
        if utils.match_response(http_res, "4XX", "*"):
            http_res_text = await utils.stream_to_text_async(http_res)
            raise errors.SupertoneDefaultError(
                "API error occurred", http_res, http_res_text
            )
        if utils.match_response(http_res, "5XX", "*"):
            http_res_text = await utils.stream_to_text_async(http_res)
            raise errors.SupertoneDefaultError(
                "API error occurred", http_res, http_res_text
            )

        raise errors.SupertoneDefaultError("Unexpected response received", http_res)
