From 42a49adb7f41d35dee996162352546019974ef6f Mon Sep 17 00:00:00 2001 From: fern-api <115122769+fern-api[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 12:51:54 +0000 Subject: [PATCH 1/3] SDK regeneration --- src/elevenlabs/__init__.py | 9 +- src/elevenlabs/core/client_wrapper.py | 4 +- src/elevenlabs/music/__init__.py | 13 +- src/elevenlabs/music/client.py | 108 ++++++++++ src/elevenlabs/music/raw_client.py | 192 ++++++++++++++++++ src/elevenlabs/music/types/__init__.py | 7 +- ..._compose_detailed_request_output_format.py | 28 +++ src/elevenlabs/types/__init__.py | 4 - ...tailed_response_v_1_music_detailed_post.py | 44 ---- src/elevenlabs/types/user.py | 7 +- 10 files changed, 359 insertions(+), 57 deletions(-) create mode 100644 src/elevenlabs/music/types/music_compose_detailed_request_output_format.py delete mode 100644 src/elevenlabs/types/body_compose_music_with_a_detailed_response_v_1_music_detailed_post.py diff --git a/src/elevenlabs/__init__.py b/src/elevenlabs/__init__.py index 107f3aec..d09a9e8a 100644 --- a/src/elevenlabs/__init__.py +++ b/src/elevenlabs/__init__.py @@ -56,7 +56,6 @@ BatchCallRecipientStatus, BatchCallResponse, BatchCallStatus, - BodyComposeMusicWithADetailedResponseV1MusicDetailedPost, BodyGenerateARandomVoiceV1VoiceGenerationGenerateVoicePostAge, BodyGenerateARandomVoiceV1VoiceGenerationGenerateVoicePostGender, BreakdownTypes, @@ -769,7 +768,11 @@ from .dubbing import DubbingListRequestDubbingStatus, DubbingListRequestFilterByCreator from .environment import ElevenLabsEnvironment from .history import HistoryListRequestSource -from .music import MusicComposeRequestOutputFormat, MusicStreamRequestOutputFormat +from .music import ( + MusicComposeDetailedRequestOutputFormat, + MusicComposeRequestOutputFormat, + MusicStreamRequestOutputFormat, +) from .play import play, save, stream from .pronunciation_dictionaries import ( BodyAddAPronunciationDictionaryV1PronunciationDictionariesAddFromRulesPostRulesItem, @@ -886,7 +889,6 @@ "BodyAddAPronunciationDictionaryV1PronunciationDictionariesAddFromRulesPostRulesItem_Alias", "BodyAddAPronunciationDictionaryV1PronunciationDictionariesAddFromRulesPostRulesItem_Phoneme", "BodyAddAPronunciationDictionaryV1PronunciationDictionariesAddFromRulesPostWorkspaceAccess", - "BodyComposeMusicWithADetailedResponseV1MusicDetailedPost", "BodyCreatePodcastV1StudioPodcastsPostDurationScale", "BodyCreatePodcastV1StudioPodcastsPostMode", "BodyCreatePodcastV1StudioPodcastsPostMode_Bulletin", @@ -1270,6 +1272,7 @@ "ModerationStatusResponseModelSafetyStatus", "ModerationStatusResponseModelWarningStatus", "MultichannelSpeechToTextResponseModel", + "MusicComposeDetailedRequestOutputFormat", "MusicComposeRequestOutputFormat", "MusicPrompt", "MusicStreamRequestOutputFormat", diff --git a/src/elevenlabs/core/client_wrapper.py b/src/elevenlabs/core/client_wrapper.py index 688f111c..76f445e9 100644 --- a/src/elevenlabs/core/client_wrapper.py +++ b/src/elevenlabs/core/client_wrapper.py @@ -14,10 +14,10 @@ def __init__(self, *, api_key: typing.Optional[str] = None, base_url: str, timeo def get_headers(self) -> typing.Dict[str, str]: headers: typing.Dict[str, str] = { - "User-Agent": "elevenlabs/v2.10.0", + "User-Agent": "elevenlabs/v2.11.0", "X-Fern-Language": "Python", "X-Fern-SDK-Name": "elevenlabs", - "X-Fern-SDK-Version": "v2.10.0", + "X-Fern-SDK-Version": "v2.11.0", } if self._api_key is not None: headers["xi-api-key"] = self._api_key diff --git a/src/elevenlabs/music/__init__.py b/src/elevenlabs/music/__init__.py index 10111260..28d8ce0e 100644 --- a/src/elevenlabs/music/__init__.py +++ b/src/elevenlabs/music/__init__.py @@ -2,7 +2,16 @@ # isort: skip_file -from .types import MusicComposeRequestOutputFormat, MusicStreamRequestOutputFormat +from .types import ( + MusicComposeDetailedRequestOutputFormat, + MusicComposeRequestOutputFormat, + MusicStreamRequestOutputFormat, +) from . import composition_plan -__all__ = ["MusicComposeRequestOutputFormat", "MusicStreamRequestOutputFormat", "composition_plan"] +__all__ = [ + "MusicComposeDetailedRequestOutputFormat", + "MusicComposeRequestOutputFormat", + "MusicStreamRequestOutputFormat", + "composition_plan", +] diff --git a/src/elevenlabs/music/client.py b/src/elevenlabs/music/client.py index 984922a4..975d1ab5 100644 --- a/src/elevenlabs/music/client.py +++ b/src/elevenlabs/music/client.py @@ -7,6 +7,7 @@ from ..types.music_prompt import MusicPrompt from .composition_plan.client import AsyncCompositionPlanClient, CompositionPlanClient from .raw_client import AsyncRawMusicClient, RawMusicClient +from .types.music_compose_detailed_request_output_format import MusicComposeDetailedRequestOutputFormat from .types.music_compose_request_output_format import MusicComposeRequestOutputFormat from .types.music_stream_request_output_format import MusicStreamRequestOutputFormat @@ -83,6 +84,59 @@ def compose( ) as r: yield from r.data + def compose_detailed( + self, + *, + output_format: typing.Optional[MusicComposeDetailedRequestOutputFormat] = None, + prompt: typing.Optional[str] = OMIT, + music_prompt: typing.Optional[MusicPrompt] = OMIT, + composition_plan: typing.Optional[MusicPrompt] = OMIT, + music_length_ms: typing.Optional[int] = OMIT, + model_id: typing.Optional[typing.Literal["music_v1"]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Iterator[bytes]: + """ + Compose a song from a prompt or a composition plan. + + Parameters + ---------- + output_format : typing.Optional[MusicComposeDetailedRequestOutputFormat] + Output format of the generated audio. Formatted as codec_sample_rate_bitrate. So an mp3 with 22.05kHz sample rate at 32kbs is represented as mp3_22050_32. MP3 with 192kbps bitrate requires you to be subscribed to Creator tier or above. PCM with 44.1kHz sample rate requires you to be subscribed to Pro tier or above. Note that the μ-law format (sometimes written mu-law, often approximated as u-law) is commonly used for Twilio audio inputs. + + prompt : typing.Optional[str] + A simple text prompt to generate a song from. Cannot be used in conjunction with `composition_plan`. + + music_prompt : typing.Optional[MusicPrompt] + A music prompt. Deprecated. Use `composition_plan` instead. + + composition_plan : typing.Optional[MusicPrompt] + A detailed composition plan to guide music generation. Cannot be used in conjunction with `prompt`. + + music_length_ms : typing.Optional[int] + The length of the song to generate in milliseconds. Used only in conjunction with `prompt`. Must be between 10000ms and 300000ms. Optional - if not provided, the model will choose a length based on the prompt. + + model_id : typing.Optional[typing.Literal["music_v1"]] + The model to use for the generation. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. + + Returns + ------- + typing.Iterator[bytes] + Successful Response + """ + with self._raw_client.compose_detailed( + output_format=output_format, + prompt=prompt, + music_prompt=music_prompt, + composition_plan=composition_plan, + music_length_ms=music_length_ms, + model_id=model_id, + request_options=request_options, + ) as r: + yield from r.data + def stream( self, *, @@ -207,6 +261,60 @@ async def compose( async for _chunk in r.data: yield _chunk + async def compose_detailed( + self, + *, + output_format: typing.Optional[MusicComposeDetailedRequestOutputFormat] = None, + prompt: typing.Optional[str] = OMIT, + music_prompt: typing.Optional[MusicPrompt] = OMIT, + composition_plan: typing.Optional[MusicPrompt] = OMIT, + music_length_ms: typing.Optional[int] = OMIT, + model_id: typing.Optional[typing.Literal["music_v1"]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.AsyncIterator[bytes]: + """ + Compose a song from a prompt or a composition plan. + + Parameters + ---------- + output_format : typing.Optional[MusicComposeDetailedRequestOutputFormat] + Output format of the generated audio. Formatted as codec_sample_rate_bitrate. So an mp3 with 22.05kHz sample rate at 32kbs is represented as mp3_22050_32. MP3 with 192kbps bitrate requires you to be subscribed to Creator tier or above. PCM with 44.1kHz sample rate requires you to be subscribed to Pro tier or above. Note that the μ-law format (sometimes written mu-law, often approximated as u-law) is commonly used for Twilio audio inputs. + + prompt : typing.Optional[str] + A simple text prompt to generate a song from. Cannot be used in conjunction with `composition_plan`. + + music_prompt : typing.Optional[MusicPrompt] + A music prompt. Deprecated. Use `composition_plan` instead. + + composition_plan : typing.Optional[MusicPrompt] + A detailed composition plan to guide music generation. Cannot be used in conjunction with `prompt`. + + music_length_ms : typing.Optional[int] + The length of the song to generate in milliseconds. Used only in conjunction with `prompt`. Must be between 10000ms and 300000ms. Optional - if not provided, the model will choose a length based on the prompt. + + model_id : typing.Optional[typing.Literal["music_v1"]] + The model to use for the generation. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. + + Returns + ------- + typing.AsyncIterator[bytes] + Successful Response + """ + async with self._raw_client.compose_detailed( + output_format=output_format, + prompt=prompt, + music_prompt=music_prompt, + composition_plan=composition_plan, + music_length_ms=music_length_ms, + model_id=model_id, + request_options=request_options, + ) as r: + async for _chunk in r.data: + yield _chunk + async def stream( self, *, diff --git a/src/elevenlabs/music/raw_client.py b/src/elevenlabs/music/raw_client.py index eb51bb10..0a0c38b4 100644 --- a/src/elevenlabs/music/raw_client.py +++ b/src/elevenlabs/music/raw_client.py @@ -13,6 +13,7 @@ from ..errors.unprocessable_entity_error import UnprocessableEntityError from ..types.http_validation_error import HttpValidationError from ..types.music_prompt import MusicPrompt +from .types.music_compose_detailed_request_output_format import MusicComposeDetailedRequestOutputFormat from .types.music_compose_request_output_format import MusicComposeRequestOutputFormat from .types.music_stream_request_output_format import MusicStreamRequestOutputFormat @@ -119,6 +120,101 @@ def _stream() -> HttpResponse[typing.Iterator[bytes]]: yield _stream() + @contextlib.contextmanager + def compose_detailed( + self, + *, + output_format: typing.Optional[MusicComposeDetailedRequestOutputFormat] = None, + prompt: typing.Optional[str] = OMIT, + music_prompt: typing.Optional[MusicPrompt] = OMIT, + composition_plan: typing.Optional[MusicPrompt] = OMIT, + music_length_ms: typing.Optional[int] = OMIT, + model_id: typing.Optional[typing.Literal["music_v1"]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Iterator[HttpResponse[typing.Iterator[bytes]]]: + """ + Compose a song from a prompt or a composition plan. + + Parameters + ---------- + output_format : typing.Optional[MusicComposeDetailedRequestOutputFormat] + Output format of the generated audio. Formatted as codec_sample_rate_bitrate. So an mp3 with 22.05kHz sample rate at 32kbs is represented as mp3_22050_32. MP3 with 192kbps bitrate requires you to be subscribed to Creator tier or above. PCM with 44.1kHz sample rate requires you to be subscribed to Pro tier or above. Note that the μ-law format (sometimes written mu-law, often approximated as u-law) is commonly used for Twilio audio inputs. + + prompt : typing.Optional[str] + A simple text prompt to generate a song from. Cannot be used in conjunction with `composition_plan`. + + music_prompt : typing.Optional[MusicPrompt] + A music prompt. Deprecated. Use `composition_plan` instead. + + composition_plan : typing.Optional[MusicPrompt] + A detailed composition plan to guide music generation. Cannot be used in conjunction with `prompt`. + + music_length_ms : typing.Optional[int] + The length of the song to generate in milliseconds. Used only in conjunction with `prompt`. Must be between 10000ms and 300000ms. Optional - if not provided, the model will choose a length based on the prompt. + + model_id : typing.Optional[typing.Literal["music_v1"]] + The model to use for the generation. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. + + Returns + ------- + typing.Iterator[HttpResponse[typing.Iterator[bytes]]] + Successful Response + """ + with self._client_wrapper.httpx_client.stream( + "v1/music/detailed", + method="POST", + params={ + "output_format": output_format, + }, + json={ + "prompt": prompt, + "music_prompt": convert_and_respect_annotation_metadata( + object_=music_prompt, annotation=MusicPrompt, direction="write" + ), + "composition_plan": convert_and_respect_annotation_metadata( + object_=composition_plan, annotation=MusicPrompt, direction="write" + ), + "music_length_ms": music_length_ms, + "model_id": model_id, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) as _response: + + def _stream() -> HttpResponse[typing.Iterator[bytes]]: + try: + if 200 <= _response.status_code < 300: + _chunk_size = request_options.get("chunk_size", 1024) if request_options is not None else 1024 + return HttpResponse( + response=_response, data=(_chunk for _chunk in _response.iter_bytes(chunk_size=_chunk_size)) + ) + _response.read() + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + HttpValidationError, + construct_type( + type_=HttpValidationError, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.text + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + yield _stream() + @contextlib.contextmanager def stream( self, @@ -315,6 +411,102 @@ async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[bytes]]: yield await _stream() + @contextlib.asynccontextmanager + async def compose_detailed( + self, + *, + output_format: typing.Optional[MusicComposeDetailedRequestOutputFormat] = None, + prompt: typing.Optional[str] = OMIT, + music_prompt: typing.Optional[MusicPrompt] = OMIT, + composition_plan: typing.Optional[MusicPrompt] = OMIT, + music_length_ms: typing.Optional[int] = OMIT, + model_id: typing.Optional[typing.Literal["music_v1"]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.AsyncIterator[AsyncHttpResponse[typing.AsyncIterator[bytes]]]: + """ + Compose a song from a prompt or a composition plan. + + Parameters + ---------- + output_format : typing.Optional[MusicComposeDetailedRequestOutputFormat] + Output format of the generated audio. Formatted as codec_sample_rate_bitrate. So an mp3 with 22.05kHz sample rate at 32kbs is represented as mp3_22050_32. MP3 with 192kbps bitrate requires you to be subscribed to Creator tier or above. PCM with 44.1kHz sample rate requires you to be subscribed to Pro tier or above. Note that the μ-law format (sometimes written mu-law, often approximated as u-law) is commonly used for Twilio audio inputs. + + prompt : typing.Optional[str] + A simple text prompt to generate a song from. Cannot be used in conjunction with `composition_plan`. + + music_prompt : typing.Optional[MusicPrompt] + A music prompt. Deprecated. Use `composition_plan` instead. + + composition_plan : typing.Optional[MusicPrompt] + A detailed composition plan to guide music generation. Cannot be used in conjunction with `prompt`. + + music_length_ms : typing.Optional[int] + The length of the song to generate in milliseconds. Used only in conjunction with `prompt`. Must be between 10000ms and 300000ms. Optional - if not provided, the model will choose a length based on the prompt. + + model_id : typing.Optional[typing.Literal["music_v1"]] + The model to use for the generation. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. + + Returns + ------- + typing.AsyncIterator[AsyncHttpResponse[typing.AsyncIterator[bytes]]] + Successful Response + """ + async with self._client_wrapper.httpx_client.stream( + "v1/music/detailed", + method="POST", + params={ + "output_format": output_format, + }, + json={ + "prompt": prompt, + "music_prompt": convert_and_respect_annotation_metadata( + object_=music_prompt, annotation=MusicPrompt, direction="write" + ), + "composition_plan": convert_and_respect_annotation_metadata( + object_=composition_plan, annotation=MusicPrompt, direction="write" + ), + "music_length_ms": music_length_ms, + "model_id": model_id, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) as _response: + + async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[bytes]]: + try: + if 200 <= _response.status_code < 300: + _chunk_size = request_options.get("chunk_size", 1024) if request_options is not None else 1024 + return AsyncHttpResponse( + response=_response, + data=(_chunk async for _chunk in _response.aiter_bytes(chunk_size=_chunk_size)), + ) + await _response.aread() + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + HttpValidationError, + construct_type( + type_=HttpValidationError, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.text + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + yield await _stream() + @contextlib.asynccontextmanager async def stream( self, diff --git a/src/elevenlabs/music/types/__init__.py b/src/elevenlabs/music/types/__init__.py index b282372e..0ee5210c 100644 --- a/src/elevenlabs/music/types/__init__.py +++ b/src/elevenlabs/music/types/__init__.py @@ -2,7 +2,12 @@ # isort: skip_file +from .music_compose_detailed_request_output_format import MusicComposeDetailedRequestOutputFormat from .music_compose_request_output_format import MusicComposeRequestOutputFormat from .music_stream_request_output_format import MusicStreamRequestOutputFormat -__all__ = ["MusicComposeRequestOutputFormat", "MusicStreamRequestOutputFormat"] +__all__ = [ + "MusicComposeDetailedRequestOutputFormat", + "MusicComposeRequestOutputFormat", + "MusicStreamRequestOutputFormat", +] diff --git a/src/elevenlabs/music/types/music_compose_detailed_request_output_format.py b/src/elevenlabs/music/types/music_compose_detailed_request_output_format.py new file mode 100644 index 00000000..cbf22684 --- /dev/null +++ b/src/elevenlabs/music/types/music_compose_detailed_request_output_format.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +MusicComposeDetailedRequestOutputFormat = typing.Union[ + typing.Literal[ + "mp3_22050_32", + "mp3_44100_32", + "mp3_44100_64", + "mp3_44100_96", + "mp3_44100_128", + "mp3_44100_192", + "pcm_8000", + "pcm_16000", + "pcm_22050", + "pcm_24000", + "pcm_44100", + "pcm_48000", + "ulaw_8000", + "alaw_8000", + "opus_48000_32", + "opus_48000_64", + "opus_48000_96", + "opus_48000_128", + "opus_48000_192", + ], + typing.Any, +] diff --git a/src/elevenlabs/types/__init__.py b/src/elevenlabs/types/__init__.py index 7ed686b9..0d348598 100644 --- a/src/elevenlabs/types/__init__.py +++ b/src/elevenlabs/types/__init__.py @@ -57,9 +57,6 @@ from .batch_call_recipient_status import BatchCallRecipientStatus from .batch_call_response import BatchCallResponse from .batch_call_status import BatchCallStatus -from .body_compose_music_with_a_detailed_response_v_1_music_detailed_post import ( - BodyComposeMusicWithADetailedResponseV1MusicDetailedPost, -) from .body_generate_a_random_voice_v_1_voice_generation_generate_voice_post_age import ( BodyGenerateARandomVoiceV1VoiceGenerationGenerateVoicePostAge, ) @@ -899,7 +896,6 @@ "BatchCallRecipientStatus", "BatchCallResponse", "BatchCallStatus", - "BodyComposeMusicWithADetailedResponseV1MusicDetailedPost", "BodyGenerateARandomVoiceV1VoiceGenerationGenerateVoicePostAge", "BodyGenerateARandomVoiceV1VoiceGenerationGenerateVoicePostGender", "BreakdownTypes", diff --git a/src/elevenlabs/types/body_compose_music_with_a_detailed_response_v_1_music_detailed_post.py b/src/elevenlabs/types/body_compose_music_with_a_detailed_response_v_1_music_detailed_post.py deleted file mode 100644 index 67bef216..00000000 --- a/src/elevenlabs/types/body_compose_music_with_a_detailed_response_v_1_music_detailed_post.py +++ /dev/null @@ -1,44 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2 -from ..core.unchecked_base_model import UncheckedBaseModel -from .music_prompt import MusicPrompt - - -class BodyComposeMusicWithADetailedResponseV1MusicDetailedPost(UncheckedBaseModel): - prompt: typing.Optional[str] = pydantic.Field(default=None) - """ - A simple text prompt to generate a song from. Cannot be used in conjunction with `composition_plan`. - """ - - music_prompt: typing.Optional[MusicPrompt] = pydantic.Field(default=None) - """ - A music prompt. Deprecated. Use `composition_plan` instead. - """ - - composition_plan: typing.Optional[MusicPrompt] = pydantic.Field(default=None) - """ - A detailed composition plan to guide music generation. Cannot be used in conjunction with `prompt`. - """ - - music_length_ms: typing.Optional[int] = pydantic.Field(default=None) - """ - The length of the song to generate in milliseconds. Used only in conjunction with `prompt`. Must be between 10000ms and 300000ms. Optional - if not provided, the model will choose a length based on the prompt. - """ - - model_id: typing.Optional[typing.Literal["music_v1"]] = pydantic.Field(default=None) - """ - The model to use for the generation. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/elevenlabs/types/user.py b/src/elevenlabs/types/user.py index 857d0126..c2080846 100644 --- a/src/elevenlabs/types/user.py +++ b/src/elevenlabs/types/user.py @@ -27,7 +27,7 @@ class User(UncheckedBaseModel): is_new_user: bool = pydantic.Field() """ - Whether the user is new. + Whether the user is new. This field is deprecated and will be removed in the future. Use 'created_at' instead. """ xi_api_key: typing.Optional[str] = pydantic.Field(default=None) @@ -75,6 +75,11 @@ class User(UncheckedBaseModel): The Partnerstack partner default link of the user. """ + created_at: int = pydantic.Field() + """ + The unix timestamp of the user's creation. 0 if the user was created before the unix timestamp was added. + """ + if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 else: From 21375cc331f977c8cd85f90cffdf8c61ed32c196 Mon Sep 17 00:00:00 2001 From: Paul Asjes Date: Wed, 20 Aug 2025 16:30:10 +0200 Subject: [PATCH 2/3] compose_detailed parsing --- src/elevenlabs/client.py | 3 + src/elevenlabs/music_custom.py | 286 +++++++++++++++++++++++++++++++++ 2 files changed, 289 insertions(+) create mode 100644 src/elevenlabs/music_custom.py diff --git a/src/elevenlabs/client.py b/src/elevenlabs/client.py index 00b05309..a1c37c9e 100644 --- a/src/elevenlabs/client.py +++ b/src/elevenlabs/client.py @@ -8,6 +8,7 @@ from .environment import ElevenLabsEnvironment from .realtime_tts import RealtimeTextToSpeechClient from .webhooks_custom import WebhooksClient, AsyncWebhooksClient +from .music_custom import MusicClient, AsyncMusicClient # this is used as the default value for optional parameters @@ -59,6 +60,7 @@ def __init__( ) self.text_to_speech = RealtimeTextToSpeechClient(client_wrapper=self._client_wrapper) self.webhooks = WebhooksClient(client_wrapper=self._client_wrapper) + self.music = MusicClient(client_wrapper=self._client_wrapper) class AsyncElevenLabs(AsyncBaseElevenLabs): @@ -102,3 +104,4 @@ def __init__( httpx_client=httpx_client ) self.webhooks = AsyncWebhooksClient(client_wrapper=self._client_wrapper) + self.music = AsyncMusicClient(client_wrapper=self._client_wrapper) diff --git a/src/elevenlabs/music_custom.py b/src/elevenlabs/music_custom.py new file mode 100644 index 00000000..1929f9de --- /dev/null +++ b/src/elevenlabs/music_custom.py @@ -0,0 +1,286 @@ +import typing +import json +import re +from dataclasses import dataclass + +from elevenlabs.music.client import MusicClient as AutogeneratedMusicClient, AsyncMusicClient as AutogeneratedAsyncMusicClient +from elevenlabs.types.music_prompt import MusicPrompt +from elevenlabs.music.types.music_compose_detailed_request_output_format import MusicComposeDetailedRequestOutputFormat +from elevenlabs.core.request_options import RequestOptions + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +@dataclass +class SongMetadata: + title: str + description: str + genres: typing.List[str] + languages: typing.List[str] + is_explicit: bool + + +@dataclass +class MultipartResponse: + json: typing.Dict[str, typing.Any] # Contains compositionPlan and songMetadata + audio: bytes + filename: str + + +class MusicClient(AutogeneratedMusicClient): + """ + A client to handle ElevenLabs music-related functionality + Extends the autogenerated client to include custom music methods + """ + + def compose_detailed( + self, + *, + output_format: typing.Optional[MusicComposeDetailedRequestOutputFormat] = None, + prompt: typing.Optional[str] = OMIT, + music_prompt: typing.Optional[MusicPrompt] = OMIT, + composition_plan: typing.Optional[MusicPrompt] = OMIT, + music_length_ms: typing.Optional[int] = OMIT, + model_id: typing.Optional[typing.Literal["music_v1"]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> MultipartResponse: + """ + Compose a song from a prompt or a composition plan with detailed response parsing. + This method calls the original compose_detailed and then parses the stream response. + + Returns a MultipartResponse containing parsed JSON metadata, audio bytes, and filename. + """ + # Call the parent method to get the stream + stream = super().compose_detailed( + output_format=output_format, + prompt=prompt, + music_prompt=music_prompt, + composition_plan=composition_plan, + music_length_ms=music_length_ms, + model_id=model_id, + request_options=request_options, + ) + + # Parse the stream using the parsing method + return self._parse_multipart(stream) + + def _parse_multipart(self, stream: typing.Iterator[bytes]) -> MultipartResponse: + """ + Reads a byte stream containing multipart data and parses it into JSON and audio parts. + + Args: + stream: Iterator of bytes from ElevenLabs music API response + + Returns: + MultipartResponse containing parsed JSON metadata, audio bytes, and filename + """ + # Collect all chunks into a single bytes object + chunks = [] + for chunk in stream: + chunks.append(chunk) + + # Combine all chunks into a single buffer + response_bytes = b''.join(chunks) + + # Parse the multipart content + response_text = response_bytes.decode('utf-8', errors='ignore') + lines = response_text.split('\n') + + if not lines: + raise ValueError("Empty response from music API") + + boundary = lines[0].strip() + + # Find the JSON part (should be early in the response) + json_data = None + filename = 'generated_music.mp3' + + # Parse JSON from the text representation + for i in range(min(10, len(lines))): + if 'Content-Type: application/json' in lines[i] and i + 2 < len(lines): + json_line = lines[i + 2] + if json_line.strip() and json_line.startswith('{'): + try: + json_data = json.loads(json_line) + print('✓ Successfully parsed JSON metadata') + except json.JSONDecodeError as e: + print(f'Failed to parse JSON: {e}') + break + + # Extract filename from headers + for i in range(min(20, len(lines))): + if 'filename=' in lines[i]: + match = re.search(r'filename="([^"]+)"', lines[i]) + if match: + filename = match.group(1) + break + + # Find where the audio data starts (after the second boundary and headers) + boundary_bytes = boundary.encode('utf-8') + first_boundary = -1 + second_boundary = -1 + + for i in range(len(response_bytes) - len(boundary_bytes) + 1): + if response_bytes[i:i + len(boundary_bytes)] == boundary_bytes: + if first_boundary == -1: + first_boundary = i + elif second_boundary == -1: + second_boundary = i + break + + if second_boundary == -1: + raise ValueError('Could not find audio part boundary') + + # Find the start of audio data (after headers and empty line) + audio_start = second_boundary + len(boundary_bytes) + + # Skip past the headers to find the empty line (\n\n) + while audio_start < len(response_bytes) - 1: + if (response_bytes[audio_start] == 0x0A and + response_bytes[audio_start + 1] == 0x0A): + # Found \n\n - audio starts after this + audio_start += 2 + break + audio_start += 1 + + # Audio goes until the end (or until we find another boundary) + audio_buffer = response_bytes[audio_start:] + + if not json_data: + raise ValueError('Could not parse JSON data') + + return MultipartResponse( + json=json_data, + audio=audio_buffer, + filename=filename + ) + + +class AsyncMusicClient(AutogeneratedAsyncMusicClient): + """ + An async client to handle ElevenLabs music-related functionality + Extends the autogenerated async client to include custom music methods + """ + + async def compose_detailed( + self, + *, + output_format: typing.Optional[MusicComposeDetailedRequestOutputFormat] = None, + prompt: typing.Optional[str] = OMIT, + music_prompt: typing.Optional[MusicPrompt] = OMIT, + composition_plan: typing.Optional[MusicPrompt] = OMIT, + music_length_ms: typing.Optional[int] = OMIT, + model_id: typing.Optional[typing.Literal["music_v1"]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> MultipartResponse: + """ + Compose a song from a prompt or a composition plan with detailed response parsing. + This method calls the original compose_detailed and then parses the stream response. + + Returns a MultipartResponse containing parsed JSON metadata, audio bytes, and filename. + """ + # Call the parent method to get the stream + stream = super().compose_detailed( + output_format=output_format, + prompt=prompt, + music_prompt=music_prompt, + composition_plan=composition_plan, + music_length_ms=music_length_ms, + model_id=model_id, + request_options=request_options, + ) + + # Parse the stream using the parsing method + return await self._parse_multipart_async(stream) + + async def _parse_multipart_async(self, stream: typing.AsyncIterator[bytes]) -> MultipartResponse: + """ + Reads an async byte stream containing multipart data and parses it into JSON and audio parts. + + Args: + stream: AsyncIterator of bytes from ElevenLabs music API response + + Returns: + MultipartResponse containing parsed JSON metadata, audio bytes, and filename + """ + # Collect all chunks into a single bytes object + chunks = [] + async for chunk in stream: + chunks.append(chunk) + + # Combine all chunks into a single buffer + response_bytes = b''.join(chunks) + + # Parse the multipart content + response_text = response_bytes.decode('utf-8', errors='ignore') + lines = response_text.split('\n') + + if not lines: + raise ValueError("Empty response from music API") + + boundary = lines[0].strip() + + # Find the JSON part (should be early in the response) + json_data = None + filename = 'generated_music.mp3' + + # Parse JSON from the text representation + for i in range(min(10, len(lines))): + if 'Content-Type: application/json' in lines[i] and i + 2 < len(lines): + json_line = lines[i + 2] + if json_line.strip() and json_line.startswith('{'): + try: + json_data = json.loads(json_line) + print('✓ Successfully parsed JSON metadata') + except json.JSONDecodeError as e: + print(f'Failed to parse JSON: {e}') + break + + # Extract filename from headers + for i in range(min(20, len(lines))): + if 'filename=' in lines[i]: + match = re.search(r'filename="([^"]+)"', lines[i]) + if match: + filename = match.group(1) + break + + # Find where the audio data starts (after the second boundary and headers) + boundary_bytes = boundary.encode('utf-8') + first_boundary = -1 + second_boundary = -1 + + for i in range(len(response_bytes) - len(boundary_bytes) + 1): + if response_bytes[i:i + len(boundary_bytes)] == boundary_bytes: + if first_boundary == -1: + first_boundary = i + elif second_boundary == -1: + second_boundary = i + break + + if second_boundary == -1: + raise ValueError('Could not find audio part boundary') + + # Find the start of audio data (after headers and empty line) + audio_start = second_boundary + len(boundary_bytes) + + # Skip past the headers to find the empty line (\n\n) + while audio_start < len(response_bytes) - 1: + if (response_bytes[audio_start] == 0x0A and + response_bytes[audio_start + 1] == 0x0A): + # Found \n\n - audio starts after this + audio_start += 2 + break + audio_start += 1 + + # Audio goes until the end (or until we find another boundary) + audio_buffer = response_bytes[audio_start:] + + if not json_data: + raise ValueError('Could not parse JSON data') + + return MultipartResponse( + json=json_data, + audio=audio_buffer, + filename=filename + ) \ No newline at end of file From 0c7dc132dcba5b2d88c64436b1d3d397144c22bd Mon Sep 17 00:00:00 2001 From: Paul Asjes Date: Wed, 20 Aug 2025 16:33:17 +0200 Subject: [PATCH 3/3] ignore override --- src/elevenlabs/music_custom.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/elevenlabs/music_custom.py b/src/elevenlabs/music_custom.py index 1929f9de..ae199183 100644 --- a/src/elevenlabs/music_custom.py +++ b/src/elevenlabs/music_custom.py @@ -34,7 +34,7 @@ class MusicClient(AutogeneratedMusicClient): Extends the autogenerated client to include custom music methods """ - def compose_detailed( + def compose_detailed( # type: ignore[override] self, *, output_format: typing.Optional[MusicComposeDetailedRequestOutputFormat] = None, @@ -163,7 +163,7 @@ class AsyncMusicClient(AutogeneratedAsyncMusicClient): Extends the autogenerated async client to include custom music methods """ - async def compose_detailed( + async def compose_detailed( # type: ignore[override] self, *, output_format: typing.Optional[MusicComposeDetailedRequestOutputFormat] = None,