magic_hour 0.35.0__py3-none-any.whl → 0.36.1__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 (94) 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 -8
  41. magic_hour/resources/v1/face_swap/client.py +242 -0
  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 +40 -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 +52 -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 +186 -0
  56. magic_hour/resources/v1/lip_sync/README.md +87 -9
  57. magic_hour/resources/v1/lip_sync/client.py +210 -0
  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 +52 -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 -10
  67. magic_hour/resources/v1/video_to_video/client.py +222 -0
  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 +12 -0
  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_generate_body_assets.py +17 -0
  87. magic_hour/types/params/v1_lip_sync_generate_body_assets.py +36 -0
  88. magic_hour/types/params/v1_photo_colorizer_generate_body_assets.py +17 -0
  89. magic_hour/types/params/v1_video_to_video_generate_body_assets.py +27 -0
  90. magic_hour-0.36.1.dist-info/METADATA +306 -0
  91. {magic_hour-0.35.0.dist-info → magic_hour-0.36.1.dist-info}/RECORD +93 -65
  92. magic_hour-0.35.0.dist-info/METADATA +0 -166
  93. {magic_hour-0.35.0.dist-info → magic_hour-0.36.1.dist-info}/LICENSE +0 -0
  94. {magic_hour-0.35.0.dist-info → magic_hour-0.36.1.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,
@@ -51,6 +172,7 @@ class FaceSwapClient:
51
172
 
52
173
  This field is retained only for backward compatibility and will be removed in a future release.
53
174
  name: The name of video. This value is mainly used for your own identification of the video.
175
+ style: Style of the face swap video.
54
176
  width: `width` is deprecated and no longer influences the output video's resolution.
55
177
 
56
178
  Output resolution is determined by the **minimum** of:
@@ -88,6 +210,7 @@ class FaceSwapClient:
88
210
  end_seconds=15.0,
89
211
  start_seconds=0.0,
90
212
  name="Face Swap video",
213
+ style={"version": "default"},
91
214
  )
92
215
  ```
93
216
  """
@@ -95,6 +218,7 @@ class FaceSwapClient:
95
218
  item={
96
219
  "height": height,
97
220
  "name": name,
221
+ "style": style,
98
222
  "width": width,
99
223
  "assets": assets,
100
224
  "end_seconds": end_seconds,
@@ -116,6 +240,118 @@ class AsyncFaceSwapClient:
116
240
  def __init__(self, *, base_client: AsyncBaseClient):
117
241
  self._base_client = base_client
118
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
+
119
355
  async def create(
120
356
  self,
121
357
  *,
@@ -128,6 +364,9 @@ class AsyncFaceSwapClient:
128
364
  name: typing.Union[
129
365
  typing.Optional[str], type_utils.NotGiven
130
366
  ] = type_utils.NOT_GIVEN,
367
+ style: typing.Union[
368
+ typing.Optional[params.V1FaceSwapCreateBodyStyle], type_utils.NotGiven
369
+ ] = type_utils.NOT_GIVEN,
131
370
  width: typing.Union[
132
371
  typing.Optional[int], type_utils.NotGiven
133
372
  ] = type_utils.NOT_GIVEN,
@@ -152,6 +391,7 @@ class AsyncFaceSwapClient:
152
391
 
153
392
  This field is retained only for backward compatibility and will be removed in a future release.
154
393
  name: The name of video. This value is mainly used for your own identification of the video.
394
+ style: Style of the face swap video.
155
395
  width: `width` is deprecated and no longer influences the output video's resolution.
156
396
 
157
397
  Output resolution is determined by the **minimum** of:
@@ -189,6 +429,7 @@ class AsyncFaceSwapClient:
189
429
  end_seconds=15.0,
190
430
  start_seconds=0.0,
191
431
  name="Face Swap video",
432
+ style={"version": "default"},
192
433
  )
193
434
  ```
194
435
  """
@@ -196,6 +437,7 @@ class AsyncFaceSwapClient:
196
437
  item={
197
438
  "height": height,
198
439
  "name": name,
440
+ "style": style,
199
441
  "width": width,
200
442
  "assets": assets,
201
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
  *,
@@ -0,0 +1,40 @@
1
+ # v1_files
2
+
3
+ <!-- CUSTOM DOCS START -->
4
+
5
+ ### Upload File <a name="upload-file"></a>
6
+
7
+ Upload a local file to Magic Hour Storage. The returned value is used for subsequent API calls.
8
+
9
+ #### Parameters
10
+
11
+ | Parameter | Required | Description | Example |
12
+ | --------- | :------: | ------------------------------------------------------------------- | ------------------ |
13
+ | `file` | ✓ | A local file path, path like object, file URL, or file like object. | `"/tmp/image.png"` |
14
+
15
+ #### Synchronous Client
16
+
17
+ ```python
18
+ from magic_hour import Client
19
+ from os import getenv
20
+
21
+ client = Client(token=getenv("API_TOKEN"))
22
+ file_path = client.v1.files.upload_file("/path/to/your/image.jpg")
23
+ ```
24
+
25
+ #### Asynchronous Client
26
+
27
+ ```python
28
+ from magic_hour import AsyncClient
29
+ from os import getenv
30
+
31
+ client = AsyncClient(token=getenv("API_TOKEN"))
32
+ file_path = await client.v1.files.upload_file("/path/to/your/image.jpg")
33
+ ```
34
+
35
+
36
+ <!-- CUSTOM DOCS END -->
37
+
38
+ ## Submodules
39
+ - [upload_urls](upload_urls/README.md) - upload_urls
40
+