diff --git a/.sdk.json b/.sdk.json index fdee532..5a87530 100644 --- a/.sdk.json +++ b/.sdk.json @@ -1,5 +1,5 @@ { - "id": "b2d6ef4f-f33d-419f-85e6-a388096b49c1", + "id": "6b61dfff-c839-4c96-affc-fe4f3f004aff", "tracked_paths": [ { "editable": true, @@ -273,6 +273,18 @@ "editable": true, "path": "magic_hour/resources/v1/files/upload_urls/client.py" }, + { + "editable": true, + "path": "magic_hour/resources/v1/head_swap/README.md" + }, + { + "editable": true, + "path": "magic_hour/resources/v1/head_swap/__init__.py" + }, + { + "editable": true, + "path": "magic_hour/resources/v1/head_swap/client.py" + }, { "editable": true, "path": "magic_hour/resources/v1/image_background_remover/README.md" @@ -469,6 +481,10 @@ "editable": false, "path": "magic_hour/types/models/v1_files_upload_urls_create_response_items_item.py" }, + { + "editable": false, + "path": "magic_hour/types/models/v1_head_swap_create_response.py" + }, { "editable": false, "path": "magic_hour/types/models/v1_image_background_remover_create_response.py" @@ -717,6 +733,14 @@ "editable": false, "path": "magic_hour/types/params/v1_files_upload_urls_create_body_items_item.py" }, + { + "editable": false, + "path": "magic_hour/types/params/v1_head_swap_create_body.py" + }, + { + "editable": false, + "path": "magic_hour/types/params/v1_head_swap_create_body_assets.py" + }, { "editable": false, "path": "magic_hour/types/params/v1_image_background_remover_create_body.py" @@ -857,6 +881,10 @@ "editable": false, "path": "tests/test_v1_files_upload_urls_client.py" }, + { + "editable": false, + "path": "tests/test_v1_head_swap_client.py" + }, { "editable": false, "path": "tests/test_v1_image_background_remover_client.py" diff --git a/README.md b/README.md index 05e2b29..120c153 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,11 @@ download_urls = result.downloads - [create](magic_hour/resources/v1/files/upload_urls/README.md#create) - Generate asset upload urls +### [v1.head_swap](magic_hour/resources/v1/head_swap/README.md) + +- [create](magic_hour/resources/v1/head_swap/README.md#create) - Head Swap +- [generate](magic_hour/resources/v1/head_swap/README.md#generate) - Head Swap Generate Workflow + ### [v1.image_background_remover](magic_hour/resources/v1/image_background_remover/README.md) - [create](magic_hour/resources/v1/image_background_remover/README.md#create) - Image Background Remover diff --git a/magic_hour/README.md b/magic_hour/README.md index b31fd66..e43b358 100644 --- a/magic_hour/README.md +++ b/magic_hour/README.md @@ -22,6 +22,7 @@ - [face_swap_photo](resources/v1/face_swap_photo/README.md) - face_swap_photo - [upload_urls](resources/v1/files/upload_urls/README.md) - upload_urls - [files](resources/v1/files/README.md) - files +- [head_swap](resources/v1/head_swap/README.md) - head_swap - [image_background_remover](resources/v1/image_background_remover/README.md) - image_background_remover - [image_projects](resources/v1/image_projects/README.md) - image_projects - [image_to_video](resources/v1/image_to_video/README.md) - image_to_video diff --git a/magic_hour/environment.py b/magic_hour/environment.py index aca0a7a..b79e95d 100644 --- a/magic_hour/environment.py +++ b/magic_hour/environment.py @@ -6,7 +6,7 @@ class Environment(enum.Enum): """Pre-defined base URLs for the API""" ENVIRONMENT = "https://api.magichour.ai" - MOCK_SERVER = "https://api.sideko.dev/v1/mock/magichour/magic-hour/0.57.1" + MOCK_SERVER = "https://api.sideko.dev/v1/mock/magichour/magic-hour/0.58.0" def _get_base_url( diff --git a/magic_hour/resources/v1/README.md b/magic_hour/resources/v1/README.md index 8fc912d..466a0b8 100644 --- a/magic_hour/resources/v1/README.md +++ b/magic_hour/resources/v1/README.md @@ -21,6 +21,7 @@ - [face_swap](face_swap/README.md) - face_swap - [face_swap_photo](face_swap_photo/README.md) - face_swap_photo - [files](files/README.md) - files +- [head_swap](head_swap/README.md) - head_swap - [image_background_remover](image_background_remover/README.md) - image_background_remover - [image_projects](image_projects/README.md) - image_projects - [image_to_video](image_to_video/README.md) - image_to_video diff --git a/magic_hour/resources/v1/client.py b/magic_hour/resources/v1/client.py index 39d77f8..5223e40 100644 --- a/magic_hour/resources/v1/client.py +++ b/magic_hour/resources/v1/client.py @@ -65,6 +65,7 @@ FaceSwapPhotoClient, ) from magic_hour.resources.v1.files import AsyncFilesClient, FilesClient +from magic_hour.resources.v1.head_swap import AsyncHeadSwapClient, HeadSwapClient from magic_hour.resources.v1.image_background_remover import ( AsyncImageBackgroundRemoverClient, ImageBackgroundRemoverClient, @@ -136,6 +137,7 @@ def __init__(self, *, base_client: SyncBaseClient): self.audio_projects = AudioProjectsClient(base_client=self._base_client) self.ai_voice_generator = AiVoiceGeneratorClient(base_client=self._base_client) self.ai_voice_cloner = AiVoiceClonerClient(base_client=self._base_client) + self.head_swap = HeadSwapClient(base_client=self._base_client) class AsyncV1Client: @@ -187,3 +189,4 @@ def __init__(self, *, base_client: AsyncBaseClient): base_client=self._base_client ) self.ai_voice_cloner = AsyncAiVoiceClonerClient(base_client=self._base_client) + self.head_swap = AsyncHeadSwapClient(base_client=self._base_client) diff --git a/magic_hour/resources/v1/head_swap/README.md b/magic_hour/resources/v1/head_swap/README.md new file mode 100644 index 0000000..4731f9b --- /dev/null +++ b/magic_hour/resources/v1/head_swap/README.md @@ -0,0 +1,129 @@ +# v1.head_swap + +## Module Functions + + + +### Head Swap Generate Workflow + +The workflow performs the following action + +1. upload local assets to Magic Hour storage. So you can pass in a local path instead of having to upload files yourself +2. trigger a generation +3. poll for a completion status. This is configurable +4. if success, download the output to local directory + +> [!TIP] +> This is the recommended way to use the SDK unless you have specific needs where it is necessary to split up the actions. + +#### Parameters + +In addition to the parameters listed in the `.create` section below, `.generate` introduces 3 new parameters: + +- `wait_for_completion` (bool, default True): Whether to wait for the project to complete. +- `download_outputs` (bool, default True): Whether to download the generated files +- `download_directory` (str, optional): Directory to save downloaded files (defaults to current directory) + +#### Synchronous Client + +```python +from magic_hour import Client +from os import getenv + +client = Client(token=getenv("API_TOKEN")) +res = client.v1.head_swap.generate( + assets={ + "body_file_path": "/path/to/body.png", + "head_file_path": "/path/to/head.png", + }, + max_resolution=1024, + name="My Head Swap image", + wait_for_completion=True, + download_outputs=True, + download_directory=".", +) +``` + +#### Asynchronous Client + +```python +from magic_hour import AsyncClient +from os import getenv + +client = AsyncClient(token=getenv("API_TOKEN")) +res = await client.v1.head_swap.generate( + assets={ + "body_file_path": "/path/to/body.png", + "head_file_path": "/path/to/head.png", + }, + max_resolution=1024, + name="My Head Swap image", + wait_for_completion=True, + download_outputs=True, + download_directory=".", +) +``` + + + +### Head Swap + +Swap a head onto a body image. Each image costs 10 credits. Output resolution depends on your subscription; you may set `max_resolution` lower than your plan maximum if desired. + +**API Endpoint**: `POST /v1/head-swap` + +#### Parameters + +| Parameter | Required | Description | Example | +| ------------------- | :------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| `assets` | ✓ | Provide the body and head images for head swap | `{"body_file_path": "api-assets/id/1234.png", "head_file_path": "api-assets/id/5678.png"}` | +| `└─ body_file_path` | ✓ | Image that receives the swapped head. This value is either - a direct URL to the video file - `file_path` field from the response of the [upload urls API](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls). See the [file upload guide](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls#input-file) for details. | `"api-assets/id/1234.png"` | +| `└─ head_file_path` | ✓ | Image of the head to place on the body. This value is either - a direct URL to the video file - `file_path` field from the response of the [upload urls API](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls). See the [file upload guide](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls#input-file) for details. | `"api-assets/id/5678.png"` | +| `max_resolution` | ✗ | Constrains the larger dimension (height or width) of the output. Omit to use the maximum allowed for your plan (capped at 2048px). Values above your plan maximum are clamped down to your plan's maximum. | `1024` | +| `name` | ✗ | Give your image a custom name for easy identification. | `"My Head Swap image"` | + +#### Synchronous Client + +```python +from magic_hour import Client +from os import getenv + +client = Client(token=getenv("API_TOKEN")) +res = client.v1.head_swap.create( + assets={ + "body_file_path": "api-assets/id/1234.png", + "head_file_path": "api-assets/id/5678.png", + }, + max_resolution=1024, + name="My Head Swap image", +) +``` + +#### Asynchronous Client + +```python +from magic_hour import AsyncClient +from os import getenv + +client = AsyncClient(token=getenv("API_TOKEN")) +res = await client.v1.head_swap.create( + assets={ + "body_file_path": "api-assets/id/1234.png", + "head_file_path": "api-assets/id/5678.png", + }, + max_resolution=1024, + name="My Head Swap image", +) +``` + +#### Response + +##### Type + +[V1HeadSwapCreateResponse](/magic_hour/types/models/v1_head_swap_create_response.py) + +##### Example + +```python +{"credits_charged": 10, "frame_cost": 10, "id": "cuid-example"} +``` diff --git a/magic_hour/resources/v1/head_swap/__init__.py b/magic_hour/resources/v1/head_swap/__init__.py new file mode 100644 index 0000000..baf61e0 --- /dev/null +++ b/magic_hour/resources/v1/head_swap/__init__.py @@ -0,0 +1,4 @@ +from .client import AsyncHeadSwapClient, HeadSwapClient + + +__all__ = ["AsyncHeadSwapClient", "HeadSwapClient"] diff --git a/magic_hour/resources/v1/head_swap/client.py b/magic_hour/resources/v1/head_swap/client.py new file mode 100644 index 0000000..5d3066b --- /dev/null +++ b/magic_hour/resources/v1/head_swap/client.py @@ -0,0 +1,292 @@ +import typing + +from magic_hour.helpers.logger import get_sdk_logger +from magic_hour.resources.v1.files.client import AsyncFilesClient, FilesClient +from magic_hour.resources.v1.image_projects.client import ( + AsyncImageProjectsClient, + ImageProjectsClient, +) +from magic_hour.types import models, params +from make_api_request import ( + AsyncBaseClient, + RequestOptions, + SyncBaseClient, + default_request_options, + to_encodable, + type_utils, +) + + +logger = get_sdk_logger(__name__) + + +class HeadSwapClient: + def __init__(self, *, base_client: SyncBaseClient): + self._base_client = base_client + + def generate( + self, + *, + assets: params.V1HeadSwapGenerateBodyAssets, + max_resolution: typing.Union[ + typing.Optional[int], type_utils.NotGiven + ] = type_utils.NOT_GIVEN, + name: typing.Union[ + typing.Optional[str], type_utils.NotGiven + ] = type_utils.NOT_GIVEN, + wait_for_completion: bool = True, + download_outputs: bool = True, + download_directory: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ): + """ + Generate head swap (alias for create with additional functionality). + + Swap a head onto a body image. Each image costs 10 credits. Output resolution depends on your subscription; you may set `max_resolution` lower than your plan maximum if desired. + + Args: + max_resolution: Constrains the larger dimension (height or width) of the output. Omit to use the maximum allowed for your plan (capped at 2048px). Values above your plan maximum are clamped down to your plan's maximum. + name: Give your image a custom name for easy identification. + assets: Provide the body and head images for head swap + wait_for_completion: Whether to wait for the image project to complete + download_outputs: Whether to download the outputs + download_directory: The directory to download the outputs to. If not provided, the outputs will be downloaded to the current working directory + request_options: Additional options to customize the HTTP request + + Returns: + V1ImageProjectsGetResponseWithDownloads: The response from the Head Swap API with the downloaded paths if `download_outputs` is True. + + Examples: + ```py + client.v1.head_swap.generate( + assets={ + "body_file_path": "/path/to/body.png", + "head_file_path": "/path/to/head.png", + }, + max_resolution=1024, + name="My Head Swap image", + wait_for_completion=True, + download_outputs=True, + download_directory=".", + ) + ``` + """ + + file_client = FilesClient(base_client=self._base_client) + + body_file_path = assets["body_file_path"] + assets["body_file_path"] = file_client.upload_file(file=body_file_path) + + head_file_path = assets["head_file_path"] + assets["head_file_path"] = file_client.upload_file(file=head_file_path) + + create_response = self.create( + assets=assets, + max_resolution=max_resolution, + name=name, + request_options=request_options, + ) + logger.info(f"Head Swap response: {create_response}") + + image_projects_client = ImageProjectsClient(base_client=self._base_client) + response = image_projects_client.check_result( + id=create_response.id, + wait_for_completion=wait_for_completion, + download_outputs=download_outputs, + download_directory=download_directory, + ) + + return response + + def create( + self, + *, + assets: params.V1HeadSwapCreateBodyAssets, + max_resolution: typing.Union[ + typing.Optional[int], type_utils.NotGiven + ] = type_utils.NOT_GIVEN, + name: typing.Union[ + typing.Optional[str], type_utils.NotGiven + ] = type_utils.NOT_GIVEN, + request_options: typing.Optional[RequestOptions] = None, + ) -> models.V1HeadSwapCreateResponse: + """ + Head Swap + + Swap a head onto a body image. Each image costs 10 credits. Output resolution depends on your subscription; you may set `max_resolution` lower than your plan maximum if desired. + + POST /v1/head-swap + + Args: + max_resolution: Constrains the larger dimension (height or width) of the output. Omit to use the maximum allowed for your plan (capped at 2048px). Values above your plan maximum are clamped down to your plan's maximum. + name: Give your image a custom name for easy identification. + assets: Provide the body and head images for head swap + request_options: Additional options to customize the HTTP request + + Returns: + Success + + Raises: + ApiError: A custom exception class that provides additional context + for API errors, including the HTTP status code and response body. + + Examples: + ```py + client.v1.head_swap.create( + assets={ + "body_file_path": "api-assets/id/1234.png", + "head_file_path": "api-assets/id/5678.png", + }, + max_resolution=1024, + name="My Head Swap image", + ) + ``` + """ + _json = to_encodable( + item={"max_resolution": max_resolution, "name": name, "assets": assets}, + dump_with=params._SerializerV1HeadSwapCreateBody, + ) + return self._base_client.request( + method="POST", + path="/v1/head-swap", + auth_names=["bearerAuth"], + json=_json, + cast_to=models.V1HeadSwapCreateResponse, + request_options=request_options or default_request_options(), + ) + + +class AsyncHeadSwapClient: + def __init__(self, *, base_client: AsyncBaseClient): + self._base_client = base_client + + async def generate( + self, + *, + assets: params.V1HeadSwapGenerateBodyAssets, + max_resolution: typing.Union[ + typing.Optional[int], type_utils.NotGiven + ] = type_utils.NOT_GIVEN, + name: typing.Union[ + typing.Optional[str], type_utils.NotGiven + ] = type_utils.NOT_GIVEN, + wait_for_completion: bool = True, + download_outputs: bool = True, + download_directory: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ): + """ + Generate head swap (alias for create with additional functionality). + + Swap a head onto a body image. Each image costs 10 credits. Output resolution depends on your subscription; you may set `max_resolution` lower than your plan maximum if desired. + + Args: + max_resolution: Constrains the larger dimension (height or width) of the output. Omit to use the maximum allowed for your plan (capped at 2048px). Values above your plan maximum are clamped down to your plan's maximum. + name: Give your image a custom name for easy identification. + assets: Provide the body and head images for head swap + wait_for_completion: Whether to wait for the image project to complete + download_outputs: Whether to download the outputs + download_directory: The directory to download the outputs to. If not provided, the outputs will be downloaded to the current working directory + request_options: Additional options to customize the HTTP request + + Returns: + V1ImageProjectsGetResponseWithDownloads: The response from the Head Swap API with the downloaded paths if `download_outputs` is True. + + Examples: + ```py + await client.v1.head_swap.generate( + assets={ + "body_file_path": "/path/to/body.png", + "head_file_path": "/path/to/head.png", + }, + max_resolution=1024, + name="My Head Swap image", + wait_for_completion=True, + download_outputs=True, + download_directory=".", + ) + ``` + """ + + file_client = AsyncFilesClient(base_client=self._base_client) + + body_file_path = assets["body_file_path"] + assets["body_file_path"] = await file_client.upload_file(file=body_file_path) + + head_file_path = assets["head_file_path"] + assets["head_file_path"] = await file_client.upload_file(file=head_file_path) + + create_response = await self.create( + assets=assets, + max_resolution=max_resolution, + name=name, + request_options=request_options, + ) + logger.info(f"Head Swap response: {create_response}") + + image_projects_client = AsyncImageProjectsClient(base_client=self._base_client) + response = await image_projects_client.check_result( + id=create_response.id, + wait_for_completion=wait_for_completion, + download_outputs=download_outputs, + download_directory=download_directory, + ) + + return response + + async def create( + self, + *, + assets: params.V1HeadSwapCreateBodyAssets, + max_resolution: typing.Union[ + typing.Optional[int], type_utils.NotGiven + ] = type_utils.NOT_GIVEN, + name: typing.Union[ + typing.Optional[str], type_utils.NotGiven + ] = type_utils.NOT_GIVEN, + request_options: typing.Optional[RequestOptions] = None, + ) -> models.V1HeadSwapCreateResponse: + """ + Head Swap + + Swap a head onto a body image. Each image costs 10 credits. Output resolution depends on your subscription; you may set `max_resolution` lower than your plan maximum if desired. + + POST /v1/head-swap + + Args: + max_resolution: Constrains the larger dimension (height or width) of the output. Omit to use the maximum allowed for your plan (capped at 2048px). Values above your plan maximum are clamped down to your plan's maximum. + name: Give your image a custom name for easy identification. + assets: Provide the body and head images for head swap + request_options: Additional options to customize the HTTP request + + Returns: + Success + + Raises: + ApiError: A custom exception class that provides additional context + for API errors, including the HTTP status code and response body. + + Examples: + ```py + await client.v1.head_swap.create( + assets={ + "body_file_path": "api-assets/id/1234.png", + "head_file_path": "api-assets/id/5678.png", + }, + max_resolution=1024, + name="My Head Swap image", + ) + ``` + """ + _json = to_encodable( + item={"max_resolution": max_resolution, "name": name, "assets": assets}, + dump_with=params._SerializerV1HeadSwapCreateBody, + ) + return await self._base_client.request( + method="POST", + path="/v1/head-swap", + auth_names=["bearerAuth"], + json=_json, + cast_to=models.V1HeadSwapCreateResponse, + request_options=request_options or default_request_options(), + ) diff --git a/magic_hour/types/models/__init__.py b/magic_hour/types/models/__init__.py index bb3f2c9..2bd5012 100644 --- a/magic_hour/types/models/__init__.py +++ b/magic_hour/types/models/__init__.py @@ -32,6 +32,7 @@ from .v1_files_upload_urls_create_response_items_item import ( V1FilesUploadUrlsCreateResponseItemsItem, ) +from .v1_head_swap_create_response import V1HeadSwapCreateResponse from .v1_image_background_remover_create_response import ( V1ImageBackgroundRemoverCreateResponse, ) @@ -78,6 +79,7 @@ "V1FaceSwapPhotoCreateResponse", "V1FilesUploadUrlsCreateResponse", "V1FilesUploadUrlsCreateResponseItemsItem", + "V1HeadSwapCreateResponse", "V1ImageBackgroundRemoverCreateResponse", "V1ImageProjectsGetResponse", "V1ImageProjectsGetResponseDownloadsItem", diff --git a/magic_hour/types/models/v1_head_swap_create_response.py b/magic_hour/types/models/v1_head_swap_create_response.py new file mode 100644 index 0000000..99baf42 --- /dev/null +++ b/magic_hour/types/models/v1_head_swap_create_response.py @@ -0,0 +1,33 @@ +import pydantic + + +class V1HeadSwapCreateResponse(pydantic.BaseModel): + """ + Success + """ + + model_config = pydantic.ConfigDict( + arbitrary_types_allowed=True, + populate_by_name=True, + ) + + credits_charged: int = pydantic.Field( + alias="credits_charged", + ) + """ + The amount of credits deducted from your account to generate the image. We charge credits right when the request is made. + + If an error occurred while generating the image(s), credits will be refunded and this field will be updated to include the refund. + """ + frame_cost: int = pydantic.Field( + alias="frame_cost", + ) + """ + Deprecated: Previously represented the number of frames (original name of our credit system) used for image generation. Use 'credits_charged' instead. + """ + id: str = pydantic.Field( + alias="id", + ) + """ + Unique ID of the image. Use it with the [Get image Project API](https://docs.magichour.ai/api-reference/image-projects/get-image-details) to fetch status and downloads. + """ diff --git a/magic_hour/types/params/__init__.py b/magic_hour/types/params/__init__.py index 9235168..34e5542 100644 --- a/magic_hour/types/params/__init__.py +++ b/magic_hour/types/params/__init__.py @@ -217,6 +217,15 @@ V1FilesUploadUrlsCreateBodyItemsItem, _SerializerV1FilesUploadUrlsCreateBodyItemsItem, ) +from .v1_head_swap_create_body import ( + V1HeadSwapCreateBody, + _SerializerV1HeadSwapCreateBody, +) +from .v1_head_swap_create_body_assets import ( + V1HeadSwapCreateBodyAssets, + _SerializerV1HeadSwapCreateBodyAssets, +) +from .v1_head_swap_generate_body_assets import V1HeadSwapGenerateBodyAssets from .v1_image_background_remover_create_body import ( V1ImageBackgroundRemoverCreateBody, _SerializerV1ImageBackgroundRemoverCreateBody, @@ -347,6 +356,9 @@ "V1FaceSwapPhotoGenerateBodyAssetsFaceMappingsItem", "V1FilesUploadUrlsCreateBody", "V1FilesUploadUrlsCreateBodyItemsItem", + "V1HeadSwapCreateBody", + "V1HeadSwapCreateBodyAssets", + "V1HeadSwapGenerateBodyAssets", "V1ImageBackgroundRemoverCreateBody", "V1ImageBackgroundRemoverCreateBodyAssets", "V1ImageBackgroundRemoverGenerateBodyAssets", @@ -415,6 +427,8 @@ "_SerializerV1FaceSwapPhotoCreateBodyAssetsFaceMappingsItem", "_SerializerV1FilesUploadUrlsCreateBody", "_SerializerV1FilesUploadUrlsCreateBodyItemsItem", + "_SerializerV1HeadSwapCreateBody", + "_SerializerV1HeadSwapCreateBodyAssets", "_SerializerV1ImageBackgroundRemoverCreateBody", "_SerializerV1ImageBackgroundRemoverCreateBodyAssets", "_SerializerV1ImageToVideoCreateBody", diff --git a/magic_hour/types/params/v1_head_swap_create_body.py b/magic_hour/types/params/v1_head_swap_create_body.py new file mode 100644 index 0000000..7a28e8b --- /dev/null +++ b/magic_hour/types/params/v1_head_swap_create_body.py @@ -0,0 +1,48 @@ +import pydantic +import typing +import typing_extensions + +from .v1_head_swap_create_body_assets import ( + V1HeadSwapCreateBodyAssets, + _SerializerV1HeadSwapCreateBodyAssets, +) + + +class V1HeadSwapCreateBody(typing_extensions.TypedDict): + """ + V1HeadSwapCreateBody + """ + + assets: typing_extensions.Required[V1HeadSwapCreateBodyAssets] + """ + Provide the body and head images for head swap + """ + + max_resolution: typing_extensions.NotRequired[int] + """ + Constrains the larger dimension (height or width) of the output. Omit to use the maximum allowed for your plan (capped at 2048px). Values above your plan maximum are clamped down to your plan's maximum. + """ + + name: typing_extensions.NotRequired[str] + """ + Give your image a custom name for easy identification. + """ + + +class _SerializerV1HeadSwapCreateBody(pydantic.BaseModel): + """ + Serializer for V1HeadSwapCreateBody handling case conversions + and file omissions as dictated by the API + """ + + model_config = pydantic.ConfigDict( + populate_by_name=True, + ) + + assets: _SerializerV1HeadSwapCreateBodyAssets = pydantic.Field( + alias="assets", + ) + max_resolution: typing.Optional[int] = pydantic.Field( + alias="max_resolution", default=None + ) + name: typing.Optional[str] = pydantic.Field(alias="name", default=None) diff --git a/magic_hour/types/params/v1_head_swap_create_body_assets.py b/magic_hour/types/params/v1_head_swap_create_body_assets.py new file mode 100644 index 0000000..58de801 --- /dev/null +++ b/magic_hour/types/params/v1_head_swap_create_body_assets.py @@ -0,0 +1,46 @@ +import pydantic +import typing_extensions + + +class V1HeadSwapCreateBodyAssets(typing_extensions.TypedDict): + """ + Provide the body and head images for head swap + """ + + body_file_path: typing_extensions.Required[str] + """ + Image that receives the swapped head. This value is either + - a direct URL to the video file + - `file_path` field from the response of the [upload urls API](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls). + + See the [file upload guide](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls#input-file) for details. + + """ + + head_file_path: typing_extensions.Required[str] + """ + Image of the head to place on the body. This value is either + - a direct URL to the video file + - `file_path` field from the response of the [upload urls API](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls). + + See the [file upload guide](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls#input-file) for details. + + """ + + +class _SerializerV1HeadSwapCreateBodyAssets(pydantic.BaseModel): + """ + Serializer for V1HeadSwapCreateBodyAssets handling case conversions + and file omissions as dictated by the API + """ + + model_config = pydantic.ConfigDict( + populate_by_name=True, + ) + + body_file_path: str = pydantic.Field( + alias="body_file_path", + ) + head_file_path: str = pydantic.Field( + alias="head_file_path", + ) diff --git a/magic_hour/types/params/v1_head_swap_generate_body_assets.py b/magic_hour/types/params/v1_head_swap_generate_body_assets.py new file mode 100644 index 0000000..7fdc294 --- /dev/null +++ b/magic_hour/types/params/v1_head_swap_generate_body_assets.py @@ -0,0 +1,25 @@ +import typing_extensions + + +class V1HeadSwapGenerateBodyAssets(typing_extensions.TypedDict): + """ + Provide the body and head images for head swap + """ + + body_file_path: typing_extensions.Required[str] + """ + Image that receives the swapped head. This value is either + - a direct URL to the image file + - a path to a local file + + Note: if the path begins with `api-assets`, it will be assumed to already be uploaded to Magic Hour's storage, and will not be uploaded again. + """ + + head_file_path: typing_extensions.Required[str] + """ + Image of the head to place on the body. This value is either + - a direct URL to the image file + - a path to a local file + + Note: if the path begins with `api-assets`, it will be assumed to already be uploaded to Magic Hour's storage, and will not be uploaded again. + """ diff --git a/pyproject.toml b/pyproject.toml index d42d0e1..26b1a3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "magic_hour" -version = "0.57.1" +version = "0.58.0" description = "Python SDK for Magic Hour API" readme = "README.md" authors = [] diff --git a/tests/test_v1_head_swap_client.py b/tests/test_v1_head_swap_client.py new file mode 100644 index 0000000..45bc447 --- /dev/null +++ b/tests/test_v1_head_swap_client.py @@ -0,0 +1,79 @@ +import pydantic +import pytest + +from magic_hour import AsyncClient, Client +from magic_hour.environment import Environment +from magic_hour.types import models + + +def test_create_200_success_all_params() -> None: + """Tests a POST request to the /v1/head-swap endpoint. + + Operation: create + Test Case ID: success_all_params + Expected Status: 200 + Mode: Synchronous execution + + Response : models.V1HeadSwapCreateResponse + + Validates: + - Authentication requirements are satisfied + - All required input parameters are properly handled + - Response status code is correct + - Response data matches expected schema + + This test uses example data to verify the endpoint behavior. + """ + # tests calling sync method with example data + client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER) + response = client.v1.head_swap.create( + assets={ + "body_file_path": "api-assets/id/1234.png", + "head_file_path": "api-assets/id/5678.png", + }, + max_resolution=1024, + name="My Head Swap image", + ) + try: + pydantic.TypeAdapter(models.V1HeadSwapCreateResponse).validate_python(response) + is_valid_response_schema = True + except pydantic.ValidationError: + is_valid_response_schema = False + assert is_valid_response_schema, "failed response type check" + + +@pytest.mark.asyncio +async def test_await_create_200_success_all_params() -> None: + """Tests a POST request to the /v1/head-swap endpoint. + + Operation: create + Test Case ID: success_all_params + Expected Status: 200 + Mode: Asynchronous execution + + Response : models.V1HeadSwapCreateResponse + + Validates: + - Authentication requirements are satisfied + - All required input parameters are properly handled + - Response status code is correct + - Response data matches expected schema + + This test uses example data to verify the endpoint behavior. + """ + # tests calling async method with example data + client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER) + response = await client.v1.head_swap.create( + assets={ + "body_file_path": "api-assets/id/1234.png", + "head_file_path": "api-assets/id/5678.png", + }, + max_resolution=1024, + name="My Head Swap image", + ) + try: + pydantic.TypeAdapter(models.V1HeadSwapCreateResponse).validate_python(response) + is_valid_response_schema = True + except pydantic.ValidationError: + is_valid_response_schema = False + assert is_valid_response_schema, "failed response type check"