diff --git a/magic_hour/resources/v1/audio_to_video/README.md b/magic_hour/resources/v1/audio_to_video/README.md index 63baee1..920a028 100644 --- a/magic_hour/resources/v1/audio_to_video/README.md +++ b/magic_hour/resources/v1/audio_to_video/README.md @@ -2,6 +2,76 @@ ## Module Functions + + +### Audio To Video 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 Additional 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.audio_to_video.generate( + assets={ + "audio_file_path": "/path/to/1234.mp3", + "image_file_path": "/path/to/1234.png", + }, + end_seconds=15.0, + name="My Audio To Video video", + resolution="720p", + start_seconds=0.0, + style={"prompt": "Car driving through a city"}, + 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.audio_to_video.generate( + assets={ + "audio_file_path": "/path/to/1234.mp3", + "image_file_path": "/path/to/1234.png", + }, + end_seconds=15.0, + name="My Audio To Video video", + resolution="720p", + start_seconds=0.0, + style={"prompt": "Car driving through a city"}, + wait_for_completion=True, + download_outputs=True, + download_directory="." +) +``` + + + ### Audio-to-Video **What this API does** diff --git a/magic_hour/resources/v1/audio_to_video/client.py b/magic_hour/resources/v1/audio_to_video/client.py index 78c6a0a..770d4b6 100644 --- a/magic_hour/resources/v1/audio_to_video/client.py +++ b/magic_hour/resources/v1/audio_to_video/client.py @@ -1,6 +1,12 @@ import typing import typing_extensions +from magic_hour.helpers.logger import get_sdk_logger +from magic_hour.resources.v1.files.client import AsyncFilesClient, FilesClient +from magic_hour.resources.v1.video_projects.client import ( + AsyncVideoProjectsClient, + VideoProjectsClient, +) from magic_hour.types import models, params from make_api_request import ( AsyncBaseClient, @@ -12,10 +18,104 @@ ) +logger = get_sdk_logger(__name__) + + class AudioToVideoClient: def __init__(self, *, base_client: SyncBaseClient): self._base_client = base_client + def generate( + self, + *, + assets: params.V1AudioToVideoGenerateBodyAssets, + end_seconds: float, + name: typing.Union[ + typing.Optional[str], type_utils.NotGiven + ] = type_utils.NOT_GIVEN, + resolution: typing.Union[ + typing.Optional[typing_extensions.Literal["1080p", "480p", "720p"]], + type_utils.NotGiven, + ] = type_utils.NOT_GIVEN, + start_seconds: typing.Union[ + typing.Optional[float], type_utils.NotGiven + ] = type_utils.NOT_GIVEN, + style: typing.Union[ + typing.Optional[params.V1AudioToVideoCreateBodyStyle], 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 audio-to-video (alias for create with additional functionality). + + Create an Audio To Video video. Credits are only charged for the frames that actually render. + + Args: + name: Give your video a custom name for easy identification. + resolution: Output video resolution. Defaults to `720p` on paid tiers and `480p` on free tiers. + start_seconds: Start time of your clip (seconds). Must be ≥ 0. + style: Attributes used to dictate the style of the output + assets: Provide the audio file and an optional reference image. + end_seconds: End time of your clip (seconds). Must be greater than start_seconds. + wait_for_completion: Whether to wait for the video 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: + V1VideoProjectsGetResponseWithDownloads: The response from the Audio-to-Video API with the downloaded paths if `download_outputs` is True. + + Examples: + ```py + response = client.v1.audio_to_video.generate( + assets={ + "audio_file_path": "path/to/audio.mp3", + "image_file_path": "path/to/image.png", + }, + end_seconds=15.0, + name="My Audio To Video video", + resolution="720p", + start_seconds=0.0, + wait_for_completion=True, + download_outputs=True, + download_directory=".", + ) + ``` + """ + + file_client = FilesClient(base_client=self._base_client) + + audio_file_path = assets["audio_file_path"] + assets["audio_file_path"] = file_client.upload_file(file=audio_file_path) + + if "image_file_path" in assets and assets["image_file_path"]: + image_file_path = assets["image_file_path"] + assets["image_file_path"] = file_client.upload_file(file=image_file_path) + + create_response = self.create( + assets=assets, + end_seconds=end_seconds, + name=name, + resolution=resolution, + start_seconds=start_seconds, + style=style, + request_options=request_options, + ) + logger.info(f"Audio-to-Video response: {create_response}") + + video_projects_client = VideoProjectsClient(base_client=self._base_client) + response = video_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, *, @@ -119,6 +219,99 @@ class AsyncAudioToVideoClient: def __init__(self, *, base_client: AsyncBaseClient): self._base_client = base_client + async def generate( + self, + *, + assets: params.V1AudioToVideoGenerateBodyAssets, + end_seconds: float, + name: typing.Union[ + typing.Optional[str], type_utils.NotGiven + ] = type_utils.NOT_GIVEN, + resolution: typing.Union[ + typing.Optional[typing_extensions.Literal["1080p", "480p", "720p"]], + type_utils.NotGiven, + ] = type_utils.NOT_GIVEN, + start_seconds: typing.Union[ + typing.Optional[float], type_utils.NotGiven + ] = type_utils.NOT_GIVEN, + style: typing.Union[ + typing.Optional[params.V1AudioToVideoCreateBodyStyle], 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 audio-to-video (alias for create with additional functionality). + + Create an Audio To Video video. Credits are only charged for the frames that actually render. + + Args: + name: Give your video a custom name for easy identification. + resolution: Output video resolution. Defaults to `720p` on paid tiers and `480p` on free tiers. + start_seconds: Start time of your clip (seconds). Must be ≥ 0. + style: Attributes used to dictate the style of the output + assets: Provide the audio file and an optional reference image. + end_seconds: End time of your clip (seconds). Must be greater than start_seconds. + wait_for_completion: Whether to wait for the video 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: + V1VideoProjectsGetResponseWithDownloads: The response from the Audio-to-Video API with the downloaded paths if `download_outputs` is True. + + Examples: + ```py + response = await client.v1.audio_to_video.generate( + assets={ + "audio_file_path": "path/to/audio.mp3", + "image_file_path": "path/to/image.png", + }, + end_seconds=15.0, + name="My Audio To Video video", + resolution="720p", + start_seconds=0.0, + wait_for_completion=True, + download_outputs=True, + download_directory=".", + ) + ``` + """ + + file_client = AsyncFilesClient(base_client=self._base_client) + + audio_file_path = assets["audio_file_path"] + assets["audio_file_path"] = await file_client.upload_file(file=audio_file_path) + + if "image_file_path" in assets and assets["image_file_path"]: + image_file_path = assets["image_file_path"] + assets["image_file_path"] = await file_client.upload_file( + file=image_file_path + ) + + create_response = await self.create( + assets=assets, + end_seconds=end_seconds, + name=name, + resolution=resolution, + start_seconds=start_seconds, + style=style, + request_options=request_options, + ) + logger.info(f"Audio-to-Video response: {create_response}") + + video_projects_client = AsyncVideoProjectsClient(base_client=self._base_client) + response = await video_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, *, diff --git a/magic_hour/types/params/__init__.py b/magic_hour/types/params/__init__.py index a3f0d89..adaefec 100644 --- a/magic_hour/types/params/__init__.py +++ b/magic_hour/types/params/__init__.py @@ -157,6 +157,7 @@ V1AudioToVideoCreateBodyStyle, _SerializerV1AudioToVideoCreateBodyStyle, ) +from .v1_audio_to_video_generate_body_assets import V1AudioToVideoGenerateBodyAssets from .v1_auto_subtitle_generator_create_body import ( V1AutoSubtitleGeneratorCreateBody, _SerializerV1AutoSubtitleGeneratorCreateBody, @@ -358,6 +359,7 @@ "V1AudioToVideoCreateBody", "V1AudioToVideoCreateBodyAssets", "V1AudioToVideoCreateBodyStyle", + "V1AudioToVideoGenerateBodyAssets", "V1AutoSubtitleGeneratorCreateBody", "V1AutoSubtitleGeneratorCreateBodyAssets", "V1AutoSubtitleGeneratorCreateBodyStyle", diff --git a/magic_hour/types/params/v1_audio_to_video_generate_body_assets.py b/magic_hour/types/params/v1_audio_to_video_generate_body_assets.py new file mode 100644 index 0000000..850de00 --- /dev/null +++ b/magic_hour/types/params/v1_audio_to_video_generate_body_assets.py @@ -0,0 +1,25 @@ +import typing_extensions + + +class V1AudioToVideoGenerateBodyAssets(typing_extensions.TypedDict): + """ + Provide the audio file and an optional reference image. + """ + + audio_file_path: typing_extensions.Required[str] + """ + The path of the audio file. This value is either + - a direct URL to the audio 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. + """ + + image_file_path: typing_extensions.NotRequired[str] + """ + Reference image for the initial frame of the video. 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. + """