From 1c35e1b20b629913df834d87caf4d8784007e862 Mon Sep 17 00:00:00 2001 From: Omar-V2 Date: Fri, 14 Aug 2026 23:04:18 +0100 Subject: [PATCH] Add reference_audios, generate_audio, and 1080p to video generation --- CHANGELOG.md | 5 + pyproject.toml | 1 + src/xai_sdk/aio/video.py | 30 +++++- src/xai_sdk/proto/v5/video_pb2.py | 10 +- src/xai_sdk/proto/v5/video_pb2.pyi | 2 + src/xai_sdk/proto/v6/video_pb2.py | 10 +- src/xai_sdk/proto/v6/video_pb2.pyi | 2 + src/xai_sdk/sync/video.py | 30 +++++- src/xai_sdk/types/__init__.py | 5 +- src/xai_sdk/types/model.py | 3 +- src/xai_sdk/types/video.py | 35 ++++++- src/xai_sdk/video.py | 36 ++++++- tests/aio/video_test.py | 158 +++++++++++++++++++++++++++- tests/sync/video_test.py | 163 ++++++++++++++++++++++++++++- uv.lock | 2 + 15 files changed, 472 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb1621c..47c70ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] ### Added +- Added `reference_audios` and `generate_audio` parameters to video generation. `reference_audios` (on `client.video.generate` / `start` / `prepare`) is a list of audio sources for reference-to-video — each entry is a TypedDict such as `{"voice_id": "ara"}`; only supported for `grok-imagine-video-1.5`. `generate_audio` (on `generate` / `start` / `prepare`) controls whether the generated video includes audio (`true`) or is silent (`false`); when omitted the server defaults to `true`. +- Added `"1080p"` as an accepted video `resolution` value (maps to `VIDEO_RESOLUTION_1080P`). Only supported on models that advertise 1080p (e.g. `grok-imagine-video-1.5` for image-to-video). - Added a `quality` parameter (`"low"`, `"medium"`) to image generation (`client.image.sample`, `sample_batch`, and batch `prepare`), mapping to the `GenerateImageRequest.quality` field. When omitted, the default is `"medium"`. Only supported for `grok-imagine-image-2.0`. - Added `grok-imagine-image-2.0` to the `ImageGenerationModel` known-model type literal - **`xhigh` Reasoning Effort**: Added `"xhigh"` as an accepted `reasoning_effort` value (maps to `EFFORT_XHIGH`; supported by models such as `grok-4.6`) @@ -11,6 +13,9 @@ - **Public File URLs**: Added `client.files.create_public_url()` and `client.files.revoke_public_url()` (sync and async) to create and revoke publicly shareable, unauthenticated URLs for stored files. `create_public_url()` accepts an optional `expires_after` (an `int` in seconds or a `datetime.timedelta`). - **Files List Filter**: `client.files.list()` (sync and async) now accepts an optional `filter` parameter to narrow results server-side by fields such as `content_type`, `size_bytes`, `created_at`, `upload_status`, and `public_url` (e.g. `filter='public_url != null'`). +### Changed +- Model type literals now match the current public model catalog: added `grok-imagine-video-1.5` and removed `grok-imagine-video-1.5-preview` (now an alias of `grok-imagine-video-1.5`) from `VideoGenerationModel`, and removed `grok-imagine-image-pro` (an alias of `grok-imagine-image-quality`) from `ImageGenerationModel`. These literals are type hints for editor autocomplete only — alias strings continue to work when passed to the API. + ### Fixed - `chat.append(response)` now sets `tool_call_id` on replayed tool-role messages (recovered from the tool call echoed on the output), so servers can pair replayed tool turns with their originating calls. This fixes stateless multi-turn follow-ups to server-side tools whose results are re-hydrated by ID — e.g. editing a previously generated image in-memory without `previous_response_id`. diff --git a/pyproject.toml b/pyproject.toml index 747cf5b..493c593 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ "aiohttp>=3.8.6,<4", "packaging>=25.0,<26", "opentelemetry-sdk>=1.36.0,<2", + "typing-extensions>=4.14.0,<5", ] requires-python = ">=3.10" classifiers = [ diff --git a/src/xai_sdk/aio/video.py b/src/xai_sdk/aio/video.py index a1c0ed3..2353f05 100644 --- a/src/xai_sdk/aio/video.py +++ b/src/xai_sdk/aio/video.py @@ -9,7 +9,7 @@ from ..poll_timer import PollTimer from ..proto import batch_pb2, deferred_pb2, image_pb2, video_pb2 from ..telemetry import get_tracer -from ..types import VideoGenerationModel +from ..types import ReferenceAudio, VideoGenerationModel from ..video import ( DEFAULT_VIDEO_POLL_INTERVAL, DEFAULT_VIDEO_TIMEOUT, @@ -47,6 +47,8 @@ def prepare( resolution: Optional[VideoResolution] = None, reference_image_urls: Optional[Sequence[str]] = None, reference_image_file_ids: Optional[Sequence[str]] = None, + reference_audios: Optional[Sequence[ReferenceAudio]] = None, + generate_audio: Optional[bool] = None, storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> batch_pb2.BatchRequest: """Prepares a video generation request for batch processing. @@ -81,6 +83,14 @@ def prepare( reference-to-video (R2V) generation. May be combined with ``reference_image_urls`` to mix URL/base64 and file-ID references in the same request. + reference_audios: Optional list of reference audio sources for + reference-to-video (R2V) generation. Each entry is a TypedDict + such as ``{"voice_id": "ara"}``. See the + `voice catalog `_ + for available presets. At most three entries. Only supported + for ``grok-imagine-video-1.5``. + generate_audio: Whether the generated video includes an audio track. + Defaults to ``True``. Set to ``False`` for a silent video. storage_options: Persist the result to the Files API. Accepts a dict with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. Set ``public_url`` to also create a publicly shareable URL. @@ -133,6 +143,8 @@ def prepare( resolution=resolution, reference_image_urls=reference_image_urls, reference_image_file_ids=reference_image_file_ids, + reference_audios=reference_audios, + generate_audio=generate_audio, storage_options=storage_options, ) @@ -209,6 +221,8 @@ async def start( resolution: Optional[VideoResolution] = None, reference_image_urls: Optional[Sequence[str]] = None, reference_image_file_ids: Optional[Sequence[str]] = None, + reference_audios: Optional[Sequence[ReferenceAudio]] = None, + generate_audio: Optional[bool] = None, storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> deferred_pb2.StartDeferredResponse: """Starts a video generation request and returns a request_id for polling. @@ -227,6 +241,8 @@ async def start( resolution=resolution, reference_image_urls=reference_image_urls, reference_image_file_ids=reference_image_file_ids, + reference_audios=reference_audios, + generate_audio=generate_audio, storage_options=storage_options, ) @@ -256,6 +272,8 @@ async def generate( resolution: Optional[VideoResolution] = None, reference_image_urls: Optional[Sequence[str]] = None, reference_image_file_ids: Optional[Sequence[str]] = None, + reference_audios: Optional[Sequence[ReferenceAudio]] = None, + generate_audio: Optional[bool] = None, storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, timeout: Optional[datetime.timedelta] = None, interval: Optional[datetime.timedelta] = None, @@ -304,6 +322,14 @@ async def generate( reference-to-video (R2V) generation. May be combined with `reference_image_urls` to mix URL/base64 and file-ID references in the same request. + reference_audios: Optional list of reference audio sources for + reference-to-video (R2V) generation. Each entry is a TypedDict + such as ``{"voice_id": "ara"}``. See the + `voice catalog `_ + for available presets. At most three entries. Only supported + for ``grok-imagine-video-1.5``. + generate_audio: Whether the generated video includes an audio track. + Defaults to `True`. Set to `False` for a silent video. storage_options: Persist the result to the Files API. Accepts a dict with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. Set ``public_url`` to also create a publicly shareable URL. @@ -384,6 +410,8 @@ async def generate( resolution=resolution, reference_image_urls=reference_image_urls, reference_image_file_ids=reference_image_file_ids, + reference_audios=reference_audios, + generate_audio=generate_audio, storage_options=storage_options, ) diff --git a/src/xai_sdk/proto/v5/video_pb2.py b/src/xai_sdk/proto/v5/video_pb2.py index de3fc74..c8aff19 100644 --- a/src/xai_sdk/proto/v5/video_pb2.py +++ b/src/xai_sdk/proto/v5/video_pb2.py @@ -27,7 +27,7 @@ from . import usage_pb2 as xai_dot_api_dot_v1_dot_usage__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16xai/api/v1/video.proto\x12\x07xai_api\x1a\x19xai/api/v1/deferred.proto\x1a\x16xai/api/v1/image.proto\x1a\x16xai/api/v1/usage.proto\"J\n\x0fVideoUrlContent\x12\x12\n\x03url\x18\x01 \x01(\tH\x00R\x03url\x12\x19\n\x07\x66ile_id\x18\x02 \x01(\tH\x00R\x06\x66ileIdB\x08\n\x06source\">\n\x0f\x41udioUrlContent\x12\x1b\n\x08voice_id\x18\x02 \x01(\tH\x00R\x07voiceIdB\x08\n\x06sourceJ\x04\x08\x01\x10\x02\"\x9e\x05\n\x14GenerateVideoRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05image\x18\x02 \x01(\x0b\x32\x18.xai_api.ImageUrlContentR\x05image\x12\x14\n\x05model\x18\x03 \x01(\tR\x05model\x12\x1f\n\x08\x64uration\x18\x04 \x01(\x05H\x00R\x08\x64uration\x88\x01\x01\x12.\n\x05video\x18\x06 \x01(\x0b\x32\x18.xai_api.VideoUrlContentR\x05video\x12\x41\n\x0c\x61spect_ratio\x18\x07 \x01(\x0e\x32\x19.xai_api.VideoAspectRatioH\x01R\x0b\x61spectRatio\x88\x01\x01\x12=\n\nresolution\x18\x08 \x01(\x0e\x32\x18.xai_api.VideoResolutionH\x02R\nresolution\x88\x01\x01\x12\x43\n\x10reference_images\x18\r \x03(\x0b\x32\x18.xai_api.ImageUrlContentR\x0freferenceImages\x12\x45\n\x0fstorage_options\x18\x0e \x01(\x0b\x32\x17.xai_api.StorageOptionsH\x03R\x0estorageOptions\x88\x01\x01\x12\x43\n\x10reference_audios\x18\x10 \x03(\x0b\x32\x18.xai_api.AudioUrlContentR\x0freferenceAudios\x12*\n\x0egenerate_audio\x18\x11 \x01(\x08H\x04R\rgenerateAudio\x88\x01\x01\x42\x0b\n\t_durationB\x0f\n\r_aspect_ratioB\r\n\x0b_resolutionB\x12\n\x10_storage_optionsB\x11\n\x0f_generate_audioJ\x04\x08\x0f\x10\x10\"8\n\x17GetDeferredVideoRequest\x12\x1d\n\nrequest_id\x18\x01 \x01(\tR\trequestId\"\xd8\x01\n\rVideoResponse\x12-\n\x05video\x18\x01 \x01(\x0b\x32\x17.xai_api.GeneratedVideoR\x05video\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12,\n\x05usage\x18\x03 \x01(\x0b\x32\x16.xai_api.SamplingUsageR\x05usage\x12.\n\x05\x65rror\x18\x06 \x01(\x0b\x32\x13.xai_api.VideoErrorH\x00R\x05\x65rror\x88\x01\x01\x12\x1a\n\x08progress\x18\x07 \x01(\x05R\x08progressB\x08\n\x06_error\"\xf4\x01\n\x0eGeneratedVideo\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\x1a\n\x08\x64uration\x18\x04 \x01(\x05R\x08\x64uration\x12-\n\x12respect_moderation\x18\x05 \x01(\x08R\x11respectModeration\x12\x39\n\x0b\x66ile_output\x18\x06 \x01(\x0b\x32\x13.xai_api.FileOutputH\x00R\nfileOutput\x88\x01\x01\x12(\n\rstorage_error\x18\x07 \x01(\tH\x01R\x0cstorageError\x88\x01\x01\x42\x0e\n\x0c_file_outputB\x10\n\x0e_storage_error\"\x91\x01\n\x18GetDeferredVideoResponse\x12/\n\x06status\x18\x01 \x01(\x0e\x32\x17.xai_api.DeferredStatusR\x06status\x12\x37\n\x08response\x18\x02 \x01(\x0b\x32\x16.xai_api.VideoResponseH\x00R\x08response\x88\x01\x01\x42\x0b\n\t_response\":\n\nVideoError\x12\x12\n\x04\x63ode\x18\x01 \x01(\tR\x04\x63ode\x12\x18\n\x07message\x18\x02 \x01(\tR\x07message\"\x81\x02\n\x12\x45xtendVideoRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05video\x18\x02 \x01(\x0b\x32\x18.xai_api.VideoUrlContentR\x05video\x12\x14\n\x05model\x18\x03 \x01(\tR\x05model\x12\x1f\n\x08\x64uration\x18\x04 \x01(\x05H\x00R\x08\x64uration\x88\x01\x01\x12\x45\n\x0fstorage_options\x18\x06 \x01(\x0b\x32\x17.xai_api.StorageOptionsH\x01R\x0estorageOptions\x88\x01\x01\x42\x0b\n\t_durationB\x12\n\x10_storage_optionsJ\x04\x08\x07\x10\x08*\xfc\x01\n\x10VideoAspectRatio\x12\"\n\x1eVIDEO_ASPECT_RATIO_UNSPECIFIED\x10\x00\x12\x1a\n\x16VIDEO_ASPECT_RATIO_1_1\x10\x01\x12\x1b\n\x17VIDEO_ASPECT_RATIO_16_9\x10\x02\x12\x1b\n\x17VIDEO_ASPECT_RATIO_9_16\x10\x03\x12\x1a\n\x16VIDEO_ASPECT_RATIO_4_3\x10\x04\x12\x1a\n\x16VIDEO_ASPECT_RATIO_3_4\x10\x05\x12\x1a\n\x16VIDEO_ASPECT_RATIO_3_2\x10\x06\x12\x1a\n\x16VIDEO_ASPECT_RATIO_2_3\x10\x07*i\n\x0fVideoResolution\x12 \n\x1cVIDEO_RESOLUTION_UNSPECIFIED\x10\x00\x12\x19\n\x15VIDEO_RESOLUTION_480P\x10\x01\x12\x19\n\x15VIDEO_RESOLUTION_720P\x10\x02\x32\x82\x02\n\x05Video\x12P\n\rGenerateVideo\x12\x1d.xai_api.GenerateVideoRequest\x1a\x1e.xai_api.StartDeferredResponse\"\x00\x12L\n\x0b\x45xtendVideo\x12\x1b.xai_api.ExtendVideoRequest\x1a\x1e.xai_api.StartDeferredResponse\"\x00\x12Y\n\x10GetDeferredVideo\x12 .xai_api.GetDeferredVideoRequest\x1a!.xai_api.GetDeferredVideoResponse\"\x00\x42Q\n\x0b\x63om.xai_apiB\nVideoProtoP\x01\xa2\x02\x03XXX\xaa\x02\x06XaiApi\xca\x02\x06XaiApi\xe2\x02\x12XaiApi\\GPBMetadata\xea\x02\x06XaiApib\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16xai/api/v1/video.proto\x12\x07xai_api\x1a\x19xai/api/v1/deferred.proto\x1a\x16xai/api/v1/image.proto\x1a\x16xai/api/v1/usage.proto\"J\n\x0fVideoUrlContent\x12\x12\n\x03url\x18\x01 \x01(\tH\x00R\x03url\x12\x19\n\x07\x66ile_id\x18\x02 \x01(\tH\x00R\x06\x66ileIdB\x08\n\x06source\">\n\x0f\x41udioUrlContent\x12\x1b\n\x08voice_id\x18\x02 \x01(\tH\x00R\x07voiceIdB\x08\n\x06sourceJ\x04\x08\x01\x10\x02\"\x9e\x05\n\x14GenerateVideoRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05image\x18\x02 \x01(\x0b\x32\x18.xai_api.ImageUrlContentR\x05image\x12\x14\n\x05model\x18\x03 \x01(\tR\x05model\x12\x1f\n\x08\x64uration\x18\x04 \x01(\x05H\x00R\x08\x64uration\x88\x01\x01\x12.\n\x05video\x18\x06 \x01(\x0b\x32\x18.xai_api.VideoUrlContentR\x05video\x12\x41\n\x0c\x61spect_ratio\x18\x07 \x01(\x0e\x32\x19.xai_api.VideoAspectRatioH\x01R\x0b\x61spectRatio\x88\x01\x01\x12=\n\nresolution\x18\x08 \x01(\x0e\x32\x18.xai_api.VideoResolutionH\x02R\nresolution\x88\x01\x01\x12\x43\n\x10reference_images\x18\r \x03(\x0b\x32\x18.xai_api.ImageUrlContentR\x0freferenceImages\x12\x45\n\x0fstorage_options\x18\x0e \x01(\x0b\x32\x17.xai_api.StorageOptionsH\x03R\x0estorageOptions\x88\x01\x01\x12\x43\n\x10reference_audios\x18\x10 \x03(\x0b\x32\x18.xai_api.AudioUrlContentR\x0freferenceAudios\x12*\n\x0egenerate_audio\x18\x11 \x01(\x08H\x04R\rgenerateAudio\x88\x01\x01\x42\x0b\n\t_durationB\x0f\n\r_aspect_ratioB\r\n\x0b_resolutionB\x12\n\x10_storage_optionsB\x11\n\x0f_generate_audioJ\x04\x08\x0f\x10\x10\"8\n\x17GetDeferredVideoRequest\x12\x1d\n\nrequest_id\x18\x01 \x01(\tR\trequestId\"\xd8\x01\n\rVideoResponse\x12-\n\x05video\x18\x01 \x01(\x0b\x32\x17.xai_api.GeneratedVideoR\x05video\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12,\n\x05usage\x18\x03 \x01(\x0b\x32\x16.xai_api.SamplingUsageR\x05usage\x12.\n\x05\x65rror\x18\x06 \x01(\x0b\x32\x13.xai_api.VideoErrorH\x00R\x05\x65rror\x88\x01\x01\x12\x1a\n\x08progress\x18\x07 \x01(\x05R\x08progressB\x08\n\x06_error\"\xf4\x01\n\x0eGeneratedVideo\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\x1a\n\x08\x64uration\x18\x04 \x01(\x05R\x08\x64uration\x12-\n\x12respect_moderation\x18\x05 \x01(\x08R\x11respectModeration\x12\x39\n\x0b\x66ile_output\x18\x06 \x01(\x0b\x32\x13.xai_api.FileOutputH\x00R\nfileOutput\x88\x01\x01\x12(\n\rstorage_error\x18\x07 \x01(\tH\x01R\x0cstorageError\x88\x01\x01\x42\x0e\n\x0c_file_outputB\x10\n\x0e_storage_error\"\x91\x01\n\x18GetDeferredVideoResponse\x12/\n\x06status\x18\x01 \x01(\x0e\x32\x17.xai_api.DeferredStatusR\x06status\x12\x37\n\x08response\x18\x02 \x01(\x0b\x32\x16.xai_api.VideoResponseH\x00R\x08response\x88\x01\x01\x42\x0b\n\t_response\":\n\nVideoError\x12\x12\n\x04\x63ode\x18\x01 \x01(\tR\x04\x63ode\x12\x18\n\x07message\x18\x02 \x01(\tR\x07message\"\x81\x02\n\x12\x45xtendVideoRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05video\x18\x02 \x01(\x0b\x32\x18.xai_api.VideoUrlContentR\x05video\x12\x14\n\x05model\x18\x03 \x01(\tR\x05model\x12\x1f\n\x08\x64uration\x18\x04 \x01(\x05H\x00R\x08\x64uration\x88\x01\x01\x12\x45\n\x0fstorage_options\x18\x06 \x01(\x0b\x32\x17.xai_api.StorageOptionsH\x01R\x0estorageOptions\x88\x01\x01\x42\x0b\n\t_durationB\x12\n\x10_storage_optionsJ\x04\x08\x07\x10\x08*\xfc\x01\n\x10VideoAspectRatio\x12\"\n\x1eVIDEO_ASPECT_RATIO_UNSPECIFIED\x10\x00\x12\x1a\n\x16VIDEO_ASPECT_RATIO_1_1\x10\x01\x12\x1b\n\x17VIDEO_ASPECT_RATIO_16_9\x10\x02\x12\x1b\n\x17VIDEO_ASPECT_RATIO_9_16\x10\x03\x12\x1a\n\x16VIDEO_ASPECT_RATIO_4_3\x10\x04\x12\x1a\n\x16VIDEO_ASPECT_RATIO_3_4\x10\x05\x12\x1a\n\x16VIDEO_ASPECT_RATIO_3_2\x10\x06\x12\x1a\n\x16VIDEO_ASPECT_RATIO_2_3\x10\x07*\x85\x01\n\x0fVideoResolution\x12 \n\x1cVIDEO_RESOLUTION_UNSPECIFIED\x10\x00\x12\x19\n\x15VIDEO_RESOLUTION_480P\x10\x01\x12\x19\n\x15VIDEO_RESOLUTION_720P\x10\x02\x12\x1a\n\x16VIDEO_RESOLUTION_1080P\x10\x03\x32\x82\x02\n\x05Video\x12P\n\rGenerateVideo\x12\x1d.xai_api.GenerateVideoRequest\x1a\x1e.xai_api.StartDeferredResponse\"\x00\x12L\n\x0b\x45xtendVideo\x12\x1b.xai_api.ExtendVideoRequest\x1a\x1e.xai_api.StartDeferredResponse\"\x00\x12Y\n\x10GetDeferredVideo\x12 .xai_api.GetDeferredVideoRequest\x1a!.xai_api.GetDeferredVideoResponse\"\x00\x42Q\n\x0b\x63om.xai_apiB\nVideoProtoP\x01\xa2\x02\x03XXX\xaa\x02\x06XaiApi\xca\x02\x06XaiApi\xe2\x02\x12XaiApi\\GPBMetadata\xea\x02\x06XaiApib\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -37,8 +37,8 @@ _globals['DESCRIPTOR']._serialized_options = b'\n\013com.xai_apiB\nVideoProtoP\001\242\002\003XXX\252\002\006XaiApi\312\002\006XaiApi\342\002\022XaiApi\\GPBMetadata\352\002\006XaiApi' _globals['_VIDEOASPECTRATIO']._serialized_start=1916 _globals['_VIDEOASPECTRATIO']._serialized_end=2168 - _globals['_VIDEORESOLUTION']._serialized_start=2170 - _globals['_VIDEORESOLUTION']._serialized_end=2275 + _globals['_VIDEORESOLUTION']._serialized_start=2171 + _globals['_VIDEORESOLUTION']._serialized_end=2304 _globals['_VIDEOURLCONTENT']._serialized_start=110 _globals['_VIDEOURLCONTENT']._serialized_end=184 _globals['_AUDIOURLCONTENT']._serialized_start=186 @@ -57,6 +57,6 @@ _globals['_VIDEOERROR']._serialized_end=1653 _globals['_EXTENDVIDEOREQUEST']._serialized_start=1656 _globals['_EXTENDVIDEOREQUEST']._serialized_end=1913 - _globals['_VIDEO']._serialized_start=2278 - _globals['_VIDEO']._serialized_end=2536 + _globals['_VIDEO']._serialized_start=2307 + _globals['_VIDEO']._serialized_end=2565 # @@protoc_insertion_point(module_scope) diff --git a/src/xai_sdk/proto/v5/video_pb2.pyi b/src/xai_sdk/proto/v5/video_pb2.pyi index 853e81e..c38c0a8 100644 --- a/src/xai_sdk/proto/v5/video_pb2.pyi +++ b/src/xai_sdk/proto/v5/video_pb2.pyi @@ -25,6 +25,7 @@ class VideoResolution(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): VIDEO_RESOLUTION_UNSPECIFIED: _ClassVar[VideoResolution] VIDEO_RESOLUTION_480P: _ClassVar[VideoResolution] VIDEO_RESOLUTION_720P: _ClassVar[VideoResolution] + VIDEO_RESOLUTION_1080P: _ClassVar[VideoResolution] VIDEO_ASPECT_RATIO_UNSPECIFIED: VideoAspectRatio VIDEO_ASPECT_RATIO_1_1: VideoAspectRatio VIDEO_ASPECT_RATIO_16_9: VideoAspectRatio @@ -36,6 +37,7 @@ VIDEO_ASPECT_RATIO_2_3: VideoAspectRatio VIDEO_RESOLUTION_UNSPECIFIED: VideoResolution VIDEO_RESOLUTION_480P: VideoResolution VIDEO_RESOLUTION_720P: VideoResolution +VIDEO_RESOLUTION_1080P: VideoResolution class VideoUrlContent(_message.Message): __slots__ = ("url", "file_id") diff --git a/src/xai_sdk/proto/v6/video_pb2.py b/src/xai_sdk/proto/v6/video_pb2.py index 5689d51..d2cc6ed 100644 --- a/src/xai_sdk/proto/v6/video_pb2.py +++ b/src/xai_sdk/proto/v6/video_pb2.py @@ -27,7 +27,7 @@ from . import usage_pb2 as xai_dot_api_dot_v1_dot_usage__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16xai/api/v1/video.proto\x12\x07xai_api\x1a\x19xai/api/v1/deferred.proto\x1a\x16xai/api/v1/image.proto\x1a\x16xai/api/v1/usage.proto\"J\n\x0fVideoUrlContent\x12\x12\n\x03url\x18\x01 \x01(\tH\x00R\x03url\x12\x19\n\x07\x66ile_id\x18\x02 \x01(\tH\x00R\x06\x66ileIdB\x08\n\x06source\">\n\x0f\x41udioUrlContent\x12\x1b\n\x08voice_id\x18\x02 \x01(\tH\x00R\x07voiceIdB\x08\n\x06sourceJ\x04\x08\x01\x10\x02\"\x9e\x05\n\x14GenerateVideoRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05image\x18\x02 \x01(\x0b\x32\x18.xai_api.ImageUrlContentR\x05image\x12\x14\n\x05model\x18\x03 \x01(\tR\x05model\x12\x1f\n\x08\x64uration\x18\x04 \x01(\x05H\x00R\x08\x64uration\x88\x01\x01\x12.\n\x05video\x18\x06 \x01(\x0b\x32\x18.xai_api.VideoUrlContentR\x05video\x12\x41\n\x0c\x61spect_ratio\x18\x07 \x01(\x0e\x32\x19.xai_api.VideoAspectRatioH\x01R\x0b\x61spectRatio\x88\x01\x01\x12=\n\nresolution\x18\x08 \x01(\x0e\x32\x18.xai_api.VideoResolutionH\x02R\nresolution\x88\x01\x01\x12\x43\n\x10reference_images\x18\r \x03(\x0b\x32\x18.xai_api.ImageUrlContentR\x0freferenceImages\x12\x45\n\x0fstorage_options\x18\x0e \x01(\x0b\x32\x17.xai_api.StorageOptionsH\x03R\x0estorageOptions\x88\x01\x01\x12\x43\n\x10reference_audios\x18\x10 \x03(\x0b\x32\x18.xai_api.AudioUrlContentR\x0freferenceAudios\x12*\n\x0egenerate_audio\x18\x11 \x01(\x08H\x04R\rgenerateAudio\x88\x01\x01\x42\x0b\n\t_durationB\x0f\n\r_aspect_ratioB\r\n\x0b_resolutionB\x12\n\x10_storage_optionsB\x11\n\x0f_generate_audioJ\x04\x08\x0f\x10\x10\"8\n\x17GetDeferredVideoRequest\x12\x1d\n\nrequest_id\x18\x01 \x01(\tR\trequestId\"\xd8\x01\n\rVideoResponse\x12-\n\x05video\x18\x01 \x01(\x0b\x32\x17.xai_api.GeneratedVideoR\x05video\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12,\n\x05usage\x18\x03 \x01(\x0b\x32\x16.xai_api.SamplingUsageR\x05usage\x12.\n\x05\x65rror\x18\x06 \x01(\x0b\x32\x13.xai_api.VideoErrorH\x00R\x05\x65rror\x88\x01\x01\x12\x1a\n\x08progress\x18\x07 \x01(\x05R\x08progressB\x08\n\x06_error\"\xf4\x01\n\x0eGeneratedVideo\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\x1a\n\x08\x64uration\x18\x04 \x01(\x05R\x08\x64uration\x12-\n\x12respect_moderation\x18\x05 \x01(\x08R\x11respectModeration\x12\x39\n\x0b\x66ile_output\x18\x06 \x01(\x0b\x32\x13.xai_api.FileOutputH\x00R\nfileOutput\x88\x01\x01\x12(\n\rstorage_error\x18\x07 \x01(\tH\x01R\x0cstorageError\x88\x01\x01\x42\x0e\n\x0c_file_outputB\x10\n\x0e_storage_error\"\x91\x01\n\x18GetDeferredVideoResponse\x12/\n\x06status\x18\x01 \x01(\x0e\x32\x17.xai_api.DeferredStatusR\x06status\x12\x37\n\x08response\x18\x02 \x01(\x0b\x32\x16.xai_api.VideoResponseH\x00R\x08response\x88\x01\x01\x42\x0b\n\t_response\":\n\nVideoError\x12\x12\n\x04\x63ode\x18\x01 \x01(\tR\x04\x63ode\x12\x18\n\x07message\x18\x02 \x01(\tR\x07message\"\x81\x02\n\x12\x45xtendVideoRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05video\x18\x02 \x01(\x0b\x32\x18.xai_api.VideoUrlContentR\x05video\x12\x14\n\x05model\x18\x03 \x01(\tR\x05model\x12\x1f\n\x08\x64uration\x18\x04 \x01(\x05H\x00R\x08\x64uration\x88\x01\x01\x12\x45\n\x0fstorage_options\x18\x06 \x01(\x0b\x32\x17.xai_api.StorageOptionsH\x01R\x0estorageOptions\x88\x01\x01\x42\x0b\n\t_durationB\x12\n\x10_storage_optionsJ\x04\x08\x07\x10\x08*\xfc\x01\n\x10VideoAspectRatio\x12\"\n\x1eVIDEO_ASPECT_RATIO_UNSPECIFIED\x10\x00\x12\x1a\n\x16VIDEO_ASPECT_RATIO_1_1\x10\x01\x12\x1b\n\x17VIDEO_ASPECT_RATIO_16_9\x10\x02\x12\x1b\n\x17VIDEO_ASPECT_RATIO_9_16\x10\x03\x12\x1a\n\x16VIDEO_ASPECT_RATIO_4_3\x10\x04\x12\x1a\n\x16VIDEO_ASPECT_RATIO_3_4\x10\x05\x12\x1a\n\x16VIDEO_ASPECT_RATIO_3_2\x10\x06\x12\x1a\n\x16VIDEO_ASPECT_RATIO_2_3\x10\x07*i\n\x0fVideoResolution\x12 \n\x1cVIDEO_RESOLUTION_UNSPECIFIED\x10\x00\x12\x19\n\x15VIDEO_RESOLUTION_480P\x10\x01\x12\x19\n\x15VIDEO_RESOLUTION_720P\x10\x02\x32\x82\x02\n\x05Video\x12P\n\rGenerateVideo\x12\x1d.xai_api.GenerateVideoRequest\x1a\x1e.xai_api.StartDeferredResponse\"\x00\x12L\n\x0b\x45xtendVideo\x12\x1b.xai_api.ExtendVideoRequest\x1a\x1e.xai_api.StartDeferredResponse\"\x00\x12Y\n\x10GetDeferredVideo\x12 .xai_api.GetDeferredVideoRequest\x1a!.xai_api.GetDeferredVideoResponse\"\x00\x42Q\n\x0b\x63om.xai_apiB\nVideoProtoP\x01\xa2\x02\x03XXX\xaa\x02\x06XaiApi\xca\x02\x06XaiApi\xe2\x02\x12XaiApi\\GPBMetadata\xea\x02\x06XaiApib\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16xai/api/v1/video.proto\x12\x07xai_api\x1a\x19xai/api/v1/deferred.proto\x1a\x16xai/api/v1/image.proto\x1a\x16xai/api/v1/usage.proto\"J\n\x0fVideoUrlContent\x12\x12\n\x03url\x18\x01 \x01(\tH\x00R\x03url\x12\x19\n\x07\x66ile_id\x18\x02 \x01(\tH\x00R\x06\x66ileIdB\x08\n\x06source\">\n\x0f\x41udioUrlContent\x12\x1b\n\x08voice_id\x18\x02 \x01(\tH\x00R\x07voiceIdB\x08\n\x06sourceJ\x04\x08\x01\x10\x02\"\x9e\x05\n\x14GenerateVideoRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05image\x18\x02 \x01(\x0b\x32\x18.xai_api.ImageUrlContentR\x05image\x12\x14\n\x05model\x18\x03 \x01(\tR\x05model\x12\x1f\n\x08\x64uration\x18\x04 \x01(\x05H\x00R\x08\x64uration\x88\x01\x01\x12.\n\x05video\x18\x06 \x01(\x0b\x32\x18.xai_api.VideoUrlContentR\x05video\x12\x41\n\x0c\x61spect_ratio\x18\x07 \x01(\x0e\x32\x19.xai_api.VideoAspectRatioH\x01R\x0b\x61spectRatio\x88\x01\x01\x12=\n\nresolution\x18\x08 \x01(\x0e\x32\x18.xai_api.VideoResolutionH\x02R\nresolution\x88\x01\x01\x12\x43\n\x10reference_images\x18\r \x03(\x0b\x32\x18.xai_api.ImageUrlContentR\x0freferenceImages\x12\x45\n\x0fstorage_options\x18\x0e \x01(\x0b\x32\x17.xai_api.StorageOptionsH\x03R\x0estorageOptions\x88\x01\x01\x12\x43\n\x10reference_audios\x18\x10 \x03(\x0b\x32\x18.xai_api.AudioUrlContentR\x0freferenceAudios\x12*\n\x0egenerate_audio\x18\x11 \x01(\x08H\x04R\rgenerateAudio\x88\x01\x01\x42\x0b\n\t_durationB\x0f\n\r_aspect_ratioB\r\n\x0b_resolutionB\x12\n\x10_storage_optionsB\x11\n\x0f_generate_audioJ\x04\x08\x0f\x10\x10\"8\n\x17GetDeferredVideoRequest\x12\x1d\n\nrequest_id\x18\x01 \x01(\tR\trequestId\"\xd8\x01\n\rVideoResponse\x12-\n\x05video\x18\x01 \x01(\x0b\x32\x17.xai_api.GeneratedVideoR\x05video\x12\x14\n\x05model\x18\x02 \x01(\tR\x05model\x12,\n\x05usage\x18\x03 \x01(\x0b\x32\x16.xai_api.SamplingUsageR\x05usage\x12.\n\x05\x65rror\x18\x06 \x01(\x0b\x32\x13.xai_api.VideoErrorH\x00R\x05\x65rror\x88\x01\x01\x12\x1a\n\x08progress\x18\x07 \x01(\x05R\x08progressB\x08\n\x06_error\"\xf4\x01\n\x0eGeneratedVideo\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\x1a\n\x08\x64uration\x18\x04 \x01(\x05R\x08\x64uration\x12-\n\x12respect_moderation\x18\x05 \x01(\x08R\x11respectModeration\x12\x39\n\x0b\x66ile_output\x18\x06 \x01(\x0b\x32\x13.xai_api.FileOutputH\x00R\nfileOutput\x88\x01\x01\x12(\n\rstorage_error\x18\x07 \x01(\tH\x01R\x0cstorageError\x88\x01\x01\x42\x0e\n\x0c_file_outputB\x10\n\x0e_storage_error\"\x91\x01\n\x18GetDeferredVideoResponse\x12/\n\x06status\x18\x01 \x01(\x0e\x32\x17.xai_api.DeferredStatusR\x06status\x12\x37\n\x08response\x18\x02 \x01(\x0b\x32\x16.xai_api.VideoResponseH\x00R\x08response\x88\x01\x01\x42\x0b\n\t_response\":\n\nVideoError\x12\x12\n\x04\x63ode\x18\x01 \x01(\tR\x04\x63ode\x12\x18\n\x07message\x18\x02 \x01(\tR\x07message\"\x81\x02\n\x12\x45xtendVideoRequest\x12\x16\n\x06prompt\x18\x01 \x01(\tR\x06prompt\x12.\n\x05video\x18\x02 \x01(\x0b\x32\x18.xai_api.VideoUrlContentR\x05video\x12\x14\n\x05model\x18\x03 \x01(\tR\x05model\x12\x1f\n\x08\x64uration\x18\x04 \x01(\x05H\x00R\x08\x64uration\x88\x01\x01\x12\x45\n\x0fstorage_options\x18\x06 \x01(\x0b\x32\x17.xai_api.StorageOptionsH\x01R\x0estorageOptions\x88\x01\x01\x42\x0b\n\t_durationB\x12\n\x10_storage_optionsJ\x04\x08\x07\x10\x08*\xfc\x01\n\x10VideoAspectRatio\x12\"\n\x1eVIDEO_ASPECT_RATIO_UNSPECIFIED\x10\x00\x12\x1a\n\x16VIDEO_ASPECT_RATIO_1_1\x10\x01\x12\x1b\n\x17VIDEO_ASPECT_RATIO_16_9\x10\x02\x12\x1b\n\x17VIDEO_ASPECT_RATIO_9_16\x10\x03\x12\x1a\n\x16VIDEO_ASPECT_RATIO_4_3\x10\x04\x12\x1a\n\x16VIDEO_ASPECT_RATIO_3_4\x10\x05\x12\x1a\n\x16VIDEO_ASPECT_RATIO_3_2\x10\x06\x12\x1a\n\x16VIDEO_ASPECT_RATIO_2_3\x10\x07*\x85\x01\n\x0fVideoResolution\x12 \n\x1cVIDEO_RESOLUTION_UNSPECIFIED\x10\x00\x12\x19\n\x15VIDEO_RESOLUTION_480P\x10\x01\x12\x19\n\x15VIDEO_RESOLUTION_720P\x10\x02\x12\x1a\n\x16VIDEO_RESOLUTION_1080P\x10\x03\x32\x82\x02\n\x05Video\x12P\n\rGenerateVideo\x12\x1d.xai_api.GenerateVideoRequest\x1a\x1e.xai_api.StartDeferredResponse\"\x00\x12L\n\x0b\x45xtendVideo\x12\x1b.xai_api.ExtendVideoRequest\x1a\x1e.xai_api.StartDeferredResponse\"\x00\x12Y\n\x10GetDeferredVideo\x12 .xai_api.GetDeferredVideoRequest\x1a!.xai_api.GetDeferredVideoResponse\"\x00\x42Q\n\x0b\x63om.xai_apiB\nVideoProtoP\x01\xa2\x02\x03XXX\xaa\x02\x06XaiApi\xca\x02\x06XaiApi\xe2\x02\x12XaiApi\\GPBMetadata\xea\x02\x06XaiApib\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -37,8 +37,8 @@ _globals['DESCRIPTOR']._serialized_options = b'\n\013com.xai_apiB\nVideoProtoP\001\242\002\003XXX\252\002\006XaiApi\312\002\006XaiApi\342\002\022XaiApi\\GPBMetadata\352\002\006XaiApi' _globals['_VIDEOASPECTRATIO']._serialized_start=1916 _globals['_VIDEOASPECTRATIO']._serialized_end=2168 - _globals['_VIDEORESOLUTION']._serialized_start=2170 - _globals['_VIDEORESOLUTION']._serialized_end=2275 + _globals['_VIDEORESOLUTION']._serialized_start=2171 + _globals['_VIDEORESOLUTION']._serialized_end=2304 _globals['_VIDEOURLCONTENT']._serialized_start=110 _globals['_VIDEOURLCONTENT']._serialized_end=184 _globals['_AUDIOURLCONTENT']._serialized_start=186 @@ -57,6 +57,6 @@ _globals['_VIDEOERROR']._serialized_end=1653 _globals['_EXTENDVIDEOREQUEST']._serialized_start=1656 _globals['_EXTENDVIDEOREQUEST']._serialized_end=1913 - _globals['_VIDEO']._serialized_start=2278 - _globals['_VIDEO']._serialized_end=2536 + _globals['_VIDEO']._serialized_start=2307 + _globals['_VIDEO']._serialized_end=2565 # @@protoc_insertion_point(module_scope) diff --git a/src/xai_sdk/proto/v6/video_pb2.pyi b/src/xai_sdk/proto/v6/video_pb2.pyi index 70bec33..bbe457b 100644 --- a/src/xai_sdk/proto/v6/video_pb2.pyi +++ b/src/xai_sdk/proto/v6/video_pb2.pyi @@ -26,6 +26,7 @@ class VideoResolution(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): VIDEO_RESOLUTION_UNSPECIFIED: _ClassVar[VideoResolution] VIDEO_RESOLUTION_480P: _ClassVar[VideoResolution] VIDEO_RESOLUTION_720P: _ClassVar[VideoResolution] + VIDEO_RESOLUTION_1080P: _ClassVar[VideoResolution] VIDEO_ASPECT_RATIO_UNSPECIFIED: VideoAspectRatio VIDEO_ASPECT_RATIO_1_1: VideoAspectRatio VIDEO_ASPECT_RATIO_16_9: VideoAspectRatio @@ -37,6 +38,7 @@ VIDEO_ASPECT_RATIO_2_3: VideoAspectRatio VIDEO_RESOLUTION_UNSPECIFIED: VideoResolution VIDEO_RESOLUTION_480P: VideoResolution VIDEO_RESOLUTION_720P: VideoResolution +VIDEO_RESOLUTION_1080P: VideoResolution class VideoUrlContent(_message.Message): __slots__ = ("url", "file_id") diff --git a/src/xai_sdk/sync/video.py b/src/xai_sdk/sync/video.py index cd3ef7e..a095821 100644 --- a/src/xai_sdk/sync/video.py +++ b/src/xai_sdk/sync/video.py @@ -9,7 +9,7 @@ from ..poll_timer import PollTimer from ..proto import batch_pb2, deferred_pb2, image_pb2, video_pb2 from ..telemetry import get_tracer -from ..types import VideoGenerationModel +from ..types import ReferenceAudio, VideoGenerationModel from ..video import ( DEFAULT_VIDEO_POLL_INTERVAL, DEFAULT_VIDEO_TIMEOUT, @@ -47,6 +47,8 @@ def prepare( resolution: Optional[VideoResolution] = None, reference_image_urls: Optional[Sequence[str]] = None, reference_image_file_ids: Optional[Sequence[str]] = None, + reference_audios: Optional[Sequence[ReferenceAudio]] = None, + generate_audio: Optional[bool] = None, storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> batch_pb2.BatchRequest: """Prepares a video generation request for batch processing. @@ -81,6 +83,14 @@ def prepare( reference-to-video (R2V) generation. May be combined with ``reference_image_urls`` to mix URL/base64 and file-ID references in the same request. + reference_audios: Optional list of reference audio sources for + reference-to-video (R2V) generation. Each entry is a TypedDict + such as ``{"voice_id": "ara"}``. See the + `voice catalog `_ + for available presets. At most three entries. Only supported + for ``grok-imagine-video-1.5``. + generate_audio: Whether the generated video includes an audio track. + Defaults to ``True``. Set to ``False`` for a silent video. storage_options: Persist the result to the Files API. Accepts a dict with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. Set ``public_url`` to also create a publicly shareable URL. @@ -133,6 +143,8 @@ def prepare( resolution=resolution, reference_image_urls=reference_image_urls, reference_image_file_ids=reference_image_file_ids, + reference_audios=reference_audios, + generate_audio=generate_audio, storage_options=storage_options, ) @@ -209,6 +221,8 @@ def start( resolution: Optional[VideoResolution] = None, reference_image_urls: Optional[Sequence[str]] = None, reference_image_file_ids: Optional[Sequence[str]] = None, + reference_audios: Optional[Sequence[ReferenceAudio]] = None, + generate_audio: Optional[bool] = None, storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> deferred_pb2.StartDeferredResponse: """Starts a video generation request and returns a request_id for polling. @@ -227,6 +241,8 @@ def start( resolution=resolution, reference_image_urls=reference_image_urls, reference_image_file_ids=reference_image_file_ids, + reference_audios=reference_audios, + generate_audio=generate_audio, storage_options=storage_options, ) @@ -256,6 +272,8 @@ def generate( resolution: Optional[VideoResolution] = None, reference_image_urls: Optional[Sequence[str]] = None, reference_image_file_ids: Optional[Sequence[str]] = None, + reference_audios: Optional[Sequence[ReferenceAudio]] = None, + generate_audio: Optional[bool] = None, storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, timeout: Optional[datetime.timedelta] = None, interval: Optional[datetime.timedelta] = None, @@ -304,6 +322,14 @@ def generate( reference-to-video (R2V) generation. May be combined with `reference_image_urls` to mix URL/base64 and file-ID references in the same request. + reference_audios: Optional list of reference audio sources for + reference-to-video (R2V) generation. Each entry is a TypedDict + such as ``{"voice_id": "ara"}``. See the + `voice catalog `_ + for available presets. At most three entries. Only supported + for ``grok-imagine-video-1.5``. + generate_audio: Whether the generated video includes an audio track. + Defaults to `True`. Set to `False` for a silent video. storage_options: Persist the result to the Files API. Accepts a dict with a required ``filename`` and optional ``expires_after`` and ``public_url`` keys. Set ``public_url`` to also create a publicly shareable URL. @@ -384,6 +410,8 @@ def generate( resolution=resolution, reference_image_urls=reference_image_urls, reference_image_file_ids=reference_image_file_ids, + reference_audios=reference_audios, + generate_audio=generate_audio, storage_options=storage_options, ) diff --git a/src/xai_sdk/types/__init__.py b/src/xai_sdk/types/__init__.py index 0cef157..3bd6d4e 100644 --- a/src/xai_sdk/types/__init__.py +++ b/src/xai_sdk/types/__init__.py @@ -12,7 +12,7 @@ from .common import ServiceTier from .image import ImageAspectRatio, ImageFormat, ImageQuality, ImageResolution from .model import AllModels, ChatModel, ImageGenerationModel, VideoGenerationModel -from .video import VideoAspectRatio, VideoResolution +from .video import ReferenceAudio, ReferenceAudioValidator, VideoAspectRatio, VideoResolution, VoiceAudioRef __all__ = [ "AgentCount", @@ -29,10 +29,13 @@ "IncludeOption", "IncludeOptionMap", "ReasoningEffort", + "ReferenceAudio", + "ReferenceAudioValidator", "ResponseFormat", "ServiceTier", "ToolMode", "VideoAspectRatio", "VideoGenerationModel", "VideoResolution", + "VoiceAudioRef", ] diff --git a/src/xai_sdk/types/model.py b/src/xai_sdk/types/model.py index e02167c..25332ed 100644 --- a/src/xai_sdk/types/model.py +++ b/src/xai_sdk/types/model.py @@ -45,13 +45,12 @@ ImageGenerationModel: TypeAlias = Literal[ "grok-imagine-image", "grok-imagine-image-2.0", - "grok-imagine-image-pro", "grok-imagine-image-quality", ] VideoGenerationModel: TypeAlias = Literal[ "grok-imagine-video", - "grok-imagine-video-1.5-preview", + "grok-imagine-video-1.5", ] AllModels: TypeAlias = Union[ diff --git a/src/xai_sdk/types/video.py b/src/xai_sdk/types/video.py index 3c9067b..c9e21c4 100644 --- a/src/xai_sdk/types/video.py +++ b/src/xai_sdk/types/video.py @@ -1,12 +1,18 @@ -from typing import Literal, TypeAlias +from typing import Annotated, Literal, TypeAlias + +from pydantic import StringConstraints, TypeAdapter +from typing_extensions import TypedDict from ..proto import video_pb2 __all__ = [ + "ReferenceAudio", + "ReferenceAudioValidator", "VideoAspectRatio", "VideoAspectRatioMap", "VideoResolution", "VideoResolutionMap", + "VoiceAudioRef", ] # Aspect ratio for video generation. @@ -21,7 +27,31 @@ ] # Resolution for video generation. -VideoResolution: TypeAlias = Literal["480p", "720p"] +VideoResolution: TypeAlias = Literal["480p", "720p", "1080p"] + +# Non-empty string after stripping whitespace (Pydantic runtime check). +_NonEmptyStr = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] + + +class VoiceAudioRef(TypedDict): + """Reference audio that uses a preset voice from the xAI voice catalog. + + ``voice_id`` is a first-party preset id shared with text-to-speech + (e.g. ``"ara"``, ``"leo"``). See the + `voice catalog `_ + for the full list of available presets. + """ + + voice_id: _NonEmptyStr + + +# Structured reference-audio entry for ``reference_audios``. Additional source +# kinds can be unioned in later without changing the top-level parameter. +ReferenceAudio: TypeAlias = VoiceAudioRef + +# Runtime validation for public ``reference_audios`` entries. +ReferenceAudioValidator = TypeAdapter(ReferenceAudio) + VideoAspectRatioMap: dict[VideoAspectRatio, "video_pb2.VideoAspectRatio"] = { "1:1": video_pb2.VideoAspectRatio.VIDEO_ASPECT_RATIO_1_1, @@ -36,4 +66,5 @@ VideoResolutionMap: dict[VideoResolution, "video_pb2.VideoResolution"] = { "480p": video_pb2.VideoResolution.VIDEO_RESOLUTION_480P, "720p": video_pb2.VideoResolution.VIDEO_RESOLUTION_720P, + "1080p": video_pb2.VideoResolution.VIDEO_RESOLUTION_1080P, } diff --git a/src/xai_sdk/video.py b/src/xai_sdk/video.py index b110896..ce227da 100644 --- a/src/xai_sdk/video.py +++ b/src/xai_sdk/video.py @@ -9,7 +9,14 @@ from .proto import image_pb2, usage_pb2, video_pb2, video_pb2_grpc from .telemetry import should_disable_sensitive_attributes from .types import VideoGenerationModel -from .types.video import VideoAspectRatio, VideoAspectRatioMap, VideoResolution, VideoResolutionMap +from .types.video import ( + ReferenceAudio, + ReferenceAudioValidator, + VideoAspectRatio, + VideoAspectRatioMap, + VideoResolution, + VideoResolutionMap, +) DEFAULT_VIDEO_POLL_INTERVAL = datetime.timedelta(seconds=1) DEFAULT_VIDEO_TIMEOUT = datetime.timedelta(minutes=10) @@ -146,6 +153,8 @@ def _make_generate_request( resolution: Optional[VideoResolution], reference_image_urls: Optional[Sequence[str]], reference_image_file_ids: Optional[Sequence[str]] = None, + reference_audios: Optional[Sequence[ReferenceAudio]] = None, + generate_audio: Optional[bool] = None, storage_options: Optional[Union[StorageOptions, image_pb2.StorageOptions]] = None, ) -> video_pb2.GenerateVideoRequest: _validate_video_inputs( @@ -171,6 +180,9 @@ def _make_generate_request( if resolution is not None: request.resolution = convert_video_resolution_to_pb(resolution) _set_reference_images(request, reference_image_urls, reference_image_file_ids) + _set_reference_audios(request, reference_audios) + if generate_audio is not None: + request.generate_audio = generate_audio if storage_options is not None: request.storage_options.CopyFrom(_resolve_storage_options_pb(storage_options)) @@ -210,6 +222,26 @@ def _set_reference_images( ) +def _resolve_reference_audio(entry: ReferenceAudio) -> video_pb2.AudioUrlContent: + """Converts a public ``reference_audios`` entry into the request wire form. + + Validates with :data:`~xai_sdk.types.video.ReferenceAudioValidator` (Pydantic). + Today only ``{"voice_id": ...}`` is supported; new source kinds should be + added here without changing the top-level ``reference_audios`` parameter. + """ + validated = ReferenceAudioValidator.validate_python(entry) + return video_pb2.AudioUrlContent(voice_id=validated["voice_id"]) + + +def _set_reference_audios( + request: video_pb2.GenerateVideoRequest, + reference_audios: Optional[Sequence[ReferenceAudio]], +) -> None: + """Populates `reference_audios` from validated TypedDict entries.""" + if reference_audios is not None: + request.reference_audios.extend(_resolve_reference_audio(entry) for entry in reference_audios) + + def _make_span_request_attributes(request: video_pb2.GenerateVideoRequest) -> dict[str, Any]: """Creates the video generation span request attributes.""" attributes: dict[str, Any] = { @@ -244,6 +276,8 @@ def _make_span_request_attributes(request: video_pb2.GenerateVideoRequest) -> di attributes["gen_ai.request.video.resolution"] = ( video_pb2.VideoResolution.Name(request.resolution).removeprefix("VIDEO_RESOLUTION_").lower() ) + if request.HasField("generate_audio"): + attributes["gen_ai.request.video.generate_audio"] = request.generate_audio return attributes diff --git a/tests/aio/video_test.py b/tests/aio/video_test.py index ded74c7..c32e6cf 100644 --- a/tests/aio/video_test.py +++ b/tests/aio/video_test.py @@ -1,6 +1,7 @@ import warnings from unittest import mock +import pydantic import pytest import pytest_asyncio from google.protobuf import timestamp_pb2 @@ -9,7 +10,12 @@ from xai_sdk import AsyncClient from xai_sdk.cost import USD_PER_TICK from xai_sdk.proto import batch_pb2, deferred_pb2, image_pb2, usage_pb2, video_pb2 -from xai_sdk.video import VideoGenerationError, VideoResponse +from xai_sdk.video import ( + VideoGenerationError, + VideoResponse, + _make_generate_request, + _make_span_request_attributes, +) from .. import server @@ -110,6 +116,156 @@ async def test_prepare_with_reference_image_urls(client: AsyncClient): assert batch_req.video_request.reference_images[1].image_url == ref_urls[1] +@pytest.mark.asyncio(loop_scope="session") +async def test_generate_passes_reference_audios(client: AsyncClient): + server.clear_last_video_request() + + await client.video.generate( + prompt="foo", + model="grok-imagine-video", + reference_audios=[{"voice_id": "ara"}, {"voice_id": "leo"}], + ) + + request = server.get_last_video_request() + assert request is not None + assert [audio.voice_id for audio in request.reference_audios] == ["ara", "leo"] + + +@pytest.mark.asyncio(loop_scope="session") +async def test_generate_strips_reference_audio_voice_id_whitespace(client: AsyncClient): + server.clear_last_video_request() + + await client.video.generate( + prompt="foo", + model="grok-imagine-video", + reference_audios=[{"voice_id": " ara "}], + ) + + request = server.get_last_video_request() + assert request is not None + assert request.reference_audios[0].voice_id == "ara" + + +@pytest.mark.asyncio(loop_scope="session") +@pytest.mark.parametrize( + "entry", + [ + pytest.param({"voice_id": ""}, id="empty"), + pytest.param({"voice_id": " "}, id="whitespace"), + pytest.param({}, id="missing-voice_id"), + pytest.param({"voice_id": 123}, id="wrong-type"), + ], +) +async def test_generate_rejects_invalid_reference_audio(client: AsyncClient, entry): + with pytest.raises(pydantic.ValidationError): + await client.video.generate( + prompt="foo", + model="grok-imagine-video", + reference_audios=[entry], # type: ignore[list-item] + ) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_generate_passes_generate_audio(client: AsyncClient): + server.clear_last_video_request() + + await client.video.generate(prompt="foo", model="grok-imagine-video", generate_audio=False) + + request = server.get_last_video_request() + assert request is not None + assert request.HasField("generate_audio") + assert request.generate_audio is False + + +@pytest.mark.asyncio(loop_scope="session") +async def test_generate_omits_reference_audios_and_generate_audio_by_default(client: AsyncClient): + server.clear_last_video_request() + + await client.video.generate(prompt="foo", model="grok-imagine-video") + + request = server.get_last_video_request() + assert request is not None + assert len(request.reference_audios) == 0 + assert not request.HasField("generate_audio") + + +@pytest.mark.asyncio(loop_scope="session") +async def test_prepare_passes_generate_audio(client: AsyncClient): + batch_req = client.video.prepare( + prompt="Generate with audio control", + model="grok-imagine-video", + generate_audio=True, + ) + + assert batch_req.video_request.HasField("generate_audio") + assert batch_req.video_request.generate_audio is True + assert len(batch_req.video_request.reference_audios) == 0 + + +@pytest.mark.asyncio(loop_scope="session") +async def test_prepare_passes_reference_audios(client: AsyncClient): + batch_req = client.video.prepare( + prompt="Generate from references", + model="grok-imagine-video", + reference_audios=[{"voice_id": "ara"}, {"voice_id": "leo"}], + ) + + assert [audio.voice_id for audio in batch_req.video_request.reference_audios] == ["ara", "leo"] + + +@pytest.mark.asyncio(loop_scope="session") +async def test_prepare_strips_reference_audio_voice_id_whitespace(client: AsyncClient): + batch_req = client.video.prepare( + prompt="foo", + model="grok-imagine-video", + reference_audios=[{"voice_id": " ara "}], + ) + + assert batch_req.video_request.reference_audios[0].voice_id == "ara" + + +@pytest.mark.asyncio(loop_scope="session") +@pytest.mark.parametrize( + "entry", + [ + pytest.param({"voice_id": ""}, id="empty"), + pytest.param({"voice_id": " "}, id="whitespace"), + pytest.param({}, id="missing-voice_id"), + pytest.param({"voice_id": 123}, id="wrong-type"), + ], +) +async def test_prepare_rejects_invalid_reference_audio(client: AsyncClient, entry): + with pytest.raises(pydantic.ValidationError): + client.video.prepare( + prompt="foo", + model="grok-imagine-video", + reference_audios=[entry], # type: ignore[list-item] + ) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_prepare_omits_reference_audios_by_default(client: AsyncClient): + batch_req = client.video.prepare(prompt="foo", model="grok-imagine-video") + + assert len(batch_req.video_request.reference_audios) == 0 + + +def test_generate_audio_included_in_span_request_attributes(): + request = _make_generate_request( + prompt="foo", + model="grok-imagine-video", + image_url=None, + video_url=None, + duration=None, + aspect_ratio=None, + resolution=None, + reference_image_urls=None, + generate_audio=False, + ) + attributes = _make_span_request_attributes(request) + assert attributes["gen_ai.request.video.generate_audio"] is False + + @mock.patch("xai_sdk.aio.video.tracer") @pytest.mark.asyncio(loop_scope="session") async def test_generate_creates_span_with_correct_attributes(mock_tracer: mock.MagicMock, client: AsyncClient): diff --git a/tests/sync/video_test.py b/tests/sync/video_test.py index e2a6533..80954d9 100644 --- a/tests/sync/video_test.py +++ b/tests/sync/video_test.py @@ -1,6 +1,7 @@ import warnings from unittest import mock +import pydantic import pytest from google.protobuf import timestamp_pb2 from opentelemetry.trace import SpanKind @@ -8,7 +9,12 @@ from xai_sdk import Client from xai_sdk.cost import USD_PER_TICK from xai_sdk.proto import batch_pb2, deferred_pb2, image_pb2, usage_pb2, video_pb2 -from xai_sdk.video import VideoGenerationError, VideoResponse +from xai_sdk.video import ( + VideoGenerationError, + VideoResponse, + _make_generate_request, + _make_span_request_attributes, +) from .. import server @@ -103,6 +109,161 @@ def test_prepare_with_reference_image_urls(client: Client): assert batch_req.video_request.reference_images[1].image_url == ref_urls[1] +def test_generate_passes_reference_audios(client: Client): + server.clear_last_video_request() + + client.video.generate( + prompt="foo", + model="grok-imagine-video", + reference_audios=[{"voice_id": "ara"}, {"voice_id": "leo"}], + ) + + request = server.get_last_video_request() + assert request is not None + assert [audio.voice_id for audio in request.reference_audios] == ["ara", "leo"] + + +def test_generate_strips_reference_audio_voice_id_whitespace(client: Client): + server.clear_last_video_request() + + client.video.generate( + prompt="foo", + model="grok-imagine-video", + reference_audios=[{"voice_id": " ara "}], + ) + + request = server.get_last_video_request() + assert request is not None + assert request.reference_audios[0].voice_id == "ara" + + +@pytest.mark.parametrize( + "entry", + [ + pytest.param({"voice_id": ""}, id="empty"), + pytest.param({"voice_id": " "}, id="whitespace"), + pytest.param({}, id="missing-voice_id"), + pytest.param({"voice_id": 123}, id="wrong-type"), + ], +) +def test_generate_rejects_invalid_reference_audio(client: Client, entry): + with pytest.raises(pydantic.ValidationError): + client.video.generate( + prompt="foo", + model="grok-imagine-video", + reference_audios=[entry], # type: ignore[list-item] + ) + + +def test_generate_passes_generate_audio(client: Client): + server.clear_last_video_request() + + client.video.generate(prompt="foo", model="grok-imagine-video", generate_audio=False) + + request = server.get_last_video_request() + assert request is not None + assert request.HasField("generate_audio") + assert request.generate_audio is False + + +def test_generate_omits_reference_audios_and_generate_audio_by_default(client: Client): + server.clear_last_video_request() + + client.video.generate(prompt="foo", model="grok-imagine-video") + + request = server.get_last_video_request() + assert request is not None + assert len(request.reference_audios) == 0 + assert not request.HasField("generate_audio") + + +def test_prepare_passes_generate_audio(client: Client): + batch_req = client.video.prepare( + prompt="Generate with audio control", + model="grok-imagine-video", + generate_audio=True, + ) + + assert batch_req.video_request.HasField("generate_audio") + assert batch_req.video_request.generate_audio is True + assert len(batch_req.video_request.reference_audios) == 0 + + +def test_prepare_passes_reference_audios(client: Client): + batch_req = client.video.prepare( + prompt="Generate from references", + model="grok-imagine-video", + reference_audios=[{"voice_id": "ara"}, {"voice_id": "leo"}], + ) + + assert [audio.voice_id for audio in batch_req.video_request.reference_audios] == ["ara", "leo"] + + +def test_prepare_strips_reference_audio_voice_id_whitespace(client: Client): + batch_req = client.video.prepare( + prompt="foo", + model="grok-imagine-video", + reference_audios=[{"voice_id": " ara "}], + ) + + assert batch_req.video_request.reference_audios[0].voice_id == "ara" + + +@pytest.mark.parametrize( + "entry", + [ + pytest.param({"voice_id": ""}, id="empty"), + pytest.param({"voice_id": " "}, id="whitespace"), + pytest.param({}, id="missing-voice_id"), + pytest.param({"voice_id": 123}, id="wrong-type"), + ], +) +def test_prepare_rejects_invalid_reference_audio(client: Client, entry): + with pytest.raises(pydantic.ValidationError): + client.video.prepare( + prompt="foo", + model="grok-imagine-video", + reference_audios=[entry], # type: ignore[list-item] + ) + + +def test_prepare_omits_reference_audios_by_default(client: Client): + batch_req = client.video.prepare(prompt="foo", model="grok-imagine-video") + + assert len(batch_req.video_request.reference_audios) == 0 + + +def test_generate_audio_included_in_span_request_attributes(): + request = _make_generate_request( + prompt="foo", + model="grok-imagine-video", + image_url=None, + video_url=None, + duration=None, + aspect_ratio=None, + resolution=None, + reference_image_urls=None, + generate_audio=False, + ) + attributes = _make_span_request_attributes(request) + assert attributes["gen_ai.request.video.generate_audio"] is False + + +def test_generate_audio_omitted_from_span_request_attributes_by_default(): + request = _make_generate_request( + prompt="foo", + model="grok-imagine-video", + image_url=None, + video_url=None, + duration=None, + aspect_ratio=None, + resolution=None, + reference_image_urls=None, + ) + attributes = _make_span_request_attributes(request) + assert "gen_ai.request.video.generate_audio" not in attributes + + @mock.patch("xai_sdk.sync.video.tracer") def test_generate_creates_span_with_correct_attributes(mock_tracer: mock.MagicMock, client: Client): mock_span = mock.MagicMock() diff --git a/uv.lock b/uv.lock index 1dbe689..3672596 100644 --- a/uv.lock +++ b/uv.lock @@ -1835,6 +1835,7 @@ dependencies = [ { name = "protobuf" }, { name = "pydantic" }, { name = "requests" }, + { name = "typing-extensions" }, ] [package.optional-dependencies] @@ -1875,6 +1876,7 @@ requires-dist = [ { name = "protobuf", specifier = ">=5.29.4,<7" }, { name = "pydantic", specifier = ">=2.5.3,<3" }, { name = "requests", specifier = ">=2.31.0,<3" }, + { name = "typing-extensions", specifier = ">=4.14.0,<5" }, ] provides-extras = ["telemetry-grpc", "telemetry-http"]