From 839c38bc95d9147b140481907621942c5c503ead Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:10:39 +0000 Subject: [PATCH 1/4] fix(client): preserve hardcoded query params when merging with user params --- src/runwayml/_base_client.py | 4 +++ tests/test_client.py | 48 ++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/runwayml/_base_client.py b/src/runwayml/_base_client.py index 5845965..4a464b2 100644 --- a/src/runwayml/_base_client.py +++ b/src/runwayml/_base_client.py @@ -540,6 +540,10 @@ def _build_request( files = cast(HttpxRequestFiles, ForceMultipartDict()) prepared_url = self._prepare_url(options.url) + # preserve hard-coded query params from the url + if params and prepared_url.query: + params = {**dict(prepared_url.params.items()), **params} + prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0]) if "_" in prepared_url.host: # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} diff --git a/tests/test_client.py b/tests/test_client.py index cff614b..f755671 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -429,6 +429,30 @@ def test_default_query_option(self) -> None: client.close() + def test_hardcoded_query_params_in_url(self, client: RunwayML) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: RunwayML) -> None: request = client._build_request( FinalRequestOptions( @@ -1340,6 +1364,30 @@ async def test_default_query_option(self) -> None: await client.close() + async def test_hardcoded_query_params_in_url(self, async_client: AsyncRunwayML) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: RunwayML) -> None: request = client._build_request( FinalRequestOptions( From 75b6deda8c5733115af29c9374ae10f3d4f4716a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 09:16:11 +0000 Subject: [PATCH 2/4] feat(api): add avatar_videos resource --- .stats.yml | 8 +- api.md | 12 + src/runwayml/_client.py | 38 ++++ src/runwayml/resources/__init__.py | 42 ++++ src/runwayml/resources/avatar_videos.py | 205 ++++++++++++++++++ src/runwayml/resources/voices.py | 16 +- src/runwayml/types/__init__.py | 2 + .../types/avatar_video_create_params.py | 153 +++++++++++++ .../types/avatar_video_create_response.py | 13 ++ src/runwayml/types/voice_create_params.py | 26 ++- src/runwayml/types/voice_preview_params.py | 8 +- tests/api_resources/test_avatar_videos.py | 164 ++++++++++++++ tests/api_resources/test_voices.py | 28 +-- 13 files changed, 683 insertions(+), 32 deletions(-) create mode 100644 src/runwayml/resources/avatar_videos.py create mode 100644 src/runwayml/types/avatar_video_create_params.py create mode 100644 src/runwayml/types/avatar_video_create_response.py create mode 100644 tests/api_resources/test_avatar_videos.py diff --git a/.stats.yml b/.stats.yml index e1f2c3e..4a5de71 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 36 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runwayml%2Frunwayml-8efb262fec205b8ab10f02929369aa1eb1ae72fba3cc8037552f439ab63c1f2d.yml -openapi_spec_hash: 265ec44f88060288ab3dc2142eb29585 -config_hash: 8e05a8613b4c0e602d485566da4e5264 +configured_endpoints: 37 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runwayml%2Frunwayml-ee023fb84f0e74914e23b019b4c0951108b7ff83b983563c91ae07986524b674.yml +openapi_spec_hash: 2472895eb74737ecef2cbee707e50cac +config_hash: 3063a17ad98d447287f6f7eab9d6d1d6 diff --git a/api.md b/api.md index 0697df7..14ba311 100644 --- a/api.md +++ b/api.md @@ -165,6 +165,18 @@ Methods: - client.avatars.list(\*\*params) -> SyncCursorPage[AvatarListResponse] - client.avatars.delete(id) -> None +# AvatarVideos + +Types: + +```python +from runwayml.types import AvatarVideoCreateResponse +``` + +Methods: + +- client.avatar_videos.create(\*\*params) -> AvatarVideoCreateResponse + # Documents Types: diff --git a/src/runwayml/_client.py b/src/runwayml/_client.py index 898ef6e..3bc29b0 100644 --- a/src/runwayml/_client.py +++ b/src/runwayml/_client.py @@ -40,6 +40,7 @@ workflows, organization, sound_effect, + avatar_videos, text_to_image, text_to_video, voice_dubbing, @@ -60,6 +61,7 @@ from .resources.workflows import WorkflowsResource, AsyncWorkflowsResource from .resources.organization import OrganizationResource, AsyncOrganizationResource from .resources.sound_effect import SoundEffectResource, AsyncSoundEffectResource + from .resources.avatar_videos import AvatarVideosResource, AsyncAvatarVideosResource from .resources.text_to_image import TextToImageResource, AsyncTextToImageResource from .resources.text_to_video import TextToVideoResource, AsyncTextToVideoResource from .resources.voice_dubbing import VoiceDubbingResource, AsyncVoiceDubbingResource @@ -240,6 +242,12 @@ def avatars(self) -> AvatarsResource: return AvatarsResource(self) + @cached_property + def avatar_videos(self) -> AvatarVideosResource: + from .resources.avatar_videos import AvatarVideosResource + + return AvatarVideosResource(self) + @cached_property def documents(self) -> DocumentsResource: from .resources.documents import DocumentsResource @@ -542,6 +550,12 @@ def avatars(self) -> AsyncAvatarsResource: return AsyncAvatarsResource(self) + @cached_property + def avatar_videos(self) -> AsyncAvatarVideosResource: + from .resources.avatar_videos import AsyncAvatarVideosResource + + return AsyncAvatarVideosResource(self) + @cached_property def documents(self) -> AsyncDocumentsResource: from .resources.documents import AsyncDocumentsResource @@ -789,6 +803,12 @@ def avatars(self) -> avatars.AvatarsResourceWithRawResponse: return AvatarsResourceWithRawResponse(self._client.avatars) + @cached_property + def avatar_videos(self) -> avatar_videos.AvatarVideosResourceWithRawResponse: + from .resources.avatar_videos import AvatarVideosResourceWithRawResponse + + return AvatarVideosResourceWithRawResponse(self._client.avatar_videos) + @cached_property def documents(self) -> documents.DocumentsResourceWithRawResponse: from .resources.documents import DocumentsResourceWithRawResponse @@ -921,6 +941,12 @@ def avatars(self) -> avatars.AsyncAvatarsResourceWithRawResponse: return AsyncAvatarsResourceWithRawResponse(self._client.avatars) + @cached_property + def avatar_videos(self) -> avatar_videos.AsyncAvatarVideosResourceWithRawResponse: + from .resources.avatar_videos import AsyncAvatarVideosResourceWithRawResponse + + return AsyncAvatarVideosResourceWithRawResponse(self._client.avatar_videos) + @cached_property def documents(self) -> documents.AsyncDocumentsResourceWithRawResponse: from .resources.documents import AsyncDocumentsResourceWithRawResponse @@ -1053,6 +1079,12 @@ def avatars(self) -> avatars.AvatarsResourceWithStreamingResponse: return AvatarsResourceWithStreamingResponse(self._client.avatars) + @cached_property + def avatar_videos(self) -> avatar_videos.AvatarVideosResourceWithStreamingResponse: + from .resources.avatar_videos import AvatarVideosResourceWithStreamingResponse + + return AvatarVideosResourceWithStreamingResponse(self._client.avatar_videos) + @cached_property def documents(self) -> documents.DocumentsResourceWithStreamingResponse: from .resources.documents import DocumentsResourceWithStreamingResponse @@ -1185,6 +1217,12 @@ def avatars(self) -> avatars.AsyncAvatarsResourceWithStreamingResponse: return AsyncAvatarsResourceWithStreamingResponse(self._client.avatars) + @cached_property + def avatar_videos(self) -> avatar_videos.AsyncAvatarVideosResourceWithStreamingResponse: + from .resources.avatar_videos import AsyncAvatarVideosResourceWithStreamingResponse + + return AsyncAvatarVideosResourceWithStreamingResponse(self._client.avatar_videos) + @cached_property def documents(self) -> documents.AsyncDocumentsResourceWithStreamingResponse: from .resources.documents import AsyncDocumentsResourceWithStreamingResponse diff --git a/src/runwayml/resources/__init__.py b/src/runwayml/resources/__init__.py index ff71c4e..9f06fac 100644 --- a/src/runwayml/resources/__init__.py +++ b/src/runwayml/resources/__init__.py @@ -16,6 +16,14 @@ VoicesResourceWithStreamingResponse, AsyncVoicesResourceWithStreamingResponse, ) +from .avatars import ( + AvatarsResource, + AsyncAvatarsResource, + AvatarsResourceWithRawResponse, + AsyncAvatarsResourceWithRawResponse, + AvatarsResourceWithStreamingResponse, + AsyncAvatarsResourceWithStreamingResponse, +) from .uploads import ( UploadsResource, AsyncUploadsResource, @@ -24,6 +32,14 @@ UploadsResourceWithStreamingResponse, AsyncUploadsResourceWithStreamingResponse, ) +from .documents import ( + DocumentsResource, + AsyncDocumentsResource, + DocumentsResourceWithRawResponse, + AsyncDocumentsResourceWithRawResponse, + DocumentsResourceWithStreamingResponse, + AsyncDocumentsResourceWithStreamingResponse, +) from .workflows import ( WorkflowsResource, AsyncWorkflowsResource, @@ -48,6 +64,14 @@ SoundEffectResourceWithStreamingResponse, AsyncSoundEffectResourceWithStreamingResponse, ) +from .avatar_videos import ( + AvatarVideosResource, + AsyncAvatarVideosResource, + AvatarVideosResourceWithRawResponse, + AsyncAvatarVideosResourceWithRawResponse, + AvatarVideosResourceWithStreamingResponse, + AsyncAvatarVideosResourceWithStreamingResponse, +) from .text_to_image import ( TextToImageResource, AsyncTextToImageResource, @@ -216,6 +240,24 @@ "AsyncUploadsResourceWithRawResponse", "UploadsResourceWithStreamingResponse", "AsyncUploadsResourceWithStreamingResponse", + "AvatarsResource", + "AsyncAvatarsResource", + "AvatarsResourceWithRawResponse", + "AsyncAvatarsResourceWithRawResponse", + "AvatarsResourceWithStreamingResponse", + "AsyncAvatarsResourceWithStreamingResponse", + "AvatarVideosResource", + "AsyncAvatarVideosResource", + "AvatarVideosResourceWithRawResponse", + "AsyncAvatarVideosResourceWithRawResponse", + "AvatarVideosResourceWithStreamingResponse", + "AsyncAvatarVideosResourceWithStreamingResponse", + "DocumentsResource", + "AsyncDocumentsResource", + "DocumentsResourceWithRawResponse", + "AsyncDocumentsResourceWithRawResponse", + "DocumentsResourceWithStreamingResponse", + "AsyncDocumentsResourceWithStreamingResponse", "RealtimeSessionsResource", "AsyncRealtimeSessionsResource", "RealtimeSessionsResourceWithRawResponse", diff --git a/src/runwayml/resources/avatar_videos.py b/src/runwayml/resources/avatar_videos.py new file mode 100644 index 0000000..43f9a3c --- /dev/null +++ b/src/runwayml/resources/avatar_videos.py @@ -0,0 +1,205 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from ..types import avatar_video_create_params +from .._types import Body, Query, Headers, NotGiven, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.avatar_video_create_response import AvatarVideoCreateResponse + +__all__ = ["AvatarVideosResource", "AsyncAvatarVideosResource"] + + +class AvatarVideosResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> AvatarVideosResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runwayml/sdk-python#accessing-raw-response-data-eg-headers + """ + return AvatarVideosResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AvatarVideosResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runwayml/sdk-python#with_streaming_response + """ + return AvatarVideosResourceWithStreamingResponse(self) + + def create( + self, + *, + avatar: avatar_video_create_params.Avatar, + model: Literal["gwm1_avatars"], + speech: avatar_video_create_params.Speech, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AvatarVideoCreateResponse: + """Start an asynchronous task to generate a video of an avatar speaking. + + Provide + `speech` with `type: "audio"` (audio file) or `type: "text"` (text script for + TTS). Poll `GET /v1/tasks/:id` to check progress and retrieve the output video + URL once complete. + + Args: + avatar: The avatar configuration for the session. + + model: The model to use for avatar video generation. + + speech: The speech source for avatar video generation. Either an audio file or text + script. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/avatar_videos", + body=maybe_transform( + { + "avatar": avatar, + "model": model, + "speech": speech, + }, + avatar_video_create_params.AvatarVideoCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AvatarVideoCreateResponse, + ) + + +class AsyncAvatarVideosResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncAvatarVideosResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runwayml/sdk-python#accessing-raw-response-data-eg-headers + """ + return AsyncAvatarVideosResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAvatarVideosResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runwayml/sdk-python#with_streaming_response + """ + return AsyncAvatarVideosResourceWithStreamingResponse(self) + + async def create( + self, + *, + avatar: avatar_video_create_params.Avatar, + model: Literal["gwm1_avatars"], + speech: avatar_video_create_params.Speech, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AvatarVideoCreateResponse: + """Start an asynchronous task to generate a video of an avatar speaking. + + Provide + `speech` with `type: "audio"` (audio file) or `type: "text"` (text script for + TTS). Poll `GET /v1/tasks/:id` to check progress and retrieve the output video + URL once complete. + + Args: + avatar: The avatar configuration for the session. + + model: The model to use for avatar video generation. + + speech: The speech source for avatar video generation. Either an audio file or text + script. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/avatar_videos", + body=await async_maybe_transform( + { + "avatar": avatar, + "model": model, + "speech": speech, + }, + avatar_video_create_params.AvatarVideoCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AvatarVideoCreateResponse, + ) + + +class AvatarVideosResourceWithRawResponse: + def __init__(self, avatar_videos: AvatarVideosResource) -> None: + self._avatar_videos = avatar_videos + + self.create = to_raw_response_wrapper( + avatar_videos.create, + ) + + +class AsyncAvatarVideosResourceWithRawResponse: + def __init__(self, avatar_videos: AsyncAvatarVideosResource) -> None: + self._avatar_videos = avatar_videos + + self.create = async_to_raw_response_wrapper( + avatar_videos.create, + ) + + +class AvatarVideosResourceWithStreamingResponse: + def __init__(self, avatar_videos: AvatarVideosResource) -> None: + self._avatar_videos = avatar_videos + + self.create = to_streamed_response_wrapper( + avatar_videos.create, + ) + + +class AsyncAvatarVideosResourceWithStreamingResponse: + def __init__(self, avatar_videos: AsyncAvatarVideosResource) -> None: + self._avatar_videos = avatar_videos + + self.create = async_to_streamed_response_wrapper( + avatar_videos.create, + ) diff --git a/src/runwayml/resources/voices.py b/src/runwayml/resources/voices.py index 7ec7166..bd7ec81 100644 --- a/src/runwayml/resources/voices.py +++ b/src/runwayml/resources/voices.py @@ -62,7 +62,8 @@ def create( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> VoiceCreateResponse: """ - Create a custom voice from a text description. + Create a custom voice from a text description, or clone a voice from an audio + sample. Args: from_: The source configuration for creating the voice. @@ -218,7 +219,7 @@ def delete( def preview( self, *, - model: Literal["eleven_multilingual_ttv_v2", "eleven_ttv_v3"], + model: Literal["eleven_ttv_v3", "eleven_multilingual_ttv_v2"], prompt: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -233,7 +234,8 @@ def preview( audition a voice before creating it. Args: - model: The voice design model to use. + model: The voice design model to use. Prefer eleven_ttv_v3 (latest); + eleven_multilingual_ttv_v2 is the previous generation. prompt: A text description of the desired voice characteristics. Must be at least 20 characters. @@ -296,7 +298,8 @@ async def create( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> VoiceCreateResponse: """ - Create a custom voice from a text description. + Create a custom voice from a text description, or clone a voice from an audio + sample. Args: from_: The source configuration for creating the voice. @@ -452,7 +455,7 @@ async def delete( async def preview( self, *, - model: Literal["eleven_multilingual_ttv_v2", "eleven_ttv_v3"], + model: Literal["eleven_ttv_v3", "eleven_multilingual_ttv_v2"], prompt: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -467,7 +470,8 @@ async def preview( audition a voice before creating it. Args: - model: The voice design model to use. + model: The voice design model to use. Prefer eleven_ttv_v3 (latest); + eleven_multilingual_ttv_v2 is the previous generation. prompt: A text description of the desired voice characteristics. Must be at least 20 characters. diff --git a/src/runwayml/types/__init__.py b/src/runwayml/types/__init__.py index a3e8611..16995b4 100644 --- a/src/runwayml/types/__init__.py +++ b/src/runwayml/types/__init__.py @@ -25,12 +25,14 @@ from .voice_retrieve_response import VoiceRetrieveResponse as VoiceRetrieveResponse from .avatar_retrieve_response import AvatarRetrieveResponse as AvatarRetrieveResponse from .document_create_response import DocumentCreateResponse as DocumentCreateResponse +from .avatar_video_create_params import AvatarVideoCreateParams as AvatarVideoCreateParams from .document_retrieve_response import DocumentRetrieveResponse as DocumentRetrieveResponse from .sound_effect_create_params import SoundEffectCreateParams as SoundEffectCreateParams from .workflow_retrieve_response import WorkflowRetrieveResponse as WorkflowRetrieveResponse from .text_to_image_create_params import TextToImageCreateParams as TextToImageCreateParams from .text_to_video_create_params import TextToVideoCreateParams as TextToVideoCreateParams from .voice_dubbing_create_params import VoiceDubbingCreateParams as VoiceDubbingCreateParams +from .avatar_video_create_response import AvatarVideoCreateResponse as AvatarVideoCreateResponse from .image_to_video_create_params import ImageToVideoCreateParams as ImageToVideoCreateParams from .sound_effect_create_response import SoundEffectCreateResponse as SoundEffectCreateResponse from .text_to_speech_create_params import TextToSpeechCreateParams as TextToSpeechCreateParams diff --git a/src/runwayml/types/avatar_video_create_params.py b/src/runwayml/types/avatar_video_create_params.py new file mode 100644 index 0000000..f647a6d --- /dev/null +++ b/src/runwayml/types/avatar_video_create_params.py @@ -0,0 +1,153 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict + +from .._utils import PropertyInfo + +__all__ = [ + "AvatarVideoCreateParams", + "Avatar", + "AvatarRunwayPreset", + "AvatarCustom", + "Speech", + "SpeechAudio", + "SpeechText", + "SpeechTextVoice", + "SpeechTextVoicePreset", + "SpeechTextVoiceCustom", +] + + +class AvatarVideoCreateParams(TypedDict, total=False): + avatar: Required[Avatar] + """The avatar configuration for the session.""" + + model: Required[Literal["gwm1_avatars"]] + """The model to use for avatar video generation.""" + + speech: Required[Speech] + """The speech source for avatar video generation. + + Either an audio file or text script. + """ + + +class AvatarRunwayPreset(TypedDict, total=False): + """A preset avatar from Runway.""" + + preset_id: Required[ + Annotated[ + Literal[ + "game-character", + "music-superstar", + "game-character-man", + "cat-character", + "influencer", + "tennis-coach", + "human-resource", + "fashion-designer", + "cooking-teacher", + ], + PropertyInfo(alias="presetId"), + ] + ] + """ID of a preset avatar.""" + + type: Required[Literal["runway-preset"]] + + +class AvatarCustom(TypedDict, total=False): + """A user-created avatar.""" + + avatar_id: Required[Annotated[str, PropertyInfo(alias="avatarId")]] + """ID of a user-created avatar.""" + + type: Required[Literal["custom"]] + + +Avatar: TypeAlias = Union[AvatarRunwayPreset, AvatarCustom] + + +class SpeechAudio(TypedDict, total=False): + """Provide an audio file for the avatar to speak.""" + + audio: Required[str] + """A HTTPS URL.""" + + type: Required[Literal["audio"]] + + +class SpeechTextVoicePreset(TypedDict, total=False): + """A preset voice from the Runway API.""" + + preset_id: Required[ + Annotated[ + Literal[ + "victoria", + "vincent", + "clara", + "drew", + "skye", + "max", + "morgan", + "felix", + "mia", + "marcus", + "summer", + "ruby", + "aurora", + "jasper", + "leo", + "adrian", + "nina", + "emma", + "blake", + "david", + "maya", + "nathan", + "sam", + "georgia", + "petra", + "adam", + "zach", + "violet", + "roman", + "luna", + ], + PropertyInfo(alias="presetId"), + ] + ] + + type: Required[Literal["preset"]] + + +class SpeechTextVoiceCustom(TypedDict, total=False): + """A custom voice created via the Voices API.""" + + id: Required[str] + + type: Required[Literal["custom"]] + + +SpeechTextVoice: TypeAlias = Union[SpeechTextVoicePreset, SpeechTextVoiceCustom] + + +class SpeechText(TypedDict, total=False): + """Provide text for the avatar to speak via TTS.""" + + text: Required[str] + """Text script for speech-driven video generation.""" + + type: Required[Literal["text"]] + + voice: SpeechTextVoice + """Optional voice override for TTS. + + If not provided, the avatar's configured voice is used. + """ + + +Speech: TypeAlias = Union[SpeechAudio, SpeechText] diff --git a/src/runwayml/types/avatar_video_create_response.py b/src/runwayml/types/avatar_video_create_response.py new file mode 100644 index 0000000..4c7cec2 --- /dev/null +++ b/src/runwayml/types/avatar_video_create_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["AvatarVideoCreateResponse"] + + +class AvatarVideoCreateResponse(BaseModel): + id: str + """The ID of the avatar video task. + + Use `GET /v1/tasks/:id` to poll for status and output. + """ diff --git a/src/runwayml/types/voice_create_params.py b/src/runwayml/types/voice_create_params.py index c008f45..c7acad5 100644 --- a/src/runwayml/types/voice_create_params.py +++ b/src/runwayml/types/voice_create_params.py @@ -2,12 +2,12 @@ from __future__ import annotations -from typing import Optional -from typing_extensions import Literal, Required, Annotated, TypedDict +from typing import Union, Optional +from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict from .._utils import PropertyInfo -__all__ = ["VoiceCreateParams", "From"] +__all__ = ["VoiceCreateParams", "From", "FromText", "FromAudio"] class VoiceCreateParams(TypedDict, total=False): @@ -21,11 +21,15 @@ class VoiceCreateParams(TypedDict, total=False): """An optional description of the voice.""" -class From(TypedDict, total=False): +class FromText(TypedDict, total=False): """The source configuration for creating the voice.""" - model: Required[Literal["eleven_multilingual_ttv_v2", "eleven_ttv_v3"]] - """The voice design model to use.""" + model: Required[Literal["eleven_ttv_v3", "eleven_multilingual_ttv_v2"]] + """The voice design model to use. + + Prefer eleven_ttv_v3 (latest); eleven_multilingual_ttv_v2 is the previous + generation. + """ prompt: Required[str] """A text description of the desired voice characteristics. @@ -34,3 +38,13 @@ class From(TypedDict, total=False): """ type: Required[Literal["text"]] + + +class FromAudio(TypedDict, total=False): + audio: Required[str] + """A HTTPS URL.""" + + type: Required[Literal["audio"]] + + +From: TypeAlias = Union[FromText, FromAudio] diff --git a/src/runwayml/types/voice_preview_params.py b/src/runwayml/types/voice_preview_params.py index f603c8a..bf5c30b 100644 --- a/src/runwayml/types/voice_preview_params.py +++ b/src/runwayml/types/voice_preview_params.py @@ -8,8 +8,12 @@ class VoicePreviewParams(TypedDict, total=False): - model: Required[Literal["eleven_multilingual_ttv_v2", "eleven_ttv_v3"]] - """The voice design model to use.""" + model: Required[Literal["eleven_ttv_v3", "eleven_multilingual_ttv_v2"]] + """The voice design model to use. + + Prefer eleven_ttv_v3 (latest); eleven_multilingual_ttv_v2 is the previous + generation. + """ prompt: Required[str] """A text description of the desired voice characteristics. diff --git a/tests/api_resources/test_avatar_videos.py b/tests/api_resources/test_avatar_videos.py new file mode 100644 index 0000000..60984eb --- /dev/null +++ b/tests/api_resources/test_avatar_videos.py @@ -0,0 +1,164 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from runwayml import RunwayML, AsyncRunwayML +from tests.utils import assert_matches_type +from runwayml.types import AvatarVideoCreateResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAvatarVideos: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: RunwayML) -> None: + avatar_video = client.avatar_videos.create( + avatar={ + "preset_id": "game-character", + "type": "runway-preset", + }, + model="gwm1_avatars", + speech={ + "audio": "https://example.com/file", + "type": "audio", + }, + ) + assert_matches_type(AvatarVideoCreateResponse, avatar_video, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: RunwayML) -> None: + avatar_video = client.avatar_videos.create( + avatar={ + "preset_id": "game-character", + "type": "runway-preset", + }, + model="gwm1_avatars", + speech={ + "audio": "https://example.com/file", + "type": "audio", + }, + ) + assert_matches_type(AvatarVideoCreateResponse, avatar_video, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: RunwayML) -> None: + response = client.avatar_videos.with_raw_response.create( + avatar={ + "preset_id": "game-character", + "type": "runway-preset", + }, + model="gwm1_avatars", + speech={ + "audio": "https://example.com/file", + "type": "audio", + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + avatar_video = response.parse() + assert_matches_type(AvatarVideoCreateResponse, avatar_video, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: RunwayML) -> None: + with client.avatar_videos.with_streaming_response.create( + avatar={ + "preset_id": "game-character", + "type": "runway-preset", + }, + model="gwm1_avatars", + speech={ + "audio": "https://example.com/file", + "type": "audio", + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + avatar_video = response.parse() + assert_matches_type(AvatarVideoCreateResponse, avatar_video, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncAvatarVideos: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncRunwayML) -> None: + avatar_video = await async_client.avatar_videos.create( + avatar={ + "preset_id": "game-character", + "type": "runway-preset", + }, + model="gwm1_avatars", + speech={ + "audio": "https://example.com/file", + "type": "audio", + }, + ) + assert_matches_type(AvatarVideoCreateResponse, avatar_video, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncRunwayML) -> None: + avatar_video = await async_client.avatar_videos.create( + avatar={ + "preset_id": "game-character", + "type": "runway-preset", + }, + model="gwm1_avatars", + speech={ + "audio": "https://example.com/file", + "type": "audio", + }, + ) + assert_matches_type(AvatarVideoCreateResponse, avatar_video, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncRunwayML) -> None: + response = await async_client.avatar_videos.with_raw_response.create( + avatar={ + "preset_id": "game-character", + "type": "runway-preset", + }, + model="gwm1_avatars", + speech={ + "audio": "https://example.com/file", + "type": "audio", + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + avatar_video = await response.parse() + assert_matches_type(AvatarVideoCreateResponse, avatar_video, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncRunwayML) -> None: + async with async_client.avatar_videos.with_streaming_response.create( + avatar={ + "preset_id": "game-character", + "type": "runway-preset", + }, + model="gwm1_avatars", + speech={ + "audio": "https://example.com/file", + "type": "audio", + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + avatar_video = await response.parse() + assert_matches_type(AvatarVideoCreateResponse, avatar_video, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_voices.py b/tests/api_resources/test_voices.py index 548a5aa..9076209 100644 --- a/tests/api_resources/test_voices.py +++ b/tests/api_resources/test_voices.py @@ -27,7 +27,7 @@ class TestVoices: def test_method_create(self, client: RunwayML) -> None: voice = client.voices.create( from_={ - "model": "eleven_multilingual_ttv_v2", + "model": "eleven_ttv_v3", "prompt": "xxxxxxxxxxxxxxxxxxxx", "type": "text", }, @@ -39,7 +39,7 @@ def test_method_create(self, client: RunwayML) -> None: def test_method_create_with_all_params(self, client: RunwayML) -> None: voice = client.voices.create( from_={ - "model": "eleven_multilingual_ttv_v2", + "model": "eleven_ttv_v3", "prompt": "xxxxxxxxxxxxxxxxxxxx", "type": "text", }, @@ -52,7 +52,7 @@ def test_method_create_with_all_params(self, client: RunwayML) -> None: def test_raw_response_create(self, client: RunwayML) -> None: response = client.voices.with_raw_response.create( from_={ - "model": "eleven_multilingual_ttv_v2", + "model": "eleven_ttv_v3", "prompt": "xxxxxxxxxxxxxxxxxxxx", "type": "text", }, @@ -68,7 +68,7 @@ def test_raw_response_create(self, client: RunwayML) -> None: def test_streaming_response_create(self, client: RunwayML) -> None: with client.voices.with_streaming_response.create( from_={ - "model": "eleven_multilingual_ttv_v2", + "model": "eleven_ttv_v3", "prompt": "xxxxxxxxxxxxxxxxxxxx", "type": "text", }, @@ -200,7 +200,7 @@ def test_path_params_delete(self, client: RunwayML) -> None: @parametrize def test_method_preview(self, client: RunwayML) -> None: voice = client.voices.preview( - model="eleven_multilingual_ttv_v2", + model="eleven_ttv_v3", prompt="xxxxxxxxxxxxxxxxxxxx", ) assert_matches_type(VoicePreviewResponse, voice, path=["response"]) @@ -208,7 +208,7 @@ def test_method_preview(self, client: RunwayML) -> None: @parametrize def test_raw_response_preview(self, client: RunwayML) -> None: response = client.voices.with_raw_response.preview( - model="eleven_multilingual_ttv_v2", + model="eleven_ttv_v3", prompt="xxxxxxxxxxxxxxxxxxxx", ) @@ -220,7 +220,7 @@ def test_raw_response_preview(self, client: RunwayML) -> None: @parametrize def test_streaming_response_preview(self, client: RunwayML) -> None: with client.voices.with_streaming_response.preview( - model="eleven_multilingual_ttv_v2", + model="eleven_ttv_v3", prompt="xxxxxxxxxxxxxxxxxxxx", ) as response: assert not response.is_closed @@ -241,7 +241,7 @@ class TestAsyncVoices: async def test_method_create(self, async_client: AsyncRunwayML) -> None: voice = await async_client.voices.create( from_={ - "model": "eleven_multilingual_ttv_v2", + "model": "eleven_ttv_v3", "prompt": "xxxxxxxxxxxxxxxxxxxx", "type": "text", }, @@ -253,7 +253,7 @@ async def test_method_create(self, async_client: AsyncRunwayML) -> None: async def test_method_create_with_all_params(self, async_client: AsyncRunwayML) -> None: voice = await async_client.voices.create( from_={ - "model": "eleven_multilingual_ttv_v2", + "model": "eleven_ttv_v3", "prompt": "xxxxxxxxxxxxxxxxxxxx", "type": "text", }, @@ -266,7 +266,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncRunwayML) async def test_raw_response_create(self, async_client: AsyncRunwayML) -> None: response = await async_client.voices.with_raw_response.create( from_={ - "model": "eleven_multilingual_ttv_v2", + "model": "eleven_ttv_v3", "prompt": "xxxxxxxxxxxxxxxxxxxx", "type": "text", }, @@ -282,7 +282,7 @@ async def test_raw_response_create(self, async_client: AsyncRunwayML) -> None: async def test_streaming_response_create(self, async_client: AsyncRunwayML) -> None: async with async_client.voices.with_streaming_response.create( from_={ - "model": "eleven_multilingual_ttv_v2", + "model": "eleven_ttv_v3", "prompt": "xxxxxxxxxxxxxxxxxxxx", "type": "text", }, @@ -414,7 +414,7 @@ async def test_path_params_delete(self, async_client: AsyncRunwayML) -> None: @parametrize async def test_method_preview(self, async_client: AsyncRunwayML) -> None: voice = await async_client.voices.preview( - model="eleven_multilingual_ttv_v2", + model="eleven_ttv_v3", prompt="xxxxxxxxxxxxxxxxxxxx", ) assert_matches_type(VoicePreviewResponse, voice, path=["response"]) @@ -422,7 +422,7 @@ async def test_method_preview(self, async_client: AsyncRunwayML) -> None: @parametrize async def test_raw_response_preview(self, async_client: AsyncRunwayML) -> None: response = await async_client.voices.with_raw_response.preview( - model="eleven_multilingual_ttv_v2", + model="eleven_ttv_v3", prompt="xxxxxxxxxxxxxxxxxxxx", ) @@ -434,7 +434,7 @@ async def test_raw_response_preview(self, async_client: AsyncRunwayML) -> None: @parametrize async def test_streaming_response_preview(self, async_client: AsyncRunwayML) -> None: async with async_client.voices.with_streaming_response.preview( - model="eleven_multilingual_ttv_v2", + model="eleven_ttv_v3", prompt="xxxxxxxxxxxxxxxxxxxx", ) as response: assert not response.is_closed From e8fe41ba8c95f2263a08faefe2d791374b1db5d0 Mon Sep 17 00:00:00 2001 From: Daniil Date: Fri, 10 Apr 2026 11:32:22 +0200 Subject: [PATCH 3/4] feat(polling): add wait_for_task_output to avatar_videos.create --- src/runwayml/resources/avatar_videos.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/runwayml/resources/avatar_videos.py b/src/runwayml/resources/avatar_videos.py index 43f9a3c..c4abd94 100644 --- a/src/runwayml/resources/avatar_videos.py +++ b/src/runwayml/resources/avatar_videos.py @@ -17,6 +17,12 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) +from ..lib.polling import ( + NewTaskCreatedResponse, + AsyncNewTaskCreatedResponse, + create_waitable_resource, + create_async_waitable_resource, +) from .._base_client import make_request_options from ..types.avatar_video_create_response import AvatarVideoCreateResponse @@ -55,7 +61,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AvatarVideoCreateResponse: + ) -> NewTaskCreatedResponse: """Start an asynchronous task to generate a video of an avatar speaking. Provide @@ -92,7 +98,7 @@ def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=AvatarVideoCreateResponse, + cast_to=create_waitable_resource(AvatarVideoCreateResponse, self._client), ) @@ -128,7 +134,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AvatarVideoCreateResponse: + ) -> AsyncNewTaskCreatedResponse: """Start an asynchronous task to generate a video of an avatar speaking. Provide @@ -165,7 +171,7 @@ async def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=AvatarVideoCreateResponse, + cast_to=create_async_waitable_resource(AvatarVideoCreateResponse, self._client), ) From df9e90835b34e7e58bc508a95b622a919414e33b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 09:32:51 +0000 Subject: [PATCH 4/4] release: 4.11.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 19 +++++++++++++++++++ pyproject.toml | 2 +- src/runwayml/_version.py | 2 +- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ad56a78..a43b15a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "4.10.0" + ".": "4.11.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a8084fe..401169b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 4.11.0 (2026-04-10) + +Full Changelog: [v4.10.0...v4.11.0](https://github.com/runwayml/sdk-python/compare/v4.10.0...v4.11.0) + +### Features + +* **api:** add avatar_videos resource ([75b6ded](https://github.com/runwayml/sdk-python/commit/75b6deda8c5733115af29c9374ae10f3d4f4716a)) +* **polling:** add wait_for_task_output to avatar_videos.create ([e8fe41b](https://github.com/runwayml/sdk-python/commit/e8fe41ba8c95f2263a08faefe2d791374b1db5d0)) + + +### Bug Fixes + +* **client:** preserve hardcoded query params when merging with user params ([839c38b](https://github.com/runwayml/sdk-python/commit/839c38bc95d9147b140481907621942c5c503ead)) + + +### Chores + +* pin GitHub Actions to full commit SHAs ([8f7689b](https://github.com/runwayml/sdk-python/commit/8f7689bac99f57930dd88a9ab3d4c9a279a13888)) + ## 4.10.0 (2026-04-01) Full Changelog: [v4.9.0...v4.10.0](https://github.com/runwayml/sdk-python/compare/v4.9.0...v4.10.0) diff --git a/pyproject.toml b/pyproject.toml index dcbf477..c8a9c2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "runwayml" -version = "4.10.0" +version = "4.11.0" description = "The official Python library for the runwayml API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/runwayml/_version.py b/src/runwayml/_version.py index 1f2d85c..2d1640a 100644 --- a/src/runwayml/_version.py +++ b/src/runwayml/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "runwayml" -__version__ = "4.10.0" # x-release-please-version +__version__ = "4.11.0" # x-release-please-version