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
 
3
4
  from magic_hour.core import (
@@ -8,13 +9,130 @@ from magic_hour.core import (
8
9
  to_encodable,
9
10
  type_utils,
10
11
  )
12
+ from magic_hour.resources.v1.files.client import AsyncFilesClient, FilesClient
13
+ from magic_hour.resources.v1.video_projects.client import (
14
+ AsyncVideoProjectsClient,
15
+ VideoProjectsClient,
16
+ )
11
17
  from magic_hour.types import models, params
12
18
 
13
19
 
20
+ logging.basicConfig(level=logging.INFO)
21
+ logger = logging.getLogger(__name__)
22
+
23
+
14
24
  class FaceSwapClient:
15
25
  def __init__(self, *, base_client: SyncBaseClient):
16
26
  self._base_client = base_client
17
27
 
28
+ def generate(
29
+ self,
30
+ *,
31
+ assets: params.V1FaceSwapGenerateBodyAssets,
32
+ end_seconds: float,
33
+ start_seconds: float,
34
+ height: typing.Union[
35
+ typing.Optional[int], type_utils.NotGiven
36
+ ] = type_utils.NOT_GIVEN,
37
+ name: typing.Union[
38
+ typing.Optional[str], type_utils.NotGiven
39
+ ] = type_utils.NOT_GIVEN,
40
+ style: typing.Union[
41
+ typing.Optional[params.V1FaceSwapCreateBodyStyle], type_utils.NotGiven
42
+ ] = type_utils.NOT_GIVEN,
43
+ width: typing.Union[
44
+ typing.Optional[int], type_utils.NotGiven
45
+ ] = type_utils.NOT_GIVEN,
46
+ wait_for_completion: bool = True,
47
+ download_outputs: bool = True,
48
+ download_directory: typing.Optional[str] = None,
49
+ request_options: typing.Optional[RequestOptions] = None,
50
+ ):
51
+ """
52
+ Generate face swap video (alias for create with additional functionality).
53
+
54
+ Create a Face Swap 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.
55
+
56
+ Args:
57
+ height: `height` is deprecated and no longer influences the output video's resolution.
58
+ name: The name of video. This value is mainly used for your own identification of the video.
59
+ width: `width` is deprecated and no longer influences the output video's resolution.
60
+ assets: Provide the assets for face swap. For video, The `video_source` field determines whether `video_file_path` or `youtube_url` field is used
61
+ end_seconds: The end time of the input video in seconds. This value is used to trim the input video. The value must be greater than 0.1, and more than the start_seconds.
62
+ start_seconds: The start time of the input video in seconds. This value is used to trim the input video. The value must be greater than 0.
63
+ wait_for_completion: Whether to wait for the video project to complete
64
+ download_outputs: Whether to download the outputs
65
+ download_directory: The directory to download the outputs to. If not provided, the outputs will be downloaded to the current working directory
66
+ request_options: Additional options to customize the HTTP request
67
+
68
+ Returns:
69
+ V1VideoProjectsGetResponseWithDownloads: The response from the Face Swap API with the downloaded paths if `download_outputs` is True.
70
+
71
+ Examples:
72
+ ```py
73
+ response = client.v1.face_swap.generate(
74
+ assets={
75
+ "face_swap_mode": "all-faces",
76
+ "video_file_path": "path/to/video.mp4",
77
+ "video_source": "file",
78
+ "image_file_path": "path/to/image.png",
79
+ },
80
+ end_seconds=15.0,
81
+ start_seconds=0.0,
82
+ wait_for_completion=True,
83
+ download_outputs=True,
84
+ download_directory="outputs/",
85
+ )
86
+ ```
87
+ """
88
+
89
+ file_client = FilesClient(base_client=self._base_client)
90
+
91
+ # Upload image file if provided (required for all-faces mode)
92
+ if "image_file_path" in assets and assets["image_file_path"]:
93
+ image_file_path = assets["image_file_path"]
94
+ assets["image_file_path"] = file_client.upload_file(file=image_file_path)
95
+
96
+ # Upload video file if video_source is "file" and video_file_path is provided
97
+ if (
98
+ assets.get("video_source") == "file"
99
+ and "video_file_path" in assets
100
+ and assets["video_file_path"]
101
+ ):
102
+ video_file_path = assets["video_file_path"]
103
+ assets["video_file_path"] = file_client.upload_file(file=video_file_path)
104
+
105
+ # Upload face mappings if present
106
+ if "face_mappings" in assets and assets["face_mappings"]:
107
+ for face_mapping in assets["face_mappings"]:
108
+ if "new_face" in face_mapping and face_mapping["new_face"]:
109
+ new_face_file_path = face_mapping["new_face"]
110
+ face_mapping["new_face"] = file_client.upload_file(
111
+ file=new_face_file_path
112
+ )
113
+
114
+ create_response = self.create(
115
+ assets=assets,
116
+ end_seconds=end_seconds,
117
+ start_seconds=start_seconds,
118
+ height=height,
119
+ name=name,
120
+ style=style,
121
+ width=width,
122
+ request_options=request_options,
123
+ )
124
+ logger.info(f"Face Swap response: {create_response}")
125
+
126
+ video_projects_client = VideoProjectsClient(base_client=self._base_client)
127
+ response = video_projects_client.check_result(
128
+ id=create_response.id,
129
+ wait_for_completion=wait_for_completion,
130
+ download_outputs=download_outputs,
131
+ download_directory=download_directory,
132
+ )
133
+
134
+ return response
135
+
18
136
  def create(
19
137
  self,
20
138
  *,
@@ -27,6 +145,9 @@ class FaceSwapClient:
27
145
  name: typing.Union[
28
146
  typing.Optional[str], type_utils.NotGiven
29
147
  ] = type_utils.NOT_GIVEN,
148
+ style: typing.Union[
149
+ typing.Optional[params.V1FaceSwapCreateBodyStyle], type_utils.NotGiven
150
+ ] = type_utils.NOT_GIVEN,
30
151
  width: typing.Union[
31
152
  typing.Optional[int], type_utils.NotGiven
32
153
  ] = type_utils.NOT_GIVEN,
@@ -43,23 +164,22 @@ class FaceSwapClient:
43
164
  POST /v1/face-swap
44
165
 
45
166
  Args:
46
- height: Used to determine the dimensions of the output video.
47
-
48
- * If height is provided, width will also be required. The larger value between width and height will be used to determine the maximum output resolution while maintaining the original aspect ratio.
49
- * If both height and width are omitted, the video will be resized according to your subscription's maximum resolution, while preserving aspect ratio.
167
+ height: `height` is deprecated and no longer influences the output video's resolution.
50
168
 
51
- Note: if the video's original resolution is less than the maximum, the video will not be resized.
169
+ Output resolution is determined by the **minimum** of:
170
+ - The resolution of the input video
171
+ - The maximum resolution allowed by your subscription tier. See our [pricing page](https://magichour.ai/pricing) for more details.
52
172
 
53
- See our [pricing page](https://magichour.ai/pricing) for more details.
173
+ This field is retained only for backward compatibility and will be removed in a future release.
54
174
  name: The name of video. This value is mainly used for your own identification of the video.
55
- width: Used to determine the dimensions of the output video.
175
+ style: Style of the face swap video.
176
+ width: `width` is deprecated and no longer influences the output video's resolution.
56
177
 
57
- * If width is provided, height will also be required. The larger value between width and height will be used to determine the maximum output resolution while maintaining the original aspect ratio.
58
- * If both height and width are omitted, the video will be resized according to your subscription's maximum resolution, while preserving aspect ratio.
178
+ Output resolution is determined by the **minimum** of:
179
+ - The resolution of the input video
180
+ - The maximum resolution allowed by your subscription tier. See our [pricing page](https://magichour.ai/pricing) for more details.
59
181
 
60
- Note: if the video's original resolution is less than the maximum, the video will not be resized.
61
-
62
- See our [pricing page](https://magichour.ai/pricing) for more details.
182
+ This field is retained only for backward compatibility and will be removed in a future release.
63
183
  assets: Provide the assets for face swap. For video, The `video_source` field determines whether `video_file_path` or `youtube_url` field is used
64
184
  end_seconds: The end time of the input video in seconds. This value is used to trim the input video. The value must be greater than 0.1, and more than the start_seconds.
65
185
  start_seconds: The start time of the input video in seconds. This value is used to trim the input video. The value must be greater than 0.
@@ -89,9 +209,8 @@ class FaceSwapClient:
89
209
  },
90
210
  end_seconds=15.0,
91
211
  start_seconds=0.0,
92
- height=960,
93
212
  name="Face Swap video",
94
- width=512,
213
+ style={"version": "default"},
95
214
  )
96
215
  ```
97
216
  """
@@ -99,6 +218,7 @@ class FaceSwapClient:
99
218
  item={
100
219
  "height": height,
101
220
  "name": name,
221
+ "style": style,
102
222
  "width": width,
103
223
  "assets": assets,
104
224
  "end_seconds": end_seconds,
@@ -120,6 +240,118 @@ class AsyncFaceSwapClient:
120
240
  def __init__(self, *, base_client: AsyncBaseClient):
121
241
  self._base_client = base_client
122
242
 
243
+ async def generate(
244
+ self,
245
+ *,
246
+ assets: params.V1FaceSwapGenerateBodyAssets,
247
+ end_seconds: float,
248
+ start_seconds: float,
249
+ height: typing.Union[
250
+ typing.Optional[int], type_utils.NotGiven
251
+ ] = type_utils.NOT_GIVEN,
252
+ name: typing.Union[
253
+ typing.Optional[str], type_utils.NotGiven
254
+ ] = type_utils.NOT_GIVEN,
255
+ style: typing.Union[
256
+ typing.Optional[params.V1FaceSwapCreateBodyStyle], type_utils.NotGiven
257
+ ] = type_utils.NOT_GIVEN,
258
+ width: typing.Union[
259
+ typing.Optional[int], type_utils.NotGiven
260
+ ] = type_utils.NOT_GIVEN,
261
+ wait_for_completion: bool = True,
262
+ download_outputs: bool = True,
263
+ download_directory: typing.Optional[str] = None,
264
+ request_options: typing.Optional[RequestOptions] = None,
265
+ ):
266
+ """
267
+ Generate face swap video (alias for create with additional functionality).
268
+
269
+ Create a Face Swap 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.
270
+
271
+ Args:
272
+ height: `height` is deprecated and no longer influences the output video's resolution.
273
+ name: The name of video. This value is mainly used for your own identification of the video.
274
+ width: `width` is deprecated and no longer influences the output video's resolution.
275
+ assets: Provide the assets for face swap. For video, The `video_source` field determines whether `video_file_path` or `youtube_url` field is used
276
+ end_seconds: The end time of the input video in seconds. This value is used to trim the input video. The value must be greater than 0.1, and more than the start_seconds.
277
+ start_seconds: The start time of the input video in seconds. This value is used to trim the input video. The value must be greater than 0.
278
+ wait_for_completion: Whether to wait for the video project to complete
279
+ download_outputs: Whether to download the outputs
280
+ download_directory: The directory to download the outputs to. If not provided, the outputs will be downloaded to the current working directory
281
+ request_options: Additional options to customize the HTTP request
282
+
283
+ Returns:
284
+ V1VideoProjectsGetResponseWithDownloads: The response from the Face Swap API with the downloaded paths if `download_outputs` is True.
285
+
286
+ Examples:
287
+ ```py
288
+ response = await client.v1.face_swap.generate(
289
+ assets={
290
+ "face_swap_mode": "all-faces",
291
+ "video_file_path": "path/to/video.mp4",
292
+ "video_source": "file",
293
+ "image_file_path": "path/to/image.png",
294
+ },
295
+ end_seconds=15.0,
296
+ start_seconds=0.0,
297
+ wait_for_completion=True,
298
+ download_outputs=True,
299
+ download_directory="outputs/",
300
+ )
301
+ ```
302
+ """
303
+
304
+ file_client = AsyncFilesClient(base_client=self._base_client)
305
+
306
+ # Upload image file if provided (required for all-faces mode)
307
+ if "image_file_path" in assets and assets["image_file_path"]:
308
+ image_file_path = assets["image_file_path"]
309
+ assets["image_file_path"] = await file_client.upload_file(
310
+ file=image_file_path
311
+ )
312
+
313
+ # Upload video file if video_source is "file" and video_file_path is provided
314
+ if (
315
+ assets.get("video_source") == "file"
316
+ and "video_file_path" in assets
317
+ and assets["video_file_path"]
318
+ ):
319
+ video_file_path = assets["video_file_path"]
320
+ assets["video_file_path"] = await file_client.upload_file(
321
+ file=video_file_path
322
+ )
323
+
324
+ # Upload face mappings if present
325
+ if "face_mappings" in assets and assets["face_mappings"]:
326
+ for face_mapping in assets["face_mappings"]:
327
+ if "new_face" in face_mapping and face_mapping["new_face"]:
328
+ new_face_file_path = face_mapping["new_face"]
329
+ face_mapping["new_face"] = await file_client.upload_file(
330
+ file=new_face_file_path
331
+ )
332
+
333
+ create_response = await self.create(
334
+ assets=assets,
335
+ end_seconds=end_seconds,
336
+ start_seconds=start_seconds,
337
+ height=height,
338
+ name=name,
339
+ style=style,
340
+ width=width,
341
+ request_options=request_options,
342
+ )
343
+ logger.info(f"Face Swap response: {create_response}")
344
+
345
+ video_projects_client = AsyncVideoProjectsClient(base_client=self._base_client)
346
+ response = await video_projects_client.check_result(
347
+ id=create_response.id,
348
+ wait_for_completion=wait_for_completion,
349
+ download_outputs=download_outputs,
350
+ download_directory=download_directory,
351
+ )
352
+
353
+ return response
354
+
123
355
  async def create(
124
356
  self,
125
357
  *,
@@ -132,6 +364,9 @@ class AsyncFaceSwapClient:
132
364
  name: typing.Union[
133
365
  typing.Optional[str], type_utils.NotGiven
134
366
  ] = type_utils.NOT_GIVEN,
367
+ style: typing.Union[
368
+ typing.Optional[params.V1FaceSwapCreateBodyStyle], type_utils.NotGiven
369
+ ] = type_utils.NOT_GIVEN,
135
370
  width: typing.Union[
136
371
  typing.Optional[int], type_utils.NotGiven
137
372
  ] = type_utils.NOT_GIVEN,
@@ -148,23 +383,22 @@ class AsyncFaceSwapClient:
148
383
  POST /v1/face-swap
149
384
 
150
385
  Args:
151
- height: Used to determine the dimensions of the output video.
152
-
153
- * If height is provided, width will also be required. The larger value between width and height will be used to determine the maximum output resolution while maintaining the original aspect ratio.
154
- * If both height and width are omitted, the video will be resized according to your subscription's maximum resolution, while preserving aspect ratio.
386
+ height: `height` is deprecated and no longer influences the output video's resolution.
155
387
 
156
- Note: if the video's original resolution is less than the maximum, the video will not be resized.
388
+ Output resolution is determined by the **minimum** of:
389
+ - The resolution of the input video
390
+ - The maximum resolution allowed by your subscription tier. See our [pricing page](https://magichour.ai/pricing) for more details.
157
391
 
158
- See our [pricing page](https://magichour.ai/pricing) for more details.
392
+ This field is retained only for backward compatibility and will be removed in a future release.
159
393
  name: The name of video. This value is mainly used for your own identification of the video.
160
- width: Used to determine the dimensions of the output video.
161
-
162
- * If width is provided, height will also be required. The larger value between width and height will be used to determine the maximum output resolution while maintaining the original aspect ratio.
163
- * If both height and width are omitted, the video will be resized according to your subscription's maximum resolution, while preserving aspect ratio.
394
+ style: Style of the face swap video.
395
+ width: `width` is deprecated and no longer influences the output video's resolution.
164
396
 
165
- Note: if the video's original resolution is less than the maximum, the video will not be resized.
397
+ Output resolution is determined by the **minimum** of:
398
+ - The resolution of the input video
399
+ - The maximum resolution allowed by your subscription tier. See our [pricing page](https://magichour.ai/pricing) for more details.
166
400
 
167
- See our [pricing page](https://magichour.ai/pricing) for more details.
401
+ This field is retained only for backward compatibility and will be removed in a future release.
168
402
  assets: Provide the assets for face swap. For video, The `video_source` field determines whether `video_file_path` or `youtube_url` field is used
169
403
  end_seconds: The end time of the input video in seconds. This value is used to trim the input video. The value must be greater than 0.1, and more than the start_seconds.
170
404
  start_seconds: The start time of the input video in seconds. This value is used to trim the input video. The value must be greater than 0.
@@ -194,9 +428,8 @@ class AsyncFaceSwapClient:
194
428
  },
195
429
  end_seconds=15.0,
196
430
  start_seconds=0.0,
197
- height=960,
198
431
  name="Face Swap video",
199
- width=512,
432
+ style={"version": "default"},
200
433
  )
201
434
  ```
202
435
  """
@@ -204,6 +437,7 @@ class AsyncFaceSwapClient:
204
437
  item={
205
438
  "height": height,
206
439
  "name": name,
440
+ "style": style,
207
441
  "width": width,
208
442
  "assets": assets,
209
443
  "end_seconds": end_seconds,
@@ -1,3 +1,82 @@
1
+ # v1_face_swap_photo
2
+
3
+ ## Module Functions
4
+
5
+ <!-- CUSTOM DOCS START -->
6
+
7
+ ### Face Swap Photo Generate Workflow <a name="generate"></a>
8
+
9
+ The workflow performs the following action
10
+
11
+ 1. upload local assets to Magic Hour storage. So you can pass in a local path instead of having to upload files yourself
12
+ 2. trigger a generation
13
+ 3. poll for a completion status. This is configurable
14
+ 4. if success, download the output to local directory
15
+
16
+ > [!TIP]
17
+ > This is the recommended way to use the SDK unless you have specific needs where it is necessary to split up the actions.
18
+
19
+ #### Parameters
20
+
21
+ In Additional to the parameters listed in the `.create` section below, `.generate` introduces 3 new parameters:
22
+
23
+ - `wait_for_completion` (bool, default True): Whether to wait for the project to complete.
24
+ - `download_outputs` (bool, default True): Whether to download the generated files
25
+ - `download_directory` (str, optional): Directory to save downloaded files (defaults to current directory)
26
+
27
+ #### Synchronous Client
28
+
29
+ ```python
30
+ from magic_hour import Client
31
+ from os import getenv
32
+
33
+ client = Client(token=getenv("API_TOKEN"))
34
+ res = client.v1.face_swap_photo.generate(
35
+ assets={
36
+ "face_mappings": [
37
+ {
38
+ "new_face": "/path/to/1234.png",
39
+ "original_face": "api-assets/id/0-0.png",
40
+ }
41
+ ],
42
+ "face_swap_mode": "all-faces",
43
+ "source_file_path": "/path/to/1234.png",
44
+ "target_file_path": "/path/to/1234.png",
45
+ },
46
+ name="Face Swap image",
47
+ wait_for_completion=True,
48
+ download_outputs=True,
49
+ download_directory="outputs"
50
+ )
51
+ ```
52
+
53
+ #### Asynchronous Client
54
+
55
+ ```python
56
+ from magic_hour import AsyncClient
57
+ from os import getenv
58
+
59
+ client = AsyncClient(token=getenv("API_TOKEN"))
60
+ res = await client.v1.face_swap_photo.generate(
61
+ assets={
62
+ "face_mappings": [
63
+ {
64
+ "new_face": "/path/to/1234.png",
65
+ "original_face": "api-assets/id/0-0.png",
66
+ }
67
+ ],
68
+ "face_swap_mode": "all-faces",
69
+ "source_file_path": "/path/to/1234.png",
70
+ "target_file_path": "/path/to/1234.png",
71
+ },
72
+ name="Face Swap image",
73
+ wait_for_completion=True,
74
+ download_outputs=True,
75
+ download_directory="outputs"
76
+ )
77
+ ```
78
+
79
+ <!-- CUSTOM DOCS END -->
1
80
 
2
81
  ### Face Swap Photo <a name="create"></a>
3
82
 
@@ -10,6 +89,10 @@ Create a face swap photo. Each photo costs 5 credits. The height/width of the ou
10
89
  | Parameter | Required | Description | Example |
11
90
  |-----------|:--------:|-------------|--------|
12
91
  | `assets` | ✓ | Provide the assets for face swap photo | `{"face_mappings": [{"new_face": "api-assets/id/1234.png", "original_face": "api-assets/id/0-0.png"}], "face_swap_mode": "all-faces", "source_file_path": "api-assets/id/1234.png", "target_file_path": "api-assets/id/1234.png"}` |
92
+ | `└─ face_mappings` | ✗ | This is the array of face mappings used for multiple face swap. The value is required if `face_swap_mode` is `individual-faces`. | `[{"new_face": "api-assets/id/1234.png", "original_face": "api-assets/id/0-0.png"}]` |
93
+ | `└─ face_swap_mode` | ✗ | The mode of face swap. * `all-faces` - Swap all faces in the target image or video. `source_file_path` is required. * `individual-faces` - Swap individual faces in the target image or video. `source_faces` is required. | `"all-faces"` |
94
+ | `└─ source_file_path` | ✗ | This is the image from which the face is extracted. The value is required if `face_swap_mode` is `all-faces`. 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). Please refer to the [Input File documentation](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls#input-file) to learn more. | `"api-assets/id/1234.png"` |
95
+ | `└─ target_file_path` | ✓ | This is the image where the face from the source image will be placed. 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). Please refer to the [Input File documentation](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls#input-file) to learn more. | `"api-assets/id/1234.png"` |
13
96
  | `name` | ✗ | The name of image. This value is mainly used for your own identification of the image. | `"Face Swap image"` |
14
97
 
15
98
  #### Synchronous Client
@@ -67,3 +150,4 @@ res = await client.v1.face_swap_photo.create(
67
150
 
68
151
  ##### Example
69
152
  `{"credits_charged": 5, "frame_cost": 5, "id": "cuid-example"}`
153
+
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import typing
2
3
 
3
4
  from magic_hour.core import (
@@ -8,13 +9,101 @@ from magic_hour.core import (
8
9
  to_encodable,
9
10
  type_utils,
10
11
  )
12
+ from magic_hour.resources.v1.files.client import AsyncFilesClient, FilesClient
13
+ from magic_hour.resources.v1.image_projects.client import (
14
+ AsyncImageProjectsClient,
15
+ ImageProjectsClient,
16
+ )
11
17
  from magic_hour.types import models, params
12
18
 
13
19
 
20
+ logging.basicConfig(level=logging.INFO)
21
+ logger = logging.getLogger(__name__)
22
+
23
+
14
24
  class FaceSwapPhotoClient:
15
25
  def __init__(self, *, base_client: SyncBaseClient):
16
26
  self._base_client = base_client
17
27
 
28
+ def generate(
29
+ self,
30
+ *,
31
+ assets: params.V1FaceSwapPhotoGenerateBodyAssets,
32
+ name: typing.Union[
33
+ typing.Optional[str], type_utils.NotGiven
34
+ ] = type_utils.NOT_GIVEN,
35
+ wait_for_completion: bool = True,
36
+ download_outputs: bool = True,
37
+ download_directory: typing.Optional[str] = None,
38
+ request_options: typing.Optional[RequestOptions] = None,
39
+ ):
40
+ """
41
+ Generate face swap photo (alias for create with additional functionality).
42
+
43
+ Swap faces in a photo using AI. Each face swap costs 5 credits.
44
+
45
+ Args:
46
+ name: The name of image. This value is mainly used for your own identification of the image.
47
+ assets: Provide the assets for face swap photo
48
+ wait_for_completion: Whether to wait for the image project to complete
49
+ download_outputs: Whether to download the outputs
50
+ download_directory: The directory to download the outputs to. If not provided, the outputs will be downloaded to the current working directory
51
+ request_options: Additional options to customize the HTTP request
52
+
53
+ Returns:
54
+ V1ImageProjectsGetResponseWithDownloads: The response from the Face Swap Photo API with the downloaded paths if `download_outputs` is True.
55
+
56
+ Examples:
57
+ ```py
58
+ client.v1.face_swap_photo.generate(
59
+ assets={
60
+ "face_swap_mode": "all-faces",
61
+ "source_file_path": "api-assets/id/1234.png",
62
+ "target_file_path": "api-assets/id/1234.png",
63
+ },
64
+ name="Face Swap image",
65
+ wait_for_completion=True,
66
+ download_outputs=True,
67
+ download_directory="./outputs/",
68
+ )
69
+ ```
70
+ """
71
+
72
+ file_client = FilesClient(base_client=self._base_client)
73
+
74
+ # Upload source image file if present
75
+ if "source_file_path" in assets and assets["source_file_path"]:
76
+ source_file_path = assets["source_file_path"]
77
+ assets["source_file_path"] = file_client.upload_file(file=source_file_path)
78
+
79
+ # Upload target image file
80
+ target_file_path = assets["target_file_path"]
81
+ assets["target_file_path"] = file_client.upload_file(file=target_file_path)
82
+
83
+ # Upload face mappings if present
84
+ if "face_mappings" in assets and assets["face_mappings"]:
85
+ for face_mapping in assets["face_mappings"]:
86
+ if "new_face" in face_mapping and face_mapping["new_face"]:
87
+ new_face_file_path = face_mapping["new_face"]
88
+ face_mapping["new_face"] = file_client.upload_file(
89
+ file=new_face_file_path
90
+ )
91
+
92
+ create_response = self.create(
93
+ assets=assets, name=name, request_options=request_options
94
+ )
95
+ logger.info(f"Face Swap Photo response: {create_response}")
96
+
97
+ image_projects_client = ImageProjectsClient(base_client=self._base_client)
98
+ response = image_projects_client.check_result(
99
+ id=create_response.id,
100
+ wait_for_completion=wait_for_completion,
101
+ download_outputs=download_outputs,
102
+ download_directory=download_directory,
103
+ )
104
+
105
+ return response
106
+
18
107
  def create(
19
108
  self,
20
109
  *,
@@ -79,6 +168,89 @@ class AsyncFaceSwapPhotoClient:
79
168
  def __init__(self, *, base_client: AsyncBaseClient):
80
169
  self._base_client = base_client
81
170
 
171
+ async def generate(
172
+ self,
173
+ *,
174
+ assets: params.V1FaceSwapPhotoGenerateBodyAssets,
175
+ name: typing.Union[
176
+ typing.Optional[str], type_utils.NotGiven
177
+ ] = type_utils.NOT_GIVEN,
178
+ wait_for_completion: bool = True,
179
+ download_outputs: bool = True,
180
+ download_directory: typing.Optional[str] = None,
181
+ request_options: typing.Optional[RequestOptions] = None,
182
+ ):
183
+ """
184
+ Generate face swap photo (alias for create with additional functionality).
185
+
186
+ Swap faces in a photo using AI. Each face swap costs 5 credits.
187
+
188
+ Args:
189
+ name: The name of image. This value is mainly used for your own identification of the image.
190
+ assets: Provide the assets for face swap photo
191
+ wait_for_completion: Whether to wait for the image project to complete
192
+ download_outputs: Whether to download the outputs
193
+ download_directory: The directory to download the outputs to. If not provided, the outputs will be downloaded to the current working directory
194
+ request_options: Additional options to customize the HTTP request
195
+
196
+ Returns:
197
+ V1ImageProjectsGetResponseWithDownloads: The response from the Face Swap Photo API with the downloaded paths if `download_outputs` is True.
198
+
199
+ Examples:
200
+ ```py
201
+ await client.v1.face_swap_photo.generate(
202
+ assets={
203
+ "face_swap_mode": "all-faces",
204
+ "source_file_path": "api-assets/id/1234.png",
205
+ "target_file_path": "api-assets/id/1234.png",
206
+ },
207
+ name="Face Swap image",
208
+ wait_for_completion=True,
209
+ download_outputs=True,
210
+ download_directory="./outputs/",
211
+ )
212
+ ```
213
+ """
214
+
215
+ file_client = AsyncFilesClient(base_client=self._base_client)
216
+
217
+ # Upload source image file if present
218
+ if "source_file_path" in assets and assets["source_file_path"]:
219
+ source_file_path = assets["source_file_path"]
220
+ assets["source_file_path"] = await file_client.upload_file(
221
+ file=source_file_path
222
+ )
223
+
224
+ # Upload target image file
225
+ target_file_path = assets["target_file_path"]
226
+ assets["target_file_path"] = await file_client.upload_file(
227
+ file=target_file_path
228
+ )
229
+
230
+ # Upload face mappings if present
231
+ if "face_mappings" in assets and assets["face_mappings"]:
232
+ for face_mapping in assets["face_mappings"]:
233
+ if "new_face" in face_mapping and face_mapping["new_face"]:
234
+ new_face_file_path = face_mapping["new_face"]
235
+ face_mapping["new_face"] = await file_client.upload_file(
236
+ file=new_face_file_path
237
+ )
238
+
239
+ create_response = await self.create(
240
+ assets=assets, name=name, request_options=request_options
241
+ )
242
+ logger.info(f"Face Swap Photo response: {create_response}")
243
+
244
+ image_projects_client = AsyncImageProjectsClient(base_client=self._base_client)
245
+ response = await image_projects_client.check_result(
246
+ id=create_response.id,
247
+ wait_for_completion=wait_for_completion,
248
+ download_outputs=download_outputs,
249
+ download_directory=download_directory,
250
+ )
251
+
252
+ return response
253
+
82
254
  async def create(
83
255
  self,
84
256
  *,