magic_hour 0.34.0__py3-none-any.whl → 0.36.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of magic_hour might be problematic. Click here for more details.

Files changed (97) hide show
  1. magic_hour/README.md +35 -0
  2. magic_hour/core/base_client.py +6 -5
  3. magic_hour/core/query.py +12 -6
  4. magic_hour/core/request.py +3 -3
  5. magic_hour/core/response.py +18 -14
  6. magic_hour/core/utils.py +3 -3
  7. magic_hour/environment.py +1 -1
  8. magic_hour/helpers/__init__.py +3 -0
  9. magic_hour/helpers/download.py +75 -0
  10. magic_hour/resources/v1/README.md +33 -0
  11. magic_hour/resources/v1/ai_clothes_changer/README.md +73 -0
  12. magic_hour/resources/v1/ai_clothes_changer/client.py +146 -0
  13. magic_hour/resources/v1/ai_face_editor/README.md +110 -0
  14. magic_hour/resources/v1/ai_face_editor/client.py +168 -0
  15. magic_hour/resources/v1/ai_gif_generator/README.md +59 -0
  16. magic_hour/resources/v1/ai_gif_generator/client.py +119 -0
  17. magic_hour/resources/v1/ai_headshot_generator/README.md +60 -0
  18. magic_hour/resources/v1/ai_headshot_generator/client.py +140 -0
  19. magic_hour/resources/v1/ai_image_editor/README.md +64 -0
  20. magic_hour/resources/v1/ai_image_editor/client.py +136 -0
  21. magic_hour/resources/v1/ai_image_generator/README.md +66 -0
  22. magic_hour/resources/v1/ai_image_generator/client.py +139 -0
  23. magic_hour/resources/v1/ai_image_upscaler/README.md +67 -0
  24. magic_hour/resources/v1/ai_image_upscaler/client.py +150 -0
  25. magic_hour/resources/v1/ai_meme_generator/README.md +71 -0
  26. magic_hour/resources/v1/ai_meme_generator/client.py +127 -0
  27. magic_hour/resources/v1/ai_photo_editor/README.md +98 -7
  28. magic_hour/resources/v1/ai_photo_editor/client.py +174 -0
  29. magic_hour/resources/v1/ai_qr_code_generator/README.md +63 -0
  30. magic_hour/resources/v1/ai_qr_code_generator/client.py +123 -0
  31. magic_hour/resources/v1/ai_talking_photo/README.md +74 -0
  32. magic_hour/resources/v1/ai_talking_photo/client.py +170 -0
  33. magic_hour/resources/v1/animation/README.md +100 -0
  34. magic_hour/resources/v1/animation/client.py +218 -0
  35. magic_hour/resources/v1/auto_subtitle_generator/README.md +69 -0
  36. magic_hour/resources/v1/auto_subtitle_generator/client.py +178 -0
  37. magic_hour/resources/v1/face_detection/README.md +59 -0
  38. magic_hour/resources/v1/face_detection/__init__.py +10 -2
  39. magic_hour/resources/v1/face_detection/client.py +179 -0
  40. magic_hour/resources/v1/face_swap/README.md +105 -12
  41. magic_hour/resources/v1/face_swap/client.py +262 -28
  42. magic_hour/resources/v1/face_swap_photo/README.md +84 -0
  43. magic_hour/resources/v1/face_swap_photo/client.py +172 -0
  44. magic_hour/resources/v1/files/README.md +6 -0
  45. magic_hour/resources/v1/files/client.py +350 -0
  46. magic_hour/resources/v1/files/client_test.py +414 -0
  47. magic_hour/resources/v1/files/upload_urls/README.md +8 -0
  48. magic_hour/resources/v1/image_background_remover/README.md +68 -0
  49. magic_hour/resources/v1/image_background_remover/client.py +130 -0
  50. magic_hour/resources/v1/image_projects/README.md +8 -0
  51. magic_hour/resources/v1/image_projects/__init__.py +10 -2
  52. magic_hour/resources/v1/image_projects/client.py +138 -0
  53. magic_hour/resources/v1/image_projects/client_test.py +527 -0
  54. magic_hour/resources/v1/image_to_video/README.md +77 -9
  55. magic_hour/resources/v1/image_to_video/client.py +210 -8
  56. magic_hour/resources/v1/lip_sync/README.md +87 -13
  57. magic_hour/resources/v1/lip_sync/client.py +230 -28
  58. magic_hour/resources/v1/photo_colorizer/README.md +59 -0
  59. magic_hour/resources/v1/photo_colorizer/client.py +130 -0
  60. magic_hour/resources/v1/text_to_video/README.md +68 -0
  61. magic_hour/resources/v1/text_to_video/client.py +151 -0
  62. magic_hour/resources/v1/video_projects/README.md +8 -0
  63. magic_hour/resources/v1/video_projects/__init__.py +10 -2
  64. magic_hour/resources/v1/video_projects/client.py +137 -0
  65. magic_hour/resources/v1/video_projects/client_test.py +527 -0
  66. magic_hour/resources/v1/video_to_video/README.md +98 -14
  67. magic_hour/resources/v1/video_to_video/client.py +242 -28
  68. magic_hour/types/params/__init__.py +58 -0
  69. magic_hour/types/params/v1_ai_clothes_changer_generate_body_assets.py +33 -0
  70. magic_hour/types/params/v1_ai_face_editor_generate_body_assets.py +17 -0
  71. magic_hour/types/params/v1_ai_headshot_generator_generate_body_assets.py +17 -0
  72. magic_hour/types/params/v1_ai_image_editor_generate_body_assets.py +17 -0
  73. magic_hour/types/params/v1_ai_image_upscaler_generate_body_assets.py +17 -0
  74. magic_hour/types/params/v1_ai_photo_editor_generate_body_assets.py +17 -0
  75. magic_hour/types/params/v1_ai_talking_photo_generate_body_assets.py +26 -0
  76. magic_hour/types/params/v1_animation_generate_body_assets.py +39 -0
  77. magic_hour/types/params/v1_auto_subtitle_generator_generate_body_assets.py +17 -0
  78. magic_hour/types/params/v1_face_detection_generate_body_assets.py +17 -0
  79. magic_hour/types/params/v1_face_swap_create_body.py +24 -14
  80. magic_hour/types/params/v1_face_swap_create_body_style.py +33 -0
  81. magic_hour/types/params/v1_face_swap_generate_body_assets.py +56 -0
  82. magic_hour/types/params/v1_face_swap_generate_body_assets_face_mappings_item.py +25 -0
  83. magic_hour/types/params/v1_face_swap_photo_generate_body_assets.py +47 -0
  84. magic_hour/types/params/v1_face_swap_photo_generate_body_assets_face_mappings_item.py +25 -0
  85. magic_hour/types/params/v1_image_background_remover_generate_body_assets.py +27 -0
  86. magic_hour/types/params/v1_image_to_video_create_body.py +14 -6
  87. magic_hour/types/params/v1_image_to_video_generate_body_assets.py +17 -0
  88. magic_hour/types/params/v1_lip_sync_create_body.py +12 -14
  89. magic_hour/types/params/v1_lip_sync_generate_body_assets.py +36 -0
  90. magic_hour/types/params/v1_photo_colorizer_generate_body_assets.py +17 -0
  91. magic_hour/types/params/v1_video_to_video_create_body.py +12 -14
  92. magic_hour/types/params/v1_video_to_video_generate_body_assets.py +27 -0
  93. magic_hour-0.36.0.dist-info/METADATA +303 -0
  94. {magic_hour-0.34.0.dist-info → magic_hour-0.36.0.dist-info}/RECORD +96 -68
  95. magic_hour-0.34.0.dist-info/METADATA +0 -166
  96. {magic_hour-0.34.0.dist-info → magic_hour-0.36.0.dist-info}/LICENSE +0 -0
  97. {magic_hour-0.34.0.dist-info → magic_hour-0.36.0.dist-info}/WHEEL +0 -0
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import typing
2
3
  import typing_extensions
3
4
 
@@ -9,13 +10,92 @@ from magic_hour.core import (
9
10
  to_encodable,
10
11
  type_utils,
11
12
  )
13
+ from magic_hour.resources.v1.video_projects.client import (
14
+ AsyncVideoProjectsClient,
15
+ VideoProjectsClient,
16
+ )
12
17
  from magic_hour.types import models, params
13
18
 
14
19
 
20
+ logging.basicConfig(level=logging.INFO)
21
+ logger = logging.getLogger(__name__)
22
+
23
+
15
24
  class TextToVideoClient:
16
25
  def __init__(self, *, base_client: SyncBaseClient):
17
26
  self._base_client = base_client
18
27
 
28
+ def generate(
29
+ self,
30
+ *,
31
+ end_seconds: float,
32
+ orientation: typing_extensions.Literal["landscape", "portrait", "square"],
33
+ style: params.V1TextToVideoCreateBodyStyle,
34
+ name: typing.Union[
35
+ typing.Optional[str], type_utils.NotGiven
36
+ ] = type_utils.NOT_GIVEN,
37
+ resolution: typing.Union[
38
+ typing.Optional[typing_extensions.Literal["1080p", "480p", "720p"]],
39
+ type_utils.NotGiven,
40
+ ] = type_utils.NOT_GIVEN,
41
+ wait_for_completion: bool = True,
42
+ download_outputs: bool = True,
43
+ download_directory: typing.Optional[str] = None,
44
+ request_options: typing.Optional[RequestOptions] = None,
45
+ ):
46
+ """
47
+ Generate text-to-video (alias for create with additional functionality).
48
+
49
+ Create a Text To Video video. The estimated frame cost is calculated using 30 FPS. This amount is deducted from your account balance when a video is queued. Once the video is complete, the cost will be updated based on the actual number of frames rendered.
50
+
51
+ Args:
52
+ name: The name of video. This value is mainly used for your own identification of the video.
53
+ resolution: Controls the output video resolution. Defaults to `720p` if not specified.
54
+ end_seconds: The total duration of the output video in seconds.
55
+ orientation: Determines the orientation of the output video
56
+ style: V1TextToVideoCreateBodyStyle
57
+ wait_for_completion: Whether to wait for the video project to complete
58
+ download_outputs: Whether to download the outputs
59
+ download_directory: The directory to download the outputs to. If not provided, the outputs will be downloaded to the current working directory
60
+ request_options: Additional options to customize the HTTP request
61
+
62
+ Returns:
63
+ V1VideoProjectsGetResponseWithDownloads: The response from the Text-to-Video API with the downloaded paths if `download_outputs` is True.
64
+
65
+ Examples:
66
+ ```py
67
+ response = client.v1.text_to_video.generate(
68
+ end_seconds=5.0,
69
+ orientation="landscape",
70
+ style={"prompt": "a dog running through a meadow"},
71
+ resolution="720p",
72
+ wait_for_completion=True,
73
+ download_outputs=True,
74
+ download_directory="outputs/",
75
+ )
76
+ ```
77
+ """
78
+
79
+ create_response = self.create(
80
+ end_seconds=end_seconds,
81
+ orientation=orientation,
82
+ style=style,
83
+ name=name,
84
+ resolution=resolution,
85
+ request_options=request_options,
86
+ )
87
+ logger.info(f"Text-to-Video response: {create_response}")
88
+
89
+ video_projects_client = VideoProjectsClient(base_client=self._base_client)
90
+ response = video_projects_client.check_result(
91
+ id=create_response.id,
92
+ wait_for_completion=wait_for_completion,
93
+ download_outputs=download_outputs,
94
+ download_directory=download_directory,
95
+ )
96
+
97
+ return response
98
+
19
99
  def create(
20
100
  self,
21
101
  *,
@@ -102,6 +182,77 @@ class AsyncTextToVideoClient:
102
182
  def __init__(self, *, base_client: AsyncBaseClient):
103
183
  self._base_client = base_client
104
184
 
185
+ async def generate(
186
+ self,
187
+ *,
188
+ end_seconds: float,
189
+ orientation: typing_extensions.Literal["landscape", "portrait", "square"],
190
+ style: params.V1TextToVideoCreateBodyStyle,
191
+ name: typing.Union[
192
+ typing.Optional[str], type_utils.NotGiven
193
+ ] = type_utils.NOT_GIVEN,
194
+ resolution: typing.Union[
195
+ typing.Optional[typing_extensions.Literal["1080p", "480p", "720p"]],
196
+ type_utils.NotGiven,
197
+ ] = type_utils.NOT_GIVEN,
198
+ wait_for_completion: bool = True,
199
+ download_outputs: bool = True,
200
+ download_directory: typing.Optional[str] = None,
201
+ request_options: typing.Optional[RequestOptions] = None,
202
+ ):
203
+ """
204
+ Generate text-to-video (alias for create with additional functionality).
205
+
206
+ Create a Text To Video video. The estimated frame cost is calculated using 30 FPS. This amount is deducted from your account balance when a video is queued. Once the video is complete, the cost will be updated based on the actual number of frames rendered.
207
+
208
+ Args:
209
+ name: The name of video. This value is mainly used for your own identification of the video.
210
+ resolution: Controls the output video resolution. Defaults to `720p` if not specified.
211
+ end_seconds: The total duration of the output video in seconds.
212
+ orientation: Determines the orientation of the output video
213
+ style: V1TextToVideoCreateBodyStyle
214
+ wait_for_completion: Whether to wait for the video project to complete
215
+ download_outputs: Whether to download the outputs
216
+ download_directory: The directory to download the outputs to. If not provided, the outputs will be downloaded to the current working directory
217
+ request_options: Additional options to customize the HTTP request
218
+
219
+ Returns:
220
+ V1VideoProjectsGetResponseWithDownloads: The response from the Text-to-Video API with the downloaded paths if `download_outputs` is True.
221
+
222
+ Examples:
223
+ ```py
224
+ response = await client.v1.text_to_video.generate(
225
+ end_seconds=5.0,
226
+ orientation="landscape",
227
+ style={"prompt": "a dog running through a meadow"},
228
+ resolution="720p",
229
+ wait_for_completion=True,
230
+ download_outputs=True,
231
+ download_directory="outputs/",
232
+ )
233
+ ```
234
+ """
235
+
236
+ create_response = await self.create(
237
+ end_seconds=end_seconds,
238
+ orientation=orientation,
239
+ style=style,
240
+ name=name,
241
+ resolution=resolution,
242
+ request_options=request_options,
243
+ )
244
+ logger.info(f"Text-to-Video response: {create_response}")
245
+
246
+ video_projects_client = AsyncVideoProjectsClient(base_client=self._base_client)
247
+ response = await video_projects_client.check_result(
248
+ id=create_response.id,
249
+ wait_for_completion=wait_for_completion,
250
+ download_outputs=download_outputs,
251
+ download_directory=download_directory,
252
+ )
253
+
254
+ return response
255
+
105
256
  async def create(
106
257
  self,
107
258
  *,
@@ -1,3 +1,10 @@
1
+ # v1_video_projects
2
+
3
+ ## Module Functions
4
+
5
+ <!-- CUSTOM DOCS START -->
6
+
7
+ <!-- CUSTOM DOCS END -->
1
8
 
2
9
  ### Delete video <a name="delete"></a>
3
10
 
@@ -83,3 +90,4 @@ res = await client.v1.video_projects.get(id="cuid-example")
83
90
 
84
91
  ##### Example
85
92
  `{"created_at": "1970-01-01T00:00:00", "credits_charged": 450, "download": {"expires_at": "2024-10-19T05:16:19.027Z", "url": "https://videos.magichour.ai/id/output.mp4"}, "downloads": [{"expires_at": "2024-10-19T05:16:19.027Z", "url": "https://videos.magichour.ai/id/output.mp4"}], "enabled": True, "end_seconds": 15.0, "error": {"code": "no_source_face", "message": "Please use an image with a detectable face"}, "fps": 30.0, "height": 960, "id": "cuid-example", "name": "Example Name", "start_seconds": 0.0, "status": "complete", "total_frame_cost": 450, "type_": "FACE_SWAP", "width": 512}`
93
+
@@ -1,4 +1,12 @@
1
- from .client import AsyncVideoProjectsClient, VideoProjectsClient
1
+ from .client import (
2
+ AsyncVideoProjectsClient,
3
+ V1VideoProjectsGetResponseWithDownloads,
4
+ VideoProjectsClient,
5
+ )
2
6
 
3
7
 
4
- __all__ = ["AsyncVideoProjectsClient", "VideoProjectsClient"]
8
+ __all__ = [
9
+ "AsyncVideoProjectsClient",
10
+ "V1VideoProjectsGetResponseWithDownloads",
11
+ "VideoProjectsClient",
12
+ ]
@@ -1,3 +1,7 @@
1
+ import logging
2
+ import os
3
+ import pydantic
4
+ import time
1
5
  import typing
2
6
 
3
7
  from magic_hour.core import (
@@ -6,13 +10,87 @@ from magic_hour.core import (
6
10
  SyncBaseClient,
7
11
  default_request_options,
8
12
  )
13
+ from magic_hour.helpers.download import download_files_async, download_files_sync
9
14
  from magic_hour.types import models
10
15
 
11
16
 
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class V1VideoProjectsGetResponseWithDownloads(models.V1VideoProjectsGetResponse):
21
+ downloaded_paths: typing.Optional[typing.List[str]] = pydantic.Field(
22
+ default=None, alias="downloaded_paths"
23
+ )
24
+ """
25
+ The paths to the downloaded files.
26
+
27
+ This field is only populated if `download_outputs` is True and the video project is complete.
28
+ """
29
+
30
+
12
31
  class VideoProjectsClient:
13
32
  def __init__(self, *, base_client: SyncBaseClient):
14
33
  self._base_client = base_client
15
34
 
35
+ def check_result(
36
+ self,
37
+ id: str,
38
+ wait_for_completion: bool,
39
+ download_outputs: bool,
40
+ download_directory: typing.Optional[str] = None,
41
+ ) -> V1VideoProjectsGetResponseWithDownloads:
42
+ """
43
+ Check the result of a video project with optional waiting and downloading.
44
+
45
+ This method retrieves the status of a video project and optionally waits for completion
46
+ and downloads the output files.
47
+
48
+ Args:
49
+ id: Unique ID of the video project
50
+ wait_for_completion: Whether to wait for the video project to complete
51
+ download_outputs: Whether to download the outputs
52
+ download_directory: The directory to download the outputs to. If not provided,
53
+ the outputs will be downloaded to the current working directory
54
+
55
+ Returns:
56
+ V1VideoProjectsGetResponseWithDownloads: The video project response with optional
57
+ downloaded file paths included
58
+ """
59
+ api_response = self.get(id=id)
60
+ if not wait_for_completion:
61
+ response = V1VideoProjectsGetResponseWithDownloads(
62
+ **api_response.model_dump()
63
+ )
64
+ return response
65
+
66
+ poll_interval = float(os.getenv("MAGIC_HOUR_POLL_INTERVAL", "0.5"))
67
+
68
+ status = api_response.status
69
+
70
+ while status not in ["complete", "error", "canceled"]:
71
+ api_response = self.get(id=id)
72
+ status = api_response.status
73
+ time.sleep(poll_interval)
74
+
75
+ if api_response.status != "complete":
76
+ log = logger.error if api_response.status == "error" else logger.info
77
+ log(
78
+ f"Video project {id} has status {api_response.status}: {api_response.error}"
79
+ )
80
+ return V1VideoProjectsGetResponseWithDownloads(**api_response.model_dump())
81
+
82
+ if not download_outputs:
83
+ return V1VideoProjectsGetResponseWithDownloads(**api_response.model_dump())
84
+
85
+ downloaded_paths = download_files_sync(
86
+ downloads=api_response.downloads,
87
+ download_directory=download_directory,
88
+ )
89
+
90
+ return V1VideoProjectsGetResponseWithDownloads(
91
+ **api_response.model_dump(), downloaded_paths=downloaded_paths
92
+ )
93
+
16
94
  def delete(
17
95
  self, *, id: str, request_options: typing.Optional[RequestOptions] = None
18
96
  ) -> None:
@@ -95,6 +173,65 @@ class AsyncVideoProjectsClient:
95
173
  def __init__(self, *, base_client: AsyncBaseClient):
96
174
  self._base_client = base_client
97
175
 
176
+ async def check_result(
177
+ self,
178
+ id: str,
179
+ wait_for_completion: bool,
180
+ download_outputs: bool,
181
+ download_directory: typing.Optional[str] = None,
182
+ ) -> V1VideoProjectsGetResponseWithDownloads:
183
+ """
184
+ Check the result of a video project with optional waiting and downloading.
185
+
186
+ This method retrieves the status of a video project and optionally waits for completion
187
+ and downloads the output files.
188
+
189
+ Args:
190
+ id: Unique ID of the video project
191
+ wait_for_completion: Whether to wait for the video project to complete
192
+ download_outputs: Whether to download the outputs
193
+ download_directory: The directory to download the outputs to. If not provided,
194
+ the outputs will be downloaded to the current working directory
195
+
196
+ Returns:
197
+ V1VideoProjectsGetResponseWithDownloads: The video project response with optional
198
+ downloaded file paths included
199
+ """
200
+ api_response = await self.get(id=id)
201
+ if not wait_for_completion:
202
+ response = V1VideoProjectsGetResponseWithDownloads(
203
+ **api_response.model_dump()
204
+ )
205
+ return response
206
+
207
+ poll_interval = float(os.getenv("MAGIC_HOUR_POLL_INTERVAL", "0.5"))
208
+
209
+ status = api_response.status
210
+
211
+ while status not in ["complete", "error", "canceled"]:
212
+ api_response = await self.get(id=id)
213
+ status = api_response.status
214
+ time.sleep(poll_interval)
215
+
216
+ if api_response.status != "complete":
217
+ log = logger.error if api_response.status == "error" else logger.info
218
+ log(
219
+ f"Video project {id} has status {api_response.status}: {api_response.error}"
220
+ )
221
+ return V1VideoProjectsGetResponseWithDownloads(**api_response.model_dump())
222
+
223
+ if not download_outputs:
224
+ return V1VideoProjectsGetResponseWithDownloads(**api_response.model_dump())
225
+
226
+ downloaded_paths = await download_files_async(
227
+ downloads=api_response.downloads,
228
+ download_directory=download_directory,
229
+ )
230
+
231
+ return V1VideoProjectsGetResponseWithDownloads(
232
+ **api_response.model_dump(), downloaded_paths=downloaded_paths
233
+ )
234
+
98
235
  async def delete(
99
236
  self, *, id: str, request_options: typing.Optional[RequestOptions] = None
100
237
  ) -> None: